├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ ├── actions.yml │ └── codeql.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── LICENSE.md ├── README.md ├── __mocks__ └── obsidian.ts ├── bun.lockb ├── content ├── .obsidian │ ├── app.json │ ├── appearance.json │ ├── bookmarks.json │ ├── command-palette.json │ ├── community-plugins.json │ ├── core-plugins-migration.json │ ├── core-plugins.json │ ├── hotkeys.json │ ├── plugins │ │ ├── dataview │ │ │ ├── main.js │ │ │ ├── manifest.json │ │ │ └── styles.css │ │ ├── hot-reload │ │ │ ├── LICENSE │ │ │ ├── main.js │ │ │ └── manifest.json │ │ ├── obsidian-excalidraw-plugin │ │ │ ├── data.json │ │ │ ├── main.js │ │ │ ├── manifest.json │ │ │ └── styles.css │ │ ├── obsidian-style-settings │ │ │ ├── main.js │ │ │ ├── manifest.json │ │ │ └── styles.css │ │ └── quartz-syncer │ │ │ ├── .gitignore │ │ │ └── README.md │ └── types.json ├── 0 QA hell │ ├── 0 File with headers.md │ ├── 1 File with bookmarked block.md │ ├── Break links + transclusions.md │ ├── Empty file.md │ └── File with code blocks.md ├── 000 Home.md ├── 001 Links.md ├── 002 Hidden page.md ├── 003 Non published page.md ├── 004 Publishing this garden.md ├── 005 Custom filters.md ├── 006 Custom title.md ├── 007 Custom permalink.md ├── 008 Pinned note.md ├── 009 Comments.md ├── 010 custom createdAt.md ├── 011 Custom updatedAt.md ├── 012 Callouts.md ├── 012-B Callouts less broken.md ├── 013 Custom path.md ├── 014 Customer path and permalink.md ├── 015 Code blocks.md ├── A Assets │ ├── travolta.png │ ├── travolta.webp │ └── unused_image.png ├── E Embeds │ ├── E01 SVG.md │ ├── E02 PNG published.md │ ├── E03 PNG_not_published.md │ ├── E04 PNG reuse.md │ ├── E05 WEBP published.md │ ├── E07 Image with alt attributes.md │ ├── Transclusions │ │ ├── T1 BaseFile.md │ │ ├── T2 Too deep to transclude.md │ │ ├── T3 Transcluded block.md │ │ ├── T4 Transcluded header.md │ │ ├── T5 transclude custom filters.md │ │ ├── T6 transclusion inside codeblock.md │ │ └── files │ │ │ ├── T2 First transclusion.md │ │ │ ├── T3 Second transclusion.md │ │ │ ├── T4 Deeper.md │ │ │ └── T5 Even deeper.md │ └── garden-gate.svg ├── Excalidraw │ ├── Drawing 2023-09-23 22.41.09.excalidraw.md │ └── with image.excalidraw.md ├── L Languages │ ├── Chinese.md │ └── Transclude Headers.md ├── L Links │ ├── 01 Link to header.md │ └── 02 Header with special character.md ├── P Plugins │ ├── PD Dataview │ │ ├── PD0 - note with summary.md │ │ ├── PD1 Dataview.md │ │ ├── PD2 Inline queries.md │ │ ├── PD3 Inline JS queries.md │ │ └── PD4 DataviewJs queries.md │ └── PE Excalidraw │ │ ├── PE1 Transcluded excalidraw.md │ │ └── PE2 excalidraw with image.md └── Path Rewriting │ ├── 004 Folder set to root.md │ ├── Subfolder │ └── How deep do the rewrite rules go.md │ └── Subfolder2 │ └── More specific path rewriting.md ├── docs ├── Changelog.md ├── Guides │ ├── Configuring a specific folder for Quartz content.md │ ├── Generating an access token.md │ ├── Using an Obsidian theme in Quartz.md │ └── index.md ├── Media │ └── Access Token │ │ ├── access-token-confirmation-popup.png │ │ ├── access-token-contents-permission.png │ │ ├── access-token-copy-generated-token.png │ │ ├── access-token-expiration-date.png │ │ ├── access-token-generate-token-button.png │ │ ├── access-token-name.png │ │ ├── access-token-obsidian-settings.png │ │ ├── access-token-permissions-options.png │ │ └── access-token-repository-access.png ├── Settings │ ├── GitHub │ │ ├── Access token.md │ │ ├── Repository name.md │ │ ├── Username.md │ │ ├── Vault root folder name.md │ │ └── index.md │ ├── Integrations │ │ ├── Datacore.md │ │ ├── Dataview.md │ │ ├── Excalidraw.md │ │ └── index.md │ ├── Note properties │ │ ├── Include all properties.md │ │ ├── Include created timestamp.md │ │ ├── Include modified timestamp.md │ │ ├── Include published timestamp.md │ │ ├── Publish key.md │ │ └── index.md │ ├── Quartz │ │ ├── Apply embeds.md │ │ ├── Content folder.md │ │ ├── Use full image resolution.md │ │ └── index.md │ ├── Themes │ │ └── index.md │ └── index.md ├── Setup Guide.md ├── Troubleshooting │ ├── Authentication.md │ ├── Dataview.md │ ├── Excalidraw.md │ ├── Frontmatter.md │ ├── Quartz.md │ └── index.md ├── Usage Guide.md ├── Useful resources │ └── index.md ├── index.md └── tags │ ├── datacore.md │ ├── dataview.md │ ├── excalidraw.md │ ├── frontmatter.md │ ├── guides.md │ ├── index.md │ ├── integration.md │ ├── quartz.md │ ├── resources.md │ ├── settings │ ├── frontmatter.md │ ├── github.md │ ├── index.md │ ├── integrations.md │ ├── quartz.md │ └── themes.md │ └── themes.md ├── esbuild.config.mjs ├── jest.config.js ├── justfile ├── main.ts ├── manifest.json ├── package-lock.json ├── package.json ├── quartz-syncer.svg ├── scripts └── generateSyncerSettings.mjs ├── src ├── compiler │ ├── DatacoreCompiler.ts │ ├── DataviewCompiler.ts │ ├── FrontmatterCompiler.ts │ ├── SyncerPageCompiler.ts │ ├── replaceBlockIDs.test.ts │ └── replaceBlockIDs.ts ├── models │ ├── SyncerTab.ts │ ├── TreeNode.ts │ └── settings.ts ├── publishFile │ ├── FileMetaDataManager.ts │ ├── ObsidianFrontMatterEngine.ts │ ├── PublishFile.ts │ └── Validator.ts ├── publisher │ ├── PublishStatusManager.ts │ └── Publisher.ts ├── repositoryConnection │ ├── QuartzSyncerSiteManager.ts │ └── RepositoryConnection.ts ├── test │ └── snapshot │ │ ├── generateSyncerSnapshot.ts │ │ └── snapshot.md ├── ui │ ├── Icon.svelte │ ├── LineDiff.svelte │ ├── TreeView │ │ ├── TreeNode.svelte │ │ └── TreeView.svelte │ └── suggest │ │ ├── constants.ts │ │ └── folder.ts ├── utils │ ├── markdown.ts │ ├── regexes.ts │ ├── styles.ts │ ├── utils.test.ts │ └── utils.ts └── views │ ├── PublicationCenter │ ├── DiffView.svelte │ ├── PublicationCenter.svelte │ └── PublicationCenter.ts │ ├── PublishStatusBar.ts │ ├── QuartzSyncerSettingTab.ts │ └── SettingsView │ ├── SettingView.ts │ └── Views │ ├── FrontmatterSettings.ts │ ├── GithubSettings.ts │ ├── IntegrationSettings.ts │ ├── QuartzSettings.ts │ └── ThemesSettings.ts ├── styles.css ├── tsconfig.json └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | indent_style = tab 8 | indent_size = 4 9 | tab_width = 4 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build 3 | main.js 4 | *.mjs 5 | jest.config.js 6 | 7 | .eslintrc.js 8 | src/testVault/* 9 | content/* 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**.js"], 4 | "parser": "@typescript-eslint/parser", 5 | "plugins": ["@typescript-eslint", "prettier"], 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:svelte/recommended" 11 | ], 12 | "parserOptions": { 13 | "sourceType": "module", 14 | "project": "./tsconfig.json", 15 | "extraFileExtensions": [".svelte"] 16 | }, 17 | "overrides": [ 18 | { 19 | "files": ["*.svelte"], 20 | "parser": "svelte-eslint-parser", 21 | "parserOptions": { 22 | "parser": "@typescript-eslint/parser" 23 | } 24 | } 25 | ], 26 | "rules": { 27 | "prettier/prettier": "error", 28 | "@typescript-eslint/ban-ts-comment": "warn", 29 | "@typescript-eslint/no-unused-vars": [ 30 | "error", 31 | { 32 | "argsIgnorePattern": "^_", 33 | "varsIgnorePattern": "^_", 34 | "caughtErrorsIgnorePattern": "^_" 35 | } 36 | ], 37 | "@typescript-eslint/no-explicit-any": ["error"], 38 | // allow prettier to use SmartTabs 39 | "no-mixed-spaces-and-tabs": "off", 40 | "padding-line-between-statements": [ 41 | "warn", 42 | { 43 | "blankLine": "always", 44 | "prev": "*", 45 | "next": [ 46 | "return", 47 | "if", 48 | "multiline-const", 49 | "function", 50 | "multiline-expression", 51 | "multiline-let", 52 | "block-like" 53 | ] 54 | }, 55 | { 56 | "blankLine": "always", 57 | "prev": ["function"], 58 | "next": "*" 59 | } 60 | ], 61 | "svelte/no-at-html-tags": "off" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: saberzero1 2 | github: [saberzero1] 3 | ko_fi: saberzero1 4 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | types: [edited, synchronize, opened] 4 | push: 5 | branches: [master, main] 6 | workflow_dispatch: 7 | jobs: 8 | lint-and-check-formatting: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: "Use node version" 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version-file: .nvmrc 16 | cache: 'npm' 17 | - name: "Install dependencies" 18 | run: npm install 19 | - name: "Linting with ESLint" 20 | run: npm run lint 21 | - name: Check formatting with Prettier 22 | run: npm run check-formatting 23 | build: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: "Use node version" 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version-file: .nvmrc 31 | cache: 'npm' 32 | - name: "Install dependencies" 33 | run: npm install 34 | - name: "Build" 35 | run: npm run build 36 | - name: Archive production artifacts 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: dist-without-markdown 40 | path: | 41 | main.js 42 | run-tests: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: "Use node version" 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version-file: .nvmrc 50 | cache: 'npm' 51 | - name: "Install dependencies" 52 | run: npm install 53 | - name: Run tests 54 | run: npm run test 55 | run-typecheck: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v4 59 | - name: "Use node version" 60 | uses: actions/setup-node@v4 61 | with: 62 | node-version-file: .nvmrc 63 | cache: 'npm' 64 | - name: "Install dependencies" 65 | run: npm install 66 | - name: Typechecking 67 | run: npm run typecheck 68 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main", "backup" ] 17 | pull_request: 18 | branches: [ "main", "backup" ] 19 | schedule: 20 | - cron: '45 23 * * 0' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: javascript-typescript 47 | build-mode: none 48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Initializes the CodeQL tools for scanning. 61 | - name: Initialize CodeQL 62 | uses: github/codeql-action/init@v3 63 | with: 64 | languages: ${{ matrix.language }} 65 | build-mode: ${{ matrix.build-mode }} 66 | # If you wish to specify custom queries, you can do so here or in a config file. 67 | # By default, queries listed here will override any specified in a config file. 68 | # Prefix the list here with "+" to use these queries and those in the config file. 69 | 70 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 71 | # queries: security-extended,security-and-quality 72 | 73 | # If the analyze step fails for one of the languages you are analyzing with 74 | # "We were unable to automatically build your code", modify the matrix above 75 | # to set the build mode to "manual" for that language. Then modify this step 76 | # to build your code. 77 | # ℹ️ Command-line programs to run using the OS shell. 78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 79 | - if: matrix.build-mode == 'manual' 80 | shell: bash 81 | run: | 82 | echo 'If you are using a "manual" build mode for one or more of the' \ 83 | 'languages you are analyzing, replace this with the commands to build' \ 84 | 'your code, for example:' 85 | echo ' make bootstrap' 86 | echo ' make release' 87 | exit 1 88 | 89 | - name: Perform CodeQL Analysis 90 | uses: github/codeql-action/analyze@v3 91 | with: 92 | category: "/language:${{matrix.language}}" 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | /main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | !src/testVault/.obsidian/plugins/obsidian-excalidraw-plugin/data.json 21 | !src/testVault/.obsidian/plugins/dataview/data.json 22 | !content/.obsidian/plugins/obsidian-excalidraw-plugin/data.json 23 | !content/.obsidian/plugins/dataview/data.json 24 | deploy.ps1 25 | deploy.sh 26 | 27 | # mac 28 | .DS_Store 29 | yarn.lock 30 | 31 | .env 32 | src/testVault/.obsidian/workspace.json 33 | content/.obsidian/workspace.json 34 | 35 | # Documentation 36 | docs/.obsidian 37 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.6.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | main.js 3 | 4 | esbuild.config.mjs 5 | package-lock.json 6 | node_modules 7 | coverage 8 | .vscode 9 | .parcel-cache 10 | content/* 11 | 12 | *.yml 13 | src/testVault/* 14 | content/* 15 | 16 | package-lock.json 17 | package.json 18 | 19 | *.md 20 | *.mjs 21 | README.md 22 | *.md 23 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": true, 4 | "singleQuote": false, 5 | "bracketSpacing": true, 6 | "useTabs": true, 7 | "plugins": ["prettier-plugin-svelte"], 8 | "overrides": [ 9 | { 10 | "files": [".prettierrc", ".eslintrc"], 11 | "options": { "parser": "json" } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Emile Bangma 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 | # Quartz Syncer 2 | 3 | Quartz Syncer is an [Obsidian](https://obsidian.md/) plugin for managing and publishing notes to [Quartz](https://quartz.jzhao.xyz/), the fast, batteries-included static-site generator that transforms Markdown content into fully functional websites. 4 | 5 | ## Installation 6 | 7 | Install the plugin by downloading it from the Obsidian Community plugins browser in Obsidian. 8 | 9 | Alternatively, install the plugin by downloading it from the [Release Tab](https://github.com/saberzero1/quartz-syncer/releases), or through the [Obsidian42 Brat plugin](https://github.com/TfTHacker/obsidian42-brat). 10 | 11 | ## Setup 12 | 13 | > [!TIP] 14 | > **Quartz Syncer documentation** 15 | > 16 | > For the most up-to-date information on Quartz Syncer, please refer to the [official documentation](https://saberzero1.github.io/quartz-syncer-docs/). 17 | 18 | New to Quartz Syncer? please follow the [setup guide](https://saberzero1.github.io/quartz-syncer-docs/Setup-Guide) to get started. 19 | 20 | ## Usage 21 | 22 | Unsure on how to use Quartz Syncer, or just curious about its usage? Check the [usage guide](https://saberzero1.github.io/quartz-syncer-docs/Usage-Guide). 23 | 24 | ## Advanced usage 25 | 26 | For more advanced usages of Quartz Syncer, check the [guides section](https://saberzero1.github.io/quartz-syncer-docs/Guides/). 27 | 28 | ## Troubleshooting 29 | 30 | > [!IMPORTANT] 31 | > **Quartz-related questions** 32 | > 33 | > For issue or questions related to Quartz, not Quartz Syncer, please consult the [Quartz documentation](https://quartz.jzhao.xyz/) or reach out through the communication channels provided there. 34 | 35 | If you need help with Quartz Syncer, or if you have a question, please first check the [troubleshooting section](https://saberzero1.github.io/quartz-syncer-docs/Troubleshooting/). If your question or issue is not listed, feel free to [reach out for help](https://saberzero1.github.io/quartz-syncer-docs/Troubleshooting/#i-have-a-different-issue-not-listed-here). 36 | 37 | ## Disclosures 38 | 39 | As per the [Obsidian developer policies](https://docs.obsidian.md/Developer+policies#Disclosures): 40 | 41 | - **Account requirements**: Quartz Syncer needs to access your Quartz repository on GitHub in order to be able to publish your notes. A GitHub account is therefore required, though also implictly expected, if you're using Quartz. 42 | - **Network use**: Quartz Syncer accesses the network to manage and publish your notes to your Quartz repository on GitHub. Quartz Syncer uses the [GitHub REST API](https://docs.github.com/en/rest) to access your Quartz repository over the network. 43 | - **Accessing files outside of Obsidian vaults**: Quartz Syncer only manages explictly marked *and* user-selected notes in your Quartz repository `content` folder. Quartz Syncer also fetches the current contents of this folder to compare changes against your notes. Quartz Syncer doesn't write any notes to your Obsidian vault, Quartz Syncer only writes to your Quartz repository (one-way only: from Obsidian vault to Quartz repository.) When [authentication is properly set up with a fine-grained access token](https://saberzero1.github.io/quartz-syncer-docs/Guides/Generating-an-access-token#generating-a-fine-grained-access-token), Quartz Syncer is only able to modify file contents on your Quartz repository. Quartz Syncer cannot access other repositories on the same GitHub account or organization, nor modify any other settings in your Quartz Repository. 44 | 45 | ## Acknowledgements 46 | 47 | Quartz Syncer would not have been build without the following: 48 | 49 | - [Obsidian Digital Garden](https://dg-docs.ole.dev/), on top of which most of this plugin was initially built. 50 | - [Quartz](https://quartz.jzhao.xyz/), for the amazing and welcoming community. Come say hi in the Discord server sometimes. 51 | - [Obsidian Linter](https://github.com/platers/obsidian-linter), for inspiring the tabbed settings UI. 52 | - [Dataview](https://blacksmithgu.github.io/obsidian-dataview/), for their great API integration, allowing me to properly integrate it in Quartz. 53 | - [Datacore](https://blacksmithgu.github.io/datacore/), for their wonderful integration despite its infancy, allowing easy integration into Quartz. 54 | - [Obsidian Publish](https://obsidian.md/publish), for inspiring me to create a similar solution for Quartz. 55 | - The entire Obsidian community, for all your weird and amazing creations. Keep it up. 56 | -------------------------------------------------------------------------------- /__mocks__/obsidian.ts: -------------------------------------------------------------------------------- 1 | const obsidian = {}; 2 | 3 | module.exports = obsidian; 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/bun.lockb -------------------------------------------------------------------------------- /content/.obsidian/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "alwaysUpdateLinks": true 3 | } -------------------------------------------------------------------------------- /content/.obsidian/appearance.json: -------------------------------------------------------------------------------- 1 | { 2 | "accentColor": "" 3 | } -------------------------------------------------------------------------------- /content/.obsidian/bookmarks.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "type": "file", 5 | "ctime": 1695841980647, 6 | "path": "0 QA hell/0 File with headers.md", 7 | "subpath": "#^897989" 8 | }, 9 | { 10 | "type": "file", 11 | "ctime": 1695842041737, 12 | "path": "0 QA hell/1 File with bookmarked block.md", 13 | "subpath": "#^c954d2", 14 | "title": "cheese" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /content/.obsidian/command-palette.json: -------------------------------------------------------------------------------- 1 | { 2 | "pinned": [ 3 | "app:reload", 4 | "quartzsyncer:generate-garden-snapshot" 5 | ] 6 | } -------------------------------------------------------------------------------- /content/.obsidian/community-plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | "quartzsyncer", 3 | "hot-reload", 4 | "obsidian-excalidraw-plugin", 5 | "dataview", 6 | "quartz-syncer" 7 | ] -------------------------------------------------------------------------------- /content/.obsidian/core-plugins-migration.json: -------------------------------------------------------------------------------- 1 | { 2 | "file-explorer": true, 3 | "global-search": true, 4 | "switcher": true, 5 | "graph": true, 6 | "backlink": true, 7 | "canvas": true, 8 | "outgoing-link": true, 9 | "tag-pane": true, 10 | "properties": false, 11 | "page-preview": true, 12 | "daily-notes": true, 13 | "templates": true, 14 | "note-composer": true, 15 | "command-palette": true, 16 | "slash-command": false, 17 | "editor-status": true, 18 | "bookmarks": true, 19 | "markdown-importer": false, 20 | "zk-prefixer": false, 21 | "random-note": false, 22 | "outline": true, 23 | "word-count": true, 24 | "slides": false, 25 | "audio-recorder": false, 26 | "workspaces": false, 27 | "file-recovery": true, 28 | "publish": false, 29 | "sync": false 30 | } -------------------------------------------------------------------------------- /content/.obsidian/core-plugins.json: -------------------------------------------------------------------------------- 1 | { 2 | "file-explorer": true, 3 | "global-search": true, 4 | "switcher": true, 5 | "graph": true, 6 | "backlink": true, 7 | "canvas": true, 8 | "outgoing-link": true, 9 | "tag-pane": true, 10 | "properties": false, 11 | "page-preview": true, 12 | "daily-notes": true, 13 | "templates": true, 14 | "note-composer": true, 15 | "command-palette": true, 16 | "slash-command": false, 17 | "editor-status": true, 18 | "bookmarks": true, 19 | "markdown-importer": false, 20 | "zk-prefixer": false, 21 | "random-note": false, 22 | "outline": true, 23 | "word-count": true, 24 | "slides": false, 25 | "audio-recorder": false, 26 | "workspaces": false, 27 | "file-recovery": true, 28 | "publish": false, 29 | "sync": false 30 | } -------------------------------------------------------------------------------- /content/.obsidian/hotkeys.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /content/.obsidian/plugins/dataview/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dataview", 3 | "name": "Dataview", 4 | "version": "0.5.67", 5 | "minAppVersion": "0.13.11", 6 | "description": "Complex data views for the data-obsessed.", 7 | "author": "Michael Brenan ", 8 | "authorUrl": "https://github.com/blacksmithgu", 9 | "helpUrl": "https://blacksmithgu.github.io/obsidian-dataview/", 10 | "isDesktopOnly": false 11 | } 12 | -------------------------------------------------------------------------------- /content/.obsidian/plugins/dataview/styles.css: -------------------------------------------------------------------------------- 1 | /** Live Preview padding fixes, specifically for DataviewJS custom HTML elements. */ 2 | .is-live-preview .block-language-dataviewjs > p, .is-live-preview .block-language-dataviewjs > span { 3 | line-height: 1.0; 4 | } 5 | 6 | .block-language-dataview { 7 | overflow-y: auto; 8 | } 9 | 10 | /*****************/ 11 | /** Table Views **/ 12 | /*****************/ 13 | 14 | /* List View Default Styling; rendered internally as a table. */ 15 | .table-view-table { 16 | width: 100%; 17 | } 18 | 19 | .table-view-table > thead > tr, .table-view-table > tbody > tr { 20 | margin-top: 1em; 21 | margin-bottom: 1em; 22 | text-align: left; 23 | } 24 | 25 | .table-view-table > tbody > tr:hover { 26 | background-color: var(--table-row-background-hover); 27 | } 28 | 29 | .table-view-table > thead > tr > th { 30 | font-weight: 700; 31 | font-size: larger; 32 | border-top: none; 33 | border-left: none; 34 | border-right: none; 35 | border-bottom: solid; 36 | 37 | max-width: 100%; 38 | } 39 | 40 | .table-view-table > tbody > tr > td { 41 | text-align: left; 42 | border: none; 43 | font-weight: 400; 44 | max-width: 100%; 45 | } 46 | 47 | .table-view-table ul, .table-view-table ol { 48 | margin-block-start: 0.2em !important; 49 | margin-block-end: 0.2em !important; 50 | } 51 | 52 | /** Rendered value styling for any view. */ 53 | .dataview-result-list-root-ul { 54 | padding: 0em !important; 55 | margin: 0em !important; 56 | } 57 | 58 | .dataview-result-list-ul { 59 | margin-block-start: 0.2em !important; 60 | margin-block-end: 0.2em !important; 61 | } 62 | 63 | /** Generic grouping styling. */ 64 | .dataview.result-group { 65 | padding-left: 8px; 66 | } 67 | 68 | /*******************/ 69 | /** Inline Fields **/ 70 | /*******************/ 71 | 72 | .dataview.inline-field-key { 73 | padding-left: 8px; 74 | padding-right: 8px; 75 | font-family: var(--font-monospace); 76 | background-color: var(--background-primary-alt); 77 | color: var(--text-nav-selected); 78 | } 79 | 80 | .dataview.inline-field-value { 81 | padding-left: 8px; 82 | padding-right: 8px; 83 | font-family: var(--font-monospace); 84 | background-color: var(--background-secondary-alt); 85 | color: var(--text-nav-selected); 86 | } 87 | 88 | .dataview.inline-field-standalone-value { 89 | padding-left: 8px; 90 | padding-right: 8px; 91 | font-family: var(--font-monospace); 92 | background-color: var(--background-secondary-alt); 93 | color: var(--text-nav-selected); 94 | } 95 | 96 | /***************/ 97 | /** Task View **/ 98 | /***************/ 99 | 100 | .dataview.task-list-item, .dataview.task-list-basic-item { 101 | margin-top: 3px; 102 | margin-bottom: 3px; 103 | transition: 0.4s; 104 | } 105 | 106 | .dataview.task-list-item:hover, .dataview.task-list-basic-item:hover { 107 | background-color: var(--text-selection); 108 | box-shadow: -40px 0 0 var(--text-selection); 109 | cursor: pointer; 110 | } 111 | 112 | /*****************/ 113 | /** Error Views **/ 114 | /*****************/ 115 | 116 | div.dataview-error-box { 117 | width: 100%; 118 | min-height: 150px; 119 | display: flex; 120 | align-items: center; 121 | justify-content: center; 122 | border: 4px dashed var(--background-secondary); 123 | } 124 | 125 | .dataview-error-message { 126 | color: var(--text-muted); 127 | text-align: center; 128 | } 129 | 130 | /*************************/ 131 | /** Additional Metadata **/ 132 | /*************************/ 133 | 134 | .dataview.small-text { 135 | font-size: smaller; 136 | color: var(--text-muted); 137 | margin-left: 3px; 138 | } 139 | 140 | .dataview.small-text::before { 141 | content: "("; 142 | } 143 | 144 | .dataview.small-text::after { 145 | content: ")"; 146 | } 147 | -------------------------------------------------------------------------------- /content/.obsidian/plugins/hot-reload/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 PJ Eby 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /content/.obsidian/plugins/hot-reload/main.js: -------------------------------------------------------------------------------- 1 | const {Plugin, Notice, debounce} = require("obsidian"); 2 | const fs = require("fs"); 3 | 4 | const watchNeeded = window.process.platform !== "darwin" && window.process.platform !== "win32"; 5 | 6 | module.exports = class HotReload extends Plugin { 7 | 8 | statCache = new Map(); // path -> Stat 9 | queue = Promise.resolve(); 10 | 11 | run(val, err) { 12 | return this.queue = this.queue.then(val, err); 13 | } 14 | 15 | reindexPlugins = debounce(() => this.run(() => this.getPluginNames()), 500, true); 16 | requestScan = debounce(() => this.run(() => this.checkVersions()), 250, true); 17 | 18 | onload() { 19 | app.workspace.onLayoutReady(async ()=> { 20 | this.pluginReloaders = {}; 21 | this.inProgress = null; 22 | await this.getPluginNames(); 23 | this.registerEvent( this.app.vault.on("raw", this.requestScan)); 24 | this.watch(".obsidian/plugins"); 25 | this.requestScan(); 26 | this.addCommand({ 27 | id: "scan-for-changes", 28 | name: "Check plugins for changes and reload them", 29 | callback: () => this.requestScan() 30 | }) 31 | }); 32 | } 33 | 34 | watch(path) { 35 | if (this.app.vault.adapter.watchers.hasOwnProperty(path)) return; 36 | const realPath = [this.app.vault.adapter.basePath, path].join("/"); 37 | const lstat = fs.lstatSync(realPath); 38 | if (lstat && (watchNeeded || lstat.isSymbolicLink()) && fs.statSync(realPath).isDirectory()) { 39 | this.app.vault.adapter.startWatchPath(path, false); 40 | } 41 | } 42 | 43 | async checkVersions() { 44 | const base = this.app.plugins.getPluginFolder(); 45 | for (const dir of Object.keys(this.pluginNames)) { 46 | for (const file of ["manifest.json", "main.js", "styles.css", ".hotreload"]) { 47 | const path = `${base}/${dir}/${file}`; 48 | const stat = await app.vault.adapter.stat(path); 49 | if (stat) { 50 | if (this.statCache.has(path) && stat.mtime !== this.statCache.get(path).mtime) { 51 | this.onFileChange(path); 52 | } 53 | this.statCache.set(path, stat); 54 | } 55 | } 56 | } 57 | } 58 | 59 | async getPluginNames() { 60 | const plugins = {}, enabled = new Set(); 61 | for (const {id, dir} of Object.values(app.plugins.manifests)) { 62 | this.watch(dir); 63 | plugins[dir.split("/").pop()] = id; 64 | if ( 65 | await this.app.vault.exists(dir+"/.git") || 66 | await this.app.vault.exists(dir+"/.hotreload") 67 | ) enabled.add(id); 68 | } 69 | this.pluginNames = plugins; 70 | this.enabledPlugins = enabled; 71 | } 72 | 73 | onFileChange(filename) { 74 | if (!filename.startsWith(this.app.plugins.getPluginFolder()+"/")) return; 75 | const path = filename.split("/"); 76 | const base = path.pop(), dir = path.pop(); 77 | if (path.length === 1 && dir === "plugins") return this.watch(filename); 78 | if (path.length != 2) return; 79 | const plugin = dir && this.pluginNames[dir]; 80 | if (base === "manifest.json" || base === ".hotreload" || base === ".git" || !plugin) return this.reindexPlugins(); 81 | if (base !== "main.js" && base !== "styles.css") return; 82 | if (!this.enabledPlugins.has(plugin)) return; 83 | const reloader = this.pluginReloaders[plugin] || ( 84 | this.pluginReloaders[plugin] = debounce(() => this.run(() => this.reload(plugin), console.error), 750, true) 85 | ); 86 | reloader(); 87 | } 88 | 89 | async reload(plugin) { 90 | const plugins = app.plugins; 91 | 92 | // Don't reload disabled plugins 93 | if (!plugins.enabledPlugins.has(plugin)) return; 94 | 95 | await plugins.disablePlugin(plugin); 96 | console.debug("disabled", plugin); 97 | 98 | // Ensure sourcemaps are loaded (Obsidian 14+) 99 | const oldDebug = localStorage.getItem("debug-plugin"); 100 | localStorage.setItem("debug-plugin", "1"); 101 | try { 102 | await plugins.enablePlugin(plugin); 103 | } finally { 104 | // Restore previous setting 105 | if (oldDebug === null) localStorage.removeItem("debug-plugin"); else localStorage.setItem("debug-plugin", oldDebug); 106 | } 107 | console.debug("enabled", plugin); 108 | new Notice(`Plugin "${plugin}" has been reloaded`); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /content/.obsidian/plugins/hot-reload/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "hot-reload", 3 | "name": "Hot Reload", 4 | "author": "PJ Eby", 5 | "authorUrl": "https://github.com/pjeby", 6 | "version": "0.1.10", 7 | "minAppVersion": "0.15.9", 8 | "description": "Automatically reload in-development plugins when their files are changed", 9 | "isDesktopOnly": true 10 | } 11 | -------------------------------------------------------------------------------- /content/.obsidian/plugins/obsidian-excalidraw-plugin/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-excalidraw-plugin", 3 | "name": "Excalidraw", 4 | "version": "2.6.5", 5 | "minAppVersion": "1.1.6", 6 | "description": "An Obsidian plugin to edit and view Excalidraw drawings", 7 | "author": "Zsolt Viczian", 8 | "authorUrl": "https://www.zsolt.blog", 9 | "fundingUrl": "https://ko-fi.com/zsolt", 10 | "helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme", 11 | "isDesktopOnly": false 12 | } 13 | -------------------------------------------------------------------------------- /content/.obsidian/plugins/obsidian-style-settings/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-style-settings", 3 | "name": "Style Settings", 4 | "version": "1.0.9", 5 | "minAppVersion": "0.11.5", 6 | "description": "Offers controls for adjusting theme, plugin, and snippet CSS variables.", 7 | "author": "mgmeyers", 8 | "authorUrl": "https://github.com/mgmeyers/obsidian-style-settings", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /content/.obsidian/plugins/quartz-syncer/.gitignore: -------------------------------------------------------------------------------- 1 | manifest.json 2 | main.js 3 | styles.css 4 | data.json 5 | -------------------------------------------------------------------------------- /content/.obsidian/plugins/quartz-syncer/README.md: -------------------------------------------------------------------------------- 1 | `npm run dev`` should copy into here, but the files are gitignored 2 | -------------------------------------------------------------------------------- /content/.obsidian/types.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "aliases": "aliases", 4 | "cssclasses": "multitext", 5 | "tags": "tags", 6 | "publish": "checkbox", 7 | "home": "checkbox", 8 | "pinned": "checkbox", 9 | "customCreated": "text", 10 | "excalidraw-plugin": "text", 11 | "excalidraw-export-transparent": "checkbox", 12 | "excalidraw-mask": "checkbox", 13 | "excalidraw-export-dark": "checkbox", 14 | "excalidraw-export-padding": "number", 15 | "excalidraw-export-pngscale": "number", 16 | "excalidraw-link-prefix": "text", 17 | "excalidraw-url-prefix": "text", 18 | "excalidraw-link-brackets": "checkbox", 19 | "excalidraw-onload-script": "text", 20 | "excalidraw-linkbutton-opacity": "number", 21 | "excalidraw-default-mode": "text", 22 | "excalidraw-font": "text", 23 | "excalidraw-font-color": "text", 24 | "excalidraw-border-color": "text", 25 | "excalidraw-css": "text", 26 | "excalidraw-autoexport": "text", 27 | "excalidraw-iframe-theme": "text", 28 | "excalidraw-export-embed-scene": "checkbox", 29 | "excalidraw-embeddable-theme": "text", 30 | "excalidraw-open-md": "checkbox" 31 | } 32 | } -------------------------------------------------------------------------------- /content/0 QA hell/0 File with headers.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | This is above the header 4 | ## Header 5 | 6 | This should be in this header block 7 | 8 | ## Another header 9 | 10 | This shouldn't be under a header transclusion 11 | 12 | 13 | 14 | Cheese ^897989 -------------------------------------------------------------------------------- /content/0 QA hell/1 File with bookmarked block.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | I'm honestly not sure what blocks are and if they exist without bookmarks but here's cheese: 4 | 5 | cheese ^c954d2 -------------------------------------------------------------------------------- /content/0 QA hell/Break links + transclusions.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | Link with whitespace after link part: 5 | [[Empty file | hehe this one breaks for real]] 6 | 7 | Whitespace and bar in name: 8 | ![[Empty file| whitespace. | whoa what's this]] 9 | 10 | Random hashes in transclusion title: 11 | ![[Empty file|## ## i think i fixed this one ## earlier #lol]] 12 | 13 | This is a header ref which doesn't exist: 14 | ![[0 File with headers#whodis]] 15 | 16 | 17 | This is a header transclusion that is slightly that uses a special character in the header 18 | 19 | ![[0 File with headers#Header?]] 20 | 21 | -------------------------------------------------------------------------------- /content/0 QA hell/Empty file.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/content/0 QA hell/Empty file.md -------------------------------------------------------------------------------- /content/0 QA hell/File with code blocks.md: -------------------------------------------------------------------------------- 1 | 2 | ```js 3 | const asdf = 0 4 | asdf++ 5 | ``` 6 | 7 | ```json 8 | { 9 | gotta: "love-dat-josn" 10 | } 11 | ``` 12 | 13 | ``` 14 | this is just text i guess 15 | ``` 16 | 17 | `bonus oneliner` 18 | 19 | -------------------------------------------------------------------------------- /content/000 Home.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | publish: true 4 | --- 5 | ## Welcome 6 | 7 | Welcome to the digital garden testing vault! 8 | 9 | This vault is part of the `quartz-syncer` repository, and meant to act as a staging area for 10 | 11 | 1. providing a maintainable testing ground for the digital garden features 12 | 2. documenting features in action 13 | 14 | Hopefully in the future it can be part of automated testing too! Say, snapshot testing the output from this garden would actually be relatively easy! :) 15 | 16 | > [!info] See README for instructions on adding info to .env for testing 17 | 18 | 19 | ## Snapshot tests 20 | 21 | This test vault enables snapshot testing of the garden compilation! To generate the snapshot: 22 | 23 | - run `Generate Syncer Snapshot` from the command palette 24 | - Snapshot generation is also run on plugin load. 25 | 26 | 27 | ## Plugins 28 | 29 | This garden should have the following plugins 30 | 31 | - [x] [[PE1 Transcluded excalidraw]] 32 | - [x] [[PD1 Dataview]] 33 | - [x] hot reload (reloads obsidian dev plugins on changes) -------------------------------------------------------------------------------- /content/001 Links.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | [[002 Hidden page]] 5 | 6 | [[003 Non published page]] 7 | 8 | [[000 Home| Aliased link to home]] 9 | 10 | [[000 Home | Link containing whitespace which works in obsidian but doesn't in garden :) - yes, this could be a ticket but lo and behold]] 11 | 12 | -------------------------------------------------------------------------------- /content/002 Hidden page.md: -------------------------------------------------------------------------------- 1 | --- 2 | draft: true 3 | publish: true 4 | --- 5 | This page is hidden from the folder tree! -------------------------------------------------------------------------------- /content/003 Non published page.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: false 3 | --- 4 | This page should not be published! -------------------------------------------------------------------------------- /content/004 Publishing this garden.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | To use this test garden, add a test garden token / username / repo to `.env` (see README.md) 5 | 6 | 7 | -------------------------------------------------------------------------------- /content/005 Custom filters.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | 6 | this plugin has custom filter that turns ❄️ (snow emoji) into 🌞 (THE SUN). When published, this file should have a lot of sun-emojis. 7 | 8 | 9 | ❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️ 10 | -------------------------------------------------------------------------------- /content/006 Custom title.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 006 THIS IS A CUSTOM TITLE 3 | publish: true 4 | --- 5 | [Custom title](https://docs.ole.dev/advanced/note-specific-settings/) 6 | -------------------------------------------------------------------------------- /content/007 Custom permalink.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | permalink: my-name-is-permalink/custom-permalink 4 | --- 5 | [Custom permalink](https://docs.ole.dev/advanced/note-specific-settings/) 6 | 7 | -------------------------------------------------------------------------------- /content/008 Pinned note.md: -------------------------------------------------------------------------------- 1 | --- 2 | pinned: true 3 | publish: true 4 | --- 5 | Hello! I'm a pinned note (should be at the top yeah!) -------------------------------------------------------------------------------- /content/009 Comments.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | This is the only content on this page 5 | 6 | %% THEY'LL NEVER FIND ME!! %% -------------------------------------------------------------------------------- /content/010 custom createdAt.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | customCreated: 2020-01-01 4 | --- 5 | This file should have createdAt: 2020-01-01 -------------------------------------------------------------------------------- /content/011 Custom updatedAt.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | customUpdated: 2021-01-01 4 | --- 5 | This file should have updatedAt: 2021-01-01 6 | -------------------------------------------------------------------------------- /content/012 Callouts.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | #known-issue 5 | 6 | > [!info] 7 | > This is a callout 8 | 9 | 10 | > [!info]- this one is closed by default 11 | > > [!success] and has a callout inside of it :o 12 | > > anything is possible :) 13 | 14 | 15 | > [!info] This one has dataview in it... 16 | > ```dataview 17 | > list 18 | > from "012 Callouts" 19 | > ``` 20 | 21 | > [!success] this one has a friggin note embedded in it 22 | > ![[File with code blocks]] 23 | 24 | 25 | -------------------------------------------------------------------------------- /content/012-B Callouts less broken.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | > [!info] 6 | > This is a callout 7 | 8 | 9 | > [!info]- this one is closed by default 10 | > > [!success] and has a callout inside of it :o 11 | > > anything is possible :) 12 | 13 | #known-issue 14 | > [!success] this one has a friggin note embedded in it 15 | > ![[File with code blocks]] 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /content/013 Custom path.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | path: custom path/should also write to permalink 4 | --- 5 | -------------------------------------------------------------------------------- /content/014 Customer path and permalink.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | path: custom path/should not overwrite permalink 4 | permalink: custom link/shouldBeDifferentToPath 5 | --- 6 | -------------------------------------------------------------------------------- /content/015 Code blocks.md: -------------------------------------------------------------------------------- 1 | --- 2 | dg-publish: true 3 | --- 4 | These codeblocks should not be modified upon publish. 5 | 6 | Sample 1 7 | ```jinja2 8 | {{ highlight_text }}{% if highlight_location and highlight_location_url %} ([via]({{highlight_location_url}})){% elif highlight_location %} ({{highlight_location}}){% endif %} ^rwhi{{highlight_id}} 9 | {% if highlight_note %} 10 | {{ highlight_note }} ^rwhi{{highlight_id}}-note 11 | {% endif %} 12 | ``` 13 | 14 | Sample 2 15 | ```md 16 | In medieval Latin a florilegium (plural florilegia) was a compilation of excerpts from other writings. 17 | The word is from the Latin flos (flower) and legere (to gather): literally a gathering of flowers, or collection of fine extracts from the body of a larger work. ([via](https://en.wikipedia.org/wiki/Florilegium)) ^rwhi724352030 18 | ``` 19 | 20 | Sample 3 21 | ``` 22 | This codeblock has a transclusion syntax in it. 23 | Check it out: ![[001 Links]] 24 | ``` 25 | 26 | And for sanity, here's some block references outside of code blocks: foobar ^test-123 -------------------------------------------------------------------------------- /content/A Assets/travolta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/content/A Assets/travolta.png -------------------------------------------------------------------------------- /content/A Assets/travolta.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/content/A Assets/travolta.webp -------------------------------------------------------------------------------- /content/A Assets/unused_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/content/A Assets/unused_image.png -------------------------------------------------------------------------------- /content/E Embeds/E01 SVG.md: -------------------------------------------------------------------------------- 1 | Here's an SVG embed: 2 | 3 | ![[garden-gate.svg]] -------------------------------------------------------------------------------- /content/E Embeds/E02 PNG published.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | 6 | ![[travolta.png]] -------------------------------------------------------------------------------- /content/E Embeds/E03 PNG_not_published.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: false 3 | --- 4 | 5 | 6 | 7 | ![[unused_image.png|100]] -------------------------------------------------------------------------------- /content/E Embeds/E04 PNG reuse.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | This file uses the same image as in [[E03 PNG_not_published]]. When removing the other one, the image should not be removed. 5 | 6 | ![[unused_image.png|100]] -------------------------------------------------------------------------------- /content/E Embeds/E05 WEBP published.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | ![[travolta.webp]] -------------------------------------------------------------------------------- /content/E Embeds/E07 Image with alt attributes.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | This should render to a 200 px wide image with the alt text "center" 5 | Like so: `![travolta.png|center|200](/img/user/A%20Assets/travolta.png)` 6 | ![[travolta.png|center|200]] 7 | 8 | 9 | This should render to an image with the alt text "left", like so: 10 | `[travolta.png|left](/img/user/A%20Assets/travolta.png)` 11 | ![[travolta.png|left]] -------------------------------------------------------------------------------- /content/E Embeds/Transclusions/T1 BaseFile.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | ![[T2 First transclusion]] -------------------------------------------------------------------------------- /content/E Embeds/Transclusions/T2 Too deep to transclude.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | This one isn't isn't transcluded anymore (too deep) 5 | 6 | ![[travolta.png|100]] -------------------------------------------------------------------------------- /content/E Embeds/Transclusions/T3 Transcluded block.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | Below it should just say "cheese": 6 | 7 | ![[1 File with bookmarked block#^c954d2]] 8 | -------------------------------------------------------------------------------- /content/E Embeds/Transclusions/T4 Transcluded header.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | Below should be a header and one line of text: 6 | ![[0 File with headers#Header]] 7 | 8 | -------------------------------------------------------------------------------- /content/E Embeds/Transclusions/T5 transclude custom filters.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | ![[005 Custom filters]] -------------------------------------------------------------------------------- /content/E Embeds/Transclusions/T6 transclusion inside codeblock.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | #known-issue [Issue](https://github.com/oleeskild/quartz-syncer/issues/113) 5 | 6 | Transclusions inside code blocks should not show transcluded content, but the literal text inside. Currently it transcludes the content 7 | 8 | `![[005 Custom filters]]` 9 | 10 | ``` 11 | ![[005 Custom filters]] 12 | ``` -------------------------------------------------------------------------------- /content/E Embeds/Transclusions/files/T2 First transclusion.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: false 3 | --- 4 | How deep can you go? 5 | 6 | [[T4 Deeper]] 7 | 8 | ![[T3 Second transclusion|Spice it up]] -------------------------------------------------------------------------------- /content/E Embeds/Transclusions/files/T3 Second transclusion.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: false 3 | --- 4 | I can go deeper! 5 | 6 | ![[T4 Deeper]] 7 | 8 | Bonus: 9 | ![[PE1 Transcluded excalidraw]] 10 | 11 | Bonus pic: 12 | 13 | ![[travolta.png|100]] 14 | 15 | -------------------------------------------------------------------------------- /content/E Embeds/Transclusions/files/T4 Deeper.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: false 3 | --- 4 | There must be a limit!!! 5 | 6 | [[T1 BaseFile]] recursive linking yay 7 | 8 | ![[T5 Even deeper]] -------------------------------------------------------------------------------- /content/E Embeds/Transclusions/files/T5 Even deeper.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: false 3 | --- 4 | This is as far as we can go! Or is it??? 5 | 6 | ![[T2 Too deep to transclude| This transclusion will be left out :(())]] -------------------------------------------------------------------------------- /content/E Embeds/garden-gate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /content/Excalidraw/with image.excalidraw.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | excalidraw-plugin: parsed 4 | tags: [excalidraw] 5 | 6 | --- 7 | ==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠== 8 | 9 | 10 | # Text Elements 11 | tis an image ^etjlThVL 12 | 13 | it's not uploaded automatically ^Pu1GJH4c 14 | 15 | this is a bug of sorts ^m40NMLqr 16 | 17 | 18 | # Embedded files 19 | f5de7e7b9672dcaec815dbbc90d72635f638da20: [[travolta.png]] 20 | 21 | %% 22 | # Drawing 23 | ```json 24 | { 25 | "type": "excalidraw", 26 | "version": 2, 27 | "source": "https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.9.19", 28 | "elements": [ 29 | { 30 | "type": "text", 31 | "version": 17, 32 | "versionNonce": 1765827278, 33 | "isDeleted": false, 34 | "id": "etjlThVL", 35 | "fillStyle": "hachure", 36 | "strokeWidth": 1, 37 | "strokeStyle": "solid", 38 | "roughness": 1, 39 | "opacity": 100, 40 | "angle": 0, 41 | "x": -87.19921875, 42 | "y": 99.1875, 43 | "strokeColor": "#1e1e1e", 44 | "backgroundColor": "transparent", 45 | "width": 120.55990600585938, 46 | "height": 25, 47 | "seed": 1087805266, 48 | "groupIds": [], 49 | "frameId": null, 50 | "roundness": null, 51 | "boundElements": [], 52 | "updated": 1696178356575, 53 | "link": null, 54 | "locked": false, 55 | "fontSize": 20, 56 | "fontFamily": 1, 57 | "text": "tis an image", 58 | "rawText": "tis an image", 59 | "textAlign": "left", 60 | "verticalAlign": "top", 61 | "containerId": null, 62 | "originalText": "tis an image", 63 | "lineHeight": 1.25, 64 | "baseline": 18 65 | }, 66 | { 67 | "type": "text", 68 | "version": 91, 69 | "versionNonce": 1701927762, 70 | "isDeleted": false, 71 | "id": "Pu1GJH4c", 72 | "fillStyle": "hachure", 73 | "strokeWidth": 1, 74 | "strokeStyle": "solid", 75 | "roughness": 1, 76 | "opacity": 100, 77 | "angle": 0, 78 | "x": -149.30078125, 79 | "y": 152.25390625, 80 | "strokeColor": "#1e1e1e", 81 | "backgroundColor": "transparent", 82 | "width": 310.2197265625, 83 | "height": 25, 84 | "seed": 1793030738, 85 | "groupIds": [], 86 | "frameId": null, 87 | "roundness": null, 88 | "boundElements": [], 89 | "updated": 1696178434561, 90 | "link": null, 91 | "locked": false, 92 | "fontSize": 20, 93 | "fontFamily": 1, 94 | "text": "it's not uploaded automatically", 95 | "rawText": "it's not uploaded automatically", 96 | "textAlign": "left", 97 | "verticalAlign": "top", 98 | "containerId": null, 99 | "originalText": "it's not uploaded automatically", 100 | "lineHeight": 1.25, 101 | "baseline": 18 102 | }, 103 | { 104 | "type": "text", 105 | "version": 23, 106 | "versionNonce": 2137370830, 107 | "isDeleted": false, 108 | "id": "m40NMLqr", 109 | "fillStyle": "hachure", 110 | "strokeWidth": 1, 111 | "strokeStyle": "solid", 112 | "roughness": 1, 113 | "opacity": 100, 114 | "angle": 0, 115 | "x": -130.09765625, 116 | "y": 232.0859375, 117 | "strokeColor": "#1e1e1e", 118 | "backgroundColor": "transparent", 119 | "width": 220.039794921875, 120 | "height": 25, 121 | "seed": 546057938, 122 | "groupIds": [], 123 | "frameId": null, 124 | "roundness": null, 125 | "boundElements": [], 126 | "updated": 1696178440853, 127 | "link": null, 128 | "locked": false, 129 | "fontSize": 20, 130 | "fontFamily": 1, 131 | "text": "this is a bug of sorts", 132 | "rawText": "this is a bug of sorts", 133 | "textAlign": "left", 134 | "verticalAlign": "top", 135 | "containerId": null, 136 | "originalText": "this is a bug of sorts", 137 | "lineHeight": 1.25, 138 | "baseline": 18 139 | }, 140 | { 141 | "id": "HcrLXEvs44rS67hJzeV1v", 142 | "type": "image", 143 | "x": -175.90966796875, 144 | "y": -266.54738451086945, 145 | "width": 356.52173913043475, 146 | "height": 356.52173913043475, 147 | "angle": 0, 148 | "strokeColor": "transparent", 149 | "backgroundColor": "transparent", 150 | "fillStyle": "hachure", 151 | "strokeWidth": 1, 152 | "strokeStyle": "solid", 153 | "roughness": 1, 154 | "opacity": 100, 155 | "groupIds": [], 156 | "frameId": null, 157 | "roundness": null, 158 | "seed": 1427244047, 159 | "version": 24, 160 | "versionNonce": 350668687, 161 | "isDeleted": false, 162 | "boundElements": null, 163 | "updated": 1696271725883, 164 | "link": null, 165 | "locked": false, 166 | "status": "pending", 167 | "fileId": "f5de7e7b9672dcaec815dbbc90d72635f638da20", 168 | "scale": [ 169 | 1, 170 | 1 171 | ] 172 | } 173 | ], 174 | "appState": { 175 | "theme": "light", 176 | "viewBackgroundColor": "#ffffff", 177 | "currentItemStrokeColor": "#1e1e1e", 178 | "currentItemBackgroundColor": "transparent", 179 | "currentItemFillStyle": "hachure", 180 | "currentItemStrokeWidth": 1, 181 | "currentItemStrokeStyle": "solid", 182 | "currentItemRoughness": 1, 183 | "currentItemOpacity": 100, 184 | "currentItemFontFamily": 1, 185 | "currentItemFontSize": 20, 186 | "currentItemTextAlign": "left", 187 | "currentItemStartArrowhead": null, 188 | "currentItemEndArrowhead": "arrow", 189 | "scrollX": 156.7996136209239, 190 | "scrollY": 234.79127038043467, 191 | "zoom": { 192 | "value": 1.1500000000000001 193 | }, 194 | "currentItemRoundness": "round", 195 | "gridSize": null, 196 | "gridColor": { 197 | "Bold": "#C9C9C9FF", 198 | "Regular": "#EDEDEDFF" 199 | }, 200 | "currentStrokeOptions": null, 201 | "previousGridSize": null, 202 | "frameRendering": { 203 | "enabled": true, 204 | "clip": true, 205 | "name": true, 206 | "outline": true 207 | } 208 | }, 209 | "files": {} 210 | } 211 | ``` 212 | %% -------------------------------------------------------------------------------- /content/L Languages/Chinese.md: -------------------------------------------------------------------------------- 1 | 2 | Here is a header with only Chinese 3 | # 解决 4 | 5 | This should be visible when transcluding the header above 6 | 7 | 8 | # 使用 9 | This should not be visible in transclusions 10 | -------------------------------------------------------------------------------- /content/L Languages/Transclude Headers.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | ![[Chinese#解决]] 6 | -------------------------------------------------------------------------------- /content/L Links/01 Link to header.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | Link to header should keep header link info 5 | [[000 Home#Welcome]] 6 | 7 | Link to header with special characters should work 8 | ![[02 Header with special character#A header With a colon]] -------------------------------------------------------------------------------- /content/L Links/02 Header with special character.md: -------------------------------------------------------------------------------- 1 | # A header: With a colon 2 | Body under header 3 | 4 | # Another Header 5 | Not expected to be part of transclusion -------------------------------------------------------------------------------- /content/P Plugins/PD Dataview/PD0 - note with summary.md: -------------------------------------------------------------------------------- 1 | [summary:: this note is about foo] 2 | -------------------------------------------------------------------------------- /content/P Plugins/PD Dataview/PD1 Dataview.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | I'm a list of all files in this folder: 5 | 6 | ```dataview 7 | list 8 | from "P Plugins" 9 | ``` 10 | -------------------------------------------------------------------------------- /content/P Plugins/PD Dataview/PD2 Inline queries.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | 6 | `=this.file.name` 7 | 8 | `= [[PD0 - note with summary]].summary` 9 | -------------------------------------------------------------------------------- /content/P Plugins/PD Dataview/PD3 Inline JS queries.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | `$=2+1` 6 | `$=dv.pages().length + dv.pages().length` 7 | `$=dv.paragraph("A paragraph")` 8 | -------------------------------------------------------------------------------- /content/P Plugins/PD Dataview/PD4 DataviewJs queries.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | ```dataviewjs 5 | dv.header(2, 'Header 2'); 6 | let pg = dv.current().file.name; 7 | dv.paragraph(pg) 8 | ``` 9 | 10 | ```dataviewjs 11 | dv.table(["name", "link"], 12 | dv.pages() 13 | .where(p => p.file.name.includes("Custom")) 14 | .map(p => [ 15 | p.file.name, 16 | p.file.link 17 | ]) 18 | ) 19 | ``` 20 | 21 | -------------------------------------------------------------------------------- /content/P Plugins/PE Excalidraw/PE1 Transcluded excalidraw.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | 5 | ![[Drawing 2023-09-23 22.41.09.excalidraw]] -------------------------------------------------------------------------------- /content/P Plugins/PE Excalidraw/PE2 excalidraw with image.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | #known-issue 5 | 6 | 7 | ![[with image.excalidraw]] 8 | 9 | -------------------------------------------------------------------------------- /content/Path Rewriting/004 Folder set to root.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | This folder is set in path rewrite settings as 5 | 6 | `folder:` 7 | 8 | This means this file should be in the root directory :) 9 | 10 | This subfolder also contains path rewrite testing! 11 | -------------------------------------------------------------------------------- /content/Path Rewriting/Subfolder/How deep do the rewrite rules go.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | With the rewrite rules: 5 | 6 | ``` 7 | Path Rewriting: 8 | Subfolder:subfolder-rewritten 9 | Path Rewriting/Subfolder:this-will-never-hit 10 | ``` 11 | 12 | Will this file be in folder `subfolder-rewritten` or in `Subfolder`? 13 | 14 | It should be in Subfolder as "matching exits on first hit" 15 | -------------------------------------------------------------------------------- /content/Path Rewriting/Subfolder2/More specific path rewriting.md: -------------------------------------------------------------------------------- 1 | --- 2 | publish: true 3 | --- 4 | This Subfolder has been rewritten before the rule to rewrite the Path Rewriting folder to root 5 | 6 | ``` 7 | Path Rewriting/Subfolder2:fun-folder 8 | Path Rewriting: 9 | ``` -------------------------------------------------------------------------------- /docs/Changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Roadmap and Changelog 3 | description: Changelog and feature roadmap for Quartz Syncer. 4 | created: 2025-05-16T12:59:31Z+0200 5 | modified: 2025-06-10T22:47:21Z+0200 6 | publish: true 7 | --- 8 | 9 | ## Upcoming 10 | 11 | ## Planned 12 | 13 | - Excalidraw support. 14 | - Canvas support. 15 | - Built-in Quartz Themes support. 16 | - TTRPG-related plugin support. 17 | 18 | ## Someday 19 | 20 | - Manage Quartz configuration. 21 | - Manage Quartz layout. 22 | - Manage Quartz components. 23 | 24 | ## Released 25 | 26 | ### Version 1.7.1 27 | 28 | - Datacore integration improvements. 29 | - Fixed Datacore callouts. 30 | - Fixed aliases and permalinks not passing through when set inside note properties. 31 | - Updated documentation. 32 | 33 | ### Version 1.7.0 34 | 35 | - Experimental [[Settings/Integrations/Datacore|Datacore]] support. 36 | - Updated documentation. 37 | 38 | ### Version 1.6.9 39 | 40 | - Fixed MathJax embedding inside callouts and multi-layer embeddings. 41 | - Updated documentation. 42 | 43 | ### Version 1.6.8 44 | 45 | - Fixed DataviewJS queries not always returning correct data. 46 | - Fixed MathJax collapsing to inline queries when embedded. 47 | - Updated documentation. 48 | 49 | ### Version 1.6.7 50 | 51 | - Fixed issues related to alias generation on non-embedded links to blocks or headings. 52 | - Removed redundant `decodeURI` call on blobs. 53 | - Removed redundant function calls in Dataview processor. 54 | - Removed unused utility functions. 55 | - Replaced many CSS rules with Obsidian CSS properties to better integrate with Obsidian themes. 56 | - Fixed grammatical error in embed settings description. 57 | - Expanded settings tabs to show names on desktop. 58 | - Tablet and mobile still only show name on the active tab. 59 | - Updated dependencies. 60 | 61 | ### Version 1.6.6 62 | 63 | - Re-implemented the settings modal to use built-in Obsidian functionality. 64 | - Settings that are overridden by other settings are now automatically hidden. 65 | - Settings modal will now remember the last settings tab opened, instead of always opening the GitHub tab. 66 | - Added missing heading to the Frontmatter settings tab. 67 | - Removed redundant rendering calls. 68 | - Restructured import statements. 69 | 70 | ### Version 1.6.5 71 | 72 | - Fixed icons in publication center not showing correctly. 73 | - Fixed broken link in project README file. 74 | - Removed unnecessary warning. 75 | - Updated manifest. 76 | 77 | ### Version 1.6.4 78 | 79 | - Fixed embedded math blocks incorrectly parsing to inlined math blocks. 80 | - Moved all inlined styling to CSS classes. 81 | - Changed buttons in publication center to sticky. 82 | - This makes the buttons always visible at the bottom of the modal, even when displaying larger tree. 83 | - Removed unused code. 84 | - Updated manifest. 85 | - Updated documentation. 86 | 87 | ### Version 1.6.3 88 | 89 | - Fixed mobile folder selector styling. 90 | 91 | ### Version 1.6.2 92 | 93 | - Use Obsidian built-in for rendering Dataviewjs to markdown. 94 | 95 | ### Version 1.6.1 96 | 97 | - Implemented Obsidian built-in folder suggester. 98 | - Removed now-unused popperjs dependency. 99 | - Moved cog loading animation styling to CSS. 100 | 101 | ### Version 1.6.0 102 | 103 | - Fixed embedding issues when embedding blocks. 104 | - Embeddings are now handled by Quartz Syncer instead of Quartz. 105 | - This behavior can be configured in the settings `Quartz > Apply embeds`. 106 | - Added settings menu for Quartz Themes. 107 | - This currently does nothing, but will be used for managing and applying Obsidian themes to Quartz. 108 | - Fixed some minor typos. 109 | - Updated documentation. 110 | 111 | ### Version 1.5.3 112 | 113 | - Added note counts to the publication center. 114 | - Added tabbed settings menu. 115 | - Updated documentation. 116 | 117 | ### Version 1.5.2 118 | 119 | - Restored use of `innerHTML` in the Dataview compiler. 120 | - This only affects `dataviewjs` queries. These are an optional, opt-in component of the Dataview plugin. See [Dataview JavaScript API documentation](https://blacksmithgu.github.io/obsidian-dataview/api/intro/) for details. 121 | 122 | ### Version 1.5.1 123 | 124 | - Addressed Obsidian's automated feedback. 125 | 126 | ### Version 1.5.0 127 | 128 | - Added to Obsidian Community Plugin list. 129 | - Added separate settings for configuring note properties/frontmatter. 130 | - Added option to only pass valid Quartz properties. 131 | - This can be overridden by setting the new `Include all properties` option. 132 | - Updated publication center headings to better reflect functionality. 133 | - Cleaned up all unused and redundant functions. 134 | - Finished up all of the essential documentation. 135 | 136 | ### Version 1.4.1 137 | 138 | - Fixed links to embedded notes resolving to the wrong URL when deploying from a subfolder. 139 | 140 | ### Version 1.4.0 141 | 142 | - Added support for setting a vault folder as Quartz content folder. 143 | - This setting is recommended for users with mixed content vaults or users who want to dedicate a specific folder to their Quartz site content. 144 | - Specific folder can be configured in the plugin settings. 145 | - Added configuration option for specifying publish frontmatter key. 146 | - The default key is `publish`, matching Obsidian Publish's default key. 147 | - The frontmatter key is used to expose notes to Quartz Syncer. Any note without the specified key or eith they key set to `false`/unchecked checkbox will not show up in Quartz Syncer. 148 | - The key should be a boolean/Checkbox (source mode/live-preview mode respectively.) 149 | - Added Obsidian commands for modifying a note's publication status: 150 | - `Quartz Syncer: Add publish flag`: adds the specified publish flag to the current note frontmatter if not already present and sets it to `true`. This exposes the note to Quartz Syncer. 151 | - `Quartz Syncer: Remove publish flag`: adds the specified publish flag to the current note frontmatter if not already present and sets it to `false`. This hides the note from Quartz Syncer. 152 | - `Quartz Syncer: Toggle publication status`: toggles the note's visibility to Quartz Syncer. 153 | - Added support for all embed filetypes that Quartz also supports. This includes the following: 154 | - Note: `md` 155 | - Image: `png`, `jpg`, `jpeg`, `gif`, `webp`, `svg` 156 | - Audio (new): `mp3`, `wav`, `ogg` 157 | - Video (new): `mp4`, `mkv`, `mov`, `avi` 158 | - Document (new): `pdf` 159 | - Added a new icon. 160 | - Added this documention website. 161 | - Fixed deleting multiple files in single commit. 162 | 163 | ### Version 1.3.1 164 | 165 | - Fixed Dataview query results not displaying correctly inside nested callouts. 166 | 167 | ### Version 1.3.0 168 | 169 | - Exposed the following options for user configuration in plugin settings view: 170 | - Pass frontmatter. 171 | - Image file compression. 172 | - Use permalink for Quartz URL. 173 | - Pass note's created date to Quartz. 174 | - Pass note's last modification date to Quartz. 175 | - Fixed incorrect links to embedded notes. 176 | - Fixed path resolution resolving to wrong path in some cases. 177 | 178 | ### Version 1.2.0 179 | 180 | - Initial beta release as Quartz Syncer. 181 | 182 | ### Version 1.1.0 183 | 184 | - Support for configuring Quartz path. 185 | 186 | ### Version 1.0.0 187 | 188 | - Forked from [Ole Eskild Steensen](https://github.com/oleeskild)'s [Digital Garden plugin](https://github.com/oleeskild/obsidian-digital-garden). 189 | -------------------------------------------------------------------------------- /docs/Guides/Configuring a specific folder for Quartz content.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuring a specific folder for Quartz content 3 | description: Guide on how to configure a specific folder in your vault for Quartz instead of your entire vault. 4 | created: 2025-05-16T11:22:39Z+0200 5 | modified: 2025-05-20T12:00:57Z+0200 6 | publish: true 7 | tags: [guides] 8 | --- 9 | 10 | > [!IMPORTANT] Mixed content vaults 11 | > 12 | > By default, Quartz Syncer assumes your entire vault is used for Quartz content. You can change this behavior by configuring a specific vault folder using the [[Vault root folder name]] setting. 13 | 14 | ## Configuring a specific vault folder 15 | 16 | Open Quartz Syncer settings (`Settings > Community Plugin > Quartz Syncer`) and navigate to the `Vault root folder name` setting. Start typing the folder name in the search box. The search box in this setting automatically matches any folder in your Obsidian vault. 17 | 18 | To use your entire vault, set the folder to `/` or leave the search box empty. This is the default behavior. 19 | 20 | ### Setting effects 21 | 22 | > [!HINT] Don't forget to add an `index.md` to your configured vault folder 23 | > 24 | > This `index.md` note will serve as your Quartz website landing page. 25 | 26 | When a folder other than the vault root (`/`) is configured, the following changes are made when compiling notes for Quartz: 27 | 28 | - All internal links are rewritten to remove the path to the folder. 29 | - If [[Settings/Integrations/Dataview|Dataview integration]] is enabled, all Dataview query results are rewritten to remove the path to the folder. 30 | - All internal embeds links are rewritten to remove the path to the folder. 31 | 32 | The final result that is deployed to your Quartz content folder is as if your configured folder is the root of your website. 33 | -------------------------------------------------------------------------------- /docs/Guides/Generating an access token.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Generating a fine-grained access token 3 | description: Guide on how to generated an authentication token for GitHub. 4 | created: 2025-05-15T00:00:00Z+0200 5 | modified: 2025-05-21T00:05:10Z+0200 6 | publish: true 7 | tags: [guides] 8 | --- 9 | 10 | > [!IMPORTANT] Expiration dates 11 | > A fine-grained authentication token expires after the specified date. Tokens always expire after one year, even if the expiration date is unset or set further in the future than one year. 12 | 13 | ## Generating a fine-grained access token 14 | 15 | 1. Go to [this page](https://github.com/settings/personal-access-tokens/new) and apply the following settings: 16 | 1. *Token name*: The name to identify this token. I'd recommend something that indicates it is for Quartz Syncer, like `Quartz Syncer token`. ![[access-token-name.png]] 17 | 2. *Expiration*: When this token will expire. Defaults to 30 days from now. GitHub will send you an email when your token is about to expire. ![[access-token-expiration-date.png]] 18 | 3. *Repository access*: Select **Only select repositories** and in the drop-down select your Quartz repository. ![[access-token-repository-access.png]] 19 | 4. *Permissions*: Click **Repository permissions** to open all options. ![[access-token-permissions-options.png]] 20 | 5. Scroll to the **Contents** option and change *Access: No access* to *Access: Read and write*. This will allow Quartz Syncer to manage your Quartz' content folder. ![[access-token-contents-permission.png]] 21 | 2. Now scroll down and click the button that says **Generate token**. ![[access-token-generate-token-button.png]] 22 | 3. A popup with show with the current settings. Click **Generate token** to confirm. ![[access-token-confirmation-popup.png]] 23 | 4. Click the copy button to copy the generated access token. ![[access-token-copy-generated-token.png]] 24 | 5. Open Obsidian. 25 | 6. Open Obsidian's settings and click on **Quartz Syncer** under *Community Plugins*. 26 | 7. Paste the generated token in the **GitHub token** field. ![[access-token-obsidian-settings.png]] 27 | 28 | ## Generating a classic access token 29 | 30 | > [!DANGER] Classic access tokens have access to all repositories. If possible, use a fine-grained access token! 31 | 32 | To generate a classic access token, [click here](https://github.com/settings/tokens/new?scopes=repo). Add a **Note** and click **Generate token** at the bottom. 33 | -------------------------------------------------------------------------------- /docs/Guides/Using an Obsidian theme in Quartz.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using an Obsidian theme in Quartz 3 | description: Guide on how to use Quartz Themes to use an Obsidian theme in Quartz. 4 | created: 2025-05-16T11:05:44Z+0200 5 | modified: 2025-05-20T12:00:52Z+0200 6 | publish: true 7 | tags: [guides] 8 | --- 9 | 10 | > [!IMPORTANT] Quartz Themes 11 | > 12 | > [Quartz Themes](https://github.com/saberzero1/quartz-themes) is a separate project from the author of Quartz Syncer. It aims to regularly port the current set of available Obsidian Community Themes to a Quartz-compatible format. 13 | > 14 | -------------------------------------------------------------------------------- /docs/Guides/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Guides 3 | description: Guides and tutorials for using Quartz Syncer. 4 | created: 2025-05-15T00:00:00Z+0200 5 | modified: 2025-05-20T20:32:45Z+0200 6 | publish: true 7 | --- 8 | 9 | ```dataview 10 | TABLE WITHOUT ID file.link AS Category, file.frontmatter.description AS Description 11 | WHERE file.folder = this.file.folder 12 | WHERE file != this.file 13 | SORT file.frontmatter.title ASC 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/Media/Access Token/access-token-confirmation-popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/docs/Media/Access Token/access-token-confirmation-popup.png -------------------------------------------------------------------------------- /docs/Media/Access Token/access-token-contents-permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/docs/Media/Access Token/access-token-contents-permission.png -------------------------------------------------------------------------------- /docs/Media/Access Token/access-token-copy-generated-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/docs/Media/Access Token/access-token-copy-generated-token.png -------------------------------------------------------------------------------- /docs/Media/Access Token/access-token-expiration-date.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/docs/Media/Access Token/access-token-expiration-date.png -------------------------------------------------------------------------------- /docs/Media/Access Token/access-token-generate-token-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/docs/Media/Access Token/access-token-generate-token-button.png -------------------------------------------------------------------------------- /docs/Media/Access Token/access-token-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/docs/Media/Access Token/access-token-name.png -------------------------------------------------------------------------------- /docs/Media/Access Token/access-token-obsidian-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/docs/Media/Access Token/access-token-obsidian-settings.png -------------------------------------------------------------------------------- /docs/Media/Access Token/access-token-permissions-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/docs/Media/Access Token/access-token-permissions-options.png -------------------------------------------------------------------------------- /docs/Media/Access Token/access-token-repository-access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saberzero1/quartz-syncer/5b4399314b35672dea0f201808c2cd24d6704c16/docs/Media/Access Token/access-token-repository-access.png -------------------------------------------------------------------------------- /docs/Settings/GitHub/Access token.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Access token 3 | description: Access token to authenticate to GitHub. 4 | created: 2025-05-15T12:23:56Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/github] 8 | default_value: '`""`' 9 | --- 10 | -------------------------------------------------------------------------------- /docs/Settings/GitHub/Repository name.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Repository Name 3 | description: Quartz repository name on GitHub. 4 | created: 2025-05-15T11:07:52Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/github] 8 | default_value: '`"quartz"`' 9 | --- 10 | -------------------------------------------------------------------------------- /docs/Settings/GitHub/Username.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Username 3 | description: Username or organization that own the Quartz repository on GitHub. 4 | created: 2025-05-15T11:05:41Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/github] 8 | default_value: '`""`' 9 | --- 10 | 11 | modified: 20 12 | -------------------------------------------------------------------------------- /docs/Settings/GitHub/Vault root folder name.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vault root folder name 3 | description: Folder in vault to use for Quartz notes. 4 | created: 2025-05-15T12:24:59Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/github] 8 | default_value: "`/` (entire vault)" 9 | --- 10 | -------------------------------------------------------------------------------- /docs/Settings/GitHub/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GitHub 3 | description: Quartz Syncer settings related to GitHub. 4 | created: 2025-05-15T10:59:23Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/github] 8 | --- 9 | 10 | ```dataview 11 | TABLE WITHOUT ID file.link AS Category, file.frontmatter.description AS Description, file.frontmatter.default_value AS "Default value" 12 | WHERE file.folder = this.file.folder 13 | WHERE file != this.file 14 | SORT file.frontmatter.title ASC 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/Settings/Integrations/Datacore.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Datacore 3 | description: Whether to enable support for the Datacore plugin. Requires Datacore to be installed and enabled. 4 | created: 2025-06-09T20:48:56Z+0200 5 | modified: 2025-06-10T22:22:53Z+0200 6 | publish: true 7 | tags: [datacore, integration, settings/integrations] 8 | default_value: "false" 9 | --- 10 | 11 | > [!WARNING] Datacore is still in early development 12 | > 13 | > Not all features may work correctly 14 | 15 | ## Supported features 16 | 17 | ### Datacore Views 18 | 19 | ```js title="datacorejsx" 20 | return function View() { 21 | return

Hello!

; 22 | } 23 | ``` 24 | 25 | ```datacorejsx 26 | return function View() { 27 | return

Hello!

; 28 | } 29 | ``` 30 | 31 | ### Datacore Lists 32 | 33 | ```js title="datacorejsx" 34 | return function View() { 35 | const pages = dc.useQuery('@page and #datacore'); 36 | 37 | return pages.$link} />; 38 | } 39 | ``` 40 | 41 | ```datacorejsx 42 | return function View() { 43 | const pages = dc.useQuery('@page and #datacore'); 44 | 45 | return pages.$link} />; 46 | } 47 | ``` 48 | 49 | ### Datacore Tables 50 | 51 | ```js title="datacorejsx" 52 | return function View() { 53 | const pages = dc.useQuery("@page and #datacore"); 54 | 55 | const COLUMNS = [ 56 | {id: "Name", value: page => page.$link}, 57 | {id: "Tags", value: page => page.$tags} 58 | ]; 59 | 60 | return ; 61 | } 62 | ``` 63 | 64 | ```datacorejsx 65 | return function View() { 66 | const pages = dc.useQuery("@page and #datacore"); 67 | 68 | const COLUMNS = [ 69 | {id: "Name", value: page => page.$link}, 70 | {id: "Tags", value: page => page.$tags} 71 | ]; 72 | 73 | return ; 74 | } 75 | ``` 76 | 77 | ### Datacore Cards 78 | 79 | ```js title="datacorejsx" 80 | return function View() { 81 | return ; 82 | } 83 | ``` 84 | 85 | ```datacorejsx 86 | return function View() { 87 | return ; 88 | } 89 | ``` 90 | 91 | ### Datacore Callouts 92 | 93 | ```js title="datacorejsx" 94 | return function View() { 95 | return Hello!; 96 | } 97 | ``` 98 | 99 | ```datacorejsx 100 | return function View() { 101 | return Hello!; 102 | } 103 | ``` 104 | 105 | ## See also 106 | 107 | - [Obsidian Rocks article on Datacore](https://obsidian.rocks/getting-started-with-datacore/), whose query examples are rendered above. 108 | - [Datacore documentation](https://blacksmithgu.github.io/datacore/) 109 | -------------------------------------------------------------------------------- /docs/Settings/Integrations/Dataview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dataview 3 | description: Whether to enable support for the Dataview plugin. Requires Dataview to be installed and enabled. 4 | created: 2025-05-15T15:53:42Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [dataview, integration, settings/integrations] 8 | default_value: "true" 9 | --- 10 | -------------------------------------------------------------------------------- /docs/Settings/Integrations/Excalidraw.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Excalidraw 3 | description: Whether to enable support for the Excalidraw plugin. Feature is currently disabled. 4 | created: 2025-05-15T20:32:34Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [excalidraw, integration, settings/integrations] 8 | default_value: "false" 9 | --- 10 | 11 | > [!WARNING] Upcoming feature 12 | > Excalidraw support is not fully implemented yet. 13 | -------------------------------------------------------------------------------- /docs/Settings/Integrations/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Integrations 3 | description: Quartz Syncer settings related to integrations with other Obsidian plugins. 4 | created: 2025-05-15T15:52:53Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/integrations] 8 | --- 9 | 10 | ```dataview 11 | TABLE WITHOUT ID file.link AS Category, file.frontmatter.description AS Description, file.frontmatter.default_value AS "Default value" 12 | WHERE file.folder = this.file.folder 13 | WHERE file != this.file 14 | SORT file.frontmatter.title ASC 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/Settings/Note properties/Include all properties.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Include all properties 3 | description: Whether to include all note properties instead of only note properties that are used by Quartz. 4 | created: 2025-05-17T17:19:43Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/frontmatter] 8 | default_value: "false" 9 | --- 10 | 11 | > [!WARNING] This setting will override other settings 12 | > 13 | > When enabled, will override. 14 | -------------------------------------------------------------------------------- /docs/Settings/Note properties/Include created timestamp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Include created timestamp 3 | description: Whether to pass a note property to set creation date in Quartz. Required when `defaultDateType` in Quartz is set to "created". 4 | created: 2025-05-17T17:22:36Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/frontmatter] 8 | default_value: "true" 9 | --- 10 | -------------------------------------------------------------------------------- /docs/Settings/Note properties/Include modified timestamp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Include modified timestamp 3 | description: Whether to pass a note property to set modification date in Quartz. Required when `defaultDateType` in Quartz is set to "modified". 4 | created: 2025-05-17T17:26:44Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/frontmatter] 8 | default_value: "true" 9 | --- 10 | -------------------------------------------------------------------------------- /docs/Settings/Note properties/Include published timestamp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Include published timestamp 3 | description: Whether to pass a note property to set publication date in Quartz. Required when `defaultDateType` in Quartz is set to "published". 4 | created: 2025-05-17T17:29:12Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/frontmatter] 8 | default_value: "false" 9 | --- 10 | -------------------------------------------------------------------------------- /docs/Settings/Note properties/Publish key.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Publish key 3 | description: What frontmatter key to check for to determine whether a note is visible to Quartz Syncer. 4 | created: 2025-05-17T09:59:49Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/frontmatter] 8 | default_value: "`publish`" 9 | --- 10 | -------------------------------------------------------------------------------- /docs/Settings/Note properties/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Note properties (frontmatter) 3 | description: Quartz Syncer settings related to note properties or frontmatter. 4 | created: 2025-05-17T15:08:00Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/frontmatter] 8 | --- 9 | 10 | ```dataview 11 | TABLE WITHOUT ID file.link AS Category, file.frontmatter.description AS Description, file.frontmatter.default_value AS "Default value" 12 | WHERE file.folder = this.file.folder 13 | WHERE file != this.file 14 | SORT file.frontmatter.title ASC 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/Settings/Quartz/Apply embeds.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Apply embeds 3 | description: Whether to let Quartz Syncer should handle link embeddings. 4 | created: 2025-05-21T11:39:18Z+0200 5 | modified: 2025-06-09T20:06:08Z+0200 6 | publish: true 7 | tags: [settings/quartz] 8 | default_value: "true" 9 | --- 10 | 11 | When enabled, embed links are handled by Quartz Syncer. 12 | 13 | ## Recommended configuration 14 | 15 | This setting should be **Enabled** if: 16 | 17 | - You want to embed (parts of) non-published notes inside published notes. 18 | - You don't want embedded content to be a separate page. 19 | - You don't want links to the original content next to your embeddings. 20 | - Your notes are composed mostly of embed, and you want clean pages instead of many links. 21 | 22 | This setting should be **Disabled** if: 23 | 24 | - You want to include embedded content as separate pages as well. 25 | - You want to prevent accidentally embedding private content. (Embedding a non-published note will result in a broken in link in Quartz, instead of embedded content.) 26 | - You have implemented custom logic for embedding in Quartz that requires embed links to be passed to Quartz. 27 | 28 | > [!EXAMPLE] Example 29 | > 30 | > Assuming the following notes are to be published: 31 | > 32 | > ```markdown 33 | > // index.md 34 | > # Hello 35 | > 36 | > ![[embed]] 37 | > ``` 38 | > 39 | > ```markdown 40 | > // embed.md 41 | > ## Goodbye 42 | > 43 | > Look mom, I'm embedded! 44 | > ``` 45 | > 46 | > > [!SUCCESS] `Apply embeds` enabled 47 | > > 48 | > > ```markdown 49 | > > // index.md 50 | > > # Hello 51 | > > 52 | > > ## Goodbye 53 | > > 54 | > > Look mom, I'm embedded! 55 | > > ``` 56 | > > 57 | > > ```markdown 58 | > > // embed.md 59 | > > // This file is not published, unless explicitly marked with the configured publish flag. 60 | > > ``` 61 | > 62 | > > [!FAILURE] `Apply embeds` disabled 63 | > > 64 | > > ```markdown 65 | > > // index.md 66 | > > # Hello 67 | > > 68 | > > ![[embed]] 69 | > > ``` 70 | > > 71 | > > ```markdown 72 | > > // embed.md 73 | > > ## Goodbye 74 | > > 75 | > > Look mom, I'm Embedded! 76 | > > ``` 77 | -------------------------------------------------------------------------------- /docs/Settings/Quartz/Content folder.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Content folder 3 | description: What folder in your Quartz repository notes should be published to. 4 | created: 2025-05-16T12:21:25Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/quartz] 8 | default_value: "`content`" 9 | --- 10 | 11 | > [!WARNING] Quartz expects `content` by default 12 | > 13 | > Unless you have customized your Quartz repository to use a different folder for your notes, it is recommended to keep this setting unchanged. 14 | -------------------------------------------------------------------------------- /docs/Settings/Quartz/Use full image resolution.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Use full image resolution 3 | description: Whether to use full resolution or to apply compression to image assets. 4 | created: 2025-05-17T10:02:24Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/quartz] 8 | default_value: "true" 9 | --- 10 | -------------------------------------------------------------------------------- /docs/Settings/Quartz/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quartz 3 | description: Quartz Syncer settings related to Quartz. 4 | created: 2025-05-15T16:03:13Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/quartz] 8 | --- 9 | 10 | ```dataview 11 | TABLE WITHOUT ID file.link AS Category, file.frontmatter.description AS Description, file.frontmatter.default_value AS "Default value" 12 | WHERE file.folder = this.file.folder 13 | WHERE file != this.file 14 | SORT file.frontmatter.title ASC 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/Settings/Themes/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Themes 3 | description: Quartz Syncer settings related to Quartz Themes. 4 | created: 2025-05-15T11:00:30Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [settings/themes, themes] 8 | --- 9 | 10 | > [!WARNING] Upcoming feature 11 | > Configuring Quartz Themes using Quartz Syncer is an upcoming feature. It it not released yet. 12 | > 13 | > If you want to use an Obsidian theme in Quartz right now, see the [Quartz Themes GitHub repository](https://github.com/saberzero1/quartz-themes#installation) for installation instructions. 14 | 15 | ```dataview 16 | TABLE WITHOUT ID file.link AS Category, file.frontmatter.description AS Description 17 | WHERE file.folder = this.file.folder 18 | WHERE file != this.file 19 | SORT file.frontmatter.title ASC 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/Settings/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Settings 3 | description: Overview of all settings. 4 | created: 2025-05-07T22:37:11Z+0200 5 | modified: 2025-05-21T19:59:04Z+0200 6 | publish: true 7 | tags: [settings] 8 | --- 9 | 10 | ## Settings 11 | 12 | ```dataview 13 | TABLE WITHOUT ID link(file.link, file.frontmatter.title) AS Category, file.frontmatter.description AS Description 14 | WHERE startswith(file.folder, this.file.folder) 15 | WHERE file != this.file 16 | WHERE file.name = "index" 17 | SORT file.frontmatter.title ASC 18 | ``` 19 | 20 | ## Commands 21 | 22 | | Command | Effect | 23 | | --- | --- | 24 | | `Quartz Syncer: Open publication center` | Opens the modal to manage the Quartz content on GitHub. | 25 | | `Quartz Syncer: Add publish flag` | Adds the configured publish flag to the frontmatter and sets it to `true`. | 26 | | `Quartz Syncer: Remove publish flag` | Adds the configured publish flag to the frontmatter and sets it to `false`. | 27 | | `Quartz Syncer: Toggle publication status` | Adds the configured publish flag to the frontmatter and toggles it between `true` and `false`. | 28 | -------------------------------------------------------------------------------- /docs/Setup Guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup Guide 3 | description: Instructions for setting up Quartz Syncer plugin. 4 | created: 2025-05-05T12:00:00Z+0200 5 | modified: 2025-05-31T13:01:56Z+0200 6 | publish: true 7 | tags: [guides] 8 | --- 9 | 10 | > [!WARNING]- A GitHub account is required to use Quartz and Quartz Syncer 11 | > You can sign up for free [here](https://github.com/signup). 12 | 13 | > [!WARNING] Set up Quartz first 14 | > This plugin manages Quartz content from Obsidian. Please set up Quartz if you have not already. See instructions below. 15 | > 16 | 17 | ## Set up Quartz 18 | 19 | ### Create Quartz GitHub repository 20 | 21 | If you haven't set up a Quartz repository on GitHub yet, [click here](https://github.com/new?template_name=quartz&template_owner=jackyzha0) to create it using the Quartz template. 22 | 23 | ### Configure Quartz settings 24 | 25 | #### Configure `quartz.config.ts` 26 | 27 | Configure the following settings in the `quartz.config.ts` file: 28 | 29 | (Below example only shows a subset of all settings. Please do not remove any settings.) 30 | 31 | ```ts title="quartz.config.ts" {3,6,12,25} 32 | const config: QuartzConfig = { 33 | configuration: { 34 | pageTitle: "Quartz 4", 35 | // Change to your desired site title 36 | ... 37 | baseUrl: "quartz.jzhao.xyz", 38 | // Change to your site URL without https://. 39 | // This is your own domain, 40 | // or ".github.io/" when using GitHub Pages. 41 | // See below for details 42 | ... 43 | defaultDateType: "modified", 44 | // Change to tell Quartz what date to display on notes 45 | // Valid options: 46 | // "created", use when the note was created. 47 | // "modified", use when the note was last modified. 48 | // "published", use when the note was published. 49 | // See Quartz docs for details. 50 | ... 51 | } 52 | } 53 | plugins: { 54 | transformers: [ 55 | ... 56 | Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), 57 | // Sets how Quartz should resolve links between notes. 58 | // Should match the settings you use in Obsidian. 59 | // Valid options: 60 | // "shortest" 61 | // "relative" 62 | // "absolute" 63 | ... 64 | ] 65 | ... 66 | } 67 | } 68 | ``` 69 | 70 | #### Configure automatic deployment 71 | 72 | If you haven't already, set up Quartz to automatically deploy on push: 73 | 74 | > [!INFO] GitHub Pages setup (recommended) 75 | > In your GitHub repository, go to `Settings > Pages` and set `Source` to "GitHub Actions". 76 | > 77 | > Next, add one of the following deploy scripts: 78 | > > [!EXAMPLE]- Option 1: Default Quartz 79 | > > For using Quartz without adding an Obsidian Theme. 80 | > > 81 | > > Add the following script as `.github/workflows/deploy.yaml`: 82 | > > 83 | > > ```yaml title=".github/workflows/deploy.yaml" 84 | > > name: Deploy Quartz site to GitHub Pages 85 | > > 86 | > > on: 87 | > > push: 88 | > > branches: 89 | > > - v4 90 | > > 91 | > > permissions: 92 | > > contents: read 93 | > > pages: write 94 | > > id-token: write 95 | > > 96 | > > concurrency: 97 | > > group: "pages" 98 | > > cancel-in-progress: false 99 | > > 100 | > > jobs: 101 | > > build: 102 | > > runs-on: ubuntu-22.04 103 | > > steps: 104 | > > - uses: actions/checkout@v4 105 | > > with: 106 | > > fetch-depth: 0 # Fetch all history for git info 107 | > > - uses: actions/setup-node@v4 108 | > > with: 109 | > > node-version: 22 110 | > > - name: Install Dependencies 111 | > > run: npm ci 112 | > > - name: Build Quartz 113 | > > run: npx quartz build 114 | > > - name: Upload artifact 115 | > > uses: actions/upload-pages-artifact@v3 116 | > > with: 117 | > > path: public 118 | > > 119 | > > deploy: 120 | > > needs: build 121 | > > environment: 122 | > > name: github-pages 123 | > > url: ${{ steps.deployment.outputs.page_url }} 124 | > > runs-on: ubuntu-latest 125 | > > steps: 126 | > > - name: Deploy to GitHub Pages 127 | > > id: deployment 128 | > > uses: actions/deploy-pages@v4 129 | > > ``` 130 | > 131 | > > [!EXAMPLE]- Option 2: Quartz with Quartz Themes 132 | > > For using an Obsidian Theme with Quartz. 133 | > > 134 | > > > [!IMPORTANT] Don't forget to replace `THEME_NAME` with your Obsidian theme of choice 135 | > > > A list of theme options can be [found here](https://github.com/saberzero1/quartz-themes?tab=readme-ov-file#supported-themes). 136 | > > 137 | > > Add the following script as `.github/workflows/deploy.yaml`: 138 | > > 139 | > > ```yaml title=".github/workflows/deploy.yaml" {8-9, 32-33} 140 | > > name: Deploy Quartz site to GitHub Pages 141 | > > 142 | > > on: 143 | > > push: 144 | > > branches: 145 | > > - v4 146 | > > 147 | > > env: 148 | > > THEME_NAME: tokyo-night 149 | > > 150 | > > permissions: 151 | > > contents: read 152 | > > pages: write 153 | > > id-token: write 154 | > > 155 | > > concurrency: 156 | > > group: "pages" 157 | > > cancel-in-progress: false 158 | > > 159 | > > jobs: 160 | > > build: 161 | > > runs-on: ubuntu-22.04 162 | > > steps: 163 | > > - uses: actions/checkout@v4 164 | > > with: 165 | > > fetch-depth: 0 # Fetch all history for git info 166 | > > - uses: actions/setup-node@v4 167 | > > with: 168 | > > node-version: 22 169 | > > - name: Install Dependencies 170 | > > run: npm ci 171 | > > - name: Fetch Quartz Theme 172 | > > run: curl -s -S https://raw.githubusercontent.com/saberzero1/quartz-themes/master/action.sh | bash -s -- $THEME_NAME 173 | > > - name: Build Quartz 174 | > > run: npx quartz build 175 | > > - name: Upload artifact 176 | > > uses: actions/upload-pages-artifact@v3 177 | > > with: 178 | > > path: public 179 | > > 180 | > > deploy: 181 | > > needs: build 182 | > > environment: 183 | > > name: github-pages 184 | > > url: ${{ steps.deployment.outputs.page_url }} 185 | > > runs-on: ubuntu-latest 186 | > > steps: 187 | > > - name: Deploy to GitHub Pages 188 | > > id: deployment 189 | > > uses: actions/deploy-pages@v4 190 | > > ``` 191 | 192 | For other options, see the [Quartz docs on hosting](https://quartz.jzhao.xyz/hosting). 193 | 194 | ![[Generating an access token#Generating a fine-grained access token]] 195 | 196 | ## Set up Quartz Syncer 197 | 198 | In Obsidian, open `Settings > Community Plugins > Quartz Syncer > Options` and configure the following fields: 199 | 200 | - GitHub repo name: the name of your repository on GitHub. 201 | - GitHub username: the name of the GitHub user (or organization) the repository belongs to. 202 | - GitHub token: the [[Generating an access token#Generating a fine-grained access token|generated authentication token]] to allow Quartz Syncer to manage your Quartz content folder. 203 | 204 | > [!EXAMPLE]- Configuration Example 205 | > Using the original Quartz repository as an example: 206 | > 207 | > The repository is hosted at . 208 | > 209 | > - *Repository name*: `quartz` 210 | > - *Username*: `jackyzha0` 211 | > - *GitHub token*: generated token. usually starts with `github_pat_` or `ghp_`. 212 | 213 | After setting all three fields, you should get a green checkmark in the Quartz Syncer options. If not, check [[Authentication|the relevant troubleshooting page]] for help. 214 | 215 | ## That's it 216 | 217 | You should now be able to start publishing to Quartz using Quartz Syncer. 218 | 219 | For further details, check the [[Usage Guide]]. 220 | -------------------------------------------------------------------------------- /docs/Troubleshooting/Authentication.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authentication 3 | description: Troubleshooting issues related to GitHub authentication. 4 | created: 2025-05-05T00:00:00Z+0200 5 | modified: 2025-05-17T18:47:50Z+0200 6 | publish: true 7 | --- 8 | 9 | > [!INFO] Expired authentication token? 10 | > The most frequent issue with authentication is an expired GitHub authentication token. You can check this in the Quartz Syncer settings inside Obsidian (`Settings > Community Plugins > Quartz Syncer`). 11 | > 12 | > If you see an authentication error, please follow the [[Generating an access token#Generating a fine-grained access token|guide on generating an access token]]. 13 | 14 | ## My Authentication Token is correct, but I get an error when publishing 15 | 16 | Please ensure you have the proper rights to your Quartz repository. 17 | 18 | If you're on a university network connection or similar network that is shared with many other users, your connection to GitHub might be rate-limited. 19 | 20 | ## I have a different issue not listed here 21 | 22 | Please raise an [issue on GitHub](https://github.com/saberzero1/quartz-syncer/issues). 23 | -------------------------------------------------------------------------------- /docs/Troubleshooting/Dataview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dataview 3 | description: Troubleshooting issues related to using the Dataview Obsidian plugin. 4 | created: 2025-05-05T00:00:00Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [dataview] 8 | --- 9 | 10 | > [!INFO] Dataview usage 11 | > For question regarding Dataview syntax, please refer to the [Dataview documentation](https://blacksmithgu.github.io/obsidian-dataview/) 12 | 13 | ## Dataview Query is empty in Quartz but not in Obsidian 14 | 15 | Please ensure the notes in your Dataview query result are published to Quartz. 16 | 17 | ## Dataview doesn't work in Quartz Syncer 18 | 19 | Quartz Syncer uses the Dataview plugin for Dataview support. Please ensure the Dataview plugin is installed and enabled. 20 | 21 | ## Dataviewjs syntax doesn't work 22 | 23 | Please ensure Dataviewjs syntax is enable in the Dataview plugin settings. 24 | 25 | ## I have a different issue not listed here 26 | 27 | Please raise an [issue on GitHub](https://github.com/saberzero1/quartz-syncer/issues). 28 | -------------------------------------------------------------------------------- /docs/Troubleshooting/Excalidraw.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Excalidraw 3 | description: Troubleshooting issues related to using the Excalidraw Obsidian plugin. 4 | created: 2025-05-05T00:00:00Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [excalidraw] 8 | --- 9 | 10 | > [!WARNING] Excalidraw is currently disabled in Quartz Syncer 11 | > A fix is being worked on. 12 | 13 | > [!INFO] Excalidraw usage 14 | > For question regarding Excalidraw usage, please refer to the [Excaldraw documentation](https://excalidraw-obsidian.online) 15 | 16 | ## I have a different issue not listed here 17 | 18 | Please raise an [issue on GitHub](https://github.com/saberzero1/quartz-syncer/issues). 19 | -------------------------------------------------------------------------------- /docs/Troubleshooting/Frontmatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Frontmatter 3 | description: Troubleshooting issues related to Frontmatter. 4 | created: 2025-05-05T00:00:00Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [frontmatter] 8 | --- 9 | 10 | > [!WARNING] Quartz Syncer passes **all** frontmatter tags by default. Please be mindful when publishing. 11 | 12 | > [!INFO] Frontmatter usage 13 | > Frontmatter is a way to add metadata to markdown files in YAML format popularized by Jekyll. You can find the Jekyll docs on frontmatter by [clicking here](https://jekyllrb.com/docs/front-matter/). 14 | > 15 | > Quartz Syncer requires the `publish` tag to be set in order for a note to appear in the publishing dialog. 16 | > > [!EXAMPLE]- Minimum frontmatter required for Quartz Syncer 17 | > > 18 | > > ```markdown 19 | > > --- 20 | > > publish: true 21 | > > --- 22 | > > rest of the note here... 23 | > > ``` 24 | -------------------------------------------------------------------------------- /docs/Troubleshooting/Quartz.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quartz 3 | description: Troubleshooting issues related to Quartz. 4 | created: 2025-05-05T00:00:00Z+0200 5 | modified: 2025-05-20T20:31:59Z+0200 6 | publish: true 7 | tags: [quartz] 8 | --- 9 | 10 | > [!INFO] Quartz usage 11 | > For question regarding Quartz usage, please refer to the [Quartz documentation](https://quartz.jzhao.xyz/). 12 | > 13 | > For support regarding Quartz usage, please connect to us in the [Quartz Community Discord Server](https://discord.gg/cRFFHYye7t). 14 | 15 | ## Can I use an Obsidian Theme in Quartz? 16 | 17 | See [[Using an Obsidian theme in Quartz]]. 18 | 19 | ## What theme is this site using? 20 | 21 | [Tokyo Night](https://github.com/saberzero1/quartz-themes/blob/master/themes/tokyo-night/README.md). 22 | 23 | ## Can I configure Quartz settings from Obsidian using Quartz Syncer? 24 | 25 | Not yet. It is planned to be added in the future. 26 | 27 | ## I have a different issue not listed here 28 | 29 | Please raise an [issue on GitHub](https://github.com/saberzero1/quartz-syncer/issues). 30 | -------------------------------------------------------------------------------- /docs/Troubleshooting/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Troubleshooting 3 | description: Troubleshooting common issues. 4 | created: 2025-05-05T00:00:00Z+0200 5 | modified: 2025-05-17T18:47:50Z+0200 6 | publish: true 7 | --- 8 | 9 | ```dataview 10 | TABLE WITHOUT ID file.link AS Category, file.frontmatter.description AS Description 11 | WHERE file.folder = this.file.folder 12 | WHERE file != this.file 13 | SORT file.frontmatter.title ASC 14 | ``` 15 | 16 | ## I have a different issue not listed here 17 | 18 | Please raise an [issue on GitHub](https://github.com/saberzero1/quartz-syncer/issues). 19 | -------------------------------------------------------------------------------- /docs/Usage Guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage Guide 3 | description: Details on using Quartz Syncer. 4 | created: 2025-05-05T00:00:00Z+0200 5 | modified: 2025-05-20T12:06:55Z+0200 6 | publish: true 7 | tags: [guides] 8 | --- 9 | 10 | > [!WARNING] One-way sync only 11 | > 12 | > Quartz Syncer applies changes to your notes when they are pushed to your Quartz repository, like parsing [[Settings/Integrations/Dataview|Dataview]] queries and filtering [[Settings/Note properties/index|note properties]] to ensure your notes are fully Quartz-compatible. This means the notes in your Quartz repository should be considered a one-way sync (Obsidian to Quartz.) 13 | > 14 | > For syncing notes between devices, consider using [Obsidian Sync](https://obsidian.md/sync) or one of the many [community plugins](https://obsidian.md/plugins?search=sync). 15 | 16 | ## Opening the publication center 17 | 18 | There are two ways to open the publication center: 19 | 20 | 1. From the command palette: `Quartz Syncer: Open publication center`. 21 | 2. Clicking the Quartz crystal icon. 22 | 1. (On desktop): By default, in the vertical bar on the left side of your screen. 23 | 2. (On mobile): By default, press the hamburger button on the bottom right (three horizontal lines) and press `Quartz Syncer publication center`. 24 | 25 | ## Marking files as publishable 26 | 27 | By default, Quartz Syncer will not show any notes when you open the publication center. In order to flag a note as visible to Quartz Syncer, you will need to add a [property](https://help.obsidian.md/properties) to the notes you want to publish. 28 | 29 | > [!TIP] The `publish` property 30 | > 31 | > By default, a checkbox property `publish` has to be added to notes to make them visible to Quartz Syncer. This property [[Publish key|can be configured in the plugin settings]]. The checkbox has to be checked/property set to true to show up in Quartz Syncer. 32 | 33 | > [!TIP] Adding the `publish` property 34 | > 35 | > There are several ways to add the `publish` property to your notes: 36 | > 1. Using the Quartz Syncer command `Quartz Syncer: add publication flag` 37 | > 2. Using a community plugin to automatically add the configured flag 38 | > 3. Manually, via the `+ Add property` button 39 | 40 | ## Publishing notes to Quartz 41 | 42 | Open the publication center (see above for instructions). Your eligible notes will be under one of the following headings: 43 | 44 | - **Unpublished notes**: notes that are in your Obsidian vault, but not in your Quartz repository. Any notes checked here will be published to Quartz. 45 | - **Changed notes**: notes that are in your Obsidian vault and in your Quartz repository, but they don't match. This is usually because the note has been changed in your vault. Any notes checked here will be updated in Quartz. 46 | - You can click the icon next to your note's title to preview the exact changes that will be made. 47 | - **Published notes (select to unpublished)**: notes that are in your in your Quartz repository. Any notes checked here will be unpublished and removed from your Quartz repository. Notes deleted from your vault need to be unpublished here to remove them from Quartz. 48 | - **Unchanged notes**: a list of all unchanged notes that are currently published in your Quartz repository. It includes only unchanged notes that are also in your vault. 49 | 50 | After you are satisfied with your selection, click the big button on the bottom left that says `PUBLISH SELECTED CHANGES` and watch the magic happen. 51 | 52 | > [!INFO] Why does Quartz Syncer sometimes make two commits? 53 | > 54 | >To prevent any potential inconsistencies with git, Quartz Syncer sometimes makes two commits in a row: the first commit adds and/or updates any notes selected. The second commit removes any notes selected for unpublishing. 55 | > 56 | >If you're unsure if your notes have been correctly published, you can check by opening the publication center again after Quartz Syncer finished publishing. The new state of your notes should be reflected in the publication center. 57 | -------------------------------------------------------------------------------- /docs/Useful resources/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Useful resources 3 | description: Useful resources for Obsidian, Quartz, etc. 4 | created: 2025-05-24T12:39:52Z+0200 5 | modified: 2025-05-24T12:43:55Z+0200 6 | publish: true 7 | tags: [resources] 8 | --- 9 | 10 | ```dataview 11 | TABLE WITHOUT ID file.link AS Category, file.frontmatter.description AS Description 12 | WHERE file.folder = this.file.folder 13 | WHERE file != this.file 14 | SORT file.frontmatter.title ASC 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quartz Syncer Documentation 3 | description: Quartz Syncer allows you to manage your Quartz site content from Obsidian. 4 | created: 2025-04-20T00:00:00Z+0200 5 | modified: 2025-06-10T22:01:34Z+0200 6 | publish: true 7 | aliases: [home] 8 | --- 9 | 10 | [Quartz Syncer](https://github.com/saberzero1/quartz-syncer) is an [Obsidian](https://obsidian.md/) plugin for managing and publishing notes to [Quartz](https://quartz.jzhao.xyz/), the fast, batteries-included static-site generator that transforms Markdown content into fully functional websites. 11 | 12 | ## Installation 13 | 14 | ~~Install the plugin by downloading it from the Obsidian Community plugins browser in Obsidian.~~ ([Quartz Syncer has been posted to the Obsidian plugin repository and is awaiting review](https://github.com/obsidianmd/obsidian-releases/pull/6427)) 15 | 16 | Alternatively, install the plugin by downloading it from the [Release Tab](https://github.com/saberzero1/quartz-syncer/releases), or through the [Obsidian42 Brat plugin](https://github.com/TfTHacker/obsidian42-brat). 17 | 18 | ## Setup 19 | 20 | New to Quartz Syncer? please follow the [[Setup Guide|setup guide]] to get started. 21 | 22 | ## Usage 23 | 24 | Unsure on how to use Quartz Syncer, or just curious about its usage? Check out the [[Usage Guide|usage guide]]. 25 | 26 | ## Advanced usage 27 | 28 | For more advanced use-cases of Quartz Syncer, check the [[Guides/index|guides section]]. 29 | 30 | ## Troubleshooting 31 | 32 | > [!INFO] Quartz-related questions 33 | > 34 | > For issue or questions related to Quartz, not Quartz Syncer, please consult the [Quartz documentation](https://quartz.jzhao.xyz/) or reach out through the communication channels provided there. 35 | 36 | If you need help with Quartz Syncer, or if you have a question, please first check the [[Troubleshooting/index|troubleshooting section]]. If your question or issue is not listed, feel free to [[Troubleshooting/index#I have a different issue not listed here|reach out for help]]. 37 | 38 | ## Acknowledgements 39 | 40 | Quartz Syncer would not have been build without the following: 41 | 42 | - [Obsidian Digital Garden](https://dg-docs.ole.dev/), on top of which most of this plugin was initially built. 43 | - [Quartz](https://quartz.jzhao.xyz/), for the amazing and welcoming community. Come say hi in the Discord server sometimes. 44 | - [Obsidian Linter](https://github.com/platers/obsidian-linter), for inspiring the tabbed settings UI. 45 | - [Dataview](https://blacksmithgu.github.io/obsidian-dataview/), for their great API integration, allowing me to properly integrate it in Quartz. 46 | - [Datacore](https://blacksmithgu.github.io/datacore/), for their wonderful integration despite its infancy, allowing easy integration into Quartz. 47 | - [Obsidian Publish](https://obsidian.md/publish), for inspiring me to create a similar solution for Quartz. 48 | - The entire Obsidian community, for all your weird and amazing creations. Keep it up. 49 | -------------------------------------------------------------------------------- /docs/tags/datacore.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Datacore 3 | description: Overview of Datacore tag. 4 | created: 2025-06-09T20:54:45Z+0200 5 | modified: 2025-06-09T20:55:16Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/dataview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dataview 3 | description: Overview of Dataview tag. 4 | created: 2025-05-17T14:19:00Z+0200 5 | modified: 2025-05-17T18:47:49Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/excalidraw.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Excalidraw 3 | description: Overview of Excalidraw tag. 4 | created: 2025-05-17T14:19:39Z+0200 5 | modified: 2025-05-17T18:47:49Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/frontmatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Frontmatter 3 | description: Overview of frontmatter tag. 4 | created: 2025-05-20T20:30:05Z+0200 5 | modified: 2025-05-20T20:30:28Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/guides.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Guides 3 | description: Overview of guides tag. 4 | created: 2025-05-20T11:59:06Z+0200 5 | modified: 2025-05-20T11:59:42Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tags 3 | description: Overview of all tags. 4 | created: 2025-05-17T14:17:08Z+0200 5 | modified: 2025-05-17T18:47:49Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/integration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Integration 3 | description: Overview of integration tag. 4 | created: 2025-05-17T14:17:52Z+0200 5 | modified: 2025-05-17T18:47:49Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/quartz.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quartz 3 | description: Overview of Quartz tag. 4 | created: 2025-05-20T20:29:41Z+0200 5 | modified: 2025-05-20T20:30:03Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/resources.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Resources 3 | description: Overview of resources tag. 4 | created: 2025-05-24T12:41:54Z+0200 5 | modified: 2025-05-24T12:42:48Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/settings/frontmatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Frontmatter settings 3 | description: Overview of frontmatter settings tag. 4 | created: 2025-05-20T12:41:12Z+0200 5 | modified: 2025-05-20T12:43:38Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/settings/github.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GitHub settings 3 | description: Overview of GitHub settings tag. 4 | created: 2025-05-20T12:41:39Z+0200 5 | modified: 2025-05-20T12:43:34Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/settings/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Settings 3 | description: Overview of settings tag. 4 | created: 2025-05-20T12:39:43Z+0200 5 | modified: 2025-05-20T12:40:20Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/settings/integrations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Integration settings 3 | description: Overview of integration settings tag. 4 | created: 2025-05-20T12:42:45Z+0200 5 | modified: 2025-05-20T12:43:45Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/settings/quartz.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quartz settings 3 | description: Overview of Quartz settings tag. 4 | created: 2025-05-20T12:44:20Z+0200 5 | modified: 2025-05-20T12:44:45Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/settings/themes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Themes settings 3 | description: Overview of Themes settings tag. 4 | created: 2025-05-20T12:43:50Z+0200 5 | modified: 2025-05-20T12:44:17Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /docs/tags/themes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Themes 3 | description: Overview of themes tag. 4 | created: 2025-05-20T12:40:43Z+0200 5 | modified: 2025-05-20T12:41:04Z+0200 6 | publish: true 7 | --- 8 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from 'builtin-modules' 4 | import esbuildSvelte from "esbuild-svelte"; 5 | import sveltePreprocess from "svelte-preprocess"; 6 | 7 | const banner = 8 | `/* 9 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 10 | if you want to view the source, please visit the github repository of this plugin 11 | */ 12 | `; 13 | 14 | const prod = (process.argv[2] === 'production'); 15 | 16 | esbuild.build({ 17 | banner: { 18 | js: banner, 19 | }, 20 | entryPoints: ['main.ts'], 21 | bundle: true, 22 | external: ['obsidian', 'electron', ...builtins], 23 | format: 'cjs', 24 | target: 'es2018', 25 | logLevel: "info", 26 | sourcemap: prod ? false : 'inline', 27 | treeShaking: true, 28 | outfile: 'main.js', 29 | plugins: [ 30 | esbuildSvelte({ 31 | compilerOptions: { css: 'injected'}, 32 | preprocess: sveltePreprocess(), 33 | }), 34 | ], 35 | }).catch(() => process.exit(1)); 36 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest/presets/js-with-ts", 3 | testEnvironment: "node", 4 | }; 5 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | default: 2 | just --list 3 | 4 | full: lint check prod 5 | 6 | dev: 7 | npm run dev 8 | mkdir -p ./docs/.obsidian/plugins/quartz-syncer 9 | cp main.js ./docs/.obsidian/plugins/quartz-syncer 10 | cp manifest.json ./docs/.obsidian/plugins/quartz-syncer 11 | cp styles.css ./docs/.obsidian/plugins/quartz-syncer 12 | 13 | prod: 14 | npm run build 15 | mkdir -p ./docs/.obsidian/plugins/quartz-syncer 16 | cp main.js ./docs/.obsidian/plugins/quartz-syncer 17 | cp manifest.json ./docs/.obsidian/plugins/quartz-syncer 18 | cp styles.css ./docs/.obsidian/plugins/quartz-syncer 19 | 20 | lint: 21 | npm run format 22 | 23 | test: 24 | npm run test 25 | 26 | check: 27 | npm run lint 28 | npm run test 29 | npm run check-formatting 30 | npm run typecheck 31 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { Notice, Platform, Plugin, Workspace, addIcon } from "obsidian"; 2 | import Publisher from "./src/publisher/Publisher"; 3 | import QuartzSyncerSettings from "./src/models/settings"; 4 | import { quartzSyncerIcon } from "./src/ui/suggest/constants"; 5 | import { PublicationCenter } from "src/views/PublicationCenter/PublicationCenter"; 6 | import PublishStatusManager from "src/publisher/PublishStatusManager"; 7 | import ObsidianFrontMatterEngine from "src/publishFile/ObsidianFrontMatterEngine"; 8 | import QuartzSyncerSiteManager from "src/repositoryConnection/QuartzSyncerSiteManager"; 9 | import { QuartzSyncerSettingTab } from "./src/views/QuartzSyncerSettingTab"; 10 | import Logger from "js-logger"; 11 | 12 | const DEFAULT_SETTINGS: QuartzSyncerSettings = { 13 | githubRepo: "quartz", 14 | githubToken: "", 15 | githubUserName: "", 16 | useFullResolutionImages: false, 17 | noteSettingsIsInitialized: false, 18 | 19 | // Quartz related settings 20 | contentFolder: "content", 21 | vaultPath: "/", 22 | 23 | // Timestamp related settings 24 | showCreatedTimestamp: true, 25 | createdTimestampKey: "created", 26 | showUpdatedTimestamp: true, 27 | updatedTimestampKey: "modified", 28 | showPublishedTimestamp: false, 29 | publishedTimestampKey: "published", 30 | timestampFormat: "MMM dd, yyyy h:mm a", 31 | 32 | pathRewriteRules: "", 33 | 34 | usePermalink: false, 35 | 36 | useDatacore: false, 37 | useDataview: true, 38 | useExcalidraw: false, 39 | 40 | useThemes: false, 41 | 42 | includeAllFrontmatter: false, 43 | 44 | applyEmbeds: true, 45 | 46 | publishFrontmatterKey: "publish", 47 | 48 | lastUsedSettingsTab: "github", 49 | 50 | logLevel: undefined, 51 | }; 52 | 53 | Logger.useDefaults({ 54 | defaultLevel: Logger.WARN, 55 | formatter: function (messages, _context) { 56 | messages.unshift(new Date().toUTCString()); 57 | messages.unshift("QS: "); 58 | }, 59 | }); 60 | 61 | export default class QuartzSyncer extends Plugin { 62 | settings!: QuartzSyncerSettings; 63 | appVersion!: string; 64 | 65 | publishModal!: PublicationCenter; 66 | 67 | async onload() { 68 | this.appVersion = this.manifest.version; 69 | 70 | await this.loadSettings(); 71 | 72 | this.settings.logLevel && Logger.setLevel(this.settings.logLevel); 73 | 74 | Logger.info("Initializing QuartzSyncer plugin v" + this.appVersion); 75 | 76 | Logger.info("Quartz Syncer log level set to " + Logger.getLevel().name); 77 | this.addSettingTab(new QuartzSyncerSettingTab(this.app, this)); 78 | 79 | await this.addCommands(); 80 | 81 | addIcon("quartz-syncer-icon", quartzSyncerIcon); 82 | 83 | this.addRibbonIcon( 84 | "quartz-syncer-icon", 85 | "Quartz Syncer publication center", 86 | async () => { 87 | this.openPublishModal(); 88 | }, 89 | ); 90 | } 91 | 92 | onunload() {} 93 | 94 | async loadSettings() { 95 | this.settings = Object.assign( 96 | {}, 97 | DEFAULT_SETTINGS, 98 | await this.loadData(), 99 | ); 100 | } 101 | 102 | async saveSettings(): Promise { 103 | await this.saveData(this.settings); 104 | } 105 | 106 | async addCommands() { 107 | if (this.settings["ENABLE_DEVELOPER_TOOLS"] && Platform.isDesktop) { 108 | Logger.info("Developer tools enabled"); 109 | 110 | const publisher = new Publisher( 111 | this.app, 112 | this.app.vault, 113 | this.app.metadataCache, 114 | this.settings, 115 | ); 116 | 117 | import("./src/test/snapshot/generateSyncerSnapshot") 118 | .then((snapshotGen) => { 119 | this.addCommand({ 120 | id: "generate-syncer-snapshot", 121 | name: "Generate Syncer Snapshot", 122 | callback: async () => { 123 | await snapshotGen.generateSyncerSnapshot( 124 | this.settings, 125 | publisher, 126 | ); 127 | }, 128 | }); 129 | }) 130 | .catch((e) => { 131 | Logger.error("Unable to load generateSyncerSnapshot", e); 132 | }); 133 | } 134 | 135 | this.addCommand({ 136 | id: "open-publish-modal", 137 | name: "Open publication center", 138 | callback: async () => { 139 | this.openPublishModal(); 140 | }, 141 | }); 142 | 143 | this.addCommand({ 144 | id: "mark-note-for-publish", 145 | name: "Add publication flag", 146 | callback: async () => { 147 | this.setPublishFlagValue(true); 148 | }, 149 | }); 150 | 151 | this.addCommand({ 152 | id: "unmark-note-for-publish", 153 | name: "Remove publication flag", 154 | callback: async () => { 155 | this.setPublishFlagValue(false); 156 | }, 157 | }); 158 | 159 | this.addCommand({ 160 | id: "mark-toggle-publish-status", 161 | name: "Toggle publication flag", 162 | callback: async () => { 163 | this.togglePublishFlag(); 164 | }, 165 | }); 166 | } 167 | 168 | private getActiveFile(workspace: Workspace) { 169 | const activeFile = workspace.getActiveFile(); 170 | 171 | if (!activeFile) { 172 | new Notice( 173 | "No file is open/active. Please open a file and try again.", 174 | ); 175 | 176 | return null; 177 | } 178 | 179 | return activeFile; 180 | } 181 | 182 | async setPublishFlagValue(value: boolean) { 183 | const activeFile = this.getActiveFile(this.app.workspace); 184 | 185 | if (!activeFile) { 186 | return; 187 | } 188 | 189 | const engine = new ObsidianFrontMatterEngine( 190 | this.app.vault, 191 | this.app.metadataCache, 192 | activeFile, 193 | ); 194 | engine.set(this.settings.publishFrontmatterKey, value).apply(); 195 | } 196 | 197 | async togglePublishFlag() { 198 | const activeFile = this.getActiveFile(this.app.workspace); 199 | 200 | if (!activeFile) { 201 | return; 202 | } 203 | 204 | const engine = new ObsidianFrontMatterEngine( 205 | this.app.vault, 206 | this.app.metadataCache, 207 | activeFile, 208 | ); 209 | 210 | engine 211 | .set( 212 | this.settings.publishFrontmatterKey, 213 | !engine.get(this.settings.publishFrontmatterKey), 214 | ) 215 | .apply(); 216 | } 217 | 218 | openPublishModal() { 219 | if (!this.publishModal) { 220 | const siteManager = new QuartzSyncerSiteManager( 221 | this.app.metadataCache, 222 | this.settings, 223 | ); 224 | 225 | const publisher = new Publisher( 226 | this.app, 227 | this.app.vault, 228 | this.app.metadataCache, 229 | this.settings, 230 | ); 231 | 232 | const publishStatusManager = new PublishStatusManager( 233 | siteManager, 234 | publisher, 235 | ); 236 | 237 | this.publishModal = new PublicationCenter( 238 | this.app, 239 | publishStatusManager, 240 | publisher, 241 | siteManager, 242 | this.settings, 243 | ); 244 | } 245 | this.publishModal.open(); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "quartz-syncer", 3 | "name": "Quartz Syncer", 4 | "version": "1.7.1", 5 | "minAppVersion": "1.8.10", 6 | "description": "Manage and publish your notes to Quartz, the fast, batteries-included static-site generator.", 7 | "author": "Emile Bangma", 8 | "authorUrl": "https://github.com/saberzero1", 9 | "fundingUrl": { 10 | "Buy Me a Coffee": "https://buymeacoffee.com/saberzero1", 11 | "GitHub Sponsor": "https://github.com/sponsors/saberzero1", 12 | "Ko-Fi": "https://ko-fi.com/saberzero1" 13 | }, 14 | "isDesktopOnly": false 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quartz-syncer", 3 | "version": "1.7.1", 4 | "description": "A plugin used for publishing notes to Quartz", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "postdev": "node scripts/generateSyncerSettings.mjs", 9 | "dev1": "parcel main.ts", 10 | "build": "node esbuild.config.mjs production", 11 | "test": "jest", 12 | "prepare": "husky install", 13 | "lint": "eslint . --ext .ts --ext .svelte", 14 | "lint-fix": "eslint . --fix --ext .ts --ext .svelte", 15 | "format": "prettier --write .", 16 | "check-formatting": "prettier --check .", 17 | "typecheck": "tsc" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@tsconfig/svelte": "^5.0.2", 24 | "@types/crypto-js": "^4.1.2", 25 | "@types/diff": "^5.0.4", 26 | "@types/jest": "^29.5.4", 27 | "@types/luxon": "^3.3.2", 28 | "@types/node": "^20.6.0", 29 | "@typescript-eslint/eslint-plugin": "^6.7.0", 30 | "@typescript-eslint/parser": "^6.7.0", 31 | "builtin-modules": "^3.3.0", 32 | "copyfiles": "^2.4.1", 33 | "dotenv": "^16.3.1", 34 | "esbuild": "^0.25.5", 35 | "esbuild-svelte": "^0.8.0", 36 | "eslint": "^8.49.0", 37 | "eslint-plugin-prettier": "^5.0.0", 38 | "eslint-plugin-svelte": "^2.33.2", 39 | "husky": "^8.0.3", 40 | "jest": "^29.7.0", 41 | "lint-staged": "^16.1.0", 42 | "obsidian": "^1.4.11", 43 | "prettier": "^3.0.3", 44 | "prettier-plugin-svelte": "^3.0.3", 45 | "svelte": "^4.2.0", 46 | "svelte-preprocess": "^5.0.4", 47 | "ts-jest": "^29.1.1", 48 | "tslib": "^2.6.2", 49 | "typescript": "^5.2.2" 50 | }, 51 | "dependencies": { 52 | "@blacksmithgu/datacore": "^0.1.24", 53 | "@octokit/core": "^5.0.0", 54 | "@sindresorhus/slugify": "^1.1.2", 55 | "axios": "^1.5.0", 56 | "crypto-js": "^4.1.1", 57 | "diff": "^5.1.0", 58 | "js-base64": "^3.7.5", 59 | "js-logger": "^1.6.1", 60 | "luxon": "^3.4.3", 61 | "lz-string": "^1.5.0", 62 | "obsidian-dataview": "^0.5.68" 63 | }, 64 | "lint-staged": { 65 | "*.{ts}": [ 66 | "prettier --write", 67 | "eslint --fix ." 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /quartz-syncer.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /scripts/generateSyncerSettings.mjs: -------------------------------------------------------------------------------- 1 | import dotevnt from "dotenv"; 2 | import fs from "fs"; 3 | dotevnt.config(); 4 | import copyfiles from "copyfiles"; 5 | import Logger from "js-logger"; 6 | 7 | const quartzSettings = { 8 | githubRepo: 9 | process.env.GITHUB_REPO || "add your repo to .env as GITHUB_REPO", 10 | githubToken: 11 | process.env.GITHUB_TOKEN || "add your token to .env as GITHUB_TOKEN", 12 | githubUserName: 13 | process.env.GITHUB_USERNAME || 14 | "add your username to .env as GITHUB_USERNAME", 15 | 16 | useFullResolutionImages: false, 17 | noteSettingsIsInitialized: true, 18 | 19 | contentFolder: "content", 20 | vaultPath: "/", 21 | useDatacore: false, 22 | useDataview: true, 23 | useExcalidraw: false, 24 | 25 | useThemes: false, 26 | 27 | includeAllFrontmatter: false, 28 | 29 | applyEmbeds: true, 30 | 31 | showCreatedTimestamp: true, 32 | createdTimestampKey: "created", 33 | showUpdatedTimestamp: true, 34 | updatedTimestampKey: "modified", 35 | showPublishedTimestamp: false, 36 | publishedTimestampKey: "published", 37 | timestampFormat: "MMM dd, yyyy h:mm a", 38 | 39 | pathRewriteRules: ` 40 | Path Rewriting/Subfolder2:fun-folder 41 | Path Rewriting: 42 | Subfolder:subfolder-rewritten 43 | Path Rewriting/Subfolder:this-will-never-hit`, 44 | publishFrontmatterKey: "publish", 45 | usePermalink: true, 46 | 47 | lastUsedSettingsTab: "github", 48 | 49 | ENABLE_DEVELOPER_TOOLS: true, 50 | devPluginPath: `${process.cwd()}`, 51 | logLevel: Logger.DEBUG, 52 | }; 53 | 54 | const TEST_VAULT_PATH = 55 | "content/.obsidian/plugins/quartz-syncer/"; 56 | 57 | console.log("Creating test vault data.json"); 58 | // write garden settings to test vault 59 | fs.writeFile( 60 | `${TEST_VAULT_PATH}/data.json`, 61 | JSON.stringify(quartzSettings, null, 2), 62 | function (err) { 63 | if (err) { 64 | console.log(err); 65 | } 66 | }, 67 | ); 68 | 69 | const FILES_TO_COPY = ["main.js", "manifest.json", "styles.css"]; 70 | console.log("Copying generated files to test vault"); 71 | // copy generated files to test vault 72 | copyfiles([...FILES_TO_COPY, TEST_VAULT_PATH], {}, () => {}); 73 | -------------------------------------------------------------------------------- /src/compiler/DataviewCompiler.ts: -------------------------------------------------------------------------------- 1 | import { Component, Notice, htmlToMarkdown } from "obsidian"; 2 | import { TCompilerStep } from "src/compiler/SyncerPageCompiler"; 3 | import { cleanQueryResult, delay, escapeRegExp } from "src/utils/utils"; 4 | import { DataviewApi, getAPI } from "obsidian-dataview"; 5 | import { PublishFile } from "src/publishFile/PublishFile"; 6 | import Logger from "js-logger"; 7 | 8 | export class DataviewCompiler { 9 | constructor() {} 10 | 11 | compile: TCompilerStep = (file) => async (text) => { 12 | let replacedText = text; 13 | const dataViewRegex = /```dataview\s(.+?)```/gms; 14 | const dvApi = getAPI(); 15 | 16 | if (!dvApi) return replacedText; 17 | const matches = text.matchAll(dataViewRegex); 18 | 19 | const dataviewJsPrefix = dvApi.settings.dataviewJsKeyword; 20 | 21 | const dataViewJsRegex = new RegExp( 22 | "```" + escapeRegExp(dataviewJsPrefix) + "\\s(.+?)```", 23 | "gsm", 24 | ); 25 | const dataviewJsMatches = text.matchAll(dataViewJsRegex); 26 | 27 | const inlineQueryPrefix = dvApi.settings.inlineQueryPrefix; 28 | 29 | const inlineDataViewRegex = new RegExp( 30 | "`" + escapeRegExp(inlineQueryPrefix) + "(.+?)`", 31 | "gsm", 32 | ); 33 | const inlineMatches = text.matchAll(inlineDataViewRegex); 34 | 35 | const inlineJsQueryPrefix = dvApi.settings.inlineJsQueryPrefix; 36 | 37 | const inlineJsDataViewRegex = new RegExp( 38 | "`" + escapeRegExp(inlineJsQueryPrefix) + "(.+?)`", 39 | "gsm", 40 | ); 41 | const inlineJsMatches = text.matchAll(inlineJsDataViewRegex); 42 | 43 | if ( 44 | !matches && 45 | !inlineMatches && 46 | !dataviewJsMatches && 47 | !inlineJsMatches 48 | ) { 49 | return text; 50 | } 51 | 52 | //Code block queries 53 | for (const queryBlock of matches) { 54 | try { 55 | const block = queryBlock[0]; 56 | const query = queryBlock[1]; 57 | 58 | const { isInsideCalloutDepth, finalQuery } = 59 | this.sanitizeQuery(query); 60 | 61 | let markdown = await dvApi.tryQueryMarkdown( 62 | finalQuery, 63 | file.getPath(), 64 | ); 65 | 66 | if (isInsideCalloutDepth > 0) { 67 | markdown = this.surroundWithCalloutBlock( 68 | markdown, 69 | isInsideCalloutDepth, 70 | ); 71 | } 72 | 73 | replacedText = replacedText.replace(block, `${markdown}`); 74 | } catch (e) { 75 | console.log(e); 76 | 77 | new Notice( 78 | "Unable to render dataview query. Please update the dataview plugin to the latest version.", 79 | ); 80 | 81 | return queryBlock[0]; 82 | } 83 | } 84 | 85 | for (const queryBlock of dataviewJsMatches) { 86 | try { 87 | const block = queryBlock[0]; 88 | const query = queryBlock[1]; 89 | 90 | const dataviewResult = await tryExecuteJs(query, file, dvApi); 91 | 92 | if (dataviewResult) { 93 | replacedText = replacedText.replace( 94 | block, 95 | dataviewResult.toString() ?? "", 96 | ); 97 | } 98 | } catch (e) { 99 | console.log(e); 100 | 101 | new Notice( 102 | "Unable to render dataviewjs query. Please update the dataview plugin to the latest version.", 103 | ); 104 | 105 | return queryBlock[0]; 106 | } 107 | } 108 | 109 | //Inline queries 110 | for (const inlineQuery of inlineMatches) { 111 | try { 112 | const code = inlineQuery[0]; 113 | const query = inlineQuery[1]; 114 | 115 | const dataviewResult = tryDVEvaluate(query.trim(), file, dvApi); 116 | 117 | if (dataviewResult) { 118 | replacedText = replacedText.replace( 119 | code, 120 | dataviewResult.toString() ?? "", 121 | ); 122 | } 123 | } catch (e) { 124 | console.log(e); 125 | 126 | new Notice( 127 | "Unable to render inline dataview query. Please update the dataview plugin to the latest version.", 128 | ); 129 | 130 | return inlineQuery[0]; 131 | } 132 | } 133 | 134 | for (const inlineJsQuery of inlineJsMatches) { 135 | try { 136 | const code = inlineJsQuery[0]; 137 | const query = inlineJsQuery[1]; 138 | 139 | let result: string | undefined | null = ""; 140 | 141 | result = tryDVEvaluate(query, file, dvApi); 142 | 143 | if (!result) { 144 | result = tryEval(query); 145 | } 146 | 147 | if (!result) { 148 | result = await tryExecuteJs(query, file, dvApi); 149 | } 150 | 151 | replacedText = replacedText.replace( 152 | code, 153 | result ?? "Unable to render query", 154 | ); 155 | } catch (e) { 156 | Logger.error(e); 157 | 158 | new Notice( 159 | "Unable to render inline dataviewjs query. Please update the dataview plugin to the latest version.", 160 | ); 161 | 162 | return inlineJsQuery[0]; 163 | } 164 | } 165 | 166 | return replacedText; 167 | }; 168 | 169 | /** 170 | * Splits input in lines. 171 | * Prepends the callout/quote sign to each line, 172 | * returns all the lines as a single string 173 | * 174 | */ 175 | surroundWithCalloutBlock(input: string, depth: number = 1): string { 176 | const tmp = input.split("\n"); 177 | 178 | const calloutSymbol = "> ".repeat(depth); 179 | 180 | return " " + tmp.join(`\n${calloutSymbol}`); 181 | } 182 | 183 | /** 184 | * Checks if a query is inside a callout block. 185 | * Removes the callout symbols and re-join sanitized parts. 186 | * Also returns the boolean that indicates if the query was inside a callout. 187 | * @param query 188 | * @returns 189 | */ 190 | sanitizeQuery(query: string): { 191 | isInsideCalloutDepth: number; 192 | finalQuery: string; 193 | } { 194 | let isInsideCalloutDepth = 0; 195 | const parts = query.split("\n"); 196 | const sanitized = []; 197 | 198 | for (const part of parts) { 199 | let depthPivot = 0; 200 | 201 | if (part.startsWith(">")) { 202 | depthPivot += 1; 203 | let intermediate = part.substring(1).trim(); 204 | 205 | while (intermediate.startsWith(">")) { 206 | intermediate = intermediate.substring(1).trim(); 207 | depthPivot += 1; 208 | } 209 | sanitized.push(intermediate); 210 | } else { 211 | sanitized.push(part); 212 | } 213 | isInsideCalloutDepth = Math.max(isInsideCalloutDepth, depthPivot); 214 | } 215 | let finalQuery = query; 216 | 217 | if (isInsideCalloutDepth > 0) { 218 | finalQuery = sanitized.join("\n"); 219 | } 220 | 221 | return { isInsideCalloutDepth, finalQuery }; 222 | } 223 | } 224 | 225 | function tryDVEvaluate( 226 | query: string, 227 | file: PublishFile, 228 | dvApi: DataviewApi, 229 | ): string | undefined | null { 230 | let result = ""; 231 | 232 | try { 233 | const dataviewResult = dvApi.tryEvaluate(query.trim(), { 234 | this: dvApi.page(file.getPath()) ?? {}, 235 | }); 236 | result = dataviewResult?.toString() ?? ""; 237 | } catch (e) { 238 | Logger.warn("dvapi.tryEvaluate did not yield any result", e); 239 | } 240 | 241 | return result; 242 | } 243 | 244 | function tryEval(query: string) { 245 | let result = ""; 246 | 247 | try { 248 | result = (0, eval)("const dv = DataviewAPI;" + query); //https://esbuild.github.io/content-types/#direct-eval 249 | } catch (e) { 250 | Logger.warn("eval did not yield any result", e); 251 | } 252 | 253 | return result; 254 | } 255 | 256 | async function tryExecuteJs( 257 | query: string, 258 | file: PublishFile, 259 | dvApi: DataviewApi, 260 | ) { 261 | const div = createEl("div"); 262 | const component = new Component(); 263 | component.load(); 264 | await dvApi.executeJs(query, div, component, file.getPath()); 265 | let counter = 0; 266 | 267 | while (!div.querySelector("[data-tag-name]") && counter < 100) { 268 | await delay(5); 269 | counter++; 270 | } 271 | 272 | const markdown = htmlToMarkdown(div) || ""; 273 | 274 | const result = cleanQueryResult(markdown); 275 | 276 | return result; 277 | } 278 | -------------------------------------------------------------------------------- /src/compiler/replaceBlockIDs.test.ts: -------------------------------------------------------------------------------- 1 | import replaceBlockIDs from "./replaceBlockIDs"; 2 | 3 | describe("replaceBlockIDs", () => { 4 | it("should replace block IDs in markdown", () => { 5 | const EXPECTED_MARKDOWN = ` 6 | # header 7 | 8 | foo ^block-id-1234 9 | 10 | bar ^block-id-5678 11 | `; 12 | 13 | const result = replaceBlockIDs(EXPECTED_MARKDOWN); 14 | 15 | expect(result).toBe(` 16 | # header 17 | 18 | foo ^block-id-1234 19 | 20 | bar ^block-id-5678 21 | `); 22 | }); 23 | 24 | it("should not replace block IDs in code blocks", () => { 25 | const CODEBLOCK_WITH_BLOCKIDS = ` 26 | \`\`\` 27 | foobar. 28 | this is a code block. 29 | but it contains a block ID to try to fool the compiler 30 | and, consequently, wreck your garden. 31 | here it goes: ^block-id-1234 32 | and for fun, here's another: ^block-id-5678 33 | \`\`\` 34 | 35 | additionally, block IDs outside of code blocks should be replaced 36 | for example, ^block-id-9999 37 | and ^block-id-0000 38 | `; 39 | 40 | const result = replaceBlockIDs(CODEBLOCK_WITH_BLOCKIDS); 41 | 42 | expect(result).toBe(` 43 | \`\`\` 44 | foobar. 45 | this is a code block. 46 | but it contains a block ID to try to fool the compiler 47 | and, consequently, wreck your garden. 48 | here it goes: ^block-id-1234 49 | and for fun, here's another: ^block-id-5678 50 | \`\`\` 51 | 52 | additionally, block IDs outside of code blocks should be replaced 53 | for example, ^block-id-9999 54 | and ^block-id-0000 55 | `); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/compiler/replaceBlockIDs.ts: -------------------------------------------------------------------------------- 1 | export default function replaceBlockIDs(markdown: string) { 2 | const block_pattern = / \^([\w\d-]+)/g; 3 | const complex_block_pattern = /\n\^([\w\d-]+)\n/g; 4 | 5 | // To ensure code blocks are not modified... 6 | const codeBlockPattern = /```[\s\S]*?```/g; 7 | 8 | // Extract code blocks and replace them with placeholders 9 | const codeBlocks: string[] = []; 10 | 11 | markdown = markdown.replace(codeBlockPattern, (match) => { 12 | codeBlocks.push(match); 13 | 14 | return `{{CODE_BLOCK_${codeBlocks.length - 1}}}`; 15 | }); 16 | 17 | // Replace patterns outside code blocks 18 | markdown = markdown.replace( 19 | complex_block_pattern, 20 | (_match: string, $1: string) => { 21 | return `\n^${$1}\n`; 22 | }, 23 | ); 24 | 25 | markdown = markdown.replace(block_pattern, (_match: string, $1: string) => { 26 | return ` ^${$1}`; 27 | }); 28 | 29 | // Reinsert code blocks 30 | codeBlocks.forEach((block, index) => { 31 | markdown = markdown.replace(`{{CODE_BLOCK_${index}}}`, block); 32 | }); 33 | 34 | return markdown; 35 | } 36 | -------------------------------------------------------------------------------- /src/models/SyncerTab.ts: -------------------------------------------------------------------------------- 1 | import { QuartzSettings } from "src/views/SettingsView/Views/QuartzSettings"; 2 | import { GithubSettings } from "src/views/SettingsView/Views/GithubSettings"; 3 | import { FrontmatterSettings } from "src/views/SettingsView/Views/FrontmatterSettings"; 4 | import { IntegrationSettings } from "src/views/SettingsView/Views/IntegrationSettings"; 5 | import { ThemesSettings } from "src/views/SettingsView/Views/ThemesSettings"; 6 | 7 | type QuartzSyncerSettingTabCollection = ( 8 | | QuartzSettings 9 | | GithubSettings 10 | | FrontmatterSettings 11 | | IntegrationSettings 12 | | ThemesSettings 13 | )[]; 14 | 15 | export default QuartzSyncerSettingTabCollection; 16 | -------------------------------------------------------------------------------- /src/models/TreeNode.ts: -------------------------------------------------------------------------------- 1 | type TreeNode = { 2 | name: string; 3 | children?: TreeNode[]; 4 | isRoot: boolean; 5 | path: string; 6 | checked: boolean; 7 | indeterminate: boolean; 8 | }; 9 | 10 | export default TreeNode; 11 | -------------------------------------------------------------------------------- /src/models/settings.ts: -------------------------------------------------------------------------------- 1 | import { ILogLevel } from "js-logger"; 2 | 3 | /** Saved to data.json, changing requires a migration */ 4 | export default interface QuartzSyncerSettings { 5 | githubToken: string; 6 | githubRepo: string; 7 | githubUserName: string; 8 | 9 | useFullResolutionImages: boolean; 10 | 11 | noteSettingsIsInitialized: boolean; 12 | 13 | contentFolder: string; 14 | vaultPath: string; 15 | 16 | showCreatedTimestamp: boolean; 17 | createdTimestampKey: string; 18 | 19 | showUpdatedTimestamp: boolean; 20 | updatedTimestampKey: string; 21 | 22 | showPublishedTimestamp: boolean; 23 | publishedTimestampKey: string; 24 | 25 | timestampFormat: string; 26 | 27 | pathRewriteRules: string; 28 | 29 | usePermalink: boolean; 30 | 31 | applyEmbeds: boolean; 32 | 33 | publishFrontmatterKey: string; 34 | 35 | useDatacore: boolean; 36 | useDataview: boolean; 37 | useExcalidraw: boolean; 38 | 39 | useThemes: boolean; 40 | 41 | includeAllFrontmatter: boolean; 42 | 43 | lastUsedSettingsTab: string; 44 | 45 | ENABLE_DEVELOPER_TOOLS?: boolean; 46 | devPluginPath?: string; 47 | logLevel?: ILogLevel; 48 | } 49 | -------------------------------------------------------------------------------- /src/publishFile/FileMetaDataManager.ts: -------------------------------------------------------------------------------- 1 | import { FrontMatterCache, TFile } from "obsidian"; 2 | import QuartzSyncerSettings from "src/models/settings"; 3 | import { DateTime } from "luxon"; 4 | 5 | export class FileMetadataManager { 6 | file: TFile; 7 | frontmatter: FrontMatterCache; 8 | settings: QuartzSyncerSettings; 9 | 10 | constructor( 11 | file: TFile, 12 | frontmatter: FrontMatterCache, 13 | settings: QuartzSyncerSettings, 14 | ) { 15 | this.file = file; 16 | this.frontmatter = frontmatter; 17 | this.settings = settings; 18 | } 19 | 20 | getCreatedAt(): string { 21 | const createdKey = this.settings.createdTimestampKey; 22 | 23 | if (createdKey) { 24 | const customCreatedDate = this.frontmatter[createdKey]; 25 | 26 | if (!customCreatedDate) { 27 | return ""; 28 | } 29 | 30 | return customCreatedDate; 31 | } 32 | 33 | return DateTime.fromMillis(this.file.stat.ctime).toISO() as string; 34 | } 35 | 36 | getUpdatedAt(): string { 37 | const updatedKey = this.settings.updatedTimestampKey; 38 | 39 | if (updatedKey) { 40 | const customUpdatedDate = this.frontmatter[updatedKey]; 41 | 42 | if (!customUpdatedDate) { 43 | return ""; 44 | } 45 | 46 | return this.frontmatter[updatedKey]; 47 | } 48 | 49 | return DateTime.fromMillis(this.file.stat.mtime).toISO() as string; 50 | } 51 | 52 | getPublishedAt(): string { 53 | const publishedKey = this.settings.publishedTimestampKey; 54 | 55 | if (publishedKey) { 56 | const customPublishedDate = this.frontmatter[publishedKey]; 57 | 58 | if (!customPublishedDate) { 59 | return ""; 60 | } 61 | 62 | return customPublishedDate; 63 | } 64 | 65 | return DateTime.fromMillis(this.file.stat.mtime).toISO() as string; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/publishFile/ObsidianFrontMatterEngine.ts: -------------------------------------------------------------------------------- 1 | import { MetadataCache, TFile, Vault } from "obsidian"; 2 | 3 | export interface IFrontMatterEngine { 4 | set(key: string, value: string | boolean | number): IFrontMatterEngine; 5 | remove(key: string): IFrontMatterEngine; 6 | get(key: string): string | boolean | number; 7 | apply(): Promise; 8 | } 9 | 10 | export default class ObsidianFrontMatterEngine implements IFrontMatterEngine { 11 | metadataCache: MetadataCache; 12 | file: TFile; 13 | vault: Vault; 14 | 15 | generatedFrontMatter: Record = {}; 16 | 17 | constructor(vault: Vault, metadataCache: MetadataCache, file: TFile) { 18 | this.metadataCache = metadataCache; 19 | this.vault = vault; 20 | this.file = file; 21 | } 22 | 23 | set( 24 | key: string, 25 | value: string | boolean | number, 26 | ): ObsidianFrontMatterEngine { 27 | this.generatedFrontMatter[key] = value; 28 | 29 | return this; 30 | } 31 | 32 | remove(key: string): ObsidianFrontMatterEngine { 33 | this.generatedFrontMatter[key] = undefined; 34 | 35 | return this; 36 | } 37 | 38 | get(key: string): string | boolean | number { 39 | return this.getFrontMatterSnapshot()[key]; 40 | } 41 | 42 | async apply(): Promise { 43 | const newFrontMatter = this.getFrontMatterSnapshot(); 44 | 45 | const content = await this.vault.cachedRead(this.file); 46 | const frontmatterRegex = /^\s*?---\n([\s\S]*?)\n---/g; 47 | const yaml = this.frontMatterToYaml(newFrontMatter); 48 | let newContent = ""; 49 | 50 | if (content.match(frontmatterRegex)) { 51 | newContent = content.replace(frontmatterRegex, (_match) => { 52 | return yaml; 53 | }); 54 | } else { 55 | newContent = `${yaml}\n${content}`; 56 | } 57 | 58 | await this.vault.modify(this.file, newContent); 59 | } 60 | 61 | private frontMatterToYaml(frontMatter: Record) { 62 | for (const key of Object.keys(frontMatter)) { 63 | if (frontMatter[key] === undefined) { 64 | delete frontMatter[key]; 65 | } 66 | } 67 | 68 | if (Object.keys(frontMatter).length === 0) { 69 | return ""; 70 | } 71 | 72 | let yaml = "---\n"; 73 | 74 | for (const key of Object.keys(frontMatter)) { 75 | yaml += `${key}: ${frontMatter[key]}\n`; 76 | } 77 | yaml += "---"; 78 | 79 | return yaml; 80 | } 81 | 82 | private getFrontMatterSnapshot() { 83 | const cachedFrontMatter = { 84 | ...this.metadataCache.getCache(this.file?.path)?.frontmatter, 85 | }; 86 | delete cachedFrontMatter["position"]; 87 | 88 | return { ...cachedFrontMatter, ...this.generatedFrontMatter }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/publishFile/PublishFile.ts: -------------------------------------------------------------------------------- 1 | import { MetadataCache, TFile, Vault } from "obsidian"; 2 | import { 3 | SyncerPageCompiler, 4 | TCompiledFile, 5 | } from "src/compiler/SyncerPageCompiler"; 6 | import { 7 | FrontmatterCompiler, 8 | TFrontmatter, 9 | } from "src/compiler/FrontmatterCompiler"; 10 | import QuartzSyncerSettings from "src/models/settings"; 11 | import { hasPublishFlag } from "src/publishFile/Validator"; 12 | import { FileMetadataManager } from "src/publishFile/FileMetaDataManager"; 13 | 14 | interface IPublishFileProps { 15 | file: TFile; 16 | vault: Vault; 17 | compiler: SyncerPageCompiler; 18 | metadataCache: MetadataCache; 19 | settings: QuartzSyncerSettings; 20 | } 21 | 22 | export class PublishFile { 23 | file: TFile; 24 | compiler: SyncerPageCompiler; 25 | vault: Vault; 26 | compiledFile?: TCompiledFile; 27 | metadataCache: MetadataCache; 28 | frontmatter: TFrontmatter; 29 | settings: QuartzSyncerSettings; 30 | // Access props and other file metadata 31 | meta: FileMetadataManager; 32 | 33 | constructor({ 34 | file, 35 | compiler, 36 | metadataCache, 37 | vault, 38 | settings, 39 | }: IPublishFileProps) { 40 | this.compiler = compiler; 41 | this.metadataCache = metadataCache; 42 | this.file = file; 43 | this.settings = settings; 44 | this.vault = vault; 45 | this.frontmatter = this.getFrontmatter(); 46 | 47 | this.meta = new FileMetadataManager(file, this.frontmatter, settings); 48 | } 49 | 50 | async compile(): Promise { 51 | const compiledFile = await this.compiler.generateMarkdown(this); 52 | 53 | return new CompiledPublishFile( 54 | { 55 | file: this.file, 56 | compiler: this.compiler, 57 | metadataCache: this.metadataCache, 58 | vault: this.vault, 59 | settings: this.settings, 60 | }, 61 | compiledFile, 62 | ); 63 | } 64 | 65 | // TODO: This doesn't work yet, but file should be able to tell it's type 66 | getType(): "excalidraw" | "markdown" { 67 | if (this.file.name.endsWith(".excalidraw")) { 68 | return "excalidraw"; 69 | } 70 | 71 | return "markdown"; 72 | } 73 | 74 | shouldPublish(): boolean { 75 | return hasPublishFlag( 76 | this.settings.publishFrontmatterKey, 77 | this.frontmatter, 78 | ); 79 | } 80 | 81 | async getBlobLinks() { 82 | return this.compiler.extractBlobLinks(this); 83 | } 84 | 85 | async cachedRead() { 86 | return this.vault.cachedRead(this.file); 87 | } 88 | 89 | getMetadata() { 90 | return this.metadataCache.getCache(this.file.path) ?? {}; 91 | } 92 | 93 | getBlock(blockId: string) { 94 | return this.getMetadata().blocks?.[blockId]; 95 | } 96 | 97 | getFrontmatter() { 98 | return this.metadataCache.getCache(this.file.path)?.frontmatter ?? {}; 99 | } 100 | 101 | /** Add other possible sorting logic here, eg if we add sortWeight 102 | * We might also want to sort by meta.getPath for rewritten garden path 103 | */ 104 | compare(other: PublishFile) { 105 | return this.file.path.localeCompare(other.file.path); 106 | } 107 | 108 | getPath = () => this.file.path; 109 | getVaultPath = () => { 110 | if ( 111 | this.settings.vaultPath !== "/" && 112 | this.file.path.startsWith(this.settings.vaultPath) 113 | ) { 114 | return this.file.path.replace(this.settings.vaultPath, ""); 115 | } 116 | 117 | return this.file.path; 118 | }; 119 | getCompiledFrontmatter() { 120 | const frontmatterCompiler = new FrontmatterCompiler(this.settings); 121 | 122 | const metadata = 123 | this.metadataCache.getCache(this.file.path)?.frontmatter ?? {}; 124 | 125 | return frontmatterCompiler.compile(this, metadata); 126 | } 127 | } 128 | 129 | export class CompiledPublishFile extends PublishFile { 130 | compiledFile: TCompiledFile; 131 | remoteHash?: string; 132 | 133 | constructor(props: IPublishFileProps, compiledFile: TCompiledFile) { 134 | super(props); 135 | 136 | this.compiledFile = compiledFile; 137 | } 138 | 139 | getCompiledFile() { 140 | return this.compiledFile; 141 | } 142 | 143 | setRemoteHash(hash: string) { 144 | this.remoteHash = hash; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/publishFile/Validator.ts: -------------------------------------------------------------------------------- 1 | import { FrontMatterCache, Notice } from "obsidian"; 2 | 3 | export const hasPublishFlag = ( 4 | flag: string, 5 | frontMatter?: FrontMatterCache, 6 | ): boolean => !!frontMatter?.[flag]; 7 | 8 | export function isPublishFrontmatterValid( 9 | flag: string, 10 | frontMatter?: FrontMatterCache, 11 | ): boolean { 12 | if (!hasPublishFlag(flag, frontMatter)) { 13 | new Notice( 14 | "Note does not have the publish: true set. Please add this and try again.", 15 | ); 16 | 17 | return false; 18 | } 19 | 20 | return true; 21 | } 22 | -------------------------------------------------------------------------------- /src/publisher/PublishStatusManager.ts: -------------------------------------------------------------------------------- 1 | import QuartzSyncerSiteManager from "src/repositoryConnection/QuartzSyncerSiteManager"; 2 | import Publisher from "src/publisher/Publisher"; 3 | import { generateBlobHash } from "src/utils/utils"; 4 | import { CompiledPublishFile } from "src/publishFile/PublishFile"; 5 | 6 | /** 7 | * Manages the publishing status of notes and blobs for a digital garden. 8 | */ 9 | export default class PublishStatusManager implements IPublishStatusManager { 10 | siteManager: QuartzSyncerSiteManager; 11 | publisher: Publisher; 12 | 13 | constructor(siteManager: QuartzSyncerSiteManager, publisher: Publisher) { 14 | this.siteManager = siteManager; 15 | this.publisher = publisher; 16 | } 17 | 18 | getDeletedNotePaths(): Promise { 19 | throw new Error("Method not implemented."); 20 | } 21 | 22 | getDeletedBlobsPaths(): Promise { 23 | throw new Error("Method not implemented."); 24 | } 25 | 26 | private generateDeletedContentPaths( 27 | remoteNoteHashes: { [key: string]: string }, 28 | marked: string[], 29 | ): Array<{ path: string; sha: string }> { 30 | const isJsFile = (key: string) => key.endsWith(".js"); 31 | 32 | const isMarkedForPublish = (key: string) => 33 | marked.find((f) => f === key); 34 | 35 | const deletedPaths = Object.keys(remoteNoteHashes).filter( 36 | (key) => !isJsFile(key) && !isMarkedForPublish(key), 37 | ); 38 | 39 | const pathsWithSha = deletedPaths.map((path) => { 40 | return { 41 | path, 42 | sha: remoteNoteHashes[path], 43 | }; 44 | }); 45 | 46 | return pathsWithSha; 47 | } 48 | 49 | async getPublishStatus(): Promise { 50 | const unpublishedNotes: Array = []; 51 | const publishedNotes: Array = []; 52 | const changedNotes: Array = []; 53 | 54 | const contentTree = 55 | await this.siteManager.userSyncerConnection.getContent("HEAD"); 56 | 57 | if (!contentTree) { 58 | throw new Error("Could not get content tree from base garden"); 59 | } 60 | 61 | const remoteNoteHashes = 62 | await this.siteManager.getNoteHashes(contentTree); 63 | 64 | const remoteBlobHashes = 65 | await this.siteManager.getBlobHashes(contentTree); 66 | 67 | const marked = await this.publisher.getFilesMarkedForPublishing(); 68 | 69 | for (const file of marked.notes) { 70 | const compiledFile = await file.compile(); 71 | const [content, _] = compiledFile.getCompiledFile(); 72 | 73 | const localHash = generateBlobHash(content); 74 | const remoteHash = remoteNoteHashes[file.getVaultPath()]; 75 | 76 | if (!remoteHash) { 77 | unpublishedNotes.push(compiledFile); 78 | } else if (remoteHash === localHash) { 79 | compiledFile.setRemoteHash(remoteHash); 80 | publishedNotes.push(compiledFile); 81 | } else { 82 | compiledFile.setRemoteHash(remoteHash); 83 | changedNotes.push(compiledFile); 84 | } 85 | } 86 | 87 | const deletedNotePaths = this.generateDeletedContentPaths( 88 | remoteNoteHashes, 89 | marked.notes.map((f) => f.getVaultPath()), 90 | ); 91 | 92 | const deletedBlobPaths = this.generateDeletedContentPaths( 93 | remoteBlobHashes, 94 | marked.blobs, 95 | ); 96 | // These might already be sorted, as getFilesMarkedForPublishing sorts already 97 | publishedNotes.sort((a, b) => a.compare(b)); 98 | publishedNotes.sort((a, b) => a.compare(b)); 99 | changedNotes.sort((a, b) => a.compare(b)); 100 | deletedNotePaths.sort((a, b) => a.path.localeCompare(b.path)); 101 | 102 | return { 103 | unpublishedNotes, 104 | publishedNotes, 105 | changedNotes, 106 | deletedNotePaths, 107 | deletedBlobPaths, 108 | }; 109 | } 110 | } 111 | 112 | interface PathToRemove { 113 | path: string; 114 | sha: string; 115 | } 116 | 117 | export interface PublishStatus { 118 | unpublishedNotes: Array; 119 | publishedNotes: Array; 120 | changedNotes: Array; 121 | deletedNotePaths: Array; 122 | deletedBlobPaths: Array; 123 | } 124 | 125 | export interface IPublishStatusManager { 126 | getPublishStatus(): Promise; 127 | } 128 | -------------------------------------------------------------------------------- /src/repositoryConnection/QuartzSyncerSiteManager.ts: -------------------------------------------------------------------------------- 1 | import type QuartzSyncerSettings from "src/models/settings"; 2 | import { type MetadataCache, Notice } from "obsidian"; 3 | import { Base64 } from "js-base64"; 4 | import { 5 | RepositoryConnection, 6 | TRepositoryContent, 7 | } from "src/repositoryConnection/RepositoryConnection"; 8 | import Logger from "js-logger"; 9 | 10 | const logger = Logger.get("quartz-syncer-site-manager"); 11 | export interface PathRewriteRule { 12 | from: string; 13 | to: string; 14 | } 15 | 16 | export type VaultPathRule = PathRewriteRule; 17 | 18 | type ContentTreeItem = { 19 | path: string; 20 | sha: string; 21 | type: string; 22 | }; 23 | 24 | /** 25 | * Manages the Quartz website contents by handling various site configurations, files, 26 | * and interactions with GitHub via Octokit. Responsible for operations like updating 27 | * environment variables, fetching and updating notes & blobs, and creating pull requests 28 | * for site changes. 29 | */ 30 | 31 | export default class QuartzSyncerSiteManager { 32 | settings: QuartzSyncerSettings; 33 | metadataCache: MetadataCache; 34 | baseSyncerConnection: RepositoryConnection; 35 | userSyncerConnection: RepositoryConnection; 36 | 37 | constructor(metadataCache: MetadataCache, settings: QuartzSyncerSettings) { 38 | this.settings = settings; 39 | this.metadataCache = metadataCache; 40 | 41 | this.baseSyncerConnection = new RepositoryConnection({ 42 | githubToken: settings.githubToken, 43 | githubUserName: "saberzero1", 44 | quartzRepository: "quartz", 45 | contentFolder: "content", 46 | vaultPath: "/", 47 | }); 48 | 49 | this.userSyncerConnection = new RepositoryConnection({ 50 | githubToken: settings.githubToken, 51 | githubUserName: settings.githubUserName, 52 | quartzRepository: settings.githubRepo, 53 | contentFolder: settings.contentFolder, 54 | vaultPath: settings.vaultPath, 55 | }); 56 | } 57 | 58 | async updateEnv() { 59 | const envValues = { 60 | SHOW_CREATED_TIMESTAMP: this.settings.showCreatedTimestamp, 61 | SHOW_UPDATED_TIMESTAMP: this.settings.showUpdatedTimestamp, 62 | SHOW_PUBLISHED_TIMESTAMP: this.settings.showPublishedTimestamp, 63 | TIMESTAMP_FORMAT: this.settings.timestampFormat, 64 | USE_FULL_RESOLUTION_IMAGES: this.settings.useFullResolutionImages, 65 | } as Record; 66 | 67 | const keysToSet = { 68 | ...envValues, 69 | }; 70 | 71 | const envSettings = Object.entries(keysToSet) 72 | .map(([key, value]) => `${key}=${value}`) 73 | .join("\n"); 74 | 75 | const base64Settings = Base64.encode(envSettings); 76 | 77 | const currentFile = await this.userSyncerConnection.getFile(".env"); 78 | 79 | const decodedCurrentFile = Base64.decode(currentFile?.content ?? ""); 80 | 81 | if (decodedCurrentFile === envSettings) { 82 | logger.info("No changes to .env file"); 83 | 84 | new Notice("Settings already up to date!"); 85 | 86 | return; 87 | } 88 | 89 | await this.userSyncerConnection.updateFile({ 90 | path: ".env", 91 | content: base64Settings, 92 | message: "Update settings", 93 | sha: currentFile?.sha, 94 | }); 95 | } 96 | 97 | async getNoteContent(path: string): Promise { 98 | if (path.startsWith("/")) { 99 | path = path.substring(1); 100 | } 101 | 102 | const response = await this.userSyncerConnection.getFile( 103 | `${this.settings.contentFolder}/${path}`, 104 | ); 105 | 106 | if (!response) { 107 | return ""; 108 | } 109 | 110 | const content = Base64.decode(response.content); 111 | 112 | return content; 113 | } 114 | 115 | async getNoteHashes( 116 | contentTree: NonNullable, 117 | ): Promise> { 118 | const files = contentTree.tree; 119 | 120 | const notes = files.filter( 121 | (x): x is ContentTreeItem => 122 | typeof x.path === "string" && 123 | x.path.startsWith(this.settings.contentFolder) && 124 | x.type === "blob" && 125 | x.path.endsWith(".md"), 126 | ); 127 | const hashes: Record = {}; 128 | 129 | for (const note of notes) { 130 | const vaultPath = note.path.replace( 131 | this.settings.contentFolder, 132 | "", 133 | ); 134 | //.replace(this.settings.vaultPath, ""); 135 | 136 | const actualVaultPath = vaultPath.startsWith("/") 137 | ? vaultPath.substring(1) 138 | : vaultPath; 139 | hashes[actualVaultPath] = note.sha; 140 | } 141 | 142 | return hashes; 143 | } 144 | 145 | async getBlobHashes( 146 | contentTree: NonNullable, 147 | ): Promise> { 148 | const files = contentTree.tree ?? []; 149 | 150 | const blobs = files.filter( 151 | (x): x is ContentTreeItem => 152 | typeof x.path === "string" && 153 | x.path.startsWith(this.settings.contentFolder) && 154 | x.type === "blob", 155 | ); 156 | const hashes: Record = {}; 157 | 158 | for (const blob of blobs) { 159 | const vaultPath = blob.path.replace( 160 | this.settings.contentFolder, 161 | "", 162 | ); 163 | 164 | const actualVaultPath = vaultPath.startsWith("/") 165 | ? vaultPath.substring(1) 166 | : vaultPath; 167 | hashes[actualVaultPath] = blob.sha; 168 | } 169 | 170 | return hashes; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/test/snapshot/generateSyncerSnapshot.ts: -------------------------------------------------------------------------------- 1 | import Publisher from "src/publisher/Publisher"; 2 | import { Notice, Platform } from "obsidian"; 3 | import QuartzSyncerSettings from "src/models/settings"; 4 | import fs from "fs/promises"; 5 | 6 | const SNAPSHOT_PATH = "src/test/snapshot/snapshot.md"; 7 | 8 | export const generateSyncerSnapshot = async ( 9 | settings: QuartzSyncerSettings, 10 | publisher: Publisher, 11 | ) => { 12 | const devPluginPath = settings.devPluginPath; 13 | 14 | if (!devPluginPath) { 15 | new Notice("devPluginPath missing, run generateSyncerSettings.mjs"); 16 | 17 | return; 18 | } 19 | 20 | const marked = await publisher.getFilesMarkedForPublishing(); 21 | let fileString = "IMAGES: \n"; 22 | fileString += marked.blobs.map((path) => `${path}\n`); 23 | 24 | const assetPaths = new Set(); 25 | 26 | for (const file of marked.notes) { 27 | fileString += "==========\n"; 28 | fileString += `${file.getPath()}\n`; 29 | fileString += "==========\n"; 30 | 31 | const [content, assets] = 32 | await publisher.compiler.generateMarkdown(file); 33 | assets.blobs.map((blob) => assetPaths.add(blob.path)); 34 | 35 | fileString += `${content}\n`; 36 | fileString += Array.from(assetPaths).map((path) => `${path}\n`); 37 | } 38 | fileString += "==========\n"; 39 | 40 | const fullSnapshotPath = `${devPluginPath}/${SNAPSHOT_PATH}`; 41 | 42 | if (Platform.isDesktop) { 43 | await fs.writeFile(fullSnapshotPath, fileString); 44 | } 45 | new Notice(`Snapshot written to ${fullSnapshotPath}`); 46 | new Notice(`Check snapshot to make sure nothing has accidentally changed`); 47 | }; 48 | -------------------------------------------------------------------------------- /src/ui/Icon.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | {@html getIcon(name)?.outerHTML} 7 | -------------------------------------------------------------------------------- /src/ui/LineDiff.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | {#if diff} 8 |
9 | {#each diff as part} 10 | {#if part.added} 11 | {part.value} 15 | {:else if part.removed} 16 | {part.value} 20 | {/if} 21 | {/each} 22 |
23 | {/if} 24 |
25 | -------------------------------------------------------------------------------- /src/ui/TreeView/TreeNode.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 59 | 60 |
    61 |
  • 62 | {#if tree.children} 63 | 64 | 65 | 66 | 67 | 68 | 69 | {#if !isRoot} 70 | 71 | {#if !readOnly} 72 | 81 | {/if} 82 | {tree.name} 83 | {:else} 84 | {#if !readOnly} 85 | 94 | {/if} 95 | 96 | {tree.name} 99 | {/if} 100 | 101 | {#if expanded} 102 | {#each tree.children as child} 103 | dispatchShowDiff(e.detail.node)} 106 | {enableShowDiff} 107 | {readOnly} 108 | tree={child} 109 | /> 110 | {/each} 111 | {/if} 112 | {:else if !isRoot} 113 | 114 | 115 | 116 | 117 | {#if !readOnly} 118 | 127 | {/if} 128 | 129 | {tree.name} 130 | 131 | {#if enableShowDiff} 132 | 137 | 138 | 139 | {/if} 140 | 141 | {/if} 142 |
  • 143 |
144 | -------------------------------------------------------------------------------- /src/ui/TreeView/TreeView.svelte: -------------------------------------------------------------------------------- 1 | 71 | 72 |
73 | showDiff(e.detail.node.path)} 79 | /> 80 |
81 | -------------------------------------------------------------------------------- /src/ui/suggest/constants.ts: -------------------------------------------------------------------------------- 1 | export const quartzSyncerIcon = ``; 2 | -------------------------------------------------------------------------------- /src/ui/suggest/folder.ts: -------------------------------------------------------------------------------- 1 | import { App, AbstractInputSuggest, TFolder } from "obsidian"; 2 | 3 | export class FolderSuggest extends AbstractInputSuggest { 4 | private folders: string[]; 5 | private inputEl: HTMLInputElement; 6 | 7 | constructor(app: App, inputEl: HTMLInputElement) { 8 | super(app, inputEl); 9 | 10 | this.inputEl = inputEl; 11 | 12 | this.folders = ["/"].concat( 13 | this.app.vault 14 | .getAllLoadedFiles() 15 | .filter((item) => item instanceof TFolder) 16 | .map((folder) => folder.path + "/") 17 | .filter((folder) => folder !== "//"), 18 | ); 19 | } 20 | 21 | getSuggestions(inputStr: string): string[] { 22 | const inputLower = inputStr.toLowerCase(); 23 | 24 | return this.folders.filter((folder) => 25 | folder.toLowerCase().includes(inputLower), 26 | ); 27 | } 28 | 29 | renderSuggestion(folder: string, el: HTMLElement): void { 30 | el.setText(folder); 31 | } 32 | 33 | selectSuggestion(folder: string): void { 34 | this.inputEl.value = folder; 35 | const event = new Event("input"); 36 | this.inputEl.dispatchEvent(event); 37 | this.close(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/markdown.ts: -------------------------------------------------------------------------------- 1 | const seperateHashesFromHeader = ( 2 | rawHeading: string, 3 | ): { hashes: string; title: string } => { 4 | const regex = /^(?#+)(?\s?)(?.*)$/; 5 | const matches = rawHeading.match(regex); 6 | 7 | if (matches?.groups) { 8 | const hashes = matches.groups["hashes"]; 9 | const title = matches.groups["title"]; 10 | 11 | return { 12 | hashes, 13 | title, 14 | }; 15 | } 16 | 17 | // always return one hash for valid md heading 18 | return { hashes: "#", title: rawHeading.trim() }; 19 | }; 20 | 21 | export const fixMarkdownHeaderSyntax = (rawHeading: string): string => { 22 | const { hashes, title } = seperateHashesFromHeader(rawHeading); 23 | 24 | return `${hashes} ${title}`; 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/regexes.ts: -------------------------------------------------------------------------------- 1 | export const FRONTMATTER_REGEX = /^\s*?---\n([\s\S]*?)\n---/g; 2 | 3 | export const BLOCKREF_REGEX = /(\^\w+(\n|$))/g; 4 | 5 | export const CODE_FENCE_REGEX = /`(.*?)`/g; 6 | 7 | export const CODEBLOCK_REGEX = /```.*?\n[\s\S]+?```/g; 8 | 9 | export const EXCALIDRAW_REGEX = /:\[\[(\d*?,\d*?)\],.*?\]\]/g; 10 | 11 | export const TRANSCLUDED_SVG_REGEX = 12 | /!\[\[(.*?)(\.(svg))\|(.*?)\]\]|!\[\[(.*?)(\.(svg))\]\]/g; 13 | 14 | export const DATAVIEW_LINK_TARGET_BLANK_REGEX = 15 | /target=["']_blank["'] rel=["']noopener["']/g; 16 | 17 | export const TRANSCLUDED_FILE_REGEX = 18 | /!\[\[(.*?)(\.(png|jpg|jpeg|gif|webp|mp4|mkv|mov|avi|mp3|wav|ogg|pdf))\|(.*?)\]\]|!\[\[(.*?)(\.(png|jpg|jpeg|gif|webp|mp4|mkv|mov|avi|mp3|wav|ogg|pdf))\]\]/g; 19 | 20 | export const FILE_REGEX = 21 | /!\[(.*?)\]\((.*?)(\.(png|jpg|jpeg|gif|webp|mp4|mkv|mov|avi|mp3|wav|ogg|pdf))\)/g; 22 | -------------------------------------------------------------------------------- /src/utils/styles.ts: -------------------------------------------------------------------------------- 1 | export const datacoreCallout = `.datacore .callout-content,.datacore.callout{transition:.1s cubic-bezier(.02, .01, .47, 1);margin-top:10px;margin-bottom:10px}.datacore .callout-fold{align-self:center}`; 2 | export const datacoreCard = `.datacore-card{display:flex;flex-direction:column;padding:1.2rem;border-radius:.5em;background-color:var(--background-secondary,rgba(0,0,0,0)); min-width: 89%; border: 2px solid var(--table-border-color,var(--gray)); overflow-y: auto;}.datacore-card-title { margin-bottom: .6em; display: flex; justify-content: space-between; font-size: 1.8em;}.datacore-card-title.centered { justify-content: center !important;}.datacore-card-content,.datacore-card-inner,.datacore-card { transition: all .3s cubic-bezier(.65,.05,.36,1);}.datacore-card-inner { overflow-y: auto; overflow-x: hidden; max-height: 500px;}.datacore-card .datacore-card-collapser,.datacore-card.is-collapsed .datacore-card-collapser { transition: all .5s cubic-bezier(.65,.05,.36,1);}.datacore-card-content { flex-grow: 1;}.datacore-card-inner { display: flex;}.datacore-card:not(.datacore-card.is-collapsed) .datacore-card-collapser { transform: rotate(180deg);}.datacore-card.is-collapsed .datacore-card-collapser { transform: rotate(0deg) !important;}.datacore-card-collapse,.datacore-card-collapser svg { min-width: 1em; min-height: 1em; fill: currentColor; vertical-align: middle;}.datacore-card.is-collapsed .datacore-card-collapser { transform: rotate(0deg);}.datacore-card .datacore-card-footer { font-size: .7em; text-align: right; padding: 0;}`; 3 | export const datacoreEmbed = `.datacore-span-embed{position:relative;padding:1px 8px;margin:4px 0;background-color:var(--color-base-25,var(--light-gray))}.datacore-embed-source{position:absolute;top:4px;right:4px;padding-left:4px;padding-right:4px;background-color:var(--background-secondary-alt,var(--light-gray));color:var(--text-faint,var(--dark-gray));font-size:var(--font-smallest, 12px)}`; 4 | export const datacoreFields = `.datacore-list-item-fields{color:var(--text-normal,var(--dark-gray))!important}.datacore-list-item-fields>.datacore-field+.datacore-field{margin-left:.4em}.datacore-field{display:inline-flex;align-items:center;box-sizing:border-box;border-radius:.25em;font-size:.85em;align-items:center}.datacore-field .field-title{flex-grow:0;font-weight:700;height:inherit;display:inline-block}`; 5 | export const datacoreTable = `.datacore-table{width:100%}.datacore-table>tbody>tr,.datacore-table>thead>tr{margin-top:1em;margin-bottom:1em;text-align:left}.datacore-table>tbody>tr:hover{background-color:var(--text-selection,transparent)!important}.datacore-table>thead>tr>th{font-weight:700;font-size:larger;border-top:none;border-left:none;border-right:none;border-bottom:solid;max-width:100%}.datacore-table>tbody>tr>td{text-align:left;border:none;font-weight:400;max-width:100%}.datacore-table ol,.datacore-table ul{margin-block-start:0.2em!important;margin-block-end:0.2em!important}.datacore-table-header-cell-content{width:auto;display:inline-flex;flex-direction:row}.datacore-table-sort{flex-grow:0;margin-right:.25em;align-items:center}.datacore-table-header-title{align-items:center;flex-grow:1}`; 6 | -------------------------------------------------------------------------------- /src/utils/utils.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | import { getSyncerPathForNote, getRewriteRules, wrapAround } from "./utils"; 3 | import { PathRewriteRule } from "src/repositoryConnection/QuartzSyncerSiteManager"; 4 | 5 | describe("utils", () => { 6 | describe("getSyncerPathForNote", () => { 7 | const TESTS: Array<{ 8 | name: string; 9 | input: { quartzPath: string; rule: PathRewriteRule }; 10 | expected: string; 11 | }> = [ 12 | { 13 | name: "replaces a path according to rules", 14 | input: { 15 | quartzPath: "defaultSyncerPath/content/note.md", 16 | rule: { from: "defaultSyncerPath", to: "quartzPath" }, 17 | }, 18 | expected: "quartzPath/content/note.md", 19 | }, 20 | ]; 21 | 22 | for (const test of TESTS) { 23 | it(test.name, () => { 24 | assert.strictEqual( 25 | getSyncerPathForNote( 26 | test.input.quartzPath, 27 | test.input.rule, 28 | ), 29 | test.expected, 30 | ); 31 | }); 32 | } 33 | 34 | it("handles rewrites to base path correctly", () => { 35 | const rewriteRule: PathRewriteRule = { 36 | from: "defaultSyncerPath", 37 | to: "", 38 | }; 39 | const quartzPath = "defaultSyncerPath/content/note.md"; 40 | 41 | const result = getSyncerPathForNote(quartzPath, rewriteRule); 42 | 43 | expect(result).toBe("content/note.md"); 44 | }); 45 | }); 46 | 47 | describe("getRewriteRules", () => { 48 | const TESTS: Array<{ 49 | name: string; 50 | input: string; 51 | expected: PathRewriteRule; 52 | }> = [ 53 | { 54 | name: "returns an empty array when no rules are provided", 55 | input: "", 56 | expected: { from: "", to: "/" }, 57 | }, 58 | { 59 | name: "parses a single rewrite rule", 60 | input: "defaultSyncerPath", 61 | expected: { from: "defaultSyncerPath", to: "/" }, 62 | }, 63 | ]; 64 | 65 | for (const test of TESTS) { 66 | it(test.name, () => { 67 | assert.deepStrictEqual( 68 | getRewriteRules(test.input), 69 | test.expected, 70 | ); 71 | }); 72 | } 73 | }); 74 | 75 | describe("wrapAround", () => { 76 | it("wraps around a positive number", () => { 77 | assert.strictEqual(wrapAround(5, 2), 1); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/views/PublicationCenter/DiffView.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import * as Diff from "diff"; 3 | export let diff: Diff.Change[]; 4 | </script> 5 | 6 | <div> 7 | <div class="info callout"> 8 | Differences between your local file and the published file. 9 | <br /> 10 | The content may look a bit different from your note. This is because it shows 11 | the note after being processed by the plugin. 12 | </div> 13 | <hr /> 14 | {#if diff} 15 | <div> 16 | {#each diff as part} 17 | {#if part.added} 18 | <pre 19 | class="quartz-syncer-diff-view quartz-syncer-diff-added">{part.value}</pre> 20 | {:else if part.removed} 21 | <pre 22 | class="quartz-syncer-diff-view quartz-syncer-diff-removed">{part.value}</pre> 23 | {:else} 24 | <pre>{part.value}</pre> 25 | {/if} 26 | {/each} 27 | </div> 28 | {/if} 29 | </div> 30 | -------------------------------------------------------------------------------- /src/views/PublicationCenter/PublicationCenter.ts: -------------------------------------------------------------------------------- 1 | import { type App, Modal, getIcon, Vault, TFile } from "obsidian"; 2 | import QuartzSyncerSettings from "src/models/settings"; 3 | import { PublishFile } from "src/publishFile/PublishFile"; 4 | import QuartzSyncerSiteManager from "src/repositoryConnection/QuartzSyncerSiteManager"; 5 | import PublishStatusManager from "src/publisher/PublishStatusManager"; 6 | import Publisher from "src/publisher/Publisher"; 7 | import PublicationCenterSvelte from "src/views/PublicationCenter/PublicationCenter.svelte"; 8 | import DiffView from "src/views/PublicationCenter/DiffView.svelte"; 9 | import * as Diff from "diff"; 10 | 11 | export class PublicationCenter { 12 | modal: Modal; 13 | settings: QuartzSyncerSettings; 14 | publishStatusManager: PublishStatusManager; 15 | publisher: Publisher; 16 | siteManager: QuartzSyncerSiteManager; 17 | vault: Vault; 18 | 19 | publicationCenterUi!: PublicationCenterSvelte; 20 | 21 | constructor( 22 | app: App, 23 | publishStatusManager: PublishStatusManager, 24 | publisher: Publisher, 25 | siteManager: QuartzSyncerSiteManager, 26 | settings: QuartzSyncerSettings, 27 | ) { 28 | this.modal = new Modal(app); 29 | this.settings = settings; 30 | this.publishStatusManager = publishStatusManager; 31 | this.publisher = publisher; 32 | this.siteManager = siteManager; 33 | this.vault = app.vault; 34 | 35 | this.modal.titleEl 36 | .createEl("span", { text: "Publication center" }) 37 | .prepend(this.getIcon("quartz-syncer-icon")); 38 | 39 | this.modal.titleEl.addClass("quartz-syncer-modal-title"); 40 | this.modal.contentEl.addClass("quartz-syncer-modal-content"); 41 | } 42 | 43 | getIcon(name: string): Node { 44 | const icon = getIcon(name) ?? document.createElement("span"); 45 | 46 | if (icon instanceof SVGSVGElement) { 47 | icon.addClass("quartz-syncer-svg-icon"); 48 | } 49 | 50 | return icon; 51 | } 52 | 53 | private showDiff = async (notePath: string) => { 54 | try { 55 | const remoteContent = 56 | await this.siteManager.getNoteContent(notePath); 57 | 58 | let localNotePath = ""; 59 | 60 | if ( 61 | this.settings.vaultPath !== "/" && 62 | this.settings.vaultPath !== "" 63 | ) { 64 | localNotePath = this.settings.vaultPath + notePath; 65 | } else { 66 | localNotePath = notePath; 67 | } 68 | 69 | const localFile = this.vault.getAbstractFileByPath(localNotePath); 70 | 71 | if (localFile instanceof TFile) { 72 | const localPublishFile = new PublishFile({ 73 | file: localFile, 74 | vault: this.vault, 75 | compiler: this.publisher.compiler, 76 | metadataCache: this.publisher.metadataCache, 77 | settings: this.settings, 78 | }); 79 | 80 | const [localContent, _] = 81 | await this.publisher.compiler.generateMarkdown( 82 | localPublishFile, 83 | ); 84 | 85 | const diff = Diff.diffLines(remoteContent, localContent); 86 | let diffView: DiffView | undefined; 87 | const diffModal = new Modal(this.modal.app); 88 | 89 | diffModal.titleEl 90 | .createEl("span", { text: `${localFile.basename}` }) 91 | .prepend(this.getIcon("file-diff")); 92 | 93 | diffModal.onOpen = () => { 94 | diffView = new DiffView({ 95 | target: diffModal.contentEl, 96 | props: { diff: diff }, 97 | }); 98 | }; 99 | 100 | this.modal.onClose = () => { 101 | if (diffView) { 102 | diffView.$destroy(); 103 | } 104 | }; 105 | 106 | diffModal.open(); 107 | } 108 | } catch (e) { 109 | console.error(e); 110 | } 111 | }; 112 | open = () => { 113 | this.modal.onClose = () => { 114 | this.publicationCenterUi.$destroy(); 115 | }; 116 | 117 | this.modal.onOpen = () => { 118 | this.modal.contentEl.empty(); 119 | 120 | this.publicationCenterUi = new PublicationCenterSvelte({ 121 | target: this.modal.contentEl, 122 | props: { 123 | publishStatusManager: this.publishStatusManager, 124 | publisher: this.publisher, 125 | showDiff: this.showDiff, 126 | close: () => { 127 | this.modal.close(); 128 | }, 129 | }, 130 | }); 131 | }; 132 | 133 | this.modal.open(); 134 | }; 135 | } 136 | -------------------------------------------------------------------------------- /src/views/PublishStatusBar.ts: -------------------------------------------------------------------------------- 1 | export class PublishStatusBar { 2 | statusBarItem: HTMLElement; 3 | counter: number; 4 | numberOfNotesToPublish: number; 5 | 6 | status: HTMLElement; 7 | constructor(statusBarItem: HTMLElement, numberOfNotesToPublish: number) { 8 | this.statusBarItem = statusBarItem; 9 | this.counter = 0; 10 | this.numberOfNotesToPublish = numberOfNotesToPublish; 11 | 12 | this.statusBarItem.createEl("span", { text: "Quartz Syncer: " }); 13 | 14 | this.status = this.statusBarItem.createEl("span", { 15 | text: `${this.numberOfNotesToPublish} files marked for publishing`, 16 | }); 17 | } 18 | 19 | incrementMultiple(increments: number) { 20 | this.counter += increments; 21 | 22 | this.status.innerText = `⌛Publishing files: ${this.counter}/${this.numberOfNotesToPublish}`; 23 | } 24 | increment() { 25 | this.status.innerText = `⌛Publishing files: ${++this.counter}/${ 26 | this.numberOfNotesToPublish 27 | }`; 28 | } 29 | 30 | finish(displayDurationMillisec: number) { 31 | this.status.innerText = `✅ Published files: ${this.counter}/${this.numberOfNotesToPublish}`; 32 | 33 | setTimeout(() => { 34 | this.statusBarItem.remove(); 35 | }, displayDurationMillisec); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/views/QuartzSyncerSettingTab.ts: -------------------------------------------------------------------------------- 1 | import { PluginSettingTab, App } from "obsidian"; 2 | import QuartzSyncer from "main"; 3 | import QuartzSyncerSiteManager from "src/repositoryConnection/QuartzSyncerSiteManager"; 4 | import SettingView from "src/views/SettingsView/SettingView"; 5 | 6 | export class QuartzSyncerSettingTab extends PluginSettingTab { 7 | app: App; 8 | plugin: QuartzSyncer; 9 | 10 | constructor(app: App, plugin: QuartzSyncer) { 11 | super(app, plugin); 12 | this.app = app; 13 | this.plugin = plugin; 14 | 15 | if (!this.plugin.settings.noteSettingsIsInitialized) { 16 | const siteManager = new QuartzSyncerSiteManager( 17 | this.app.metadataCache, 18 | this.plugin.settings, 19 | ); 20 | siteManager.updateEnv(); 21 | this.plugin.settings.noteSettingsIsInitialized = true; 22 | this.plugin.saveData(this.plugin.settings); 23 | } 24 | } 25 | 26 | async display(): Promise<void> { 27 | const { containerEl } = this; 28 | 29 | const settingView = new SettingView( 30 | this.app, 31 | this.plugin, 32 | containerEl, 33 | this.plugin.settings, 34 | async () => await this.plugin.saveData(this.plugin.settings), 35 | ); 36 | 37 | await settingView.initialize(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/views/SettingsView/Views/FrontmatterSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting, App, PluginSettingTab } from "obsidian"; 2 | import SettingView from "src/views/SettingsView/SettingView"; 3 | import QuartzSyncer from "main"; 4 | 5 | export class FrontmatterSettings extends PluginSettingTab { 6 | app: App; 7 | plugin: QuartzSyncer; 8 | settings: SettingView; 9 | private settingsRootElement: HTMLElement; 10 | 11 | constructor( 12 | app: App, 13 | plugin: QuartzSyncer, 14 | settings: SettingView, 15 | settingsRootElement: HTMLElement, 16 | ) { 17 | super(app, plugin); 18 | this.app = app; 19 | this.plugin = plugin; 20 | this.settings = settings; 21 | this.plugin = plugin; 22 | this.settingsRootElement = settingsRootElement; 23 | } 24 | 25 | display(): void { 26 | this.settingsRootElement.empty(); 27 | this.settingsRootElement.addClass("quartz-syncer-github-settings"); 28 | 29 | this.initializeFrontmatterHeader(); 30 | this.initializePublishFrontmatterKeySetting(); 31 | this.initializeShowCreatedTimestampSetting(); 32 | this.initializeShowUpdatedTimestampSetting(); 33 | this.initializeShowPublishedTimestampSetting(); 34 | this.initializeEnablePermalinkSetting(); 35 | this.initializeIncludeAllFrontmatterSetting(); 36 | 37 | this.settings.settings.lastUsedSettingsTab = "frontmatter"; 38 | this.settings.saveSettings(); 39 | } 40 | 41 | initializeFrontmatterHeader = () => { 42 | new Setting(this.settingsRootElement) 43 | .setName("Note properties (frontmatter)") 44 | .setDesc( 45 | "Quartz Syncer will apply these settings to your Quartz notes' properties or frontmatter.", 46 | ) 47 | .setHeading(); 48 | }; 49 | 50 | private initializePublishFrontmatterKeySetting() { 51 | new Setting(this.settingsRootElement) 52 | .setName("Publish key") 53 | .setDesc( 54 | 'Note property key used to mark a note as eligible to publish. Quartz Syncer will ignore all notes without this property. By default "publish".', 55 | ) 56 | .addText((text) => 57 | text 58 | .setPlaceholder("publish") 59 | .setValue(this.settings.settings.publishFrontmatterKey) 60 | .onChange(async (value) => { 61 | if (value.length === 0) { 62 | value = "publish"; 63 | } 64 | this.settings.settings.publishFrontmatterKey = value; 65 | await this.settings.saveSettings(); 66 | }), 67 | ); 68 | } 69 | 70 | private initializeIncludeAllFrontmatterSetting() { 71 | new Setting(this.settingsRootElement) 72 | .setName("Include all properties") 73 | .setDesc( 74 | "Include all note properties in the Quartz Syncer note. Enabling this will overrides other property settings to include all properties keys and values. Even note properties that are not used by Quartz will be included in the note's frontmatter. You shouldn't need this setting unless you have Quartz components that require non-standard properties.", 75 | ) 76 | .addToggle((toggle) => 77 | toggle 78 | .setValue(this.settings.settings.includeAllFrontmatter) 79 | .onChange(async (value) => { 80 | this.settings.settings.includeAllFrontmatter = value; 81 | await this.settings.saveSettings(); 82 | this.display(); 83 | }), 84 | ); 85 | } 86 | 87 | private initializeShowCreatedTimestampSetting() { 88 | if (!this.settings.settings.includeAllFrontmatter) { 89 | new Setting(this.settingsRootElement) 90 | .setName("Include created timestamp") 91 | .setDesc( 92 | "Include the created timestamp in your note's properties.", 93 | ) 94 | .addToggle((toggle) => 95 | toggle 96 | .setValue(this.settings.settings.showCreatedTimestamp) 97 | .setDisabled( 98 | this.settings.settings.includeAllFrontmatter, 99 | ) 100 | .onChange(async (value) => { 101 | this.settings.settings.showCreatedTimestamp = value; 102 | await this.settings.saveSettings(); 103 | }), 104 | ); 105 | } 106 | } 107 | 108 | private initializeShowUpdatedTimestampSetting() { 109 | if (!this.settings.settings.includeAllFrontmatter) { 110 | new Setting(this.settingsRootElement) 111 | .setName("Include modified timestamp") 112 | .setDesc( 113 | "Include the modified timestamp in your note's properties.", 114 | ) 115 | .addToggle((toggle) => 116 | toggle 117 | .setValue(this.settings.settings.showUpdatedTimestamp) 118 | .setDisabled( 119 | this.settings.settings.includeAllFrontmatter, 120 | ) 121 | .onChange(async (value) => { 122 | this.settings.settings.showUpdatedTimestamp = value; 123 | await this.settings.saveSettings(); 124 | }), 125 | ); 126 | } 127 | } 128 | 129 | private initializeShowPublishedTimestampSetting() { 130 | if (!this.settings.settings.includeAllFrontmatter) { 131 | new Setting(this.settingsRootElement) 132 | .setName("Include published timestamp") 133 | .setDesc( 134 | "Include the published timestamp in your note's properties.", 135 | ) 136 | .addToggle((toggle) => 137 | toggle 138 | .setValue(this.settings.settings.showPublishedTimestamp) 139 | .setDisabled( 140 | this.settings.settings.includeAllFrontmatter, 141 | ) 142 | .onChange(async (value) => { 143 | this.settings.settings.showPublishedTimestamp = 144 | value; 145 | await this.settings.saveSettings(); 146 | }), 147 | ); 148 | } 149 | } 150 | 151 | private initializeEnablePermalinkSetting() { 152 | new Setting(this.settingsRootElement) 153 | .setName("Enable permalinks") 154 | .setDesc( 155 | "Use the note's permalink as the Quartz note's URL if \"permalink\" is not in the frontmatter. This will override the default Quartz URL.", 156 | ) 157 | .addToggle((toggle) => 158 | toggle 159 | .setValue(this.settings.settings.usePermalink) 160 | .setDisabled(this.settings.settings.includeAllFrontmatter) 161 | .onChange(async (value) => { 162 | this.settings.settings.usePermalink = value; 163 | await this.settings.saveSettings(); 164 | }), 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/views/SettingsView/Views/IntegrationSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting, App, PluginSettingTab } from "obsidian"; 2 | import SettingView from "src/views/SettingsView/SettingView"; 3 | import QuartzSyncer from "main"; 4 | import { isPluginEnabled } from "src/utils/utils"; 5 | 6 | export class IntegrationSettings extends PluginSettingTab { 7 | app: App; 8 | plugin: QuartzSyncer; 9 | settings: SettingView; 10 | private settingsRootElement: HTMLElement; 11 | 12 | constructor( 13 | app: App, 14 | plugin: QuartzSyncer, 15 | settings: SettingView, 16 | settingsRootElement: HTMLElement, 17 | ) { 18 | super(app, plugin); 19 | this.app = app; 20 | this.plugin = plugin; 21 | this.settings = settings; 22 | this.settingsRootElement = settingsRootElement; 23 | } 24 | 25 | display(): void { 26 | this.settingsRootElement.empty(); 27 | this.settingsRootElement.addClass("quartz-syncer-github-settings"); 28 | 29 | this.initializePluginIntegrationHeader(); 30 | this.initializeDataviewSetting(); 31 | this.initializeDatacoreSetting(); 32 | this.initializeExcalidrawSetting(); 33 | 34 | this.settings.settings.lastUsedSettingsTab = "integration"; 35 | this.settings.saveSettings(); 36 | } 37 | 38 | initializePluginIntegrationHeader = () => { 39 | new Setting(this.settingsRootElement) 40 | .setName("Plugin integration") 41 | .setDesc( 42 | "Quartz Syncer will use these Obsidian plugins with your Quartz notes.", 43 | ) 44 | .setHeading(); 45 | }; 46 | 47 | private initializeDatacoreSetting() { 48 | const datacoreEnabled = isPluginEnabled("datacore"); 49 | 50 | new Setting(this.settingsRootElement) 51 | .setName("Enable Datacore integration") 52 | .setDesc( 53 | "Converts Datacore queries into Quartz-compatible markdown. Currently, this is an experimental feature and may not work as expected.", 54 | ) 55 | .addToggle((toggle) => 56 | toggle 57 | .setValue( 58 | this.settings.settings.useDatacore && datacoreEnabled, 59 | ) 60 | .setDisabled(!datacoreEnabled) 61 | .onChange(async (value) => { 62 | this.settings.settings.useDatacore = 63 | value && datacoreEnabled; 64 | await this.settings.saveSettings(); 65 | }), 66 | ) 67 | .setClass( 68 | `${ 69 | datacoreEnabled 70 | ? "quartz-syncer-settings-enabled" 71 | : "quartz-syncer-settings-disabled" 72 | }`, 73 | ); 74 | } 75 | 76 | private initializeDataviewSetting() { 77 | const dataviewEnabled = isPluginEnabled("dataview"); 78 | 79 | new Setting(this.settingsRootElement) 80 | .setName("Enable Dataview integration") 81 | .setDesc( 82 | "Converts Dataview queries into Quartz-compatible markdown.", 83 | ) 84 | .addToggle((toggle) => 85 | toggle 86 | .setValue( 87 | this.settings.settings.useDataview && dataviewEnabled, 88 | ) 89 | .setDisabled(!dataviewEnabled) 90 | .onChange(async (value) => { 91 | this.settings.settings.useDataview = 92 | value && dataviewEnabled; 93 | await this.settings.saveSettings(); 94 | }), 95 | ) 96 | .setClass( 97 | `${ 98 | dataviewEnabled 99 | ? "quartz-syncer-settings-enabled" 100 | : "quartz-syncer-settings-disabled" 101 | }`, 102 | ); 103 | } 104 | 105 | private initializeExcalidrawSetting() { 106 | new Setting(this.settingsRootElement) 107 | .setName("Enable Excalidraw integration") 108 | .setDesc( 109 | "Converts Excalidraw drawings into Quartz-compatible format.", 110 | ) 111 | .addToggle((toggle) => 112 | toggle 113 | .setValue(this.settings.settings.useExcalidraw) 114 | .setValue(false) 115 | .setDisabled(true) 116 | .onChange(async (value) => { 117 | this.settings.settings.useExcalidraw = value; 118 | await this.settings.saveSettings(); 119 | }), 120 | ) 121 | .setClass("quartz-syncer-settings-upcoming"); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/views/SettingsView/Views/QuartzSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting, App, PluginSettingTab } from "obsidian"; 2 | import SettingView from "src/views/SettingsView/SettingView"; 3 | import QuartzSyncer from "main"; 4 | 5 | export class QuartzSettings extends PluginSettingTab { 6 | app: App; 7 | plugin: QuartzSyncer; 8 | settings: SettingView; 9 | private settingsRootElement: HTMLElement; 10 | 11 | constructor( 12 | app: App, 13 | plugin: QuartzSyncer, 14 | settings: SettingView, 15 | settingsRootElement: HTMLElement, 16 | ) { 17 | super(app, plugin); 18 | this.app = app; 19 | this.plugin = plugin; 20 | this.settings = settings; 21 | this.settingsRootElement = settingsRootElement; 22 | } 23 | 24 | display(): void { 25 | this.settingsRootElement.empty(); 26 | this.settingsRootElement.addClass("quartz-syncer-github-settings"); 27 | 28 | this.initializeQuartzHeader(); 29 | this.initializeQuartzContentFolder(); 30 | this.initializeUseFullImageResolutionSetting(); 31 | this.initializeApplyEmbedsSetting(); 32 | 33 | this.settings.settings.lastUsedSettingsTab = "quartz"; 34 | this.settings.saveSettings(); 35 | } 36 | 37 | initializeQuartzHeader = () => { 38 | new Setting(this.settingsRootElement) 39 | .setName("Quartz") 40 | .setDesc( 41 | "Quartz Syncer will apply these settings to your Quartz notes.", 42 | ) 43 | .setHeading(); 44 | }; 45 | 46 | private initializeUseFullImageResolutionSetting() { 47 | new Setting(this.settingsRootElement) 48 | .setName("Use full image resolution") 49 | .setDesc( 50 | "By default, Quartz Syncer will use lower resolution images to save space. If you want to use the full resolution blob, enable this setting.", 51 | ) 52 | .addToggle((toggle) => 53 | toggle 54 | .setValue(this.settings.settings.useFullResolutionImages) 55 | .onChange(async (value) => { 56 | this.settings.settings.useFullResolutionImages = value; 57 | await this.settings.saveSettings(); 58 | }), 59 | ); 60 | } 61 | 62 | private initializeApplyEmbedsSetting() { 63 | new Setting(this.settingsRootElement) 64 | .setName("Apply embeds") 65 | .setDesc( 66 | "By default, Quartz Syncer will apply embeds directly to your notes. If you want to let Quartz handle embeds, disable this setting.", 67 | ) 68 | .addToggle((toggle) => 69 | toggle 70 | .setValue(this.settings.settings.applyEmbeds) 71 | .onChange(async (value) => { 72 | this.settings.settings.applyEmbeds = value; 73 | await this.settings.saveSettings(); 74 | }), 75 | ); 76 | } 77 | 78 | private initializeQuartzContentFolder() { 79 | new Setting(this.settingsRootElement) 80 | .setName("Content folder") 81 | .setDesc( 82 | 'The folder in your Quartz repository where Quartz Syncer should store your notes. By default "content"', 83 | ) 84 | .addText((text) => 85 | text 86 | .setPlaceholder("content") 87 | .setValue(this.settings.settings.contentFolder) 88 | .onChange(async (value) => { 89 | this.settings.settings.contentFolder = value; 90 | await this.settings.saveSettings(); 91 | }), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/views/SettingsView/Views/ThemesSettings.ts: -------------------------------------------------------------------------------- 1 | import { Setting, App, PluginSettingTab } from "obsidian"; 2 | import SettingView from "src/views/SettingsView/SettingView"; 3 | import QuartzSyncer from "main"; 4 | 5 | export class ThemesSettings extends PluginSettingTab { 6 | app: App; 7 | plugin: QuartzSyncer; 8 | settings: SettingView; 9 | settingsRootElement: HTMLElement; 10 | 11 | constructor( 12 | app: App, 13 | plugin: QuartzSyncer, 14 | settings: SettingView, 15 | settingsRootElement: HTMLElement, 16 | ) { 17 | super(app, plugin); 18 | this.app = app; 19 | this.plugin = plugin; 20 | this.settings = settings; 21 | this.settingsRootElement = settingsRootElement; 22 | this.settingsRootElement.classList.add("settings-tab-content"); 23 | } 24 | 25 | display(): void { 26 | this.settingsRootElement.empty(); 27 | this.settingsRootElement.addClass("quartz-syncer-github-settings"); 28 | 29 | this.initializeThemesHeader(); 30 | this.initializeThemeSetting(); 31 | 32 | this.settings.settings.lastUsedSettingsTab = "themes"; 33 | this.settings.saveSettings(); 34 | } 35 | 36 | initializeThemesHeader = () => { 37 | new Setting(this.settingsRootElement) 38 | .setName("Quartz Themes") 39 | .setDesc( 40 | "Quartz Themes is a project that aims to regularly convert Obsidian themes to a Quartz-compatible format. Quartz Syncer will install the chosen theme in Quartz from the Quartz Themes repository.", 41 | ) 42 | .setHeading(); 43 | }; 44 | 45 | initializeThemeSetting = () => { 46 | new Setting(this.settingsRootElement) 47 | .setName("Theme") 48 | .setDesc("Select the theme for your Quartz site.") 49 | .addToggle((toggle) => 50 | toggle 51 | .setValue(this.settings.settings.useThemes) 52 | .setValue(false) 53 | .setDisabled(true) 54 | .onChange((value) => { 55 | this.settings.settings.useThemes = value; 56 | this.settings.saveSettings(); 57 | }), 58 | ) 59 | .setClass("quartz-syncer-settings-upcoming"); 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "es2018", 8 | "allowJs": true, 9 | "noEmit": true, 10 | "noImplicitAny": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "esModuleInterop": true, 14 | "verbatimModuleSyntax": false, 15 | "lib": ["DOM", "ES5", "ES6", "ES7"], 16 | "types": ["svelte", "node", "jest"] 17 | }, 18 | "include": ["**/*.ts", "**/*.js", "**/*.svelte"] 19 | } 20 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.7.1": "1.8.10", 3 | "1.7.0": "1.8.10", 4 | "1.6.9": "1.8.10", 5 | "1.6.8": "1.8.10", 6 | "1.6.7": "1.8.10", 7 | "1.6.6": "1.8.10", 8 | "1.6.5": "1.8.10", 9 | "1.6.4": "1.8.10", 10 | "1.6.3": "1.8.10", 11 | "1.6.2": "1.8.10", 12 | "1.6.1": "1.8.10", 13 | "1.6.0": "1.8.10", 14 | "1.5.3": "1.8.10", 15 | "1.5.2": "1.8.10", 16 | "1.5.1": "1.8.10", 17 | "1.5.0": "1.8.10", 18 | "1.4.1": "1.8.10", 19 | "1.4.0": "1.8.10", 20 | "1.3.1": "0.12.0", 21 | "1.3.0": "0.12.0", 22 | "1.2.1": "0.12.0", 23 | "1.2.0": "0.12.0", 24 | "1.1.7": "0.12.0", 25 | "1.1.6": "0.12.0", 26 | "1.1.5": "0.12.0", 27 | "1.1.4": "0.12.0", 28 | "1.1.3": "0.12.0", 29 | "1.1.2": "0.12.0", 30 | "1.1.1": "0.12.0", 31 | "1.1.0": "0.12.0", 32 | "1.0.0": "0.12.0" 33 | } 34 | --------------------------------------------------------------------------------