mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
adding hugo
This commit is contained in:
parent
50a1d6f3f1
commit
6e16449e5f
34 changed files with 3458 additions and 0 deletions
67
LICENSE.md
Normal file
67
LICENSE.md
Normal file
|
@ -0,0 +1,67 @@
|
|||
Simple Public License (SimPL-2.0)
|
||||
=================================
|
||||
|
||||
Preamble
|
||||
--------
|
||||
|
||||
This Simple Public License 2.0 (SimPL-2.0 for short) is a plain language
|
||||
implementation of GPL 2.0. The words are different, but the goal is the
|
||||
same - to guarantee for all users the freedom to share and change
|
||||
software. If anyone wonders about the meaning of the SimPL, they should
|
||||
interpret it as consistent with GPL 2.0.
|
||||
|
||||
|
||||
Simple Public License (SimPL) 2.0
|
||||
=================================
|
||||
|
||||
The SimPL applies to the software's source and object code and comes
|
||||
with any rights that I have in it (other than trademarks). You agree to
|
||||
the SimPL by copying, distributing, or making a derivative work of the
|
||||
software.
|
||||
|
||||
You get the royalty free right to:
|
||||
|
||||
- Use the software for any purpose;
|
||||
- Make derivative works of it (this is called a "Derived Work");
|
||||
- Copy and distribute it and any Derived Work.
|
||||
|
||||
If you distribute the software or a Derived Work, you must give back to
|
||||
the community by:
|
||||
|
||||
- Prominently noting the date of any changes you make;
|
||||
- Leaving other people's copyright notices, warranty disclaimers, and
|
||||
license terms in place;
|
||||
- Providing the source code, build scripts, installation scripts, and
|
||||
interface definitions in a form that is easy to get and best to
|
||||
modify;
|
||||
- Licensing it to everyone under SimPL, or substantially similar terms
|
||||
(such as GPL 2.0), without adding further restrictions to the rights
|
||||
provided;
|
||||
- Conspicuously announcing that it is available under that license.
|
||||
|
||||
There are some things that you must shoulder:
|
||||
|
||||
- You get NO WARRANTIES. None of any kind;
|
||||
- If the software damages you in any way, you may only recover direct
|
||||
damages up to the amount you paid for it (that is zero if you did
|
||||
not pay anything). You may not recover any other damages, including
|
||||
those called "consequential damages." (The state or country where
|
||||
you live may not allow you to limit your liability in this way, so
|
||||
this may not apply to you);
|
||||
|
||||
The SimPL continues perpetually, except that your license rights end
|
||||
automatically if:
|
||||
|
||||
- You do not abide by the "give back to the community" terms (your
|
||||
licensees get to keep their rights if they abide);
|
||||
- Anyone prevents you from distributing the software under the terms
|
||||
of the SimPL.
|
||||
|
||||
License for the License
|
||||
-----------------------
|
||||
|
||||
You may do anything that you want with the SimPL text; it's a license
|
||||
form to use in any way that you find helpful. To avoid confusion,
|
||||
however, if you change the terms in any way then you may not call your
|
||||
license the Simple Public License or the SimPL (but feel free to
|
||||
acknowledge that your license is "based on the Simple Public License").
|
4
docs/config.json
Normal file
4
docs/config.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"Indexes" : {"tag": "tags"},
|
||||
"BaseUrl" : "http://localhost"
|
||||
}
|
19
docs/content/doc/configuration.md
Normal file
19
docs/content/doc/configuration.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"title": "Configuring Hugo",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
The directory structure and templates provide the majority of the
|
||||
configuration for a site. In fact a config file isn't even needed for many websites
|
||||
since the defaults used follow commonly used patterns.
|
||||
|
||||
The following is an example of a config file with the default values
|
||||
|
||||
{
|
||||
"SourceDir" : "content",
|
||||
"LayoutDir" : "layouts",
|
||||
"PublishDir" : "public",
|
||||
"BuildDrafts" : false,
|
||||
"Tags" : { "category" : "categories", "tag" : "tags" },
|
||||
"BaseUrl" : "http://yourSite.com/"
|
||||
}
|
10
docs/content/doc/contributing.md
Normal file
10
docs/content/doc/contributing.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"title": "Contributing to Hugo",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
1. Fork it from https://github.com/spf13/hugo
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
4. Push to the branch (`git push origin my-new-feature`)
|
||||
5. Create new Pull Request
|
9
docs/content/doc/contributors.md
Normal file
9
docs/content/doc/contributors.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"title": "Contributors",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
Hugo was built with love and golang by:
|
||||
|
||||
* [spf13](https://github.com/spf13)
|
||||
|
40
docs/content/doc/example.md
Normal file
40
docs/content/doc/example.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"title": "Example Content File",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
Somethings are better shown than explained. The following is a very basic example of a content file:
|
||||
|
||||
**mysite/project/nitro.md <- http://mysite.com/project/nitro.html**
|
||||
|
||||
{
|
||||
"Title": "Nitro : A quick and simple profiler for golang",
|
||||
"Description": "",
|
||||
"Keywords": [ "Development", "golang", "profiling" ],
|
||||
"Tags": [ "Development", "golang", "profiling" ],
|
||||
"Pubdate": "2013-06-19",
|
||||
"Topics": [ "Development", "GoLang" ],
|
||||
"Slug": "nitro",
|
||||
"project_url": "http://github.com/spf13/nitro"
|
||||
}
|
||||
|
||||
# Nitro
|
||||
|
||||
Quick and easy performance analyzer library for golang.
|
||||
|
||||
## Overview
|
||||
|
||||
Nitro is a quick and easy performance analyzer library for golang.
|
||||
It is useful for comparing A/B against different drafts of functions
|
||||
or different functions.
|
||||
|
||||
## Implementing Nitro
|
||||
|
||||
Using Nitro is simple. First use go get to install the latest version
|
||||
of the library.
|
||||
|
||||
$ go get github.com/spf13/nitro
|
||||
|
||||
Next include nitro in your application.
|
||||
|
||||
|
38
docs/content/doc/front-matter.md
Normal file
38
docs/content/doc/front-matter.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"title": "Front Matter",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
The front matter is one of the features that gives Hugo it's strength. It enables
|
||||
you to include the meta data of the content right with it. Hugo supports a few
|
||||
different formats. The main format supported is JSON. Here is an example:
|
||||
|
||||
{
|
||||
"Title": "spf13-vim 3.0 release and new website",
|
||||
"Description": "spf13-vim is a cross platform distribution of vim plugins and resources for Vim.",
|
||||
"Tags": [ ".vimrc", "plugins", "spf13-vim", "vim" ],
|
||||
"Pubdate": "2012-04-06",
|
||||
"Categories": [ "Development", "VIM" ],
|
||||
"Slug": "spf13-vim-3-0-release-and-new-website"
|
||||
}
|
||||
|
||||
### Variables
|
||||
There are a few predefined variables that Hugo is aware of and utilizes. The user can also create
|
||||
any variable they want to. These will be placed into the `.Params` variable available to the templates.
|
||||
|
||||
#### Required
|
||||
|
||||
**Title** The title for the content. <br>
|
||||
**Description** The description for the content.<br>
|
||||
**Pubdate** The date the content will be sorted by.<br>
|
||||
**Indexes** These will use the field name of the plural form of the index (see tags and categories above)
|
||||
|
||||
#### Optional
|
||||
|
||||
**Draft** If true the content will not be rendered unless `hugo` is called with -d<br>
|
||||
**Type** The type of the content (will be derived from the directory automatically if unset).<br>
|
||||
**Slug** The token to appear in the tail of the url.<br>
|
||||
*or*<br>
|
||||
**Url** The full path to the content from the web root.<br>
|
||||
*If neither is present the filename will be used.*
|
||||
|
18
docs/content/doc/installing.md
Normal file
18
docs/content/doc/installing.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"title": "Installing Hugo",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
Installation is very easy. Simply download the appropriate version for your
|
||||
platform.
|
||||
|
||||
Hugo is written in GoLang with support for Windows, Linux and OSX.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Please make sure that you place the executable in your path. `/usr/local/bin`
|
||||
is the most probable location.
|
||||
</div>
|
||||
|
||||
|
||||
Hugo doesn't have any external dependencies, but can benefit from external
|
||||
programs.
|
75
docs/content/doc/license.md
Normal file
75
docs/content/doc/license.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"title": "License",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
Hugo is released under the Simple Public License.
|
||||
|
||||
|
||||
Simple Public License (SimPL-2.0)
|
||||
=================================
|
||||
|
||||
Preamble
|
||||
--------
|
||||
|
||||
This Simple Public License 2.0 (SimPL-2.0 for short) is a plain language
|
||||
implementation of GPL 2.0. The words are different, but the goal is the
|
||||
same - to guarantee for all users the freedom to share and change
|
||||
software. If anyone wonders about the meaning of the SimPL, they should
|
||||
interpret it as consistent with GPL 2.0.
|
||||
|
||||
|
||||
Simple Public License (SimPL) 2.0
|
||||
=================================
|
||||
|
||||
The SimPL applies to the software's source and object code and comes
|
||||
with any rights that I have in it (other than trademarks). You agree to
|
||||
the SimPL by copying, distributing, or making a derivative work of the
|
||||
software.
|
||||
|
||||
You get the royalty free right to:
|
||||
|
||||
- Use the software for any purpose;
|
||||
- Make derivative works of it (this is called a "Derived Work");
|
||||
- Copy and distribute it and any Derived Work.
|
||||
|
||||
If you distribute the software or a Derived Work, you must give back to
|
||||
the community by:
|
||||
|
||||
- Prominently noting the date of any changes you make;
|
||||
- Leaving other people's copyright notices, warranty disclaimers, and
|
||||
license terms in place;
|
||||
- Providing the source code, build scripts, installation scripts, and
|
||||
interface definitions in a form that is easy to get and best to
|
||||
modify;
|
||||
- Licensing it to everyone under SimPL, or substantially similar terms
|
||||
(such as GPL 2.0), without adding further restrictions to the rights
|
||||
provided;
|
||||
- Conspicuously announcing that it is available under that license.
|
||||
|
||||
There are some things that you must shoulder:
|
||||
|
||||
- You get NO WARRANTIES. None of any kind;
|
||||
- If the software damages you in any way, you may only recover direct
|
||||
damages up to the amount you paid for it (that is zero if you did
|
||||
not pay anything). You may not recover any other damages, including
|
||||
those called "consequential damages." (The state or country where
|
||||
you live may not allow you to limit your liability in this way, so
|
||||
this may not apply to you);
|
||||
|
||||
The SimPL continues perpetually, except that your license rights end
|
||||
automatically if:
|
||||
|
||||
- You do not abide by the "give back to the community" terms (your
|
||||
licensees get to keep their rights if they abide);
|
||||
- Anyone prevents you from distributing the software under the terms
|
||||
of the SimPL.
|
||||
|
||||
License for the License
|
||||
-----------------------
|
||||
|
||||
You may do anything that you want with the SimPL text; it's a license
|
||||
form to use in any way that you find helpful. To avoid confusion,
|
||||
however, if you change the terms in any way then you may not call your
|
||||
license the Simple Public License or the SimPL (but feel free to
|
||||
acknowledge that your license is "based on the Simple Public License").
|
22
docs/content/doc/organization.md
Normal file
22
docs/content/doc/organization.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"title": "Organization",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
Hugo uses markdown files with headers commonly called the front matter. Hugo respects the organization
|
||||
that you provide for your content to minimize any extra configuration, though this can be overridden
|
||||
by additional configuration in the front matter.
|
||||
|
||||
## Organization
|
||||
In Hugo the content should be arranged in the same way they are intended for the rendered website.
|
||||
Without any additional configuration the following will just work.
|
||||
|
||||
.
|
||||
└── content
|
||||
├── post
|
||||
| ├── firstpost.md // <- http://site.com/post/firstpost.html
|
||||
| └── secondpost.md // <- http://site.com/post/secondpost.html
|
||||
└── quote
|
||||
├── first.md // <- http://site.com/quote/first.html
|
||||
└── second.md // <- http://site.com/quote/second.html
|
||||
|
14
docs/content/doc/release-notes.md
Normal file
14
docs/content/doc/release-notes.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"title": "Release Notes",
|
||||
"Pubdate": "2013-07-01"
|
||||
|
||||
}
|
||||
|
||||
* **0.7.0** July 4, 2013
|
||||
* Hugo now includes a simple server
|
||||
* First public release
|
||||
* **0.6.0** July 2, 2013
|
||||
* Hugo includes an example documentation site which it builds
|
||||
* **0.5.0** June 25, 2013
|
||||
* Hugo is quite usable and able to build spf13.com
|
||||
|
18
docs/content/doc/roadmap.md
Normal file
18
docs/content/doc/roadmap.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"title": "Roadmap",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
In no particular order, here is what I'm working on:
|
||||
|
||||
* Pagination
|
||||
* Support for top level pages (other than homepage)
|
||||
* Series support
|
||||
* Syntax highlighting
|
||||
* Previous & Next
|
||||
* Related Posts
|
||||
* Support for TOML front matter
|
||||
* Proper YAML support for front matter
|
||||
* Support for other formats
|
||||
|
||||
|
76
docs/content/doc/shortcodes.md
Normal file
76
docs/content/doc/shortcodes.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"title": "Shortcodes",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
Because Hugo uses markdown for it's content format, it was clear that there's a lot of things that
|
||||
markdown doesn't support well. This is good, the simple nature of markdown is exactly why we chose it.
|
||||
|
||||
However we cannot accept being constrained by our simple format. Also unacceptable is writing raw
|
||||
html in our markdown every time we want to include unsupported content such as a video. To do
|
||||
so is in complete opposition to the intent of using a bare bones format for our content and
|
||||
utilizing templates to apply styling for display.
|
||||
|
||||
To avoid both of these limitations Hugo has full support for shortcodes.
|
||||
|
||||
### What is a shortcode?
|
||||
A shortcode is a simple snippet inside a markdown file that Hugo will render using a template.
|
||||
|
||||
Short codes are designated by the opening and closing characters of '{{%' and '%}}' respectively.
|
||||
Short codes are space delimited. The first word is always the name of the shortcode. Following the
|
||||
name are the parameters. The author of the shortcode can choose if the short code
|
||||
will use positional parameters or named parameters (but not both). A good rule of thumb is that if a
|
||||
short code has a single required value in the case of the youtube example below then positional
|
||||
works very well. For more complex layouts with optional parameters named parameters work best.
|
||||
|
||||
The format for named parameters models that of html with the format name="value"
|
||||
|
||||
### Example: youtube
|
||||
*Example has an extra space so Hugo doesn't actually render it*
|
||||
|
||||
{{ % youtube 09jf3ow9jfw %}}
|
||||
|
||||
This would be rendered as
|
||||
|
||||
<div class="embed video-player">
|
||||
<iframe class="youtube-player" type="text/html"
|
||||
width="640" height="385"
|
||||
src="http://www.youtube.com/embed/09jf3ow9jfw"
|
||||
allowfullscreen frameborder="0">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
### Example: image with caption
|
||||
*Example has an extra space so Hugo doesn't actually render it*
|
||||
|
||||
{{ % img src="/media/spf13.jpg" title="Steve Francia" %}}
|
||||
|
||||
Would be rendered as:
|
||||
|
||||
<figure >
|
||||
<img src="/media/spf13.jpg" />
|
||||
<figcaption>
|
||||
<h4>Steve Francia</h4>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
|
||||
### Creating a shortcode
|
||||
|
||||
All that you need to do to create a shortcode is place a template in the layouts/shortcodes directory.
|
||||
|
||||
The template name will be the name of the shortcode.
|
||||
|
||||
**Inside the template**
|
||||
|
||||
To access a parameter by either position or name the index method can be used.
|
||||
|
||||
{{ index .Params 0 }}
|
||||
or
|
||||
{{ index .Params "class" }}
|
||||
|
||||
To check if a parameter has been provided use the isset method provided by Hugo.
|
||||
|
||||
{{ if isset .Params "class"}} class="{{ index .Params "class"}}" {{ end }}
|
||||
|
||||
|
54
docs/content/doc/source-directory.md
Normal file
54
docs/content/doc/source-directory.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"title": "Source Directory Organization",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
Hugo takes a single directory and uses it as the input for creating a complete website.
|
||||
|
||||
Hugo has a very small amount of configuration, while remaining highly customizable.
|
||||
It accomplishes by assuming that you will only provide templates with the intent of
|
||||
using them.
|
||||
|
||||
An example directory may look like:
|
||||
|
||||
.
|
||||
├── config.json
|
||||
├── content
|
||||
| ├── post
|
||||
| | ├── firstpost.md
|
||||
| | └── secondpost.md
|
||||
| └── quote
|
||||
| | ├── first.md
|
||||
| | └── second.md
|
||||
├── layouts
|
||||
| ├── chrome
|
||||
| | ├── header.html
|
||||
| | └── footer.html
|
||||
| ├── indexes
|
||||
| | ├── category.html
|
||||
| | ├── post.html
|
||||
| | ├── quote.html
|
||||
| | └── tag.html
|
||||
| ├── post
|
||||
| | ├── li.html
|
||||
| | ├── single.html
|
||||
| | └── summary.html
|
||||
| ├── quote
|
||||
| | ├── li.html
|
||||
| | ├── single.html
|
||||
| | └── summary.html
|
||||
| ├── shortcodes
|
||||
| | ├── img.html
|
||||
| | ├── vimeo.html
|
||||
| | └── youtube.html
|
||||
| ├── index.html
|
||||
| └── rss.xml
|
||||
└── public
|
||||
|
||||
This directory structure tells us a lot about this site:
|
||||
|
||||
1. the website intends to have two different types of content, posts and quotes.
|
||||
2. It will also apply two different indexes to that content, categories and tags.
|
||||
3. It will be displaying content in 3 different views, a list, a summary and a full page view.
|
||||
|
||||
Included with the repository is an this example site ready to be rendered.
|
66
docs/content/doc/templates.md
Normal file
66
docs/content/doc/templates.md
Normal file
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"title": "Templates",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
Hugo uses the excellent golang html/template library for it's template engine. It is an extremely
|
||||
lightweight engine that provides a very small amount of logic. In our
|
||||
experience that it is just the right amount of logic to be able to create a good static website
|
||||
|
||||
This document will not cover how to use golang templates, but the [golang docs](http://golang.org/pkg/html/template/)
|
||||
provide a good introduction.
|
||||
|
||||
### Template roles
|
||||
|
||||
There are 5 different kinds of templates that Hugo works with.
|
||||
|
||||
#### index.html
|
||||
This file must exist in the layouts directory. It is the template used to render the
|
||||
homepage of your site.
|
||||
|
||||
#### rss.xml
|
||||
This file must exist in the layouts directory. It will be used to render all rss documents.
|
||||
The one provided in the example application will generate an ATOM format.
|
||||
|
||||
*Important: Hugo will automatically add the following header line to this file.*
|
||||
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
|
||||
|
||||
#### Indexes
|
||||
An index is a page that list multiple pieces of content. If you think of a typical blog, the tag
|
||||
pages are good examples of indexes.
|
||||
|
||||
|
||||
#### Content Type(s)
|
||||
Hugo supports multiple types of content. Another way of looking at this is that Hugo has the ability
|
||||
to render content in a variety of ways as determined by the type.
|
||||
|
||||
#### Chrome
|
||||
Chrome is simply the decoration of your site. It's not a requirement to have this, but in practice
|
||||
it's very convenient. Hugo doesn't know anything about Chrome, it's simply a convention that you may
|
||||
likely find beneficial. As you create the rest of your templates you will include templates from the
|
||||
/layout/chrome directory. I've found it helpful to include a header and footer template
|
||||
in Chrome so I can include those in the other full page layouts (index.html, indexes/ type/single.html).
|
||||
|
||||
### Adding a new content type
|
||||
|
||||
Adding a type is easy.
|
||||
|
||||
**Step 1:**
|
||||
Create a directory with the name of the type in layouts.Type is always singular. *Eg /layouts/post*.
|
||||
|
||||
**Step 2:**
|
||||
Create a file called single.html inside your directory. *Eg /layouts/post/single.html*.
|
||||
|
||||
**Step 3:**
|
||||
Create a file with the same name as your directory in /layouts/indexes/. *Eg /layouts/index/post.html*.
|
||||
|
||||
**Step 4:**
|
||||
Many sites support rendering content in a few different ways, for instance a single page view and a
|
||||
summary view to be used when displaying a list of contents on a single page. Hugo makes no assumptions
|
||||
here about how you want to display your content, and will support as many different views of a content
|
||||
type as your site requires. All that is required for these additional views is that a template
|
||||
exists in each layout/type directory with the same name.
|
||||
|
||||
For these, reviewing this example site will be very helpful in order to understand how these types work.
|
||||
|
52
docs/content/doc/usage.md
Normal file
52
docs/content/doc/usage.md
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"title": "Using Hugo",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
Make sure either hugo is in your path or provide a path to it.
|
||||
|
||||
$ hugo --help
|
||||
usage: hugo [flags] []
|
||||
-b="": hostname (and path) to the root eg. http://spf13.com/
|
||||
-c="config.json": config file (default is path/config.json)
|
||||
-d=false: include content marked as draft
|
||||
-h=false: show this help
|
||||
-k=false: analyze content and provide feedback
|
||||
-p="": filesystem path to read files relative from
|
||||
-w=false: watch filesystem for changes and recreate as needed
|
||||
-s=false: a (very) simple webserver
|
||||
-p="1313": port for webserver to run on
|
||||
|
||||
## Common Usage Example:
|
||||
|
||||
The most common use is probably to run hugo with your current
|
||||
directory being the input directory.
|
||||
|
||||
|
||||
$ hugo
|
||||
> X pages created
|
||||
> Y indicies created
|
||||
|
||||
|
||||
If you are working on things and want to see the changes
|
||||
immediately, tell Hugo to watch for changes.
|
||||
<br>
|
||||
**It will
|
||||
recreate the site faster than you can tab over to
|
||||
your browser to view the changes.**
|
||||
|
||||
$ hugo -p ~/mysite -w
|
||||
Watching for changes. Press ctrl+c to stop
|
||||
15 pages created
|
||||
0 tags created
|
||||
|
||||
Hugo can even run a server and create your site at the same time!
|
||||
|
||||
$hugo -p ~/mysite -w -s
|
||||
Watching for changes. Press ctrl+c to stop
|
||||
15 pages created
|
||||
0 tags created
|
||||
Web Server is available at http://localhost:1313
|
||||
Press ctrl+c to stop
|
||||
|
||||
|
29
docs/content/doc/variables.md
Normal file
29
docs/content/doc/variables.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"title": "Variables",
|
||||
"Pubdate": "2013-07-01"
|
||||
}
|
||||
|
||||
Hugo makes a set of values available to the templates. Go templates are context based. The following
|
||||
are available in the context for the templates.
|
||||
|
||||
**.Title** The title for the content. <br>
|
||||
**.Description** The description for the content.<br>
|
||||
**.Keywords** The meta keywords for this content.<br>
|
||||
**.Date** The date the content is published on.<br>
|
||||
**.Indexes** These will use the field name of the plural form of the index (see tags and categories above)<br>
|
||||
**.Permalink** The Permanent link for this page.<br>
|
||||
**.FuzzyWordCount** The approximate number of words in the content.<br>
|
||||
**.RSSLink** Link to the indexes' rss link <br>
|
||||
|
||||
Any value defined in the front matter, including indexes will be made available under `.Params`.
|
||||
Take for example I'm using tags and categories as my indexes. The following would be how I would access them:
|
||||
|
||||
**.Params.Tags** <br>
|
||||
**.Params.Categories** <br>
|
||||
|
||||
Also available is `.Site` which has the following:
|
||||
|
||||
**.Site.BaseUrl** The base URL for the site as defined in the config.json file.<br>
|
||||
**.Site.Indexes** The names of the indexes of the site.<br>
|
||||
**.Site.LastChange** The date of the last change of the most recent content.<br>
|
||||
**.Site.Recent** Array of all content ordered by Date, newest first<br>
|
12
docs/layouts/chrome/footer.html
Normal file
12
docs/layouts/chrome/footer.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<footer id="footer">
|
||||
<p class="pull-right"><a href="#top">Back to top</a></p>
|
||||
Made by <a href="http://spf13.com">Steve Francia</a>.<br>
|
||||
Code licensed under the <a href="https://github.com/spf13/hugo/blob/master/LICENSE.md">Simple Public License 2.0</a>.<br>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
20
docs/layouts/chrome/header.html
Normal file
20
docs/layouts/chrome/header.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ .Title }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
|
||||
{{ template "chrome/includes.html" . }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar"></div>
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<div class="well" style="background-color: #222; color: #ccc;">
|
||||
<h1>Hugo</h1>
|
||||
<p>A Fast and Flexible Static Site Generator built with love by <a href="http://spf13.com">spf13</a> in GO</p>
|
||||
</div>
|
||||
{{ template "chrome/menu.html" . }}
|
||||
</div>
|
||||
<div class="span9">
|
2
docs/layouts/chrome/includes.html
Normal file
2
docs/layouts/chrome/includes.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/static/css/bootstrap-responsive.css">
|
28
docs/layouts/chrome/menu.html
Normal file
28
docs/layouts/chrome/menu.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
<ul class="nav nav-list">
|
||||
<li> <a href="/">Home</a> </li>
|
||||
<li class="divider"></li>
|
||||
<li class="nav-header">Getting Started</li>
|
||||
<li> <a href="/doc/installing.html">Installing Hugo</a> </li>
|
||||
<li> <a href="/doc/usage.html">Usage</a> </li>
|
||||
<li> <a href="/doc/configuration.html">Configuration</a> </li>
|
||||
<li> <a href="/doc/source-directory.html">Input Directory Layout</a> </li>
|
||||
<li class="divider"></li>
|
||||
<li class="nav-header">Layout</li>
|
||||
<li> <a href="/doc/templates.html">Templates</a> </li>
|
||||
<li> <a href="/doc/variables.html">Variables</a> </li>
|
||||
<li class="divider"></li>
|
||||
<li class="nav-header">Content</li>
|
||||
<li> <a href="/doc/organization.html">Organization</a> </li>
|
||||
<li> <a href="/doc/front-matter.html">Front Matter</a> </li>
|
||||
<li> <a href="/doc/example.html">Example</a> </li>
|
||||
<li class="divider"></li>
|
||||
<li class="nav-header">Extras</li>
|
||||
<li> <a href="/doc/shortcodes.html">ShortCodes</a> </li>
|
||||
<li class="divider"></li>
|
||||
<li class="nav-header">Meta</li>
|
||||
<li> <a href="/doc/release-notes.html">Release Notes</a> </li>
|
||||
<li> <a href="/doc/roadmap.html">Roadmap</a> </li>
|
||||
<li> <a href="/doc/contributing.html">Contributing</a> </li>
|
||||
<li> <a href="/doc/contributors.html">Contributors</a> </li>
|
||||
<li> <a href="/doc/license.html">License</a> </li>
|
||||
</ul>
|
4
docs/layouts/doc/single.html
Normal file
4
docs/layouts/doc/single.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
{{ template "chrome/header.html" . }}
|
||||
<h1>{{ .Title }}</h1>
|
||||
{{ .Content }}
|
||||
{{ template "chrome/footer.html" . }}
|
46
docs/layouts/index.html
Normal file
46
docs/layouts/index.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hugo Static Site Generator written in GO lang</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
|
||||
{{ template "chrome/includes.html" . }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar"></div>
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
{{ template "chrome/menu.html" . }}
|
||||
</div>
|
||||
<div class="span9">
|
||||
|
||||
<div class="hero-unit" style="background-color: #222; color: #ccc;">
|
||||
<h1>Hugo</h1>
|
||||
<p>A Fast and Flexible Static Site Generator built with love by <a href="http://spf13.com">spf13</a> in GO</p>
|
||||
<p>
|
||||
<a class="btn btn-large btn-success" href="/doc/installing.html">Get Started</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<h2>Fast
|
||||
<br>
|
||||
</h2>
|
||||
<p>Written in GoLang for speed, Hugo is significantly faster than other
|
||||
static site generators. It's so fast that it will render the site in
|
||||
less time than it takes to switch to your browser and reload.</p>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<h2>Flexible</h2>
|
||||
<p>Hugo is made to be very flexible. Define your own content types. Define
|
||||
your own indexes. Build your own templates, shortcodes and more.</p>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<h2>Fun</h2>
|
||||
<p>Hugo is more fun than you can shake a stick at. Hugo removes all
|
||||
the cruft of building a site allowing you to focus on creating the
|
||||
best site possible.</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ template "chrome/footer.html" }}
|
BIN
docs/public/static/.DS_Store
vendored
Normal file
BIN
docs/public/static/.DS_Store
vendored
Normal file
Binary file not shown.
1109
docs/public/static/css/bootstrap-responsive.css
vendored
Executable file
1109
docs/public/static/css/bootstrap-responsive.css
vendored
Executable file
File diff suppressed because it is too large
Load diff
9
docs/public/static/css/bootstrap.min.css
vendored
Normal file
9
docs/public/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
143
hugolib/config.go
Normal file
143
hugolib/config.go
Normal file
|
@ -0,0 +1,143 @@
|
|||
// Copyright © 2013 Steve Francia <spf@spf13.com>.
|
||||
//
|
||||
// Licensed under the Simple Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://opensource.org/licenses/Simple-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// config file items
|
||||
type Config struct {
|
||||
SourceDir, PublishDir, BaseUrl, StaticDir string
|
||||
Path, CacheDir, LayoutDir, DefaultLayout string
|
||||
Indexes map[string]string // singular, plural
|
||||
ProcessFilters map[string][]string
|
||||
BuildDrafts bool
|
||||
}
|
||||
|
||||
var c Config
|
||||
|
||||
// Read cfgfile or setup defaults.
|
||||
func SetupConfig(cfgfile *string, path *string) *Config {
|
||||
c.setPath(*path)
|
||||
|
||||
configPath, err := c.findConfigFile(*cfgfile)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("%v", err)
|
||||
fmt.Println(" using defaults instead")
|
||||
}
|
||||
|
||||
// set defaults
|
||||
|
||||
c.SourceDir = "content"
|
||||
c.LayoutDir = "layouts"
|
||||
c.PublishDir = "public"
|
||||
c.StaticDir = "static"
|
||||
c.DefaultLayout = "post"
|
||||
c.BuildDrafts = false
|
||||
|
||||
file, err := ioutil.ReadFile(configPath)
|
||||
if err == nil {
|
||||
if err := json.Unmarshal(file, &c); err != nil {
|
||||
fmt.Printf("Error parsing config: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// set index defaults if none provided
|
||||
if len(c.Indexes) == 0 {
|
||||
c.Indexes = make(map[string]string)
|
||||
c.Indexes["tag"] = "tags"
|
||||
c.Indexes["category"] = "categories"
|
||||
}
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c *Config) setPath(p string) {
|
||||
if p == "" {
|
||||
path, err := FindPath()
|
||||
if err != nil {
|
||||
fmt.Printf("Error finding path: %s", err)
|
||||
}
|
||||
c.Path = path
|
||||
} else {
|
||||
path, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
fmt.Printf("Error finding path: %s", err)
|
||||
}
|
||||
c.Path = path
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) GetPath() string {
|
||||
if c.Path == "" {
|
||||
c.setPath("")
|
||||
}
|
||||
return c.Path
|
||||
}
|
||||
|
||||
func FindPath() (string, error) {
|
||||
serverFile, err := filepath.Abs(os.Args[0])
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Can't get absolute path for executable: %v", err)
|
||||
}
|
||||
|
||||
path := filepath.Dir(serverFile)
|
||||
realFile, err := filepath.EvalSymlinks(serverFile)
|
||||
|
||||
if err != nil {
|
||||
if _, err = os.Stat(serverFile + ".exe"); err == nil {
|
||||
realFile = filepath.Clean(serverFile + ".exe")
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && realFile != serverFile {
|
||||
path = filepath.Dir(realFile)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (c *Config) GetAbsPath(name string) string {
|
||||
if path.IsAbs(name) {
|
||||
return name
|
||||
}
|
||||
|
||||
p := filepath.Join(c.GetPath(), name)
|
||||
return p
|
||||
}
|
||||
|
||||
func (c *Config) findConfigFile(configFileName string) (string, error) {
|
||||
// If the full path is given, just use that
|
||||
if path.IsAbs(configFileName) {
|
||||
return configFileName, nil
|
||||
}
|
||||
|
||||
// Else check the local directory
|
||||
t := c.GetAbsPath(configFileName)
|
||||
if b, _ := exists(t); b {
|
||||
return t, nil
|
||||
} else {
|
||||
return "", fmt.Errorf("config file not found at: %s", t)
|
||||
}
|
||||
|
||||
return "", nil // This line won't ever happen.. looking forward to go 1.1 when I don't need it
|
||||
}
|
309
hugolib/helpers.go
Normal file
309
hugolib/helpers.go
Normal file
|
@ -0,0 +1,309 @@
|
|||
// Copyright © 2013 Steve Francia <spf@spf13.com>.
|
||||
//
|
||||
// Licensed under the Simple Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://opensource.org/licenses/Simple-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/kr/pretty"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var sanitizeRegexp = regexp.MustCompile("[^a-zA-Z0-9/_-]")
|
||||
|
||||
// TODO: Make these wrappers private
|
||||
// Wrapper around Fprintf taking verbose flag in account.
|
||||
func Printvf(format string, a ...interface{}) {
|
||||
//if *verbose {
|
||||
fmt.Fprintf(os.Stderr, format, a...)
|
||||
//}
|
||||
}
|
||||
|
||||
func Printer(x interface{}) {
|
||||
fmt.Printf("%#v", pretty.Formatter(x))
|
||||
fmt.Println("")
|
||||
}
|
||||
|
||||
// Wrapper around Fprintln taking verbose flag in account.
|
||||
func Printvln(a ...interface{}) {
|
||||
//if *verbose {
|
||||
fmt.Fprintln(os.Stderr, a...)
|
||||
//}
|
||||
}
|
||||
|
||||
func FatalErr(str string) {
|
||||
fmt.Println(str)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func PrintErr(str string, a ...interface{}) {
|
||||
fmt.Fprintln(os.Stderr, str, a)
|
||||
}
|
||||
|
||||
func Error(str string, a ...interface{}) {
|
||||
fmt.Fprintln(os.Stderr, str, a)
|
||||
}
|
||||
|
||||
func interfaceToStringToDate(i interface{}) time.Time {
|
||||
s := interfaceToString(i)
|
||||
d, e := time.Parse("02 Jan 06 15:04 MST", s)
|
||||
|
||||
if e != nil {
|
||||
d, e = time.Parse("2006-01-02", s)
|
||||
}
|
||||
|
||||
if e != nil {
|
||||
d, e = time.Parse("02 Jan 06", s)
|
||||
}
|
||||
|
||||
return d
|
||||
|
||||
}
|
||||
|
||||
func interfaceToBool(i interface{}) bool {
|
||||
switch b := i.(type) {
|
||||
case bool:
|
||||
return b
|
||||
default:
|
||||
Error("Only Boolean values are supported for this JSON key")
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func interfaceArrayToStringArray(i interface{}) []string {
|
||||
var a []string
|
||||
|
||||
switch vv := i.(type) {
|
||||
case []interface{}:
|
||||
for _, u := range vv {
|
||||
a = append(a, interfaceToString(u))
|
||||
}
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func interfaceToString(i interface{}) string {
|
||||
switch s := i.(type) {
|
||||
case string:
|
||||
return s
|
||||
default:
|
||||
Error("Only Strings are supported for this JSON key")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check if Exists && is Directory
|
||||
func dirExists(path string) (bool, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err == nil && fi.IsDir() {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check if File / Directory Exists
|
||||
func exists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
func mkdirIf(path string) {
|
||||
err := os.Mkdir(path, 0777)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Urlize(url string) string {
|
||||
return Sanitize(strings.ToLower(strings.Replace(strings.TrimSpace(url), " ", "-", -1)))
|
||||
}
|
||||
|
||||
func Gt(a interface{}, b interface{}) bool {
|
||||
var left, right int64
|
||||
av := reflect.ValueOf(a)
|
||||
|
||||
switch av.Kind() {
|
||||
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
|
||||
left = int64(av.Len())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
left = av.Int()
|
||||
case reflect.String:
|
||||
left, _ = strconv.ParseInt(av.String(), 10, 64)
|
||||
}
|
||||
|
||||
bv := reflect.ValueOf(b)
|
||||
|
||||
switch bv.Kind() {
|
||||
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
|
||||
right = int64(bv.Len())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
right = bv.Int()
|
||||
case reflect.String:
|
||||
right, _ = strconv.ParseInt(bv.String(), 10, 64)
|
||||
}
|
||||
|
||||
return left > right
|
||||
}
|
||||
|
||||
func IsSet(a interface{}, key interface{}) bool {
|
||||
av := reflect.ValueOf(a)
|
||||
kv := reflect.ValueOf(key)
|
||||
|
||||
switch av.Kind() {
|
||||
case reflect.Array, reflect.Chan, reflect.Slice:
|
||||
if int64(av.Len()) > kv.Int() {
|
||||
return true
|
||||
}
|
||||
case reflect.Map:
|
||||
if kv.Type() == av.Type().Key() {
|
||||
return av.MapIndex(kv).IsValid()
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func ReturnWhenSet(a interface{}, index int) interface{} {
|
||||
av := reflect.ValueOf(a)
|
||||
|
||||
switch av.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
if av.Len() > index {
|
||||
|
||||
avv := av.Index(index)
|
||||
switch avv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return avv.Int()
|
||||
case reflect.String:
|
||||
return avv.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func Sanitize(s string) string {
|
||||
return sanitizeRegexp.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
func fileExt(path string) (file, ext string) {
|
||||
if strings.Contains(path, ".") {
|
||||
i := len(path) - 1
|
||||
for path[i] != '.' {
|
||||
i--
|
||||
}
|
||||
return path[:i], path[i+1:]
|
||||
}
|
||||
return path, ""
|
||||
}
|
||||
|
||||
func replaceExtension(path string, newExt string) string {
|
||||
f, _ := fileExt(path)
|
||||
return f + "." + newExt
|
||||
}
|
||||
|
||||
func TotalWords(s string) int {
|
||||
return len(strings.Fields(s))
|
||||
}
|
||||
|
||||
func WordCount(s string) map[string]int {
|
||||
m := make(map[string]int)
|
||||
for _, f := range strings.Fields(s) {
|
||||
m[f] += 1
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func StripHTML(s string) string {
|
||||
output := ""
|
||||
|
||||
// Shortcut strings with no tags in them
|
||||
if !strings.ContainsAny(s, "<>") {
|
||||
output = s
|
||||
} else {
|
||||
s = strings.Replace(s, "\n", " ", -1)
|
||||
s = strings.Replace(s, "</p>", " \n", -1)
|
||||
s = strings.Replace(s, "<br>", " \n", -1)
|
||||
s = strings.Replace(s, "</br>", " \n", -1)
|
||||
|
||||
// Walk through the string removing all tags
|
||||
b := new(bytes.Buffer)
|
||||
inTag := false
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '<':
|
||||
inTag = true
|
||||
case '>':
|
||||
inTag = false
|
||||
default:
|
||||
if !inTag {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
output = b.String()
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func TruncateWords(s string, max int) string {
|
||||
words := strings.Fields(s)
|
||||
if max > len(words) {
|
||||
return strings.Join(words, " ")
|
||||
}
|
||||
|
||||
return strings.Join(words[:max], " ")
|
||||
}
|
||||
|
||||
func TruncateWordsToWholeSentence(s string, max int) string {
|
||||
words := strings.Fields(s)
|
||||
if max > len(words) {
|
||||
return strings.Join(words, " ")
|
||||
}
|
||||
|
||||
for counter, word := range words[max:] {
|
||||
if strings.HasSuffix(word, ".") ||
|
||||
strings.HasSuffix(word, "?") ||
|
||||
strings.HasSuffix(word, ".\"") ||
|
||||
strings.HasSuffix(word, "!") {
|
||||
return strings.Join(words[:max+counter+1], " ")
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(words[:max], " ")
|
||||
}
|
||||
|
||||
func MakePermalink(domain string, path string) string {
|
||||
return strings.TrimRight(domain, "/") + "/" + strings.TrimLeft(path, "/")
|
||||
}
|
58
hugolib/index.go
Normal file
58
hugolib/index.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Copyright © 2013 Steve Francia <spf@spf13.com>.
|
||||
//
|
||||
// Licensed under the Simple Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://opensource.org/licenses/Simple-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
type Index map[string]Pages
|
||||
type IndexList map[string]Index
|
||||
|
||||
type OrderedIndex []*Pages
|
||||
type OrderedIndexList map[string]OrderedIndex
|
||||
|
||||
// KeyPrep... Indexes should be case insensitive. Can make it easily conditional later.
|
||||
func kp(in string) string {
|
||||
return Urlize(in)
|
||||
}
|
||||
|
||||
func (i Index) Get(key string) Pages { return i[kp(key)] }
|
||||
func (i Index) Count(key string) int { return len(i[kp(key)]) }
|
||||
func (i Index) Add(key string, p *Page) {
|
||||
key = kp(key)
|
||||
i[key] = append(i[key], p)
|
||||
}
|
||||
|
||||
func (l IndexList) BuildOrderedIndexList() *OrderedIndexList {
|
||||
oil := make(OrderedIndexList, len(l))
|
||||
for idx_name, index := range l {
|
||||
i := 0
|
||||
oi := make(OrderedIndex, len(index))
|
||||
for _, e := range index {
|
||||
oi[i] = &e
|
||||
i++
|
||||
}
|
||||
oi.Sort()
|
||||
oil[idx_name] = oi
|
||||
}
|
||||
return &oil
|
||||
}
|
||||
|
||||
func (idx OrderedIndex) Len() int { return len(idx) }
|
||||
|
||||
func (idx OrderedIndex) Less(i, j int) bool { return len(*idx[i]) < len(*idx[j]) }
|
||||
func (idx OrderedIndex) Swap(i, j int) { idx[i], idx[j] = idx[j], idx[i] }
|
||||
func (idx OrderedIndex) Sort() { sort.Sort(idx) }
|
||||
func (idx OrderedIndex) Limit(n int) OrderedIndex { return idx[0:n] }
|
43
hugolib/node.go
Normal file
43
hugolib/node.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Copyright © 2013 Steve Francia <spf@spf13.com>.
|
||||
//
|
||||
// Licensed under the Simple Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://opensource.org/licenses/Simple-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
Url string
|
||||
Permalink template.HTML
|
||||
RSSlink template.HTML
|
||||
Site SiteInfo
|
||||
layout string
|
||||
Data map[string]interface{}
|
||||
Section string
|
||||
Slug string
|
||||
Title string
|
||||
Description string
|
||||
Keywords []string
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
func (n *Node) GetSection() string {
|
||||
s := ""
|
||||
if n.Section != "" {
|
||||
s = n.Section
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
381
hugolib/page.go
Normal file
381
hugolib/page.go
Normal file
|
@ -0,0 +1,381 @@
|
|||
// Copyright © 2013 Steve Francia <spf@spf13.com>.
|
||||
//
|
||||
// Licensed under the Simple Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://opensource.org/licenses/Simple-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/theplant/blackfriday"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ = filepath.Base("")
|
||||
|
||||
type Page struct {
|
||||
Status string
|
||||
Images []string
|
||||
Content template.HTML
|
||||
Summary template.HTML
|
||||
RawMarkdown string // TODO should be []byte
|
||||
Params map[string]interface{}
|
||||
RenderedContent *bytes.Buffer
|
||||
contentType string
|
||||
Draft bool
|
||||
Tmpl *template.Template
|
||||
PageMeta
|
||||
File
|
||||
Position
|
||||
Node
|
||||
}
|
||||
|
||||
const summaryLength = 70
|
||||
|
||||
type File struct {
|
||||
FileName, OutFile, Extension string
|
||||
}
|
||||
|
||||
type PageMeta struct {
|
||||
WordCount int
|
||||
FuzzyWordCount int
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
Prev *Page
|
||||
Next *Page
|
||||
}
|
||||
|
||||
type Pages []*Page
|
||||
|
||||
func (p Pages) Len() int { return len(p) }
|
||||
func (p Pages) Less(i, j int) bool { return p[i].Date.Unix() > p[j].Date.Unix() }
|
||||
func (p Pages) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
// TODO eliminate unnecessary things
|
||||
func (p Pages) Sort() { sort.Sort(p) }
|
||||
func (p Pages) Limit(n int) Pages { return p[0:n] }
|
||||
|
||||
func initializePage(filename string) (page Page) {
|
||||
page = Page{}
|
||||
page.Date, _ = time.Parse("20060102", "20080101")
|
||||
page.FileName = filename
|
||||
page.contentType = ""
|
||||
page.Extension = "html"
|
||||
page.Params = make(map[string]interface{})
|
||||
page.Keywords = make([]string, 10, 30)
|
||||
page.setSection()
|
||||
|
||||
return page
|
||||
}
|
||||
|
||||
func (p *Page) setSection() {
|
||||
x := strings.Split(p.FileName, "/")
|
||||
|
||||
if section := x[len(x)-2]; section != "content" {
|
||||
p.Section = section
|
||||
}
|
||||
}
|
||||
|
||||
func (page *Page) Type() string {
|
||||
if page.contentType != "" {
|
||||
return page.contentType
|
||||
}
|
||||
|
||||
if x := page.GetSection(); x != "" {
|
||||
return x
|
||||
}
|
||||
|
||||
return "page"
|
||||
}
|
||||
|
||||
func (page *Page) Layout(l ...string) string {
|
||||
layout := ""
|
||||
if len(l) == 0 {
|
||||
layout = "single"
|
||||
} else {
|
||||
layout = l[0]
|
||||
}
|
||||
|
||||
if x := page.layout; x != "" {
|
||||
return x
|
||||
}
|
||||
|
||||
return strings.ToLower(page.Type()) + "/" + layout + ".html"
|
||||
}
|
||||
|
||||
// TODO should return errors as well
|
||||
// TODO new page should return just a page
|
||||
// TODO initalize separately... load from reader (file, or []byte)
|
||||
func NewPage(filename string) *Page {
|
||||
p := initializePage(filename)
|
||||
if err := p.buildPageFromFile(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
p.analyzePage()
|
||||
|
||||
return &p
|
||||
}
|
||||
|
||||
func (p *Page) analyzePage() {
|
||||
p.WordCount = TotalWords(p.RawMarkdown)
|
||||
p.FuzzyWordCount = int((p.WordCount+100)/100) * 100
|
||||
}
|
||||
|
||||
// TODO //rewrite to use byte methods instead
|
||||
func (page *Page) parseJsonMetaData(data []byte) ([]string, error) {
|
||||
var err error
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
datum := lines[0:]
|
||||
|
||||
// go through content parse between "{" and "}"
|
||||
// must be on their own lines (for now)
|
||||
var found = 0
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "{" {
|
||||
found += 1
|
||||
}
|
||||
|
||||
if line == "}" {
|
||||
found -= 1
|
||||
}
|
||||
|
||||
if found == 0 {
|
||||
datum = lines[0 : i+1]
|
||||
lines = lines[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = page.handleJsonMetaData([]byte(strings.Join(datum, "\n")))
|
||||
|
||||
return lines, err
|
||||
}
|
||||
|
||||
func (p *Page) Permalink() template.HTML {
|
||||
if len(strings.TrimSpace(p.Slug)) > 0 {
|
||||
return template.HTML(MakePermalink(string(p.Site.BaseUrl), strings.TrimSpace(p.Section)+"/"+p.Slug))
|
||||
} else if len(strings.TrimSpace(p.Url)) > 2 {
|
||||
return template.HTML(MakePermalink(string(p.Site.BaseUrl), strings.TrimSpace(p.Url)))
|
||||
} else {
|
||||
_, t := filepath.Split(p.FileName)
|
||||
x := replaceExtension(strings.TrimSpace(t), p.Extension)
|
||||
return template.HTML(MakePermalink(string(p.Site.BaseUrl), strings.TrimSpace(p.Section)+"/"+x))
|
||||
}
|
||||
}
|
||||
|
||||
func (page *Page) handleJsonMetaData(datum []byte) error {
|
||||
var f interface{}
|
||||
if err := json.Unmarshal(datum, &f); err != nil {
|
||||
return fmt.Errorf("Invalide JSON in $v \nError parsing page meta data: %s", page.FileName, err)
|
||||
}
|
||||
|
||||
m := f.(map[string]interface{})
|
||||
|
||||
for k, v := range m {
|
||||
switch strings.ToLower(k) {
|
||||
case "title":
|
||||
page.Title = interfaceToString(v)
|
||||
case "description":
|
||||
page.Description = interfaceToString(v)
|
||||
case "slug":
|
||||
page.Slug = Urlize(interfaceToString(v))
|
||||
case "url":
|
||||
if url := interfaceToString(v); strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
||||
return fmt.Errorf("Only relative urls are supported, %v provided", url)
|
||||
}
|
||||
page.Url = Urlize(interfaceToString(v))
|
||||
case "type":
|
||||
page.contentType = interfaceToString(v)
|
||||
case "keywords":
|
||||
page.Keywords = interfaceArrayToStringArray(v)
|
||||
case "date", "pubdate":
|
||||
page.Date = interfaceToStringToDate(v)
|
||||
case "draft":
|
||||
page.Draft = interfaceToBool(v)
|
||||
case "layout":
|
||||
page.layout = interfaceToString(v)
|
||||
case "status":
|
||||
page.Status = interfaceToString(v)
|
||||
default:
|
||||
// If not one of the explicit values, store in Params
|
||||
//fmt.Println(strings.ToLower(k))
|
||||
switch vv := v.(type) {
|
||||
case string: // handle string values
|
||||
page.Params[strings.ToLower(k)] = vv
|
||||
default: // handle array of strings as well
|
||||
switch vvv := vv.(type) {
|
||||
case []interface{}:
|
||||
var a = make([]string, len(vvv))
|
||||
for i, u := range vvv {
|
||||
a[i] = interfaceToString(u)
|
||||
}
|
||||
page.Params[strings.ToLower(k)] = a
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//Printer(page.Params)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (page *Page) GetParam(key string) interface{} {
|
||||
v := page.Params[strings.ToLower(key)]
|
||||
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v.(type) {
|
||||
case string:
|
||||
return interfaceToString(v)
|
||||
case []string:
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (page *Page) parseFileMetaData(data []byte) ([]string, error) {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
|
||||
// go through content parse from --- to ---
|
||||
var found = 0
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if found == 1 {
|
||||
// parse line for param
|
||||
colonIndex := strings.Index(line, ":")
|
||||
if colonIndex > 0 {
|
||||
key := strings.TrimSpace(line[:colonIndex])
|
||||
value := strings.TrimSpace(line[colonIndex+1:])
|
||||
value = strings.Trim(value, "\"") //remove quotes
|
||||
switch key {
|
||||
case "title":
|
||||
page.Title = value
|
||||
case "layout":
|
||||
page.layout = value
|
||||
case "extension":
|
||||
page.Extension = "." + value
|
||||
default:
|
||||
page.Params[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
} else if found >= 2 {
|
||||
// params over
|
||||
lines = lines[i:]
|
||||
break
|
||||
}
|
||||
|
||||
if line == "---" {
|
||||
found += 1
|
||||
}
|
||||
}
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
func (page *Page) Err(message string) {
|
||||
fmt.Println(page.FileName + " : " + message)
|
||||
}
|
||||
|
||||
// TODO return error on last line instead of nil
|
||||
func (page *Page) parseFileHeading(data []byte) ([]string, error) {
|
||||
if len(data) == 0 {
|
||||
page.Err("Empty File, skipping")
|
||||
} else {
|
||||
if data[0] == '-' {
|
||||
return page.parseFileMetaData(data)
|
||||
}
|
||||
return page.parseJsonMetaData(data)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *Page) Render(layout ...string) template.HTML {
|
||||
curLayout := ""
|
||||
|
||||
if len(layout) > 0 {
|
||||
curLayout = layout[0]
|
||||
}
|
||||
|
||||
return template.HTML(string(p.ExecuteTemplate(curLayout).Bytes()))
|
||||
}
|
||||
|
||||
func (p *Page) ExecuteTemplate(layout string) *bytes.Buffer {
|
||||
l := p.Layout(layout)
|
||||
buffer := new(bytes.Buffer)
|
||||
p.Tmpl.ExecuteTemplate(buffer, l, p)
|
||||
return buffer
|
||||
}
|
||||
|
||||
func (page *Page) readFile() []byte {
|
||||
var data, err = ioutil.ReadFile(page.FileName)
|
||||
if err != nil {
|
||||
PrintErr("Error Reading: " + page.FileName)
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (page *Page) buildPageFromFile() error {
|
||||
data := page.readFile()
|
||||
|
||||
content, err := page.parseFileHeading(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := page.setOutFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
page.convertMarkdown(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Page) setOutFile() error {
|
||||
if len(strings.TrimSpace(p.Slug)) > 0 {
|
||||
// Use Slug if provided
|
||||
p.OutFile = strings.TrimSpace(p.Slug + "." + p.Extension)
|
||||
} else if len(strings.TrimSpace(p.Url)) > 2 {
|
||||
// Use Url if provided & Slug missing
|
||||
p.OutFile = strings.TrimSpace(p.Url)
|
||||
} else {
|
||||
// Fall back to filename
|
||||
_, t := filepath.Split(p.FileName)
|
||||
p.OutFile = replaceExtension(strings.TrimSpace(t), p.Extension)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (page *Page) convertMarkdown(lines []string) {
|
||||
|
||||
page.RawMarkdown = strings.Join(lines, "\n")
|
||||
content := string(blackfriday.MarkdownCommon([]byte(page.RawMarkdown)))
|
||||
page.Content = template.HTML(content)
|
||||
page.Summary = template.HTML(TruncateWordsToWholeSentence(StripHTML(StripShortcodes(content)), summaryLength))
|
||||
}
|
131
hugolib/shortcode.go
Normal file
131
hugolib/shortcode.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
// Copyright © 2013 Steve Francia <spf@spf13.com>.
|
||||
//
|
||||
// Licensed under the Simple Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://opensource.org/licenses/Simple-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var _ = fmt.Println
|
||||
|
||||
type ShortcodeFunc func([]string) string
|
||||
|
||||
type Shortcode struct {
|
||||
Name string
|
||||
Func ShortcodeFunc
|
||||
}
|
||||
|
||||
type ShortcodeWithPage struct {
|
||||
Params interface{}
|
||||
Page *Page
|
||||
}
|
||||
|
||||
type Shortcodes map[string]ShortcodeFunc
|
||||
|
||||
func ShortcodesHandle(stringToParse string, p *Page, t *template.Template) string {
|
||||
posStart := strings.Index(stringToParse, "{{%")
|
||||
if posStart > 0 {
|
||||
posEnd := strings.Index(stringToParse[posStart:], "%}}") + posStart
|
||||
if posEnd > posStart {
|
||||
name, par := SplitParams(stringToParse[posStart+3 : posEnd])
|
||||
params := Tokenize(par)
|
||||
var data = &ShortcodeWithPage{Params: params, Page: p}
|
||||
newString := stringToParse[:posStart] + ShortcodeRender(name, data, t) + ShortcodesHandle(stringToParse[posEnd+3:], p, t)
|
||||
return newString
|
||||
}
|
||||
}
|
||||
return stringToParse
|
||||
}
|
||||
|
||||
func StripShortcodes(stringToParse string) string {
|
||||
posStart := strings.Index(stringToParse, "{{%")
|
||||
if posStart > 0 {
|
||||
posEnd := strings.Index(stringToParse[posStart:], "%}}") + posStart
|
||||
if posEnd > posStart {
|
||||
newString := stringToParse[:posStart] + StripShortcodes(stringToParse[posEnd+3:])
|
||||
return newString
|
||||
}
|
||||
}
|
||||
return stringToParse
|
||||
}
|
||||
|
||||
func Tokenize(in string) interface{} {
|
||||
first := strings.Fields(in)
|
||||
var final = make([]string, 0)
|
||||
var keys = make([]string, 0)
|
||||
inQuote := false
|
||||
start := 0
|
||||
|
||||
for i, v := range first {
|
||||
index := strings.Index(v, "=")
|
||||
|
||||
if !inQuote {
|
||||
if index > 1 {
|
||||
keys = append(keys, v[:index])
|
||||
v = v[index+1:]
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(v, "“") && !inQuote {
|
||||
final = append(final, v)
|
||||
} else if inQuote && strings.HasSuffix(v, "”") && !strings.HasSuffix(v, "\\\"") {
|
||||
first[i] = v[:len(v)-7]
|
||||
final = append(final, strings.Join(first[start:i+1], " "))
|
||||
inQuote = false
|
||||
} else if strings.HasPrefix(v, "“") && !inQuote {
|
||||
if strings.HasSuffix(v, "”") {
|
||||
final = append(final, v[7:len(v)-7])
|
||||
} else {
|
||||
start = i
|
||||
first[i] = v[7:]
|
||||
inQuote = true
|
||||
}
|
||||
}
|
||||
|
||||
// No closing "... just make remainder the final token
|
||||
if inQuote && i == len(first) {
|
||||
final = append(final, first[start:len(first)]...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
var m = make(map[string]string)
|
||||
for i, k := range keys {
|
||||
m[k] = final[i]
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
func SplitParams(in string) (name string, par2 string) {
|
||||
i := strings.IndexFunc(strings.TrimSpace(in), unicode.IsSpace)
|
||||
if i < 1 {
|
||||
return strings.TrimSpace(in), ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(in[:i+1]), strings.TrimSpace(in[i+1:])
|
||||
}
|
||||
|
||||
func ShortcodeRender(name string, data *ShortcodeWithPage, t *template.Template) string {
|
||||
buffer := new(bytes.Buffer)
|
||||
t.ExecuteTemplate(buffer, "shortcodes/"+name+".html", data)
|
||||
return buffer.String()
|
||||
}
|
362
hugolib/site.go
Normal file
362
hugolib/site.go
Normal file
|
@ -0,0 +1,362 @@
|
|||
// Copyright © 2013 Steve Francia <spf@spf13.com>.
|
||||
//
|
||||
// Licensed under the Simple Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://opensource.org/licenses/Simple-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
|
||||
import (
|
||||
"bitbucket.org/pkg/inflect"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/spf13/nitro"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
//"sync"
|
||||
)
|
||||
|
||||
type Site struct {
|
||||
c Config
|
||||
Pages Pages
|
||||
Tmpl *template.Template
|
||||
Indexes IndexList
|
||||
Files []string
|
||||
Directories []string
|
||||
Sections Index
|
||||
Info SiteInfo
|
||||
Shortcodes map[string]ShortcodeFunc
|
||||
timer *nitro.B
|
||||
}
|
||||
|
||||
type SiteInfo struct {
|
||||
BaseUrl template.URL
|
||||
Indexes *OrderedIndexList
|
||||
Recent *Pages
|
||||
LastChange time.Time
|
||||
}
|
||||
|
||||
func (s *Site) getFromIndex(kind string, name string) Pages {
|
||||
return s.Indexes[kind][name]
|
||||
}
|
||||
|
||||
func NewSite(config *Config) *Site {
|
||||
return &Site{c: *config, timer: nitro.Initalize()}
|
||||
}
|
||||
|
||||
func (site *Site) Build() {
|
||||
site.Process()
|
||||
site.Render()
|
||||
site.Write()
|
||||
}
|
||||
|
||||
func (site *Site) Analyze() {
|
||||
site.Process()
|
||||
site.checkDescriptions()
|
||||
}
|
||||
|
||||
func (site *Site) Process() {
|
||||
site.initialize()
|
||||
site.prepTemplates()
|
||||
site.timer.Step("initialize & template prep")
|
||||
site.CreatePages()
|
||||
site.timer.Step("import pages")
|
||||
site.BuildSiteMeta()
|
||||
site.timer.Step("build indexes")
|
||||
}
|
||||
|
||||
func (site *Site) Render() {
|
||||
site.RenderIndexes()
|
||||
site.timer.Step("render and write indexes")
|
||||
site.RenderLists()
|
||||
site.timer.Step("render and write lists")
|
||||
site.RenderPages()
|
||||
site.timer.Step("render pages")
|
||||
site.ProcessShortcodes()
|
||||
site.timer.Step("render shortcodes")
|
||||
site.RenderHomePage()
|
||||
site.timer.Step("render and write homepage")
|
||||
}
|
||||
|
||||
func (site *Site) Write() {
|
||||
site.WritePages()
|
||||
site.timer.Step("write pages")
|
||||
}
|
||||
|
||||
func (site *Site) checkDescriptions() {
|
||||
for _, p := range site.Pages {
|
||||
if len(p.Description) < 60 {
|
||||
fmt.Print(p.FileName + " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) prepTemplates() {
|
||||
var templates = template.New("")
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"urlize": Urlize,
|
||||
"gt": Gt,
|
||||
"isset": IsSet,
|
||||
"echoParam": ReturnWhenSet,
|
||||
}
|
||||
|
||||
templates.Funcs(funcMap)
|
||||
|
||||
walker := func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
PrintErr("Walker: ", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
filetext, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
text := string(filetext)
|
||||
name := path[len(s.c.GetAbsPath(s.c.LayoutDir))+1:]
|
||||
t := templates.New(name)
|
||||
template.Must(t.Parse(text))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
filepath.Walk(s.c.GetAbsPath(s.c.LayoutDir), walker)
|
||||
|
||||
s.Tmpl = templates
|
||||
}
|
||||
|
||||
func (s *Site) initialize() {
|
||||
site := s
|
||||
|
||||
s.checkDirectories()
|
||||
|
||||
walker := func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
PrintErr("Walker: ", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
site.Directories = append(site.Directories, path)
|
||||
return nil
|
||||
} else {
|
||||
site.Files = append(site.Files, path)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
filepath.Walk(s.c.GetAbsPath(s.c.SourceDir), walker)
|
||||
|
||||
s.Info = SiteInfo{BaseUrl: template.URL(s.c.BaseUrl)}
|
||||
|
||||
s.Shortcodes = make(map[string]ShortcodeFunc)
|
||||
}
|
||||
|
||||
func (s *Site) checkDirectories() {
|
||||
if b, _ := dirExists(s.c.GetAbsPath(s.c.LayoutDir)); !b {
|
||||
FatalErr("No layout directory found, expecting to find it at " + s.c.GetAbsPath(s.c.LayoutDir))
|
||||
}
|
||||
if b, _ := dirExists(s.c.GetAbsPath(s.c.SourceDir)); !b {
|
||||
FatalErr("No source directory found, expecting to find it at " + s.c.GetAbsPath(s.c.SourceDir))
|
||||
}
|
||||
mkdirIf(s.c.GetAbsPath(s.c.PublishDir))
|
||||
}
|
||||
|
||||
func (s *Site) ProcessShortcodes() {
|
||||
for i, _ := range s.Pages {
|
||||
var bb bytes.Buffer
|
||||
bb.WriteString(ShortcodesHandle(s.Pages[i].RenderedContent.String(), s.Pages[i], s.Tmpl))
|
||||
s.Pages[i].RenderedContent = &bb
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) CreatePages() {
|
||||
for _, fileName := range s.Files {
|
||||
page := NewPage(fileName)
|
||||
page.Site = s.Info
|
||||
page.Tmpl = s.Tmpl
|
||||
if s.c.BuildDrafts || !page.Draft {
|
||||
s.Pages = append(s.Pages, page)
|
||||
}
|
||||
}
|
||||
|
||||
s.Pages.Sort()
|
||||
}
|
||||
|
||||
func (s *Site) BuildSiteMeta() {
|
||||
s.Indexes = make(IndexList)
|
||||
s.Sections = make(Index)
|
||||
|
||||
for _, plural := range s.c.Indexes {
|
||||
s.Indexes[plural] = make(Index)
|
||||
for i, p := range s.Pages {
|
||||
vals := p.GetParam(plural)
|
||||
|
||||
if vals != nil {
|
||||
for _, idx := range vals.([]string) {
|
||||
s.Indexes[plural].Add(idx, s.Pages[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, _ := range s.Indexes[plural] {
|
||||
s.Indexes[plural][k].Sort()
|
||||
}
|
||||
}
|
||||
|
||||
for i, p := range s.Pages {
|
||||
sect := p.GetSection()
|
||||
s.Sections.Add(sect, s.Pages[i])
|
||||
}
|
||||
|
||||
for k, _ := range s.Sections {
|
||||
s.Sections[k].Sort()
|
||||
}
|
||||
|
||||
s.Info.Indexes = s.Indexes.BuildOrderedIndexList()
|
||||
|
||||
s.Info.LastChange = s.Pages[0].Date
|
||||
}
|
||||
|
||||
func (s *Site) RenderPages() {
|
||||
for i, _ := range s.Pages {
|
||||
s.Pages[i].RenderedContent = s.RenderThing(s.Pages[i], s.Pages[i].Layout())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) WritePages() {
|
||||
for _, p := range s.Pages {
|
||||
s.WritePublic(p.Section, p.OutFile, p.RenderedContent.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) RenderIndexes() {
|
||||
for singular, plural := range s.c.Indexes {
|
||||
for k, o := range s.Indexes[plural] {
|
||||
n := s.NewNode()
|
||||
n.Title = strings.Title(k)
|
||||
url := Urlize(plural + "/" + k)
|
||||
n.Url = url + ".html"
|
||||
n.Permalink = template.HTML(MakePermalink(string(n.Site.BaseUrl), string(n.Url)))
|
||||
n.RSSlink = template.HTML(MakePermalink(string(n.Site.BaseUrl), string(url+".xml")))
|
||||
n.Date = o[0].Date
|
||||
n.Data[singular] = o
|
||||
n.Data["Pages"] = o
|
||||
layout := "indexes/" + singular + ".html"
|
||||
|
||||
x := s.RenderThing(n, layout)
|
||||
s.WritePublic(plural, k+".html", x.Bytes())
|
||||
|
||||
if a := s.Tmpl.Lookup("rss.xml"); a != nil {
|
||||
// XML Feed
|
||||
y := s.NewXMLBuffer()
|
||||
n.Url = Urlize(plural + "/" + k + ".xml")
|
||||
s.Tmpl.ExecuteTemplate(y, "rss.xml", n)
|
||||
s.WritePublic(plural, k+".xml", y.Bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) RenderLists() {
|
||||
for section, data := range s.Sections {
|
||||
n := s.NewNode()
|
||||
n.Title = strings.Title(inflect.Pluralize(section))
|
||||
n.Url = Urlize(section + "/index.html")
|
||||
n.Permalink = template.HTML(MakePermalink(string(n.Site.BaseUrl), string(n.Url)))
|
||||
n.RSSlink = template.HTML(MakePermalink(string(n.Site.BaseUrl), string(section+"/index.xml")))
|
||||
n.Date = data[0].Date
|
||||
n.Data["Pages"] = data
|
||||
layout := "indexes/" + section + ".html"
|
||||
|
||||
x := s.RenderThing(n, layout)
|
||||
s.WritePublic(section, "index.html", x.Bytes())
|
||||
|
||||
if a := s.Tmpl.Lookup("rss.xml"); a != nil {
|
||||
// XML Feed
|
||||
n.Url = Urlize(section + "/index.xml")
|
||||
y := s.NewXMLBuffer()
|
||||
s.Tmpl.ExecuteTemplate(y, "rss.xml", n)
|
||||
s.WritePublic(section, "index.xml", y.Bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) RenderHomePage() {
|
||||
n := s.NewNode()
|
||||
n.Title = ""
|
||||
n.Url = Urlize(string(n.Site.BaseUrl))
|
||||
n.RSSlink = template.HTML(MakePermalink(string(n.Site.BaseUrl), string("/index.xml")))
|
||||
n.Permalink = template.HTML(string(n.Site.BaseUrl))
|
||||
n.Date = s.Pages[0].Date
|
||||
if len(s.Pages) < 9 {
|
||||
n.Data["Pages"] = s.Pages
|
||||
} else {
|
||||
n.Data["Pages"] = s.Pages[:9]
|
||||
}
|
||||
x := s.RenderThing(n, "index.html")
|
||||
s.WritePublic("", "index.html", x.Bytes())
|
||||
|
||||
if a := s.Tmpl.Lookup("rss.xml"); a != nil {
|
||||
// XML Feed
|
||||
n.Url = Urlize("index.xml")
|
||||
y := s.NewXMLBuffer()
|
||||
s.Tmpl.ExecuteTemplate(y, "rss.xml", n)
|
||||
s.WritePublic("", "index.xml", y.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) Stats() {
|
||||
fmt.Printf("%d pages created \n", len(s.Pages))
|
||||
for _, pl := range s.c.Indexes {
|
||||
fmt.Printf("%d %s created\n", len(s.Indexes[pl]), pl)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) NewNode() Node {
|
||||
var y Node
|
||||
y.Data = make(map[string]interface{})
|
||||
y.Site = s.Info
|
||||
|
||||
return y
|
||||
}
|
||||
|
||||
func (s *Site) RenderThing(d interface{}, layout string) *bytes.Buffer {
|
||||
buffer := new(bytes.Buffer)
|
||||
s.Tmpl.ExecuteTemplate(buffer, layout, d)
|
||||
return buffer
|
||||
}
|
||||
|
||||
func (s *Site) NewXMLBuffer() *bytes.Buffer {
|
||||
header := "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n"
|
||||
return bytes.NewBufferString(header)
|
||||
}
|
||||
|
||||
func (s *Site) WritePublic(path string, filename string, content []byte) {
|
||||
AbsPath := ""
|
||||
if path != "" {
|
||||
// TODO double check the following line.. calling GetAbsPath 2x seems wrong
|
||||
mkdirIf(s.c.GetAbsPath(filepath.Join(s.c.GetAbsPath(s.c.PublishDir), path)))
|
||||
AbsPath = filepath.Join(s.c.GetAbsPath(s.c.PublishDir), path, filename)
|
||||
} else {
|
||||
AbsPath = filepath.Join(s.c.GetAbsPath(s.c.PublishDir), filename)
|
||||
}
|
||||
|
||||
file, _ := os.Create(AbsPath)
|
||||
defer file.Close()
|
||||
|
||||
file.Write(content)
|
||||
}
|
190
main.go
Normal file
190
main.go
Normal file
|
@ -0,0 +1,190 @@
|
|||
// Copyright © 2013 Steve Francia <spf@spf13.com>.
|
||||
//
|
||||
// Licensed under the Simple Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://opensource.org/licenses/Simple-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/howeyc/fsnotify"
|
||||
"github.com/spf13/hugo/hugolib"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/pprof"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
cfgFiledefault = "config.json"
|
||||
)
|
||||
|
||||
var (
|
||||
baseUrl = flag.String("b", "", "hostname (and path) to the root eg. http://spf13.com/")
|
||||
cfgfile = flag.String("c", cfgFiledefault, "config file (default is path/config.json)")
|
||||
checkMode = flag.Bool("k", false, "analyze content and provide feedback")
|
||||
draft = flag.Bool("d", false, "include content marked as draft")
|
||||
help = flag.Bool("h", false, "show this help")
|
||||
path = flag.String("p", "", "filesystem path to read files relative from")
|
||||
verbose = flag.Bool("v", false, "verbose output")
|
||||
cpuprofile = flag.Int("cpuprofile", 0, "Number of times to create the site and profile it")
|
||||
watchMode = flag.Bool("w", false, "watch filesystem for changes and recreate as needed")
|
||||
server = flag.Bool("s", false, "run a (very) simple web server")
|
||||
port = flag.String("port", "1313", "port to run web server on, default :1313")
|
||||
)
|
||||
|
||||
func usage() {
|
||||
PrintErr("usage: hugo [flags]", "")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
if *help {
|
||||
usage()
|
||||
}
|
||||
|
||||
config := hugolib.SetupConfig(cfgfile, path)
|
||||
config.BuildDrafts = *draft
|
||||
|
||||
if *baseUrl != "" {
|
||||
config.BaseUrl = *baseUrl
|
||||
}
|
||||
|
||||
if *cpuprofile != 0 {
|
||||
f, err := os.Create("/tmp/hugo-cpuprofile")
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pprof.StartCPUProfile(f)
|
||||
defer pprof.StopCPUProfile()
|
||||
|
||||
for i := 0; i < *cpuprofile; i++ {
|
||||
_ = buildSite(config)
|
||||
}
|
||||
}
|
||||
|
||||
if *checkMode {
|
||||
site := hugolib.NewSite(config)
|
||||
site.Analyze()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if *watchMode {
|
||||
fmt.Println("Watching for changes. Press ctrl+c to stop")
|
||||
_ = buildSite(config)
|
||||
err := NewWatcher(config, *port, *server)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
_ = buildSite(config)
|
||||
|
||||
if *server {
|
||||
serve(*port, config)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func serve(port string, config *hugolib.Config) {
|
||||
fmt.Println("Web Server is available at http://localhost:" + port)
|
||||
fmt.Println("Press ctrl+c to stop")
|
||||
panic(http.ListenAndServe(":"+port, http.FileServer(http.Dir(config.PublishDir))))
|
||||
}
|
||||
|
||||
func buildSite(config *hugolib.Config) *hugolib.Site {
|
||||
site := hugolib.NewSite(config)
|
||||
site.Build()
|
||||
|
||||
site.Stats()
|
||||
|
||||
return site
|
||||
}
|
||||
|
||||
func watchChange(c *hugolib.Config) {
|
||||
fmt.Println("Change detected, rebuilding site\n")
|
||||
buildSite(c)
|
||||
}
|
||||
|
||||
func NewWatcher(c *hugolib.Config, port string, server bool) error {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
var wg sync.WaitGroup
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
defer watcher.Close()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case ev := <-watcher.Event:
|
||||
var _ = ev
|
||||
watchChange(c)
|
||||
// TODO add newly created directories to the watch list
|
||||
case err := <-watcher.Error:
|
||||
if err != nil {
|
||||
fmt.Println("error:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for _, d := range getDirList(c) {
|
||||
if d != "" {
|
||||
_ = watcher.Watch(d)
|
||||
}
|
||||
}
|
||||
|
||||
if server {
|
||||
go serve(port, c)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDirList(c *hugolib.Config) []string {
|
||||
var a []string
|
||||
walker := func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
PrintErr("Walker: ", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
a = append(a, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
filepath.Walk(c.GetAbsPath(c.SourceDir), walker)
|
||||
filepath.Walk(c.GetAbsPath(c.LayoutDir), walker)
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func PrintErr(str string, a ...interface{}) {
|
||||
fmt.Fprintln(os.Stderr, str, a)
|
||||
}
|
Loading…
Reference in a new issue