From 1caa82a697033ba7de861ab4fb87827e9b6fa28a Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Mon, 26 Oct 2026 15:33:43 -0500 Subject: [PATCH] fix(mobile): code in a note saved with Windows line endings stops showing up as tags A note saved with CRLF line endings leaked its code into the phone's tags and links: `#include ` inside a ```c fence became a tag, and a [[link]] in a fence counted as a real link. vault-core splits a body at "\n", so every line still ends in "\r", and the fence pattern ended in `(.*)$` with no m flag. JavaScript's `.` never matches "\r", so a CRLF fence never opened. This is the phones' copy of stripCodeContent, the same change the desktop made to its four copies in ZenNotes/zennotes 7afe7a3b (2.77.0): `\r?` before `$`. The info string still excludes the CR, so an LF note scans byte for byte as before and every line is kept. The Go server's copy was already right (RE2's `.` matches "\r"). Tests: vault-core-crlf.test.ts loads the real module and checks tags and links in a CRLF fence (both fail before) and that an LF note and the line count are unchanged. --- src/bridge/vault-core-crlf.test.ts | 29 +++++++++++++++++++++++++++++ src/bridge/vault-core.ts | 3 ++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/bridge/vault-core-crlf.test.ts diff --git a/src/bridge/vault-core-crlf.test.ts b/src/bridge/vault-core-crlf.test.ts new file mode 100644 index 0000000..bc749ee --- /dev/null +++ b/src/bridge/vault-core-crlf.test.ts @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' +import { loadMobileModule } from '../../tooling/load-mobile-module.ts' + +// A note saved with Windows line endings (CRLF) splits at "\n" into lines +// that still end in "\r", and JavaScript's `.` never matches "\r", so the +// fence pattern's `(.*)$` never matched: a CRLF fence never opened, and the +// code inside it leaked into the note's tags and links. The desktop fixed +// its copies of stripCodeContent in ZenNotes/zennotes 7afe7a3b (2.77.0); +// this is the phones' copy of the same logic. +const { stripCodeContent, extractTags, extractWikilinks } = await loadMobileModule(['./src/bridge/vault-core']) + +describe('vault-core reads code fences in notes saved with CRLF line endings', () => { + it('ignores #tags inside a fence', () => { + assert.deepEqual(extractTags('```c\r\n#include \r\n```\r\nreal #ok\r\n'), ['ok']) + assert.deepEqual(extractTags('- item\r\n ~~~\r\n #nope\r\n ~~~\r\n #after\r\n'), ['after']) + }) + + it('ignores [[links]] inside a fence', () => { + assert.deepEqual(extractWikilinks('```\r\n[[Not a link]]\r\n```\r\nSee [[Real note]]\r\n'), ['Real note']) + }) + + it('keeps every line, and reads an LF note exactly as before', () => { + const crlf = '# Note\r\n```\r\n#code\r\n```\r\ntext\r\n' + assert.equal(stripCodeContent(crlf).split('\n').length, crlf.split('\n').length) + const lf = '# Note\n```js\nconst tag = "#x"\n```\nplain #kept\n' + assert.equal(stripCodeContent(lf), '# Note\n \n \n \nplain #kept\n') + }) +}) diff --git a/src/bridge/vault-core.ts b/src/bridge/vault-core.ts index 2e0b40e..6f57773 100644 --- a/src/bridge/vault-core.ts +++ b/src/bridge/vault-core.ts @@ -207,7 +207,8 @@ export function stripCodeContent(body: string): string { let fenceLen = 0 for (let i = 0; i < lines.length; i++) { const line = lines[i] as string - const m = /^[ \t]*(`{3,}|~{3,})(.*)$/.exec(line) + // `.` never matches `\r`, so the `\r?` is what lets a CRLF-saved fence line match. + const m = /^[ \t]*(`{3,}|~{3,})(.*)\r?$/.exec(line) if (m) { const marker = m[1] as string const char = marker[0] as string