Compare commits

...

239 Commits

Author SHA1 Message Date
spf13 4f335f0c7f Updated release notes. 2013-11-15 23:47:53 -05:00
spf13 445b7d23fb Updating docs to include ‘first’ functionality for the homepage 2013-11-15 21:45:52 -05:00
spf13 aedfa6a2c4 Version bump 0.9 2013-11-15 21:43:46 -05:00
spf13 ad2c0b5616 Homepage now has all content available, use sorting and first to control rendering 2013-11-14 09:37:58 -05:00
spf13 13fa7cb748 lowercase template function first to be consistent with all other template functions 2013-11-14 09:32:49 -05:00
Michael D. Johas Teener 50d9046b64 remove initialization loop error from compile
copied HugoCmd to a local var, initialize that variable in the init
func, and then use the local var in the InitializeConfig func.
2013-11-12 17:36:23 -06:00
Phil Pennock 40d05f12a7 Truncated; .Site.Params; First function
* Add `.Truncated` bool to each page; will be set true if the
  `.Summary` is truncated and it's worth showing a "more" link of some
  kind.
* Add `Params` to the site config, defining `.Site.Params` accessible
  to each page; this lets the site maintainer associate arbitrary data
  with names, on a site-wide basis.
* Provide a `First` function to templates:
  * Use-case: `{{range First 5 .Site.Recent}}` or anything else which
    is a simple iterable provided by hugolib
* Tests by me for `.Truncated` and `First`

Also @noahcampbell contributed towards this:

* Add UnitTest for `.Site.Params`:
> Digging into this test case a bit more, I'm realizing that we need
> to create a param test case to ensure that for each type we render
> (page, index, homepage, rss, etc.) that the proper fields are
> represented.  This will help us refactor without fear in the
> future.

Sample config.yaml:

```yaml
title: "Test site"
params:
  Subtitle: "More tests always good"
  AuthorName: "John Doe"
  SidebarRecentLimit: 5
```

Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-11-12 22:49:54 +00:00
spf13 6017599a3c restored behavior of respecting config values unless set by command flags. fixed #116 2013-11-12 09:38:43 -05:00
Noah Campbell ef595aedfc Handle schema-less urls when apply absurl
Fixes #114
2013-11-09 14:35:09 +00:00
spf13 90a902c843 fixing double slash during absurlify 2013-11-09 01:33:00 -05:00
spf13 b69694a3ae Merge branch 'master' of github.com:spf13/hugo 2013-11-09 01:17:40 -05:00
spf13 532e2e7b93 Some mobile and other fixes for docs 2013-11-09 01:16:34 -05:00
Steve Francia 0b6a11c9e3 Merge pull request #113 from oyvindsk/master
minor documentation fix
2013-11-07 16:33:06 -08:00
Øyvind Skaar adc559b09f minor documentation fix 2013-11-07 19:47:42 +01:00
Noah Campbell ad04f6c899 Adding baseline 2013-11-05 22:51:40 +00:00
Noah Campbell 86233c00a0 Remove the hugo-nav function
Remove the hugo-nav since it relied on a slow library.  The current
build reimplements the absurl functionality based on string replace.
Discovered that my prior implementation missed the requirement for
making absolute paths (/path) absolute with the host, whereas a relative
path is left untouched.  Updated the test cases to support this if this
is reimplemented.
2013-11-05 22:28:06 +00:00
Noah Campbell 1cebce12ad Adding RSS test case.
Checks to make sure the xml document starts with <?xml.  Previously, the
html translate package would write additional details into the document
that caused it to fail.
2013-11-05 07:03:02 +00:00
Noah Campbell b22364570b Fix Test Name 2013-11-05 06:39:50 +00:00
spf13 1fbcaf9279 Buffers instead of pipes 2013-11-05 00:28:08 -05:00
spf13 226bc8f59f Updated release notes 2013-11-05 00:24:04 -05:00
Noah Campbell 23a5711d26 Fix benchmark panic
Need to initialize the Config with InitializeConfig().
2013-11-01 20:36:11 -07:00
spf13 23a711a29a Hugo updated to work with latest cobra 2013-11-01 18:43:41 -04:00
Noah Campbell 9af47f07d3 Improve rendering time
50% speedup.  Fix #91

to run the benchmark:

		go test -test.run=NONE -bench=".*" -test.benchmem=true ./transform/ > new.txt

to compare the results:

		/usr/local/go/misc/benchcmp baseline.txt new.txt

Speedup and memory improvements

		benchmark             old ns/op    new ns/op    delta
		BenchmarkChain           101219        50453  -50.15%
		BenchmarkTransform        51625        45531  -11.80%

		benchmark            old allocs   new allocs    delta
		BenchmarkChain              222          103  -53.60%
		BenchmarkTransform          135          106  -21.48%

		benchmark             old bytes    new bytes    delta
		BenchmarkChain            23919        10998  -54.02%
		BenchmarkTransform        11858        10665  -10.06%
2013-11-01 09:59:57 -07:00
Noah Campbell f4cb8e1688 Adding benchmark for transformation module. 2013-11-01 09:59:57 -07:00
Noah Campbell 789aa6ad76 Removing check for directory: static, layouts
Removed these checks so a single file in content can generate a site.

For example, given a site with a content directory and an index.html,
running hugo -s dir will generate a project without any more input.
2013-10-31 16:06:24 -07:00
spf13 861472bea5 Small tweaks to Homepage and CSS 2013-10-31 09:51:42 -04:00
spf13 1d0d280e20 Addition of an indexes section to the docs. Updated most of the existing index content. 2013-10-31 09:51:13 -04:00
spf13 a7dae30a8f More work on indexes 2013-10-31 09:49:29 -04:00
spf13 bc7c9221f3 Better mobile support on docs site 2013-10-26 02:28:25 -04:00
spf13 90355eec79 Big visual overhaul of the docs. Docs now using indexes for all menu generation. 2013-10-26 02:18:14 -04:00
spf13 df0523ff7f Flipping around weight order. higher weight at the bottom. use negative to pin to top. 2013-10-26 00:20:48 -04:00
spf13 5003f7f7af Docs menu now powered by indexes.. converting some of the content to use this. 2013-10-25 18:42:46 -04:00
spf13 d20b41a2cf Big index overhaul. Now supporting ordering tokens by count or alphabetically. Also made full indexes available to the Site variable. 2013-10-25 18:40:55 -04:00
spf13 9388f23606 Adding support for linkTitles 2013-10-25 18:37:53 -04:00
spf13 b580a25d1f Better error handling when rendering error found when in watch mode
In watch mode it should continue to watch for changes, in any other mode it should exit with a -1 error code so can check for success when scripting
2013-10-25 18:03:14 -04:00
spf13 764abd2067 Update homepage 2013-10-24 16:49:17 -07:00
spf13 dde965a5cd Migrate docs to Bootstrap 3.0 2013-10-24 16:49:01 -07:00
spf13 cd71eb7389 Watching doesn't built site 2x on write (ignores rename events). Also ignores temporary files written by editors. 2013-10-24 16:45:24 -07:00
spf13 a5606b06ca Adding support for ordering content in indexes by other than date (weighted indexes) 2013-10-24 15:24:47 -07:00
spf13 471fb1ff69 Adding support for date field in front matter as date (as TOML provides) 2013-10-24 15:18:57 -07:00
spf13 f3c816eabd Adding support for more front matter types (int, float & date) 2013-10-24 14:31:04 -07:00
spf13 3558e3d6f0 Add support for weighted pages
Now pages can be sorted by other than date
2013-10-18 11:01:31 -04:00
spf13 90090175f8 Adding preliminary support for weighted indexes (for ordering by other than date) 2013-10-17 23:57:25 -04:00
spf13 678ddef46a Adding a quick start guide to hugo docs 2013-10-17 22:53:04 -04:00
spf13 4d333e81ee New Homepage for Hugo 2013-10-17 22:52:28 -04:00
spf13 4263094d75 Adding support for MinRead. Closed #106 2013-10-15 09:32:21 -04:00
spf13 be5ace1588 Wordcount restored. fixed #92 2013-10-15 09:15:52 -04:00
spf13 e58d8fe791 fixed #95, fixed #93 2013-10-09 23:38:51 -04:00
spf13 f5fda80486 simplified buildSite & better error handling around it 2013-10-09 19:14:26 -04:00
spf13 0318f7c149 Clean up server & build site logic. Fixed #94 2013-10-09 18:53:46 -04:00
spf13 e6ace71fec server command now builds prior to serving 2013-10-09 18:24:40 -04:00
Noah Campbell 4993152dda Permalink to include multiple directories levels 2013-10-08 18:44:15 +02:00
Noah Campbell 6e1268f45b Test case for permalink functionality 2013-10-08 18:44:15 +02:00
Noah Campbell 895638433e Adding back the hugo homepage.
Deleted the page accidentially
2013-10-08 18:44:15 +02:00
Noah Campbell 9500ec1b6b Refactor layout selection code
The render code path would use a fallback if there was an exception.
This change instead relies on explicit declaration of the layout to use
and includes a check to see if the layout indeed exists before
attempting to render it.
2013-10-08 18:44:15 +02:00
Noah Campbell 197aacb647 Add support for Relative Permalinks 2013-10-08 18:42:03 +02:00
Noah Campbell 06da609138 Refactor Permalink to private function
This will allow for reuse of this particular function.
2013-10-08 18:42:03 +02:00
Noah Campbell 6fa6f69a4a Change transformer order 2013-10-08 18:42:03 +02:00
Noah Campbell d712d6f331 Moving transformation on a per-page level 2013-10-08 18:42:03 +02:00
Noah Campbell 9032a228b0 Better handle missing layouts
Panic is too extreme.  Instead the library will write out a message in
verbose mode.
2013-10-08 18:42:02 +02:00
Noah Campbell 54a2790fce Use io.Pipe for chaning render tasks.
Also clean up logic for finding layouts.
2013-10-08 18:42:02 +02:00
Noah Campbell 689cda1740 Code cleanup 2013-10-08 18:40:41 +02:00
Noah Campbell 19cb6c7819 Move writing next to rendering
All render(Thing) calls have a WritePublic call directly after it.  This
refactor creates one function that makes the specific call sequence.
2013-10-08 18:40:40 +02:00
Noah Campbell 2176d2c197 Reducing scope of RenderThing* 2013-10-08 18:37:50 +02:00
Noah Campbell ff8b52758d Move in memory target into target module. 2013-10-08 18:37:50 +02:00
Noah Campbell 80009b427f Change the order of Apply to be more Unixy
Typically the destination is on the left and the src is on the right.
2013-10-08 18:37:50 +02:00
Noah Campbell 94a3184ad0 Fixing up source code formatting. 2013-10-08 18:37:50 +02:00
Noah Campbell 5a66fa3954 Chain transformers and test cases
Transformers can now be chained together, working on the output of the
previous run.
2013-10-08 18:37:50 +02:00
Noah Campbell eb117eb904 Move AbsURL into seperate file 2013-10-08 18:37:49 +02:00
Noah Campbell f0211b84a1 Rename Transform to AbsURL 2013-10-08 18:37:49 +02:00
Noah Campbell 03d1a57fea Set the name of the Nav attribute to select 2013-10-08 18:37:49 +02:00
spf13 5e14af957a Proper handling of 404 page return value 2013-10-07 09:24:13 -04:00
spf13 7468292c4e Fixing bug where RSS for site was rendered in wrong location 2013-10-07 09:23:42 -04:00
spf13 d829e05036 Fixing bug where only the first index list was created. 2013-10-07 09:07:14 -04:00
spf13 2aaf92b515 Summary can now contain short codes when using <!--more--> to define end of summary. 2013-10-04 16:07:24 -04:00
spf13 be7ba0e98f server defaults to localhost unless overridden by command line flags 2013-10-04 16:03:13 -04:00
spf13 266f583a8c Restoring former snippet behavior & adding test to ensure future behavior 2013-10-04 12:28:28 -04:00
spf13 dcfcbac589 Enabling Nitro ('--stepAnalysis') again. Fix #58 2013-10-03 09:44:45 -04:00
spf13 18f2b82658 Switching to the rjson library which is more friendly to human generated json. 2013-10-01 22:45:24 -04:00
spf13 48e1068e3e fixed #85 2013-09-30 22:40:15 -04:00
spf13 8efb90ebd5 Updating usage documentation to new interface 2013-09-30 22:39:06 -04:00
spf13 3ae8dda203 Restoring build and watch functionality 2013-09-30 22:38:32 -04:00
Kyle Mahan aa9b9d596e minor documentation cleanup.
- changed it's -> its where appropriate.
- added post/ parent directory above happy/ to the organization examples

Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-09-29 19:50:25 -07:00
windch 8ce4bc7ab8 Update filesystem.go
bugfix: .Files() called twice in line 253, 256 in site.go. thus source files captured twice.

Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-09-29 19:43:38 -07:00
spf13 94d7fe52f8 Change the interface to use commands and flags instead of just flags.
Better organization of main (controller style) code.
2013-09-29 02:10:29 -04:00
spf13 92cff05582 go fmt so code 2013-09-29 02:10:29 -04:00
spf13 ff2b98c9dd Better error message when no source content is found. 2013-09-29 02:10:29 -04:00
Noah Campbell f34ea6108d Add the ability to set navbar li class to active
First cut at doing post html processing.  This utility can be used to
mark pages as active.
2013-09-28 23:05:16 -07:00
Noah Campbell db50154e75 Support index.html indexes in content directory
If a file named index.html exists in a directory, or root, it will be
rendered as if ugly urls are turned on.  This allows for top level
content to not need a supporting layout file and content in content.
This change should not affect anyone who is using the perscribed way.

I also cleaned up a bunch of one off functions in site.go.
2013-09-24 21:27:25 -07:00
Noah Campbell 4250bf8e30 Fixes #80 - Homepage now renders correctly
I mistakenly make RenderHomePage a NOP.  Bad programmer.
2013-09-24 14:41:21 -07:00
Noah Campbell c9223cfd7b source: Fix failing build on windows
The +build directive was not being picked up.  Apparently needs a space
after the +build line.
2013-09-24 14:36:22 -07:00
Noah Campbell 8df88496e2 Fix breaking test cases on unix platform
Rename platform parameter file to agnostic name. Both darwin and linux
can share the same paramters.
2013-09-20 20:08:52 -07:00
Noah Campbell bffe4baf42 Create a TargetPath() method that provides OutFile
Moved the generation of the target path to the page breaking all
dependecies on Site.
2013-09-20 17:24:25 -07:00
Noah Campbell 52e8c7a0ac Section is determined by the source, not the url
This change allows for top level html content to exists.
2013-09-20 17:03:43 -07:00
Noah Campbell 784077da4d Fix fragments being AbsUrlified in final html
Found that fragments were getting the BaseURL applied creating a proper
anchor url and redirecting off the page.
2013-09-18 15:48:36 -07:00
Noah Campbell 311e102223 Allow non-markdown content in content directory
Allow content that is not markdown and does not need to be rendered to
exists in the content directory.  Currently any valid html or xml
document can exist.  Templates are applied to these documents as well.
If you need to have content that doesn't have templates or AbsUrlify
like operations, then continue to put this content in static and it will
be copied over.
2013-09-18 14:21:27 -07:00
Noah Campbell 5374242ff7 More expressive --check output 2013-09-18 11:52:30 -07:00
Noah Campbell c510140c0c Add renderer information to --check
Now reports if the page will be rendered or not and by which render
engine.
2013-09-18 10:27:56 -07:00
Noah Campbell 67b2abaf09 Add IsRenderable to Page
As pages are read from the target, they will be assessed if they should
be rendered or not.  The logic for IsRenderable is in the parser/page.go
and looks for anything exception '<'.
2013-09-18 10:17:43 -07:00
Noah Campbell d8e1834910 Fix parsing edge case of frontmatter
When the frontmatter contains a - (or other delimiter) close to the
closing frontmatter delimiter, frontmatter detection would fail.
2013-09-18 09:15:46 -07:00
Noah Campbell a82efe5bb1 Merge remote-tracking branch 'origin/parser' into mrg_praser
Also brought in parse for github.com/noahcampbell/akebia

Conflicts:
	hugolib/page.go
	hugolib/page_test.go
2013-09-17 15:52:40 -07:00
Noah Campbell 6b0752e8c0 Move AbUrlify to post content transformation
Currently the a@href and script@src elements will have BaseUrl applied
to their elements prior to being written to disk.
2013-09-17 14:16:06 -07:00
Noah Campbell c6fe87b14e Add a transform step
This allows for the manipulation of the DOM.  This is expected to be
applied after the templates are rendered.
2013-09-17 13:04:28 -07:00
Noah Campbell c75da346e1 Fix alias documentation.
Fixes #79
2013-09-13 16:06:39 -07:00
Noah Campbell 172ff5ea7a Initialize HTMLRedirectAlias before ShowPlan()
Without it, no alias information is generated when running --check
2013-09-13 15:06:37 -07:00
Noah Campbell d45fb72f67 Add /index.html to unadorned alias paths
Bring code to be better in line with documentation.
2013-09-13 14:51:28 -07:00
Noah Campbell 803a0fce1e Remove RenderedContent from page.
The output is written directly to disk after being written.  Not
entirely sure why it was split into two steps.
2013-09-13 10:30:27 -07:00
Noah Campbell 2ebfb33fe0 Move alias logic to target module
I want to move all logic to writing aliases to target so I can pave the
way for writing aliases specific to other runtimes (like .htaccess for
apache or a script for updating AWS or symlinking on a filesystem).
2013-09-12 21:20:00 -07:00
Noah Campbell 2f10da1570 Move alias rendering to target 2013-09-12 16:18:30 -07:00
Noah Campbell 74b55fc7c8 Normalize paths within hugo
filepath was used inconsistently throughout the hugolib.  With the
introduction of source and target modules, all path are normalized to
"/".  This simplifies the processing of paths.  It does mean that
contributors need to be aware of using path/filepath in any module other
than source or target is not recommended.  The current exception is
hugolib/config.go
2013-09-12 10:48:59 -07:00
Nelson Silva 998b2f73f8 Added safeHtml template function 2013-09-10 17:19:45 -04:00
Noah Campbell 6274aa0a64 Homepage "/" respects PublishDir
It wasn't taking the value of PublishDir into consideration for the
special case of the homepage "/".

Fixes #75
2013-09-05 09:57:25 -07:00
Noah Campbell 610c06e658 Introduce source.Filesystem
This provides an abstraction over how files are processed by Hugo.  This
allows for alternatives like CMS systems or Dropbox, etc.
2013-09-04 22:42:52 -07:00
Noah Campbell d4d9da9f3a Remove page module dependence on opening files
The site is responsible for reading files, page only operates on
buffers.
2013-09-04 19:36:06 -07:00
Noah Campbell cb00917af6 Expand the ShowPlan functionality 2013-09-03 20:52:50 -07:00
Noah Campbell 4004687fb2 Move to target.Filesystem
Moving the ugly urls logic to the target.  There is still UglyUrl logic
in page for the permlink but this is dealing with the generate of urls.
2013-09-03 20:01:55 -07:00
Noah Campbell 7919603fb5 Add Translate to target
Translate handles Ugly Urls.
2013-09-03 20:00:22 -07:00
Noah Campbell c6ad532b94 Add file reporting to planner 2013-09-03 20:00:22 -07:00
Noah Campbell 13d2c55206 Adding Planner 2013-09-03 20:00:21 -07:00
Noah Campbell 79d9f82e79 Code reorg, helpers.go has been decomposed.
It started with wanting to move templates in template bundles and the
rest followed.  I did my best to start grouping related functions
together, but there are some that I missed.  There is also the method
Urlize that seems to be a special function used in both worlds.  I'll
need to revisit this method.
2013-09-03 16:16:07 -07:00
Noah Campbell 207d8fb7af Date rendering unit tests in pages
Tests to ensure rendering dates in templates is working correctly.
Actually, I was running into invalid templates not giving warnings when
I was trying to render a date.
2013-09-03 14:51:06 -07:00
Noah Campbell 3ecc698f5e Remove hugolib.HTML and hugolib.URL types
These types were not be rendered correctly by the html/template package.
Removing them gets the correct behavior.

Fixes #74
2013-09-03 12:43:56 -07:00
Noah Campbell a591a10626 Include building hugo docs as part of test.
If the building of the doc fails, tarvis will fail.  This will help with
pull requests.
2013-09-03 11:29:43 -07:00
Noah Campbell d841d522f1 Include building hugo docs as part of test.
If the building of the doc fails, tarvis will fail.  This will help with
pull requests.
2013-09-01 20:03:10 -07:00
Fabrizio (Misto) Milo ba82a20321 Add support for amber files
If a layout file ends with .amber it will interpreted as a Amber file

Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-09-01 19:51:32 -07:00
Fabrizio (Misto) Milo ee5865f239 Abstract html/template dependency
Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-09-01 19:51:27 -07:00
Fabrizio (Misto) Milo 0a9dc705f3 Use s everywhere for consistency
Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-09-01 19:51:21 -07:00
Noah Campbell b268e639ba Return an error (other than 0) when ./hugo fails
Being a good OS citizen so folks can compose hugo into their tool chain.
Also helps with git bisect run.
2013-09-01 11:39:32 -07:00
Mark Sanborn 6c8e7edbb4 The <!--more--> (summary divider) now works even if it is on the same line as content
Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-09-01 07:44:29 -07:00
Noah Campbell 4349216deb Small code cleanup 2013-08-31 20:00:57 -07:00
Noah Campbell 0fdea0c2c2 Return an error with WritePublic 2013-08-30 21:24:25 -07:00
Noah Campbell 097b782a80 Removing site.Directories.
Please revert if this is used somewhere.
2013-08-30 21:13:50 -07:00
Noah Campbell b14b61af37 Externalize the writing of content to a target
Introducing the target module in hugo.  This provides the simple
interface for writing content given a label (filename) and a io.Reader
containing the content to be written.

If site.Target is not set, it defaults back to the original behavior of
writing to file system.

In hugolib/site_url_test.go I have an InMemoryTarget for testing
purposes and use it to see if the final output of a render matches.
2013-08-30 20:45:42 -07:00
Noah Campbell bc3c229002 Ensure Section is set before returning Page object. 2013-08-30 20:13:22 -07:00
Noah Campbell c32f401b15 Revert "Return errors when rendering"
This reverts commit e66ba5d2a7.

Fixed #69
2013-08-30 20:08:13 -07:00
Noah Campbell a792ec09ce Cleanup formatting - go fmt ./...
Remember to run go fmt ./... before committing.  Looks sternly in mirror
2013-08-30 14:39:12 -07:00
Mark Sanborn 4ed43e8076 Fixed bug where Url specified in front matter as pretty url wouldnt render
Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-08-30 14:39:11 -07:00
Daniel Alan Miller 71678a7183 Adding .Summary to page variables
Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-08-29 10:08:03 -07:00
Fabrizio (Misto) Milo 3ab5245049 clean up logic
Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-08-28 20:39:58 -07:00
Ross Lawley 1bb00b8c19 Refactored added RenderThingOrDefault and tests
Signed-off-by: Noah Campbell <noahcampbell@gmail.com>

Conflicts:
	hugolib/site.go
2013-08-27 12:40:53 -07:00
Noah Campbell 554375b2ad Using new parser. 2013-08-25 20:27:03 -07:00
Hugo Duncan 1de1992664 Return any error reported by RenderHomePage
Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-08-23 16:55:40 -07:00
Ross Lawley 9930011ea2 Wordpress summaries
Allow full control of summaries which can be rendered as html rather
than text.  Using a `<!--more-->` html comment in your markdown / rst
you can indiciate where the summary should end and have the summary
converted to html.

Signed-off-by: Noah Campbell <noahcampbell@gmail.com>

Conflicts:
	hugolib/page_test.go
2013-08-23 16:46:19 -07:00
Ross Lawley 7b1f0960e3 Add 404.html for gh-pages
Signed-off-by: Noah Campbell <noahcampbell@gmail.com>
2013-08-23 16:38:35 -07:00
Noah Campbell f28a8fa0c2 RenderThing test cases
Provide unit test support RenderThing.

One observation is that creating the site.Tmpl variable is a one time
event.  site.Tmpl doesn't like additional templates with the same name.
This means that updating a template while in --watch mode requires
throwing away the entire Site object and creating a new one.  Not that
this is a bad idea, but it is something I discovered while working on
these unit tests.
2013-08-23 16:21:28 -07:00
Noah Campbell 9d15262ee5 Test cases for ignoreDotFile
Meant to commit this earlier, but it's a basic unit test.
2013-08-23 14:57:21 -07:00
Noah Campbell 0fabd51ab1 Update .gitignore to ignore *.swo files
For the fellow vim user.
2013-08-23 14:57:21 -07:00
Noah Campbell 7461ed63ae Fix benchmark so the buffer is read each time.
The bytes.Buffer was exhausted after the first read.  Creating a new
reader each invocation catpures the correctly timing.
2013-08-23 14:16:37 -07:00
Noah Campbell 599e6672f7 Removing GetSection
Using GetXXX is not idiomatic to Go.  Also added a bunch of unit testing
around this method.
2013-08-23 14:14:54 -07:00
Noah Campbell ae7112977d Skip "dot" files in layout
As a vim user, .filename.swp files pop up.  This change prevents hugo
from reading those files.
2013-08-23 13:58:52 -07:00
Steve Francia eb4288e3cd Merge pull request #53 from hugoduncan/feature/print-error-on-invalid-index
Print error if index value in a page is wrong type
2013-08-20 20:56:33 -07:00
Steve Francia 00839567c7 Merge pull request #52 from rozza/StaticSync
Static changes shouldn't be destructive to existing files
2013-08-20 20:55:56 -07:00
Steve Francia 35b35a7004 Merge pull request #51 from cabello/patch-3
Remove extra quotes from meta data
2013-08-20 20:54:51 -07:00
Steve Francia 6f424175bf Merge pull request #50 from cabello/patch-2
Check .Prev and .Next pointers before using it
2013-08-20 20:54:31 -07:00
Steve Francia 3d0dc1acb1 Merge pull request #49 from cabello/patch-1
Fix broken link to Index page
2013-08-20 20:54:06 -07:00
Hugo Duncan 301d2bafcd Print error if index value in a page is wrong type
This was causing a panic with no information displayed about the
page causing the error.
2013-08-20 16:39:26 -04:00
Ross Lawley 5aa47a7b07 Static changes shouldn't be destructive to existing files
Currently changing css deletes all the site published html when it copies.
Refs #46
2013-08-20 20:32:32 +01:00
Danilo Cabello 8415c5e6c7 Remove extra quotes from meta data 2013-08-20 13:52:51 -04:00
Danilo Cabello acd5ea0e75 Check .Prev and .Next pointers before using it 2013-08-20 12:13:27 -04:00
Danilo Cabello 8058abd707 Fix broken link to Index page 2013-08-20 11:56:55 -04:00
Steve Francia eff8457ac9 Merge pull request #46 from rawfalafel/sync_static
Sync files that have changed in the static directory
2013-08-19 05:32:36 -07:00
rawfalafel 2dcdd67378 Watch StaticDir and sync to PublishDir on change
New behavior adds a special case for file changes inside the static directory to fsync PublishDir
2013-08-19 01:17:37 -04:00
Steve Francia c4bcdebc59 Merge pull request #44 from cactus/avoid-redirect-with-slugs
avoid possible redirects with non-ugly slug urls
2013-08-17 21:04:51 -07:00
Steve Francia e2744d403c Merge pull request #43 from hugoduncan/feature/allow-xhtml-aliases
Enable aliases from .xhtml paths
2013-08-17 21:04:14 -07:00
Steve Francia 2542836bbc Merge pull request #41 from rozza/skipStatic
Skip Static directory if its in your content directory
2013-08-17 20:59:07 -07:00
spf13 8f330626bc Merge branch 'noahcampbell-index_reporting' 2013-08-17 23:53:35 -04:00
Noah Campbell c713beba4d Formatting cleanup 2013-08-17 23:52:16 -04:00
Noah Campbell ec821739bc Removing the use of slash
An oversight on my behalf.  The FromSlash method is used when writing
out the public file name.  There is one place where the slashes are
required which is setting the output file.  I replaced those instances
with filepath.Join which should do the right thing depending on the OS.
2013-08-17 23:46:57 -04:00
Noah Campbell 8eca8f8aa0 Detect missed index from front matter 2013-08-17 23:45:03 -04:00
Noah Campbell e66ba5d2a7 Return errors when rendering 2013-08-17 23:45:03 -04:00
spf13 0a79edd48a Removing extra links on homepage 2013-08-17 21:54:39 -04:00
spf13 3ae8078d3a Adding "fork me on github" banner 2013-08-17 21:54:39 -04:00
spf13 8c0ab4def1 Complete overhaul of the docs 2013-08-17 21:54:39 -04:00
Steve Francia b76b80c564 Merge pull request #40 from VonC/clarify_uglyurls_flag
Clarify uglyurls flag.
2013-08-17 06:35:31 -07:00
elij b9e835b101 avoid handling a redirect from slug to slug/
because the url lacks a trailing /, many webservers will issue a
redirect to the canonical url with trailing slash for directory index
w/index.htm(l).
Append a slash to avoid this.
2013-08-16 13:14:20 -07:00
Hugo Duncan 23a98ad05c Enable aliases from .xhtml paths
When redirecting an alias from a .xhtml path, served with default content type,
a redirect only works if the html element has a xmlns attribute.  This adds the
attribute when the alias path ends in .xhtml
2013-08-16 00:29:46 -04:00
Ross Lawley 0f143dcf14 Skip Static directory if its in your content directory
Allows organisation where all source files are in one directory:

```
`config.yaml`:

contentdir: "source"
staticdir: "source/static"
...

 .
	└── source
		├── post
		|	├── firstpost.md	// <- http://site.com/post/firstpost.html
		|	└── secondpost.md	// <- http://site.com/post/secondpost.html
		└── static
			└── css
				 └── site.css	// <- http://site.com/css/site.css
```
2013-08-15 20:05:46 +01:00
VonC 9308cd6a7a Clarify uglyurls flag.
Mention more clearly that, for generating `/filename.html`, you need to
set the `uglyurls` flag to `true`.
2013-08-14 16:47:17 +02:00
spf13 3c3fc45d3c Merge branch 'master' of github.com:spf13/hugo 2013-08-14 10:19:59 -04:00
spf13 480e01eb15 Further work on path/section stuff. Tests passing now. 2013-08-14 08:57:14 -04:00
Steve Francia 7a51a8a5a3 Merge pull request #38 from noahcampbell/workflow_dotfile
Workflow dotfile
2013-08-13 20:03:18 -07:00
spf13 b4bcc591e4 Now support for nested paths. Better section detection. 2013-08-13 19:39:24 -04:00
spf13 6e27239485 Merge branch 'master' of github.com:spf13/hugo 2013-08-13 10:47:58 -04:00
Steve Francia ca5a94a988 Merge pull request #37 from noahcampbell/master
Test GetParam and the various incarnations of frontmatter.
2013-08-13 07:44:43 -07:00
Steve Francia c661d9803e Merge pull request #31 from cactus/issue-31
sanitizeRegex chopping dots
2013-08-13 07:44:00 -07:00
spf13 ec02fa4bdd Adding support for a default content type template
default template found at layouts/_default/single.html
2013-08-13 09:23:43 -04:00
spf13 8968524900 Adding support for recent content. 2013-08-13 08:43:42 -04:00
Noah Campbell 97eb9225a7 Ignore dotfiles in content directory
This supports my personal workflow of using vim which places a temporary file in the same directory as the file I'm editing.
2013-08-12 20:40:52 -07:00
Noah Campbell 5664780cca gofmt pass
Clean up test files.
2013-08-12 20:40:34 -07:00
Noah Campbell 2d11d1bd67 Test GetParam and the various incarnations of frontmatter. 2013-08-12 20:25:32 -07:00
elij 31a1ade1b4 move "dot" in regex to avoid it being part of a range 2013-08-12 19:55:52 -07:00
Steve Francia c689d46aa1 Merge pull request #32 from cactus/issue-32
indexes rss.xml has wrong permalinks with ugly urls off
2013-08-12 19:42:44 -07:00
elij b13afc4178 fix sanitizeRegex to not strip "dots"
sanitizeRegex was stripping dots in permalinks when generating
RenderIndexes (noted during feed/rss generation).

permalink was being set to `.../indexxml` instead of `.../index.xml`.

Adding "dot" to the regex whitelist fixed the issue.
2013-08-12 19:02:43 -07:00
elij 023567b05e fix bad tab/space due to paste 2013-08-12 19:01:23 -07:00
elij 2f9b582dbe fix wrong renderlist feed permalink
when not using ugly urls, the feed permalink does not end up in the
expected location, and instead always behaves as if using ugly urls.

this fixes that behavior and inserts the feed xml file into the
directory as index.xml.

fixes #32
2013-08-12 19:00:29 -07:00
Steve Francia cb39f052d1 Merge pull request #36 from noahcampbell/master
Use / for template names regardless of platform.
2013-08-12 15:36:06 -07:00
Noah Campbell 8c03141307 Use / for template names regardless of platform.
The path seperator was causing templates to not be loaded on windows.
Now all template names use / internally.
2013-08-12 15:03:06 -07:00
Steve Francia ec1a3a8db9 Merge pull request #35 from noahcampbell/master
Address the time parsing issue.
2013-08-12 14:43:56 -07:00
Noah Campbell 3fdcd0ba7c Support for non-standard formats.
Forgot to include existing formats.  Integration tests include new
format.
2013-08-12 14:35:39 -07:00
Noah Campbell 0305c82513 Move timezone tests to integration test.
Interacting with timezones will result in checks against the filesystem.
This access, by definition, is an integration test.  Creating a
*integration_test.go file will signify this change.

When interacting with Travis-ci.org, the ubuntu boxes plus go 1.1 do not
seem to support shortcode timezones, think PST.  In this case, the tests
are skipped.  This is not ideal, but the IRC #go-nuts channel has
indicated timezone support is still lacking.  We should advise users of
hugo that timezone support may be an issue and report any odd behavior.
The workaround is to use numeric timezones (-08:00 for PST, etc.)
2013-08-12 12:04:04 -07:00
Noah Campbell f610d45cd8 Add additional details to date test cases. 2013-08-12 09:14:30 -07:00
Noah Campbell dd19d0cc77 Provide better support for various date formats.
Fixes #30 as long as the date is well formatted.
2013-08-12 09:14:29 -07:00
VonC 17aafb39dd Avoid error if no content.
The homepage should still be generated.
This is useful especially in the beginning, where you start just with
the homepage.
2013-08-12 14:31:39 +02:00
Steve Francia 5b3b0f9556 Merge pull request #26 from noahcampbell/master
Add support for continuous testing.
2013-08-11 07:23:08 -07:00
spf13 0233708907 Started new release notes, added nitro step for aliases 2013-08-10 15:44:22 +01:00
spf13 ac26de205e Adding correct canonical link to alias pages 2013-08-10 15:41:10 +01:00
spf13 d5518c0966 Adding support for aliases (redirects) 2013-08-10 15:35:34 +01:00
spf13 45ce6e2b30 Merge branch 'redirect' of https://github.com/rozza/hugo into rozza-redirect
Conflicts:
	hugolib/page.go
2013-08-10 14:08:38 +01:00
Noah Campbell bb273df4cd Add support for continuous testing.
Using travis-ci.org
2013-08-09 18:34:51 -07:00
spf13 733c0207cb Merge branch 'master' of github.com:spf13/hugo
Conflicts:
	hugolib/site.go
2013-08-10 02:07:35 +01:00
Steve Francia 2bf24877a6 Merge pull request #25 from noahcampbell/master
Internal API changes and introduce testing.
2013-08-09 18:01:51 -07:00
Noah Campbell 2bbecc7bc8 Better reporting when the template is missing. 2013-08-09 17:36:32 -07:00
Noah Campbell 309db474c7 Nitro timer is encapsulated.
Remove the need for NewSite by relying on appropriate defaults.  Renamed
site.c to site.Config to allow Sites to be created outside the package.
2013-08-09 17:36:32 -07:00
Noah Campbell e26b43f6d9 Adding more test cases 2013-08-09 17:36:32 -07:00
Noah Campbell e67db666c8 Adding benchmark for parsing pages using unicode.
The resulting comparsion is not equal because NewPage reads the file each time where ReadFrom just reads a buffer in memory.
2013-08-09 17:36:32 -07:00
Noah Campbell 085ce15f7c Adding ability to read from io.Reader
This allows for testing without relying on the file system.  Parsing algorithm to not read the entire file into memory.
2013-08-09 17:36:32 -07:00
Noah Campbell 274d324c8b Introduce unit testing for page.go 2013-08-09 17:36:32 -07:00
Noah Campbell fa55cd9857 Remove unreachable code path. 2013-08-09 17:36:32 -07:00
Noah Campbell 0595f27e6d Using a composite literal to create a page.
Wonder why the need for composite structs.  Not sure if my go knowledge is lacking or if this is cruft from other programming language doctrine.
2013-08-09 17:36:32 -07:00
Noah Campbell 19538a1bd6 Support pages without folders 2013-08-09 17:36:31 -07:00
Noah Campbell fc5e92cc24 Provide better error handling
Change the method signatures to follow the check ok pattern.
2013-08-09 17:36:31 -07:00
Ross Lawley e2a28114d1 Add redirect to page parameters and redirects example 2013-08-09 21:57:22 +01:00
Steve Francia 4f17ad69a7 Merge pull request #22 from nsabine/master
Added examples to indexes.md
2013-08-09 02:16:10 -07:00
Steve Francia 7a13434dd9 Merge pull request #21 from duncanbeevers/master
Added .Content variable to docs
2013-08-09 02:15:50 -07:00
Nick Sabine a8b3e1537f Added examples to indexes.md 2013-08-09 00:10:35 -04:00
Duncan Beevers 04a0dbbf73 Added .Content variable to docs 2013-08-08 16:13:03 -07:00
Steve Francia 6a5e4b363a Merge pull request #20 from VonC/version
Make sure hugo --version prints the version and does *nothing* else.
2013-08-08 15:52:50 -07:00
VonC 49b8ac5fbc Make sure hugo --version prints the version and does *nothing* else.
Any program, when asks to print their version, only prints that, and then
stops.
hugo checks the config (and prints a warning message if not found), and
proceeds to generate the site!
Yet, the user just wanted to check the version.

