feat(notes): datoerne følger med, billederne bliver tegnet, og listen kan sorteres
Release / Build and publish image (push) Failing after 9m54s
Release / Build and publish image (push) Failing after 9m54s
Fire ting, som en rigtig import af tolv hundrede noter fandt på en time. **Billederne blev vist som ord.** `markdownToHtml` havde ingen regel for `` overhovedet, så en note fra Apple Noter viste sin egen billedadresse som tekst. Filen var hentet, gemt og hæftet på noten; det eneste, der manglede, var at nogen tegnede den. Kun vores egen adresse bliver til et billede — et `` fra en indsat tekst ville ellers hente fra en fremmed vært, når noten åbnes, og en note er det sidste sted, man vil have en sporingspixel. Og vejen tilbage skulle med: uden `IMG` i inlineToMarkdown ville den første gemning have taget hvert billede ud af noten igen. **Titlen blev revet i stykker.** Noter deler en linje op i så mange stumper, den har brug for — "Aalborg Kommune" står som `<h1>A</h1><h1>alb</h1><h1>org Kom</h1><h1>mune</h1>`, fordi der er en attributkørsel bag hver stump — og eksporten læste et linjeskift ved hver af dem. Noten kom ud som fire overskrifter oven på hinanden, altså ødelagt på den mest synlige måde der findes: i sin egen titel. Samme rod gav `**M****øde**` for fed og `*M**øde*` for kursiv, hvor de to stjerner i midten læses som fed. Nu brydes der kun, når blokken skifter, og et mærke, der lukkes og straks åbnes igen, er ingenting. **Datoerne fandtes ikke.** Eksporten læste dem aldrig, og Markdown havde ingen plads til dem — så en flytning af tolv hundrede noter blev en bunke, der alle var skrevet samme aften. Rækkefølgen er halvdelen af, hvad et arkiv er. Nu står de i et YAML-hoved, som Obsidian og Bear allerede skriver, og importen læser det. `SetNoteTimes` frem for gennem SaveNote: `updated_at` betyder "hvornår skrev *dette* program noten sidst", og lod man enhver kalder sætte den, ville kolonnen betyde det, den sidste skriver havde lyst til. **Og listen kan sorteres** — senest rørt, oprettet, navn. Favoritter står øverst uanset hvad; det er hele meningen med at gøre en note til favorit. Eksporten henter nu i klumper. Ét kald pr. note tog halvanden time for tolv hundrede, ikke fordi Noter er langsom, men fordi hvert kald er en proces, der skal startes — og det gjorde det umuligt at rette en fejl i oversættelsen og se resultatet, for hver runde var en frokostpause. Antallet af bilag kom med i klumpen af samme grund: ni ud af ti kald var en proces startet for at få svaret nul. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2ffbd9533b
commit
9b4e7686f9
@@ -369,13 +369,24 @@ func (s *Server) handleImportNotes(w http.ResponseWriter, r *http.Request) {
|
||||
continue
|
||||
}
|
||||
|
||||
n := &store.Note{CreatedBy: user.ID, Body: string(body)}
|
||||
text, wrote, changed := readFrontMatter(string(body))
|
||||
|
||||
n := &store.Note{CreatedBy: user.ID, Body: text}
|
||||
if err := s.db.SaveNote(r.Context(), n); err != nil {
|
||||
s.log.Warn("import note", "err", err, "file", name)
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
// After the save, not as part of it: SaveNote owns `updated_at` and is right
|
||||
// to, so the dates a note had before this program existed are written over
|
||||
// the top rather than passed through it.
|
||||
if !wrote.IsZero() || !changed.IsZero() {
|
||||
if err := s.db.SetNoteTimes(r.Context(), n.ID, wrote, changed); err != nil {
|
||||
s.log.Warn("import note dates", "err", err, "file", name)
|
||||
}
|
||||
}
|
||||
|
||||
// The pictures the note referred to, now that it has an id to hang them on.
|
||||
// The text is rewritten to the addresses they were given, so an image that
|
||||
// sat in the middle of a paragraph is still in the middle of it.
|
||||
@@ -466,6 +477,67 @@ func (s *Server) attachToNote(
|
||||
return body, attached
|
||||
}
|
||||
|
||||
// readFrontMatter takes the YAML block off the top of a Markdown file and reads
|
||||
// the two dates out of it, returning the rest of the text.
|
||||
//
|
||||
// Not a YAML parser, and deliberately not: the block this reads is the one this
|
||||
// program writes, plus whatever Obsidian and Bear write, and all of them use the
|
||||
// same handful of `key: value` lines. Pulling in a parser to read two dates would
|
||||
// be a dependency that can fail in ways nobody here would recognise.
|
||||
//
|
||||
// A file with no front matter comes back untouched — that is the normal case, and
|
||||
// it must never lose its first line to a rule about a block that is not there.
|
||||
func readFrontMatter(body string) (text string, created, modified time.Time) {
|
||||
if !strings.HasPrefix(body, "---\n") && !strings.HasPrefix(body, "---\r\n") {
|
||||
return body, time.Time{}, time.Time{}
|
||||
}
|
||||
rest := body[strings.Index(body, "\n")+1:]
|
||||
|
||||
end := strings.Index(rest, "\n---")
|
||||
if end < 0 {
|
||||
// An opening fence with no close is not front matter; it is a horizontal
|
||||
// rule somebody wrote at the top of their note.
|
||||
return body, time.Time{}, time.Time{}
|
||||
}
|
||||
block := rest[:end]
|
||||
after := rest[end+len("\n---"):]
|
||||
after = strings.TrimPrefix(after, "\r")
|
||||
after = strings.TrimPrefix(after, "\n")
|
||||
|
||||
for _, line := range strings.Split(block, "\n") {
|
||||
key, value, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
value = strings.TrimSpace(strings.Trim(strings.TrimSpace(value), `"'`))
|
||||
switch strings.ToLower(strings.TrimSpace(key)) {
|
||||
case "created", "created_at", "date":
|
||||
created = parseNoteTime(value)
|
||||
case "modified", "updated", "updated_at":
|
||||
modified = parseNoteTime(value)
|
||||
}
|
||||
}
|
||||
return strings.TrimLeft(after, "\n"), created, modified
|
||||
}
|
||||
|
||||
// parseNoteTime accepts the shapes these files actually carry. A date without a
|
||||
// time is that day at midnight, local — the note was written on that day, and
|
||||
// pretending to know the hour would be worse than not having one.
|
||||
func parseNoteTime(v string) time.Time {
|
||||
for _, layout := range []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02 15:04",
|
||||
"2006-01-02",
|
||||
} {
|
||||
if t, err := time.ParseInLocation(layout, v, time.Local); err == nil {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// mimeOf guesses from the extension. The archive does not carry a type, and
|
||||
// sniffing the bytes would mean reading every file twice.
|
||||
func mimeOf(name string) string {
|
||||
|
||||
Reference in New Issue
Block a user