Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Line endings.
#
# Without this file, a Windows clone with the default `core.autocrlf=true`
# checks the whole tree out CRLF. Every test in `scripts/` that reads `src/` as
# text then matches patterns against bytes the repository does not contain —
# fifteen files had to be hand-patched that way while cutting v2.7.0 (#452).
#
# This is the belt. `scripts/sourceTree.ts`'s `readSource` is the braces, and it
# is the half that actually holds: `.gitattributes` does not touch a tree that is
# already checked out (that needs `git add --renormalize .`), and it cannot stop
# an editor from writing CRLF into a file it saves.
#
# Every tracked file in this repo is LF today, so this changes no stored bytes
# and no working tree on Linux or macOS. What it changes is the *next* Windows
# clone.
* text=auto eol=lf

# Consumed by Windows-only toolchains, which is where a lone LF is a real risk
# rather than a theoretical one. These are pinned to CRLF so the bytes NSIS and
# Chocolatey see are the bytes they see today on a `core.autocrlf=true` runner —
# this file is not the place to also change the installer build.
*.nsi text eol=crlf
*.nsh text eol=crlf
*.ps1 text eol=crlf
*.bat text eol=crlf
*.cmd text eol=crlf

# Never touched, never diffed as text.
*.png binary
*.gif binary
*.svg text eol=lf
*.ico binary
*.icns binary
11 changes: 5 additions & 6 deletions scripts/checkedReadMigration.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { offsetOf, readSourceFiles, sliceBetween } from './sourceTree.js';
import { offsetOf, readSource, readSourceFiles, sliceBetween } from './sourceTree.js';

