mirror of
https://github.com/TriliumNext/Trilium.git
synced 2026-10-11 22:49:56 +00:00
fix(edit-docs): write exported files with LF line endings
The exported trees carry the line endings of whatever the note content happened to hold, so a Windows run of edit-docs wrote CRLF or mixed endings into docs/, the help HTML and the demo. Git normalizes the difference away on staging, which leaves a working tree full of files that read as changed until they are added. Route every entry extractZip writes through normalizeLineEndings, which collapses CRLF pairs and leaves a lone CR alone, the same rule Git applies. Binary entries are returned untouched and detected the way Git detects them: a NUL byte within the first 8000 bytes. The demo/** rules in .gitattributes match only a root-level demo/, never apps/edit-docs/demo/, so that tree had no eol attribute at all and is now covered by the export instead. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
87fae79c54
commit
3b099726d1
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { rewriteHelpLinks } from "./utils.js";
|
||||
import { normalizeLineEndings, rewriteHelpLinks } from "./utils.js";
|
||||
|
||||
// utils.ts bootstraps Electron at module load (registers the `trilium-app://` scheme and
|
||||
// pulls in the desktop window service, whose resource_dir lookup calls process.exit in a
|
||||
// non-Electron test runner). Stub those side-effectful imports so the pure helpers below
|
||||
// (rewriteHelpLinks) can be imported in isolation.
|
||||
// (rewriteHelpLinks, normalizeLineEndings) can be imported in isolation.
|
||||
vi.mock("electron", () => ({ default: { app: {}, protocol: { registerSchemesAsPrivileged: () => {} } } }));
|
||||
vi.mock("@triliumnext/desktop/src/protocol.js", () => ({ registerTriliumAppScheme: () => {}, setupTriliumAppProtocol: () => {} }));
|
||||
vi.mock("@triliumnext/desktop/src/services/window.js", () => ({ default: {}, setupWindowing: () => {} }));
|
||||
@@ -66,3 +66,36 @@ describe("rewriteHelpLinks", () => {
|
||||
expect(rewriteHelpLinks(input)).toBe(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeLineEndings", () => {
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
it("rewrites CRLF to LF and keeps a lone CR, LF and trailing CR", () => {
|
||||
const input = encoder.encode("# Title\r\n\r\nA line.\nAnother\rline.\r");
|
||||
expect(decoder.decode(normalizeLineEndings(input))).toBe("# Title\n\nA line.\nAnother\rline.\r");
|
||||
});
|
||||
|
||||
it("keeps multi-byte characters intact", () => {
|
||||
const input = encoder.encode("Ștergere\r\nnotițe 😀\r\n");
|
||||
expect(decoder.decode(normalizeLineEndings(input))).toBe("Ștergere\nnotițe 😀\n");
|
||||
});
|
||||
|
||||
it("returns the same content when there is no CRLF to rewrite", () => {
|
||||
const input = encoder.encode("Already normalized.\n");
|
||||
expect(normalizeLineEndings(input)).toBe(input);
|
||||
});
|
||||
|
||||
it("leaves binary content untouched", () => {
|
||||
// A PNG header, whose byte 4 is the NUL that marks the content as binary, followed by
|
||||
// pixel data that happens to contain a CRLF pair.
|
||||
const input = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x0d, 0x0a, 0xff]);
|
||||
expect(normalizeLineEndings(input)).toBe(input);
|
||||
});
|
||||
|
||||
it("normalizes content whose NUL sits past the 8000-byte detection window", () => {
|
||||
const prefix = "a".repeat(8000);
|
||||
const input = encoder.encode(`${prefix}\r\n\0`);
|
||||
expect(decoder.decode(normalizeLineEndings(input))).toBe(`${prefix}\n\0`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,10 +220,52 @@ export async function extractZip(zipFilePath: string, outputPath: string, ignore
|
||||
const fileContent = await readContent();
|
||||
|
||||
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
||||
await fs.writeFile(destPath, fileContent);
|
||||
await fs.writeFile(destPath, normalizeLineEndings(fileContent));
|
||||
}
|
||||
});
|
||||
promise.resolve();
|
||||
}, 1000);
|
||||
await promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites CRLF line endings to LF, so that `extractZip` writes the same bytes on Windows as it
|
||||
* does elsewhere. The exported trees (`docs/`, the help HTML, the demo) are stored with LF, and
|
||||
* Git only hides the difference once a file is staged.
|
||||
*
|
||||
* Binary entries such as images keep their bytes. They are detected the way Git detects them: a
|
||||
* NUL byte within the first {@link BINARY_DETECTION_LENGTH} bytes marks the content as binary.
|
||||
*/
|
||||
export function normalizeLineEndings(content: Uint8Array): Uint8Array {
|
||||
if (isBinary(content)) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const normalized = new Uint8Array(content.length);
|
||||
let length = 0;
|
||||
for (const [index, byte] of content.entries()) {
|
||||
if (byte === CARRIAGE_RETURN && content[index + 1] === LINE_FEED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized[length++] = byte;
|
||||
}
|
||||
|
||||
return length === content.length ? content : normalized.subarray(0, length);
|
||||
}
|
||||
|
||||
const NUL = 0x00;
|
||||
const LINE_FEED = 0x0a;
|
||||
const CARRIAGE_RETURN = 0x0d;
|
||||
const BINARY_DETECTION_LENGTH = 8000;
|
||||
|
||||
function isBinary(content: Uint8Array) {
|
||||
const end = Math.min(content.length, BINARY_DETECTION_LENGTH);
|
||||
for (let index = 0; index < end; index++) {
|
||||
if (content[index] === NUL) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user