├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── components.json ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── docs.svg └── icon.png ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── assets │ ├── pencil.svg │ └── trash.svg ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ ├── main.rs │ └── menu.rs ├── tauri.conf.json └── tauri.macos.conf.json ├── src ├── App.tsx ├── Project.tsx ├── ThemeProvider.tsx ├── components │ ├── Editor │ │ ├── Editor.tsx │ │ ├── Menu.tsx │ │ ├── NodeViews │ │ │ ├── CodeBlockView.tsx │ │ │ ├── Image │ │ │ │ ├── Image.tsx │ │ │ │ └── Options.tsx │ │ │ ├── TableView.tsx │ │ │ └── types.ts │ │ ├── Popover │ │ │ └── Link.tsx │ │ ├── Publish.tsx │ │ ├── TableOfContents.tsx │ │ ├── Titles.tsx │ │ ├── extensions │ │ │ ├── CodeBlockLowlight │ │ │ │ ├── index.ts │ │ │ │ └── lowlightPlugin.ts │ │ │ ├── link-text.ts │ │ │ ├── metadata.ts │ │ │ └── table-of-contents.ts │ │ └── hasLink.ts │ ├── Main │ │ ├── AddProject.tsx │ │ ├── Apps.tsx │ │ ├── EmptyProject.tsx │ │ └── Projects.tsx │ ├── Option.tsx │ ├── Project │ │ ├── App.tsx │ │ ├── FileTree │ │ │ ├── File.tsx │ │ │ ├── Root.tsx │ │ │ ├── SortItem.tsx │ │ │ └── Tree.tsx │ │ ├── Selector.tsx │ │ └── createFile.tsx │ ├── Settings │ │ ├── AppSettings.tsx │ │ └── CommandMenu.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── button.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── select.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── hooks │ └── useEditor.ts ├── lib │ ├── tableShortcut.ts │ └── utils.ts ├── main.tsx ├── store │ ├── appStore.ts │ └── restoreState.ts ├── styles │ └── globals.css ├── utils │ ├── appStore.ts │ ├── getFileMeta.ts │ ├── getImgUrl.ts │ ├── htmlToMarkdown │ │ ├── index.ts │ │ └── turndown │ │ │ ├── index.ts │ │ │ ├── listItem.ts │ │ │ ├── paragraph.ts │ │ │ └── table │ │ │ ├── table.ts │ │ │ ├── tableCell.ts │ │ │ └── tableRow.ts │ ├── markdown.ts │ ├── markdownToHtml │ │ ├── helpers.ts │ │ ├── index.ts │ │ └── marked │ │ │ └── index.ts │ ├── parseMd.ts │ ├── removePath.ts │ └── types.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "master" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | platform: [macos-12, macos-latest, ubuntu-20.04, windows-latest] 16 | runs-on: ${{ matrix.platform }} 17 | 18 | env: 19 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 20 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Install dependencies (ubuntu only) 27 | if: matrix.platform == 'ubuntu-20.04' 28 | # You can remove libayatana-appindicator3-dev if you don't use the system tray feature. 29 | run: | 30 | sudo apt-get update 31 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev 32 | 33 | - name: Rust setup 34 | uses: dtolnay/rust-toolchain@stable 35 | 36 | - name: Rust cache 37 | uses: swatinem/rust-cache@v2 38 | with: 39 | workspaces: "./src-tauri -> target" 40 | 41 | - name: Sync node version and setup cache 42 | uses: pnpm/action-setup@v3 43 | with: 44 | version: 8 45 | - name: Use Node.js ${{ matrix.node-version }} 46 | uses: actions/setup-node@v4 47 | with: 48 | node-version: "lts/*" 49 | cache: "pnpm" 50 | - name: Install dependencies 51 | run: pnpm install 52 | 53 | - name: Install frontend dependencies 54 | run: pnpm install 55 | 56 | - name: Build the app 57 | uses: tauri-apps/tauri-action@v0 58 | 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | with: 62 | tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags. 63 | releaseName: "Marker v__VERSION__" # tauri-action replaces \_\_VERSION\_\_ with the app version. 64 | releaseBody: "Includes new features / bug fixes" 65 | releaseDraft: true 66 | prerelease: false 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 TK 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 |
2 | 3 |

Marker

4 |

An open-source, user-friendly UI for viewing and editing markdown files

