hello world! 🌸
All checks were successful
Playwright Tests / test (push) Successful in 2m27s

This commit is contained in:
commit 8c645a73b3
Signed by: tablet
GPG Key ID: 924A5F6AF051E87C
41 changed files with 5070 additions and 0 deletions

14
.editorconfig Normal file

@ -0,0 +1,14 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,json,ts,tsx,jsx}]
charset = utf-8
indent_style = tab
indent_size = 2
[*.yml]
indent_style = space
indent_size = 2

4
.gitattributes vendored Normal file

@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated

38
.github/workflows/playwright.yml vendored Normal file

@ -0,0 +1,38 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: |
apt-get update
apt-get install -y jq xdg-utils xvfb
curled=$(curl -L https://github.com/obsidianmd/obsidian-releases/raw/refs/heads/master/desktop-releases.json | jq .latestVersion | sed s/\"//g)
curl -Lo obsidian.deb "https://github.com/obsidianmd/obsidian-releases/releases/download/v$curled/obsidian_${curled}_amd64.deb"
corepack enable
yarn install
cd packages/test-project
yarn playwright install-deps
dpkg -i ../../obsidian.deb
- name: compile and test!
env:
XDG_CONFIG_HOME: /root/.config
run: |
yarn workspace obsidian-testing-framework run tsc
yarn workspace obsidian-sample-plugin run test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

4
.gitignore vendored Normal file

@ -0,0 +1,4 @@
test-results/
playwright-report/
node_modules
lib*

3
.vscode/settings.json vendored Normal file

@ -0,0 +1,3 @@
{
"nuxt.isNuxtApp": false
}

BIN
.yarn/install-state.gz vendored Normal file

Binary file not shown.

2
.yarnrc.yml Normal file

@ -0,0 +1,2 @@
nmMode: classic
nodeLinker: node-modules

2
README.md Normal file

@ -0,0 +1,2 @@
# Typescript module

8
package.json Normal file

@ -0,0 +1,8 @@
{
"name": "obsidian-testing-framework-parent",
"private": true,
"packageManager": "yarn@4.5.1",
"workspaces": [
"./packages/*"
]
}

@ -0,0 +1,43 @@
{
"name": "obsidian-testing-framework",
"packageManager": "yarn@4.5.1",
"dependencies": {
"@codemirror/language": "https://github.com/lishid/cm-language",
"@codemirror/state": "^6.0.1",
"@codemirror/view": "^6.0.1",
"asar": "^3.2.0",
"electron": "^33.0.2",
"playwright": "^1.48.1",
"tmp": "^0.2.3",
"typescript": "^5.6.3"
},
"version": "",
"exports": {
".": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"./utils": {
"types": "./lib/util.d.ts",
"default": "./lib/util.js"
},
"./fixture": {
"types": "./lib/fixtures.d.ts",
"default": "./lib/fixtures.js"
}
},
"main": "./lib/index.js",
"typings": "./lib/index.d.ts",
"scripts": {
"build": "tsc",
"lint": "tslint -c tslint.json src/**/*.ts",
"prepublish": "npm run build"
},
"devDependencies": {
"@playwright/test": "^1.48.1",
"@types/tmp": "^0",
"obsidian": "latest",
"vitest": "^2.1.3"
},
"type": "module"
}

@ -0,0 +1,11 @@
import { ElectronApplication, JSHandle, Page } from "playwright";
import { ObsidianTestingConfig } from "./index.js";
import { App } from "obsidian";
// import { getFile } from "./util.js";
export interface ObsidianTestFixtures {
electronApp: ElectronApplication;
page: Page;
obsidian: ObsidianTestingConfig;
appHandle: JSHandle<App>;
}

@ -0,0 +1,138 @@
import { test as base } from "@playwright/test";
import { _electron as electron } from "playwright";
import { Fixtures } from "@playwright/test";
import path from "path";
import { ObsidianTestFixtures } from "./fixtures.js";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { getApp, waitForIndexingComplete } from "./util.js";
import { execSync } from "child_process";
import { randomBytes } from "crypto";
export interface ObsidianTestingConfig {
vault?: string;
}
export function getExe(): string {
checkToy();
if (process.platform == "win32") {
return path.join(
process.env.LOCALAPPDATA!,
"Obsidian",
"Resources",
"app.asar"
);
}
const possibleDirs = [
"/opt/Obsidian",
"/usr/lib/Obsidian",
"/opt/obsidian",
"/usr/lib/obsidian",
"/var/lib/flatpak/app/md.obsidian.Obsidian/current/active/files",
"/snap/obsidian/current",
];
for (let i = 0; i < possibleDirs.length; i++) {
if (existsSync(possibleDirs[i])) {
console.log(execSync(`ls -l ${possibleDirs[i]}`).toString());
return path.join(possibleDirs[i], "resources", "app.asar");
}
}
return "";
}
function checkToy() {
if (process.platform == "darwin") {
throw new Error("use a non-toy operating system, dumbass");
}
}
function generateVaultConfig(vault: string) {
const vaultHash = randomBytes(16).toString("hex").toLocaleLowerCase();
let configLocation;
checkToy();
if (process.platform == "win32") {
configLocation = path.join(`${process.env.LOCALAPPDATA}`, "Obsidian");
} else {
configLocation = path.join(`${process.env.XDG_CONFIG_HOME}`, "obsidian");
try {
mkdirSync(configLocation, { recursive: true });
} catch (e) {}
}
const obsidianConfigFile = path.join(configLocation, "obsidian.json");
if (!existsSync(obsidianConfigFile)) {
writeFileSync(obsidianConfigFile, JSON.stringify({ vaults: {} }));
}
const json: {
vaults: {
[key: string]: {
path: string;
ts: number;
open?: boolean;
};
};
} = JSON.parse(readFileSync(obsidianConfigFile).toString());
if (!Object.values(json.vaults).some((a) => a.path === vault)) {
json.vaults[vaultHash] = {
path: vault,
ts: Date.now(),
};
writeFileSync(obsidianConfigFile, JSON.stringify(json));
writeFileSync(path.join(configLocation, `${vaultHash}.json`), "{}");
return vaultHash;
} else {
return Object.entries(json.vaults).find(a => a[1].path === vault)![0];
}
}
const obsidianTestFixtures: Fixtures<ObsidianTestFixtures> = {
electronApp: [
async ({ obsidian: { vault } }, run) => {
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
console.log("asar located at:", getExe());
let uriArg = "";
if (vault) {
let id = generateVaultConfig(vault);
if (!!id) {
uriArg = `obsidian://open?vault=${encodeURIComponent(id)}`;
}
}
const electronApp = await electron.launch({
timeout: 60000,
args: [getExe(), uriArg].filter((a) => !!a) as string[],
});
electronApp.on("console", async (msg) => {
console.log(
...(await Promise.all(msg.args().map((a) => a.jsonValue())))
);
});
await electronApp.waitForEvent("window");
await run(electronApp);
await electronApp.close();
},
{ timeout: 60000 },
],
page: [
async ({ electronApp }, run) => {
const windows = electronApp.windows();
// console.log("windows", windows);
let page = windows[windows.length - 1]!;
await page.waitForLoadState("domcontentloaded");
page = electronApp.windows()[electronApp.windows().length - 1]!;
await page.waitForEvent("load");
await page.waitForLoadState("domcontentloaded");
await waitForIndexingComplete(await getApp(page));
page.on("console", async (msg) => {
console.log(
...(await Promise.all(msg.args().map((a) => a.jsonValue())))
);
});
await run(page);
},
{ timeout: 60000 },
],
obsidian: [{}, { option: true }],
};
// @ts-ignore some error about a string type now having `undefined` as part of it's union
export const test = base.extend<ObsidianTestFixtures>(obsidianTestFixtures);

@ -0,0 +1,114 @@
import {
_electron,
_electron as electron,
ElectronApplication,
Page,
} from "playwright";
import {expect} from "vitest"
import { App, TFile, Vault } from "obsidian";
import { execSync } from "child_process";
import path from "path";
import { EOL } from "os";
type RawOptions = NonNullable<Parameters<typeof _electron.launch>[0]>;
export type TestOptions = Omit<RawOptions, "executablePath"> & {
vault: string;
};
export class ObsidianTester {
#loadPromise: Promise<void>;
public electronApp: ElectronApplication | null = null;
public page: Page;
public app: App;
constructor(options: Partial<TestOptions & { vault: string }>) {
const args = [...(options.args || [])];
if (options.vault)
args.push(`obsidian://open?vault=${encodeURI(options.vault)}`);
this.#loadPromise = new Promise((res, rej) => {
electron
.launch({
...options,
args,
executablePath: ObsidianTester.getExe(),
})
.then((v) => {
this.postInit(v).then(() => {
res();
});
})
.catch((e) => rej(e));
});
}
public get vault(): Vault {
return this.app.vault
}
public async doWithApp(
callback: (app: App) => Promise<unknown>
): Promise<unknown> {
await this.#loadPromise;
return await callback(this.app);
}
public async doWithVault(
callback: (vault: Vault) => Promise<unknown>
): Promise<unknown> {
await this.#loadPromise;
return await callback(this.app.vault);
}
public async assertFileEquals(path: string, expectedContent: string, cached: boolean = true) {
await this.#loadPromise;
const fileContent = await this.readFile(path, cached);
expect(fileContent).toEqual(this.normalizeEOL(expectedContent));
}
public async assertLineEquals(path: string, lineNumber: number, expectedContent: string, cached: boolean = true) {
await this.#loadPromise;
const fileContent = await this.readFile(path, cached);
expect(fileContent.split("\n")[lineNumber]).toEqual(this.normalizeEOL(expectedContent));
}
public async assertLinesEqual(filePath: string, start: number, end: number, expected: string, cached: boolean = true) {
await this.#loadPromise;
const fileContent = await this.readFile(filePath, cached);
const lines = fileContent.split("\n").slice(start, end);
const expectedLines = this.normalizeEOL(expected).split("\n");
expect(lines.every((l, i) => l == expectedLines[i])).toEqual(true);
}
public getFile(file: string): TFile {
let f = this.app.vault.getFileByPath(file);
if(!f) {
throw new Error("File does not exist in vault.");
}
return f;
}
normalizeEOL(str: string): string {
return str.split(/\r\n|\r|\n/).join("\n");
}
async readFile(path: string, cached: boolean = true): Promise<string> {
await this.#loadPromise;
const file = this.getFile(path);
return this.normalizeEOL(await (cached ? this.app.vault.cachedRead(file) : this.app.vault.read(file)));
}
private async postInit(electronApp: ElectronApplication) {
this.electronApp = electronApp;
this.page = await this.electronApp.firstWindow();
this.app = await this.page.evaluate<App>("window.app");
}
public static getExe(): string {
if (process.platform == "win32") {
return path.join(process.env.LOCALAPPDATA!, "Obsidian", "Obsidian.exe");
}
if (process.platform == "darwin") {
throw new Error("use a non-toy operating system, dumbass");
}
return execSync("command -v obsidian").toString();
}
}