// Runes and the Tauri bridge, shimmed the way truncatedBufferGuard.test.ts
// shims them: the stores are runes modules, and Node's test runner gives every
Expand Down Expand Up @@ -152,7 +151,7 @@ test('the completed buffer is refused or accepted according to that verdict', as

// --- the two component call sites -------------------------------------------

const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8');
const viewer = readSource('src/lib/MarkdownViewer.svelte');

test('the unchecked read command is gone, and nothing calls it', () => {
// This was a three-file allowlist — the files #379 migrated. That is the
Expand All @@ -168,7 +167,7 @@ test('the unchecked read command is gone, and nothing calls it', () => {
.map(({ path }) => path);
assert.deepEqual(offenders, [], 'read_file_content no longer exists; read_file_content_checked is the read');

const rust = readFileSync('src-tauri/src/lib.rs', 'utf8');
const rust = readSource('src-tauri/src/lib.rs');
assert.doesNotMatch(rust, /\basync fn read_file_content\(/, 'the unchecked command must stay deleted');
assert.doesNotMatch(rust, /^\s*read_file_content,\s*$/m, 'and must not be registered again');
});
Expand Down Expand Up @@ -200,7 +199,7 @@ test('the session can tell a refusal from a failure', () => {
// both halves for real; this only pins that the tab is consulted at all,
// since a predicate that answers from memory alone is the defect.
const body = sliceBetween(
readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8'),
readSource('src/lib/sessions/documentSession.svelte.ts'),
'function isLossySaveRefused(',
'function updateLoading',
);
Expand Down Expand Up @@ -228,7 +227,7 @@ test('a tab that can only be refused stops re-arming the timer', () => {
// only drops afterwards — and "Save As" to a new file clears
// `hasReplacementChars`, which restores it.
assert.match(
readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8'),
readSource('src/lib/sessions/documentSession.svelte.ts'),
/lossySaveWarnedTabs\.delete\(tab\.id\)/,
);
});
5 changes: 3 additions & 2 deletions scripts/documentLoadFailure.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

const session = readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8');
import { readSource } from './sourceTree.js';

const session = readSource('src/lib/sessions/documentSession.svelte.ts');

test('a transient missing-file read error preserves the open tab', () => {
assert.doesNotMatch(session, /tabManager\.closeTab/);
Expand Down
5 changes: 2 additions & 3 deletions scripts/documentWatcherSession.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { sliceFrom } from './sourceTree.js';
import { readSource, sliceFrom } from './sourceTree.js';

const session = readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8');
const session = readSource('src/lib/sessions/documentSession.svelte.ts');

test('self writes suppress watcher reloads only during their grace period', () => {
const handler = sliceFrom(session, 'function shouldReloadExternalChange');
Expand Down
4 changes: 2 additions & 2 deletions scripts/editorContextMenuI18n.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { getSupportedLanguages, t, translations, type LanguageCode, type Translation } from '../src/lib/utils/i18n.js';
import { readSource } from './sourceTree.js';

// WHAT THIS FILE COVERS, AND WHAT IT DOES NOT
//
Expand All @@ -16,7 +16,7 @@ import { getSupportedLanguages, t, translations, type LanguageCode, type Transla
// Monaco behaves that way when the effect re-runs — verifying that needs a
// running editor.

const editor = readFileSync('src/lib/components/Editor.svelte', 'utf8');
const editor = readSource('src/lib/components/Editor.svelte');

const supported = getSupportedLanguages().map((l) => l.code);

Expand Down
7 changes: 3 additions & 4 deletions scripts/editorOptionWiring.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { sliceBetween } from './sourceTree.js';
import { readSource, sliceBetween } from './sourceTree.js';

// Editor.svelte translates the settings store into Monaco options and
// keybindings. Every regression locked here came from that translation layer
// being wired to the wrong shape: a string option read as a boolean, a
// modifier that macOS never delivers, one key bound twice, and a native
// behaviour dropped by the action that replaced it.

const editor = readFileSync('src/lib/components/Editor.svelte', 'utf8');
const settingsStore = readFileSync('src/lib/stores/settings.svelte.ts', 'utf8');
const editor = readSource('src/lib/components/Editor.svelte');
const settingsStore = readSource('src/lib/stores/settings.svelte.ts');

function count(source: string, pattern: RegExp): number {
return source.match(pattern)?.length ?? 0;
Expand Down
7 changes: 3 additions & 4 deletions scripts/editorPdfExport.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { compile } from 'svelte/compiler';

import { offsetOf, sliceBetween, sliceFrom } from './sourceTree.js';
import { offsetOf, readSource, sliceBetween, sliceFrom } from './sourceTree.js';

/*
* Export PDF from plain edit mode produced a blank page, for two independent
Expand Down Expand Up @@ -33,8 +32,8 @@ import { offsetOf, sliceBetween, sliceFrom } from './sourceTree.js';
* the note on those tests for what that does and does not establish.
*/

const styles = readFileSync('src/styles.css', 'utf8');
const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8');
const styles = readSource('src/styles.css');
const viewer = readSource('src/lib/MarkdownViewer.svelte');

const componentCss = (() => {
const compiled = compile(viewer, { filename: 'MarkdownViewer.svelte', css: 'external' });
Expand Down
7 changes: 4 additions & 3 deletions scripts/explicitSaveCancelsAutoSave.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFileSync } from 'node:fs';

import { readSource } from './sourceTree.js';

// An explicit save and the 1.5s auto-save debounce can both be aimed at one
// tab. The debounce is armed on the last keystroke and disarmed by the
Expand Down Expand Up @@ -166,7 +167,7 @@ test('no call site takes the cancel duty back from saveContent', async () => {
// forget it again. A cancel immediately before a `saveContent` is the
// shape that says someone stopped trusting the function to do it.
for (const path of ['src/lib/MarkdownViewer.svelte', 'src/lib/sessions/documentSession.svelte.ts']) {
const body = readFileSync(path, 'utf8');
const body = readSource(path);
assert.doesNotMatch(
body,
/cancelPendingAutoSave\([^)]*\);\s*(?:\/\/[^\n]*\n\s*)*(?:const \w+ = )?(?:await |return )*saveContent\(/,
Expand All @@ -177,7 +178,7 @@ test('no call site takes the cancel duty back from saveContent', async () => {
// And the duty is discharged before the write, not after — the source-level
// mirror of the ordering test above, so a refactor that reorders the two
// inside `saveContent` is caught even if the stub harness stops seeing it.
const session = readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8');
const session = readSource('src/lib/sessions/documentSession.svelte.ts');
const saveContentBody = session.slice(session.indexOf('async function saveContent('));
const cancel = saveContentBody.indexOf('options.cancelPendingAutoSave(');
const write = saveContentBody.indexOf("invoke('save_file_content'");
Expand Down
6 changes: 3 additions & 3 deletions scripts/exportFoldParity.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { buildExportDocument } from '../src/lib/utils/export.js';
import { readSource } from './sourceTree.js';

const styles = readFileSync('src/styles.css', 'utf8');
const exportSource = readFileSync('src/lib/utils/export.ts', 'utf8');
const styles = readSource('src/styles.css');
const exportSource = readSource('src/lib/utils/export.ts');

// Both export routes have to agree on what is in the file. The HTML route
// decides in TypeScript (it renders every fold open); the print/PDF route
Expand Down
6 changes: 3 additions & 3 deletions scripts/exportSanitize.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import {
Expand All @@ -8,9 +7,10 @@ import {
MARKDOWN_SANITIZE_CONFIG,
} from '../src/lib/utils/sanitize.js';
import { hasMarkdownLinkExtension } from '../src/lib/utils/markdownLinks.js';
import { readSource } from './sourceTree.js';

const exportSource = readFileSync('src/lib/utils/export.ts', 'utf8');
const sanitizeSource = readFileSync('src/lib/utils/sanitize.ts', 'utf8');
const exportSource = readSource('src/lib/utils/export.ts');
const sanitizeSource = readSource('src/lib/utils/sanitize.ts');

// The report's proof of concept. A document containing this line renders as
// nothing in the preview (the viewer sanitizes), so the user sees no sign of
Expand Down
6 changes: 3 additions & 3 deletions scripts/exportedThemeFidelity.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { buildExportDocument, exportThemeAttribute } from '../src/lib/utils/export.js';
import { readSource } from './sourceTree.js';

const styles = readFileSync('src/styles.css', 'utf8');
const exportSource = readFileSync('src/lib/utils/export.ts', 'utf8');
const styles = readSource('src/styles.css');
const exportSource = readSource('src/lib/utils/export.ts');

/** The opening `<html …>` tag of a built export. */
function htmlTag(document: string): string {
Expand Down
5 changes: 2 additions & 3 deletions scripts/externalChangeReload.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { sliceBetween } from './sourceTree.js';
import { readSource, sliceBetween } from './sourceTree.js';

// Live Mode watches the open file and reloads it when something else writes
// it — git checkout, a cloud sync, a second Markpad window. The reload path
Expand Down Expand Up @@ -33,7 +32,7 @@ g.window.__TAURI_INTERNALS__ = {
const { tabManager } = await import('../src/lib/stores/tabs.svelte.js');
const { createDocumentSession } = await import('../src/lib/sessions/documentSession.svelte.js');

const viewer = readFileSync(new URL('../src/lib/MarkdownViewer.svelte', import.meta.url), 'utf8');
const viewer = readSource(new URL('../src/lib/MarkdownViewer.svelte', import.meta.url));

function makeSession() {
return createDocumentSession({
Expand Down
5 changes: 3 additions & 2 deletions scripts/fileAssociations.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

const config = JSON.parse(readFileSync('src-tauri/tauri.conf.json', 'utf8')) as {
import { readSource } from './sourceTree.js';

const config = JSON.parse(readSource('src-tauri/tauri.conf.json')) as {
bundle: { fileAssociations: Array<{ ext: string[] }> };
};

Expand Down
9 changes: 4 additions & 5 deletions scripts/foldKeys.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { sliceFrom } from './sourceTree.js';
import { readSource, sliceFrom } from './sourceTree.js';

// Fold state is keyed by `h.id || textContent`. comrak emits the
// deduplicated heading id on an empty inner <a class="anchor">, not on the
Expand All @@ -11,7 +10,7 @@ import { sliceFrom } from './sourceTree.js';
// id-based fold keys never match the preview's text-based ones.

test('processMarkdownHtml promotes the anchor id onto the heading element', () => {
const source = readFileSync('src/lib/utils/markdown.ts', 'utf8');
const source = readSource('src/lib/utils/markdown.ts');

const headingLoop = sliceFrom(source, 'querySelectorAll("h1, h2, h3, h4, h5, h6")');
assert.match(headingLoop, /querySelector\("a\.anchor"\)/, 'heading loop looks up the comrak anchor');
Expand All @@ -20,12 +19,12 @@ test('processMarkdownHtml promotes the anchor id onto the heading element', () =
});

test('fold restore keys by heading id before falling back to text', () => {
const source = readFileSync('src/lib/utils/markdown.ts', 'utf8');
const source = readSource('src/lib/utils/markdown.ts');
assert.match(source, /const \w+ = \w+\.id \|\| \w+\.textContent/, 'restore key prefers the (now populated) heading id');
});

test('viewer fold handlers key by heading id before falling back to text', () => {
const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8');
const viewer = readSource('src/lib/MarkdownViewer.svelte');
assert.match(viewer, /foldableHeader\.id \|\| foldableHeader\.textContent/, 'preview chevron keys by heading id first');
assert.match(viewer, /\[id="\$\{CSS\.escape\(key\)\}"\]\.foldable-header/, 'toggleFold resolves the heading by id');
});
15 changes: 8 additions & 7 deletions scripts/foldLayout.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';

import { readSource } from './sourceTree.js';

test('fold layout observes rendered content and publishes its measured height', () => {
const source = readFileSync('src/lib/utils/foldLayout.ts', 'utf8');
const source = readSource('src/lib/utils/foldLayout.ts');

assert.match(source, /new ResizeObserver/);
assert.match(source, /--fold-content-height/);
assert.match(source, /requestAnimationFrame/);
});

test('fold wrapper animates an explicit measured height instead of a fractional grid track', () => {
const styles = readFileSync('src/styles.css', 'utf8');
const styles = readSource('src/styles.css');
const expandedRule = styles.match(/\.foldable-content-wrapper\s*\{([^}]*)\}/)?.[1] || '';

assert.match(expandedRule, /height:\s*var\(--fold-content-height/);
Expand All @@ -27,8 +28,8 @@ test('fold wrapper animates an explicit measured height instead of a fractional
// lands on a target that is still moving — a defect that only shows up as "find
// sometimes scrolls to the wrong place".
test('the find-bar fold re-aim delay outlasts the CSS fold transition', () => {
const findBar = readFileSync('src/lib/components/FindBar.svelte', 'utf8');
const styles = readFileSync('src/styles.css', 'utf8');
const findBar = readSource('src/lib/components/FindBar.svelte');
const styles = readSource('src/styles.css');

const declared = findBar.match(/const FOLD_TRANSITION_MS = (\d+);/);
assert.ok(declared, 'FindBar.svelte must declare the delay it waits for the fold to settle');
Expand All @@ -44,14 +45,14 @@ test('the find-bar fold re-aim delay outlasts the CSS fold transition', () => {
});

test('preview lifecycle starts and cleans up fold observation', () => {
const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8');
const viewer = readSource('src/lib/MarkdownViewer.svelte');

assert.match(viewer, /observeFoldLayout\((?:markdownBody|body)\)/);
assert.match(viewer, /stopObservingFoldLayout\?\.\(\)/);
});

test('fold measurement pauses while the preview pane is hidden by edit mode', () => {
const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8');
const viewer = readSource('src/lib/MarkdownViewer.svelte');

assert.match(viewer, /if \(!html \|\| !body \|\| \(isEditing && !isSplit\)\) return;/);
});
11 changes: 5 additions & 6 deletions scripts/foldStatePerDocument.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@
*/

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import ts from 'typescript';

import { installShimDom, parseHtml, type ShimElement } from './renderProtocolDom.ts';
import { offsetOf } from './sourceTree.js';
import { offsetOf, readSource } from './sourceTree.js';

// ---------------------------------------------------------------- environment

Expand Down Expand Up @@ -67,9 +66,9 @@ installShimDom();
const { tabManager } = await import('../src/lib/stores/tabs.svelte.js');
const { processMarkdownHtml } = await import('../src/lib/utils/markdown.ts');

const viewer = readFileSync(new URL('../src/lib/MarkdownViewer.svelte', import.meta.url), 'utf8');
const toc = readFileSync(new URL('../src/lib/components/Toc.svelte', import.meta.url), 'utf8');
const session = readFileSync(new URL('../src/lib/sessions/documentSession.svelte.ts', import.meta.url), 'utf8');
const viewer = readSource(new URL('../src/lib/MarkdownViewer.svelte', import.meta.url));
const toc = readSource(new URL('../src/lib/components/Toc.svelte', import.meta.url));
const session = readSource(new URL('../src/lib/sessions/documentSession.svelte.ts', import.meta.url));

// ------------------------------------------------------------ source plucking

Expand Down Expand Up @@ -124,7 +123,7 @@ function pluckFunction(source: string, name: string, required = true): string {
type Declaration = { declare: string; refresh: string };

function pluckDeclaration(source: string, name: string, required = true): Declaration | null {
const match = new RegExp(`\\r?\\n\\t(let|const) ${name} = ([^\\r\\n]*);\\r?\\n`).exec(source);
const match = new RegExp(`\\n\\t(let|const) ${name} = ([^\\n]*);\\n`).exec(source);
if (!match) {
assert.ok(!required, `expected the component to declare ${name} on one line`);
return null;
Expand Down
Loading
Loading