From 54293ac3a1b51ff79ec3da219d7cd4ccafc56a80 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Thu, 11 Apr 2024 22:30:55 +0200 Subject: [PATCH] fix some issues; migrate to bun' --- .github/workflows/release.yml | 13 +- automation/build/buildBanner.ts | 38 ++++ .../build/esbuild.config.ts | 29 ++- automation/build/esbuild.dev.config.ts | 65 +++++++ automation/config.json | 5 + automation/release.ts | 171 ++++++++++++++++++ automation/stats.ts | 160 ++++++++++++++++ automation/tsconfig.json | 20 ++ automation/utils/shellUtils.ts | 168 +++++++++++++++++ automation/utils/utils.ts | 6 + automation/utils/versionUtils.ts | 108 +++++++++++ bun.lockb | Bin 0 -> 167843 bytes esbuild.dev.config.mjs | 63 ------- exampleVault/.obsidian/app.json | 1 + exampleVault/.obsidian/appearance.json | 4 + exampleVault/.obsidian/community-plugins.json | 1 + .../.obsidian/core-plugins-migration.json | 31 ++++ exampleVault/.obsidian/core-plugins.json | 20 ++ exampleVault/.obsidian/hotkeys.json | 8 + .../obsidian-media-db-plugin/.hotreload | 0 .../obsidian-media-db-plugin/manifest.json | 10 + .../obsidian-media-db-plugin/styles.css | 126 +++++++++++++ exampleVault/.obsidian/workspace.json | 151 ++++++++++++++++ exampleVault/index.md | 0 jest.config.js | 7 - manifest-beta.json | 10 + manifest.json | 4 +- package.json | 54 ++++-- src/api/APIManager.ts | 8 +- src/api/APIModel.ts | 6 +- src/api/apis/LocGovAPI.ts | 64 ------- src/api/apis/SteamAPI.ts | 8 +- src/main.ts | 105 ++++++----- src/models/MediaTypeModel.ts | 6 +- src/models/WikiModel.ts | 6 +- src/settings/Icon.svelte | 34 ++-- src/settings/PropertyMapper.ts | 14 +- src/settings/PropertyMapping.ts | 2 +- .../PropertyMappingModelComponent.svelte | 44 ++--- .../PropertyMappingModelsComponent.svelte | 15 +- src/settings/Settings.ts | 115 ++++++------ src/settings/suggesters/Suggest.ts | 2 +- src/tests/ParameterizedAPI.test.ts | 104 ----------- src/tests/ResponseMocks/MALMockMovie.json | 30 --- .../MusicBrainzMockResponse.json | 25 --- src/tests/ResponseMocks/OMDBMockResponse.json | 13 -- .../ResponseMocks/SteamAPIMockResponse.json | 14 -- .../ResponseMocks/WikipediaMockResponse.json | 20 -- src/tests/TestAPI.ts | 25 --- src/tests/mockHelpers.ts | 95 ---------- src/tests/utils.test.ts | 29 --- src/utils/MediaTypeManager.ts | 2 +- src/utils/ModalHelper.ts | 1 - src/utils/Utils.ts | 10 +- src/utils/YAMLConverter.ts | 43 ----- tsconfig.json | 11 +- 56 files changed, 1360 insertions(+), 764 deletions(-) create mode 100644 automation/build/buildBanner.ts rename esbuild.config.mjs => automation/build/esbuild.config.ts (61%) create mode 100644 automation/build/esbuild.dev.config.ts create mode 100644 automation/config.json create mode 100644 automation/release.ts create mode 100644 automation/stats.ts create mode 100644 automation/tsconfig.json create mode 100644 automation/utils/shellUtils.ts create mode 100644 automation/utils/utils.ts create mode 100644 automation/utils/versionUtils.ts create mode 100755 bun.lockb delete mode 100644 esbuild.dev.config.mjs create mode 100644 exampleVault/.obsidian/app.json create mode 100644 exampleVault/.obsidian/appearance.json create mode 100644 exampleVault/.obsidian/community-plugins.json create mode 100644 exampleVault/.obsidian/core-plugins-migration.json create mode 100644 exampleVault/.obsidian/core-plugins.json create mode 100644 exampleVault/.obsidian/hotkeys.json create mode 100644 exampleVault/.obsidian/plugins/obsidian-media-db-plugin/.hotreload create mode 100644 exampleVault/.obsidian/plugins/obsidian-media-db-plugin/manifest.json create mode 100644 exampleVault/.obsidian/plugins/obsidian-media-db-plugin/styles.css create mode 100644 exampleVault/.obsidian/workspace.json create mode 100644 exampleVault/index.md delete mode 100644 jest.config.js create mode 100644 manifest-beta.json delete mode 100644 src/api/apis/LocGovAPI.ts delete mode 100644 src/tests/ParameterizedAPI.test.ts delete mode 100644 src/tests/ResponseMocks/MALMockMovie.json delete mode 100644 src/tests/ResponseMocks/MusicBrainzMockResponse.json delete mode 100644 src/tests/ResponseMocks/OMDBMockResponse.json delete mode 100644 src/tests/ResponseMocks/SteamAPIMockResponse.json delete mode 100644 src/tests/ResponseMocks/WikipediaMockResponse.json delete mode 100644 src/tests/TestAPI.ts delete mode 100644 src/tests/mockHelpers.ts delete mode 100644 src/tests/utils.test.ts delete mode 100644 src/utils/YAMLConverter.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc02e9a..de18c0f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,6 @@ -name: Build obsidian plugin +name: Build Obsidian Plugin + +# adapted from https://github.com/argenos/nldates-obsidian/blob/master/.github/workflows/release.yml on: push: @@ -15,15 +17,14 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Use Node.js - uses: actions/setup-node@v3 + - uses: oven-sh/setup-bun@v1 with: - node-version: '16.x' # You might need to adjust this value to your own version + bun-version: latest - name: Build id: build run: | - yarn - yarn run build --if-present + bun install + bun run build mkdir ${{ env.PLUGIN_NAME }} cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} diff --git a/automation/build/buildBanner.ts b/automation/build/buildBanner.ts new file mode 100644 index 0000000..b8a62cf --- /dev/null +++ b/automation/build/buildBanner.ts @@ -0,0 +1,38 @@ +import manifest from '../../manifest.json' assert { type: 'json' }; + +export function getBuildBanner(buildType: string, getVersion: (version: string) => string) { + return `/* +------------------------------------------- +${manifest.name} - ${buildType} +------------------------------------------- +By: ${manifest.author} (${manifest.authorUrl}) +Time: ${new Date().toUTCString()} +Version: ${getVersion(manifest.version)} +------------------------------------------- +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +------------------------------------------- +MIT License + +Copyright (c) ${new Date().getFullYear()} ${manifest.author} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +`; +} diff --git a/esbuild.config.mjs b/automation/build/esbuild.config.ts similarity index 61% rename from esbuild.config.mjs rename to automation/build/esbuild.config.ts index 1ef8bd7..91c5fb9 100644 --- a/esbuild.config.mjs +++ b/automation/build/esbuild.config.ts @@ -1,21 +1,10 @@ -import esbuild from 'esbuild'; import builtins from 'builtin-modules'; +import esbuild from 'esbuild'; import esbuildSvelte from 'esbuild-svelte'; import sveltePreprocess from 'svelte-preprocess'; -import manifest from './manifest.json' assert { type: 'json' }; +import { getBuildBanner } from 'build/buildBanner'; -const banner = `/* -------------------------------------------- -${manifest.name} - Release Build -------------------------------------------- -By: ${manifest.author} (${manifest.authorUrl}) -Time: ${new Date().toUTCString()} -Version: ${manifest.version} -------------------------------------------- -THIS IS A GENERATED/BUNDLED FILE BY ESBUILD -if you want to view the source, please visit the github repository of this plugin -*/ -`; +const banner = getBuildBanner('Release Build', version => version); const build = await esbuild.build({ banner: { @@ -47,14 +36,22 @@ const build = await esbuild.build({ outfile: 'main.js', minify: true, metafile: true, + define: { + MB_GLOBAL_CONFIG_DEV_BUILD: 'false', + }, plugins: [ esbuildSvelte({ - compilerOptions: { css: 'injected' }, + compilerOptions: { css: 'injected', dev: false, sveltePath: 'svelte' }, preprocess: sveltePreprocess(), filterWarnings: warning => { // we don't want warnings from node modules that we can do nothing about - return !warning.filename.includes('node_modules'); + return !warning.filename?.includes('node_modules'); }, }), ], }); + +const file = Bun.file('meta.txt'); +await Bun.write(file, JSON.stringify(build.metafile, null, '\t')); + +process.exit(0); diff --git a/automation/build/esbuild.dev.config.ts b/automation/build/esbuild.dev.config.ts new file mode 100644 index 0000000..6b96549 --- /dev/null +++ b/automation/build/esbuild.dev.config.ts @@ -0,0 +1,65 @@ +import esbuild from 'esbuild'; +import copy from 'esbuild-plugin-copy-watch'; +import esbuildSvelte from 'esbuild-svelte'; +import sveltePreprocess from 'svelte-preprocess'; +import manifest from '../../manifest.json' assert { type: 'json' }; +import { getBuildBanner } from 'build/buildBanner'; + +const banner = getBuildBanner('Dev Build', _ => 'Dev Build'); + +const context = await esbuild.context({ + banner: { + js: banner, + }, + entryPoints: ['src/main.ts'], + bundle: true, + external: [ + 'obsidian', + 'electron', + '@codemirror/autocomplete', + '@codemirror/collab', + '@codemirror/commands', + '@codemirror/language', + '@codemirror/lint', + '@codemirror/search', + '@codemirror/state', + '@codemirror/view', + '@lezer/common', + '@lezer/highlight', + '@lezer/lr', + ], + format: 'cjs', + target: 'es2018', + logLevel: 'info', + sourcemap: 'inline', + treeShaking: true, + outdir: `exampleVault/.obsidian/plugins/${manifest.id}/`, + outbase: 'src', + define: { + MB_GLOBAL_CONFIG_DEV_BUILD: 'true', + }, + plugins: [ + copy({ + paths: [ + { + from: './styles.css', + to: '', + }, + { + from: './manifest.json', + to: '', + }, + ], + }), + esbuildSvelte({ + compilerOptions: { css: 'injected', dev: true, sveltePath: 'svelte' }, + preprocess: sveltePreprocess(), + filterWarnings: warning => { + // we don't want warnings from node modules that we can do nothing about + return !warning.filename?.includes('node_modules'); + }, + }), + ], +}); + +await context.watch(); diff --git a/automation/config.json b/automation/config.json new file mode 100644 index 0000000..c9a8947 --- /dev/null +++ b/automation/config.json @@ -0,0 +1,5 @@ +{ + "devBranch": "master", + "releaseBranch": "release", + "github": "https://github.com/mProjectsCode/obsidian-media-db-plugin" +} diff --git a/automation/release.ts b/automation/release.ts new file mode 100644 index 0000000..29203ba --- /dev/null +++ b/automation/release.ts @@ -0,0 +1,171 @@ +import { UserError } from 'utils/utils'; +import { CanaryVersion, Version, getIncrementOptions, parseVersion, stringifyVersion } from 'utils/versionUtils'; +import config from './config.json'; +import { $choice as $choice, $confirm, $seq, CMD_FMT, Verboseness } from 'utils/shellUtils'; + +async function runPreconditions(): Promise { + // run preconditions + await $seq( + [`bun run format`, `bun run lint:fix`, `bun run test`], + (cmd: string) => { + throw new UserError(`precondition "${cmd}" failed`); + }, + () => {}, + undefined, + Verboseness.VERBOSE, + ); + + // add changed files + await $seq( + [`git add .`], + () => { + throw new UserError('failed to add preconditions changes to git'); + }, + () => {}, + undefined, + Verboseness.NORMAL, + ); + + // check if there were any changes + let changesToCommit = false; + await $seq( + [`git diff --quiet`, `git diff --cached --quiet`], + () => { + changesToCommit = true; + }, + () => {}, + undefined, + Verboseness.QUITET, + ); + + // if there were any changes, commit them + if (changesToCommit) { + await $seq( + [`git commit -m "[auto] run release preconditions"`], + () => { + throw new UserError('failed to add preconditions changes to git'); + }, + () => {}, + undefined, + Verboseness.NORMAL, + ); + } +} + +async function run() { + console.log('looking for untracked changes ...'); + + // check for any uncommited files and exit if there are any + await $seq( + [`git add .`, `git diff --quiet`, `git diff --cached --quiet`, `git checkout ${config.devBranch}`], + () => { + throw new UserError('there are still untracked changes'); + }, + () => {}, + undefined, + Verboseness.QUITET, + ); + + console.log('\nrunning preconditions ...\n'); + + await runPreconditions(); + + console.log('\nbumping versions ...\n'); + + const manifestFile = Bun.file('./manifest.json'); + const manifest = await manifestFile.json(); + + const versionString: string = manifest.version; + const currentVersion: Version = parseVersion(versionString); + const currentVersionString = stringifyVersion(currentVersion); + + const versionIncrementOptions = getIncrementOptions(currentVersion); + + const selectedIndex = await $choice( + `Current version "${currentVersionString}". Select new version`, + versionIncrementOptions.map(x => stringifyVersion(x)), + ); + const newVersion = versionIncrementOptions[selectedIndex]; + const newVersionString = stringifyVersion(newVersion); + + console.log(''); + + await $confirm(`Version will be updated "${currentVersionString}" -> "${newVersionString}". Are you sure`, () => { + throw new UserError('user canceled script'); + }); + + if (!(newVersion instanceof CanaryVersion)) { + manifest.version = newVersionString; + } + + await Bun.write(manifestFile, JSON.stringify(manifest, null, '\t')); + + const betaManifest = structuredClone(manifest); + betaManifest.version = newVersionString; + + const betaManifestFile = Bun.file('./manifest-beta.json'); + await Bun.write(betaManifestFile, JSON.stringify(betaManifest, null, '\t')); + + if (!(newVersion instanceof CanaryVersion)) { + const versionsFile = Bun.file('./versions.json'); + const versionsJson = await versionsFile.json(); + + versionsJson[newVersionString] = manifest.minAppVersion; + + await Bun.write(versionsFile, JSON.stringify(versionsJson, null, '\t')); + + const packageFile = Bun.file('./package.json'); + const packageJson = await packageFile.json(); + + packageJson.version = newVersionString; + + await Bun.write(packageFile, JSON.stringify(packageJson, null, '\t')); + } + + await $seq( + [`bun run format`, `git add .`, `git commit -m "[auto] bump version to \`${newVersionString}\`"`], + () => { + throw new UserError('failed to add preconditions changes to git'); + }, + () => {}, + undefined, + Verboseness.NORMAL, + ); + + console.log('\ncreating release tag ...\n'); + + await $seq( + [ + `git checkout ${config.releaseBranch}`, + `git merge ${config.devBranch} --commit -m "[auto] merge \`${newVersionString}\` release commit"`, + `git push origin ${config.releaseBranch}`, + `git tag -a ${newVersionString} -m "release version ${newVersionString}"`, + `git push origin ${newVersionString}`, + `git checkout ${config.devBranch}`, + `git merge ${config.releaseBranch}`, + `git push origin ${config.devBranch}`, + ], + () => { + throw new UserError('failed to merge or create tag'); + }, + () => {}, + undefined, + Verboseness.NORMAL, + ); + + console.log(''); + + console.log(`${CMD_FMT.BgGreen}done${CMD_FMT.Reset}`); + console.log(`${config.github}`); + console.log(`${config.github}/releases/tag/${newVersionString}`); +} + +try { + await run(); +} catch (e) { + if (e instanceof UserError) { + console.error(e.message); + } else { + console.error(e); + } +} diff --git a/automation/stats.ts b/automation/stats.ts new file mode 100644 index 0000000..2a34c03 --- /dev/null +++ b/automation/stats.ts @@ -0,0 +1,160 @@ +import * as fs from 'fs'; + +interface Stat { + fileType: string; + count: number; + lines: number; +} + +abstract class StatsBase { + parent: StatsBase | undefined; + path: string; + name: string; + stats: Stat[]; + + constructor(parent: StatsBase | undefined, path: string, name: string, stats: Stat[]) { + this.parent = parent; + + this.path = path; + this.name = name; + this.stats = stats; + } + + abstract addChild(child: StatsBase): void; + + abstract mergeStats(stats: Stat[]): void; + + abstract print(depth: number, lastChildArr: boolean[]): void; + + abstract sort(): void; + + getPrefix(depth: number, lastChildArr: boolean[]): string { + let prefix = ''; + for (let i = 0; i < depth; i++) { + prefix += lastChildArr[i] ? ' ' : '│ '; + } + + if (lastChildArr.at(-1)) { + prefix += '└─ '; + } else { + prefix += '├─ '; + } + + return prefix; + } +} + +class FolderStats extends StatsBase { + children: StatsBase[]; + + constructor(parent: StatsBase | undefined, path: string, name: string) { + super(parent, path, name, []); + + this.children = []; + } + + addChild(child: StatsBase) { + this.children.push(child); + this.mergeStats(child.stats); + } + + mergeStats(stats: Stat[]): void { + // console.log(this, stats); + for (const stat of stats) { + const existingStat = this.stats.find(s => s.fileType === stat.fileType); + if (existingStat) { + existingStat.count += stat.count; + existingStat.lines += stat.lines; + } else { + this.stats.push(structuredClone(stat)); + } + } + + this.parent?.mergeStats(stats); + } + + print(depth: number, lastChildArr: boolean[]): void { + console.log( + `${this.getPrefix(depth, lastChildArr)}${this.name} | ${this.stats.reduce((acc, s) => acc + s.count, 0)} files | ${this.stats.reduce((acc, s) => acc + s.lines, 0)} lines`, + ); + for (let i = 0; i < this.children.length; i++) { + const child = this.children[i]; + child.print(depth + 1, [...lastChildArr, i === this.children.length - 1]); + } + } + + sort(): void { + this.children.sort((a, b) => { + if (a instanceof FolderStats && b instanceof FileStats) { + return 1; + } else if (a instanceof FileStats && b instanceof FolderStats) { + return -1; + } else { + return a.name.localeCompare(b.name); + } + }); + this.children.forEach(c => c.sort()); + } +} + +class FileStats extends StatsBase { + constructor(parent: StatsBase, path: string, name: string, stats: Stat[]) { + super(parent, path, name, stats); + } + + addChild(_child: StatsBase): void { + throw new Error('Cannot add child to file'); + } + + mergeStats(_stats: Stat[]): void { + throw new Error('Cannot merge stats to file'); + } + + print(depth: number, lastChildArr: boolean[]): void { + console.log(`${this.getPrefix(depth, lastChildArr)}${this.name} | ${this.stats[0].lines} lines`); + } + + sort(): void {} +} + +function collectStats() { + const root = new FolderStats(undefined, './src', 'src'); + const ignore = ['node_modules', 'extraTypes', 'bun.lockb']; + + const todo: FolderStats[] = [root]; + + while (todo.length > 0) { + const current = todo.pop()!; + const children = fs.readdirSync(current.path, { withFileTypes: true }); + + for (const child of children) { + if (ignore.includes(child.name)) { + continue; + } + + if (child.isDirectory()) { + const folder = new FolderStats(current, `${current.path}/${child.name}`, child.name); + current.addChild(folder); + + todo.push(folder); + } else { + const content = fs.readFileSync(`${current.path}/${child.name}`, 'utf-8'); + + const file = new FileStats(current, `${current.path}/${child.name}`, child.name, [ + { + fileType: child.name.split('.').splice(1).join('.'), + count: 1, + lines: content.split('\n').length, + }, + ]); + current.addChild(file); + } + } + } + + root.sort(); + + root.print(0, [true]); +} + +collectStats(); diff --git a/automation/tsconfig.json b/automation/tsconfig.json new file mode 100644 index 0000000..1fd339b --- /dev/null +++ b/automation/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "module": "ESNext", + "target": "ESNext", + "allowJs": true, + "noImplicitAny": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "lib": ["DOM", "ES5", "ES6", "ES7", "Es2021"], + "types": ["bun-types"], + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true + }, + "include": ["**/*.ts"] +} diff --git a/automation/utils/shellUtils.ts b/automation/utils/shellUtils.ts new file mode 100644 index 0000000..d1c58e0 --- /dev/null +++ b/automation/utils/shellUtils.ts @@ -0,0 +1,168 @@ +import { Subprocess } from 'bun'; +import stringArgv from 'string-argv'; + +export enum Verboseness { + QUITET, + NORMAL, + VERBOSE, +} + +function exec(c: string, cwd?: string): Subprocess<'ignore', 'pipe', 'inherit'> { + return Bun.spawn(stringArgv(c), { cwd: cwd }); +} + +export async function $(cmd: string, cwd?: string | undefined, verboseness: Verboseness = Verboseness.NORMAL): Promise<{ stdout: string; stderr: string; exit: number }> { + if (verboseness === Verboseness.NORMAL || verboseness === Verboseness.VERBOSE) { + if (cwd !== undefined) { + console.log(`\n${CMD_FMT.Bright}running${CMD_FMT.Reset} in ${cwd} - ${cmd}\n`); + } else { + console.log(`\n${CMD_FMT.Bright}running${CMD_FMT.Reset} - ${cmd}\n`); + } + } + + const proc = exec(cmd, cwd); + const stdout = await new Response(proc.stdout).text(); + const stderr = await new Response(proc.stderr).text(); + + if (verboseness === Verboseness.VERBOSE) { + if (stdout !== '') { + console.log( + stdout + .split('\n') + .map(x => `${CMD_FMT.FgGray}>${CMD_FMT.Reset} ${x}\n`) + .join(''), + ); + } + + if (stderr !== '') { + console.log( + stderr + .split('\n') + .map(x => `${CMD_FMT.FgRed}>${CMD_FMT.Reset} ${x}\n`) + .join(''), + ); + } + } + + const exit = await proc.exited; + + if (verboseness === Verboseness.NORMAL || verboseness === Verboseness.VERBOSE) { + if (exit === 0) { + console.log(`${CMD_FMT.FgGreen}success${CMD_FMT.Reset} - ${cmd}\n`); + } else { + console.log(`${CMD_FMT.FgRed}fail${CMD_FMT.Reset} - ${cmd} - code ${exit}\n`); + } + } + + return { + stdout, + stderr, + exit, + }; +} + +export async function $seq( + cmds: string[], + onError: (cmd: string, index: number) => void, + onSuccess: () => void, + cwd?: string | undefined, + verboseness: Verboseness = Verboseness.NORMAL, +): Promise { + const results = []; + for (let i = 0; i < cmds.length; i += 1) { + const cmd = cmds[i]; + const result = await $(cmd, cwd, verboseness); + + if (result.exit !== 0) { + onError(cmd, i); + return; + } + + results.push(result); + } + onSuccess(); +} + +export async function $input(message: string): Promise { + console.write(`${message} `); + const stdin = Bun.stdin.stream(); + const reader = stdin.getReader(); + const chunk = await reader.read(); + reader.releaseLock(); + const text = Buffer.from(chunk.value ?? '').toString(); + return text.trim(); +} + +export async function $choice(message: string, options: string[]): Promise { + console.log(`${message} `); + + let optionNumbers = new Map(); + + for (let i = 0; i < options.length; i++) { + const option = options[i]; + console.log(`[${i}] ${option}`); + + optionNumbers.set(i.toString(), i); + } + + let ret: undefined | number = undefined; + + while (ret === undefined) { + const selectedStr = await $input(`Select [${[...optionNumbers.keys()].join('/')}]:`); + + ret = optionNumbers.get(selectedStr); + + if (ret === undefined) { + console.log(`${CMD_FMT.FgRed}invalid selection, please select a valid option${CMD_FMT.Reset}`); + } + } + + return ret; +} + +export async function $confirm(message: string, onReject: () => void): Promise { + while (true) { + const answer = await $input(`${message} [Y/N]?`); + + if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { + return; + } + + if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') { + onReject(); + return; + } + + console.log(`${CMD_FMT.FgRed}invalid selection, please select a valid option${CMD_FMT.Reset}`); + } +} + +export const CMD_FMT = { + Reset: '\x1b[0m', + Bright: '\x1b[1m', + Dim: '\x1b[2m', + Underscore: '\x1b[4m', + Blink: '\x1b[5m', + Reverse: '\x1b[7m', + Hidden: '\x1b[8m', + + FgBlack: '\x1b[30m', + FgRed: '\x1b[31m', + FgGreen: '\x1b[32m', + FgYellow: '\x1b[33m', + FgBlue: '\x1b[34m', + FgMagenta: '\x1b[35m', + FgCyan: '\x1b[36m', + FgWhite: '\x1b[37m', + FgGray: '\x1b[90m', + + BgBlack: '\x1b[40m', + BgRed: '\x1b[41m', + BgGreen: '\x1b[42m', + BgYellow: '\x1b[43m', + BgBlue: '\x1b[44m', + BgMagenta: '\x1b[45m', + BgCyan: '\x1b[46m', + BgWhite: '\x1b[47m', + BgGray: '\x1b[100m', +}; diff --git a/automation/utils/utils.ts b/automation/utils/utils.ts new file mode 100644 index 0000000..f5d72ee --- /dev/null +++ b/automation/utils/utils.ts @@ -0,0 +1,6 @@ +export class UserError extends Error {} + +export interface ProjectConfig { + corePackages: string[]; + packages: string[]; +} diff --git a/automation/utils/versionUtils.ts b/automation/utils/versionUtils.ts new file mode 100644 index 0000000..62aad22 --- /dev/null +++ b/automation/utils/versionUtils.ts @@ -0,0 +1,108 @@ +import { Parser } from '@lemons_dev/parsinom/lib/Parser'; +import { P_UTILS } from '@lemons_dev/parsinom/lib/ParserUtils'; +import { P } from '@lemons_dev/parsinom/lib/ParsiNOM'; +import Moment from 'moment'; +import { UserError } from 'utils/utils'; + +export class Version { + major: number; + minor: number; + patch: number; + + constructor(major: number, minor: number, patch: number) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + toString(): string { + return `${this.major}.${this.minor}.${this.patch}`; + } +} + +export class CanaryVersion extends Version { + canary: string; + + constructor(major: number, minor: number, patch: number, canary: string) { + super(major, minor, patch); + this.canary = canary; + } + + toString(): string { + return `${super.toString()}-canary.${this.canary}`; + } +} + +const numberParser: Parser = P_UTILS.digits() + .map(x => Number.parseInt(x)) + .chain(x => { + if (Number.isNaN(x)) { + return P.fail('a number'); + } else { + return P.succeed(x); + } + }); + +const canaryParser: Parser = P.sequenceMap( + (_, c1, c2, c3) => { + return c1 + c2 + c3; + }, + P.string('-canary.'), + P_UTILS.digit() + .repeat(8, 8) + .map(x => x.join('')), + P.string('T'), + P_UTILS.digit() + .repeat(6, 6) + .map(x => x.join('')), +); + +export const versionParser: Parser = P.or( + P.sequenceMap( + (major, _1, minor, _2, patch) => { + return new Version(major, minor, patch); + }, + numberParser, + P.string('.'), + numberParser, + P.string('.'), + numberParser, + P_UTILS.eof(), + ), + P.sequenceMap( + (major, _1, minor, _2, patch, canary) => { + return new CanaryVersion(major, minor, patch, canary); + }, + numberParser, + P.string('.'), + numberParser, + P.string('.'), + numberParser, + canaryParser, + P_UTILS.eof(), + ), +); + +export function parseVersion(str: string): Version { + const parserRes = versionParser.tryParse(str); + if (parserRes.success) { + return parserRes.value; + } else { + throw new UserError(`failed to parse manifest version "${str}"`); + } +} + +export function stringifyVersion(version: Version): string { + return version.toString(); +} + +export function getIncrementOptions(version: Version): [Version, Version, Version, CanaryVersion] { + const moment = Moment(); + const canary = moment.utcOffset(0).format('YYYYMMDDTHHmmss'); + return [ + new Version(version.major + 1, 0, 0), + new Version(version.major, version.minor + 1, 0), + new Version(version.major, version.minor, version.patch + 1), + new CanaryVersion(version.major, version.minor, version.patch, canary), + ]; +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..233fe38d7df48ec891a9f6fab4c4630222822d91 GIT binary patch literal 167843 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 { - // we don't want warnings from node modules that we can do nothing about - return !warning.filename.includes('node_modules'); - }, - }), - ], - }) - .catch(() => process.exit(1)); - -await context.watch(); diff --git a/exampleVault/.obsidian/app.json b/exampleVault/.obsidian/app.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/exampleVault/.obsidian/app.json @@ -0,0 +1 @@ +{} diff --git a/exampleVault/.obsidian/appearance.json b/exampleVault/.obsidian/appearance.json new file mode 100644 index 0000000..af4da16 --- /dev/null +++ b/exampleVault/.obsidian/appearance.json @@ -0,0 +1,4 @@ +{ + "accentColor": "", + "theme": "obsidian" +} diff --git a/exampleVault/.obsidian/community-plugins.json b/exampleVault/.obsidian/community-plugins.json new file mode 100644 index 0000000..35fa2d8 --- /dev/null +++ b/exampleVault/.obsidian/community-plugins.json @@ -0,0 +1 @@ +["obsidian-media-db-plugin"] diff --git a/exampleVault/.obsidian/core-plugins-migration.json b/exampleVault/.obsidian/core-plugins-migration.json new file mode 100644 index 0000000..ba79b99 --- /dev/null +++ b/exampleVault/.obsidian/core-plugins-migration.json @@ -0,0 +1,31 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "properties": false, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": false, + "browser": false +} diff --git a/exampleVault/.obsidian/core-plugins.json b/exampleVault/.obsidian/core-plugins.json new file mode 100644 index 0000000..abcf2ca --- /dev/null +++ b/exampleVault/.obsidian/core-plugins.json @@ -0,0 +1,20 @@ +[ + "file-explorer", + "global-search", + "switcher", + "graph", + "backlink", + "canvas", + "outgoing-link", + "tag-pane", + "page-preview", + "daily-notes", + "templates", + "note-composer", + "command-palette", + "editor-status", + "bookmarks", + "outline", + "word-count", + "file-recovery" +] diff --git a/exampleVault/.obsidian/hotkeys.json b/exampleVault/.obsidian/hotkeys.json new file mode 100644 index 0000000..94c3be5 --- /dev/null +++ b/exampleVault/.obsidian/hotkeys.json @@ -0,0 +1,8 @@ +{ + "app:reload": [ + { + "modifiers": ["Mod"], + "key": "R" + } + ] +} diff --git a/exampleVault/.obsidian/plugins/obsidian-media-db-plugin/.hotreload b/exampleVault/.obsidian/plugins/obsidian-media-db-plugin/.hotreload new file mode 100644 index 0000000..e69de29 diff --git a/exampleVault/.obsidian/plugins/obsidian-media-db-plugin/manifest.json b/exampleVault/.obsidian/plugins/obsidian-media-db-plugin/manifest.json new file mode 100644 index 0000000..2535f1e --- /dev/null +++ b/exampleVault/.obsidian/plugins/obsidian-media-db-plugin/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "obsidian-media-db-plugin", + "name": "Media DB", + "version": "0.6.0", + "minAppVersion": "1.5.0", + "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", + "author": "Moritz Jung", + "authorUrl": "https://www.moritzjung.dev", + "isDesktopOnly": false +} diff --git a/exampleVault/.obsidian/plugins/obsidian-media-db-plugin/styles.css b/exampleVault/.obsidian/plugins/obsidian-media-db-plugin/styles.css new file mode 100644 index 0000000..6d4bb8d --- /dev/null +++ b/exampleVault/.obsidian/plugins/obsidian-media-db-plugin/styles.css @@ -0,0 +1,126 @@ +.media-db-plugin-list-wrapper { + display: flex; + align-content: center; + margin-bottom: 5px; + margin-top: 5px; +} + +.media-db-plugin-list-toggle { +} + +.media-db-plugin-list-text-wrapper { + flex: 1; +} + +.media-db-plugin-list-text { + display: block; +} + +small.media-db-plugin-list-text { + color: var(--text-muted); +} + +.media-db-plugin-select-modal { + display: contents; +} + +.media-db-plugin-select-wrapper { + display: flex; + flex-direction: column; + margin: 5px; + overflow-y: auto; +} + +.media-db-plugin-select-element { + cursor: pointer; + border-left: 5px solid transparent; + padding: 5px; + margin: 5px 0 5px 0; + border-radius: 5px; + white-space: pre-wrap; + font-size: 16px; +} + +.media-db-plugin-select-element-selected { + border-left: 5px solid var(--interactive-accent) !important; + background: var(--background-secondary-alt); +} + +.media-db-plugin-select-element-hover { + background: var(--background-secondary-alt); +} + +.media-db-plugin-preview-modal { + display: contents; +} + +.media-db-plugin-preview-wrapper { + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.media-db-plugin-spacer { + margin-bottom: 10px; +} + +/* region property mappings */ +.media-db-plugin-property-mappings-model-container { + border: 1px solid var(--background-modifier-border); + border-radius: 5px; + padding: 10px; + width: 100%; +} + +.media-db-plugin-property-mappings-container { + margin: 10px 0; + display: flex; + flex-direction: column; + gap: 5px; +} + +.media-db-plugin-property-mapping-element { + display: flex; + flex-direction: row; + gap: 10px; +} + +.media-db-plugin-property-mapping-element-property-name-wrapper { + min-width: 160px; + background: var(--background-modifier-form-field); + padding: 2px 5px; + border-radius: 5px; + + display: flex; + align-items: center; +} + +.media-db-plugin-property-mapping-element-property-name { + margin: 0; +} + +.media-db-plugin-property-mappings-save-button { + margin: 0; +} + +.media-db-plugin-property-mapping-to { + display: flex; + align-items: center; +} + +.media-db-plugin-property-mapping-validation { + color: var(--text-error); + margin-bottom: 5px; +} + +.media-db-plugin-button:focus { + /*outline: 1px solid white;*/ +} + +.media-db-plugin-preview { + border-radius: var(--modal-radius); + border: var(--modal-border-width) solid var(--modal-border-color); + padding: var(--size-4-4); +} + +/* endregion */ diff --git a/exampleVault/.obsidian/workspace.json b/exampleVault/.obsidian/workspace.json new file mode 100644 index 0000000..83c106a --- /dev/null +++ b/exampleVault/.obsidian/workspace.json @@ -0,0 +1,151 @@ +{ + "main": { + "id": "9618d22a4d8c511c", + "type": "split", + "children": [ + { + "id": "982b4a04c4a0e996", + "type": "tabs", + "children": [ + { + "id": "b912b847e62b0980", + "type": "leaf", + "state": { + "type": "empty", + "state": {} + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "a448fa9a0495e7a4", + "type": "split", + "children": [ + { + "id": "0148e129668970ae", + "type": "tabs", + "children": [ + { + "id": "5f549c8ae9af971e", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical" + } + } + }, + { + "id": "b782c94865111b8f", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + } + } + }, + { + "id": "942c8e2bba79177d", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {} + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "8a6d32cd2e32c39d", + "type": "split", + "children": [ + { + "id": "b712a27a0deec50f", + "type": "tabs", + "children": [ + { + "id": "81240f6821eb54dc", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + } + } + }, + { + "id": "2dda1a67c07f367d", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "linksCollapsed": false, + "unlinkedCollapsed": true + } + } + }, + { + "id": "cb0aecbffefcb594", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true + } + } + }, + { + "id": "1be09685a129aa6e", + "type": "leaf", + "state": { + "type": "outline", + "state": {} + } + } + ] + } + ], + "direction": "horizontal", + "width": 300, + "collapsed": true + }, + "left-ribbon": { + "hiddenItems": { + "obsidian-media-db-plugin:Add new Media DB entry": false, + "switcher:Open quick switcher": false, + "graph:Open graph view": false, + "canvas:Create new canvas": false, + "daily-notes:Open today's daily note": false, + "templates:Insert template": false, + "command-palette:Open command palette": false + } + }, + "active": "b912b847e62b0980", + "lastOpenFiles": [ + "Media DB/games/Limbo (2010).md", + "Media DB/games/Hollow Knight (2017).md", + "Media DB/movies/2001 - A Space Odyssey (1968).md", + "Media DB/games", + "Media DB/movies", + "Media DB" + ] +} \ No newline at end of file diff --git a/exampleVault/index.md b/exampleVault/index.md new file mode 100644 index 0000000..e69de29 diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 6807d50..0000000 --- a/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - roots: ['/src', ''], - testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'], - transform: { - '^.+\\.(ts|tsx)$': 'ts-jest', - }, -}; diff --git a/manifest-beta.json b/manifest-beta.json new file mode 100644 index 0000000..2535f1e --- /dev/null +++ b/manifest-beta.json @@ -0,0 +1,10 @@ +{ + "id": "obsidian-media-db-plugin", + "name": "Media DB", + "version": "0.6.0", + "minAppVersion": "1.5.0", + "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", + "author": "Moritz Jung", + "authorUrl": "https://www.moritzjung.dev", + "isDesktopOnly": false +} diff --git a/manifest.json b/manifest.json index 9ff6d1f..2535f1e 100644 --- a/manifest.json +++ b/manifest.json @@ -2,9 +2,9 @@ "id": "obsidian-media-db-plugin", "name": "Media DB", "version": "0.6.0", - "minAppVersion": "0.14.0", + "minAppVersion": "1.5.0", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", - "authorUrl": "https://mprojectscode.github.io/", + "authorUrl": "https://www.moritzjung.dev", "isDesktopOnly": false } diff --git a/package.json b/package.json index af4b1df..fe25423 100644 --- a/package.json +++ b/package.json @@ -4,34 +4,48 @@ "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "main": "main.js", "scripts": { - "dev": "node esbuild.dev.config.mjs", - "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", - "version": "node version-bump.mjs && git add manifest.json versions.json", - "test": "jest", - "format": "prettier --write ." + "dev": "bun run automation/build/esbuild.dev.config.ts", + "build": "bun run tsc && bun run automation/build/esbuild.config.ts", + "tsc": "tsc -noEmit -skipLibCheck", + "test": "bun test", + "test:log": "LOG_TESTS=true bun test", + "format": "prettier --write --plugin prettier-plugin-svelte .", + "format:check": "prettier --check --plugin prettier-plugin-svelte .", + "lint": "eslint --max-warnings=0 src/**", + "lint:fix": "eslint --max-warnings=0 --fix src/**", + "svelte-check": "svelte-check --compiler-warnings \"unused-export-let:ignore\"", + "check": "bun run format:check && bun run tsc && bun run lint && bun run test", + "check:fix": "bun run format && bun run tsc && bun run lint:fix && bun run test", + "release": "bun run automation/release.ts", + "stats": "bun run automation/stats.ts" }, "keywords": [], "author": "Moritz Jung", "license": "GPL-3.0", "devDependencies": { "@popperjs/core": "^2.11.8", - "@tsconfig/svelte": "^5.0.2", - "@types/jest": "^29.5.11", - "@types/node": "^20.10.4", - "@typescript-eslint/eslint-plugin": "^6.14.0", - "@typescript-eslint/parser": "^6.14.0", + "@lemons_dev/parsinom": "^0.0.12", + "@happy-dom/global-registrator": "^14.3.6", + "@tsconfig/svelte": "^5.0.3", + "@types/bun": "^1.0.10", + "@typescript-eslint/eslint-plugin": "^7.3.1", + "@typescript-eslint/parser": "^7.3.1", "builtin-modules": "^3.3.0", - "esbuild": "^0.19.9", + "esbuild": "^0.20.2", + "esbuild-plugin-copy-watch": "^2.1.0", "esbuild-svelte": "^0.8.0", + "eslint": "^8.57.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-isaacscript": "^3.12.2", "eslint-plugin-only-warn": "^1.1.0", - "jest": "^29.7.0", - "jest-fetch-mock": "^3.0.3", - "obsidian": "^1.4.11", - "prettier": "3.1.1", - "svelte": "^4.2.8", - "svelte-preprocess": "^5.1.2", - "ts-jest": "^29.1.1", - "tslib": "2.6.2", - "typescript": "^5.3.3" + "obsidian": "latest", + "prettier": "^3.2.5", + "prettier-plugin-svelte": "^3.2.2", + "string-argv": "^0.3.2", + "svelte": "^4.2.12", + "svelte-check": "^3.6.8", + "svelte-preprocess": "^5.1.3", + "tslib": "^2.6.2", + "typescript": "^5.4.3" } } diff --git a/src/api/APIManager.ts b/src/api/APIManager.ts index 17f0f36..3340a67 100644 --- a/src/api/APIManager.ts +++ b/src/api/APIManager.ts @@ -21,8 +21,12 @@ export class APIManager { for (const api of this.apis) { if (apisToQuery.contains(api.apiName)) { - const apiRes = await api.searchByTitle(query); - res = res.concat(apiRes); + try { + const apiRes = await api.searchByTitle(query); + res = res.concat(apiRes); + } catch (e) { + console.warn(e); + } } } diff --git a/src/api/APIModel.ts b/src/api/APIModel.ts index 6230457..5ea4e23 100644 --- a/src/api/APIModel.ts +++ b/src/api/APIModel.ts @@ -1,6 +1,5 @@ import { MediaTypeModel } from '../models/MediaTypeModel'; import { MediaType } from '../utils/MediaType'; -import { MediaDbPluginSettings } from 'src/settings/Settings'; import MediaDbPlugin from '../main'; export abstract class APIModel { @@ -20,7 +19,10 @@ export abstract class APIModel { abstract getById(id: string): Promise; hasType(type: MediaType): boolean { - if (this.types.contains(type) && (Boolean((this.plugin.settings.apiToggle as any)?.[this.apiName]?.[type] as MediaDbPluginSettings) === true || (this.plugin.settings.apiToggle as any)?.[this.apiName]?.[type] as MediaDbPluginSettings === undefined)) { + if ( + this.types.contains(type) && + (Boolean((this.plugin.settings.apiToggle as any)?.[this.apiName]?.[type]) === true || (this.plugin.settings.apiToggle as any)?.[this.apiName]?.[type] === undefined) + ) { return true; } } diff --git a/src/api/apis/LocGovAPI.ts b/src/api/apis/LocGovAPI.ts deleted file mode 100644 index 97addde..0000000 --- a/src/api/apis/LocGovAPI.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { APIModel } from '../APIModel'; -import { MediaTypeModel } from '../../models/MediaTypeModel'; -import MediaDbPlugin from '../../main'; - -// WIP -export class LocGovAPI extends APIModel { - plugin: MediaDbPlugin; - typeMappings: Map; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'loc.gov API'; - this.apiDescription = 'A free API for the Library of Congress collections.'; - this.apiUrl = 'https://libraryofcongress.github.io/data-exploration/index.html'; - this.types = []; - this.typeMappings = new Map(); - // this.typeMappings.set('movie', 'movie'); - } - - async searchByTitle(title: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by Title`); - - const searchUrl = `https://www.loc.gov/search/?q=${encodeURIComponent(title)}&fo=json&c=20`; - const fetchData = await fetch(searchUrl); - console.debug(fetchData); - - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from an API.`); - } - - const data = await fetchData.json(); - console.debug(data); - let ret: MediaTypeModel[] = []; - - throw new Error('MDB | Under construction, API implementation not finished'); - - // return ret; - } - - async getById(id: string): Promise { - console.log(`MDB | api "${this.apiName}" queried by ID`); - - const searchUrl = `https://www.loc.gov/item/${encodeURIComponent(id)}/?fo=json`; - const fetchData = await fetch(searchUrl); - if (fetchData.status !== 200) { - throw Error(`MDB | Received status code ${fetchData.status} from an API.`); - } - - const data = await fetchData.json(); - console.debug(data); - const result = data.data; - - const type = this.typeMappings.get(result.type.toLowerCase()); - if (type === undefined) { - throw Error(`${result.type.toLowerCase()} is an unsupported type.`); - } - - throw new Error('MDB | Under construction, API implementation not finished'); - - // return; - } -} diff --git a/src/api/apis/SteamAPI.ts b/src/api/apis/SteamAPI.ts index 1f06ace..45182e0 100644 --- a/src/api/apis/SteamAPI.ts +++ b/src/api/apis/SteamAPI.ts @@ -41,7 +41,13 @@ export class SteamAPI extends APIModel { const filteredData = []; for (const app of data.applist.apps) { - if (app.name.toLowerCase().includes(title.toLowerCase())) { + if ( + app.name + .normalize('NFD') + .replace(/\p{Diacritic}/gu, '') + .toLowerCase() + .includes(title.toLowerCase()) + ) { filteredData.push(app); } if (filteredData.length > 20) { diff --git a/src/main.ts b/src/main.ts index 46f0b7d..60faa26 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,11 +22,19 @@ import { BoardGameGeekAPI } from './api/apis/BoardGameGeekAPI'; import { OpenLibraryAPI } from './api/apis/OpenLibraryAPI'; import { MobyGamesAPI } from './api/apis/MobyGamesAPI'; import { PropertyMapper } from './settings/PropertyMapper'; -import { YAMLConverter } from './utils/YAMLConverter'; import { MediaDbFolderImportModal } from './modals/MediaDbFolderImportModal'; import { PropertyMapping, PropertyMappingModel } from './settings/PropertyMapping'; import { ModalHelper, ModalResultCode, SearchModalOptions } from './utils/ModalHelper'; import { DateFormatter } from './utils/DateFormatter'; +import { MediaType } from 'src/utils/MediaType'; + +export type Metadata = Record; + +export interface MediaTypeModelObj { + id: string; + type: MediaType; + dataSource: string; +} export default class MediaDbPlugin extends Plugin { settings: MediaDbPluginSettings; @@ -186,7 +194,12 @@ export default class MediaDbPlugin extends Plugin { let apiSearchResults: MediaTypeModel[] = await this.modalHelper.openSearchModal(searchModalOptions ?? {}, async searchModalData => { types = searchModalData.types; const apis = this.apiManager.apis.filter(x => x.hasTypeOverlap(searchModalData.types)).map(x => x.apiName); - return await this.apiManager.query(searchModalData.query, apis); + try { + return await this.apiManager.query(searchModalData.query, apis); + } catch (e) { + console.warn(e); + return []; + } }); if (!apiSearchResults) { @@ -309,21 +322,40 @@ export default class MediaDbPlugin extends Plugin { generateMediaDbNoteFrontmatterPreview(mediaTypeModel: MediaTypeModel): string { const fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); - return this.settings.useCustomYamlStringifier ? YAMLConverter.toYaml(fileMetadata) : stringifyYaml(fileMetadata); + return stringifyYaml(fileMetadata); } + /** + * Generates the content of a note from a media model and some options. + * + * @param mediaTypeModel + * @param options + */ async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { - let template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app); + const template = await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app); - if (this.settings.useDefaultFrontMatter || !template) { - return this.generateContentWithDefaultFrontMatter(mediaTypeModel, options, template); - } else { - return this.generateContentWithCustomFrontMatter(mediaTypeModel, options, template); - } + return this.generateContentWithDefaultFrontMatter(mediaTypeModel, options, template); + + // if (this.settings.useDefaultFrontMatter || !template) { + // return this.generateContentWithDefaultFrontMatter(mediaTypeModel, options, template); + // } else { + // return this.generateContentWithCustomFrontMatter(mediaTypeModel, options, template); + // } } async generateContentWithDefaultFrontMatter(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions, template?: string): Promise { - let fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); + let fileMetadata: Record; + + if (this.settings.useDefaultFrontMatter) { + fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); + } else { + fileMetadata = { + id: mediaTypeModel.id, + type: mediaTypeModel.type, + dataSource: mediaTypeModel.dataSource, + }; + } + let fileContent = ''; template = options.attachTemplate ? template : ''; @@ -331,27 +363,20 @@ export default class MediaDbPlugin extends Plugin { ({ fileMetadata, fileContent } = await this.attachTemplate(fileMetadata, fileContent, template)); if (this.settings.enableTemplaterIntegration && hasTemplaterPlugin(this.app)) { - // Only support stringifyYaml for templater plugin // Include the media variable in all templater commands by using a top level JavaScript execution command. fileContent = `---\n<%* const media = ${JSON.stringify(mediaTypeModel)} %>\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; } else { - fileContent = `---\n${this.settings.useCustomYamlStringifier ? YAMLConverter.toYaml(fileMetadata) : stringifyYaml(fileMetadata)}---\n` + fileContent; + fileContent = `---\n${stringifyYaml(fileMetadata)}---\n${fileContent}`; } return fileContent; } async generateContentWithCustomFrontMatter(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions, template: string): Promise { - const frontMatterRegex = /^---*\n([\s\S]*?)\n---\h*/; + const regExp = new RegExp(this.frontMatterRexExpPattern); - const match = template.match(frontMatterRegex); - - if (!match || match.length !== 2) { - throw new Error('Cannot find YAML front matter for template.'); - } - - let frontMatter = parseYaml(match[1]); - let fileContent: string = template.replace(frontMatterRegex, ''); + const frontMatter = this.getMetaDataFromFileContent(template); + let fileContent: string = template.replace(regExp, ''); // Updating a previous file if (options.attachFile) { @@ -359,7 +384,7 @@ export default class MediaDbPlugin extends Plugin { // Use contents (below front matter) from previous file fileContent = await this.app.vault.read(options.attachFile); - const regExp = new RegExp(this.frontMatterRexExpPattern); + fileContent = fileContent.replace(regExp, ''); fileContent = fileContent.startsWith('\n') ? fileContent.substring(1) : fileContent; @@ -391,18 +416,19 @@ export default class MediaDbPlugin extends Plugin { // Include the media variable in all templater commands by using a top level JavaScript execution command. fileContent = `---\n<%* const media = ${JSON.stringify(mediaTypeModel)} %>\n${stringifyYaml(frontMatter)}---\n${fileContent}`; } else { - fileContent = `---\n${this.settings.useCustomYamlStringifier ? YAMLConverter.toYaml(frontMatter) : stringifyYaml(frontMatter)}---\n` + fileContent; + fileContent = `---\n${stringifyYaml(frontMatter)}---\n${fileContent}`; } return fileContent; } - async attachFile(fileMetadata: any, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: any; fileContent: string }> { + async attachFile(fileMetadata: Metadata, fileContent: string, fileToAttach?: TFile): Promise<{ fileMetadata: Metadata; fileContent: string }> { if (!fileToAttach) { return { fileMetadata: fileMetadata, fileContent: fileContent }; } const attachFileMetadata: any = this.getMetadataFromFileCache(fileToAttach); + // TODO: better object merging fileMetadata = Object.assign(attachFileMetadata, fileMetadata); let attachFileContent: string = await this.app.vault.read(fileToAttach); @@ -414,12 +440,13 @@ export default class MediaDbPlugin extends Plugin { return { fileMetadata: fileMetadata, fileContent: fileContent }; } - async attachTemplate(fileMetadata: any, fileContent: string, template: string): Promise<{ fileMetadata: any; fileContent: string }> { + async attachTemplate(fileMetadata: Metadata, fileContent: string, template: string): Promise<{ fileMetadata: Metadata; fileContent: string }> { if (!template) { return { fileMetadata: fileMetadata, fileContent: fileContent }; } - const templateMetadata: any = this.getMetaDataFromFileContent(template); + const templateMetadata: Metadata = this.getMetaDataFromFileContent(template); + // TODO: better object merging fileMetadata = Object.assign(templateMetadata, fileMetadata); const regExp = new RegExp(this.frontMatterRexExpPattern); @@ -429,8 +456,8 @@ export default class MediaDbPlugin extends Plugin { return { fileMetadata: fileMetadata, fileContent: fileContent }; } - getMetaDataFromFileContent(fileContent: string): any { - let metadata: any; + getMetaDataFromFileContent(fileContent: string): Metadata { + let metadata: Metadata; const regExp = new RegExp(this.frontMatterRexExpPattern); const frontMatterRegExpResult = regExp.exec(fileContent); @@ -455,15 +482,9 @@ export default class MediaDbPlugin extends Plugin { return metadata; } - getMetadataFromFileCache(file: TFile): any { - let metadata: any = this.app.metadataCache.getFileCache(file).frontmatter; - if (metadata) { - metadata = Object.assign({}, metadata); // copy - delete metadata.position; - } else { - metadata = {}; - } - return metadata; + getMetadataFromFileCache(file: TFile): Metadata { + const metadata: Metadata | undefined = this.app.metadataCache.getFileCache(file).frontmatter; + return structuredClone(metadata ?? {}); } /** @@ -513,7 +534,7 @@ export default class MediaDbPlugin extends Plugin { throw new Error('MDB | there is no active note'); } - let metadata: any = this.getMetadataFromFileCache(activeFile); + let metadata = this.getMetadataFromFileCache(activeFile); metadata = this.modelPropertyMapper.convertObjectBack(metadata); console.debug(`MDB | read metadata`, metadata); @@ -522,10 +543,12 @@ export default class MediaDbPlugin extends Plugin { throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); } - const oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(metadata, metadata.type); + const validOldMetadata: MediaTypeModelObj = metadata as unknown as MediaTypeModelObj; + + const oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(validOldMetadata, validOldMetadata.type); // console.debug(oldMediaTypeModel); - let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(metadata.id, metadata.dataSource); + let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(validOldMetadata.id, validOldMetadata.dataSource); if (!newMediaTypeModel) { return; } @@ -533,8 +556,6 @@ export default class MediaDbPlugin extends Plugin { newMediaTypeModel = Object.assign(oldMediaTypeModel, newMediaTypeModel.getWithOutUserData()); // console.debug(newMediaTypeModel); - // deletion not happening anymore why is this log statement still here - console.debug('MDB | deleting old entry'); if (onlyMetadata) { await this.createMediaDbNoteFromModel(newMediaTypeModel, { attachFile: activeFile, folder: activeFile.parent, openNote: true }); } else { diff --git a/src/models/MediaTypeModel.ts b/src/models/MediaTypeModel.ts index 5418c73..c59e87f 100644 --- a/src/models/MediaTypeModel.ts +++ b/src/models/MediaTypeModel.ts @@ -31,12 +31,12 @@ export abstract class MediaTypeModel { abstract getTags(): string[]; - toMetaDataObject(): object { + toMetaDataObject(): Record { return { ...this.getWithOutUserData(), ...this.userData, tags: this.getTags().join('/') }; } - getWithOutUserData(): object { - const copy = Object.assign({}, this); + getWithOutUserData(): Record { + const copy = structuredClone(this) as Record; delete copy.userData; return copy; } diff --git a/src/models/WikiModel.ts b/src/models/WikiModel.ts index fa385bc..f325008 100644 --- a/src/models/WikiModel.ts +++ b/src/models/WikiModel.ts @@ -17,7 +17,7 @@ export class WikiModel extends MediaTypeModel { length: number; article: string; - userData: {}; + userData: Record; constructor(obj: any = {}) { super(); @@ -45,8 +45,8 @@ export class WikiModel extends MediaTypeModel { return MediaType.Wiki; } - override getWithOutUserData(): object { - const copy = Object.assign({}, this); + override getWithOutUserData(): Record { + const copy = structuredClone(this) as Record; delete copy.userData; delete copy.article; return copy; diff --git a/src/settings/Icon.svelte b/src/settings/Icon.svelte index 23f2002..47edb27 100644 --- a/src/settings/Icon.svelte +++ b/src/settings/Icon.svelte @@ -1,8 +1,8 @@ - - {#if iconName.length > 0}
{/if} + + diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index 1ca6a28..90f930f 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -15,15 +15,13 @@ export class PropertyMapper { * * @param obj */ - convertObject(obj: object): object { + convertObject(obj: Record): Record { if (!obj.hasOwnProperty('type')) { return obj; } - // @ts-ignore // console.log(obj.type); - // @ts-ignore if (MEDIA_TYPES.filter(x => x.toString() == obj.type).length < 1) { return obj; } @@ -31,7 +29,7 @@ export class PropertyMapper { // @ts-ignore const propertyMappings = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type).properties; - const newObj: object = {}; + const newObj: Record = {}; for (const [key, value] of Object.entries(obj)) { for (const propertyMapping of propertyMappings) { @@ -59,20 +57,18 @@ export class PropertyMapper { * * @param obj */ - convertObjectBack(obj: object): object { + convertObjectBack(obj: Record): Record { if (!obj.hasOwnProperty('type')) { return obj; } - // @ts-ignore - if (MEDIA_TYPES.contains(obj.type)) { + if (MEDIA_TYPES.contains(obj.type as any)) { return obj; } - // @ts-ignore const propertyMappings = this.plugin.settings.propertyMappingModels.find(x => x.type === obj.type).properties; - const originalObj: object = {}; + const originalObj: Record = {}; objLoop: for (const [key, value] of Object.entries(obj)) { // first try if it is a normal property diff --git a/src/settings/PropertyMapping.ts b/src/settings/PropertyMapping.ts index e46fdc0..f122c83 100644 --- a/src/settings/PropertyMapping.ts +++ b/src/settings/PropertyMapping.ts @@ -68,7 +68,7 @@ export class PropertyMappingModel { }; } - getMappedProperties() { + getMappedProperties(): PropertyMapping[] { return this.properties.filter(x => x.mapping === PropertyMappingOption.Map); } diff --git a/src/settings/PropertyMappingModelComponent.svelte b/src/settings/PropertyMappingModelComponent.svelte index 7e13b20..ffa4805 100644 --- a/src/settings/PropertyMappingModelComponent.svelte +++ b/src/settings/PropertyMappingModelComponent.svelte @@ -1,12 +1,12 @@ - -
{capitalizeFirstLetter(model.type)}
- { #each model.properties as property } + {#each model.properties as property}
{property.property}
- { #if property.locked } -
- property can not be remapped -
- { :else } + {#if property.locked} +
property can not be remapped
+ {:else} - { #if property.mapping === PropertyMappingOption.Map } - + {#if property.mapping === PropertyMappingOption.Map} +
- +
- { /if } - { /if } + {/if} + {/if}
- { /each } + {/each}
- { #if !validationResult?.res } + {#if !validationResult?.res}
{validationResult?.err?.message}
- { /if } + {/if}
+ + diff --git a/src/settings/PropertyMappingModelsComponent.svelte b/src/settings/PropertyMappingModelsComponent.svelte index 10442b7..dace243 100644 --- a/src/settings/PropertyMappingModelsComponent.svelte +++ b/src/settings/PropertyMappingModelsComponent.svelte @@ -1,19 +1,15 @@ - -
- { #each models as model } - - { /each } + {#each models as model} + + {/each}
+ + diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index d5a3578..5d91d47 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -13,7 +13,6 @@ export interface MediaDbPluginSettings { OMDbKey: string; MobyGamesKey: string; sfwFilter: boolean; - useCustomYamlStringifier: boolean; templates: boolean; customDateFormat: string; openNoteInNewTab: boolean; @@ -24,18 +23,18 @@ export interface MediaDbPluginSettings { movie: boolean; series: boolean; game: boolean; - }, + }; MALAPI: { movie: boolean; series: boolean; - }, + }; SteamAPI: { game: boolean; - }, + }; MobyGamesAPI: { game: boolean; - } - }, + }; + }; movieTemplate: string; seriesTemplate: string; mangaTemplate: string; @@ -79,7 +78,6 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { OMDbKey: '', MobyGamesKey: '', sfwFilter: true, - useCustomYamlStringifier: true, templates: true, customDateFormat: 'L', openNoteInNewTab: true, @@ -100,7 +98,7 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { }, MobyGamesAPI: { game: true, - } + }, }, movieTemplate: '', seriesTemplate: '', @@ -190,7 +188,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.OMDbKey) .onChange(data => { this.plugin.settings.OMDbKey = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -202,7 +200,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.MobyGamesKey) .onChange(data => { this.plugin.settings.MobyGamesKey = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -212,17 +210,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .addToggle(cb => { cb.setValue(this.plugin.settings.sfwFilter).onChange(data => { this.plugin.settings.sfwFilter = data; - this.plugin.saveSettings(); - }); - }); - - new Setting(containerEl) - .setName('YAML formatter') - .setDesc('Add optional quotation marks around strings in the metadata block.') - .addToggle(cb => { - cb.setValue(this.plugin.settings.useCustomYamlStringifier).onChange(data => { - this.plugin.settings.useCustomYamlStringifier = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -232,7 +220,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .addToggle(cb => { cb.setValue(this.plugin.settings.templates).onChange(data => { this.plugin.settings.templates = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -241,10 +229,10 @@ export class MediaDbSettingTab extends PluginSettingTab { .setDesc( fragWithHTML( "Your custom date format. Use 'YYYY-MM-DD' for example.
" + - "For more syntax, refer to
format reference.
" + - "Your current syntax looks like this: " + - this.plugin.dateFormatter.getPreview() + - '', + "For more syntax, refer to format reference.
" + + "Your current syntax looks like this: " + + this.plugin.dateFormatter.getPreview() + + '', ), ) .addText(cb => { @@ -254,7 +242,7 @@ export class MediaDbSettingTab extends PluginSettingTab { const newDateFormat = data ? data : DEFAULT_SETTINGS.customDateFormat; this.plugin.settings.customDateFormat = newDateFormat; document.getElementById('media-db-dateformat-preview').textContent = this.plugin.dateFormatter.getPreview(newDateFormat); // update preview - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -264,17 +252,17 @@ export class MediaDbSettingTab extends PluginSettingTab { .addToggle(cb => { cb.setValue(this.plugin.settings.openNoteInNewTab).onChange(data => { this.plugin.settings.openNoteInNewTab = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); new Setting(containerEl) .setName('Use default front matter') - .setDesc('Wheter to use the default front matter. If disabled, the front matter from the template will be used. Same as mapping everything to remove.') + .setDesc('Whether to use the default front matter. If disabled, the front matter from the template will be used. Same as mapping everything to remove.') .addToggle(cb => { cb.setValue(this.plugin.settings.useDefaultFrontMatter).onChange(data => { this.plugin.settings.useDefaultFrontMatter = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); // Redraw settings to display/remove the property mappings this.display(); }); @@ -288,11 +276,10 @@ export class MediaDbSettingTab extends PluginSettingTab { .addToggle(cb => { cb.setValue(this.plugin.settings.enableTemplaterIntegration).onChange(data => { this.plugin.settings.enableTemplaterIntegration = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); - containerEl.createEl('h3', { text: 'APIs Per Media Type' }); containerEl.createEl('h5', { text: 'Movies' }); new Setting(containerEl) @@ -301,7 +288,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .addToggle(cb => { cb.setValue(this.plugin.settings.apiToggle.OMDbAPI.movie).onChange(data => { this.plugin.settings.apiToggle.OMDbAPI.movie = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); new Setting(containerEl) @@ -310,7 +297,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .addToggle(cb => { cb.setValue(this.plugin.settings.apiToggle.MALAPI.movie).onChange(data => { this.plugin.settings.apiToggle.MALAPI.movie = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); containerEl.createEl('h5', { text: 'Series' }); @@ -320,7 +307,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .addToggle(cb => { cb.setValue(this.plugin.settings.apiToggle.OMDbAPI.series).onChange(data => { this.plugin.settings.apiToggle.OMDbAPI.series = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); new Setting(containerEl) @@ -329,7 +316,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .addToggle(cb => { cb.setValue(this.plugin.settings.apiToggle.MALAPI.series).onChange(data => { this.plugin.settings.apiToggle.MALAPI.series = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); containerEl.createEl('h5', { text: 'Games' }); @@ -339,7 +326,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .addToggle(cb => { cb.setValue(this.plugin.settings.apiToggle.OMDbAPI.game).onChange(data => { this.plugin.settings.apiToggle.OMDbAPI.game = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); new Setting(containerEl) @@ -348,7 +335,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .addToggle(cb => { cb.setValue(this.plugin.settings.apiToggle.SteamAPI.game).onChange(data => { this.plugin.settings.apiToggle.SteamAPI.game = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); new Setting(containerEl) @@ -357,7 +344,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .addToggle(cb => { cb.setValue(this.plugin.settings.apiToggle.MobyGamesAPI.game).onChange(data => { this.plugin.settings.apiToggle.MobyGamesAPI.game = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -372,7 +359,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.movieFolder) .onChange(data => { this.plugin.settings.movieFolder = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -385,7 +372,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.seriesFolder) .onChange(data => { this.plugin.settings.seriesFolder = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -398,7 +385,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.mangaFolder) .onChange(data => { this.plugin.settings.mangaFolder = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -411,7 +398,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.gameFolder) .onChange(data => { this.plugin.settings.gameFolder = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -424,7 +411,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.wikiFolder) .onChange(data => { this.plugin.settings.wikiFolder = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -437,7 +424,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.musicReleaseFolder) .onChange(data => { this.plugin.settings.musicReleaseFolder = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -450,7 +437,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.boardgameFolder) .onChange(data => { this.plugin.settings.boardgameFolder = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); new Setting(containerEl) @@ -462,7 +449,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.bookFolder) .onChange(data => { this.plugin.settings.bookFolder = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); // endregion @@ -478,7 +465,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.movieTemplate) .onChange(data => { this.plugin.settings.movieTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -491,7 +478,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.seriesTemplate) .onChange(data => { this.plugin.settings.seriesTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -504,7 +491,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.mangaTemplate) .onChange(data => { this.plugin.settings.mangaTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -517,7 +504,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.gameTemplate) .onChange(data => { this.plugin.settings.gameTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -530,7 +517,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.wikiTemplate) .onChange(data => { this.plugin.settings.wikiTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -543,7 +530,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.musicReleaseTemplate) .onChange(data => { this.plugin.settings.musicReleaseTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -556,7 +543,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.boardgameTemplate) .onChange(data => { this.plugin.settings.boardgameTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -569,7 +556,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.bookTemplate) .onChange(data => { this.plugin.settings.bookTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); // endregion @@ -584,7 +571,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.movieFileNameTemplate) .onChange(data => { this.plugin.settings.movieFileNameTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -596,7 +583,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.seriesFileNameTemplate) .onChange(data => { this.plugin.settings.seriesFileNameTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -608,7 +595,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.mangaFileNameTemplate) .onChange(data => { this.plugin.settings.mangaFileNameTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -620,7 +607,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.gameFileNameTemplate) .onChange(data => { this.plugin.settings.gameFileNameTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -632,7 +619,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.wikiFileNameTemplate) .onChange(data => { this.plugin.settings.wikiFileNameTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -644,7 +631,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.musicReleaseFileNameTemplate) .onChange(data => { this.plugin.settings.musicReleaseFileNameTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -656,7 +643,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.boardgameFileNameTemplate) .onChange(data => { this.plugin.settings.boardgameFileNameTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); @@ -668,7 +655,7 @@ export class MediaDbSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.bookFileNameTemplate) .onChange(data => { this.plugin.settings.bookFileNameTemplate = data; - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }); }); // endregion @@ -709,7 +696,7 @@ export class MediaDbSettingTab extends PluginSettingTab { this.plugin.settings.propertyMappingModels = propertyMappingModels; new Notice(`MDB: Property Mappings for ${model.type} saved successfully.`); - this.plugin.saveSettings(); + void this.plugin.saveSettings(); }, }, }); diff --git a/src/settings/suggesters/Suggest.ts b/src/settings/suggesters/Suggest.ts index 331db6b..e64ebd3 100644 --- a/src/settings/suggesters/Suggest.ts +++ b/src/settings/suggesters/Suggest.ts @@ -165,7 +165,7 @@ export abstract class TextInputSuggest implements ISuggestOwner { (this.app).keymap.popScope(this.scope); this.suggest.setSuggestions([]); - this.popper.destroy(); + this.popper?.destroy(); this.suggestEl.detach(); } diff --git a/src/tests/ParameterizedAPI.test.ts b/src/tests/ParameterizedAPI.test.ts deleted file mode 100644 index a72430b..0000000 --- a/src/tests/ParameterizedAPI.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; -import { MediaDbPluginSettings } from 'src/settings/Settings'; -import { LocGovAPI } from '../api/apis/LocGovAPI'; -import { MALAPI } from '../api/apis/MALAPI'; -import { MusicBrainzAPI } from '../api/apis/MusicBrainzAPI'; -import { OMDbAPI } from '../api/apis/OMDbAPI'; -import { SteamAPI } from '../api/apis/SteamAPI'; -import { WikipediaAPI } from '../api/apis/WikipediaAPI'; -import MediaDbPlugin from '../main'; -import { setMALResponseMock, setMusicBrainzResponseMock, setOMDbResponseMock, setSteamResponseMock, setWikipediaResponseMock } from './mockHelpers'; -import MALMockMovie from './ResponseMocks/MALMockMovie.json'; -import MusicBrainzResponseMock from './ResponseMocks/MusicBrainzMockResponse.json'; -import OMDBMockMovie from './ResponseMocks/OMDBMockResponse.json'; -import SteamAPIResponseMock from './ResponseMocks/SteamAPIMockResponse.json'; -import WikipediaMockResponse from './ResponseMocks/WikipediaMockResponse.json'; - -enableFetchMocks(); -export let apiMock: OMDbAPI | MALAPI | LocGovAPI | MusicBrainzAPI | SteamAPI | WikipediaAPI; - -describe.each([{ name: OMDbAPI }, { name: MALAPI }, { name: LocGovAPI }, { name: MusicBrainzAPI }, { name: SteamAPI }, { name: WikipediaAPI }])( - '$name.name', - ({ name: parameterizedApi }) => { - beforeAll(() => { - let settingsMock: MediaDbPluginSettings = {} as MediaDbPluginSettings; - let pluginMock = {} as MediaDbPlugin; - pluginMock.settings = settingsMock; - // TODO: add fake API key? - apiMock = new parameterizedApi(pluginMock); - }); - - beforeEach(() => { - fetchMock.resetMocks(); - }); - - test('searchByTitle behavior when API returns garbage data', async () => { - const garbageResponse = JSON.stringify({ - data: 'string', - }); - fetchMock.mockResponseOnce(garbageResponse); - await expect(apiMock.searchByTitle('sample')).resolves.toEqual([]); - // } - expect(fetch).toHaveBeenCalledTimes(1); - }); - - test('searchByTitle behavior when requestUrl/fetch returns 401', async () => { - let sampleResponse = { - data: 'string', - }; - fetchMock.mockResponse(JSON.stringify(sampleResponse), { status: 401 }); - // TODO: Check API name and fix message - // TODO: Externalize string - await expect(apiMock.searchByTitle('sample')).rejects.toThrow(`MDB | Received status code ${401} from an API.`); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - - test('searchByTitle behavior when requestUrl/fetch returns 403', async () => { - let sampleResponse = { - data: 'string', - }; - fetchMock.mockResponse(JSON.stringify(sampleResponse), { status: 403 }); - // TODO: Check API name and fix message - // TODO: Externalize string/import? - await expect(apiMock.searchByTitle('sample')).rejects.toThrow(`MDB | Received status code ${403} from an API.`); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - - test('searchByTitle behavior when requestUrl/fetch returns 200', async () => { - let sampleResponse; - let ret; - switch (parameterizedApi) { - case OMDbAPI: - ret = setOMDbResponseMock(); - sampleResponse = OMDBMockMovie; - break; - case WikipediaAPI: - ret = setWikipediaResponseMock(); - sampleResponse = WikipediaMockResponse; - break; - case MALAPI: - // TODO: MAL needs more tests for different types of content - ret = setMALResponseMock(); - sampleResponse = MALMockMovie; - case LocGovAPI: - // TODO: Add soon - break; - case SteamAPI: - sampleResponse = SteamAPIResponseMock; - ret = setSteamResponseMock(); - break; - case MusicBrainzAPI: - sampleResponse = MusicBrainzResponseMock; - ret = setMusicBrainzResponseMock(); - break; - default: - throw Error(); - } - fetchMock.mockResponse(JSON.stringify(sampleResponse), { status: 200 }); - // TODO: Check API name and fix message - // TODO: Externalize string - await expect(apiMock.searchByTitle('Hooking Season Playtest')).resolves.toEqual(ret); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - }, -); diff --git a/src/tests/ResponseMocks/MALMockMovie.json b/src/tests/ResponseMocks/MALMockMovie.json deleted file mode 100644 index 7a605fb..0000000 --- a/src/tests/ResponseMocks/MALMockMovie.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "data": [ - { - "mal_id": 2890, - "url": "https://myanimelist.net/anime/2890/Gake_no_Ue_no_Ponyo", - "title": "Gake no Ue no Ponyo", - "title_english": "Ponyo", - "type": "Movie", - "source": "Original", - "episodes": 1, - "aired": { - "from": "2008-07-19T00:00:00+00:00", - "to": null, - "prop": { - "from": { - "day": 19, - "month": 7, - "year": 2008 - }, - "to": { - "day": null, - "month": null, - "year": null - } - }, - "string": "Jul 19, 2008" - } - } - ] -} diff --git a/src/tests/ResponseMocks/MusicBrainzMockResponse.json b/src/tests/ResponseMocks/MusicBrainzMockResponse.json deleted file mode 100644 index ec89553..0000000 --- a/src/tests/ResponseMocks/MusicBrainzMockResponse.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "release-groups": [ - { - "id": "9cf08bf9-1948-4087-abe1-783210ea1fae", - "primary-type-id": "f529b476-6e62-324f-b0aa-1f3e33d313fc", - "title": "Halo Halo", - "first-release-date": "2013-07-08", - "primary-type": "Album", - "artist-credit": [ - { - "name": "Halo Halo", - "artist": { - "name": "Halo Halo" - } - } - ], - "releases": [ - { - "id": "58dd1d57-2201-472e-9e36-5d497dcedb6f", - "title": "Halo Halo" - } - ] - } - ] -} diff --git a/src/tests/ResponseMocks/OMDBMockResponse.json b/src/tests/ResponseMocks/OMDBMockResponse.json deleted file mode 100644 index 6f43f5b..0000000 --- a/src/tests/ResponseMocks/OMDBMockResponse.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Search": [ - { - "Title": "Guardians of the Galaxy", - "Year": "2014", - "imdbID": "tt2015381", - "Type": "movie", - "Poster": "https://m.media-amazon.com/images/M/MV5BMTAwMjU5OTgxNjZeQTJeQWpwZ15BbWU4MDUxNDYxODEx._V1_SX300.jpg" - } - ], - "totalResults": "1", - "Response": "True" -} diff --git a/src/tests/ResponseMocks/SteamAPIMockResponse.json b/src/tests/ResponseMocks/SteamAPIMockResponse.json deleted file mode 100644 index af2fc9c..0000000 --- a/src/tests/ResponseMocks/SteamAPIMockResponse.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "applist": { - "apps": [ - { - "appid": 2076590, - "name": "Hooking Season Playtest" - }, - { - "appid": 2076600, - "name": "MonsterTamer" - } - ] - } -} diff --git a/src/tests/ResponseMocks/WikipediaMockResponse.json b/src/tests/ResponseMocks/WikipediaMockResponse.json deleted file mode 100644 index 7f8e91e..0000000 --- a/src/tests/ResponseMocks/WikipediaMockResponse.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "query": { - "searchinfo": { - "totalhits": 1199001, - "suggestion": "book", - "suggestionsnippet": "book" - }, - "search": [ - { - "ns": 0, - "title": "Book", - "pageid": 3778, - "size": 68829, - "wordcount": 8821, - "snippet": "called books or chapters or parts, are parts. The intellectual content in a physical book need not be a composition, nor even be called a book. Books can", - "timestamp": "2022-08-19T19:13:56Z" - } - ] - } -} diff --git a/src/tests/TestAPI.ts b/src/tests/TestAPI.ts deleted file mode 100644 index db7d11e..0000000 --- a/src/tests/TestAPI.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { APIModel } from '../api/APIModel'; -import { MediaTypeModel } from '../models/MediaTypeModel'; -import MediaDbPlugin from '../main'; - -export class TestAPI extends APIModel { - plugin: MediaDbPlugin; - - constructor(plugin: MediaDbPlugin) { - super(); - - this.plugin = plugin; - this.apiName = 'TestAPI'; - this.apiDescription = 'A test API for automated testing.'; - this.apiUrl = ''; - this.types = []; - } - - async getById(id: string): Promise { - return undefined; - } - - async searchByTitle(title: string): Promise { - return [] as MediaTypeModel[]; - } -} diff --git a/src/tests/mockHelpers.ts b/src/tests/mockHelpers.ts deleted file mode 100644 index 190942b..0000000 --- a/src/tests/mockHelpers.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { GameModel } from '../models/GameModel'; -import { MovieModel } from '../models/MovieModel'; -import { MusicReleaseModel } from '../models/MusicReleaseModel'; -import { WikiModel } from '../models/WikiModel'; -import { MediaType } from '../utils/MediaType'; -import { apiMock } from './ParameterizedAPI.test'; -import MALMockMovie from './ResponseMocks/MALMockMovie.json'; -import MusicBrainzResponseMock from './ResponseMocks/MusicBrainzMockResponse.json'; -import OMDBMockMovie from './ResponseMocks/OMDBMockResponse.json'; -import SteamAPIResponseMock from './ResponseMocks/SteamAPIMockResponse.json'; -import WikipediaMockResponse from './ResponseMocks/WikipediaMockResponse.json'; - -export function setWikipediaResponseMock() { - let ret = []; - let wikiresponse = WikipediaMockResponse.query.search[0]; - ret.push( - new WikiModel({ - type: 'wiki', - title: wikiresponse.title, - englishTitle: wikiresponse.title, - year: '', - dataSource: apiMock.apiName, - id: wikiresponse.pageid, - }), - ); - return ret; -} - -export function setOMDbResponseMock() { - let ret = []; - let omdbresponse = OMDBMockMovie.Search[0]; - ret.push( - new MovieModel({ - type: 'wiki', - title: omdbresponse.Title, - englishTitle: omdbresponse.Title, - year: omdbresponse.Year, - dataSource: apiMock.apiName, - id: omdbresponse.imdbID, - }), - ); - return ret; -} - -export function setMALResponseMock() { - let ret = []; - let result = MALMockMovie.data[0]; - ret.push( - new MovieModel({ - type: result.type, - title: result.title, - englishTitle: result.title_english, - year: result.aired.prop.from.year, - dataSource: apiMock.apiName, - id: result.mal_id, - }), - ); - return ret; -} - -export function setSteamResponseMock() { - let ret = []; - let steamResponse = SteamAPIResponseMock.applist.apps[0]; - ret.push( - new GameModel({ - type: MediaType.Game, - title: steamResponse.name, - englishTitle: steamResponse.name, - year: '', - dataSource: apiMock.apiName, - id: steamResponse.appid, - }), - ); - return ret; -} - -export function setMusicBrainzResponseMock() { - let ret = []; - let result = MusicBrainzResponseMock['release-groups'][0]; - ret.push( - new MusicReleaseModel({ - type: 'musicRelease', - title: result.title, - englishTitle: result.title, - year: new Date(result['first-release-date']).getFullYear().toString(), - dataSource: apiMock.apiName, - url: '', - id: result.id, - - artists: result['artist-credit'].map((a: any) => a.name), - subType: result['primary-type'], - } as MusicReleaseModel), - ); - return ret; -} diff --git a/src/tests/utils.test.ts b/src/tests/utils.test.ts deleted file mode 100644 index f8a422c..0000000 --- a/src/tests/utils.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { containsOnlyLettersAndUnderscores, replaceIllegalFileNameCharactersInString, wrapAround } from '../utils/Utils'; - -test('If wrapAround wraps correctly', () => { - expect(wrapAround(100, 5)).toBe(0); - expect(wrapAround(100, 7)).toBe(2); -}); - -test('If wrapAround errors out when dividing by zero', () => { - expect(wrapAround(100, 0)).toThrow(); -}); - -test('If wrapAround errors out when size is negative', () => { - expect(wrapAround(100, -5)).toThrow(); -}); - -test('Letter and underscore string validity', () => { - expect(containsOnlyLettersAndUnderscores('asdkfj_')).toBe(true); - expect(containsOnlyLettersAndUnderscores('asdkfj0')).toBe(false); -}); - -// since this is used to check if a string is a valid name for an object property, unicode characters shouldn't be allowed, thus the name of the function is misleading -test('Letter and underscore unicode char test', () => { - expect(containsOnlyLettersAndUnderscores('asdkaÈj')).toBe(true); - expect(containsOnlyLettersAndUnderscores('asdkaÈj0')).toBe(false); -}); - -test('Valid filename test', () => { - expect(replaceIllegalFileNameCharactersInString('what?is\\this:')).toBe('whatisthis -'); -}); diff --git a/src/utils/MediaTypeManager.ts b/src/utils/MediaTypeManager.ts index 50ec761..54679af 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -28,7 +28,7 @@ export class MediaTypeManager { mediaTemplateMap: Map; mediaFolderMap: Map; - constructor() { } + constructor() {} updateTemplates(settings: MediaDbPluginSettings): void { this.mediaFileNameTemplateMap = new Map(); diff --git a/src/utils/ModalHelper.ts b/src/utils/ModalHelper.ts index f1e9acc..6a4efa1 100644 --- a/src/utils/ModalHelper.ts +++ b/src/utils/ModalHelper.ts @@ -5,7 +5,6 @@ import { MediaDbSearchResultModal } from '../modals/MediaDbSearchResultModal'; import { Notice } from 'obsidian'; import MediaDbPlugin from '../main'; import { MediaDbPreviewModal } from 'src/modals/MediaDbPreviewModal'; -import { CreateNoteOptions } from './Utils'; import { MediaDbSearchModal } from '../modals/MediaDbSearchModal'; import { MediaType } from './MediaType'; diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 56cb080..f6b4e9f 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -145,7 +145,9 @@ export function markdownTable(content: string[][]): string { return table; } -export const fragWithHTML = (html: string) => createFragment(frag => (frag.createDiv().innerHTML = html)); +export function fragWithHTML(html: string): DocumentFragment { + return createFragment(frag => (frag.createDiv().innerHTML = html)); +} export function dateToString(date: Date): string { return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`; @@ -207,13 +209,13 @@ export function unCamelCase(str: string): string { // space before last upper in a sequence followed by lower .replace(/\b([A-Z]+)([A-Z])([a-z])/, '$1 $2$3') // uppercase the first character - .replace(/^./, function(str) { + .replace(/^./, function (str) { return str.toUpperCase(); }) ); } -export function hasTemplaterPlugin(app: App) { +export function hasTemplaterPlugin(app: App): boolean { const templater = (app as any).plugins.plugins['templater-obsidian']; return !!templater; @@ -221,7 +223,7 @@ export function hasTemplaterPlugin(app: App) { // Copied from https://github.com/anpigon/obsidian-book-search-plugin // Licensed under the MIT license. Copyright (c) 2020 Jake Runzer -export async function useTemplaterPluginInFile(app: App, file: TFile) { +export async function useTemplaterPluginInFile(app: App, file: TFile): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const templater = (app as any).plugins.plugins['templater-obsidian']; if (templater && !templater?.settings['trigger_on_file_creation']) { diff --git a/src/utils/YAMLConverter.ts b/src/utils/YAMLConverter.ts deleted file mode 100644 index 85ec21b..0000000 --- a/src/utils/YAMLConverter.ts +++ /dev/null @@ -1,43 +0,0 @@ -export class YAMLConverter { - static toYaml(obj: any): string { - let output = ''; - - for (const [key, value] of Object.entries(obj)) { - output += `${key}: ${YAMLConverter.toYamlString(value, 0)}\n`; - } - - return output; - } - - private static toYamlString(value: any, indentation: number): string { - if (value == null) { - return 'null'; - } - - if (typeof value === 'boolean') { - return value ? 'true' : 'false'; - } else if (typeof value === 'number') { - return value.toString(); - } else if (typeof value === 'string') { - return '"' + value.replace('"', '\\"') + '"'; - } else if (typeof value === 'object') { - let output = ''; - - if (Array.isArray(value)) { - for (const valueElement of value) { - output += `\n${YAMLConverter.calculateSpacing(indentation)} - ${YAMLConverter.toYamlString(valueElement, indentation + 1)}`; - } - } else { - for (const [objKey, objValue] of Object.entries(value)) { - output += `\n${YAMLConverter.calculateSpacing(indentation)} ${objKey}: ${YAMLConverter.toYamlString(objValue, indentation + 1)}`; - } - } - - return output; - } - } - - private static calculateSpacing(indentation: number): string { - return ' '.repeat(indentation * 4); - } -} diff --git a/tsconfig.json b/tsconfig.json index 162b4e8..c13a687 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,6 @@ { "compilerOptions": { - "types": ["svelte", "node", "jest"], "baseUrl": ".", - "inlineSourceMap": true, - "inlineSources": true, "module": "ESNext", "target": "ES6", "allowJs": true, @@ -11,9 +8,9 @@ "moduleResolution": "node", "importHelpers": true, "isolatedModules": true, - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "lib": ["DOM", "ES5", "ES6", "ES7", "Es2021"] + "lib": ["DOM", "ESNext"], + "types": ["svelte"], + "allowSyntheticDefaultImports": true }, - "include": ["**/*.ts"] + "include": ["src/**/*.ts", "tests/**/*.ts"] }