@ -0,0 +1,99 @@
import { App, TFile } from "obsidian";
import { JSHandle, Page } from "playwright";
import { expect } from "@playwright/test";
// type TestArgs = Parameters<Parameters<typeof test>[2]>[0];
export async function assertFileEquals(
page: Page,
path: string,
expectedContent: string,
cached: boolean = true
) {
const fileContent = await readFile(page, path, cached);
expect(fileContent).toEqual(normalizeEOL(expectedContent));
}
export async function assertLineEquals(
page: Page,
path: string,
lineNumber: number,
expectedContent: string,
cached: boolean = true
) {
const fileContent = await readFile(page, path, cached);
expect(fileContent.split("\n")[lineNumber]).toEqual(
normalizeEOL(expectedContent)
);
}
export async function getApp(page: Page): Promise<JSHandle<App>> {
return await page.evaluateHandle("window.app");
}
export async function assertLinesEqual(
page: Page,
filePath: string,
start: number,
end: number,
expected: string,
cached: boolean = true
) {
const fileContent = await readFile(page, filePath, cached);
const lines = fileContent.split("\n").slice(start, end);
const expectedLines = normalizeEOL(expected).split("\n");
expect(lines.every((l, i) => l == expectedLines[i])).toEqual(true);
}
export function getFile(app: App, file: string): TFile {
let f = app.vault.getFileByPath(file);
if (!f) {
throw new Error("File does not exist in vault.");
}
return f;
}
function normalizeEOL(str: string): string {
return str.split(/\r\n|\r|\n/).join("\n");
}
export async function readFile(
app: Page,
path: string,
cached: boolean = true
): Promise<string> {
return normalizeEOL(
await doWithApp(app, (a) => {
const file = getFile(a, path);
return cached ? a.vault.cachedRead(file) : a.vault.read(file);
})
);
}
export async function doWithApp<T = unknown>(
page: Page,
callback: (a: App) => T | Promise<T>
): Promise<T> {
const cbStr = callback.toString();
return await page.evaluate<T, string>(async (cb) => {
const func = new Function(`return (${cb})(window.app)`)
return await func();
}, cbStr);
}
export function waitForIndexingComplete(appHandle: JSHandle<App>) {
return appHandle.evaluate(() => {
return new Promise((res2, rej2) => {
let resolved = false;
app.metadataCache.on("resolved", () => {
res2(null);
resolved = true;
});
setTimeout(() => !resolved && rej2("timeout"), 10000);
});
});
}
export const pageUtils = {
getFile,
}

