diff options
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/generate/imworkingon.html.tmpl | 134 | ||||
-rw-r--r-- | cmd/generate/main.go | 84 | ||||
-rw-r--r-- | cmd/generate/src_contribs.go | 103 | ||||
-rw-r--r-- | cmd/generate/src_tags.go | 24 | ||||
-rw-r--r-- | cmd/generate/src_upstreams.go | 46 |
5 files changed, 391 insertions, 0 deletions
diff --git a/cmd/generate/imworkingon.html.tmpl b/cmd/generate/imworkingon.html.tmpl new file mode 100644 index 0000000..54d9431 --- /dev/null +++ b/cmd/generate/imworkingon.html.tmpl @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Luke is working on</title> + <style> + body { + width: 98%; + max-width: 1024px; + margin-left: auto; + margin-right: auto; + + font-family: sans-serif; + } + * { + box-model: border-box; + } + kbd, code, samp, tt, pre { + background: #DDDDFF; + } + kbd, code, samp, tt, { + white-space: pre-wrap; + } + h1, h2, h3, h4, h5, h6 { + font-family: sans-serif; + } + h1 { + text-align: center; + background-color: #DDDDFF; + } + a { + text-decoration: none; + } + a:hover, a:focus { + text-decoration: underline; + } + article { + border: solid 1px #333333; + border-radius: 1em; + margin: 0.5em; + } + div > p:first-child { + margin-top: 0; + } + div > p:last-child { + margin-bottom: 0; + } + + /* tags */ + article.tag { + padding: 0.5em 2em; + } + article.tag > h2 { + margin: 0 0 0.25em -1em; + } + + /* contribs */ + article.contrib { + display: grid; + grid-template-columns: 25% 75%; + padding: 0; + overflow: hidden; + } + article.contrib > div { + padding: 0.5em; + } + article.contrib div.contrib-upstream-name { + grid-row: 1 / 3; + grid-column: 1; + text-align: center; + background-color: #DDDDFF; + border-right: solid 1px #8D8DA6; + font-weight: bold; + padding-top: 1em; + } + article.contrib div.contrib-upstream-desc { + grid-row: 3; + grid-column: 1; + background-color: #DDDDFF; + border-top: solid 1px #8D8DA6; + border-right: solid 1px #8D8DA6; + } + article.contrib div.contrib-urls { + grid-row: 1; + grid-column: 2; + padding-bottom: 0; + } + article.contrib div.contrib-tags { + grid-row: 2; + grid-column: 2; + padding-top: 0; + } + article.contrib div.contrib-desc { + grid-row: 3; + grid-column: 2; + border-top: solid 1px #8D8DA6; + } + </style> + </head> + <body> + <section id="tags"> + <h1>Luke is working on...</h1> + <p>... improving the GNU/Linux ecosystem.</p> + {{- range $tagName, $tagInfo := .Tags }} + <article class="tag" id="tag-{{ $tagName }}"> + <h2><a href="#tag-{{ $tagName }}">#{{ $tagName }}</a> : {{ $tagInfo.PrettyName }}</h2> + <div clasg="tag-desc">{{ $tagInfo.Desc | md2html }}</div> + </article> + {{- end }} + </section> + <section id="contribs"> + <h1>... by contributing...</h1> + {{- range $contrib := .Contributions }} + {{ $upstream := $contrib | getUpstream }} + <article class="contrib"> + <div class="contrib-upstream-name"><a href="{{ index $upstream.URLs 0 }}">{{ $upstream.Name }}</a></div> + <div class="contrib-upstream-desc">{{ $upstream.Desc | md2html }}</div> + <div class="contrib-urls"> + {{- range $url := $contrib.URLs }} + <a href="{{ $url }}"><tt>{{ $url }}</tt></a><br /> + {{- end }} + </div> + <div class="contrib-tags"> + {{- range $tag := $contrib.Tags }} + <a href="#tag-{{ $tag }}">#{{ $tag }}</a> {{/* */}} + {{- end }} + </div> + <div class="contrib-submitted-at">{{ $contrib.SubmittedAt }}</div> + <div class="contrib-desc">{{ $contrib.Desc | md2html }}</div> + </article> + {{- end }} + </section> + </body> +</html> diff --git a/cmd/generate/main.go b/cmd/generate/main.go new file mode 100644 index 0000000..ce5ce0d --- /dev/null +++ b/cmd/generate/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "bytes" + _ "embed" + "fmt" + "os" + "strings" + + "html/template" + + "github.com/yuin/goldmark" +) + +func MarkdownToHTML(md string) (template.HTML, error) { + var html strings.Builder + if err := goldmark.Convert([]byte(md), &html); err != nil { + return template.HTML(""), err + } + return template.HTML(html.String()), nil +} + +var githubProjects = map[string]string{ + "flori/json": "ruby-json", +} + +func main() { + if err := mainWithError(); err != nil { + fmt.Fprintf(os.Stderr, "%s: error: %v\n", os.Args[0], err) + os.Exit(1) + } +} + +//go:embed imworkingon.html.tmpl +var htmlTmplStr string + +func mainWithError() error { + contribs, err := ReadContribs("contribs.yml") + if err != nil { + return err + } + tags, err := ReadUpstreams("tags.yml") + if err != nil { + return err + } + upstreams, err := ReadUpstreams("upstreams.yml") + if err != nil { + return err + } + tmpl := template.Must(template.New("imworkingon.html"). + Funcs(template.FuncMap{ + "md2html": MarkdownToHTML, + "getUpstream": func(c Contribution) Upstream { + // First try any of the documented upstreams. + for _, cURL := range c.URLs { + for _, upstream := range upstreams { + for _, uURL := range upstream.URLs { + prefix := uURL + if !strings.HasSuffix(prefix, "/") { + prefix += "/" + } + if cURL == uURL || strings.HasPrefix(cURL, prefix) { + return upstream + } + } + } + } + return Upstream{URLs: []string{c.URLs[0]}, Name: "???"} + }, + }). + Parse(htmlTmplStr)) + var out bytes.Buffer + if err := tmpl.Execute(&out, map[string]any{ + "Contribs": contribs, + "Tags": tags, + "upstreams": upstreams, + }); err != nil { + return err + } + if err := os.WriteFile("plan.html", out.Bytes(), 0666); err != nil { + return err + } + return nil +} diff --git a/cmd/generate/src_contribs.go b/cmd/generate/src_contribs.go new file mode 100644 index 0000000..eaff24b --- /dev/null +++ b/cmd/generate/src_contribs.go @@ -0,0 +1,103 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "regexp" + "strings" + "time" + + "sigs.k8s.io/yaml" +) + +type Contribution struct { + URLs []string `json:"urls"` + Tags []string `json:"tags"` + SponsoredBy string `json:"sponsored-by"` + Desc string `json:"desc"` + + SubmittedAt time.Time `json:"submitted-at"` +} + +func ReadContribs(filename string) ([]Contribution, error) { + bs, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + var ret []Contribution + if err := yaml.UnmarshalStrict(bs, &ret); err != nil { + return nil, err + } + for i := range ret { + contrib := ret[i] + if err := contrib.Fill(); err != nil { + return nil, err + } + ret[i] = contrib + } + return ret, nil +} + +func (c *Contribution) Fill() error { + var err error + if c.SubmittedAt.IsZero() { + c.SubmittedAt, err = c.getSubmittedAt() + if err != nil { + return err + } + } + return nil +} + +var ( + reGitHubPR = regexp.MustCompile(`^https://github.com/([^/?#]+)/([^/?#]+)/pull/([0-9]+)(?:\?[^#]*)?(?:#.*)?$`) + rePiperMailDate = regexp.MustCompile(`^\s*<I>([^<]+)</I>\s*$`) +) + +func (c Contribution) getSubmittedAt() (time.Time, error) { + if m := reGitHubPR.FindStringSubmatch(c.URLs[0]); m != nil { + user := m[1] + repo := m[2] + prnum := m[3] + resp, err := http.Get("https://api.github.com/repos/" + user + "/" + repo + "/pulls/" + prnum) + if err != nil { + return time.Time{}, err + } + if resp.StatusCode != http.StatusOK { + return time.Time{}, fmt.Errorf("unexpected HTTP status: %v", resp.Status) + } + jsonBytes, err := io.ReadAll(resp.Body) + if err != nil { + return time.Time{}, err + } + var obj struct { + CreatedAt time.Time `json:"created_at"` + } + if err := json.Unmarshal(jsonBytes, &obj); err != nil { + return time.Time{}, err + } + return obj.CreatedAt, nil + } + if strings.Contains(c.URLs[0], "/pipermail/") { + resp, err := http.Get(c.URLs[0]) + if err != nil { + return time.Time{}, err + } + if resp.StatusCode != http.StatusOK { + return time.Time{}, fmt.Errorf("unexpected HTTP status: %v", resp.Status) + } + htmlBytes, err := io.ReadAll(resp.Body) + if err != nil { + return time.Time{}, err + } + for _, line := range strings.Split(string(htmlBytes), "\n") { + if m := rePiperMailDate.FindStringSubmatch(line); m != nil { + return time.Parse(time.UnixDate, m[1]) + } + } + } + return time.Time{}, fmt.Errorf("idk how to get timestamps for %q", c.URLs[0]) +} diff --git a/cmd/generate/src_tags.go b/cmd/generate/src_tags.go new file mode 100644 index 0000000..497f37e --- /dev/null +++ b/cmd/generate/src_tags.go @@ -0,0 +1,24 @@ +package main + +import ( + "os" + + "sigs.k8s.io/yaml" +) + +type TagInfo struct { + PrettyName string `json:"prettyName"` + Desc string `json:"desc"` +} + +func ReadTags(filename string) (map[string]TagInfo, error) { + bs, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + var ret map[string]TagInfo + if err := yaml.UnmarshalStrict(bs, &ret); err != nil { + return nil, err + } + return ret, nil +} diff --git a/cmd/generate/src_upstreams.go b/cmd/generate/src_upstreams.go new file mode 100644 index 0000000..1ea7750 --- /dev/null +++ b/cmd/generate/src_upstreams.go @@ -0,0 +1,46 @@ +package main + +import ( + _ "embed" + "net/url" + "os" + "path" + + "sigs.k8s.io/yaml" +) + +type Upstream struct { + URLs []string `json:"urls"` + Name string `json:"name"` + Desc string `json:"desc"` +} + +func ReadUpstreams(filename string) ([]Upstream, error) { + bs, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + var ret []Upstream + if err := yaml.UnmarshalStrict(bs, &ret); err != nil { + return []Upstream{}, err + } + for i := range ret { + upstream := ret[i] + if err := upstream.Fill(); err != nil { + return nil, err + } + ret[i] = upstream + } + return ret, nil +} + +func (upstream *Upstream) Fill() error { + if upstream.Name == "" { + u, err := url.Parse(upstream.URLs[0]) + if err != nil { + return err + } + _, upstream.Name = path.Split(path.Clean(u.Path)) + } + return nil +} |