diff options
| author | Nicolas Paul <n@nc0.fr> | 2023-04-26 03:45:11 +0200 |
|---|---|---|
| committer | Nicolas Paul <n@nc0.fr> | 2023-04-26 03:45:11 +0200 |
| commit | 3b41c3e2008498a804a03154f5b876827f1f4e7c (patch) | |
| tree | bd36b8671ad9b99fecfd353e335e1660287224c6 | |
| parent | dab887d5b26df8ba45bf61e426736684a1d8df78 (diff) | |
Add HTML generation
| -rw-r--r-- | .DS_Store | bin | 0 -> 6148 bytes | |||
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | README | 114 | ||||
| -rw-r--r-- | README.md | 152 | ||||
| -rw-r--r-- | crocc.go | 103 | ||||
| -rw-r--r-- | go.mod | 7 | ||||
| -rw-r--r-- | go.sum | 5 | ||||
| -rw-r--r-- | template.go | 45 | ||||
| -rw-r--r-- | testdata/.DS_Store | bin | 0 -> 6148 bytes | |||
| -rw-r--r-- | testdata/.keep | 0 | ||||
| -rw-r--r-- | testdata/src/.crocc.html | 19 | ||||
| -rw-r--r-- | testdata/src/folder/another.md | 11 | ||||
| -rw-r--r-- | testdata/src/foo.txt | 1 | ||||
| -rw-r--r-- | testdata/src/hidden.md | 16 | ||||
| -rw-r--r-- | testdata/src/index.md | 59 | ||||
| -rw-r--r-- | transformations.go | 105 |
16 files changed, 509 insertions, 131 deletions
diff --git a/.DS_Store b/.DS_Store Binary files differnew file mode 100644 index 0000000..06d8f50 --- /dev/null +++ b/.DS_Store @@ -7,4 +7,5 @@ *.o *.out *.exe -crocc
\ No newline at end of file +crocc +/testdata/dst
\ No newline at end of file @@ -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 @@ -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 +} @@ -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 ) @@ -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 Binary files differnew file mode 100644 index 0000000..acd344d --- /dev/null +++ b/testdata/.DS_Store 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 +} |