5 |
6 | 7 | ## Download 8 | 9 | Navigate to the [release page](https://github.com/tk04/Marker/releases) and select the installer that matches your platform. 10 | 11 | #### Using Hombrew 12 | ```bash 13 | $ brew install --cask tk04/tap/marker 14 | ``` 15 | 16 | #### [AUR](https://aur.archlinux.org/packages/marker-md) for Arch Linux 17 | ##### Using `paru` 18 | ```bash 19 | $ paru -S marker-md 20 | ``` 21 | 22 | ##### Using `yay` 23 | ```bash 24 | $ yay -S marker-md 25 | ``` 26 | 27 | ## Building Locally 28 | 29 | To build Marker locally, clone this repo and run the following commands (make sure to have Rust already installed on your system): 30 | 31 | ```sh 32 | $ pnpm install && npx tauri build 33 | ``` 34 | 35 | ## Contributing 36 | 37 | If you feel that Marker is missing something, feel free to open a PR. Contributions are welcome and highly appreciated. 38 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.mjs", 8 | "css": "./src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + React + TS 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Marker", 3 | "type": "module", 4 | "version": "1.4.1", 5 | "scripts": { 6 | "dev": "vite --port 3000", 7 | "build": "tsc && vite build", 8 | "tauri": "tauri" 9 | }, 10 | "dependencies": { 11 | "@radix-ui/react-alert-dialog": "^1.0.5", 12 | "@radix-ui/react-dialog": "^1.0.5", 13 | "@radix-ui/react-dropdown-menu": "^2.0.6", 14 | "@radix-ui/react-label": "^2.0.2", 15 | "@radix-ui/react-popover": "^1.0.7", 16 | "@radix-ui/react-select": "^2.0.0", 17 | "@radix-ui/react-slider": "^1.1.2", 18 | "@radix-ui/react-slot": "^1.0.2", 19 | "@radix-ui/react-switch": "^1.0.3", 20 | "@radix-ui/react-toast": "^1.1.5", 21 | "@tauri-apps/api": "^1.5.3", 22 | "@tiptap/core": "^2.4.0", 23 | "@tiptap/extension-bubble-menu": "^2.4.0", 24 | "@tiptap/extension-bullet-list": "^2.4.0", 25 | "@tiptap/extension-character-count": "^2.4.0", 26 | "@tiptap/extension-code": "^2.4.0", 27 | "@tiptap/extension-code-block": "^2.4.0", 28 | "@tiptap/extension-document": "^2.4.0", 29 | "@tiptap/extension-heading": "^2.4.0", 30 | "@tiptap/extension-image": "^2.4.0", 31 | "@tiptap/extension-link": "^2.4.0", 32 | "@tiptap/extension-list-item": "^2.4.0", 33 | "@tiptap/extension-ordered-list": "^2.4.0", 34 | "@tiptap/extension-paragraph": "^2.4.0", 35 | "@tiptap/extension-placeholder": "^2.4.0", 36 | "@tiptap/extension-table": "^2.4.0", 37 | "@tiptap/extension-table-cell": "^2.4.0", 38 | "@tiptap/extension-table-header": "^2.4.0", 39 | "@tiptap/extension-table-row": "^2.4.0", 40 | "@tiptap/extension-task-item": "^2.4.0", 41 | "@tiptap/extension-task-list": "^2.4.0", 42 | "@tiptap/pm": "^2.4.0", 43 | "@tiptap/react": "^2.4.0", 44 | "@tiptap/starter-kit": "^2.4.0", 45 | "@types/react": "^18.2.59", 46 | "@types/react-dom": "^18.2.19", 47 | "antd": "^5.14.2", 48 | "class-variance-authority": "^0.7.0", 49 | "clsx": "^2.1.0", 50 | "cmdk": "^1.0.0", 51 | "hast-util-to-html": "^9.0.0", 52 | "he": "^1.2.0", 53 | "highlight.js": "^11.9.0", 54 | "lowlight": "^3.1.0", 55 | "lucide-react": "^0.341.0", 56 | "marked": "^12.0.0", 57 | "react": "^18.2.0", 58 | "react-color": "^2.19.3", 59 | "react-dom": "^18.2.0", 60 | "react-icons": "^5.0.1", 61 | "react-router-dom": "^6.22.2", 62 | "tailwind-merge": "^2.2.1", 63 | "tailwindcss-animate": "^1.0.7", 64 | "tauri-plugin-context-menu": "^0.7.0", 65 | "tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1", 66 | "turndown": "^7.1.2", 67 | "typescript": "^5.3.3", 68 | "uuid": "^9.0.1", 69 | "yaml": "^2.4.0", 70 | "zustand": "^4.5.2" 71 | }, 72 | "devDependencies": { 73 | "@tauri-apps/cli": "^1.5.10", 74 | "@types/he": "^1.2.3", 75 | "@types/node": "^20.11.24", 76 | "@types/react-color": "^3.0.12", 77 | "@types/turndown": "^5.0.4", 78 | "@types/uuid": "^9.0.8", 79 | "@vitejs/plugin-react": "^4.2.1", 80 | "autoprefixer": "^10.4.18", 81 | "postcss": "^8.4.35", 82 | "tailwindcss": "^3.4.1", 83 | "vite": "^5.1.5" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "tailwindcss/nesting": {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /public/docs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/public/icon.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "marker" 3 | version = "1.4.1" 4 | description = "A Secure Visual Markdown Editor" 5 | authors = ["tk"] 6 | license = "" 7 | repository = "" 8 | default-run = "marker" 9 | edition = "2021" 10 | rust-version = "1.60" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.5.1", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "1.6.0", features = [ "window-create", "native-tls-vendored", "updater", "window-unmaximize", "window-maximize", "window-start-dragging", "shell-open", "protocol-asset", "shell-execute", "dialog-all", "fs-all", "path-all"] } 21 | tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } 22 | tauri-plugin-context-menu = "0.7.1" 23 | 24 | [target.'cfg(target_os = "macos")'.dependencies] 25 | objc = "0.2.7" 26 | cocoa = "0.25.0" 27 | 28 | [features] 29 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. 30 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. 31 | # DO NOT REMOVE!! 32 | custom-protocol = [ "tauri/custom-protocol" ] 33 | -------------------------------------------------------------------------------- /src-tauri/assets/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src-tauri/assets/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tk04/Marker/b878afcb2c8895702cacce2613f9081d3682ddc7/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | use serde::{Deserialize, Serialize}; 4 | use std::fs::metadata; 5 | use std::time::SystemTime; 6 | use tauri::{Manager, Window}; 7 | use tauri_plugin_context_menu; 8 | mod menu; 9 | 10 | #[derive(Serialize, Deserialize, Debug)] 11 | pub struct FileMeta { 12 | pub created_at: Option, 13 | pub updated_at: Option, 14 | } 15 | 16 | pub enum ToolbarThickness { 17 | Thick, 18 | Medium, 19 | Thin, 20 | } 21 | pub trait WindowExt { 22 | #[cfg(target_os = "macos")] 23 | fn set_transparent_titlebar(&self); 24 | } 25 | #[cfg(target_os = "macos")] 26 | impl WindowExt for Window { 27 | fn set_transparent_titlebar(&self) { 28 | use cocoa::appkit::{NSWindow, NSWindowTitleVisibility}; 29 | use objc::{class, msg_send, sel, sel_impl}; 30 | 31 | unsafe { 32 | let id = self.ns_window().unwrap() as cocoa::base::id; 33 | 34 | id.setTitlebarAppearsTransparent_(cocoa::base::YES); 35 | let thickness = ToolbarThickness::Medium; 36 | match thickness { 37 | ToolbarThickness::Thick => { 38 | self.set_title("").ok(); 39 | id.setToolbar_(msg_send![class!(NSToolbar), new]); 40 | } 41 | ToolbarThickness::Medium => { 42 | id.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); 43 | id.setToolbar_(msg_send![class!(NSToolbar), new]); 44 | } 45 | ToolbarThickness::Thin => { 46 | id.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | #[tauri::command] 53 | fn get_file_metadata(filepath: String) -> FileMeta { 54 | if let Ok(meta) = metadata(filepath) { 55 | return FileMeta { 56 | updated_at: meta.modified().ok(), 57 | created_at: meta.created().ok(), 58 | }; 59 | } 60 | FileMeta { 61 | created_at: None, 62 | updated_at: None, 63 | } 64 | } 65 | 66 | fn main() { 67 | let context = tauri::generate_context!(); 68 | tauri::Builder::default() 69 | .setup(|app| { 70 | #[cfg(target_os = "macos")] 71 | { 72 | let win = app.get_window("main").unwrap(); 73 | win.set_transparent_titlebar(); 74 | } 75 | Ok(()) 76 | }) 77 | .plugin(tauri_plugin_store::Builder::default().build()) 78 | .plugin(tauri_plugin_context_menu::init()) 79 | .invoke_handler(tauri::generate_handler![get_file_metadata]) 80 | .menu(menu::os_default(&context.package_info().name)) 81 | .run(context) 82 | .expect("error while running tauri application"); 83 | } 84 | -------------------------------------------------------------------------------- /src-tauri/src/menu.rs: -------------------------------------------------------------------------------- 1 | use tauri::{AboutMetadata, CustomMenuItem, Menu, MenuItem, Submenu}; 2 | pub fn os_default(#[allow(unused)] app_name: &str) -> Menu { 3 | let mut menu = Menu::new(); 4 | let settings = CustomMenuItem::new("settings", "Settings").accelerator("CmdOrCtrl+,"); 5 | #[cfg(target_os = "macos")] 6 | { 7 | menu = menu.add_submenu(Submenu::new( 8 | app_name, 9 | Menu::new() 10 | .add_native_item(MenuItem::About( 11 | app_name.to_string(), 12 | AboutMetadata::default(), 13 | )) 14 | .add_native_item(MenuItem::Separator) 15 | .add_item(settings) 16 | .add_native_item(MenuItem::Separator) 17 | .add_native_item(MenuItem::Services) 18 | .add_native_item(MenuItem::Separator) 19 | .add_native_item(MenuItem::Hide) 20 | .add_native_item(MenuItem::HideOthers) 21 | .add_native_item(MenuItem::ShowAll) 22 | .add_native_item(MenuItem::Separator) 23 | .add_native_item(MenuItem::Quit), 24 | )); 25 | } 26 | 27 | let mut file_menu = Menu::new(); 28 | file_menu = file_menu.add_native_item(MenuItem::CloseWindow); 29 | #[cfg(not(target_os = "macos"))] 30 | { 31 | file_menu = file_menu.add_item(settings).add_native_item(MenuItem::Quit); 32 | } 33 | menu = menu.add_submenu(Submenu::new("File", file_menu)); 34 | 35 | #[cfg(not(target_os = "linux"))] 36 | let mut edit_menu = Menu::new(); 37 | #[cfg(target_os = "macos")] 38 | { 39 | edit_menu = edit_menu.add_native_item(MenuItem::Undo); 40 | edit_menu = edit_menu.add_native_item(MenuItem::Redo); 41 | edit_menu = edit_menu.add_native_item(MenuItem::Separator); 42 | } 43 | #[cfg(not(target_os = "linux"))] 44 | { 45 | edit_menu = edit_menu.add_native_item(MenuItem::Cut); 46 | edit_menu = edit_menu.add_native_item(MenuItem::Copy); 47 | edit_menu = edit_menu.add_native_item(MenuItem::Paste); 48 | } 49 | #[cfg(target_os = "macos")] 50 | { 51 | edit_menu = edit_menu.add_native_item(MenuItem::SelectAll); 52 | } 53 | #[cfg(not(target_os = "linux"))] 54 | { 55 | menu = menu.add_submenu(Submenu::new("Edit", edit_menu)); 56 | } 57 | #[cfg(target_os = "macos")] 58 | { 59 | menu = menu.add_submenu(Submenu::new( 60 | "View", 61 | Menu::new().add_native_item(MenuItem::EnterFullScreen), 62 | )); 63 | } 64 | 65 | let mut window_menu = Menu::new(); 66 | window_menu = window_menu.add_native_item(MenuItem::Minimize); 67 | #[cfg(target_os = "macos")] 68 | { 69 | window_menu = window_menu.add_native_item(MenuItem::Zoom); 70 | window_menu = window_menu.add_native_item(MenuItem::Separator); 71 | } 72 | window_menu = window_menu.add_native_item(MenuItem::CloseWindow); 73 | menu = menu.add_submenu(Submenu::new("Window", window_menu)); 74 | 75 | menu 76 | } 77 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json", 3 | "build": { 4 | "beforeBuildCommand": "pnpm build", 5 | "beforeDevCommand": "pnpm dev", 6 | "devPath": "http://localhost:3000", 7 | "distDir": "../dist" 8 | }, 9 | "package": { 10 | "productName": "Marker", 11 | "version": "1.4.1" 12 | }, 13 | "tauri": { 14 | "allowlist": { 15 | "all": false, 16 | "shell": { 17 | "open": "^(?:mailto:|https://|tel:).*\\?open=true$", 18 | "execute": true, 19 | "scope": [ 20 | { 21 | "name": "git", 22 | "cmd": "git", 23 | "args": true 24 | } 25 | ] 26 | }, 27 | "protocol": { 28 | "asset": true, 29 | "assetScope": ["$HOME/**/*"] 30 | }, 31 | "dialog": { 32 | "all": true 33 | }, 34 | "path": { 35 | "all": true 36 | }, 37 | "fs": { 38 | "all": true, 39 | "scope": ["$HOME/**/*", "$APPDATA/**/*"] 40 | }, 41 | "window": { 42 | "all": false, 43 | "maximize": true, 44 | "unmaximize": true, 45 | "startDragging": true, 46 | "create": true 47 | } 48 | }, 49 | "bundle": { 50 | "active": true, 51 | "category": "DeveloperTool", 52 | "copyright": "", 53 | "deb": { 54 | "depends": [] 55 | }, 56 | "externalBin": [], 57 | "icon": [ 58 | "icons/32x32.png", 59 | "icons/128x128.png", 60 | "icons/128x128@2x.png", 61 | "icons/icon.icns", 62 | "icons/icon.ico" 63 | ], 64 | "identifier": "com.marker.app", 65 | "longDescription": "", 66 | "macOS": { 67 | "entitlements": null, 68 | "exceptionDomain": "", 69 | "frameworks": [], 70 | "providerShortName": null, 71 | "signingIdentity": null 72 | }, 73 | "resources": ["assets/"], 74 | "shortDescription": "", 75 | "targets": "all", 76 | "windows": { 77 | "certificateThumbprint": null, 78 | "digestAlgorithm": "sha256", 79 | "timestampUrl": "" 80 | } 81 | }, 82 | "security": { 83 | "csp": null 84 | }, 85 | "updater": { 86 | "active": true, 87 | "endpoints": [ 88 | "https://github.com/tk04/Marker/releases/download/master/latest.json" 89 | ], 90 | "dialog": true, 91 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc3MTU3NzVGQ0U5NzQyMDEKUldRQlFwZk9YM2NWZHg1TUZLVlI1Z3VMOStTY1lSeENJb2R1cnpnQjgzZk5TS0c3SFFha2VPWVoK" 92 | }, 93 | "windows": [ 94 | { 95 | "fullscreen": false, 96 | "height": 600, 97 | "resizable": true, 98 | "center": true, 99 | "title": "Marker", 100 | "hiddenTitle": false, 101 | "titleBarStyle": "Transparent", 102 | "width": 800, 103 | "minWidth": 800, 104 | "minHeight": 400, 105 | "fileDropEnabled": false 106 | } 107 | ] 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src-tauri/tauri.macos.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "tauri": { 3 | "windows": [ 4 | { 5 | "fullscreen": false, 6 | "height": 600, 7 | "resizable": true, 8 | "center": true, 9 | "title": "Marker", 10 | "hiddenTitle": false, 11 | "titleBarStyle": "Overlay", 12 | "width": 800, 13 | "minWidth": 800, 14 | "minHeight": 400, 15 | "fileDropEnabled": false 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import Apps from "./components/Main/Apps"; 2 | 3 | function App() { 4 | return ( 5 |
6 |
7 |

