Commit cf0243271b42

Vincent Demeester <vincent@sbr.pm>
2024-06-13 10:47:54
tools/go-org-readwise: generate a title file from an entry
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent b69709e
Changed files (5)
tools
go-org-readwise
tools/go-org-readwise/internal/org/org.go
@@ -1,8 +1,30 @@
 package org
 
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+
+	"github.com/niklasfasching/go-org/org"
+	"github.com/vdemeester/home/tools/go-org-readwise/internal/readwise"
+)
+
+const (
+	// denote-id-format "%Y%m%dT%H%M%S"
+	denoteDateFormat = "20060102T150405"
+	// punctionation that is removed from file names.
+	denoteExcludedPunctuationRegexpStr = "[][{}!@#$%^&*()=+'\"?,.|;:~`‘’“”/]*"
+)
+
+var (
+	denoteExcludedPunctuationRegexp = regexp.MustCompile(denoteExcludedPunctuationRegexpStr)
+	replaceHypensRegexp             = regexp.MustCompile("[-]+")
+)
+
 /*
 For each results:
-- Define a filename (denote naming — gonna be weird but meh)
+- Define a filename (denote naming — gonna be weird but meh) — from title + first highlight date
 - Detect if the file exists
 - If the file doesn't exist, create the file
 - If the file exist, append
@@ -10,3 +32,81 @@ For each results:
 For the file format: org file with denote naming
 And use the update date to add new highlights
 */
+
+func Sync(ctx context.Context, target string, results []readwise.Result) error {
+	for _, result := range results {
+		// FIXME: handle the case where tags where added after
+		// a sync. In that case, we want to try different
+		// titles (without tags, …) ; most likely we want to
+		// use a regexp to "detect" part of the thing.
+		filename := denoteFilename(result)
+		fmt.Println("file", filename)
+	}
+	return nil
+}
+
+// See https://protesilaos.com/emacs/denote#h:4e9c7512-84dc-4dfb-9fa9-e15d51178e5d
+// DATE==SIGNATURE--TITLE__KEYWORDS.EXTENSION
+// Examples:
+// - 20240611T100401--tuesday-11-june-2024__journal.org
+// - 20240511T100401==readwise--foo__bar_baz.org
+func denoteFilename(result readwise.Result) string {
+	var date, signature, title, keywords string
+	// The DATE field represents the date in year-month-day format
+	// followed by the capital letter T (for “time”) and the
+	// current time in hour-minute-second notation. The
+	// presentation is compact: 20220531T091625. The DATE serves
+	// as the unique identifier of each note and, as such, is also
+	// known as the file’s ID or identifier.
+	date = result.FirstHighlightDate().Format(denoteDateFormat)
+
+	// File names can include a string of alphanumeric characters
+	// in the SIGNATURE field. Signatures have no clearly defined
+	// purpose and are up to the user to define. One use-case is
+	// to use them to establish sequential relations between files
+	// (e.g. 1, 1a, 1b, 1b1, 1b2, …).
+	// We use signature to mark files synced from readwise.
+	signature = "==readwise"
+
+	// The TITLE field is the title of the note, as provided by
+	// the user. It automatically gets downcased by default and is
+	// also hyphenated (Sluggification of file name
+	// components). An entry about “Economics in the Euro Area”
+	// produces an economics-in-the-euro-area string for the TITLE
+	// of the file name.
+	title = sluggify(result.Title)
+
+	// The KEYWORDS field consists of one or more entries
+	// demarcated by an underscore (the separator is inserted
+	// automatically). Each keyword is a string provided by the
+	// user at the relevant prompt which broadly describes the
+	// contents of the entry.
+	if len(result.BookTags) > 0 {
+		tags := make([]string, len(result.BookTags))
+		for i, t := range result.BookTags {
+			tags[i] = sluggify(t.Name)
+		}
+		keywords = "__" + strings.Join(tags, "_")
+	}
+
+	return fmt.Sprintf("%s%s--%s%s.org", date, signature, title, keywords)
+}
+
+func sluggify(s string) string {
+	// Remove punctuation
+	s = denoteExcludedPunctuationRegexp.ReplaceAllString(s, "")
+	// Replace spaces with hypens
+	s = strings.ReplaceAll(s, " ", "-")
+	// Replace underscore with hypens
+	s = strings.ReplaceAll(s, "_", "-")
+	// Replace multiple hypens with a single one
+	s = replaceHypensRegexp.ReplaceAllString(s, "-")
+	// Remove any leading and trailing hypen
+	s = strings.TrimPrefix(s, "-")
+	s = strings.TrimSuffix(s, "-")
+	return s
+}
+
+func Foo() {
+	org.New()
+}
tools/go-org-readwise/internal/org/org_test.go
@@ -0,0 +1,34 @@
+package org
+
+import "testing"
+
+func TestSluggify(t *testing.T) {
+	testCases := []struct {
+		input    string
+		expected string
+	}{{
+		input:    "",
+		expected: "",
+	}, {
+		input:    "abcde",
+		expected: "abcde",
+	}, {
+		input:    "abcde---",
+		expected: "abcde",
+	}, {
+		input:    "a-b c--de",
+		expected: "a-b-c-de",
+	}, {
+		input:    "a_bc__de",
+		expected: "a-bc-de",
+	}, {
+		input:    "abcde$[)",
+		expected: "abcde",
+	}}
+	for _, tc := range testCases {
+		output := sluggify(tc.input)
+		if output != tc.expected {
+			t.Errorf("input \"%s\": expected %s, got %s", tc.input, tc.expected, output)
+		}
+	}
+}
tools/go-org-readwise/internal/readwise/types.go
@@ -1,5 +1,7 @@
 package readwise
 
