Nav apraksta

blog.go 8.3KB

    package main import ( "crypto/md5" "crypto/rand" "encoding/hex" "encoding/json" "flag" "fmt" "html/template" "io" "io/ioutil" "net/url" "os" "strings" "unicode" "github.com/russross/blackfriday" "gopkg.in/yaml.v2" ) type Post struct { Id string `yaml:"id"` Title string `yaml:"title"` URL string `yaml:"url"` Content string `yaml:"content"` Tags []string `yaml:"tags"` Date string `yaml:"date"` Type string `yaml:"type"` } var flags struct { writeBack bool hashIds bool reverse bool css string title string } var dataPath string = "blog.yaml" var defaultStyle = ` article { margin-bottom: 2em; } article header { display: flex; align-items: center; } article h1 { margin: 0; } article header .permalink { margin-left: 0.1em; text-decoration: none; color: #555; visibility: hidden; } article header:hover .permalink { visibility: visible; } article time { margin-left: 1em; color: #666; } article img { max-width: 80vw; max-height: 50vh; } article .tags { list-style-type: none; padding: 0; } article .tags .tag-link { float: left; color: black; margin-right: 0.5em; } article .tags .tag-link:visited { color: #555; } article.does-not-match { display: none; } ` func init() { flag.BoolVar(&flags.writeBack, "write-back", false, "Rewrite the YAML file with the generated ids") flag.BoolVar(&flags.hashIds, "hash-ids", false, "Use hash-based post ids") flag.BoolVar(&flags.reverse, "reverse", false, "Reverse the order of the articles in the file") flag.StringVar(&flags.css, "css", defaultStyle, "Custom `css` styles to use") flag.StringVar(&flags.title, "title", "A blog", "Custom `title` to use") flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [flags] [<blog.yaml> [<blog.html>]]\n\n", os.Args[0]) flag.PrintDefaults() } } func main() { flag.Parse() if flag.NArg() > 0 { dataPath = flag.Arg(0) } f, err := os.Open(dataPath) if err != nil { exit(err) } defer f.Close() out := os.Stdout if flag.NArg() > 1 { out, err = os.Create(flag.Arg(1)) if err != nil { exit(err) } } data, err := ioutil.ReadAll(f) if err != nil { exit(err) } var posts []Post err = yaml.Unmarshal(data, &posts) if err != nil { exit(err) } fmt.Fprintf(out, `<!doctype html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>%s</title> <style>%s</style> </head> <body> `, flags.title, flags.css) if flags.reverse { l := len(posts) reversePosts := make([]Post, l) for i := 0; i < l; i++ { reversePosts[i] = posts[l-i-1] } posts = reversePosts } for i, post := range posts { if post.Id == "" { posts[i].Id = generateId(post) post = posts[i] } var err error switch post.Type { case "shell": err = shellTmpl.Execute(out, post) case "link": err = linkTmpl.Execute(out, post) case "image": err = imageTmpl.Execute(out, post) case "song": err = songTmpl.Execute(out, post) case "text": err = textTmpl.Execute(out, post) case "video": u, err := url.Parse(post.URL) if err != nil { exit(fmt.Errorf("invalid video url '%s'", post.URL)) } id := u.Query().Get("v") if (u.Host != "youtube.com" && u.Host != "www.youtube.com") || id == "" { exit(fmt.Errorf("unsupported video url '%s'", post.URL)) } post.URL = id err = videoTmpl.Execute(out, post) default: fmt.Fprintf(os.Stderr, "Error: no output for type '%s'\n", post.Type) os.Exit(1) } if err != nil { exit(err) } } fmt.Fprintf(out, ` <script> window.addEventListener("click", function(ev) { if (ev.target.classList.contains("tag-link")) { if (ev.target.href == "") { return; } var u = new URL(ev.target.href); if (!u.hash.startsWith("#tag:")) { return; } var tag = u.hash.substr(5); filterTag(tag); } }) function filterTag(tag) { var articles = document.querySelectorAll("article"); for (var i = 0; i < articles.length; i++) { var article = articles[i]; var matches = false; if (article && 'tags' in article.dataset) { var tags = JSON.parse(article.dataset.tags); for (var j = 0; j < tags.length; j++) { if (tags[j] == tag) { matches = true; break; } } } if (!matches) { article.classList.add("does-not-match"); } } } </script>`) fmt.Fprintf(out, "\n</body>\n</html>\n") out.Close() if flags.writeBack { dataOut, err := yaml.Marshal(posts) if err != nil { exit(err) } ioutil.WriteFile(dataPath, dataOut, 0664) } } var funcs = template.FuncMap{ "markdown": func(markdown string) template.HTML { return template.HTML(blackfriday.MarkdownCommon([]byte(markdown))) }, "safe_url": func(s string) template.URL { return template.URL(s) }, "json": func(v interface{}) (string, error) { buf, err := json.Marshal(v) return string(buf), err }, } var baseTmpl = template.Must(template.New("base"). Funcs(funcs).Parse(` {{ define "title" }} <header> {{- if .Title }} <h1>{{ .Title }}</h1>{{ end }} <a class="permalink" href="#{{ .Id }}">∞</a> {{- if .Date }} <time>{{ .Date }}</time>{{ end }} </header> {{ end }} {{ define "tags" }} <ul class="tags"> {{- range . }} <li><a class="tag-link" href="#tag:{{ safe_url . }}">{{ . }}</a></li> {{ end -}} </ul> {{ end }} `)) var shellTmpl = template.Must(baseTmpl.New("shell"). Funcs(funcs).Parse(` <article id="{{ .Id }}" class="shell"> <header> <h1><code class="language-shell">{{ .Title }}</code></h1> <a class="permalink" href="#{{ .Id }}">∞</a> {{- if .Date }} <time>{{ .Date }}</time>{{ end }} </header> {{- if .Content }} {{ markdown .Content }} {{- end -}} </article> `)) var linkTmpl = template.Must(baseTmpl.New("link"). Funcs(funcs).Parse(` <article id="{{ .Id }}" class="link"> <header> <h1><a href="{{ .URL }}">{{ .Title }}</a></h1> <a class="permalink" href="#{{ .Id }}">∞</a> {{- if .Date }} <time>{{ .Date }}</time>{{ end }} </header> {{- if .Content }} {{ markdown .Content }} {{- end -}} </article> `)) var imageTmpl = template.Must(baseTmpl.New("image"). Funcs(funcs).Parse(` <article id="{{ .Id }}" class="image"> {{- template "title" . }} <img src="{{ safe_url .URL }}" /> {{- if .Content }} {{ markdown .Content }} {{- end -}} </article> `)) var songTmpl = template.Must(baseTmpl.New("song"). Funcs(funcs).Parse(` <article id="{{ .Id }}" class="song"> {{- template "title" . }} <audio src="{{ safe_url .URL }}" controls> Your browser can't play {{ .URL }}. </audio> {{- if .Content }} {{ markdown .Content }} {{- end -}} </article> `)) var textTmpl = template.Must(baseTmpl.New("text"). Funcs(funcs).Parse(` <article id="{{ .Id }}" class="text" {{- if .Tags }} data-tags="{{ json .Tags }}"{{ end }}> {{- template "title" . }} {{- if .Content }} {{ markdown .Content }} {{- end -}} <footer> {{ template "tags" .Tags }} </footer> </article> `)) var videoTmpl = template.Must(baseTmpl.Parse(` <article id="{{ .Id }}" class="video"> {{- template "title" . }} <iframe width="560" height="315" src="https://www.youtube.com/embed/{{ .URL }}" frameborder="0" allowfullscreen></iframe> {{- if .Content }} {{ markdown .Content }} {{- end -}} </article> `)) func exit(err error) { fmt.Fprintf(os.Stderr, "Error: %s\n", err) os.Exit(1) } func generateId(p Post) string { if flags.hashIds { return hashId(p) } return slugId(p) } func hashId(p Post) string { h := md5.New() io.WriteString(h, p.Title) io.WriteString(h, p.Content) io.WriteString(h, p.Type) return hex.EncodeToString(h.Sum(nil)) } func randomId() string { buf := make([]byte, 16) _, err := rand.Read(buf) if err != nil { panic(err) } return hex.EncodeToString(buf) } var usedSlugs = map[string]int{} func slugId(p Post) string { slug := toSlug(p.Title) n, ok := usedSlugs[slug] if ok { n += 1 } else { n = 1 } usedSlugs[slug] = n if slug != "" && n == 1 { return slug } else if slug == "" { return fmt.Sprintf("%d", n) } return fmt.Sprintf("%s-%d", slug, n) } func toSlug(s string) string { lastChar := ' ' s = strings.Map(func(ch rune) rune { var newChar rune switch { case unicode.IsLetter(ch) || unicode.IsDigit(ch): newChar = unicode.ToLower(ch) default: if lastChar == '-' { return -1 } newChar = '-' } lastChar = newChar return newChar }, s) return strings.Trim(s, "-") }