obsidian#normalizePath TypeScript Examples

The following examples show how to use obsidian#normalizePath. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: utils.ts    From nldates-obsidian with MIT License 7 votes vote down vote up
export function generateMarkdownLink(app: App, subpath: string, alias?: string) {
  const useMarkdownLinks = (app.vault as any).getConfig("useMarkdownLinks");
  const path = normalizePath(subpath);

  if (useMarkdownLinks) {
    if (alias) {
      return `[${alias}](${path.replace(/ /g, "%20")})`;
    } else {
      return `[${subpath}](${path})`;
    }
  } else {
    if (alias) {
      return `[[${path}|${alias}]]`;
    } else {
      return `[[${path}]]`;
    }
  }
}
Example #2
Source File: fileutils.ts    From obsidian-ReadItLater with MIT License 6 votes vote down vote up
export function pathJoin(dir: string, subpath: string): string {
    const result = path.join(dir, subpath);
    // it seems that obsidian do not understand paths with backslashes in Windows, so turn them into forward slashes
    return normalizePath(result.replace(/\\/g, '/'));
}
Example #3
Source File: main.ts    From obsidian-ReadItLater with MIT License 6 votes vote down vote up
async writeFile(fileName: string, content: string): Promise<void> {
        let filePath;
        fileName = normalizeFilename(fileName);
        await checkAndCreateFolder(this.app.vault, this.settings.inboxDir);

        if (this.settings.inboxDir) {
            filePath = normalizePath(`${this.settings.inboxDir}/${fileName}`);
        } else {
            filePath = normalizePath(`/${fileName}`);
        }

        if (await this.app.vault.adapter.exists(filePath)) {
            new Notice(`${fileName} already exists!`);
        } else {
            const newFile = await this.app.vault.create(filePath, content);
            if (this.settings.openNewNote) {
                this.app.workspace.getLeaf(false).openFile(newFile);
            }
            new Notice(`${fileName} created successful`);
        }
    }
Example #4
Source File: main.ts    From obsidian-custom-attachment-location with GNU General Public License v3.0 6 votes vote down vote up
getAttachmentFolderFullPath(mdFolderPath: string, mdFileName: string)
    {
        let attachmentFolder = '';

        if(this.useRelativePath)
            attachmentFolder = Path.join(mdFolderPath, this.getAttachmentFolderPath(mdFileName));
        else
        {
            attachmentFolder = this.getAttachmentFolderPath(mdFileName);
        }
        return normalizePath(attachmentFolder);
    }
Example #5
Source File: offlineDic.ts    From obsidian-dictionary with GNU Affero General Public License v3.0 6 votes vote down vote up
async getOfflineDictionary(): Promise<Record<string, OfflineDic>> {
        const { plugin } = this.manager;
        const { adapter } = plugin.app.vault;
        const path = normalizePath(`${plugin.manifest.dir}/offlineDictionary.json`);
        if (!this.offlineDic) {
            if (!await adapter.exists(path)) {
                const data = await request({ url: `https://github.com/phibr0/obsidian-dictionary/releases/download/${plugin.manifest.version}/dictionary.json` });
                await adapter.write(path, data);
            }
            this.offlineDic = JSON.parse(await adapter.read(path));
        }
        return this.offlineDic;
    }
Example #6
Source File: functions.ts    From obsidian-rss with GNU General Public License v3.0 6 votes vote down vote up
export async function createNewNote(plugin: RssReaderPlugin, item: RssFeedItem): Promise<void> {
    const activeFile = plugin.app.workspace.getActiveFile();
    let dir = plugin.app.fileManager.getNewFileParent(activeFile ? activeFile.path : "").path;

    if (plugin.settings.saveLocation === "custom") {
        dir = plugin.settings.saveLocationFolder;
    }

    let filename = applyTemplate(plugin, item, plugin.settings.defaultFilename);
    //make sure there are no slashes in the title.
    filename = filename.replace(/[\/\\:]/g, ' ');

    if (plugin.settings.askForFilename) {
        const inputPrompt = new TextInputPrompt(plugin.app, t("specify_name"), t("cannot_contain") + " * \" \\ / < > : | ?", filename, filename);
        await inputPrompt
            .openAndGetValue(async (text: TextComponent) => {
                const value = text.getValue();
                if (value.match(FILE_NAME_REGEX)) {
                    inputPrompt.setValidationError(text, t("invalid_filename"));
                    return;
                }
                const filePath = normalizePath([dir, `${value}.md`].join('/'));

                if (isInVault(plugin.app, filePath, '')) {
                    inputPrompt.setValidationError(text, t("note_exists"));
                    return;
                }
                inputPrompt.close();
                await createNewFile(plugin, item, filePath, value);
            });
    } else {
        const replacedTitle = filename.replace(FILE_NAME_REGEX, '');
        const filePath = normalizePath([dir, `${replacedTitle}.md`].join('/'));
        await createNewFile(plugin, item, filePath, item.title);
    }


}
Example #7
Source File: InternalModuleFile.ts    From Templater with GNU Affero General Public License v3.0 6 votes vote down vote up
generate_move(): (path: string) => Promise<string> {
        return async (path: string) => {
            const new_path = normalizePath(
                `${path}.${this.config.target_file.extension}`
            );
            await this.app.fileManager.renameFile(
                this.config.target_file,
                new_path
            );
            return "";
        };
    }
Example #8
Source File: InternalModuleFile.ts    From Templater with GNU Affero General Public License v3.0 6 votes vote down vote up
generate_rename(): (new_title: string) => Promise<string> {
        return async (new_title: string) => {
            if (new_title.match(/[\\/:]+/g)) {
                throw new TemplaterError(
                    "File name cannot contain any of these characters: \\ / :"
                );
            }
            const new_path = normalizePath(
                `${this.config.target_file.parent.path}/${new_title}.${this.config.target_file.extension}`
            );
            await this.app.fileManager.renameFile(
                this.config.target_file,
                new_path
            );
            return "";
        };
    }
Example #9
Source File: Utils.ts    From Templater with GNU Affero General Public License v3.0 6 votes vote down vote up
export function resolve_tfolder(app: App, folder_str: string): TFolder {
    folder_str = normalizePath(folder_str);

    const folder = app.vault.getAbstractFileByPath(folder_str);
    if (!folder) {
        throw new TemplaterError(`Folder "${folder_str}" doesn't exist`);
    }
    if (!(folder instanceof TFolder)) {
        throw new TemplaterError(`${folder_str} is a file, not a folder`);
    }

    return folder;
}
Example #10
Source File: Utils.ts    From Templater with GNU Affero General Public License v3.0 6 votes vote down vote up
export function resolve_tfile(app: App, file_str: string): TFile {
    file_str = normalizePath(file_str);

    const file = app.vault.getAbstractFileByPath(file_str);
    if (!file) {
        throw new TemplaterError(`File "${file_str}" doesn't exist`);
    }
    if (!(file instanceof TFile)) {
        throw new TemplaterError(`${file_str} is a folder, not a file`);
    }

    return file;
}
Example #11
Source File: checkAndCreateFolder.ts    From obsidian-ReadItLater with MIT License 6 votes vote down vote up
/**
 * Open or create a folderpath if it does not exist
 * @param vault
 * @param folderpath
 */
export async function checkAndCreateFolder(vault: Vault, folderpath: string) {
    folderpath = normalizePath(folderpath);
    const folder = vault.getAbstractFileByPath(folderpath);
    if (folder && folder instanceof TFolder) {
        return;
    }
    await vault.createFolder(folderpath);
}
Example #12
Source File: defineGenericAnnotation.tsx    From obsidian-annotator with GNU Affero General Public License v3.0 6 votes vote down vote up
getProxiedUrl = (url: URL | string, props, vault): string => {
    const proxiedUrl = proxy(url, props);

    if (proxiedUrl.protocol == 'vault:') {
        return getVaultPathResourceUrl(normalizePath(proxiedUrl.pathname), vault);
    }
    if (proxiedUrl.protocol == 'zip:') {
        const pathName = normalizePath(proxiedUrl.pathname);
        const res = resourceUrls.get(pathName) || resourceUrls.get(`${pathName}.html`);
        if (res) return res;
        console.error('file not found', { url });
        debugger;
    }
    return proxiedUrl.toString();
}
Example #13
Source File: main.ts    From obsidian-citation-plugin with MIT License 6 votes vote down vote up
/**
   * Run a case-insensitive search for the literature note file corresponding to
   * the given citekey. If no corresponding file is found, create one.
   */
  async getOrCreateLiteratureNoteFile(citekey: string): Promise<TFile> {
    const path = this.getPathForCitekey(citekey);
    const normalizedPath = normalizePath(path);

    let file = this.app.vault.getAbstractFileByPath(normalizedPath);
    if (file == null) {
      // First try a case-insensitive lookup.
      const matches = this.app.vault
        .getMarkdownFiles()
        .filter((f) => f.path.toLowerCase() == normalizedPath.toLowerCase());
      if (matches.length > 0) {
        file = matches[0];
      } else {
        try {
          file = await this.app.vault.create(
            path,
            this.getInitialContentForCitekey(citekey),
          );
        } catch (exc) {
          this.literatureNoteErrorNotifier.show();
          throw exc;
        }
      }
    }

    return file as TFile;
  }
Example #14
Source File: settings.ts    From obsidian-consistent-attachments-and-links with MIT License 5 votes vote down vote up
getNormalizedPath(path: string): string {
        return path.length == 0 ? path : normalizePath(path);
    }
Example #15
Source File: main.ts    From obsidian-dictionary with GNU Affero General Public License v3.0 5 votes vote down vote up
async saveCache(): Promise<void> {
        await this.app.vault.adapter.write(normalizePath(`${this.manifest.dir}/cache.json`), JSON.stringify(this.cache));
    }
Example #16
Source File: main.ts    From obsidian-dictionary with GNU Affero General Public License v3.0 5 votes vote down vote up
async loadCacheFromDisk(): Promise<DictionaryCache> {
        const path = normalizePath(`${this.manifest.dir}/cache.json`);
        if (!(await this.app.vault.adapter.exists(path))) {
            await this.app.vault.adapter.write(path, "{}");
        }
        return JSON.parse(await this.app.vault.adapter.read(path)) as DictionaryCache;
    }
Example #17
Source File: InternalModuleFile.ts    From Templater with GNU Affero General Public License v3.0 5 votes vote down vote up
generate_find_tfile(): (filename: string) => TFile {
        return (filename: string) => {
            const path = normalizePath(filename);
            return this.app.metadataCache.getFirstLinkpathDest(path, "");
        };
    }
Example #18
Source File: Templater.ts    From Templater with GNU Affero General Public License v3.0 5 votes vote down vote up
static async on_file_creation(
        templater: Templater,
        file: TAbstractFile
    ): Promise<void> {
        if (!(file instanceof TFile) || file.extension !== "md") {
            return;
        }

        // Avoids template replacement when syncing template files
        const template_folder = normalizePath(
            templater.plugin.settings.templates_folder
        );
        if (file.path.includes(template_folder) && template_folder !== "/") {
            return;
        }

        // TODO: find a better way to do this
        // Currently, I have to wait for the daily note plugin to add the file content before replacing
        // Not a problem with Calendar however since it creates the file with the existing content
        await delay(300);

        if (
            file.stat.size == 0 &&
            templater.plugin.settings.enable_folder_templates
        ) {
            const folder_template_match =
                templater.get_new_file_template_for_folder(file.parent);
            if (!folder_template_match) {
                return;
            }

            const template_file: TFile = await errorWrapper(
                async (): Promise<TFile> => {
                    return resolve_tfile(templater.app, folder_template_match);
                },
                `Couldn't find template ${folder_template_match}`
            );
            // errorWrapper failed
            if (template_file == null) {
                return;
            }
            await templater.write_template_to_file(template_file, file);
        } else {
            await templater.overwrite_file_commands(file);
        }
    }
