home

My NixOS systems configurations.
Log | Files | Refs | LICENSE

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:
Mtools/go-org-readwise/go.mod | 8++++++++
Mtools/go-org-readwise/internal/org/org.go | 102++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Atools/go-org-readwise/internal/org/org_test.go | 34++++++++++++++++++++++++++++++++++
Mtools/go-org-readwise/internal/readwise/types.go | 41+++++++++++++++++++++++++----------------
Mtools/go-org-readwise/main.go | 24+++++++-----------------
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) {