From 369d2c3e395903f6aff1d1869a81290d8bc994fa Mon Sep 17 00:00:00 2001 From: Laria Carolin Chabowski Date: Sat, 14 Oct 2017 16:04:26 +0200 Subject: Initial commit --- README.md | 4 + data/static/microawesome.woff | Bin 0 -> 2412 bytes data/static/style.css | 181 ++++++++++++++++++++++++++++++++++++++++++ data/templates/main.html | 40 ++++++++++ data/templates/notfound.html | 3 + data/templates/project.html | 22 +++++ data/templates/tag.html | 5 ++ main.go | 171 +++++++++++++++++++++++++++++++++++++++ projects/project.go | 144 +++++++++++++++++++++++++++++++++ projects/readme.go | 133 +++++++++++++++++++++++++++++++ 10 files changed, 703 insertions(+) create mode 100644 README.md create mode 100644 data/static/microawesome.woff create mode 100644 data/static/style.css create mode 100644 data/templates/main.html create mode 100644 data/templates/notfound.html create mode 100644 data/templates/project.html create mode 100644 data/templates/tag.html create mode 100644 main.go create mode 100644 projects/project.go create mode 100644 projects/readme.go 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 Binary files /dev/null and b/data/static/microawesome.woff 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"}} + +{{end}} + + + + + + + {{ if .Title }}{{ .Title }} — {{ end }}code.laria.me + {{ if .Description }}{{ end }} + {{ if .Tags }}{{ end }} + + + + +
+

code.laria.me

+
+ {{block "content" .}} +

List of projects

+ {{template "Projectlist" .Projects}} + +

Tags

+ + {{end}} + + + 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"}} +

{{.Title}}

+

{{.Description}}

+

Links

+ +

Tags

+ + {{if .GoGet}} +

+ This project is go get-able: go get code.laria.me/{{.Name}} +

+ {{end}} + {{if .License}}

License: {{.License}}

{{end}} + {{if .FormattedReadme}} +

Readme

+
{{.FormattedReadme}}
+ {{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"}} +

Tag: {{.Tag}}

+ + {{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, + "", + 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("
" + html.EscapeString(string(raw)) + "
") + } +} + +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 "Failed formatting markdown :(" + } + + 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) +} -- cgit v1.2.3-54-g00ecf