Liberate history package from the qtpl shackles

This commit is contained in:
Timur Ismagilov 2025-07-03 11:35:57 +03:00
parent 7e1948c93f
commit 5dfbbdb775
4 changed files with 110 additions and 460 deletions

View File

@ -2,16 +2,67 @@ package history
import (
"fmt"
"html"
"log/slog"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/bouncepaw/mycorrhiza/internal/cfg"
"github.com/bouncepaw/mycorrhiza/internal/files"
)
// WithRevisions returns an HTML representation of `revs` that is meant to be inserted in a history page.
func WithRevisions(hyphaName string, revs []Revision) string {
var buf strings.Builder
for _, grp := range groupRevisionsByMonth(revs) {
currentYear := grp[0].Time.Year()
currentMonth := grp[0].Time.Month()
sectionId := fmt.Sprintf("%04d-%02d", currentYear, currentMonth)
buf.WriteString(fmt.Sprintf(
`<section class="history__month">
<a href="#%s" class="history__month-anchor">
<h2 id="%s" class="history__month-title">%d %s</h2>
</a>
<ul class="history__entries">`,
sectionId, sectionId, currentYear, currentMonth.String(),
))
for _, rev := range grp {
buf.WriteString(fmt.Sprintf(
`<li class="history__entry">
<a class="history-entry" href="/rev/%s/%s">
<time class="history-entry__time">%s</time>
</a>
<span class="history-entry__hash"><a href="/primitive-diff/%s/%s">%s</a></span>
<span class="history-entry__msg">%s</span>`,
rev.Hash, hyphaName,
rev.timeToDisplay(),
rev.Hash, hyphaName, rev.Hash,
html.EscapeString(rev.Message),
))
if rev.Username != "anon" {
buf.WriteString(fmt.Sprintf(
`<span class="history-entry__author">by <a href="/hypha/%s/%s" rel="author">%s</a></span>`,
cfg.UserHypha, rev.Username, rev.Username,
))
}
buf.WriteString("</li>\n")
}
buf.WriteString(`</ul></section>`)
}
return buf.String()
}
// Revision represents a revision of a hypha.
type Revision struct {
// Hash is usually short.
@ -24,6 +75,62 @@ type Revision struct {
hyphaeAffectedBuf []string
}
// HyphaeDiffsHTML returns a comma-separated list of diffs links of current revision for every affected file as HTML string.
func (rev Revision) HyphaeDiffsHTML() string {
entries := rev.hyphaeAffected()
if len(entries) == 1 {
return fmt.Sprintf(
`<a href="/primitive-diff/%s/%s">%s</a>`,
rev.Hash, entries[0], rev.Hash,
)
}
var buf strings.Builder
for i, hyphaName := range entries {
if i > 0 {
buf.WriteString(`<span aria-hidden="true">, </span>`)
}
buf.WriteString(`<a href="/primitive-diff/`)
buf.WriteString(rev.Hash)
buf.WriteString(`/`)
buf.WriteString(hyphaName)
buf.WriteString(`">`)
if i == 0 {
buf.WriteString(rev.Hash)
buf.WriteString("&nbsp;")
}
buf.WriteString(hyphaName)
buf.WriteString(`</a>`)
}
return buf.String()
}
// descriptionForFeed generates a good enough HTML contents for a web feed.
func (rev *Revision) descriptionForFeed() string {
return fmt.Sprintf(
`<p><b>%s</b> (by %s at %s)</p>
<p>Hyphae affected: %s</p>
<pre><code>%s</code></pre>`,
rev.Message, rev.Username, rev.TimeString(),
rev.HyphaeLinksHTML(),
rev.textDiff(),
)
}
// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
func (rev Revision) HyphaeLinksHTML() string {
var buf strings.Builder
for i, hyphaName := range rev.hyphaeAffected() {
if i > 0 {
buf.WriteString(`<span aria-hidden="true">, <span>`)
}
urlSafeHyphaName := url.PathEscape(hyphaName)
buf.WriteString(fmt.Sprintf(`<a href="/hypha/%s">%s</a>`, urlSafeHyphaName, hyphaName))
}
return buf.String()
}
// gitLog calls `git log` and parses the results.
func gitLog(args ...string) ([]Revision, error) {
args = append([]string{

View File

@ -1,73 +0,0 @@
{% import "fmt" %}
{% import "github.com/bouncepaw/mycorrhiza/internal/cfg" %}
HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
{% func (rev Revision) HyphaeLinksHTML() %}
{% stripspace %}
{% for i, hyphaName := range rev.hyphaeAffected() %}
{% if i > 0 %}
<span aria-hidden="true">, </span>
{% endif %}
<a href="/hypha/{%s hyphaName %}">{%s hyphaName %}</a>
{% endfor %}
{% endstripspace %}
{% endfunc %}
HyphaeDiffsHTML returns a comma-separated list of diffs links of current revision for every affected file as HTML string.
{% func (rev Revision) HyphaeDiffsHTML() %}
{% code entries := rev.hyphaeAffected() %}
{% stripspace %}
{% if len(entries) == 1 %}
<a href="/primitive-diff/{%s rev.Hash %}/{%s entries[0] %}">{%s rev.Hash %}</a>
{% else %}
{% for i, hyphaName := range entries %}
{% if i > 0 %}
<span aria-hidden="true">, </span>
{% endif %}
<a href="/primitive-diff/{%s rev.Hash %}/{%s hyphaName %}">
{% if i == 0 %}
{%s rev.Hash %}&nbsp;
{% endif %}
{%s hyphaName %}</a>
{% endfor %}
{% endif %}
{% endstripspace %}
{% endfunc %}
descriptionForFeed generates a good enough HTML contents for a web feed.
{% func (rev *Revision) descriptionForFeed() %}
<p><b>{%s rev.Message %}</b> (by {%s rev.Username %} at {%s rev.TimeString() %})</p>
<p>Hyphae affected: {%= rev.HyphaeLinksHTML() %}</p>
<pre><code>{%s rev.textDiff() %}</code></pre>
{% endfunc %}
WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
{% func WithRevisions(hyphaName string, revs []Revision) %}
{% for _, grp := range groupRevisionsByMonth(revs) %}
{% code
currentYear := grp[0].Time.Year()
currentMonth := grp[0].Time.Month()
sectionId := fmt.Sprintf("%04d-%02d", currentYear, currentMonth)
%}
<section class="history__month">
<a href="#{%s sectionId %}" class="history__month-anchor">
<h2 id="{%s sectionId %}" class="history__month-title">{%d currentYear %} {%s currentMonth.String() %}</h2>
</a>
<ul class="history__entries">
{% for _, rev := range grp %}
<li class="history__entry">
<a class="history-entry" href="/rev/{%s rev.Hash %}/{%s hyphaName %}">
<time class="history-entry__time">{%s rev.timeToDisplay() %}</time>
</a>
<span class="history-entry__hash"><a href="/primitive-diff/{%s rev.Hash %}/{%s hyphaName %}">{%s rev.Hash %}</a></span>
<span class="history-entry__msg">{%s rev.Message %}</span>
{% if rev.Username != "anon" %}
<span class="history-entry__author">by <a href="/hypha/{%s cfg.UserHypha %}/{%s rev.Username %}" rel="author">{%s rev.Username %}</a></span>
{% endif %}
</li>
{% endfor %}
</ul>
</section>
{% endfor %}
{% endfunc %}

View File

@ -1,384 +0,0 @@
// Code generated by qtc from "view.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
//line history/view.qtpl:1
package history
//line history/view.qtpl:1
import "fmt"
//line history/view.qtpl:2
import "github.com/bouncepaw/mycorrhiza/internal/cfg"
// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
//line history/view.qtpl:5
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line history/view.qtpl:5
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line history/view.qtpl:5
func (rev Revision) StreamHyphaeLinksHTML(qw422016 *qt422016.Writer) {
//line history/view.qtpl:5
qw422016.N().S(`
`)
//line history/view.qtpl:7
for i, hyphaName := range rev.hyphaeAffected() {
//line history/view.qtpl:8
if i > 0 {
//line history/view.qtpl:8
qw422016.N().S(`<span aria-hidden="true">, </span>`)
//line history/view.qtpl:10
}
//line history/view.qtpl:10
qw422016.N().S(`<a href="/hypha/`)
//line history/view.qtpl:11
qw422016.E().S(hyphaName)
//line history/view.qtpl:11
qw422016.N().S(`">`)
//line history/view.qtpl:11
qw422016.E().S(hyphaName)
//line history/view.qtpl:11
qw422016.N().S(`</a>`)
//line history/view.qtpl:12
}
//line history/view.qtpl:13
qw422016.N().S(`
`)
//line history/view.qtpl:14
}
//line history/view.qtpl:14
func (rev Revision) WriteHyphaeLinksHTML(qq422016 qtio422016.Writer) {
//line history/view.qtpl:14
qw422016 := qt422016.AcquireWriter(qq422016)
//line history/view.qtpl:14
rev.StreamHyphaeLinksHTML(qw422016)
//line history/view.qtpl:14
qt422016.ReleaseWriter(qw422016)
//line history/view.qtpl:14
}
//line history/view.qtpl:14
func (rev Revision) HyphaeLinksHTML() string {
//line history/view.qtpl:14
qb422016 := qt422016.AcquireByteBuffer()
//line history/view.qtpl:14
rev.WriteHyphaeLinksHTML(qb422016)
//line history/view.qtpl:14
qs422016 := string(qb422016.B)
//line history/view.qtpl:14
qt422016.ReleaseByteBuffer(qb422016)
//line history/view.qtpl:14
return qs422016
//line history/view.qtpl:14
}
// HyphaeDiffsHTML returns a comma-separated list of diffs links of current revision for every affected file as HTML string.
//line history/view.qtpl:18
func (rev Revision) StreamHyphaeDiffsHTML(qw422016 *qt422016.Writer) {
//line history/view.qtpl:18
qw422016.N().S(`
`)
//line history/view.qtpl:19
entries := rev.hyphaeAffected()
//line history/view.qtpl:19
qw422016.N().S(`
`)
//line history/view.qtpl:21
if len(entries) == 1 {
//line history/view.qtpl:21
qw422016.N().S(`<a href="/primitive-diff/`)
//line history/view.qtpl:22
qw422016.E().S(rev.Hash)
//line history/view.qtpl:22
qw422016.N().S(`/`)
//line history/view.qtpl:22
qw422016.E().S(entries[0])
//line history/view.qtpl:22
qw422016.N().S(`">`)
//line history/view.qtpl:22
qw422016.E().S(rev.Hash)
//line history/view.qtpl:22
qw422016.N().S(`</a>`)
//line history/view.qtpl:23
} else {
//line history/view.qtpl:24
for i, hyphaName := range entries {
//line history/view.qtpl:25
if i > 0 {
//line history/view.qtpl:25
qw422016.N().S(`<span aria-hidden="true">, </span>`)
//line history/view.qtpl:27
}
//line history/view.qtpl:27
qw422016.N().S(`<a href="/primitive-diff/`)
//line history/view.qtpl:28
qw422016.E().S(rev.Hash)
//line history/view.qtpl:28
qw422016.N().S(`/`)
//line history/view.qtpl:28
qw422016.E().S(hyphaName)
//line history/view.qtpl:28
qw422016.N().S(`">`)
//line history/view.qtpl:29
if i == 0 {
//line history/view.qtpl:30
qw422016.E().S(rev.Hash)
//line history/view.qtpl:30
qw422016.N().S(`&nbsp;`)
//line history/view.qtpl:31
}
//line history/view.qtpl:32
qw422016.E().S(hyphaName)
//line history/view.qtpl:32
qw422016.N().S(`</a>`)
//line history/view.qtpl:33
}
//line history/view.qtpl:34
}
//line history/view.qtpl:35
qw422016.N().S(`
`)
//line history/view.qtpl:36
}
//line history/view.qtpl:36
func (rev Revision) WriteHyphaeDiffsHTML(qq422016 qtio422016.Writer) {
//line history/view.qtpl:36
qw422016 := qt422016.AcquireWriter(qq422016)
//line history/view.qtpl:36
rev.StreamHyphaeDiffsHTML(qw422016)
//line history/view.qtpl:36
qt422016.ReleaseWriter(qw422016)
//line history/view.qtpl:36
}
//line history/view.qtpl:36
func (rev Revision) HyphaeDiffsHTML() string {
//line history/view.qtpl:36
qb422016 := qt422016.AcquireByteBuffer()
//line history/view.qtpl:36
rev.WriteHyphaeDiffsHTML(qb422016)
//line history/view.qtpl:36
qs422016 := string(qb422016.B)
//line history/view.qtpl:36
qt422016.ReleaseByteBuffer(qb422016)
//line history/view.qtpl:36
return qs422016
//line history/view.qtpl:36
}
// descriptionForFeed generates a good enough HTML contents for a web feed.
//line history/view.qtpl:39
func (rev *Revision) streamdescriptionForFeed(qw422016 *qt422016.Writer) {
//line history/view.qtpl:39
qw422016.N().S(`
<p><b>`)
//line history/view.qtpl:40
qw422016.E().S(rev.Message)
//line history/view.qtpl:40
qw422016.N().S(`</b> (by `)
//line history/view.qtpl:40
qw422016.E().S(rev.Username)
//line history/view.qtpl:40
qw422016.N().S(` at `)
//line history/view.qtpl:40
qw422016.E().S(rev.TimeString())
//line history/view.qtpl:40
qw422016.N().S(`)</p>
<p>Hyphae affected: `)
//line history/view.qtpl:41
rev.StreamHyphaeLinksHTML(qw422016)
//line history/view.qtpl:41
qw422016.N().S(`</p>
<pre><code>`)
//line history/view.qtpl:42
qw422016.E().S(rev.textDiff())
//line history/view.qtpl:42
qw422016.N().S(`</code></pre>
`)
//line history/view.qtpl:43
}
//line history/view.qtpl:43
func (rev *Revision) writedescriptionForFeed(qq422016 qtio422016.Writer) {
//line history/view.qtpl:43
qw422016 := qt422016.AcquireWriter(qq422016)
//line history/view.qtpl:43
rev.streamdescriptionForFeed(qw422016)
//line history/view.qtpl:43
qt422016.ReleaseWriter(qw422016)
//line history/view.qtpl:43
}
//line history/view.qtpl:43
func (rev *Revision) descriptionForFeed() string {
//line history/view.qtpl:43
qb422016 := qt422016.AcquireByteBuffer()
//line history/view.qtpl:43
rev.writedescriptionForFeed(qb422016)
//line history/view.qtpl:43
qs422016 := string(qb422016.B)
//line history/view.qtpl:43
qt422016.ReleaseByteBuffer(qb422016)
//line history/view.qtpl:43
return qs422016
//line history/view.qtpl:43
}
// WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page.
//line history/view.qtpl:46
func StreamWithRevisions(qw422016 *qt422016.Writer, hyphaName string, revs []Revision) {
//line history/view.qtpl:46
qw422016.N().S(`
`)
//line history/view.qtpl:47
for _, grp := range groupRevisionsByMonth(revs) {
//line history/view.qtpl:47
qw422016.N().S(`
`)
//line history/view.qtpl:49
currentYear := grp[0].Time.Year()
currentMonth := grp[0].Time.Month()
sectionId := fmt.Sprintf("%04d-%02d", currentYear, currentMonth)
//line history/view.qtpl:52
qw422016.N().S(`
<section class="history__month">
<a href="#`)
//line history/view.qtpl:54
qw422016.E().S(sectionId)
//line history/view.qtpl:54
qw422016.N().S(`" class="history__month-anchor">
<h2 id="`)
//line history/view.qtpl:55
qw422016.E().S(sectionId)
//line history/view.qtpl:55
qw422016.N().S(`" class="history__month-title">`)
//line history/view.qtpl:55
qw422016.N().D(currentYear)
//line history/view.qtpl:55
qw422016.N().S(` `)
//line history/view.qtpl:55
qw422016.E().S(currentMonth.String())
//line history/view.qtpl:55
qw422016.N().S(`</h2>
</a>
<ul class="history__entries">
`)
//line history/view.qtpl:58
for _, rev := range grp {
//line history/view.qtpl:58
qw422016.N().S(`
<li class="history__entry">
<a class="history-entry" href="/rev/`)
//line history/view.qtpl:60
qw422016.E().S(rev.Hash)
//line history/view.qtpl:60
qw422016.N().S(`/`)
//line history/view.qtpl:60
qw422016.E().S(hyphaName)
//line history/view.qtpl:60
qw422016.N().S(`">
<time class="history-entry__time">`)
//line history/view.qtpl:61
qw422016.E().S(rev.timeToDisplay())
//line history/view.qtpl:61
qw422016.N().S(`</time>
</a>
<span class="history-entry__hash"><a href="/primitive-diff/`)
//line history/view.qtpl:63
qw422016.E().S(rev.Hash)
//line history/view.qtpl:63
qw422016.N().S(`/`)
//line history/view.qtpl:63
qw422016.E().S(hyphaName)
//line history/view.qtpl:63
qw422016.N().S(`">`)
//line history/view.qtpl:63
qw422016.E().S(rev.Hash)
//line history/view.qtpl:63
qw422016.N().S(`</a></span>
<span class="history-entry__msg">`)
//line history/view.qtpl:64
qw422016.E().S(rev.Message)
//line history/view.qtpl:64
qw422016.N().S(`</span>
`)
//line history/view.qtpl:65
if rev.Username != "anon" {
//line history/view.qtpl:65
qw422016.N().S(`
<span class="history-entry__author">by <a href="/hypha/`)
//line history/view.qtpl:66
qw422016.E().S(cfg.UserHypha)
//line history/view.qtpl:66
qw422016.N().S(`/`)
//line history/view.qtpl:66
qw422016.E().S(rev.Username)
//line history/view.qtpl:66
qw422016.N().S(`" rel="author">`)
//line history/view.qtpl:66
qw422016.E().S(rev.Username)
//line history/view.qtpl:66
qw422016.N().S(`</a></span>
`)
//line history/view.qtpl:67
}
//line history/view.qtpl:67
qw422016.N().S(`
</li>
`)
//line history/view.qtpl:69
}
//line history/view.qtpl:69
qw422016.N().S(`
</ul>
</section>
`)
//line history/view.qtpl:72
}
//line history/view.qtpl:72
qw422016.N().S(`
`)
//line history/view.qtpl:73
}
//line history/view.qtpl:73
func WriteWithRevisions(qq422016 qtio422016.Writer, hyphaName string, revs []Revision) {
//line history/view.qtpl:73
qw422016 := qt422016.AcquireWriter(qq422016)
//line history/view.qtpl:73
StreamWithRevisions(qw422016, hyphaName, revs)
//line history/view.qtpl:73
qt422016.ReleaseWriter(qw422016)
//line history/view.qtpl:73
}
//line history/view.qtpl:73
func WithRevisions(hyphaName string, revs []Revision) string {
//line history/view.qtpl:73
qb422016 := qt422016.AcquireByteBuffer()
//line history/view.qtpl:73
WriteWithRevisions(qb422016, hyphaName, revs)
//line history/view.qtpl:73
qs422016 := string(qb422016.B)
//line history/view.qtpl:73
qt422016.ReleaseByteBuffer(qb422016)
//line history/view.qtpl:73
return qs422016
//line history/view.qtpl:73
}

View File

@ -1,10 +1,12 @@
// Command mycorrhiza is a program that runs a mycorrhiza wiki.
//
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=history
//go:generate go run github.com/valyala/quicktemplate/qtc -dir=mycoopts
package main
import (
"log/slog"
"os"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/internal/backlinks"
"github.com/bouncepaw/mycorrhiza/internal/categories"
@ -19,8 +21,6 @@ import (
"github.com/bouncepaw/mycorrhiza/web"
"github.com/bouncepaw/mycorrhiza/web/static"
"github.com/bouncepaw/mycorrhiza/web/viewutil"
"log/slog"
"os"
)
func main() {