Example #19
Source File: main.ts    From obsidian-custom-attachment-location with GNU General Public License v3.0 5 votes vote down vote up
display(): void {
        let {containerEl} = this;

        containerEl.empty();

        containerEl.createEl('h2', {text: 'Custom Attachment Location'});

        let el = new Setting(containerEl)
            .setName('Location for New Attachments')
            .setDesc('Start with "./" to use relative path. Available variables: ${filename}.(NOTE: DO NOT start with "/" or end with "/". )')
            .addText(text => text
                .setPlaceholder('./assets/${filename}')
                .setValue(this.plugin.settings.attachmentFolderPath)
                .onChange(async (value: string) => {
                    console.log('attachmentFolder: ' + value);
                    value = normalizePath(value);
                    console.log('normalized attachmentFolder: ' + value);

                    this.plugin.settings.attachmentFolderPath = value;
                    if(value.startsWith('./'))
                        this.plugin.useRelativePath = true;
                    else
                        this.plugin.useRelativePath = false;
                    await this.plugin.saveSettings();
                }));
        el.controlEl.addEventListener('change',  (()=>{this.display();}));


        new Setting(containerEl)
            .setName('Pasted Image Name')
            .setDesc('Available variables: ${filename}, ${date}.')
            .addText(text => text
                .setPlaceholder('image-${date}')
                .setValue(this.plugin.settings.pastedImageFileName)
                .onChange(async (value: string) => {
                    console.log('pastedImageFileName: ' + value);
                    this.plugin.settings.pastedImageFileName = value;
                    await this.plugin.saveSettings();
                }));

        new Setting(containerEl)
            .setName('Date Format')
            .setDesc('YYYYMMDDHHmmssSSS')
            .addMomentFormat(text => text
                .setDefaultFormat('YYYYMMDDHHmmssSSS')
                .setValue(this.plugin.settings.dateTimeFormat)
                .onChange(async (value: string) => {
                    console.log('dateTimeFormat: ' + value);
                    this.plugin.settings.dateTimeFormat = value || 'YYYYMMDDHHmmssSSS';
                    await this.plugin.saveSettings();
                }));


        new Setting(containerEl)
            .setName('Automatically rename attachment folder')
            .setDesc('When renaming md files, automatically rename attachment folder if folder name contains "${filename}".')
            .addToggle(toggle => toggle
                .setValue(this.plugin.settings.autoRenameFolder)
                .onChange(async (value: boolean) => {
                    this.plugin.settings.autoRenameFolder = value;
                    this.display();
                    await this.plugin.saveSettings();
                }));

        if(this.plugin.settings.autoRenameFolder)
            new Setting(containerEl)
            .setName('Automatically rename attachment files [Experimental]')
            .setDesc('When renaming md files, automatically rename attachment files if file name contains "${filename}".')
            .addToggle(toggle => toggle
                .setValue(this.plugin.settings.autoRenameFiles)
                .onChange(async (value: boolean) => {
                    this.plugin.settings.autoRenameFiles = value;
                    await this.plugin.saveSettings();
                }));
    }
Example #20
Source File: main.ts    From obsidian-custom-attachment-location with GNU General Public License v3.0 5 votes vote down vote up
async handleRename(newFile: TFile, oldFilePath: string){
        console.log('Handle Rename');

        //if autoRename is off or not a markdown file
        if(!this.settings.autoRenameFolder || newFile.extension !== 'md')
            return;

        let newName = newFile.basename;

        let oldName = Path.basename(oldFilePath, '.md');

        let mdFolderPath: string = Path.dirname(newFile.path);
        let oldMdFolderPath: string = Path.dirname(oldFilePath);
        let oldAttachmentFolderPath: string = this.getAttachmentFolderFullPath(oldMdFolderPath, oldName);
        let newAttachmentFolderPath: string = this.getAttachmentFolderFullPath(mdFolderPath, newName);

        //check if old attachment folder exists and is necessary to rename Folder
        if(await this.adapter.exists(oldAttachmentFolderPath) && (oldAttachmentFolderPath !== newAttachmentFolderPath))
        {
            let tfolder: TAbstractFile = this.app.vault.getAbstractFileByPath(oldAttachmentFolderPath);

            if(tfolder == null)
                return;

            let newAttachmentParentFolderPath: string = Path.dirname(newAttachmentFolderPath)
            if (!(await this.adapter.exists(newAttachmentParentFolderPath))) {
                await this.app.vault.createFolder(newAttachmentParentFolderPath);
            }

            await this.app.fileManager.renameFile(tfolder, newAttachmentFolderPath);

            let oldAttachmentParentFolderPath: string = Path.dirname(oldAttachmentFolderPath)
            let oldAttachmentParentFolderList: ListedFiles = await this.adapter.list(oldAttachmentParentFolderPath);
            if (oldAttachmentParentFolderList.folders.length === 0 && oldAttachmentParentFolderList.files.length === 0) {
              await this.adapter.rmdir(oldAttachmentParentFolderPath, true);
            }
    
            this.updateAttachmentFolderConfig(this.getAttachmentFolderPath(newName));
        }

        //if autoRenameFiles is off
        if(!this.settings.autoRenameFiles)
            return;

        let embeds = this.app.metadataCache.getCache(newFile.path)?.embeds;
        if(!embeds)
            return;

        let files: string[] = [];

        for(let embed of embeds)
        {
            let link = embed.link;
            if(link.endsWith('.png') || link.endsWith('jpeg'))
                files.push(Path.basename(link));
            else
                continue;

        }

        let attachmentFiles: ListedFiles= await this.adapter.list(newAttachmentFolderPath);
        for(let file of attachmentFiles.files)
        {
            console.log(file);
            let filePath = file;
            let fileName = Path.basename(filePath);
            if((files.indexOf(fileName) > -1) && fileName.contains(oldName))
            {
                fileName = fileName.replace(oldName, newName);
                let newFilePath = normalizePath(Path.join(newAttachmentFolderPath, fileName));
                let tfile = this.app.vault.getAbstractFileByPath(filePath);
                await this.app.fileManager.renameFile(tfile, newFilePath);
            }
            else
                continue;
        }
    }
Example #21
Source File: view.ts    From obsidian-fantasy-calendar with MIT License 4 votes vote down vote up
build() {
        this.contentEl.empty();
        this._app = new CalendarUI({
            target: this.contentEl,
            props: {
                calendar: this.helper,
                fullView: this.full,
                yearView: this.yearView,
                moons: this.moons,
                displayWeeks: this.helper.displayWeeks,
                displayDayNumber: this.dayNumber
            }
        });
        this._app.$on("day-click", (event: CustomEvent<DayHelper>) => {
            const day = event.detail;

            if (day.events.length) return;
            this.createEventForDay(day.date);
        });

        this._app.$on("day-doubleclick", (event: CustomEvent<DayHelper>) => {
            const day = event.detail;
            if (!day.events.length) return;

            this.helper.viewing.day = day.number;
            this.helper.viewing.month = day.month.number;
            this.helper.viewing.year = day.month.year;

            this.yearView = false;

            this._app.$set({ yearView: false });
            this._app.$set({ dayView: true });
            this.triggerHelperEvent("day-update", false);
        });

        this._app.$on(
            "day-context-menu",
            (event: CustomEvent<{ day: DayHelper; evt: MouseEvent }>) => {
                const { day, evt } = event.detail;

                const menu = new Menu(this.app);

                menu.setNoIcon();

                if (!this.full) {
                    menu.addItem((item) => {
                        item.setTitle("Open Day").onClick(() => {
                            this.openDay({
                                day: day.number,
                                month: this.helper.displayed.month,
                                year: this.helper.displayed.year
                            });
                        });
                    });
                }
                menu.addItem((item) => {
                    item.setTitle("Set as Today").onClick(() => {
                        this.calendar.current = day.date;

                        this.helper.current.day = day.number;

                        this.triggerHelperEvent("day-update");

                        this.saveCalendars();
                    });
                });
                menu.addItem((item) =>
                    item.setTitle("New Event").onClick(() => {
                        this.createEventForDay(day.date);
                    })
                );
                menu.showAtMouseEvent(evt);
            }
        );

        this._app.$on("settings", (event: CustomEvent<MouseEvent>) => {
            const evt = event.detail;
            const menu = new Menu(this.app);

            menu.setNoIcon();
            menu.addItem((item) => {
                item.setTitle(
                    `${this.calendar.displayWeeks ? "Hide" : "Show"} Weeks`
                ).onClick(() => {
                    this.calendar.displayWeeks = !this.calendar.displayWeeks;
                    this.helper.update(this.calendar);
                    this._app.$set({
                        displayWeeks: this.calendar.displayWeeks
                    });
                    this.saveCalendars();
                });
            });
            menu.addItem((item) => {
                item.setTitle(
                    `Open ${this.yearView ? "Month" : "Year"}`
                ).onClick(() => {
                    this.yearView = !this.yearView;
                    this._app.$set({ yearView: this.yearView });
                });
            });
            menu.addItem((item) => {
                item.setTitle(
                    this.moons ? "Hide Moons" : "Display Moons"
                ).onClick(() => {
                    this.toggleMoons();
                });
            });
            menu.addItem((item) => {
                item.setTitle(
                    this.dayNumber ? "Hide Day Number" : "Display Day Number"
                ).onClick(() => {
                    this.dayNumber = !this.dayNumber;
                    this.calendar.static.displayDayNumber = this.dayNumber;
                    this._app.$set({ displayDayNumber: this.dayNumber });
                    this.saveCalendars();
                });
            });
            menu.addItem((item) => {
                item.setTitle("View Day");

                item.onClick(() => {
                    this.openDate();
                });
            });
            menu.addItem((item) => {
                item.setTitle("Switch Calendars");
                item.setDisabled(this.plugin.data.calendars.length <= 1);
                item.onClick(() => {
                    const modal = new SwitchModal(this.plugin, this.calendar);

                    modal.onClose = () => {
                        if (!modal.confirmed) return;

                        this.setCurrentCalendar(modal.calendar);
                    };
                    modal.open();
                });
            });

            menu.showAtMouseEvent(evt);
        });

        this._app.$on(
            "event-click",
            (evt: CustomEvent<{ event: Event; modifier: boolean }>) => {
                const { event, modifier } = evt.detail;
                if (event.note) {
                    let leaves: WorkspaceLeaf[] = [];
                    this.app.workspace.iterateAllLeaves((leaf) => {
                        if (!(leaf.view instanceof MarkdownView)) return;
                        if (leaf.view.file.basename === event.note) {
                            leaves.push(leaf);
                        }
                    });
                    if (leaves.length) {
                        this.app.workspace.setActiveLeaf(leaves[0]);
                    } else {
                        this.app.workspace.openLinkText(
                            event.note,
                            "",
                            this.full || modifier
                        );
                    }
                } else {
                    const modal = new ViewEventModal(event, this.plugin);
                    modal.open();
                }
            }
        );

        this._app.$on(
            "event-mouseover",
            (evt: CustomEvent<{ target: HTMLElement; event: Event }>) => {
                if (!this.plugin.data.eventPreview) return;
                const { target, event } = evt.detail;
                if (event.note) {
                    this.app.workspace.trigger(
                        "link-hover",
                        this, //hover popover, but don't need
                        target, //targetEl
                        event.note, //linkText
                        "" //source
                    );
                }
            }
        );

        this._app.$on(
            "event-context",
            (custom: CustomEvent<{ evt: MouseEvent; event: Event }>) => {
                const { evt, event } = custom.detail;

                const menu = new Menu(this.app);

                menu.setNoIcon();

                if (!event.note) {
                    menu.addItem((item) => {
                        item.setTitle("Create Note").onClick(async () => {
                            const path =
                                this.app.workspace.getActiveFile()?.path;
                            const newFilePath = path
                                ? this.app.fileManager.getNewFileParent(path)
                                      ?.parent ?? "/"
                                : "/";

                            const date = `${event.date.year}-${
                                event.date.month + 1
                            }-${event.date.day}`;

                            let end: string;
                            if (event.end) {
                                end = `${event.end.year}-${
                                    event.end.month + 1
                                }-${event.end.day}`;
                            }

                            const content = {
                                "fc-calendar": this.calendar.name,
                                "fc-date": date,
                                ...(event.end ? { "fc-end": end } : {}),
                                ...(event.category
                                    ? {
                                          "fc-category":
                                              this.calendar.categories.find(
                                                  (cat) =>
                                                      cat.id == event.category
                                              )?.name
                                      }
                                    : {}),
                                "fc-display-name": event.name
                            };
                            event.note = normalizePath(
                                `${newFilePath}/${event.name}.md`
                            );

                            let file = this.app.vault.getAbstractFileByPath(
                                event.note
                            );
                            if (!file) {
                                file = await this.app.vault.create(
                                    event.note,
                                    `---\n${stringifyYaml(content)}\n---`
                                );
                            }
                            this.saveCalendars();

                            if (file instanceof TFile) {
                                const fileViews =
                                    this.app.workspace.getLeavesOfType(
                                        "markdown"
                                    );
                                const existing = fileViews.find((l) => {
                                    l.view instanceof FileView &&
                                        l.view.file.path == event.note;
                                });
                                if (existing) {
                                    this.app.workspace.setActiveLeaf(existing);
                                } else {
                                    await this.app.workspace
                                        .getUnpinnedLeaf()
                                        .openFile(file, {
                                            active: true
                                        });
                                }
                            }
                        });
                    });
                }

                menu.addItem((item) => {
                    item.setTitle("Edit Event").onClick(() => {
                        const modal = new CreateEventModal(
                            this.plugin,
                            this.calendar,
                            event
                        );

                        modal.onClose = () => {
                            if (!modal.saved) return;

                            const existing = this.calendar.events.find(
                                (e) => e.id == event.id
                            );

                            this.calendar.events.splice(
                                this.calendar.events.indexOf(existing),
                                1,
                                modal.event
                            );

                            this.helper.refreshMonth(
                                modal.event.date.month,
                                modal.event.date.year
                            );
                            if (
                                modal.event.date.month != existing.date.month ||
                                modal.event.date.year != existing.date.year
                            ) {
                                this.helper.refreshMonth(
                                    existing.date.month,
                                    existing.date.year
                                );
                            }

                            this.saveCalendars();

                            this._app.$set({
                                calendar: this.helper
                            });

                            this.triggerHelperEvent("day-update");
                        };

                        modal.open();
                    });
                });

                menu.addItem((item) => {
                    item.setTitle("Delete Event").onClick(async () => {
                        if (
                            !this.plugin.data.exit.event &&
                            !(await confirmEventDeletion(this.plugin))
                        )
                            return;
                        const existing = this.calendar.events.find(
                            (e) => e.id == event.id
                        );

                        this.calendar.events.splice(
                            this.calendar.events.indexOf(existing),
                            1
                        );

                        this.helper.refreshMonth(
                            existing.date.month,
                            existing.date.year
                        );

                        this.saveCalendars();

                        this._app.$set({
                            calendar: this.helper
                        });

                        this.triggerHelperEvent("day-update");
                    });
                });

                menu.showAtMouseEvent(evt);
            }
        );

        this._app.$on("event", (e: CustomEvent<CurrentCalendarData>) => {
            const date = e.detail;
            this.createEventForDay(date);
        });

        this._app.$on("reset", () => {
            this.helper.reset();

            this.yearView = false;

            this._app.$set({ yearView: false });
            this._app.$set({ dayView: true });
            this.triggerHelperEvent("day-update", false);
        });
    }
