564039112a
CI / test (pull_request) Failing after 10s
Self-contained single-file HTML export for cards. Mobile-first,
dark theme, zero dependencies. Each card type gets its own
interactive treatment: snippet tap-to-copy, template slot filling,
checklist with progress bar, decision structured layout, link
tap targets. Filter chips by type, search across all cards.
Usage: nib export -f html -o deck.html
nib export -f html -t triage -o triage.html
182 lines
3.3 KiB
Go
182 lines
3.3 KiB
Go
package export
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"html/template"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/lerko/nib/internal/db"
|
|
"github.com/lerko/nib/internal/display"
|
|
)
|
|
|
|
//go:embed template.html
|
|
var templateHTML string
|
|
|
|
type TemplateSlot struct {
|
|
Name string `json:"name"`
|
|
Default string `json:"default"`
|
|
}
|
|
|
|
type CheckStep struct {
|
|
Text string `json:"text"`
|
|
Done bool `json:"done"`
|
|
}
|
|
|
|
type DecisionData struct {
|
|
Chose string `json:"chose"`
|
|
Why string `json:"why"`
|
|
Rejected []string `json:"rejected"`
|
|
}
|
|
|
|
type CardView struct {
|
|
ID string
|
|
Glyph string
|
|
CardType string
|
|
Title string
|
|
Body template.HTML
|
|
Description template.HTML
|
|
SearchText string
|
|
Pinned bool
|
|
Tags []string
|
|
UseCount int
|
|
LinkURL string
|
|
Slots []TemplateSlot
|
|
TemplateBody string
|
|
Steps []CheckStep
|
|
Progress int
|
|
Decision DecisionData
|
|
}
|
|
|
|
type DeckView struct {
|
|
Title string
|
|
Count int
|
|
ExportedAt string
|
|
Types []string
|
|
Cards []CardView
|
|
}
|
|
|
|
func RenderHTML(w io.Writer, entities []*db.Entity, title string) error {
|
|
tmpl, err := template.New("deck").Parse(templateHTML)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
seen := map[string]bool{}
|
|
var types []string
|
|
var cards []CardView
|
|
|
|
for _, e := range entities {
|
|
if e.CardType == nil {
|
|
continue
|
|
}
|
|
ct := string(*e.CardType)
|
|
if !seen[ct] {
|
|
seen[ct] = true
|
|
types = append(types, ct)
|
|
}
|
|
cards = append(cards, buildCardView(e))
|
|
}
|
|
|
|
data := DeckView{
|
|
Title: title,
|
|
Count: len(cards),
|
|
ExportedAt: time.Now().Format("Jan 2, 2006"),
|
|
Types: types,
|
|
Cards: cards,
|
|
}
|
|
|
|
return tmpl.Execute(w, data)
|
|
}
|
|
|
|
func buildCardView(e *db.Entity) CardView {
|
|
ct := ""
|
|
if e.CardType != nil {
|
|
ct = string(*e.CardType)
|
|
}
|
|
|
|
title := ""
|
|
if e.Title != nil {
|
|
title = *e.Title
|
|
}
|
|
|
|
body := e.Body
|
|
desc := ""
|
|
if e.Description != nil {
|
|
desc = *e.Description
|
|
}
|
|
|
|
search := strings.ToLower(title + " " + body + " " + desc + " " + strings.Join(e.Tags, " "))
|
|
|
|
cv := CardView{
|
|
ID: e.ID,
|
|
Glyph: display.DisplayGlyph(e.Glyph, e.CardType),
|
|
CardType: ct,
|
|
Title: title,
|
|
Body: template.HTML(template.HTMLEscapeString(body)),
|
|
SearchText: search,
|
|
Pinned: e.Pinned,
|
|
Tags: e.Tags,
|
|
UseCount: e.UseCount,
|
|
}
|
|
|
|
if desc != "" {
|
|
cv.Description = template.HTML(template.HTMLEscapeString(desc))
|
|
}
|
|
|
|
if e.CardData != nil {
|
|
parseCardData(&cv, ct, *e.CardData, body)
|
|
}
|
|
|
|
return cv
|
|
}
|
|
|
|
func parseCardData(cv *CardView, ct string, raw string, body string) {
|
|
switch ct {
|
|
case "snippet":
|
|
// body is the snippet content, already set
|
|
|
|
case "template":
|
|
var data struct {
|
|
Slots []TemplateSlot `json:"slots"`
|
|
}
|
|
if json.Unmarshal([]byte(raw), &data) == nil {
|
|
cv.Slots = data.Slots
|
|
}
|
|
cv.TemplateBody = body
|
|
|
|
case "checklist":
|
|
var data struct {
|
|
Steps []CheckStep `json:"steps"`
|
|
}
|
|
if json.Unmarshal([]byte(raw), &data) == nil {
|
|
cv.Steps = data.Steps
|
|
done := 0
|
|
for _, s := range cv.Steps {
|
|
if s.Done {
|
|
done++
|
|
}
|
|
}
|
|
if len(cv.Steps) > 0 {
|
|
cv.Progress = (done * 100) / len(cv.Steps)
|
|
}
|
|
}
|
|
|
|
case "decision":
|
|
var dec DecisionData
|
|
if json.Unmarshal([]byte(raw), &dec) == nil {
|
|
cv.Decision = dec
|
|
}
|
|
|
|
case "link":
|
|
var data struct {
|
|
URL string `json:"url"`
|
|
}
|
|
if json.Unmarshal([]byte(raw), &data) == nil {
|
|
cv.LinkURL = data.URL
|
|
}
|
|
}
|
|
}
|