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', }; };