Example #22
Source File: localDictionaryBuilder.ts    From obsidian-dictionary with GNU Affero General Public License v3.0 4 votes vote down vote up
async newNote(content: DictionaryWord, openNote = true): Promise<void> {

        const { plugin, settings } = this;

        let audioLinks = '';
        content.phonetics.forEach((value, i, a) => {
            if (value.audio) {
                audioLinks += '- ' + (value.audio.startsWith("http") ? value.audio : "https:" + value.audio);
                if (i != a.length - 1) {
                    audioLinks += '\n';
                }
            }
        });

        let phonetics = '';
        content.phonetics.forEach((value, i, a) => {
            if (value.text) {
                phonetics += '- ' + (value.audio ? `<details><summary>${value.text}</summary><audio controls><source src="${value.audio.startsWith("http") ? value.audio : "https:" + value.audio}"></audio></details>` : value.text);
                if (i != a.length - 1) {
                    phonetics += '\n';
                }
            }
        });

        let meanings = '';
        content.meanings.forEach((value, i) => {
            meanings += '### ' + this.cap(value.partOfSpeech ?? t("Meaning {{i}}").replace(/{{i}}/g, (i + 1).toString())) + '\n\n';
            value.definitions.forEach((def, j, b) => {
                meanings += def.definition + '\n\n';
                if (def.example) {
                    meanings += '> ' + def.example + '\n\n';
                }
                if (def.synonyms && def.synonyms.length != 0) {
                    def.synonyms.forEach((syn, i, a) => {
                        meanings += syn;
                        if (i != a.length - 1) {
                            meanings += ', ';
                        }
                    })
                    meanings += '\n\n'
                }
                if (j != b.length - 1) {
                    meanings += '---\n\n';
                }
            });
        });

        let file: TFile;
        const langString = RFC[settings.defaultLanguage];
        const path = `${settings.folder ? settings.folder + '/' : ''}${settings.languageSpecificSubFolders ? langString + '/' : ''}${settings.prefix.replace(/{{lang}}/ig, langString)}${settings.capitalizedFileName ? this.cap(content.word) : content.word}${settings.suffix.replace(/{{lang}}/ig, langString)}.md`;
        let contents = settings.template
            .replace(/{{notice}}/ig, t('Autogenerated by Obsidian Dictionary Plugin'))
            .replace(/{{word}}/ig, settings.capitalizedFileName ? this.cap(content.word) : content.word)
            .replace(/{{pronunciationheader}}/ig, t('Pronunciation'))
            .replace(/{{phoneticlist}}/ig, phonetics)
            .replace(/{{meaningheader}}/ig, t('Meanings'))
            .replace(/{{meanings}}/ig, meanings)
            .replace(/{{lang}}/ig, langString)
            .replace(/{{audioLinks}}/ig, audioLinks);

        if (content.origin) {
            contents = contents
                .replace(/{{originHeader}}/ig, t('Origin'))
                .replace(/{{origin}}/ig, content.origin);
        } else {
            contents = contents
                .replace(/{{originHeader}}/ig, '')
                .replace(/{{origin}}/ig, '');
        }

        try {
            if (!(await plugin.app.vault.adapter.exists(normalizePath(`${settings.folder ? settings.folder + '/' : ''}${settings.languageSpecificSubFolders ? langString + '/' : ''}`)))) {
                await plugin.app.vault.createFolder(normalizePath(`${settings.folder ? settings.folder + '/' : ''}${settings.languageSpecificSubFolders ? langString + '/' : ''}`));
            }
            file = await plugin.app.vault.create(normalizePath(path), contents);
            if (openNote) {
                const leaf = plugin.app.workspace.splitActiveLeaf();
                await leaf.openFile(file);
                plugin.app.workspace.setActiveLeaf(leaf);
            }
        } catch (error) {
            new OverwriteModal(this.plugin, normalizePath(path), contents, openNote).open();
        }
    }
Example #23
Source File: main.ts    From obsidian-tracker with MIT License 4 votes vote down vote up
async getFiles(
        files: TFile[],
        renderInfo: RenderInfo,
        includeSubFolders: boolean = true
    ) {
        if (!files) return;

        let folderToSearch = renderInfo.folder;
        let useSpecifiedFilesOnly = renderInfo.specifiedFilesOnly;
        let specifiedFiles = renderInfo.file;
        let filesContainsLinkedFiles = renderInfo.fileContainsLinkedFiles;
        let fileMultiplierAfterLink = renderInfo.fileMultiplierAfterLink;

        // Include files in folder
        // console.log(useSpecifiedFilesOnly);
        if (!useSpecifiedFilesOnly) {
            let folder = this.app.vault.getAbstractFileByPath(
                normalizePath(folderToSearch)
            );
            if (folder && folder instanceof TFolder) {
                let folderFiles = this.getFilesInFolder(folder);
                for (let file of folderFiles) {
                    files.push(file);
                }
            }
        }

        // Include specified file
        // console.log(specifiedFiles);
        for (let filePath of specifiedFiles) {
            let path = filePath;
            if (!path.endsWith(".md")) {
                path += ".md";
            }
            path = normalizePath(path);
            // console.log(path);

            let file = this.app.vault.getAbstractFileByPath(path);
            // console.log(file);
            if (file && file instanceof TFile) {
                files.push(file);
            }
        }
        // console.log(files);

        // Include files in pointed by links in file
        // console.log(filesContainsLinkedFiles);
        // console.log(fileMultiplierAfterLink);
        let linkedFileMultiplier = 1;
        let searchFileMultifpierAfterLink = true;
        if (fileMultiplierAfterLink === "") {
            searchFileMultifpierAfterLink = false;
        } else if (/^[0-9]+$/.test(fileMultiplierAfterLink)) {
            // integer
            linkedFileMultiplier = parseFloat(fileMultiplierAfterLink);
            searchFileMultifpierAfterLink = false;
        } else if (!/\?<value>/.test(fileMultiplierAfterLink)) {
            // no 'value' named group
            searchFileMultifpierAfterLink = false;
        }
        for (let filePath of filesContainsLinkedFiles) {
            if (!filePath.endsWith(".md")) {
                filePath += ".md";
            }
            let file = this.app.vault.getAbstractFileByPath(
                normalizePath(filePath)
            );
            if (file && file instanceof TFile) {
                // Get linked files
                let fileCache = this.app.metadataCache.getFileCache(file);
                let fileContent = await this.app.vault.adapter.read(file.path);
                let lines = fileContent.split(
                    /\r\n|[\n\v\f\r\x85\u2028\u2029]/
                );
                // console.log(lines);

                if (!fileCache?.links) continue;

                for (let link of fileCache.links) {
                    if (!link) continue;
                    let linkedFile =
                        this.app.metadataCache.getFirstLinkpathDest(
                            link.link,
                            filePath
                        );
                    if (linkedFile && linkedFile instanceof TFile) {
                        if (searchFileMultifpierAfterLink) {
                            // Get the line of link in file
                            let lineNumber = link.position.end.line;
                            // console.log(lineNumber);
                            if (lineNumber >= 0 && lineNumber < lines.length) {
                                let line = lines[lineNumber];
                                // console.log(line);

                                // Try extract multiplier
                                // if (link.position)
                                let splitted = line.split(link.original);
                                // console.log(splitted);
                                if (splitted.length === 2) {
                                    let toParse = splitted[1].trim();
                                    let strRegex = fileMultiplierAfterLink;
                                    let regex = new RegExp(strRegex, "gm");
                                    let match;
                                    while ((match = regex.exec(toParse))) {
                                        // console.log(match);
                                        if (
                                            typeof match.groups !==
                                                "undefined" &&
                                            typeof match.groups.value !==
                                                "undefined"
                                        ) {
                                            // must have group name 'value'
                                            let retParse =
                                                helper.parseFloatFromAny(
                                                    match.groups.value.trim(),
                                                    renderInfo.textValueMap
                                                );
                                            if (retParse.value !== null) {
                                                linkedFileMultiplier =
                                                    retParse.value;
                                                break;
                                            }
                                        }
                                    }
                                }
                            }
                        }

                        for (let i = 0; i < linkedFileMultiplier; i++) {
                            files.push(linkedFile);
                        }
                    }
                }
            }
        }

        // console.log(files);
    }
