From 822980076713a722f8ed30f66a45f49cf0e3e12a Mon Sep 17 00:00:00 2001 From: Kristian Wind Date: Wed, 19 Aug 2026 21:24:05 +0200 Subject: [PATCH] fix(notes): lister overlevede ikke en gemning, og tallene blev klippet af MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seks ting fundet ved at bruge appen, i den rækkefølge de gjorde ondt. En liste inde i en blok blev læst som tekst. htmlToMarkdown havde én gren for ul og ol og lod alt andet falde ned i inlineToMarkdown, som samler efterkommere til én streng — så et blokcitat med en liste i kom tilbage som "> PunktPunkt 2", og en note fra Apple Noter, der er fuld af citater, mistede hvert punktmærke ved den første gemning. Ingen havde rørt de linjer. Nu læses en blok, der rummer blokke, igennem frem for flad, og et citat sætter "> " foran det, der kommer indefra. Indlejrede lister fandtes ikke. Den flade `list`-variabel kunne kun beskrive ét niveau, så et indrykket punkt blev lagt ved siden af sit ophav. Nu en stak, og underlisten lægges inde i sit
  • — det er den forskel, der kan læses tilbage. En liste, der begyndte ved ti, begyndte ved ét bagefter. `start` skrives nu med og læses med, så en gemning uden ændringer ikke gør noten om. Tallet blev klippet af. Mærket sættes uden for tekstkanten, og 16px rakte til et punkttegn og ikke til "10." — resten hang ud over kanten, hvor .page beskærer. Renden er nu 2.2em og deles af begge slags, så punkter og tal flugter. # viste seks vilkårlige projekter ud af tyve og så færdig ud. Loftet er væk, listen kan rulle, og det, der begynder med det tastede, står øverst. Og to ting om, hvordan det ser ud. Noteruden har fået sin egen flade, fordi et ark, der holder op et sted, skiller det, man skriver, fra det program, man skriver det i — `--surface` frem for `#fff`, så de fem temaer hver får deres. Noter stod for sig selv over en streg, hvilket sagde det rigtige og kostede halvfjerds pixels; den står nu i rækken med de faste visninger og bærer et ark i stedet for en prik, hvilket siger det samme gratis. Prøven skriver de fem listeformer, gemmer gennem den rigtige editor og læser filen bagefter. Co-Authored-By: Claude Opus 5 --- web/e2e/smoke.spec.js | 76 ++++++++++ web/src/lib/components/NoteEditor.svelte | 64 ++++++++- web/src/lib/components/Sidebar.svelte | 65 ++++++--- web/src/lib/richtext.js | 175 +++++++++++++++++++---- web/src/routes/noter/+page.svelte | 18 ++- 5 files changed, 341 insertions(+), 57 deletions(-) diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index d330b91..0bf9457 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -2312,3 +2312,79 @@ test('en kodeblok ser ud som en terminal og bliver farvet', async ({ page }) => expect(trouble).toEqual([]); }); + +test('lister overlever en gemning — også i et citat, og også med numre', async ({ page }) => { + const trouble = watchForTrouble(page); + await page.goto('/noter'); + + // Formerne, der hver især gik tabt: en punktliste, en nummereret, en der + // begynder et andet sted end ved ét, en liste i en liste, og en liste inde i et + // citat. Den sidste er den, en note fra Apple Noter er fuld af. + const markdown = [ + '# Former', + '', + '- Punkt', + '- Punkt 2', + '', + '1. Nummer', + '2. Nummer 2', + '', + '10. Ti', + '11. Elleve', + '', + '- Ydre', + ' - Indre', + '', + '> Et citat', + '> - med en liste i' + ].join('\n'); + + const id = await page.evaluate(async (body) => { + const r = await fetch('/api/v1/notes', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json', 'Sec-Fetch-Site': 'same-origin' }, + body: JSON.stringify({ body }) + }); + return (await r.json()).id; + }, markdown); + + await page.goto('/noter?note=' + id); + const ed = page.getByRole('textbox', { name: 'Notens tekst' }); + await expect(ed.locator('ul > li').first()).toBeVisible(); + + // Vist som lister, ikke som linjer med bindestreger i. + await expect(ed.locator('ol')).toHaveCount(2); + await expect(ed.locator('blockquote ul li')).toHaveText('med en liste i'); + // Den indlejrede ligger inde i sit punkt, ikke ved siden af. + await expect(ed.locator('ul li ul li')).toHaveText('Indre'); + // Og den, der begyndte ved ti, gør det stadig. + await expect(ed.locator('ol[start="10"] li').first()).toHaveText('Ti'); + + // Rør noten, sådan som en tastning gør, og lad den gemme. + await ed.click(); + await page.keyboard.press('End'); + await page.keyboard.type(' '); + await page.waitForTimeout(1200); + + // Det, der står i filen bagefter, er det, der stod i den før. + // + // Det var her, det gik galt: alt fra det første punkt blev til én linje — + // "PunktPunkt 2NummerNummer 2" — fordi en liste inde i en blok blev læst som + // tekst. Ingen havde rørt de linjer. + const body = await page.evaluate(async (noteId) => { + const r = await fetch('/api/v1/notes/' + noteId, { + credentials: 'include', + headers: { 'Sec-Fetch-Site': 'same-origin' } + }); + return (await r.json()).body; + }, id); + + expect(body).toContain('- Punkt\n- Punkt 2'); + expect(body).toContain('1. Nummer\n2. Nummer 2'); + expect(body).toContain('10. Ti\n11. Elleve'); + expect(body).toContain('- Ydre\n - Indre'); + expect(body).toContain('> Et citat\n> - med en liste i'); + + expect(trouble).toEqual([]); +}); diff --git a/web/src/lib/components/NoteEditor.svelte b/web/src/lib/components/NoteEditor.svelte index e363cc3..2083c25 100644 --- a/web/src/lib/components/NoteEditor.svelte +++ b/web/src/lib/components/NoteEditor.svelte @@ -22,6 +22,8 @@ let { note, onchange, onsave } = $props(); let editor; + // Forslagslisten, så tastaturvalget kan rulle den frem. + let list; let stylesOpen = $state(false); // Which marks are on where the caret is, so the toolbar shows state rather than // only offering actions. @@ -166,9 +168,29 @@ return; } const term = partial.term.toLowerCase(); - suggestions = app.projects - .filter((p) => !p.is_inbox && p.name.toLowerCase().includes(term)) - .slice(0, 6); + + // Alle der passer, ikke de seks første. + // + // Loftet på seks var sat, dengang listen ikke kunne rulle, og det gjorde + // noget værre end at skjule: med tyve projekter og et blot tastet `#` viste + // den seks vilkårlige og så færdig ud. Man kunne ikke se, at der manglede + // noget — kun at ens eget projekt ikke var der. + // + // Rækkefølgen bærer nu det, loftet gjorde forkert: det, der begynder med + // det tastede, står øverst, resten alfabetisk. Skriver man "ga", er + // GarageRisteriet den første, ikke den, der tilfældigvis lå først. + const matches = app.projects.filter( + (p) => !p.is_inbox && p.name.toLowerCase().includes(term) + ); + matches.sort((a, b) => { + const an = a.name.toLowerCase(); + const bn = b.name.toLowerCase(); + const ap = an.startsWith(term); + const bp = bn.startsWith(term); + if (ap !== bp) return ap ? -1 : 1; + return an.localeCompare(bn, 'da'); + }); + suggestions = matches; chosen = 0; } @@ -227,6 +249,19 @@ } } + /** + * Ruller det valgte forslag frem, når listen er længere end sin egen kasse. + * + * Efter opdateringen, ikke før: `chosen` er netop sat, og rækken bærer klassen + * først når Svelte har skrevet den ud. Uden ventetiden ruller den til den + * forrige. + */ + function keepChosenInView() { + requestAnimationFrame(() => { + list?.querySelector('button.on')?.scrollIntoView({ block: 'nearest' }); + }); + } + function onkeydown(event) { // The suggestion list owns the arrows and return while it is open, the way // every other completion does. Escape closes it without choosing. @@ -234,11 +269,13 @@ if (event.key === 'ArrowDown') { event.preventDefault(); chosen = (chosen + 1) % suggestions.length; + keepChosenInView(); return; } if (event.key === 'ArrowUp') { event.preventDefault(); chosen = (chosen - 1 + suggestions.length) % suggestions.length; + keepChosenInView(); return; } if (event.key === 'Enter' || event.key === 'Tab') { @@ -366,7 +403,7 @@ -
      +
        {#each suggestions as project, i (project.id)}