@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"lib": [
"es2015",
"DOM",
"ESNext.Disposable"
],
"outDir": "lib",
"baseUrl": ".",
"strict": true,
"alwaysStrict": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"downlevelIteration": true,
"declaration": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"pretty": true,
"types": ["node"],
},
"include": [
"typings/**/*",
"src/**/*"
],
"exclude": ["packages/test-project/**/*.ts"]
}

@ -0,0 +1,6 @@
import {App} from "obsidian";
declare global {
var app: App;
var __callback: <T>(app: App) => T;
}
export {}

@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

@ -0,0 +1,3 @@
node_modules/
main.js

@ -0,0 +1,23 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

26
packages/test-project/.gitignore vendored Normal file

@ -0,0 +1,26 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

@ -0,0 +1 @@
tag-version-prefix=""

@ -0,0 +1,96 @@
# Obsidian Sample Plugin
This is a sample plugin for Obsidian (https://obsidian.md).
This project uses TypeScript to provide type checking and documentation.
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
**Note:** The Obsidian API is still in early alpha and is subject to change at any time!
This sample plugin demonstrates some of the basic functionality the plugin API can do.
- Adds a ribbon icon, which shows a Notice when clicked.
- Adds a command "Open Sample Modal" which opens a Modal.
- Adds a plugin setting tab to the settings page.
- Registers a global click event and output 'click' to the console.
- Registers a global interval which logs 'setInterval' to the console.
## First time developing plugins?
Quick starting guide for new plugin devs:
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
- Install NodeJS, then run `npm i` in the command line under your repo folder.
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
- Reload Obsidian to load the new version of your plugin.
- Enable plugin in settings window.
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
## Releasing new releases
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
- Publish the release.
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
## Adding your plugin to the community plugin list
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
- Publish an initial version.
- Make sure you have a `README.md` file in the root of your repo.
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
## How to use
- Clone this repo.
- Make sure your NodeJS is at least v16 (`node --version`).
- `npm i` or `yarn` to install dependencies.
- `npm run dev` to start compilation in watch mode.
## Manually installing the plugin
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
## Improve code quality with eslint (optional)
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
- To use eslint with this project, make sure to install eslint from terminal:
- `npm install -g eslint`
- To use eslint to analyze this project use this command:
- `eslint main.ts`
- eslint will then create a report with suggestions for code improvement by file and line number.
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
- `eslint .\src\`
## Funding URL
You can include funding URLs where people who use your plugin can financially support it.
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
```json
{
"fundingUrl": "https://buymeacoffee.com"
}
```
If you have multiple URLs, you can also do:
```json
{
"fundingUrl": {
"Buy Me a Coffee": "https://buymeacoffee.com",
"GitHub Sponsor": "https://github.com/sponsors",
"Patreon": "https://www.patreon.com/"
}
}
```
## API Documentation
See https://github.com/obsidianmd/obsidian-api

@ -0,0 +1,22 @@
import { expect } from '@playwright/test';
import {test} from "obsidian-testing-framework"
import {doWithApp, getApp} from "obsidian-testing-framework/utils";
test('something', async ({ page }) => {
console.log(page.url());
expect(/obsidian\.md/i.test(page.url())).toBeTruthy()
});
test("idk", async({page}) => {
console.log("idk")
let what = await doWithApp(page,async (app) => {
console.log("hi", Object.keys(app), (app.metadataCache));
let thing = app.metadataCache.getFirstLinkpathDest("Welcome", "/");
console.log("THING", thing);
await new Promise(res => setTimeout(res, 5000))
console.log(thing?.path);
const what = {...thing};
delete what.parent;
delete what.vault;
return what;
});
expect(what.basename).toEqual("Welcome")
})

@ -0,0 +1,49 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["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",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

@ -0,0 +1,134 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
// Remember to rename these classes and interfaces!
interface MyPluginSettings {
mySetting: string;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
async onload() {
await this.loadSettings();
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice('This is a notice!', 0);
});
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new SampleModal(this.app).open();
}
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
if (!checking) {
new SampleModal(this.app).open();
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
}
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
});
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
}
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
}
}

@ -0,0 +1,11 @@
{
"id": "sample-plugin",
"name": "Sample Plugin",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Demonstrates some of the capabilities of the Obsidian API.",
"author": "Obsidian",
"authorUrl": "https://obsidian.md",
"fundingUrl": "https://obsidian.md/pricing",
"isDesktopOnly": false
}

@ -0,0 +1,34 @@
{
"name": "obsidian-sample-plugin",
"version": "1.0.0",
"private": true,
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"test": "xvfb-maybe playwright test"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"obsidian-testing-framework": "workspace:^"
},
"devDependencies": {
"@playwright/test": "^1.48.1",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"playwright": "^1.48.1",
"tslib": "2.4.0",
"typescript": "4.7.4",
"vitest": "^2.1.3",
"xvfb-maybe": "^0.2.1"
},
"type": "module"
}

@ -0,0 +1,52 @@
import { defineConfig, devices } from '@playwright/test';
import {ObsidianTestFixtures} from "obsidian-testing-framework/fixture";
import os from "os"
import path from 'path';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig<ObsidianTestFixtures>({
testDir: './e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: 1,
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
obsidian: {
vault: path.resolve(process.cwd(), "..", "..", "vault")
}
},
/* Configure projects for major browsers */
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

@ -0,0 +1,437 @@
import { test, expect, type Page } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('https://demo.playwright.dev/todomvc');
});
const TODO_ITEMS = [
'buy some cheese',
'feed the cat',
'book a doctors appointment'
] as const;
test.describe('New Todo', () => {
test('should allow me to add todo items', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create 1st todo.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
// Make sure the list only has one todo item.
await expect(page.getByTestId('todo-title')).toHaveText([
TODO_ITEMS[0]
]);
// Create 2nd todo.
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press('Enter');
// Make sure the list now has two todo items.
await expect(page.getByTestId('todo-title')).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[1]
]);
await checkNumberOfTodosInLocalStorage(page, 2);
});
test('should clear text input field when an item is added', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create one todo item.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
// Check that input is empty.
await expect(newTodo).toBeEmpty();
await checkNumberOfTodosInLocalStorage(page, 1);
});
test('should append new items to the bottom of the list', async ({ page }) => {
// Create 3 items.
await createDefaultTodos(page);
// create a todo count locator
const todoCount = page.getByTestId('todo-count')
// Check test using different methods.
await expect(page.getByText('3 items left')).toBeVisible();
await expect(todoCount).toHaveText('3 items left');
await expect(todoCount).toContainText('3');
await expect(todoCount).toHaveText(/3/);
// Check all items in one call.
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
await checkNumberOfTodosInLocalStorage(page, 3);
});
});
test.describe('Mark all as completed', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test.afterEach(async ({ page }) => {
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should allow me to mark all items as completed', async ({ page }) => {
// Complete all todos.
await page.getByLabel('Mark all as complete').check();
// Ensure all todos have 'completed' class.
await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
});
test('should allow me to clear the complete state of all items', async ({ page }) => {
const toggleAll = page.getByLabel('Mark all as complete');
// Check and then immediately uncheck.
await toggleAll.check();
await toggleAll.uncheck();
// Should be no completed classes.
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
});
test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
const toggleAll = page.getByLabel('Mark all as complete');
await toggleAll.check();
await expect(toggleAll).toBeChecked();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Uncheck first todo.
const firstTodo = page.getByTestId('todo-item').nth(0);
await firstTodo.getByRole('checkbox').uncheck();
// Reuse toggleAll locator and make sure its not checked.
await expect(toggleAll).not.toBeChecked();
await firstTodo.getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Assert the toggle all is checked again.
await expect(toggleAll).toBeChecked();
});
});
test.describe('Item', () => {
test('should allow me to mark items as complete', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
// Check first item.
const firstTodo = page.getByTestId('todo-item').nth(0);
await firstTodo.getByRole('checkbox').check();
await expect(firstTodo).toHaveClass('completed');
// Check second item.
const secondTodo = page.getByTestId('todo-item').nth(1);
await expect(secondTodo).not.toHaveClass('completed');
await secondTodo.getByRole('checkbox').check();
// Assert completed class.
await expect(firstTodo).toHaveClass('completed');
await expect(secondTodo).toHaveClass('completed');
});
test('should allow me to un-mark items as complete', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
const firstTodo = page.getByTestId('todo-item').nth(0);
const secondTodo = page.getByTestId('todo-item').nth(1);
const firstTodoCheckbox = firstTodo.getByRole('checkbox');
await firstTodoCheckbox.check();
await expect(firstTodo).toHaveClass('completed');
await expect(secondTodo).not.toHaveClass('completed');
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await firstTodoCheckbox.uncheck();
await expect(firstTodo).not.toHaveClass('completed');
await expect(secondTodo).not.toHaveClass('completed');
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
});
test('should allow me to edit an item', async ({ page }) => {
await createDefaultTodos(page);
const todoItems = page.getByTestId('todo-item');
const secondTodo = todoItems.nth(1);
await secondTodo.dblclick();
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
// Explicitly assert the new text value.
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2]
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
});
test.describe('Editing', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should hide other controls when editing', async ({ page }) => {
const todoItem = page.getByTestId('todo-item').nth(1);
await todoItem.dblclick();
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
await expect(todoItem.locator('label', {
hasText: TODO_ITEMS[1],
})).not.toBeVisible();
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should save edits on blur', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
test('should trim entered text', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
test('should remove the item if an empty text string was entered', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[2],
]);
});
test('should cancel edits on escape', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
await expect(todoItems).toHaveText(TODO_ITEMS);
});
});
test.describe('Counter', () => {
test('should display the current number of todo items', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// create a todo count locator
const todoCount = page.getByTestId('todo-count')
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
await expect(todoCount).toContainText('1');
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press('Enter');
await expect(todoCount).toContainText('2');
await checkNumberOfTodosInLocalStorage(page, 2);
});
});
test.describe('Clear completed button', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
});
test('should display the correct text', async ({ page }) => {
await page.locator('.todo-list li .toggle').first().check();
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
});
test('should remove completed items when clicked', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).getByRole('checkbox').check();
await page.getByRole('button', { name: 'Clear completed' }).click();
await expect(todoItems).toHaveCount(2);
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test('should be hidden when there are no items that are completed', async ({ page }) => {
await page.locator('.todo-list li .toggle').first().check();
await page.getByRole('button', { name: 'Clear completed' }).click();
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
});
});
test.describe('Persistence', () => {
test('should persist its data', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
const todoItems = page.getByTestId('todo-item');
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
await firstTodoCheck.check();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(['completed', '']);
// Ensure there is 1 completed item.
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
// Now reload.
await page.reload();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(['completed', '']);
});
});
test.describe('Routing', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
// make sure the app had a chance to save updated todos in storage
// before navigating to a new view, otherwise the items can get lost :(
// in some frameworks like Durandal
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
});
test('should allow me to display active items', async ({ page }) => {
const todoItem = page.getByTestId('todo-item');
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Active' }).click();
await expect(todoItem).toHaveCount(2);
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test('should respect the back button', async ({ page }) => {
const todoItem = page.getByTestId('todo-item');
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await test.step('Showing all items', async () => {
await page.getByRole('link', { name: 'All' }).click();
await expect(todoItem).toHaveCount(3);
});
await test.step('Showing active items', async () => {
await page.getByRole('link', { name: 'Active' }).click();
});
await test.step('Showing completed items', async () => {
await page.getByRole('link', { name: 'Completed' }).click();
});
await expect(todoItem).toHaveCount(1);
await page.goBack();
await expect(todoItem).toHaveCount(2);
await page.goBack();
await expect(todoItem).toHaveCount(3);
});
test('should allow me to display completed items', async ({ page }) => {
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Completed' }).click();
await expect(page.getByTestId('todo-item')).toHaveCount(1);
});
test('should allow me to display all items', async ({ page }) => {
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Active' }).click();
await page.getByRole('link', { name: 'Completed' }).click();
await page.getByRole('link', { name: 'All' }).click();
await expect(page.getByTestId('todo-item')).toHaveCount(3);
});
test('should highlight the currently applied filter', async ({ page }) => {
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
//create locators for active and completed links
const activeLink = page.getByRole('link', { name: 'Active' });
const completedLink = page.getByRole('link', { name: 'Completed' });
await activeLink.click();
// Page change - active items.
await expect(activeLink).toHaveClass('selected');
await completedLink.click();
// Page change - completed items.
await expect(completedLink).toHaveClass('selected');
});
});
async function createDefaultTodos(page: Page) {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
}
async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
return await page.waitForFunction(e => {
return JSON.parse(localStorage['react-todos']).length === e;
}, expected);
}
async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
return await page.waitForFunction(e => {
return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
}, expected);
}
async function checkTodosInLocalStorage(page: Page, title: string) {
return await page.waitForFunction(t => {
return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
}, title);
}

@ -0,0 +1,24 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "Node16",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "Node16",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
],
},
"include": [
"**/*.ts"
]
}

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}

1
vault/.obsidian/app.json vendored Normal file

@ -0,0 +1 @@
{}

1
vault/.obsidian/appearance.json vendored Normal file

@ -0,0 +1 @@
{}

@ -0,0 +1,3 @@
[
"obsidian-advanced-uri"
]

30
vault/.obsidian/core-plugins.json vendored Normal file

@ -0,0 +1,30 @@
{
"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
}

22
vault/.obsidian/graph.json vendored Normal file

@ -0,0 +1,22 @@
{
"collapse-filter": true,
"search": "",
"showTags": false,
"showAttachments": false,
"hideUnresolved": false,
"showOrphans": true,
"collapse-color-groups": true,
"colorGroups": [],
"collapse-display": true,
"showArrow": false,
"textFadeMultiplier": 0,
"nodeSizeMultiplier": 1,
"lineSizeMultiplier": 1,
"collapse-forces": true,
"centerStrength": 0.518713248970312,
"repelStrength": 10,
"linkStrength": 1,
"linkDistance": 250,
"scale": 1,
"close": false
}

0
vault/.obsidian/plugins/.gitkeep vendored Normal file

159
vault/.obsidian/workspace.json vendored Normal file

@ -0,0 +1,159 @@
{
"main": {
"id": "930871bd163b9a7a",
"type": "split",
"children": [
{
"id": "634dfa4a86e61f7f",
"type": "tabs",
"children": [
{
"id": "b123fea9f84365d8",
"type": "leaf",
"state": {
"type": "empty",
"state": {},
"icon": "lucide-file",
"title": "New tab"
}
}
]
}
],
"direction": "vertical"
},
"left": {
"id": "a21ebe5947a5c71f",
"type": "split",
"children": [
{
"id": "32175daed5963aa7",
"type": "tabs",
"children": [
{
"id": "868b1c060db3a110",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical"
},
"icon": "lucide-folder-closed",
"title": "Files"
}
},
{
"id": "4bc75e71b51e11b0",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "Search"
}
},
{
"id": "6d851af11dfb13da",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {},
"icon": "lucide-bookmark",
"title": "Bookmarks"
}
}
]
}
],
"direction": "horizontal",
"width": 300
},
"right": {
"id": "5d6a3e51ae6c758f",
"type": "split",
"children": [
{
"id": "1c37035be69547e5",
"type": "tabs",
"children": [
{
"id": "33ec4bd3963497ab",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "Backlinks"
}
},
{
"id": "2487192d0b99fdd8",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "Outgoing links"
}
},
{
"id": "fec498f9a6d0f797",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true
},
"icon": "lucide-tags",
"title": "Tags"
}
},
{
"id": "5e98b7f33c5bb964",
"type": "leaf",
"state": {
"type": "outline",
"state": {},
"icon": "lucide-list",
"title": "Outline"
}
}
]
}
],
"direction": "horizontal",
"width": 300,
"collapsed": true
},
"left-ribbon": {
"hiddenItems": {
"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": "b123fea9f84365d8",
"lastOpenFiles": []
}

5
vault/Welcome.md Normal file

@ -0,0 +1,5 @@
This is your new *vault*.
Make a note of something, [[create a link]], or try [the Importer](https://help.obsidian.md/Plugins/Importer)!
When you're ready, delete this note and make the vault your own.

3382
yarn.lock Normal file

File diff suppressed because it is too large Load Diff