├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── manifest.json ├── media └── mapForNote.gif ├── package.json ├── postcss.config.js ├── rollup.config.js ├── src ├── data │ ├── mapCal.ts │ └── mfnTestData.ts ├── less │ └── index.less ├── mapForNoteComponent.ts ├── mapForNoteIndex.ts ├── mapForNoteSettings.ts ├── translations │ ├── helper.ts │ └── locale │ │ ├── ar.ts │ │ ├── cz.ts │ │ ├── da.ts │ │ ├── de.ts │ │ ├── en-gb.ts │ │ ├── en.ts │ │ ├── es.ts │ │ ├── fr.ts │ │ ├── hi.ts │ │ ├── id.ts │ │ ├── it.ts │ │ ├── ja.ts │ │ ├── ko.ts │ │ ├── nl.ts │ │ ├── no.ts │ │ ├── pl.ts │ │ ├── pt-br.ts │ │ ├── pt.ts │ │ ├── ro.ts │ │ ├── ru.ts │ │ ├── tr.ts │ │ ├── zh-cn.ts │ │ └── zh-tw.ts ├── ui │ ├── file-suggest.ts │ ├── mapCore.tsx │ ├── mfnInput.tsx │ ├── mfnLabel.tsx │ ├── mfnMap.tsx │ ├── mfnNode.tsx │ └── suggest.ts └── utils │ ├── consts.ts │ ├── edge.ts │ ├── graph.ts │ ├── index.ts │ ├── lib.ts │ ├── obInternalLink.ts │ └── vertex.ts ├── styles.css ├── tailwind.js ├── tsconfig.json ├── version-bump.mjs ├── versions.json ├── vite.config.js ├── yarn-error.log └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | plugins: ["react", "@typescript-eslint", "prettier"], 5 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 6 | rules: { 7 | "@typescript-eslint/no-unused-vars": [2, { args: "all", argsIgnorePattern: "^_" }], 8 | "@typescript-eslint/no-empty-interface": ["off"], 9 | "@typescript-eslint/no-explicit-any": ["off"], 10 | "react/react-in-jsx-scope": "off", 11 | "@typescript-eslint/no-namespace": "off", 12 | }, 13 | env: { 14 | browser: true, 15 | commonjs: true, 16 | es2021: true, 17 | node: true, 18 | }, 19 | parserOptions: { 20 | ecmaFeatures: { 21 | jsx: true, 22 | }, 23 | ecmaVersion: "latest", 24 | sourceType: "module", 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # Zip 9 | *.zip 10 | *.rar 11 | 12 | # npm 13 | node_modules 14 | 15 | # Don't include the compiled main.js file in the repo. 16 | # They should be uploaded to GitHub releases instead. 17 | main.js 18 | 19 | # Exclude sourcemaps 20 | *.map 21 | 22 | # obsidian 23 | data.json 24 | 25 | # Exclude macOS Finder (System Explorer) View States 26 | .DS_Store 27 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | quoteProps: 'as-needed', 7 | trailingComma: 'all', 8 | bracketSpacing: true, 9 | arrowParens: 'always', 10 | endOfLine: 'auto', 11 | }; 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Liam Cain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Map For Note [Deprecated] 2 | 3 | > This project is deprecated because of its low performance and I have no time to polish it, use [Path-Finder](https://github.com/jerrywcy/obsidian-path-finder) instead. 4 | 5 | A plugin for search paths between notes. 6 | 7 | ![example](https://github.com/Quorafind/Obsidian-Map-For-Note/blob/main/media/mapForNote.gif) 8 | 9 | ## Settings 10 | 11 | No now. 12 | 13 | ## Thanks 14 | 15 | - [Obsidian-Journey](https://github.com/akaalias/obsidian-journey-plugin) 16 | - [SS-graph](https://github.com/boycgit/ss-graph) 17 | - [Dagre-reactjs](https://github.com/bobthekingofegypt/dagre-reactjs) 18 | 19 | ## How to Install 20 | 21 | ### From Plugin Market in Obsidian 22 | 23 | 💜: Directly install from Obsidian Market[Not available]. 24 | 25 | ### From BRAT 26 | 27 | 🚗: Add `Quorafind/Obsidian-Map-For-Note` to BRAT. 28 | 29 | ### Download Manually 30 | 31 | 🚚: Download the latest release. Extract and put the three files (main.js, manifest.json, styles.css) to 32 | folder `{{obsidian_vault}}/.obsidian/plugins/Obsidian-Map-For-Note`. 33 | 34 | ## Say Thank You 35 | 36 | If you are enjoy using Obsidian-Task-Progress-Bar then please support my work and enthusiasm by buying me a coffee 37 | on [https://www.buymeacoffee.com/boninall](https://www.buymeacoffee.com/boninall). 38 | 39 | 40 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-map-for-note", 3 | "name": "Map For Note", 4 | "description": "Search paths between note and note", 5 | "version": "1.0.0", 6 | "author": "Boninall", 7 | "authorUrl": "https://github.com/Quorafind/", 8 | "isDesktopOnly": false, 9 | "minAppVersion": "0.14.6" 10 | } -------------------------------------------------------------------------------- /media/mapForNote.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quorafind/Obsidian-Map-For-Note/970addfa5eb8dfdb79acba8a37ebc864520502c5/media/mapForNote.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-map-for-note", 3 | "version": "1.0.0", 4 | "description": "Search paths between note and note", 5 | "author": "Boninall", 6 | "main": "main.js", 7 | "license": "MIT", 8 | "scripts": { 9 | "lint": "eslint . --ext .ts", 10 | "dev": "npm run lint && vite build --watch --mode=development ", 11 | "build:nolint": "NODE_ENV=production rollup -c", 12 | "build": "vite build", 13 | "test": "jest", 14 | "test:watch": "yarn test -- --watch", 15 | "build:style": "postcss ./src/less/index.less -o ./styles.css --watch", 16 | "bumpversion": "node version-bump.mjs && git add manifest.json versions.json" 17 | }, 18 | "devDependencies": { 19 | "obsidian": "^0.14.8", 20 | "react": "^18.1.0", 21 | "react-dom": "^18.1.0", 22 | "tslib": "2.3.1", 23 | "@babel/core": "7.13.8", 24 | "@babel/preset-react": "7.12.13", 25 | "@babel/preset-typescript": "7.13.0", 26 | "@popperjs/core": "^2.11.5", 27 | "@rollup/plugin-babel": "^5.3.0", 28 | "@rollup/plugin-commonjs": "^17.1.0", 29 | "@rollup/plugin-json": "^4.1.0", 30 | "@rollup/plugin-node-resolve": "^11.2.0", 31 | "@rollup/plugin-replace": "^2.4.1", 32 | "@rollup/plugin-typescript": "^8.2.0", 33 | "@types/node": "14.14.34", 34 | "@types/papaparse": "5.2.5", 35 | "@types/react": "^18.0.9", 36 | "@types/react-dom": "^18.0.4", 37 | "@typescript-eslint/eslint-plugin": "^4.17.0", 38 | "@typescript-eslint/parser": "^4.17.0", 39 | "@vitejs/plugin-react": "^1.2.0", 40 | "autoprefixer": "^10.4.7", 41 | "babel": "^6.23.0", 42 | "dagre-reactjs": "^1.0.0-alpha.2", 43 | "eslint": "^7.22.0", 44 | "eslint-config-prettier": "^8.3.0", 45 | "eslint-plugin-prettier": "^4.0.0", 46 | "eslint-plugin-react": "^7.27.1", 47 | "less": "^4.1.2", 48 | "postcss-import": "^14.1.0", 49 | "postcss-less": "^6.0.0", 50 | "prettier": "^2.6.0", 51 | "rollup": "2.41.2", 52 | "rollup-plugin-less": "^1.1.3", 53 | "ss-queue": "1.0.x", 54 | "ss-stack": "1.0.x", 55 | "tailwindcss": "^3.0.24", 56 | "typescript": "4.6.2", 57 | "vite": "^2.3.8" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | module.exports = { 3 | plugins: [tailwindcss('./tailwind.js'), require('autoprefixer')], 4 | }; 5 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import replace from '@rollup/plugin-replace'; 4 | import resolve from '@rollup/plugin-node-resolve'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | import less from 'rollup-plugin-less'; 7 | 8 | const isProd = process.env.BUILD === 'production'; 9 | 10 | export default { 11 | input: 'src/mapForNoteIndex.ts', 12 | output: { 13 | format: 'cjs', 14 | file: 'main.js', 15 | exports: 'default', 16 | sourcemap: 'inline', 17 | sourcemapExcludeSources: isProd, 18 | }, 19 | external: ['obsidian', 'fs', 'os', 'path'], 20 | sourceMap: true, 21 | plugins: [ 22 | less(), 23 | typescript(), 24 | resolve({ 25 | browser: true, 26 | }), 27 | replace({ 28 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 29 | }), 30 | babel({ 31 | presets: ['@babel/preset-react', '@babel/preset-typescript'], 32 | }), 33 | commonjs(), 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /src/data/mapCal.ts: -------------------------------------------------------------------------------- 1 | import { Graph, GraphEdge, GraphVertex } from '../utils'; 2 | import { skipMOCs, useBackLinks, useForwardLinks } from '../mapForNoteComponent'; 3 | 4 | export const getPathsFromVertex = (startNode: GraphVertex, endNode: GraphVertex, sg: Graph): GraphVertex[][] => { 5 | const pathIterator = sg.findAllPath(startNode, endNode); 6 | return Array.from(pathIterator); 7 | }; 8 | 9 | const setMap = async (): Promise => { 10 | //Get All Resolved Links From Obsidian 11 | const resolvedLinks = app.metadataCache.resolvedLinks; 12 | 13 | // configure directed true/false 14 | const sg = new Graph(); 15 | 16 | for (const key in resolvedLinks) { 17 | //Get Resolved Links By Specific Note 18 | const valueMap = resolvedLinks[key]; 19 | 20 | //Get MOC links 21 | let outboundLinkCounter = 0; 22 | if (skipMOCs) { 23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 | for (const linkKey in valueMap) { 25 | outboundLinkCounter++; 26 | } 27 | } 28 | 29 | if (skipMOCs && !(outboundLinkCounter > 20) && !key.includes('MOC')) { 30 | const baseNode = new GraphVertex(key); 31 | // look at each link 32 | for (const linkKey in valueMap) { 33 | if (linkKey.includes('MOC')) continue; 34 | const targetNode = new GraphVertex(linkKey); 35 | 36 | if (useForwardLinks) { 37 | // console.log(" Adding FORWARDLINK edge " + nodeBasename + " -> " + target); 38 | if (baseNode?.value != targetNode?.value) { 39 | if (sg.findEdge(baseNode, targetNode) == null) { 40 | sg.addEdge(new GraphEdge(baseNode, targetNode)); 41 | } 42 | } 43 | } 44 | 45 | // allow backlinks 46 | if (useBackLinks) { 47 | if (baseNode?.value != targetNode?.value) { 48 | if (sg.findEdge(targetNode, baseNode) == null) { 49 | sg.addEdge(new GraphEdge(targetNode, baseNode)); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | // if (useTags) { 57 | // const text = await app.vault.adapter.read(key); 58 | // // eslint-disable-next-line @typescript-eslint/ban-ts-comment 59 | // // @ts-ignore 60 | // const result = text.matchAll(/#[\w|äÄöÖüÜß/\-_]+/gim); 61 | // const ts = Array.from(result); 62 | // 63 | // for (let i = 0; i < ts.length; i++) { 64 | // let tag = String(ts[i]); 65 | // tag = tag.trim(); 66 | // let tagNode = sg.getVertexByKey(tag); 67 | // 68 | // if (tagNode == null) { 69 | // tagNode = new GraphVertex(tag); 70 | // } 71 | // 72 | // if (sg.findEdge(baseNode, tagNode) == null) { 73 | // const edge = new GraphEdge(baseNode, tagNode, 0); 74 | // sg.addEdge(edge); 75 | // } 76 | // 77 | // if (sg.findEdge(tagNode, baseNode) == null) { 78 | // const edge = new GraphEdge(tagNode, baseNode, 0); 79 | // sg.addEdge(edge); 80 | // } 81 | // } 82 | // } 83 | } 84 | return sg; 85 | }; 86 | 87 | export const findPath = async (start: string, end: string): Promise => { 88 | const map = await setMap(); 89 | const endNode = map.getVertexByKey(end); 90 | const startNode = map.getVertexByKey(start); 91 | return getPathsFromVertex(startNode, endNode, map); 92 | }; 93 | -------------------------------------------------------------------------------- /src/data/mfnTestData.ts: -------------------------------------------------------------------------------- 1 | import { RecursivePartial, NodeOptions, EdgeOptions } from 'dagre-reactjs'; 2 | 3 | export const foreignObjects: { 4 | nodes: Array>; 5 | edges: Array>; 6 | } = { 7 | nodes: [ 8 | { 9 | id: '0', 10 | label: 'Scan for tests', 11 | meta: { 12 | description: 'run a scan on the test directory', 13 | }, 14 | styles: { 15 | shape: { 16 | styles: { fill: '#fff', stroke: '#000', strokeWidth: '0' }, 17 | }, 18 | node: { 19 | padding: { 20 | top: 0, 21 | bottom: 0, 22 | left: 0, 23 | right: 0, 24 | }, 25 | }, 26 | label: {}, 27 | }, 28 | labelType: 'foreign', 29 | }, 30 | { 31 | id: '1', 32 | label: 'Scan for tests', 33 | meta: { 34 | description: 'run a scan on the test directory', 35 | }, 36 | styles: { 37 | shape: { 38 | styles: { fill: '#fff', stroke: '#000', strokeWidth: '0' }, 39 | }, 40 | node: { 41 | padding: { 42 | top: 0, 43 | bottom: 0, 44 | left: 0, 45 | right: 0, 46 | }, 47 | }, 48 | label: {}, 49 | }, 50 | labelType: 'foreign', 51 | }, 52 | { 53 | id: '2', 54 | label: 'Add new tests', 55 | meta: { 56 | description: 'add the new test cases to the database', 57 | }, 58 | styles: { 59 | shape: { 60 | styles: { fill: '#fff', stroke: '#000', strokeWidth: '0' }, 61 | }, 62 | node: { 63 | padding: { 64 | top: 0, 65 | bottom: 0, 66 | left: 0, 67 | right: 0, 68 | }, 69 | }, 70 | label: {}, 71 | }, 72 | labelType: 'foreign', 73 | }, 74 | ], 75 | edges: [ 76 | { 77 | from: '0', 78 | to: '2', 79 | label: 'Execute in memory', 80 | labelType: 'foreign', 81 | meta: { 82 | description: 'I have no idea what to say about this so here is some text', 83 | }, 84 | }, 85 | { 86 | from: '1', 87 | to: '2', 88 | label: 'Execute in memory', 89 | labelType: 'foreign', 90 | meta: { 91 | description: 'I have no idea what to say about this so here is some text', 92 | }, 93 | }, 94 | ], 95 | }; 96 | -------------------------------------------------------------------------------- /src/less/index.less: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .edge-label { 6 | font-weight: bold; 7 | } 8 | 9 | .edge-description { 10 | font-size: 10px; 11 | } 12 | -------------------------------------------------------------------------------- /src/mapForNoteComponent.ts: -------------------------------------------------------------------------------- 1 | import { ItemView, Scope, WorkspaceLeaf } from 'obsidian'; 2 | import React from 'react'; 3 | import { createRoot } from 'react-dom/client'; 4 | import { mapViewType } from './utils/consts'; 5 | 6 | import NoteMap from './ui/mfnMap'; 7 | import type mapForNotePlugin from './mapForNoteIndex'; 8 | 9 | declare module 'obsidian' { 10 | interface App { 11 | isMobile(): boolean; 12 | } 13 | 14 | interface Workspace { 15 | on(name: 'hover-link', callback: (e: MouseEvent) => any, ctx?: any): EventRef; 16 | } 17 | } 18 | 19 | export class MapForNote extends ItemView { 20 | private mapForNoteComponent: React.ReactElement; 21 | scope: Scope = new Scope(this.app.scope); 22 | plugin: mapForNotePlugin; 23 | 24 | constructor(leaf: WorkspaceLeaf, plugin: mapForNotePlugin) { 25 | super(leaf); 26 | this.plugin = plugin; 27 | } 28 | 29 | getViewType(): string { 30 | return mapViewType; 31 | } 32 | 33 | getDisplayText(): string { 34 | return 'Map For Note'; 35 | } 36 | 37 | getIcon(): string { 38 | return 'map'; 39 | } 40 | 41 | async onOpen(): Promise { 42 | useForwardLinks = this.plugin.settings.useForwardLinks; 43 | useBackLinks = this.plugin.settings.useBackLinks; 44 | useTags = this.plugin.settings.useTags; 45 | skipMOCs = this.plugin.settings.skipMOCs; 46 | 47 | // this.scope.register(['Mod'], 'Enter', () => { 48 | // return true; 49 | // }); 50 | 51 | this.mapForNoteComponent = React.createElement(NoteMap); 52 | const root = createRoot((this as any).contentEl!); 53 | root.render(this.mapForNoteComponent); 54 | } 55 | } 56 | 57 | export let useForwardLinks: boolean; 58 | export let useBackLinks: boolean; 59 | export let useTags: boolean; 60 | export let skipMOCs: boolean; 61 | -------------------------------------------------------------------------------- /src/mapForNoteIndex.ts: -------------------------------------------------------------------------------- 1 | import { FileView, Notice, Platform, Plugin, WorkspaceLeaf } from 'obsidian'; 2 | import { mapViewType } from './utils/consts'; 3 | import { MapForNote } from './mapForNoteComponent'; 4 | import { t } from './translations/helper'; 5 | import { DEFAULT_SETTINGS, MapForNoteSettings, MapForNoteSettingTab } from './mapForNoteSettings'; 6 | 7 | export default class mapForNotePlugin extends Plugin { 8 | private view: MapForNote; 9 | public settings: MapForNoteSettings; 10 | 11 | async onload(): Promise { 12 | await this.loadSettings(); 13 | this.registerView(mapViewType, (leaf: WorkspaceLeaf) => (this.view = new MapForNote(leaf, this))); 14 | 15 | this.addSettingTab(new MapForNoteSettingTab(this.app, this)); 16 | // this.app.workspace.onLayoutReady(this.onLayoutReady.bind(this)); 17 | this.addRibbonIcon('map', 'Map For Note', () => { 18 | new Notice(t('Open Map For Note Successfully')); 19 | this.openMapView(); 20 | }); 21 | 22 | this.addCommand({ 23 | id: 'open-map-for-note', 24 | name: 'Open Map For Note', 25 | callback: () => this.openMapView(), 26 | hotkeys: [], 27 | }); 28 | } 29 | 30 | public async loadSettings() { 31 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 32 | } 33 | 34 | async saveSettings() { 35 | await this.saveData(this.settings); 36 | } 37 | 38 | onunload() { 39 | this.app.workspace.detachLeavesOfType(mapViewType); 40 | new Notice(t('Close Map For Note Successfully')); 41 | } 42 | 43 | onLayoutReady(): void { 44 | this.addSettingTab(new MapForNoteSettingTab(this.app, this)); 45 | } 46 | 47 | async openMapView() { 48 | const workspace = this.app.workspace; 49 | workspace.detachLeavesOfType(mapViewType); 50 | const leaf = workspace.getLeaf( 51 | !Platform.isMobile && workspace.activeLeaf && workspace.activeLeaf.view instanceof FileView, 52 | ); 53 | await leaf.setViewState({ type: mapViewType }); 54 | workspace.revealLeaf(leaf); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/mapForNoteSettings.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting } from 'obsidian'; 2 | import { t } from './translations/helper'; 3 | import mapForNotePlugin from './mapForNoteIndex'; 4 | 5 | export interface MapForNoteSettings { 6 | useForwardLinks: boolean; 7 | useBackLinks: boolean; 8 | useTags: boolean; 9 | skipMOCs: boolean; 10 | } 11 | 12 | export const DEFAULT_SETTINGS: MapForNoteSettings = { 13 | useForwardLinks: false, 14 | useBackLinks: true, 15 | useTags: false, 16 | skipMOCs: true, 17 | }; 18 | 19 | export class MapForNoteSettingTab extends PluginSettingTab { 20 | plugin: mapForNotePlugin; 21 | //eslint-disable-next-line 22 | private applyDebounceTimer: number = 0; 23 | 24 | constructor(app: App, plugin: mapForNotePlugin) { 25 | super(app, plugin); 26 | this.plugin = plugin; 27 | } 28 | 29 | applySettingsUpdate() { 30 | clearTimeout(this.applyDebounceTimer); 31 | const plugin = this.plugin; 32 | this.applyDebounceTimer = window.setTimeout(() => { 33 | plugin.saveSettings(); 34 | }, 100); 35 | } 36 | 37 | //eslint-disable-next-line 38 | async hide() {} 39 | 40 | async display() { 41 | await this.plugin.loadSettings(); 42 | 43 | const { containerEl } = this; 44 | this.containerEl.empty(); 45 | 46 | // this.containerEl.createEl('h1', { text: t('Regular Options') }); 47 | 48 | // new Setting(containerEl) 49 | // .setName('Use Forward-links') 50 | // .setDesc( 51 | // "If set, allows to travel using forward-links. If you have a graph like this: A -> B -> C and you ask about the story between A and C, it will give you 'A, B, C' since A forward-links to B and B forward-links to C", 52 | // ) 53 | // .addToggle((toggle) => 54 | // toggle.setValue(this.plugin.settings.useForwardLinks).onChange((value) => { 55 | // this.plugin.settings.useForwardLinks = value; 56 | // this.plugin.saveData(this.plugin.settings); 57 | // }), 58 | // ); 59 | // 60 | // new Setting(containerEl) 61 | // .setName('Use Back-links') 62 | // .setDesc( 63 | // "If set, allows to travel using back-links. If you have a graph like this: A -> B -> C and you ask about the story between C and A, it will give you 'C, B, A' since C has a back-link from B and B has a back-link from A", 64 | // ) 65 | // .addToggle((toggle) => 66 | // toggle.setValue(this.plugin.settings.useBackLinks).onChange((value) => { 67 | // this.plugin.settings.useBackLinks = value; 68 | // this.plugin.saveData(this.plugin.settings); 69 | // }), 70 | // ); 71 | // 72 | // containerEl.createEl('h3', { text: 'Include Tags' }); 73 | // 74 | // new Setting(containerEl) 75 | // .setName('Use Tags') 76 | // .setDesc('If set, allows to travel using tags. ') 77 | // .addToggle((toggle) => 78 | // toggle.setValue(this.plugin.settings.useTags).onChange((value) => { 79 | // this.plugin.settings.useTags = value; 80 | // this.plugin.saveData(this.plugin.settings); 81 | // }), 82 | // ); 83 | // 84 | // containerEl.createEl('h3', { text: 'Avoid traveling via certain notes and folders' }); 85 | // 86 | // new Setting(containerEl) 87 | // .setName('Take the scenic route') 88 | // .setDesc( 89 | // "If set, will skip 'hub' notes with too many links (MOCs). Configure exactly how many links make a MOC below.", 90 | // ) 91 | // .addToggle((toggle) => 92 | // toggle.setValue(this.plugin.settings.skipMOCs).onChange((value) => { 93 | // this.plugin.settings.skipMOCs = value; 94 | // this.plugin.saveData(this.plugin.settings); 95 | // }), 96 | // ); 97 | 98 | this.containerEl.createEl('h1', { text: t('Say Thank You') }); 99 | 100 | new Setting(containerEl) 101 | .setName(t('Donate')) 102 | .setDesc(t('If you like this plugin, consider donating to support continued development:')) 103 | // .setClass("AT-extra") 104 | .addButton((bt) => { 105 | bt.buttonEl.outerHTML = ``; 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/translations/helper.ts: -------------------------------------------------------------------------------- 1 | // Code from https://github.com/valentine195/obsidian-admonition/blob/master/src/lang/helpers.ts 2 | 3 | import {moment} from 'obsidian'; 4 | 5 | import ar from './locale/ar'; 6 | import cz from './locale/cz'; 7 | import da from './locale/da'; 8 | import de from './locale/de'; 9 | import en from './locale/en'; 10 | import enGB from './locale/en-gb'; 11 | import es from './locale/es'; 12 | import fr from './locale/fr'; 13 | import hi from './locale/hi'; 14 | import id from './locale/id'; 15 | import it from './locale/it'; 16 | import ja from './locale/ja'; 17 | import ko from './locale/ko'; 18 | import nl from './locale/nl'; 19 | import no from './locale/no'; 20 | import pl from './locale/pl'; 21 | import pt from './locale/pt'; 22 | import ptBR from './locale/pt-br'; 23 | import ro from './locale/ro'; 24 | import ru from './locale/ru'; 25 | import tr from './locale/tr'; 26 | import zhCN from './locale/zh-cn'; 27 | import zhTW from './locale/zh-tw'; 28 | 29 | const localeMap: {[k: string]: Partial} = { 30 | ar, 31 | cs: cz, 32 | da, 33 | de, 34 | en, 35 | 'en-gb': enGB, 36 | es, 37 | fr, 38 | hi, 39 | id, 40 | it, 41 | ja, 42 | ko, 43 | nl, 44 | nn: no, 45 | pl, 46 | pt, 47 | 'pt-br': ptBR, 48 | ro, 49 | ru, 50 | tr, 51 | 'zh-cn': zhCN, 52 | 'zh-tw': zhTW, 53 | }; 54 | 55 | const locale = localeMap[moment.locale()]; 56 | 57 | export function t(str: keyof typeof en): string { 58 | return (locale && locale[str]) || en[str]; 59 | } 60 | -------------------------------------------------------------------------------- /src/translations/locale/ar.ts: -------------------------------------------------------------------------------- 1 | // العربية 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/cz.ts: -------------------------------------------------------------------------------- 1 | // čeština 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/da.ts: -------------------------------------------------------------------------------- 1 | // Dansk 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/de.ts: -------------------------------------------------------------------------------- 1 | // Deutsch 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/en-gb.ts: -------------------------------------------------------------------------------- 1 | // British English 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/en.ts: -------------------------------------------------------------------------------- 1 | // English 2 | 3 | export default { 4 | // setting.ts 5 | 'Regular Options': 'Regular Options', 6 | welcome: 'Welcome', 7 | 'Open Map For Note Successfully': 'Open Map For Note Successfully', 8 | 'Close Map For Note Successfully': 'Close Map For Note Successfully', 9 | 'Say Thank You': 'Say Thank You', 10 | Donate: 'Donate', 11 | 'If you like this plugin, consider donating to support continued development:': 12 | 'If you like this plugin, consider donating to support continued development:', 13 | }; 14 | -------------------------------------------------------------------------------- /src/translations/locale/es.ts: -------------------------------------------------------------------------------- 1 | // Español 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/fr.ts: -------------------------------------------------------------------------------- 1 | // français 2 | 3 | export default { 4 | // setting.ts 5 | }; 6 | -------------------------------------------------------------------------------- /src/translations/locale/hi.ts: -------------------------------------------------------------------------------- 1 | // हिन्दी 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/id.ts: -------------------------------------------------------------------------------- 1 | // Bahasa Indonesia 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/it.ts: -------------------------------------------------------------------------------- 1 | // Italiano 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/ja.ts: -------------------------------------------------------------------------------- 1 | // 日本語 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/ko.ts: -------------------------------------------------------------------------------- 1 | // 한국어 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/nl.ts: -------------------------------------------------------------------------------- 1 | // Nederlands 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/no.ts: -------------------------------------------------------------------------------- 1 | // Norsk 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/pl.ts: -------------------------------------------------------------------------------- 1 | // język polski 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/pt-br.ts: -------------------------------------------------------------------------------- 1 | // Português do Brasil 2 | // Brazilian Portuguese 3 | 4 | export default {}; 5 | -------------------------------------------------------------------------------- /src/translations/locale/pt.ts: -------------------------------------------------------------------------------- 1 | // Português 2 | 3 | export default { 4 | // setting.ts 5 | }; 6 | -------------------------------------------------------------------------------- /src/translations/locale/ro.ts: -------------------------------------------------------------------------------- 1 | // Română 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/ru.ts: -------------------------------------------------------------------------------- 1 | // русский 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/tr.ts: -------------------------------------------------------------------------------- 1 | // Türkçe 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/translations/locale/zh-cn.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // setting.ts 3 | 'Regular Options': '常规选项', 4 | 'Open Map For Note Successfully': '成功打开节点地图', 5 | 'Close Map For Note Successfully': '成功关闭节点地图插件', 6 | 'Say Thank You': '说声感谢', 7 | Donate: '买杯咖啡', 8 | 'If you like this plugin, consider donating to support continued development:': 9 | '如果你喜欢这个插件,请考虑捐赠来支持后续开发', 10 | }; 11 | -------------------------------------------------------------------------------- /src/translations/locale/zh-tw.ts: -------------------------------------------------------------------------------- 1 | // 繁體中文 2 | 3 | export default {}; 4 | -------------------------------------------------------------------------------- /src/ui/file-suggest.ts: -------------------------------------------------------------------------------- 1 | import { TAbstractFile, TFile, TFolder } from 'obsidian'; 2 | 3 | import { TextInputSuggest } from './suggest'; 4 | 5 | export class FileSuggest extends TextInputSuggest { 6 | getSuggestions(inputStr: string): TFile[] { 7 | const abstractFiles = this.app.vault.getMarkdownFiles(); 8 | const files: TFile[] = []; 9 | const lowerCaseInputStr = inputStr.toLowerCase(); 10 | 11 | abstractFiles.forEach((file: TFile) => { 12 | if (file.path.toLowerCase().contains(lowerCaseInputStr)) { 13 | files.push(file); 14 | } 15 | }); 16 | 17 | return files; 18 | } 19 | 20 | renderSuggestion(file: TFile, el: HTMLElement): void { 21 | el.setText(file.path); 22 | } 23 | 24 | selectSuggestion(file: TFile): void { 25 | this.inputEl.value = file.path; 26 | this.inputEl.trigger('input'); 27 | this.close(); 28 | } 29 | } 30 | 31 | export class FolderSuggest extends TextInputSuggest { 32 | getSuggestions(inputStr: string): TFolder[] { 33 | const abstractFiles = this.app.vault.getAllLoadedFiles(); 34 | const folders: TFolder[] = []; 35 | const lowerCaseInputStr = inputStr.toLowerCase(); 36 | 37 | abstractFiles.forEach((folder: TAbstractFile) => { 38 | if (folder instanceof TFolder && folder.path.toLowerCase().contains(lowerCaseInputStr)) { 39 | folders.push(folder); 40 | } 41 | }); 42 | 43 | return folders; 44 | } 45 | 46 | renderSuggestion(file: TFolder, el: HTMLElement): void { 47 | el.setText(file.path); 48 | } 49 | 50 | selectSuggestion(file: TFolder): void { 51 | this.inputEl.value = file.path; 52 | this.inputEl.trigger('input'); 53 | this.close(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ui/mapCore.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import useState from 'react-usestateref'; 3 | import { DagreReact, NodeOptions, EdgeOptions, RecursivePartial } from 'dagre-reactjs'; 4 | import { Foreign } from './mfnNode'; 5 | import '../less/index.less'; 6 | import { Size } from 'dagre-reactjs'; 7 | 8 | interface Props { 9 | nodes: Array>; 10 | edges: Array>; 11 | } 12 | 13 | const DEFAULT_NODE_CONFIG = { 14 | styles: { 15 | node: { 16 | padding: { 17 | top: 10, 18 | bottom: 10, 19 | left: 10, 20 | right: 10, 21 | }, 22 | }, 23 | shape: {}, 24 | label: {}, 25 | }, 26 | }; 27 | 28 | const MapCoreComponent: React.FC = (props) => { 29 | const [, setNodes, nodesRef] = useState(props.nodes); 30 | const [, setEdges, edgesRef] = useState(props.edges); 31 | const [, setStage, stageRef] = useState(1); 32 | React.useEffect(() => { 33 | setNodes(props.nodes); 34 | setEdges(props.edges); 35 | setStage(stageRef.current + 1); 36 | }, [props.nodes, props.edges]); 37 | 38 | const customNodeLabels = { 39 | foreign: { 40 | renderer: Foreign, 41 | html: true, 42 | }, 43 | }; 44 | 45 | const svgSet = (width: number, height: number) => { 46 | setTimeout(() => { 47 | const svg = document.getElementById('schedule'); 48 | // svg.setAttribute('width', width + 'px'); 49 | // svg.setAttribute('height', height + 'px'); 50 | svg.setAttribute('viewBox', `0 0 ${width} ${height}`); 51 | }, 100); 52 | // svg.setAttribute('viewBox', `${x} ${bbox.y} ${bbox.width} ${bbox.height}`); 53 | }; 54 | 55 | return ( 56 | <> 57 |
58 | 59 | svgSet(width, height)} 66 | graphOptions={{ 67 | marginx: 15, 68 | marginy: 15, 69 | rankdir: 'TB', 70 | ranksep: 55, 71 | nodesep: 35, 72 | }} 73 | /> 74 | 75 |
76 | 77 | ); 78 | }; 79 | 80 | export default MapCoreComponent; 81 | -------------------------------------------------------------------------------- /src/ui/mfnInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { FileSuggest } from './file-suggest'; 3 | import { Simulate } from 'react-dom/test-utils'; 4 | import input = Simulate.input; 5 | 6 | export function MfnInput(props) { 7 | const el = useRef(null); 8 | const { onChange, onFocus, onBlur, value, defaultValue, ...attrs } = props; 9 | const _value = 'value' in props ? value : 'defaultValue' in props ? defaultValue : null; 10 | 11 | const handleChange = (e) => { 12 | if (onChange) { 13 | onChange(e); 14 | } 15 | }; 16 | const forceSetValue = () => { 17 | if ('value' in props && el.current) { 18 | const input = el.current; 19 | input.value = value; 20 | input.setAttribute('value', value); 21 | } 22 | }; 23 | 24 | let inputing = false; 25 | 26 | return ( 27 | { 31 | if (!input) { 32 | return; 33 | } 34 | 35 | el.current = input; 36 | forceSetValue(); 37 | }} 38 | // react 在focus/blur时会重新设值,如果没有下面的操作,会导致focus/blur之后,变空 39 | // TODO 由于是异步操作,会导致文字闪动,光标定位到最末尾 40 | onFocus={(e) => { 41 | setTimeout(forceSetValue, 10); 42 | onFocus && onFocus(e); 43 | }} 44 | onBlur={(e) => { 45 | setTimeout(forceSetValue, 150); 46 | onBlur && onBlur(e); 47 | }} 48 | onCompositionStart={() => { 49 | inputing = true; 50 | }} 51 | onCompositionEnd={(e) => { 52 | inputing = false; 53 | handleChange(e); 54 | }} 55 | onChange={(e) => { 56 | if (!inputing) { 57 | handleChange(e); 58 | } 59 | }} 60 | /> 61 | ); 62 | } 63 | 64 | export default MfnInput; 65 | -------------------------------------------------------------------------------- /src/ui/mfnLabel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { CustomEdgeLabelProps } from 'dagre-reactjs'; 3 | 4 | export const ForeignLabel: React.FC = ({ edgeMeta }) => { 5 | return ( 6 |
12 |
{edgeMeta.label}
13 |
{edgeMeta.meta.description}
14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/ui/mfnMap.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useCallback, useEffect } from 'react'; 3 | import { findPath } from '../data/mapCal'; 4 | import '../less/index.less'; 5 | import MapCoreComponent from './mapCore'; 6 | import { Notice, TFile } from 'obsidian'; 7 | import { FileSuggest } from './file-suggest'; 8 | import useState from 'react-usestateref'; 9 | 10 | interface Props {} 11 | 12 | function testStartAndEnd(start: string, end: string) { 13 | if (start === end) return false; 14 | const startFile = app.metadataCache.getFirstLinkpathDest(start, ''); 15 | const endFile = app.metadataCache.getFirstLinkpathDest(end, ''); 16 | if (startFile instanceof TFile && endFile instanceof TFile) return true; 17 | else return false; 18 | } 19 | 20 | function unique(arr, key) { 21 | if (!arr) return arr; 22 | if (key === undefined) return [...new Set(arr)]; 23 | const map = { 24 | string: (e) => e[key], 25 | function: (e) => key(e), 26 | }; 27 | const fn = map[typeof key]; 28 | const obj = arr.reduce((o, e) => ((o[fn(e)] = e), o), {}); 29 | return Object.values(obj); 30 | } 31 | 32 | const NoteMap: React.FC = () => { 33 | const inputStartRef = React.useRef(); 34 | const inputEndRef = React.useRef(); 35 | const [start, setStart, startRef] = useState(''); 36 | const [end, setEnd, endRef] = useState(''); 37 | const [data, setData] = useState({ 38 | nodes: [], 39 | edges: [], 40 | }); 41 | 42 | useEffect(() => { 43 | setTimeout(() => { 44 | new FileSuggest(app, inputStartRef.current); 45 | new FileSuggest(app, inputEndRef.current); 46 | }, 1000); 47 | }, []); 48 | 49 | const handleStartInput = useCallback((e) => { 50 | setStart(e.target.value); 51 | }, []); 52 | 53 | const handleEndInput = useCallback((e) => { 54 | setEnd(e.target.value); 55 | }, []); 56 | 57 | const calResult = async () => { 58 | if (inputStartRef.current?.value != start && inputStartRef.current?.value.length > 0) { 59 | setStart(inputStartRef.current?.value); 60 | } 61 | if (inputEndRef.current?.value != start && inputEndRef.current?.value.length > 0) { 62 | setEnd(inputEndRef.current?.value); 63 | } 64 | if (!testStartAndEnd(startRef.current, endRef.current)) { 65 | new Notice('Wrong Nodes'); 66 | return; 67 | } 68 | const data = await findPath(startRef.current, endRef.current); 69 | 70 | console.log(data); 71 | if (data.length === 0) { 72 | new Notice('No Paths between Two Nodes'); 73 | return; 74 | } 75 | const tempNodes = []; 76 | const tempEdges = []; 77 | data.forEach((path) => { 78 | path.map((v) => { 79 | tempNodes.push({ 80 | id: v.value, 81 | label: v.value, 82 | meta: { 83 | description: '', 84 | }, 85 | styles: { 86 | shape: { 87 | styles: { fill: '#fff', stroke: '#000', strokeWidth: '0' }, 88 | }, 89 | node: { 90 | padding: { 91 | top: 0, 92 | bottom: 0, 93 | left: 0, 94 | right: 0, 95 | }, 96 | }, 97 | label: {}, 98 | }, 99 | labelType: 'foreign', 100 | }); 101 | }); 102 | for (let i = 0; i < path.length - 1; i++) { 103 | const source = path[i].value; 104 | const target = path[parseInt(String(i)) + 1].value; 105 | tempEdges.push({ 106 | from: source, 107 | to: target, 108 | }); 109 | } 110 | }); 111 | const uniqueNodes = unique(tempNodes, 'id'); 112 | const uniqueEdges = tempEdges.filter(function (a) { 113 | const key = a.from + '|' + a.to; 114 | if (!this[key]) { 115 | this[key] = true; 116 | return true; 117 | } 118 | }, Object.create(null)); 119 | setData({ 120 | nodes: uniqueNodes, 121 | edges: uniqueEdges, 122 | }); 123 | }; 124 | 125 | return ( 126 |
127 |
128 |
129 |
130 |

起始

131 | 132 |
133 |
134 |

终点

135 | 136 |
137 |
138 |
139 | 145 |
146 |
147 |
148 | 149 |
150 |
151 | ); 152 | }; 153 | 154 | export default NoteMap; 155 | -------------------------------------------------------------------------------- /src/ui/mfnNode.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { CustomNodeLabelProps } from 'dagre-reactjs'; 3 | import { replaceMd } from '../utils/obInternalLink'; 4 | import { Platform } from 'obsidian'; 5 | 6 | export const Foreign: React.FC = ({ node }) => { 7 | const boxMouseOverHandler = (e: React.MouseEvent, label: string) => { 8 | // const box: HTMLDivElement = event.currentTarget; 9 | // box.style.backgroundColor = 'lightblue'; 10 | const targetEl = e.target as HTMLElement; 11 | 12 | if (!e.ctrlKey && !e.metaKey) return; 13 | 14 | if (targetEl.tagName !== 'A') return; 15 | 16 | if (targetEl.hasClass('internal-link')) { 17 | app.workspace.trigger('hover-link', { 18 | event: e, 19 | source: 'map-for-note-view', 20 | hoverParent: targetEl.parentElement, 21 | targetEl, 22 | linktext: targetEl.getAttr('href'), 23 | sourcePath: app.metadataCache.getFirstLinkpathDest(decodeURIComponent(label), '').path, 24 | }); 25 | } 26 | }; 27 | 28 | // This function will be triggered when the mouse pointer is moving out the box 29 | // const boxMouseOutHandler = (event: React.MouseEvent) => { 30 | // const box: HTMLDivElement = event.currentTarget; 31 | // box.style.backgroundColor = 'lightgreen'; 32 | // }; 33 | 34 | const onClickEvent = (e: React.MouseEvent, label: string): void => { 35 | const file = app.metadataCache.getFirstLinkpathDest('', label); 36 | let leaf; 37 | if (!Platform.isMobile) { 38 | leaf = app.workspace.splitActiveLeaf(); 39 | } else { 40 | leaf = app.workspace.activeLeaf; 41 | if (leaf === null) { 42 | leaf = app.workspace.getLeaf(true); 43 | } 44 | } 45 | leaf.openFile(file); 46 | return; 47 | }; 48 | 49 | return ( 50 |
62 | {/*
{node.label}
*/} 63 | {/*
{node.meta.description}
*/} 64 |
boxMouseOverHandler(e, node.label)} 66 | onClick={(e) => onClickEvent(e, node.label)} 67 | dangerouslySetInnerHTML={{ __html: replaceMd(node.label, node.label) }} 68 | >
69 |
70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/ui/suggest.ts: -------------------------------------------------------------------------------- 1 | import { createPopper, type Instance as PopperInstance } from '@popperjs/core'; 2 | import { App, type ISuggestOwner, Scope } from 'obsidian'; 3 | import { wrapAround } from '../utils/lib'; 4 | 5 | class Suggest { 6 | private owner: ISuggestOwner; 7 | private values: T[]; 8 | private suggestions: HTMLDivElement[]; 9 | private selectedItem: number; 10 | private containerEl: HTMLElement; 11 | 12 | constructor(owner: ISuggestOwner, containerEl: HTMLElement, scope: Scope) { 13 | this.owner = owner; 14 | this.containerEl = containerEl; 15 | 16 | containerEl.on('click', '.suggestion-item', this.onSuggestionClick.bind(this)); 17 | containerEl.on('mousemove', '.suggestion-item', this.onSuggestionMouseover.bind(this)); 18 | 19 | scope.register([], 'ArrowUp', (event) => { 20 | if (!event.isComposing) { 21 | this.setSelectedItem(this.selectedItem - 1, true); 22 | return false; 23 | } 24 | }); 25 | 26 | scope.register([], 'ArrowDown', (event) => { 27 | if (!event.isComposing) { 28 | this.setSelectedItem(this.selectedItem + 1, true); 29 | return false; 30 | } 31 | }); 32 | 33 | scope.register([], 'Enter', (event) => { 34 | if (!event.isComposing) { 35 | this.useSelectedItem(event); 36 | return false; 37 | } 38 | }); 39 | } 40 | 41 | onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void { 42 | event.preventDefault(); 43 | 44 | const item = this.suggestions.indexOf(el); 45 | this.setSelectedItem(item, false); 46 | this.useSelectedItem(event); 47 | } 48 | 49 | onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void { 50 | const item = this.suggestions.indexOf(el); 51 | this.setSelectedItem(item, false); 52 | } 53 | 54 | setSuggestions(values: T[]) { 55 | this.containerEl.empty(); 56 | const suggestionEls: HTMLDivElement[] = []; 57 | 58 | values.forEach((value) => { 59 | const suggestionEl = this.containerEl.createDiv('suggestion-item'); 60 | this.owner.renderSuggestion(value, suggestionEl); 61 | suggestionEls.push(suggestionEl); 62 | }); 63 | 64 | this.values = values; 65 | this.suggestions = suggestionEls; 66 | this.setSelectedItem(0, false); 67 | } 68 | 69 | useSelectedItem(event: MouseEvent | KeyboardEvent) { 70 | const currentValue = this.values[this.selectedItem]; 71 | if (currentValue) { 72 | this.owner.selectSuggestion(currentValue, event); 73 | } 74 | } 75 | 76 | setSelectedItem(selectedIndex: number, scrollIntoView: boolean) { 77 | const normalizedIndex = wrapAround(selectedIndex, this.suggestions.length); 78 | const prevSelectedSuggestion = this.suggestions[this.selectedItem]; 79 | const selectedSuggestion = this.suggestions[normalizedIndex]; 80 | 81 | prevSelectedSuggestion?.removeClass('is-selected'); 82 | selectedSuggestion?.addClass('is-selected'); 83 | 84 | this.selectedItem = normalizedIndex; 85 | 86 | if (scrollIntoView) { 87 | selectedSuggestion.scrollIntoView(false); 88 | } 89 | } 90 | } 91 | 92 | export abstract class TextInputSuggest implements ISuggestOwner { 93 | protected app: App; 94 | protected inputEl: HTMLInputElement; 95 | 96 | private popper: PopperInstance; 97 | private scope: Scope; 98 | private suggestEl: HTMLElement; 99 | private suggest: Suggest; 100 | 101 | constructor(app: App, inputEl: HTMLInputElement) { 102 | this.app = app; 103 | this.inputEl = inputEl; 104 | this.scope = new Scope(); 105 | 106 | this.suggestEl = createDiv('suggestion-container'); 107 | const suggestion = this.suggestEl.createDiv('suggestion'); 108 | this.suggest = new Suggest(this, suggestion, this.scope); 109 | 110 | this.scope.register([], 'Escape', this.close.bind(this)); 111 | 112 | this.inputEl.addEventListener('input', this.onInputChanged.bind(this)); 113 | this.inputEl.addEventListener('focus', this.onInputChanged.bind(this)); 114 | this.inputEl.addEventListener('blur', this.close.bind(this)); 115 | this.suggestEl.on('mousedown', '.suggestion-container', (event: MouseEvent) => { 116 | event.preventDefault(); 117 | }); 118 | } 119 | 120 | onInputChanged(): void { 121 | const inputStr = this.inputEl.value; 122 | const suggestions = this.getSuggestions(inputStr); 123 | 124 | if (suggestions.length > 0) { 125 | this.suggest.setSuggestions(suggestions); 126 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 127 | this.open((this.app).dom.appContainerEl, this.inputEl); 128 | } 129 | } 130 | 131 | open(container: HTMLElement, inputEl: HTMLElement): void { 132 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 133 | (this.app).keymap.pushScope(this.scope); 134 | 135 | container.appendChild(this.suggestEl); 136 | this.popper = createPopper(inputEl, this.suggestEl, { 137 | placement: 'bottom-start', 138 | modifiers: [ 139 | { 140 | name: 'sameWidth', 141 | enabled: true, 142 | fn: ({ state, instance }) => { 143 | // Note: positioning needs to be calculated twice - 144 | // first pass - positioning it according to the width of the popper 145 | // second pass - position it with the width bound to the reference element 146 | // we need to early exit to avoid an infinite loop 147 | const targetWidth = `${state.rects.reference.width}px`; 148 | if (state.styles.popper.width === targetWidth) { 149 | return; 150 | } 151 | state.styles.popper.width = targetWidth; 152 | instance.update(); 153 | }, 154 | phase: 'beforeWrite', 155 | requires: ['computeStyles'], 156 | }, 157 | ], 158 | }); 159 | } 160 | 161 | close(): void { 162 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 163 | (this.app).keymap.popScope(this.scope); 164 | 165 | this.suggest.setSuggestions([]); 166 | this.popper.destroy(); 167 | this.suggestEl.detach(); 168 | } 169 | 170 | abstract getSuggestions(inputStr: string): T[]; 171 | 172 | abstract renderSuggestion(item: T, el: HTMLElement): void; 173 | 174 | abstract selectSuggestion(item: T): void; 175 | } 176 | -------------------------------------------------------------------------------- /src/utils/consts.ts: -------------------------------------------------------------------------------- 1 | export const mapViewType = 'map-for-note-view'; 2 | -------------------------------------------------------------------------------- /src/utils/edge.ts: -------------------------------------------------------------------------------- 1 | import { GraphVertex } from './vertex'; 2 | export class GraphEdge { 3 | weight: number; 4 | startVertex: GraphVertex; 5 | endVertex: GraphVertex; 6 | 7 | /** 8 | * Creates an instance of GraphEdge. 9 | * @param {GraphVertex} startVertex - 开始节点 10 | * @param {GraphVertex} endVertex - 结束节点 11 | * @param {number} [weight=0] - 边的权重 12 | * @memberof GraphEdge 13 | */ 14 | constructor(startVertex: GraphVertex, endVertex: GraphVertex, weight = 0) { 15 | this.startVertex = startVertex; 16 | this.endVertex = endVertex; 17 | this.weight = weight; 18 | } 19 | 20 | /** 21 | * 获取 edge 的 key 值 22 | * 23 | * @returns {string} 24 | * @memberof GraphEdge 25 | */ 26 | getKey(): string { 27 | const startVertexKey = this.startVertex.getKey(); 28 | const endVertexKey = this.endVertex.getKey(); 29 | 30 | return `${startVertexKey}_${endVertexKey}`; 31 | } 32 | 33 | /** 34 | * 翻转这条边的指向 35 | * 36 | * @returns {GraphEdge} 37 | * @memberof GraphEdge 38 | */ 39 | reverse() { 40 | const tmp = this.startVertex; 41 | this.startVertex = this.endVertex; 42 | this.endVertex = tmp; 43 | 44 | return this; 45 | } 46 | 47 | /** 48 | * 复制边 49 | * 50 | * @returns {GraphEdge} 51 | * @memberof GraphEdge 52 | */ 53 | clone() { 54 | return new GraphEdge(this.startVertex, this.endVertex, this.weight); 55 | } 56 | 57 | /** 58 | * 重写 toString 方法 59 | * 60 | * @returns {string} 61 | * @memberof GraphEdge 62 | */ 63 | toString(): string { 64 | return this.getKey(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/graph.ts: -------------------------------------------------------------------------------- 1 | import Queue from 'ss-queue'; 2 | import Stack from 'ss-stack'; 3 | import { GraphEdge } from './edge'; 4 | import { GraphVertex } from './vertex'; 5 | import { invariant } from './lib'; 6 | 7 | /* ---------------------------------------------------- 8 | util for find all path 9 | ----------------------------------------------------- */ 10 | 11 | /** 12 | * 根据当前节点构建双栈 13 | * 14 | * @param {GraphVertex} vertex - 当前节点 15 | * @param {Stack} mainStack - 主栈 16 | * @param {Stack} neighborStack - 邻接节点栈 17 | * @param {Map} visited - 已访问缓存 18 | */ 19 | function buildDualStack( 20 | vertex: GraphVertex, 21 | mainStack: Stack, 22 | neighborStack: Stack, 23 | visited: Map, 24 | ) { 25 | if (vertex) { 26 | mainStack.push(vertex); // 将主节点入栈 27 | visited.set(vertex.getKey(), true); // 标记已被访问过 28 | 29 | // 获取 vertex 的邻接节点 30 | const neighborsTemp = vertex.getNeighbors(); 31 | const neighbors = neighborsTemp.filter((v: GraphVertex) => !visited.get(v.getKey())); 32 | neighborStack.push(neighbors); 33 | } 34 | } 35 | 36 | /** 37 | * 削减双栈(让双栈都减少一层) 38 | * 39 | * @param {Stack} mainStack 40 | * @param {Stack} neighborStack 41 | * @param {Map} visited 42 | */ 43 | function cutdownDualStack( 44 | mainStack: Stack, 45 | neighborStack: Stack, 46 | visited: Map, 47 | ) { 48 | // 将目标元素从 mainStack 中弹出, 49 | const droppedMain = mainStack.pop(); 50 | 51 | // 同时标记当前节点可以再次访问 52 | if (droppedMain) { 53 | visited.set(droppedMain.getKey(), false); 54 | } 55 | // 同时一并将 neighborStack 弹出元素 56 | neighborStack.pop(); 57 | } 58 | 59 | // ============== 60 | 61 | export class Graph { 62 | isDirected: boolean; 63 | vertices: { [key: string]: GraphVertex }; 64 | edges: { [key: string]: GraphEdge }; 65 | 66 | /** 67 | * Creates an instance of Graph. 68 | * @param {boolean} [isDirected=false] - 是否是有向图 69 | * @memberof Graph 70 | */ 71 | constructor(isDirected = false) { 72 | this.vertices = {}; 73 | this.edges = {}; 74 | this.isDirected = isDirected; 75 | } 76 | 77 | /** 78 | * 添加节点 79 | * 80 | * @param {GraphVertex} newVertex 81 | * @returns {Graph} 82 | * @memberof Graph 83 | */ 84 | addVertex(newVertex: GraphVertex): Graph { 85 | this.vertices[newVertex.getKey()] = newVertex; 86 | 87 | return this; 88 | } 89 | 90 | /** 91 | * 根据 key 值返回指定节点 92 | * 93 | * @param {string} vertexKey 94 | * @returns {GraphVertex} 95 | * @memberof Graph 96 | */ 97 | getVertexByKey(vertexKey: string): GraphVertex { 98 | return this.vertices[vertexKey]; 99 | } 100 | 101 | /** 102 | * 返回指定节点的相邻节点 103 | * 104 | * @param {GraphVertex} vertex 105 | * @returns {GraphVertex[]} 106 | */ 107 | getNeighbors(vertex: GraphVertex): GraphVertex[] { 108 | return vertex.getNeighbors(); 109 | } 110 | 111 | /** 112 | * 返回图中所有的节点 113 | * 114 | * @returns {GraphVertex[]} 115 | * @memberof Graph 116 | */ 117 | getAllVertices(): GraphVertex[] { 118 | return Object.values(this.vertices); 119 | } 120 | 121 | /** 122 | * 返回图中所有的边 123 | * 124 | * @returns {GraphEdge[]} 125 | * @memberof Graph 126 | */ 127 | getAllEdges(): GraphEdge[] { 128 | return Object.values(this.edges); 129 | } 130 | 131 | /** 132 | * 给图中添加边 133 | * 134 | * @param {GraphEdge} edge - 待添加的边 135 | * @param {boolean} [disableErrorWhenExist=false] - 是否关闭错误提示(当边已经存在图中的时候),默认是有错误提示 136 | * @returns {Graph} 137 | * @memberof Graph 138 | */ 139 | addEdge(edge: GraphEdge, disableErrorWhenExist = false): Graph { 140 | // 判断边是否已经添加到图中 141 | if (this.edges[edge.getKey()]) { 142 | if (disableErrorWhenExist) { 143 | return this; 144 | } else { 145 | invariant(false, 'Edge has already been added before'); 146 | } 147 | } 148 | 149 | // 首先找到开始和结束节点 150 | let startVertex = this.getVertexByKey(edge.startVertex.getKey()); 151 | let endVertex = this.getVertexByKey(edge.endVertex.getKey()); 152 | 153 | // 如果开始节点不存在图中,需要先添加 154 | if (!startVertex) { 155 | this.addVertex(edge.startVertex); 156 | startVertex = this.getVertexByKey(edge.startVertex.getKey()); // 注意:需要重新获取一次,不然 startVertex 不存在 157 | } 158 | 159 | // 如果结束节点不存在图中,需要先添加 160 | if (!endVertex) { 161 | this.addVertex(edge.endVertex); 162 | endVertex = this.getVertexByKey(edge.endVertex.getKey()); // 注意:需要重新获取一次,不然 endVertex 不存在 163 | } 164 | 165 | // 在图中添加这条边 166 | this.edges[edge.getKey()] = edge; 167 | 168 | // 根据是否是双向图 169 | if (this.isDirected) { 170 | // 如果是有向图,那么只用给 startVertex 添加此边 171 | startVertex.addEdge(edge); 172 | } else { 173 | // 否则就给开始、结束节点都添加该边 174 | startVertex.addEdge(edge); 175 | // 给结束节点添加边,需要 clone、然后再反向 176 | const clonedEdge = edge.clone(); 177 | endVertex.addEdge(clonedEdge.reverse()); 178 | } 179 | 180 | return this; 181 | } 182 | 183 | /** 184 | * 删除图中的某条边 185 | * 186 | * @param {GraphEdge} edge - 边的实例 187 | * @param {boolean} [disableErrorWhenExist=false] - 是否关闭错误提示(当图中不存在边时),默认是有错误提示 188 | * @returns {Graph} 189 | * @memberof Graph 190 | */ 191 | deleteEdge(edge: GraphEdge, disableErrorWhenExist = false): Graph { 192 | // 判断边是否存在 193 | if (!this.edges[edge.getKey()]) { 194 | if (disableErrorWhenExist) { 195 | return this; 196 | } else { 197 | invariant(false, 'Edge not found in graph'); 198 | } 199 | } 200 | 201 | // 先删除该边 202 | delete this.edges[edge.getKey()]; 203 | 204 | // 同时找到该边所在开始节点和结束节点 205 | const startVertex = this.getVertexByKey(edge.startVertex.getKey()); 206 | const endVertex = this.getVertexByKey(edge.endVertex.getKey()); 207 | 208 | // 分别在节点上删除该边 209 | startVertex.deleteEdge(edge); 210 | endVertex.deleteEdge(edge); 211 | 212 | return this; 213 | } 214 | 215 | /** 216 | * 查找开始节点和结束节点之间的那条边 217 | * 218 | * @param {GraphVertex} startVertex - 开始节点 219 | * @param {GraphVertex} endVertex - 结束节点 220 | * @returns {(GraphEdge | null)} 221 | * @memberof Graph 222 | */ 223 | findEdge(startVertex: GraphVertex, endVertex: GraphVertex): GraphEdge | null { 224 | // 首先判断开始节点是否在图中 225 | const vertex = this.getVertexByKey(startVertex.getKey()); 226 | 227 | if (!vertex) { 228 | return null; 229 | } 230 | 231 | // 然后通过开始节点 - 结束节点对应的边的实例 232 | return vertex.findEdge(endVertex); 233 | } 234 | 235 | /** 236 | * 返回图中所有边的权重之和 237 | * 238 | * @returns {number} 239 | * @memberof Graph 240 | */ 241 | getWeight(): number { 242 | return this.getAllEdges().reduce((weight, graphEdge) => { 243 | return weight + graphEdge.weight; 244 | }, 0); 245 | } 246 | 247 | /** 248 | * 让图中所有的边都方向 249 | * 250 | * @returns {Graph} 251 | * @memberof Graph 252 | */ 253 | reverse(): Graph { 254 | // 遍历所有的边 255 | this.getAllEdges().forEach((edge: GraphEdge) => { 256 | // 先将边从图中删除(反向操作之前一定要删除边,不然数据会存在不一致性 - key 没有反向,而 this.edges[key] 反向了 ) 257 | this.deleteEdge(edge); 258 | 259 | // 然后将边进行反向操作 260 | edge.reverse(); 261 | 262 | // 再将边添加回图中 263 | this.addEdge(edge); 264 | }); 265 | return this; 266 | } 267 | 268 | /** 269 | * 返回“节点 - 索引”映射表 270 | * 271 | * @returns {{ [key: string]: number }} - 映射表对象 272 | * @memberof Graph 273 | */ 274 | getVerticesIndices(): { [key: string]: number } { 275 | const verticesIndices: { [key: string]: number } = {}; 276 | this.getAllVertices().forEach((vertex, index) => { 277 | verticesIndices[vertex.getKey()] = index; 278 | }); 279 | 280 | return verticesIndices; 281 | } 282 | 283 | /** 284 | * 生成邻接矩阵 285 | * 286 | * @returns {number[][]} 287 | * @memberof Graph 288 | */ 289 | getAdjacencyMatrix(): number[][] { 290 | // 获取所有的节点列表 291 | const vertices = this.getAllVertices(); 292 | // 获取节点索引映射表 293 | const verticesIndices = this.getVerticesIndices(); 294 | 295 | // 初始化邻接矩阵,赋值 `Infinity` 表示两点之间不可达 296 | // 邻接矩阵是 N x N 大小的 297 | const adjacencyMatrix = Array(vertices.length) 298 | .fill(null) 299 | .map(() => { 300 | return Array(vertices.length).fill(Infinity); 301 | }); 302 | 303 | // 给每一列赋值 304 | vertices.forEach((vertex, vertexIndex) => { 305 | vertex.getNeighbors().forEach((neighbor) => { 306 | const neighborIndex = verticesIndices[neighbor.getKey()]; 307 | const edge = this.findEdge(vertex, neighbor); 308 | // eslint-disable-next-line 309 | if (!!edge) { 310 | adjacencyMatrix[vertexIndex][neighborIndex] = edge.weight; 311 | } 312 | }); 313 | }); 314 | 315 | return adjacencyMatrix; 316 | } 317 | 318 | /** 319 | * 重写 toString 方法,仅仅是打印出所有节点的列表 320 | * @return {string} 321 | */ 322 | toString() { 323 | return Object.keys(this.vertices).toString(); 324 | } 325 | 326 | /** 327 | * Breadth-first search (BFS) 328 | * 329 | * @param {GraphVertex} first - first node to start the bfs 330 | * @memberof Graph 331 | */ 332 | *bfs(first: GraphVertex) { 333 | const visited = new Map(); 334 | const nodeQueue = new Queue(); 335 | 336 | // 将第一个节点入栈 337 | nodeQueue.enqueue(first); 338 | 339 | while (!nodeQueue.isEmpty()) { 340 | const node = nodeQueue.dequeue(); 341 | // 确保当前节点没有被访问过 342 | if (node && !visited.has(node.getKey())) { 343 | yield node; 344 | visited.set(node.getKey(), node); 345 | // 挨个将相邻节点放到队列中去 346 | node.getNeighbors().forEach((neighbor: GraphVertex) => nodeQueue.enqueue(neighbor)); 347 | } 348 | } 349 | } 350 | 351 | /** 352 | * Depth-first search (DFS) 353 | * 354 | * @param {GraphVertex} first - first node to start the dfs 355 | * @memberof Graph 356 | */ 357 | *dfs(first: GraphVertex) { 358 | const visited = new Map(); 359 | const nodeStack = new Stack(); 360 | 361 | nodeStack.push(first); 362 | 363 | while (!nodeStack.isEmpty()) { 364 | const node = nodeStack.pop(); 365 | if (node && !visited.has(node.getKey())) { 366 | yield node; 367 | visited.set(node.getKey(), node); 368 | // 挨个将相邻节点放到 stack 中去 369 | node.getNeighbors().forEach((neighbor: GraphVertex) => nodeStack.push(neighbor)); 370 | } 371 | } 372 | } 373 | 374 | *findAllPath(source: GraphVertex, target: GraphVertex) { 375 | const path: GraphVertex[] = []; 376 | if (source === target) { 377 | path.push(source); 378 | yield path; 379 | return; 380 | } 381 | 382 | // 保存访问过的节点 383 | const visited = new Map(); 384 | 385 | // 使用双栈法来实现所有链路的查找 386 | const mainStack = new Stack(); 387 | const neighborStack = new Stack(); 388 | 389 | buildDualStack(source, mainStack, neighborStack, visited); 390 | 391 | // 监视邻接节点数量 392 | while (!mainStack.isEmpty()) { 393 | // 将邻接栈的数组先弹出 394 | const curNeighbors = neighborStack.pop(); 395 | 396 | // 如果邻接栈有元素可用,就将其堆放在 mainStack 上 397 | if (curNeighbors && curNeighbors.length) { 398 | let nextVertex = curNeighbors.shift(); 399 | if (nextVertex.edges.size === 0) { 400 | nextVertex = this.getVertexByKey(nextVertex.getKey()); 401 | } 402 | neighborStack.push(curNeighbors); // 将其压栈压回去 403 | // 如果存在下一个节点 404 | if (nextVertex) { 405 | buildDualStack(nextVertex, mainStack, neighborStack, visited); 406 | } 407 | } else { 408 | neighborStack.push(curNeighbors); // 将其压栈压回去,不然接下来的 cutdownDualStack 会导致 pop 两次 409 | // 如果邻接节点是空数组,也削减一层 410 | cutdownDualStack(mainStack, neighborStack, visited); 411 | continue; // 继续下一次循环 412 | } 413 | 414 | // 查看 mainStack 栈顶元素 415 | const peekNode = mainStack.peek; 416 | // debugger; 417 | // 检查该元素是否是目标节点,则当前 mainStack 就是一条路径 418 | if (peekNode === target) { 419 | yield mainStack.toArray(); 420 | 421 | // 削减一层 422 | cutdownDualStack(mainStack, neighborStack, visited); 423 | } 424 | 425 | if (mainStack.stack.length > 10) { 426 | // 削减一层 427 | cutdownDualStack(mainStack, neighborStack, visited); 428 | } 429 | } 430 | 431 | // 如果迭代完毕 hasTarget 都还是 false,说明没有目标对象; 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './edge'; 3 | export * from './vertex'; 4 | export * from './graph'; 5 | -------------------------------------------------------------------------------- /src/utils/lib.ts: -------------------------------------------------------------------------------- 1 | export function invariant(check: boolean, message: string, scope = 'ss-graph') { 2 | if (!check) { 3 | throw new Error(`${scope ? '[' + scope + ']' : ''} Invariant failed: ${message}`); 4 | } 5 | } 6 | 7 | // https://github.com/lodash/lodash/blob/3.0.8-npm-packages/lodash.isfunction/index.js 8 | const funcTag = '[object Function]', 9 | genTag = '[object GeneratorFunction]'; 10 | const objectToString = Object.prototype.toString; 11 | 12 | function isObject(value: any) { 13 | const type = typeof value; 14 | return !!value && (type == 'object' || type == 'function'); 15 | } 16 | 17 | export function isFunction(value: any): boolean { 18 | const tag = isObject(value) ? objectToString.call(value) : ''; 19 | return tag == funcTag || tag == genTag; 20 | } 21 | 22 | /** 23 | * 判断对象是否存在 24 | * @param {*} val - 待判断的对象 25 | * @param {bool} andString - 也要考虑字符串的 'undefined' 和 'null' 情况 26 | */ 27 | export function isExist(val: any, andString = true): boolean { 28 | const result = typeof val !== 'undefined' && val !== null; 29 | 30 | if (andString) { 31 | return result && val !== 'undefined' && val !== 'null'; 32 | } else { 33 | return result; 34 | } 35 | } 36 | 37 | export const wrapAround = (value: number, size: number): number => { 38 | return ((value % size) + size) % size; 39 | }; 40 | -------------------------------------------------------------------------------- /src/utils/obInternalLink.ts: -------------------------------------------------------------------------------- 1 | import { TFile } from 'obsidian'; 2 | 3 | export const replaceMd = (internalLink: string, label: string): string => { 4 | const file = app.metadataCache.getFirstLinkpathDest(decodeURIComponent(internalLink), ''); 5 | 6 | // let filePath; 7 | 8 | if (file instanceof TFile) { 9 | // filePath = file.path; 10 | if (label) { 11 | // console.log(`${label}`); 12 | // return `${label}`; 13 | return `${file.basename}`; 14 | } else { 15 | // return `${internalLink}`; 16 | return `${file.basename}`; 17 | } 18 | } else if (label) { 19 | return `${label}`; 20 | // return `${label}`; 21 | } else { 22 | return `${internalLink}`; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/utils/vertex.ts: -------------------------------------------------------------------------------- 1 | import { isExist, invariant } from './lib'; 2 | import { GraphEdge } from './edge'; 3 | 4 | export class GraphVertex { 5 | value: any; 6 | edges: Map; 7 | 8 | /** 9 | * Creates an instance of GraphVertex. 10 | * @param {*} value - 节点值 11 | * @memberof GraphVertex 12 | */ 13 | constructor(value: any) { 14 | invariant(isExist(value), 'Graph vertex must have a value'); 15 | 16 | // Normally you would store string value like vertex name. 17 | // But generally it may be any object as well 18 | this.value = value; 19 | 20 | // 使用链表保存当前节点的边的集合 21 | this.edges = new Map(); 22 | } 23 | 24 | /** 25 | * 将 edge 添加到当前节点的边链表中 26 | * 27 | * @param {GraphEdge} edge - edge 实例 28 | * @returns {GraphVertex} 29 | * @memberof GraphVertex 30 | */ 31 | addEdge(edge: GraphEdge) { 32 | this.edges.set(edge.getKey(), edge); 33 | return this; 34 | } 35 | 36 | /** 37 | * 将 edge 从当前节点的边链表中删除 38 | * 39 | * @param {GraphEdge} edge - edge 实例 40 | * @memberof GraphVertex 41 | */ 42 | deleteEdge(edge: GraphEdge) { 43 | this.edges.delete(edge.getKey()); 44 | } 45 | 46 | /** 47 | * 获取所有当前节点的相邻节点 48 | * 49 | * @returns {GraphVertex[]} 50 | * @memberof GraphVertex 51 | */ 52 | getNeighbors(): GraphVertex[] { 53 | const edges = this.getEdges(); 54 | 55 | const neighborsConverter = (edge: GraphEdge) => { 56 | return edge.startVertex.value === this.value ? edge.endVertex : edge.startVertex; 57 | }; 58 | 59 | // Return either start or end vertex. 60 | // For undirected graphs it is possible that current vertex will be the end one. 61 | return edges.map(neighborsConverter); 62 | } 63 | 64 | /** 65 | * 获取所有的边 66 | * 67 | * @returns {GraphEdge[]} 68 | * @memberof GraphVertex 69 | */ 70 | getEdges(): GraphEdge[] { 71 | return Array.from(this.edges.values()); 72 | } 73 | 74 | /** 75 | * 获取当前节点的度 76 | * 77 | * @returns {number} 78 | * @memberof GraphVertex 79 | */ 80 | getDegree(): number { 81 | return this.getEdges().length; 82 | } 83 | 84 | /** 85 | * 判断某条边是否存在于当前节点上 86 | * 87 | * @param {GraphEdge} requiredEdge 88 | * @returns {boolean} 89 | * @memberof GraphVertex 90 | */ 91 | hasEdge(requiredEdge: GraphEdge): boolean { 92 | const edgeNode = this.getEdges().find((edge: GraphEdge) => edge === requiredEdge); 93 | return !!edgeNode; 94 | } 95 | 96 | /** 97 | * 判断某个节点是否是和当前节点连接 98 | * 99 | * @param {GraphVertex} vertex 100 | * @returns {boolean} 101 | * @memberof GraphVertex 102 | */ 103 | hasNeighbor(vertex: GraphVertex): boolean { 104 | const vertexNode = this.getEdges().find( 105 | (edge: GraphEdge) => edge.startVertex === vertex || edge.endVertex === vertex, 106 | ); 107 | 108 | return !!vertexNode; 109 | } 110 | 111 | /** 112 | * 查找当前节点到指定节点的边, 113 | * 如果存在返回该边,否则返回 null 114 | * 115 | * @param {GraphVertex} vertex 116 | * @returns {*} 117 | * @memberof GraphVertex 118 | */ 119 | findEdge(vertex: GraphVertex): any { 120 | const edgeFinder = (edge: GraphEdge) => { 121 | return edge.startVertex === vertex || edge.endVertex === vertex; 122 | }; 123 | 124 | const edge = this.getEdges().find(edgeFinder); 125 | 126 | return edge ? edge : null; 127 | } 128 | 129 | /** 130 | * 返回当前节点 key 值 131 | * 132 | * @returns {*} 133 | * @memberof GraphVertex 134 | */ 135 | getKey(): any { 136 | return this.value; 137 | } 138 | 139 | /** 140 | * 删除当前节点所有的边 141 | * 142 | * @returns {GraphVertex} 143 | * @memberof GraphVertex 144 | */ 145 | deleteAllEdges(): GraphVertex { 146 | this.getEdges().forEach((edge: GraphEdge) => this.deleteEdge(edge)); 147 | return this; 148 | } 149 | 150 | /** 151 | * 重写 toString 方法 152 | * 153 | * @param {(value: any)=>string} [callback] - 支持自定义 toString 格式 154 | * @returns {string} 155 | * @memberof GraphVertex 156 | */ 157 | toString(callback?: (value: any) => string): string { 158 | return callback ? callback(this.value) : `${this.value}`; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.0.24 | MIT License | https://tailwindcss.com 3 | *//* 4 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 5 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 6 | */ 7 | 8 | *, 9 | ::before, 10 | ::after { 11 | box-sizing: border-box; /* 1 */ 12 | border-width: 0; /* 2 */ 13 | border-style: solid; /* 2 */ 14 | border-color: #e5e7eb; /* 2 */ 15 | } 16 | 17 | ::before, 18 | ::after { 19 | --tw-content: ''; 20 | } 21 | 22 | /* 23 | 1. Use a consistent sensible line-height in all browsers. 24 | 2. Prevent adjustments of font size after orientation changes in iOS. 25 | 3. Use a more readable tab size. 26 | 4. Use the user's configured `sans` font-family by default. 27 | */ 28 | 29 | html { 30 | line-height: 1.5; /* 1 */ 31 | -webkit-text-size-adjust: 100%; /* 2 */ 32 | -moz-tab-size: 4; /* 3 */ 33 | -o-tab-size: 4; 34 | tab-size: 4; /* 3 */ 35 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ 36 | } 37 | 38 | /* 39 | 1. Remove the margin in all browsers. 40 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 41 | */ 42 | 43 | body { 44 | margin: 0; /* 1 */ 45 | line-height: inherit; /* 2 */ 46 | } 47 | 48 | /* 49 | 1. Add the correct height in Firefox. 50 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 51 | 3. Ensure horizontal rules are visible by default. 52 | */ 53 | 54 | hr { 55 | height: 0; /* 1 */ 56 | color: inherit; /* 2 */ 57 | border-top-width: 1px; /* 3 */ 58 | } 59 | 60 | /* 61 | Add the correct text decoration in Chrome, Edge, and Safari. 62 | */ 63 | 64 | abbr:where([title]) { 65 | -webkit-text-decoration: underline dotted; 66 | text-decoration: underline dotted; 67 | } 68 | 69 | /* 70 | Remove the default font size and weight for headings. 71 | */ 72 | 73 | h1, 74 | h2, 75 | h3, 76 | h4, 77 | h5, 78 | h6 { 79 | font-size: inherit; 80 | font-weight: inherit; 81 | } 82 | 83 | /* 84 | Reset links to optimize for opt-in styling instead of opt-out. 85 | */ 86 | 87 | a { 88 | color: inherit; 89 | text-decoration: inherit; 90 | } 91 | 92 | /* 93 | Add the correct font weight in Edge and Safari. 94 | */ 95 | 96 | b, 97 | strong { 98 | font-weight: bolder; 99 | } 100 | 101 | /* 102 | 1. Use the user's configured `mono` font family by default. 103 | 2. Correct the odd `em` font sizing in all browsers. 104 | */ 105 | 106 | code, 107 | kbd, 108 | samp, 109 | pre { 110 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ 111 | font-size: 1em; /* 2 */ 112 | } 113 | 114 | /* 115 | Add the correct font size in all browsers. 116 | */ 117 | 118 | small { 119 | font-size: 80%; 120 | } 121 | 122 | /* 123 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 124 | */ 125 | 126 | sub, 127 | sup { 128 | font-size: 75%; 129 | line-height: 0; 130 | position: relative; 131 | vertical-align: baseline; 132 | } 133 | 134 | sub { 135 | bottom: -0.25em; 136 | } 137 | 138 | sup { 139 | top: -0.5em; 140 | } 141 | 142 | /* 143 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 144 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 145 | 3. Remove gaps between table borders by default. 146 | */ 147 | 148 | table { 149 | text-indent: 0; /* 1 */ 150 | border-color: inherit; /* 2 */ 151 | border-collapse: collapse; /* 3 */ 152 | } 153 | 154 | /* 155 | 1. Change the font styles in all browsers. 156 | 2. Remove the margin in Firefox and Safari. 157 | 3. Remove default padding in all browsers. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: inherit; /* 1 */ 168 | color: inherit; /* 1 */ 169 | margin: 0; /* 2 */ 170 | padding: 0; /* 3 */ 171 | } 172 | 173 | /* 174 | Remove the inheritance of text transform in Edge and Firefox. 175 | */ 176 | 177 | button, 178 | select { 179 | text-transform: none; 180 | } 181 | 182 | /* 183 | 1. Correct the inability to style clickable types in iOS and Safari. 184 | 2. Remove default button styles. 185 | */ 186 | 187 | button, 188 | [type='button'], 189 | [type='reset'], 190 | [type='submit'] { 191 | -webkit-appearance: button; /* 1 */ 192 | background-color: transparent; /* 2 */ 193 | background-image: none; /* 2 */ 194 | } 195 | 196 | /* 197 | Use the modern Firefox focus style for all focusable elements. 198 | */ 199 | 200 | :-moz-focusring { 201 | outline: auto; 202 | } 203 | 204 | /* 205 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 206 | */ 207 | 208 | :-moz-ui-invalid { 209 | box-shadow: none; 210 | } 211 | 212 | /* 213 | Add the correct vertical alignment in Chrome and Firefox. 214 | */ 215 | 216 | progress { 217 | vertical-align: baseline; 218 | } 219 | 220 | /* 221 | Correct the cursor style of increment and decrement buttons in Safari. 222 | */ 223 | 224 | ::-webkit-inner-spin-button, 225 | ::-webkit-outer-spin-button { 226 | height: auto; 227 | } 228 | 229 | /* 230 | 1. Correct the odd appearance in Chrome and Safari. 231 | 2. Correct the outline style in Safari. 232 | */ 233 | 234 | [type='search'] { 235 | -webkit-appearance: textfield; /* 1 */ 236 | outline-offset: -2px; /* 2 */ 237 | } 238 | 239 | /* 240 | Remove the inner padding in Chrome and Safari on macOS. 241 | */ 242 | 243 | ::-webkit-search-decoration { 244 | -webkit-appearance: none; 245 | } 246 | 247 | /* 248 | 1. Correct the inability to style clickable types in iOS and Safari. 249 | 2. Change font properties to `inherit` in Safari. 250 | */ 251 | 252 | ::-webkit-file-upload-button { 253 | -webkit-appearance: button; /* 1 */ 254 | font: inherit; /* 2 */ 255 | } 256 | 257 | /* 258 | Add the correct display in Chrome and Safari. 259 | */ 260 | 261 | summary { 262 | display: list-item; 263 | } 264 | 265 | /* 266 | Removes the default spacing and border for appropriate elements. 267 | */ 268 | 269 | blockquote, 270 | dl, 271 | dd, 272 | h1, 273 | h2, 274 | h3, 275 | h4, 276 | h5, 277 | h6, 278 | hr, 279 | figure, 280 | p, 281 | pre { 282 | margin: 0; 283 | } 284 | 285 | fieldset { 286 | margin: 0; 287 | padding: 0; 288 | } 289 | 290 | legend { 291 | padding: 0; 292 | } 293 | 294 | ol, 295 | ul, 296 | menu { 297 | list-style: none; 298 | margin: 0; 299 | padding: 0; 300 | } 301 | 302 | /* 303 | Prevent resizing textareas horizontally by default. 304 | */ 305 | 306 | textarea { 307 | resize: vertical; 308 | } 309 | 310 | /* 311 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 312 | 2. Set the default placeholder color to the user's configured gray 400 color. 313 | */ 314 | 315 | input::-moz-placeholder, textarea::-moz-placeholder { 316 | opacity: 1; /* 1 */ 317 | color: #9ca3af; /* 2 */ 318 | } 319 | 320 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 321 | opacity: 1; /* 1 */ 322 | color: #9ca3af; /* 2 */ 323 | } 324 | 325 | input::placeholder, 326 | textarea::placeholder { 327 | opacity: 1; /* 1 */ 328 | color: #9ca3af; /* 2 */ 329 | } 330 | 331 | /* 332 | Set the default cursor for buttons. 333 | */ 334 | 335 | button, 336 | [role="button"] { 337 | cursor: pointer; 338 | } 339 | 340 | /* 341 | Make sure disabled buttons don't get the pointer cursor. 342 | */ 343 | :disabled { 344 | cursor: default; 345 | } 346 | 347 | /* 348 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 349 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 350 | This can trigger a poorly considered lint error in some tools but is included by design. 351 | */ 352 | 353 | img, 354 | svg, 355 | video, 356 | canvas, 357 | audio, 358 | iframe, 359 | embed, 360 | object { 361 | display: block; /* 1 */ 362 | vertical-align: middle; /* 2 */ 363 | } 364 | 365 | /* 366 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 367 | */ 368 | 369 | img, 370 | video { 371 | max-width: 100%; 372 | height: auto; 373 | } 374 | 375 | /* 376 | Ensure the default browser behavior of the `hidden` attribute. 377 | */ 378 | 379 | [hidden] { 380 | display: none; 381 | } 382 | 383 | *, ::before, ::after { 384 | --tw-translate-x: 0; 385 | --tw-translate-y: 0; 386 | --tw-rotate: 0; 387 | --tw-skew-x: 0; 388 | --tw-skew-y: 0; 389 | --tw-scale-x: 1; 390 | --tw-scale-y: 1; 391 | --tw-pan-x: ; 392 | --tw-pan-y: ; 393 | --tw-pinch-zoom: ; 394 | --tw-scroll-snap-strictness: proximity; 395 | --tw-ordinal: ; 396 | --tw-slashed-zero: ; 397 | --tw-numeric-figure: ; 398 | --tw-numeric-spacing: ; 399 | --tw-numeric-fraction: ; 400 | --tw-ring-inset: ; 401 | --tw-ring-offset-width: 0px; 402 | --tw-ring-offset-color: #fff; 403 | --tw-ring-color: rgb(59 130 246 / 0.5); 404 | --tw-ring-offset-shadow: 0 0 #0000; 405 | --tw-ring-shadow: 0 0 #0000; 406 | --tw-shadow: 0 0 #0000; 407 | --tw-shadow-colored: 0 0 #0000; 408 | --tw-blur: ; 409 | --tw-brightness: ; 410 | --tw-contrast: ; 411 | --tw-grayscale: ; 412 | --tw-hue-rotate: ; 413 | --tw-invert: ; 414 | --tw-saturate: ; 415 | --tw-sepia: ; 416 | --tw-drop-shadow: ; 417 | --tw-backdrop-blur: ; 418 | --tw-backdrop-brightness: ; 419 | --tw-backdrop-contrast: ; 420 | --tw-backdrop-grayscale: ; 421 | --tw-backdrop-hue-rotate: ; 422 | --tw-backdrop-invert: ; 423 | --tw-backdrop-opacity: ; 424 | --tw-backdrop-saturate: ; 425 | --tw-backdrop-sepia: ; 426 | } 427 | .mfn-ml-auto { 428 | margin-left: auto; 429 | } 430 | .mfn-mr-auto { 431 | margin-right: auto; 432 | } 433 | .mfn-ml-6 { 434 | margin-left: 1.5rem; 435 | } 436 | .mfn-ml-2 { 437 | margin-left: 0.5rem; 438 | } 439 | .mfn-mt-4 { 440 | margin-top: 1rem; 441 | } 442 | .mfn-mr-6 { 443 | margin-right: 1.5rem; 444 | } 445 | .mfn-mt-12 { 446 | margin-top: 3rem; 447 | } 448 | .mfn-mb-16 { 449 | margin-bottom: 4rem; 450 | } 451 | .mfn-block { 452 | display: block; 453 | } 454 | .mfn-flex { 455 | display: flex; 456 | } 457 | .mfn-h-2\/3 { 458 | height: 66.666667%; 459 | } 460 | .mfn-h-full { 461 | height: 100%; 462 | } 463 | .mfn-h-32 { 464 | height: 8rem; 465 | } 466 | .mfn-h-8 { 467 | height: 2rem; 468 | } 469 | .mfn-h-3\/4 { 470 | height: 75%; 471 | } 472 | .mfn-w-2\/3 { 473 | width: 66.666667%; 474 | } 475 | .mfn-w-full { 476 | width: 100%; 477 | } 478 | .mfn-w-96 { 479 | width: 24rem; 480 | } 481 | .mfn-w-24 { 482 | width: 6rem; 483 | } 484 | .mfn-w-3\/4 { 485 | width: 75%; 486 | } 487 | .mfn-flex-row { 488 | flex-direction: row; 489 | } 490 | .mfn-flex-col { 491 | flex-direction: column; 492 | } 493 | .mfn-place-content-center { 494 | place-content: center; 495 | } 496 | .mfn-justify-center { 497 | justify-content: center; 498 | } 499 | .mfn-justify-between { 500 | justify-content: space-between; 501 | } 502 | .mfn-justify-around { 503 | justify-content: space-around; 504 | } 505 | .mfn-scroll-auto { 506 | scroll-behavior: auto; 507 | } 508 | .mfn-rounded-lg { 509 | border-radius: 0.5rem; 510 | } 511 | .mfn-bg-slate-100 { 512 | --tw-bg-opacity: 1; 513 | background-color: rgb(241 245 249 / var(--tw-bg-opacity)); 514 | } 515 | .mfn-bg-gray-100 { 516 | --tw-bg-opacity: 1; 517 | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); 518 | } 519 | .mfn-bg-paper-like { 520 | background-image: url('data:image/png;base64,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'); 521 | } 522 | .mfn-pt-8 { 523 | padding-top: 2rem; 524 | } 525 | .mfn-pt-1 { 526 | padding-top: 0.25rem; 527 | } 528 | .mfn-shadow-xl { 529 | --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); 530 | --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); 531 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 532 | } 533 | .mfn-shadow-md { 534 | --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 535 | --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); 536 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 537 | } 538 | .edge-label { 539 | font-weight: bold; 540 | } 541 | .edge-description { 542 | font-size: 10px; 543 | } 544 | -------------------------------------------------------------------------------- /tailwind.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 3 | presets: [], 4 | darkMode: 'media', // or 'class' 5 | prefix: 'mfn-', 6 | theme: { 7 | screens: { 8 | sm: '640px', 9 | md: '768px', 10 | lg: '1024px', 11 | xl: '1280px', 12 | '2xl': '1536px', 13 | }, 14 | colors: ({ colors }) => ({ 15 | inherit: colors.inherit, 16 | current: colors.current, 17 | transparent: colors.transparent, 18 | black: colors.black, 19 | white: colors.white, 20 | slate: colors.slate, 21 | gray: colors.gray, 22 | zinc: colors.zinc, 23 | neutral: colors.neutral, 24 | stone: colors.stone, 25 | red: colors.red, 26 | orange: colors.orange, 27 | amber: colors.amber, 28 | yellow: colors.yellow, 29 | lime: colors.lime, 30 | green: colors.green, 31 | emerald: colors.emerald, 32 | teal: colors.teal, 33 | cyan: colors.cyan, 34 | sky: colors.sky, 35 | blue: colors.blue, 36 | indigo: colors.indigo, 37 | violet: colors.violet, 38 | purple: colors.purple, 39 | fuchsia: colors.fuchsia, 40 | pink: colors.pink, 41 | rose: colors.rose, 42 | }), 43 | columns: { 44 | auto: 'auto', 45 | 1: '1', 46 | 2: '2', 47 | 3: '3', 48 | 4: '4', 49 | 5: '5', 50 | 6: '6', 51 | 7: '7', 52 | 8: '8', 53 | 9: '9', 54 | 10: '10', 55 | 11: '11', 56 | 12: '12', 57 | '3xs': '16rem', 58 | '2xs': '18rem', 59 | xs: '20rem', 60 | sm: '24rem', 61 | md: '28rem', 62 | lg: '32rem', 63 | xl: '36rem', 64 | '2xl': '42rem', 65 | '3xl': '48rem', 66 | '4xl': '56rem', 67 | '5xl': '64rem', 68 | '6xl': '72rem', 69 | '7xl': '80rem', 70 | }, 71 | spacing: { 72 | px: '1px', 73 | 0: '0px', 74 | 0.5: '0.125rem', 75 | 1: '0.25rem', 76 | 1.5: '0.375rem', 77 | 2: '0.5rem', 78 | 2.5: '0.625rem', 79 | 3: '0.75rem', 80 | 3.5: '0.875rem', 81 | 4: '1rem', 82 | 5: '1.25rem', 83 | 6: '1.5rem', 84 | 7: '1.75rem', 85 | 8: '2rem', 86 | 9: '2.25rem', 87 | 10: '2.5rem', 88 | 11: '2.75rem', 89 | 12: '3rem', 90 | 14: '3.5rem', 91 | 16: '4rem', 92 | 20: '5rem', 93 | 24: '6rem', 94 | 28: '7rem', 95 | 32: '8rem', 96 | 36: '9rem', 97 | 40: '10rem', 98 | 44: '11rem', 99 | 48: '12rem', 100 | 52: '13rem', 101 | 56: '14rem', 102 | 60: '15rem', 103 | 64: '16rem', 104 | 72: '18rem', 105 | 80: '20rem', 106 | 96: '24rem', 107 | }, 108 | animation: { 109 | none: 'none', 110 | spin: 'spin 1s linear infinite', 111 | ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite', 112 | pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', 113 | bounce: 'bounce 1s infinite', 114 | }, 115 | aspectRatio: { 116 | auto: 'auto', 117 | square: '1 / 1', 118 | video: '16 / 9', 119 | }, 120 | backdropBlur: ({ theme }) => theme('blur'), 121 | backdropBrightness: ({ theme }) => theme('brightness'), 122 | backdropContrast: ({ theme }) => theme('contrast'), 123 | backdropGrayscale: ({ theme }) => theme('grayscale'), 124 | backdropHueRotate: ({ theme }) => theme('hueRotate'), 125 | backdropInvert: ({ theme }) => theme('invert'), 126 | backdropOpacity: ({ theme }) => theme('opacity'), 127 | backdropSaturate: ({ theme }) => theme('saturate'), 128 | backdropSepia: ({ theme }) => theme('sepia'), 129 | backgroundColor: ({ theme }) => theme('colors'), 130 | backgroundImage: { 131 | none: 'none', 132 | 'paper-like': 133 | "url('data:image/png;base64,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')", 134 | 'gradient-to-t': 'linear-gradient(to top, var(--tw-gradient-stops))', 135 | 'gradient-to-tr': 'linear-gradient(to top right, var(--tw-gradient-stops))', 136 | 'gradient-to-r': 'linear-gradient(to right, var(--tw-gradient-stops))', 137 | 'gradient-to-br': 'linear-gradient(to bottom right, var(--tw-gradient-stops))', 138 | 'gradient-to-b': 'linear-gradient(to bottom, var(--tw-gradient-stops))', 139 | 'gradient-to-bl': 'linear-gradient(to bottom left, var(--tw-gradient-stops))', 140 | 'gradient-to-l': 'linear-gradient(to left, var(--tw-gradient-stops))', 141 | 'gradient-to-tl': 'linear-gradient(to top left, var(--tw-gradient-stops))', 142 | }, 143 | backgroundOpacity: ({ theme }) => theme('opacity'), 144 | backgroundPosition: { 145 | bottom: 'bottom', 146 | center: 'center', 147 | left: 'left', 148 | 'left-bottom': 'left bottom', 149 | 'left-top': 'left top', 150 | right: 'right', 151 | 'right-bottom': 'right bottom', 152 | 'right-top': 'right top', 153 | top: 'top', 154 | }, 155 | backgroundSize: { 156 | auto: 'auto', 157 | cover: 'cover', 158 | contain: 'contain', 159 | }, 160 | blur: { 161 | 0: '0', 162 | none: '0', 163 | sm: '4px', 164 | DEFAULT: '8px', 165 | md: '12px', 166 | lg: '16px', 167 | xl: '24px', 168 | '2xl': '40px', 169 | '3xl': '64px', 170 | }, 171 | brightness: { 172 | 0: '0', 173 | 50: '.5', 174 | 75: '.75', 175 | 90: '.9', 176 | 95: '.95', 177 | 100: '1', 178 | 105: '1.05', 179 | 110: '1.1', 180 | 125: '1.25', 181 | 150: '1.5', 182 | 200: '2', 183 | }, 184 | borderColor: ({ theme }) => ({ 185 | ...theme('colors'), 186 | DEFAULT: theme('colors.gray.200', 'currentColor'), 187 | }), 188 | borderOpacity: ({ theme }) => theme('opacity'), 189 | borderRadius: { 190 | none: '0px', 191 | sm: '0.125rem', 192 | DEFAULT: '0.25rem', 193 | md: '0.375rem', 194 | lg: '0.5rem', 195 | xl: '0.75rem', 196 | '2xl': '1rem', 197 | '3xl': '1.5rem', 198 | full: '9999px', 199 | }, 200 | /* 201 | borderSpacing: ({ theme }) => ({ 202 | ...theme('spacing'), 203 | }), 204 | */ 205 | borderWidth: { 206 | DEFAULT: '1px', 207 | 0: '0px', 208 | 2: '2px', 209 | 4: '4px', 210 | 8: '8px', 211 | }, 212 | boxShadow: { 213 | sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)', 214 | DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', 215 | md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', 216 | lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', 217 | xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', 218 | '2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)', 219 | inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)', 220 | none: 'none', 221 | }, 222 | boxShadowColor: ({ theme }) => theme('colors'), 223 | caretColor: ({ theme }) => theme('colors'), 224 | accentColor: ({ theme }) => ({ 225 | ...theme('colors'), 226 | auto: 'auto', 227 | }), 228 | contrast: { 229 | 0: '0', 230 | 50: '.5', 231 | 75: '.75', 232 | 100: '1', 233 | 125: '1.25', 234 | 150: '1.5', 235 | 200: '2', 236 | }, 237 | container: {}, 238 | content: { 239 | none: 'none', 240 | }, 241 | cursor: { 242 | auto: 'auto', 243 | default: 'default', 244 | pointer: 'pointer', 245 | wait: 'wait', 246 | text: 'text', 247 | move: 'move', 248 | help: 'help', 249 | 'not-allowed': 'not-allowed', 250 | none: 'none', 251 | 'context-menu': 'context-menu', 252 | progress: 'progress', 253 | cell: 'cell', 254 | crosshair: 'crosshair', 255 | 'vertical-text': 'vertical-text', 256 | alias: 'alias', 257 | copy: 'copy', 258 | 'no-drop': 'no-drop', 259 | grab: 'grab', 260 | grabbing: 'grabbing', 261 | 'all-scroll': 'all-scroll', 262 | 'col-resize': 'col-resize', 263 | 'row-resize': 'row-resize', 264 | 'n-resize': 'n-resize', 265 | 'e-resize': 'e-resize', 266 | 's-resize': 's-resize', 267 | 'w-resize': 'w-resize', 268 | 'ne-resize': 'ne-resize', 269 | 'nw-resize': 'nw-resize', 270 | 'se-resize': 'se-resize', 271 | 'sw-resize': 'sw-resize', 272 | 'ew-resize': 'ew-resize', 273 | 'ns-resize': 'ns-resize', 274 | 'nesw-resize': 'nesw-resize', 275 | 'nwse-resize': 'nwse-resize', 276 | 'zoom-in': 'zoom-in', 277 | 'zoom-out': 'zoom-out', 278 | }, 279 | divideColor: ({ theme }) => theme('borderColor'), 280 | divideOpacity: ({ theme }) => theme('borderOpacity'), 281 | divideWidth: ({ theme }) => theme('borderWidth'), 282 | dropShadow: { 283 | sm: '0 1px 1px rgb(0 0 0 / 0.05)', 284 | DEFAULT: ['0 1px 2px rgb(0 0 0 / 0.1)', '0 1px 1px rgb(0 0 0 / 0.06)'], 285 | md: ['0 4px 3px rgb(0 0 0 / 0.07)', '0 2px 2px rgb(0 0 0 / 0.06)'], 286 | lg: ['0 10px 8px rgb(0 0 0 / 0.04)', '0 4px 3px rgb(0 0 0 / 0.1)'], 287 | xl: ['0 20px 13px rgb(0 0 0 / 0.03)', '0 8px 5px rgb(0 0 0 / 0.08)'], 288 | '2xl': '0 25px 25px rgb(0 0 0 / 0.15)', 289 | none: '0 0 #0000', 290 | }, 291 | fill: ({ theme }) => theme('colors'), 292 | grayscale: { 293 | 0: '0', 294 | DEFAULT: '100%', 295 | }, 296 | hueRotate: { 297 | 0: '0deg', 298 | 15: '15deg', 299 | 30: '30deg', 300 | 60: '60deg', 301 | 90: '90deg', 302 | 180: '180deg', 303 | }, 304 | invert: { 305 | 0: '0', 306 | DEFAULT: '100%', 307 | }, 308 | flex: { 309 | 1: '1 1 0%', 310 | auto: '1 1 auto', 311 | initial: '0 1 auto', 312 | none: 'none', 313 | }, 314 | flexBasis: ({ theme }) => ({ 315 | auto: 'auto', 316 | ...theme('spacing'), 317 | '1/2': '50%', 318 | '1/3': '33.333333%', 319 | '2/3': '66.666667%', 320 | '1/4': '25%', 321 | '2/4': '50%', 322 | '3/4': '75%', 323 | '1/5': '20%', 324 | '2/5': '40%', 325 | '3/5': '60%', 326 | '4/5': '80%', 327 | '1/6': '16.666667%', 328 | '2/6': '33.333333%', 329 | '3/6': '50%', 330 | '4/6': '66.666667%', 331 | '5/6': '83.333333%', 332 | '1/12': '8.333333%', 333 | '2/12': '16.666667%', 334 | '3/12': '25%', 335 | '4/12': '33.333333%', 336 | '5/12': '41.666667%', 337 | '6/12': '50%', 338 | '7/12': '58.333333%', 339 | '8/12': '66.666667%', 340 | '9/12': '75%', 341 | '10/12': '83.333333%', 342 | '11/12': '91.666667%', 343 | full: '100%', 344 | }), 345 | flexGrow: { 346 | 0: '0', 347 | DEFAULT: '1', 348 | }, 349 | flexShrink: { 350 | 0: '0', 351 | DEFAULT: '1', 352 | }, 353 | fontFamily: { 354 | sans: [ 355 | 'ui-sans-serif', 356 | 'system-ui', 357 | '-apple-system', 358 | 'BlinkMacSystemFont', 359 | '"Segoe UI"', 360 | 'Roboto', 361 | '"Helvetica Neue"', 362 | 'Arial', 363 | '"Noto Sans"', 364 | 'sans-serif', 365 | '"Apple Color Emoji"', 366 | '"Segoe UI Emoji"', 367 | '"Segoe UI Symbol"', 368 | '"Noto Color Emoji"', 369 | ], 370 | serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'], 371 | mono: [ 372 | 'ui-monospace', 373 | 'SFMono-Regular', 374 | 'Menlo', 375 | 'Monaco', 376 | 'Consolas', 377 | '"Liberation Mono"', 378 | '"Courier New"', 379 | 'monospace', 380 | ], 381 | }, 382 | fontSize: { 383 | xs: ['0.75rem', { lineHeight: '1rem' }], 384 | sm: ['0.875rem', { lineHeight: '1.25rem' }], 385 | base: ['1rem', { lineHeight: '1.5rem' }], 386 | lg: ['1.125rem', { lineHeight: '1.75rem' }], 387 | xl: ['1.25rem', { lineHeight: '1.75rem' }], 388 | '2xl': ['1.5rem', { lineHeight: '2rem' }], 389 | '3xl': ['1.875rem', { lineHeight: '2.25rem' }], 390 | '4xl': ['2.25rem', { lineHeight: '2.5rem' }], 391 | '5xl': ['3rem', { lineHeight: '1' }], 392 | '6xl': ['3.75rem', { lineHeight: '1' }], 393 | '7xl': ['4.5rem', { lineHeight: '1' }], 394 | '8xl': ['6rem', { lineHeight: '1' }], 395 | '9xl': ['8rem', { lineHeight: '1' }], 396 | }, 397 | fontWeight: { 398 | thin: '100', 399 | extralight: '200', 400 | light: '300', 401 | normal: '400', 402 | medium: '500', 403 | semibold: '600', 404 | bold: '700', 405 | extrabold: '800', 406 | black: '900', 407 | }, 408 | gap: ({ theme }) => theme('spacing'), 409 | gradientColorStops: ({ theme }) => theme('colors'), 410 | gridAutoColumns: { 411 | auto: 'auto', 412 | min: 'min-content', 413 | max: 'max-content', 414 | fr: 'minmax(0, 1fr)', 415 | }, 416 | gridAutoRows: { 417 | auto: 'auto', 418 | min: 'min-content', 419 | max: 'max-content', 420 | fr: 'minmax(0, 1fr)', 421 | }, 422 | gridColumn: { 423 | auto: 'auto', 424 | 'span-1': 'span 1 / span 1', 425 | 'span-2': 'span 2 / span 2', 426 | 'span-3': 'span 3 / span 3', 427 | 'span-4': 'span 4 / span 4', 428 | 'span-5': 'span 5 / span 5', 429 | 'span-6': 'span 6 / span 6', 430 | 'span-7': 'span 7 / span 7', 431 | 'span-8': 'span 8 / span 8', 432 | 'span-9': 'span 9 / span 9', 433 | 'span-10': 'span 10 / span 10', 434 | 'span-11': 'span 11 / span 11', 435 | 'span-12': 'span 12 / span 12', 436 | 'span-full': '1 / -1', 437 | }, 438 | gridColumnEnd: { 439 | auto: 'auto', 440 | 1: '1', 441 | 2: '2', 442 | 3: '3', 443 | 4: '4', 444 | 5: '5', 445 | 6: '6', 446 | 7: '7', 447 | 8: '8', 448 | 9: '9', 449 | 10: '10', 450 | 11: '11', 451 | 12: '12', 452 | 13: '13', 453 | }, 454 | gridColumnStart: { 455 | auto: 'auto', 456 | 1: '1', 457 | 2: '2', 458 | 3: '3', 459 | 4: '4', 460 | 5: '5', 461 | 6: '6', 462 | 7: '7', 463 | 8: '8', 464 | 9: '9', 465 | 10: '10', 466 | 11: '11', 467 | 12: '12', 468 | 13: '13', 469 | }, 470 | gridRow: { 471 | auto: 'auto', 472 | 'span-1': 'span 1 / span 1', 473 | 'span-2': 'span 2 / span 2', 474 | 'span-3': 'span 3 / span 3', 475 | 'span-4': 'span 4 / span 4', 476 | 'span-5': 'span 5 / span 5', 477 | 'span-6': 'span 6 / span 6', 478 | 'span-full': '1 / -1', 479 | }, 480 | gridRowStart: { 481 | auto: 'auto', 482 | 1: '1', 483 | 2: '2', 484 | 3: '3', 485 | 4: '4', 486 | 5: '5', 487 | 6: '6', 488 | 7: '7', 489 | }, 490 | gridRowEnd: { 491 | auto: 'auto', 492 | 1: '1', 493 | 2: '2', 494 | 3: '3', 495 | 4: '4', 496 | 5: '5', 497 | 6: '6', 498 | 7: '7', 499 | }, 500 | gridTemplateColumns: { 501 | none: 'none', 502 | 1: 'repeat(1, minmax(0, 1fr))', 503 | 2: 'repeat(2, minmax(0, 1fr))', 504 | 3: 'repeat(3, minmax(0, 1fr))', 505 | 4: 'repeat(4, minmax(0, 1fr))', 506 | 5: 'repeat(5, minmax(0, 1fr))', 507 | 6: 'repeat(6, minmax(0, 1fr))', 508 | 7: 'repeat(7, minmax(0, 1fr))', 509 | 8: 'repeat(8, minmax(0, 1fr))', 510 | 9: 'repeat(9, minmax(0, 1fr))', 511 | 10: 'repeat(10, minmax(0, 1fr))', 512 | 11: 'repeat(11, minmax(0, 1fr))', 513 | 12: 'repeat(12, minmax(0, 1fr))', 514 | }, 515 | gridTemplateRows: { 516 | none: 'none', 517 | 1: 'repeat(1, minmax(0, 1fr))', 518 | 2: 'repeat(2, minmax(0, 1fr))', 519 | 3: 'repeat(3, minmax(0, 1fr))', 520 | 4: 'repeat(4, minmax(0, 1fr))', 521 | 5: 'repeat(5, minmax(0, 1fr))', 522 | 6: 'repeat(6, minmax(0, 1fr))', 523 | }, 524 | height: ({ theme }) => ({ 525 | auto: 'auto', 526 | ...theme('spacing'), 527 | '1/2': '50%', 528 | '1/3': '33.333333%', 529 | '2/3': '66.666667%', 530 | '1/4': '25%', 531 | '2/4': '50%', 532 | '3/4': '75%', 533 | '1/5': '20%', 534 | '2/5': '40%', 535 | '3/5': '60%', 536 | '4/5': '80%', 537 | '1/6': '16.666667%', 538 | '2/6': '33.333333%', 539 | '3/6': '50%', 540 | '4/6': '66.666667%', 541 | '5/6': '83.333333%', 542 | full: '100%', 543 | screen: '100vh', 544 | min: 'min-content', 545 | max: 'max-content', 546 | fit: 'fit-content', 547 | }), 548 | inset: ({ theme }) => ({ 549 | auto: 'auto', 550 | ...theme('spacing'), 551 | '1/2': '50%', 552 | '1/3': '33.333333%', 553 | '2/3': '66.666667%', 554 | '1/4': '25%', 555 | '2/4': '50%', 556 | '3/4': '75%', 557 | full: '100%', 558 | }), 559 | keyframes: { 560 | spin: { 561 | to: { 562 | transform: 'rotate(360deg)', 563 | }, 564 | }, 565 | ping: { 566 | '75%, 100%': { 567 | transform: 'scale(2)', 568 | opacity: '0', 569 | }, 570 | }, 571 | pulse: { 572 | '50%': { 573 | opacity: '.5', 574 | }, 575 | }, 576 | bounce: { 577 | '0%, 100%': { 578 | transform: 'translateY(-25%)', 579 | animationTimingFunction: 'cubic-bezier(0.8,0,1,1)', 580 | }, 581 | '50%': { 582 | transform: 'none', 583 | animationTimingFunction: 'cubic-bezier(0,0,0.2,1)', 584 | }, 585 | }, 586 | }, 587 | letterSpacing: { 588 | tighter: '-0.05em', 589 | tight: '-0.025em', 590 | normal: '0em', 591 | wide: '0.025em', 592 | wider: '0.05em', 593 | widest: '0.1em', 594 | }, 595 | lineHeight: { 596 | none: '1', 597 | tight: '1.25', 598 | snug: '1.375', 599 | normal: '1.5', 600 | relaxed: '1.625', 601 | loose: '2', 602 | 3: '.75rem', 603 | 4: '1rem', 604 | 5: '1.25rem', 605 | 6: '1.5rem', 606 | 7: '1.75rem', 607 | 8: '2rem', 608 | 9: '2.25rem', 609 | 10: '2.5rem', 610 | }, 611 | listStyleType: { 612 | none: 'none', 613 | disc: 'disc', 614 | decimal: 'decimal', 615 | }, 616 | margin: ({ theme }) => ({ 617 | auto: 'auto', 618 | ...theme('spacing'), 619 | }), 620 | maxHeight: ({ theme }) => ({ 621 | ...theme('spacing'), 622 | full: '100%', 623 | screen: '100vh', 624 | min: 'min-content', 625 | max: 'max-content', 626 | fit: 'fit-content', 627 | }), 628 | maxWidth: ({ theme, breakpoints }) => ({ 629 | none: 'none', 630 | 0: '0rem', 631 | xs: '20rem', 632 | sm: '24rem', 633 | md: '28rem', 634 | lg: '32rem', 635 | xl: '36rem', 636 | '2xl': '42rem', 637 | '3xl': '48rem', 638 | '4xl': '56rem', 639 | '5xl': '64rem', 640 | '6xl': '72rem', 641 | '7xl': '80rem', 642 | full: '100%', 643 | min: 'min-content', 644 | max: 'max-content', 645 | fit: 'fit-content', 646 | prose: '65ch', 647 | ...breakpoints(theme('screens')), 648 | }), 649 | minHeight: { 650 | 0: '0px', 651 | full: '100%', 652 | screen: '100vh', 653 | min: 'min-content', 654 | max: 'max-content', 655 | fit: 'fit-content', 656 | }, 657 | minWidth: { 658 | 0: '0px', 659 | full: '100%', 660 | min: 'min-content', 661 | max: 'max-content', 662 | fit: 'fit-content', 663 | }, 664 | objectPosition: { 665 | bottom: 'bottom', 666 | center: 'center', 667 | left: 'left', 668 | 'left-bottom': 'left bottom', 669 | 'left-top': 'left top', 670 | right: 'right', 671 | 'right-bottom': 'right bottom', 672 | 'right-top': 'right top', 673 | top: 'top', 674 | }, 675 | opacity: { 676 | 0: '0', 677 | 5: '0.05', 678 | 10: '0.1', 679 | 20: '0.2', 680 | 25: '0.25', 681 | 30: '0.3', 682 | 40: '0.4', 683 | 50: '0.5', 684 | 60: '0.6', 685 | 70: '0.7', 686 | 75: '0.75', 687 | 80: '0.8', 688 | 90: '0.9', 689 | 95: '0.95', 690 | 100: '1', 691 | }, 692 | order: { 693 | first: '-9999', 694 | last: '9999', 695 | none: '0', 696 | 1: '1', 697 | 2: '2', 698 | 3: '3', 699 | 4: '4', 700 | 5: '5', 701 | 6: '6', 702 | 7: '7', 703 | 8: '8', 704 | 9: '9', 705 | 10: '10', 706 | 11: '11', 707 | 12: '12', 708 | }, 709 | padding: ({ theme }) => theme('spacing'), 710 | placeholderColor: ({ theme }) => theme('colors'), 711 | placeholderOpacity: ({ theme }) => theme('opacity'), 712 | outlineColor: ({ theme }) => theme('colors'), 713 | outlineOffset: { 714 | 0: '0px', 715 | 1: '1px', 716 | 2: '2px', 717 | 4: '4px', 718 | 8: '8px', 719 | }, 720 | outlineWidth: { 721 | 0: '0px', 722 | 1: '1px', 723 | 2: '2px', 724 | 4: '4px', 725 | 8: '8px', 726 | }, 727 | ringColor: ({ theme }) => ({ 728 | DEFAULT: theme('colors.blue.500', '#3b82f6'), 729 | ...theme('colors'), 730 | }), 731 | ringOffsetColor: ({ theme }) => theme('colors'), 732 | ringOffsetWidth: { 733 | 0: '0px', 734 | 1: '1px', 735 | 2: '2px', 736 | 4: '4px', 737 | 8: '8px', 738 | }, 739 | ringOpacity: ({ theme }) => ({ 740 | DEFAULT: '0.5', 741 | ...theme('opacity'), 742 | }), 743 | ringWidth: { 744 | DEFAULT: '3px', 745 | 0: '0px', 746 | 1: '1px', 747 | 2: '2px', 748 | 4: '4px', 749 | 8: '8px', 750 | }, 751 | rotate: { 752 | 0: '0deg', 753 | 1: '1deg', 754 | 2: '2deg', 755 | 3: '3deg', 756 | 6: '6deg', 757 | 12: '12deg', 758 | 45: '45deg', 759 | 90: '90deg', 760 | 180: '180deg', 761 | }, 762 | saturate: { 763 | 0: '0', 764 | 50: '.5', 765 | 100: '1', 766 | 150: '1.5', 767 | 200: '2', 768 | }, 769 | scale: { 770 | 0: '0', 771 | 50: '.5', 772 | 75: '.75', 773 | 90: '.9', 774 | 95: '.95', 775 | 100: '1', 776 | 105: '1.05', 777 | 110: '1.1', 778 | 125: '1.25', 779 | 150: '1.5', 780 | }, 781 | scrollMargin: ({ theme }) => ({ 782 | ...theme('spacing'), 783 | }), 784 | scrollPadding: ({ theme }) => theme('spacing'), 785 | sepia: { 786 | 0: '0', 787 | DEFAULT: '100%', 788 | }, 789 | skew: { 790 | 0: '0deg', 791 | 1: '1deg', 792 | 2: '2deg', 793 | 3: '3deg', 794 | 6: '6deg', 795 | 12: '12deg', 796 | }, 797 | space: ({ theme }) => ({ 798 | ...theme('spacing'), 799 | }), 800 | stroke: ({ theme }) => theme('colors'), 801 | strokeWidth: { 802 | 0: '0', 803 | 1: '1', 804 | 2: '2', 805 | }, 806 | textColor: ({ theme }) => theme('colors'), 807 | textDecorationColor: ({ theme }) => theme('colors'), 808 | textDecorationThickness: { 809 | auto: 'auto', 810 | 'from-font': 'from-font', 811 | 0: '0px', 812 | 1: '1px', 813 | 2: '2px', 814 | 4: '4px', 815 | 8: '8px', 816 | }, 817 | textUnderlineOffset: { 818 | auto: 'auto', 819 | 0: '0px', 820 | 1: '1px', 821 | 2: '2px', 822 | 4: '4px', 823 | 8: '8px', 824 | }, 825 | textIndent: ({ theme }) => ({ 826 | ...theme('spacing'), 827 | }), 828 | textOpacity: ({ theme }) => theme('opacity'), 829 | transformOrigin: { 830 | center: 'center', 831 | top: 'top', 832 | 'top-right': 'top right', 833 | right: 'right', 834 | 'bottom-right': 'bottom right', 835 | bottom: 'bottom', 836 | 'bottom-left': 'bottom left', 837 | left: 'left', 838 | 'top-left': 'top left', 839 | }, 840 | transitionDelay: { 841 | 75: '75ms', 842 | 100: '100ms', 843 | 150: '150ms', 844 | 200: '200ms', 845 | 300: '300ms', 846 | 500: '500ms', 847 | 700: '700ms', 848 | 1000: '1000ms', 849 | }, 850 | transitionDuration: { 851 | DEFAULT: '150ms', 852 | 75: '75ms', 853 | 100: '100ms', 854 | 150: '150ms', 855 | 200: '200ms', 856 | 300: '300ms', 857 | 500: '500ms', 858 | 700: '700ms', 859 | 1000: '1000ms', 860 | }, 861 | transitionProperty: { 862 | none: 'none', 863 | all: 'all', 864 | DEFAULT: 865 | 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter', 866 | colors: 'color, background-color, border-color, text-decoration-color, fill, stroke', 867 | opacity: 'opacity', 868 | shadow: 'box-shadow', 869 | transform: 'transform', 870 | }, 871 | transitionTimingFunction: { 872 | DEFAULT: 'cubic-bezier(0.4, 0, 0.2, 1)', 873 | linear: 'linear', 874 | in: 'cubic-bezier(0.4, 0, 1, 1)', 875 | out: 'cubic-bezier(0, 0, 0.2, 1)', 876 | 'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)', 877 | }, 878 | translate: ({ theme }) => ({ 879 | ...theme('spacing'), 880 | '1/2': '50%', 881 | '1/3': '33.333333%', 882 | '2/3': '66.666667%', 883 | '1/4': '25%', 884 | '2/4': '50%', 885 | '3/4': '75%', 886 | full: '100%', 887 | }), 888 | width: ({ theme }) => ({ 889 | auto: 'auto', 890 | ...theme('spacing'), 891 | '1/2': '50%', 892 | '1/3': '33.333333%', 893 | '2/3': '66.666667%', 894 | '1/4': '25%', 895 | '2/4': '50%', 896 | '3/4': '75%', 897 | '1/5': '20%', 898 | '2/5': '40%', 899 | '3/5': '60%', 900 | '4/5': '80%', 901 | '1/6': '16.666667%', 902 | '2/6': '33.333333%', 903 | '3/6': '50%', 904 | '4/6': '66.666667%', 905 | '5/6': '83.333333%', 906 | '1/12': '8.333333%', 907 | '2/12': '16.666667%', 908 | '3/12': '25%', 909 | '4/12': '33.333333%', 910 | '5/12': '41.666667%', 911 | '6/12': '50%', 912 | '7/12': '58.333333%', 913 | '8/12': '66.666667%', 914 | '9/12': '75%', 915 | '10/12': '83.333333%', 916 | '11/12': '91.666667%', 917 | full: '100%', 918 | screen: '100vw', 919 | min: 'min-content', 920 | max: 'max-content', 921 | fit: 'fit-content', 922 | }), 923 | willChange: { 924 | auto: 'auto', 925 | scroll: 'scroll-position', 926 | contents: 'contents', 927 | transform: 'transform', 928 | }, 929 | zIndex: { 930 | auto: 'auto', 931 | 0: '0', 932 | 10: '10', 933 | 20: '20', 934 | 30: '30', 935 | 40: '40', 936 | 50: '50', 937 | }, 938 | }, 939 | variantOrder: [ 940 | 'first', 941 | 'last', 942 | 'odd', 943 | 'even', 944 | 'visited', 945 | 'checked', 946 | 'empty', 947 | 'read-only', 948 | 'group-hover', 949 | 'group-focus', 950 | 'focus-within', 951 | 'hover', 952 | 'focus', 953 | 'focus-visible', 954 | 'active', 955 | 'disabled', 956 | ], 957 | plugins: [], 958 | }; 959 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "allowSyntheticDefaultImports": true, 5 | "inlineSourceMap": true, 6 | "inlineSources": true, 7 | "jsx": "react", 8 | "module": "ESNext", 9 | "target": "esnext", 10 | "allowJs": true, 11 | "noImplicitAny": false, 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "lib": ["dom", "esnext"] 15 | }, 16 | "include": ["**/*.ts", "**/*.tsx"] 17 | } 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.14.6" 3 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | import postcssImport from 'postcss-import'; 5 | import autoprefixer from 'autoprefixer'; 6 | import tailwindcss from 'tailwindcss'; 7 | 8 | export default defineConfig(({ mode }) => { 9 | return { 10 | plugins: [react()], 11 | build: { 12 | sourcemap: mode === 'development' ? 'inline' : false, 13 | minify: false, 14 | // Use Vite lib mode https://vitejs.dev/guide/build.html#library-mode 15 | commonjsOptions: { 16 | ignoreTryCatch: false, 17 | }, 18 | lib: { 19 | entry: path.resolve(__dirname, './src/mapForNoteIndex.ts'), 20 | formats: ['cjs'], 21 | }, 22 | css: { 23 | postcss: { 24 | plugins: [postcssImport, autoprefixer, tailwindcss], 25 | }, 26 | }, 27 | rollupOptions: { 28 | output: { 29 | // Overwrite default Vite output fileName 30 | entryFileNames: 'main.js', 31 | assetFileNames: 'styles.css', 32 | }, 33 | external: ['obsidian'], 34 | }, 35 | // Use root as the output dir 36 | emptyOutDir: false, 37 | outDir: '.', 38 | }, 39 | }; 40 | }); 41 | --------------------------------------------------------------------------------