This patch makes sure hugo stops after printing the version.
2013-08-08 22:21:56 +02:00
spf13 a870f4d955 Fixing missing trailing slash in baseUrl 2013-08-08 12:30:01 +01:00
Steve Francia d89c7ec7a2 Merge pull request #19 from nsabine/master
Populated page Site metadata.  Enables accessing Indexes from Page template.
2013-08-07 14:05:07 -07:00
Nick Sabine 780e2f311b Populated page Site metadata. Enables accessing Indexes from Page template. 2013-08-07 14:23:24 -04:00
Steve Francia 42de9bd8bb Merge pull request #15 from hhatto/update-doc
update document for content directory
2013-08-06 08:05:18 -07:00
spf13 0e57fcc9c2 Merge branch 'master' of github.com:spf13/hugo 2013-08-06 01:10:10 +02:00
spf13 783f0d6154 fixing badlink on homepage 2013-08-06 01:06:59 +02:00
spf13 6789b6c5ce removing erroneous debugging output 2013-08-06 01:05:50 +02:00
Hideo Hattori 78afe8d344 update document for content directory 2013-08-06 03:14:40 +09:00
Steve Francia c5715e9800 Merge pull request #12 from imrehg/docfix
Fix example in docs that wouldn't work in practice
2013-08-05 07:13:42 -07:00
Gergely Imreh f31ec3c280 Fix example in docs that wouldn't work in practice
The pflag package used in hugo has to use the "=" sign
for double dash options such as --source. Thus the original
example `--source ~/mysite` is already incorrect. Adding
the = sign though woul not fix things in this case, since
`--source=~/mysite` does not get resolved to /home/username/mysite,
but looks for the ./~/mysite directory within the current directory.

To resolve this, either the directory name has to be changed in
the docs not to use the "~" sign, or have to change to use the
single dash version of the command line flag. The latter seems
to be more in line with the rest of the example.

