Merge pull request 'fix: CRLF notes and configurable test vault path (#3)' (#8) from feat/3-crlf-vault-path into main
This commit is contained in:
commit
4aad7e3255
@ -16,7 +16,7 @@
|
||||
- Ключи API — в настройках плагина (`data.json`), в git не попадают.
|
||||
|
||||
## Команды
|
||||
- `npm run dev` — сборка в watch-режиме в тестовый vault.
|
||||
- `npm run dev` — сборка в watch-режиме в тестовый vault. Другой vault — через `VOICE_NOTE_VAULT=<путь> npm run dev`; если vault не найден, копирование пропускается с предупреждением.
|
||||
- `npm test` — vitest, вся папка `tests/`. Зелёным считается весь набор.
|
||||
- `npm run build` — продакшн-сборка `main.js`.
|
||||
- `npm run typecheck` — два прохода: весь проект с Node-типами и `src/core` через `tsconfig.core.json` без Node-типов (`process`, `Buffer` в core — ошибка компиляции).
|
||||
|
||||
@ -4,13 +4,19 @@ import { builtinModules } from 'node:module';
|
||||
import process from 'node:process';
|
||||
|
||||
const prod = process.argv[2] === 'production';
|
||||
const VAULT = '/Users/andrey.epifancev/Library/Mobile Documents/iCloud~md~obsidian/Documents/voice-note-test';
|
||||
// Путь к тестовому vault: переменная окружения VOICE_NOTE_VAULT, иначе vault автора.
|
||||
const VAULT =
|
||||
process.env.VOICE_NOTE_VAULT ||
|
||||
'/Users/andrey.epifancev/Library/Mobile Documents/iCloud~md~obsidian/Documents/voice-note-test';
|
||||
const PLUGIN_DIR = `${VAULT}/.obsidian/plugins/voice-note`;
|
||||
|
||||
// В dev-режиме после каждой сборки копируем плагин в тестовый vault, если он есть на этой машине.
|
||||
const copyToVault = {
|
||||
name: 'copy-to-vault',
|
||||
setup(build) {
|
||||
build.onStart(() => {
|
||||
if (!existsSync(VAULT)) console.warn(`[copy-to-vault] vault не найден, копирование пропущено: ${VAULT}. Задайте VOICE_NOTE_VAULT.`);
|
||||
});
|
||||
build.onEnd((result) => {
|
||||
if (result.errors.length > 0 || !existsSync(VAULT)) return;
|
||||
mkdirSync(PLUGIN_DIR, { recursive: true });
|
||||
|
||||
@ -16,6 +16,18 @@ export const VOICE_KEYS = [
|
||||
] as const;
|
||||
export type VoiceKey = (typeof VOICE_KEYS)[number];
|
||||
|
||||
// Заметка с CRLF (правка в другом редакторе, Windows) читается и пишется в своём стиле:
|
||||
// иначе '---\r' не опознаётся как граница frontmatter и патч дописывает второй блок.
|
||||
const LINE_BREAK = /\r?\n/;
|
||||
|
||||
function eolOf(content: string): string {
|
||||
return content.includes('\r\n') ? '\r\n' : '\n';
|
||||
}
|
||||
|
||||
function withEol(text: string, eol: string): string {
|
||||
return eol === '\n' ? text : text.replace(/\r?\n/g, eol);
|
||||
}
|
||||
|
||||
// Строки — всегда в двойных кавычках: JSON-строка является валидной YAML-строкой
|
||||
// и экранирует переводы строк, кавычки и двоеточия в одну строку.
|
||||
function yaml(value: string | number): string {
|
||||
@ -62,7 +74,7 @@ function unquote(raw: string): string {
|
||||
const VOICE_LINE = /^(voice-[a-z-]+):[ \t]*(.*)$/;
|
||||
|
||||
export function readVoiceFields(content: string): Record<string, string> {
|
||||
const lines = content.split('\n');
|
||||
const lines = content.split(LINE_BREAK);
|
||||
const range = frontmatterRange(lines);
|
||||
const fields: Record<string, string> = {};
|
||||
if (!range) return fields;
|
||||
@ -111,7 +123,7 @@ export function parseRecord(fields: Record<string, unknown>): VoiceRecord | null
|
||||
// Меняет только перечисленные ключи; undefined — удалить строку.
|
||||
// Остальные строки frontmatter и их порядок не трогаются.
|
||||
export function patchFrontmatter(content: string, fields: Record<string, string | undefined>): string {
|
||||
const lines = content.split('\n');
|
||||
const lines = content.split(LINE_BREAK);
|
||||
let range = frontmatterRange(lines);
|
||||
if (!range) {
|
||||
lines.unshift('---', '---');
|
||||
@ -140,7 +152,7 @@ export function patchFrontmatter(content: string, fields: Record<string, string
|
||||
end++;
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
return lines.join(eolOf(content));
|
||||
}
|
||||
|
||||
export const MARKER_END = '%%/voice-note%%';
|
||||
@ -182,15 +194,16 @@ export function renderNote(content: string, record: VoiceRecord, text?: string):
|
||||
if (record.status === 'inserted' && readVoiceFields(content)['voice-status'] === 'inserted') {
|
||||
return content;
|
||||
}
|
||||
const eol = eolOf(content);
|
||||
const patched = patchFrontmatter(content, recordToFields(record));
|
||||
const start = patched.indexOf(markerStart(record.id));
|
||||
const end = start === -1 ? -1 : patched.indexOf(MARKER_END, start);
|
||||
|
||||
if (record.status === 'inserted') {
|
||||
const body = (text ?? '').trim();
|
||||
if (end === -1) return `${patched.replace(/\s+$/, '')}\n\n${body}\n`;
|
||||
const body = withEol((text ?? '').trim(), eol);
|
||||
if (end === -1) return `${patched.replace(/\s+$/, '')}${eol}${eol}${body}${eol}`;
|
||||
return patched.slice(0, start) + body + patched.slice(end + MARKER_END.length);
|
||||
}
|
||||
if (end === -1) return patched;
|
||||
return patched.slice(0, start) + region(record) + patched.slice(end + MARKER_END.length);
|
||||
return patched.slice(0, start) + withEol(region(record), eol) + patched.slice(end + MARKER_END.length);
|
||||
}
|
||||
|
||||
@ -106,3 +106,33 @@ describe('renderNote: transcript', () => {
|
||||
expect(out.startsWith('---\ntags: [idea]\nvoice-id: "20260927-111503-0000"\nvoice-status: "inserted"\n')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderNote: CRLF notes', () => {
|
||||
const crlf = (s: string) => s.replace(/\n/g, '\r\n');
|
||||
|
||||
it('updates the status region in place and keeps CRLF', () => {
|
||||
const failed: VoiceRecord = { ...saved, status: 'failed', error: 'HTTP 401' };
|
||||
const lf = `${initialNote(saved, 0)}после\n`;
|
||||
expect(renderNote(crlf(lf), failed)).toBe(crlf(renderNote(lf, failed)));
|
||||
});
|
||||
|
||||
it('inserts the transcript with CRLF, including multi-line text', () => {
|
||||
const content = crlf(`${initialNote(saved, 0)}купить молоко\n`);
|
||||
expect(renderNote(content, inserted, 'строка 1\nстрока 2')).toBe(
|
||||
crlf(
|
||||
`${FM.replace('"saved"', '"inserted"')}![[voice/source/20260927-111503-0000.m4a]]\n\nстрока 1\nстрока 2\nкупить молоко\n`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('appends at the end with CRLF when markers were removed', () => {
|
||||
expect(renderNote(crlf(`${FM}без маркеров\n`), inserted, 'текст')).toBe(
|
||||
crlf(`${FM.replace('"saved"', '"inserted"')}без маркеров\n\nтекст\n`),
|
||||
);
|
||||
});
|
||||
|
||||
it('is a no-op when already inserted', () => {
|
||||
const once = renderNote(crlf(initialNote(saved, 0)), inserted, 'первый');
|
||||
expect(renderNote(once, inserted, 'второй')).toBe(once);
|
||||
});
|
||||
});
|
||||
|
||||
@ -136,3 +136,35 @@ describe('parseRecord', () => {
|
||||
expect(parseRecord(fields)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CRLF line endings', () => {
|
||||
const crlf = (s: string) => s.replace(/\n/g, '\r\n');
|
||||
|
||||
it('readVoiceFields finds the block and strips \\r from values', () => {
|
||||
expect(readVoiceFields(crlf('---\nvoice-id: "a"\nvoice-attempts: 2\n---\nbody\n'))).toEqual({
|
||||
'voice-id': 'a',
|
||||
'voice-attempts': '2',
|
||||
});
|
||||
});
|
||||
|
||||
it('patchFrontmatter edits the existing block and keeps CRLF', () => {
|
||||
const content = crlf('---\ntags: [idea]\nvoice-status: "saved"\n---\nbody\n');
|
||||
expect(patchFrontmatter(content, { 'voice-status': '"sent"', 'voice-attempts': '1' })).toBe(
|
||||
crlf('---\ntags: [idea]\nvoice-status: "sent"\nvoice-attempts: 1\n---\nbody\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a block with CRLF when the note has none', () => {
|
||||
expect(patchFrontmatter(crlf('body\n'), { 'voice-id': '"a"' })).toBe(crlf('---\nvoice-id: "a"\n---\nbody\n'));
|
||||
});
|
||||
|
||||
it('a mixed file gets one block, written in CRLF', () => {
|
||||
const content = '---\r\nvoice-status: "saved"\n---\r\nbody\n';
|
||||
expect(patchFrontmatter(content, { 'voice-status': '"sent"' })).toBe(crlf('---\nvoice-status: "sent"\n---\nbody\n'));
|
||||
});
|
||||
|
||||
it('round-trips through readVoiceFields + parseRecord', () => {
|
||||
const content = crlf(patchFrontmatter('', recordToFields(rec)));
|
||||
expect(parseRecord(readVoiceFields(content))).toEqual(rec);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user