Example #24
Source File: main.ts    From obsidian-tracker with MIT License 4 votes vote down vote up
// TODO: remove this.app and move to collecting.ts
    async collectDataFromTable(
        dataMap: DataMap,
        renderInfo: RenderInfo,
        processInfo: CollectingProcessInfo
    ) {
        // console.log("collectDataFromTable");

        let tableQueries = renderInfo.queries.filter(
            (q) => q.getType() === SearchType.Table
        );
        // console.log(tableQueries);
        // Separate queries by tables and xDatasets/yDatasets
        let tables: Array<TableData> = [];
        let tableFileNotFound = false;
        for (let query of tableQueries) {
            let filePath = query.getParentTarget();
            let file = this.app.vault.getAbstractFileByPath(
                normalizePath(filePath + ".md")
            );
            if (!file || !(file instanceof TFile)) {
                tableFileNotFound = true;
                break;
            }

            let tableIndex = query.getAccessor();
            let isX = query.usedAsXDataset;

            let table = tables.find(
                (t) => t.filePath === filePath && t.tableIndex === tableIndex
            );
            if (table) {
                if (isX) {
                    table.xDataset = query;
                } else {
                    table.yDatasets.push(query);
                }
            } else {
                let tableData = new TableData(filePath, tableIndex);
                if (isX) {
                    tableData.xDataset = query;
                } else {
                    tableData.yDatasets.push(query);
                }
                tables.push(tableData);
            }
        }
        // console.log(tables);

        if (tableFileNotFound) {
            processInfo.errorMessage = "File containing tables not found";
            return;
        }

        for (let tableData of tables) {
            //extract xDataset from query
            let xDatasetQuery = tableData.xDataset;
            if (!xDatasetQuery) {
                // missing xDataset
                continue;
            }
            let yDatasetQueries = tableData.yDatasets;
            let filePath = xDatasetQuery.getParentTarget();
            let tableIndex = xDatasetQuery.getAccessor();

            // Get table text
            let textTable = "";
            filePath = filePath + ".md";
            let file = this.app.vault.getAbstractFileByPath(
                normalizePath(filePath)
            );
            if (file && file instanceof TFile) {
                processInfo.fileAvailable++;
                let content = await this.app.vault.adapter.read(file.path);
                // console.log(content);

                // Test this in Regex101
                // This is a not-so-strict table selector
                // ((\r?\n){2}|^)([^\r\n]*\|[^\r\n]*(\r?\n)?)+(?=(\r?\n){2}|$)
                let strMDTableRegex =
                    "((\\r?\\n){2}|^)([^\\r\\n]*\\|[^\\r\\n]*(\\r?\\n)?)+(?=(\\r?\\n){2}|$)";
                // console.log(strMDTableRegex);
                let mdTableRegex = new RegExp(strMDTableRegex, "gm");
                let match;
                let indTable = 0;

                while ((match = mdTableRegex.exec(content))) {
                    // console.log(match);
                    if (indTable === tableIndex) {
                        textTable = match[0];
                        break;
                    }
                    indTable++;
                }
            } else {
                // file not exists
                continue;
            }
            // console.log(textTable);

            let tableLines = textTable.split(/\r?\n/);
            tableLines = tableLines.filter((line) => {
                return line !== "";
            });
            let numColumns = 0;
            let numDataRows = 0;
            // console.log(tableLines);

            // Make sure it is a valid table first
            if (tableLines.length >= 2) {
                // Must have header and separator line
                let headerLine = tableLines.shift().trim();
                headerLine = helper.trimByChar(headerLine, "|");
                let headerSplitted = headerLine.split("|");
                numColumns = headerSplitted.length;

                let sepLine = tableLines.shift().trim();
                sepLine = helper.trimByChar(sepLine, "|");
                let spepLineSplitted = sepLine.split("|");
                for (let col of spepLineSplitted) {
                    if (!col.includes("-")) {
                        break; // Not a valid sep
                    }
                }

                numDataRows = tableLines.length;
            }

            if (numDataRows == 0) continue;

            // get x data
            let columnXDataset = xDatasetQuery.getAccessor(1);
            if (columnXDataset >= numColumns) continue;
            let xValues = [];

            let indLine = 0;
            for (let tableLine of tableLines) {
                let dataRow = helper.trimByChar(tableLine.trim(), "|");
                let dataRowSplitted = dataRow.split("|");
                if (columnXDataset < dataRowSplitted.length) {
                    let data = dataRowSplitted[columnXDataset].trim();
                    let date = helper.strToDate(data, renderInfo.dateFormat);

                    if (date.isValid()) {
                        xValues.push(date);

                        if (
                            !processInfo.minDate.isValid() &&
                            !processInfo.maxDate.isValid()
                        ) {
                            processInfo.minDate = date.clone();
                            processInfo.maxDate = date.clone();
                        } else {
                            if (date < processInfo.minDate) {
                                processInfo.minDate = date.clone();
                            }
                            if (date > processInfo.maxDate) {
                                processInfo.maxDate = date.clone();
                            }
                        }
                    } else {
                        xValues.push(null);
                    }
                } else {
                    xValues.push(null);
                }
                indLine++;
            }
            // console.log(xValues);

            if (
                xValues.every((v) => {
                    return v === null;
                })
            ) {
                processInfo.errorMessage =
                    "No valid date as X value found in table";
                return;
            } else {
                processInfo.gotAnyValidXValue ||= true;
            }

            // get y data
            for (let yDatasetQuery of yDatasetQueries) {
                let columnOfInterest = yDatasetQuery.getAccessor(1);
                // console.log(`columnOfInterest: ${columnOfInterest}, numColumns: ${numColumns}`);
                if (columnOfInterest >= numColumns) continue;

                let indLine = 0;
                for (let tableLine of tableLines) {
                    let dataRow = helper.trimByChar(tableLine.trim(), "|");
                    let dataRowSplitted = dataRow.split("|");
                    if (columnOfInterest < dataRowSplitted.length) {
                        let data = dataRowSplitted[columnOfInterest].trim();
                        let splitted = data.split(yDatasetQuery.getSeparator());
                        // console.log(splitted);
                        if (!splitted) continue;
                        if (splitted.length === 1) {
                            let retParse = helper.parseFloatFromAny(
                                splitted[0],
                                renderInfo.textValueMap
                            );
                            // console.log(retParse);
                            if (retParse.value !== null) {
                                if (retParse.type === ValueType.Time) {
                                    yDatasetQuery.valueType = ValueType.Time;
                                }
                                let value = retParse.value;
                                if (
                                    indLine < xValues.length &&
                                    xValues[indLine]
                                ) {
                                    processInfo.gotAnyValidYValue ||= true;
                                    collecting.addToDataMap(
                                        dataMap,
                                        helper.dateToStr(
                                            xValues[indLine],
                                            renderInfo.dateFormat
                                        ),
                                        yDatasetQuery,
                                        value
                                    );
                                }
                            }
                        } else if (
                            splitted.length > yDatasetQuery.getAccessor(2) &&
                            yDatasetQuery.getAccessor(2) >= 0
                        ) {
                            let value = null;
                            let splittedPart =
                                splitted[yDatasetQuery.getAccessor(2)].trim();
                            // console.log(splittedPart);
                            let retParse = helper.parseFloatFromAny(
                                splittedPart,
                                renderInfo.textValueMap
                            );
                            // console.log(retParse);
                            if (retParse.value !== null) {
                                if (retParse.type === ValueType.Time) {
                                    yDatasetQuery.valueType = ValueType.Time;
                                }
                                value = retParse.value;
                                if (
                                    indLine < xValues.length &&
                                    xValues[indLine]
                                ) {
                                    processInfo.gotAnyValidYValue ||= true;
                                    collecting.addToDataMap(
                                        dataMap,
                                        helper.dateToStr(
                                            xValues[indLine],
                                            renderInfo.dateFormat
                                        ),
                                        yDatasetQuery,
                                        value
                                    );
                                }
                            }
                        }
                    }

                    indLine++;
                } // Loop over tableLines
            }
        }
    }
