aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaria Carolin Chabowski <laria@laria.me>2017-10-14 16:04:26 +0200
committerLaria Carolin Chabowski <laria@laria.me>2017-10-14 20:58:18 +0200
commit369d2c3e395903f6aff1d1869a81290d8bc994fa (patch)
tree4c3629906ebbfc8833af10c8f963f23619ecf00c
downloadcode.laria.me-369d2c3e395903f6aff1d1869a81290d8bc994fa.tar.gz
code.laria.me-369d2c3e395903f6aff1d1869a81290d8bc994fa.tar.bz2
code.laria.me-369d2c3e395903f6aff1d1869a81290d8bc994fa.zip
Initial commit
-rw-r--r--README.md4
-rw-r--r--data/static/microawesome.woffbin0 -> 2412 bytes
-rw-r--r--data/static/style.css181
-rw-r--r--data/templates/main.html40
-rw-r--r--data/templates/notfound.html3
-rw-r--r--data/templates/project.html22
-rw-r--r--data/templates/tag.html5
-rw-r--r--main.go171
-rw-r--r--projects/project.go144
-rw-r--r--projects/readme.go133
10 files changed, 703 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..51d5db6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+code.laria.me
+=============
+
+The code that powers [code.laria.me](https://code.laria.me)
diff --git a/data/static/microawesome.woff b/data/static/microawesome.woff
new file mode 100644
index 0000000..50e8fcc
--- /dev/null
+++ b/data/static/microawesome.woff
Binary files differ
diff --git a/data/static/style.css b/data/static/style.css
new file mode 100644
index 0000000..aaf6dd1
--- /dev/null
+++ b/data/static/style.css
@@ -0,0 +1,181 @@
+@font-face {
+ font-family: "microawesome";
+ src: url("microawesome.woff") format("woff");
+}
+
+html {
+ margin: 0;
+ padding: 0;
+ background: #3a3a3a;
+ color: #f8f8f8;
+ font-family: sans-serif;
+ font-size: 14pt;
+}
+body {
+ margin: 20px auto 10px;
+ width: 85ch;
+ max-width: 95%;
+}
+a {
+ text-decoration: none;
+}
+a:link {
+ color: #7cafc2;
+}
+a:link:hover {
+ color: #9abecb;
+}
+a:visited {
+ color: #ba8baf;
+}
+a:visited:hover {
+ color: #c7a9c0;
+}
+
+pre {
+ background: #2d2d2d;
+ color: #d3d0c8;
+ padding: 10px;
+ border-radius: 4px;
+ overflow-x: auto;
+}
+code {
+ background: #2d2d2d;
+ color: #d3d0c8;
+ border-radius: 4px;
+ padding: 2px 3px 2px;
+ font-size: 85%
+}
+pre code {
+ padding: 0;
+ font-size: 100%;
+}
+
+header {
+ border-bottom: 1px solid #515151;
+ margin: 0 0 5mm;
+ padding-bottom: 3mm;
+}
+
+header h1 {
+ font-family: monospace;
+ margin: 0;
+ font-size: 1.2rem;
+}
+
+header h1 a {
+ text-decoration: none;
+}
+
+footer {
+ text-align: center;
+ font-size: 0.75rem;
+ margin: 10mm 0 15mm;
+ border-top: 1px solid #515151;
+ padding-top: 5mm;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0,0,0,0);
+ border: 0;
+}
+
+/* Tag list */
+.tags {
+ list-style: none;
+ padding: 0;
+}
+.tags li {
+ margin-right: 5mm;
+ display: inline-block;
+}
+.tags li:last-child {
+ margin-right: 0;
+}
+
+.tags li a {
+ text-decoration: none;
+}
+
+.tags li a:before {
+ font: "tag";
+ content: "\f02b";
+ margin-right: 2mm;
+ font-size: 80%
+}
+
+.description {
+ font-style: italic;
+ font-family: serif;
+ padding-left: 5mm;
+}
+
+.shortdesc {
+ font-style: italic;
+ color: #d3d0c8;
+}
+.linkicon {
+ margin-right: 1.5mm;
+}
+.links {
+ list-style: none;
+ padding: 0;
+}
+.links li {
+ display: inline-block;
+ margin-right: 8mm;
+}
+.links li:last-child {
+ margin-right: 0;
+}
+.links li a:before {
+ font-family: "microawesome";
+ margin-right: 2mm;
+ font-size: 80%;
+ content: "\f08e";
+}
+.links li.github a:before{content:"\f09b"}
+.links li.rss a:before{content:"\f09e"}
+.links li.code a:before{content:"\f121"}
+.links li.fork a:before{content:"\f126"}
+.links li.mail a:before{content:"\f2b6"}
+
+#readme {
+ border: 1px dotted #2a2a2a;
+ padding: 2mm;
+}
+.tagcloud {
+ list-style: none;
+ padding: 0;
+}
+.tagcloud li {
+ display: inline;
+}
+.tagcloud-5 {font-size: 130%}
+.tagcloud-4 {font-size: 110%}
+.tagcloud-3 {font-size: 95%}
+.tagcloud-2 {font-size: 80%}
+.tagcloud-1 {font-size: 70%}
+
+h1 {font-size: 150%}
+h2 {font-size: 140%}
+h3 {font-size: 130%}
+h4 {font-size: 120%}
+h5 {font-size: 110%}
+h6 {font-size: 105%}
+section h1 {font-size: 120%}
+section h2 {font-size: 112%}
+section h3 {font-size: 104%}
+section h4 {font-size: 96%}
+section h5 {font-size: 88%}
+section h6 {font-size: 84%}
+
+@media screen and (max-width: 640px) {
+ html {font-size: 12pt}
+}
diff --git a/data/templates/main.html b/data/templates/main.html
new file mode 100644
index 0000000..73da434
--- /dev/null
+++ b/data/templates/main.html
@@ -0,0 +1,40 @@
+{{define "Projectlist"}}
+ <ul>{{range .}}
+ <li class="projectlist">
+ <a href="/{{.Name}}">{{.Title}}</a>
+ {{if .Shortdesc}}
+ <span class="shortdesc"> — {{.Shortdesc}}</span>
+ {{end}}
+ </li>
+ {{end}}</ul>
+{{end}}
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>{{ if .Title }}{{ .Title }} — {{ end }}code.laria.me</title>
+ {{ if .Description }}<meta name="description" content="{{ .Description }}">{{ end }}
+ {{ if .Tags }}<meta name="keywords" content="{{join .Tags " " }}">{{ end }}
+ <meta name="referrer" content="no-referrer">
+ <link rel="stylesheet" type="text/css" href="/static/style.css">
+</head>
+<body>
+ <header>
+ <h1><a href="/" class="rootlink">code.laria.me</a></h1>
+ </header>
+ {{block "content" .}}
+ <h2>List of projects</h2>
+ {{template "Projectlist" .Projects}}
+
+ <h2>Tags</h2>
+ <ul class="tagcloud">{{range .Tagcloud}}
+ <li class="tagcloud-{{.Size}}"><a href="/tags/{{.Tag}}">{{.Tag}}</a></li>
+ {{end}}</ul>
+ {{end}}
+ <footer>
+ <a href="//hi-im.laria.me/impressum/">Impressum</a>
+ </footer>
+</body>
+</html>
diff --git a/data/templates/notfound.html b/data/templates/notfound.html
new file mode 100644
index 0000000..6099512
--- /dev/null
+++ b/data/templates/notfound.html
@@ -0,0 +1,3 @@
+{{define "content"}}
+404 Not found
+{{end}}
diff --git a/data/templates/project.html b/data/templates/project.html
new file mode 100644
index 0000000..cb5ad5d
--- /dev/null
+++ b/data/templates/project.html
@@ -0,0 +1,22 @@
+{{define "content"}}
+ <h2>{{.Title}}</h2>
+ <p class="description">{{.Description}}</p>
+ <h3 class="sr-only">Links</h3>
+ <ul class="links">
+ {{range .Links}}
+ <li class="{{.Class}}"><a href="{{.Href}}">{{.Title}}</a></li>
+ {{end}}
+ </ul>
+ <h3 class="sr-only">Tags</h3>
+ <ul class="tags">{{range .Tags}}<li class=""><a href="/tags/{{.}}" rel="tag">{{.}}</a></li>{{end}}</ul>
+ {{if .GoGet}}
+ <p class="goget">
+ This project is <code>go get</code>-able: <code>go get code.laria.me/{{.Name}}</code>
+ </p>
+ {{end}}
+ {{if .License}}<p class="license">License: {{.License}}</p>{{end}}
+ {{if .FormattedReadme}}
+ <h3>Readme</h3>
+ <section id="readme">{{.FormattedReadme}}</section>
+ {{end}}
+{{end}}
diff --git a/data/templates/tag.html b/data/templates/tag.html
new file mode 100644
index 0000000..b2736b9
--- /dev/null
+++ b/data/templates/tag.html
@@ -0,0 +1,5 @@
+{{define "content"}}
+ <h2>Tag: {{.Tag}}</h2>
+
+ {{template "Projectlist" .Projects}}
+{{end}}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..70ccac4
--- /dev/null
+++ b/main.go
@@ -0,0 +1,171 @@
+package main
+
+import (
+ "code.laria.me/code.laria.me/projects"
+ "flag"
+ "fmt"
+ "html/template"
+ "log"
+ "net/http"
+ "path"
+ "strings"
+)
+
+var (
+ HttpLaddr = flag.String("http_laddr", ":8093", "Serve HTTP from this address")
+ DataDir = flag.String("data_dir", "/usr/lib/code.laria.me", "Data directory (static files/templates)")
+ ProjectDir = flag.String("project_dir", "/srv/code.laria.me", "Project directory")
+)
+
+func main() {
+ flag.Parse()
+
+ repo := projects.NewProjectRepo(*ProjectDir)
+ repo.ScanProjects()
+
+ templates := NewTemplates(path.Join(*DataDir, "templates"))
+ if err := templates.LoadAll(); err != nil {
+ log.Fatalln(err)
+ }
+
+ http.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
+ if req.URL.Query().Get("go-get") == "1" {
+ for _, p := range repo.Projects {
+ if p.GoGet != "" {
+ fmt.Fprintf(
+ resp,
+ "<meta name=\"go-import\" content=\"code.laria.me/%s %s\">",
+ p.Name,
+ p.GoGet,
+ )
+ }
+ }
+
+ return
+ }
+
+ if req.URL.Path == "/" {
+ var data mainTemplateData
+ data.Projects = repo.Projects
+ data.Tagcloud = repo.Tagcloud
+ data.Tags = []string{"programming", "code", "git", "source", "sourcecode"}
+
+ templates.Main.Execute(resp, data)
+ return
+ }
+
+ proj, ok := repo.Projects[req.URL.Path[1:]]
+ if ok {
+ templates.Project.Execute(resp, proj)
+ return
+ }
+
+ resp.WriteHeader(404)
+ templates.Notfound.Execute(resp, commonTemplateData{})
+ })
+ http.Handle("/tags/", http.StripPrefix("/tags/", http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+ tag := req.URL.String()
+ projs, ok := repo.Tags[tag]
+ if !ok {
+ resp.WriteHeader(404)
+ templates.Notfound.Execute(resp, commonTemplateData{})
+ return
+ }
+
+ var data tagTemplateData
+
+ data.Tags = []string{tag}
+ data.Title = tag
+ data.Tag = tag
+ data.Projects = make(map[string]projects.Project)
+
+ for pname := range projs {
+ p, ok := repo.Projects[pname]
+ if ok {
+ data.Projects[pname] = p
+ }
+ }
+
+ templates.Tag.Execute(resp, data)
+ })))
+ http.HandleFunc("/__refresh__", func(resp http.ResponseWriter, req *http.Request) {
+ // Should be shielded from the internet, e.g. by putting this into the nginx config:
+ // location /__refresh__ { return 403; }
+
+ resp.Header().Add("Content-Type", "text/plain")
+
+ if err := templates.LoadAll(); err != nil {
+ fmt.Fprintf(resp, "Failed loading templates: %s", err)
+
+ log.Panicln(err)
+ }
+ repo.ScanProjects()
+ resp.Write([]byte("OK"))
+ })
+ http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(path.Join(*DataDir, "static")))))
+
+ log.Fatalln(http.ListenAndServe(*HttpLaddr, nil))
+}
+
+type Templates struct {
+ Main, Project, Tag, Notfound *template.Template
+ dir string
+}
+
+func NewTemplates(dir string) *Templates {
+ return &Templates{
+ dir: dir,
+ }
+}
+
+func (t *Templates) LoadAll() error {
+ var err error
+ if err = t.loadMain(); err != nil {
+ return err
+ }
+ if t.Project, err = t.loadSub("project"); err != nil {
+ return err
+ }
+ if t.Tag, err = t.loadSub("tag"); err != nil {
+ return err
+ }
+ if t.Notfound, err = t.loadSub("notfound"); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (t *Templates) loadMain() (err error) {
+ t.Main, err = template.New("main.html").Funcs(template.FuncMap{
+ "join": strings.Join,
+ }).ParseFiles(path.Join(t.dir, "main.html"))
+ if err != nil {
+ return err
+ }
+ t.Main.Funcs(template.FuncMap{
+ "join": strings.Join,
+ })
+
+ return nil
+}
+
+func (t *Templates) loadSub(name string) (*template.Template, error) {
+ return template.Must(t.Main.Clone()).ParseFiles(path.Join(t.dir, name+".html"))
+}
+
+type commonTemplateData struct {
+ Title, Description string
+ Tags []string
+}
+
+type mainTemplateData struct {
+ commonTemplateData
+ Projects map[string]projects.Project
+ Tagcloud map[string]projects.TagcloudElem
+}
+
+type tagTemplateData struct {
+ commonTemplateData
+ Tag string
+ Projects map[string]projects.Project
+}
diff --git a/projects/project.go b/projects/project.go
new file mode 100644
index 0000000..85239c9
--- /dev/null
+++ b/projects/project.go
@@ -0,0 +1,144 @@
+package projects
+
+import (
+ "encoding/json"
+ "html/template"
+ "log"
+ "math"
+ "os"
+ "path"
+ "strings"
+)
+
+type Link struct {
+ Title string
+ Href string
+ Class string `json:",omitempty"`
+}
+
+type Project struct {
+ Name string `json:"-"`
+
+ Title string
+ Description string
+ Shortdesc string
+ GoGet string `json:",omitempty"`
+ ReadmePath string `json:",omitempty"`
+ Git string `json:",omitempty"` // local git repo
+ License string `json:",omitempty"`
+ Links []Link
+ Tags []string
+
+ dir string `json:"-"`
+ FormattedReadme template.HTML `json:"-"`
+}
+
+func LoadProject(name string) (p Project, err error) {
+ f, err := os.Open(name)
+ if err != nil {
+ return p, err
+ }
+ defer f.Close()
+
+ dec := json.NewDecoder(f)
+ if err := dec.Decode(&p); err != nil {
+ return p, err
+ }
+
+ p.dir = path.Dir(name)
+ p.formatReadme()
+ p.Name = strings.Split(path.Base(name), ".")[0]
+
+ return
+}
+
+type ProjectRepo struct {
+ Dir string
+ Projects map[string]Project
+ Tags map[string]map[string]struct{}
+ Tagcloud map[string]TagcloudElem
+}
+
+const TAGCLOUD_SIZE_CATEGORIES = 5
+
+type TagcloudElem struct {
+ Tag string
+ Size int
+}
+
+func NewProjectRepo(dir string) *ProjectRepo {
+ return &ProjectRepo{
+ Dir: dir,
+ Projects: make(map[string]Project),
+ Tags: make(map[string]map[string]struct{}),
+ Tagcloud: make(map[string]TagcloudElem),
+ }
+}
+
+func (repo *ProjectRepo) updateTags() {
+ tags := make(map[string]map[string]struct{})
+ tagcounts := make(map[string]int)
+ maxcount := 0
+
+ for name, proj := range repo.Projects {
+ for _, tag := range proj.Tags {
+ if _, ok := tags[tag]; !ok {
+ tags[tag] = make(map[string]struct{})
+ }
+ tags[tag][name] = struct{}{}
+
+ tagcount := tagcounts[tag] + 1
+ tagcounts[tag] = tagcount
+ if tagcount > maxcount {
+ maxcount = tagcount
+ }
+ }
+ }
+
+ tagcloud := make(map[string]TagcloudElem)
+ if maxcount > 0 {
+ for tag, count := range tagcounts {
+ tagcloud[tag] = TagcloudElem{
+ Tag: tag,
+ Size: int(math.Ceil((float64(count) / float64(maxcount)) * TAGCLOUD_SIZE_CATEGORIES)),
+ }
+ }
+ }
+
+ repo.Tags = tags
+ repo.Tagcloud = tagcloud
+}
+
+func (repo *ProjectRepo) ScanProjects() {
+ d, err := os.Open(repo.Dir)
+ if err != nil {
+ log.Printf("Failed scanning projects: Could not open dir: %s", err)
+ return
+ }
+ defer d.Close()
+
+ names, err := d.Readdirnames(0)
+ if err != nil {
+ log.Printf("Failed scanning projects: Could not readdir: %s", err)
+ return
+ }
+
+ projects := make(map[string]Project)
+
+ for _, fname := range names {
+ if path.Ext(fname) != ".json" {
+ continue
+ }
+
+ p, err := LoadProject(path.Join(repo.Dir, fname))
+ if err != nil {
+ log.Printf("Skip project file %s: %s", fname, err)
+ continue
+ }
+
+ projects[p.Name] = p
+ }
+
+ repo.Projects = projects
+ repo.updateTags()
+}
diff --git a/projects/readme.go b/projects/readme.go
new file mode 100644
index 0000000..2fda69b
--- /dev/null
+++ b/projects/readme.go
@@ -0,0 +1,133 @@
+package projects
+
+import (
+ "bytes"
+ "github.com/libgit2/git2go"
+ "html"
+ "html/template"
+ "io"
+ "log"
+ "os"
+ "os/exec"
+ "path"
+ "strings"
+)
+
+type ReadmeFormat string
+
+const (
+ ReadmeMarkdown ReadmeFormat = "markdown"
+ ReadmePlain ReadmeFormat = "plain"
+)
+
+func (f ReadmeFormat) Format(raw []byte) template.HTML {
+ switch f {
+ case ReadmeMarkdown:
+ return formatMarkdown(raw)
+ default:
+ return template.HTML("<pre><code>" + html.EscapeString(string(raw)) + "</code></pre>")
+ }
+}
+
+func formatMarkdown(raw []byte) template.HTML {
+ p := exec.Command("markdown")
+
+ buf := new(bytes.Buffer)
+
+ p.Stdin = bytes.NewReader(raw)
+ p.Stdout = buf
+
+ if err := p.Run(); err != nil {
+ log.Printf("Failed formatting markdown readme: %s", err)
+ return "<strong>Failed formatting markdown :(</strong>"
+ }
+
+ return template.HTML(buf.Bytes())
+}
+
+func gitReadme(gitpath string) (raw []byte, name string, err error) {
+ repo, err := git.OpenRepository(gitpath)
+ if err != nil {
+ return raw, name, err
+ }
+
+ master_tree_obj, err := repo.RevparseSingle("master:") // Root tree of commit at top of master
+ if err != nil {
+ return raw, name, err
+ }
+
+ master_tree, err := master_tree_obj.AsTree()
+ if err != nil {
+ return raw, name, err
+ }
+
+ var inner_err error = nil
+ err = master_tree.Walk(func(_ string, entry *git.TreeEntry) int {
+ if !(strings.HasPrefix(strings.ToLower(entry.Name), "readme") && entry.Type == git.ObjectBlob) {
+ return 1
+ }
+
+ name = entry.Name
+ blob, err := repo.LookupBlob(entry.Id)
+ if err == nil {
+ raw = blob.Contents()
+ } else {
+ inner_err = err
+ }
+ return -1
+ })
+
+ if err == nil && inner_err != nil {
+ err = inner_err
+ }
+
+ return
+}
+
+func (p *Project) formatReadme() {
+ readme_name := ""
+ var readme_raw []byte
+ if p.ReadmePath != "" {
+ fullpath := p.ReadmePath
+ if !path.IsAbs(fullpath) {
+ fullpath = path.Join(p.dir, fullpath)
+ }
+ fullpath = path.Clean(fullpath)
+
+ f, err := os.Open(fullpath)
+ if err != nil {
+ log.Printf("Could not get readme for %s: failed opening %s: %s", p.Name, fullpath, err)
+ return
+ }
+ defer f.Close()
+
+ buf := new(bytes.Buffer)
+ if _, err := io.Copy(buf, f); err != nil {
+ log.Printf("Could not get readme for %s: failed reading %s: %s", p.Name, fullpath, err)
+ return
+ }
+
+ readme_raw = buf.Bytes()
+ readme_name = fullpath
+ } else if len(p.Git) > 0 {
+ var err error
+ readme_raw, readme_name, err = gitReadme(p.Git)
+ if err != nil {
+ log.Printf("Could not get readme for %s from git: %s", p.Name, err)
+ return
+ }
+ if readme_name == "" {
+ return
+ }
+ } else {
+ return
+ }
+
+ format := ReadmePlain
+ switch strings.ToLower(path.Ext(readme_name)) {
+ case ".md", ".mkd", ".markdown":
+ format = ReadmeMarkdown
+ }
+
+ p.FormattedReadme = format.Format(readme_raw)
+}