diff options
Diffstat (limited to 'article')
-rw-r--r-- | article/article.go | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/article/article.go b/article/article.go new file mode 100644 index 0000000..0a219a1 --- /dev/null +++ b/article/article.go @@ -0,0 +1,293 @@ +package article + +import ( + "bufio" + "database/sql" + "errors" + "html" + "io" + "os" + "path" + "regexp" + "strings" + "time" + + "code.laria.me/laria.me/dbutils" + "code.laria.me/laria.me/markdown" +) + +var ( + ErrBrokenHeader = errors.New("The article header is broken") + ErrMissingMandatoryHeaders = errors.New("The article header is missing some mandatory headers") +) + +type Article struct { + Slug string + Published time.Time + Hidden bool + Title string + SummaryHtml string + FullHtml string + Tags map[string]struct{} +} + +var reHtmlTag = regexp.MustCompile(`<([^>'"]+|'[^']*'|"[^"]*")*>`) +var reHtmlEntity = regexp.MustCompile(`&[^;]*;`) + +func stripTags(s string) string { + s = reHtmlTag.ReplaceAllString(s, "") + s = reHtmlEntity.ReplaceAllStringFunc(s, html.UnescapeString) + return s +} + +func (a Article) updateDbDetails(tx *sql.Tx, id int64) error { + _, err := tx.Exec(` + UPDATE article SET + published = ?, + hidden = ?, + title = ?, + summary_html = ?, + full_html = ?, + full_plain = ? + WHERE article_id = ? + `, a.Published.Format("2006-01-02 15:04:05"), a.Hidden, a.Title, a.SummaryHtml, a.FullHtml, stripTags(a.FullHtml), id) + + return err +} + +func (a Article) createInDb(tx *sql.Tx) (int64, error) { + res, err := tx.Exec(` + INSERT INTO article SET + slug = ?, + published = ?, + hidden = ?, + title = ?, + summary_html = ?, + full_html = ?, + full_plain = ? + `, a.Slug, a.Published.Format("2006-01-02 15:04:05"), a.Hidden, a.Title, a.SummaryHtml, a.FullHtml, stripTags(a.FullHtml)) + + if err != nil { + return 0, err + } + + return res.LastInsertId() +} + +func removeTags(tx *sql.Tx, id int64) error { + _, err := tx.Exec(`DELETE FROM article_tag WHERE article_id = ?`, id) + return err +} + +func setTags(tx *sql.Tx, id int64, tags map[string]struct{}) error { + stmt, err := tx.Prepare(`REPLACE INTO article_tag SET article_id = ?, tag = ?`) + if err != nil { + return err + } + defer stmt.Close() + + for tag := range tags { + if _, err = stmt.Exec(id, tag); err != nil { + return err + } + } + + return nil +} + +func (a Article) saveToDb(tx *sql.Tx) (int64, error) { + var id int64 + switch err := tx.QueryRow(`SELECT article_id FROM article WHERE slug = ?`, a.Slug).Scan(&id); err { + case nil: + if err = a.updateDbDetails(tx, id); err != nil { + return 0, err + } + case sql.ErrNoRows: + id, err = a.createInDb(tx) + if err != nil { + return 0, err + } + default: + return 0, err + } + + if err := removeTags(tx, id); err != nil { + return 0, err + } + + if err := setTags(tx, id, a.Tags); err != nil { + return 0, err + } + + return id, nil +} + +func (a Article) SaveToDb(db *sql.DB) (int64, error) { + tx, err := db.Begin() + if err != nil { + return 0, err + } + + id, err := a.saveToDb(tx) + err = dbutils.TxCommitIfOk(tx, err) + return id, err +} + +func DeleteArticlesFromDbExcept(db *sql.DB, slugs []string) error { + if len(slugs) == 0 { + return nil + } + + query := new(strings.Builder) + query.WriteString("DELETE FROM article WHERE slug NOT IN (?") + + for i := 1; i < len(slugs); i++ { // intentionally starting at 1, since the first '?' is already there + query.WriteString(",?") + } + + query.WriteString(")") + + slugsAsInterfaces := make([]interface{}, 0, len(slugs)) + for _, slug := range slugs { + slugsAsInterfaces = append(slugsAsInterfaces, interface{}(slug)) + } + + _, err := db.Exec(query.String(), slugsAsInterfaces...) + return err +} + +func splitTags(s string) map[string]struct{} { + tags := make(map[string]struct{}) + + parts := strings.Split(s, ",") + for _, part := range parts { + tag := strings.TrimSpace(part) + if tag != "" { + tags[tag] = struct{}{} + } + } + + return tags +} + +func parseDate(s string) (time.Time, error) { + return time.Parse("2006-01-02 15:04:05", s) +} + +func parseHeader(scanner *bufio.Scanner) (Article, error) { + var article Article + var err error + + seenTitle := false + seenPublished := false + + for scanner.Scan() { + line := scanner.Text() + line = strings.TrimSpace(line) + + if line == "" { + break + } + + parts := strings.SplitN(line, ":", 2) + if len(parts) != 2 { + return Article{}, ErrBrokenHeader + } + + key := strings.ToLower(strings.TrimSpace(parts[0])) + value := strings.TrimSpace(parts[1]) + + switch key { + case "title": + article.Title = value + seenTitle = true + case "tags": + article.Tags = splitTags(value) + case "date": + if article.Published, err = parseDate(value); err != nil { + return Article{}, err + } + seenPublished = true + case "hidden": + article.Hidden = strings.ToLower(value) == "yes" + } + } + + if err = scanner.Err(); err != nil { + return Article{}, err + } + + if !seenTitle || !seenPublished { + return Article{}, ErrMissingMandatoryHeaders + } + + return article, nil +} + +var reMore = regexp.MustCompile(`^\s*~~+(?i:more)~~+\s*$`) + +func parseText(scanner *bufio.Scanner, article *Article) error { + var err error + + builder := new(strings.Builder) + + for scanner.Scan() { + line := scanner.Text() + + if reMore.MatchString(line) { + if article.SummaryHtml, err = markdown.Parse(builder.String()); err != nil { + return err + } + + continue + } + + builder.WriteString(line) + builder.WriteRune('\n') + } + + if err = scanner.Err(); err != nil { + return err + } + + if article.FullHtml, err = markdown.Parse(builder.String()); err != nil { + return err + } + + return nil +} + +func ParseArticle(r io.Reader) (Article, error) { + var article Article + + scanner := bufio.NewScanner(r) + article, err := parseHeader(scanner) + if err != nil { + return Article{}, err + } + + if err = parseText(scanner, &article); err != nil { + return Article{}, err + } + + return article, nil +} + +func LoadArticle(filename string) (Article, error) { + parts := strings.Split(path.Base(filename), ".") + slug := strings.Join(parts[:len(parts)-1], ".") + + f, err := os.Open(filename) + if err != nil { + return Article{}, err + } + defer f.Close() + + article, err := ParseArticle(f) + if err != nil { + return Article{}, err + } + + article.Slug = slug + return article, nil +} |