Projects

8 | 9 |
10 |
11 | ); 12 | } 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /src/Project.tsx: -------------------------------------------------------------------------------- 1 | import { useLoaderData } from "react-router-dom"; 2 | import { Dir } from "./utils/types"; 3 | import App from "@/components/Project/App"; 4 | 5 | function Project() { 6 | const { project }: { project: Dir } = useLoaderData() as any; 7 | return ; 8 | } 9 | 10 | export default Project; 11 | -------------------------------------------------------------------------------- /src/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react"; 2 | 3 | export type Theme = "dark" | "light" | "system"; 4 | 5 | type ThemeProviderProps = { 6 | children: React.ReactNode; 7 | defaultTheme?: Theme; 8 | storageKey?: string; 9 | }; 10 | 11 | type ThemeProviderState = { 12 | theme: Theme; 13 | setTheme: (theme: Theme) => void; 14 | }; 15 | 16 | const initialState: ThemeProviderState = { 17 | theme: "system", 18 | setTheme: () => null, 19 | }; 20 | 21 | const ThemeProviderContext = createContext(initialState); 22 | 23 | export function ThemeProvider({ 24 | children, 25 | defaultTheme = "system", 26 | storageKey = "ui-theme", 27 | ...props 28 | }: ThemeProviderProps) { 29 | const [theme, setTheme] = useState( 30 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, 31 | ); 32 | function changeTheme() { 33 | const root = window.document.documentElement; 34 | 35 | root.classList.remove("light", "dark"); 36 | 37 | if (theme === "system") { 38 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") 39 | .matches 40 | ? "dark" 41 | : "light"; 42 | 43 | root.classList.add(systemTheme); 44 | return; 45 | } 46 | 47 | root.classList.add(theme); 48 | } 49 | 50 | useEffect(() => { 51 | changeTheme(); 52 | 53 | window 54 | .matchMedia("(prefers-color-scheme: dark)") 55 | .addEventListener("change", changeTheme); 56 | 57 | return () => { 58 | window 59 | .matchMedia("(prefers-color-scheme: dark)") 60 | .removeEventListener("change", changeTheme); 61 | }; 62 | }, [theme]); 63 | 64 | const value = { 65 | theme, 66 | setTheme: (theme: Theme) => { 67 | localStorage.setItem(storageKey, theme); 68 | setTheme(theme); 69 | }, 70 | }; 71 | 72 | return ( 73 | 74 | {children} 75 | 76 | ); 77 | } 78 | 79 | export const useTheme = () => { 80 | const context = useContext(ThemeProviderContext); 81 | 82 | if (context === undefined) 83 | throw new Error("useTheme must be used within a ThemeProvider"); 84 | 85 | return context; 86 | }; 87 | -------------------------------------------------------------------------------- /src/components/Editor/Editor.tsx: -------------------------------------------------------------------------------- 1 | import yaml from "yaml"; 2 | 3 | import { writeTextFile, type FileEntry } from "@tauri-apps/api/fs"; 4 | import { EditorContent, isMacOS } from "@tiptap/react"; 5 | import Titles from "./Titles"; 6 | import { useEffect, useRef, useState } from "react"; 7 | import Menu from "./Menu"; 8 | import LinkPopover from "./Popover/Link"; 9 | import useTextEditor from "@/hooks/useEditor.ts"; 10 | import Publish from "./Publish"; 11 | import { htmlToMarkdown, readMarkdownFile } from "@/utils/markdown"; 12 | import { Node } from "@tiptap/pm/model"; 13 | import TableOfContents from "./TableOfContents"; 14 | import useStore from "@/store/appStore"; 15 | import type { Editor as EditorType } from "@tiptap/core"; 16 | 17 | export type TOC = { node: Node; level: number }[]; 18 | interface props { 19 | file: FileEntry; 20 | projectPath: string; 21 | collapse: boolean; 22 | } 23 | const Editor: React.FC = ({ projectPath, file, collapse }) => { 24 | const settings = useStore((s) => s.settings); 25 | const [metadata, setMetadata] = useState<{ [key: string]: any } | null>(null); 26 | const editor = useTextEditor({ 27 | content: "", 28 | onUpdate, 29 | filePath: file.path, 30 | projectDir: projectPath, 31 | loadFile: loadFile, 32 | }); 33 | 34 | const saveFileTimeoutRef = useRef(null); 35 | function clearSaveFileTimeout() { 36 | if (saveFileTimeoutRef.current != null) { 37 | clearTimeout(saveFileTimeoutRef.current); 38 | } 39 | } 40 | 41 | function onUpdate() { 42 | clearSaveFileTimeout(); 43 | saveFileTimeoutRef.current = setTimeout(saveFile, 200); 44 | } 45 | 46 | async function saveFile() { 47 | try { 48 | let mdContent = "---\n" + yaml.stringify(metadata) + "---\n"; 49 | mdContent += htmlToMarkdown(editor?.getHTML() || ""); 50 | await writeTextFile(file.path, mdContent); 51 | } catch { 52 | alert( 53 | "An error occurred when trying to save this file. Let us know by opening an issue at https://github.com/tk04/marker", 54 | ); 55 | } 56 | } 57 | useEffect(() => { 58 | if (metadata != null) { 59 | onUpdate(); 60 | } 61 | }, [metadata]); 62 | 63 | async function loadFile(editor: EditorType | null) { 64 | if (!editor) return; 65 | const { metadata, html } = await readMarkdownFile(file.path); 66 | 67 | editor.commands.updateMetadata({ 68 | filePath: file.path, 69 | }); 70 | editor.commands.setContent(html); 71 | setMetadata(metadata); 72 | 73 | editor.commands.focus("start"); 74 | document.querySelector(".editor")?.scroll({ top: 0 }); 75 | 76 | // reset history (see https://github.com/ueberdosis/tiptap/issues/491#issuecomment-1261056162) 77 | // @ts-ignore 78 | if (editor.state.history$) { 79 | // @ts-ignore 80 | editor.state.history$.prevRanges = null; 81 | // @ts-ignore 82 | editor.state.history$.done.eventCount = 0; 83 | } 84 | } 85 | useEffect(() => { 86 | clearSaveFileTimeout(); 87 | let timeout = setTimeout(loadFile.bind(null, editor), 0); 88 | return () => { 89 | clearTimeout(timeout); 90 | }; 91 | }, [file.path]); 92 | 93 | if (!editor) return; 94 | 95 | return ( 96 |
97 | 98 | 99 | 100 |

101 | {editor.storage.characterCount.words()} words 102 |

103 |
107 |
108 |
109 |

{file.path.replace(projectPath + "/", "")}

110 |
111 |
112 |
113 |
114 | loadFile(editor)} 118 | /> 119 |
120 |
121 | {settings.showTOC && ( 122 |
123 | 124 |
125 | )} 126 |
130 |
131 |
132 | 133 | 134 | 138 |
139 |
140 |
141 |
142 | ); 143 | }; 144 | 145 | export default Editor; 146 | -------------------------------------------------------------------------------- /src/components/Editor/Menu.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@tiptap/react"; 2 | import { BubbleMenu } from "@tiptap/react"; 3 | 4 | import type { ReactElement } from "react"; 5 | import { 6 | AiOutlineBold, 7 | AiOutlineItalic, 8 | AiOutlineStrikethrough, 9 | } from "react-icons/ai"; 10 | import { HiListBullet } from "react-icons/hi2"; 11 | import { BsListOl } from "react-icons/bs"; 12 | import { HiOutlineCode } from "react-icons/hi"; 13 | import { TbBlockquote } from "react-icons/tb"; 14 | import { RxDividerHorizontal } from "react-icons/rx"; 15 | 16 | const MenuItem = ({ 17 | icon, 18 | onClick, 19 | isActive = false, 20 | }: { 21 | icon: ReactElement; 22 | isActive?: boolean; 23 | onClick?: () => void; 24 | }) => { 25 | return ( 26 | 34 | ); 35 | }; 36 | interface props { 37 | editor: Editor; 38 | } 39 | const Menu: React.FC = ({ editor }) => { 40 | let menu = editor.chain(); 41 | return ( 42 | 48 | } 50 | isActive={editor?.isActive("bold")} 51 | onClick={() => menu?.focus().toggleBold().run()} 52 | /> 53 | 54 | } 56 | isActive={editor?.isActive("italic")} 57 | onClick={() => menu?.focus().toggleItalic().run()} 58 | /> 59 | 60 | } 62 | isActive={editor?.isActive("strike")} 63 | onClick={() => menu?.focus().toggleStrike().run()} 64 | /> 65 | 66 | } 68 | isActive={editor?.isActive("codeBlock")} 69 | onClick={() => menu?.focus().toggleCodeBlock().run()} 70 | /> 71 | 72 | } 74 | isActive={editor?.isActive("bulletList")} 75 | onClick={() => menu?.focus().toggleBulletList().run()} 76 | /> 77 | 78 | } 80 | isActive={editor?.isActive("orderedList")} 81 | onClick={() => menu?.focus().toggleOrderedList().run()} 82 | /> 83 | 84 | } 86 | isActive={editor?.isActive("blockquote")} 87 | onClick={() => menu?.focus().toggleBlockquote().run()} 88 | /> 89 | 90 | } 92 | isActive={editor?.isActive("horizontalRule")} 93 | onClick={() => menu?.focus().setHorizontalRule().run()} 94 | /> 95 | 96 | ); 97 | }; 98 | export default Menu; 99 | -------------------------------------------------------------------------------- /src/components/Editor/NodeViews/CodeBlockView.tsx: -------------------------------------------------------------------------------- 1 | import { NodeViewContent, NodeViewWrapper } from "@tiptap/react"; 2 | import { FaSort } from "react-icons/fa"; 3 | import props from "./types"; 4 | 5 | import { common } from "lowlight"; 6 | 7 | import { 8 | Command, 9 | CommandEmpty, 10 | CommandInput, 11 | CommandItem, 12 | } from "@/components/ui/command"; 13 | 14 | import { 15 | Popover, 16 | PopoverContent, 17 | PopoverTrigger, 18 | } from "@/components/ui/popover"; 19 | import { useState } from "react"; 20 | import { CommandList } from "cmdk"; 21 | 22 | const langs = Object.keys(common); 23 | const CodeBlockView: React.FC = ({ node, updateAttributes }) => { 24 | const [open, setOpen] = useState(false); 25 | return ( 26 | 27 |
28 | setOpen(val)}> 29 | setOpen(true)} 33 | > 34 | 42 | 43 | 44 | 45 | 49 | No language found. 50 | 51 | {langs.map((lang) => ( 52 | { 56 | updateAttributes({ language: currentValue }); 57 | setOpen(false); 58 | }} 59 | > 60 | {lang} 61 | 62 | ))} 63 | 64 | 65 | 66 | 67 |
68 |
69 |         
70 |       
71 |
72 | ); 73 | }; 74 | export default CodeBlockView; 75 | -------------------------------------------------------------------------------- /src/components/Editor/NodeViews/Image/Image.tsx: -------------------------------------------------------------------------------- 1 | import { NodeViewWrapper } from "@tiptap/react"; 2 | import { BiDotsVerticalRounded } from "react-icons/bi"; 3 | import { useEffect, useRef, useState } from "react"; 4 | import { 5 | Popover, 6 | PopoverContent, 7 | PopoverTrigger, 8 | } from "@/components/ui/popover"; 9 | import Options from "./Options"; 10 | import type props from "../types"; 11 | import getImgUrl from "@/utils/getImgUrl"; 12 | 13 | var imageExtensions = [ 14 | "svg", 15 | "jpg", 16 | "jpeg", 17 | "png", 18 | "gif", 19 | "bmp", 20 | "webp", 21 | "heif", 22 | ]; 23 | const ImageView: React.FC = ({ 24 | editor, 25 | node, 26 | selected, 27 | updateAttributes, 28 | }) => { 29 | const ref = useRef(null); 30 | const [src, setSrc] = useState(node.attrs?.src); 31 | const [open, setOpen] = useState(false); 32 | const [isImg, setIsImage] = useState(true); 33 | async function updateAssetSrc() { 34 | const src = await getImgUrl( 35 | editor.storage.metadata.filePath, 36 | node.attrs.src, 37 | ); 38 | setSrc(src); 39 | } 40 | useEffect(() => { 41 | if ( 42 | !node?.attrs?.src?.startsWith("asset://") && 43 | !node?.attrs?.src?.startsWith("http") 44 | ) { 45 | updateAssetSrc(); 46 | } 47 | 48 | const ext: string = node.attrs.src.split("?")[0].split(".").pop(); 49 | setIsImage(imageExtensions.includes(ext.toLowerCase())); 50 | }, [node.attrs.src]); 51 | 52 | const updateAlt = (alt: string) => { 53 | updateAttributes({ 54 | alt, 55 | }); 56 | }; 57 | return ( 58 | 59 |
67 | {isImg ? ( 68 | 69 | ) : ( 70 | 73 | )} 74 | {selected && isImg && ( 75 | setOpen(val)}> 76 | 77 | 78 | 79 | 80 | setOpen(false)} 84 | updateAttributes={updateAttributes} 85 | /> 86 | 87 | 88 | )} 89 |
90 |
91 | ); 92 | }; 93 | export default ImageView; 94 | -------------------------------------------------------------------------------- /src/components/Editor/NodeViews/Image/Options.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | interface props { 4 | alt: string; 5 | updateAlt: (alt: string) => void; 6 | closeModal: () => void; 7 | updateAttributes: (attrs: object) => void; 8 | } 9 | const Options: React.FC = ({ alt, updateAlt, closeModal }) => { 10 | let inputRef = useRef(null); 11 | return ( 12 |
13 |
{ 16 | e.preventDefault(); 17 | 18 | updateAlt(inputRef.current?.value || alt); 19 | closeModal(); 20 | }} 21 | > 22 |
23 | 24 | 29 |
30 | 36 |
37 |
38 | ); 39 | }; 40 | export default Options; 41 | -------------------------------------------------------------------------------- /src/components/Editor/NodeViews/TableView.tsx: -------------------------------------------------------------------------------- 1 | import { NodeViewContent, NodeViewWrapper } from "@tiptap/react"; 2 | 3 | import { HiPlus } from "react-icons/hi2"; 4 | import props from "./types"; 5 | 6 | const TableView: React.FC = ({ editor }) => { 7 | return ( 8 | 9 |
10 |
11 | 12 | 20 |
21 | 22 | 30 |
31 |
32 | ); 33 | }; 34 | 35 | export default TableView; 36 | -------------------------------------------------------------------------------- /src/components/Editor/NodeViews/types.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "@tiptap/pm/model"; 2 | import { Editor } from "@tiptap/react"; 3 | export default interface props { 4 | node: Node; 5 | selected: boolean; 6 | updateAttributes: (attrs: object) => void; 7 | editor: Editor; 8 | getPos: () => number; 9 | deleteNode: () => void; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Editor/Popover/Link.tsx: -------------------------------------------------------------------------------- 1 | import { Editor } from "@tiptap/react"; 2 | 3 | import { TbTrash } from "react-icons/tb"; 4 | import { FiEdit } from "react-icons/fi"; 5 | import { getLinkText } from "../hasLink"; 6 | import { 7 | Popover, 8 | PopoverContent, 9 | PopoverTrigger, 10 | } from "@/components/ui/popover"; 11 | import { useEffect, useRef, useState } from "react"; 12 | 13 | interface props { 14 | editor: Editor | null; 15 | } 16 | const LinkPopover: React.FC = ({ editor }) => { 17 | const ref = useRef(null); 18 | const [open, setOpen] = useState(false); 19 | const [link, setLink] = useState(""); 20 | const [text, setText] = useState(""); 21 | 22 | if (ref.current) { 23 | if (editor?.isActive("link")) { 24 | let { from } = editor.state.selection; 25 | let startPos = editor.view.coordsAtPos(from); 26 | ref.current!.style.display = "block"; 27 | ref.current!.style.top = `${startPos.bottom + 10 + window.scrollY}px`; 28 | ref.current!.style.left = `${startPos.left}px`; 29 | } else { 30 | ref.current!.style.display = "none"; 31 | } 32 | } 33 | const scrollHandler = () => { 34 | if (editor?.isActive("link")) { 35 | //@ts-ignore 36 | let { from } = editor.state.selection; 37 | let startPos = editor.view.coordsAtPos(from); 38 | ref.current!.style.display = "block"; 39 | ref.current!.style.top = `${startPos.bottom + 10 + window.scrollY}px`; 40 | } 41 | }; 42 | useEffect(() => { 43 | if (!editor) return; 44 | let textEditor = document.querySelector(".editor"); 45 | textEditor?.addEventListener("scroll", scrollHandler); 46 | 47 | return () => { 48 | window.removeEventListener("scroll", scrollHandler); 49 | }; 50 | }, [editor?.isActive("link")]); 51 | const clickHandler = () => { 52 | if (!editor) return; 53 | const previousUrl = editor.getAttributes("link").href; 54 | const { view, state } = editor; 55 | const { from, to } = view.state.selection; 56 | //@ts-ignore 57 | let nodeBefore = view.state.selection.$cursor?.nodeBefore; 58 | //@ts-ignore 59 | let nodeAfter = view.state.selection.$cursor?.nodeAfter; 60 | 61 | if (previousUrl && (nodeBefore || nodeAfter)) { 62 | let linkText = getLinkText(nodeBefore, nodeAfter); 63 | setText(linkText); 64 | } else { 65 | setText(state.doc.textBetween(from, to, "")); 66 | } 67 | setLink(editor.getAttributes("link").href); 68 | }; 69 | if (!editor) return; 70 | 71 | return ( 72 |
77 | {editor.isActive("link") && ( 78 |
79 | 84 | {editor.getAttributes("link").href} 85 | 86 |

-

87 | setOpen(val)}> 88 | 89 | 90 | 91 | 92 |
{ 94 | e.preventDefault(); 95 | 96 | if (link === null) { 97 | return; 98 | } 99 | if (link === "") { 100 | editor 101 | .chain() 102 | .focus() 103 | .extendMarkRange("link") 104 | .unsetLink() 105 | .run(); 106 | return; 107 | } 108 | editor 109 | .chain() 110 | .focus() 111 | .extendMarkRange("link") 112 | .setLink({ href: link }) 113 | .command(({ tr }) => { 114 | tr.insertText(text); 115 | return true; 116 | }) 117 | .run(); 118 | setOpen(false); 119 | }} 120 | > 121 |
122 | 123 | setText(e.target.value)} 126 | className="w-full text-[15px] border rounded-md px-2 focus:outline-none py-1" 127 | /> 128 |
129 | 130 |
131 |
132 | 133 | setLink(e.target.value)} 136 | className="w-full text-[15px] border rounded-md px-2 focus:outline-none py-1" 137 | /> 138 |
139 |
140 | 146 |
147 |
148 |
149 | 161 |
162 | )} 163 |
164 | ); 165 | }; 166 | export default LinkPopover; 167 | -------------------------------------------------------------------------------- /src/components/Editor/Publish.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from "@tauri-apps/api/shell"; 2 | 3 | import { readTextFile, writeTextFile } from "@tauri-apps/api/fs"; 4 | import markdown from "highlight.js/lib/languages/markdown"; 5 | import "highlight.js/styles/nord.css"; 6 | import hljs from "highlight.js/lib/core"; 7 | hljs.registerLanguage("md", markdown); 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogDescription, 12 | DialogFooter, 13 | DialogHeader, 14 | DialogTitle, 15 | DialogTrigger, 16 | } from "@/components/ui/dialog"; 17 | import { Button } from "../ui/button"; 18 | import { useEffect, useRef, useState } from "react"; 19 | import { join } from "@tauri-apps/api/path"; 20 | import { Input } from "../ui/input"; 21 | import { Label } from "../ui/label"; 22 | import { useToast } from "../ui/use-toast"; 23 | import { IoCheckmarkSharp } from "react-icons/io5"; 24 | 25 | interface props { 26 | projectPath: string; 27 | filePath: string; 28 | reRender: () => void; 29 | } 30 | const Publish: React.FC = ({ filePath, projectPath, reRender }) => { 31 | const { toast } = useToast(); 32 | const mdRef = useRef(null); 33 | const commitMsgRef = useRef(null); 34 | const [error, setError] = useState(); 35 | const [content, setContent] = useState(); 36 | const [open, setOpen] = useState(false); 37 | async function publishHandler() { 38 | const txtContent = mdRef.current!.textContent; 39 | if (!txtContent) { 40 | setError( 41 | `An error happened while trying to get this file's text content.`, 42 | ); 43 | return; 44 | } 45 | await writeTextFile(filePath, txtContent); 46 | const dir = await join(filePath, "../"); 47 | const addCmd = await new Command( 48 | "git", 49 | ["add", filePath.replace(dir + "/", "")], 50 | { 51 | cwd: dir, 52 | }, 53 | ).execute(); 54 | if (addCmd.code != 0) { 55 | setError(addCmd.stderr || addCmd.stdout); 56 | return; 57 | } 58 | 59 | const commitCmd = await new Command( 60 | "git", 61 | ["commit", "-m", commitMsgRef.current!.value], 62 | { 63 | cwd: dir, 64 | }, 65 | ).execute(); 66 | if (commitCmd.code != 0) { 67 | setError(commitCmd.stderr || commitCmd.stdout); 68 | return; 69 | } 70 | 71 | const pushCmd = await new Command("git", ["push"], { 72 | cwd: dir, 73 | }).execute(); 74 | if (pushCmd.code != 0) { 75 | setError(pushCmd.stderr || pushCmd.stdout); 76 | return; 77 | } 78 | //re-render to show changes in updated markdown content 79 | reRender(); 80 | 81 | toast({ 82 | title: "Pushed changes successfully", 83 | variant: "successfull", 84 | action: , 85 | }); 86 | } 87 | async function getContent() { 88 | let data = await readTextFile(filePath); 89 | setContent(data); 90 | } 91 | useEffect(() => { 92 | if (open) { 93 | getContent(); 94 | } 95 | let timeout: ReturnType; 96 | if (open && content) { 97 | timeout = setTimeout(() => { 98 | if (!mdRef.current) return; 99 | mdRef.current.removeAttribute("data-highlighted"); 100 | hljs.highlightElement(mdRef.current); 101 | }, 0); 102 | } 103 | return () => { 104 | if (timeout) { 105 | clearTimeout(timeout); 106 | } 107 | }; 108 | }, [open, content]); 109 | function highlightMarkdown() { 110 | if (!mdRef.current) return; 111 | mdRef.current.removeAttribute("data-highlighted"); 112 | hljs.highlightElement(mdRef.current); 113 | } 114 | 115 | return ( 116 |
117 | setOpen(e)}> 118 | Publish 119 | 120 | 121 | Commit Changes 122 | 123 | We will commit your changes using git. Make sure you have a git 124 | repo setup in your project's directory. 125 | 126 | 127 | 128 |
129 |

Raw Markdown

130 |
131 |               
137 |                 {content}
138 |               
139 |             
140 |
141 | 142 | 149 |
150 |
151 | {error &&

{error}

} 152 | 153 | 156 | 157 |
158 |
159 |
160 | ); 161 | }; 162 | export default Publish; 163 | -------------------------------------------------------------------------------- /src/components/Editor/TableOfContents.tsx: -------------------------------------------------------------------------------- 1 | import { TOC } from "./Editor"; 2 | import { memo } from "react"; 3 | 4 | interface props { 5 | toc: TOC; 6 | } 7 | const TableOfContents: React.FC = ({ toc }) => { 8 | return ( 9 |
10 |

Table of Contents

11 |
12 | {toc.map((element) => ( 13 | 31 | ))} 32 |
33 | ); 34 | }; 35 | export default memo(TableOfContents); 36 | -------------------------------------------------------------------------------- /src/components/Editor/Titles.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useEffect, 3 | useRef, 4 | type Dispatch, 5 | type SetStateAction, 6 | } from "react"; 7 | 8 | type Metadata = { [key: string]: any }; 9 | interface props { 10 | metadata?: Metadata; 11 | setMetadata: Dispatch>; 12 | } 13 | const Titles: React.FC = ({ metadata, setMetadata }) => { 14 | const titleRef = useRef(null); 15 | const subTitleRef = useRef(null); 16 | function resizeInput() { 17 | if (titleRef.current && subTitleRef.current) { 18 | titleRef.current!.style.height = "auto"; 19 | titleRef.current!.style.height = 20 | titleRef.current!.scrollHeight + 4 + "px"; 21 | 22 | subTitleRef.current!.style.height = "auto"; 23 | subTitleRef.current!.style.height = 24 | subTitleRef.current!.scrollHeight + 4 + "px"; 25 | } 26 | } 27 | useEffect(() => { 28 | resizeInput(); 29 | }, [metadata]); 30 | 31 | useEffect(() => { 32 | window.addEventListener("resize", resizeInput); 33 | return () => { 34 | window.removeEventListener("resize", resizeInput); 35 | }; 36 | }, []); 37 | return ( 38 |
39 |