summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/generate/imworkingon.html.tmpl134
-rw-r--r--cmd/generate/main.go84
-rw-r--r--cmd/generate/src_contribs.go103
-rw-r--r--cmd/generate/src_tags.go24
-rw-r--r--cmd/generate/src_upstreams.go46
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
+}