mirror of
https://github.com/fancive/obsidian-parallel-reader.git
synced 2026-07-22 17:20:24 +00:00
refactor: extract batch generation helpers
Change-Id: I16ad657b783b65cc5b3e36a6ea163cc19740b651
This commit is contained in:
parent
b199f56a35
commit
2d4bef9539
4 changed files with 120 additions and 29 deletions
40
main.js
40
main.js
File diff suppressed because one or more lines are too long
29
main.ts
29
main.ts
|
|
@ -1,6 +1,14 @@
|
|||
'use strict';
|
||||
import { MarkdownView, Modal, Notice, Plugin, TFile } from 'obsidian';
|
||||
import { findLineForAnchor } from './src/anchor';
|
||||
import {
|
||||
batchProgressVars,
|
||||
createBatchStats,
|
||||
normalizeBatchFolderInput,
|
||||
recordBatchSkip,
|
||||
selectBatchFiles,
|
||||
shouldSkipBatchFile,
|
||||
} from './src/batch';
|
||||
import { serializeCacheFile, shouldConfirmRegenerate, touchCacheEntry } from './src/cache';
|
||||
import { CacheManager } from './src/cache-manager';
|
||||
import { activeIndexAfterCardDelete, removeCardAt, updateCardAt } from './src/cards';
|
||||
|
|
@ -560,27 +568,24 @@ class ParallelReaderPlugin extends Plugin {
|
|||
new FolderPromptModal(plugin.app).open();
|
||||
});
|
||||
if (folderPath === null) return;
|
||||
const allFiles = this.app.vault.getMarkdownFiles().filter((f) => {
|
||||
if (folderPath === '') return !f.path.includes('/');
|
||||
return f.parent?.path === folderPath;
|
||||
});
|
||||
const allFiles = selectBatchFiles(this.app.vault.getMarkdownFiles(), folderPath);
|
||||
if (allFiles.length === 0) {
|
||||
new Notice(this.t('batchNoMarkdown'));
|
||||
return;
|
||||
}
|
||||
let skipped = 0;
|
||||
let stats = createBatchStats(allFiles.length);
|
||||
for (let i = 0; i < allFiles.length; i++) {
|
||||
const file = allFiles[i];
|
||||
new Notice(this.t('batchProgress', { current: i + 1, total: allFiles.length }));
|
||||
new Notice(this.t('batchProgress', batchProgressVars(i, allFiles.length)));
|
||||
const content = await this.app.vault.read(file);
|
||||
const entry = this.cacheManager.get(file.path);
|
||||
if (entry && cacheEntryMatches(entry, content, this.settings)) {
|
||||
skipped++;
|
||||
if (shouldSkipBatchFile(entry, content, this.settings)) {
|
||||
stats = recordBatchSkip(stats);
|
||||
continue;
|
||||
}
|
||||
await this.runForFile(file, false);
|
||||
}
|
||||
new Notice(this.t('batchDone', { total: allFiles.length, skipped }));
|
||||
new Notice(this.t('batchDone', { total: stats.total, skipped: stats.skipped }));
|
||||
}
|
||||
|
||||
resolveCardAnchors(content: string, rawCards: RawCard[]) {
|
||||
|
|
@ -718,6 +723,7 @@ export const __test = {
|
|||
buildGeminiBody,
|
||||
buildOpenAiChatBody,
|
||||
buildOpenAiResponsesBody,
|
||||
batchProgressVars,
|
||||
buildPrompts,
|
||||
cardsToMarkdown,
|
||||
cacheEntryMatches,
|
||||
|
|
@ -728,16 +734,21 @@ export const __test = {
|
|||
extractJson,
|
||||
findLineForAnchor,
|
||||
folderPathsForTarget,
|
||||
createBatchStats,
|
||||
generationFingerprint,
|
||||
getApiBaseUrl,
|
||||
modelForApi,
|
||||
normalizeBatchFolderInput,
|
||||
normalizeCardsPayload,
|
||||
nextCardIndex,
|
||||
pruneCacheEntries,
|
||||
recordBatchSkip,
|
||||
removeCardAt,
|
||||
resolveCliPath,
|
||||
serializeCacheFile,
|
||||
shouldConfirmRegenerate,
|
||||
shouldSkipBatchFile,
|
||||
selectBatchFiles,
|
||||
summarizeViaApi,
|
||||
touchCacheEntry,
|
||||
translate,
|
||||
|
|
|
|||
49
src/batch.ts
Normal file
49
src/batch.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
'use strict';
|
||||
|
||||
import { cacheEntryMatches } from './settings';
|
||||
import type { CacheEntry, PluginSettings } from './types';
|
||||
|
||||
export interface BatchFileLike {
|
||||
path: string;
|
||||
parent?: { path: string } | null;
|
||||
}
|
||||
|
||||
export interface BatchStats {
|
||||
total: number;
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
export function normalizeBatchFolderInput(input: string): string {
|
||||
return (input || '')
|
||||
.trim()
|
||||
.split('/')
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part && part !== '.' && part !== '..')
|
||||
.join('/');
|
||||
}
|
||||
|
||||
export function isFileInBatchFolder(file: BatchFileLike, folderPath: string): boolean {
|
||||
if (!folderPath) return !file.path.includes('/');
|
||||
return file.parent?.path === folderPath;
|
||||
}
|
||||
|
||||
export function selectBatchFiles<T extends BatchFileLike>(files: T[], folderPath: string): T[] {
|
||||
const normalized = normalizeBatchFolderInput(folderPath);
|
||||
return files.filter((file) => isFileInBatchFolder(file, normalized));
|
||||
}
|
||||
|
||||
export function batchProgressVars(index: number, total: number): { current: number; total: number } {
|
||||
return { current: index + 1, total };
|
||||
}
|
||||
|
||||
export function createBatchStats(total: number): BatchStats {
|
||||
return { total, skipped: 0 };
|
||||
}
|
||||
|
||||
export function recordBatchSkip(stats: BatchStats): BatchStats {
|
||||
return { ...stats, skipped: stats.skipped + 1 };
|
||||
}
|
||||
|
||||
export function shouldSkipBatchFile(entry: CacheEntry | null, content: string, settings: PluginSettings): boolean {
|
||||
return !!entry && cacheEntryMatches(entry, content, settings);
|
||||
}
|
||||
|
|
@ -178,6 +178,27 @@ assert.deepStrictEqual(t.folderPathsForTarget('../../etc/passwd'), ['etc', 'etc/
|
|||
assert.deepStrictEqual(t.folderPathsForTarget('./a/../b'), ['a', 'a/b'], 'strips . and .. segments');
|
||||
assert.deepStrictEqual(t.folderPathsForTarget('a/../../b'), ['a', 'a/b'], 'strips mid-path ..');
|
||||
|
||||
// ── batch.ts ──
|
||||
|
||||
assert.strictEqual(t.normalizeBatchFolderInput('/Reading//Articles/'), 'Reading/Articles', 'normalizes folder input');
|
||||
assert.strictEqual(t.normalizeBatchFolderInput('../Inbox/./Daily'), 'Inbox/Daily', 'strips unsafe folder segments');
|
||||
const batchFiles = [
|
||||
{ path: 'root.md', parent: null },
|
||||
{ path: 'Reading/note.md', parent: { path: 'Reading' } },
|
||||
{ path: 'Reading/Deep/nested.md', parent: { path: 'Reading/Deep' } },
|
||||
];
|
||||
assert.deepStrictEqual(t.selectBatchFiles(batchFiles, '').map((file) => file.path), ['root.md'], 'root folder only');
|
||||
assert.deepStrictEqual(
|
||||
t.selectBatchFiles(batchFiles, '/Reading/').map((file) => file.path),
|
||||
['Reading/note.md'],
|
||||
'selected folder is non-recursive',
|
||||
);
|
||||
assert.deepStrictEqual(t.batchProgressVars(1, 3), { current: 2, total: 3 }, 'progress vars are one-based');
|
||||
const batchStats = t.createBatchStats(3);
|
||||
const skippedStats = t.recordBatchSkip(batchStats);
|
||||
assert.deepStrictEqual(batchStats, { total: 3, skipped: 0 }, 'batch stats are immutable');
|
||||
assert.deepStrictEqual(skippedStats, { total: 3, skipped: 1 }, 'batch stats accumulate');
|
||||
|
||||
// ── i18n.ts ──
|
||||
|
||||
assert.strictEqual(t.translate({ uiLanguage: 'zh' }, 'appTitle'), '对照阅读笔记', 'zh translation');
|
||||
|
|
@ -266,6 +287,16 @@ assert.strictEqual(
|
|||
true, 'matching entry returns true'
|
||||
);
|
||||
assert.strictEqual(t.cacheEntryMatches(null, 'test', baseSettings), false, 'null entry returns false');
|
||||
assert.strictEqual(
|
||||
t.shouldSkipBatchFile({
|
||||
schemaVersion: t.CACHE_SCHEMA_VERSION,
|
||||
contentHash: hash,
|
||||
settingsHash: t.generationFingerprint(baseSettings),
|
||||
}, 'test', baseSettings),
|
||||
true,
|
||||
'fresh cache entry is skipped during batch',
|
||||
);
|
||||
assert.strictEqual(t.shouldSkipBatchFile(null, 'test', baseSettings), false, 'missing cache entry is not skipped');
|
||||
|
||||
// pruneCacheEntries
|
||||
const cache = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue