文件
mynode/packages/lib/commands/convertNoteToMarkdown.ts
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

94 行
3.9 KiB
TypeScript

import { _, _n } from '../locale';
import Note from '../models/Note';
import { CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService';
import { MarkupLanguage } from '@joplin/renderer';
import { runtime as convertHtmlToMarkdown } from './convertHtmlToMarkdown';
import shim, { ToastType } from '../shim';
import { NoteEntity } from '../services/database/types';
import { itemIsReadOnly } from '../models/utils/readOnly';
import { ModelType } from '../BaseModel';
import ItemChange from '../models/ItemChange';
import Setting from '../models/Setting';
import isNoteLockEnabled from '../services/noteLock/isNoteLockEnabled';
import NoteLockNote, { NoteLockNoteEntity } from '../services/noteLock/NoteLockNote';
import NoteLockSession from '../services/noteLock/NoteLockSession';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('convertNoteToMarkdown');
export const declaration: CommandDeclaration = {
name: 'convertNoteToMarkdown',
label: () => _('Convert to Markdown'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext, noteIds: string|string[] = []) => {
if (typeof noteIds === 'string') {
noteIds = [noteIds];
}
if (noteIds.length === 0) {
noteIds = context.state.selectedNoteIds;
}
const notes: NoteEntity[] = await Note.loadItemsByIdsOrFail(noteIds);
try {
const firstLockedNote = isNoteLockEnabled() ? notes.find(note => note.markup_language !== MarkupLanguage.Markdown && NoteLockNote.isLocked(note)) : null;
if (firstLockedNote && !NoteLockSession.instance().isUnlocked()) {
throw new Error(_('Cannot convert locked note: "%s"', firstLockedNote.title));
}
// Captured once so a session lock mid-run cannot fail the remaining conversions.
const noteLockKey = firstLockedNote ? NoteLockSession.instance().decryptedKey() : null;
let isFirst = true;
let processedCount = 0;
for (const note of notes) {
if (note.markup_language === MarkupLanguage.Markdown) {
logger.warn('Skipping item: Already Markdown.');
continue;
}
if (await itemIsReadOnly(Note, ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note.id, Setting.value('sync.userId'), context.state.shareService)) {
throw new Error(_('Cannot convert read-only item: "%s"', note.title));
}
// Every note converts through a full gated load and save, so a locked body is
// decrypted for the conversion and the converted note is encrypted again.
const sourceNote: NoteLockNoteEntity = await Note.load(note.id, { useNoteLock: true, noteLockKey });
const markdownBody = await convertHtmlToMarkdown().execute(context, sourceNote.body);
const backupNote = await Note.duplicate(note.id, {
changes: {
user_created_time: note.user_created_time,
user_updated_time: note.user_updated_time,
},
});
note.body = markdownBody;
note.markup_language = MarkupLanguage.Markdown;
note.updated_time = Date.now();
const toSave = { ...note, isDecrypted: sourceNote.isDecrypted };
await Note.save(toSave, { autoTimestamp: false, useNoteLock: true, noteLockKey });
await Note.delete(backupNote.id, { toTrash: true });
processedCount ++;
if (isFirst) {
context.dispatch({
type: 'NOTE_SELECT',
id: note.id,
});
isFirst = false;
}
}
void shim.showToast(_n(
'The note has been converted to Markdown and an HTML copy has been moved to the trash',
'The notes have been converted to Markdown and HTML copies have been moved to the trash',
processedCount,
), { type: ToastType.Success });
} catch (error) {
await shim.showErrorDialog(_('Could not convert notes to Markdown: %s', error.message));
}
},
enabledCondition: 'selectionIncludesHtmlNotes && (multipleNotesSelected || !noteIsReadOnly) && !noteLockContentUnavailable',
};
};