Skip to content

Commit 5325021

Browse files
ComputelessComputersisyphus-dev-ai
andcommitted
preserve blank lines in markdown round-trip
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 119a4b3 commit 5325021

3 files changed

Lines changed: 30 additions & 11 deletions

File tree

src/components/editor/extensions/paragraph/ParagraphExtension.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ export const CustomParagraph = Paragraph.extend({
2424
},
2525

2626
renderMarkdown: (node, helpers,) => {
27-
const content = helpers.renderChildren(node.content || [],);
28-
if (content === "") {
29-
return `${EMPTY_MARKER}\n\n`;
30-
}
31-
return `${content}\n\n`;
27+
return helpers.renderChildren(node.content || [],);
3228
},
3329
},);

src/components/journal/EditableNote.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,10 @@ export default function EditableNote({ note, placeholder = "Start writing...", }
147147
// Parse markdown via setContent (useEditor's content prop doesn't run through Markdown extension)
148148
useEffect(() => {
149149
if (editor && note.content) {
150-
const current = editor.getMarkdown();
151-
if (current !== note.content) {
150+
// Normalize both sides: strip ZWSP + collapse blank lines so the
151+
// preprocessed note.content and the serialised editor output compare equal.
152+
const norm = (s: string,) => s.replace(/\u200B/g, "",).replace(/\n{2,}/g, "\n\n",).trimEnd();
153+
if (norm(editor.getMarkdown(),) !== norm(note.content,)) {
152154
editor.commands.setContent(note.content, { contentType: "markdown", },);
153155
}
154156
}

src/services/storage.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,31 @@ function stripNbsp(markdown: string,): string {
1919
return markdown.replace(/&nbsp;/g, "",);
2020
}
2121

22+
const ZWSP = "\u200B";
23+
24+
/**
25+
* Convert blank lines in markdown into zero-width-space paragraphs so
26+
* TipTap renders them as visible empty lines in the editor.
27+
*/
28+
function preserveBlankLines(markdown: string,): string {
29+
return markdown.trimEnd().replace(/\n{2,}/g, `\n\n${ZWSP}\n\n`,);
30+
}
31+
32+
/**
33+
* Normalize serialized markdown for writing to disk:
34+
* strip ZWSP characters and collapse runs of 3+ newlines back to 2.
35+
*/
36+
function normalizeForSave(markdown: string,): string {
37+
return markdown.replace(/\u200B/g, "",).replace(/\n{3,}/g, "\n\n",);
38+
}
39+
2240
export async function saveDailyNote(note: DailyNote,): Promise<void> {
2341
const noteDir = await getNoteDir(note.date,);
2442
await ensureDir(noteDir,);
2543
const filepath = await getNotePath(note.date,);
26-
// Convert asset:// URLs back to relative paths before writing
27-
const markdown = stripNbsp(unresolveMarkdownImages(note.content,),);
44+
// Convert asset:// URLs back to relative paths, strip ZWSP, normalize blanks
45+
let markdown = normalizeForSave(stripNbsp(unresolveMarkdownImages(note.content,),),);
46+
if (!markdown.endsWith("\n",)) markdown += "\n";
2847
await writeTextFile(filepath, markdown,);
2948
}
3049

@@ -37,8 +56,10 @@ export async function loadDailyNote(date: string,): Promise<DailyNote | null> {
3756
}
3857

3958
const raw = await readTextFile(filepath,);
40-
// Strip &nbsp; from existing files, then resolve asset paths
41-
const content = await resolveMarkdownImages(stripNbsp(raw,),);
59+
// Strip &nbsp; from existing files, resolve asset paths, then
60+
// convert blank lines into ZWSP paragraphs so TipTap shows them.
61+
const resolved = await resolveMarkdownImages(stripNbsp(raw,),);
62+
const content = preserveBlankLines(resolved,);
4263
return { date, content, };
4364
}
4465

0 commit comments

Comments
 (0)