summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicolas Paul <n@nc0.fr>2023-04-26 03:45:11 +0200
committerNicolas Paul <n@nc0.fr>2023-04-26 03:45:11 +0200
commit3b41c3e2008498a804a03154f5b876827f1f4e7c (patch)
treebd36b8671ad9b99fecfd353e335e1660287224c6
parentdab887d5b26df8ba45bf61e426736684a1d8df78 (diff)
Add HTML generation
-rw-r--r--.DS_Storebin0 -> 6148 bytes
-rw-r--r--.gitignore3
-rw-r--r--README114
-rw-r--r--README.md152
-rw-r--r--crocc.go103
-rw-r--r--go.mod7
-rw-r--r--go.sum5
-rw-r--r--template.go45
-rw-r--r--testdata/.DS_Storebin0 -> 6148 bytes
-rw-r--r--testdata/.keep0
-rw-r--r--testdata/src/.crocc.html19
-rw-r--r--testdata/src/folder/another.md11
-rw-r--r--testdata/src/foo.txt1
-rw-r--r--testdata/src/hidden.md16
-rw-r--r--testdata/src/index.md59
-rw-r--r--transformations.go105
16 files changed, 509 insertions, 131 deletions
diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..06d8f50
--- /dev/null
+++ b/.DS_Store
Binary files differ
diff --git a/.gitignore b/.gitignore
index 27e3c4c..0e38e3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,5 @@
*.o
*.out
*.exe
-crocc \ No newline at end of file
+crocc
+/testdata/dst \ No newline at end of file
diff --git a/README b/README
deleted file mode 100644
index 6a4af29..0000000
--- a/README
+++ /dev/null
@@ -1,114 +0,0 @@
-Crocc 🐊
-=======
-
-Crocc is a simple and fast static-site generator based on Markdown.
-It generates HTML files from Markdown documents.
-
-Usage
-=====
-
-Let's say you have a directory containing the following files:
-
- src/
- ├── __template.html
- ├── index.md
- ├── about.md
- ├── bar.png
- └── contact.md
-
-The `__template.html` file is the template used to generate the HTML pages.
-The `index.md`, `about.md` and `contact.md` files are Markdown documents.
-The `bar.png` file is a static file.
-
-To generate the HTML files, run the following command:
-
- $ crocc -out=dst -url="http://example.com" -sitemap src
-
-The `dst` directory will contain the following files:
-
- dst/
- ├── index.html
- ├── about.html
- ├── bar.png
- ├── contact.html
- └── sitemap.xml
-
-You can now upload the `dst` directory to your web server and you're done!
-
-Documentation
-=============
-
-Flags
------
-
-* `-out`: The output directory. Default is `dst`.
-* `-url`: The URL of the site. Default is `http://localhost`.
-* `-sitemap`: If set to `true`, a sitemap will be generated.
- Default is `false`.
-* `-verbose`: If set to `true`, verbose output will be printed.
- Default is `false`.
-* `-help`: Print the help message.
-* `-version`: Print the version number.
-* `-hidden`: If set to `true`, hidden documents will be generated.
- Default is `false`.
-
-Input/output directory
-----------------------
-
-The input directory is the directory containing the various files used to
-build the site.
-The input directory must contain a `__template.html` file, which is the
-template used to generate the HTML pages.
-
-The output directory is the directory where the generated HTML files will be
-written.
-The output directory must not exist before running Crocc.
-
-Crocc will copy all the files in the input directory to the output directory,
-except the `__template.html` file.
-During the copy, Crocc will transform Markdown documents to HTML files.
-
-Markdown document
------------------
-
-The Markdown document must have a YAML header, also known as "front matter".
-The YAML header is a set of key-value pairs separated by a colon.
-The YAML header is followed by the Markdown document.
-
-Front matter keys:
-* `title`: The title of the document. Required.
-* `description`: The description of the document. Required.
-* `publication_time`: The date of the document. Required.
-* `last_update_time`: The date of the last update of the document. Not required.
-* `keywords`: The tags of the document, as a list of strings. Required.
-* `author`: The author of the document. Default is `""`.
-* `hide`: If set to `true`, the document will not be generated.
- Default is `false`.
-
-Example
-
- ---
- title: Hello World
- description: This is a simple example of a Markdown document.
- publication_time: 2020-01-01T00:00:00Z
- last_update_time: 2020-01-01T03:00:00Z
- keywords: [example, hello, world]
- author: John Doe
- hide: true
- ---
-
- # Hello World!
-
-Page template
--------------
-
-To create HTML pages, Crocc uses a template file.
-The template file, written in HTML using Go template syntax, must be located in
-the `INPUTDIR/__template.html` file.
-
-License
-=======
-
-Copyright (c) 2023 Nicolas Paul All rights reserved.
-Use of this source code is governed by a BSD-style license that can be found
-in the LICENSE file.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6021ead
--- /dev/null
+++ b/README.md
@@ -0,0 +1,152 @@
+# Crocc
+
+Crocc is a simple static-site generator based on Markdown.
+The main goal of Crocc is to offer simplicity, as opposed to other static-site
+generation tools such as Hugo or Jekyll.
+Indeed, you only need Markdown to write content in a productive manner, and
+everything else is standard scripts (JavaScript, CSS, images, etc.).
+
+## Usage
+
+Let's say you have a directory containing the following files:
+
+```
+src/
+├── .crocc.html
+├── index.md
+├── about.md
+├── bar.png
+└── contact.md
+```
+
+The `.crocc.html` file is the template used to generate the HTML pages.
+The `index.md`, `about.md` and `contact.md` files are Markdown documents.
+The `bar.png` file is a static file.
+
+To generate the HTML files, run the following command:
+
+```bash
+$ crocc -out=dst -url="http://example.com" -sitemap src
+```
+
+The `dst` directory will contain the following files:
+
+```
+dst/
+├── index.html
+├── about.html
+├── bar.png
+├── contact.html
+└── sitemap.xml
+```
+
+You can now upload the `dst` directory to your web server and you're done!
+
+## Documentation
+
+### Input/output directory
+
+The input directory is the directory containing the various files used to
+build the site.
+The input directory must contain a `.crocc.html` file, which is the
+template used to generate the HTML pages.
+
+> Only the top-level template is used. Nested templates are not supported.
+
+The output directory is the directory where the generated HTML files will be
+written.
+The output directory must not exist before running Crocc.
+
+Crocc will copy all the files in the input directory to the output directory,
+except the `.crocc.html` file.
+During the copy, Crocc will transform Markdown documents to HTML files.
+
+### Markdown document
+
+The Markdown document must have a YAML header, also known as "front matter".
+The YAML header is a set of key-value pairs separated by a colon.
+The YAML header is followed by the Markdown document.
+
+Front matter keys:
+* `title`: The title of the document. Required.
+* `description`: The description of the document. Required.
+* `publication_time`: The date of the document. Required.
+* `last_update_time`: The date of the last update of the document. Not required.
+* `keywords`: The tags of the document, as a list of strings. Required.
+* `author`: The author of the document. Default is `""`.
+* `hide`: If set to `true`, the document will not be generated.
+Default is `false`.
+
+Example:
+
+```md
+---
+title: Hello World
+description: This is a simple example of a Markdown document.
+publication_time: 2020-01-01T00:00:00Z
+last_update_time: 2020-01-01T03:00:00Z
+keywords: [example, hello, world]
+author: John Doe
+hide: true
+---
+
+# Hello World!
+```
+
+### Page template
+
+To create HTML pages, Crocc uses a template file.
+The template file, written in HTML using Go template syntax, must be located in
+the `$INPUT/.crocc.html` file.
+
+The template file is injected with a set of variables. A variable can be used
+in the template file using the `{{ .VariableName }}` syntax.
+Read the [Go template documentation](https: //golang.org/pkg/text/template) for
+more information.
+
+The following variables are available:
+* `.Title`: The title of the document.
+* `.Description`: The description of the document.
+* `.PublicationTime`: The date of the document.
+* `.LastUpdateTime`: The date of the last update of the document.
+* `.Keywords`: The tags of the document, as a string separated by commas.
+* `.Author`: The author of the document.
+* `.Content`: The content of the document, as HTML.
+* `.Site`: The URL of the site.
+* `.Generator`: A string containing the name and version of the generator.
+* `.Sitemap`: The URL of the sitemap.
+
+Here is a sample template:
+
+```html
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>{{ .Title }}</title>
+ <meta name="description" content="{{ .Description }}">
+ <meta name="keywords" content="{{ .Keywords }}">
+ <meta name="author" content="{{ .Author }}">
+ <meta name="generator" content="{{ .Generator }}">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+<body>
+ <header>
+ <h1>{{ .Title }}</h1>
+ </header>
+ <main>
+ {{ .Content }}
+ </main>
+ <footer>
+ <p>
+ Last update: {{ .LastUpdateTime }}
+ </p>
+ </footer>
+</body>
+</html>
+```
+
+## License
+
+The project is governed by a BSD-style license that can be found in the
+[LICENSE](LICENSE) file. \ No newline at end of file
diff --git a/crocc.go b/crocc.go
index c4b6d2e..52fbd10 100644
--- a/crocc.go
+++ b/crocc.go
@@ -9,15 +9,17 @@ import (
"flag"
"fmt"
"log"
+ "os"
+ "path/filepath"
"runtime"
+ "text/template"
)
var (
- outputdir = flag.String("out", "dst", "output directory")
- url = flag.String("url", "http://localhost", "site URL")
- sitemap = flag.Bool("sitemap", false, "generate sitemap.xml")
+ out = flag.String("out", "dst", "output directory")
+ url = flag.String("url", "http://localhost", "site URL")
+ // TODO(nc0): sitemap = flag.Bool("sitemap", false, "generate sitemap.xml")
generateHidden = flag.Bool("hidden", false, "generate hidden pages")
- verbose = flag.Bool("v", false, "verbose output")
printVersion = flag.Bool("version", false, "print version and exit")
)
@@ -39,9 +41,14 @@ var (
date string
)
+var (
+ in string
+ htmlTemplate template.Template
+)
+
func init() {
flag.Usage = func() {
- log.Println(usage)
+ fmt.Println(usage)
flag.PrintDefaults()
}
}
@@ -62,18 +69,84 @@ func main() {
return
}
- inputdir := flag.Arg(0)
- if inputdir == "" {
+ // Check input directory
+ in = flag.Arg(0)
+ if in == "" {
log.Fatalln("no input directory specified")
}
- if *verbose {
- log.Printf(`Version: %s
-Input directory: %s
-Output directory: %s
-Site URL: %s
-Generate sitemap: %t
-Generate hidden pages: %t`, versionFormat(), inputdir, *outputdir, *url,
- *sitemap, *generateHidden)
+ if _, err := os.Stat(in); os.IsNotExist(err) {
+ log.Fatalf("input directory %q does not exist", in)
+ }
+
+ // Check output directory
+ if _, err := os.Stat(*out); !os.IsNotExist(err) {
+ log.Fatalf("output directory %q already exists", *out)
+ }
+
+ // Retrieve template file
+ templatePath := filepath.Join(in, ".crocc.html")
+ if _, err := os.Stat(templatePath); os.IsNotExist(err) {
+ log.Fatalf("template file %q does not exist", templatePath)
+ }
+ tp, err := os.ReadFile(templatePath)
+ if err != nil {
+ log.Fatalf("error reading template file %q: %v", templatePath, err)
+ }
+ htmlTemplate = *template.Must(template.New("html-template").Parse(string(tp)))
+
+ // Logic
+ if err := Crocc(in); err != nil {
+ log.Fatalf("unable to complete generation from %q: %v", in, err)
}
}
+
+// Crocc is the function that applies to every file in a directory.
+func Crocc(root string) error {
+ files, err := os.ReadDir(root)
+ if err != nil {
+ return err
+ }
+
+ for _, file := range files {
+ filename := file.Name()
+ log.Printf("processing %q", filename)
+
+ // Ignore template file
+ if filename == ".crocc.html" {
+ continue
+ }
+
+ // If the file is a directory, create it in the output directory
+ if file.IsDir() {
+ if err := TransformDirectory(root, filename, *out); err != nil {
+ return err
+ }
+
+ if err := Crocc(filepath.Join(root, filename)); err != nil {
+ return err
+ }
+
+ continue
+ }
+
+ // Copy non-Markdown files into the output directory
+ if filepath.Ext(filename) != ".md" &&
+ filepath.Ext(filename) != ".markdown" &&
+ filepath.Ext(filename) != ".mdown" &&
+ filepath.Ext(filename) != ".Markdown" {
+ if err := TransformNonMarkdownFile(root, filename, *out); err != nil {
+ return err
+ }
+
+ continue
+ }
+
+ // Transform Markdown files into HTML
+ if err := TransformMarkdownFile(root, filename, *out); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/go.mod b/go.mod
index f3a916d..54d3532 100644
--- a/go.mod
+++ b/go.mod
@@ -3,9 +3,14 @@ module go.nc0.fr/crocc
go 1.20
require (
- github.com/BurntSushi/toml v0.3.1 // indirect
github.com/adrg/frontmatter v0.2.0
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
github.com/google/go-cmp v0.5.9
+ github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4
+)
+
+require (
+ github.com/BurntSushi/toml v0.3.1 // indirect
+ golang.org/x/net v0.9.0 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
)
diff --git a/go.sum b/go.sum
index 5fd0e7b..dbc1e37 100644
--- a/go.sum
+++ b/go.sum
@@ -6,6 +6,11 @@ github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a h1:AWZzzFrqyjY
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
+github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/template.go b/template.go
new file mode 100644
index 0000000..8bca4fd
--- /dev/null
+++ b/template.go
@@ -0,0 +1,45 @@
+// Copyright (c) 2023 Nicolas Paul All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "time"
+
+ "github.com/yosssi/gohtml"
+)
+
+// TemplateData is the data passed to the HTML template.
+type TemplateData struct {
+ Title string
+ Description string
+ PublicationTime string
+ LastUpdateTime string
+ Keywords string
+ Author string
+ Content string
+ Site string
+ Generator string
+ Sitemap string
+}
+
+// GenerateHTML generates the HTML file from the Markdown document.
+func GenerateHTML(file io.Writer, fm FrontMatter, content string) error {
+ // gohtml.NewWriter(file) is a simple wrapper thats beautify the HTML
+ // output.
+ return htmlTemplate.Execute(gohtml.NewWriter(file), TemplateData{
+ Title: fm.Title,
+ Description: fm.Description,
+ PublicationTime: fm.PublicationTime.Format(time.RFC3339),
+ LastUpdateTime: fm.LastUpdateTime.Format(time.RFC3339),
+ Keywords: fmt.Sprintf("%s", fm.Keywords),
+ Author: fm.Author,
+ Content: content,
+ Site: *url,
+ Generator: fmt.Sprintf("crocc %s (https://crocc.nc0.fr)", version),
+ Sitemap: fmt.Sprintf("%s/sitemap.xml", *url),
+ })
+}
diff --git a/testdata/.DS_Store b/testdata/.DS_Store
new file mode 100644
index 0000000..acd344d
--- /dev/null
+++ b/testdata/.DS_Store
Binary files differ
diff --git a/testdata/.keep b/testdata/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testdata/.keep
diff --git a/testdata/src/.crocc.html b/testdata/src/.crocc.html
new file mode 100644
index 0000000..ee699f1
--- /dev/null
+++ b/testdata/src/.crocc.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<!--
+ Copyright (c) 2023 Nicolas Paul All rights reserved.
+ Use of this source code is governed by a BSD-style
+ license that can be found in the LICENSE file.
+-->
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>
+ {{ .Title }}
+ </title>
+ </head>
+ <body>
+ {{ .Content }}
+ </body>
+</html> \ No newline at end of file
diff --git a/testdata/src/folder/another.md b/testdata/src/folder/another.md
new file mode 100644
index 0000000..a8427f0
--- /dev/null
+++ b/testdata/src/folder/another.md
@@ -0,0 +1,11 @@
+---
+title: Another
+description: Some another Markdown
+keywords:
+ - foo
+publication_time: 2019-01-01T00:00:00Z
+author: John Doe
+---
+# Another
+
+This is some another Markdown. \ No newline at end of file
diff --git a/testdata/src/foo.txt b/testdata/src/foo.txt
new file mode 100644
index 0000000..e238773
--- /dev/null
+++ b/testdata/src/foo.txt
@@ -0,0 +1 @@
+this file should get copied as is \ No newline at end of file
diff --git a/testdata/src/hidden.md b/testdata/src/hidden.md
new file mode 100644
index 0000000..925972f
--- /dev/null
+++ b/testdata/src/hidden.md
@@ -0,0 +1,16 @@
+---
+title: Hidden
+description: Some hidden Markdown
+keywords:
+ - foo
+ - bar
+ - baz
+publication_time: 2019-01-01T00:00:00Z
+author: John Doe
+hidden: true
+---
+# Hidden
+
+This is some hidden Markdown.
+The file should not be rendered, unless the crocc is run with the
+`-hidden` flag.
diff --git a/testdata/src/index.md b/testdata/src/index.md
new file mode 100644
index 0000000..757972f
--- /dev/null
+++ b/testdata/src/index.md
@@ -0,0 +1,59 @@
+---
+title: Index
+description: Some Markdown
+keywords:
+ - foo
+ - bar
+ - baz
+publication_time: 2019-01-01T00:00:00Z
+author: John Doe
+---
+# Index
+
+This is some Markdown.
+
+1. foo
+2. bar
+3. baz
+
+**bold**, *italic*, `code`, [link](https://example.com), ~~strikethrough~~.
+
+> blockquote
+> blah blah blah
+
+## Subheading
+### Subsubheading
+#### Subsubsubheading
+##### Subsubsubsubheading
+###### Subsubsubsubsubheading
+
+| foo | bar | baz |
+| --- | --- | --- |
+| 1 | 2 | 3 |
+| 4 | 5 | 6 |
+| 7 | 8 | 9 |
+
+```go
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Hello, world!")
+}
+```
+
+- foo
+- bar
+- baz
+ + qux
+
+Did you know that `foo` is a thing?[^1]
+And that `bar` is a thing?[^2]
+
+[^1]: foo is a thing.
+[^2]: bar is a thing.
+
+2^3 = 8
+
+H~2~O
diff --git a/transformations.go b/transformations.go
new file mode 100644
index 0000000..7053a94
--- /dev/null
+++ b/transformations.go
@@ -0,0 +1,105 @@
+// Copyright (c) 2023 Nicolas Paul All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/gomarkdown/markdown"
+ "github.com/gomarkdown/markdown/html"
+ "github.com/gomarkdown/markdown/parser"
+)
+
+// TransformMarkdownFile simply copy a non-markdown file to the output
+// directory.
+func TransformNonMarkdownFile(inputDir, inputFile, outputDir string) error {
+ inputPath := filepath.Join(inputDir, inputFile)
+
+ input, err := os.ReadFile(inputPath)
+ if err != nil {
+ return err
+ }
+
+ outputPath := filepath.Join(outputDir, inputFile)
+ if err := os.WriteFile(outputPath, input, 0666); err != nil {
+ return err
+ }
+
+ log.Printf("copied file %q to %q", inputPath, outputPath)
+
+ return nil
+}
+
+// TransformDirectory creates a directory in the output directory.
+func TransformDirectory(inputDir, inputFile, outputDir string) error {
+ outputPath := filepath.Join(outputDir, inputFile)
+
+ if err := os.MkdirAll(outputPath, 0777); err != nil {
+ return err
+ }
+
+ log.Printf("created directory %q", outputPath)
+
+ return nil
+}
+
+// TransformMarkdownFile generates the corresponding HTML document from a
+// Markdown file.
+func TransformMarkdownFile(inputDir, inputFile, outputDir string) error {
+ inputPath := filepath.Join(inputDir, inputFile)
+
+ // The output file is the same as the input file, but with a different
+ // extension.
+ fn := strings.TrimSuffix(inputFile, filepath.Ext(inputFile)) + ".html"
+ outputPath := filepath.Join(outputDir, fn)
+
+ contentRaw, err := os.ReadFile(inputPath)
+ if err != nil {
+ return err
+ }
+
+ // Parse front matter
+ fm, contentMD, err := ParseFrontMatter(contentRaw)
+ if err != nil {
+ return err
+ }
+
+ // Skip hidden files unless -hidden is specified
+ if fm.Hide && !*generateHidden {
+ log.Printf("skipped hidden file %q", inputPath)
+ return nil
+ }
+
+ // Render Markdown to HTML
+ pExtensions := parser.Tables | parser.FencedCode |
+ parser.Autolink | parser.Strikethrough | parser.SpaceHeadings |
+ parser.HeadingIDs | parser.BackslashLineBreak |
+ parser.AutoHeadingIDs | parser.Footnotes | parser.SuperSubscript |
+ parser.NoIntraEmphasis
+ p := parser.NewWithExtensions(pExtensions)
+ doc := p.Parse(contentMD)
+
+ htmlFlags := html.Smartypants | html.SmartypantsFractions |
+ html.SmartypantsDashes | html.SmartypantsLatexDashes |
+ html.HrefTargetBlank | html.LazyLoadImages
+ renderer := html.NewRenderer(html.RendererOptions{Flags: htmlFlags})
+ contentHTML := markdown.Render(doc, renderer)
+
+ outputFile, err := os.Create(outputPath)
+ if err != nil {
+ return err
+ }
+ defer outputFile.Close()
+
+ if err := GenerateHTML(outputFile, fm, string(contentHTML)); err != nil {
+ return err
+ }
+
+ log.Printf("generated file %q", outputPath)
+ return nil
+}