├── .npmrc ├── global.d.ts ├── .eslintignore ├── src ├── types │ └── qrcode.d.ts ├── modals │ ├── index.ts │ ├── utils.ts │ ├── ErrorModal.ts │ ├── LoadingModal.ts │ ├── PreviewModal.ts │ ├── MessageModal.ts │ └── PublishResultModal.ts ├── interface │ └── index.ts ├── actions │ ├── unpublish.ts │ ├── send.ts │ ├── save.ts │ ├── publish.ts │ ├── insert-metadata.ts │ ├── preview.ts │ ├── aigen.ts │ ├── set-channel.ts │ └── index.ts ├── i18n │ ├── index.ts │ └── lang │ │ ├── zh.json │ │ ├── zh-tw.json │ │ ├── ja.json │ │ └── en.json ├── oauth │ └── oauth.js ├── setting │ └── index.ts ├── frontmatter.ts └── util.ts ├── .editorconfig ├── manifest.json ├── .gitignore ├── styles.css ├── tsconfig.json ├── version-bump.mjs ├── .eslintrc ├── package.json ├── versions.json ├── esbuild.config.mjs ├── README.md ├── INSTALL.md ├── .github └── workflows │ └── release.yaml ├── main.ts ├── LICENSE └── pnpm-lock.yaml /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'quail-js'; -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /src/types/qrcode.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'qrcode'; -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /src/modals/index.ts: -------------------------------------------------------------------------------- 1 | export { ErrorModal } from './ErrorModal'; 2 | export { MessageModal } from './MessageModal'; 3 | export { PublishResultModal } from './PublishResultModal'; 4 | export { LoadingModal } from './LoadingModal'; 5 | export { PreviewModal } from './PreviewModal'; -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "quail", 3 | "name": "Quaily", 4 | "version": "2.0.14", 5 | "minAppVersion": "1.0.0", 6 | "description": "Save, publish, delivery notes via Quaily.com as newsletters and blogs.", 7 | "author": "Lyric", 8 | "authorUrl": "https://quaily.com", 9 | "isDesktopOnly": false 10 | } -------------------------------------------------------------------------------- /src/modals/utils.ts: -------------------------------------------------------------------------------- 1 | export function constructModalTitle(container: HTMLElement, text: string) { 2 | const title = container.createEl('h2', { 3 | text: text 4 | }); 5 | Object.assign(title.style, { 6 | margin: '0 0 1rem 0', 7 | fontSize: '1.1rem', 8 | fontWeight: 'bold', 9 | textAlign: 'center', 10 | }); 11 | return title; 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | 24 | *.sh -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This CSS file will be included with your plugin, and 4 | available in the app when your plugin is enabled. 5 | 6 | If your plugin does not need CSS, delete this file. 7 | 8 | */ 9 | pre.error-message { 10 | margin-bottom: 1rem; 11 | background: rgba(255, 100, 100, 0.2); 12 | padding: 0.5rem 1rem; 13 | border-radius: 2px; 14 | border: 1px solid rgba(255,100,100,0.4); 15 | white-space: normal; 16 | word-break: break-word; 17 | } 18 | pre.error-message code { 19 | color: #e13838; 20 | } 21 | .text-center { 22 | text-align: center; 23 | } 24 | a { 25 | color: inherit; 26 | text-decoration: none; 27 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strictNullChecks": true, 15 | "resolveJsonModule": true, 16 | "lib": [ 17 | "DOM", 18 | "ES5", 19 | "ES6", 20 | "ES7" 21 | ] 22 | }, 23 | "include": [ 24 | "**/*.ts" 25 | , "src/actions/index.ts" ] 26 | } 27 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off", 22 | "@typescript-eslint/no-explicit-any": "off" 23 | } 24 | } -------------------------------------------------------------------------------- /src/interface/index.ts: -------------------------------------------------------------------------------- 1 | export interface QuailPluginSettings { 2 | listID: string; 3 | listSlug: string; 4 | 5 | accessToken: string; 6 | refreshToken: string; 7 | tokenExpiry: string; 8 | 9 | me: any; 10 | lists: any; 11 | 12 | strictLineBreaks: boolean; 13 | useEnglishCmds: boolean; 14 | useFirstImageAsCover: boolean; 15 | } 16 | 17 | export interface QuailImageItem { 18 | pathname: string; 19 | formalized_pathname: string; 20 | name: string; 21 | data: ArrayBuffer; 22 | mimeType: string; 23 | } 24 | 25 | export interface QuailPlugin { 26 | settings: QuailPluginSettings; 27 | client: any; 28 | auxiliaClient: any; 29 | 30 | onload(): Promise; 31 | onunload(): void; 32 | getClients(): void; 33 | loadActions(): Promise; 34 | loadSettings(): Promise; 35 | saveSettings(): Promise; 36 | updateChannels(): Promise; 37 | login(): Promise; 38 | refreshToken(): Promise; 39 | clearTokens(): Promise; 40 | isLogged(): boolean; 41 | updateToken(): void; 42 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-quail-plugin", 3 | "version": "2.0.14", 4 | "description": "Save, publish, delivery notes via Quaily as newsletters and blogs. This is a quail (https://quaily.com) plugin for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "AGPL-3.0", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@types/qrcode": "^1.5.5", 17 | "@typescript-eslint/eslint-plugin": "5.29.0", 18 | "@typescript-eslint/parser": "5.29.0", 19 | "builtin-modules": "3.3.0", 20 | "esbuild": "0.17.3", 21 | "obsidian": "latest", 22 | "quail-js": "0.3.18", 23 | "tslib": "2.4.0", 24 | "typescript": "4.7.4" 25 | }, 26 | "dependencies": { 27 | "dayjs": "^1.11.7", 28 | "qrcode": "^1.5.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.1": "1.0.0", 3 | "1.0.2": "1.0.0", 4 | "1.0.3": "1.0.0", 5 | "1.0.4": "1.0.0", 6 | "1.0.5": "1.0.0", 7 | "1.0.6": "1.0.0", 8 | "1.0.7": "1.0.0", 9 | "1.0.8": "1.0.0", 10 | "1.0.9": "1.0.0", 11 | "1.0.10": "1.0.0", 12 | "1.0.11": "1.0.0", 13 | "1.0.12": "1.0.0", 14 | "1.0.13": "1.0.0", 15 | "1.0.14": "1.0.0", 16 | "1.0.15": "1.0.0", 17 | "1.0.16": "1.0.0", 18 | "1.0.17": "1.0.0", 19 | "1.0.18": "1.0.0", 20 | "1.0.19": "1.0.0", 21 | "1.0.20": "1.0.0", 22 | "1.0.21": "1.0.0", 23 | "1.1.0": "1.0.0", 24 | "1.1.1": "1.0.0", 25 | "1.1.2": "1.0.0", 26 | "1.1.3": "1.0.0", 27 | "1.1.4": "1.0.0", 28 | "1.1.5": "1.0.0", 29 | "1.1.6": "1.0.0", 30 | "1.1.7": "1.0.0", 31 | "2.0.0": "1.0.0", 32 | "2.0.1": "1.0.0", 33 | "2.0.2": "1.0.0", 34 | "2.0.3": "1.0.0", 35 | "2.0.4": "1.0.0", 36 | "2.0.5": "1.0.0", 37 | "2.0.6": "1.0.0", 38 | "2.0.7": "1.0.0", 39 | "2.0.8": "1.0.0", 40 | "2.0.9": "1.0.0", 41 | "2.0.10": "1.0.0", 42 | "2.0.11": "1.0.0", 43 | "2.0.12": "1.0.0", 44 | "2.0.13": "1.0.0", 45 | "2.0.14": "1.0.0" 46 | } -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === "production"); 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@electron/remote", 24 | "@codemirror/autocomplete", 25 | "@codemirror/collab", 26 | "@codemirror/commands", 27 | "@codemirror/language", 28 | "@codemirror/lint", 29 | "@codemirror/search", 30 | "@codemirror/state", 31 | "@codemirror/view", 32 | "@lezer/common", 33 | "@lezer/highlight", 34 | "@lezer/lr", 35 | ...builtins], 36 | format: "cjs", 37 | target: "es2018", 38 | logLevel: "info", 39 | sourcemap: prod ? false : "inline", 40 | treeShaking: true, 41 | outfile: "main.js", 42 | }); 43 | 44 | if (prod) { 45 | await context.rebuild(); 46 | process.exit(0); 47 | } else { 48 | await context.watch(); 49 | } -------------------------------------------------------------------------------- /src/actions/unpublish.ts: -------------------------------------------------------------------------------- 1 | import { App, Notice } from 'obsidian'; 2 | import { QuailPluginSettings } from '../interface'; 3 | import { LoadingModal, ErrorModal } from '../modals'; 4 | import util from '../util'; 5 | import { t, english } from 'src/i18n'; 6 | 7 | 8 | export default function unpublish(app: App, client: any, settings: QuailPluginSettings) { 9 | let name = english('actions.unpublish'); 10 | if (!settings.useEnglishCmds) { 11 | name = t('actions.unpublish'); 12 | } 13 | 14 | return { 15 | id: 'unpublish', 16 | name: name, 17 | callback: async () => { 18 | const { frontmatter, err } = await util.getActiveFileContent(app); 19 | if (err != null) { 20 | new ErrorModal(app, new Error(err)).open(); 21 | return; 22 | } 23 | 24 | const loadingModal = new LoadingModal(app) 25 | loadingModal.open(); 26 | 27 | try { 28 | await client.unpublishPost(settings.listID, frontmatter?.slug); 29 | new Notice(t('notices.unpublish_success')); 30 | } catch (e) { 31 | loadingModal.close(); 32 | new ErrorModal(app, e).open(); 33 | } finally { 34 | loadingModal.close(); 35 | } 36 | } 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/actions/send.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'obsidian'; 2 | import { QuailPluginSettings } from '../interface'; 3 | import { LoadingModal, MessageModal, ErrorModal } from '../modals'; 4 | import util from '../util'; 5 | import { english, t } from 'src/i18n'; 6 | export default function send(app: App, client: any, settings: QuailPluginSettings) { 7 | let name = english('actions.send'); 8 | if (!settings.useEnglishCmds) { 9 | name = t('actions.send'); 10 | } 11 | 12 | return { 13 | id: 'deliver', 14 | name: name, 15 | callback: async () => { 16 | const { frontmatter, err } = await util.getActiveFileContent(app); 17 | if (err != null) { 18 | new ErrorModal(app, new Error(err.toString())).open(); 19 | return; 20 | } 21 | 22 | const loadingModal = new LoadingModal(app) 23 | loadingModal.open(); 24 | 25 | try { 26 | await client.deliverPost(settings.listID, frontmatter?.slug) 27 | new MessageModal(app, { 28 | title: t('message_modal.send_post.title'), 29 | message: t('message_modal.send_post.desc'), 30 | icon: '🚀', 31 | }).open(); 32 | } catch (e) { 33 | loadingModal.close(); 34 | new ErrorModal(app, e).open(); 35 | return; 36 | } finally { 37 | loadingModal.close(); 38 | } 39 | } 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/actions/save.ts: -------------------------------------------------------------------------------- 1 | import { App, Notice } from 'obsidian'; 2 | import { LoadingModal, ErrorModal, PublishResultModal } from '../modals'; 3 | import { QuailPluginSettings } from '../interface'; 4 | import { savePost } from './index'; 5 | import { t, english } from 'src/i18n'; 6 | export default function save(app: App, client: any, auxiliaClient: any, settings: QuailPluginSettings) { 7 | let name = english('actions.save'); 8 | if (!settings.useEnglishCmds) { 9 | name = t('actions.save'); 10 | } 11 | 12 | return { 13 | id: 'save', 14 | name: name, 15 | callback: async () => { 16 | const loadingModal = new LoadingModal(app) 17 | loadingModal.open(); 18 | 19 | let pt: any = null; 20 | try { 21 | pt = await savePost(app, client, auxiliaClient, settings); 22 | } catch (e) { 23 | new ErrorModal(app, e).open(); 24 | loadingModal.close(); 25 | return; 26 | } finally { 27 | loadingModal.close(); 28 | } 29 | 30 | const slug = pt?.slug || ''; 31 | if (slug) { 32 | if (pt?.published_at !== null) { 33 | const payload:any = { 34 | title: pt.title, 35 | summary: pt.summary, 36 | coverImageUrl: pt.cover_image_url, 37 | url: `https://quaily.com/${settings.listSlug}/p/${slug}` 38 | } 39 | new PublishResultModal(app, client, payload).open(); 40 | } else { 41 | new Notice(t('notices.post_saved')); 42 | } 43 | } 44 | } 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import en from './lang/en.json' 2 | import ja from './lang/ja.json' 3 | import zh from './lang/zh.json' 4 | import zhTw from './lang/zh-tw.json' 5 | 6 | const supportedLangs = [ 7 | 'en', 'ja', 'zh', 'zh-tw', 8 | ]; 9 | 10 | export const messages:any = { 11 | en, ja, zh, 'zh-tw': zhTw, 12 | } 13 | 14 | function detectLang() { 15 | let lang = window.localStorage.getItem('language'); 16 | if (lang && supportedLangs.includes(lang)) { 17 | return lang 18 | } 19 | 20 | lang = navigator.language.toLowerCase(); 21 | if (lang.length > 5) { 22 | lang = lang.substring(0, 5); 23 | } 24 | 25 | if (lang && supportedLangs.includes(lang)) { 26 | return lang 27 | } else { 28 | return 'en' 29 | } 30 | } 31 | 32 | function _t(lang: string, name: string, data?: Record) { 33 | const locale = messages[lang]; 34 | let msg = ""; 35 | 36 | if (locale) { 37 | msg = locale[name]; 38 | if (typeof msg === "undefined") { 39 | msg = messages.en[name]; 40 | if (typeof msg === "undefined") { 41 | msg = name; 42 | } 43 | } 44 | } 45 | 46 | if (data) { 47 | for (const key in data) { 48 | msg = msg.replace(`{${key}}`, data[key]); 49 | } 50 | } 51 | 52 | return msg; 53 | } 54 | 55 | function t(name: string, data?: Record) { 56 | const lang = detectLang(); 57 | return _t(lang, name, data); 58 | } 59 | 60 | function english(name: string, data?: Record) { 61 | // always return english 62 | return _t('en', name, data); 63 | } 64 | 65 | export { 66 | t, 67 | detectLang, 68 | english, 69 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Plugin for [Quaily.com](https://quaily.com) 2 | 3 | This is a plugin for [Quaily.com](https://quaily.com), a publishing service, that allows you to publish your Obsidian notes to Quaily as newsletters or blog posts. 4 | 5 | ## Getting Started 6 | 7 | ### Blog Posts 8 | - [🧩 Introducing the Improved Obsidian Quail Plugin](https://quaily.com/blog/p/introducing-the-new-and-improved-obsidian-quail-plugin) 9 | - [📝 Write with Favorite Editor and Publish to Quaily](https://quaily.com/blog/p/write-with-favorite-editor-and-publish-to-quaily) 10 | 11 | ### Documentation 12 | 13 | - [Obsidian Plugin - Quaily documentation](https://docs.quaily.com/writer/obsidian-plugin.html) 14 | 15 | ## Screenshots 16 | 17 | Commands: 18 | 19 | ![](https://static.quail.ink/media/19zumyzr.webp) 20 | 21 | Publish Result: 22 | 23 | ![](https://static.quail.ink/media/18eu4z68.webp) 24 | 25 | 26 | ## Features 27 | 28 | Use `Ctrl/Cmd + P` to open the command palette and search for `Quaily` to see all available commands. 29 | 30 | - Multiple languages support 31 | - Multiple channels support 32 | - Publish/unpublish posts to https://quaily.com 33 | - Deliver published posts to subscribers 34 | - Generate summary and tags by AI 35 | - Preview on mobile devices or desktop browsers 36 | 37 | ## Installation 38 | 39 | ### Install from Obsidian community plugins 40 | 41 | 1. Open Obsidian 42 | 2. Go to Settings > Community plugins, search for "Quail" 43 | 3. Click on the "Install" button next to this plugin 44 | 4. Click on the "Enable" button to activate the plugin 45 | 46 | ### Install from source code 47 | 48 | Please refer to [INSTALL.md](INSTALL.md). 49 | 50 | ## License 51 | 52 | This project is licensed under the AGPL-3.0 License - see the [LICENSE](LICENSE) file for details 53 | -------------------------------------------------------------------------------- /src/actions/publish.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'obsidian'; 2 | import { LoadingModal, ErrorModal, PublishResultModal } from '../modals'; 3 | import { QuailPluginSettings } from '../interface'; 4 | import { savePost } from './index'; 5 | import { t, english } from 'src/i18n'; 6 | export default function publish(app: App, client: any, auxiliaClient: any, settings: QuailPluginSettings) { 7 | let name = english('actions.publish'); 8 | if (!settings.useEnglishCmds) { 9 | name = t('actions.publish'); 10 | } 11 | 12 | return { 13 | id: 'quail-publish', 14 | name: name, 15 | callback: async () => { 16 | const file = app.workspace.getActiveFile(); 17 | if (file !== null) { 18 | const loadingModal = new LoadingModal(app) 19 | loadingModal.open(); 20 | 21 | let pt:any = null; 22 | try { 23 | pt = await savePost(app, client, auxiliaClient, settings); 24 | } catch (e) { 25 | new ErrorModal(app, e).open(); 26 | loadingModal.close(); 27 | return; 28 | } 29 | 30 | try { 31 | pt = await client.publishPost(settings.listID, pt.slug); 32 | } catch (e) { 33 | new ErrorModal(app, e).open(); 34 | loadingModal.close(); 35 | return; 36 | } finally { 37 | loadingModal.close(); 38 | } 39 | 40 | const slug = pt.slug || ''; 41 | if (slug) { 42 | const viewUrl = `https://quaily.com/${settings.listSlug}/p/${slug}`; 43 | new PublishResultModal(app, client, { 44 | url: viewUrl, title: pt.title, summary: pt.summary, coverImageUrl: pt.cover_image_url 45 | }).open(); 46 | } else { 47 | new ErrorModal(app, new Error("resp.slug is empty.")).open(); 48 | } 49 | } 50 | } 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/actions/insert-metadata.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'obsidian'; 2 | import { MessageModal } from '../modals'; 3 | import fm from '../frontmatter'; 4 | import { t, english } from 'src/i18n'; 5 | import aigen from './aigen'; 6 | import { QuailPluginSettings } from 'src/interface'; 7 | 8 | export default function insertMetadata(app: App, auxiliaClient: any, settings: QuailPluginSettings) { 9 | let name = english('actions.insert_metadata'); 10 | if (!settings.useEnglishCmds) { 11 | name = t('actions.insert_metadata'); 12 | } 13 | 14 | return { 15 | id: 'insert-metadata', 16 | name: name, 17 | callback: async () => { 18 | const file = app.workspace.getActiveFile(); 19 | if (file) { 20 | const proc = (frontmatter: any) => { 21 | if (frontmatter === null || Object.values(frontmatter).length === 0) { 22 | const fmc:any = fm.emptyFrontmatter() 23 | for (const key in fmc) { 24 | if (Object.prototype.hasOwnProperty.call(fmc, key)) { 25 | frontmatter[key] = fmc[key]; 26 | } 27 | } 28 | } else { 29 | const modal = new MessageModal(app, { 30 | title: t('message_modal.metadata_exists.title'), 31 | message: t('message_modal.metadata_exists.desc'), 32 | icon: "🔔", 33 | iconColor: "orange", 34 | actions: [{ 35 | text: t('common.generate'), 36 | primary: true, 37 | click: (dialog: any) => { 38 | aigen(app, auxiliaClient, settings).callback(); 39 | dialog.close(); 40 | } 41 | },{ 42 | text: t('common.cancel'), 43 | close: true, 44 | }] 45 | }) 46 | modal.open(); 47 | } 48 | } 49 | app.fileManager.processFrontMatter(file, proc); 50 | } 51 | } 52 | }; 53 | } -------------------------------------------------------------------------------- /src/actions/preview.ts: -------------------------------------------------------------------------------- 1 | import { App, Notice } from 'obsidian'; 2 | import { ErrorModal, LoadingModal } from '../modals'; 3 | import { QuailPluginSettings } from '../interface'; 4 | import { PreviewModal } from '../modals'; 5 | import { t, english } from 'src/i18n'; 6 | export default function preview(app: App, client: any, auxiliaClient: any, settings: QuailPluginSettings) { 7 | let name = english('actions.preview'); 8 | if (!settings.useEnglishCmds) { 9 | name = t('actions.preview'); 10 | } 11 | 12 | return { 13 | id: 'quail-preview', 14 | name: name, 15 | callback: async () => { 16 | const loadingModal = new LoadingModal(app); 17 | loadingModal.open(); 18 | 19 | try { 20 | const activeFile = app.workspace.getActiveFile(); 21 | if (!activeFile) { 22 | new Notice('No active file'); 23 | return; 24 | } 25 | 26 | // First save the post to ensure we have the latest version 27 | const { savePost } = await import('./index'); 28 | const post = await savePost(app, client, auxiliaClient, settings); 29 | 30 | if (!post || !post.id || !post.list_id) { 31 | new Notice('Failed to save post or get post details'); 32 | return; 33 | } 34 | 35 | // Get origin for the token request 36 | const origin = window.location.origin; 37 | 38 | // Issue ephemeral token 39 | const resp = await client.issueEphemeralToken(origin); 40 | if (!resp || !resp.ephemeral_token) { 41 | throw new Error('Failed to generate preview token'); 42 | } 43 | 44 | if (resp.ephemeral_token) { 45 | // Generate preview URL 46 | const previewUrl = client.getPostPreviewUrl(post.list_id, post.id, resp.ephemeral_token); 47 | // Open modal with QR code and button instead of opening URL directly 48 | new PreviewModal(app, previewUrl).open(); 49 | } 50 | 51 | } catch (error) { 52 | new ErrorModal(app, error).open(); 53 | } finally { 54 | loadingModal.close(); 55 | } 56 | } 57 | }; 58 | } -------------------------------------------------------------------------------- /src/actions/aigen.ts: -------------------------------------------------------------------------------- 1 | import util from '../util'; 2 | import fm from "../frontmatter"; 3 | import { LoadingModal, ErrorModal } from '../modals'; 4 | import { App } from 'obsidian'; 5 | import { QuailPluginSettings } from 'src/interface'; 6 | import { t, english } from 'src/i18n'; 7 | 8 | const aigen = function (app: App, auxiliaClient: any, settings: QuailPluginSettings) { 9 | let name = english('actions.ai_gen_metadata'); 10 | if (!settings.useEnglishCmds) { 11 | name = t('actions.ai_gen_metadata'); 12 | } 13 | 14 | return { 15 | id: 'ai-gen-metadata', 16 | name: name, 17 | callback: async () => { 18 | const content = await util.getActiveFileMarkdown(app); 19 | const file = app.workspace.getActiveFile(); 20 | const loadingModal = new LoadingModal(app) 21 | loadingModal.open(); 22 | 23 | if (file) { 24 | const title = file.name.replace(/\.md$/, ''); 25 | const fmc:any = await fm.suggestFrontmatter(auxiliaClient, title, content, []) 26 | const proc = (frontmatter: any) => { 27 | if (file) { 28 | try { 29 | for (const key in fmc) { 30 | if (Object.prototype.hasOwnProperty.call(fmc, key)) { 31 | if (key === 'summary' || key === 'tags') { 32 | // update `summary` and `tags` 33 | frontmatter[key] = fmc[key]; 34 | } else { 35 | // for other fields, only update if empty 36 | if (frontmatter[key] === null || frontmatter[key] === undefined || frontmatter[key] === '') { 37 | frontmatter[key] = fmc[key]; 38 | } 39 | } 40 | } 41 | } 42 | } catch (e) { 43 | new ErrorModal(app, e).open(); 44 | } finally { 45 | loadingModal.close(); 46 | } 47 | } 48 | } 49 | app.fileManager.processFrontMatter(file, proc); 50 | } else { 51 | loadingModal.close(); 52 | } 53 | } 54 | } 55 | } 56 | 57 | export default aigen; -------------------------------------------------------------------------------- /src/modals/ErrorModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal } from 'obsidian'; 2 | import { constructModalTitle } from './utils'; 3 | import { t } from 'src/i18n'; 4 | export class ErrorModal extends Modal { 5 | message = ''; 6 | 7 | constructor(app: App, error: Error) { 8 | super(app); 9 | this.message = error.message; 10 | } 11 | 12 | onOpen() { 13 | const {contentEl} = this; 14 | constructModalTitle(contentEl, t('error_modal.title')); 15 | 16 | const p = document.createElement('p'); 17 | Object.assign(p.style, { 18 | color: 'var(--text-muted)', 19 | textAlign: 'center', 20 | }); 21 | p.appendText(t('error_modal.error_message')); 22 | 23 | const pre = document.createElement('pre'); 24 | pre.className = 'error-message'; 25 | Object.assign(pre.style, { 26 | fontSize: '0.8rem', 27 | }); 28 | 29 | const code = document.createElement('code'); 30 | 31 | code.appendText(this.message); 32 | pre.appendChild(code); 33 | p.appendChild(pre); 34 | contentEl.appendChild(p); 35 | 36 | const buttonContainer = contentEl.createDiv(); 37 | Object.assign(buttonContainer.style, { 38 | display: 'flex', 39 | gap: '0.5rem', 40 | justifyContent: 'center', 41 | marginBottom: '0.5rem' 42 | }); 43 | 44 | // Visit button 45 | const visitButton = buttonContainer.createEl('button', { 46 | cls: 'mod-cta', 47 | text: t('common.close') 48 | }); 49 | Object.assign(visitButton.style, { 50 | minWidth: '100px' 51 | }); 52 | visitButton.onclick = () => { 53 | this.close(); 54 | }; 55 | 56 | // Copy link button 57 | const copyButton = buttonContainer.createEl('button', { 58 | text: t('error_modal.copy') 59 | }); 60 | Object.assign(copyButton.style, { 61 | minWidth: '100px' 62 | }); 63 | copyButton.onclick = async () => { 64 | await navigator.clipboard.writeText(this.message); 65 | copyButton.setText(t('common.copied')); 66 | setTimeout(() => { 67 | copyButton.setText(t('error_modal.copy')); 68 | }, 2000); 69 | }; 70 | } 71 | 72 | onClose() { 73 | const {contentEl} = this; 74 | contentEl.empty(); 75 | } 76 | } -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Quail's Obsidian plugin can be found in the Obsidian community plugin list. You can install it directly from there. 4 | 5 | 1. Open Obsidian's settings page and click on the "Community Plugins" tab. 6 | 2. Click the "Browse" button at the right of the "Community Plugins" tab. A plugin list will appear. 7 | 3. Search for "Quail" in the plugin list and click the "Install" button. 8 | 9 | ## Other installation methods 10 | 11 | If you want to try other versions of the plugin, or want to 12 | 13 | **Installing with obsidian42-brat plugin** 14 | 15 | [obsidian42-brat](https://github.com/TfTHacker/obsidian42-brat) is an Obsidian plugin that can be used to install and test other plugins that haven't made it to the marketplace. Therefore, you can first install the obsidian42-brat plugin and then use it to install Quail's Obsidian plugin. 16 | 17 | 1. Install the obsidian42-brat plugin: 18 | In Obsidian's settings, click on the "Community Plugins" tab, then click the "Install Plugins" button, search for "obsidian42-brat," and click the "Install" button. Once installed, enable the plugin. 19 | 2. Add Quail to the list of plugins in the obsidian42-brat plugin: 20 | In the settings of Obsidian42-brat, click "Add Beta Plugin," input `https://github.com/quail-ink/obsidian-quail`, and click the "Add" button. 21 | 3. You can check the "Auto-update plugins at startup" option, so that the Obsidian42-brat plugin will automatically update Quail's plugin upon startup. 22 | 23 | **Manual Installation** 24 | 25 | 1. Download the latest version of the plugin from the [GitHub release page](https://github.com/quail-ink/obsidian-quail/releases/) 26 | 2. Unzip the downloaded file and you will see there is a "obsidian-quail" folder. 27 | 3. Go to Obsidian's settings page and click on the "Community Plugins" tab. 28 | 4. Click the "Folder" button at the right of the "Community Plugins" tab. A file view will appear. Copy the "obsidian-quail" folder into the file view. 29 | 5. Restart Obsidian 30 | 6. Go to "Community Plugins" tab and find the Quail plugin and click the "Enable" button. 31 | 32 | **Installing from source** 33 | 34 | Clone the plugin. 35 | 36 | ```bash 37 | git clone https://github.com/quail-ink/obsidian-quail.git 38 | cd obsidian-quail 39 | ``` 40 | 41 | Build the plugin. 42 | 43 | ```bash 44 | npm install 45 | npm run build 46 | ``` 47 | 48 | Copy the plugin to your vault. 49 | 50 | ``` 51 | mkdir $VAULT_PATH/.obsidian/plugins/obsidian-quail 52 | mv main.js styles.css manifest.json $VAULT_PATH/.obsidian/plugins/obsidian-quail 53 | ``` 54 | -------------------------------------------------------------------------------- /src/actions/set-channel.ts: -------------------------------------------------------------------------------- 1 | import { App, Notice, SuggestModal } from 'obsidian'; 2 | import { MessageModal, ErrorModal } from '../modals'; 3 | import { QuailPluginSettings } from '../interface'; 4 | import { t, english } from 'src/i18n'; 5 | class ChannelSuggestModal extends SuggestModal<{title: string, id: string}> { 6 | channelList: Array<{title: string, id: string}>; 7 | onSelect: (item: {title: string, id: string}) => void; 8 | 9 | constructor(app: App, channelList: Array<{title: string, id: string}>, onSelect: (item: {title: string, id: string}) => void) { 10 | super(app); 11 | this.channelList = channelList; 12 | this.onSelect = onSelect; 13 | this.setPlaceholder(t('actions.set_channel.select_channel')); 14 | } 15 | 16 | getSuggestions(): Array<{title: string, id: string}> { 17 | return this.channelList; 18 | } 19 | 20 | renderSuggestion(item: {title: string, id: string}, el: HTMLElement) { 21 | el.createEl("div", { text: item.title }); 22 | } 23 | 24 | onChooseSuggestion(item: {title: string, id: string}) { 25 | this.onSelect(item); 26 | } 27 | } 28 | 29 | export default function setChannel(app: App, settings: QuailPluginSettings, saveSettings: () => Promise) { 30 | let name = english('actions.set_channel'); 31 | if (!settings.useEnglishCmds) { 32 | name = t('actions.set_channel'); 33 | } 34 | 35 | return { 36 | id: 'quail-set-channel', 37 | name: name, 38 | callback: async () => { 39 | try { 40 | const lists = settings.lists; 41 | if (!lists || lists.length === 0) { 42 | new MessageModal(app, { 43 | title: t('message_modal.no_channels_found.title'), 44 | message: t('message_modal.no_channels_found.desc'), 45 | icon: "⚠️", 46 | iconColor: "orange" 47 | }).open(); 48 | return; 49 | } 50 | 51 | const channelList = lists.map((list: any) => ({ 52 | title: list.title, 53 | id: list.id 54 | })); 55 | 56 | new ChannelSuggestModal(app, channelList, async (item) => { 57 | for (let ix = 0; ix < lists.length; ix++) { 58 | if (lists[ix].id === item.id) { 59 | settings.listID = lists[ix].id; 60 | settings.listSlug = lists[ix].slug; 61 | await saveSettings(); 62 | new Notice(t('notices.set_channel_success', { title: item.title })); 63 | return; 64 | } 65 | } 66 | }).open(); 67 | } catch (e) { 68 | new ErrorModal(app, e).open(); 69 | } 70 | } 71 | }; 72 | } -------------------------------------------------------------------------------- /src/i18n/lang/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions.ai_gen_metadata": "AI 生成元数据", 3 | "actions.insert_metadata": "插入元数据模板", 4 | "actions.preview": "预览", 5 | "actions.publish": "发布", 6 | "actions.save": "保存", 7 | "actions.send": "推送", 8 | "actions.set_channel": "设置默认频道", 9 | "actions.set_channel.select_channel": "选择一个频道", 10 | "actions.unpublish": "取消发布", 11 | "common.cancel": "取消", 12 | "common.close": "关闭", 13 | "common.copied": "复制", 14 | "common.generate": "生成", 15 | "common.login": "登录", 16 | "common.logout": "登出", 17 | "common.ok": "好", 18 | "error_modal.copy": "复制消息", 19 | "error_modal.error_message": "错误消息", 20 | "error_modal.title": "哎呀,出了点问题", 21 | "loading_modal.text.almost_there": "快了", 22 | "loading_modal.text.just_a_moment": "片刻", 23 | "loading_modal.text.loading": "加载", 24 | "loading_modal.text.still_loading": "仍在加载", 25 | "loading_modal.text.working_on_it": "在干活", 26 | "message_modal.failed_to_verify_meta.title": "未能验证元数据", 27 | "message_modal.metadata_exists.desc": "请手动编辑或使用人工智能生成。", 28 | "message_modal.metadata_exists.title": "元数据已经存在", 29 | "message_modal.no_channels_found.desc": "请首先创建一个频道。", 30 | "message_modal.no_channels_found.title": "没有频道", 31 | "message_modal.send_post.desc": "这篇文章已被添加到发送队列中。可能需要几分钟才能发送出去。", 32 | "message_modal.send_post.title": "通过Quaily发送", 33 | "message_modal.title": "Quaily 的消息", 34 | "notices.post_saved": "✅ 文章已保存!", 35 | "notices.set_channel_success": "✅ {title} 是默认频道。", 36 | "notices.unpublish_success": "📕 本帖已被取消发布。不再对读者可见。", 37 | "preview_modal.desktop.desc": "点击在浏览器中预览", 38 | "preview_modal.desktop.title": "💻 桌面预览", 39 | "preview_modal.hint": "预览将在10分钟后过期。请勿与他人分享链接。", 40 | "preview_modal.mobile.desc": "扫描二维码在移动设备上预览", 41 | "preview_modal.mobile.title": "📱 移动端预览", 42 | "preview_modal.preview": "预览", 43 | "preview_modal.title": "预览文章", 44 | "publish_result_modal.copy_link": "复制链接", 45 | "publish_result_modal.title": "🎉 成功发布!", 46 | "publish_result_modal.visit_post": "查看文章", 47 | "settings.account.logged.desc": "已登录为 {email}", 48 | "settings.account.logged.title": "你好,{name}", 49 | "settings.account.need_to_login.desc": "请登录以使用插件", 50 | "settings.account.need_to_login.title": "登录到 Quaily", 51 | "settings.behavior": "行为", 52 | "settings.behavior.use_english_cmds.desc": "继续在 Obsidian 命令面板中使用英文命令(需要重启插件)", 53 | "settings.behavior.use_english_cmds.title": "使用英文命令", 54 | "settings.behavior.use_first_image_as_cover.desc": "如果封面为空,将使用文章中的第一张图片", 55 | "settings.behavior.use_first_image_as_cover.title": "使用第一张图片作为封面", 56 | "settings.channel": "频道", 57 | "settings.channel.create": "创建频道", 58 | "settings.channel.desc": "选择想要使用的频道。", 59 | "settings.channel.empty": "没有频道", 60 | "settings.channel.title": "频道", 61 | "settings.editor": "编辑器", 62 | "settings.editor.strict_line_breaks.desc": "Markdown 规范忽略单行换行。如果想保留它们,请启用此选项。", 63 | "settings.editor.strict_line_breaks.title": "严格的换行" 64 | } -------------------------------------------------------------------------------- /src/i18n/lang/zh-tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions.ai_gen_metadata": "AI 生成元數據", 3 | "actions.insert_metadata": "插入元數據模板", 4 | "actions.preview": "預覽", 5 | "actions.publish": "發佈", 6 | "actions.save": "保存", 7 | "actions.send": "推送", 8 | "actions.set_channel": "設置默認頻道", 9 | "actions.set_channel.select_channel": "選擇一個頻道", 10 | "actions.unpublish": "取消發佈", 11 | "common.cancel": "取消", 12 | "common.close": "關閉", 13 | "common.copied": "複製", 14 | "common.generate": "生成", 15 | "common.login": "登入", 16 | "common.logout": "登出", 17 | "common.ok": "好", 18 | "error_modal.copy": "複製訊息", 19 | "error_modal.error_message": "錯誤訊息", 20 | "error_modal.title": "哎呀,出了點問題。", 21 | "loading_modal.text.almost_there": "快到了", 22 | "loading_modal.text.just_a_moment": "等一下", 23 | "loading_modal.text.loading": "載入中", 24 | "loading_modal.text.still_loading": "載入中", 25 | "loading_modal.text.working_on_it": "正在努力", 26 | "message_modal.failed_to_verify_meta.title": "無法驗證元數據", 27 | "message_modal.metadata_exists.desc": "請手動編輯或使用人工智慧生成。", 28 | "message_modal.metadata_exists.title": "元數據已存在", 29 | "message_modal.no_channels_found.desc": "請先建立一個頻道。", 30 | "message_modal.no_channels_found.title": "無頻道", 31 | "message_modal.send_post.desc": "這篇文章已經被加入寄送佇列中。可能需要幾分鐘才能寄出。", 32 | "message_modal.send_post.title": "透過Quaily發送", 33 | "message_modal.title": "Quaily 的一則訊息", 34 | "notices.post_saved": "✅ 文章已儲存!", 35 | "notices.set_channel_success": "✅ {title} 是預設頻道。", 36 | "notices.unpublish_success": "📕 這篇文章已被取消發佈。讀者將無法看到。", 37 | "preview_modal.desktop.desc": "點擊在瀏覽器中預覽", 38 | "preview_modal.desktop.title": "💻 在桌面上預覽", 39 | "preview_modal.hint": "預覽將在10分鐘後過期。請勿與他人分享鏈接。", 40 | "preview_modal.mobile.desc": "掃描QR碼在手機上預覽", 41 | "preview_modal.mobile.title": "📱 手機預覽", 42 | "preview_modal.preview": "預覽", 43 | "preview_modal.title": "預覽文章", 44 | "publish_result_modal.copy_link": "複製鏈接", 45 | "publish_result_modal.title": "🎉 成功發佈!", 46 | "publish_result_modal.visit_post": "查看文章", 47 | "settings.account.logged.desc": "已登入為 {email}", 48 | "settings.account.logged.title": "你好,{name}", 49 | "settings.account.need_to_login.desc": "請登入以使用插件", 50 | "settings.account.need_to_login.title": "登入 Quaily", 51 | "settings.behavior": "行為", 52 | "settings.behavior.use_english_cmds.desc": "在 Obsidian 命令面板中繼續使用英文命令(需要重啟外掛)", 53 | "settings.behavior.use_english_cmds.title": "使用英文命令", 54 | "settings.behavior.use_first_image_as_cover.desc": "如果封面為空,將使用文章中的第一張圖片", 55 | "settings.behavior.use_first_image_as_cover.title": "使用第一張圖片作為文章封面", 56 | "settings.channel": "頻道", 57 | "settings.channel.create": "創建頻道", 58 | "settings.channel.desc": "選擇要使用的頻道。", 59 | "settings.channel.empty": "沒有頻道", 60 | "settings.channel.title": "頻道", 61 | "settings.editor": "編輯器", 62 | "settings.editor.strict_line_breaks.desc": "Markdown 忽略單行換行。如果想保留它們,請啟用此選項。", 63 | "settings.editor.strict_line_breaks.title": "嚴格的換行" 64 | } -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | env: 8 | PLUGIN_NAME: obsidian-quail 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | permissions: write-all 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - uses: actions/setup-node@master 19 | with: 20 | node-version: "18" 21 | 22 | - uses: pnpm/action-setup@v4 23 | name: Install pnpm 24 | with: 25 | version: 8 26 | run_install: false 27 | 28 | - name: Build 29 | id: build 30 | run: | 31 | pnpm i 32 | pnpm build 33 | mkdir ${{ env.PLUGIN_NAME }} 34 | cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} 35 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 36 | ls 37 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 38 | - name: Create Release 39 | id: create_release 40 | uses: actions/create-release@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | VERSION: ${{ github.ref }} 44 | with: 45 | tag_name: ${{ github.ref }} 46 | release_name: ${{ github.ref }} 47 | draft: false 48 | prerelease: false 49 | - name: Upload zip file 50 | id: upload-zip 51 | uses: actions/upload-release-asset@v1 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | with: 55 | upload_url: ${{ steps.create_release.outputs.upload_url }} 56 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 57 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 58 | asset_content_type: application/zip 59 | - name: Upload main.js 60 | id: upload-main 61 | uses: actions/upload-release-asset@v1 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | with: 65 | upload_url: ${{ steps.create_release.outputs.upload_url }} 66 | asset_path: ./main.js 67 | asset_name: main.js 68 | asset_content_type: text/javascript 69 | - name: Upload manifest.json 70 | id: upload-manifest 71 | uses: actions/upload-release-asset@v1 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | with: 75 | upload_url: ${{ steps.create_release.outputs.upload_url }} 76 | asset_path: ./manifest.json 77 | asset_name: manifest.json 78 | asset_content_type: application/json 79 | - name: Upload styles.css 80 | id: upload-css 81 | uses: actions/upload-release-asset@v1 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | upload_url: ${{ steps.create_release.outputs.upload_url }} 86 | asset_path: ./styles.css 87 | asset_name: styles.css 88 | asset_content_type: text/css 89 | -------------------------------------------------------------------------------- /src/modals/LoadingModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal } from 'obsidian'; 2 | import { t } from 'src/i18n'; 3 | 4 | export class LoadingModal extends Modal { 5 | private loadingInterval: number | null = null; 6 | private frames: string[] = [ 7 | '😐', '😐', '😐', '😐', '😐', '😐', '😐', '😐','😐', '😐', '😐', '😑', 8 | '😐', '😐', '😐', '😐', '😑' 9 | ]; 10 | private currentFrame = 0; 11 | private loadingTextElement: HTMLElement | null = null; 12 | private asciiArtElement: HTMLElement | null = null; 13 | private loadingTexts = [ 14 | t('loading_modal.text.loading'), 15 | t('loading_modal.text.still_loading'), 16 | t('loading_modal.text.almost_there'), 17 | t('loading_modal.text.just_a_moment'), 18 | t('loading_modal.text.working_on_it') 19 | ]; 20 | private textIndex = 0; 21 | private textOpacity = 1.0; 22 | private fadeDirection = 'out'; 23 | 24 | constructor(app: App) { 25 | super(app); 26 | } 27 | 28 | onOpen() { 29 | this.setTitle("") 30 | 31 | const {contentEl} = this; 32 | this.asciiArtElement = document.createElement('pre'); 33 | this.asciiArtElement.className = 'loading-ascii-art'; 34 | Object.assign(this.asciiArtElement.style, { 35 | fontSize: '40px', 36 | lineHeight: '1.2', 37 | margin: '1rem 0', 38 | textAlign: 'center', 39 | }); 40 | contentEl.appendChild(this.asciiArtElement); 41 | 42 | this.loadingTextElement = document.createElement('p'); 43 | this.loadingTextElement.className = 'text-center loading-text'; 44 | Object.assign(this.loadingTextElement.style, { 45 | marginTop: '10px', 46 | transition: 'opacity 0.3s ease', 47 | fontSize: '0.7rem', 48 | }); 49 | this.loadingTextElement.setText(this.loadingTexts[this.textIndex]); 50 | contentEl.appendChild(this.loadingTextElement); 51 | 52 | this.updateFrame(); 53 | this.loadingInterval = window.setInterval(() => { 54 | this.updateFrame(); 55 | }, 100); 56 | } 57 | 58 | updateFrame() { 59 | if (!this.asciiArtElement || !this.loadingTextElement) return; 60 | 61 | this.currentFrame = (this.currentFrame + 1) % this.frames.length; 62 | const cuteArt = [ 63 | " " + this.frames[this.currentFrame] + " ", 64 | ].join("\n"); 65 | 66 | this.asciiArtElement.setText(cuteArt); 67 | 68 | if (this.fadeDirection === 'out') { 69 | this.textOpacity -= 0.1; 70 | if (this.textOpacity <= 0) { 71 | this.textOpacity = 0; 72 | this.fadeDirection = 'in'; 73 | this.textIndex = (this.textIndex + 1) % this.loadingTexts.length; 74 | this.loadingTextElement.setText(this.loadingTexts[this.textIndex]); 75 | } 76 | } else { 77 | this.textOpacity += 0.1; 78 | if (this.textOpacity >= 1) { 79 | this.textOpacity = 1; 80 | this.fadeDirection = 'out'; 81 | } 82 | } 83 | 84 | this.loadingTextElement.style.opacity = this.textOpacity.toString(); 85 | } 86 | 87 | onClose() { 88 | const {contentEl} = this; 89 | if (this.loadingInterval) { 90 | clearInterval(this.loadingInterval); 91 | this.loadingInterval = null; 92 | } 93 | contentEl.empty(); 94 | } 95 | } -------------------------------------------------------------------------------- /src/i18n/lang/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions.ai_gen_metadata": "AIによるメタの生成", 3 | "actions.insert_metadata": "メタデータテンプレートを挿入", 4 | "actions.preview": "プレビュー", 5 | "actions.publish": "公開", 6 | "actions.save": "保存", 7 | "actions.send": "送る", 8 | "actions.set_channel": "デフォルトチャンネルを設定", 9 | "actions.set_channel.select_channel": "チャンネルを選択してください。", 10 | "actions.unpublish": "非公開", 11 | "common.cancel": "キャンセル", 12 | "common.close": "閉じる", 13 | "common.copied": "コピーされました", 14 | "common.generate": "生成", 15 | "common.login": "ログイン", 16 | "common.logout": "ログアウト", 17 | "common.ok": "OK", 18 | "error_modal.copy": "メッセージをコピー", 19 | "error_modal.error_message": "エラーメッセージ", 20 | "error_modal.title": "おっと、何かがうまくいかなかったよ", 21 | "loading_modal.text.almost_there": "もう少しで到着します。", 22 | "loading_modal.text.just_a_moment": "ちょっと待ってください。", 23 | "loading_modal.text.loading": "ローディング", 24 | "loading_modal.text.still_loading": "まだ読み込み中", 25 | "loading_modal.text.working_on_it": "取り組んでいます", 26 | "message_modal.failed_to_verify_meta.title": "メタデータの検証に失敗しました。", 27 | "message_modal.metadata_exists.desc": "手動で編集するか、AIを使用して生成してください。", 28 | "message_modal.metadata_exists.title": "メタデータはすでに存在します。", 29 | "message_modal.no_channels_found.desc": "最初にチャンネルを作成してください。", 30 | "message_modal.no_channels_found.title": "チャンネルなし", 31 | "message_modal.send_post.desc": "この記事は送信キューに追加されました。送信には数分かかる場合があります。", 32 | "message_modal.send_post.title": "クエイリーによる送信", 33 | "message_modal.title": "クエイリーからのメッセージ", 34 | "notices.post_saved": "記事が保存されました!", 35 | "notices.set_channel_success": "✅ {title} はデフォルトチャンネルです。", 36 | "notices.unpublish_success": "この記事は非公開になりました。読者にはもう表示されません。", 37 | "preview_modal.desktop.desc": "ブラウザでプレビューするにはクリックしてください。", 38 | "preview_modal.desktop.title": "💻 デスクトップでのプレビュー", 39 | "preview_modal.hint": "10分後にプレビューが期限切れになります。他の人とリンクを共有しないでください。", 40 | "preview_modal.mobile.desc": "モバイルデバイスでプレビューするにはQRコードをスキャンしてください。", 41 | "preview_modal.mobile.title": "📱 モバイルでのプレビュー", 42 | "preview_modal.preview": "プレビュー", 43 | "preview_modal.title": "記事をプレビュー", 44 | "publish_result_modal.copy_link": "リンクをコピー", 45 | "publish_result_modal.title": "🎉 成功的に公開されました!", 46 | "publish_result_modal.visit_post": "記事を見る", 47 | "settings.account.logged.desc": "{email}としてログインしています", 48 | "settings.account.logged.title": "こんにちは、{name}", 49 | "settings.account.need_to_login.desc": "プラグインを使用するにはログインしてください。", 50 | "settings.account.need_to_login.title": "Quailyにログイン", 51 | "settings.behavior": "動作", 52 | "settings.behavior.use_english_cmds.desc": "オブシディアンコマンドパレットで英語版コマンドを引き続き使用してください(プラグインを再起動する必要があります)", 53 | "settings.behavior.use_english_cmds.title": "英語のコマンドを使用してください。", 54 | "settings.behavior.use_first_image_as_cover.desc": "投稿のカバーが空の場合、最初の画像がカバーとして使用されます", 55 | "settings.behavior.use_first_image_as_cover.title": "最初の画像を投稿のカバーとして使用する", 56 | "settings.channel": "チャンネル", 57 | "settings.channel.create": "チャンネルを作成します。", 58 | "settings.channel.desc": "使用したいチャンネルを選択してください。", 59 | "settings.channel.empty": "チャンネルなし", 60 | "settings.channel.title": "チャンネル", 61 | "settings.editor": "編集", 62 | "settings.editor.strict_line_breaks.desc": "Markdown 仕様は単一行の改行を無視します。これを保持したい場合は、このオプションを有効にしてください。", 63 | "settings.editor.strict_line_breaks.title": "厳格な改行" 64 | } -------------------------------------------------------------------------------- /src/modals/PreviewModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal, Setting } from 'obsidian'; 2 | import * as QRCode from 'qrcode'; 3 | import { constructModalTitle } from './utils'; 4 | import { t } from 'src/i18n'; 5 | export class PreviewModal extends Modal { 6 | private url: string; 7 | 8 | constructor(app: App, url: string) { 9 | super(app); 10 | this.url = url; 11 | } 12 | 13 | async onOpen() { 14 | const { contentEl } = this; 15 | this.setTitle(""); 16 | 17 | const container = contentEl.createDiv(); 18 | Object.assign(container.style, { 19 | maxWidth: '600px', 20 | }); 21 | 22 | constructModalTitle(container, t('preview_modal.title')); 23 | 24 | const hint = container.createDiv({ 25 | text: t('preview_modal.hint'), 26 | }); 27 | Object.assign(hint.style, { 28 | background: 'var(--background-secondary)', 29 | padding: '0.8rem', 30 | borderRadius: '6px', 31 | margin: '0 0 1rem 0', 32 | wordBreak: 'break-all', 33 | color: 'var(--text-accent)', 34 | }); 35 | 36 | // the top container is the container for the QR code and the text 37 | const topContainer = container.createDiv(); 38 | Object.assign(topContainer.style, { 39 | display: 'flex', 40 | justifyContent: 'space-between', 41 | margin: '0 0 1rem 0', 42 | background: 'var(--background-primary)', 43 | padding: 'var(--size-4-3)', 44 | borderRadius: 'var(--radius-s)', 45 | border: '1px solid var(--background-modifier-border)' 46 | }); 47 | 48 | const topContainerLeft = topContainer.createDiv(); 49 | Object.assign(topContainerLeft.style, { 50 | flex: 1, 51 | }); 52 | const topContainerLeftTitle = topContainerLeft.createDiv({ 53 | text: t('preview_modal.mobile.title'), 54 | }); 55 | topContainerLeftTitle.classList.add('setting-item-name'); 56 | 57 | const topContainerLeftDesc = topContainerLeft.createDiv({ 58 | text: t('preview_modal.mobile.desc'), 59 | }); 60 | topContainerLeftDesc.classList.add('setting-item-description'); 61 | 62 | const topContainerRight = topContainer.createDiv(); 63 | Object.assign(topContainerRight.style, { 64 | }); 65 | 66 | // Create container for QR code with some padding 67 | const qrContainer = topContainerRight.createDiv({ cls: 'quail-qr-container' }); 68 | // Create canvas for QR code 69 | const canvas = document.createElement('canvas'); 70 | qrContainer.appendChild(canvas); 71 | 72 | try { 73 | await QRCode.toCanvas(canvas, this.url, { 74 | width: 200, 75 | margin: 1, 76 | }); 77 | 78 | } catch (err) { 79 | qrContainer.createEl('p', { text: `Failed to generate QR code: ${err}` }); 80 | } 81 | 82 | // Add button to open in browser 83 | const buttonContainer = contentEl.createDiv(); 84 | Object.assign(buttonContainer.style, { 85 | margin: '0', 86 | background: 'var(--background-primary)', 87 | padding: '0.8rem 0.8rem 0 0.8rem', 88 | borderRadius: 'var(--radius-s)', 89 | border: '1px solid var(--background-modifier-border)' 90 | }); 91 | new Setting(buttonContainer) 92 | .setName(t('preview_modal.desktop.title')) 93 | .setDesc(t('preview_modal.desktop.desc')) 94 | .addButton((btn) => 95 | btn.setButtonText(t('preview_modal.preview')) 96 | .setCta() 97 | .onClick(() => { 98 | window.open(this.url, '_blank'); 99 | this.close(); 100 | }) 101 | ); 102 | } 103 | 104 | onClose() { 105 | const { contentEl } = this; 106 | contentEl.empty(); 107 | } 108 | } -------------------------------------------------------------------------------- /src/i18n/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "common.ok": "OK", 3 | "common.cancel": "Cancel", 4 | "common.close": "Close", 5 | "common.login": "Login", 6 | "common.logout": "Logout", 7 | "common.copied": "Copied", 8 | "common.generate": "Generate", 9 | "error_modal.title": "Oooops, something went wrong", 10 | "error_modal.error_message": "Error Message", 11 | "error_modal.copy": "Copy message", 12 | "message_modal.title": "A Message from Quaily", 13 | 14 | "preview_modal.title": "Preview Your Post", 15 | "preview_modal.hint": "Preview will be expired in 10 minutes. Don't share the link with others.", 16 | "preview_modal.mobile.title": "📱 Preview on mobile", 17 | "preview_modal.mobile.desc": "Scan the QRcode to preview on the mobile device", 18 | "preview_modal.desktop.title": "💻 Preview on desktop", 19 | "preview_modal.desktop.desc": "Click to preview in the browser", 20 | "preview_modal.preview": "Preview", 21 | 22 | "publish_result_modal.title": "🎉 Successfully Published!", 23 | "publish_result_modal.copy_link": "Copy link", 24 | "publish_result_modal.visit_post": "View Post", 25 | 26 | "loading_modal.text.loading": "Loading", 27 | "loading_modal.text.still_loading": "Still loading", 28 | "loading_modal.text.almost_there": "Almost there", 29 | "loading_modal.text.just_a_moment": "Just a moment", 30 | "loading_modal.text.working_on_it": "Working on it", 31 | 32 | "message_modal.failed_to_verify_meta.title": "Failed to verify the metadata", 33 | "message_modal.metadata_exists.title": "Metadata already exists", 34 | "message_modal.metadata_exists.desc": "Please edit manually or use AI to generate it", 35 | "message_modal.send_post.title": "Sending by Quaily", 36 | "message_modal.send_post.desc": "This post has been added into the sending queue. It may take a few minutes to send out.", 37 | "message_modal.no_channels_found.title": "No Channel", 38 | "message_modal.no_channels_found.desc": "Please create a channel at first.", 39 | 40 | "notices.post_saved": "✅ Post saved!", 41 | "notices.set_channel_success": "✅ {title} is the default channel.", 42 | "notices.unpublish_success": "📕 This post has been unpublished. No more visible to readers.", 43 | 44 | "actions.ai_gen_metadata": "Generate metadata by AI", 45 | "actions.send": "Send", 46 | "actions.save": "Save", 47 | "actions.publish": "Publish", 48 | "actions.unpublish": "Unpublish", 49 | "actions.insert_metadata": "Insert metadata template", 50 | "actions.preview": "Preview", 51 | "actions.set_channel": "Set default channel", 52 | "actions.set_channel.select_channel": "Select a channel", 53 | 54 | "settings.account.logged.title": "Hello, {name}", 55 | "settings.account.logged.desc": "You are logged in as {email}", 56 | "settings.account.need_to_login.title": "Login to Quaily", 57 | "settings.account.need_to_login.desc": "Please login to use the plugin", 58 | "settings.channel.title": "Channel", 59 | "settings.channel.desc": "Select the channel you want to use", 60 | "settings.channel.create": "Create a channel", 61 | "settings.channel.empty": "No channel", 62 | "settings.behavior.use_english_cmds.title": "Use English commands", 63 | "settings.behavior.use_english_cmds.desc": "Keep using English version commands in obsidian command palette (require to restart the plugin)", 64 | "settings.behavior": "Behavior", 65 | "settings.behavior.use_first_image_as_cover.title": "Use the first image as post cover", 66 | "settings.behavior.use_first_image_as_cover.desc": "If the post cover is empty, the first image in the post will be used", 67 | "settings.editor": "Editor", 68 | "settings.editor.strict_line_breaks.title": "Strict line breaks", 69 | "settings.editor.strict_line_breaks.desc": "Markdown specs ignore single line breaks. If you want to keep them, enable this option." 70 | } -------------------------------------------------------------------------------- /src/modals/MessageModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal } from 'obsidian'; 2 | import { t } from 'src/i18n'; 3 | export class MessageModal extends Modal { 4 | message = ''; 5 | title = ''; 6 | icon = '🤖'; 7 | iconColor = 'accent'; 8 | iconColors:any = { 9 | 'green': { 10 | 'dimm-1': 'rgba(16, 185, 129, .05)', 11 | 'dimm-2': 'rgba(16, 185, 129, .2)', 12 | }, 13 | 'red': { 14 | 'dimm-1': 'rgba(244, 63, 94, .05)', 15 | 'dimm-2': 'rgba(244, 63, 94, .2)', 16 | }, 17 | 'orange': { 18 | 'dimm-1': 'rgba(234, 179, 8, .05)', 19 | 'dimm-2': 'rgba(234, 179, 8, .2)', 20 | }, 21 | 'blue': { 22 | 'dimm-1': 'rgba(13, 117, 252, .05)', 23 | 'dimm-2': 'rgba(13, 117, 252, .2)', 24 | }, 25 | } 26 | actions: any[] = []; 27 | 28 | constructor(app: App, { title, message, icon, iconColor, actions }: any) { 29 | super(app); 30 | this.message = message; 31 | this.title = title || t('message_modal.title'); 32 | this.icon = icon || '🤖'; 33 | this.iconColor = iconColor || 'accent'; 34 | this.actions = actions || [{ 35 | text: t('common.ok'), 36 | primary: true, 37 | click: () => { 38 | this.close(); 39 | } 40 | }]; 41 | } 42 | 43 | onOpen() { 44 | const {contentEl} = this; 45 | 46 | this.setTitle("") 47 | 48 | const text = this.message.replace(/\n/g, '
'); 49 | 50 | // 51 | const container = contentEl.createDiv(); 52 | Object.assign(container.style, { 53 | display: 'flex', 54 | margin: '0', 55 | flexDirection: 'column', 56 | alignItems: 'center', 57 | }); 58 | const iconWrapper = container.createDiv(); 59 | Object.assign(iconWrapper.style, { 60 | display: 'flex', 61 | alignItems: 'center', 62 | justifyContent: 'center', 63 | margin: '0 0 1.5rem 0' 64 | }); 65 | const iconInner = iconWrapper.createDiv(); 66 | Object.assign(iconInner.style, { 67 | display: 'flex', 68 | alignItems: 'center', 69 | justifyContent: 'center', 70 | margin: '0', 71 | borderRadius: '50em', 72 | padding: '0.5rem', 73 | }); 74 | if (this.iconColors[this.iconColor]) { 75 | Object.assign(iconInner.style, { 76 | backgroundColor: this.iconColors[this.iconColor]['dimm-2'], 77 | boxShadow: `0 0 0 8px ${this.iconColors[this.iconColor]['dimm-1']}`, 78 | }); 79 | } else { 80 | Object.assign(iconInner.style, { 81 | backgroundColor: `hsl(calc(var(--accent-h) - 1), calc(var(--accent-s) * 1.01), calc(var(--accent-l) * 1.4))`, 82 | boxShadow: `0 0 0 8px hsl(calc(var(--accent-h) - 1), calc(var(--accent-s) * 1.01), calc(var(--accent-l) * 1.47))`, 83 | }); 84 | } 85 | const icon = iconInner.createDiv(); 86 | Object.assign(icon.style, { 87 | height: '22px', 88 | width: '22px', 89 | borderRadius: '50em', 90 | display: 'flex', 91 | alignItems: 'center', 92 | justifyContent: 'center', 93 | }); 94 | icon.innerText = this.icon; 95 | 96 | 97 | const content = container.createDiv(); 98 | Object.assign(content.style, { 99 | flex: '1', 100 | margin: '0 1rem', 101 | textAlign: 'center', 102 | }); 103 | 104 | const t = content.createEl('h2', { 105 | text: this.title, 106 | }); 107 | Object.assign(t.style, { 108 | fontSize: '1rem', 109 | fontWeight: 'bold', 110 | margin: '0 0 0.5rem 0', 111 | }); 112 | 113 | const p = content.createDiv(); 114 | Object.assign(p.style, { 115 | lineHeight: '1.5', 116 | color: 'var(--text-muted)', 117 | margin: '0 0 1rem 0', 118 | }); 119 | p.innerHTML = text; 120 | 121 | // Add a button to close the modal 122 | const buttonContainer = container.createDiv(); 123 | Object.assign(buttonContainer.style, { 124 | display: 'flex', 125 | justifyContent: 'center', 126 | margin: '0 0 0.5rem 0', 127 | gap: '0.5rem', 128 | }); 129 | for (const action of this.actions) { 130 | const button = buttonContainer.createEl('button', { 131 | text: action.text, 132 | }); 133 | Object.assign(button.style, { 134 | minWidth: '100px' 135 | }); 136 | if (action.primary) { 137 | button.classList.add('mod-cta'); 138 | } 139 | if (action.danger) { 140 | button.classList.add('mod-warning'); 141 | } 142 | if (action.close) { 143 | button.addEventListener('click', () => { 144 | this.close(); 145 | }); 146 | } else { 147 | if (action.click) { 148 | button.addEventListener('click', () => { 149 | action.click(this); 150 | }); 151 | } 152 | } 153 | button.focus(); 154 | } 155 | } 156 | 157 | onClose() { 158 | const {contentEl} = this; 159 | contentEl.empty(); 160 | } 161 | } -------------------------------------------------------------------------------- /src/modals/PublishResultModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal } from 'obsidian'; 2 | import { constructModalTitle } from './utils'; 3 | import { t } from 'src/i18n'; 4 | 5 | export class PublishResultModal extends Modal { 6 | dialogTitle: string; 7 | url: string; 8 | title: string; 9 | summary: string; 10 | coverImageUrl: string | null; 11 | client: any; 12 | 13 | constructor(app: App, client: any, {scene, url, title, summary, coverImageUrl }: { scene?: string, url: string, title?: string, summary?: string, coverImageUrl?: string }) { 14 | super(app); 15 | this.url = url; 16 | this.dialogTitle = t('publish_result_modal.title'); 17 | this.title = title || ''; 18 | this.summary = summary || ''; 19 | this.coverImageUrl = coverImageUrl || null; 20 | this.client = client; 21 | } 22 | 23 | onOpen() { 24 | const {contentEl} = this; 25 | contentEl.empty(); 26 | 27 | this.setTitle(""); 28 | 29 | 30 | // Create container for content 31 | const container = contentEl.createDiv(); 32 | Object.assign(container.style, { 33 | maxWidth: '600px', 34 | }); 35 | 36 | constructModalTitle(container, this.dialogTitle); 37 | 38 | const postPreview = container.createDiv(); 39 | Object.assign(postPreview.style, { 40 | margin: '0 0 1rem 0', 41 | display: 'flex', 42 | background: 'var(--background-primary)', 43 | padding: 'var(--size-4-3)', 44 | borderRadius: 'var(--radius-s)', 45 | border: '1px solid var(--background-modifier-border)' 46 | }); 47 | 48 | // Add cover image if provided 49 | if (this.coverImageUrl) { 50 | const imageContainer = postPreview.createDiv(); 51 | Object.assign(imageContainer.style, { 52 | margin: '0 1rem 0 0', 53 | width: '128px', 54 | height: '128px', 55 | flexBasis: '128px', 56 | minWidth: '128px', 57 | borderRadius: '2px', 58 | boxShadow: '0 3px 10px rgba(0, 0, 0, 0.05)' 59 | }); 60 | 61 | const img = imageContainer.createEl('img', { 62 | attr: { 63 | src: this.coverImageUrl, 64 | alt: 'Post cover image' 65 | } 66 | }); 67 | Object.assign(img.style, { 68 | width: '100%', 69 | height: '100%', 70 | objectFit: 'cover', 71 | borderRadius: '2px' 72 | }); 73 | } 74 | 75 | const postContent = postPreview.createDiv(); 76 | 77 | // Add post title if provided 78 | if (this.title) { 79 | const postTitle = postContent.createEl('h3', { 80 | text: this.title 81 | }); 82 | Object.assign(postTitle.style, { 83 | margin: '0 0 0.5rem 0', 84 | fontSize: '1.5em', 85 | fontWeight: '600' 86 | }); 87 | } 88 | 89 | // Add summary if provided 90 | if (this.summary) { 91 | const summaryEl = postContent.createEl('p', { 92 | text: this.summary 93 | }); 94 | // at most 2 lines 95 | Object.assign(summaryEl.style, { 96 | margin: '0', 97 | lineHeight: '1.5', 98 | color: 'var(--text-muted)', 99 | WebkitLineClamp: '2', 100 | WebkitBoxOrient: 'vertical', 101 | display: '-webkit-box', 102 | overflow: 'hidden' 103 | }); 104 | } 105 | 106 | // Add URL with link 107 | const urlContainer = container.createDiv(); 108 | Object.assign(urlContainer.style, { 109 | background: 'var(--background-secondary)', 110 | padding: '0.75rem', 111 | borderRadius: '6px', 112 | margin: '1rem 0', 113 | wordBreak: 'break-all' 114 | }); 115 | 116 | const urlLink = urlContainer.createEl('a', { 117 | href: this.url, 118 | text: this.url 119 | }); 120 | Object.assign(urlLink.style, { 121 | color: 'var(--text-accent)', 122 | textDecoration: 'none' 123 | }); 124 | urlLink.setAttribute('target', '_blank'); 125 | 126 | // Add action buttons 127 | const buttonContainer = container.createDiv(); 128 | Object.assign(buttonContainer.style, { 129 | display: 'flex', 130 | gap: '0.5rem', 131 | justifyContent: 'center', 132 | marginBottom: '0.5rem' 133 | }); 134 | 135 | // Copy link button 136 | const copyButton = buttonContainer.createEl('button', { 137 | text: t('publish_result_modal.copy_link') 138 | }); 139 | Object.assign(copyButton.style, { 140 | minWidth: '100px' 141 | }); 142 | copyButton.onclick = async () => { 143 | await navigator.clipboard.writeText(this.url); 144 | copyButton.setText(t('common.copied')); 145 | setTimeout(() => { 146 | copyButton.setText(t('publish_result_modal.copy_link')); 147 | }, 2000); 148 | }; 149 | // Visit button 150 | const visitButton = buttonContainer.createEl('button', { 151 | cls: 'mod-cta', 152 | text: t('publish_result_modal.visit_post') 153 | }); 154 | Object.assign(visitButton.style, { 155 | minWidth: '100px' 156 | }); 157 | visitButton.onclick = () => { 158 | window.open(this.url, '_blank'); 159 | this.close(); 160 | }; 161 | 162 | } 163 | 164 | onClose() { 165 | const {contentEl} = this; 166 | contentEl.empty(); 167 | } 168 | } -------------------------------------------------------------------------------- /src/oauth/oauth.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { BrowserWindow } from "@electron/remote"; 3 | 4 | const authBase = "https://quaily.com"; 5 | const apiBase = "https://api.quail.ink"; 6 | const clientID = "866c9dba-e267-47b8-ad48-2ca9105dd3cd"; 7 | const redirectURI = "http://localhost:63812/oauth/code"; 8 | const scopes = ["user.full", "post.write"]; 9 | 10 | function generateCodeVerifier() { 11 | // Create 32 random bytes 12 | const randomBytes = new Uint8Array(32); 13 | crypto.getRandomValues(randomBytes); 14 | 15 | // Base64-url encode (raw) 16 | // 1. Convert bytes to a normal Base64 17 | // 2. Replace unsafe URL chars (+, /, =) so we get a 'raw url encoding' 18 | let base64String = btoa(String.fromCharCode(...randomBytes)) 19 | .replace(/\+/g, "-") 20 | .replace(/\//g, "_") 21 | .replace(/=/g, ""); 22 | 23 | return base64String; 24 | } 25 | 26 | function getCodeFromUrl(url) { 27 | const redirectUrl = new URL(url); 28 | // If it's your expected callback route: 29 | // We got the callback, so we can parse params, e.g. ?code=XYZ 30 | const code = redirectUrl.searchParams.get('code'); 31 | const returnedState = redirectUrl.searchParams.get("state") || ""; 32 | if (code || returnedState) { 33 | // Exchange your code for a token, or do other flow tasks 34 | return { code, returnedState }; 35 | } 36 | return { code: "", returnedState: "" }; 37 | } 38 | 39 | 40 | async function startLoginElectron() { 41 | const codeVerifier = generateCodeVerifier(); 42 | const codeChallenge = codeVerifier; // for plain 43 | const state = crypto.randomUUID(); 44 | 45 | // Construct the authorization URL 46 | const authURL = new URL("/oauth/authorize", authBase); 47 | authURL.searchParams.set("response_type", "code"); 48 | authURL.searchParams.set("client_id", clientID); 49 | authURL.searchParams.set("redirect_uri", redirectURI); 50 | authURL.searchParams.set("scope", scopes.join(" ")); 51 | authURL.searchParams.set("state", state); 52 | authURL.searchParams.set("code_challenge", codeChallenge); 53 | authURL.searchParams.set("code_challenge_method", "plain"); 54 | 55 | // Open a window to show the provider’s login/consent page 56 | const loginWindow = new BrowserWindow({ 57 | width: 600, 58 | height: 800, 59 | webPreferences: { 60 | nodeIntegration: false, 61 | contextIsolation: true, 62 | }, 63 | }); 64 | 65 | // Listen for the callback URL via webRequest 66 | const { session: { webRequest } } = loginWindow.webContents; 67 | 68 | const filter = { 69 | urls: [ 70 | "http://localhost:63812/oauth/code*" 71 | ] 72 | }; 73 | 74 | return new Promise((resolve, reject) => { 75 | const handleCallback = async (event, url) => { 76 | const { code, returnedState } = getCodeFromUrl(event, url, loginWindow); 77 | if (returnedState !== state) { 78 | throw new Error("State mismatch. Potential CSRF attack or lost session."); 79 | } 80 | if (!code) { 81 | throw new Error("No authorization code found in callback."); 82 | } 83 | // We can close the login window now 84 | loginWindow.close(); 85 | 86 | // Exchange the code for a token 87 | const token = await exchangeCodeForToken( 88 | code, 89 | codeVerifier, 90 | redirectURI, 91 | clientID 92 | ); 93 | return token; 94 | } 95 | // Listen for a navigation or redirect 96 | loginWindow.webContents.on('will-navigate', (event, url) => { 97 | event.preventDefault(); 98 | try { 99 | const token = handleCallback(url); 100 | resolve(token); 101 | } catch (err) { 102 | loginWindow.close(); 103 | reject(err); 104 | } 105 | }); 106 | 107 | // Also handle redirect 108 | loginWindow.webContents.on('did-redirect-navigation', (event, url, isInPlace, isMainFrame) => { 109 | event.preventDefault(); 110 | try { 111 | const token = handleCallback(url); 112 | resolve(token); 113 | } catch (err) { 114 | loginWindow.close(); 115 | reject(err); 116 | } 117 | }); 118 | 119 | // Intercept the callback request 120 | webRequest.onBeforeRequest(filter, async ({ url }) => { 121 | try { 122 | const token = handleCallback(url); 123 | resolve(token); 124 | } catch (err) { 125 | loginWindow.close(); 126 | reject(err); 127 | } 128 | }); 129 | 130 | // Load the authorization URL 131 | loginWindow.loadURL(authURL.toString()); 132 | 133 | loginWindow.on("closed", () => { 134 | }); 135 | }); 136 | } 137 | 138 | /** 139 | * Exchange an authorization code + code_verifier for an access token. 140 | * 141 | * @param {string} code 142 | * @param {string} verifier 143 | * @param {string} redirectURI 144 | */ 145 | async function exchangeCodeForToken(code, verifier, redirectURI) { 146 | // token endpoint 147 | const tokenURL = new URL("/oauth/token", apiBase); 148 | 149 | // Build form data 150 | const bodyData = new URLSearchParams(); 151 | bodyData.set("grant_type", "authorization_code"); 152 | bodyData.set("code", code); 153 | bodyData.set("redirect_uri", redirectURI); 154 | bodyData.set("client_id", clientID); 155 | bodyData.set("code_verifier", verifier); 156 | 157 | const response = await fetch(tokenURL.toString(), { 158 | method: "POST", 159 | headers: { 160 | "Content-Type": "application/x-www-form-urlencoded", 161 | }, 162 | body: bodyData, 163 | }); 164 | 165 | if (!response.ok) { 166 | const errorText = await response.text(); 167 | throw new Error(`Token endpoint error: ${response.status} ${errorText}`); 168 | } 169 | 170 | // Return JSON: { access_token, refresh_token, expires_in, ... } 171 | return await response.json(); 172 | } 173 | 174 | async function refreshToken(refreshToken) { 175 | const tokenURL = new URL("/oauth/token", apiBase); 176 | 177 | const data = new URLSearchParams(); 178 | data.set("grant_type", "refresh_token"); 179 | data.set("refresh_token", refreshToken); 180 | data.set("client_id", clientID); 181 | 182 | const resp = await fetch(tokenURL.toString(), { 183 | method: "POST", 184 | headers: { 185 | "Content-Type": "application/x-www-form-urlencoded", 186 | }, 187 | body: data, 188 | }); 189 | 190 | if (!resp.ok) { 191 | const errorText = await resp.text(); 192 | throw new Error(`Refresh token error: ${resp.status} ${errorText}`); 193 | } 194 | 195 | return await resp.json(); 196 | } 197 | 198 | export { 199 | startLoginElectron, 200 | exchangeCodeForToken, 201 | refreshToken 202 | } -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'obsidian'; 2 | import { getActions } from './src/actions'; 3 | import { QuailPluginSettings } from './src/interface'; 4 | import { Client, AuxiliaClient } from 'quail-js'; 5 | import { startLoginElectron, refreshToken } from './src/oauth/oauth'; 6 | import QuailSettingTab from './src/setting'; 7 | 8 | const DEFAULT_SETTINGS: QuailPluginSettings = { 9 | listID: '', 10 | listSlug: '', 11 | strictLineBreaks: true, 12 | useEnglishCmds: false, 13 | useFirstImageAsCover: false, 14 | // tokens 15 | accessToken: '', 16 | refreshToken: '', 17 | tokenExpiry: '', 18 | // user info 19 | me: null, 20 | lists: [], 21 | } 22 | 23 | export default class QuailPlugin extends Plugin implements QuailPlugin { 24 | settings: QuailPluginSettings; 25 | client: any; 26 | auxiliaClient: any; 27 | 28 | async onload() { 29 | await this.loadSettings(); 30 | 31 | await this.updateToken(); 32 | 33 | this.getClients(); 34 | 35 | if (this.isLogged()) { 36 | await this.updateChannels(); 37 | 38 | await this.saveSettings(); 39 | } 40 | 41 | await this.loadActions(); 42 | // const actions = getActions(this.client, this); 43 | // for (let ix = 0; ix < actions.length; ix++) { 44 | // const action:any = actions[ix]; 45 | // this.addCommand(action); 46 | // } 47 | 48 | // This adds a settings tab so the user can configure various aspects of the plugin 49 | this.addSettingTab(new QuailSettingTab(this.app, this)); 50 | 51 | // If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin) 52 | // Using this function will automatically remove the event listener when this plugin is disabled. 53 | // this.registerDomEvent(document, 'click', (evt: MouseEvent) => { 54 | // }); 55 | 56 | // When registering intervals, this function will automatically clear the interval when the plugin is disabled. 57 | // this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000)); 58 | } 59 | 60 | onunload() { 61 | } 62 | 63 | getClients() { 64 | this.client = new Client({ 65 | access_token: this.settings.accessToken, 66 | apibase: 'https://api.quail.ink', 67 | debug: false, 68 | }); 69 | this.auxiliaClient = new AuxiliaClient({ 70 | access_token: this.settings.accessToken, 71 | apibase: 'https://api.quail.ink', 72 | debug: false, 73 | }); 74 | } 75 | 76 | async loadActions() { 77 | const actions = getActions(this); 78 | if (actions.length === 0) { 79 | console.error("quaily.loadActions: no actions found"); 80 | return; 81 | } else if (actions.length === 1 && actions[0].id === 'quail-login') { 82 | this.addCommand(actions[0]); 83 | } else { 84 | (this.app as any).commands.removeCommand('quail-login'); 85 | for (let ix = 0; ix < actions.length; ix++) { 86 | const action:any = actions[ix]; 87 | this.addCommand(action); 88 | } 89 | } 90 | } 91 | 92 | async loadSettings() { 93 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 94 | } 95 | 96 | async saveSettings() { 97 | await this.saveData(this.settings); 98 | } 99 | 100 | async updateChannels() { 101 | const lists = await this.client.getUserLists(this.settings.me.id); 102 | this.settings.lists = lists; 103 | let found = false; 104 | for (let ix = 0; ix < lists.length; ix++) { 105 | const list = lists[ix]; 106 | if (`${list.id}` === this.settings.listID || list.slug === this.settings.listSlug) { 107 | found = true; 108 | this.settings.listID = list.id; 109 | this.settings.listSlug = list.slug; 110 | break; 111 | } 112 | } 113 | if (!found) { 114 | if (lists.length > 0) { 115 | this.settings.listID = lists[0].id; 116 | this.settings.listSlug = lists[0].slug; 117 | } else { 118 | this.settings.listID = ''; 119 | this.settings.listSlug = ''; 120 | } 121 | } 122 | } 123 | 124 | async login() { 125 | try { 126 | console.log("quaily.login: oauth flow start"); 127 | // Start the login flow in a popup 128 | const token = await startLoginElectron(); 129 | // if your auth server is at localhost:8080 130 | console.log("quaily.login: token expiry:", token.expiry); 131 | this.settings.accessToken = token.access_token; 132 | this.settings.refreshToken = token.refresh_token; 133 | this.settings.tokenExpiry = token.expiry; 134 | 135 | // update the client 136 | this.getClients(); 137 | 138 | // get user info 139 | const me = await this.client.getMe(); 140 | this.settings.me = me; 141 | 142 | // get lists 143 | await this.updateChannels(); 144 | 145 | await this.saveSettings(); 146 | 147 | await this.loadActions(); 148 | // store them somewhere safe (e.g. Obsidian plugin storage) 149 | } catch (err) { 150 | console.error("quaily.login: oauth flow error:", err); 151 | } 152 | } 153 | 154 | async refreshToken() { 155 | try { 156 | console.log("quaily.refreshToken: refresh flow start"); 157 | // Start the login flow in a popup 158 | const token = await refreshToken(this.settings.refreshToken) 159 | // if your auth server is at localhost:8080 160 | console.log("quaily.refreshToken: access token expiry:", token.expiry); 161 | this.settings.accessToken = token.access_token; 162 | this.settings.refreshToken = token.refresh_token; 163 | this.settings.tokenExpiry = token.expiry; 164 | await this.saveSettings(); 165 | } catch (err) { 166 | console.error("quaily.refreshToken: refresh token flow error:", err); 167 | } 168 | } 169 | 170 | async clearTokens() { 171 | console.log("quaily.clearTokens: clear tokens"); 172 | this.settings.accessToken = ''; 173 | this.settings.refreshToken = ''; 174 | this.settings.tokenExpiry = ''; 175 | await this.saveSettings(); 176 | await this.loadActions(); 177 | } 178 | 179 | isLogged () { 180 | if (this.settings.accessToken === '' 181 | || this.settings.refreshToken === '' 182 | || this.settings.tokenExpiry === '' 183 | || this.settings.me === null 184 | || this.settings.lists?.length === 0 185 | ) { 186 | return false; 187 | } 188 | return true 189 | } 190 | 191 | updateToken() { 192 | if (this.settings.tokenExpiry !== '') { 193 | const expiry = new Date(this.settings.tokenExpiry); 194 | const now = new Date(); 195 | const refreshTokenThreshold = 3600*24*364*1000; 196 | const accessTokenThreshold = 3600*12*1000; 197 | if (expiry.getTime() <= now.getTime() - refreshTokenThreshold) { 198 | // if the expiry is more than 364 days ago, need to login again 199 | console.log("quaily.updateToken: token expired, clear tokens", expiry, now); 200 | this.clearTokens(); 201 | } else if (expiry.getTime() <= now.getTime() - accessTokenThreshold) { 202 | // refresh the token if it's less than 12 hours from expiry 203 | console.log("quaily.updateToken: token expired, refresh token", expiry, now); 204 | this.refreshToken(); 205 | } else { 206 | this.refreshToken(); 207 | console.log("quaily.updateToken: token is still valid, nothing to do"); 208 | } 209 | } else { 210 | console.log("quaily.updateToken: no token found, clear tokens"); 211 | this.clearTokens(); 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/setting/index.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting } from 'obsidian'; 2 | import { QuailPlugin } from '../interface'; 3 | import manifest from '../../manifest.json'; 4 | import { ErrorModal, LoadingModal, MessageModal, PublishResultModal } from 'src/modals'; 5 | import { t } from 'src/i18n'; 6 | 7 | class QuailSettingTab extends PluginSettingTab { 8 | plugin: QuailPlugin; 9 | app: App; 10 | showDebugCounter = 0; 11 | showDebugSection = false; 12 | constructor(app: App, plugin: QuailPlugin) { 13 | super(app, plugin as never); // Type assertion to fix type mismatch 14 | this.plugin = plugin; 15 | this.app = app; 16 | } 17 | 18 | display(): void { 19 | const {containerEl} = this; 20 | 21 | containerEl.empty(); 22 | 23 | containerEl.createEl("h5", { text: "Quaily" }); 24 | 25 | if (this.plugin.isLogged()) { 26 | new Setting(containerEl) 27 | .setHeading() 28 | .setName(t('settings.account.logged.title', {name: this.plugin.settings.me.name})) 29 | .setDesc(t('settings.account.logged.desc', {email: this.plugin.settings.me.email})) 30 | .addButton(button => button 31 | .setButtonText(t('common.logout')) 32 | .setWarning() 33 | .onClick(async () => { 34 | await this.plugin.clearTokens(); 35 | this.display(); 36 | }) 37 | ) 38 | } else { 39 | new Setting(containerEl) 40 | .setHeading() 41 | .setName(t('settings.account.need_to_login.title')) 42 | .setDesc(t('settings.account.need_to_login.desc')) 43 | .addButton(button => button 44 | .setCta() 45 | .setButtonText(t('common.login')) 46 | .onClick(async () => { 47 | await this.plugin.login(); 48 | this.display(); 49 | }) 50 | ) 51 | } 52 | 53 | const chSec = new Setting(containerEl) 54 | .setName(t('settings.channel.title')) 55 | .setDesc(t('settings.channel.desc')) 56 | if (this.plugin.settings.lists?.length !== 0) { 57 | chSec.addDropdown(dropdown => { 58 | if (this.plugin.settings.lists?.length === 0) { 59 | dropdown.addOption('none', t('settings.channel.empty')); 60 | } else { 61 | for (let ix = 0; ix < this.plugin.settings.lists.length; ix++) { 62 | const list = this.plugin.settings.lists[ix]; 63 | dropdown.addOption(list.id, list.title); 64 | } 65 | } 66 | dropdown.setValue(this.plugin.settings.listID); 67 | dropdown.onChange(async (value) => { 68 | this.plugin.settings.listID = value; 69 | this.plugin.settings.listSlug = this.plugin.settings.lists.find((list:any) => list.id === value)?.slug || ''; 70 | await this.plugin.saveSettings(); 71 | }); 72 | }) 73 | } else { 74 | chSec.addButton(button => button 75 | .setCta() 76 | .setButtonText(t('settings.channel.create')) 77 | .onClick(async () => { 78 | window.open('https://quaily.com/dashboard', '_blank'); 79 | }) 80 | ) 81 | } 82 | 83 | containerEl.createEl("h6", { text: t('settings.behavior') }); 84 | 85 | new Setting(containerEl) 86 | .setName(t('settings.behavior.use_english_cmds.title')) 87 | .setDesc(t('settings.behavior.use_english_cmds.desc')) 88 | .addToggle(toggle => toggle 89 | .setValue(this.plugin.settings.useEnglishCmds) 90 | .onChange(async (value) => { 91 | this.plugin.settings.useEnglishCmds = value; 92 | await this.plugin.saveSettings(); 93 | })); 94 | 95 | new Setting(containerEl) 96 | .setName(t('settings.behavior.use_first_image_as_cover.title')) 97 | .setDesc(t('settings.behavior.use_first_image_as_cover.desc')) 98 | .addToggle(toggle => toggle 99 | .setValue(this.plugin.settings.useFirstImageAsCover) 100 | .onChange(async (value) => { 101 | this.plugin.settings.useFirstImageAsCover = value; 102 | await this.plugin.saveSettings(); 103 | })); 104 | 105 | 106 | containerEl.createEl("h6", { text: t('settings.editor') }); 107 | 108 | new Setting(containerEl) 109 | .setName(t('settings.editor.strict_line_breaks.title')) 110 | .setDesc(t('settings.editor.strict_line_breaks.desc')) 111 | .addToggle(toggle => toggle 112 | .setValue(this.plugin.settings.strictLineBreaks) 113 | .onChange(async (value) => { 114 | this.plugin.settings.strictLineBreaks = value; 115 | await this.plugin.saveSettings(); 116 | })); 117 | 118 | const version = containerEl.createDiv({ 119 | cls: "setting-item", 120 | }); 121 | 122 | version.innerText = `version: ${manifest.version}`; 123 | version.style.fontSize = "0.8em"; 124 | version.style.width = "100%"; 125 | version.style.textAlign = "left"; 126 | version.style.color = "gray"; 127 | version.onclick = () => { 128 | if (this.showDebugCounter === 4) { 129 | this.showDebugSection = !this.showDebugSection; 130 | this.showDebugCounter = 0; 131 | this.display(); 132 | } 133 | this.showDebugCounter++; 134 | } 135 | 136 | if (this.showDebugSection) { 137 | // debug section 138 | containerEl.createEl("h6", { text: "Debug" }); 139 | 140 | const textareaContainer = containerEl.createDiv({ 141 | cls: "setting-item", 142 | }); 143 | 144 | const textarea = textareaContainer.createEl("textarea", { 145 | cls: "setting-item-control", 146 | attr: { placeholder: "Enter your settings here...", disabled: "true" }, 147 | }); 148 | 149 | textarea.style.width = "100%"; 150 | textarea.style.height = "200px"; 151 | textarea.style.textAlign = "left"; 152 | textarea.value = `list id: ${this.plugin.settings.listID} 153 | list slug: ${this.plugin.settings.listSlug} 154 | access token: ${this.plugin.settings.accessToken} 155 | refresh token: ${this.plugin.settings.refreshToken} 156 | token expiry: ${this.plugin.settings.tokenExpiry}`; 157 | 158 | const buttonsSec = new Setting(containerEl) 159 | .setName('Dialog Test') 160 | buttonsSec.addButton(button => button 161 | .setButtonText('Publish') 162 | .onClick(async () => { 163 | new PublishResultModal(this.app, null, { 164 | url: "https://quaily.com", 165 | title: "This is a test title", 166 | summary: "This is a test summary. The gray fox jumps over the lazy dog.", 167 | coverImageUrl: "https://quaily.com/portal-images/illustration/finance-you-0.webp" 168 | }).open(); 169 | }) 170 | ) 171 | buttonsSec.addButton(button => button 172 | .setButtonText('Message') 173 | .onClick(async () => { 174 | new MessageModal(this.app, { title: 'Test', message: 'This is a test message.', icon: '🤖', iconColor: 'blue' }).open(); 175 | }) 176 | ) 177 | buttonsSec.addButton(button => button 178 | .setButtonText('Loading') 179 | .onClick(async () => { 180 | new LoadingModal(this.app).open(); 181 | }) 182 | ) 183 | buttonsSec.addButton(button => button 184 | .setButtonText('Error') 185 | .onClick(async () => { 186 | new ErrorModal(this.app, new Error('This is a test error.')).open(); 187 | }) 188 | ) 189 | buttonsSec.addButton(button => button 190 | .setButtonText('expire token') 191 | .onClick(async () => { 192 | this.plugin.settings.tokenExpiry = '2025-03-15T00:00:00Z'; 193 | await this.plugin.saveSettings(); 194 | }) 195 | ) 196 | } 197 | } 198 | } 199 | 200 | export default QuailSettingTab; -------------------------------------------------------------------------------- /src/frontmatter.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | 3 | function encodeQuote(str:string) { 4 | return str.replace(/"/g, '\\"'); 5 | } 6 | 7 | export default { 8 | suggestFrontmatter: async function(client:any, title: string, content: string, images: any[]) { 9 | const ret:Record = {}; 10 | // default datetime 11 | const now = dayjs(); 12 | ret["datetime"] = now.format("YYYY-MM-DD HH:mm"); 13 | 14 | // default slug, summary, tags 15 | const resp = await client.generateFrontmatter(title, content, ['slug', 'summary', 'tags']); 16 | ret["slug"] = encodeQuote(resp.slug as string); 17 | ret["summary"] = encodeQuote(resp.summary as string); 18 | ret["tags"] = encodeQuote(resp.tags as string); 19 | ret["cover_image_url"] = ""; 20 | return { 21 | "slug": `${ret.slug}`, 22 | "datetime": `${ret.datetime}`, 23 | "summary": `${ret.summary}`, 24 | "tags": `${ret.tags}`, 25 | "cover_image_url": `${ret.cover_image_url}` 26 | }; 27 | }, 28 | 29 | emptyFrontmatter: function() { 30 | const now = dayjs(); 31 | return { 32 | "slug": "INSERT_YOUR_SLUG_HERE", 33 | "datetime": `${now.format("YYYY-MM-DD HH:mm")}`, 34 | "summary": "INSERT_YOUR_SUMMARY_HERE", 35 | "tags": "INSERT_YOUR_TAGS_HERE", 36 | "theme": "light", 37 | "cover_image_url": "", 38 | } 39 | }, 40 | 41 | replaceFields: function (frontmatter: Record): Record { 42 | // possible transformation 43 | // - summary: description, subtitle 44 | // - datetime: date 45 | const ret :Record = Object.assign({}, frontmatter); 46 | const now = dayjs().format('YYYY-MM-DDTHH:mm:ssZ'); 47 | ret.summary = frontmatter.summary || frontmatter.description || frontmatter.subtitle || ''; 48 | ret.datetime = frontmatter.datetime || frontmatter.date || now; 49 | ret.cover_image_url = frontmatter.cover_image_url || frontmatter.cover || ''; 50 | return ret 51 | }, 52 | 53 | verifyFrontmatter: function (frontmatter: Record): { verified: boolean, reason: string } { 54 | const keys: Record = {}; 55 | 56 | // slug is required 57 | if (!frontmatter.slug) { 58 | return { verified: false, reason: '`slug` is required' }; 59 | } 60 | 61 | for (const key in frontmatter) { 62 | if (Object.prototype.hasOwnProperty.call(frontmatter, key)) { 63 | const value = frontmatter[key]; 64 | const obj:any = { validated: false, reason: '' }; 65 | switch (key) { 66 | case "slug": 67 | // slug is number, english, dash 68 | if (typeof value === "string") { 69 | if (/^[a-zA-Z0-9-_]+$/.test(value)) { 70 | obj.validated = true; 71 | } else { 72 | obj.reason = '`slug` can only contain english, number, dash and underline'; 73 | } 74 | } else { 75 | obj.reason = '`slug` must be string'; 76 | } 77 | keys[key] = obj; 78 | break; 79 | case "title": 80 | // title is string 81 | if (typeof value === "string") { 82 | obj.validated = true; 83 | } else { 84 | obj.reason = '`title` must be string'; 85 | } 86 | keys[key] = obj; 87 | break; 88 | case "tags": { 89 | // tags is string, split by ',' 90 | console.log(typeof value, value.constructor.name); 91 | const re = /\s*([^\s,]+)\s*(?:,\s*|$)/g; 92 | if (typeof value === "string") { 93 | const trimed = value.trim(); 94 | if (trimed.length !== 0) { 95 | if (re.test(trimed)) { 96 | obj.validated = true; 97 | } else { 98 | obj.reason = '`tags` must be string, split by comma'; 99 | } 100 | } else { 101 | obj.validated = true; 102 | } 103 | } else if (Array.isArray(value)) { 104 | obj.validated = true; 105 | } else { 106 | obj.reason = '`tags` must be string'; 107 | } 108 | keys[key] = obj; 109 | break; 110 | } 111 | case "datetime": 112 | // datetime is string that can be parsed by dayjs 113 | if (typeof value === "string") { 114 | obj.validated = true; 115 | } else { 116 | obj.reason = '`datetime` must be date string'; 117 | } 118 | keys[key] = obj; 119 | break; 120 | case "summary": 121 | // summary is string 122 | if (typeof value === "string") { 123 | obj.validated = true; 124 | } else { 125 | obj.reason = '`summary` must be string'; 126 | } 127 | keys[key] = obj; 128 | break; 129 | case "cover_image_url": 130 | // cover_image_url is string 131 | if (typeof value === "string") { 132 | obj.validated = true; 133 | } else { 134 | obj.reason = '`cover_image_url` must be string'; 135 | } 136 | keys[key] = obj; 137 | break; 138 | default: 139 | break; 140 | } 141 | } 142 | } 143 | 144 | for (const key in keys) { 145 | if (Object.prototype.hasOwnProperty.call(keys, key)) { 146 | const item = keys[key]; 147 | if (!item.validated) { 148 | return item; 149 | } 150 | } 151 | } 152 | return { verified: true, reason: '' }; 153 | }, 154 | 155 | formalizeFrontmatter: function (frontmatter: any, text: string): any { 156 | const ret :Record = {} 157 | if (frontmatter?.slug?.trim().length === 0) { 158 | return false 159 | } 160 | ret.slug = frontmatter.slug.trim(); 161 | 162 | if (frontmatter?.datetime?.trim().length !== 0) { 163 | try { 164 | ret.datetime = dayjs(frontmatter.datetime.trim()).format('YYYY-MM-DDTHH:mm:ssZ'); 165 | } catch (e) { 166 | ret.datetime = dayjs().format('YYYY-MM-DDTHH:mm:ssZ'); 167 | } 168 | } else { 169 | ret.datetime = dayjs().format('YYYY-MM-DDTHH:mm:ssZ'); 170 | } 171 | 172 | ret.summary = frontmatter.summary?.trim() || '' 173 | 174 | if (frontmatter?.cover_image_url?.trim().length !== 0) { 175 | ret.cover_image_url = frontmatter.cover_image_url?.trim() || ""; 176 | } 177 | 178 | if (frontmatter?.tags.constructor.name === "Array") { 179 | if (frontmatter?.tags.length !== 0) { 180 | ret.tags = frontmatter.tags.join(",") || ""; 181 | } 182 | const tags = frontmatter.tags.map((x:any) => { 183 | if (typeof x === "string") { 184 | return x.trim(); 185 | } 186 | return ""; 187 | }).filter((x:any) => x.length !== 0); 188 | ret.tags = tags.join(",") || ""; 189 | } else if (frontmatter?.tags.constructor.name === "String") { 190 | if (frontmatter?.tags?.trim().length !== 0) { 191 | ret.tags = frontmatter.tags?.trim() || ""; 192 | } 193 | } else { 194 | ret.tags = ""; 195 | } 196 | 197 | if (frontmatter?.title?.trim().length !== 0) { 198 | ret.title = frontmatter.title?.trim() || ""; 199 | } 200 | 201 | return ret; 202 | } 203 | } 204 | 205 | -------------------------------------------------------------------------------- /src/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'obsidian'; 2 | import { LoadingModal, MessageModal, ErrorModal } from '../modals'; 3 | import util from '../util'; 4 | import { QuailPluginSettings } from '../interface'; 5 | import fm from "../frontmatter"; 6 | import aigen from './aigen'; 7 | import save from './save'; 8 | import preview from './preview'; 9 | import publish from './publish'; 10 | import unpublish from './unpublish'; 11 | import send from './send'; 12 | import setChannel from './set-channel'; 13 | import insertMetadata from './insert-metadata'; 14 | import { t } from 'src/i18n'; 15 | 16 | async function uploadAttachment(client: any, image: any) { 17 | const formData = new FormData(); 18 | const picArray = new Uint8Array(image.data).buffer; 19 | 20 | formData.append('file', new Blob([picArray], { type: image.mimeType }), image.name); 21 | 22 | const resp = await client.uploadAttachment(formData); 23 | return resp.view_url 24 | } 25 | 26 | async function arrangeArticle(app: App, client: any, auxiliaClient: any, settings: QuailPluginSettings) { 27 | const { title, content, frontmatter: frontmatterO, images, err } = await util.getActiveFileContent(app); 28 | if (err != null) { 29 | new ErrorModal(app, new Error(err.toString())).open(); 30 | return { frontmatter: null, content: null}; 31 | } 32 | 33 | const frontmatter = fm.replaceFields(frontmatterO); 34 | 35 | const { verified, reason } = fm.verifyFrontmatter(frontmatter) 36 | if (!verified) { 37 | new MessageModal(app, { 38 | title: t('message_modal.failed_to_verify_meta.title'), 39 | message: reason, 40 | icon: "🤖", 41 | iconColor: "orange", 42 | actions: [{ 43 | text: t('common.generate'), 44 | primary: true, 45 | click: (dialog: any) => { 46 | aigen(app, auxiliaClient, settings).callback(); 47 | dialog.close(); 48 | } 49 | },{ 50 | text: t('common.cancel'), 51 | close: true, 52 | }] 53 | }).open(); 54 | 55 | return { frontmatter: null, content: null, }; 56 | } 57 | 58 | // upload images 59 | const oldUrls:string[] = []; 60 | const newUrls:string[] = []; 61 | for (let ix = 0; ix < images.length; ix++) { 62 | const img = images[ix]; 63 | if (img) { 64 | try { 65 | const viewUrl = await uploadAttachment(client, img) 66 | newUrls.push(viewUrl) 67 | oldUrls.push(img.pathname) 68 | console.log(`quaily.upload image: ${img.pathname}, new url: ${viewUrl}`) 69 | } catch (e) { 70 | console.log("quaily.upload image error: ", e) 71 | new ErrorModal(app, new Error(e)).open(); 72 | return { frontmatter: null, content: null}; 73 | } 74 | } 75 | } 76 | 77 | // upload cover image 78 | if (frontmatter?.cover_image) { 79 | try { 80 | const viewUrl = await uploadAttachment(client, frontmatter.cover_image) 81 | frontmatter.cover_image_url = viewUrl; 82 | console.log(`quaily.upload cover: ${frontmatter.cover_image.pathname}, new url: ${viewUrl}`) 83 | } catch (e) { 84 | console.log("quaily.upload cover error: ", e) 85 | new ErrorModal(app, new Error(e)).open(); 86 | return { frontmatter: null, content: null}; 87 | } 88 | } 89 | 90 | // replace image urls 91 | const ret:any = util.replaceImageUrls(content, oldUrls, newUrls); 92 | const newContent = ret.content.trim() || ''; 93 | const imageUrls = ret.image_urls.filter((url: string) => { 94 | return url.startsWith('https://') || url.startsWith('http://') || url.startsWith('//') 95 | }) 96 | const fmt = fm.formalizeFrontmatter(frontmatter, newContent); 97 | 98 | if (frontmatter.cover_image === null && frontmatter.cover_image_url === '') { 99 | // if the cover image is empty, use the first image as cover 100 | if (settings.useFirstImageAsCover && imageUrls.length > 0) { 101 | fmt.cover_image_url = imageUrls[0]; 102 | } 103 | } 104 | 105 | return { 106 | title: title, 107 | frontmatter: fmt, 108 | content: newContent, 109 | } 110 | } 111 | 112 | 113 | export async function savePost(app: App, client: any, auxiliaClient:any, settings: QuailPluginSettings) { 114 | const { title, frontmatter, content } = await arrangeArticle(app, client, auxiliaClient, settings); 115 | if (content == null || title == null) { 116 | return; 117 | } 118 | 119 | 120 | const checkMetadata = (fm: any) => { 121 | const fields = ['slug', 'summary', 'tags']; 122 | for (let i = 0; i < fields.length; i++) { 123 | if (fm[fields[i]] === '' || fm[fields[i]] === null || fm[fields[i]] === undefined) { 124 | return false; 125 | } 126 | } 127 | return true 128 | } 129 | 130 | if (!checkMetadata(frontmatter)) { 131 | const file = app.workspace.getActiveFile(); 132 | if (file) { 133 | // try to generate metadata 134 | const fmc:any = await fm.suggestFrontmatter(auxiliaClient, title, content, []) 135 | const proc = (frontmatter:any) => { 136 | if (file) { 137 | const loadingModal = new LoadingModal(app) 138 | loadingModal.open(); 139 | try { 140 | for (const key in fmc) { 141 | if (Object.prototype.hasOwnProperty.call(fmc, key)) { 142 | if (frontmatter[key] === '' || frontmatter[key] === null || frontmatter[key] === undefined) { 143 | console.log(`quaily.savePost: update metadata: ${key} = ${fmc[key]}`) 144 | frontmatter[key] = fmc[key]; 145 | } 146 | } 147 | } 148 | } catch (e) { 149 | loadingModal.close(); 150 | new ErrorModal(app, e).open(); 151 | } finally { 152 | loadingModal.close(); 153 | } 154 | } 155 | } 156 | app.fileManager.processFrontMatter(file, proc); 157 | } else { 158 | return ; 159 | } 160 | } 161 | 162 | let newContent = content; 163 | if (!settings.strictLineBreaks) { 164 | // \n -> \n\n 165 | newContent = newContent.replace(/\n/g, '\n\n'); 166 | } 167 | 168 | const payload = { 169 | slug: frontmatter.slug, 170 | title: frontmatter.title || title, 171 | cover_image_url: frontmatter.cover_image_url, 172 | summary: frontmatter.summary, 173 | content: newContent, 174 | tags: frontmatter.tags, 175 | theme: frontmatter.theme, 176 | first_published_at: frontmatter.datetime, 177 | } 178 | 179 | let resp:any = null; 180 | try { 181 | resp = await client.createPost(settings.listID, payload); 182 | } catch (e) { 183 | new ErrorModal(app, e).open(); 184 | return; 185 | } finally { 186 | // 187 | } 188 | 189 | return resp; 190 | } 191 | 192 | export function getActions(plugin: any) { 193 | const app = plugin.app; 194 | const settings = plugin.settings; 195 | const client = plugin.client; 196 | const auxiliaClient = plugin.auxiliaClient; 197 | 198 | const loginAction = [ 199 | { 200 | id: 'quail-login', 201 | name: 'Login', 202 | callback: async () => { 203 | await plugin.login(); 204 | } 205 | } 206 | ]; 207 | 208 | // check token status and expiry 209 | if (settings.accessToken === '' || settings.refreshToken === '' || settings.tokenExpiry === '') { 210 | return loginAction; 211 | } 212 | 213 | return [ 214 | publish(app, client, auxiliaClient, settings), 215 | unpublish(app, client, settings), 216 | save(app, client, auxiliaClient, settings), 217 | preview(app, client, auxiliaClient, settings), 218 | send(app, client, settings), 219 | aigen(app, auxiliaClient, settings), 220 | setChannel(app, settings, plugin.saveSettings.bind(plugin)), 221 | insertMetadata(app, auxiliaClient, settings), 222 | ]; 223 | } 224 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { TFile, App } from 'obsidian'; 2 | 3 | export default { 4 | getImagePaths : function (markdownContent: string) { 5 | const imageRegex = /!\[(.*?)\]\((.*?)\)/g; // matches markdown image syntax 6 | const matches:string[] = []; 7 | let match:RegExpExecArray|null = null; 8 | while ((match = imageRegex.exec(markdownContent))) { 9 | if (match && match.length > 2) { 10 | const item = match[2]; 11 | if (!item.startsWith("https://") && !item.startsWith("http://")) { 12 | matches.push(item); 13 | } 14 | } 15 | } 16 | return matches; 17 | }, 18 | 19 | getMimeType: function (ext: string) { 20 | const mimeTypeMap :Record = { 21 | 'jpg': 'image/jpeg', 22 | 'jpeg': 'image/jpeg', 23 | 'png': 'image/png', 24 | 'gif': 'image/gif', 25 | 'bmp': 'image/bmp', 26 | 'webp': 'image/webp', 27 | 'svg': 'image/svg+xml' 28 | }; 29 | const mimeType = mimeTypeMap[ext.toLowerCase()]; 30 | if (!mimeType) { 31 | return null 32 | } 33 | 34 | return mimeType; 35 | }, 36 | 37 | getActiveFileFrontmatter: async function (app: App) { 38 | const file = app.workspace.getActiveFile(); 39 | 40 | if (file === null) { 41 | return {frontmatter: null, content: ""} 42 | } 43 | 44 | const text = await app.vault.cachedRead(file); 45 | let content = text; 46 | 47 | const frontmatter:Record = {} 48 | const fc = (app.metadataCache.getFileCache(file) as any) 49 | const fmc = fc?.frontmatter; 50 | const fmp = fc?.frontmatterPosition; 51 | if (fmc && fmp && fmc !== undefined) { 52 | const end = fmp.end.line + 1; // accont for ending --- 53 | content = text.split("\n").slice(end).join("\n"); 54 | for (const key in fmc) { 55 | if (Object.prototype.hasOwnProperty.call(fmc, key)) { 56 | const item = fmc[key]; 57 | frontmatter[key] = item; 58 | } 59 | } 60 | } 61 | 62 | return { 63 | frontmatter, 64 | position: fmp, 65 | content, 66 | } 67 | }, 68 | 69 | getActiveFileMarkdown: async function (app: App) { 70 | const file = app.workspace.getActiveFile(); 71 | if (file === null) { 72 | return "" 73 | } 74 | 75 | const text = await app.vault.cachedRead(file); 76 | let content = text; 77 | 78 | const fc = (app.metadataCache.getFileCache(file) as any) 79 | const fmc = fc?.frontmatter; 80 | const fmp = fc?.frontmatterPosition; 81 | if (fmc && fmp && fmc !== undefined) { 82 | const end = fmp.end.line + 1; // accont for ending --- 83 | content = text.split("\n").slice(end).join("\n"); 84 | } 85 | 86 | return content; 87 | }, 88 | 89 | 90 | getCoverImage: function (app:App, path: string) { 91 | const files = app.vault.getFiles(); 92 | for (let ix = 0; ix < files.length; ix++) { 93 | const fd = files[ix]; 94 | if (fd.path === path) { 95 | return fd; 96 | } 97 | } 98 | return null; 99 | }, 100 | 101 | getImageFiles: function (app:App, currentMd: TFile) { 102 | const resolvedLinks = app.metadataCache.resolvedLinks; 103 | const files:TFile[] = []; 104 | for (const [mdFile, links] of Object.entries(resolvedLinks)) { 105 | if (currentMd.path === mdFile) { 106 | for (const [filePath, nr] of Object.entries(links)) { 107 | const ext = filePath.split('.').pop()?.toLocaleLowerCase() || ""; 108 | if (this.getMimeType(ext) !== null) { 109 | try { 110 | const AttachFile: TFile = 111 | app.vault.getAbstractFileByPath(filePath) as TFile; 112 | if (AttachFile instanceof TFile) { 113 | files.push(AttachFile); 114 | } 115 | } catch (error) { 116 | console.error(`quaily.getImageFiles: error: ${error}`); 117 | } 118 | } 119 | } 120 | } 121 | } 122 | return files; 123 | }, 124 | 125 | getActiveFileContent: async function (app: App) { 126 | const file = app.workspace.getActiveFile(); 127 | if (file) { 128 | 129 | const { frontmatter: fmc, content } = await this.getActiveFileFrontmatter(app) 130 | 131 | const coverImagePath = fmc?.cover_image_url?.trim() || "" 132 | 133 | const imgFiles = this.getImageFiles(app, file); 134 | 135 | const coverFile = this.getCoverImage(app, coverImagePath); 136 | 137 | imgFiles.push(coverFile); 138 | 139 | let coverImage:any = null; 140 | const images:Array = []; 141 | for (let ix = 0; ix < imgFiles.length; ix++) { 142 | const fd = imgFiles[ix]; 143 | if (fd) { 144 | const mimeType = this.getMimeType(fd.extension) 145 | if (mimeType === "") { 146 | continue; 147 | } 148 | const img = await app.vault.readBinary(fd) 149 | if (img.byteLength) { 150 | console.log(`quaily.getActiveFileContent: found: ${fd.path}, size: ${img.byteLength}`); 151 | const imgWrapper = { 152 | pathname: fd.path, 153 | name: fd.name, 154 | data: img, 155 | mimeType, 156 | } 157 | if (fd.path === coverImagePath) { 158 | coverImage = imgWrapper 159 | } 160 | images.push(imgWrapper); 161 | } 162 | } 163 | } 164 | 165 | const title = file.name.replace(/\.md$/, ''); 166 | 167 | return { 168 | title, 169 | content, 170 | frontmatter: { 171 | title: fmc?.title || '', 172 | slug: fmc?.slug || '', 173 | tags: fmc?.tags || '', 174 | datetime: fmc?.datetime || '', 175 | summary: fmc?.summary || '', 176 | cover_image_url: fmc?.cover_image_url || '', 177 | cover_image: coverImage, 178 | }, 179 | images, 180 | err: null, 181 | } 182 | } 183 | return { 184 | title: "", 185 | content: "", 186 | frontmatter: null, 187 | images: [], 188 | err: "no active file", 189 | } 190 | }, 191 | 192 | replaceImageUrls: function (content: string, oldUrls: string[], newUrls: string[]) { 193 | if (oldUrls.length !== newUrls.length) { 194 | console.log("quaily.replaceImageUrls: the number of old and new urls do not match, return original content"); 195 | return content; 196 | } 197 | const urlMap: any = {}; 198 | for (let ix = 0; ix < oldUrls.length; ix++) { 199 | const oldUrl = oldUrls[ix]; 200 | const newUrl = newUrls[ix]; 201 | urlMap[oldUrl] = { 202 | used: false, 203 | newUrl 204 | }; 205 | } 206 | 207 | const lines = content.split("\n"); 208 | const newLines = []; 209 | const secondRoundLines = []; 210 | const allImageURLs = []; 211 | 212 | // first round, replace ![alt](url) with ![alt](newUrl) 213 | // and replace ![[path]] with ![name](newUrl) 214 | for (let ix = 0; ix < lines.length; ix++) { 215 | const line = lines[ix]; 216 | let newLine = line; 217 | if (line.startsWith("![") && line.endsWith(")")) { 218 | const match = line.match(/!\[(.*?)\]\((.*?)\)/); 219 | if (match !== null && match.length > 1) { 220 | const oldUrl = decodeURIComponent(match[2]); 221 | if (urlMap[oldUrl]) { 222 | newLine = line.replace(`(${match[2]})`, `(${urlMap[oldUrl].newUrl})`); 223 | urlMap[oldUrl].used = true; 224 | allImageURLs.push(urlMap[oldUrl].newUrl) 225 | } else { 226 | console.log("quaily.replaceImageUrls: ignore image", oldUrl) 227 | allImageURLs.push(oldUrl) 228 | } 229 | } 230 | } else if (line.startsWith("![[") && line.endsWith("]]")) { 231 | const match = line.match(/!\[\[(.*?)\]\]/); 232 | if (match !== null && match.length > 0) { 233 | const oldUrl = decodeURIComponent(match[1]); 234 | const name = oldUrl.split("/").pop(); 235 | if (urlMap[oldUrl]) { 236 | newLine = line.replace(`![[${match[1]}]]`, `![${name || oldUrl}](${urlMap[oldUrl].newUrl})`); 237 | urlMap[oldUrl].used = true; 238 | allImageURLs.push(urlMap[oldUrl].newUrl) 239 | } else { 240 | secondRoundLines.push({line, index: ix}); 241 | } 242 | } 243 | } 244 | newLines.push(newLine); 245 | } 246 | 247 | // second round, replace ![[name]] with ![name](newUrl) 248 | // if it is a name, it could be duplicated, so we need to find the first unused one in the urlMap 249 | for (let ix = 0; ix < secondRoundLines.length; ix++) { 250 | const {index, line} = secondRoundLines[ix]; 251 | let newLine = line; 252 | if (line.startsWith("![[") && line.endsWith("]]")) { 253 | const match = line.match(/!\[\[(.*?)\]\]/); 254 | if (match !== null && match.length > 0) { 255 | const name = decodeURIComponent(match[1]); 256 | let handled = false; 257 | for (const k in urlMap) { 258 | if (urlMap[k].used === false && k.endsWith(name)) { 259 | newLine = line.replace(`![[${match[1]}]]`, `![${name}](${urlMap[k].newUrl})`); 260 | handled = true; 261 | allImageURLs.push(urlMap[k].newUrl) 262 | } 263 | } 264 | if (!handled) { 265 | console.log("quaily.replaceImageUrls:ignore image", name) 266 | allImageURLs.push(name) 267 | } 268 | } 269 | } 270 | newLines[index] = newLine; 271 | } 272 | return { 273 | content: newLines.join("\n"), 274 | image_urls: allImageURLs, 275 | } 276 | }, 277 | } 278 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | dayjs: 12 | specifier: ^1.11.7 13 | version: 1.11.11 14 | qrcode: 15 | specifier: ^1.5.3 16 | version: 1.5.4 17 | devDependencies: 18 | '@types/node': 19 | specifier: ^16.11.6 20 | version: 16.18.101 21 | '@types/qrcode': 22 | specifier: ^1.5.5 23 | version: 1.5.5 24 | '@typescript-eslint/eslint-plugin': 25 | specifier: 5.29.0 26 | version: 5.29.0(@typescript-eslint/parser@5.29.0(eslint@8.57.0)(typescript@4.7.4))(eslint@8.57.0)(typescript@4.7.4) 27 | '@typescript-eslint/parser': 28 | specifier: 5.29.0 29 | version: 5.29.0(eslint@8.57.0)(typescript@4.7.4) 30 | builtin-modules: 31 | specifier: 3.3.0 32 | version: 3.3.0 33 | esbuild: 34 | specifier: 0.17.3 35 | version: 0.17.3 36 | obsidian: 37 | specifier: latest 38 | version: 1.5.7-1(@codemirror/state@6.4.1)(@codemirror/view@6.28.4) 39 | quail-js: 40 | specifier: 0.3.18 41 | version: 0.3.18 42 | tslib: 43 | specifier: 2.4.0 44 | version: 2.4.0 45 | typescript: 46 | specifier: 4.7.4 47 | version: 4.7.4 48 | 49 | packages: 50 | 51 | '@codemirror/state@6.4.1': 52 | resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==} 53 | 54 | '@codemirror/view@6.28.4': 55 | resolution: {integrity: sha512-QScv95fiviSQ/CaVGflxAvvvDy/9wi0RFyDl4LkHHWiMr/UPebyuTspmYSeN5Nx6eujcPYwsQzA6ZIZucKZVHQ==} 56 | 57 | '@esbuild/android-arm64@0.17.3': 58 | resolution: {integrity: sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ==} 59 | engines: {node: '>=12'} 60 | cpu: [arm64] 61 | os: [android] 62 | 63 | '@esbuild/android-arm@0.17.3': 64 | resolution: {integrity: sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg==} 65 | engines: {node: '>=12'} 66 | cpu: [arm] 67 | os: [android] 68 | 69 | '@esbuild/android-x64@0.17.3': 70 | resolution: {integrity: sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A==} 71 | engines: {node: '>=12'} 72 | cpu: [x64] 73 | os: [android] 74 | 75 | '@esbuild/darwin-arm64@0.17.3': 76 | resolution: {integrity: sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw==} 77 | engines: {node: '>=12'} 78 | cpu: [arm64] 79 | os: [darwin] 80 | 81 | '@esbuild/darwin-x64@0.17.3': 82 | resolution: {integrity: sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA==} 83 | engines: {node: '>=12'} 84 | cpu: [x64] 85 | os: [darwin] 86 | 87 | '@esbuild/freebsd-arm64@0.17.3': 88 | resolution: {integrity: sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w==} 89 | engines: {node: '>=12'} 90 | cpu: [arm64] 91 | os: [freebsd] 92 | 93 | '@esbuild/freebsd-x64@0.17.3': 94 | resolution: {integrity: sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg==} 95 | engines: {node: '>=12'} 96 | cpu: [x64] 97 | os: [freebsd] 98 | 99 | '@esbuild/linux-arm64@0.17.3': 100 | resolution: {integrity: sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g==} 101 | engines: {node: '>=12'} 102 | cpu: [arm64] 103 | os: [linux] 104 | 105 | '@esbuild/linux-arm@0.17.3': 106 | resolution: {integrity: sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ==} 107 | engines: {node: '>=12'} 108 | cpu: [arm] 109 | os: [linux] 110 | 111 | '@esbuild/linux-ia32@0.17.3': 112 | resolution: {integrity: sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA==} 113 | engines: {node: '>=12'} 114 | cpu: [ia32] 115 | os: [linux] 116 | 117 | '@esbuild/linux-loong64@0.17.3': 118 | resolution: {integrity: sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA==} 119 | engines: {node: '>=12'} 120 | cpu: [loong64] 121 | os: [linux] 122 | 123 | '@esbuild/linux-mips64el@0.17.3': 124 | resolution: {integrity: sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ==} 125 | engines: {node: '>=12'} 126 | cpu: [mips64el] 127 | os: [linux] 128 | 129 | '@esbuild/linux-ppc64@0.17.3': 130 | resolution: {integrity: sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ==} 131 | engines: {node: '>=12'} 132 | cpu: [ppc64] 133 | os: [linux] 134 | 135 | '@esbuild/linux-riscv64@0.17.3': 136 | resolution: {integrity: sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow==} 137 | engines: {node: '>=12'} 138 | cpu: [riscv64] 139 | os: [linux] 140 | 141 | '@esbuild/linux-s390x@0.17.3': 142 | resolution: {integrity: sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug==} 143 | engines: {node: '>=12'} 144 | cpu: [s390x] 145 | os: [linux] 146 | 147 | '@esbuild/linux-x64@0.17.3': 148 | resolution: {integrity: sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg==} 149 | engines: {node: '>=12'} 150 | cpu: [x64] 151 | os: [linux] 152 | 153 | '@esbuild/netbsd-x64@0.17.3': 154 | resolution: {integrity: sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg==} 155 | engines: {node: '>=12'} 156 | cpu: [x64] 157 | os: [netbsd] 158 | 159 | '@esbuild/openbsd-x64@0.17.3': 160 | resolution: {integrity: sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA==} 161 | engines: {node: '>=12'} 162 | cpu: [x64] 163 | os: [openbsd] 164 | 165 | '@esbuild/sunos-x64@0.17.3': 166 | resolution: {integrity: sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q==} 167 | engines: {node: '>=12'} 168 | cpu: [x64] 169 | os: [sunos] 170 | 171 | '@esbuild/win32-arm64@0.17.3': 172 | resolution: {integrity: sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A==} 173 | engines: {node: '>=12'} 174 | cpu: [arm64] 175 | os: [win32] 176 | 177 | '@esbuild/win32-ia32@0.17.3': 178 | resolution: {integrity: sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg==} 179 | engines: {node: '>=12'} 180 | cpu: [ia32] 181 | os: [win32] 182 | 183 | '@esbuild/win32-x64@0.17.3': 184 | resolution: {integrity: sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g==} 185 | engines: {node: '>=12'} 186 | cpu: [x64] 187 | os: [win32] 188 | 189 | '@eslint-community/eslint-utils@4.4.0': 190 | resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} 191 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 192 | peerDependencies: 193 | eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 194 | 195 | '@eslint-community/regexpp@4.11.0': 196 | resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} 197 | engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 198 | 199 | '@eslint/eslintrc@2.1.4': 200 | resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} 201 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 202 | 203 | '@eslint/js@8.57.0': 204 | resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} 205 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 206 | 207 | '@humanwhocodes/config-array@0.11.14': 208 | resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} 209 | engines: {node: '>=10.10.0'} 210 | deprecated: Use @eslint/config-array instead 211 | 212 | '@humanwhocodes/module-importer@1.0.1': 213 | resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 214 | engines: {node: '>=12.22'} 215 | 216 | '@humanwhocodes/object-schema@2.0.3': 217 | resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} 218 | deprecated: Use @eslint/object-schema instead 219 | 220 | '@nodelib/fs.scandir@2.1.5': 221 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 222 | engines: {node: '>= 8'} 223 | 224 | '@nodelib/fs.stat@2.0.5': 225 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 226 | engines: {node: '>= 8'} 227 | 228 | '@nodelib/fs.walk@1.2.8': 229 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 230 | engines: {node: '>= 8'} 231 | 232 | '@types/codemirror@5.60.8': 233 | resolution: {integrity: sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==} 234 | 235 | '@types/estree@1.0.5': 236 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 237 | 238 | '@types/json-schema@7.0.15': 239 | resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 240 | 241 | '@types/node@16.18.101': 242 | resolution: {integrity: sha512-AAsx9Rgz2IzG8KJ6tXd6ndNkVcu+GYB6U/SnFAaokSPNx2N7dcIIfnighYUNumvj6YS2q39Dejz5tT0NCV7CWA==} 243 | 244 | '@types/qrcode@1.5.5': 245 | resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} 246 | 247 | '@types/tern@0.23.9': 248 | resolution: {integrity: sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==} 249 | 250 | '@typescript-eslint/eslint-plugin@5.29.0': 251 | resolution: {integrity: sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w==} 252 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 253 | peerDependencies: 254 | '@typescript-eslint/parser': ^5.0.0 255 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 256 | typescript: '*' 257 | peerDependenciesMeta: 258 | typescript: 259 | optional: true 260 | 261 | '@typescript-eslint/parser@5.29.0': 262 | resolution: {integrity: sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==} 263 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 264 | peerDependencies: 265 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 266 | typescript: '*' 267 | peerDependenciesMeta: 268 | typescript: 269 | optional: true 270 | 271 | '@typescript-eslint/scope-manager@5.29.0': 272 | resolution: {integrity: sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA==} 273 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 274 | 275 | '@typescript-eslint/type-utils@5.29.0': 276 | resolution: {integrity: sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg==} 277 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 278 | peerDependencies: 279 | eslint: '*' 280 | typescript: '*' 281 | peerDependenciesMeta: 282 | typescript: 283 | optional: true 284 | 285 | '@typescript-eslint/types@5.29.0': 286 | resolution: {integrity: sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg==} 287 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 288 | 289 | '@typescript-eslint/typescript-estree@5.29.0': 290 | resolution: {integrity: sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ==} 291 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 292 | peerDependencies: 293 | typescript: '*' 294 | peerDependenciesMeta: 295 | typescript: 296 | optional: true 297 | 298 | '@typescript-eslint/utils@5.29.0': 299 | resolution: {integrity: sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A==} 300 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 301 | peerDependencies: 302 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 303 | 304 | '@typescript-eslint/visitor-keys@5.29.0': 305 | resolution: {integrity: sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ==} 306 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 307 | 308 | '@ungap/structured-clone@1.2.0': 309 | resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} 310 | 311 | acorn-jsx@5.3.2: 312 | resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 313 | peerDependencies: 314 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 315 | 316 | acorn@8.12.1: 317 | resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} 318 | engines: {node: '>=0.4.0'} 319 | hasBin: true 320 | 321 | ajv@6.12.6: 322 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 323 | 324 | ansi-regex@5.0.1: 325 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 326 | engines: {node: '>=8'} 327 | 328 | ansi-styles@4.3.0: 329 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 330 | engines: {node: '>=8'} 331 | 332 | argparse@2.0.1: 333 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 334 | 335 | array-union@2.1.0: 336 | resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 337 | engines: {node: '>=8'} 338 | 339 | balanced-match@1.0.2: 340 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 341 | 342 | brace-expansion@1.1.11: 343 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 344 | 345 | braces@3.0.3: 346 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 347 | engines: {node: '>=8'} 348 | 349 | builtin-modules@3.3.0: 350 | resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} 351 | engines: {node: '>=6'} 352 | 353 | callsites@3.1.0: 354 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 355 | engines: {node: '>=6'} 356 | 357 | camelcase@5.3.1: 358 | resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} 359 | engines: {node: '>=6'} 360 | 361 | chalk@4.1.2: 362 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 363 | engines: {node: '>=10'} 364 | 365 | cliui@6.0.0: 366 | resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} 367 | 368 | color-convert@2.0.1: 369 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 370 | engines: {node: '>=7.0.0'} 371 | 372 | color-name@1.1.4: 373 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 374 | 375 | concat-map@0.0.1: 376 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 377 | 378 | cross-spawn@7.0.3: 379 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 380 | engines: {node: '>= 8'} 381 | 382 | dayjs@1.11.11: 383 | resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==} 384 | 385 | debug@4.3.5: 386 | resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} 387 | engines: {node: '>=6.0'} 388 | peerDependencies: 389 | supports-color: '*' 390 | peerDependenciesMeta: 391 | supports-color: 392 | optional: true 393 | 394 | decamelize@1.2.0: 395 | resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} 396 | engines: {node: '>=0.10.0'} 397 | 398 | deep-is@0.1.4: 399 | resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 400 | 401 | dijkstrajs@1.0.3: 402 | resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} 403 | 404 | dir-glob@3.0.1: 405 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 406 | engines: {node: '>=8'} 407 | 408 | doctrine@3.0.0: 409 | resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} 410 | engines: {node: '>=6.0.0'} 411 | 412 | emoji-regex@8.0.0: 413 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 414 | 415 | esbuild@0.17.3: 416 | resolution: {integrity: sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g==} 417 | engines: {node: '>=12'} 418 | hasBin: true 419 | 420 | escape-string-regexp@4.0.0: 421 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 422 | engines: {node: '>=10'} 423 | 424 | eslint-scope@5.1.1: 425 | resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} 426 | engines: {node: '>=8.0.0'} 427 | 428 | eslint-scope@7.2.2: 429 | resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} 430 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 431 | 432 | eslint-utils@3.0.0: 433 | resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} 434 | engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} 435 | peerDependencies: 436 | eslint: '>=5' 437 | 438 | eslint-visitor-keys@2.1.0: 439 | resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} 440 | engines: {node: '>=10'} 441 | 442 | eslint-visitor-keys@3.4.3: 443 | resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 444 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 445 | 446 | eslint@8.57.0: 447 | resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} 448 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 449 | hasBin: true 450 | 451 | espree@9.6.1: 452 | resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} 453 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 454 | 455 | esquery@1.5.0: 456 | resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} 457 | engines: {node: '>=0.10'} 458 | 459 | esrecurse@4.3.0: 460 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 461 | engines: {node: '>=4.0'} 462 | 463 | estraverse@4.3.0: 464 | resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} 465 | engines: {node: '>=4.0'} 466 | 467 | estraverse@5.3.0: 468 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 469 | engines: {node: '>=4.0'} 470 | 471 | esutils@2.0.3: 472 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 473 | engines: {node: '>=0.10.0'} 474 | 475 | fast-deep-equal@3.1.3: 476 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 477 | 478 | fast-glob@3.3.2: 479 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 480 | engines: {node: '>=8.6.0'} 481 | 482 | fast-json-stable-stringify@2.1.0: 483 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 484 | 485 | fast-levenshtein@2.0.6: 486 | resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 487 | 488 | fastq@1.17.1: 489 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 490 | 491 | file-entry-cache@6.0.1: 492 | resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} 493 | engines: {node: ^10.12.0 || >=12.0.0} 494 | 495 | fill-range@7.1.1: 496 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 497 | engines: {node: '>=8'} 498 | 499 | find-up@4.1.0: 500 | resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} 501 | engines: {node: '>=8'} 502 | 503 | find-up@5.0.0: 504 | resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 505 | engines: {node: '>=10'} 506 | 507 | flat-cache@3.2.0: 508 | resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} 509 | engines: {node: ^10.12.0 || >=12.0.0} 510 | 511 | flatted@3.3.1: 512 | resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} 513 | 514 | fs.realpath@1.0.0: 515 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 516 | 517 | functional-red-black-tree@1.0.1: 518 | resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} 519 | 520 | get-caller-file@2.0.5: 521 | resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 522 | engines: {node: 6.* || 8.* || >= 10.*} 523 | 524 | glob-parent@5.1.2: 525 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 526 | engines: {node: '>= 6'} 527 | 528 | glob-parent@6.0.2: 529 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 530 | engines: {node: '>=10.13.0'} 531 | 532 | glob@7.2.3: 533 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 534 | deprecated: Glob versions prior to v9 are no longer supported 535 | 536 | globals@13.24.0: 537 | resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} 538 | engines: {node: '>=8'} 539 | 540 | globby@11.1.0: 541 | resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} 542 | engines: {node: '>=10'} 543 | 544 | graphemer@1.4.0: 545 | resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} 546 | 547 | has-flag@4.0.0: 548 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 549 | engines: {node: '>=8'} 550 | 551 | ignore@5.3.1: 552 | resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} 553 | engines: {node: '>= 4'} 554 | 555 | import-fresh@3.3.0: 556 | resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} 557 | engines: {node: '>=6'} 558 | 559 | imurmurhash@0.1.4: 560 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 561 | engines: {node: '>=0.8.19'} 562 | 563 | inflight@1.0.6: 564 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 565 | deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. 566 | 567 | inherits@2.0.4: 568 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 569 | 570 | is-extglob@2.1.1: 571 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 572 | engines: {node: '>=0.10.0'} 573 | 574 | is-fullwidth-code-point@3.0.0: 575 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 576 | engines: {node: '>=8'} 577 | 578 | is-glob@4.0.3: 579 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 580 | engines: {node: '>=0.10.0'} 581 | 582 | is-number@7.0.0: 583 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 584 | engines: {node: '>=0.12.0'} 585 | 586 | is-path-inside@3.0.3: 587 | resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} 588 | engines: {node: '>=8'} 589 | 590 | isexe@2.0.0: 591 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 592 | 593 | js-yaml@4.1.0: 594 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 595 | hasBin: true 596 | 597 | json-buffer@3.0.1: 598 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 599 | 600 | json-schema-traverse@0.4.1: 601 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 602 | 603 | json-stable-stringify-without-jsonify@1.0.1: 604 | resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 605 | 606 | keyv@4.5.4: 607 | resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 608 | 609 | levn@0.4.1: 610 | resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 611 | engines: {node: '>= 0.8.0'} 612 | 613 | locate-path@5.0.0: 614 | resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} 615 | engines: {node: '>=8'} 616 | 617 | locate-path@6.0.0: 618 | resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 619 | engines: {node: '>=10'} 620 | 621 | lodash.merge@4.6.2: 622 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 623 | 624 | merge2@1.4.1: 625 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 626 | engines: {node: '>= 8'} 627 | 628 | micromatch@4.0.7: 629 | resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} 630 | engines: {node: '>=8.6'} 631 | 632 | minimatch@3.1.2: 633 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 634 | 635 | moment@2.29.4: 636 | resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} 637 | 638 | ms@2.1.2: 639 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 640 | 641 | natural-compare@1.4.0: 642 | resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 643 | 644 | obsidian@1.5.7-1: 645 | resolution: {integrity: sha512-T5ZRuQ1FnfXqEoakTTHVDYvzUEEoT8zSPnQCW31PVgYwG4D4tZCQfKHN2hTz1ifnCe8upvwa6mBTAP2WUA5Vng==} 646 | peerDependencies: 647 | '@codemirror/state': ^6.0.0 648 | '@codemirror/view': ^6.0.0 649 | 650 | once@1.4.0: 651 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 652 | 653 | optionator@0.9.4: 654 | resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 655 | engines: {node: '>= 0.8.0'} 656 | 657 | p-limit@2.3.0: 658 | resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} 659 | engines: {node: '>=6'} 660 | 661 | p-limit@3.1.0: 662 | resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 663 | engines: {node: '>=10'} 664 | 665 | p-locate@4.1.0: 666 | resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} 667 | engines: {node: '>=8'} 668 | 669 | p-locate@5.0.0: 670 | resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 671 | engines: {node: '>=10'} 672 | 673 | p-try@2.2.0: 674 | resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} 675 | engines: {node: '>=6'} 676 | 677 | parent-module@1.0.1: 678 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 679 | engines: {node: '>=6'} 680 | 681 | path-exists@4.0.0: 682 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 683 | engines: {node: '>=8'} 684 | 685 | path-is-absolute@1.0.1: 686 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 687 | engines: {node: '>=0.10.0'} 688 | 689 | path-key@3.1.1: 690 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 691 | engines: {node: '>=8'} 692 | 693 | path-type@4.0.0: 694 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 695 | engines: {node: '>=8'} 696 | 697 | picomatch@2.3.1: 698 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 699 | engines: {node: '>=8.6'} 700 | 701 | pngjs@5.0.0: 702 | resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} 703 | engines: {node: '>=10.13.0'} 704 | 705 | prelude-ls@1.2.1: 706 | resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 707 | engines: {node: '>= 0.8.0'} 708 | 709 | punycode@2.3.1: 710 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 711 | engines: {node: '>=6'} 712 | 713 | qrcode@1.5.4: 714 | resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} 715 | engines: {node: '>=10.13.0'} 716 | hasBin: true 717 | 718 | quail-js@0.3.18: 719 | resolution: {integrity: sha512-CXQ3y0tNVNp75pJsYsMkiJmAUPB67zEo+CjGweO0kSJfsCOdBGHak7nbIZFxxREx12YclFDGy+vPrT/61A4LaA==} 720 | 721 | queue-microtask@1.2.3: 722 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 723 | 724 | regexpp@3.2.0: 725 | resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} 726 | engines: {node: '>=8'} 727 | 728 | require-directory@2.1.1: 729 | resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 730 | engines: {node: '>=0.10.0'} 731 | 732 | require-main-filename@2.0.0: 733 | resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} 734 | 735 | resolve-from@4.0.0: 736 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 737 | engines: {node: '>=4'} 738 | 739 | reusify@1.0.4: 740 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 741 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 742 | 743 | rimraf@3.0.2: 744 | resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} 745 | deprecated: Rimraf versions prior to v4 are no longer supported 746 | hasBin: true 747 | 748 | run-parallel@1.2.0: 749 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 750 | 751 | semver@7.6.2: 752 | resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} 753 | engines: {node: '>=10'} 754 | hasBin: true 755 | 756 | set-blocking@2.0.0: 757 | resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} 758 | 759 | shebang-command@2.0.0: 760 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 761 | engines: {node: '>=8'} 762 | 763 | shebang-regex@3.0.0: 764 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 765 | engines: {node: '>=8'} 766 | 767 | slash@3.0.0: 768 | resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 769 | engines: {node: '>=8'} 770 | 771 | string-width@4.2.3: 772 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 773 | engines: {node: '>=8'} 774 | 775 | strip-ansi@6.0.1: 776 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 777 | engines: {node: '>=8'} 778 | 779 | strip-json-comments@3.1.1: 780 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 781 | engines: {node: '>=8'} 782 | 783 | style-mod@4.1.2: 784 | resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} 785 | 786 | supports-color@7.2.0: 787 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 788 | engines: {node: '>=8'} 789 | 790 | text-table@0.2.0: 791 | resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} 792 | 793 | to-regex-range@5.0.1: 794 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 795 | engines: {node: '>=8.0'} 796 | 797 | tslib@1.14.1: 798 | resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} 799 | 800 | tslib@2.4.0: 801 | resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} 802 | 803 | tsutils@3.21.0: 804 | resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} 805 | engines: {node: '>= 6'} 806 | peerDependencies: 807 | typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' 808 | 809 | type-check@0.4.0: 810 | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 811 | engines: {node: '>= 0.8.0'} 812 | 813 | type-fest@0.20.2: 814 | resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} 815 | engines: {node: '>=10'} 816 | 817 | typescript@4.7.4: 818 | resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} 819 | engines: {node: '>=4.2.0'} 820 | hasBin: true 821 | 822 | uri-js@4.4.1: 823 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 824 | 825 | w3c-keyname@2.2.8: 826 | resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} 827 | 828 | which-module@2.0.1: 829 | resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} 830 | 831 | which@2.0.2: 832 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 833 | engines: {node: '>= 8'} 834 | hasBin: true 835 | 836 | word-wrap@1.2.5: 837 | resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 838 | engines: {node: '>=0.10.0'} 839 | 840 | wrap-ansi@6.2.0: 841 | resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} 842 | engines: {node: '>=8'} 843 | 844 | wrappy@1.0.2: 845 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 846 | 847 | y18n@4.0.3: 848 | resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} 849 | 850 | yargs-parser@18.1.3: 851 | resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} 852 | engines: {node: '>=6'} 853 | 854 | yargs@15.4.1: 855 | resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} 856 | engines: {node: '>=8'} 857 | 858 | yocto-queue@0.1.0: 859 | resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 860 | engines: {node: '>=10'} 861 | 862 | snapshots: 863 | 864 | '@codemirror/state@6.4.1': {} 865 | 866 | '@codemirror/view@6.28.4': 867 | dependencies: 868 | '@codemirror/state': 6.4.1 869 | style-mod: 4.1.2 870 | w3c-keyname: 2.2.8 871 | 872 | '@esbuild/android-arm64@0.17.3': 873 | optional: true 874 | 875 | '@esbuild/android-arm@0.17.3': 876 | optional: true 877 | 878 | '@esbuild/android-x64@0.17.3': 879 | optional: true 880 | 881 | '@esbuild/darwin-arm64@0.17.3': 882 | optional: true 883 | 884 | '@esbuild/darwin-x64@0.17.3': 885 | optional: true 886 | 887 | '@esbuild/freebsd-arm64@0.17.3': 888 | optional: true 889 | 890 | '@esbuild/freebsd-x64@0.17.3': 891 | optional: true 892 | 893 | '@esbuild/linux-arm64@0.17.3': 894 | optional: true 895 | 896 | '@esbuild/linux-arm@0.17.3': 897 | optional: true 898 | 899 | '@esbuild/linux-ia32@0.17.3': 900 | optional: true 901 | 902 | '@esbuild/linux-loong64@0.17.3': 903 | optional: true 904 | 905 | '@esbuild/linux-mips64el@0.17.3': 906 | optional: true 907 | 908 | '@esbuild/linux-ppc64@0.17.3': 909 | optional: true 910 | 911 | '@esbuild/linux-riscv64@0.17.3': 912 | optional: true 913 | 914 | '@esbuild/linux-s390x@0.17.3': 915 | optional: true 916 | 917 | '@esbuild/linux-x64@0.17.3': 918 | optional: true 919 | 920 | '@esbuild/netbsd-x64@0.17.3': 921 | optional: true 922 | 923 | '@esbuild/openbsd-x64@0.17.3': 924 | optional: true 925 | 926 | '@esbuild/sunos-x64@0.17.3': 927 | optional: true 928 | 929 | '@esbuild/win32-arm64@0.17.3': 930 | optional: true 931 | 932 | '@esbuild/win32-ia32@0.17.3': 933 | optional: true 934 | 935 | '@esbuild/win32-x64@0.17.3': 936 | optional: true 937 | 938 | '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': 939 | dependencies: 940 | eslint: 8.57.0 941 | eslint-visitor-keys: 3.4.3 942 | 943 | '@eslint-community/regexpp@4.11.0': {} 944 | 945 | '@eslint/eslintrc@2.1.4': 946 | dependencies: 947 | ajv: 6.12.6 948 | debug: 4.3.5 949 | espree: 9.6.1 950 | globals: 13.24.0 951 | ignore: 5.3.1 952 | import-fresh: 3.3.0 953 | js-yaml: 4.1.0 954 | minimatch: 3.1.2 955 | strip-json-comments: 3.1.1 956 | transitivePeerDependencies: 957 | - supports-color 958 | 959 | '@eslint/js@8.57.0': {} 960 | 961 | '@humanwhocodes/config-array@0.11.14': 962 | dependencies: 963 | '@humanwhocodes/object-schema': 2.0.3 964 | debug: 4.3.5 965 | minimatch: 3.1.2 966 | transitivePeerDependencies: 967 | - supports-color 968 | 969 | '@humanwhocodes/module-importer@1.0.1': {} 970 | 971 | '@humanwhocodes/object-schema@2.0.3': {} 972 | 973 | '@nodelib/fs.scandir@2.1.5': 974 | dependencies: 975 | '@nodelib/fs.stat': 2.0.5 976 | run-parallel: 1.2.0 977 | 978 | '@nodelib/fs.stat@2.0.5': {} 979 | 980 | '@nodelib/fs.walk@1.2.8': 981 | dependencies: 982 | '@nodelib/fs.scandir': 2.1.5 983 | fastq: 1.17.1 984 | 985 | '@types/codemirror@5.60.8': 986 | dependencies: 987 | '@types/tern': 0.23.9 988 | 989 | '@types/estree@1.0.5': {} 990 | 991 | '@types/json-schema@7.0.15': {} 992 | 993 | '@types/node@16.18.101': {} 994 | 995 | '@types/qrcode@1.5.5': 996 | dependencies: 997 | '@types/node': 16.18.101 998 | 999 | '@types/tern@0.23.9': 1000 | dependencies: 1001 | '@types/estree': 1.0.5 1002 | 1003 | '@typescript-eslint/eslint-plugin@5.29.0(@typescript-eslint/parser@5.29.0(eslint@8.57.0)(typescript@4.7.4))(eslint@8.57.0)(typescript@4.7.4)': 1004 | dependencies: 1005 | '@typescript-eslint/parser': 5.29.0(eslint@8.57.0)(typescript@4.7.4) 1006 | '@typescript-eslint/scope-manager': 5.29.0 1007 | '@typescript-eslint/type-utils': 5.29.0(eslint@8.57.0)(typescript@4.7.4) 1008 | '@typescript-eslint/utils': 5.29.0(eslint@8.57.0)(typescript@4.7.4) 1009 | debug: 4.3.5 1010 | eslint: 8.57.0 1011 | functional-red-black-tree: 1.0.1 1012 | ignore: 5.3.1 1013 | regexpp: 3.2.0 1014 | semver: 7.6.2 1015 | tsutils: 3.21.0(typescript@4.7.4) 1016 | optionalDependencies: 1017 | typescript: 4.7.4 1018 | transitivePeerDependencies: 1019 | - supports-color 1020 | 1021 | '@typescript-eslint/parser@5.29.0(eslint@8.57.0)(typescript@4.7.4)': 1022 | dependencies: 1023 | '@typescript-eslint/scope-manager': 5.29.0 1024 | '@typescript-eslint/types': 5.29.0 1025 | '@typescript-eslint/typescript-estree': 5.29.0(typescript@4.7.4) 1026 | debug: 4.3.5 1027 | eslint: 8.57.0 1028 | optionalDependencies: 1029 | typescript: 4.7.4 1030 | transitivePeerDependencies: 1031 | - supports-color 1032 | 1033 | '@typescript-eslint/scope-manager@5.29.0': 1034 | dependencies: 1035 | '@typescript-eslint/types': 5.29.0 1036 | '@typescript-eslint/visitor-keys': 5.29.0 1037 | 1038 | '@typescript-eslint/type-utils@5.29.0(eslint@8.57.0)(typescript@4.7.4)': 1039 | dependencies: 1040 | '@typescript-eslint/utils': 5.29.0(eslint@8.57.0)(typescript@4.7.4) 1041 | debug: 4.3.5 1042 | eslint: 8.57.0 1043 | tsutils: 3.21.0(typescript@4.7.4) 1044 | optionalDependencies: 1045 | typescript: 4.7.4 1046 | transitivePeerDependencies: 1047 | - supports-color 1048 | 1049 | '@typescript-eslint/types@5.29.0': {} 1050 | 1051 | '@typescript-eslint/typescript-estree@5.29.0(typescript@4.7.4)': 1052 | dependencies: 1053 | '@typescript-eslint/types': 5.29.0 1054 | '@typescript-eslint/visitor-keys': 5.29.0 1055 | debug: 4.3.5 1056 | globby: 11.1.0 1057 | is-glob: 4.0.3 1058 | semver: 7.6.2 1059 | tsutils: 3.21.0(typescript@4.7.4) 1060 | optionalDependencies: 1061 | typescript: 4.7.4 1062 | transitivePeerDependencies: 1063 | - supports-color 1064 | 1065 | '@typescript-eslint/utils@5.29.0(eslint@8.57.0)(typescript@4.7.4)': 1066 | dependencies: 1067 | '@types/json-schema': 7.0.15 1068 | '@typescript-eslint/scope-manager': 5.29.0 1069 | '@typescript-eslint/types': 5.29.0 1070 | '@typescript-eslint/typescript-estree': 5.29.0(typescript@4.7.4) 1071 | eslint: 8.57.0 1072 | eslint-scope: 5.1.1 1073 | eslint-utils: 3.0.0(eslint@8.57.0) 1074 | transitivePeerDependencies: 1075 | - supports-color 1076 | - typescript 1077 | 1078 | '@typescript-eslint/visitor-keys@5.29.0': 1079 | dependencies: 1080 | '@typescript-eslint/types': 5.29.0 1081 | eslint-visitor-keys: 3.4.3 1082 | 1083 | '@ungap/structured-clone@1.2.0': {} 1084 | 1085 | acorn-jsx@5.3.2(acorn@8.12.1): 1086 | dependencies: 1087 | acorn: 8.12.1 1088 | 1089 | acorn@8.12.1: {} 1090 | 1091 | ajv@6.12.6: 1092 | dependencies: 1093 | fast-deep-equal: 3.1.3 1094 | fast-json-stable-stringify: 2.1.0 1095 | json-schema-traverse: 0.4.1 1096 | uri-js: 4.4.1 1097 | 1098 | ansi-regex@5.0.1: {} 1099 | 1100 | ansi-styles@4.3.0: 1101 | dependencies: 1102 | color-convert: 2.0.1 1103 | 1104 | argparse@2.0.1: {} 1105 | 1106 | array-union@2.1.0: {} 1107 | 1108 | balanced-match@1.0.2: {} 1109 | 1110 | brace-expansion@1.1.11: 1111 | dependencies: 1112 | balanced-match: 1.0.2 1113 | concat-map: 0.0.1 1114 | 1115 | braces@3.0.3: 1116 | dependencies: 1117 | fill-range: 7.1.1 1118 | 1119 | builtin-modules@3.3.0: {} 1120 | 1121 | callsites@3.1.0: {} 1122 | 1123 | camelcase@5.3.1: {} 1124 | 1125 | chalk@4.1.2: 1126 | dependencies: 1127 | ansi-styles: 4.3.0 1128 | supports-color: 7.2.0 1129 | 1130 | cliui@6.0.0: 1131 | dependencies: 1132 | string-width: 4.2.3 1133 | strip-ansi: 6.0.1 1134 | wrap-ansi: 6.2.0 1135 | 1136 | color-convert@2.0.1: 1137 | dependencies: 1138 | color-name: 1.1.4 1139 | 1140 | color-name@1.1.4: {} 1141 | 1142 | concat-map@0.0.1: {} 1143 | 1144 | cross-spawn@7.0.3: 1145 | dependencies: 1146 | path-key: 3.1.1 1147 | shebang-command: 2.0.0 1148 | which: 2.0.2 1149 | 1150 | dayjs@1.11.11: {} 1151 | 1152 | debug@4.3.5: 1153 | dependencies: 1154 | ms: 2.1.2 1155 | 1156 | decamelize@1.2.0: {} 1157 | 1158 | deep-is@0.1.4: {} 1159 | 1160 | dijkstrajs@1.0.3: {} 1161 | 1162 | dir-glob@3.0.1: 1163 | dependencies: 1164 | path-type: 4.0.0 1165 | 1166 | doctrine@3.0.0: 1167 | dependencies: 1168 | esutils: 2.0.3 1169 | 1170 | emoji-regex@8.0.0: {} 1171 | 1172 | esbuild@0.17.3: 1173 | optionalDependencies: 1174 | '@esbuild/android-arm': 0.17.3 1175 | '@esbuild/android-arm64': 0.17.3 1176 | '@esbuild/android-x64': 0.17.3 1177 | '@esbuild/darwin-arm64': 0.17.3 1178 | '@esbuild/darwin-x64': 0.17.3 1179 | '@esbuild/freebsd-arm64': 0.17.3 1180 | '@esbuild/freebsd-x64': 0.17.3 1181 | '@esbuild/linux-arm': 0.17.3 1182 | '@esbuild/linux-arm64': 0.17.3 1183 | '@esbuild/linux-ia32': 0.17.3 1184 | '@esbuild/linux-loong64': 0.17.3 1185 | '@esbuild/linux-mips64el': 0.17.3 1186 | '@esbuild/linux-ppc64': 0.17.3 1187 | '@esbuild/linux-riscv64': 0.17.3 1188 | '@esbuild/linux-s390x': 0.17.3 1189 | '@esbuild/linux-x64': 0.17.3 1190 | '@esbuild/netbsd-x64': 0.17.3 1191 | '@esbuild/openbsd-x64': 0.17.3 1192 | '@esbuild/sunos-x64': 0.17.3 1193 | '@esbuild/win32-arm64': 0.17.3 1194 | '@esbuild/win32-ia32': 0.17.3 1195 | '@esbuild/win32-x64': 0.17.3 1196 | 1197 | escape-string-regexp@4.0.0: {} 1198 | 1199 | eslint-scope@5.1.1: 1200 | dependencies: 1201 | esrecurse: 4.3.0 1202 | estraverse: 4.3.0 1203 | 1204 | eslint-scope@7.2.2: 1205 | dependencies: 1206 | esrecurse: 4.3.0 1207 | estraverse: 5.3.0 1208 | 1209 | eslint-utils@3.0.0(eslint@8.57.0): 1210 | dependencies: 1211 | eslint: 8.57.0 1212 | eslint-visitor-keys: 2.1.0 1213 | 1214 | eslint-visitor-keys@2.1.0: {} 1215 | 1216 | eslint-visitor-keys@3.4.3: {} 1217 | 1218 | eslint@8.57.0: 1219 | dependencies: 1220 | '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) 1221 | '@eslint-community/regexpp': 4.11.0 1222 | '@eslint/eslintrc': 2.1.4 1223 | '@eslint/js': 8.57.0 1224 | '@humanwhocodes/config-array': 0.11.14 1225 | '@humanwhocodes/module-importer': 1.0.1 1226 | '@nodelib/fs.walk': 1.2.8 1227 | '@ungap/structured-clone': 1.2.0 1228 | ajv: 6.12.6 1229 | chalk: 4.1.2 1230 | cross-spawn: 7.0.3 1231 | debug: 4.3.5 1232 | doctrine: 3.0.0 1233 | escape-string-regexp: 4.0.0 1234 | eslint-scope: 7.2.2 1235 | eslint-visitor-keys: 3.4.3 1236 | espree: 9.6.1 1237 | esquery: 1.5.0 1238 | esutils: 2.0.3 1239 | fast-deep-equal: 3.1.3 1240 | file-entry-cache: 6.0.1 1241 | find-up: 5.0.0 1242 | glob-parent: 6.0.2 1243 | globals: 13.24.0 1244 | graphemer: 1.4.0 1245 | ignore: 5.3.1 1246 | imurmurhash: 0.1.4 1247 | is-glob: 4.0.3 1248 | is-path-inside: 3.0.3 1249 | js-yaml: 4.1.0 1250 | json-stable-stringify-without-jsonify: 1.0.1 1251 | levn: 0.4.1 1252 | lodash.merge: 4.6.2 1253 | minimatch: 3.1.2 1254 | natural-compare: 1.4.0 1255 | optionator: 0.9.4 1256 | strip-ansi: 6.0.1 1257 | text-table: 0.2.0 1258 | transitivePeerDependencies: 1259 | - supports-color 1260 | 1261 | espree@9.6.1: 1262 | dependencies: 1263 | acorn: 8.12.1 1264 | acorn-jsx: 5.3.2(acorn@8.12.1) 1265 | eslint-visitor-keys: 3.4.3 1266 | 1267 | esquery@1.5.0: 1268 | dependencies: 1269 | estraverse: 5.3.0 1270 | 1271 | esrecurse@4.3.0: 1272 | dependencies: 1273 | estraverse: 5.3.0 1274 | 1275 | estraverse@4.3.0: {} 1276 | 1277 | estraverse@5.3.0: {} 1278 | 1279 | esutils@2.0.3: {} 1280 | 1281 | fast-deep-equal@3.1.3: {} 1282 | 1283 | fast-glob@3.3.2: 1284 | dependencies: 1285 | '@nodelib/fs.stat': 2.0.5 1286 | '@nodelib/fs.walk': 1.2.8 1287 | glob-parent: 5.1.2 1288 | merge2: 1.4.1 1289 | micromatch: 4.0.7 1290 | 1291 | fast-json-stable-stringify@2.1.0: {} 1292 | 1293 | fast-levenshtein@2.0.6: {} 1294 | 1295 | fastq@1.17.1: 1296 | dependencies: 1297 | reusify: 1.0.4 1298 | 1299 | file-entry-cache@6.0.1: 1300 | dependencies: 1301 | flat-cache: 3.2.0 1302 | 1303 | fill-range@7.1.1: 1304 | dependencies: 1305 | to-regex-range: 5.0.1 1306 | 1307 | find-up@4.1.0: 1308 | dependencies: 1309 | locate-path: 5.0.0 1310 | path-exists: 4.0.0 1311 | 1312 | find-up@5.0.0: 1313 | dependencies: 1314 | locate-path: 6.0.0 1315 | path-exists: 4.0.0 1316 | 1317 | flat-cache@3.2.0: 1318 | dependencies: 1319 | flatted: 3.3.1 1320 | keyv: 4.5.4 1321 | rimraf: 3.0.2 1322 | 1323 | flatted@3.3.1: {} 1324 | 1325 | fs.realpath@1.0.0: {} 1326 | 1327 | functional-red-black-tree@1.0.1: {} 1328 | 1329 | get-caller-file@2.0.5: {} 1330 | 1331 | glob-parent@5.1.2: 1332 | dependencies: 1333 | is-glob: 4.0.3 1334 | 1335 | glob-parent@6.0.2: 1336 | dependencies: 1337 | is-glob: 4.0.3 1338 | 1339 | glob@7.2.3: 1340 | dependencies: 1341 | fs.realpath: 1.0.0 1342 | inflight: 1.0.6 1343 | inherits: 2.0.4 1344 | minimatch: 3.1.2 1345 | once: 1.4.0 1346 | path-is-absolute: 1.0.1 1347 | 1348 | globals@13.24.0: 1349 | dependencies: 1350 | type-fest: 0.20.2 1351 | 1352 | globby@11.1.0: 1353 | dependencies: 1354 | array-union: 2.1.0 1355 | dir-glob: 3.0.1 1356 | fast-glob: 3.3.2 1357 | ignore: 5.3.1 1358 | merge2: 1.4.1 1359 | slash: 3.0.0 1360 | 1361 | graphemer@1.4.0: {} 1362 | 1363 | has-flag@4.0.0: {} 1364 | 1365 | ignore@5.3.1: {} 1366 | 1367 | import-fresh@3.3.0: 1368 | dependencies: 1369 | parent-module: 1.0.1 1370 | resolve-from: 4.0.0 1371 | 1372 | imurmurhash@0.1.4: {} 1373 | 1374 | inflight@1.0.6: 1375 | dependencies: 1376 | once: 1.4.0 1377 | wrappy: 1.0.2 1378 | 1379 | inherits@2.0.4: {} 1380 | 1381 | is-extglob@2.1.1: {} 1382 | 1383 | is-fullwidth-code-point@3.0.0: {} 1384 | 1385 | is-glob@4.0.3: 1386 | dependencies: 1387 | is-extglob: 2.1.1 1388 | 1389 | is-number@7.0.0: {} 1390 | 1391 | is-path-inside@3.0.3: {} 1392 | 1393 | isexe@2.0.0: {} 1394 | 1395 | js-yaml@4.1.0: 1396 | dependencies: 1397 | argparse: 2.0.1 1398 | 1399 | json-buffer@3.0.1: {} 1400 | 1401 | json-schema-traverse@0.4.1: {} 1402 | 1403 | json-stable-stringify-without-jsonify@1.0.1: {} 1404 | 1405 | keyv@4.5.4: 1406 | dependencies: 1407 | json-buffer: 3.0.1 1408 | 1409 | levn@0.4.1: 1410 | dependencies: 1411 | prelude-ls: 1.2.1 1412 | type-check: 0.4.0 1413 | 1414 | locate-path@5.0.0: 1415 | dependencies: 1416 | p-locate: 4.1.0 1417 | 1418 | locate-path@6.0.0: 1419 | dependencies: 1420 | p-locate: 5.0.0 1421 | 1422 | lodash.merge@4.6.2: {} 1423 | 1424 | merge2@1.4.1: {} 1425 | 1426 | micromatch@4.0.7: 1427 | dependencies: 1428 | braces: 3.0.3 1429 | picomatch: 2.3.1 1430 | 1431 | minimatch@3.1.2: 1432 | dependencies: 1433 | brace-expansion: 1.1.11 1434 | 1435 | moment@2.29.4: {} 1436 | 1437 | ms@2.1.2: {} 1438 | 1439 | natural-compare@1.4.0: {} 1440 | 1441 | obsidian@1.5.7-1(@codemirror/state@6.4.1)(@codemirror/view@6.28.4): 1442 | dependencies: 1443 | '@codemirror/state': 6.4.1 1444 | '@codemirror/view': 6.28.4 1445 | '@types/codemirror': 5.60.8 1446 | moment: 2.29.4 1447 | 1448 | once@1.4.0: 1449 | dependencies: 1450 | wrappy: 1.0.2 1451 | 1452 | optionator@0.9.4: 1453 | dependencies: 1454 | deep-is: 0.1.4 1455 | fast-levenshtein: 2.0.6 1456 | levn: 0.4.1 1457 | prelude-ls: 1.2.1 1458 | type-check: 0.4.0 1459 | word-wrap: 1.2.5 1460 | 1461 | p-limit@2.3.0: 1462 | dependencies: 1463 | p-try: 2.2.0 1464 | 1465 | p-limit@3.1.0: 1466 | dependencies: 1467 | yocto-queue: 0.1.0 1468 | 1469 | p-locate@4.1.0: 1470 | dependencies: 1471 | p-limit: 2.3.0 1472 | 1473 | p-locate@5.0.0: 1474 | dependencies: 1475 | p-limit: 3.1.0 1476 | 1477 | p-try@2.2.0: {} 1478 | 1479 | parent-module@1.0.1: 1480 | dependencies: 1481 | callsites: 3.1.0 1482 | 1483 | path-exists@4.0.0: {} 1484 | 1485 | path-is-absolute@1.0.1: {} 1486 | 1487 | path-key@3.1.1: {} 1488 | 1489 | path-type@4.0.0: {} 1490 | 1491 | picomatch@2.3.1: {} 1492 | 1493 | pngjs@5.0.0: {} 1494 | 1495 | prelude-ls@1.2.1: {} 1496 | 1497 | punycode@2.3.1: {} 1498 | 1499 | qrcode@1.5.4: 1500 | dependencies: 1501 | dijkstrajs: 1.0.3 1502 | pngjs: 5.0.0 1503 | yargs: 15.4.1 1504 | 1505 | quail-js@0.3.18: 1506 | dependencies: 1507 | dayjs: 1.11.11 1508 | 1509 | queue-microtask@1.2.3: {} 1510 | 1511 | regexpp@3.2.0: {} 1512 | 1513 | require-directory@2.1.1: {} 1514 | 1515 | require-main-filename@2.0.0: {} 1516 | 1517 | resolve-from@4.0.0: {} 1518 | 1519 | reusify@1.0.4: {} 1520 | 1521 | rimraf@3.0.2: 1522 | dependencies: 1523 | glob: 7.2.3 1524 | 1525 | run-parallel@1.2.0: 1526 | dependencies: 1527 | queue-microtask: 1.2.3 1528 | 1529 | semver@7.6.2: {} 1530 | 1531 | set-blocking@2.0.0: {} 1532 | 1533 | shebang-command@2.0.0: 1534 | dependencies: 1535 | shebang-regex: 3.0.0 1536 | 1537 | shebang-regex@3.0.0: {} 1538 | 1539 | slash@3.0.0: {} 1540 | 1541 | string-width@4.2.3: 1542 | dependencies: 1543 | emoji-regex: 8.0.0 1544 | is-fullwidth-code-point: 3.0.0 1545 | strip-ansi: 6.0.1 1546 | 1547 | strip-ansi@6.0.1: 1548 | dependencies: 1549 | ansi-regex: 5.0.1 1550 | 1551 | strip-json-comments@3.1.1: {} 1552 | 1553 | style-mod@4.1.2: {} 1554 | 1555 | supports-color@7.2.0: 1556 | dependencies: 1557 | has-flag: 4.0.0 1558 | 1559 | text-table@0.2.0: {} 1560 | 1561 | to-regex-range@5.0.1: 1562 | dependencies: 1563 | is-number: 7.0.0 1564 | 1565 | tslib@1.14.1: {} 1566 | 1567 | tslib@2.4.0: {} 1568 | 1569 | tsutils@3.21.0(typescript@4.7.4): 1570 | dependencies: 1571 | tslib: 1.14.1 1572 | typescript: 4.7.4 1573 | 1574 | type-check@0.4.0: 1575 | dependencies: 1576 | prelude-ls: 1.2.1 1577 | 1578 | type-fest@0.20.2: {} 1579 | 1580 | typescript@4.7.4: {} 1581 | 1582 | uri-js@4.4.1: 1583 | dependencies: 1584 | punycode: 2.3.1 1585 | 1586 | w3c-keyname@2.2.8: {} 1587 | 1588 | which-module@2.0.1: {} 1589 | 1590 | which@2.0.2: 1591 | dependencies: 1592 | isexe: 2.0.0 1593 | 1594 | word-wrap@1.2.5: {} 1595 | 1596 | wrap-ansi@6.2.0: 1597 | dependencies: 1598 | ansi-styles: 4.3.0 1599 | string-width: 4.2.3 1600 | strip-ansi: 6.0.1 1601 | 1602 | wrappy@1.0.2: {} 1603 | 1604 | y18n@4.0.3: {} 1605 | 1606 | yargs-parser@18.1.3: 1607 | dependencies: 1608 | camelcase: 5.3.1 1609 | decamelize: 1.2.0 1610 | 1611 | yargs@15.4.1: 1612 | dependencies: 1613 | cliui: 6.0.0 1614 | decamelize: 1.2.0 1615 | find-up: 4.1.0 1616 | get-caller-file: 2.0.5 1617 | require-directory: 2.1.1 1618 | require-main-filename: 2.0.0 1619 | set-blocking: 2.0.0 1620 | string-width: 4.2.3 1621 | which-module: 2.0.1 1622 | y18n: 4.0.3 1623 | yargs-parser: 18.1.3 1624 | 1625 | yocto-queue@0.1.0: {} 1626 | --------------------------------------------------------------------------------