commit cf0243271b42e0fd571e31cd6641fee9a6b8df59
parent b69709e7f3cf8a19bf4918dbbd61a996ac224623
Author: Vincent Demeester <vincent@sbr.pm>
Date: Thu, 13 Jun 2024 10:47:54 +0200
tools/go-org-readwise: generate a title file from an entry
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
Diffstat:
5 files changed, 175 insertions(+), 34 deletions(-)
diff --git a/tools/go-org-readwise/go.mod b/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
+)
diff --git a/tools/go-org-readwise/internal/org/org.go b/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()
+}
diff --git a/tools/go-org-readwise/internal/org/org_test.go b/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)
+ }
+ }
+}
diff --git a/tools/go-org-readwise/internal/readwise/types.go b/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 {
diff --git a/tools/go-org-readwise/main.go b/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) {