一些检查失败了
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
112 行
4.2 KiB
TypeScript
112 行
4.2 KiB
TypeScript
import InteropService_Importer_Md from './InteropService_Importer_Md';
|
|
import Note from '../../models/Note';
|
|
import Folder from '../../models/Folder';
|
|
import Tag from '../../models/Tag';
|
|
import shim from '../../shim';
|
|
import { parse } from '../../utils/frontMatter';
|
|
import { FolderIcon, FolderIconType } from '../database/types';
|
|
import * as yaml from 'js-yaml';
|
|
import Logger from '@joplin/utils/Logger';
|
|
|
|
|
|
const logger = Logger.create('InteropService_Importer_Md_frontmatter');
|
|
|
|
// Maps the string labels written by the exporter back to FolderIconType enum values.
|
|
const folderIconTypeFromString = (typeStr: string): FolderIconType | null => {
|
|
switch (typeStr) {
|
|
case 'emoji': return FolderIconType.Emoji;
|
|
case 'fontawesome': return FolderIconType.FontAwesome;
|
|
case 'dataurl': return FolderIconType.DataUrl;
|
|
default: return null;
|
|
}
|
|
};
|
|
|
|
// Parses the raw YAML-parsed value into a FolderIcon. The YAML FAILSAFE_SCHEMA
|
|
// returns all values as strings and uses lowercase keys (e.g. "dataurl" instead
|
|
// of "dataUrl"), so we normalise immediately after parsing to avoid carrying two
|
|
// separate interfaces around.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- The raw YAML output is untyped
|
|
const parseRawYamlToFolderIcon = (raw: any): FolderIcon | null => {
|
|
if (!raw || typeof raw !== 'object') return null;
|
|
if (typeof raw.type !== 'string') return null;
|
|
|
|
const iconType = folderIconTypeFromString(raw.type);
|
|
if (iconType === null) return null;
|
|
|
|
switch (iconType) {
|
|
case FolderIconType.Emoji:
|
|
if (!raw.emoji) return null;
|
|
return { type: FolderIconType.Emoji, emoji: raw.emoji, name: '', dataUrl: '' };
|
|
case FolderIconType.FontAwesome:
|
|
if (!raw.name) return null;
|
|
return { type: FolderIconType.FontAwesome, emoji: '', name: raw.name, dataUrl: '' };
|
|
case FolderIconType.DataUrl:
|
|
if (!raw.dataurl) return null;
|
|
return { type: FolderIconType.DataUrl, emoji: '', name: '', dataUrl: raw.dataurl };
|
|
default: {
|
|
const exhaustivenessCheck: never = iconType;
|
|
throw new Error(`Unknown folder icon type: ${exhaustivenessCheck}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
export default class InteropService_Importer_Md_frontmatter extends InteropService_Importer_Md {
|
|
|
|
public async importDirectory(dirPath: string, parentFolderId: string) {
|
|
// super.importDirectory() imports files and recursively processes
|
|
// subdirectories. It must run first so that all child folders exist
|
|
// before we try to apply metadata to them.
|
|
await super.importDirectory(dirPath, parentFolderId);
|
|
|
|
// The _folder.yml file inside `dirPath` describes the folder that
|
|
// corresponds to `dirPath` itself. In the import flow,
|
|
// `parentFolderId` is the DB ID of that folder (it was created by the
|
|
// caller — either `exec()` for the root, or the parent
|
|
// `importDirectory` for subdirectories).
|
|
await this.applyFolderMetadata(dirPath, parentFolderId);
|
|
}
|
|
|
|
private async applyFolderMetadata(dirPath: string, folderId: string) {
|
|
const metadataPath = `${dirPath}/_folder.yml`;
|
|
if (!(await shim.fsDriver().exists(metadataPath))) return;
|
|
|
|
try {
|
|
const content = await shim.fsDriver().readFile(metadataPath, 'utf-8');
|
|
const metadata = yaml.load(content, { schema: yaml.FAILSAFE_SCHEMA }) as Record<string, unknown>;
|
|
if (!metadata || !metadata.icon) return;
|
|
|
|
const folderIcon = parseRawYamlToFolderIcon(metadata.icon);
|
|
if (folderIcon) {
|
|
await Folder.save({ id: folderId, icon: JSON.stringify(folderIcon) }, { isNew: false });
|
|
}
|
|
} catch (e) {
|
|
logger.warn(`Failed to import folder metadata from ${metadataPath}:`, e);
|
|
}
|
|
}
|
|
|
|
public async importFile(filePath: string, parentFolderId: string) {
|
|
try {
|
|
const note = await super.importFile(filePath, parentFolderId);
|
|
const { metadata, tags } = parse(note.body);
|
|
|
|
const updatedNote = {
|
|
...note,
|
|
...metadata,
|
|
title: metadata.title ? metadata.title : note.title,
|
|
};
|
|
|
|
const noteItem = await Note.save(updatedNote, { isNew: false, autoTimestamp: false });
|
|
|
|
const resolvedPath = shim.fsDriver().resolve(filePath);
|
|
this.importedNotes[resolvedPath] = noteItem;
|
|
|
|
for (const tag of tags) { await Tag.addNoteTagByTitle(noteItem.id, tag); }
|
|
|
|
return noteItem;
|
|
} catch (error) {
|
|
error.message = `On ${filePath}: ${error.message}`;
|
|
throw error;
|
|
}
|
|
}
|
|
}
|