2019-11-03 20:01:13 +00:00
|
|
|
package entry
|
2019-11-03 20:01:24 +00:00
|
|
|
|
|
|
|
import (
|
2019-11-08 01:54:00 +00:00
|
|
|
"fmt"
|
2020-07-23 16:13:58 +00:00
|
|
|
"io"
|
|
|
|
"os/exec"
|
2019-11-08 01:54:00 +00:00
|
|
|
"regexp"
|
|
|
|
"strings"
|
2019-11-03 20:01:24 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"github.com/rs/zerolog/log"
|
2020-03-18 18:07:34 +00:00
|
|
|
|
|
|
|
"code.chrissexton.org/cws/cabinet/db"
|
2019-11-03 20:01:24 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type Entry struct {
|
|
|
|
db *db.Database
|
|
|
|
ID int64
|
|
|
|
Slug string
|
2019-11-08 04:39:16 +00:00
|
|
|
Title string
|
2019-11-03 20:01:24 +00:00
|
|
|
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,
|
2019-11-08 01:54:00 +00:00
|
|
|
slug text unique not null,
|
|
|
|
content text not null,
|
|
|
|
created datetime not null,
|
|
|
|
updated datetime not null,
|
2019-11-03 20:01:24 +00:00
|
|
|
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,
|
2019-11-08 01:54:00 +00:00
|
|
|
name text not null,
|
2019-11-03 20:01:24 +00:00
|
|
|
entry_id integer,
|
2019-11-08 01:54:00 +00:00
|
|
|
foreign key(entry_id) references entries(id),
|
|
|
|
constraint unique_name_id unique (name, entry_id)
|
2019-11-03 20:01:24 +00:00
|
|
|
)`
|
|
|
|
_, err = tx.Exec(q)
|
|
|
|
if err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-23 16:13:58 +00:00
|
|
|
func NewFromAdoc(db *db.Database, body string) *Entry {
|
2019-11-16 13:49:07 +00:00
|
|
|
e := New(db)
|
|
|
|
e.Content = body
|
2020-02-12 22:42:03 +00:00
|
|
|
e.Title = e.GenerateTitle()
|
|
|
|
e.Slug = e.UniqueSlug()
|
2019-11-16 13:49:07 +00:00
|
|
|
return e
|
|
|
|
}
|
|
|
|
|
2020-07-23 16:13:58 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2019-11-08 05:26:02 +00:00
|
|
|
func New(db *db.Database) *Entry {
|
|
|
|
e := Entry{
|
2019-11-03 20:01:24 +00:00
|
|
|
db: db,
|
|
|
|
ID: -1,
|
|
|
|
Created: time.Now(),
|
|
|
|
Updated: time.Now(),
|
2019-11-08 05:26:02 +00:00
|
|
|
Tags: []string{},
|
2019-11-03 20:01:24 +00:00
|
|
|
}
|
2019-11-08 05:26:02 +00:00
|
|
|
e.Title = e.GenerateTitle()
|
|
|
|
e.Slug = e.UniqueSlug()
|
|
|
|
e.Content = "= " + e.Title
|
|
|
|
return &e
|
2019-11-03 20:01:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2019-11-08 04:39:16 +00:00
|
|
|
e.Title = e.GenerateTitle()
|
2019-11-03 20:01:24 +00:00
|
|
|
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
|
|
|
|
}
|
2019-11-08 04:39:16 +00:00
|
|
|
e.Title = e.GenerateTitle()
|
2019-11-03 20:01:24 +00:00
|
|
|
return e, e.populateTags()
|
|
|
|
}
|
|
|
|
|
2020-03-18 18:07:34 +00:00
|
|
|
func SearchByTag(db *db.Database, query string, tags []string) ([]*Entry, error) {
|
2019-11-03 20:01:24 +00:00
|
|
|
entries := []*Entry{}
|
2020-03-18 18:07:34 +00:00
|
|
|
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...)
|
2019-11-08 01:54:00 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else {
|
2020-03-18 18:07:34 +00:00
|
|
|
q := `select e.*
|
|
|
|
from entries e
|
|
|
|
where
|
|
|
|
content like ?
|
|
|
|
order by updated desc`
|
|
|
|
|
|
|
|
err := db.Select(&entries, q, query)
|
2019-11-08 01:54:00 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-11-03 20:01:24 +00:00
|
|
|
}
|
2020-03-18 18:07:34 +00:00
|
|
|
|
2019-11-03 20:01:24 +00:00
|
|
|
for _, e := range entries {
|
|
|
|
e.db = db
|
2019-11-08 04:39:16 +00:00
|
|
|
e.Title = e.GenerateTitle()
|
2019-11-03 20:01:24 +00:00
|
|
|
e.populateTags()
|
|
|
|
}
|
2020-03-18 18:07:34 +00:00
|
|
|
|
2019-11-03 20:01:24 +00:00
|
|
|
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
|
|
|
|
}
|
2019-11-08 05:26:02 +00:00
|
|
|
q = `delete from entries where id = ?`
|
2019-11-03 20:01:24 +00:00
|
|
|
if _, err := tx.Exec(q, e.ID); err != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return err
|
|
|
|
}
|
2019-11-08 05:26:02 +00:00
|
|
|
return tx.Commit()
|
2019-11-03 20:01:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-11-08 01:54:00 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-11-08 04:39:16 +00:00
|
|
|
func (e *Entry) GenerateTitle() string {
|
2019-11-08 01:54:00 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-11-08 04:39:16 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-11-03 20:01:24 +00:00
|
|
|
func (e *Entry) Update() error {
|
|
|
|
if e.ID == -1 {
|
|
|
|
return e.Create()
|
|
|
|
}
|
2019-11-08 01:54:00 +00:00
|
|
|
old, err := GetByID(e.db, e.ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
e.Slug = e.UniqueSlug()
|
|
|
|
|
2019-11-03 20:01:24 +00:00
|
|
|
q := `update entries set slug=?, content=?, updated=?, author_id=? where id=?`
|
2019-11-08 01:54:00 +00:00
|
|
|
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
|
2019-11-03 20:01:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2020-03-18 18:07:34 +00:00
|
|
|
|
|
|
|
func (e *Entry) HasTag(tag string) bool {
|
|
|
|
for _, t := range e.Tags {
|
|
|
|
if strings.ToLower(tag) == strings.ToLower(t) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|