package entry import ( "fmt" "io" "os/exec" "regexp" "strings" "time" "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" "code.chrissexton.org/cws/cabinet/db" ) type Entry struct { db *db.Database ID int64 Slug string Title string Content string Tags []string Created time.Time Updated time.Time AuthorID int64 `db:"author_id"` } func PrepareTable(tx *sqlx.Tx) error { q := `create table if not exists entries ( id integer primary key, slug text unique not null, content text not null, created datetime not null, updated datetime not null, author_id integer )` _, err := tx.Exec(q) if err != nil { tx.Rollback() return err } q = `create table if not exists tags ( id integer primary key, name text not null, entry_id integer, foreign key(entry_id) references entries(id), constraint unique_name_id unique (name, entry_id) )` _, err = tx.Exec(q) if err != nil { tx.Rollback() return err } return nil } func NewFromAdoc(db *db.Database, body string) *Entry { e := New(db) e.Content = body e.Title = e.GenerateTitle() e.Slug = e.UniqueSlug() return e } func pandocMdToAdoc(body string) string { log.Debug().Str("input", body).Msgf("converting md->adoc") cmd := exec.Command("pandoc", "-f", "commonmark", "-t", "asciidoctor") stdin, err := cmd.StdinPipe() if err != nil { log.Error().Err(err).Msgf("could not get stdin") } go func() { defer stdin.Close() io.WriteString(stdin, body) }() out, err := cmd.CombinedOutput() if err != nil { log.Error().Err(err).Msgf("could not get stdout") } log.Debug().Msgf("md->adoc: %s", out) return string(out) } func NewFromMd(db *db.Database, body string) *Entry { body = pandocMdToAdoc(body) return NewFromAdoc(db, body) } func New(db *db.Database) *Entry { e := Entry{ db: db, ID: -1, Created: time.Now(), Updated: time.Now(), Tags: []string{}, } e.Title = e.GenerateTitle() e.Slug = e.UniqueSlug() e.Content = "= " + e.Title return &e } func GetBySlug(db *db.Database, slug string) (Entry, error) { e := Entry{db: db} q := `select * from entries where slug = ?` if err := db.Get(&e, q, slug); err != nil { return e, err } e.Title = e.GenerateTitle() return e, e.populateTags() } func GetByID(db *db.Database, id int64) (Entry, error) { e := Entry{db: db} q := `select * from entries where id = ?` if err := db.Get(&e, q, id); err != nil { return e, err } e.Title = e.GenerateTitle() return e, e.populateTags() } func SearchByTag(db *db.Database, query string, tags []string) ([]*Entry, error) { entries := []*Entry{} query = fmt.Sprintf("%%%s%%", query) log.Debug().Str("tag query", query).Int("len(tags)", len(tags)).Msg("searching") if len(tags) > 0 { q := `select e.* from entries e inner join tags t on e.id=t.entry_id where t.name in (?) AND content like ? order by updated desc` q, args, err := sqlx.In(q, tags, query) if err != nil { return nil, err } err = db.Select(&entries, q, args...) if err != nil { return nil, err } } else { q := `select e.* from entries e where content like ? order by updated desc` err := db.Select(&entries, q, query) if err != nil { return nil, err } } for _, e := range entries { e.db = db e.Title = e.GenerateTitle() e.populateTags() } return entries, nil } func RemoveBySlug(db *db.Database, slug string) error { tx, err := db.Begin() if err != nil { return err } e, err := GetBySlug(db, slug) if err != nil { return err } q := `delete from tags where entry_id = ?` if _, err := tx.Exec(q, e.ID); err != nil { tx.Rollback() return err } q = `delete from entries where id = ?` if _, err := tx.Exec(q, e.ID); err != nil { tx.Rollback() return err } return tx.Commit() } func (e *Entry) populateTags() error { q := `select name from tags where entry_id = ?` err := e.db.Select(&e.Tags, q, e.ID) log.Debug().Interface("entry", e).Msg("populating tags") return err } func (e *Entry) addTag(tag string) error { q := `insert into tags (name,entry_id) values (?,?)` _, err := e.db.Exec(q, tag, e.ID) return err } func (e *Entry) removeTag(tag string) error { q := `delete from tags where name=? and entry_id=?` _, err := e.db.Exec(q, tag, e.ID) return err } func (e *Entry) GenerateTitle() string { candidate := strings.Split(e.Content, "\n")[0] candidateNumber := 0 r := regexp.MustCompile(`[^a-zA-Z0-9 -]`) candidate = r.ReplaceAllString(candidate, "") candidate = strings.TrimSpace(candidate) if len(candidate) == 0 { candidate = "untitled" } q := `select slug from entries where slug like ?` slugs := []string{} if err := e.db.Select(&slugs, q, candidate+"%"); err != nil { log.Debug().Err(err).Msgf("Could not get candidate slugs: %s", err) return candidate } contains := func(s string, ss []string) bool { for _, e := range ss { if s == e { return true } } return false } tmpCandidate := candidate for contains(tmpCandidate, slugs) { candidateNumber++ tmpCandidate = fmt.Sprintf("%s-%d", candidate, candidateNumber) } return tmpCandidate } func (e *Entry) UniqueSlug() string { if e.Title == "" { e.Title = e.GenerateTitle() } candidate := e.Title r := regexp.MustCompile(`[^a-zA-Z0-9 -]`) candidate = r.ReplaceAllString(candidate, "") candidate = strings.TrimSpace(candidate) candidate = strings.ReplaceAll(candidate, " ", "-") candidate = strings.ToLower(candidate) return candidate } func (e *Entry) Update() error { if e.ID == -1 { return e.Create() } old, err := GetByID(e.db, e.ID) if err != nil { return err } e.Slug = e.UniqueSlug() q := `update entries set slug=?, content=?, updated=?, author_id=? where id=?` if _, err = e.db.Exec(q, e.Slug, e.Content, e.Updated.Unix(), e.AuthorID, e.ID); err != nil { return err } for _, t := range e.Tags { if !contains(old.Tags, t) { e.addTag(t) } } for _, t := range old.Tags { if !contains(e.Tags, t) { e.removeTag(t) } } return nil } func contains(items []string, entry string) bool { for _, e := range items { if e == entry { return true } } return false } func (e *Entry) Create() error { if e.ID != -1 { return e.Update() } tx, err := e.db.Begin() if err != nil { return err } q := `insert into entries (slug,content,created,updated,author_id) values (?,?,?,?,?)` res, err := tx.Exec(q, e.Slug, e.Content, e.Created.Unix(), e.Updated.Unix(), e.AuthorID) if err != nil { return err } e.ID, err = res.LastInsertId() if err != nil { tx.Rollback() e.ID = -1 return err } for _, t := range e.Tags { q = `insert into tags (name,entry_id) values (?,?)` _, err = tx.Exec(q, t, e.ID) if err != nil { tx.Rollback() return err } } tx.Commit() return nil } func (e *Entry) HasTag(tag string) bool { for _, t := range e.Tags { if strings.ToLower(tag) == strings.ToLower(t) { return true } } return false }