Leaving `--watch` as a double dash option to minimize the change,
though it could be either way, since the follow up example uses
the single dash version of both.
2013-08-05 21:45:01 +08:00
spf13 de9f9ae16e Fixing getting started link. 2013-08-05 11:12:39 +02:00
120 changed files with 9423 additions and 2690 deletions
+6 -5
View File
@@ -1,5 +1,6 @@
hugo
docs/public*
hugo.exe
*.swp
.DS_Store
hugo
docs/public*
hugo.exe
*.swp
*.swo
.DS_Store
+7
View File
@@ -0,0 +1,7 @@
language: go
go:
- 1.1
script:
- go test ./...
- go build
- ./hugo -s docs/
+2
View File
@@ -2,6 +2,8 @@
A Fast and Flexible Static Site Generator built with love by [spf13](http://spf13.com)
and [friends](http://github.com/spf13/hugo/graphs/contributors) in Go.
[![Build Status](https://travis-ci.org/spf13/hugo.png)](https://travis-ci.org/spf13/hugo)
## Overview
Hugo is a static site generator written in GoLang. It is optimized for
+3
View File
@@ -0,0 +1,3 @@
PASS
BenchmarkChain 500000 7074 ns/op 3913 B/op 15 allocs/op
ok github.com/spf13/hugo/transform 3.669s
+54
View File
@@ -0,0 +1,54 @@
// 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 commands
import (
"github.com/spf13/cobra"
"os"
"runtime/pprof"
)
var cpuProfilefile string
var benchmarkTimes int
var benchmark = &cobra.Command{
Use: "benchmark",
Short: "Benchmark hugo by building a site a number of times",
Long: `Hugo can build a site many times over and anlyze the
running process creating a `,
Run: func(cmd *cobra.Command, args []string) {
InitializeConfig()
bench(cmd, args)
},
}
func init() {
benchmark.Flags().StringVar(&cpuProfilefile, "outputfile", "/tmp/hugo-cpuprofile", "path/filename for the profile file")
benchmark.Flags().IntVarP(&benchmarkTimes, "count", "n", 13, "number of times to build the site")
}
func bench(cmd *cobra.Command, args []string) {
f, err := os.Create(cpuProfilefile)
if err != nil {
panic(err)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
for i := 0; i < benchmarkTimes; i++ {
_ = buildSite()
}
}
+31
View File
@@ -0,0 +1,31 @@
// 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 commands
import (
"github.com/spf13/cobra"
"github.com/spf13/hugo/hugolib"
)
var check = &cobra.Command{
Use: "check",
Short: "Check content in the source directory",
Long: `Hugo will perform some basic analysis on the
content provided and will give feedback.`,
Run: func(cmd *cobra.Command, args []string) {
InitializeConfig()
site := hugolib.Site{Config: *Config}
site.Analyze()
},
}
+214
View File
@@ -0,0 +1,214 @@
// 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 commands
import (
"fmt"
"github.com/howeyc/fsnotify"
"github.com/mostafah/fsync"
"github.com/spf13/cobra"
"github.com/spf13/hugo/hugolib"
"github.com/spf13/hugo/utils"
"github.com/spf13/nitro"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
var Config *hugolib.Config
var HugoCmd = &cobra.Command{
Use: "hugo",
Short: "Hugo is a very fast static site generator",
Long: `A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at http://hugo.spf13.com`,
Run: func(cmd *cobra.Command, args []string) {
InitializeConfig()
build()
},
}
var hugoCmdV *cobra.Command
var BuildWatch, Draft, UglyUrls, Verbose bool
var Source, Destination, BaseUrl, CfgFile string
func Execute() {
AddCommands()
utils.StopOnErr(HugoCmd.Execute())
}
func AddCommands() {
HugoCmd.AddCommand(serverCmd)
HugoCmd.AddCommand(version)
HugoCmd.AddCommand(check)
HugoCmd.AddCommand(benchmark)
}
func init() {
HugoCmd.PersistentFlags().BoolVarP(&Draft, "build-drafts", "D", false, "include content marked as draft")
HugoCmd.PersistentFlags().StringVarP(&Source, "source", "s", "", "filesystem path to read files relative from")
HugoCmd.PersistentFlags().StringVarP(&Destination, "destination", "d", "", "filesystem path to write files to")
HugoCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
HugoCmd.PersistentFlags().BoolVar(&UglyUrls, "uglyurls", false, "if true, use /filename.html instead of /filename/")
HugoCmd.PersistentFlags().StringVarP(&BaseUrl, "base-url", "b", "", "hostname (and path) to the root eg. http://spf13.com/")
HugoCmd.PersistentFlags().StringVar(&CfgFile, "config", "", "config file (default is path/config.yaml|json|toml)")
HugoCmd.PersistentFlags().BoolVar(&nitro.AnalysisOn, "stepAnalysis", false, "display memory and timing of different steps of the program")
HugoCmd.Flags().BoolVarP(&BuildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
hugoCmdV = HugoCmd
}
func InitializeConfig() {
Config = hugolib.SetupConfig(&CfgFile, &Source)
if hugoCmdV.PersistentFlags().Lookup("build-drafts").Changed {
Config.BuildDrafts = Draft
}
if hugoCmdV.PersistentFlags().Lookup("uglyurls").Changed {
Config.UglyUrls = UglyUrls
}
if hugoCmdV.PersistentFlags().Lookup("verbose").Changed {
Config.Verbose = Verbose
}
if BaseUrl != "" {
Config.BaseUrl = BaseUrl
}
if Destination != "" {
Config.PublishDir = Destination
}
}
func build(watches ...bool) {
utils.CheckErr(copyStatic(), fmt.Sprintf("Error copying static files to %s", Config.GetAbsPath(Config.PublishDir)))
watch := false
if len(watches) > 0 && watches[0] {
watch = true
}
utils.StopOnErr(buildSite(BuildWatch || watch))
if BuildWatch {
fmt.Println("Watching for changes in", Config.GetAbsPath(Config.ContentDir))
fmt.Println("Press ctrl+c to stop")
utils.CheckErr(NewWatcher(0))
}
}
func copyStatic() error {
staticDir := Config.GetAbsPath(Config.StaticDir + "/")
if _, err := os.Stat(staticDir); os.IsNotExist(err) {
return nil
}
// Copy Static to Destination
return fsync.Sync(Config.GetAbsPath(Config.PublishDir+"/"), Config.GetAbsPath(Config.StaticDir+"/"))
}
func getDirList() []string {
var a []string
walker := func(path string, fi os.FileInfo, err error) error {
if err != nil {
fmt.Println("Walker: ", err)
return nil
}
if fi.IsDir() {
a = append(a, path)
}
return nil
}
filepath.Walk(Config.GetAbsPath(Config.ContentDir), walker)
filepath.Walk(Config.GetAbsPath(Config.LayoutDir), walker)
filepath.Walk(Config.GetAbsPath(Config.StaticDir), walker)
return a
}
func buildSite(watching ...bool) (err error) {
startTime := time.Now()
site := &hugolib.Site{Config: *Config}
if len(watching) > 0 && watching[0] {
site.RunMode.Watching = true
}
err = site.Build()
if err != nil {
return
}
site.Stats()
fmt.Printf("in %v ms\n", int(1000*time.Since(startTime).Seconds()))
return nil
}
func NewWatcher(port int) error {
watcher, err := fsnotify.NewWatcher()
var wg sync.WaitGroup
if err != nil {
fmt.Println(err)
return err
}
defer watcher.Close()
wg.Add(1)
go func() {
for {
select {
case ev := <-watcher.Event:
if Verbose {
fmt.Println(ev)
}
watchChange(ev)
// TODO add newly created directories to the watch list
case err := <-watcher.Error:
if err != nil {
fmt.Println("error:", err)
}
}
}
}()
for _, d := range getDirList() {
if d != "" {
_ = watcher.Watch(d)
}
}
if port > 0 {
go serve(port)
}
wg.Wait()
return nil
}
func watchChange(ev *fsnotify.FileEvent) {
if strings.HasPrefix(ev.Name, Config.GetAbsPath(Config.StaticDir)) {
fmt.Println("Static file changed, syncing\n")
utils.CheckErr(copyStatic(), fmt.Sprintf("Error copying static files to %s", Config.GetAbsPath(Config.PublishDir)))
} else {
if !ev.IsRename() { // Rename is always accompanied by a create or modify
// Ignoring temp files created by editors (vim)
if !strings.HasSuffix(ev.Name, "~") && !strings.HasSuffix(ev.Name, ".swp") {
fmt.Println("Change detected, rebuilding site\n")
utils.StopOnErr(buildSite(true))
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
// 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 commands
import (
"fmt"
"github.com/spf13/cobra"
"net/http"
"strconv"
)
var serverPort int
var serverWatch bool
func init() {
serverCmd.Flags().IntVarP(&serverPort, "port", "p", 1313, "port to run the server on")
serverCmd.Flags().BoolVarP(&serverWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
}
var serverCmd = &cobra.Command{
Use: "server",
Short: "Hugo runs it's own a webserver to render the files",
Long: `Hugo is able to run it's own high performance web server.
Hugo will render all the files defined in the source directory and
Serve them up.`,
Run: server,
}
func server(cmd *cobra.Command, args []string) {
InitializeConfig()
// Unless command line overrides, we use localhost for the server
if BaseUrl == "" {
Config.BaseUrl = "http://localhost:" + strconv.Itoa(serverPort)
}
build(serverWatch)
// Watch runs its own server as part of the routine
if serverWatch {
fmt.Println("Watching for changes in", Config.GetAbsPath(Config.ContentDir))
err := NewWatcher(serverPort)
if err != nil {
fmt.Println(err)
}
}
serve(serverPort)
}
func serve(port int) {
if Verbose {
fmt.Println("Serving pages from " + Config.GetAbsPath(Config.PublishDir))
}
fmt.Printf("Web Server is available at http://localhost:%v\n", port)
fmt.Println("Press ctrl+c to stop")
panic(http.ListenAndServe(":"+strconv.Itoa(port), http.FileServer(http.Dir(Config.GetAbsPath(Config.PublishDir)))))
}
+28
View File
@@ -0,0 +1,28 @@
// 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 commands
import (
"fmt"
"github.com/spf13/cobra"
)
var version = &cobra.Command{
Use: "version",
Short: "Print the version number of Hugo",
Long: `All software has versions. This is Hugo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hugo Static Site Generator v0.9")
},
}
+3 -2
View File
@@ -1,4 +1,5 @@
Indexes:
indexes:
tag: 'tags'
BaseUrl: 'http://localhost'
group: 'groups'
baseurl: 'http://hugo.spf13.com'
...
@@ -1,6 +1,10 @@
---
title: "Example Content File"
Pubdate: "2013-07-01"
date: "2013-07-01"
aliases: ["/doc/example/"]
linktitle: "Example"
groups: ['content']
groups_weight: 50
---
Somethings are better shown than explained. The following is a very basic example of a content file:
@@ -8,14 +12,14 @@ Somethings are better shown than explained. The following is a very basic exampl
**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"
Title: "Nitro : A quick and simple profiler for golang"
Description: ""
Keywords: [ "Development", "golang", "profiling" ]
Tags: [ "Development", "golang", "profiling" ]
date: "2013-06-19"
Topics: [ "Development", "GoLang" ]
Slug: "nitro"
project_url: "http://github.com/spf13/nitro"
---
# Nitro
@@ -1,11 +1,14 @@
+++
title = "Front Matter"
date = "2013-07-01"
aliases = ["/doc/front-matter/"]
groups = ["content"]
groups_weight = 40
+++
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 each with their own identifying tokens.
The front matter is one of the features that gives Hugo its strength. It enables
you to include the meta data of the content right with it. Hugo supports a few
different formats each with their own identifying tokens.
Supported formats: <br>
**YAML**, identified by '\-\-\-'. <br>
@@ -18,13 +21,13 @@ Supported formats: <br>
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"
date: "2012-04-06"
categories:
- "Development"
- "VIM"
slug: "spf13-vim-3-0-release-and-new-website"
---
Content of the file goes Here
Content of the file goes Here
### TOML Example
@@ -32,14 +35,14 @@ Supported formats: <br>
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"
date = "2012-04-06"
categories = [
"Development",
"VIM"
]
slug = "spf13-vim-3-0-release-and-new-website"
+++
Content of the file goes Here
Content of the file goes Here
### JSON Example
@@ -54,7 +57,7 @@ Supported formats: <br>
],
"slug": "spf13-vim-3-0-release-and-new-website",
}
Content of the file goes Here
Content of the file goes Here
### Variables
@@ -71,6 +74,7 @@ any variable they want to. These will be placed into the `.Params` variable avai
#### Optional
**redirect** Mark the post as a redirect post<br>
**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>
**markup** (Experimental) Specify "rst" for reStructuredText (requires
+157
View File
@@ -0,0 +1,157 @@
---
title: "Content Organization"
date: "2013-07-01"
aliases: ["/doc/organization/"]
linktitle: "Organization"
groups: ['content']
groups_weight: 10
---
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. Hugo supports
content nested at any level. The top level is special in Hugo and is used as the
[section](/content/sections).
.
└── content
├── post
| ├── firstpost.md // <- http://site.com/post/firstpost/
| ├── happy
| | └── happiness.md // <- http://site.com/post/happy/happiness/
| └── secondpost.md // <- http://site.com/post/secondpost/
└── quote
├── first.md // <- http://site.com/quote/first/
└── second.md // <- http://site.com/quote/second/
**Here's the same organization run with hugo -\-uglyurls**
.
└── content
├── post
| ├── firstpost.md // <- http://site.com/post/firstpost.html
| ├── happy
| | └── happiness.md // <- http://site.com/post/happy/happiness.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
## Destinations
Hugo thinks that you organize your content with a purpose. The same structure
that works to organize your source content is used to organize the rendered
site. As displayed above, the organization of the source content will be
mirrored in the destination.
There are times when one would need more control over their content. In these
cases there are a variety of things that can be specified in the front matter to
determine the destination of a specific piece of content.
The following items are defined in order, latter items in the list will override
earlier settings.
#### filename
This isn't in the front matter, but is the actual name of the file minus the
extension. This will be the name of the file in the destination.
#### slug
Defined in the front matter, the slug can take the place of the filename for the
destination.
#### filepath
The actual path to the file on disk. Destination will create the destination
with the same path. Includes [section](/content/sections).
#### section
section can be provided in the front matter overriding the section derived from
the source content location on disk. See [section](/content/sections).
#### path
path can be provided in the front matter. This will replace the actual
path to the file on disk. Destination will create the destination with the same
path. Includes [section](/content/sections).
#### url
A complete url can be provided. This will override all the above as it pertains
to the end destination. This must be the path from the baseurl (starting with a "/").
When a url is provided it will be used exactly. Using url will ignore the
-\-uglyurls setting.
## Path breakdown in hugo
### Content
. path slug
. ⊢-------^----⊣ ⊢------^-------⊣
content/extras/indexes/category-example/index.html
. section slug
. ⊢--^--⊣ ⊢------^-------⊣
content/extras/indexes/category-example/index.html
. section slug
. ⊢--^--⊣⊢--^--⊣
content/extras/indexes/index.html
### Destination
permalink
⊢--------------^-------------⊣
http://spf13.com/projects/hugo
baseUrl section slug
⊢-----^--------⊣ ⊢--^---⊣ ⊢-^⊣
http://spf13.com/projects/hugo
baseUrl section slug
⊢-----^--------⊣ ⊢--^--⊣ ⊢--^--⊣
http://spf13.com/extras/indexes/example
baseUrl path slug
⊢-----^--------⊣ ⊢------^-----⊣ ⊢--^--⊣
http://spf13.com/extras/indexes/example
baseUrl url
⊢-----^--------⊣ ⊢-----^-----⊣
http://spf13.com/projects/hugo
baseUrl url
⊢-----^--------⊣ ⊢--------^-----------⊣
http://spf13.com/extras/indexes/example
section = which type the content is by default
based on content location
front matter overrides
slug = name.ext or name/
based on content-name.md
front matter overrides
path = section + path to file exluding slug
based on path to content location
url = relative url
defined in front matter
overrides all the above
+45
View File
@@ -0,0 +1,45 @@
---
title: "Sections"
date: "2013-07-01"
groups: ['content']
groups_weight: 20
---
Hugo thinks that you organize your content with a purpose. The same structure
that works to organize your source content is used to organize the rendered
site ( [see organization](/content/organization) ). Following this pattern Hugo
uses the top level of your content organization as **the Section**.
The following example site uses two sections, "post" and "quote".
.
└── content
├── post
| ├── firstpost.md // <- http://site.com/post/firstpost/
| ├── happy
| | └── happiness.md // <- http://site.com/happy/happiness/
| └── secondpost.md // <- http://site.com/post/secondpost/
└── quote
├── first.md // <- http://site.com/quote/first/
└── second.md // <- http://site.com/quote/second/
*Regardless of location on disk, the section can be provided in the front matter
which will affect the destination location*.
## Sections and Types
By default everything created within a section will use the content type
that matches the section name.
Section defined in the front matter have the same impact.
To change the type of a given piece of content simply define the type
in the front matter.
If a layout for a given type hasn't been provided a default type template will
be used instead provided is exists.
+52
View File
@@ -0,0 +1,52 @@
---
title: "Content Types"
date: "2013-07-01"
linktitle: "Types"
groups: ['content']
groups_weight: 30
---
Hugo has full support for multiple content types each with its own set
of meta data and template. A good example of when multiple types are
needed is to look a tumblr. A piece of content could be a photo, quote
or post, each with different meta data and rendered differently.
## Defining a content type
Creating a new content type is easy in Hugo. You simply provide the
templates that the new type will use.
It is essential to provide the single render view template as well as a
list view template.
**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.
## Assigning a content type
Hugo assumes that your site will be organized into [sections](/content/sections)
and each section will use the corresponding type. If you are taking advantage of
this then each new piece of content you place into a section will automatically
inherit the type.
Alternatively you can set the type in the meta data under the key "type".
-9
View File
@@ -1,9 +0,0 @@
---
title: "Contributors"
date: "2013-07-01"
---
Hugo was built with love and golang by:
* [spf13](https://github.com/spf13)
-183
View File
@@ -1,183 +0,0 @@
---
title: "Indexes"
Pubdate: "2013-07-01"
---
Hugo includes support for user defined indexes of content. In our
terminology an index is best thought of as tags applied to content
but they can be used for far more than just tags. Other common
uses would include categories, groups, series. For the purpose of
this document we will just use tags for our example. For a more
complete example see [spf13.com-hugo](http://github.com/spf13/spf13.com-hugo).
## Defining Indexes for a site
Indexes must be defined in the site configuration, before they
can be used throughout the site.
Here is an example configuration in YAML that specifies two indexes.
Notice the format is **singular key** : *plural value*. While
we could use an inflection library to pluralize this, they currently
support only a few languages, so instead we've opted for user defined
pluralization.
**config.yaml**
---
indexes:
tag: "tags"
topic: "topics"
baseurl: "http://spf13.com/"
title: "Steve Francia is spf13.com"
---
## Creating index templates
For each index type a template needs to be provided to render the index page.
In the case of tags, this will render the content for /tags/TAGNAME/.
The template must be called the singular name of the index and placed in
layouts/indexes
.
└── layouts
└── indexes
└── category.html
The template will be provided Data about the index.
### Variables
The following variables are available to the index template:
**.Title** The title for the content. <br>
**.Date** The date the content is published on.<br>
**.Permalink** The Permanent link for this page.<br>
**.RSSLink** Link to the indexes' rss link. <br>
**.Data.Pages** The content that is assigned this index.<br>
**.Data.`singular`** The index itself.<br>
#### Example
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
{{ range .Data.Pages }}
{{ .Render "summary"}}
{{ end }}
</div>
</section>
<aside id="meta"> </aside>
{{ template "chrome/footer.html" }}
## Assigning indexes to content
Once an index is defined at the site level, any piece of content
can be assigned to it regardless of content type or section.
Assigning content to an index is done in the front matter.
Simply create a variable with the *plural* name of the index
and assign all keys you want this content to match against.
**Index values are case insensitive**
#### Example
{
"Title": "Hugo: A fast and flexible static site generator",
"Tags": [
"Development",
"golang",
"Blogging"
],
"Slug": "hugo",
"project_url": "http://github.com/spf13/hugo"
}
## Displaying indexes within content
Within your content templates you may wish to display
the indexes that that piece of content is assigned to.
Because we are leveraging the front matter system to
define indexes for content, the indexes assigned to
each content piece are located in the usual place
(.Params.`plural`)
#### Example
<ul id="tags">
{{ range .Params.tags }}
<li><a href="tags/{{ . | urlize }}">{{ . }}</a> </li>
{{ end }}
</ul>
## Creating Indexes of Indexes
Hugo also supports creating pages that list your values for each
index along with the number of content items associated with the
index key.
This may take the form of a tag cloud or simply a list.
To have hugo create these indexes of indexes pages, simply create
a template in indexes called indexes.html
Hugo provides two different versions of the index. One alphabetically
sorted, the other sorted by most popular. It's important to recognize
that the data structure of the two is different.
#### Example indexes.html file (alphabetical)
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
<ul>
{{ $data := .Data }}
{{ range $key, $value := .Data.Index }}
<li><a href="{{ $data.Plural }}/{{ $key | urlize }}"> {{ $key }} </a> {{ len $value }} </li>
{{ end }}
</ul>
</div>
</section>
{{ template "chrome/footer.html" }}
#### Example indexes.html file (ordered)
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
<ul>
{{ range $data.OrderedIndex }}
<li><a href="{{ $data.Plural }}/{{ .Name | urlize }}"> {{ .Name }} </a> {{ .Count }} </li>
{{ end }}
</ul>
</div>
</section>
{{ template "chrome/footer.html" }}
### Variables available to indexes of indexes pages.
**.Title** The title for the content. <br>
**.Date** The date the content is published on.<br>
**.Permalink** The Permanent link for this page.<br>
**.RSSLink** Link to the indexes' rss link. <br>
**.Data.Singular** The singular name of the index <br>
**.Data.Plural** The plural name of the index<br>
**.Data.Index** The Alphabetical index<br>
**.Data.OrderedIndex** The popular index<br>
-22
View File
@@ -1,22 +0,0 @@
---
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
-26
View File
@@ -1,26 +0,0 @@
---
title: "Release Notes"
Pubdate: "2013-07-01"
---
* **0.8.0** August 2, 2013
* Added support for pretty urls (filename/index.html vs filename.html)
* Hugo supports a destination directory
* Will efficiently sync content in static to destination directory
* Cleaned up options.. now with support for short and long options
* Added support for TOML
* Added support for YAML
* Added support for Previous & Next
* Added support for indexes for the indexes
* Better Windows compatibility
* Support for series
* Adding verbose output
* Loads of bugfixes
* **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
-66
View File
@@ -1,66 +0,0 @@
---
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.
-55
View File
@@ -1,55 +0,0 @@
---
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, --base-url="": hostname (and path) to the root eg. http://spf13.com/
-D, --build-drafts=false: include content marked as draft
--config="": config file (default is path/config.yaml|json|toml)
-d, --destination="": filesystem path to write files to
-h, --help=false: show this help
--port="1313": port to run web server on, default :1313
-S, --server=false: run a (very) simple web server
-s, --source="": filesystem path to read files relative from
--uglyurls=false: use /filename.html instead of /filename/
-v, --verbose=false: verbose output
--version=false: which version of hugo
-w, --watch=false: watch filesystem for changes and recreate as needed
## 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 indexes created
in 8 ms
If you are working on things and want to see the changes
immediately, tell Hugo to watch for changes. **It will
recreate the site faster than you can tab over to
your browser to view the changes.**
$ hugo --source ~/mysite --watch
Watching for changes. Press ctrl+c to stop
15 pages created
0 tags created
in 8 ms
Hugo can even run a server and create your site at the same time!
$hugo --server -ws ~/mysite
Watching for changes. Press ctrl+c to stop
15 pages created
0 tags created
in 8 ms
Web Server is available at http://localhost:1313
Press ctrl+c to stop
+36
View File
@@ -0,0 +1,36 @@
---
title: "Aliases"
date: "2013-07-09"
aliases:
- /doc/redirects/
- /doc/alias/
- /doc/aliases/
groups: ["extras"]
groups_weight: 20
---
For people migrating existing published content to Hugo theres a good chance
you need a mechanism to handle redirecting old urls.
Luckily, this can be handled easily with aliases in Hugo.
## Example
**content/posts/my-awesome-blog-post.md**
---
aliases:
- /posts/my-original-url/
- /2010/even-earlier-url.html
---
Now when you go to any of the aliases locations they
will redirect to the page.
## Important Behaviors
1. *Hugo makes no assumptions about aliases. They also don't change based
on your UglyUrls setting. You need to provide absolute path to your webroot and the
complete filename or directory.*
2. *Aliases are rendered prior to any content and will be overwritten by
any content with the same location.*
@@ -1,9 +1,12 @@
---
title: "Shortcodes"
Pubdate: "2013-07-01"
date: "2013-07-01"
aliases: ["/doc/shortcodes/"]
groups: ["extras"]
groups_weight: 10
---
Because Hugo uses markdown for it's content format, it was clear that there's a lot of things that
Because Hugo uses markdown for its 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
+58
View File
@@ -0,0 +1,58 @@
---
title: "Example Index - Category"
date: "2013-07-01"
linktitle: "Example - Categories"
aliases: ["/extras/indexes/category"]
groups: ["indexes"]
groups_weight: 60
---
This page demonstrates what would be required to add a new index called "categories" to your site.
### config.yaml
First step is to define the index in your config file.
*Because we use both the singular and plural name of the index in our rendering it's
important to provide both here. We require this, rather than using inflection in
effort to support as many languages as possible.*
---
indexes:
category: "categories"
baseurl: "http://spf13.com/"
title: "Steve Francia is spf13.com"
---
### /layouts/indexes/category.html
For each index type a template needs to be provided to render the index page.
In the case of categories, this will render the content for /categories/CATEGORYNAME/.
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
{{ range .Data.Pages }}
{{ .Render "summary"}}
{{ end }}
</div>
</section>
{{ template "chrome/footer.html" }}
### Assigning indexes to content
Make sure that the index is set in the front matter:
{
"title": "Hugo: A fast and flexible static site generator",
"categories": [
"Development",
"golang",
"Blogging"
],
"slug": "hugo"
}
+109
View File
@@ -0,0 +1,109 @@
---
title: "Rendering Indexes"
date: "2013-07-01"
linktitle: "Displaying"
groups: ["indexes"]
groups_weight: 20
---
# For Content
### Index values assigned to this content
Within your content templates you may wish to display
the indexes that that piece of content is assigned to.
Because we are leveraging the front matter system to
define indexes for content, the indexes assigned to
each content piece are located in the usual place
(.Params.`plural`)
#### Example
<ul id="tags">
{{ range .Params.tags }}
<li><a href="tags/{{ . | urlize }}">{{ . }}</a> </li>
{{ end }}
</ul>
# Anywhere
### Displaying all keys for an index
If you wish to display the list of all keys for an index you can
find retrieve them from the `.Site` variable.
This may take the form of a tag cloud, a menu or simply a list.
The following example displays all tag keys:
#### Example
<ul id="all-tags">
{{ range .Site.Indexes.tags }}
<li><a href="/tags/{{ .Name | urlize }}">{{ .Name }}</a></li>
{{ end }}
</ul>
## Creating a menu based on indexes
Hugo can generate menus based on indexes by iterating and
nesting the index keys. This can be used to build a hierarchy
of content within your site.
To have hugo create the menu, simply create a template in chrome
called menu.html, then include it using the
`{{ template "chrome/menu.html" . }}` syntax.
This example will list all indexes, each of their keys and all the content assigned to each key.
#### Example complete menu.html file
<section id="menu">
<ul>
{{ range $indexname, $index := .Site.Indexes }}
<li><a href="/{{ $indexname | urlize }}">{{ $indexname }}</a>
<ul>
{{ range $key, $value := $index }}
<li> {{ $key }} </li>
<ul>
{{ range $value.Pages }}
<li hugo-nav="{{ .RelPermalink}}"><a href="{{ .Permalink}}"> {{ .LinkTitle }} </a> </li>
{{ end }}
</ul>
{{ end }}
</ul>
</li>
{{ end }}
</ul>
</section>
It is more likely that you would want to use a single index for navigation.
In this example we are using the `groups` index for our menu.
#### Example menu.html file using a single index
<section id="menu">
<ul>
{{ range $key, $index := .Site.Indexes.groups }}
<li> {{ $key }} </li>
<ul>
{{ range $index.Pages }}
<li hugo-nav="{{ .RelPermalink}}"><a href="{{ .Permalink}}"> {{ .LinkTitle }} </a> </li>
{{ end }}
</ul>
{{ end }}
</ul>
</section>
Or order the keys by Popularity
#### Example menu.html file using a single index
<section id="menu">
<ul>
{{ range .Site.Indexes.groups.ByCount }}
<li> {{ .Name }} </li>
<ul>
{{ range .Pages }}
<li hugo-nav="{{ .RelPermalink}}"><a href="{{ .Permalink}}"> {{ .LinkTitle }} </a> </li>
{{ end }}
</ul>
{{ end }}
</ul>
</section>
+74
View File
@@ -0,0 +1,74 @@
---
title: "Index Lists"
date: "2013-07-01"
aliases: ["/doc/indexes/", "/extras/indexes"]
linktitle: "Lists"
groups: ["indexes"]
groups_weight: 40
---
An index list is a list of all the keys that are contained in the index. When a
template is present, this will be rendered at /IndexPlural/
Hugo also supports creating pages that list your values for each index along
with the number of content items associated with the index key. These are
global pages, not attached to any specific content, but rather display the meta
data in aggregate.
To have hugo create these list of indexes pages, simply create a template in
/layouts/indexes/ called indexes.html
Hugo can order the meta data in two different ways. It can be ordered by the
number of content assigned to that key or alphabetically.
#### Example indexes.html file (alphabetical)
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
<ul>
{{ $data := .Data }}
{{ range $key, $value := .Data.Index.Alphabetical }}
<li><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}"> {{ $value.Name }} </a> {{ $value.Count }} </li>
{{ end }}
</ul>
</div>
</section>
{{ template "chrome/footer.html" }}
#### Example indexes.html file (ordered)
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
<ul>
{{ $data := .Data }}
{{ range $key, $value := .Data.Index.ByCount }}
<li><a href="{{ $data.Plural }}/{{ $value.Name | urlize }}"> {{ $value.Name }} </a> {{ $value.Count }} </li>
{{ end }}
</ul>
</div>
</section>
{{ template "chrome/footer.html" }}
### Variables available to list of indexes pages.
**.Title** The title for the content. <br>
**.Date** The date the content is published on.<br>
**.Permalink** The Permanent link for this page.<br>
**.RSSLink** Link to the indexes' rss link. <br>
**.Data.Singular** The singular name of the index <br>
**.Data.Plural** The plural name of the index<br>
**.Data.Index** The Index itself<br>
**.Data.Index.Alphabetical** The Index alphabetized<br>
**.Data.Index.ByCount** The Index ordered by popularity<br>
+75
View File
@@ -0,0 +1,75 @@
---
title: "Index Overview"
date: "2013-07-01"
aliases: ["/doc/indexes/", "/extras/indexes"]
linktitle: "Overview"
groups: ["indexes"]
groups_weight: 10
---
Hugo includes support for user defined groupings of content called indexes.
Indexes can be used to organize content in a variety of ways. For example, if I
wanted to use a wordpress style organization I would create two indexes called
"categories" and "tags". Other common uses would include categories, tags, groups,
navigation, series and many more. Just think of an index as way to organize similar content.
It's important to understand what Indexes do. At it's most basic form an index
is simply a map of a key to a list of content values.
In the hugo internals this is stored as Site.Indexes[Plural][key][]pages.
For example all the content tagged with GoLang would be found at
Site.Indexes["tags"]["golang"].
For a
more complete example see the source of [this docs site](http://github.com/spf13/hugo/docs/).
## Defining Indexes for a site
Indexes must be defined in the site configuration, before they
can be used throughout the site.
Here is an example configuration in YAML that specifies two indexes.
Notice the format is **singular key** : *plural value*. While
we could use an inflection library to pluralize this, they currently
support only a few languages, so instead we've opted for user defined
pluralization.
**config.yaml**
---
indexes:
tag: "tags"
category: "categories"
baseurl: "http://spf13.com/"
title: "Steve Francia is spf13.com"
---
## Assigning index values to content
Once an index is defined at the site level, any piece of content
can be assigned to it regardless of content type or section.
Assigning content to an index is done in the front matter.
Simply create a variable with the *plural* name of the index
and assign all keys you want this content to match against.
**Index values are case insensitive**
#### Example
{
"title": "Hugo: A fast and flexible static site generator",
"tags": [
"Development",
"golang",
"fast",
"Blogging"
],
"categories" : [
"Development"
]
"slug": "hugo",
"project_url": "http://github.com/spf13/hugo"
}
+55
View File
@@ -0,0 +1,55 @@
---
title: "Index Templates"
date: "2013-07-01"
linktitle: "Templates"
groups: ["indexes"]
groups_weight: 30
---
There are two different templates that the use of indexes will require you to provide.
The first is a list of all the content assigned to a specific index key. The
second is a [list](/indexes/lists/) of all keys for that index. This document
addresses the template used for the first type.
## Creating index templates
For each index type a template needs to be provided to render the index page.
In the case of tags, this will render the content for /tags/TAGNAME/.
The template must be called the singular name of the index and placed in
layouts/indexes
.
└── layouts
└── indexes
└── category.html
The template will be provided Data about the index.
### Variables
The following variables are available to the index template:
**.Title** The title for the content. <br>
**.Date** The date the content is published on.<br>
**.Permalink** The Permanent link for this page.<br>
**.RSSLink** Link to the indexes' rss link. <br>
**.Data.Pages** The content that is assigned this index.<br>
**.Data.`singular`** The index itself.<br>
#### Example
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
{{ range .Data.Pages }}
{{ .Render "summary"}}
{{ end }}
</div>
</section>
{{ template "chrome/footer.html" }}
+31
View File
@@ -0,0 +1,31 @@
---
title: "404.html Templates"
date: "2013-08-21"
---
When using Hugo with [github pages](http://pages.github.com/) you can provide
your own 404 template by creating a 404.html file in the root.
404 pages are of the type "node" and have all the [node
variables](/layout/variables/) available to use in the templates.
In addition to the standard node variables, the homepage has access to
all site content accessible from .Data.Pages
▾ layouts/
404.html
## 404.html
This is a basic example of a 404.html template:
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
</div>
</section>
{{ template "chrome/footer.html" }}
+82
View File
@@ -0,0 +1,82 @@
---
title: "Chrome Templates"
date: "2013-07-01"
groups: ["layout"]
groups_weight: 80
---
Chrome is a convention to create templates that are used by the other templates
throughout the site. There is nothing special about the name "chrome", feel free
to provide and use your own.
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). There is nothing special about header.html and footer.html
other than they seem like good names to use for inclusion in your other
templates.
▾ layouts/
▾ chrome/
header.html
footer.html
By ensuring that we only reference [variables](/layout/variables/) variables
used for both nodes and pages we can use the same chrome for both.
## example header.html
This header template is used for [spf13.com](http://spf13.com).
<!DOCTYPE html>
<html class="no-js" lang="en-US" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
<head>
<meta charset="utf-8">
{{ template "chrome/meta.html" . }}
<base href="{{ .Site.BaseUrl }}">
<title> {{ .Title }} : spf13.com </title>
<link rel="canonical" href="{{ .Permalink }}">
{{ if .RSSlink }}<link href="{{ .RSSlink }}" rel="alternate" type="application/rss+xml" title="{{ .Title }}" />{{ end }}
{{ template "chrome/head_includes.html" . }}
</head>
<body lang="en">
## example footer.html
This header template is used for [spf13.com](http://spf13.com).
<footer>
<div>
<p>
&copy; 2013 Steve Francia.
<a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons Attribution">Some rights reserved</a>;
please attribute properly and link back. Hosted by <a href="http://servergrove.com">ServerGrove</a>.
</p>
</div>
</footer>
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XYSYXYSY-X']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script');
ga.src = ('https:' == document.location.protocol ? 'https://ssl' :
'http://www') + '.google-analytics.com/ga.js';
ga.setAttribute('async', 'true');
document.documentElement.firstChild.appendChild(ga);
})();
</script>
</body>
</html>
**For examples of referencing these templates, see [content
templates](/layout/content/) and [homepage templates](/layout/homepage/)**
+129
View File
@@ -0,0 +1,129 @@
---
title: "Content Templates"
date: "2013-07-01"
---
Content templates are created in a directory matching the name of the content.
Content pages are of the type "page" and have all the [page
variables](/layout/variables/) available to use in the templates.
In the following examples we have created two different content types as well as
a default content type.
▾ layouts/
▾ post/
single.html
▾ project/
single.html
Hugo also has support for a default content template to be used in the event
that a specific template has not been provided for that type. The default type
works the same as the other types but the directory must be called "_default".
[Content views](/layout/views) can also be defined in the "_default" directory.
▾ layouts/
▾ _default/
single.html
## post/single.html
This content template is used for [spf13.com](http://spf13.com).
It makes use of [chrome templates](/layout/chrome)
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
{{ $baseurl := .Site.BaseUrl }}
<section id="main">
<h1 id="title">{{ .Title }}</h1>
<div>
<article id="content">
{{ .Content }}
</article>
</div>
</section>
<aside id="meta">
<div>
<section>
<h4 id="date"> {{ .Date.Format "Mon Jan 2, 2006" }} </h4>
<h5 id="wc"> {{ .FuzzyWordCount }} Words </h5>
</section>
<ul id="categories">
{{ range .Params.topics }}
<li><a href="{{ $baseurl }}/topics/{{ . | urlize }}">{{ . }}</a> </li>
{{ end }}
</ul>
<ul id="tags">
{{ range .Params.tags }}
<li> <a href="{{ $baseurl }}/tags/{{ . | urlize }}">{{ . }}</a> </li>
{{ end }}
</ul>
</div>
<div>
{{ if .Prev }}
<a class="previous" href="{{.Prev.Permalink}}"> {{.Prev.Title}}</a>
{{ end }}
{{ if .Next }}
<a class="next" href="{{.Next.Permalink}}"> {{.Next.Title}}</a>
{{ end }}
</div>
</aside>
{{ template "chrome/disqus.html" . }}
{{ template "chrome/footer.html" . }}
## project/single.html
This content template is used for [spf13.com](http://spf13.com).
It makes use of [chrome templates](/layout/chrome)
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
{{ $baseurl := .Site.BaseUrl }}
<section id="main">
<h1 id="title">{{ .Title }}</h1>
<div>
<article id="content">
{{ .Content }}
</article>
</div>
</section>
<aside id="meta">
<div>
<section>
<h4 id="date"> {{ .Date.Format "Mon Jan 2, 2006" }} </h4>
<h5 id="wc"> {{ .FuzzyWordCount }} Words </h5>
</section>
<ul id="categories">
{{ range .Params.topics }}
<li><a href="{{ $baseurl }}/topics/{{ . | urlize }}">{{ . }}</a> </li>
{{ end }}
</ul>
<ul id="tags">
{{ range .Params.tags }}
<li> <a href="{{ $baseurl }}/tags/{{ . | urlize }}">{{ . }}</a> </li>
{{ end }}
</ul>
</div>
</aside>
{{if isset .Params "project_url" }}
<div id="ribbon">
<a href="{{ index .Params "project_url" }}" rel="me">Fork me on GitHub</a>
</div>
{{ end }}
{{ template "chrome/footer.html" }}
Notice how the project/single.html template uses an additional parameter unique
to this template. This doesn't need to be defined ahead of time. If the key is
present in the front matter than it can be used in the template.
+18
View File
@@ -0,0 +1,18 @@
---
title: "Go Templates"
date: "2013-07-01"
groups: ["layout"]
groups_weight: 80
draft: true
---
Hugo uses the excellent golang html/template library for its 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 is a brief primer on using go templates. The [golang
docs](http://golang.org/pkg/html/template/) provide more details.
+52
View File
@@ -0,0 +1,52 @@
---
title: "Homepage Templates"
date: "2013-07-01"
groups: ["layout"]
groups_weight: 30
---
Home pages are of the type "node" and have all the [node
variables](/layout/variables/) available to use in the templates.
*This is the only required template for building a site and useful when
bootstrapping a new site and template.*
In addition to the standard node variables, the homepage has access to
all site content accessible from .Data.Pages
▾ layouts/
index.html
## example index.html
This content template is used for [spf13.com](http://spf13.com).
It makes use of [chrome templates](/layout/chrome)
<!DOCTYPE html>
<html class="no-js" lang="en-US" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
<head>
<meta charset="utf-8">
{{ template "chrome/meta.html" . }}
<base href="{{ .Site.BaseUrl }}">
<title>{{ .Site.Title }}</title>
<link rel="canonical" href="{{ .Permalink }}">
<link href="{{ .RSSlink }}" rel="alternate" type="application/rss+xml" title="{{ .Site.Title }}" />
{{ template "chrome/head_includes.html" . }}
</head>
<body lang="en">
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
{{ range first 10 .Data.Pages }}
{{ .Render "summary"}}
{{ end }}
</div>
</section>
{{ template "chrome/footer.html" }}
+124
View File
@@ -0,0 +1,124 @@
---
title: "Index Templates"
date: "2013-07-01"
groups: ["layout"]
groups_weight: 50
---
An index template is any template that will be used to render multiple pieces of
content (with the exception of the [homepage](/layout/homepage) which has a
dedicated template).
We are using the term index in its truest sense, a sequential arrangement of
material, especially in alphabetical or numerical order. In the case of Hugo
each index will render the content in newest first order based on the date
provided in the [front matter](/content/front-matter).
index pages are of the type "node" and have all the [node
variables](/layout/variables/) available to use in the templates.
All index templates live in the layouts/indexes directory. There are 3 different
kinds of indexes that Hugo can produce.
1. A listing of all the content for a given [section](/content/sections)
2. A listing of all the content for a given [index](/extras/indexes)
3. A listing of listings... [meta index](/extras/indexes)
It's critical that the name of the index template matches either:
1. The section name
2. The index singular name
3. "indexes"
The following illustrates the location of one of each of these types.
▾ layouts/
▾ indexes/
indexes.html
post.html
tag.html
## Example section template (post.html)
This content template is used for [spf13.com](http://spf13.com).
It makes use of [chrome templates](/layout/chrome). All examples use a
[view](/layout/views/) called either "li" or "summary" which this example site
defined.
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
<ul id="list">
{{ range .Data.Pages }}
{{ .Render "li"}}
{{ end }}
</ul>
</div>
</section>
{{ template "chrome/footer.html" }}
## Example index template (tag.html)
This content template is used for [spf13.com](http://spf13.com).
It makes use of [chrome templates](/layout/chrome). All examples use a
[view](/layout/views/) called either "li" or "summary" which this example site
defined.
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
{{ range .Data.Pages }}
{{ .Render "summary"}}
{{ end }}
</div>
</section>
{{ template "chrome/footer.html" }}
## Example listing of indexes template (indexes.html)
This content template is used for [spf13.com](http://spf13.com).
It makes use of [chrome templates](/layout/chrome). The list of indexes
templates cannot use a [content view](/layout/views) as they don't display the content, but
rather information about the content.
This particular template lists all of the Tags used on
[spf13.com](http://spf13.com) and provides a count for the number of pieces of
content tagged with each tag.
This example demonstrates two different approaches. The first uses .Data.Index and
the latter uses .Data.OrderedIndex. .Data.Index is alphabetical by key name, while
.Data.Orderedindex is ordered by the quantity of content assigned to that particular
index key. In practice you would only use one of these approaches.
{{ template "chrome/header.html" . }}
{{ template "chrome/subheader.html" . }}
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
<ul>
{{ $data := .Data }}
{{ range $key, $value := .Data.Index }}
<li><a href="{{ $data.Plural }}/{{ $key | urlize }}"> {{ $key }} </a> {{ len $value }} </li>
{{ end }}
</ul>
</div>
<ul>
{{ range $data.OrderedIndex }}
<li><a href="{{ $data.Plural }}/{{ .Name | urlize }}"> {{ .Name }} </a> {{ .Count }} </li>
{{ end }}
</ul>
</section>
{{ template "chrome/footer.html" }}
+49
View File
@@ -0,0 +1,49 @@
---
title: "RSS (feed) Templates"
date: "2013-07-01"
groups: ["layout"]
groups_weight: 40
---
A single RSS template is used to generate all of the RSS content for the entire
site.
RSS pages are of the type "node" and have all the [node
variables](/layout/variables/) available to use in the templates.
In addition to the standard node variables, the homepage has access to
all site content accessible from .Data.Pages
▾ layouts/
rss.xml
## rss.xml
This rss template is used for [spf13.com](http://spf13.com). It adheres to the
ATOM 2.0 Spec.
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ .Title }} on {{ .Site.Title }} </title>
<link>{{ .Permalink }}</link>
<language>en-us</language>
<author>Steve Francia</author>
<rights>Copyright (c) 2008 - 2013, Steve Francia; all rights reserved.</rights>
<updated>{{ .Date }}</updated>
{{ range .Data.Pages }}
<item>
<title>{{ .Title }}</title>
<link>{{ .Permalink }}</link>
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 MST" }}</pubDate>
<author>Steve Francia</author>
<guid>{{ .Permalink }}</guid>
<description>{{ .Content | html }}</description>
</item>
{{ end }}
</channel>
</rss>
*Important: Hugo will automatically add the following header line to this file
on render...please don't include this in the template as it's not valid HTML.*
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+38
View File
@@ -0,0 +1,38 @@
---
title: "Templates"
date: "2013-07-01"
aliases: ["/doc/templates/"]
linktitle: "Overview"
groups: ["layout"]
groups_weight: 10
---
Hugo uses the excellent golang html/template library for its 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
If you are new to go's templates the [go template primer](/layout/go-templates)
is a great place to start.
## Template roles
There are 6 different kinds of templates that Hugo works with.
### [Homepage](/layout/homepage/)
The homepage of your site.
### [RSS](/layout/rss/)
Used to render all rss documents.
### [Index](/layout/indexes)
Page that list multiple pieces of content.
### [Content](/layout/content)
Templates to render a single piece of content.
### [Views](/layout/views)
Different ways of rendering each content type
### [Chrome](/layout/chrome)
Simply the decoration of your site.
@@ -1,31 +1,56 @@
---
title: "Variables"
Pubdate: "2013-07-01"
date: "2013-07-01"
aliases: ["/doc/variables/"]
groups: ["layout"]
groups_weight: 20
---
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>
## Page Variables
**.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>
**.RelPermalink** The Relative permanent link for this page.<br>
**.LinkTitle** Access when creating links to this content. Will use linktitle if set in front-matter, else title<br>
**.FuzzyWordCount** The approximate number of words in the content.<br>
**.RSSLink** Link to the indexes' rss link <br>
**.Prev** Pointer to the previous content (based on pub date)<br>
**.Next** Pointer to the following content (based on pub date)<br>
**.Site** See site variables below<br>
**.Content** The content itself, defined below the front matter.<br>
**.Summary** A generated summary of the content for easily showing a snippet in a summary view.<br>
Any value defined in the front matter, including indexes will be made available under `.Params`.
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>
**.Params.Tags** <br>
**.Params.Categories** <br>
## Node Variables
In Hugo a node is any page not rendered directly by a content file. This
includes indexes, lists and the homepage.
**.Title** The title for the content.<br>
**.Date** The date the content is published on.<br>
**.Data** The data specific to this type of node.<br>
**.Permalink** The Permanent link for this node<br>
**.Url** The relative url for this node.<br>
**.RSSLink** Link to the indexes' rss link <br>
**.Site** See site variables below<br>
## Site Variables
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.Indexes** The indexes for the entire 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>
+81
View File
@@ -0,0 +1,81 @@
---
title: "Content Views"
date: "2013-07-01"
groups: ["layout"]
groups_weight: 70
---
In addition to the [single content view](/layout/content/), Hugo can render alternative views of
your content. These are especially useful in [index](/layout/indexes) templates.
To create a new view simple create a template in each of your different content
type directories with the view name. In the following example we have created a
"li" view and a "summary" view for our two content types of post and project. As
you can see these sit next to the [single content view](/layout/content)
template "single.html"
▾ layouts/
▾ post/
li.html
single.html
summary.html
▾ project/
li.html
single.html
summary.html
Hugo also has support for a default content template to be used in the event
that a specific template has not been provided for that type. The default type
works the same as the other types but the directory must be called "_default".
Content views can also be defined in the "_default" directory.
▾ layouts/
▾ _default/
li.html
single.html
summary.html
Hugo will pass the entire page object to the view template. See [page
variables](/layout/variables) for a complete list.
## Example li.html
This content template is used for [spf13.com](http://spf13.com).
<li>
<a href="{{ .Permalink }}">{{ .Title }}</a>
<div class="meta">{{ .Date.Format "Mon, Jan 2, 2006" }}</div>
</li>
## Example summary.html
This content template is used for [spf13.com](http://spf13.com).
<article class="post">
<header>
<h2><a href='{{ .Permalink }}'> {{ .Title }}</a> </h2>
<div class="post-meta">{{ .Date.Format "Mon, Jan 2, 2006" }} - {{ .FuzzyWordCount }} Words </div>
</header>
{{ .Summary }}
<footer>
<a href='{{ .Permalink }}'><nobr>Read more →</nobr></a>
</footer>
</article>
## Example render of view
Using the summary view inside of another ([index](/layout/index)) template.
<section id="main">
<div>
<h1 id="title">{{ .Title }}</h1>
{{ range .Data.Pages }}
{{ .Render "summary"}}
{{ end }}
</div>
</section>
In the above example you will notice that we have called .Render and passed in
which view to render the content with. Render is a special function available on
a content which tells the content to render itself with the provided view template.
@@ -1,15 +1,18 @@
---
title: "Contributing to Hugo"
Pubdate: "2013-07-01"
date: "2013-07-01"
aliases: ["/doc/contributing/"]
groups: ["meta"]
groups_weight: 30
---
We welcome all contributions. If you want to contribute, all
that is needed is simply fork Hugo, make changes and submit
We welcome all contributions. If you want to contribute, all
that is needed is simply fork Hugo, make changes and submit
a pull request. If you prefer, pick something from the roadmap
or contact [spf13](http://spf13.com) about what may make sense
or contact [spf13](http://spf13.com) about what may make sense
to do next.
## Overview
## Overview
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'`)
@@ -23,8 +26,8 @@ to do next.
cd hugo
go get
Because go expects all of your libraries to be found in either
$GOROOT or $GOPATH, it's helpful to symlink the project to one
Because go expects all of your libraries to be found in either
$GOROOT or $GOPATH, it's helpful to symlink the project to one
of the following paths:
* ln -s /path/to/your/hugo $GOPATH/src/github.com/spf13/hugo
+14
View File
@@ -0,0 +1,14 @@
---
title: "Contributors"
date: "2013-07-01"
aliases: ["/doc/contributors/"]
groups: ["meta"]
groups_weight: 40
---
Hugo was built with love and golang by:
* Steve Francia - [spf13](https://github.com/spf13)
* Noah Campbell - [noahcampbell](https://github.com/noahcampbell)
* [Many more](http://github.com/spf13/hugo/graphs/contributors)
@@ -1,6 +1,9 @@
---
title: "License"
Pubdate: "2013-07-01"
date: "2013-07-01"
aliases: ["/doc/license/", "/license/"]
groups: ["meta"]
groups_weight: 50
---
Hugo is released under the Simple Public License.
+59
View File
@@ -0,0 +1,59 @@
---
title: "Release Notes"
date: "2013-07-01"
aliases: ["/doc/release-notes/"]
groups: ["meta"]
groups_weight: 10
---
* **0.9.0**
* New command based interface similar to git (hugo server -s ./ )
* Amber template support
* Aliases (redirects)
* Support for top level pages (in addition to homepage)
* Complete overhaul of the documentation site
* Full Windows support
* Better index support including ordering by content weight
* Add params to site config, available in .Site.Params from templates
* Friendlier json support
* Support for html & xml content (with frontmatter support)
* Support for summary content divider (&lt;!more>)
* HTML in summary (when using divider)
* Added "Minutes to Read" functionality
* Support for a custom 404 page
* Cleanup of how content organization is handled
* Loads of unit and performance tests
* Integration with travis ci
* Static directory now watched and copied on any addition or modification
* Support for relative permalinks
* Fixed watching being triggered multiple times for the same event
* Watch now ignores temp files (as created by Vim)
* Configurable number of posts on homepage
* Front matter supports multiple types (int, string, date, float)
* Indexes can now use a default template
* Addition of truncated bool to content to determine if should show 'more' link
* Support for linkTitles
* Better handling of most errors with directions on how to resolve
* Support for more date / time formats
* Support for go 1.2
* **0.8.0** August 2, 2013
* Added support for pretty urls (filename/index.html vs filename.html)
* Hugo supports a destination directory
* Will efficiently sync content in static to destination directory
* Cleaned up options.. now with support for short and long options
* Added support for TOML
* Added support for YAML
* Added support for Previous & Next
* Added support for indexes for the indexes
* Better Windows compatibility
* Support for series
* Adding verbose output
* Loads of bugfixes
* **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
@@ -1,6 +1,9 @@
---
title: "Roadmap"
Pubdate: "2013-07-01"
date: "2013-07-01"
aliases: ["/doc/roadmap/"]
groups: ["meta"]
groups_weight: 20
---
In no particular order, here is what we are working on:
@@ -9,8 +12,6 @@ In no particular order, here is what we are working on:
* Support for top level pages (other than homepage)
* Better error handling
* Syntax highlighting
* Commands
* Actions (eg. hugo create page)
* Related Posts
* Support for other formats
@@ -1,6 +1,10 @@
---
title: "Configuring Hugo"
pubdate: "2013-07-01"
date: "2013-07-01"
aliases: ["/doc/configuration/"]
groups: ['gettingStarted']
linktitle: "Configuration"
groups_weight: 40
---
The directory structure and templates provide the majority of the
@@ -15,10 +19,10 @@ then look for a config.json file, followed by a config.toml file.
## Examples
The following is an example of a yaml config file with the default values:
The following is an example of a yaml config file with the default values:
---
sourcedir: "content"
contentdir: "content"
layoutdir: "layouts"
publishdir: "public"
builddrafts: false
@@ -29,10 +33,10 @@ The following is an example of a yaml config file with the default values:
...
The following is an example of a json config file with the default values:
The following is an example of a json config file with the default values:
{
"sourcedir": "content",
"contentdir": "content",
"layoutdir": "layouts",
"publishdir": "public",
"builddrafts": false,
@@ -44,9 +48,9 @@ The following is an example of a json config file with the default values:
}
The following is an example of a toml config file with the default values:
The following is an example of a toml config file with the default values:
sourcedir = "content"
contentdir = "content"
layoutdir = "layouts"
publishdir = "public"
builddrafts = false
@@ -55,4 +59,3 @@ The following is an example of a toml config file with the default values:
category = "categories"
tag = "tags"
@@ -1,6 +1,9 @@
---
title: "Installing Hugo"
Pubdate: "2013-07-01"
date: "2013-07-01"
aliases: ["/doc/installing/"]
groups: ['gettingStarted']
groups_weight: 20
---
Hugo is written in GoLang with support for Windows, Linux, FreeBSD and OSX.
+91
View File
@@ -0,0 +1,91 @@
---
title: "Hugo Quickstart Guide"
date: "2013-07-01"
groups: ['gettingStarted']
linktitle: "Quickstart"
groups_weight: 10
---
## Step 1. Install Hugo
Goto [hugo releases](https://github.com/spf13/hugo/releases) and download the
appropriate version for your os and architecture.
Save it somewhere specific as we will be using it in the next step.
More complete instructions are available at [installing hugo](/overview/installing/)
## Step 2. Build the Docs
Hugo has it's own example site which happens to also be the documentation site
you are reading right now.
Follow the following steps:
1. Clone the [hugo repository](http://github.com/spf13/hugo)
2. Go into the repo
3. Run hugo in server mode and build the docs
4. Open your browser to http://localhost:1313
Corresponding pseudo commands:
git clone https://github.com/spf13/hugo
cd hugo
/path/to/hugo/from/step/1/hugo server --source ./docs
> 29 pages created
> 0 tags index created
> in 27 ms
> Web Server is available at http://localhost:1313
> Press ctrl+c to stop
Once you've gotten here, follow along the rest of this page on your local build.
## Step 3. Change the docs site
Stop the Hugo process by hitting ctrl+c.
Now we are going to run hugo again, but this time with hugo in watch mode.
/path/to/hugo/from/step/1/hugo server --source ./docs --watch
> 29 pages created
> 0 tags index created
> in 27 ms
> Web Server is available at http://localhost:1313
> Watching for changes in /Users/spf13/Code/hugo/docs/content
> Press ctrl+c to stop
Open your [favorite editor](http://vim.spf13.com) and change one of the source
content pages. How about changing this very file to *fix the typo*. How about changing this very file to *fix the typo*.
Content files are found in `docs/content/`. Unless otherwise specified, files
are located at the same relative location as the url, in our case
`docs/content/overview/quickstart.md`.
Change and save this file.. Notice what happened in your terminal.
> Change detected, rebuilding site
> 29 pages created
> 0 tags index created
> in 26 ms
Refresh the browser and observe that the typo is now fixed.
Notice how quick that was. Try to refresh the site before it's finished building.. I double dare you.
Having nearly instant feedback enables you to have your creativity flow without waiting for long builds.
## Step 4. Have fun
The best way to learn something is to play with it.
Things to try:
* Add a [new content file](/content/organization/)
* Create a [new section](/content/sections/)
* Modify [a template](/layout/templates/)
* Create content with [toml front matter](/content/front-matter/)
* Define your own field in [front matter](/content/front-matter/)
* Display that [field in the template](/layout/variables/)
* Create a [new content type](/content/types/)
@@ -1,6 +1,9 @@
---
title: "Source Directory Organization"
Pubdate: "2013-07-01"
date: "2013-07-01"
aliases: ["/doc/source-directory/"]
groups: ['gettingStarted']
groups_weight: 50
---
Hugo takes a single directory and uses it as the input for creating a complete website.
+73
View File
@@ -0,0 +1,73 @@
---
title: "Using Hugo"
date: "2013-07-01"
aliases: ["/doc/usage/"]
groups: ['gettingStarted']
groups_weight: 30
---
Make sure either hugo is in your path or provide a path to it.
$ hugo help
A Fast and Flexible Static Site Generator
built with love by spf13 and friends in Go.
Complete documentation is available at http://hugo.spf13.com
Usage:
hugo [flags]
hugo [command]
Available Commands:
server :: Hugo runs it's own a webserver to render the files
version :: Print the version number of Hugo
check :: Check content in the source directory
benchmark :: Benchmark hugo by building a site a number of times
help [command] :: Help about any command
Available Flags:
-b, --base-url="": hostname (and path) to the root eg. http://spf13.com/
-D, --build-drafts=false: include content marked as draft
--config="": config file (default is path/config.yaml|json|toml)
-d, --destination="": filesystem path to write files to
-s, --source="": filesystem path to read files relative from
--uglyurls=false: if true, use /filename.html instead of /filename/
-v, --verbose=false: verbose output
-w, --watch=false: watch filesystem for changes and recreate as needed
Use "hugo help [command]" for more information about that command.
## 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 indexes created
in 8 ms
If you are working on things and want to see the changes
immediately, tell Hugo to watch for changes. **It will
recreate the site faster than you can tab over to
your browser to view the changes.**
$ hugo -s ~/mysite --watch
28 pages created
0 tags index created
in 38 ms
Watching for changes in /Users/spf13/Code/hugo/docs/content
Press ctrl+c to stop
Hugo can even run a server and create your site at the same time!
$ hugo server -ws ~/mysite
Watching for changes in /Users/spf13/Code/hugo/docs/content
Web Server is available at http://localhost:1313
Press ctrl+c to stop
28 pages created
0 tags created
in 28 ms
+10
View File
@@ -0,0 +1,10 @@
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-7131036-4', 'spf13.com');
ga('send', 'pageview');
</script>
+7 -1
View File
@@ -1,12 +1,18 @@
</div>
</div>
</div>
<div class="container">
<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>
</div>
<script src="/static/js/jquery.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
{{ template "chrome/analytics.html" . }}
</body>
</html>
+6 -11
View File
@@ -7,14 +7,9 @@
{{ 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">
{{ template "chrome/menu.html" . }}
<div class="container">
<div class="row">
<div class="col-md-12">
+3 -2
View File
@@ -1,2 +1,3 @@
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-responsive.css">
<link href="/static/css/bootstrap.min.css" rel="stylesheet"/>
<link href="/static/css/styles.css" rel="stylesheet"/>
<link href="/static/css/hugoicon.css" rel="stylesheet"/>
+70 -29
View File
@@ -1,29 +1,70 @@
<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">Installing Hugo</a></li>
<li> <a href="/doc/usage">Usage</a> </li>
<li> <a href="/doc/configuration">Configuration</a></li>
<li> <a href="/doc/source-directory">Input Directory Layout</a></li>
<li class="divider"></li>
<li class="nav-header">Layout</li>
<li> <a href="/doc/templates">Templates</a></li>
<li> <a href="/doc/variables">Variables</a></li>
<li class="divider"></li>
<li class="nav-header">Content</li>
<li> <a href="/doc/organization">Organization</a></li>
<li> <a href="/doc/front-matter">Front Matter</a></li>
<li> <a href="/doc/example">Example</a></li>
<li class="divider"></li>
<li class="nav-header">Extras</li>
<li> <a href="/doc/shortcodes">ShortCodes</a></li>
<li> <a href="/doc/indexes">Indexes</a></li>
<li class="divider"></li>
<li class="nav-header">Meta</li>
<li> <a href="/doc/release-notes">Release Notes</a></li>
<li> <a href="/doc/roadmap">Roadmap</a> </li>
<li> <a href="/doc/contributing">Contributing</a></li>
<li> <a href="/doc/contributors">Contributors</a></li>
<li> <a href="/doc/license">License</a></li>
</ul>
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/"><i class="icon-rocket "></i>&nbsp; Hugo</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Getting Started <b class="caret"></b></a>
<ul class="dropdown-menu">
{{ range $key, $value := .Site.Indexes.groups.gettingstarted.Pages }}
<li hugo-nav="{{$value.RelPermalink}}"><a href="{{$value.Permalink}}"> {{ $value.LinkTitle }} </a> </li>
{{ end }}
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Content <b class="caret"></b></a>
<ul class="dropdown-menu">
{{ range $key, $value := .Site.Indexes.groups.content.Pages }}
<li hugo-nav="{{$value.RelPermalink}}"><a href="{{$value.Permalink}}"> {{ $value.LinkTitle }} </a> </li>
{{ end }}
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Layout <b class="caret"></b></a>
<ul class="dropdown-menu">
{{ range $key, $value := .Site.Indexes.groups.layout.Pages }}
<li hugo-nav="{{$value.RelPermalink}}"><a href="{{$value.Permalink}}"> {{ $value.LinkTitle }} </a> </li>
{{ end }}
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Indexes <b class="caret"></b></a>
<ul class="dropdown-menu">
{{ range $key, $value := .Site.Indexes.groups.indexes.Pages }}
<li hugo-nav="{{$value.RelPermalink}}"><a href="{{$value.Permalink}}"> {{ $value.LinkTitle }} </a> </li>
{{ end }}
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Extras <b class="caret"></b></a>
<ul class="dropdown-menu">
{{ range $key, $value := .Site.Indexes.groups.extras.Pages }}
<li hugo-nav="{{$value.RelPermalink}}"><a href="{{$value.Permalink}}"> {{ $value.LinkTitle }} </a> </li>
{{ end }}
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Meta <b class="caret"></b></a>
<ul class="dropdown-menu">
{{ range $key, $value := .Site.Indexes.groups.meta.Pages }}
<li hugo-nav="{{$value.RelPermalink}}"><a href="{{$value.Permalink}}"> {{ $value.LinkTitle }} </a> </li>
{{ end }}
</ul>
</li>
<li><a href="http://github.com/spf13/hugo/releases"><i class="icon-arrow-down "> </i></a></li>
<li><a href="http://github.com/spf13/hugo"><i class="icon-octocat icon-2x"> </i></a></li>
</ul>
</div><!-- /.navbar-collapse -->
</nav>
Regular → Executable
+216 -54
View File
@@ -1,54 +1,216 @@
<!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>
and <a href="http://github.com/spf13/hugo/graphs/contributors">friends</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 most
other static site generators.
A typical website of moderate size can be
rendered in a fraction of a second. A good rule of thumb is that Hugo
takes around 1 millisecond for each piece of content.<br>
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.
It is written to work well with any
kind of website including blogs, tumbles and docs.</p>
</div>
<div class="span4">
<h2>Fun</h2>
<p>Hugo runs everywhere. Sites generated with Hugo work on every web
server without any special configuration. Hugo
removes all the cruft of building a site allowing you to
focus on writing great content.</p>
</div>
</div>
{{ template "chrome/footer.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<title>Hugo :: A fast and flexible static site generator</title>
<!-- Bootstrap core CSS -->
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<!-- Add custom CSS here -->
<link href="/static/css/HPstyles.css" rel="stylesheet">
<link href="/static/css/hugoicon.css" rel="stylesheet">
</head>
<body>
<!-- Full Page Image Header Area -->
<div id="top" class="header">
<div class="vert-text">
<h1>Hugo</h1>
<h3>The <em>Fast and Flexible</em> Static Site Generator</h3>
<br>
<a href="#intro" class="btn btn-clear btn-lg">LEARN MORE</a>
<a href="https://github.com/spf13/hugo/releases" class="btn btn-success btn-lg">DOWNLOAD <i class="icon-arrow-down"></i></a><br>
<a href="/overview/quickstart/" class="btn btn-info btn-lg">DOCUMENTATION</a>
</div>
</div>
<!-- /Full Page Image Header Area -->
<div id="main">
<!-- Intro -->
<div id="intro" class="counterpoint">
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center">
<i class="lead-icon icon-pacman"></i>
<h2>Make the Web Fun Again</h2>
<p class="lead">
Introducing Hugo, a new idea around making website creation simple again.
Hugo flexibly works with many formats and is ideal for
blogs, docs, portfolios and much more.
Hugo&#39;s speed fosters
creativity and makes building a website fun again.
</p>
</div>
</div>
</div>
</div>
<!-- /Intro -->
<!-- Point -->
<div id="intro" class="point">
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center">
<i class="point-icon icon-tux"></i>
<i class="point-icon icon-apple"></i>
<i class="point-icon icon-windows8"></i>
<h2>Run Anywhere</h2>
<p class="lead">
Hugo is written in pure go. It is packaged as a single binary for easy installation.
<b>Hugo doesn&#39;t depend on administrative privileges, databases, runtimes, interpreters
or packages</b>. Hugo is quite possibly the easiest to install software you&#39;ve ever used.
Simply download and run.
</p>
</div>
</div>
</div>
</div>
<!-- /Point -->
<!-- Point -->
<div class="counterpoint">
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center">
<i class="lead-icon icon-rocket"></i>
<h2>Fast & Powerful</h2>
<p class="lead">
Hugo is written for speed and performance. Great care has been
taken to ensure that Hugo build time is as short as possible.
While many factors contribute (cpu, disk speed, # of indexes)
the vast majority of websites render in less than a second.
</p>
</div>
</div>
</div>
</div>
<!-- /Point -->
<!-- Point -->
<div class="point">
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center">
<i class="point-icon icon-fork"></i>
<h2>Flexible</h2>
<p class="lead">
Hugo is designed to work how you do. Declare your own content
types. Define your own meta data in YAML, TOML or JSON.
Organize your content however you want with any URL structure.
Use indexes to group your content however you want. Best of all
this is all done with virtually no configuration, Hugo
just works.
</p>
</div>
</div>
</div>
</div>
<!-- /Point -->
<!-- Point -->
<div class="counterpoint">
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center">
<i class="lead-icon icon-octocat"></i>
<h2>Open and Free</h2>
<p class="lead">
Hugo is <a href="http://github.com/spf13/hugo">open source</a> and completely free.
</p>
</div>
</div>
</div>
</div>
<!-- /Point -->
<!-- Point -->
<div class="point">
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center">
<i class="point-icon icon-beaker-alt"></i>
<i class="point-icon icon-heart"></i>
<i class="point-icon icon-spf"></i>
<h2>Built with Love</h2>
<p class="lead">
Hugo is developed with love by <a href="http://spf13.com">spf13</a>, <a href="http://github.com/noahcampbell">Noah</a> and friends.
We welcome all contributions.
New to go? Not a problem, we will help you.
Not a developer? Help with <a href="/overview/quickstart">docs</a>, templates and themes.
</p>
</div>
</div>
</div>
</div>
<!-- /Point -->
</div>
<!-- Call to Action -->
<div id="action" class="call-to-action">
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center">
<h3>Get Started Today!</h3>
<a href="https://github.com/spf13/hugo/releases" class="btn btn-lg btn-primary">Download <i class="icon-arrow-down"></i></a>
<a href="/overview/quickstart" style="color:white;font-weight:300;">Documentation</a>
</div>
</div>
</div>
</div>
<!-- /Call to Action -->
<div id="footerSpacer"></div>
<!-- Footer -->
<footer id="footer">
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center">
<ul class="list-inline">
<li><a href="http://twitter.com/spf13" class="icon-twitter icon-2x"></a></li>
<li><a href="http://github.com/spf13/hugo" class="icon-octocat icon-2x"></a></li>
</ul>
<hr>
<p>Copyright &copy; Steve Francia 2013</p>
</div>
</div>
</div>
</footer>
<!-- /Footer -->
<!-- Bootstrap core JavaScript -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="/static/js/jquery.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<script>
$(function() {
$('a[href*=#]:not([href=#])').click(function() {
if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'')
|| location.hostname == this.hostname) {
var target = $(this.hash);
target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
if (target.length) {
$('html,body').animate({
scrollTop: target.offset().top
}, 1000);
return false;
}
}
});
});
</script>
{{ template "chrome/analytics.html" . }}
</body>
</html>
+279
View File
@@ -0,0 +1,279 @@
/* Global Styles */
html,
body {
height: 100%;
width: 100%;
letter-spacing: 0.5px;
}
.vert-text {
display: table-cell;
vertical-align: middle;
text-align: center;
}
.vert-text h1 {
padding: 0;
margin: 0;
font-size: 4.5em;
font-weight: 700;
}
/* Full Page Image Header Area */
.header {
display: table;
height: 70%;
width: 100%;
min-height: 70%;
position: relative;
background: url(../img/converse-fields.jpg) no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
z-index: 99999;
background-color: black;
}
@media only screen
and (min-device-width : 320px)
and (max-device-width : 1024px)
and (orientation : portrait)
{
.header {
display: table;
height: 70%;
width: 100%;
position: relative;
background: url(../img/converse-fields-mini.jpg) no-repeat;
background-size: 100% 100%;
z-index: 99999;
background-color: black;
}
}
@media only screen
and (min-device-width : 320px)
and (max-device-width : 1024px)
and (orientation : landscape) {
.header {
display: table;
height: 70%;
width: 100%;
position: relative;
background: url(../img/converse-fields-long.jpg) no-repeat;
background-size: 100% 100%;
z-index: 99999;
background-color: black;
}
}
.header .btn {
margin: 10px;
}
#main {
z-index: 99999;
position: relative;
background: rgb(255, 255, 255);
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
.header h4, .header h3, .header h2, .header h1 {
color: white;
text-shadow: black 1px 1px 3px;
}
.header h4 {
text-shadow: black 1px 3px 3px;
font-weight: 100;
}
.header h2 {
font-weight: 200;
}
.header h1 {
font-weight: 500;
}
.header .btn {
font-weight: 100;
}
/* Intro */
.counterpoint {
padding: 50px 0;
background-color: white;
}
.point {
background: rgb(22, 203, 230);
padding: 50px 0;
color: #ffffff;
}
.point-item {
margin-bottom: 15px;
}
i.point-icon, i.lead-icon {
border-radius: 50%;
display: inline-block;
font-size: 56px;
width: 140px;
height: 140px;
line-height: 136px;
vertical-align: middle;
text-align: center;
-webkit-transition: box-shadow 0.2s;
-moz-transition: box-shadow 0.2s;
transition: box-shadow 0.2s;
}
i.lead-icon {
border: 3px solid #222;
}
i.point-icon {
border: 3px solid #ffffff;
}
i.lead-icon:hover {
color: #fff;
border: 3px solid black;
background: black;
}
i.point-icon:hover {
color: rgb(22, 203, 230);
border: 3px solid #fff;
background: #fff;
}
a:hover {
color: rgb(52, 73, 94);
}
i.service-icon:hover {
font-size: 68px;
}
.icon-2x {
font-size: 200%;
}
/* Callout */
.callout {
color: #ffffff;
display: table;
height: 400px;
width: 100%;
background: url(/static/img/section-two.JPG) no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
}
/* Portfolio */
.portfolio {
padding: 50px 0;
}
.portfolio-item {
margin-bottom: 25px;
}
.img-portfolio {
margin: 0 auto;
}
/* Call to Action */
.call-to-action {
color: #ffffff;
background-color: rgba(255, 255, 255, 0.19);
padding: 50px 0;
}
#action {
background: url(/static/img/gray.png);
padding: 50px 0px 30px;
position: fixed;
bottom: 130px;
z-index: 99998;
width: 100%;
}
#footerSpacer {
display: block;
min-height: 310px;
background-color: rgba(0, 0, 0, 0);
}
#footer {
bottom: 0px;
left: 0px;
padding: 10px 0px;
position: fixed;
width: 100%;
font-size: 14px;
z-index: 99998;
background: rgb(255, 255, 255);
}
.call-to-action .btn {
margin: 10px;
}
/* Footer */
footer {
padding: 100px 0;
}
footer a {
color: black;
}
footer a:hover {
text-decoration: none;
}
div.point a {
color: #0a5175;
}
/* Bootstrap Addons */
.btn-clear {
color: #444;
background-color: rgba(255, 255, 255, 0.5);
border-color: #FFF;
}
.btn-clear:hover,
.btn-clear:focus,
.btn-clear:active,
.btn-clear.active,
.open .dropdown-toggle.btn-clear {
color: #333333;
background-color: #ebebeb;
border-color: #adadad;
}
.btn-clear:active,
.btn-clear.active,
.open .dropdown-toggle.btn-clear {
background-image: none;
}
File diff suppressed because it is too large Load Diff
Vendored Regular → Executable
+840 -4
View File
File diff suppressed because one or more lines are too long
+208
View File
@@ -0,0 +1,208 @@
@font-face {
font-family: 'hugo-1';
src:url('../fonts/hugo-1.eot');
src:url('../fonts/hugo-1.eot?#iefix') format('embedded-opentype'),
url('../fonts/hugo-1.woff') format('woff'),
url('../fonts/hugo-1.ttf') format('truetype'),
url('../fonts/hugo-1.svg#hugo-1') format('svg');
font-weight: normal;
font-style: normal;
}
/* Use the following CSS code if you want to use data attributes for inserting your icons */
[data-icon]:before {
font-family: 'hugo-1';
content: attr(data-icon);
speak: none;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Use the following CSS code if you want to have a class per icon */
/*
Instead of a list of all class selectors,
you can use the generic selector below, but it's slower:
[class*="icon-"] {
*/
.icon-home, .icon-html5, .icon-css3, .icon-console, .icon-link, .icon-airplane, .icon-fire, .icon-check-alt, .icon-x-altx-alt, .icon-circlestar, .icon-file-css, .icon-spf, .icon-radio-checked, .icon-embed, .icon-code, .icon-cc, .icon-cc-by, .icon-cc-nc, .icon-beaker-alt, .icon-untitled, .icon-untitled-2, .icon-untitled-3, .icon-untitled-4, .icon-tag, .icon-rocket, .icon-hugo5, .icon-octocat, .icon-untitled-5, .icon-untitled-6, .icon-untitled-7, .icon-airplane-2, .icon-heart, .icon-rocket-2, .icon-house, .icon-bolt, .icon-flow-tree, .icon-twitter, .icon-beaker, .icon-images, .icon-bubbles, .icon-meter2, .icon-power-cord, .icon-apple, .icon-windows8, .icon-tux, .icon-file-xml, .icon-fork, .icon-arrow-down, .icon-untitled-8, .icon-untitled-9, .icon-pacman, .icon-arrow-right, .icon-arrow-left, .icon-arrow-down-2, .icon-flow-branch, .icon-pen {
font-family: 'hugo-1';
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-home:before {
content: "\21";
}
.icon-html5:before {
content: "\23";
}
.icon-css3:before {
content: "\24";
}
.icon-console:before {
content: "\25";
}
.icon-link:before {
content: "\26";
}
.icon-airplane:before {
content: "\27";
}
.icon-fire:before {
content: "\28";
}
.icon-check-alt:before {
content: "\29";
}
.icon-x-altx-alt:before {
content: "\2a";
}
.icon-circlestar:before {
content: "\2b";
}
.icon-file-css:before {
content: "\2c";
}
.icon-spf:before {
content: "\2d";
}
.icon-radio-checked:before {
content: "\2e";
}
.icon-embed:before {
content: "\2f";
}
.icon-code:before {
content: "\30";
}
.icon-cc:before {
content: "\31";
}
.icon-cc-by:before {
content: "\32";
}
.icon-cc-nc:before {
content: "\33";
}
.icon-beaker-alt:before {
content: "\39";
}
.icon-untitled:before {
content: "\3a";
}
.icon-untitled-2:before {
content: "\3b";
}
.icon-untitled-3:before {
content: "\3c";
}
.icon-untitled-4:before {
content: "\3d";
}
.icon-tag:before {
content: "\3e";
}
.icon-rocket:before {
content: "\3f";
}
.icon-hugo5:before {
content: "\40";
}
.icon-octocat:before {
content: "\41";
}
.icon-untitled-5:before {
content: "\42";
}
.icon-untitled-6:before {
content: "\43";
}
.icon-untitled-7:before {
content: "\44";
}
.icon-airplane-2:before {
content: "\45";
}
.icon-heart:before {
content: "\46";
}
.icon-rocket-2:before {
content: "\47";
}
.icon-house:before {
content: "\48";
}
.icon-bolt:before {
content: "\49";
}
.icon-flow-tree:before {
content: "\4a";
}
.icon-twitter:before {
content: "\4b";
}
.icon-beaker:before {
content: "\4c";
}
.icon-images:before {
content: "\4d";
}
.icon-bubbles:before {
content: "\4e";
}
.icon-meter2:before {
content: "\4f";
}
.icon-power-cord:before {
content: "\50";
}
.icon-apple:before {
content: "\51";
}
.icon-windows8:before {
content: "\52";
}
.icon-tux:before {
content: "\53";
}
.icon-file-xml:before {
content: "\54";
}
.icon-fork:before {
content: "\55";
}
.icon-arrow-down:before {
content: "\56";
}
.icon-untitled-8:before {
content: "\e035";
}
.icon-untitled-9:before {
content: "\e034";
}
.icon-pacman:before {
content: "\e000";
}
.icon-arrow-right:before {
content: "\e001";
}
.icon-arrow-left:before {
content: "\e002";
}
.icon-arrow-down-2:before {
content: "\e003";
}
.icon-flow-branch:before {
content: "\e004";
}
.icon-pen:before {
content: "\e005";
}
+77
View File
@@ -0,0 +1,77 @@
body {
background-color: rgb(253, 253, 251);
padding-top: 70px;
letter-spacing: 0.3px;
}
h1, h2, h3 {
color: rgb(25,71,86);
}
a {
color: rgb(0,140,158);
}
a:hover {
color: rgb(21,201,239);
}
#footer {
margin-bottom: 2em;
}
nav.navbar {
background: none repeat scroll 0% 0% rgb(22, 203, 230);
border-top: 4px solid rgb(15, 166, 188);
border-bottom: 1px solid rgb(15, 166, 188);
opacity: .97;
}
.navbar-nav a > i {
font-size: 140%;
line-height: 49%;
vertical-align: -4px;
}
.navbar-inverse .navbar-nav > li > a, .navbar-inverse .navbar-brand {
color: white !important;
}
.navbar-inverse .navbar-nav > .dropdown > a .caret {
border-top-color: white;
border-bottom-color: white;
}
.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus {
text-decoration: none;
color: rgb(255, 255, 255);
background-color: rgb(15, 166, 188);
}
.navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus {
background-color: rgb(15, 166, 188);
}
.dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus {
color: rgb(255, 255, 255);
text-decoration: none;
outline: 0px none;
background-color: rgb(15, 166, 188);
}
.navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus {
background-color: rgb(15, 166, 188);
}
.navbar-inverse .navbar-toggle {
border-color: white;
}
@media (max-width: 767px) {
.navbar-inverse .navbar-nav .open .dropdown-menu>li>a {
color: white !important;
}
.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a, .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus {
background-color: rgb(15, 166, 188);
}
}
Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+11
View File
File diff suppressed because one or more lines are too long
+5
View File
File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
package hugolib
import (
"bytes"
"os"
"testing"
)
func BenchmarkParsePage(b *testing.B) {
f, _ := os.Open("redis.cn.md")
sample := new(bytes.Buffer)
sample.ReadFrom(f)
b.ResetTimer()
for i := 0; i < b.N; i++ {
p, _ := ReadFrom(bytes.NewReader(sample.Bytes()), "bench")
p = p
}
}
+7 -5
View File
@@ -33,6 +33,7 @@ type Config struct {
Title string
Indexes map[string]string // singular, plural
ProcessFilters map[string][]string
Params map[string]interface{}
BuildDrafts, UglyUrls, Verbose bool
}
@@ -103,7 +104,7 @@ func (c *Config) readInConfig() {
func (c *Config) setPath(p string) {
if p == "" {
path, err := FindPath()
path, err := findPath()
if err != nil {
fmt.Printf("Error finding path: %s", err)
}
@@ -124,7 +125,7 @@ func (c *Config) GetPath() string {
return c.Path
}
func FindPath() (string, error) {
func findPath() (string, error) {
serverFile, err := filepath.Abs(os.Args[0])
if err != nil {
@@ -147,13 +148,14 @@ func FindPath() (string, error) {
return path, nil
}
// GetAbsPath return the absolute path for a given path with the internal slashes
// properly converted.
func (c *Config) GetAbsPath(name string) string {
if path.IsAbs(name) {
if filepath.IsAbs(name) {
return name
}
p := filepath.Join(c.GetPath(), name)
return p
return filepath.ToSlash(filepath.Join(c.GetPath(), name))
}
func (c *Config) findConfigFile(configFileName string) (string, error) {
-314
View File
@@ -1,314 +0,0 @@
// 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"
"html/template"
"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 YAML 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 YAML 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) error {
return os.MkdirAll(path, 0777)
}
func Urlize(url string) string {
return Sanitize(strings.ToLower(strings.Replace(strings.TrimSpace(url), " ", "-", -1)))
}
func AbsUrl(url string, base string) template.HTML {
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
return template.HTML(url)
}
return template.HTML(MakePermalink(base, url))
}
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, "/")
}
+103 -27
View File
@@ -14,47 +14,123 @@
package hugolib
import (
"github.com/spf13/hugo/template"
"sort"
)
type IndexCount struct {
Name string
Count int
type WeightedIndexEntry struct {
Weight int
Page *Page
}
type Index map[string]Pages
type IndexList map[string]Index
type IndexedPages []WeightedIndexEntry
type OrderedIndex []IndexCount
type OrderedIndexList map[string]OrderedIndex
func (p IndexedPages) Len() int { return len(p) }
func (p IndexedPages) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p IndexedPages) Sort() { sort.Sort(p) }
func (p IndexedPages) Count() int { return len(p) }
func (p IndexedPages) Less(i, j int) bool {
if p[i].Weight == p[j].Weight {
return p[i].Page.Date.Unix() > p[j].Page.Date.Unix()
} else {
return p[i].Weight < p[j].Weight
}
}
func (ip IndexedPages) Pages() Pages {
pages := make(Pages, len(ip))
for i, _ := range ip {
pages[i] = ip[i].Page
}
return pages
}
type Index map[string]IndexedPages
type IndexList map[string]Index
// KeyPrep... Indexes should be case insensitive. Can make it easily conditional later.
func kp(in string) string {
return Urlize(in)
return template.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) {
func (i Index) Get(key string) IndexedPages { return i[kp(key)] }
func (i Index) Count(key string) int { return len(i[kp(key)]) }
func (i Index) Add(key string, w WeightedIndexEntry) {
key = kp(key)
i[key] = append(i[key], p)
i[key] = append(i[key], w)
}
func (l IndexList) BuildOrderedIndexList() OrderedIndexList {
oil := make(OrderedIndexList, len(l))
for idx_name, index := range l {
i := 0
oi := make(OrderedIndex, len(index))
for name, pages := range index {
oi[i] = IndexCount{name, len(pages)}
i++
}
sort.Sort(oi)
oil[idx_name] = oi
func (i Index) IndexArray() IndexEntries {
ies := make([]IndexEntry, len(i))
count := 0
for k, v := range i {
ies[count] = IndexEntry{Name: k, WeightedPages: v}
count++
}
return oil
return ies
}
func (idx OrderedIndex) Len() int { return len(idx) }
func (idx OrderedIndex) Less(i, j int) bool { return idx[i].Count > idx[j].Count }
func (idx OrderedIndex) Swap(i, j int) { idx[i], idx[j] = idx[j], idx[i] }
func (i Index) Alphabetical() IndexEntries {
name := func(i1, i2 *IndexEntry) bool {
return i1.Name < i2.Name
}
ia := i.IndexArray()
By(name).Sort(ia)
return ia
}
func (i Index) ByCount() IndexEntries {
count := func(i1, i2 *IndexEntry) bool {
return len(i1.WeightedPages) > len(i2.WeightedPages)
}
ia := i.IndexArray()
By(count).Sort(ia)
return ia
}
type IndexEntry struct {
Name string
WeightedPages IndexedPages
}
func (ie IndexEntry) Pages() []*Page {
return ie.WeightedPages.Pages()
}
func (ie IndexEntry) Count() int {
return len(ie.WeightedPages)
}
type IndexEntries []IndexEntry
type By func(i1, i2 *IndexEntry) bool
func (by By) Sort(indexEntrys []IndexEntry) {
ps := &indexEntrySorter{
indexEntrys: indexEntrys,
by: by, // The Sort method's receiver is the function (closure) that defines the sort order.
}
sort.Sort(ps)
}
type indexEntrySorter struct {
indexEntrys []IndexEntry
by func(p1, p2 *IndexEntry) bool // Closure used in the Less method.
}
// Len is part of sort.Interface.
func (s *indexEntrySorter) Len() int {
return len(s.indexEntrys)
}
// Swap is part of sort.Interface.
func (s *indexEntrySorter) Swap(i, j int) {
s.indexEntrys[i], s.indexEntrys[j] = s.indexEntrys[j], s.indexEntrys[i]
}
// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter.
func (s *indexEntrySorter) Less(i, j int) bool {
return s.by(&s.indexEntrys[i], &s.indexEntrys[j])
}
+16
View File
@@ -0,0 +1,16 @@
package hugolib
import (
"strings"
"testing"
)
func TestSitePossibleIndexes(t *testing.T) {
site := new(Site)
page, _ := ReadFrom(strings.NewReader(PAGE_YAML_WITH_INDEXES_A), "path/to/page")
site.Pages = append(site.Pages, page)
indexes := site.possibleIndexes()
if !compareStringSlice(indexes, []string{"tags", "categories"}) {
t.Fatalf("possible indexes do not match [tags categories]. Got: %s", indexes)
}
}
+151
View File
@@ -0,0 +1,151 @@
package hugolib
import (
"errors"
"fmt"
"os"
"strconv"
"time"
)
func interfaceToTime(i interface{}) time.Time {
switch s := i.(type) {
case time.Time:
return s
case string:
d, e := stringToDate(s)
if e == nil {
return d
}
errorf("Invalid Time/Date format")
default:
errorf("Only Time is supported for this key")
}
return *new(time.Time)
}
func interfaceToStringToDate(i interface{}) time.Time {
s := interfaceToString(i)
if d, e := stringToDate(s); e == nil {
return d
}
return time.Unix(0, 0)
}
func stringToDate(s string) (time.Time, error) {
return parseDateWith(s, []string{
time.RFC3339,
time.RFC1123Z,
time.RFC1123,
time.RFC822Z,
time.RFC822,
time.ANSIC,
time.UnixDate,
time.RubyDate,
"2006-01-02 15:04:05Z07:00",
"02 Jan 06 15:04 MST",
"2006-01-02",
"02 Jan 2006",
})
}
// TODO remove this and return a proper error.
func errorf(str string, a ...interface{}) {
fmt.Fprintln(os.Stderr, str, a)
}
func parseDateWith(s string, dates []string) (d time.Time, e error) {
for _, dateType := range dates {
if d, e = time.Parse(dateType, s); e == nil {
return
}
}
return d, errors.New(fmt.Sprintf("Unable to parse date: %s", s))
}
func interfaceToBool(i interface{}) bool {
switch b := i.(type) {
case bool:
return b
default:
errorf("Only Boolean values are supported for this YAML 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 interfaceToFloat64(i interface{}) float64 {
switch s := i.(type) {
case float64:
return s
case float32:
return float64(s)
case string:
v, err := strconv.ParseFloat(s, 64)
if err == nil {
return float64(v)
} else {
errorf("Only Floats are supported for this key\nErr:", err)
}
default:
errorf("Only Floats are supported for this key")
}
return 0.0
}
func interfaceToInt(i interface{}) int {
switch s := i.(type) {
case int:
return s
case int64:
return int(s)
case int32:
return int(s)
case int16:
return int(s)
case int8:
return int(s)
case string:
v, err := strconv.ParseInt(s, 0, 0)
if err == nil {
return int(v)
} else {
errorf("Only Ints are supported for this key\nErr:", err)
}
default:
errorf("Only Ints are supported for this key")
}
return 0
}
func interfaceToString(i interface{}) string {
switch s := i.(type) {
case string:
return s
default:
errorf("Only Strings are supported for this key")
}
return ""
}
+9 -14
View File
@@ -19,25 +19,20 @@ import (
)
type Node struct {
Url string
Permalink template.HTML
RSSlink template.HTML
Site SiteInfo
layout string
RSSlink template.HTML
Site SiteInfo
// layout string
Data map[string]interface{}
Section string
Slug string
Title string
Description string
Keywords []string
Date time.Time
UrlPath
}
func (n *Node) GetSection() string {
s := ""
if n.Section != "" {
s = n.Section
}
return s
type UrlPath struct {
Url string
Permalink template.HTML
Slug string
Section string
}
+344 -212
View File
@@ -15,50 +15,55 @@ package hugolib
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/BurntSushi/toml"
"github.com/spf13/hugo/parser"
helper "github.com/spf13/hugo/template"
"github.com/spf13/hugo/template/bundle"
"github.com/theplant/blackfriday"
"html/template"
"io/ioutil"
"io"
"launchpad.net/goyaml"
"os"
"os/exec"
"path/filepath"
json "launchpad.net/rjson"
"net/url"
"path"
"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
Markup string
Status string
Images []string
Content template.HTML
Summary template.HTML
Truncated bool
plain string // TODO should be []byte
Params map[string]interface{}
contentType string
Draft bool
Aliases []string
Tmpl bundle.Template
Markup string
renderable bool
layout string
linkTitle string
PageMeta
File
Position
Node
}
const summaryLength = 70
type File struct {
FileName, OutFile, Extension string
FileName, Extension, Dir string
}
type PageMeta struct {
WordCount int
FuzzyWordCount int
MinRead int
Weight int
}
type Position struct {
@@ -68,33 +73,116 @@ type Position struct {
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] }
func (p Pages) Len() int { return len(p) }
func (p Pages) Less(i, j int) bool {
if p[i].Weight == p[j].Weight {
return p[i].Date.Unix() > p[j].Date.Unix()
} else {
return p[i].Weight > p[j].Weight
}
}
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.Markup = "md"
page.setSection()
return page
func (p Page) Plain() string {
if len(p.plain) == 0 {
p.plain = StripHTML(StripShortcodes(string(p.Content)))
}
return p.plain
}
func (p *Page) setSection() {
x := strings.Split(p.FileName, string(os.PathSeparator))
// nb: this is only called for recognised types; so while .html might work for
// creating posts, it results in missing summaries.
func getSummaryString(content []byte, pagefmt string) (summary []byte, truncates bool) {
if bytes.Contains(content, summaryDivider) {
// If user defines split:
// Split then render
truncates = true // by definition
summary = renderBytes(bytes.Split(content, summaryDivider)[0], pagefmt)
} else {
// If hugo defines split:
// render, strip html, then split
plain := strings.TrimSpace(StripHTML(StripShortcodes(string(renderBytes(content, pagefmt)))))
summary = []byte(TruncateWordsToWholeSentence(plain, summaryLength))
truncates = len(summary) != len(plain)
}
return
}
if section := x[len(x)-2]; section != "content" {
p.Section = section
func renderBytes(content []byte, pagefmt string) []byte {
switch pagefmt {
default:
return blackfriday.MarkdownCommon(content)
case "markdown":
return blackfriday.MarkdownCommon(content)
case "rst":
return []byte(getRstContent(content))
}
}
// TODO abstract further to support loading from more
// than just files on disk. Should load reader (file, []byte)
func newPage(filename string) *Page {
page := Page{contentType: "",
File: File{FileName: filename, Extension: "html"},
Node: Node{Keywords: make([]string, 10, 30)},
Params: make(map[string]interface{})}
page.Date, _ = time.Parse("20060102", "20080101")
page.guessSection()
return &page
}
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 (p *Page) IsRenderable() bool {
return p.renderable
}
func (p *Page) guessSection() {
if p.Section == "" {
x := strings.Split(p.FileName, "/")
x = x[:len(x)-1]
if len(x) == 0 {
return
}
if x[0] == "content" {
x = x[1:]
}
p.Section = path.Join(x...)
}
}
@@ -102,15 +190,19 @@ func (page *Page) Type() string {
if page.contentType != "" {
return page.contentType
}
if x := page.GetSection(); x != "" {
page.guessSection()
if x := page.Section; x != "" {
return x
}
return "page"
}
func (page *Page) Layout(l ...string) string {
func (page *Page) Layout(l ...string) []string {
if page.layout != "" {
return layouts(page.Type(), page.layout)
}
layout := ""
if len(l) == 0 {
layout = "single"
@@ -118,143 +210,121 @@ func (page *Page) Layout(l ...string) string {
layout = l[0]
}
if x := page.layout; x != "" {
return x
}
return strings.ToLower(page.Type()) + "/" + layout + ".html"
return layouts(page.Type(), layout)
}
// 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)
func layouts(types string, layout string) (layouts []string) {
t := strings.Split(types, "/")
for i := range t {
search := t[:len(t)-i]
layouts = append(layouts, fmt.Sprintf("%s/%s.html", strings.ToLower(path.Join(search...)), layout))
}
layouts = append(layouts, fmt.Sprintf("%s.html", layout))
return
}
func ReadFrom(buf io.Reader, name string) (page *Page, err error) {
if len(name) == 0 {
return nil, errors.New("Zero length page name")
}
p := newPage(name)
if err = p.parse(buf); err != nil {
return
}
p.analyzePage()
return &p
return p, nil
}
func (p *Page) analyzePage() {
p.WordCount = TotalWords(p.RawMarkdown)
p.WordCount = TotalWords(p.Plain())
p.FuzzyWordCount = int((p.WordCount+100)/100) * 100
p.MinRead = int((p.WordCount + 212) / 213)
}
// TODO //rewrite to use byte methods instead
func (page *Page) parseYamlMetaData(data []byte) ([]string, error) {
var err error
datum, lines := splitPageContent(data, "---", "---")
d, err := page.handleYamlMetaData([]byte(strings.Join(datum, "\n")))
if err != nil {
return lines, err
}
err = page.handleMetaData(d)
return lines, err
}
func (page *Page) parseTomlMetaData(data []byte) ([]string, error) {
var err error
datum, lines := splitPageContent(data, "+++", "+++")
d, err := page.handleTomlMetaData([]byte(strings.Join(datum, "\n")))
if err != nil {
return lines, err
}
err = page.handleMetaData(d)
return lines, err
}
func (page *Page) parseJsonMetaData(data []byte) ([]string, error) {
var err error
datum, lines := splitPageContent(data, "{", "}")
d, err := page.handleJsonMetaData([]byte(strings.Join(datum, "\n")))
if err != nil {
return lines, err
}
err = page.handleMetaData(d)
return lines, err
}
func splitPageContent(data []byte, start string, end string) ([]string, []string) {
lines := strings.Split(string(data), "\n")
datum := lines[0:]
var found = 0
if start != end {
for i, line := range lines {
if strings.HasPrefix(line, start) {
found += 1
}
if strings.HasPrefix(line, end) {
found -= 1
}
if found == 0 {
datum = lines[0 : i+1]
lines = lines[i+1:]
break
}
}
} else { // Start token & end token are the same
for i, line := range lines {
if found == 1 && strings.HasPrefix(line, end) {
datum = lines[1:i]
lines = lines[i+1:]
break
}
if found == 0 && strings.HasPrefix(line, start) {
found = 1
}
}
}
return datum, lines
}
func (p *Page) Permalink() template.HTML {
if len(strings.TrimSpace(p.Slug)) > 0 {
if p.Site.Config.UglyUrls {
return template.HTML(MakePermalink(string(p.Site.BaseUrl), strings.TrimSpace(p.Section)+"/"+p.Slug+"."+p.Extension))
func (p *Page) permalink() (*url.URL, error) {
baseUrl := string(p.Site.BaseUrl)
dir := strings.TrimSpace(p.Dir)
pSlug := strings.TrimSpace(p.Slug)
pUrl := strings.TrimSpace(p.Url)
var permalink string
if len(pSlug) > 0 {
if p.Site.Config != nil && p.Site.Config.UglyUrls {
permalink = path.Join(dir, p.Slug, p.Extension)
} else {
return template.HTML(MakePermalink(string(p.Site.BaseUrl), strings.TrimSpace(p.Section)+"/"+p.Slug))
permalink = dir + "/" + p.Slug + "/"
}
} else if len(strings.TrimSpace(p.Url)) > 2 {
return template.HTML(MakePermalink(string(p.Site.BaseUrl), strings.TrimSpace(p.Url)))
} else if len(pUrl) > 2 {
permalink = pUrl
} else {
_, t := filepath.Split(p.FileName)
if p.Site.Config.UglyUrls {
_, t := path.Split(p.FileName)
if p.Site.Config != nil && p.Site.Config.UglyUrls {
x := replaceExtension(strings.TrimSpace(t), p.Extension)
return template.HTML(MakePermalink(string(p.Site.BaseUrl), strings.TrimSpace(p.Section)+"/"+x))
permalink = path.Join(dir, x)
} else {
file, _ := fileExt(strings.TrimSpace(t))
return template.HTML(MakePermalink(string(p.Site.BaseUrl), strings.TrimSpace(p.Section)+"/"+file))
permalink = path.Join(dir, file)
}
}
base, err := url.Parse(baseUrl)
if err != nil {
return nil, err
}
path, err := url.Parse(permalink)
if err != nil {
return nil, err
}
return MakePermalink(base, path), nil
}
func (p *Page) LinkTitle() string {
if len(p.linkTitle) > 0 {
return p.linkTitle
} else {
return p.Title
}
}
func (p *Page) Permalink() (string, error) {
link, err := p.permalink()
if err != nil {
return "", err
}
return link.String(), nil
}
func (p *Page) RelPermalink() (string, error) {
link, err := p.permalink()
if err != nil {
return "", err
}
link.Scheme = ""
link.Host = ""
link.User = nil
link.Opaque = ""
return link.String(), nil
}
func (page *Page) handleTomlMetaData(datum []byte) (interface{}, error) {
m := map[string]interface{}{}
datum = removeTomlIdentifier(datum)
if _, err := toml.Decode(string(datum), &m); err != nil {
return m, fmt.Errorf("Invalid TOML in %s \nError parsing page meta data: %s", page.FileName, err)
}
return m, nil
}
func removeTomlIdentifier(datum []byte) []byte {
return bytes.Replace(datum, []byte("+++"), []byte(""), -1)
}
func (page *Page) handleYamlMetaData(datum []byte) (interface{}, error) {
m := map[string]interface{}{}
if err := goyaml.Unmarshal(datum, &m); err != nil {
@@ -271,42 +341,59 @@ func (page *Page) handleJsonMetaData(datum []byte) (interface{}, error) {
return f, nil
}
func (page *Page) handleMetaData(f interface{}) error {
func (page *Page) update(f interface{}) error {
m := f.(map[string]interface{})
for k, v := range m {
switch strings.ToLower(k) {
loki := strings.ToLower(k)
switch loki {
case "title":
page.Title = interfaceToString(v)
case "linktitle":
page.linkTitle = interfaceToString(v)
case "description":
page.Description = interfaceToString(v)
case "slug":
page.Slug = Urlize(interfaceToString(v))
page.Slug = helper.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))
page.Url = helper.Urlize(interfaceToString(v))
case "type":
page.contentType = interfaceToString(v)
case "keywords":
page.Keywords = interfaceArrayToStringArray(v)
case "date", "pubdate":
page.Date = interfaceToStringToDate(v)
page.Date = interfaceToTime(v)
case "draft":
page.Draft = interfaceToBool(v)
case "layout":
page.layout = interfaceToString(v)
case "markup":
page.Markup = interfaceToString(v)
case "weight":
page.Weight = interfaceToInt(v)
case "aliases":
page.Aliases = interfaceArrayToStringArray(v)
for _, alias := range page.Aliases {
if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") {
return fmt.Errorf("Only relative aliases are supported, %v provided", alias)
}
}
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
case string:
page.Params[loki] = vv
case int64, int32, int16, int8, int:
page.Params[loki] = vv
case float64, float32:
page.Params[loki] = vv
case time.Time:
page.Params[loki] = vv
default: // handle array of strings as well
switch vvv := vv.(type) {
case []interface{}:
@@ -314,7 +401,7 @@ func (page *Page) handleMetaData(f interface{}) error {
for i, u := range vvv {
a[i] = interfaceToString(u)
}
page.Params[strings.ToLower(k)] = a
page.Params[loki] = a
}
}
}
@@ -333,31 +420,38 @@ func (page *Page) GetParam(key string) interface{} {
switch v.(type) {
case string:
return interfaceToString(v)
case int64, int32, int16, int8, int:
return interfaceToInt(v)
case float64, float32:
return interfaceToFloat64(v)
case time.Time:
return interfaceToTime(v)
case []string:
return v
}
return nil
}
func (page *Page) Err(message string) {
fmt.Println(page.FileName + " : " + message)
type frontmatterType struct {
markstart, markend []byte
parse func([]byte) (interface{}, error)
includeMark bool
}
// 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 {
switch data[0] {
case '{':
return page.parseJsonMetaData(data)
case '-':
return page.parseYamlMetaData(data)
case '+':
return page.parseTomlMetaData(data)
}
const YAML_DELIM = "---"
const TOML_DELIM = "+++"
func (page *Page) detectFrontMatter(mark rune) (f *frontmatterType) {
switch mark {
case '-':
return &frontmatterType{[]byte(YAML_DELIM), []byte(YAML_DELIM), page.handleYamlMetaData, false}
case '+':
return &frontmatterType{[]byte(TOML_DELIM), []byte(TOML_DELIM), page.handleTomlMetaData, false}
case '{':
return &frontmatterType{[]byte{'{'}, []byte{'}'}, page.handleJsonMetaData, true}
default:
return nil
}
return nil, nil
}
func (p *Page) Render(layout ...string) template.HTML {
@@ -373,63 +467,101 @@ func (p *Page) Render(layout ...string) template.HTML {
func (p *Page) ExecuteTemplate(layout string) *bytes.Buffer {
l := p.Layout(layout)
buffer := new(bytes.Buffer)
p.Tmpl.ExecuteTemplate(buffer, l, p)
for _, layout := range l {
if p.Tmpl.Lookup(layout) != nil {
p.Tmpl.ExecuteTemplate(buffer, layout, p)
break
}
}
return buffer
}
func (page *Page) readFile() []byte {
var data, err = ioutil.ReadFile(page.FileName)
if err != nil {
PrintErr("Error Reading: " + page.FileName)
return nil
func (page *Page) guessMarkupType() string {
if page.Markup != "" {
return page.Markup
}
return data
if strings.HasSuffix(page.FileName, ".md") {
return "md"
}
return "unknown"
}
func (page *Page) buildPageFromFile() error {
data := page.readFile()
content, err := page.parseFileHeading(data)
func (page *Page) parse(reader io.Reader) error {
p, err := parser.ReadFrom(reader)
if err != nil {
return err
}
switch page.Markup {
case "md":
page.convertMarkdown(content)
page.renderable = p.IsRenderable()
front := p.FrontMatter()
if len(front) != 0 {
fm := page.detectFrontMatter(rune(front[0]))
meta, err := fm.parse(front)
if err != nil {
return err
}
if err = page.update(meta); err != nil {
return err
}
}
switch page.guessMarkupType() {
case "md", "markdown", "mdown":
page.convertMarkdown(bytes.NewReader(p.Content()))
case "rst":
page.convertRestructuredText(content)
page.convertRestructuredText(bytes.NewReader(p.Content()))
case "html":
fallthrough
default:
page.Content = template.HTML(p.Content())
}
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))
func (page *Page) convertMarkdown(lines io.Reader) {
b := new(bytes.Buffer)
b.ReadFrom(lines)
content := b.Bytes()
page.Content = template.HTML(string(blackfriday.MarkdownCommon(RemoveSummaryDivider(content))))
summary, truncated := getSummaryString(content, "markdown")
page.Summary = template.HTML(string(summary))
page.Truncated = truncated
}
func (page *Page) convertRestructuredText(lines []string) {
func (page *Page) convertRestructuredText(lines io.Reader) {
b := new(bytes.Buffer)
b.ReadFrom(lines)
content := b.Bytes()
page.Content = template.HTML(getRstContent(content))
summary, truncated := getSummaryString(content, "rst")
page.Summary = template.HTML(string(summary))
page.Truncated = truncated
}
page.RawMarkdown = strings.Join(lines, "\n")
func (p *Page) TargetPath() (outfile string) {
cmd := exec.Command("rst2html.py")
cmd.Stdin = strings.NewReader(page.RawMarkdown)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
fmt.Println(err)
}
// Always use Url if it's specified
if len(strings.TrimSpace(p.Url)) > 2 {
outfile = strings.TrimSpace(p.Url)
rstLines := strings.Split(out.String(), "\n")
for i, line := range rstLines {
if strings.HasPrefix(line, "<body>") {
rstLines = (rstLines[i+1 : len(rstLines)-3])
if strings.HasSuffix(outfile, "/") {
outfile = outfile + "index.html"
}
return
}
content := strings.Join(rstLines, "\n")
page.Content = template.HTML(content)
page.Summary = template.HTML(TruncateWordsToWholeSentence(StripHTML(StripShortcodes(content)), summaryLength))
if len(strings.TrimSpace(p.Slug)) > 0 {
outfile = strings.TrimSpace(p.Slug) + "." + p.Extension
} else {
// Fall back to filename
_, t := path.Split(p.FileName)
outfile = replaceExtension(strings.TrimSpace(t), p.Extension)
}
return path.Join(p.Dir, strings.TrimSpace(outfile))
}
+79
View File
@@ -0,0 +1,79 @@
package hugolib
import (
"strings"
"testing"
)
var PAGE_YAML_WITH_INDEXES_A = `---
tags: ['a', 'b', 'c']
categories: 'd'
---
YAML frontmatter with tags and categories index.`
var PAGE_YAML_WITH_INDEXES_B = `---
tags:
- "a"
- "b"
- "c"
categories: 'd'
---
YAML frontmatter with tags and categories index.`
var PAGE_JSON_WITH_INDEXES = `{
"categories": "d",
"tags": [
"a",
"b",
"c"
]
}
JSON Front Matter with tags and categories`
var PAGE_TOML_WITH_INDEXES = `+++
tags = [ "a", "b", "c" ]
categories = "d"
+++
TOML Front Matter with tags and categories`
func TestParseIndexes(t *testing.T) {
for _, test := range []string{PAGE_TOML_WITH_INDEXES,
PAGE_JSON_WITH_INDEXES,
PAGE_YAML_WITH_INDEXES_A,
PAGE_YAML_WITH_INDEXES_B,
} {
p, err := ReadFrom(strings.NewReader(test), "page/with/index")
if err != nil {
t.Fatalf("Failed parsing %q: %s", test, err)
}
param := p.GetParam("tags")
params := param.([]string)
expected := []string{"a", "b", "c"}
if !compareStringSlice(params, expected) {
t.Errorf("Expected %s: got: %s", expected, params)
}
param = p.GetParam("categories")
singleparam := param.(string)
if singleparam != "d" {
t.Fatalf("Expected: d, got: %s", singleparam)
}
}
}
func compareStringSlice(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if b[i] != v {
return false
}
}
return true
}
+47
View File
@@ -0,0 +1,47 @@
package hugolib
import (
"html/template"
"testing"
)
func TestPermalink(t *testing.T) {
tests := []struct {
base template.URL
expectedAbs string
expectedRel string
}{
{"", "/x/y/z/boofar", "/x/y/z/boofar"},
{"http://barnew/", "http://barnew/x/y/z/boofar", "/x/y/z/boofar"},
}
for _, test := range tests {
p := &Page{
Node: Node{
UrlPath: UrlPath{Section: "z"},
Site: SiteInfo{BaseUrl: test.base},
},
File: File{FileName: "x/y/z/boofar.md", Dir: "x/y/z"},
}
u, err := p.Permalink()
if err != nil {
t.Errorf("Unable to process permalink: %s", err)
}
expected := test.expectedAbs
if u != expected {
t.Errorf("Expected abs url: %s, got: %s", expected, u)
}
u, err = p.RelPermalink()
if err != nil {
t.Errorf("Unable to process permalink: %s", err)
}
expected = test.expectedRel
if u != expected {
t.Errorf("Expected abs url: %s, got: %s", expected, u)
}
}
}
+452
View File
@@ -0,0 +1,452 @@
package hugolib
import (
"html/template"
"path"
"strings"
"testing"
"time"
)
var EMPTY_PAGE = ""
const (
SIMPLE_PAGE = "---\ntitle: Simple\n---\nSimple Page\n"
INVALID_FRONT_MATTER_MISSING = "This is a test"
RENDER_NO_FRONT_MATTER = "<!doctype><html><head></head><body>This is a test</body></html>"
INVALID_FRONT_MATTER_SHORT_DELIM = `
--
title: Short delim start
---
Short Delim
`
INVALID_FRONT_MATTER_SHORT_DELIM_ENDING = `
---
title: Short delim ending
--
Short Delim
`
INVALID_FRONT_MATTER_LEADING_WS = `
---
title: Leading WS
---
Leading
`
SIMPLE_PAGE_JSON = `
{
"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" ],
"date": "2012-04-06",
"categories": [
"Development",
"VIM"
],
"slug": "spf13-vim-3-0-release-and-new-website"
}
Content of the file goes Here
`
SIMPLE_PAGE_JSON_LOOSE = `
{
"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" ]
"date": "2012-04-06"
"categories": [
"Development"
"VIM"
],
"slug": "spf13-vim-3-0-release-and-new-website"
}
Content of the file goes Here
`
SIMPLE_PAGE_RFC3339_DATE = "---\ntitle: RFC3339 Date\ndate: \"2013-05-17T16:59:30Z\"\n---\nrfc3339 content"
SIMPLE_PAGE_JSON_MULTIPLE = `
{
"title": "foobar",
"customData": { "foo": "bar" },
"date": "2012-08-06"
}
Some text
`
SIMPLE_PAGE_JSON_COMPACT = `
{"title":"foobar","customData":{"foo":"bar"},"date":"2012-08-06"}
Text
`
SIMPLE_PAGE_NOLAYOUT = `---
title: simple_no_layout
---
No Layout called out`
SIMPLE_PAGE_LAYOUT_FOOBAR = `---
title: simple layout foobar
layout: foobar
---
Layout foobar`
SIMPLE_PAGE_TYPE_FOOBAR = `---
type: foobar
---
type foobar`
SIMPLE_PAGE_TYPE_LAYOUT = `---
type: barfoo
layout: buzfoo
---
type and layout set`
SIMPLE_PAGE_WITH_SUMMARY_DELIMITER = `---
title: Simple
---
Summary Next Line
<!--more-->
Some more text
`
SIMPLE_PAGE_WITH_SHORTCODE_IN_SUMMARY = `---
title: Simple
---
Summary Next Line. {{% img src="/not/real" %}}.
More text here.
Some more text
`
SIMPLE_PAGE_WITH_SUMMARY_DELIMITER_SAME_LINE = `---
title: Simple
---
Summary Same Line<!--more-->
Some more text
`
SIMPLE_PAGE_WITH_LONG_CONTENT = `---
title: Simple
---
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
`
)
var PAGE_WITH_VARIOUS_FRONTMATTER_TYPES = `+++
a_string = "bar"
an_integer = 1
a_float = 1.3
a_date = 1979-05-27T07:32:00Z
+++
Front Matter with various frontmatter types`
func checkError(t *testing.T, err error, expected string) {
if err == nil {
t.Fatalf("err is nil. Expected: %s", expected)
}
if err.Error() != expected {
t.Errorf("err.Error() returned: '%s'. Expected: '%s'", err.Error(), expected)
}
}
func TestDegenerateEmptyPageZeroLengthName(t *testing.T) {
_, err := ReadFrom(strings.NewReader(EMPTY_PAGE), "")
if err == nil {
t.Fatalf("A zero length page name must return an error")
}
checkError(t, err, "Zero length page name")
}
func TestDegenerateEmptyPage(t *testing.T) {
_, err := ReadFrom(strings.NewReader(EMPTY_PAGE), "test")
if err == nil {
t.Fatalf("Expected ReadFrom to return an error when an empty buffer is passed.")
}
checkError(t, err, "EOF")
}
func checkPageTitle(t *testing.T, page *Page, title string) {
if page.Title != title {
t.Fatalf("Page title is: %s. Expected %s", page.Title, title)
}
}
func checkPageContent(t *testing.T, page *Page, content string) {
if page.Content != template.HTML(content) {
t.Fatalf("Page content mismatch\nexp: %q\ngot: %q", content, page.Content)
}
}
func checkPageSummary(t *testing.T, page *Page, summary string) {
if page.Summary != template.HTML(summary) {
t.Fatalf("Page summary is: `%s`. Expected `%s`", page.Summary, summary)
}
}
func checkPageType(t *testing.T, page *Page, pageType string) {
if page.Type() != pageType {
t.Fatalf("Page type is: %s. Expected: %s", page.Type(), pageType)
}
}
func checkPageLayout(t *testing.T, page *Page, layout ...string) {
if !listEqual(page.Layout(), layout) {
t.Fatalf("Page layout is: %s. Expected: %s", page.Layout(), layout)
}
}
func checkPageDate(t *testing.T, page *Page, time time.Time) {
if page.Date != time {
t.Fatalf("Page date is: %s. Expected: %s", page.Date, time)
}
}
func checkTruncation(t *testing.T, page *Page, shouldBe bool, msg string) {
if page.Summary == "" {
t.Fatal("page has no summary, can not check truncation")
}
if page.Truncated != shouldBe {
if shouldBe {
t.Fatalf("page wasn't truncated: %s", msg)
} else {
t.Fatalf("page was truncated: %s", msg)
}
}
}
func TestCreateNewPage(t *testing.T) {
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE), "simple.md")
if err != nil {
t.Fatalf("Unable to create a page with frontmatter and body content: %s", err)
}
checkPageTitle(t, p, "Simple")
checkPageContent(t, p, "<p>Simple Page</p>\n")
checkPageSummary(t, p, "Simple Page")
checkPageType(t, p, "page")
checkPageLayout(t, p, "page/single.html", "single.html")
checkTruncation(t, p, false, "simple short page")
}
func TestPageWithDelimiter(t *testing.T) {
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_WITH_SUMMARY_DELIMITER), "simple.md")
if err != nil {
t.Fatalf("Unable to create a page with frontmatter and body content: %s", err)
}
checkPageTitle(t, p, "Simple")
checkPageContent(t, p, "<p>Summary Next Line</p>\n\n<p>Some more text</p>\n")
checkPageSummary(t, p, "<p>Summary Next Line</p>\n")
checkPageType(t, p, "page")
checkPageLayout(t, p, "page/single.html", "single.html")
checkTruncation(t, p, true, "page with summary delimiter")
}
func TestPageWithShortCodeInSummary(t *testing.T) {
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_WITH_SHORTCODE_IN_SUMMARY), "simple.md")
if err != nil {
t.Fatalf("Unable to create a page with frontmatter and body content: %s", err)
}
checkPageTitle(t, p, "Simple")
checkPageContent(t, p, "<p>Summary Next Line. {{% img src=&ldquo;/not/real&rdquo; %}}.\nMore text here.</p>\n\n<p>Some more text</p>\n")
checkPageSummary(t, p, "Summary Next Line. . More text here. Some more text")
checkPageType(t, p, "page")
checkPageLayout(t, p, "page/single.html", "single.html")
}
func TestPageWithMoreTag(t *testing.T) {
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_WITH_SUMMARY_DELIMITER_SAME_LINE), "simple.md")
if err != nil {
t.Fatalf("Unable to create a page with frontmatter and body content: %s", err)
}
checkPageTitle(t, p, "Simple")
checkPageContent(t, p, "<p>Summary Same Line</p>\n\n<p>Some more text</p>\n")
checkPageSummary(t, p, "<p>Summary Same Line</p>\n")
checkPageType(t, p, "page")
checkPageLayout(t, p, "page/single.html", "single.html")
}
func TestPageWithDate(t *testing.T) {
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_RFC3339_DATE), "simple")
if err != nil {
t.Fatalf("Unable to create a page with frontmatter and body content: %s", err)
}
d, err := time.Parse(time.RFC3339, "2013-05-17T16:59:30Z")
if err != nil {
t.Fatalf("Unable to prase page.")
}
checkPageDate(t, p, d)
}
func TestWordCount(t *testing.T) {
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_WITH_LONG_CONTENT), "simple.md")
if err != nil {
t.Fatalf("Unable to create a page with frontmatter and body content: %s", err)
}
if p.WordCount != 483 {
t.Fatalf("incorrect word count. expected %v, got %v", 483, p.WordCount)
}
if p.FuzzyWordCount != 500 {
t.Fatalf("incorrect word count. expected %v, got %v", 500, p.WordCount)
}
if p.MinRead != 3 {
t.Fatalf("incorrect min read. expected %v, got %v", 3, p.MinRead)
}
checkTruncation(t, p, true, "long page")
}
func TestCreatePage(t *testing.T) {
var tests = []struct {
r string
}{
{SIMPLE_PAGE_JSON},
{SIMPLE_PAGE_JSON_LOOSE},
{SIMPLE_PAGE_JSON_MULTIPLE},
//{strings.NewReader(SIMPLE_PAGE_JSON_COMPACT)},
}
for _, test := range tests {
if _, err := ReadFrom(strings.NewReader(test.r), "page"); err != nil {
t.Errorf("Unable to parse page: %s", err)
}
}
}
func TestDegenerateInvalidFrontMatterShortDelim(t *testing.T) {
var tests = []struct {
r string
err string
}{
{INVALID_FRONT_MATTER_SHORT_DELIM_ENDING, "Unable to read frontmatter at filepos 45: EOF"},
}
for _, test := range tests {
_, err := ReadFrom(strings.NewReader(test.r), "invalid/front/matter/short/delim")
checkError(t, err, test.err)
}
}
func TestShouldRenderContent(t *testing.T) {
var tests = []struct {
text string
render bool
}{
{INVALID_FRONT_MATTER_MISSING, true},
// TODO how to deal with malformed frontmatter. In this case it'll be rendered as markdown.
{INVALID_FRONT_MATTER_SHORT_DELIM, true},
{RENDER_NO_FRONT_MATTER, false},
}
for _, test := range tests {
p := pageMust(ReadFrom(strings.NewReader(test.text), "render/front/matter"))
if p.IsRenderable() != test.render {
t.Errorf("expected p.IsRenderable() == %t, got %t", test.render, p.IsRenderable())
}
}
}
func TestDifferentFrontMatterVarTypes(t *testing.T) {
page, _ := ReadFrom(strings.NewReader(PAGE_WITH_VARIOUS_FRONTMATTER_TYPES), "test/file1.md")
dateval, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
if page.GetParam("a_string") != "bar" {
t.Errorf("frontmatter not handling strings correctly should be %s, got: %s", "bar", page.GetParam("a_string"))
}
if page.GetParam("an_integer") != 1 {
t.Errorf("frontmatter not handling ints correctly should be %s, got: %s", "1", page.GetParam("an_integer"))
}
if page.GetParam("a_float") != 1.3 {
t.Errorf("frontmatter not handling floats correctly should be %s, got: %s", 1.3, page.GetParam("a_float"))
}
if page.GetParam("a_date") != dateval {
t.Errorf("frontmatter not handling dates correctly should be %s, got: %s", dateval, page.GetParam("a_date"))
}
}
func TestDegenerateInvalidFrontMatterLeadingWhitespace(t *testing.T) {
_, err := ReadFrom(strings.NewReader(INVALID_FRONT_MATTER_LEADING_WS), "invalid/front/matter/leading/ws")
if err != nil {
t.Fatalf("Unable to parse front matter given leading whitespace: %s", err)
}
}
func TestSectionEvaluation(t *testing.T) {
page, _ := ReadFrom(strings.NewReader(SIMPLE_PAGE), "blue/file1.md")
if page.Section != "blue" {
t.Errorf("Section should be %s, got: %s", "blue", page.Section)
}
}
func L(s ...string) []string {
return s
}
func TestLayoutOverride(t *testing.T) {
var (
path_content_two_dir = path.Join("content", "dub", "sub", "file1.md")
path_content_one_dir = path.Join("content", "gub", "file1.md")
path_content_no_dir = path.Join("content", "file1")
path_one_directory = path.Join("fub", "file1.md")
path_no_directory = path.Join("file1.md")
)
tests := []struct {
content string
path string
expectedLayout []string
}{
{SIMPLE_PAGE_NOLAYOUT, path_content_two_dir, L("dub/sub/single.html", "dub/single.html", "single.html")},
{SIMPLE_PAGE_NOLAYOUT, path_content_one_dir, L("gub/single.html", "single.html")},
{SIMPLE_PAGE_NOLAYOUT, path_content_no_dir, L("page/single.html", "single.html")},
{SIMPLE_PAGE_NOLAYOUT, path_one_directory, L("fub/single.html", "single.html")},
{SIMPLE_PAGE_NOLAYOUT, path_no_directory, L("page/single.html", "single.html")},
{SIMPLE_PAGE_LAYOUT_FOOBAR, path_content_two_dir, L("dub/sub/foobar.html", "dub/foobar.html", "foobar.html")},
{SIMPLE_PAGE_LAYOUT_FOOBAR, path_content_one_dir, L("gub/foobar.html", "foobar.html")},
{SIMPLE_PAGE_LAYOUT_FOOBAR, path_one_directory, L("fub/foobar.html", "foobar.html")},
{SIMPLE_PAGE_LAYOUT_FOOBAR, path_no_directory, L("page/foobar.html", "foobar.html")},
{SIMPLE_PAGE_TYPE_FOOBAR, path_content_two_dir, L("foobar/single.html", "single.html")},
{SIMPLE_PAGE_TYPE_FOOBAR, path_content_one_dir, L("foobar/single.html", "single.html")},
{SIMPLE_PAGE_TYPE_FOOBAR, path_content_no_dir, L("foobar/single.html", "single.html")},
{SIMPLE_PAGE_TYPE_FOOBAR, path_one_directory, L("foobar/single.html", "single.html")},
{SIMPLE_PAGE_TYPE_FOOBAR, path_no_directory, L("foobar/single.html", "single.html")},
{SIMPLE_PAGE_TYPE_LAYOUT, path_content_two_dir, L("barfoo/buzfoo.html", "buzfoo.html")},
{SIMPLE_PAGE_TYPE_LAYOUT, path_content_one_dir, L("barfoo/buzfoo.html", "buzfoo.html")},
{SIMPLE_PAGE_TYPE_LAYOUT, path_content_no_dir, L("barfoo/buzfoo.html", "buzfoo.html")},
{SIMPLE_PAGE_TYPE_LAYOUT, path_one_directory, L("barfoo/buzfoo.html", "buzfoo.html")},
{SIMPLE_PAGE_TYPE_LAYOUT, path_no_directory, L("barfoo/buzfoo.html", "buzfoo.html")},
}
for _, test := range tests {
p, err := ReadFrom(strings.NewReader(test.content), test.path)
if err != nil {
t.Fatalf("Unable to parse content:\n%s\n", test.content)
}
if !listEqual(p.Layout(), test.expectedLayout) {
t.Errorf("Layout mismatch. Expected: %s, got: %s", test.expectedLayout, p.Layout())
}
}
}
func listEqual(left, right []string) bool {
if len(left) != len(right) {
return false
}
for i := range left {
if left[i] != right[i] {
return false
}
}
return true
}
+127
View File
@@ -0,0 +1,127 @@
package hugolib
import (
"fmt"
"os"
"strings"
"testing"
"time"
)
var PAGE_WITH_INVALID_DATE = `---
date: 2010-05-02_15:29:31+08:00
---
Page With Invalid Date (replace T with _ for RFC 3339)`
var PAGE_WITH_DATE_RFC3339 = `---
date: 2010-05-02T15:29:31+08:00
---
Page With Date RFC3339`
var PAGE_WITH_DATE_RFC3339_NO_T = `---
date: 2010-05-02 15:29:31+08:00
---
Page With Date RFC3339_NO_T`
var PAGE_WITH_DATE_RFC1123 = `---
date: Sun, 02 May 2010 15:29:31 PST
---
Page With Date RFC1123`
var PAGE_WITH_DATE_RFC1123Z = `---
date: Sun, 02 May 2010 15:29:31 +0800
---
Page With Date RFC1123Z`
var PAGE_WITH_DATE_RFC822 = `---
date: 02 May 10 15:29 PST
---
Page With Date RFC822`
var PAGE_WITH_DATE_RFC822Z = `---
date: 02 May 10 15:29 +0800
---
Page With Date RFC822Z`
var PAGE_WITH_DATE_ANSIC = `---
date: Sun May 2 15:29:31 2010
---
Page With Date ANSIC`
var PAGE_WITH_DATE_UnixDate = `---
date: Sun May 2 15:29:31 PST 2010
---
Page With Date UnixDate`
var PAGE_WITH_DATE_RubyDate = `---
date: Sun May 02 15:29:31 +0800 2010
---
Page With Date RubyDate`
var PAGE_WITH_DATE_HugoYearNumeric = `---
date: 2010-05-02
---
Page With Date HugoYearNumeric`
var PAGE_WITH_DATE_HugoYear = `---
date: 02 May 2010
---
Page With Date HugoYear`
var PAGE_WITH_DATE_HugoLong = `---
date: 02 May 2010 15:29 PST
---
Page With Date HugoLong`
func TestDegenerateDateFrontMatter(t *testing.T) {
p, _ := ReadFrom(strings.NewReader(PAGE_WITH_INVALID_DATE), "page/with/invalid/date")
if p.Date != *new(time.Time) {
t.Fatalf("Date should be set to time.Time zero value. Got: %s", p.Date)
}
}
func TestParsingDateInFrontMatter(t *testing.T) {
tests := []struct {
buf string
dt string
}{
{PAGE_WITH_DATE_RFC3339, "2010-05-02T15:29:31+08:00"},
{PAGE_WITH_DATE_RFC3339_NO_T, "2010-05-02T15:29:31+08:00"},
{PAGE_WITH_DATE_RFC1123Z, "2010-05-02T15:29:31+08:00"},
{PAGE_WITH_DATE_RFC822Z, "2010-05-02T15:29:00+08:00"},
{PAGE_WITH_DATE_ANSIC, "2010-05-02T15:29:31Z"},
{PAGE_WITH_DATE_RubyDate, "2010-05-02T15:29:31+08:00"},
{PAGE_WITH_DATE_HugoYearNumeric, "2010-05-02T00:00:00Z"},
{PAGE_WITH_DATE_HugoYear, "2010-05-02T00:00:00Z"},
}
tzShortCodeTests := []struct {
buf string
dt string
}{
{PAGE_WITH_DATE_RFC1123, "2010-05-02T15:29:31-08:00"},
{PAGE_WITH_DATE_RFC822, "2010-05-02T15:29:00-08:00Z"},
{PAGE_WITH_DATE_UnixDate, "2010-05-02T15:29:31-08:00"},
{PAGE_WITH_DATE_HugoLong, "2010-05-02T15:21:00+08:00"},
}
if _, err := time.LoadLocation("PST"); err == nil {
tests = append(tests, tzShortCodeTests...)
} else {
fmt.Fprintf(os.Stderr, "Skipping shortname timezone tests.\n")
}
for _, test := range tests {
dt, e := time.Parse(time.RFC3339, test.dt)
if e != nil {
t.Fatalf("Unable to parse date time (RFC3339) for running the test: %s", e)
}
p, err := ReadFrom(strings.NewReader(test.buf), "page/with/date")
if err != nil {
t.Fatalf("Expected to be able to parse page.")
}
if !dt.Equal(p.Date) {
t.Errorf("Date does not equal frontmatter:\n%s\nExpecting: %s\n Got: %s. Diff: %s\n internal: %#v\n %#v", test.buf, dt, p.Date, dt.Sub(p.Date), dt, p.Date)
}
}
}
+34
View File
@@ -0,0 +1,34 @@
package hugolib
import (
"os"
"strings"
)
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
}
// 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
}
+51
View File
@@ -0,0 +1,51 @@
package hugolib
import (
"path"
"strings"
"testing"
)
var SIMPLE_PAGE_YAML = `---
contenttype: ""
---
Sample Text
`
func TestDegenerateMissingFolderInPageFilename(t *testing.T) {
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_YAML), path.Join("foobar"))
if err != nil {
t.Fatalf("Error in ReadFrom")
}
if p.Section != "" {
t.Fatalf("No section should be set for a file path: foobar")
}
}
func TestNewPageWithFilePath(t *testing.T) {
toCheck := []struct {
input string
section string
layout []string
}{
{path.Join("sub", "foobar.html"), "sub", L("sub/single.html", "single.html")},
{path.Join("content", "foobar.html"), "", L("page/single.html", "single.html")},
{path.Join("content", "sub", "foobar.html"), "sub", L("sub/single.html", "single.html")},
{path.Join("content", "dub", "sub", "foobar.html"), "dub/sub", L("dub/sub/single.html", "dub/single.html", "single.html")},
}
for _, el := range toCheck {
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_YAML), el.input)
p.guessSection()
if err != nil {
t.Errorf("Reading from SIMPLE_PAGE_YAML resulted in an error: %s", err)
}
if p.Section != el.section {
t.Errorf("Section not set to %s for page %s. Got: %s", el.section, el.input, p.Section)
}
if !listEqual(p.Layout(), el.layout) {
t.Errorf("Layout incorrect. Expected: '%s', Got: '%s'", el.layout, p.Layout())
}
}
}
+52
View File
@@ -0,0 +1,52 @@
package hugolib
import (
"fmt"
"io"
)
func (s *Site) ShowPlan(out io.Writer) (err error) {
if s.Source == nil || len(s.Source.Files()) <= 0 {
fmt.Fprintf(out, "No source files provided.\n")
}
for _, p := range s.Pages {
fmt.Fprintf(out, "%s", p.FileName)
if p.IsRenderable() {
fmt.Fprintf(out, " (renderer: markdown)")
} else {
fmt.Fprintf(out, " (renderer: n/a)")
}
if s.Tmpl != nil {
for _, l := range p.Layout() {
fmt.Fprintf(out, " (layout: %s, exists: %t)", l, s.Tmpl.Lookup(l) != nil)
}
}
fmt.Fprintf(out, "\n")
fmt.Fprintf(out, " canonical => ")
if s.Target == nil {
fmt.Fprintf(out, "%s\n\n", "!no target specified!")
continue
}
trns, err := s.Target.Translate(p.TargetPath())
if err != nil {
return err
}
fmt.Fprintf(out, "%s\n", trns)
if s.Alias == nil {
continue
}
for _, alias := range p.Aliases {
aliasTrans, err := s.Alias.Translate(alias)
if err != nil {
return err
}
fmt.Fprintf(out, " %s => %s\n", alias, aliasTrans)
}
fmt.Fprintln(out)
}
return
}
+697
View File
@@ -0,0 +1,697 @@
---
title: The Little Redis Book cn
---
\thispagestyle{empty}
\changepage{}{}{}{-0.5cm}{}{2cm}{}{}{}
![The Little Redis Book cn, By Karl Seguin, Translate By Jason Lai](title.png)\
\clearpage
\changepage{}{}{}{0.5cm}{}{-2cm}{}{}{}
## 关于此书
### 许可证
《The Little Redis Book》是经由Attribution-NonCommercial 3.0 Unported license许可的,你不需要为此书付钱。
你可以自由地对此书进行复制,分发,修改或者展示等操作。当然,你必须知道且认可这本书的作者是Karl Seguin,译者是赖立维,而且不应该将此书用于商业用途。
关于这个**许可证**的*详细描述*在这里:
<http://creativecommons.org/licenses/by-nc/3.0/legalcode>
### 关于作者
作者Karl Seguin是一名在多项技术领域浸淫多年的开发者。他是开源软件计划的活跃贡献者,同时也是一名技术作者以及业余演讲者。他写过若干关于Radis的文章以及一些工具。在他的一个面向业余游戏开发者的免费服务里,Redis为其中的评级和统计功能提供了支持:[mogade.com](http://mogade.com/)。
Karl之前还写了[《The Little MongoDB Book》](http://openmymind.net/2011/3/28/The-Little-MongoDB-Book/),这是一本免费且受好评,关于MongoDB的书。
他的博客是<http://openmymind.net>,你也可以关注他的Twitter帐号,via [@karlseguin](http://twitter.com/karlseguin)。
### 关于译者
译者 赖立维 是一名长在天朝的普通程序员,对许多技术都有浓厚的兴趣,是开源软件的支持者,Emacs的轻度使用者。
虽然译者已经很认真地对待这次翻译,但是限于水平有限,肯定会有不少错漏,如果发现该书的翻译有什么需要修改,可以通过他的邮箱与他联系。他的邮箱是<jasonlai256@gmail.com>。
### 致谢
必须特别感谢[Perry Neal](https://twitter.com/perryneal)一直以来的指导,我的眼界、触觉以及激情都来源于你。你为我提供了无价的帮助,感谢你。
### 最新版本
此书的最新有效资源在:
<http://github.com/karlseguin/the-little-redis-book>
中文版是英文版的一个分支,最新的中文版本在:
<https://github.com/JasonLai256/the-little-redis-book>
\clearpage
## 简介
最近几年来,关于持久化和数据查询的相关技术,其需求已经增长到了让人惊讶的程度。可以断言,关系型数据库再也不是放之四海皆准。换一句话说,围绕数据的解决方案不可能再只有唯一一种。
对于我来说,在众多新出现的解决方案和工具里,最让人兴奋的,无疑是Redis。为什么?首先是因为其让人不可思议的容易学习,只需要简短的几个小时学习时间,就能对Redis有个大概的认识。还有,Redis在处理一组特定的问题集的同时能保持相当的通用性。更准确地说就是,Redis不会尝试去解决关于数据的所有事情。在你足够了解Redis后,事情就会变得越来越清晰,什么是可行的,什么是不应该由Redis来处理的。作为一名开发人员,如此的经验当是相当的美妙。
当你能仅使用Redis去构建一个完整系统时,我想大多数人将会发现,Redis能使得他们的许多数据方案变得更为通用,不论是一个传统的关系型数据库,一个面向文档的系统,或是其它更多的东西。这是一种用来实现某些特定特性的解决方法。就类似于一个索引引擎,你不会在Lucene上构建整个程序,但当你需要足够好的搜索,为什么不使用它呢?这对你和你的用户都有好处。当然,关于Redis和索引引擎之间相似性的讨论到此为止。
本书的目的是向读者传授掌握Redis所需要的基本知识。我们将会注重于学习Redis的5种数据结构,并研究各种数据建模方法。我们还会接触到一些主要的管理细节和调试技巧。
## 入门
每个人的学习方式都不一样,有的人喜欢亲自实践学习,有的喜欢观看教学视频,还有的喜欢通过阅读来学习。对于Redis,没有什么比亲自实践学习来得效果更好的了。Redis的安装非常简单。而且通过随之安装的一个简单的命令解析程序,就能处理我们想做的一切事情。让我们先花几分钟的时间把Redis安装到我们的机器上。
### Windows平台
Redis并没有官方支持Windows平台,但还是可供选择。你不会想在这里配置实际的生产环境,不过在我过往的开发经历里并没有感到有什么限制。
首先进入<https://github.com/dmajkic/redis/downloads>,然后下载最新的版本(应该会在列表的最上方)。
获取zip文件,然后根据你的系统架构,打开`64bit``32bit`文件夹。
### *nix和MacOSX平台
对于*nix和MacOSX平台的用户,从源文件来安装是你的最佳选择。通过最新的版本号来选择,有效地址于<http://redis.io/download>。在编写此书的时候,最新的版本是2.4.6,我们可以运行下面的命令来安装该版本:
wget http://redis.googlecode.com/files/redis-2.4.6.tar.gz
tar xzf redis-2.4.6.tar.gz
cd redis-2.4.6
make
(当然,Redis同样可以通过套件管理程序来安装。例如,使用Homebrew的MaxOSX用户可以只键入`brew install redis`即可。)
如果你是通过源文件来安装,二进制可执行文件会被放置在`src`目录里。通过运行`cd src`可跳转到`src`目录。
### 运行和连接Redis
如果一切都工作正常,那Redis的二进制文件应该已经可以曼妙地跳跃于你的指尖之下。Redis只有少量的可执行文件,我们将着重于Redis的服务器和命令行界面(一个类DOS的客户端)。首先,让我们来运行服务器。在Windows平台,双击`redis-server`,在*nix/MacOSX平台则运行`./redis-server`.
如果你仔细看了启动信息,你会看到一个警告,指没能找到`redis.conf`文件。Redis将会采用内置的默认设置,这对于我们将要做的已经足够了。
然后,通过双击`redis-cli`Windows平台)或者运行`./redis-cli`*nix/MacOSX平台),启动Redis的控制台。控制台将会通过默认的端口(6379)来连接本地运行的服务器。
可以在命令行界面键入`info`命令来查看一切是不是都运行正常。你会很乐意看到这么一大组关键字-值(key-value)对的显示,这为我们查看服务器的状态提供了大量有效信息。
如果在上面的启动步骤里遇到什么问题,我建议你到[Redis的官方支持组](https://groups.google.com/forum/#!forum/redis-db)里获取帮助。
## 驱动Redis
很快你就会发现,Redis的API就如一组定义明确的函数那般容易理解。Redis具有让人难以置信的简单性,其操作过程也同样如此。这意味着,无论你是使用命令行程序,或是使用你喜欢的语言来驱动,整体的感觉都不会相差多少。因此,相对于命令行程序,如果你更愿意通过一种编程语言去驱动Redis,你不会感觉到有任何适应的问题。如果真想如此,可以到Redis的[客户端推荐页面](http://redis.io/clients)下载适合的Redis载体。
\clearpage
## 第1章 - 基础知识
是什么使Redis显得这么特别?Redis具体能解决什么类型的问题?要实际应用Redis,开发者必须储备什么知识?在我们能回答这么一些问题之前,我们需要明白Redis到底是什么。
Redis通常被人们认为是一种持久化的存储器关键字-值型存储(in-memory persistent key-value store)。我认为这种对Redis的描述并不太准确。Redis的确是将所有的数据存放于存储器(更多是是按位存储),而且也确实通过将数据写入磁盘来实现持久化,但是Redis的实际意义比单纯的关键字-值型存储要来得深远。纠正脑海里的这种误解观点非常关键,否则你对于Redis之道以及其应用的洞察力就会变得越发狭义。
事实是,Redis引入了5种不同的数据结构,只有一个是典型的关键字-值型结构。理解Redis的关键就在于搞清楚这5种数据结构,其工作的原理都是如何,有什么关联方法以及你能怎样应用这些数据结构去构建模型。首先,让我们来弄明白这些数据结构的实际意义。
应用上面提及的数据结构概念到我们熟悉的关系型数据库里,我们可以认为其引入了一个单独的数据结构——表格。表格既复杂又灵活,基于表格的存储和管理,没有多少东西是你不能进行建模的。然而,这种通用性并不是没有缺点。具体来说就是,事情并不是总能达到假设中的简单或者快速。相对于这种普遍适用(one-size-fits-all)的结构体系,我们可以使用更为专门化的结构体系。当然,因此可能有些事情我们会完成不了(至少,达不到很好的程度)。但话说回来,这样做就能确定我们可以获得想象中的简单性和速度吗?
针对特定类型的问题使用特定的数据结构?我们不就是这样进行编程的吗?你不会使用一个散列表去存储每份数据,也不会使用一个标量变量去存储。对我来说,这正是Redis的做法。如果你需要处理标量、列表、散列或者集合,为什么不直接就用标量、列表、散列和集合去存储他们?为什么不是直接调用`exists(key)`去检测一个已存在的值,而是要调用其他比O(1)(常量时间查找,不会因为待处理元素的增长而变慢)慢的操作?
### 数据库(Databases
与你熟悉的关系型数据库一致,Redis有着相同的数据库基本概念,即一个数据库包含一组数据。典型的数据库应用案例是,将一个程序的所有数据组织起来,使之与另一个程序的数据保持独立。
在Redis里,数据库简单的使用一个数字编号来进行辨认,默认数据库的数字编号是`0`。如果你想切换到一个不同的数据库,你可以使用`select`命令来实现。在命令行界面里键入`select 1`Redis应该会回复一条`OK`的信息,然后命令行界面里的提示符会变成类似`redis 127.0.0.1:6379[1]>`这样。如果你想切换回默认数据库,只要在命令行界面键入`select 0`即可。
### 命令、关键字和值(Commands, Keys and Values
Redis不仅仅是一种简单的关键字-值型存储,从其核心概念来看,Redis的5种数据结构中的每一个都至少有一个关键字和一个值。在转入其它关于Redis的有用信息之前,我们必须理解关键字和值的概念。
关键字(Keys)是用来标识数据块。我们将会很常跟关键字打交道,不过在现在,明白关键字就是类似于`users:leto`这样的表述就足够了。一般都能很好地理解到,这样关键字包含的信息是一个名为`leto`的用户。这个关键字里的冒号没有任何特殊含义,对于Redis而言,使用分隔符来组织关键字是很常见的方法。
值(Values)是关联于关键字的实际值,可以是任何东西。有时候你会存储字符串,有时候是整数,还有时候你会存储序列化对象(使用JSON、XML或其他格式)。在大多数情况下,Redis会把值看做是一个字节序列,而不会关注它们实质上是什么。要注意,不同的Redis载体处理序列化会有所不同(一些会让你自己决定)。因此,在这本书里,我们将仅讨论字符串、整数和JSON。
现在让我们活动一下手指吧。在命令行界面键入下面的命令:
set users:leto "{name: leto, planet: dune, likes: [spice]}"
这就是Redis命令的基本构成。首先我们要有一个确定的命令,在上面的语句里就是`set`。然后就是相应的参数,`set`命令接受两个参数,包括要设置的关键字,以及相应要设置的值。很多的情况是,命令接受一个关键字(当这种情况出现,其经常是第一个参数)。你能想到如何去获取这个值吗?我想你会说(当然一时拿不准也没什么):
get users:leto
关键字和值的是Redis的基本概念,而`get``set`命令是对此最简单的使用。你可以创建更多的用户,去尝试不同类型的关键字以及不同的值,看看一些不同的组合。
### 查询(Querying
随着学习的持续深入,两件事情将变得清晰起来。对于Redis而言,关键字就是一切,而值是没有任何意义。更通俗来看就是,Redis不允许你通过值来进行查询。回到上面的例子,我们就不能查询生活在`dune`行星上的用户。
对许多人来说,这会引起一些担忧。在我们生活的世界里,数据查询是如此的灵活和强大,而Redis的方式看起来是这么的原始和不高效。不要让这些扰乱你太久。要记住,Redis不是一种普遍使用(one-size-fits-all)的解决方案,确实存在这么一些事情是不应该由Redis来解决的(因为其查询的限制)。事实上,在考虑了这些情况后,你会找到新的方法去构建你的数据。
很快,我们就能看到更多实际的用例。很重要的一点是,我们要明白关于Redis的这些基本事实。这能帮助我们弄清楚为什么值可以是任何东西,因为Redis从来不需要去读取或理解它们。而且,这也可以帮助我们理清思路,然后去思考如何在这个新世界里建立模型。
### 存储器和持久化(Memory and Persistence
我们之前提及过,Redis是一种持久化的存储器内存储(in-memory persistent store)。对于持久化,默认情况下,Redis会根据已变更的关键字数量来进行判断,然后在磁盘里创建数据库的快照(snapshot)。你可以对此进行设置,如果X个关键字已变更,那么每隔Y秒存储数据库一次。默认情况下,如果1000个或更多的关键字已变更,Redis会每隔60秒存储数据库;而如果9个或更少的关键字已变更,Redis会每隔15分钟存储数据库。
除了创建磁盘快照外,Redis可以在附加模式下运行。任何时候,如果有一个关键字变更,一个单一附加(append-only)的文件会在磁盘里进行更新。在一些情况里,虽然硬件或软件可能发生错误,但用那60秒有效数据存储去换取更好性能是可以接受的。而在另一些情况里,这种损失就难以让人接受,Redis为你提供了选择。在第5章里,我们将会看到第三种选择,其将持久化任务减荷到一个从属数据库里。
至于存储器,Redis会将所有数据都保留在存储器中。显而易见,运行Redis具有不低的成本:因为RAM仍然是最昂贵的服务器硬件部件。
我很清楚有一些开发者对即使是一点点的数据空间都是那么的敏感。一本《威廉·莎士比亚全集》需要近5.5MB的存储空间。对于缩放的需求,其它的解决方案趋向于IO-bound或者CPU-bound。这些限制(RAM或者IO)将会需要你去理解更多机器实际依赖的数据类型,以及应该如何去进行存储和查询。除非你是存储大容量的多媒体文件到Redis中,否则存储器内存储应该不会是一个问题。如果这对于一个程序是个问题,你就很可能不会用IO-bound的解决方案。
Redis有虚拟存储器的支持。然而,这个功能已经被认为是失败的了(通过Redis的开发者),而且它的使用已经被废弃了。
(从另一个角度来看,一本5.5MB的《威廉·莎士比亚全集》可以通过压缩减小到近2MB。当然,Redis不会自动对值进行压缩,但是因为其将所有值都看作是字节,没有什么限制让你不能对数据进行压缩/解压,通过牺牲处理时间来换取存储空间。)
### 整体来看(Putting It Together
我们已经接触了好几个高层次的主题。在继续深入Redis之前,我想做的最后一件事情是将这些主题整合起来。这些主题包括,查询的限制,数据结构以及Redis在存储器内存储数据的方法。
当你将这3个主题整合起来,你最终会得出一个绝妙的结论:速度。一些人可能会想,当然Redis会很快速,要知道所以的东西都在存储器里。但这仅仅是其中的一部分,让Redis闪耀的真正原因是其不同于其它解决方案的特殊数据结构。
能有多快速?这依赖于很多东西,包括你正在使用着哪个命令,数据的类型等等。但Redis的性能测试是趋向于数万或数十万次操作**每秒**。你可以通过运行`redis-benchmark`(就在`redis-server``redis-cli`的同一个文件夹里)来进行测试。
我曾经试过将一组使用传统模型的代码转向使用Redis。在传统模型里,运行一个我写的载入测试,需要超过5分钟的时间来完成。而在Redis里,只需要150毫秒就完成了。你不会总能得到这么好的收获,但希望这能让你对我们所谈的东西有更清晰的理解。
理解Redis的这个特性很重要,因为这将影响到你如何去与Redis进行交互。拥有SQL背景的程序员通常会致力于让数据库的数据往返次数减至最小。这对于任何系统都是个好建议,包括Redis。然而,考虑到我们是在处理比较简单的数据结构,有时候我们还是需要与Redis服务器频繁交互,以达到我们的目的。刚开始的时候,可能会对这种数据访问模式感到不太自然。实际上,相对于我们通过Redis获得的高性能而言,这仅仅是微不足道的损失。
### 小结
虽然我们只接触和摆弄了Redis的冰山一角,但我们讨论的主题已然覆盖了很大范围内的东西。如果觉得有些事情还是不太清楚(例如查询),不用为此而担心,在下一章我们将会继续深入探讨,希望你的问题都能得到解答。
这一章的要点包括:
* 关键字(Keys)是用于标识一段数据的一个字符串
* 值(Values)是一段任意的字节序列,Redis不会关注它们实质上是什么
* Redis展示了(也实现了)5种专门的数据结构
* 上面的几点使得Redis快速而且容易使用,但要知道Redis并不适用于所有的应用场景
\clearpage
## 第2章 - 数据结构
现在开始将探究Redis的5种数据结构,我们会解释每种数据结构都是什么,包含了什么有效的方法(Method),以及你能用这些数据结构处理哪些类型的特性和数据。
目前为止,我们所知道的Redis构成仅包括命令、关键字和值,还没有接触到关于数据结构的具体概念。当我们使用`set`命令时,Redis是怎么知道我们是在使用哪个数据结构?其解决方法是,每个命令都相对应于一种特定的数据结构。例如,当你使用`set`命令,你就是将值存储到一个字符串数据结构里。而当你使用`hset`命令,你就是将值存储到一个散列数据结构里。考虑到Redis的关键字集很小,这样的机制具有相当的可管理性。
**[Redis的网站](http://redis.io/commands)里有着非常优秀的参考文档,没有任何理由去重造轮子。但为了搞清楚这些数据结构的作用,我们将会覆盖那些必须知道的重要命令。**
没有什么事情比高兴的玩和试验有趣的东西来得更重要的了。在任何时候,你都能通过键入`flushdb`命令将你数据库里的所有值清除掉,因此,不要再那么害羞了,去尝试做些疯狂的事情吧!
### 字符串(Strings)
在Redis里,字符串是最基本的数据结构。当你在思索着关键字-值对时,你就是在思索着字符串数据结构。不要被名字给搞混了,如之前说过的,你的值可以是任何东西。我更喜欢将他们称作“标量”(Scalars),但也许只有我才这样想。
我们已经看到了一个常见的字符串使用案例,即通过关键字存储对象的实例。有时候,你会频繁地用到这类操作:
set users:leto "{name: leto, planet: dune, likes: [spice]}"
除了这些外,Redis还有一些常用的操作。例如,`strlen <key>`能用来获取一个关键字对应值的长度;`getrange <key> <start> <end>`将返回指定范围内的关键字对应值;`append <key> <value>`会将value附加到已存在的关键字对应值中(如果该关键字并不存在,则会创建一个新的关键字-值对)。不要犹豫,去试试看这些命令吧。下面是我得到的:
> strlen users:leto
(integer) 42
> getrange users:leto 27 40
"likes: [spice]"
> append users:leto " OVER 9000!!"
(integer) 54
现在你可能会想,这很好,但似乎没有什么意义。你不能有效地提取出一段范围内的JSON文件,或者为其附加一些值。你是对的,这里的经验是,一些命令,尤其是关于字符串数据结构的,只有在给定了明确的数据类型后,才会有实际意义。
之前我们知道了,Redis不会去关注你的值是什么东西。通常情况下,这没有错。然而,一些字符串命令是专门为一些类型或值的结构而设计的。作为一个有些含糊的用例,我们可以看到,对于一些自定义的空间效率很高的(space-efficient)串行化对象,`append``getrange`命令将会很有用。对于一个更为具体的用例,我们可以再看一下`incr``incrby``decr``decrby`命令。这些命令会增长或者缩减一个字符串数据结构的值:
> incr stats:page:about
(integer) 1
> incr stats:page:about
(integer) 2
> incrby ratings:video:12333 5
(integer) 5
> incrby ratings:video:12333 3
(integer) 8
由此你可以想象到,Redis的字符串数据结构能很好地用于分析用途。你还可以去尝试增长`users:leto`(一个不是整数的值),然后看看会发生什么(应该会得到一个错误)。
更为进阶的用例是`setbit``getbit`命令。“今天我们有多少个独立用户访问”是个在Web应用里常见的问题,有一篇[精彩的博文](http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps/),在里面可以看到Spool是如何使用这两个命令有效地解决此问题。对于1.28亿个用户,一部笔记本电脑在不到50毫秒的时间里就给出了答复,而且只用了16MB的存储空间。
最重要的事情不是在于你是否明白位图(Bitmaps)的工作原理,或者Spool是如何去使用这些命令,而是应该要清楚Redis的字符串数据结构比你当初所想的要有用许多。然而,最常见的应用案例还是上面我们给出的:存储对象(简单或复杂)和计数。同时,由于通过关键字来获取一个值是如此之快,字符串数据结构很常被用来缓存数据。
### 散列(Hashes
我们已经知道把Redis称为一种关键字-值型存储是不太准确的,散列数据结构是一个很好的例证。你会看到,在很多方面里,散列数据结构很像字符串数据结构。两者显著的区别在于,散列数据结构提供了一个额外的间接层:一个域(Field)。因此,散列数据结构中的`set``get`是:
hset users:goku powerlevel 9000
hget users:goku powerlevel
相关的操作还包括在同一时间设置多个域、同一时间获取多个域、获取所有的域和值、列出所有的域或者删除指定的一个域:
hmset users:goku race saiyan age 737
hmget users:goku race powerlevel
hgetall users:goku
hkeys users:goku
hdel users:goku age
如你所见,散列数据结构比普通的字符串数据结构具有更多的可操作性。我们可以使用一个散列数据结构去获得更精确的描述,是存储一个用户,而不是一个序列化对象。从而得到的好处是能够提取、更新和删除具体的数据片段,而不必去获取或写入整个值。
对于散列数据结构,可以从一个经过明确定义的对象的角度来考虑,例如一个用户,关键之处在于要理解他们是如何工作的。从性能上的原因来看,这是正确的,更具粒度化的控制可能会相当有用。在下一章我们将会看到,如何用散列数据结构去组织你的数据,使查询变得更为实效。在我看来,这是散列真正耀眼的地方。
### 列表(Lists
对于一个给定的关键字,列表数据结构让你可以存储和处理一组值。你可以添加一个值到列表里、获取列表的第一个值或最后一个值以及用给定的索引来处理值。列表数据结构维护了值的顺序,提供了基于索引的高效操作。为了跟踪在网站里注册的最新用户,我们可以维护一个`newusers`的列表:
lpush newusers goku
ltrim newusers 0 50
**(译注:`ltrim`命令的具体构成是`LTRIM Key start stop`。要理解`ltrim`命令,首先要明白Key所存储的值是一个列表,理论上列表可以存放任意个值。对于指定的列表,根据所提供的两个范围参数start和stop,`ltrim`命令会将指定范围外的值都删除掉,只留下范围内的值。)**
首先,我们将一个新用户推入到列表的前端,然后对列表进行调整,使得该列表只包含50个最近被推入的用户。这是一种常见的模式。`ltrim`是一个具有O(N)时间复杂度的操作,N是被删除的值的数量。从上面的例子来看,我们总是在插入了一个用户后再进行列表调整,实际上,其将具有O(1)的时间复杂度(因为N将永远等于1)的常数性能。
这是我们第一次看到一个关键字的对应值索引另一个值。如果我们想要获取最近的10个用户的详细资料,我们可以运行下面的组合操作:
keys = redis.lrange('newusers', 0, 10)
redis.mget(*keys.map {|u| "users:#{u}"})
我们之前谈论过关于多次往返数据的模式,上面的两行Ruby代码为我们进行了很好的演示。
当然,对于存储和索引关键字的功能,并不是只有列表数据结构这种方式。值可以是任意的东西,你可以使用列表数据结构去存储日志,也可以用来跟踪用户浏览网站时的路径。如果你过往曾构建过游戏,你可能会使用列表数据结构去跟踪用户的排队活动。
### 集合
集合数据结构常常被用来存储只能唯一存在的值,并提供了许多的基于集合的操作,例如并集。集合数据结构没有对值进行排序,但是其提供了高效的基于值的操作。使用集合数据结构的典型用例是朋友名单的实现:
sadd friends:leto ghanima paul chani jessica
sadd friends:duncan paul jessica alia
不管一个用户有多少个朋友,我们都能高效地(O(1)时间复杂度)识别出用户X是不是用户Y的朋友:
sismember friends:leto jessica
sismember friends:leto vladimir
而且,我们可以查看两个或更多的人是不是有共同的朋友:
sinter friends:leto friends:duncan
甚至可以在一个新的关键字里存储结果:
sinterstore friends:leto_duncan friends:leto friends:duncan
有时候需要对值的属性进行标记和跟踪处理,但不能通过简单的复制操作完成,集合数据结构是解决此类问题的最好方法之一。当然,对于那些需要运用集合操作的地方(例如交集和并集),集合数据结构就是最好的选择。
### 分类集合(Sorted Sets
最后也是最强大的数据结构是分类集合数据结构。如果说散列数据结构类似于字符串数据结构,主要区分是域(field)的概念;那么分类集合数据结构就类似于集合数据结构,主要区分是标记(score)的概念。标记提供了排序(sorting)和秩划分(ranking)的功能。如果我们想要一个秩分类的朋友名单,可以这样做:
zadd friends:duncan 70 ghanima 95 paul 95 chani 75 jessica 1 vladimir
对于`duncan`的朋友,要怎样计算出标记(score)为90或更高的人数?
zcount friends:duncan 90 100
如何获取`chani`在名单里的秩(rank)?
zrevrank friends:duncan chani
**(译注:`zrank`命令的具体构成是`ZRANK Key menber`,要知道Key存储的Sorted Set默认是根据Score对各个menber进行升序的排列,该命令就是用来获取menber在该排列里的次序,这就是所谓的秩。)**
我们使用了`zrevrank`命令而不是`zrank`命令,这是因为Redis的默认排序是从低到高,但是在这个例子里我们的秩划分是从高到低。对于分类集合数据结构,最常见的应用案例是用来实现排行榜系统。事实上,对于一些基于整数排序,且能以标记(score)来进行有效操作的东西,使用分类集合数据结构来处理应该都是不错的选择。
### 小结
对于Redis的5种数据结构,我们进行了高层次的概述。一件有趣的事情是,相对于最初构建时的想法,你经常能用Redis创造出一些更具实效的事情。对于字符串数据结构和分类集合数据结构的使用,很有可能存在一些构建方法是还没有人想到的。当你理解了那些常用的应用案例后,你将发现Redis对于许多类型的问题,都是很理想的选择。还有,不要因为Redis展示了5种数据结构和相应的各种方法,就认为你必须要把所有的东西都用上。只使用一些命令去构建一个特性是很常见的。
\clearpage
## 第3章 - 使用数据结构
在上一章里,我们谈论了Redis的5种数据结构,对于一些可能的用途也给出了用例。现在是时候来看看一些更高级,但依然很常见的主题和设计模式。
### 大O表示法(Big O Notation
在本书中,我们之前就已经看到过大O表示法,包括O(1)和O(N)的表示。大O表示法的惯常用途是,描述一些用于处理一定数量元素的行为的综合表现。在Redis里,对于一个要处理一定数量元素的命令,大O表示法让我们能了解该命令的大概运行速度。
在Redis的文档里,每一个命令的时间复杂度都用大O表示法进行了描述,还能知道各命令的具体性能会受什么因素影响。让我们来看看一些用例。
常数时间复杂度O(1)被认为是最快速的,无论我们是在处理5个元素还是5百万个元素,最终都能得到相同的性能。对于`sismember`命令,其作用是告诉我们一个值是否属于一个集合,时间复杂度为O(1)。`sismember`命令很强大,很大部分的原因是其高效的性能特征。许多Redis命令都具有O(1)的时间复杂度。
对数时间复杂度O(log(N))被认为是第二快速的,其通过使需扫描的区间不断皱缩来快速完成处理。使用这种“分而治之”的方式,大量的元素能在几个迭代过程里被快速分解完整。`zadd`命令的时间复杂度就是O(log(N)),其中N是在分类集合中的元素数量。
再下来就是线性时间复杂度O(N),在一个表格的非索引列里进行查找就需要O(N)次操作。`ltrim`命令具有O(N)的时间复杂度,但是,在`ltrim`命令里,N不是列表所拥有的元素数量,而是被删除的元素数量。从一个具有百万元素的列表里用`ltrim`命令删除1个元素,要比从一个具有一千个元素的列表里用`ltrim`命令删除10个元素来的快速(实际上,两者很可能会是一样快,因为两个时间都非常的小)。
根据给定的最小和最大的值的标记,`zremrangebyscore`命令会在一个分类集合里进行删除元素操作,其时间复杂度是O(log(N)+M)。这看起来似乎有点儿杂乱,通过阅读文档可以知道,这里的N指的是在分类集合里的总元素数量,而M则是被删除的元素数量。可以看出,对于性能而言,被删除的元素数量很可能会比分类集合里的总元素数量更为重要。
**(译注:`zremrangebyscore`命令的具体构成是`ZREMRANGEBYSCORE Key max mix`。)**
对于`sort`命令,其时间复杂度为O(N+M*log(M)),我们将会在下一章谈论更多的相关细节。从`sort`命令的性能特征来看,可以说这是Redis里最复杂的一个命令。
还存在其他的时间复杂度描述,包括O(N^2)和O(C^N)。随着N的增大,其性能将急速下降。在Redis里,没有任何一个命令具有这些类型的时间复杂度。
值得指出的一点是,在Redis里,当我们发现一些操作具有O(N)的时间复杂度时,我们可能可以找到更为好的方法去处理。
**(译注:对于Big O Notation,相信大家都非常的熟悉,虽然原文仅仅是对该表示法进行简单的介绍,但限于个人的算法知识和文笔水平实在有限,此小节的翻译让我头痛颇久,最终成果也确实难以让人满意,望见谅。)**
### 仿多关键字查询(Pseudo Multi Key Queries
时常,你会想通过不同的关键字去查询相同的值。例如,你会想通过电子邮件(当用户开始登录时)去获取用户的具体信息,或者通过用户id(在用户登录后)去获取。有一种很不实效的解决方法,其将用户对象分别放置到两个字符串值里去:
set users:leto@dune.gov "{id: 9001, email: 'leto@dune.gov', ...}"
set users:9001 "{id: 9001, email: 'leto@dune.gov', ...}"
这种方法很糟糕,如此不但会产生两倍数量的内存,而且这将会成为数据管理的恶梦。
如果Redis允许你将一个关键字链接到另一个的话,可能情况会好很多,可惜Redis并没有提供这样的功能(而且很可能永远都不会提供)。Redis发展到现在,其开发的首要目的是要保持代码和API的整洁简单,关键字链接功能的内部实现并不符合这个前提(对于关键字,我们还有很多相关方法没有谈论到)。其实,Redis已经提供了解决的方法:散列。
使用散列数据结构,我们可以摆脱重复的缠绕:
set users:9001 "{id: 9001, email: leto@dune.gov, ...}"
hset users:lookup:email leto@dune.gov 9001
我们所做的是,使用域来作为一个二级索引,然后去引用单个用户对象。要通过id来获取用户信息,我们可以使用一个普通的`get`命令:
get users:9001
而如果想通过电子邮箱来获取用户信息,我们可以使用`hget`命令再配合使用`get`命令(Ruby代码):
id = redis.hget('users:lookup:email', 'leto@dune.gov')
user = redis.get("users:#{id}")
你很可能将会经常使用这类用法。在我看来,这就是散列真正耀眼的地方。在你了解这类用法之前,这可能不是一个明显的用例。
### 引用和索引(References and Indexes
我们已经看过几个关于值引用的用例,包括介绍列表数据结构时的用例,以及在上面使用散列数据结构来使查询更灵活一些。进行归纳后会发现,对于那些值与值间的索引和引用,我们都必须手动的去管理。诚实来讲,这确实会让人有点沮丧,尤其是当你想到那些引用相关的操作,如管理、更新和删除等,都必须手动的进行时。在Redis里,这个问题还没有很好的解决方法。
我们已经看到,集合数据结构很常被用来实现这类索引:
sadd friends:leto ghanima paul chani jessica
这个集合里的每一个成员都是一个Redis字符串数据结构的引用,而每一个引用的值则包含着用户对象的具体信息。那么如果`chani`改变了她的名字,或者删除了她的帐号,应该如何处理?从整个朋友圈的关系结构来看可能会更好理解,我们知道,`chani`也有她的朋友:
sadd friends_of:chani leto paul
如果你有什么待处理情况像上面那样,那在维护成本之外,还会有对于额外索引值的处理和存储空间的成本。这可能会令你感到有点退缩。在下一小节里,我们将会谈论减少使用额外数据交互的性能成本的一些方法(在第1章我们粗略地讨论了下)。
如果你确实在担忧着这些情况,其实,关系型数据库也有同样的开销。索引需要一定的存储空间,必须通过扫描或查找,然后才能找到相应的记录。其开销也是存在的,当然他们对此做了很多的优化工作,使之变得更为有效。
再次说明,需要在Redis里手动地管理引用确实是颇为棘手。但是,对于你关心的那些问题,包括性能或存储空间等,应该在经过测试后,才会有真正的理解。我想你会发现这不会是一个大问题。
### 数据交互和流水线(Round Trips and Pipelining
我们已经提到过,与服务器频繁交互是Redis的一种常见模式。这类情况可能很常出现,为了使我们能获益更多,值得仔细去看看我们能利用哪些特性。
许多命令能接受一个或更多的参数,也有一种关联命令(sister-command)可以接受多个参数。例如早前我们看到过`mget`命令,接受多个关键字,然后返回值:
keys = redis.lrange('newusers', 0, 10)
redis.mget(*keys.map {|u| "users:#{u}"})
或者是`sadd`命令,能添加一个或多个成员到集合里:
sadd friends:vladimir piter
sadd friends:paul jessica leto "leto II" chani
Redis还支持流水线功能。通常情况下,当一个客户端发送请求到Redis后,在发送下一个请求之前必须等待Redis的答复。使用流水线功能,你可以发送多个请求,而不需要等待Redis响应。这不但减少了网络开销,还能获得性能上的显著提高。
值得一提的是,Redis会使用存储器去排列命令,因此批量执行命令是一个好主意。至于具体要多大的批量,将取决于你要使用什么命令(更明确来说,该参数有多大)。另一方面来看,如果你要执行的命令需要差不多50个字符的关键字,你大概可以对此进行数千或数万的批量操作。
对于不同的Redis载体,在流水线里运行命令的方式会有所差异。在Ruby里,你传递一个代码块到`pipelined`方法:
redis.pipelined do
9001.times do
redis.incr('powerlevel')
end
end
正如你可能猜想到的,流水线功能可以实际地加速一连串命令的处理。
### 事务(Transactions
每一个Redis命令都具有原子性,包括那些一次处理多项事情的命令。此外,对于使用多个命令,Redis支持事务功能。
你可能不知道,但Redis实际上是单线程运行的,这就是为什么每一个Redis命令都能够保证具有原子性。当一个命令在执行时,没有其他命令会运行(我们会在往后的章节里简略谈论一下Scaling)。在你考虑到一些命令去做多项事情时,这会特别的有用。例如:
`incr`命令实际上就是一个`get`命令然后紧随一个`set`命令。
`getset`命令设置一个新的值然后返回原始值。
`setnx`命令首先测试关键字是否存在,只有当关键字不存在时才设置值
虽然这些都很有用,但在实际开发时,往往会需要运行具有原子性的一组命令。若要这样做,首先要执行`multi`命令,紧随其后的是所有你想要执行的命令(作为事务的一部分),最后执行`exec`命令去实际执行命令,或者使用`discard`命令放弃执行命令。Redis的事务功能保证了什么?
* 事务中的命令将会按顺序地被执行
* 事务中的命令将会如单个原子操作般被执行(没有其它的客户端命令会在中途被执行)
* 事务中的命令要么全部被执行,要么不会执行
你可以(也应该)在命令行界面对事务功能进行一下测试。还有一点要注意到,没有什么理由不能结合流水线功能和事务功能。
multi
hincrby groups:1percent balance -9000000000
hincrby groups:99percent balance 9000000000
exec
最后,Redis能让你指定一个关键字(或多个关键字),当关键字有改变时,可以查看或者有条件地应用一个事务。这是用于当你需要获取值,且待运行的命令基于那些值时,所有都在一个事务里。对于上面展示的代码,我们不能去实现自己的`incr`命令,因为一旦`exec`命令被调用,他们会全部被执行在一块。我们不能这么做:
redis.multi()
current = redis.get('powerlevel')
redis.set('powerlevel', current + 1)
redis.exec()
**(译注:虽然Redis是单线程运行的,但是我们可以同时运行多个Redis客户端进程,常见的并发问题还是会出现。像上面的代码,在`get`运行之后,`set`运行之前,`powerlevel`的值可能会被另一个Redis客户端给改变,从而造成错误。)**
这些不是Redis的事务功能的工作。但是,如果我们增加一个`watch``powerlevel`,我们可以这样做:
redis.watch('powerlevel')
current = redis.get('powerlevel')
redis.multi()
redis.set('powerlevel', current + 1)
redis.exec()
在我们调用`watch`后,如果另一个客户端改变了`powerlevel`的值,我们的事务将会运行失败。如果没有客户端改变`powerlevel`的值,那么事务会继续工作。我们可以在一个循环里运行这些代码,直到其能正常工作。
### 关键字反模式(Keys Anti-Pattern
在下一章中,我们将会谈论那些没有确切关联到数据结构的命令,其中的一些是管理或调试工具。然而有一个命令我想特别地在这里进行谈论:`keys`命令。这个命令需要一个模式,然后查找所有匹配的关键字。这个命令看起来很适合一些任务,但这不应该用在实际的产品代码里。为什么?因为这个命令通过线性扫描所有的关键字来进行匹配。或者,简单地说,这个命令太慢了。
人们会如此去使用这个命令?一般会用来构建一个本地的Bug追踪服务。每一个帐号都有一个`id`,你可能会通过一个看起来像`bug:account_id:bug_id`的关键字,把每一个Bug存储到一个字符串数据结构值中去。如果你在任何时候需要查询一个帐号的Bug(显示它们,或者当用户删除了帐号时删除掉这些Bugs),你可能会尝试去使用`keys`命令:
keys bug:1233:*
更好的解决方法应该使用一个散列数据结构,就像我们可以使用散列数据结构来提供一种方法去展示二级索引,因此我们可以使用域来组织数据:
hset bugs:1233 1 "{id:1, account: 1233, subject: '...'}"
hset bugs:1233 2 "{id:2, account: 1233, subject: '...'}"
从一个帐号里获取所有的Bug标识,可以简单地调用`hkeys bugs:1233`。去删除一个指定的Bug,可以调用`hdel bugs:1233 2`。如果要删除了一个帐号,可以通过`del bugs:1233`把关键字删除掉。
### 小结
结合这一章以及前一章,希望能让你得到一些洞察力,了解如何使用Redis去支持(Power)实际项目。还有其他的模式可以让你去构建各种类型的东西,但真正的关键是要理解基本的数据结构。你将能领悟到,这些数据结构是如何能够实现你最初视角之外的东西。
\clearpage
## 第4章 超越数据结构
5种数据结构组成了Redis的基础,其他没有关联特定数据结构的命令也有很多。我们已经看过一些这样的命令:`info`, `select`, `flushdb`, `multi`, `exec`, `discard`, `watch``keys `。这一章将看看其他的一些重要命令。
### 使用期限(Expiration
Redis允许你标记一个关键字的使用期限。你可以给予一个Unix时间戳形式(自1970年1月1日起)的绝对时间,或者一个基于秒的存活时间。这是一个基于关键字的命令,因此其不在乎关键字表示的是哪种类型的数据结构。
expire pages:about 30
expireat pages:about 1356933600
第一个命令将会在30秒后删除掉关键字(包括其关联的值)。第二个命令则会在2012年12月31日上午12点删除掉关键字。
这让Redis能成为一个理想的缓冲引擎。通过`ttl`命令,你可以知道一个关键字还能够存活多久。而通过`persist`命令,你可以把一个关键字的使用期限删除掉。
ttl pages:about
persist pages:about
最后,有个特殊的字符串命令,`setex`命令让你可以在一个单独的原子命令里设置一个字符串值,同时里指定一个生存期(这比任何事情都要方便)。
setex pages:about 30 '<h1>about us</h1>....'
### 发布和订阅(Publication and Subscriptions
Redis的列表数据结构有`blpop``brpop`命令,能从列表里返回且删除第一个(或最后一个)元素,或者被堵塞,直到有一个元素可供操作。这可以用来实现一个简单的队列。
**(译注:对于`blpop``brpop`命令,如果列表里没有关键字可供操作,连接将被堵塞,直到有另外的Redis客户端使用`lpush``rpush`命令推入关键字为止。)**
此外,Redis对于消息发布和频道订阅有着一流的支持。你可以打开第二个`redis-cli`窗口,去尝试一下这些功能。在第一个窗口里订阅一个频道(我们会称它为`warnings`):
subscribe warnings
其将会答复你订阅的信息。现在,在另一个窗口,发布一条消息到`warnings`频道:
publish warnings "it's over 9000!"
如果你回到第一个窗口,你应该已经接收到`warnings`频道发来的消息。
你可以订阅多个频道(`subscribe channel1 channel2 ...`),订阅一组基于模式的频道(`psubscribe warnings:*`),以及使用`unsubscribe``punsubscribe`命令停止监听一个或多个频道,或一个频道模式。
最后,可以注意到`publish`命令的返回值是1,这指出了接收到消息的客户端数量。
### 监控和延迟日志(Monitor and Slow Log
`monitor`命令可以让你查看Redis正在做什么。这是一个优秀的调试工具,能让你了解你的程序如何与Redis进行交互。在两个`redis-cli`窗口中选一个(如果其中一个还处于订阅状态,你可以使用`unsubscribe`命令退订,或者直接关掉窗口再重新打开一个新窗口)键入`monitor`命令。在另一个窗口,执行任何其他类型的命令(例如`get``set`命令)。在第一个窗口里,你应该可以看到这些命令,包括他们的参数。
在实际生产环境里,你应该谨慎运行`monitor`命令,这真的仅仅就是一个很有用的调试和开发工具。除此之外,没有更多要说的了。
随同`monitor`命令一起,Redis拥有一个`slowlog`命令,这是一个优秀的性能剖析工具。其会记录执行时间超过一定数量**微秒**的命令。在下一章节,我们会简略地涉及如何配置Redis,现在你可以按下面的输入配置Redis去记录所有的命令:
config set slowlog-log-slower-than 0
然后,执行一些命令。最后,你可以检索到所有日志,或者检索最近的那些日志:
slowlog get
slowlog get 10
通过键入`slowlog len`,你可以获取延迟日志里的日志数量。
对于每个被你键入的命令,你应该查看4个参数:
* 一个自动递增的id
* 一个Unix时间戳,表示命令开始运行的时间
* 一个微妙级的时间,显示命令运行的总时间
* 该命令以及所带参数
延迟日志保存在存储器中,因此在生产环境中运行(即使有一个低阀值)也应该不是一个问题。默认情况下,它将会追踪最近的1024个日志。
### 排序(Sort
`sort`命令是Redis最强大的命令之一。它让你可以在一个列表、集合或者分类集合里对值进行排序(分类集合是通过标记来进行排序,而不是集合里的成员)。下面是一个`sort`命令的简单用例:
rpush users:leto:guesses 5 9 10 2 4 10 19 2
sort users:leto:guesses
这将返回进行升序排序后的值。这里有一个更高级的例子:
sadd friends:ghanima leto paul chani jessica alia duncan
sort friends:ghanima limit 0 3 desc alpha
上面的命令向我们展示了,如何对已排序的记录进行分页(通过`limit`),如何返回降序排序的结果(通过`desc`),以及如何用字典序排序代替数值序排序(通过`alpha`)。
`sort`命令的真正力量是其基于引用对象来进行排序的能力。早先的时候,我们说明了列表、集合和分类集合很常被用于引用其他的Redis对象,`sort`命令能够解引用这些关系,而且通过潜在值来进行排序。例如,假设我们有一个Bug追踪器能让用户看到各类已存在问题。我们可能使用一个集合数据结构去追踪正在被监视的问题:
sadd watch:leto 12339 1382 338 9338
你可能会有强烈的感觉,想要通过id来排序这些问题(默认的排序就是这样的),但是,我们更可能是通过问题的严重性来对这些问题进行排序。为此,我们要告诉Redis将使用什么模式来进行排序。首先,为了可以看到一个有意义的结果,让我们添加多一点数据:
set severity:12339 3
set severity:1382 2
set severity:338 5
set severity:9338 4
要通过问题的严重性来降序排序这些Bug,你可以这样做:
sort watch:leto by severity:* desc
Redis将会用存储在列表(集合或分类集合)中的值去替代模式中的`*`(通过`by`)。这会创建出关键字名字,Redis将通过查询其实际值来排序。
在Redis里,虽然你可以有成千上万个关键字,类似上面展示的关系还是会引起一些混乱。幸好,`sort`命令也可以工作在散列数据结构及其相关域里。相对于拥有大量的高层次关键字,你可以利用散列:
hset bug:12339 severity 3
hset bug:12339 priority 1
hset bug:12339 details "{id: 12339, ....}"
hset bug:1382 severity 2
hset bug:1382 priority 2
hset bug:1382 details "{id: 1382, ....}"
hset bug:338 severity 5
hset bug:338 priority 3
hset bug:338 details "{id: 338, ....}"
hset bug:9338 severity 4
hset bug:9338 priority 2
hset bug:9338 details "{id: 9338, ....}"
所有的事情不仅变得更为容易管理,而且我们能通过`severity``priority`来进行排序,还可以告诉`sort`命令具体要检索出哪一个域的数据:
sort watch:leto by bug:*->priority get bug:*->details
相同的值替代出现了,但Redis还能识别`->`符号,用它来查看散列中指定的域。里面还包括了`get`参数,这里也会进行值替代和域查看,从而检索出Bug的细节(details域的数据)。
对于太大的集合,`sort`命令的执行可能会变得很慢。好消息是,`sort`命令的输出可以被存储起来:
sort watch:leto by bug:*->priority get bug:*->details store watch_by_priority:leto
使用我们已经看过的`expiration`命令,再结合`sort`命令的`store`能力,这是一个美妙的组合。
### 小结
这一章主要关注那些非特定数据结构关联的命令。和其他事情一样,它们的使用依情况而定。构建一个程序或特性时,可能不会用到使用期限、发布和订阅或者排序等功能。但知道这些功能的存在是很好的。而且,我们也只接触到了一些命令。还有更多的命令,当你消化理解完这本书后,非常值得去浏览一下[完整的命令列表](http://redis.io/commands)。
\clearpage
## 第5章 - 管理
在最后一章里,我们将集中谈论Redis运行中的一些管理方面内容。这是一个不完整的Redis管理指南,我们将会回答一些基本的问题,初接触Redis的新用户可能会很感兴趣。
### 配置(Configuration
当你第一次运行Redis的服务器,它会向你显示一个警告,指`redis.conf`文件没有被找到。这个文件可以被用来配置Redis的各个方面。一个充分定义(well-documented)的`redis.conf`文件对各个版本的Redis都有效。范例文件包含了默认的配置选项,因此,对于想要了解设置在干什么,或默认设置是什么,都会很有用。你可以在<https://github.com/antirez/redis/raw/2.4.6/redis.conf>找到这个文件。
**这个配置文件针对的是Redis 2.4.6,你应该用你的版本号替代上面URL里的"2.4.6"。运行`info`命令,其显示的第一个值就是Redis的版本号。**
因为这个文件已经是充分定义(well-documented),我们就不去再进行设置了。
除了通过`redis.conf`文件来配置Redis`config set`命令可以用来对个别值进行设置。实际上,在将`slowlog-log-slower-than`设置为0时,我们就已经使用过这个命令了。
还有一个`config get`命令能显示一个设置值。这个命令支持模式匹配,因此如果我们想要显示关联于日志(logging)的所有设置,我们可以这样做:
config get *log*
### 验证(Authentication
通过设置`requirepass`(使用`config set`命令或`redis.conf`文件),可以让Redis需要一个密码验证。当`requirepass`被设置了一个值(就是待用的密码),客户端将需要执行一个`auth password`命令。
一旦一个客户端通过了验证,就可以在任意数据库里执行任何一条命令,包括`flushall`命令,这将会清除掉每一个数据库里的所有关键字。通过配置,你可以重命名一些重要命令为混乱的字符串,从而获得一些安全性。
rename-command CONFIG 5ec4db169f9d4dddacbfb0c26ea7e5ef
rename-command FLUSHALL 1041285018a942a4922cbf76623b741e
或者,你可以将新名字设置为一个空字符串,从而禁用掉一个命令。
### 大小限制(Size Limitations
当你开始使用Redis,你可能会想知道,我能使用多少个关键字?还可能想知道,一个散列数据结构能有多少个域(尤其是当你用它来组织数据时),或者是,一个列表数据结构或集合数据结构能有多少个元素?对于每一个实例,实际限制都能达到亿万级别(hundreds of millions)。
### 复制(Replication
Redis支持复制功能,这意味着当你向一个Redis实例(Master)进行写入时,一个或多个其他实例(Slaves)能通过Master实例来保持更新。可以在配置文件里设置`slaveof`,或使用`slaveof`命令来配置一个Slave实例。对于那些没有进行这些设置的Redis实例,就可能一个Master实例。
为了更好保护你的数据,复制功能拷贝数据到不同的服务器。复制功能还能用于改善性能,因为读取请求可以被发送到Slave实例。他们可能会返回一些稍微滞后的数据,但对于大多数程序来说,这是一个值得做的折衷。
遗憾的是,Redis的复制功能还没有提供自动故障恢复。如果Master实例崩溃了,一个Slave实例需要手动的进行升级。如果你想使用Redis去达到某种高可用性,对于使用心跳监控(heartbeat monitoring)和脚本自动开关(scripts to automate the switch)的传统高可用性工具来说,现在还是一个棘手的难题。
### 备份文件(Backups
备份Redis非常简单,你可以将Redis的快照(snapshot)拷贝到任何地方,包括S3、FTP等。默认情况下,Redis会把快照存储为一个名为`dump.rdb`的文件。在任何时候,你都可以对这个文件执行`scp``ftp``cp`等常用命令。
有一种常见情况,在Master实例上会停用快照以及单一附加文件(aof),然后让一个Slave实例去处理备份事宜。这可以帮助减少Master实例的载荷。在不损害整体系统响应性的情况下,你还可以在Slave实例上设置更多主动存储的参数。
### 缩放和Redis集群(Scaling and Redis Cluster
复制功能(Replication)是一个成长中的网站可以利用的第一个工具。有一些命令会比另外一些来的昂贵(例如`sort`命令),将这些运行载荷转移到一个Slave实例里,可以保持整体系统对于查询的快速响应。
此外,通过分发你的关键字到多个Redis实例里,可以达到真正的缩放Redis(记住,Redis是单线程的,这些可以运行在同一个逻辑框里)。随着时间的推移,你将需要特别注意这些事情(尽管许多的Redis载体都提供了consistent-hashing算法)。对于数据水平分布(horizontal distribution)的考虑不在这本书所讨论的范围内。这些东西你也很可能不需要去担心,但是,无论你使用哪一种解决方案,有一些事情你还是必须意识到。
好消息是,这些工作都可在Redis集群下进行。不仅提供水平缩放(包括均衡),为了高可用性,还提供了自动故障恢复。
高可用性和缩放是可以达到的,只要你愿意为此付出时间和精力,Redis集群也使事情变得简单多了。
### 小结
在过去的一段时间里,已经有许多的计划和网站使用了Redis,毫无疑问,Redis已经可以应用于实际生产中了。然而,一些工具还是不够成熟,尤其是一些安全性和可用性相关的工具。对于Redis集群,我们希望很快就能看到其实现,这应该能为一些现有的管理挑战提供处理帮忙。
\clearpage
## 总结
在许多方面,Redis体现了一种简易的数据处理方式,其剥离掉了大部分的复杂性和抽象,并可有效的在不同系统里运行。不少情况下,选择Redis不是最佳的选择。在另一些情况里,Redis就像是为你的数据提供了特别定制的解决方案。
最终,回到我最开始所说的:Redis很容易学习。现在有许多的新技术,很难弄清楚哪些才真正值得我们花时间去学习。如果你从实际好处来考虑,Redis提供了他的简单性。我坚信,对于你和你的团队,学习Redis是最好的技术投资之一。
+65
View File
@@ -0,0 +1,65 @@
package hugolib
import (
"testing"
"bytes"
"github.com/spf13/hugo/source"
"github.com/spf13/hugo/target"
)
const RSS_TEMPLATE = `<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ .Title }} on {{ .Site.Title }} </title>
<link>{{ .Permalink }}</link>
<language>en-us</language>
<author>Steve Francia</author>
<rights>Francia; all rights reserved.</rights>
<updated>{{ .Date }}</updated>
{{ range .Data.Pages }}
<item>
<title>{{ .Title }}</title>
<link>{{ .Permalink }}</link>
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 MST" }}</pubDate>
<author>Steve Francia</author>
<guid>{{ .Permalink }}</guid>
<description>{{ .Content | html }}</description>
</item>
{{ end }}
</channel>
</rss>`
func TestRSSOutput(t *testing.T) {
files := make(map[string][]byte)
target := &target.InMemoryTarget{Files: files}
s := &Site{
Target: target,
Config: Config{BaseUrl: "http://auth/bub/"},
Source: &source.InMemorySource{WEIGHTED_SOURCES},
}
s.initializeSiteInfo()
s.prepTemplates()
// Add an rss.xml template to invoke the rss build.
s.addTemplate("rss.xml", RSS_TEMPLATE)
if err := s.CreatePages(); err != nil {
t.Fatalf("Unable to create pages: %s", err)
}
if err := s.BuildSiteMeta(); err != nil {
t.Fatalf("Unable to build site metadata: %s", err)
}
if err := s.RenderHomePage(); err != nil {
t.Fatalf("Unable to RenderHomePage: %s", err)
}
if _, ok := files[".xml"]; !ok {
t.Errorf("Unable to locate: %s", ".xml")
t.Logf("%q", files)
}
rss, _ := files[".xml"]
if !bytes.HasPrefix(rss, []byte("<?xml")) {
t.Errorf("rss feed should start with <?xml. %s", rss)
}
}
+3 -3
View File
@@ -16,7 +16,7 @@ package hugolib
import (
"bytes"
"fmt"
"html/template"
"github.com/spf13/hugo/template/bundle"
"strings"
"unicode"
)
@@ -37,7 +37,7 @@ type ShortcodeWithPage struct {
type Shortcodes map[string]ShortcodeFunc
func ShortcodesHandle(stringToParse string, p *Page, t *template.Template) string {
func ShortcodesHandle(stringToParse string, p *Page, t bundle.Template) string {
posStart := strings.Index(stringToParse, "{{%")
if posStart > 0 {
posEnd := strings.Index(stringToParse[posStart:], "%}}") + posStart
@@ -124,7 +124,7 @@ func SplitParams(in string) (name string, par2 string) {
return strings.TrimSpace(in[:i+1]), strings.TrimSpace(in[i+1:])
}
func ShortcodeRender(name string, data *ShortcodeWithPage, t *template.Template) string {
func ShortcodeRender(name string, data *ShortcodeWithPage, t bundle.Template) string {
buffer := new(bytes.Buffer)
t.ExecuteTemplate(buffer, "shortcodes/"+name+".html", data)
return buffer.String()
+472 -285
View File
@@ -16,236 +16,311 @@ package hugolib
import (
"bitbucket.org/pkg/inflect"
"bytes"
"errors"
"fmt"
"github.com/spf13/hugo/source"
"github.com/spf13/hugo/target"
helpers "github.com/spf13/hugo/template"
"github.com/spf13/hugo/template/bundle"
"github.com/spf13/hugo/transform"
"github.com/spf13/nitro"
"html/template"
"io/ioutil"
"io"
"net/url"
"os"
"path/filepath"
"strings"
"time"
//"sync"
)
const slash = string(os.PathSeparator)
var _ = transform.AbsURL
var DefaultTimer *nitro.B
func MakePermalink(base *url.URL, path *url.URL) *url.URL {
return base.ResolveReference(path)
}
// Site contains all the information relevant for constructing a static
// site. The basic flow of information is as follows:
//
// 1. A list of Files is parsed and then converted into Pages.
//
// 2. Pages contain sections (based on the file they were generated from),
// aliases and slugs (included in a pages frontmatter) which are the
// various targets that will get generated. There will be canonical
// listing.
//
// 3. Indexes are created via configuration and will present some aspect of
// the final page and typically a perm url.
//
// 4. All Pages are passed through a template based on their desired
// layout based on numerous different elements.
//
// 5. The entire collection of files is written to disk.
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
Config Config
Pages Pages
Tmpl bundle.Template
Indexes IndexList
Source source.Input
Sections Index
Info SiteInfo
Shortcodes map[string]ShortcodeFunc
timer *nitro.B
Target target.Output
Alias target.AliasPublisher
Completed chan bool
RunMode runmode
params map[string]interface{}
}
type SiteInfo struct {
BaseUrl template.URL
Indexes OrderedIndexList
Indexes IndexList
Recent *Pages
LastChange time.Time
Title string
Config *Config
Params map[string]interface{}
}
func (s *Site) getFromIndex(kind string, name string) Pages {
return s.Indexes[kind][name]
type runmode struct {
Watching bool
}
func NewSite(config *Config) *Site {
return &Site{c: *config, timer: nitro.Initalize()}
func (s *Site) Running() bool {
return s.RunMode.Watching
}
func (site *Site) Build() (err error) {
if err = site.Process(); err != nil {
func init() {
DefaultTimer = nitro.Initalize()
}
func (s *Site) timerStep(step string) {
if s.timer == nil {
s.timer = DefaultTimer
}
s.timer.Step(step)
}
func (s *Site) Build() (err error) {
if err = s.Process(); err != nil {
return
}
if err = s.Render(); err != nil {
fmt.Printf("Error rendering site: %s\nAvailable templates:\n", err)
for _, template := range s.Tmpl.Templates() {
fmt.Printf("\t%s\n", template.Name())
}
return
}
site.Render()
site.Write()
return nil
}
func (site *Site) Analyze() {
site.Process()
site.checkDescriptions()
}
func (site *Site) Process() (err error) {
site.initialize()
site.prepTemplates()
site.timer.Step("initialize & template prep")
site.CreatePages()
site.setupPrevNext()
site.timer.Step("import pages")
if err = site.BuildSiteMeta(); err != nil {
return
}
site.timer.Step("build indexes")
return
}
func (site *Site) Render() {
site.ProcessShortcodes()
site.timer.Step("render shortcodes")
site.AbsUrlify()
site.timer.Step("absolute URLify")
site.RenderIndexes()
site.RenderIndexesIndexes()
site.timer.Step("render and write indexes")
site.RenderLists()
site.timer.Step("render and write lists")
site.RenderPages()
site.timer.Step("render pages")
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) Analyze() {
s.Process()
s.initTarget()
s.Alias = &target.HTMLRedirectAlias{
PublishDir: s.absPublishDir(),
}
s.ShowPlan(os.Stdout)
}
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
s.Tmpl = bundle.NewTemplate()
s.Tmpl.LoadTemplates(s.absLayoutDir())
}
func (s *Site) initialize() {
site := s
func (s *Site) addTemplate(name, data string) error {
return s.Tmpl.AddTemplate(name, data)
}
s.checkDirectories()
func (s *Site) Process() (err error) {
if err = s.initialize(); err != nil {
return
}
s.prepTemplates()
s.timerStep("initialize & template prep")
if err = s.CreatePages(); err != nil {
return
}
s.setupPrevNext()
s.timerStep("import pages")
if err = s.BuildSiteMeta(); err != nil {
return
}
s.timerStep("build indexes")
return
}
walker := func(path string, fi os.FileInfo, err error) error {
if err != nil {
PrintErr("Walker: ", err)
return nil
func (s *Site) setupPrevNext() {
for i, page := range s.Pages {
if i < len(s.Pages)-1 {
page.Next = s.Pages[i+1]
}
if fi.IsDir() {
site.Directories = append(site.Directories, path)
return nil
} else {
site.Files = append(site.Files, path)
return nil
if i > 0 {
page.Prev = s.Pages[i-1]
}
return nil
}
}
func (s *Site) Render() (err error) {
if err = s.RenderAliases(); err != nil {
return
}
s.timerStep("render and write aliases")
s.ProcessShortcodes()
s.timerStep("render shortcodes")
if err = s.RenderIndexes(); err != nil {
return
}
s.timerStep("render and write indexes")
s.RenderIndexesIndexes()
s.timerStep("render & write index indexes")
if err = s.RenderLists(); err != nil {
return
}
s.timerStep("render and write lists")
if err = s.RenderPages(); err != nil {
return
}
s.timerStep("render and write pages")
if err = s.RenderHomePage(); err != nil {
return
}
s.timerStep("render and write homepage")
return
}
func (s *Site) checkDescriptions() {
for _, p := range s.Pages {
if len(p.Description) < 60 {
fmt.Println(p.FileName + " ")
}
}
}
func (s *Site) initialize() (err error) {
if err = s.checkDirectories(); err != nil {
return err
}
filepath.Walk(s.c.GetAbsPath(s.c.ContentDir), walker)
staticDir := s.Config.GetAbsPath(s.Config.StaticDir + "/")
s.Info = SiteInfo{BaseUrl: template.URL(s.c.BaseUrl), Title: s.c.Title, Config: &s.c}
s.Source = &source.Filesystem{
AvoidPaths: []string{staticDir},
Base: s.absContentDir(),
}
s.initializeSiteInfo()
s.Shortcodes = make(map[string]ShortcodeFunc)
return
}
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))
func (s *Site) initializeSiteInfo() {
s.Info = SiteInfo{
BaseUrl: template.URL(s.Config.BaseUrl),
Title: s.Config.Title,
Recent: &s.Pages,
Config: &s.Config,
Params: s.Config.Params,
}
if b, _ := dirExists(s.c.GetAbsPath(s.c.ContentDir)); !b {
FatalErr("No source directory found, expecting to find it at " + s.c.GetAbsPath(s.c.ContentDir))
}
// Check if File / Directory Exists
func exists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
mkdirIf(s.c.GetAbsPath(s.c.PublishDir))
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func (s *Site) absLayoutDir() string {
return s.Config.GetAbsPath(s.Config.LayoutDir)
}
func (s *Site) absContentDir() string {
return s.Config.GetAbsPath(s.Config.ContentDir)
}
func (s *Site) absPublishDir() string {
return s.Config.GetAbsPath(s.Config.PublishDir)
}
func (s *Site) checkDirectories() (err error) {
/*
if b, _ := dirExists(s.absLayoutDir()); !b {
return fmt.Errorf("No layout directory found, expecting to find it at " + s.absLayoutDir())
}
*/
if b, _ := dirExists(s.absContentDir()); !b {
return fmt.Errorf("No source directory found, expecting to find it at " + s.absContentDir())
}
return
}
func (s *Site) ProcessShortcodes() {
for i, _ := range s.Pages {
s.Pages[i].Content = template.HTML(ShortcodesHandle(string(s.Pages[i].Content), s.Pages[i], s.Tmpl))
for _, page := range s.Pages {
page.Content = template.HTML(ShortcodesHandle(string(page.Content), page, s.Tmpl))
page.Summary = template.HTML(ShortcodesHandle(string(page.Summary), page, s.Tmpl))
}
}
func (s *Site) AbsUrlify() {
for i, _ := range s.Pages {
content := string(s.Pages[i].Content)
content = strings.Replace(content, " src=\"/", " src=\""+s.c.BaseUrl+"/", -1)
content = strings.Replace(content, " src='/", " src='"+s.c.BaseUrl+"/", -1)
content = strings.Replace(content, " href='/", " href='"+s.c.BaseUrl+"/", -1)
content = strings.Replace(content, " href=\"/", " href=\""+s.c.BaseUrl+"/", -1)
s.Pages[i].Content = template.HTML(content)
func (s *Site) CreatePages() (err error) {
if s.Source == nil {
panic(fmt.Sprintf("s.Source not set %s", s.absContentDir()))
}
}
func (s *Site) CreatePages() {
for _, fileName := range s.Files {
page := NewPage(fileName)
if len(s.Source.Files()) < 1 {
return fmt.Errorf("No source files found in %s", s.absContentDir())
}
for _, file := range s.Source.Files() {
page, err := ReadFrom(file.Contents, file.LogicalName)
if err != nil {
return err
}
page.Site = s.Info
page.Tmpl = s.Tmpl
s.setOutFile(page)
if s.c.BuildDrafts || !page.Draft {
page.Section = file.Section
page.Dir = file.Dir
if s.Config.BuildDrafts || !page.Draft {
s.Pages = append(s.Pages, page)
}
}
s.Pages.Sort()
}
func (s *Site) setupPrevNext() {
for i, _ := range s.Pages {
if i < len(s.Pages)-1 {
s.Pages[i].Next = s.Pages[i+1]
}
if i > 0 {
s.Pages[i].Prev = s.Pages[i-1]
}
}
return
}
func (s *Site) BuildSiteMeta() (err error) {
s.Indexes = make(IndexList)
s.Sections = make(Index)
for _, plural := range s.c.Indexes {
for _, plural := range s.Config.Indexes {
s.Indexes[plural] = make(Index)
for i, p := range s.Pages {
for _, p := range s.Pages {
vals := p.GetParam(plural)
weight := p.GetParam(plural + "_weight")
if weight == nil {
weight = 0
}
if vals != nil {
for _, idx := range vals.([]string) {
s.Indexes[plural].Add(idx, s.Pages[i])
v, ok := vals.([]string)
if ok {
for _, idx := range v {
x := WeightedIndexEntry{weight.(int), p}
s.Indexes[plural].Add(idx, x)
}
} else {
if s.Config.Verbose {
fmt.Fprintf(os.Stderr, "Invalid %s in %s\n", plural, p.File.FileName)
}
}
}
}
@@ -255,196 +330,296 @@ func (s *Site) BuildSiteMeta() (err error) {
}
for i, p := range s.Pages {
sect := p.GetSection()
s.Sections.Add(sect, s.Pages[i])
s.Sections.Add(p.Section, WeightedIndexEntry{s.Pages[i].Weight, s.Pages[i]})
}
for k, _ := range s.Sections {
s.Sections[k].Sort()
}
s.Info.Indexes = s.Indexes.BuildOrderedIndexList()
s.Info.Indexes = s.Indexes
if len(s.Pages) == 0 {
return errors.New(fmt.Sprintf("Unable to build site metadata, no pages found in directory %s", s.c.ContentDir))
return
}
s.Info.LastChange = s.Pages[0].Date
// populate pages with site metadata
for _, p := range s.Pages {
p.Site = s.Info
}
return
}
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() {
func (s *Site) possibleIndexes() (indexes []string) {
for _, p := range s.Pages {
s.WritePublic(p.Section+slash+p.OutFile, p.RenderedContent.Bytes())
}
}
func (s *Site) setOutFile(p *Page) {
if len(strings.TrimSpace(p.Slug)) > 0 {
// Use Slug if provided
if s.c.UglyUrls {
p.OutFile = strings.TrimSpace(p.Slug + "." + p.Extension)
} else {
p.OutFile = strings.TrimSpace(p.Slug + "/index.html")
}
} 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)
if s.c.UglyUrls {
p.OutFile = replaceExtension(strings.TrimSpace(t), p.Extension)
} else {
file, _ := fileExt(strings.TrimSpace(t))
p.OutFile = file + "/index." + p.Extension
for k, _ := range p.Params {
if !inStringArray(indexes, k) {
indexes = append(indexes, k)
}
}
}
return
}
func (s *Site) RenderIndexes() {
for singular, plural := range s.c.Indexes {
func inStringArray(arr []string, el string) bool {
for _, v := range arr {
if v == el {
return true
}
}
return false
}
func (s *Site) RenderAliases() error {
for _, p := range s.Pages {
for _, a := range p.Aliases {
plink, err := p.Permalink()
if err != nil {
return err
}
if err := s.WriteAlias(a, template.HTML(plink)); err != nil {
return err
}
}
}
return nil
}
func (s *Site) RenderPages() (err error) {
for _, p := range s.Pages {
var layout []string
if !p.IsRenderable() {
self := "__" + p.TargetPath()
_, err := s.Tmpl.New(self).Parse(string(p.Content))
if err != nil {
return err
}
layout = append(layout, self)
} else {
layout = append(layout, p.Layout()...)
layout = append(layout, "_default/single.html")
}
err := s.render(p, p.TargetPath(), layout...)
if err != nil {
return err
}
}
return nil
}
func (s *Site) RenderIndexes() error {
for singular, plural := range s.Config.Indexes {
for k, o := range s.Indexes[plural] {
n := s.NewNode()
n.Title = strings.Title(k)
url := Urlize(plural + "/" + k)
plink := url
if s.c.UglyUrls {
n.Url = url + ".html"
plink = n.Url
} else {
n.Url = url + "/index.html"
}
n.Permalink = template.HTML(MakePermalink(string(n.Site.BaseUrl), string(plink)))
n.RSSlink = template.HTML(MakePermalink(string(n.Site.BaseUrl), string(url+".xml")))
n.Date = o[0].Date
url := helpers.Urlize(plural + "/" + k)
n.Url = url + ".html"
plink := n.Url
n.Permalink = permalink(s, plink)
n.RSSlink = permalink(s, url+".xml")
n.Date = o[0].Page.Date
n.Data[singular] = o
n.Data["Pages"] = o
layout := "indexes" + slash + singular + ".html"
x := s.RenderThing(n, layout)
n.Data["Pages"] = o.Pages()
layout := "indexes/" + singular + ".html"
var base string
if s.c.UglyUrls {
base = plural + slash + k
} else {
base = plural + slash + k + slash + "index"
base = plural + "/" + k
err := s.render(n, base+".html", layout)
if err != nil {
return err
}
s.WritePublic(base+".html", x.Bytes())
if a := s.Tmpl.Lookup("rss.xml"); a != nil {
// XML Feed
y := s.NewXMLBuffer()
if s.c.UglyUrls {
n.Url = Urlize(plural + "/" + k + ".xml")
} else {
n.Url = Urlize(plural + "/" + k + "/index.xml")
n.Url = helpers.Urlize(plural + "/" + k + ".xml")
n.Permalink = permalink(s, n.Url)
err := s.render(n, base+".xml", "rss.xml")
if err != nil {
return err
}
n.Permalink = template.HTML(string(n.Site.BaseUrl) + n.Url)
s.Tmpl.ExecuteTemplate(y, "rss.xml", n)
s.WritePublic(base+".xml", y.Bytes())
}
}
}
return nil
}
func (s *Site) RenderIndexesIndexes() {
layout := "indexes" + slash + "indexes.html"
func (s *Site) RenderIndexesIndexes() (err error) {
layout := "indexes/indexes.html"
if s.Tmpl.Lookup(layout) != nil {
for singular, plural := range s.c.Indexes {
for singular, plural := range s.Config.Indexes {
n := s.NewNode()
n.Title = strings.Title(plural)
url := Urlize(plural)
url := helpers.Urlize(plural)
n.Url = url + "/index.html"
n.Permalink = template.HTML(MakePermalink(string(n.Site.BaseUrl), string(n.Url)))
n.Permalink = permalink(s, n.Url)
n.Data["Singular"] = singular
n.Data["Plural"] = plural
n.Data["Index"] = s.Indexes[plural]
n.Data["OrderedIndex"] = s.Info.Indexes[plural]
fmt.Println(s.Info.Indexes)
// keep the following just for legacy reasons
n.Data["OrderedIndex"] = s.Indexes[plural]
x := s.RenderThing(n, layout)
s.WritePublic(plural+slash+"index.html", x.Bytes())
err := s.render(n, plural+"/index.html", layout)
if err != nil {
return err
}
}
}
return
}
func (s *Site) RenderLists() {
func (s *Site) RenderLists() error {
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+".xml")))
n.Date = data[0].Date
n.Data["Pages"] = data
n.Url = helpers.Urlize(section + "/" + "index.html")
n.Permalink = permalink(s, n.Url)
n.RSSlink = permalink(s, section+".xml")
n.Date = data[0].Page.Date
n.Data["Pages"] = data.Pages()
layout := "indexes/" + section + ".html"
x := s.RenderThing(n, layout)
s.WritePublic(section+slash+"index.html", x.Bytes())
err := s.render(n, section, layout, "_default/indexes.html")
if err != nil {
return err
}
if a := s.Tmpl.Lookup("rss.xml"); a != nil {
// XML Feed
n.Url = Urlize(section + ".xml")
n.Url = helpers.Urlize(section + ".xml")
n.Permalink = template.HTML(string(n.Site.BaseUrl) + n.Url)
y := s.NewXMLBuffer()
s.Tmpl.ExecuteTemplate(y, "rss.xml", n)
s.WritePublic(section+slash+"index.xml", y.Bytes())
err = s.render(n, section+".xml", "rss.xml")
if err != nil {
return err
}
}
}
return nil
}
func (s *Site) RenderHomePage() {
func (s *Site) RenderHomePage() error {
n := s.NewNode()
n.Title = n.Site.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]
n.Url = helpers.Urlize(string(n.Site.BaseUrl))
n.RSSlink = permalink(s, "index.xml")
n.Permalink = permalink(s, "")
n.Data["Pages"] = s.Pages
err := s.render(n, "/", "index.html")
if err != nil {
return err
}
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")
n.Url = helpers.Urlize("index.xml")
n.Title = "Recent Content"
n.Permalink = template.HTML(string(n.Site.BaseUrl) + "index.xml")
y := s.NewXMLBuffer()
s.Tmpl.ExecuteTemplate(y, "rss.xml", n)
s.WritePublic("index.xml", y.Bytes())
n.Permalink = permalink(s, "index.xml")
err := s.render(n, ".xml", "rss.xml")
if err != nil {
return err
}
}
if a := s.Tmpl.Lookup("404.html"); a != nil {
n.Url = helpers.Urlize("404.html")
n.Title = "404 Page not found"
n.Permalink = permalink(s, "404.html")
return s.render(n, "404.html", "404.html")
}
return nil
}
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)
for _, pl := range s.Config.Indexes {
fmt.Printf("%d %s index 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
func permalink(s *Site, plink string) template.HTML {
base, err := url.Parse(string(s.Config.BaseUrl))
if err != nil {
panic(err)
}
return y
path, err := url.Parse(plink)
if err != nil {
panic(err)
}
return template.HTML(MakePermalink(base, path).String())
}
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) NewNode() *Node {
return &Node{
Data: make(map[string]interface{}),
Site: s.Info,
}
}
func (s *Site) render(d interface{}, out string, layouts ...string) (err error) {
layout := s.findFirstLayout(layouts...)
if layout == "" {
if s.Config.Verbose {
fmt.Printf("Unable to locate layout: %s\n", layouts)
}
return
}
absURL, err := transform.AbsURL(s.Config.BaseUrl)
if err != nil {
return
}
transformer := transform.NewChain(absURL...)
var renderBuffer *bytes.Buffer
if strings.HasSuffix(out, ".xml") {
renderBuffer = s.NewXMLBuffer()
} else {
renderBuffer = new(bytes.Buffer)
}
err = s.renderThing(d, layout, renderBuffer)
if err != nil {
// Behavior here should be dependent on if running in server or watch mode.
fmt.Println(fmt.Errorf("Rendering error: %v", err))
if !s.Running() {
os.Exit(-1)
}
}
var outBuffer = new(bytes.Buffer)
if strings.HasSuffix(out, ".xml") {
outBuffer = renderBuffer
} else {
transformer.Apply(outBuffer, renderBuffer)
}
return s.WritePublic(out, outBuffer)
}
func (s *Site) findFirstLayout(layouts ...string) (layout string) {
for _, layout = range layouts {
if s.Tmpl.Lookup(layout) != nil {
return
}
}
return ""
}
func (s *Site) renderThing(d interface{}, layout string, w io.Writer) error {
// If the template doesn't exist, then return, but leave the Writer open
if s.Tmpl.Lookup(layout) == nil {
return fmt.Errorf("Layout not found: %s", layout)
}
//defer w.Close()
return s.Tmpl.ExecuteTemplate(w, layout, d)
}
func (s *Site) NewXMLBuffer() *bytes.Buffer {
@@ -452,23 +627,35 @@ func (s *Site) NewXMLBuffer() *bytes.Buffer {
return bytes.NewBufferString(header)
}
func (s *Site) WritePublic(path string, content []byte) {
func (s *Site) initTarget() {
if s.Target == nil {
s.Target = &target.Filesystem{
PublishDir: s.absPublishDir(),
UglyUrls: s.Config.UglyUrls,
}
}
}
if s.c.Verbose {
func (s *Site) WritePublic(path string, reader io.Reader) (err error) {
s.initTarget()
if s.Config.Verbose {
fmt.Println(path)
}
return s.Target.Publish(path, reader)
}
func (s *Site) WriteAlias(path string, permalink template.HTML) (err error) {
if s.Alias == nil {
s.initTarget()
s.Alias = &target.HTMLRedirectAlias{
PublishDir: s.absPublishDir(),
}
}
if s.Config.Verbose {
fmt.Println(path)
}
path, filename := filepath.Split(path)
path = filepath.FromSlash(s.c.GetAbsPath(filepath.Join(s.c.PublishDir, path)))
err := mkdirIf(path)
if err != nil {
fmt.Println(err)
}
file, _ := os.Create(filepath.Join(path, filename))
defer file.Close()
file.Write(content)
return s.Alias.Publish(path, permalink)
}
+101
View File
@@ -0,0 +1,101 @@
package hugolib
import (
"bytes"
"github.com/spf13/hugo/source"
"github.com/spf13/hugo/target"
"testing"
)
const ALIAS_DOC_1 = "---\ntitle: alias doc\naliases:\n - \"alias1/\"\n - \"alias-2/\"\n---\naliases\n"
var fakeSource = []source.ByteSource{
{
Name: "foo/bar/file.md",
Content: []byte(SIMPLE_PAGE),
},
{
Name: "alias/test/file1.md",
Content: []byte(ALIAS_DOC_1),
},
{
Name: "section/somecontent.html",
Content: []byte(RENDER_NO_FRONT_MATTER),
},
}
func checkShowPlanExpected(t *testing.T, s *Site, expected string) {
out := new(bytes.Buffer)
if err := s.ShowPlan(out); err != nil {
t.Fatalf("ShowPlan unexpectedly returned an error: %s", err)
}
got := out.String()
if got != expected {
t.Errorf("ShowPlan expected:\n%q\ngot\n%q", expected, got)
}
}
func TestDegenerateNoFiles(t *testing.T) {
checkShowPlanExpected(t, new(Site), "No source files provided.\n")
}
func TestDegenerateNoTarget(t *testing.T) {
s := &Site{
Source: &source.InMemorySource{fakeSource},
}
must(s.CreatePages())
expected := "foo/bar/file.md (renderer: markdown)\n canonical => !no target specified!\n\n" +
"alias/test/file1.md (renderer: markdown)\n canonical => !no target specified!\n\n" +
"section/somecontent.html (renderer: n/a)\n canonical => !no target specified!\n\n"
checkShowPlanExpected(t, s, expected)
}
func TestFileTarget(t *testing.T) {
s := &Site{
Source: &source.InMemorySource{fakeSource},
Target: new(target.Filesystem),
Alias: new(target.HTMLRedirectAlias),
}
must(s.CreatePages())
expected := "foo/bar/file.md (renderer: markdown)\n canonical => foo/bar/file/index.html\n\n" +
"alias/test/file1.md (renderer: markdown)\n" +
" canonical => alias/test/file1/index.html\n" +
" alias1/ => alias1/index.html\n" +
" alias-2/ => alias-2/index.html\n\n" +
"section/somecontent.html (renderer: n/a)\n canonical => section/somecontent/index.html\n\n"
checkShowPlanExpected(t, s, expected)
}
func TestFileTargetUgly(t *testing.T) {
s := &Site{
Target: &target.Filesystem{UglyUrls: true},
Source: &source.InMemorySource{fakeSource},
Alias: new(target.HTMLRedirectAlias),
}
s.CreatePages()
expected := "foo/bar/file.md (renderer: markdown)\n canonical => foo/bar/file.html\n\n" +
"alias/test/file1.md (renderer: markdown)\n" +
" canonical => alias/test/file1.html\n" +
" alias1/ => alias1/index.html\n" +
" alias-2/ => alias-2/index.html\n\n" +
"section/somecontent.html (renderer: n/a)\n canonical => section/somecontent.html\n\n"
checkShowPlanExpected(t, s, expected)
}
func TestFileTargetPublishDir(t *testing.T) {
s := &Site{
Target: &target.Filesystem{PublishDir: "../public"},
Source: &source.InMemorySource{fakeSource},
Alias: &target.HTMLRedirectAlias{PublishDir: "../public"},
}
must(s.CreatePages())
expected := "foo/bar/file.md (renderer: markdown)\n canonical => ../public/foo/bar/file/index.html\n\n" +
"alias/test/file1.md (renderer: markdown)\n" +
" canonical => ../public/alias/test/file1/index.html\n" +
" alias1/ => ../public/alias1/index.html\n" +
" alias-2/ => ../public/alias-2/index.html\n\n" +
"section/somecontent.html (renderer: n/a)\n canonical => ../public/section/somecontent/index.html\n\n"
checkShowPlanExpected(t, s, expected)
}
+456
View File
@@ -0,0 +1,456 @@
package hugolib
import (
"bytes"
"fmt"
"github.com/spf13/hugo/source"
"github.com/spf13/hugo/target"
"html/template"
"io"
"strings"
"testing"
)
const (
TEMPLATE_TITLE = "{{ .Title }}"
PAGE_SIMPLE_TITLE = `---
title: simple template
---
content`
TEMPLATE_MISSING_FUNC = "{{ .Title | funcdoesnotexists }}"
TEMPLATE_FUNC = "{{ .Title | urlize }}"
TEMPLATE_CONTENT = "{{ .Content }}"
TEMPLATE_DATE = "{{ .Date }}"
INVALID_TEMPLATE_FORMAT_DATE = "{{ .Date.Format time.RFC3339 }}"
TEMPLATE_WITH_URL_REL = "<a href=\"foobar.jpg\">Going</a>"
TEMPLATE_WITH_URL_ABS = "<a href=\"/foobar.jpg\">Going</a>"
PAGE_URL_SPECIFIED = `---
title: simple template
url: "mycategory/my-whatever-content/"
---
content`
PAGE_WITH_MD = `---
title: page with md
---
# heading 1
text
## heading 2
more text
`
)
func pageMust(p *Page, err error) *Page {
if err != nil {
panic(err)
}
return p
}
func TestDegenerateRenderThingMissingTemplate(t *testing.T) {
p, _ := ReadFrom(strings.NewReader(PAGE_SIMPLE_TITLE), "content/a/file.md")
s := new(Site)
s.prepTemplates()
err := s.renderThing(p, "foobar", nil)
if err == nil {
t.Errorf("Expected err to be returned when missing the template.")
}
}
func TestAddInvalidTemplate(t *testing.T) {
s := new(Site)
s.prepTemplates()
err := s.addTemplate("missing", TEMPLATE_MISSING_FUNC)
if err == nil {
t.Fatalf("Expecting the template to return an error")
}
}
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error { return nil }
func NopCloser(w io.Writer) io.WriteCloser {
return nopCloser{w}
}
func matchRender(t *testing.T, s *Site, p *Page, tmplName string, expected string) {
content := new(bytes.Buffer)
err := s.renderThing(p, tmplName, NopCloser(content))
if err != nil {
t.Fatalf("Unable to render template.")
}
if string(content.Bytes()) != expected {
t.Fatalf("Content did not match expected: %s. got: %s", expected, content)
}
}
func TestRenderThing(t *testing.T) {
tests := []struct {
content string
template string
expected string
}{
{PAGE_SIMPLE_TITLE, TEMPLATE_TITLE, "simple template"},
{PAGE_SIMPLE_TITLE, TEMPLATE_FUNC, "simple-template"},
{PAGE_WITH_MD, TEMPLATE_CONTENT, "<h1>heading 1</h1>\n\n<p>text</p>\n\n<h2>heading 2</h2>\n\n<p>more text</p>\n"},
{SIMPLE_PAGE_RFC3339_DATE, TEMPLATE_DATE, "2013-05-17 16:59:30 &#43;0000 UTC"},
}
s := new(Site)
s.prepTemplates()
for i, test := range tests {
p, err := ReadFrom(strings.NewReader(test.content), "content/a/file.md")
if err != nil {
t.Fatalf("Error parsing buffer: %s", err)
}
templateName := fmt.Sprintf("foobar%d", i)
err = s.addTemplate(templateName, test.template)
if err != nil {
t.Fatalf("Unable to add template")
}
p.Content = template.HTML(p.Content)
html := new(bytes.Buffer)
err = s.renderThing(p, templateName, NopCloser(html))
if err != nil {
t.Errorf("Unable to render html: %s", err)
}
if string(html.Bytes()) != test.expected {
t.Errorf("Content does not match.\nExpected\n\t'%q'\ngot\n\t'%q'", test.expected, html)
}
}
}
func HTML(in string) string {
return in
}
func TestRenderThingOrDefault(t *testing.T) {
tests := []struct {
content string
missing bool
template string
expected string
}{
{PAGE_SIMPLE_TITLE, true, TEMPLATE_TITLE, HTML("simple template")},
{PAGE_SIMPLE_TITLE, true, TEMPLATE_FUNC, HTML("simple-template")},
{PAGE_SIMPLE_TITLE, false, TEMPLATE_TITLE, HTML("simple template")},
{PAGE_SIMPLE_TITLE, false, TEMPLATE_FUNC, HTML("simple-template")},
}
files := make(map[string][]byte)
target := &target.InMemoryTarget{Files: files}
s := &Site{
Target: target,
}
s.prepTemplates()
for i, test := range tests {
p, err := ReadFrom(strings.NewReader(PAGE_SIMPLE_TITLE), "content/a/file.md")
if err != nil {
t.Fatalf("Error parsing buffer: %s", err)
}
templateName := fmt.Sprintf("default%d", i)
err = s.addTemplate(templateName, test.template)
if err != nil {
t.Fatalf("Unable to add template")
}
var err2 error
if test.missing {
err2 = s.render(p, "out", "missing", templateName)
} else {
err2 = s.render(p, "out", templateName, "missing_default")
}
if err2 != nil {
t.Errorf("Unable to render html: %s", err)
}
if string(files["out"]) != test.expected {
t.Errorf("Content does not match. Expected '%s', got '%s'", test.expected, files["out"])
}
}
}
func TestTargetPath(t *testing.T) {
tests := []struct {
doc string
content string
expectedOutFile string
expectedSection string
}{
{"content/a/file.md", PAGE_URL_SPECIFIED, "mycategory/my-whatever-content/index.html", "a"},
{"content/x/y/deepfile.md", SIMPLE_PAGE, "x/y/deepfile.html", "x/y"},
{"content/x/y/z/deeperfile.md", SIMPLE_PAGE, "x/y/z/deeperfile.html", "x/y/z"},
{"content/b/file.md", SIMPLE_PAGE, "b/file.html", "b"},
{"a/file.md", SIMPLE_PAGE, "a/file.html", "a"},
{"file.md", SIMPLE_PAGE, "file.html", ""},
}
if true {
return
}
for _, test := range tests {
s := &Site{
Config: Config{ContentDir: "content"},
}
p := pageMust(ReadFrom(strings.NewReader(test.content), s.Config.GetAbsPath(test.doc)))
expected := test.expectedOutFile
if p.TargetPath() != expected {
t.Errorf("%s => OutFile expected: '%s', got: '%s'", test.doc, expected, p.TargetPath())
}
if p.Section != test.expectedSection {
t.Errorf("%s => p.Section expected: %s, got: %s", test.doc, test.expectedSection, p.Section)
}
}
}
func TestSkipRender(t *testing.T) {
files := make(map[string][]byte)
target := &target.InMemoryTarget{Files: files}
sources := []source.ByteSource{
{"sect/doc1.html", []byte("---\nmarkup: markdown\n---\n# title\nsome *content*"), "sect"},
{"sect/doc2.html", []byte("<!doctype html><html><body>more content</body></html>"), "sect"},
{"sect/doc3.md", []byte("# doc3\n*some* content"), "sect"},
{"sect/doc4.md", []byte("---\ntitle: doc4\n---\n# doc4\n*some content*"), "sect"},
{"sect/doc5.html", []byte("<!doctype html><html>{{ template \"head\" }}<body>body5</body></html>"), "sect"},
{"sect/doc6.html", []byte("<!doctype html><html>{{ template \"head_abs\" }}<body>body5</body></html>"), "sect"},
{"doc7.html", []byte("<html><body>doc7 content</body></html>"), ""},
}
s := &Site{
Target: target,
Config: Config{Verbose: true, BaseUrl: "http://auth/bub"},
Source: &source.InMemorySource{sources},
}
s.initializeSiteInfo()
s.prepTemplates()
must(s.addTemplate("_default/single.html", "{{.Content}}"))
must(s.addTemplate("head", "<head><script src=\"script.js\"></script></head>"))
must(s.addTemplate("head_abs", "<head><script src=\"/script.js\"></script></head>"))
if err := s.CreatePages(); err != nil {
t.Fatalf("Unable to create pages: %s", err)
}
if err := s.BuildSiteMeta(); err != nil {
t.Fatalf("Unable to build site metadata: %s", err)
}
if err := s.RenderPages(); err != nil {
t.Fatalf("Unable to render pages. %s", err)
}
tests := []struct {
doc string
expected string
}{
{"sect/doc1.html", "<h1>title</h1>\n\n<p>some <em>content</em></p>\n"},
{"sect/doc2.html", "<!doctype html><html><body>more content</body></html>"},
{"sect/doc3.html", "<h1>doc3</h1>\n\n<p><em>some</em> content</p>\n"},
{"sect/doc4.html", "<h1>doc4</h1>\n\n<p><em>some content</em></p>\n"},
{"sect/doc5.html", "<!doctype html><html><head><script src=\"script.js\"></script></head><body>body5</body></html>"},
{"sect/doc6.html", "<!doctype html><html><head><script src=\"http://auth/bub/script.js\"></script></head><body>body5</body></html>"},
{"doc7.html", "<html><body>doc7 content</body></html>"},
}
for _, test := range tests {
content, ok := target.Files[test.doc]
if !ok {
t.Fatalf("Did not find %s in target. %v", test.doc, target.Files)
}
if !bytes.Equal(content, []byte(test.expected)) {
t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, string(content))
}
}
}
func TestAbsUrlify(t *testing.T) {
files := make(map[string][]byte)
target := &target.InMemoryTarget{Files: files}
sources := []source.ByteSource{
{"sect/doc1.html", []byte("<!doctype html><html><head></head><body><a href=\"#frag1\">link</a></body></html>"), "sect"},
{"content/blue/doc2.html", []byte("---\nf: t\n---\n<!doctype html><html><body>more content</body></html>"), "blue"},
}
s := &Site{
Target: target,
Config: Config{BaseUrl: "http://auth/bub"},
Source: &source.InMemorySource{sources},
}
s.initializeSiteInfo()
s.prepTemplates()
must(s.addTemplate("blue/single.html", TEMPLATE_WITH_URL_ABS))
if err := s.CreatePages(); err != nil {
t.Fatalf("Unable to create pages: %s", err)
}
if err := s.BuildSiteMeta(); err != nil {
t.Fatalf("Unable to build site metadata: %s", err)
}
if err := s.RenderPages(); err != nil {
t.Fatalf("Unable to render pages. %s", err)
}
tests := []struct {
file, expected string
}{
{"content/blue/doc2.html", "<a href=\"http://auth/bub/foobar.jpg\">Going</a>"},
{"sect/doc1.html", "<!doctype html><html><head></head><body><a href=\"#frag1\">link</a></body></html>"},
}
for _, test := range tests {
content, ok := target.Files[test.file]
if !ok {
t.Fatalf("Unable to locate rendered content: %s", test.file)
}
expected := test.expected
if string(content) != expected {
t.Errorf("AbsUrlify content expected:\n%q\ngot\n%q", expected, string(content))
}
}
}
var WEIGHTED_PAGE_1 = []byte(`+++
weight = "2"
title = "One"
+++
Front Matter with Ordered Pages`)
var WEIGHTED_PAGE_2 = []byte(`+++
weight = "6"
title = "Two"
+++
Front Matter with Ordered Pages 2`)
var WEIGHTED_PAGE_3 = []byte(`+++
weight = "4"
title = "Three"
date = "2012-04-06"
+++
Front Matter with Ordered Pages 3`)
var WEIGHTED_PAGE_4 = []byte(`+++
weight = "4"
title = "Four"
date = "2012-01-01"
+++
Front Matter with Ordered Pages 4`)
var WEIGHTED_SOURCES = []source.ByteSource{
{"sect/doc1.md", WEIGHTED_PAGE_1, "sect"},
{"sect/doc2.md", WEIGHTED_PAGE_2, "sect"},
{"sect/doc3.md", WEIGHTED_PAGE_3, "sect"},
{"sect/doc4.md", WEIGHTED_PAGE_4, "sect"},
}
func TestOrderedPages(t *testing.T) {
files := make(map[string][]byte)
target := &target.InMemoryTarget{Files: files}
s := &Site{
Target: target,
Config: Config{BaseUrl: "http://auth/bub/"},
Source: &source.InMemorySource{WEIGHTED_SOURCES},
}
s.initializeSiteInfo()
if err := s.CreatePages(); err != nil {
t.Fatalf("Unable to create pages: %s", err)
}
if err := s.BuildSiteMeta(); err != nil {
t.Fatalf("Unable to build site metadata: %s", err)
}
if s.Sections["sect"][0].Weight != 2 || s.Sections["sect"][3].Weight != 6 {
t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", 2, s.Sections["sect"][0].Weight)
}
if s.Sections["sect"][1].Page.Title != "Three" || s.Sections["sect"][2].Page.Title != "Four" {
t.Errorf("Pages in unexpected order. Second should be '%s', got '%s'", "Three", s.Sections["sect"][1].Page.Title)
}
}
var PAGE_WITH_WEIGHTED_INDEXES_2 = []byte(`+++
tags = [ "a", "b", "c" ]
tags_weight = 22
categories = ["d"]
title = "foo"
categories_weight = 44
+++
Front Matter with weighted tags and categories`)
var PAGE_WITH_WEIGHTED_INDEXES_1 = []byte(`+++
tags = [ "a" ]
tags_weight = 33
title = "bar"
categories = [ "d", "e" ]
categories_weight = 11
alias = "spf13"
date = 1979-05-27T07:32:00Z
+++
Front Matter with weighted tags and categories`)
var PAGE_WITH_WEIGHTED_INDEXES_3 = []byte(`+++
title = "bza"
categories = [ "e" ]
categories_weight = 11
alias = "spf13"
date = 2010-05-27T07:32:00Z
+++
Front Matter with weighted tags and categories`)
func TestWeightedIndexes(t *testing.T) {
files := make(map[string][]byte)
target := &target.InMemoryTarget{Files: files}
sources := []source.ByteSource{
{"sect/doc1.md", PAGE_WITH_WEIGHTED_INDEXES_1, "sect"},
{"sect/doc2.md", PAGE_WITH_WEIGHTED_INDEXES_2, "sect"},
{"sect/doc3.md", PAGE_WITH_WEIGHTED_INDEXES_3, "sect"},
}
indexes := make(map[string]string)
indexes["tag"] = "tags"
indexes["category"] = "categories"
s := &Site{
Target: target,
Config: Config{BaseUrl: "http://auth/bub/", Indexes: indexes},
Source: &source.InMemorySource{sources},
}
s.initializeSiteInfo()
if err := s.CreatePages(); err != nil {
t.Fatalf("Unable to create pages: %s", err)
}
if err := s.BuildSiteMeta(); err != nil {
t.Fatalf("Unable to build site metadata: %s", err)
}
if s.Indexes["tags"]["a"][0].Page.Title != "foo" {
t.Errorf("Pages in unexpected order, 'foo' expected first, got '%v'", s.Indexes["tags"]["a"][0].Page.Title)
}
if s.Indexes["categories"]["d"][0].Page.Title != "bar" {
t.Errorf("Pages in unexpected order, 'bar' expected first, got '%v'", s.Indexes["categories"]["d"][0].Page.Title)
}
if s.Indexes["categories"]["e"][0].Page.Title != "bza" {
t.Errorf("Pages in unexpected order, 'bza' expected first, got '%v'", s.Indexes["categories"]["e"][0].Page.Title)
}
}
+99
View File
@@ -0,0 +1,99 @@
package hugolib
import (
"github.com/spf13/hugo/source"
"github.com/spf13/hugo/target"
"html/template"
"testing"
)
const SLUG_DOC_1 = "---\ntitle: slug doc 1\nslug: slug-doc-1\naliases:\n - sd1/foo/\n - sd2\n - sd3/\n - sd4.html\n---\nslug doc 1 content\n"
const SLUG_DOC_2 = `---
title: slug doc 2
slug: slug-doc-2
---
slug doc 2 content
`
const INDEX_TEMPLATE = "{{ range .Data.Pages }}.{{ end }}"
func must(err error) {
if err != nil {
panic(err)
}
}
func mustReturn(ret *Page, err error) *Page {
if err != nil {
panic(err)
}
return ret
}
type InMemoryAliasTarget struct {
target.HTMLRedirectAlias
files map[string][]byte
}
func (t *InMemoryAliasTarget) Publish(label string, permalink template.HTML) (err error) {
f, _ := t.Translate(label)
t.files[f] = []byte("--dummy text--")
return
}
var urlFakeSource = []source.ByteSource{
{"content/blue/doc1.md", []byte(SLUG_DOC_1), "blue"},
{"content/blue/doc2.md", []byte(SLUG_DOC_2), "blue"},
}
func TestPageCount(t *testing.T) {
files := make(map[string][]byte)
target := &target.InMemoryTarget{Files: files}
alias := &InMemoryAliasTarget{files: files}
s := &Site{
Target: target,
Alias: alias,
Config: Config{UglyUrls: false},
Source: &source.InMemorySource{urlFakeSource},
}
s.initializeSiteInfo()
s.prepTemplates()
must(s.addTemplate("indexes/blue.html", INDEX_TEMPLATE))
if err := s.CreatePages(); err != nil {
t.Errorf("Unable to create pages: %s", err)
}
if err := s.BuildSiteMeta(); err != nil {
t.Errorf("Unable to build site metadata: %s", err)
}
if err := s.RenderLists(); err != nil {
t.Errorf("Unable to render site lists: %s", err)
}
if err := s.RenderAliases(); err != nil {
t.Errorf("Unable to render site lists: %s", err)
}
blueIndex := target.Files["blue"]
if blueIndex == nil {
t.Errorf("No indexed rendered. %v", target.Files)
}
expected := ".."
if string(blueIndex) != expected {
t.Errorf("Index template does not match expected: %q, got: %q", expected, string(blueIndex))
}
for _, s := range []string{
"sd1/foo/index.html",
"sd2/index.html",
"sd3/index.html",
"sd4.html",
} {
if _, ok := target.Files[s]; !ok {
t.Errorf("No alias rendered: %s", s)
}
}
}
+32
View File
@@ -0,0 +1,32 @@
package hugolib
import (
"testing"
"bytes"
)
const SITE_INFO_PARAM_TEMPLATE = `{{ .Site.Params.MyGlobalParam }}`
func TestSiteInfoParams(t *testing.T) {
s := &Site{
Config: Config{Params: map[string]interface{}{"MyGlobalParam": "FOOBAR_PARAM"}},
}
s.initialize()
if s.Info.Params["MyGlobalParam"] != "FOOBAR_PARAM" {
t.Errorf("Unable to set site.Info.Param")
}
s.prepTemplates()
s.addTemplate("template", SITE_INFO_PARAM_TEMPLATE)
buf := new(bytes.Buffer)
err := s.renderThing(s.NewNode(), "template", buf)
if err != nil {
t.Errorf("Unable to render template: %s", err)
}
if buf.String() != "FOOBAR_PARAM" {
t.Errorf("Expected FOOBAR_PARAM: got %s", buf.String())
}
}
+75
View File
@@ -0,0 +1,75 @@
package hugolib
import (
"bytes"
"fmt"
"os/exec"
"strings"
)
var summaryLength = 70
var summaryDivider = []byte("<!--more-->")
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 RemoveSummaryDivider(content []byte) []byte {
return bytes.Replace(content, summaryDivider, []byte(""), -1)
}
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 getRstContent(content []byte) string {
cleanContent := bytes.Replace(content, summaryDivider, []byte(""), 1)
cmd := exec.Command("rst2html.py", "--leave-comments")
cmd.Stdin = bytes.NewReader(cleanContent)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
fmt.Println(err)
}
rstLines := strings.Split(out.String(), "\n")
for i, line := range rstLines {
if strings.HasPrefix(line, "<body>") {
rstLines = (rstLines[i+1 : len(rstLines)-3])
}
}
return strings.Join(rstLines, "\n")
}
+2 -206
View File
@@ -14,213 +14,9 @@
package main
import (
"fmt"
"github.com/howeyc/fsnotify"
"github.com/mostafah/fsync"
flag "github.com/ogier/pflag"
"github.com/spf13/hugo/hugolib"
"log"
"net/http"
"os"
"path/filepath"
"runtime/pprof"
"sync"
"time"
"github.com/spf13/hugo/commands"
)
var (
baseUrl = flag.StringP("base-url", "b", "", "hostname (and path) to the root eg. http://spf13.com/")
cfgfile = flag.String("config", "", "config file (default is path/config.yaml|json|toml)")
checkMode = flag.Bool("check", false, "analyze content and provide feedback")
draft = flag.BoolP("build-drafts", "D", false, "include content marked as draft")
help = flag.BoolP("help", "h", false, "show this help")
source = flag.StringP("source", "s", "", "filesystem path to read files relative from")
destination = flag.StringP("destination", "d", "", "filesystem path to write files to")
verbose = flag.BoolP("verbose", "v", false, "verbose output")
version = flag.Bool("version", false, "which version of hugo")
cpuprofile = flag.Int("profile", 0, "Number of times to create the site and profile it")
watchMode = flag.BoolP("watch", "w", false, "watch filesystem for changes and recreate as needed")
server = flag.BoolP("server", "S", false, "run a (very) simple web server")
port = flag.String("port", "1313", "port to run web server on, default :1313")
uglyUrls = flag.Bool("uglyurls", false, "use /filename.html instead of /filename/ ")
)
func usage() {
PrintErr("usage: hugo [flags]", "")
flag.PrintDefaults()
os.Exit(0)
}
func main() {
flag.Usage = usage
flag.Parse()
if *help {
usage()
}
config := hugolib.SetupConfig(cfgfile, source)
config.BuildDrafts = *draft
config.UglyUrls = *uglyUrls
config.Verbose = *verbose
if *baseUrl != "" {
config.BaseUrl = *baseUrl
} else if *server {
config.BaseUrl = "http://localhost:" + *port
}
if *destination != "" {
config.PublishDir = *destination
}
if *version {
fmt.Println("Hugo Static Site Generator v0.8")
}
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)
}
}
// Copy Static to Destination first
err := fsync.SyncDel(config.GetAbsPath(config.PublishDir+"/"), config.GetAbsPath(config.StaticDir+"/"))
if err != nil {
log.Fatalf("Error copying static files to %s: %v", config.GetAbsPath(config.PublishDir), err)
}
if *checkMode {
site := hugolib.NewSite(config)
site.Analyze()
os.Exit(0)
}
if *watchMode {
fmt.Println("Watching for changes. Press ctrl+c to stop")
_, err = buildSite(config)
if err != nil {
fmt.Println(err)
return
}
err := NewWatcher(config, *port, *server)
if err != nil {
fmt.Println(err)
}
}
if _, err = buildSite(config); err != nil {
fmt.Println(err)
}
if *server {
serve(*port, config)
}
}
func serve(port string, config *hugolib.Config) {
if config.Verbose {
fmt.Println("Serving pages from " + config.GetAbsPath(config.PublishDir))
}
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.GetAbsPath(config.PublishDir)))))
}
func buildSite(config *hugolib.Config) (site *hugolib.Site, err error) {
startTime := time.Now()
site = hugolib.NewSite(config)
err = site.Build()
if err != nil {
return
}
site.Stats()
fmt.Printf("in %v ms\n", int(1000*time.Since(startTime).Seconds()))
return site, nil
}
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
if c.Verbose {
fmt.Println(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.ContentDir), walker)
filepath.Walk(c.GetAbsPath(c.LayoutDir), walker)
return a
}
func PrintErr(str string, a ...interface{}) {
fmt.Fprintln(os.Stderr, str, a)
commands.Execute()
}
+263
View File
@@ -0,0 +1,263 @@
---
title: The Git Book - Long Text
---
# Getting Started #
This chapter will be about getting started with Git. We will begin at the beginning by explaining some background on version control tools, then move on to how to get Git running on your system and finally how to get it setup to start working with. At the end of this chapter you should understand why Git is around, why you should use it and you should be all setup to do so.
## About Version Control ##
What is version control, and why should you care? Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. Even though the examples in this book show software source code as the files under version control, in reality any type of file on a computer can be placed under version control.
If you are a graphic or web designer and want to keep every version of an image or layout (which you certainly would), it is very wise to use a Version Control System (VCS). A VCS allows you to: revert files back to a previous state, revert the entire project back to a previous state, review changes made over time, see who last modified something that might be causing a problem, who introduced an issue and when, and more. Using a VCS also means that if you screw things up or lose files, you can generally recover easily. In addition, you get all this for very little overhead.
### Local Version Control Systems ###
Many peoples version-control method of choice is to copy files into another directory (perhaps a time-stamped directory, if theyre clever). This approach is very common because it is so simple, but it is also incredibly error prone. It is easy to forget which directory youre in and accidentally write to the wrong file or copy over files you dont mean to.
To deal with this issue, programmers long ago developed local VCSs that had a simple database that kept all the changes to files under revision control (see Figure 1-1).
Insert 18333fig0101.png
Figure 1-1. Local version control diagram.
One of the more popular VCS tools was a system called rcs, which is still distributed with many computers today. Even the popular Mac OS X operating system includes the rcs command when you install the Developer Tools. This tool basically works by keeping patch sets (that is, the differences between files) from one revision to another in a special format on disk; it can then recreate what any file looked like at any point in time by adding up all the patches.
### Centralized Version Control Systems ###
The next major issue that people encounter is that they need to collaborate with developers on other systems. To deal with this problem, Centralized Version Control Systems (CVCSs) were developed. These systems, such as CVS, Subversion, and Perforce, have a single server that contains all the versioned files, and a number of clients that check out files from that central place. For many years, this has been the standard for version control (see Figure 1-2).
Insert 18333fig0102.png
Figure 1-2. Centralized version control diagram.
This setup offers many advantages, especially over local VCSs. For example, everyone knows to a certain degree what everyone else on the project is doing. Administrators have fine-grained control over who can do what; and its far easier to administer a CVCS than it is to deal with local databases on every client.
However, this setup also has some serious downsides. The most obvious is the single point of failure that the centralized server represents. If that server goes down for an hour, then during that hour nobody can collaborate at all or save versioned changes to anything theyre working on. If the hard disk the central database is on becomes corrupted, and proper backups havent been kept, you lose absolutely everything—the entire history of the project except whatever single snapshots people happen to have on their local machines. Local VCS systems suffer from this same problem—whenever you have the entire history of the project in a single place, you risk losing everything.
### Distributed Version Control Systems ###
This is where Distributed Version Control Systems (DVCSs) step in. In a DVCS (such as Git, Mercurial, Bazaar or Darcs), clients dont just check out the latest snapshot of the files: they fully mirror the repository. Thus if any server dies, and these systems were collaborating via it, any of the client repositories can be copied back up to the server to restore it. Every checkout is really a full backup of all the data (see Figure 1-3).
Insert 18333fig0103.png
Figure 1-3. Distributed version control diagram.
Furthermore, many of these systems deal pretty well with having several remote repositories they can work with, so you can collaborate with different groups of people in different ways simultaneously within the same project. This allows you to set up several types of workflows that arent possible in centralized systems, such as hierarchical models.
## A Short History of Git ##
As with many great things in life, Git began with a bit of creative destruction and fiery controversy. The Linux kernel is an open source software project of fairly large scope. For most of the lifetime of the Linux kernel maintenance (19912002), changes to the software were passed around as patches and archived files. In 2002, the Linux kernel project began using a proprietary DVCS system called BitKeeper.
In 2005, the relationship between the community that developed the Linux kernel and the commercial company that developed BitKeeper broke down, and the tools free-of-charge status was revoked. This prompted the Linux development community (and in particular Linus Torvalds, the creator of Linux) to develop their own tool based on some of the lessons they learned while using BitKeeper. Some of the goals of the new system were as follows:
* Speed
* Simple design
* Strong support for non-linear development (thousands of parallel branches)
* Fully distributed
* Able to handle large projects like the Linux kernel efficiently (speed and data size)
Since its birth in 2005, Git has evolved and matured to be easy to use and yet retain these initial qualities. Its incredibly fast, its very efficient with large projects, and it has an incredible branching system for non-linear development (See Chapter 3).
## Git Basics ##
So, what is Git in a nutshell? This is an important section to absorb, because if you understand what Git is and the fundamentals of how it works, then using Git effectively will probably be much easier for you. As you learn Git, try to clear your mind of the things you may know about other VCSs, such as Subversion and Perforce; doing so will help you avoid subtle confusion when using the tool. Git stores and thinks about information much differently than these other systems, even though the user interface is fairly similar; understanding those differences will help prevent you from becoming confused while using it.
### Snapshots, Not Differences ###
The major difference between Git and any other VCS (Subversion and friends included) is the way Git thinks about its data. Conceptually, most other systems store information as a list of file-based changes. These systems (CVS, Subversion, Perforce, Bazaar, and so on) think of the information they keep as a set of files and the changes made to each file over time, as illustrated in Figure 1-4.
Insert 18333fig0104.png
Figure 1-4. Other systems tend to store data as changes to a base version of each file.
Git doesnt think of or store its data this way. Instead, Git thinks of its data more like a set of snapshots of a mini filesystem. Every time you commit, or save the state of your project in Git, it basically takes a picture of what all your files look like at that moment and stores a reference to that snapshot. To be efficient, if files have not changed, Git doesnt store the file again—just a link to the previous identical file it has already stored. Git thinks about its data more like Figure 1-5.
Insert 18333fig0105.png
Figure 1-5. Git stores data as snapshots of the project over time.
This is an important distinction between Git and nearly all other VCSs. It makes Git reconsider almost every aspect of version control that most other systems copied from the previous generation. This makes Git more like a mini filesystem with some incredibly powerful tools built on top of it, rather than simply a VCS. Well explore some of the benefits you gain by thinking of your data this way when we cover Git branching in Chapter 3.
### Nearly Every Operation Is Local ###
Most operations in Git only need local files and resources to operate — generally no information is needed from another computer on your network. If youre used to a CVCS where most operations have that network latency overhead, this aspect of Git will make you think that the gods of speed have blessed Git with unworldly powers. Because you have the entire history of the project right there on your local disk, most operations seem almost instantaneous.
For example, to browse the history of the project, Git doesnt need to go out to the server to get the history and display it for you—it simply reads it directly from your local database. This means you see the project history almost instantly. If you want to see the changes introduced between the current version of a file and the file a month ago, Git can look up the file a month ago and do a local difference calculation, instead of having to either ask a remote server to do it or pull an older version of the file from the remote server to do it locally.
This also means that there is very little you cant do if youre offline or off VPN. If you get on an airplane or a train and want to do a little work, you can commit happily until you get to a network connection to upload. If you go home and cant get your VPN client working properly, you can still work. In many other systems, doing so is either impossible or painful. In Perforce, for example, you cant do much when you arent connected to the server; and in Subversion and CVS, you can edit files, but you cant commit changes to your database (because your database is offline). This may not seem like a huge deal, but you may be surprised what a big difference it can make.
### Git Has Integrity ###
Everything in Git is check-summed before it is stored and is then referred to by that checksum. This means its impossible to change the contents of any file or directory without Git knowing about it. This functionality is built into Git at the lowest levels and is integral to its philosophy. You cant lose information in transit or get file corruption without Git being able to detect it.
The mechanism that Git uses for this checksumming is called a SHA-1 hash. This is a 40-character string composed of hexadecimal characters (09 and af) and calculated based on the contents of a file or directory structure in Git. A SHA-1 hash looks something like this:
24b9da6552252987aa493b52f8696cd6d3b00373
You will see these hash values all over the place in Git because it uses them so much. In fact, Git stores everything not by file name but in the Git database addressable by the hash value of its contents.
### Git Generally Only Adds Data ###
When you do actions in Git, nearly all of them only add data to the Git database. It is very difficult to get the system to do anything that is not undoable or to make it erase data in any way. As in any VCS, you can lose or mess up changes you havent committed yet; but after you commit a snapshot into Git, it is very difficult to lose, especially if you regularly push your database to another repository.
This makes using Git a joy because we know we can experiment without the danger of severely screwing things up. For a more in-depth look at how Git stores its data and how you can recover data that seems lost, see Chapter 9.
### The Three States ###
Now, pay attention. This is the main thing to remember about Git if you want the rest of your learning process to go smoothly. Git has three main states that your files can reside in: committed, modified, and staged. Committed means that the data is safely stored in your local database. Modified means that you have changed the file but have not committed it to your database yet. Staged means that you have marked a modified file in its current version to go into your next commit snapshot.
This leads us to the three main sections of a Git project: the Git directory, the working directory, and the staging area.
Insert 18333fig0106.png
Figure 1-6. Working directory, staging area, and git directory.
The Git directory is where Git stores the metadata and object database for your project. This is the most important part of Git, and it is what is copied when you clone a repository from another computer.
The working directory is a single checkout of one version of the project. These files are pulled out of the compressed database in the Git directory and placed on disk for you to use or modify.
The staging area is a simple file, generally contained in your Git directory, that stores information about what will go into your next commit. Its sometimes referred to as the index, but its becoming standard to refer to it as the staging area.
The basic Git workflow goes something like this:
1. You modify files in your working directory.
2. You stage the files, adding snapshots of them to your staging area.
3. You do a commit, which takes the files as they are in the staging area and stores that snapshot permanently to your Git directory.
If a particular version of a file is in the git directory, its considered committed. If its modified but has been added to the staging area, it is staged. And if it was changed since it was checked out but has not been staged, it is modified. In Chapter 2, youll learn more about these states and how you can either take advantage of them or skip the staged part entirely.
## Installing Git ##
Lets get into using some Git. First things first—you have to install it. You can get it a number of ways; the two major ones are to install it from source or to install an existing package for your platform.
### Installing from Source ###
If you can, its generally useful to install Git from source, because youll get the most recent version. Each version of Git tends to include useful UI enhancements, so getting the latest version is often the best route if you feel comfortable compiling software from source. It is also the case that many Linux distributions contain very old packages; so unless youre on a very up-to-date distro or are using backports, installing from source may be the best bet.
To install Git, you need to have the following libraries that Git depends on: curl, zlib, openssl, expat, and libiconv. For example, if youre on a system that has yum (such as Fedora) or apt-get (such as a Debian based system), you can use one of these commands to install all of the dependencies:
$ yum install curl-devel expat-devel gettext-devel \
openssl-devel zlib-devel
$ apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \
libz-dev libssl-dev
When you have all the necessary dependencies, you can go ahead and grab the latest snapshot from the Git web site:
http://git-scm.com/download
Then, compile and install:
$ tar -zxf git-1.7.2.2.tar.gz
$ cd git-1.7.2.2
$ make prefix=/usr/local all
$ sudo make prefix=/usr/local install
After this is done, you can also get Git via Git itself for updates:
$ git clone git://git.kernel.org/pub/scm/git/git.git
### Installing on Linux ###
If you want to install Git on Linux via a binary installer, you can generally do so through the basic package-management tool that comes with your distribution. If youre on Fedora, you can use yum:
$ yum install git-core
Or if youre on a Debian-based distribution like Ubuntu, try apt-get:
$ apt-get install git
### Installing on Mac ###
There are two easy ways to install Git on a Mac. The easiest is to use the graphical Git installer, which you can download from the Google Code page (see Figure 1-7):
http://code.google.com/p/git-osx-installer
Insert 18333fig0107.png
Figure 1-7. Git OS X installer.
The other major way is to install Git via MacPorts (`http://www.macports.org`). If you have MacPorts installed, install Git via
$ sudo port install git-core +svn +doc +bash_completion +gitweb
You dont have to add all the extras, but youll probably want to include +svn in case you ever have to use Git with Subversion repositories (see Chapter 8).
### Installing on Windows ###
Installing Git on Windows is very easy. The msysGit project has one of the easier installation procedures. Simply download the installer exe file from the GitHub page, and run it:
http://msysgit.github.com/
After its installed, you have both a command-line version (including an SSH client that will come in handy later) and the standard GUI.
Note on Windows usage: you should use Git with the provided msysGit shell (Unix style), it allows to use the complex lines of command given in this book. If you need, for some reason, to use the native Windows shell / command line console, you have to use double quotes instead of simple quotes (for parameters with spaces in them) and you must quote the parameters ending with the circumflex accent (^) if they are last on the line, as it is a continuation symbol in Windows.
## First-Time Git Setup ##
Now that you have Git on your system, youll want to do a few things to customize your Git environment. You should have to do these things only once; theyll stick around between upgrades. You can also change them at any time by running through the commands again.
Git comes with a tool called git config that lets you get and set configuration variables that control all aspects of how Git looks and operates. These variables can be stored in three different places:
* `/etc/gitconfig` file: Contains values for every user on the system and all their repositories. If you pass the option` --system` to `git config`, it reads and writes from this file specifically.
* `~/.gitconfig` file: Specific to your user. You can make Git read and write to this file specifically by passing the `--global` option.
* config file in the git directory (that is, `.git/config`) of whatever repository youre currently using: Specific to that single repository. Each level overrides values in the previous level, so values in `.git/config` trump those in `/etc/gitconfig`.
On Windows systems, Git looks for the `.gitconfig` file in the `$HOME` directory (`%USERPROFILE%` in Windows environment), which is `C:\Documents and Settings\$USER` or `C:\Users\$USER` for most people, depending on version (`$USER` is `%USERNAME%` in Windows environment). It also still looks for /etc/gitconfig, although its relative to the MSys root, which is wherever you decide to install Git on your Windows system when you run the installer.
### Your Identity ###
The first thing you should do when you install Git is to set your user name and e-mail address. This is important because every Git commit uses this information, and its immutably baked into the commits you pass around:
$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com
Again, you need to do this only once if you pass the `--global` option, because then Git will always use that information for anything you do on that system. If you want to override this with a different name or e-mail address for specific projects, you can run the command without the `--global` option when youre in that project.
### Your Editor ###
Now that your identity is set up, you can configure the default text editor that will be used when Git needs you to type in a message. By default, Git uses your systems default editor, which is generally Vi or Vim. If you want to use a different text editor, such as Emacs, you can do the following:
$ git config --global core.editor emacs
### Your Diff Tool ###
Another useful option you may want to configure is the default diff tool to use to resolve merge conflicts. Say you want to use vimdiff:
$ git config --global merge.tool vimdiff
Git accepts kdiff3, tkdiff, meld, xxdiff, emerge, vimdiff, gvimdiff, ecmerge, and opendiff as valid merge tools. You can also set up a custom tool; see Chapter 7 for more information about doing that.
### Checking Your Settings ###
If you want to check your settings, you can use the `git config --list` command to list all the settings Git can find at that point:
$ git config --list
user.name=Scott Chacon
user.email=schacon@gmail.com
color.status=auto
color.branch=auto
color.interactive=auto
color.diff=auto
...
You may see keys more than once, because Git reads the same key from different files (`/etc/gitconfig` and `~/.gitconfig`, for example). In this case, Git uses the last value for each unique key it sees.
You can also check what Git thinks a specific keys value is by typing `git config {key}`:
$ git config user.name
Scott Chacon
## Getting Help ##
If you ever need help while using Git, there are three ways to get the manual page (manpage) help for any of the Git commands:
$ git help <verb>
$ git <verb> --help
$ man git-<verb>
For example, you can get the manpage help for the config command by running
$ git help config
These commands are nice because you can access them anywhere, even offline.
If the manpages and this book arent enough and you need in-person help, you can try the `#git` or `#github` channel on the Freenode IRC server (irc.freenode.net). These channels are regularly filled with hundreds of people who are all very knowledgeable about Git and are often willing to help.
## Summary ##
You should have a basic understanding of what Git is and how its different from the CVCS you may have been using. You should also now have a working version of Git on your system thats set up with your personal identity. Its now time to learn some Git basics.
+276
View File
@@ -0,0 +1,276 @@
package parser
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"unicode"
)
const (
HTML_LEAD = "<"
YAML_LEAD = "-"
YAML_DELIM_UNIX = "---\n"
YAML_DELIM_DOS = "---\r\n"
TOML_LEAD = "+"
TOML_DELIM_UNIX = "+++\n"
TOML_DELIM_DOS = "+++\r\n"
JAVA_LEAD = "{"
)
var (
delims = [][]byte{
[]byte(YAML_DELIM_UNIX),
[]byte(YAML_DELIM_DOS),
[]byte(TOML_DELIM_UNIX),
[]byte(TOML_DELIM_DOS),
[]byte(JAVA_LEAD),
}
unixEnding = []byte("\n")
dosEnding = []byte("\r\n")
)
type FrontMatter []byte
type Content []byte
type Page interface {
FrontMatter() FrontMatter
Content() Content
IsRenderable() bool
}
type page struct {
render bool
frontmatter FrontMatter
content Content
}
func (p *page) Content() Content {
return p.content
}
func (p *page) FrontMatter() FrontMatter {
return p.frontmatter
}
func (p *page) IsRenderable() bool {
return p.render
}
// ReadFrom reads the content from an io.Reader and constructs a page.
func ReadFrom(r io.Reader) (p Page, err error) {
reader := bufio.NewReader(r)
if err = chompWhitespace(reader); err != nil {
return
}
firstLine, err := peekLine(reader)
if err != nil {
return
}
newp := new(page)
newp.render = shouldRender(firstLine)
if newp.render && isFrontMatterDelim(firstLine) {
left, right := determineDelims(firstLine)
fm, err := extractFrontMatterDelims(reader, left, right)
if err != nil {
return nil, err
}
newp.frontmatter = fm
}
content, err := extractContent(reader)
if err != nil {
return nil, err
}
newp.content = content
return newp, nil
}
func chompWhitespace(r io.RuneScanner) (err error) {
for {
c, _, err := r.ReadRune()
if err != nil {
return err
}
if !unicode.IsSpace(c) {
r.UnreadRune()
return nil
}
}
return
}
func peekLine(r *bufio.Reader) (line []byte, err error) {
firstFive, err := r.Peek(5)
if err != nil {
return
}
idx := bytes.IndexByte(firstFive, '\n')
if idx == -1 {
return firstFive, nil
}
idx += 1 // include newline.
return firstFive[:idx], nil
}
func shouldRender(lead []byte) (frontmatter bool) {
if len(lead) <= 0 {
return
}
if bytes.Equal(lead[:1], []byte(HTML_LEAD)) {
return
}
return true
}
func isFrontMatterDelim(data []byte) bool {
for _, d := range delims {
if bytes.HasPrefix(data, d) {
return true
}
}
return false
}
func determineDelims(firstLine []byte) (left, right []byte) {
switch len(firstLine) {
case 4:
if firstLine[0] == YAML_LEAD[0] {
return []byte(YAML_DELIM_UNIX), []byte(YAML_DELIM_UNIX)
}
return []byte(TOML_DELIM_UNIX), []byte(TOML_DELIM_UNIX)
case 5:
if firstLine[0] == YAML_LEAD[0] {
return []byte(YAML_DELIM_DOS), []byte(YAML_DELIM_DOS)
}
return []byte(TOML_DELIM_DOS), []byte(TOML_DELIM_DOS)
case 3:
fallthrough
case 2:
fallthrough
case 1:
return []byte(JAVA_LEAD), []byte("}")
default:
panic(fmt.Sprintf("Unable to determine delims from %q", firstLine))
}
return
}
func extractFrontMatterDelims(r *bufio.Reader, left, right []byte) (fm FrontMatter, err error) {
var (
c byte
level int = 0
bytesRead int = 0
sameDelim = bytes.Equal(left, right)
)
wr := new(bytes.Buffer)
for {
if c, err = r.ReadByte(); err != nil {
return nil, fmt.Errorf("Unable to read frontmatter at filepos %d: %s", bytesRead, err)
}
bytesRead += 1
switch c {
case left[0]:
var (
buf []byte = []byte{c}
remaining []byte
)
if remaining, err = r.Peek(len(left) - 1); err != nil {
return nil, err
}
buf = append(buf, remaining...)
if bytes.Equal(buf, left) {
if sameDelim {
if level == 0 {
level = 1
} else {
level = 0
}
} else {
level += 1
}
}
if _, err = wr.Write([]byte{c}); err != nil {
return nil, err
}
if level == 0 {
if _, err = r.Read(remaining); err != nil {
return nil, err
}
if _, err = wr.Write(remaining); err != nil {
return nil, err
}
}
case right[0]:
match, err := matches(r, wr, []byte{c}, right)
if err != nil {
return nil, err
}
if match {
level -= 1
}
default:
if err = wr.WriteByte(c); err != nil {
return nil, err
}
}
if level == 0 && !unicode.IsSpace(rune(c)) {
if err = chompWhitespace(r); err != nil {
if err != io.EOF {
return nil, err
}
}
return wr.Bytes(), nil
}
}
return nil, errors.New("Could not find front matter.")
}
func matches_quick(buf, expected []byte) (ok bool, err error) {
return bytes.Equal(expected, buf), nil
}
func matches(r *bufio.Reader, wr io.Writer, c, expected []byte) (ok bool, err error) {
if len(expected) == 1 {
if _, err = wr.Write(c); err != nil {
return
}
return bytes.Equal(c, expected), nil
}
buf := make([]byte, len(expected)-1)
if buf, err = r.Peek(len(expected) - 1); err != nil {
return
}
buf = append(c, buf...)
return bytes.Equal(expected, buf), nil
}
func extractContent(r io.Reader) (content Content, err error) {
wr := new(bytes.Buffer)
if _, err = wr.ReadFrom(r); err != nil {
return
}
return wr.Bytes(), nil
}
+301
View File
@@ -0,0 +1,301 @@
package parser
// TODO Support Mac Encoding (\r)
import (
"bufio"
"bytes"
"io"
"os"
"path/filepath"
"strings"
"testing"
)
var (
CONTENT_EMPTY = ""
CONTENT_NO_FRONTMATTER = "a page with no front matter"
CONTENT_WITH_FRONTMATTER = "---\ntitle: front matter\n---\nContent with front matter"
CONTENT_HTML_NODOCTYPE = "<html>\n\t<body>\n\t</body>\n</html>"
CONTENT_HTML_WITHDOCTYPE = "<!doctype html><html><body></body></html>"
CONTENT_HTML_WITH_FRONTMATTER = "---\ntitle: front matter\n---\n<!doctype><html><body></body></html>"
CONTENT_LWS_HTML = " <html><body></body></html>"
CONTENT_LWS_LF_HTML = "\n<html><body></body></html>"
CONTENT_INCOMPLETE_BEG_FM_DELIM = "--\ntitle: incomplete beg fm delim\n---\nincomplete frontmatter delim"
CONTENT_INCOMPLETE_END_FM_DELIM = "---\ntitle: incomplete end fm delim\n--\nincomplete frontmatter delim"
CONTENT_MISSING_END_FM_DELIM = "---\ntitle: incomplete end fm delim\nincomplete frontmatter delim"
CONTENT_SLUG_WORKING = "---\ntitle: slug doc 2\nslug: slug-doc-2\n\n---\nslug doc 2 content"
CONTENT_SLUG_WORKING_VARIATION = "---\ntitle: slug doc 3\nslug: slug-doc 3\n---\nslug doc 3 content"
CONTENT_SLUG_BUG = "---\ntitle: slug doc 2\nslug: slug-doc-2\n---\nslug doc 2 content"
CONTENT_FM_NO_DOC = "---\ntitle: no doc\n---"
CONTENT_WITH_JS_FM = "{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories"
CONTENT_WITH_JS_LOOSE_FM = "{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories"
)
var lineEndings = []string{"\n", "\r\n"}
var delimiters = []string{"---", "+++"}
func pageMust(p Page, err error) *page {
if err != nil {
panic(err)
}
return p.(*page)
}
func pageRecoverAndLog(t *testing.T) {
if err := recover(); err != nil {
t.Errorf("panic/recover: %s\n", err)
}
}
func TestDegenerateCreatePageFrom(t *testing.T) {
tests := []struct {
content string
}{
{CONTENT_EMPTY},
{CONTENT_MISSING_END_FM_DELIM},
{CONTENT_INCOMPLETE_END_FM_DELIM},
{CONTENT_FM_NO_DOC},
}
for _, test := range tests {
for _, ending := range lineEndings {
test.content = strings.Replace(test.content, "\n", ending, -1)
_, err := ReadFrom(strings.NewReader(test.content))
if err == nil {
t.Errorf("Content should return an err:\n%q\n", test.content)
}
}
}
}
func checkPageRender(t *testing.T, p *page, expected bool) {
if p.render != expected {
t.Errorf("page.render should be %t, got: %t", expected, p.render)
}
}
func checkPageFrontMatterIsNil(t *testing.T, p *page, content string, expected bool) {
if bool(p.frontmatter == nil) != expected {
t.Logf("\n%q\n", content)
t.Errorf("page.frontmatter == nil? %t, got %t", expected, p.frontmatter == nil)
}
}
func checkPageFrontMatterContent(t *testing.T, p *page, frontMatter string) {
if p.frontmatter == nil {
return
}
if !bytes.Equal(p.frontmatter, []byte(frontMatter)) {
t.Errorf("frontmatter mismatch\nexp: %q\ngot: %q", frontMatter, p.frontmatter)
}
}
func checkPageContent(t *testing.T, p *page, expected string) {
if !bytes.Equal(p.content, []byte(expected)) {
t.Errorf("content mismatch\nexp: %q\ngot: %q", expected, p.content)
}
}
func TestStandaloneCreatePageFrom(t *testing.T) {
tests := []struct {
content string
expectedMustRender bool
frontMatterIsNil bool
frontMatter string
bodycontent string
}{
{CONTENT_NO_FRONTMATTER, true, true, "", "a page with no front matter"},
{CONTENT_WITH_FRONTMATTER, true, false, "---\ntitle: front matter\n---\n", "Content with front matter"},
{CONTENT_HTML_NODOCTYPE, false, true, "", "<html>\n\t<body>\n\t</body>\n</html>"},
{CONTENT_HTML_WITHDOCTYPE, false, true, "", "<!doctype html><html><body></body></html>"},
{CONTENT_HTML_WITH_FRONTMATTER, true, false, "---\ntitle: front matter\n---\n", "<!doctype><html><body></body></html>"},
{CONTENT_LWS_HTML, false, true, "", "<html><body></body></html>"},
{CONTENT_LWS_LF_HTML, false, true, "", "<html><body></body></html>"},
{CONTENT_WITH_JS_FM, true, false, "{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}", "JSON Front Matter with tags and categories"},
{CONTENT_WITH_JS_LOOSE_FM, true, false, "{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}", "JSON Front Matter with tags and categories"},
{CONTENT_SLUG_WORKING, true, false, "---\ntitle: slug doc 2\nslug: slug-doc-2\n\n---\n", "slug doc 2 content"},
{CONTENT_SLUG_WORKING_VARIATION, true, false, "---\ntitle: slug doc 3\nslug: slug-doc 3\n---\n", "slug doc 3 content"},
{CONTENT_SLUG_BUG, true, false, "---\ntitle: slug doc 2\nslug: slug-doc-2\n---\n", "slug doc 2 content"},
}
for _, test := range tests {
for _, ending := range lineEndings {
test.content = strings.Replace(test.content, "\n", ending, -1)
test.frontMatter = strings.Replace(test.frontMatter, "\n", ending, -1)
test.bodycontent = strings.Replace(test.bodycontent, "\n", ending, -1)
p := pageMust(ReadFrom(strings.NewReader(test.content)))
checkPageRender(t, p, test.expectedMustRender)
checkPageFrontMatterIsNil(t, p, test.content, test.frontMatterIsNil)
checkPageFrontMatterContent(t, p, test.frontMatter)
checkPageContent(t, p, test.bodycontent)
}
}
}
func BenchmarkLongFormRender(b *testing.B) {
tests := []struct {
filename string
buf []byte
}{
{filename: "long_text_test.md"},
}
for i, test := range tests {
path := filepath.FromSlash(test.filename)
f, err := os.Open(path)
if err != nil {
b.Fatalf("Unable to open %s: %s", path, err)
}
defer f.Close()
membuf := new(bytes.Buffer)
if _, err := io.Copy(membuf, f); err != nil {
b.Fatalf("Unable to read %s: %s", path, err)
}
tests[i].buf = membuf.Bytes()
}
b.ResetTimer()
for i := 0; i <= b.N; i++ {
for _, test := range tests {
ReadFrom(bytes.NewReader(test.buf))
}
}
}
func TestPageShouldRender(t *testing.T) {
tests := []struct {
content []byte
expected bool
}{
{[]byte{}, false},
{[]byte{'<'}, false},
{[]byte{'-'}, true},
{[]byte("--"), true},
{[]byte("---"), true},
{[]byte("---\n"), true},
{[]byte{'a'}, true},
}
for _, test := range tests {
for _, ending := range lineEndings {
test.content = bytes.Replace(test.content, []byte("\n"), []byte(ending), -1)
if render := shouldRender(test.content); render != test.expected {
t.Errorf("Expected %s to shouldRender = %t, got: %t", test.content, test.expected, render)
}
}
}
}
func TestPageHasFrontMatter(t *testing.T) {
tests := []struct {
content []byte
expected bool
}{
{[]byte{'-'}, false},
{[]byte("--"), false},
{[]byte("---"), false},
{[]byte("---\n"), true},
{[]byte("---\n"), true},
{[]byte{'a'}, false},
{[]byte{'{'}, true},
{[]byte("{\n "), true},
{[]byte{'}'}, false},
}
for _, test := range tests {
for _, ending := range lineEndings {
test.content = bytes.Replace(test.content, []byte("\n"), []byte(ending), -1)
if isFrontMatterDelim := isFrontMatterDelim(test.content); isFrontMatterDelim != test.expected {
t.Errorf("Expected %q isFrontMatterDelim = %t, got: %t", test.content, test.expected, isFrontMatterDelim)
}
}
}
}
func TestExtractFrontMatter(t *testing.T) {
tests := []struct {
frontmatter string
extracted []byte
errIsNil bool
}{
{"", nil, false},
{"-", nil, false},
{"---\n", nil, false},
{"---\nfoobar", nil, false},
{"---\nfoobar\nbarfoo\nfizbaz\n", nil, false},
{"---\nblar\n-\n", nil, false},
{"---\nralb\n---\n", []byte("---\nralb\n---\n"), true},
{"---\nminc\n---\ncontent", []byte("---\nminc\n---\n"), true},
{"---\ncnim\n---\ncontent\n", []byte("---\ncnim\n---\n"), true},
{"---\ntitle: slug doc 2\nslug: slug-doc-2\n---\ncontent\n", []byte("---\ntitle: slug doc 2\nslug: slug-doc-2\n---\n"), true},
}
for _, test := range tests {
for _, ending := range lineEndings {
test.frontmatter = strings.Replace(test.frontmatter, "\n", ending, -1)
test.extracted = bytes.Replace(test.extracted, []byte("\n"), []byte(ending), -1)
for _, delim := range delimiters {
test.frontmatter = strings.Replace(test.frontmatter, "---", delim, -1)
test.extracted = bytes.Replace(test.extracted, []byte("---"), []byte(delim), -1)
line, err := peekLine(bufio.NewReader(strings.NewReader(test.frontmatter)))
if err != nil {
continue
}
l, r := determineDelims(line)
fm, err := extractFrontMatterDelims(bufio.NewReader(strings.NewReader(test.frontmatter)), l, r)
if (err == nil) != test.errIsNil {
t.Logf("\n%q\n", string(test.frontmatter))
t.Errorf("Expected err == nil => %t, got: %t. err: %s", test.errIsNil, err == nil, err)
continue
}
if !bytes.Equal(fm, test.extracted) {
t.Errorf("Frontmatter did not match:\nexp: %q\ngot: %q", string(test.extracted), fm)
}
}
}
}
}
func TestExtractFrontMatterDelim(t *testing.T) {
var (
noErrExpected = true
errExpected = false
)
tests := []struct {
frontmatter string
extracted string
errIsNil bool
}{
{"", "", errExpected},
{"{", "", errExpected},
{"{}", "{}", noErrExpected},
{" {}", " {}", noErrExpected},
{"{} ", "{}", noErrExpected},
{"{ } ", "{ }", noErrExpected},
{"{ { }", "", errExpected},
{"{ { } }", "{ { } }", noErrExpected},
{"{ { } { } }", "{ { } { } }", noErrExpected},
{"{\n{\n}\n}\n", "{\n{\n}\n}", noErrExpected},
{"{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories", "{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}", noErrExpected},
{"{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories", "{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}", noErrExpected},
}
for _, test := range tests {
fm, err := extractFrontMatterDelims(bufio.NewReader(strings.NewReader(test.frontmatter)), []byte("{"), []byte("}"))
if (err == nil) != test.errIsNil {
t.Logf("\n%q\n", string(test.frontmatter))
t.Errorf("Expected err == nil => %t, got: %t. err: %s", test.errIsNil, err == nil, err)
continue
}
if !bytes.Equal(fm, []byte(test.extracted)) {
t.Logf("\n%q\n", string(test.frontmatter))
t.Errorf("Frontmatter did not match:\nexp: %q\ngot: %q", string(test.extracted), fm)
}
}
}
+25
View File
@@ -0,0 +1,25 @@
package source
import (
"testing"
)
func TestIgnoreDotFiles(t *testing.T) {
tests := []struct {
path string
ignore bool
}{
{"barfoo.md", false},
{"foobar/barfoo.md", false},
{"foobar/.barfoo.md", true},
{".barfoo.md", true},
{".md", true},
{"", true},
}
for _, test := range tests {
if ignored := ignoreDotFile(test.path); test.ignore != ignored {
t.Errorf("File not ignored. Expected: %t, got: %t", test.ignore, ignored)
}
}
}

Some files were not shown because too many files have changed in this diff Show More