+import "time"
+
 type Export struct {
 	Count          int      `json:"count"`
 	NextPageCursor *int     `json:"nextPageCursor"`
@@ -23,24 +25,31 @@ type Result struct {
 	Highlights    []Highlight `json:"highlights"`
 }
 
-type Highlight struct {
-	Text          string `json:"text"`
-	ID            int    `json:"id"`
-	Note          string `json:"note"`
-	Location      int    `json:"location"`
-	LocationType  string `json:"location_type"`
-	HighlightedAt string `json:"highlighted_at"`
-	BookID        int    `json:"book_id"`
-	URL           string `json:"url"`
-	Color         string `json:"color"`
-	Updated       string `json:"updated"`
-	Tags          []Tag  `json:"tags"`
+func (r Result) FirstHighlightDate() *time.Time {
+	if len(r.Highlights) == 0 {
+		return nil
+	}
+	var t time.Time
+	for _, h := range r.Highlights {
+		if h.HighlightedAt.After(t) {
+			t = h.HighlightedAt
+		}
+	}
+	return &t
 }
 
-type Tags struct {
-	Count   int    `json:"count"`
-	Next    string `json:"next"`
-	Results []Tag  `json:"results"`
+type Highlight struct {
+	Text          string    `json:"text"`
+	ID            int       `json:"id"`
+	Note          string    `json:"note"`
+	Location      int       `json:"location"`
+	LocationType  string    `json:"location_type"`
+	HighlightedAt time.Time `json:"highlighted_at"`
+	BookID        int       `json:"book_id"`
+	URL           string    `json:"url"`
+	Color         string    `json:"color"`
+	Updated       time.Time `json:"updated"`
+	Tags          []Tag     `json:"tags"`
 }
 
 type Tag struct {
tools/go-org-readwise/go.mod
@@ -1,3 +1,11 @@
 module github.com/vdemeester/home/tools/go-org-readwise
 
 go 1.22
+
+require github.com/niklasfasching/go-org v1.7.0
+
+require (
+	github.com/alecthomas/chroma/v2 v2.5.0 // indirect
+	github.com/dlclark/regexp2 v1.4.0 // indirect
+	golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
+)
tools/go-org-readwise/main.go
@@ -9,6 +9,7 @@ import (
 	"path/filepath"
 	"time"
 
+	"github.com/vdemeester/home/tools/go-org-readwise/internal/org"
 	"github.com/vdemeester/home/tools/go-org-readwise/internal/readwise"
 )
 
@@ -38,29 +39,18 @@ func main() {
 	fmt.Println(*targetFolder)
 	fmt.Println("updateAfter", updateAfter)
 	ctx := context.Background()
-	highlights, err := readwise.FetchFromAPI(ctx, apikey, updateAfter)
+	results, err := readwise.FetchFromAPI(ctx, apikey, updateAfter)
 	if err != nil {
-		log.Fatalf("Error while fetching highlights: %v", err)
+		log.Fatalf("Error while fetching results: %v", err)
 	}
 	// if err := os.WriteFile(stateFile, []byte(time.Now().Format(readwise.FormatUpdatedAfter)), 0o666); err != nil {
 	// 	log.Fatalf("Error writing readwise state file in %s: %v", stateFile, err)
 	// }
-	fmt.Println("size", len(highlights))
+	fmt.Println("size", len(results))
 
-	// updateAfter := time.Now().Add(-72 * time.Hour)
-	// fmt.Println("updateAfter:", updateAfter)
-	// mhighlights, merr := readwise.FetchFromAPI(ctx, os.Getenv("READWISE_KEY"), &updateAfter)
-	// if merr != nil {
-	// 	fmt.Fprintf(os.Stderr, "%v\n", merr)
-	// 	os.Exit(1)
-	// }
-	// fmt.Println("size", len(mhighlights))
-	// for _, h := range highlights {
-	// 	fmt.Println("title", h.Title, len(h.Highlights), h.BookTags)
-	// 	// for _, hh := range h.Highlights {
-	// 	// 	fmt.Println(">>>", hh.ID, hh.Tags)
-	// 	// }
-	// }
+	if err := org.Sync(ctx, *targetFolder, results); err != nil {
+		log.Fatalf("Error syncing readwise and org file in %s folder: %v", *targetFolder, err)
+	}
 }
 
 func getUpdateAfterFromFile(stateFile string) (*time.Time, error) {