Example #25
Source File: parsing.ts    From obsidian-tracker with MIT License 4 votes vote down vote up
export function getRenderInfoFromYaml(
    yamlText: string,
    plugin: Tracker
): RenderInfo | string {
    let yaml;
    try {
        // console.log(yamlText);
        yaml = parseYaml(yamlText);
    } catch (err) {
        let errorMessage = "Error parsing YAML";
        console.log(err);
        return errorMessage;
    }
    if (!yaml) {
        let errorMessage = "Error parsing YAML";
        return errorMessage;
    }
    // console.log(yaml);
    let keysFoundInYAML = getAvailableKeysOfClass(yaml);
    // console.log(keysFoundInYAML);

    let errorMessage = "";

    // Search target
    if (!keysFoundInYAML.includes("searchTarget")) {
        let errorMessage = "Parameter 'searchTarget' not found in YAML";
        return errorMessage;
    }
    let searchTarget: Array<string> = [];
    if (typeof yaml.searchTarget === "object" && yaml.searchTarget !== null) {
        if (Array.isArray(yaml.searchTarget)) {
            for (let target of yaml.searchTarget) {
                if (typeof target === "string") {
                    if (target !== "") {
                        searchTarget.push(target);
                    } else {
                        errorMessage = "Empty search target is not allowed.";
                        break;
                    }
                }
            }
        }
    } else if (typeof yaml.searchTarget === "string") {
        let splitted = splitInputByComma(yaml.searchTarget);
        // console.log(splitted);
        if (splitted.length > 1) {
            for (let piece of splitted) {
                piece = piece.trim();
                if (piece !== "") {
                    searchTarget.push(piece);
                } else {
                    errorMessage = "Empty search target is not allowed.";
                    break;
                }
            }
        } else if (yaml.searchTarget === "") {
            errorMessage = "Empty search target is not allowed.";
        } else {
            searchTarget.push(yaml.searchTarget);
        }
    } else {
        errorMessage = "Invalid search target (searchTarget)";
    }
    for (let ind = 0; ind < searchTarget.length; ind++) {
        searchTarget[ind] = helper.replaceImgTagByAlt(searchTarget[ind]);
    }
    // console.log(searchTarget);

    if (errorMessage !== "") {
        return errorMessage;
    }

    let numDatasets = searchTarget.length;

    // Search type
    if (!keysFoundInYAML.includes("searchType")) {
        let errorMessage = "Parameter 'searchType' not found in YAML";
        return errorMessage;
    }
    let searchType: Array<SearchType> = [];
    let retSearchType = getStringArrayFromInput(
        "searchType",
        yaml.searchType,
        numDatasets,
        "",
        validateSearchType,
        false
    );
    if (typeof retSearchType === "string") {
        return retSearchType; // errorMessage
    }
    for (let strType of retSearchType) {
        switch (strType.toLowerCase()) {
            case "tag":
                searchType.push(SearchType.Tag);
                break;
            case "frontmatter":
                searchType.push(SearchType.Frontmatter);
                break;
            case "wiki":
                searchType.push(SearchType.Wiki);
                break;
            case "wiki.link":
                searchType.push(SearchType.WikiLink);
                break;
            case "wiki.display":
                searchType.push(SearchType.WikiDisplay);
                break;
            case "text":
                searchType.push(SearchType.Text);
                break;
            case "dvfield":
                searchType.push(SearchType.dvField);
                break;
            case "table":
                searchType.push(SearchType.Table);
                break;
            case "filemeta":
                searchType.push(SearchType.FileMeta);
                break;
            case "task":
                searchType.push(SearchType.Task);
                break;
            case "task.all":
                searchType.push(SearchType.Task);
                break;
            case "task.done":
                searchType.push(SearchType.TaskDone);
                break;
            case "task.notdone":
                searchType.push(SearchType.TaskNotDone);
                break;
        }
    }
    // Currently, we don't allow type 'table' used with other types
    if (
        searchType.includes(SearchType.Table) &&
        searchType.filter((t) => t !== SearchType.Table).length > 0
    ) {
        let errorMessage =
            "searchType 'table' doestn't work with other types for now";
        return errorMessage;
    }
    // console.log(searchType);

    // separator
    let multipleValueSparator: Array<string> = [];
    let retMultipleValueSparator = getStringArrayFromInput(
        "separator",
        yaml.separator,
        numDatasets,
        "", // set the default value later
        null,
        true
    );
    if (typeof retMultipleValueSparator === "string") {
        return retMultipleValueSparator; // errorMessage
    }
    multipleValueSparator = retMultipleValueSparator.map((sep) => {
        if (sep === "comma" || sep === "\\,") {
            return ",";
        }
        return sep;
    });
    // console.log(multipleValueSparator);

    // xDataset
    let retXDataset = getNumberArrayFromInput(
        "xDataset",
        yaml.xDataset,
        numDatasets,
        -1,
        true
    );
    if (typeof retXDataset === "string") {
        return retXDataset; // errorMessage
    }
    let xDataset = retXDataset.map((d: number) => {
        if (d < 0 || d >= numDatasets) {
            return -1;
        }
        return d;
    });
    // assign this to renderInfo later

    // Create queries
    let queries: Array<Query> = [];
    for (let ind = 0; ind < searchTarget.length; ind++) {
        let query = new Query(
            queries.length,
            searchType[ind],
            searchTarget[ind]
        );
        query.setSeparator(multipleValueSparator[ind]);
        if (xDataset.includes(ind)) query.usedAsXDataset = true;
        queries.push(query);
    }
    // console.log(queries);

    // Create grarph info
    let renderInfo = new RenderInfo(queries);
    let keysOfRenderInfo = getAvailableKeysOfClass(renderInfo);
    let additionalAllowedKeys = ["searchType", "searchTarget", "separator"];
    // console.log(keysOfRenderInfo);
    let yamlLineKeys = [];
    let yamlBarKeys = [];
    let yamlPieKeys = [];
    let yamlSummaryKeys = [];
    let yamlMonthKeys = [];
    let yamlHeatmapKeys = [];
    let yamlBulletKeys = [];
    for (let key of keysFoundInYAML) {
        if (/^line[0-9]*$/.test(key)) {
            yamlLineKeys.push(key);
            additionalAllowedKeys.push(key);
        }
        if (/^bar[0-9]*$/.test(key)) {
            yamlBarKeys.push(key);
            additionalAllowedKeys.push(key);
        }
        if (/^pie[0-9]*$/.test(key)) {
            yamlPieKeys.push(key);
            additionalAllowedKeys.push(key);
        }
        if (/^summary[0-9]*$/.test(key)) {
            yamlSummaryKeys.push(key);
            additionalAllowedKeys.push(key);
        }
        if (/^bullet[0-9]*$/.test(key)) {
            yamlBulletKeys.push(key);
            additionalAllowedKeys.push(key);
        }
        if (/^month[0-9]*$/.test(key)) {
            yamlMonthKeys.push(key);
            additionalAllowedKeys.push(key);
        }
        if (/^heatmap[0-9]*$/.test(key)) {
            yamlHeatmapKeys.push(key);
            additionalAllowedKeys.push(key);
        }
    }
    // Custom dataset
    let yamlCustomDatasetKeys = [];
    for (let key of keysFoundInYAML) {
        if (/^dataset[0-9]*$/.test(key)) {
            // Check the id of custom dataset is not duplicated
            let customDatasetId = -1;
            let strCustomDatasetId = key.replace("dataset", "");
            if (strCustomDatasetId === "") {
                customDatasetId = 0;
            } else {
                customDatasetId = parseFloat(strCustomDatasetId);
            }

            if (
                queries.some((q) => {
                    return q.getId() === customDatasetId;
                })
            ) {
                errorMessage = "Duplicated dataset id for key '" + key + "'";
                return errorMessage;
            }

            yamlCustomDatasetKeys.push(key);
            additionalAllowedKeys.push(key);
        }
    }
    // console.log(additionalAllowedKeys);
    for (let key of keysFoundInYAML) {
        if (
            !keysOfRenderInfo.includes(key) &&
            !additionalAllowedKeys.includes(key)
        ) {
            errorMessage = "'" + key + "' is not an available key";
            return errorMessage;
        }
    }

    let totalNumOutputs =
        yamlLineKeys.length +
        yamlBarKeys.length +
        yamlPieKeys.length +
        yamlSummaryKeys.length +
        yamlBulletKeys.length +
        yamlMonthKeys.length +
        yamlHeatmapKeys.length;
    if (totalNumOutputs === 0) {
        return "No output parameter provided, please place line, bar, pie, month, bullet, or summary.";
    }

    // Root folder to search
    renderInfo.folder = getStringFromInput(
        yaml?.folder,
        plugin.settings.folder
    );
    if (renderInfo.folder.trim() === "") {
        renderInfo.folder = plugin.settings.folder;
    }
    // console.log("renderInfo folder: " + renderInfo.folder);

    let abstractFolder = plugin.app.vault.getAbstractFileByPath(
        normalizePath(renderInfo.folder)
    );
    if (!abstractFolder || !(abstractFolder instanceof TFolder)) {
        let errorMessage = "Folder '" + renderInfo.folder + "' doesn't exist";
        return errorMessage;
    }

    // file
    if (typeof yaml.file === "string") {
        let retFiles = getStringArray("file", yaml.file);
        if (typeof retFiles === "string") {
            return retFiles; // error message
        }
        renderInfo.file = retFiles;
    }
    // console.log(renderInfo.file);

    // specifiedFilesOnly
    if (typeof yaml.specifiedFilesOnly === "boolean") {
        renderInfo.specifiedFilesOnly = yaml.specifiedFilesOnly;
    }
    // console.log(renderInfo.specifiedFilesOnly);

    // fileContainsLinkedFiles
    if (typeof yaml.fileContainsLinkedFiles === "string") {
        let retFiles = getStringArray(
            "fileContainsLinkedFiles",
            yaml.fileContainsLinkedFiles
        );
        if (typeof retFiles === "string") {
            return retFiles;
        }
        renderInfo.fileContainsLinkedFiles = retFiles;
    }
    // console.log(renderInfo.fileContainsLinkedFiles);

    // fileMultiplierAfterLink
    renderInfo.fileMultiplierAfterLink = getStringFromInput(
        yaml?.fileMultiplierAfterLink,
        renderInfo.fileMultiplierAfterLink
    );
    // console.log(renderInfo.fileMultiplierAfterLink);

    // Date format
    const dateFormat = yaml.dateFormat;
    //?? not sure why I need this to make it works,
    // without that, the assigned the renderInfo.dateFormat will become undefined
    if (typeof yaml.dateFormat === "string") {
        if (yaml.dateFormat === "") {
            renderInfo.dateFormat = plugin.settings.dateFormat;
        } else {
            renderInfo.dateFormat = dateFormat;
        }
    } else {
        renderInfo.dateFormat = plugin.settings.dateFormat;
    }
    // console.log("renderInfo dateFormat: " + renderInfo.dateFormat);

    // Date format prefix
    renderInfo.dateFormatPrefix = getStringFromInput(
        yaml?.dateFormatPrefix,
        renderInfo.dateFormatPrefix
    );

    // Date fromat suffix
    renderInfo.dateFormatSuffix = getStringFromInput(
        yaml?.dateFormatSuffix,
        renderInfo.dateFormatSuffix
    );

    // startDate, endDate
    // console.log("Parsing startDate");
    if (typeof yaml.startDate === "string") {
        if (/^([\-]?[0-9]+[\.][0-9]+|[\-]?[0-9]+)m$/.test(yaml.startDate)) {
            let errorMessage =
                "'m' for 'minute' is too small for parameter startDate, please use 'd' for 'day' or 'M' for month";
            return errorMessage;
        }
        let strStartDate = helper.getDateStringFromInputString(
            yaml.startDate,
            renderInfo.dateFormatPrefix,
            renderInfo.dateFormatSuffix
        );
        // console.log(strStartDate);

        // relative date
        let startDate = null;
        let isStartDateValid = false;
        startDate = helper.getDateByDurationToToday(
            strStartDate,
            renderInfo.dateFormat
        );
        // console.log(startDate);

        if (startDate) {
            isStartDateValid = true;
        } else {
            startDate = helper.strToDate(strStartDate, renderInfo.dateFormat);
            if (startDate.isValid()) {
                isStartDateValid = true;
            }
        }
        // console.log(startDate);

        if (!isStartDateValid || startDate === null) {
            let errorMessage =
                "Invalid startDate, the format of startDate may not match your dateFormat " +
                renderInfo.dateFormat;
            return errorMessage;
        }
        renderInfo.startDate = startDate;
    }

    // console.log("Parsing endDate");
    if (typeof yaml.endDate === "string") {
        if (/^([\-]?[0-9]+[\.][0-9]+|[\-]?[0-9]+)m$/.test(yaml.endDate)) {
            let errorMessage =
                "'m' for 'minute' is too small for parameter endDate, please use 'd' for 'day' or 'M' for month";
            return errorMessage;
        }
        let strEndDate = helper.getDateStringFromInputString(
            yaml.endDate,
            renderInfo.dateFormatPrefix,
            renderInfo.dateFormatSuffix
        );

        let endDate = null;
        let isEndDateValid = false;
        endDate = helper.getDateByDurationToToday(
            strEndDate,
            renderInfo.dateFormat
        );
        if (endDate) {
            isEndDateValid = true;
        } else {
            endDate = helper.strToDate(strEndDate, renderInfo.dateFormat);
            if (endDate.isValid()) {
                isEndDateValid = true;
            }
        }
        // console.log(endDate);

        if (!isEndDateValid || endDate === null) {
            let errorMessage =
                "Invalid endDate, the format of endDate may not match your dateFormat " +
                renderInfo.dateFormat;
            return errorMessage;
        }
        renderInfo.endDate = endDate;
    }
    if (
        renderInfo.startDate !== null &&
        renderInfo.startDate.isValid() &&
        renderInfo.endDate !== null &&
        renderInfo.endDate.isValid()
    ) {
        // Make sure endDate > startDate
        if (renderInfo.endDate < renderInfo.startDate) {
            let errorMessage =
                "Invalid date range (startDate larger than endDate)";
            return errorMessage;
        }
    }
    // console.log(renderInfo.startDate);
    // console.log(renderInfo.endDate);

    // xDataset
    renderInfo.xDataset = xDataset;
    // console.log(renderInfo.xDataset);

    // Dataset name (need xDataset to set default name)
    let retDatasetName = getStringArrayFromInput(
        "datasetName",
        yaml.datasetName,
        numDatasets,
        "untitled",
        null,
        true
    );
    if (typeof retDatasetName === "string") {
        return retDatasetName; // errorMessage
    }
    // rename untitled
    let indUntitled = 0;
    for (let ind = 0; ind < retDatasetName.length; ind++) {
        if (renderInfo.xDataset.includes(ind)) continue;
        if (retDatasetName[ind] === "untitled") {
            retDatasetName[ind] = "untitled" + indUntitled.toString();
            indUntitled++;
        }
    }
    // Check duplicated names
    if (new Set(retDatasetName).size === retDatasetName.length) {
        renderInfo.datasetName = retDatasetName;
    } else {
        let errorMessage = "Not enough dataset names or duplicated names";
        return errorMessage;
    }
    // console.log(renderInfo.datasetName);

    // constValue
    let retConstValue = getNumberArrayFromInput(
        "constValue",
        yaml.constValue,
        numDatasets,
        1.0,
        true
    );
    if (typeof retConstValue === "string") {
        return retConstValue; // errorMessage
    }
    renderInfo.constValue = retConstValue;
    // console.log(renderInfo.constValue);

    // ignoreAttachedValue
    let retIgnoreAttachedValue = getBoolArrayFromInput(
        "ignoreAttachedValue",
        yaml.ignoreAttachedValue,
        numDatasets,
        false,
        true
    );
    if (typeof retIgnoreAttachedValue === "string") {
        return retIgnoreAttachedValue;
    }
    renderInfo.ignoreAttachedValue = retIgnoreAttachedValue;
    // console.log(renderInfo.ignoreAttachedValue);

    // ignoreZeroValue
    let retIgnoreZeroValue = getBoolArrayFromInput(
        "ignoreZeroValue",
        yaml.ignoreZeroValue,
        numDatasets,
        false,
        true
    );
    if (typeof retIgnoreZeroValue === "string") {
        return retIgnoreZeroValue;
    }
    renderInfo.ignoreZeroValue = retIgnoreZeroValue;
    // console.log(renderInfo.ignoreAttachedValue);

    // accum
    let retAccum = getBoolArrayFromInput(
        "accum",
        yaml.accum,
        numDatasets,
        false,
        true
    );
    if (typeof retAccum === "string") {
        return retAccum;
    }
    renderInfo.accum = retAccum;
    // console.log(renderInfo.accum);

    // penalty
    let retPenalty = getNumberArrayFromInput(
        "penalty",
        yaml.penalty,
        numDatasets,
        null,
        true
    );
    if (typeof retPenalty === "string") {
        return retPenalty;
    }
    renderInfo.penalty = retPenalty;
    // console.log(renderInfo.penalty);

    // valueShift
    let retValueShift = getNumberArrayFromInput(
        "valueShift",
        yaml.valueShift,
        numDatasets,
        0,
        true
    );
    if (typeof retValueShift === "string") {
        return retValueShift;
    }
    renderInfo.valueShift = retValueShift;
    // console.log(renderInfo.valueShift);

    // shiftOnlyValueLargerThan
    let retShiftOnlyValueLargerThan = getNumberArrayFromInput(
        "shiftOnlyValueLargerThan",
        yaml.shiftOnlyValueLargerThan,
        numDatasets,
        null,
        true
    );
    if (typeof retShiftOnlyValueLargerThan === "string") {
        return retShiftOnlyValueLargerThan;
    }
    renderInfo.shiftOnlyValueLargerThan = retShiftOnlyValueLargerThan;
    // console.log(renderInfo.shiftOnlyValueLargerThan);

    // textValueMap
    if (typeof yaml.textValueMap !== "undefined") {
        let keys = getAvailableKeysOfClass(yaml.textValueMap);
        // console.log(texts);
        for (let key of keys) {
            let text = key.trim();
            renderInfo.textValueMap[text] = yaml.textValueMap[text];
        }
    }
    // console.log(renderInfo.textValueMap);

    // fixedScale
    if (typeof yaml.fixedScale === "number") {
        renderInfo.fixedScale = yaml.fixedScale;
    }

    // fitPanelWidth
    if (typeof yaml.fitPanelWidth === "boolean") {
        renderInfo.fitPanelWidth = yaml.fitPanelWidth;
    }

    // margin
    let retMargin = getNumberArrayFromInput("margin", yaml.margin, 4, 10, true);
    if (typeof retMargin === "string") {
        return retMargin; // errorMessage
    }
    if (retMargin.length > 4) {
        return "margin accepts not more than four values for top, right, bottom, and left margins.";
    }
    renderInfo.margin = new Margin(
        retMargin[0],
        retMargin[1],
        retMargin[2],
        retMargin[3]
    );
    // console.log(renderInfo.margin);

    // customDataset related parameters
    for (let datasetKey of yamlCustomDatasetKeys) {
        let customDataset = new CustomDatasetInfo();
        let yamlCustomDataset = yaml[datasetKey];

        let keysOfCustomDatasetInfo = getAvailableKeysOfClass(customDataset);
        let keysFoundInYAML = getAvailableKeysOfClass(yamlCustomDataset);
        // console.log(keysOfCustomDatasetInfo);
        // console.log(keysFoundInYAML);
        for (let key of keysFoundInYAML) {
            if (!keysOfCustomDatasetInfo.includes(key)) {
                errorMessage = "'" + key + "' is not an available key";
                return errorMessage;
            }
        }

        // id
        let customDatasetId = -1;
        let strCustomDatasetId = datasetKey.replace("dataset", "");
        if (strCustomDatasetId === "") {
            customDatasetId = 0;
        } else {
            customDatasetId = parseFloat(strCustomDatasetId);
        }
        customDataset.id = customDatasetId;

        // name
        customDataset.name = getStringFromInput(
            yamlCustomDataset?.name,
            customDataset.name
        );

        // xData
        let retXData = getStringArray("xData", yamlCustomDataset?.xData);
        if (typeof retXData === "string") {
            return retXData;
        }
        customDataset.xData = retXData;
        // console.log(customDataset.xData);
        let numXData = customDataset.xData.length;

        // yData
        let retYData = getStringArray("yData", yamlCustomDataset?.yData);
        if (typeof retYData === "string") {
            return retYData;
        }
        customDataset.yData = retYData;
        // console.log(customDataset.yData);
        if (customDataset.yData.length !== numXData) {
            let errorMessage =
                "Number of elements in xData and yData not matched";
            return errorMessage;
        }

        renderInfo.customDataset.push(customDataset);
    } // customDataset related parameters
    // console.log(renderInfo.customDataset);

    // line related parameters
    for (let lineKey of yamlLineKeys) {
        let line = new LineInfo();
        let yamlLine = yaml[lineKey];

        let keysOfLineInfo = getAvailableKeysOfClass(line);
        let keysFoundInYAML = getAvailableKeysOfClass(yamlLine);
        // console.log(keysOfLineInfo);
        // console.log(keysFoundInYAML);
        for (let key of keysFoundInYAML) {
            if (!keysOfLineInfo.includes(key)) {
                errorMessage = "'" + key + "' is not an available key";
                return errorMessage;
            }
        }

        let retParseCommonChartInfo = parseCommonChartInfo(yamlLine, line);
        if (typeof retParseCommonChartInfo === "string") {
            return retParseCommonChartInfo;
        }

        // lineColor
        let retLineColor = getStringArrayFromInput(
            "lineColor",
            yamlLine?.lineColor,
            numDatasets,
            "",
            validateColor,
            true
        );
        if (typeof retLineColor === "string") {
            return retLineColor; // errorMessage
        }
        line.lineColor = retLineColor;
        // console.log(line.lineColor);

        // lineWidth
        let retLineWidth = getNumberArrayFromInput(
            "lineWidth",
            yamlLine?.lineWidth,
            numDatasets,
            1.5,
            true
        );
        if (typeof retLineWidth === "string") {
            return retLineWidth; // errorMessage
        }
        line.lineWidth = retLineWidth;
        // console.log(line.lineWidth);

        // showLine
        let retShowLine = getBoolArrayFromInput(
            "showLine",
            yamlLine?.showLine,
            numDatasets,
            true,
            true
        );
        if (typeof retShowLine === "string") {
            return retShowLine;
        }
        line.showLine = retShowLine;
        // console.log(line.showLine);

        // showPoint
        let retShowPoint = getBoolArrayFromInput(
            "showPoint",
            yamlLine?.showPoint,
            numDatasets,
            true,
            true
        );
        if (typeof retShowPoint === "string") {
            return retShowPoint;
        }
        line.showPoint = retShowPoint;
        // console.log(line.showPoint);

        // pointColor
        let retPointColor = getStringArrayFromInput(
            "pointColor",
            yamlLine?.pointColor,
            numDatasets,
            "#69b3a2",
            validateColor,
            true
        );
        if (typeof retPointColor === "string") {
            return retPointColor;
        }
        line.pointColor = retPointColor;
        // console.log(line.pointColor);

        // pointBorderColor
        let retPointBorderColor = getStringArrayFromInput(
            "pointBorderColor",
            yamlLine?.pointBorderColor,
            numDatasets,
            "#69b3a2",
            validateColor,
            true
        );
        if (typeof retPointBorderColor === "string") {
            return retPointBorderColor;
        }
        line.pointBorderColor = retPointBorderColor;
        // console.log(line.pointBorderColor);

        // pointBorderWidth
        let retPointBorderWidth = getNumberArrayFromInput(
            "pointBorderWidth",
            yamlLine?.pointBorderWidth,
            numDatasets,
            0.0,
            true
        );
        if (typeof retPointBorderWidth === "string") {
            return retPointBorderWidth; // errorMessage
        }
        line.pointBorderWidth = retPointBorderWidth;
        // console.log(line.pointBorderWidth);

        // pointSize
        let retPointSize = getNumberArrayFromInput(
            "pointSize",
            yamlLine?.pointSize,
            numDatasets,
            3.0,
            true
        );
        if (typeof retPointSize === "string") {
            return retPointSize; // errorMessage
        }
        line.pointSize = retPointSize;
        // console.log(line.pointSize);

        // fillGap
        let retFillGap = getBoolArrayFromInput(
            "fillGap",
            yamlLine?.fillGap,
            numDatasets,
            false,
            true
        );
        if (typeof retFillGap === "string") {
            return retFillGap;
        }
        line.fillGap = retFillGap;
        // console.log(line.fillGap);

        // yAxisLocation
        let retYAxisLocation = getStringArrayFromInput(
            "yAxisLocation",
            yamlLine?.yAxisLocation,
            numDatasets,
            "left",
            validateYAxisLocation,
            true
        );
        if (typeof retYAxisLocation === "string") {
            return retYAxisLocation; // errorMessage
        }
        line.yAxisLocation = retYAxisLocation;
        // console.log(line.yAxisLocation);

        renderInfo.line.push(line);
    } // line related parameters
    // console.log(renderInfo.line);

    // bar related parameters
    for (let barKey of yamlBarKeys) {
        let bar = new BarInfo();
        let yamlBar = yaml[barKey];

        let keysOfBarInfo = getAvailableKeysOfClass(bar);
        let keysFoundInYAML = getAvailableKeysOfClass(yamlBar);
        // console.log(keysOfBarInfo);
        // console.log(keysFoundInYAML);
        for (let key of keysFoundInYAML) {
            if (!keysOfBarInfo.includes(key)) {
                errorMessage = "'" + key + "' is not an available key";
                return errorMessage;
            }
        }

        let retParseCommonChartInfo = parseCommonChartInfo(yamlBar, bar);
        if (typeof retParseCommonChartInfo === "string") {
            return retParseCommonChartInfo;
        }

        // barColor
        let retBarColor = getStringArrayFromInput(
            "barColor",
            yamlBar?.barColor,
            numDatasets,
            "",
            validateColor,
            true
        );
        if (typeof retBarColor === "string") {
            return retBarColor; // errorMessage
        }
        bar.barColor = retBarColor;
        // console.log(bar.barColor);

        // yAxisLocation
        let retYAxisLocation = getStringArrayFromInput(
            "yAxisLocation",
            yamlBar?.yAxisLocation,
            numDatasets,
            "left",
            validateYAxisLocation,
            true
        );
        if (typeof retYAxisLocation === "string") {
            return retYAxisLocation; // errorMessage
        }
        bar.yAxisLocation = retYAxisLocation;
        // console.log(bar.yAxisLocation);

        renderInfo.bar.push(bar);
    } // bar related parameters
    // console.log(renderInfo.bar);

    // pie related parameters
    for (let pieKey of yamlPieKeys) {
        let pie = new PieInfo();
        let yamlPie = yaml[pieKey];

        let keysOfPieInfo = getAvailableKeysOfClass(pie);
        let keysFoundInYAML = getAvailableKeysOfClass(yamlPie);
        // console.log(keysOfPieInfo);
        // console.log(keysFoundInYAML);
        for (let key of keysFoundInYAML) {
            if (!keysOfPieInfo.includes(key)) {
                errorMessage = "'" + key + "' is not an available key";
                return errorMessage;
            }
        }

        // title
        pie.title = getStringFromInput(yamlPie?.title, pie.title);
        // console.log(pie.title);

        // data
        let retData = getStringArray("data", yamlPie?.data);
        if (typeof retData === "string") {
            return retData;
        }
        pie.data = retData;
        // console.log(pie.data);
        let numData = pie.data.length;

        // dataColor
        let retDataColor = getStringArrayFromInput(
            "dataColor",
            yamlPie?.dataColor,
            numData,
            null,
            validateColor,
            true
        );
        if (typeof retDataColor === "string") {
            return retDataColor; // errorMessage
        }
        pie.dataColor = retDataColor;
        // console.log(pie.dataColor);

        // dataName
        let retDataName = getStringArrayFromInput(
            "dataName",
            yamlPie?.dataName,
            numData,
            "",
            null,
            true
        );
        if (typeof retDataName === "string") {
            return retDataName; // errorMessage
        }
        pie.dataName = retDataName;
        // console.log(pie.dataName);

        // label
        let retLabel = getStringArrayFromInput(
            "label",
            yamlPie?.label,
            numData,
            "",
            null,
            true
        );
        if (typeof retLabel === "string") {
            return retLabel; // errorMessage
        }
        pie.label = retLabel;
        // console.log(pie.label);

        // hideLabelLessThan
        if (typeof yamlPie?.hideLabelLessThan === "number") {
            pie.hideLabelLessThan = yamlPie.hideLabelLessThan;
        }
        // console.log(pie.hideLabelLessThan);

        // extLabel
        let retExtLabel = getStringArrayFromInput(
            "extLabel",
            yamlPie?.extLabel,
            numData,
            "",
            null,
            true
        );
        if (typeof retExtLabel === "string") {
            return retExtLabel; // errorMessage
        }
        pie.extLabel = retExtLabel;
        // console.log(pie.extLabel);

        // showExtLabelOnlyIfNoLabel
        if (typeof yamlPie?.showExtLabelOnlyIfNoLabel === "boolean") {
            pie.showExtLabelOnlyIfNoLabel = yamlPie.showExtLabelOnlyIfNoLabel;
        }
        // console.log(pie.showExtLabelOnlyIfNoLabel);

        // ratioInnerRadius
        if (typeof yamlPie?.ratioInnerRadius === "number") {
            pie.ratioInnerRadius = yamlPie.ratioInnerRadius;
        }
        // console.log(pie.ratioInnerRadius);

        // showLegend
        if (typeof yamlPie?.showLegend === "boolean") {
            pie.showLegend = yamlPie.showLegend;
        }

        // legendPosition
        pie.legendPosition = getStringFromInput(
            yamlPie?.legendPosition,
            "right"
        );

        // legendOrient
        let defaultLegendOrientation = "horizontal";
        if (pie.legendPosition === "top" || pie.legendPosition === "bottom") {
            defaultLegendOrientation = "horizontal";
        } else if (
            pie.legendPosition === "left" ||
            pie.legendPosition === "right"
        ) {
            defaultLegendOrientation = "vertical";
        } else {
            defaultLegendOrientation = "horizontal";
        }
        pie.legendOrientation = getStringFromInput(
            yamlPie?.legendOrientation,
            defaultLegendOrientation
        );
        // console.log(pie.legendPosition);
        // console.log(pie.legendOrientation);

        // legendBgColor
        pie.legendBgColor = getStringFromInput(
            yamlPie?.legendBgColor,
            pie.legendBgColor
        );

        // legendBorderColor
        pie.legendBorderColor = getStringFromInput(
            yamlPie?.legendBorderColor,
            pie.legendBorderColor
        );

        renderInfo.pie.push(pie);
    } // pie related parameters
    // console.log(renderInfo.pie);

    // summary related parameters
    for (let summaryKey of yamlSummaryKeys) {
        let summary = new SummaryInfo();
        let yamlSummary = yaml[summaryKey];

        let keysOfSummaryInfo = getAvailableKeysOfClass(summary);
        let keysFoundInYAML = getAvailableKeysOfClass(yamlSummary);
        // console.log(keysOfSummaryInfo);
        // console.log(keysFoundInYAML);
        for (let key of keysFoundInYAML) {
            if (!keysOfSummaryInfo.includes(key)) {
                errorMessage = "'" + key + "' is not an available key";
                return errorMessage;
            }
        }

        // template
        summary.template = getStringFromInput(
            yamlSummary?.template,
            summary.template
        );

        // style
        summary.style = getStringFromInput(yamlSummary?.style, summary.style);

        renderInfo.summary.push(summary);
    } // summary related parameters

    // Month related parameters
    for (let monthKey of yamlMonthKeys) {
        let month = new MonthInfo();
        let yamlMonth = yaml[monthKey];

        let keysOfMonthInfo = getAvailableKeysOfClass(month);
        let keysFoundInYAML = getAvailableKeysOfClass(yamlMonth);
        // console.log(keysOfSummaryInfo);
        // console.log(keysFoundInYAML);
        for (let key of keysFoundInYAML) {
            if (!keysOfMonthInfo.includes(key)) {
                errorMessage = "'" + key + "' is not an available key";
                return errorMessage;
            }
        }

        // mode
        month.mode = getStringFromInput(yamlMonth?.mode, month.mode);
        // console.log(month.mode);

        // dataset
        let retDataset = getNumberArray("dataset", yamlMonth?.dataset);
        if (typeof retDataset === "string") {
            return retDataset;
        }
        if (retDataset.length === 0) {
            // insert y dataset given
            for (let q of queries) {
                retDataset.push(q.getId());
            }
        }
        month.dataset = retDataset;
        // console.log(month.dataset);
        let numDataset = month.dataset.length;

        // startWeekOn
        month.startWeekOn = getStringFromInput(
            yamlMonth?.startWeekOn,
            month.startWeekOn
        );
        // console.log(month.startWeekOn);

        // showCircle
        if (typeof yamlMonth?.showCircle === "boolean") {
            month.showCircle = yamlMonth.showCircle;
        }
        // console.log(month.showCircle);

        // threshold
        let retThreshold = getNumberArray("threshold", yamlMonth?.threshold);
        if (typeof retThreshold === "string") {
            return retThreshold;
        }
        month.threshold = retThreshold;
        if (month.threshold.length === 0) {
            for (let indDataset = 0; indDataset < numDataset; indDataset++) {
                month.threshold.push(0);
            }
        }
        if (month.threshold.length !== month.dataset.length) {
            // console.log(month.threshold);
            // console.log(month.dataset);
            const errorMessage =
                "The number of inputs of threshold and dataset not matched";
            return errorMessage;
        }
        // console.log(month.threshold);

        // yMin
        let retYMin = getNumberArray("yMin", yamlMonth?.yMin);
        if (typeof retYMin === "string") {
            return retYMin;
        }
        month.yMin = retYMin;
        if (month.yMin.length === 0) {
            for (let indDataset = 0; indDataset < numDataset; indDataset++) {
                month.yMin.push(null);
            }
        }
        if (month.yMin.length !== month.dataset.length) {
            const errorMessage =
                "The number of inputs of yMin and dataset not matched";
            return errorMessage;
        }
        // console.log(month.yMin);

        // yMax
        let retYMax = getNumberArray("yMax", yamlMonth?.yMax);
        if (typeof retYMax === "string") {
            return retYMax;
        }
        month.yMax = retYMax;
        if (month.yMax.length === 0) {
            for (let indDataset = 0; indDataset < numDataset; indDataset++) {
                month.yMax.push(null);
            }
        }
        if (month.yMax.length !== month.dataset.length) {
            const errorMessage =
                "The number of inputs of yMin and dataset not matched";
            return errorMessage;
        }
        // console.log(month.yMax);

        // color
        month.color = getStringFromInput(yamlMonth?.color, month.color);
        // console.log(month.color);

        // dimNotInMonth
        if (typeof yamlMonth?.dimNotInMonth === "boolean") {
            month.dimNotInMonth = yamlMonth.dimNotInMonth;
        }
        // console.log(month.dimNotInMonth);

        // showStreak
        if (typeof yamlMonth?.showStreak === "boolean") {
            month.showStreak = yamlMonth.showStreak;
        }
        // console.log(month.showStreak);

        // showTodayRing
        if (typeof yamlMonth?.showTodayRing === "boolean") {
            month.showTodayRing = yamlMonth.showTodayRing;
        }
        // console.log(month.showTodayRing);

        // showSelectedValue
        if (typeof yamlMonth?.showSelectedValue === "boolean") {
            month.showSelectedValue = yamlMonth.showSelectedValue;
        }
        // console.log(month.showSelectedValue);

        // showSelectedRing
        if (typeof yamlMonth?.showSelectedRing === "boolean") {
            month.showSelectedRing = yamlMonth.showSelectedRing;
        }
        // console.log(month.showSelectedRing);

        // circleColor
        month.circleColor = getStringFromInput(
            yamlMonth?.circleColor,
            month.circleColor
        );
        // console.log(month.circleColor);

        // circleColorByValue
        if (typeof yamlMonth?.circleColorByValue === "boolean") {
            month.circleColorByValue = yamlMonth.circleColorByValue;
        }
        // console.log(month.circleColorByValue);

        // headerYearColor
        month.headerYearColor = getStringFromInput(
            yamlMonth?.headerYearColor,
            month.headerYearColor
        );
        // console.log(month.headerYearColor);

        // headerMonthColor
        month.headerMonthColor = getStringFromInput(
            yamlMonth?.headerMonthColor,
            month.headerMonthColor
        );
        // console.log(month.headerMonthColor);

        // dividingLineColor
        month.dividingLineColor = getStringFromInput(
            yamlMonth?.dividingLineColor,
            month.dividingLineColor
        );
        // console.log(month.dividingLineColor);

        // todayRingColor
        month.todayRingColor = getStringFromInput(
            yamlMonth?.todayRingColor,
            month.todayRingColor
        );
        // console.log(month.todayRingColor);

        // selectedRingColor
        month.selectedRingColor = getStringFromInput(
            yamlMonth?.selectedRingColor,
            month.selectedRingColor
        );
        // console.log(month.selectedRingColor);

        // initMonth
        month.initMonth = getStringFromInput(
            yamlMonth?.initMonth,
            month.initMonth
        );
        // console.log(month.initMonth);

        // showAnnotation
        if (typeof yamlMonth?.showAnnotation === "boolean") {
            month.showAnnotation = yamlMonth.showAnnotation;
        }
        // console.log(month.showAnnotation);

        // annotation
        let retAnnotation = getStringArray("annotation", yamlMonth?.annotation);
        if (typeof retAnnotation === "string") {
            return retAnnotation;
        }
        month.annotation = retAnnotation;
        if (month.annotation.length === 0) {
            for (let indDataset = 0; indDataset < numDataset; indDataset++) {
                month.annotation.push(null);
            }
        }
        if (month.annotation.length !== month.dataset.length) {
            const errorMessage =
                "The number of inputs of annotation and dataset not matched";
            return errorMessage;
        }
        // console.log(month.annotation);

        // showAnnotationOfAllTargets
        if (typeof yamlMonth?.showAnnotationOfAllTargets === "boolean") {
            month.showAnnotationOfAllTargets =
                yamlMonth.showAnnotationOfAllTargets;
        }
        // console.log(month.showAnnotationOfAllTargets);

        renderInfo.month.push(month);
    } // Month related parameters
    // console.log(renderInfo.month);

    // Heatmap related parameters
    for (let heatmapKey of yamlHeatmapKeys) {
        let heatmap = new HeatmapInfo();
        let yamlHeatmap = yaml[heatmapKey];

        let keysOfHeatmapInfo = getAvailableKeysOfClass(heatmap);
        let keysFoundInYAML = getAvailableKeysOfClass(yamlHeatmap);
        // console.log(keysOfHeatmapInfo);
        // console.log(keysFoundInYAML);
        for (let key of keysFoundInYAML) {
            if (!keysOfHeatmapInfo.includes(key)) {
                errorMessage = "'" + key + "' is not an available key";
                return errorMessage;
            }
        }

        renderInfo.heatmap.push(heatmap);
    }
    // console.log(renderInfo.heatmap);

    // Bullet related parameters
    for (let bulletKey of yamlBulletKeys) {
        let bullet = new BulletInfo();
        let yamlBullet = yaml[bulletKey];

        let keysOfBulletInfo = getAvailableKeysOfClass(bullet);
        let keysFoundInYAML = getAvailableKeysOfClass(yamlBullet);
        // console.log(keysOfSummaryInfo);
        // console.log(keysFoundInYAML);
        for (let key of keysFoundInYAML) {
            if (!keysOfBulletInfo.includes(key)) {
                errorMessage = "'" + key + "' is not an available key";
                return errorMessage;
            }
        }

        // title
        bullet.title = getStringFromInput(yamlBullet?.title, bullet.title);
        // console.log(bullet.title);

        // dataset
        bullet.dataset = getStringFromInput(
            yamlBullet?.dataset,
            bullet.dataset
        );
        // console.log(bullet.dataset);

        // orientation
        bullet.orientation = getStringFromInput(
            yamlBullet?.orientation,
            bullet.orientation
        );
        // console.log(bullet.orientation);

        // range
        let retRange = getNumberArray("range", yamlBullet?.range);
        if (typeof retRange === "string") {
            return retRange;
        }
        let range = retRange as Array<number>;
        // Check the value is monotonically increasing
        // Check the value is not negative
        if (range.length === 1) {
            if (range[0] < 0) {
                errorMessage = "Negative range value is not allowed";
                return errorMessage;
            }
        } else if (range.length > 1) {
            let lastBound = range[0];
            if (lastBound < 0) {
                errorMessage = "Negative range value is not allowed";
                return errorMessage;
            } else {
                for (let ind = 1; ind < range.length; ind++) {
                    if (range[ind] <= lastBound) {
                        errorMessage =
                            "Values in parameter 'range' should be monotonically increasing";
                        return errorMessage;
                    }
                }
            }
        } else {
            errorMessage = "Empty range is not allowed";
            return errorMessage;
        }
        bullet.range = range;
        let numRange = range.length;
        // console.log(renderInfo.bullet.range);

        // range color
        let retRangeColor = getStringArrayFromInput(
            "rangeColor",
            yamlBullet?.rangeColor,
            numRange,
            "",
            validateColor,
            true
        );
        if (typeof retRangeColor === "string") {
            return retRangeColor; // errorMessage
        }
        bullet.rangeColor = retRangeColor;
        // console.log(bullet.rangeColor);

        // actual value, can possess template variable
        bullet.value = getStringFromInput(yamlBullet?.value, bullet.value);
        // console.log(bullet.value);

        // value unit
        bullet.valueUnit = getStringFromInput(
            yamlBullet?.valueUnit,
            bullet.valueUnit
        );
        // console.log(bullet.valueUnit);

        // value color
        bullet.valueColor = getStringFromInput(
            yamlBullet?.valueColor,
            bullet.valueColor
        );
        // console.log(bullet.valueColor);

        // show mark
        if (typeof yamlBullet?.showMarker === "boolean") {
            bullet.showMarker = yamlBullet.showMarker;
        }
        // console.log(bullet.showMark);

        // mark value
        if (typeof yamlBullet?.markerValue === "number") {
            bullet.markerValue = yamlBullet.markerValue;
        }
        // console.log(bullet.markValue);

        // mark color
        bullet.markerColor = getStringFromInput(
            yamlBullet?.markerColor,
            bullet.markerColor
        );
        // console.log(bullet.markValue);

        renderInfo.bullet.push(bullet);
    } // Bullet related parameters
    // console.log(renderInfo.bullet);

    return renderInfo;
}
Example #26
Source File: main.ts    From obsidian-readwise with GNU General Public License v3.0 4 votes vote down vote up
async downloadArchive(exportID: number, buttonContext: ButtonComponent): Promise<void> {
    let artifactURL = `${baseURL}/api/download_artifact/${exportID}`;
    if (exportID <= this.settings.lastSavedStatusID) {
      console.log(`Readwise Official plugin: Already saved data from export ${exportID}`);
      this.handleSyncSuccess(buttonContext);
      this.notice("Readwise data is already up to date", false, 4);
      return;
    }

    let response, blob;
    try {
      response = await fetch(
        artifactURL, {headers: this.getAuthHeaders()}
      );
    } catch (e) {
      console.log("Readwise Official plugin: fetch failed in downloadArchive: ", e);
    }
    if (response && response.ok) {
      blob = await response.blob();
    } else {
      console.log("Readwise Official plugin: bad response in downloadArchive: ", response);
      this.handleSyncError(buttonContext, this.getErrorMessageFromResponse(response));
      return;
    }

    this.fs = this.app.vault.adapter;

    const blobReader = new zip.BlobReader(blob);
    const zipReader = new zip.ZipReader(blobReader);
    const entries = await zipReader.getEntries();
    this.notice("Saving files...", false, 30);
    if (entries.length) {
      for (const entry of entries) {
        let bookID: string;
        const processedFileName = normalizePath(entry.filename.replace(/^Readwise/, this.settings.readwiseDir));
        try {
          // ensure the directory exists
          let dirPath = processedFileName.replace(/\/*$/, '').replace(/^(.+)\/[^\/]*?$/, '$1');
          const exists = await this.fs.exists(dirPath);
          if (!exists) {
            await this.fs.mkdir(dirPath);
          }
          // write the actual files
          const contents = await entry.getData(new zip.TextWriter());
          let contentToSave = contents;

          let originalName = processedFileName;
          // extracting book ID from file name
          let split = processedFileName.split("--");
          if (split.length > 1) {
            originalName = split.slice(0, -1).join("--") + ".md";
            bookID = split.last().match(/\d+/g)[0];
            this.settings.booksIDsMap[originalName] = bookID;
          }
          if (await this.fs.exists(originalName)) {
            // if the file already exists we need to append content to existing one
            const existingContent = await this.fs.read(originalName);
            contentToSave = existingContent + contents;
          }
          await this.fs.write(originalName, contentToSave);
          await this.saveSettings();
        } catch (e) {
          console.log(`Readwise Official plugin: error writing ${processedFileName}:`, e);
          this.notice(`Readwise: error while writing ${processedFileName}: ${e}`, true, 4, true);
          if (bookID) {
            this.settings.booksToRefresh.push(bookID);
            await this.saveSettings();
          }
          // communicate with readwise?
        }
      }
    }
    // close the ZipReader
    await zipReader.close();
    await this.acknowledgeSyncCompleted(buttonContext);
    this.handleSyncSuccess(buttonContext, "Synced!", exportID);
    this.notice("Readwise sync completed", true, 1, true);
    // @ts-ignore
    if (this.app.isMobile) {
      this.notice("If you don't see all of your readwise files reload obsidian app", true,);
    }
  }
Example #27
Source File: main.ts    From obsidian-readwise with GNU General Public License v3.0 4 votes vote down vote up
display(): void {
    let {containerEl} = this;

    containerEl.empty();
    containerEl.createEl('h1', {text: 'Readwise Official'});
    containerEl.createEl('p', {text: 'Created by '}).createEl('a', {text: 'Readwise', href: 'https://readwise.io'});
    containerEl.getElementsByTagName('p')[0].appendText(' ?');
    containerEl.createEl('h2', {text: 'Settings'});

    if (this.plugin.settings.token) {
      new Setting(containerEl)
        .setName("Sync your Readwise data with Obsidian")
        .setDesc("On first sync, the Readwise plugin will create a new folder containing all your highlights")
        .setClass('rw-setting-sync')
        .addButton((button) => {
          button.setCta().setTooltip("Once the sync begins, you can close this plugin page")
            .setButtonText('Initiate Sync')
            .onClick(async () => {
              if (this.plugin.settings.isSyncing) {
                // NOTE: This is used to prevent multiple syncs at the same time. However, if a previous sync fails,
                //  it can stop new syncs from happening. Make sure to set isSyncing to false
                //  if there's ever errors/failures in previous sync attempts, so that
                //  we don't block syncing subsequent times.
                new Notice("Readwise sync already in progress");
              } else {
                this.plugin.clearInfoStatus(containerEl);
                this.plugin.settings.isSyncing = true;
                await this.plugin.saveData(this.plugin.settings);
                button.setButtonText("Syncing...");
                await this.plugin.requestArchive(button);
              }

            });
        });
      let el = containerEl.createEl("div", {cls: "rw-info-container"});
      containerEl.find(".rw-setting-sync > .setting-item-control ").prepend(el);

      new Setting(containerEl)
        .setName("Customize formatting options")
        .setDesc("You can customize which items export to Obsidian and how they appear from the Readwise website")
        .addButton((button) => {
          button.setButtonText("Customize").onClick(() => {
            window.open(`${baseURL}/export/obsidian/preferences`);
          });
        });

      new Setting(containerEl)
        .setName('Customize base folder')
        .setDesc("By default, the plugin will save all your highlights into a folder named Readwise")
        // TODO: change this to search filed when the API is exposed (https://github.com/obsidianmd/obsidian-api/issues/22)
        .addText(text => text
          .setPlaceholder('Defaults to: Readwise')
          .setValue(this.plugin.settings.readwiseDir)
          .onChange(async (value) => {
            this.plugin.settings.readwiseDir = normalizePath(value || "Readwise");
            await this.plugin.saveSettings();
          }));

      new Setting(containerEl)
        .setName('Configure resync frequency')
        .setDesc("If not set to Manual, Readwise will automatically resync with Obsidian when the app is open at the specified interval")
        .addDropdown(dropdown => {
          dropdown.addOption("0", "Manual");
          dropdown.addOption("60", "Every 1 hour");
          dropdown.addOption((12 * 60).toString(), "Every 12 hours");
          dropdown.addOption((24 * 60).toString(), "Every 24 hours");

          // select the currently-saved option
          dropdown.setValue(this.plugin.settings.frequency);

          dropdown.onChange((newValue) => {
            // update the plugin settings
            this.plugin.settings.frequency = newValue;
            this.plugin.saveSettings();

            // destroy & re-create the scheduled task
            this.plugin.configureSchedule();
          });
        });
      new Setting(containerEl)
        .setName("Sync automatically when Obsidian opens")
        .setDesc("If enabled, Readwise will automatically resync with Obsidian each time you open the app")
        .addToggle((toggle) => {
            toggle.setValue(this.plugin.settings.triggerOnLoad);
            toggle.onChange((val) => {
              this.plugin.settings.triggerOnLoad = val;
              this.plugin.saveSettings();
            });
          }
        );
      new Setting(containerEl)
        .setName("Resync deleted files")
        .setDesc("If enabled, you can refresh individual items by deleting the file in Obsidian and initiating a resync")
        .addToggle((toggle) => {
            toggle.setValue(this.plugin.settings.refreshBooks);
            toggle.onChange(async (val) => {
              this.plugin.settings.refreshBooks = val;
              await this.plugin.saveSettings();
              if (val) {
                this.plugin.refreshBookExport();
              }
            });
          }
        );

      if (this.plugin.settings.lastSyncFailed) {
        this.plugin.showInfoStatus(containerEl.find(".rw-setting-sync .rw-info-container").parentElement, "Last sync failed", "rw-error");
      }
    }
    if (!this.plugin.settings.token) {
      new Setting(containerEl)
        .setName("Connect Obsidian to Readwise")
        .setClass("rw-setting-connect")
        .setDesc("The Readwise plugin enables automatic syncing of all your highlights from Kindle, Instapaper, Pocket, and more. Note: Requires Readwise account.")
        .addButton((button) => {
          button.setButtonText("Connect").setCta().onClick(async (evt) => {
            const success = await this.plugin.getUserAuthToken(evt.target as HTMLElement);
            if (success) {
              this.display();
            }
          });
        });
      let el = containerEl.createEl("div", {cls: "rw-info-container"});
      containerEl.find(".rw-setting-connect > .setting-item-control ").prepend(el);
    }
    const help = containerEl.createEl('p',);
    help.innerHTML = "Question? Please see our <a href='https://help.readwise.io/article/125-how-does-the-readwise-to-obsidian-export-integration-work'>Documentation</a> or email us at <a href='mailto:[email protected]'>[email protected]</a> ?";
  }