├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── bug_report_mobile.yml │ ├── config.yml │ ├── documentation.md │ └── feature_request.md └── workflows │ ├── documentation.yml │ └── test.yml ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── __mocks__ ├── data-import │ └── web-worker │ │ └── import-manager.ts └── obsidian.ts ├── cspell.json ├── docs ├── .gitignore ├── docs │ ├── annotation │ │ ├── add-metadata.md │ │ ├── metadata-pages.md │ │ ├── metadata-tasks.md │ │ └── types-of-metadata.md │ ├── api │ │ ├── code-examples.md │ │ ├── code-reference.md │ │ ├── data-array.md │ │ └── intro.md │ ├── assets │ │ ├── books-by-genre.png │ │ ├── calendar_query_type.png │ │ ├── daily-retro-example-table.png │ │ ├── file-path-list.png │ │ ├── file-tags-indented-list.png │ │ ├── flatten-authors.png │ │ ├── game-list.png │ │ ├── game.png │ │ ├── grouped-book-example.png │ │ ├── obsidian.png │ │ └── project-task.png │ ├── changelog.md │ ├── friends.md │ ├── index.md │ ├── queries │ │ ├── data-commands.md │ │ ├── differences-to-sql.md │ │ ├── dql-js-inline.md │ │ ├── query-types.md │ │ └── structure.md │ ├── reference │ │ ├── expressions.md │ │ ├── functions.md │ │ ├── literals.md │ │ └── sources.md │ └── resources │ │ ├── develop-against-dataview.md │ │ ├── examples.md │ │ ├── faq.md │ │ └── resources-and-support.md ├── mkdocs.yml └── overrides │ └── main.html ├── jest.config.js ├── manifest-beta.json ├── manifest.json ├── package-lock.json ├── package.json ├── rollup.config.js ├── scripts ├── beta-release ├── install-built └── release ├── src ├── api │ ├── data-array.ts │ ├── extensions.ts │ ├── inline-api.ts │ ├── plugin-api.ts │ └── result.ts ├── data-import │ ├── common.ts │ ├── csv.ts │ ├── inline-field.ts │ ├── markdown-file.ts │ ├── persister.ts │ └── web-worker │ │ ├── import-entry.ts │ │ ├── import-impl.ts │ │ └── import-manager.ts ├── data-index │ ├── index.ts │ ├── resolver.ts │ └── source.ts ├── data-model │ ├── markdown.ts │ ├── serialized │ │ └── markdown.ts │ ├── transferable.ts │ └── value.ts ├── expression │ ├── binaryop.ts │ ├── context.ts │ ├── field.ts │ ├── functions.ts │ └── parse.ts ├── index.ts ├── main.ts ├── query │ ├── engine.ts │ ├── parse.ts │ └── query.ts ├── settings.ts ├── test │ ├── api │ │ └── data-array.test.ts │ ├── common.ts │ ├── data │ │ ├── index-map.test.ts │ │ ├── transferable.test.ts │ │ └── values.test.ts │ ├── function │ │ ├── aggregation.test.ts │ │ ├── coerce.test.ts │ │ ├── constructors.test.ts │ │ ├── eval.test.ts │ │ ├── functions.test.ts │ │ ├── meta.test.ts │ │ ├── string.test.ts │ │ └── vectorization.test.ts │ ├── markdown │ │ └── parse.file.test.ts │ ├── parse │ │ ├── parse.expression.test.ts │ │ ├── parse.inline.test.ts │ │ └── parse.query.test.ts │ └── util │ │ ├── normalize.test.ts │ │ └── paths.test.ts ├── typings │ ├── obsidian-ex.d.ts │ └── workers.d.ts ├── ui │ ├── export │ │ └── markdown.ts │ ├── lp-render.ts │ ├── markdown.tsx │ ├── refreshable-view.ts │ ├── render.ts │ └── views │ │ ├── calendar-view.ts │ │ ├── inline-field-live-preview.ts │ │ ├── inline-field.tsx │ │ ├── inline-view.ts │ │ ├── js-view.ts │ │ ├── list-view.tsx │ │ ├── table-view.tsx │ │ └── task-view.tsx └── util │ ├── hash.ts │ ├── locale.ts │ ├── media.ts │ └── normalize.ts ├── styles.css ├── test-vault ├── .obsidian │ ├── community-plugins.json │ └── plugins │ │ ├── dataview │ │ └── .hotreload │ │ └── hot-reload-master │ │ ├── .github │ │ └── workflows │ │ │ └── publish.yml │ │ ├── README.md │ │ ├── main.js │ │ └── manifest.json ├── Books.md ├── Home.md ├── blog │ ├── 2020-08-08-an-earlier-post.md │ └── 2021-08-08-a-post.md ├── books │ ├── Catcher in the Rye.md │ ├── Origin of Species.md │ └── The Great Gatsby.md ├── example calendars.md ├── example lists.md ├── example tables.md ├── recipes │ ├── pbj.md │ └── toast.md ├── tasks │ ├── Annotated Tasks.md │ ├── Completed Tasks.md │ ├── Grouped Sorted Tasks.md │ ├── Sorted Tasks.md │ ├── Tasks Completed on specific Date.md │ ├── Tasks in a specific section.md │ ├── Uncompleted Tasks.md │ ├── checklist.md │ └── example tasks.md └── untracked │ └── README.md ├── tsconfig-lib.json ├── tsconfig.json └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | # Matches multiple files with brace expansion notation 10 | # Set default charset 11 | [*.{ts,js}] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 4 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ["blacksmithgu"] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report (desktop) 2 | description: "File a bug report for Dataview on desktop Obsidian" 3 | title: "Bug report" 4 | labels: ["bug", "needs-investigation"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: Also tell us, what did you expect to happen? 15 | placeholder: Tell us what you see! 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: dql 20 | attributes: 21 | label: DQL 22 | description: If applicable, provide the query where the bug occurred 23 | placeholder: | 24 | ```dataview 25 | LIST FROM #example 26 | ``` 27 | - type: textarea 28 | id: js 29 | attributes: 30 | label: JS 31 | description: If applicable, provide the javascript where the bug occurred 32 | render: js 33 | - type: input 34 | id: version 35 | attributes: 36 | label: Dataview Version 37 | description: What version of Dataview are you running? 38 | placeholder: 0.4.20 39 | validations: 40 | required: true 41 | - type: input 42 | id: obsidian-version 43 | attributes: 44 | label: Obsidian Version 45 | placeholder: 0.12.19 46 | validations: 47 | required: true 48 | - type: dropdown 49 | id: os 50 | attributes: 51 | label: OS 52 | options: 53 | - Windows 54 | - MacOS 55 | - Linux 56 | validations: 57 | required: true 58 | 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_mobile.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report (mobile) 2 | description: "File a bug report for Dataview on mobile Obsidian" 3 | title: "Bug report" 4 | labels: ["bug", "needs-investigation", "mobile"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: Also tell us, what did you expect to happen? 15 | placeholder: Tell us what you see! 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: dql 20 | attributes: 21 | label: DQL 22 | description: If applicable, provide the query where the bug occurred 23 | placeholder: | 24 | ```dataview 25 | LIST FROM #example 26 | ``` 27 | - type: textarea 28 | id: js 29 | attributes: 30 | label: JS 31 | description: If applicable, provide the javascript where the bug occurred 32 | render: js 33 | - type: input 34 | id: version 35 | attributes: 36 | label: Dataview Version 37 | description: What version of Dataview are you running? 38 | placeholder: 0.4.20 39 | validations: 40 | required: true 41 | - type: input 42 | id: obsidian-version 43 | attributes: 44 | label: Obsidian Version 45 | placeholder: 0.12.19 46 | validations: 47 | required: true 48 | - type: input 49 | id: device 50 | attributes: 51 | label: Device 52 | description: What device are you running Obsidian on? Please provide the full model (version, year, etc.) 53 | placeholder: iPhone 6 54 | validations: 55 | required: true 56 | - type: input 57 | id: os 58 | attributes: 59 | label: OS 60 | description: What OS are you running Obsidian on? Please provide the full OS version. 61 | placeholder: iOS 8.1 62 | validations: 63 | required: true 64 | 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Dataview Discussions 4 | url: https://github.com/blacksmithgu/obsidian-dataview/discussions 5 | about: Please ask and answer questions here. 6 | - name: Obsidian Discord 7 | url: https://obsidian.md/community 8 | about: "Check out the #dataview channel under the Plugins section." 9 | - name: Dataview Snippet Showcase 10 | url: https://forum.obsidian.md/t/dataview-plugin-snippet-showcase 11 | about: Show off your Dataview snippets here! 12 | - name: DataviewJS Snippet Showcase 13 | url: https://forum.obsidian.md/t/dataviewjs-snippet-showcase 14 | about: Show off your DataviewJS snippets here! 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Suggest improvements to documentation 4 | title: '' 5 | labels: documentation 6 | 7 | --- 8 | 9 | **Please provide a link to the documentation page and section** 10 | 11 | **Describe the problem** 12 | A clear and concise description of what is unclear about the documentation 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. Feel free to suggest wording, full sentences, etc. 16 | 17 | **Additional context** 18 | Add any other context about the problem here. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature-request 6 | 7 | --- 8 | 9 | [//]: # (Note: If you are unsure about or have questions related to your feature request prefer making a discussion first. After we understand what you are looking for we can easily create an issue to track the solution and progress) 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | release: 4 | types: 5 | - published 6 | workflow_dispatch: 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-python@v4 13 | with: 14 | python-version: 3.x 15 | - run: | 16 | pip install mkdocs-material mkdocs-redirects 17 | cd docs 18 | mkdocs gh-deploy --force 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test project 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | format: 10 | runs-on: ubuntu-latest 11 | name: Check code formatting 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Setup node 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm install 19 | - run: npm run check-format 20 | build: 21 | runs-on: ubuntu-latest 22 | name: Build project 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Setup node 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | - run: npm install 30 | - run: npm run build 31 | test: 32 | runs-on: ubuntu-latest 33 | name: Test project 34 | steps: 35 | - uses: actions/checkout@v3 36 | - name: Setup node 37 | uses: actions/setup-node@v3 38 | with: 39 | node-version: 16 40 | - run: npm install 41 | - run: npm run test 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # VSCode 6 | .vscode 7 | .history/ 8 | 9 | # npm 10 | node_modules 11 | 12 | # build 13 | build/ 14 | 15 | # Ignore .obsidian 16 | # No one will commit these files, they just spam 'git status' 17 | test-vault/.obsidian/* 18 | # Tells obsidian what plugins are enabled 19 | !test-vault/.obsidian/community-plugins.json 20 | # This plugin should still be tracked by git. 21 | # It might need updated at some point 22 | !test-vault/.obsidian/hot-reload-master/* 23 | 24 | # Don't track this folder. For random things to try. 25 | # If its import to test though, add it somewhere where other 26 | # people can test it too. 27 | test-vault/untracked/* 28 | !test-vault/untracked/README.md 29 | 30 | lib 31 | yarn.lock 32 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": true, 4 | "embeddedLanguageFormatting": "off", 5 | "parser": "typescript", 6 | "printWidth": 120, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Michael Brenan 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. -------------------------------------------------------------------------------- /__mocks__/data-import/web-worker/import-manager.ts: -------------------------------------------------------------------------------- 1 | /** A mock for `FileImporter` which runs on the same thread. */ 2 | 3 | import { runImport } from "data-import/web-worker/import-impl"; 4 | import { CachedMetadata, MetadataCache, TFile, Vault } from "obsidian"; 5 | 6 | export class FileImporter { 7 | public constructor(public numWorkers: number, public vault: Vault, public metadataCache: MetadataCache) {} 8 | 9 | public async reload(file: TFile): Promise { 10 | let contents = await this.vault.read(file); 11 | let metadata = await this.metadataCache.getFileCache(file); 12 | return runImport(file.path, contents, file.stat, metadata as CachedMetadata) as any as T; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /__mocks__/obsidian.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "events"; 2 | 3 | /** Basic obsidian abstraction for any file or folder in a vault. */ 4 | export abstract class TAbstractFile { 5 | /** 6 | * @public 7 | */ 8 | vault: Vault; 9 | /** 10 | * @public 11 | */ 12 | path: string; 13 | /** 14 | * @public 15 | */ 16 | name: string; 17 | /** 18 | * @public 19 | */ 20 | parent: TFolder; 21 | } 22 | 23 | /** Tracks file created/modified time as well as file system size. */ 24 | export interface FileStats { 25 | /** @public */ 26 | ctime: number; 27 | /** @public */ 28 | mtime: number; 29 | /** @public */ 30 | size: number; 31 | } 32 | 33 | /** A regular file in the vault. */ 34 | export class TFile extends TAbstractFile { 35 | stat: FileStats; 36 | basename: string; 37 | extension: string; 38 | } 39 | 40 | /** A folder in the vault. */ 41 | export class TFolder extends TAbstractFile { 42 | children: TAbstractFile[]; 43 | 44 | isRoot(): boolean { 45 | return false; 46 | } 47 | } 48 | 49 | export class Vault extends EventEmitter { 50 | getFiles() { 51 | return []; 52 | } 53 | trigger(name: string, ...data: any[]): void { 54 | this.emit(name, ...data); 55 | } 56 | } 57 | 58 | export class Component { 59 | registerEvent() {} 60 | } 61 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "ignorePaths": [], 4 | "dictionaryDefinitions": [], 5 | "dictionaries": [], 6 | "words": [ 7 | "aaab", 8 | "akey", 9 | "alexfertel", 10 | "AnnaKornfeldSimpson", 11 | "apng", 12 | "artisticat", 13 | "autorelease", 14 | "bcbe", 15 | "bimap", 16 | "binaryop", 17 | "bkey", 18 | "blacksmithgu", 19 | "Brenan", 20 | "bryc", 21 | "callouts", 22 | "canonicalization", 23 | "canonicalize", 24 | "CANONICALIZER", 25 | "canonicalizes", 26 | "canonicalizing", 27 | "carlesalbasboix", 28 | "cday", 29 | "charleshan", 30 | "Cheatsheet", 31 | "Cheatsheets", 32 | "Chouffy", 33 | "clsname", 34 | "codemirror", 35 | "combinators", 36 | "compday", 37 | "comptime", 38 | "concat", 39 | "containsword", 40 | "crashy", 41 | "cres", 42 | "crog", 43 | "crpg", 44 | "cssclasses", 45 | "currencyformat", 46 | "cyrb", 47 | "dailys", 48 | "datapoints", 49 | "Datarow", 50 | "Dataview", 51 | "dataviewjs", 52 | "Dataviews", 53 | "datefield", 54 | "ddd", 55 | "DDTHH", 56 | "début", 57 | "dedup", 58 | "dformat", 59 | "dogfood", 60 | "Donadio", 61 | "dtformat", 62 | "duedate", 63 | "dueday", 64 | "duetime", 65 | "durationformat", 66 | "dvjs", 67 | "econtains", 68 | "elink", 69 | "embeddable", 70 | "endswith", 71 | "errorbox", 72 | "etags", 73 | "Evals", 74 | "eyuelt", 75 | "failable", 76 | "fastfood", 77 | "fdefault", 78 | "fileset", 79 | "Filetext", 80 | "Filipe", 81 | "frontmatter", 82 | "fullscan", 83 | "functionname", 84 | "GamerGirlandCo", 85 | "gdhjg", 86 | "gentlegiantJGC", 87 | "Gott", 88 | "Groot", 89 | "helloxx", 90 | "Hoeven", 91 | "holroy", 92 | "iamrecursion", 93 | "icontains", 94 | "iden", 95 | "ifield", 96 | "iitem", 97 | "implicits", 98 | "INDEXEDDB", 99 | "Ingrouped", 100 | "Inlines", 101 | "inlink", 102 | "inlinks", 103 | "Jeamee", 104 | "jfif", 105 | "Kanban", 106 | "kometenstaub", 107 | "ldefault", 108 | "leoccyao", 109 | "lezer", 110 | "Linkpath", 111 | "localforage", 112 | "localtime", 113 | "longkeyidontneedwhenreading", 114 | "lres", 115 | "luxon", 116 | "lwrap", 117 | "MarioRicalde", 118 | "matchreg", 119 | "maxby", 120 | "mday", 121 | "meello", 122 | "meep", 123 | "Millis", 124 | "minby", 125 | "mkdocs", 126 | "mnaoumov", 127 | "moba", 128 | "mobas", 129 | "mocsa", 130 | "mt-krainski", 131 | "mvalues", 132 | "nestedfield", 133 | "nonnull", 134 | "noopener", 135 | "Nums", 136 | "offref", 137 | "onwarn", 138 | "ooker777", 139 | "outlink", 140 | "outlinks", 141 | "padleft", 142 | "padright", 143 | "Pagerow", 144 | "papaparse", 145 | "parsimmon", 146 | "pathlike", 147 | "pjeby", 148 | "pjepg", 149 | "pleh", 150 | "preact", 151 | "protofarer", 152 | "proxied", 153 | "pymdownx", 154 | "RaviOnline", 155 | "rawlink", 156 | "recurkey", 157 | "Refreshable", 158 | "REGEXES", 159 | "regexmatch", 160 | "regexreplace", 161 | "regextest", 162 | "renderable", 163 | "Repr", 164 | "RyotaUshio", 165 | "sandboxed", 166 | "sandboxing", 167 | "seanlzx", 168 | "sheeley", 169 | "sohanglal", 170 | "somemetadata", 171 | "somidad", 172 | "spoopy", 173 | "Stardew", 174 | "startswith", 175 | "steamid", 176 | "steg", 177 | "striptime", 178 | "subargs", 179 | "subcontainer", 180 | "subeval", 181 | "subfolders", 182 | "sublists", 183 | "Subsettings", 184 | "subsources", 185 | "subtag", 186 | "Subtags", 187 | "subvalue", 188 | "subword", 189 | "succ", 190 | "superfences", 191 | "tasklist", 192 | "tcopy", 193 | "Templater", 194 | "Testcase", 195 | "toolbelt", 196 | "trunc", 197 | "typeof", 198 | "tzhou", 199 | "unindented", 200 | "unrendered", 201 | "v_mujunma", 202 | "vals", 203 | "vararg", 204 | "varargs", 205 | "vectorize", 206 | "vitaly", 207 | "vpos", 208 | "vrtmrz", 209 | "weekyear", 210 | "whitespaces", 211 | "xxhello", 212 | "ymdh", 213 | "ymdhm", 214 | "ymdhms", 215 | "zerollup", 216 | "Статус" 217 | ], 218 | "ignoreWords": [], 219 | "import": [], 220 | "enabled": true 221 | } 222 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | site/ 2 | -------------------------------------------------------------------------------- /docs/docs/annotation/metadata-pages.md: -------------------------------------------------------------------------------- 1 | # Metadata on Pages 2 | 3 | You can add fields to a markdown page (a note) in three different ways - via Frontmatter, Inline fields and Implicit fields. Read more about the first two possibilities in ["how to add metadata"](./add-metadata.md). 4 | 5 | ## Implicit Fields 6 | 7 | Dataview automatically adds a large amount of metadata to each page. These implicit and automatically added fields are collected under the field `file`. Following are available: 8 | 9 | | Field Name | Data Type | Description | 10 | | --------------------- | --------- | ----------- | 11 | | `file.name` | Text | The file name as seen in Obsidians sidebar. | 12 | | `file.folder` | Text | The path of the folder this file belongs to. | 13 | | `file.path` | Text | The full file path, including the files name. | 14 | | `file.ext` | Text | The extension of the file type; generally `md`. | 15 | | `file.link` | Link | A link to the file. | 16 | | `file.size` | Number | The size (in bytes) of the file. | 17 | | `file.ctime` | Date with Time | The date that the file was created. | 18 | | `file.cday` | Date | The date that the file was created. | 19 | | `file.mtime` | Date with Time | The date that the file was last modified. | 20 | | `file.mday` | Date | The date that the file was last modified. | 21 | | `file.tags` | List | A list of all unique tags in the note. Subtags are broken down by each level, so `#Tag/1/A` will be stored in the list as `[#Tag, #Tag/1, #Tag/1/A]`. | 22 | | `file.etags` | List | A list of all explicit tags in the note; unlike `file.tags`, does not break subtags down, i.e. `[#Tag/1/A]` | 23 | | `file.inlinks` | List | A list of all incoming links to this file, meaning all files that contain a link to this file. | 24 | | `file.outlinks` | List | A list of all outgoing links from this file, meaning all links the file contains. | 25 | | `file.aliases` | List | A list of all aliases for the note as defined via the [YAML frontmatter](https://help.obsidian.md/How+to/Add+aliases+to+note). | 26 | | `file.tasks` | List | A list of all tasks (I.e., `| [ ] some task`) in this file. | 27 | | `file.lists` | List | A list of all list elements in the file (including tasks); these elements are effectively tasks and can be rendered in task views. | 28 | | `file.frontmatter` | List | Contains the raw values of all frontmatter in form of `key | value` text values; mainly useful for checking raw frontmatter values or for dynamically listing frontmatter keys. | 29 | | `file.day` | Date | Only available if the file has a date inside its file name (of form `yyyy-mm-dd` or `yyyymmdd`), or has a `Date` field/inline field. | 30 | | `file.starred` | Boolean | If this file has been bookmarked via the Obsidian Core Plugin "Bookmarks". | 31 | 32 | ## Example page 33 | 34 | This is a small Markdown page which includes both user-defined ways to add metadata: 35 | 36 | ```markdown 37 | --- 38 | genre: "action" 39 | reviewed: false 40 | --- 41 | # Movie X 42 | #movies 43 | 44 | **Thoughts**:: It was decent. 45 | **Rating**:: 6 46 | 47 | [mood:: okay] | [length:: 2 hours] 48 | ``` 49 | 50 | In addition to the values you see here, the page has also all keys listed above available. 51 | 52 | ### Example Query 53 | 54 | You can query part of the above information with following query, for example: 55 | 56 | ~~~yaml 57 | ```dataview 58 | TABLE file.ctime, length, rating, reviewed 59 | FROM #movies 60 | ``` 61 | ~~~ 62 | -------------------------------------------------------------------------------- /docs/docs/annotation/metadata-tasks.md: -------------------------------------------------------------------------------- 1 | # Metadata on Tasks and Lists 2 | 3 | Just like pages, you can also add **fields** on list item and task level to bind it to a specific task as context. For this you need to use the [inline field syntax](add-metadata.md#inline-fields): 4 | 5 | ```markdown 6 | - [ ] Hello, this is some [metadata:: value]! 7 | - [X] I finished this on [completion:: 2021-08-15]. 8 | ``` 9 | 10 | Tasks and list items are the same data wise, so all your bullet points have all the information described here available, too. 11 | 12 | ## Field Shorthands 13 | 14 | The [Tasks](https://publish.obsidian.md/tasks/Introduction) plugin introduced a different [notation by using Emoji](https://publish.obsidian.md/tasks/Reference/Task+Formats/Tasks+Emoji+Format) to configure the different dates related to a task. In the context of Dataview, this notation is called `Field Shorthands`. The current version of Dataview only support the dates shorthands as shown below. The priorities and recurrence shorthands are not supported. 15 | 16 | === "Example" 17 | 18 | 19 | === "Example" 20 | - [ ] Due this Saturday 🗓️2021-08-29 21 | - [x] Completed last Saturday ✅2021-08-22 22 | - [ ] I made this on ➕1990-06-14 23 | - [ ] Task I can start this weekend 🛫2021-08-29 24 | - [x] Task I finished ahead of schedule ⏳2021-08-29 ✅2021-08-22 25 | 26 | There are two specifics to these emoji-shorthands. First, they omit the inline field syntax (no `[🗓️:: YYYY-MM-DD]` needed) and secondly, they map to a **textual** field name data-wise: 27 | 28 | | Field name | Short hand syntax | 29 | | ---------- | ----------------- | 30 | | due | `🗓️YYYY-MM-DD` | 31 | | completion | `✅YYYY-MM-DD` | 32 | | created | `➕YYYY-MM-DD` | 33 | | start | `🛫YYYY-MM-DD` | 34 | | scheduled | `⏳YYYY-MM-DD` | 35 | 36 | This means if you want to query for all tasks that are completed 2021-08-22, you'll write: 37 | 38 | ~~~markdown 39 | ```dataview 40 | TASK 41 | WHERE completion = date("2021-08-22") 42 | ``` 43 | ~~~ 44 | 45 | Which will list both variants - shorthands and textual annotation: 46 | 47 | ```markdown 48 | - [x] Completed last Saturday ✅2021-08-22 49 | - [x] Some Done Task [completion:: 2021-08-22] 50 | ``` 51 | 52 | ## Implicit Fields 53 | 54 | As with pages, Dataview adds a number of implicit fields to each task or list item: 55 | 56 | !!! info "Inheritance of Fields" 57 | Tasks inherit *all fields* from their parent page - so if you have a `rating` field in your page, you can also access it on your task in a `TASK` Query. 58 | 59 | 60 | | Field name | Data Type | Description | 61 | | ---------- | --------- | ----------- | 62 | | `status` | Text | The completion status of this task, as determined by the character inside the `[ ]` brackets. Generally a space `" "` for incomplete tasks and an `"x"` for completed tasks, but allows for plugins which support alternative task statuses. | 63 | | `checked` | Boolean | Whether or not this task's status is **not** empty, meaning it has some `status` character (which may or may not be `"x"`) instead of a space in its `[ ]` brackets. | 64 | | `completed` | Boolean | Whether or not this *specific* task has been completed; this does not consider the completion or non-completion of any child tasks. A task is explicitly considered "completed" if it has been marked with an `"x"`. If you use a custom status, e.g. `[-]`, `checked` will be true, whereas `completed` will be false. | 65 | | `fullyCompleted` | Boolean | Whether or not this task and **all** of its subtasks are completed. | 66 | | `text` | Text | The plain text of this task, including any metadata field annotations. | 67 | | `visual` | Text | The text of this task, which is rendered by Dataview. This field can be overridden in DataviewJS to allow for different task text to be rendered than the regular task text, while still allowing the task to be checked (since Dataview validation logic normally checks the text against the text in-file). | 68 | | `line` | Number | The line of the file this task shows up on. | 69 | | `lineCount` | Number | The number of Markdown lines that this task takes up. | 70 | | `path` | Text | The full path of the file this task is in. Equals to `file.path` for [pages](./metadata-pages.md). | 71 | | `section` | Link | Link to the section this task is contained in. | 72 | | `tags` | List | Any tags inside the task text. | 73 | | `outlinks` | List | Any links defined in this task. | 74 | | `link` | Link | Link to the closest linkable block near this task; useful for making links which go to the task. | 75 | | `children` | List | Any subtasks or sublists of this task. | 76 | | `task` | Boolean | If true, this is a task; otherwise, it is a regular list element. | 77 | | `annotated` | Boolean | True if the task text contains any metadata fields, false otherwise. | 78 | | `parent` | Number | The line number of the task above this task, if present; will be null if this is a root-level task. | 79 | | `blockId` | Text | The block ID of this task / list element, if one has been defined with the `^blockId` syntax; otherwise null. | 80 | 81 | With usage of the [shorthand syntax](#field-shorthands), following additional properties may be available: 82 | 83 | - `completion`: The date a task was completed. 84 | - `due`: The date a task is due, if it has one. 85 | - `created`: The date a task was created. 86 | - `start`: The date a task can be started. 87 | - `scheduled`: The date a task is scheduled to work on. 88 | 89 | ### Accessing Implicit Fields in Queries 90 | 91 | If you're using a [TASK](../queries/query-types.md#task) Query, your tasks are the top level information and can be used without any prefix: 92 | 93 | ~~~markdown 94 | ```dataview 95 | TASK 96 | WHERE !fullyCompleted 97 | ``` 98 | ~~~ 99 | 100 | For every other Query type, you first need to access the implicit field `file.lists` or `file.tasks` to check for these list item specific implicit fields: 101 | 102 | ~~~markdown 103 | ```dataview 104 | LIST 105 | WHERE any(file.tasks, (t) => !t.fullyCompleted) 106 | ``` 107 | ~~~ 108 | 109 | This will give you back all the file links that have unfinished tasks inside. We get back a list of tasks on page level and thus need to use a [list function](../reference/functions.md) to look at each element. 110 | -------------------------------------------------------------------------------- /docs/docs/api/code-examples.md: -------------------------------------------------------------------------------- 1 | # Codeblock Examples 2 | 3 | ## Grouped Books 4 | 5 | Group your books by genre, then create a table for each sorted by rating via a straightforward usage of 6 | the dataview rendering API: 7 | 8 | ```js 9 | for (let group of dv.pages("#book").groupBy(p => p.genre)) { 10 | dv.header(3, group.key); 11 | dv.table(["Name", "Time Read", "Rating"], 12 | group.rows 13 | .sort(k => k.rating, 'desc') 14 | .map(k => [k.file.link, k["time-read"], k.rating])) 15 | } 16 | ``` 17 | 18 | ![Grouped Books Example](../assets/grouped-book-example.png) 19 | 20 | ## Find All Direct And Indirectly Linked Pages 21 | 22 | Use a simple set + stack depth first search to find all notes linked to the current note, or a note of your choosing: 23 | 24 | ```js 25 | let page = dv.current().file.path; 26 | let pages = new Set(); 27 | 28 | let stack = [page]; 29 | while (stack.length > 0) { 30 | let elem = stack.pop(); 31 | let meta = dv.page(elem); 32 | if (!meta) continue; 33 | 34 | for (let inlink of meta.file.inlinks.concat(meta.file.outlinks).array()) { 35 | console.log(inlink); 36 | if (pages.has(inlink.path)) continue; 37 | pages.add(inlink.path); 38 | stack.push(inlink.path); 39 | } 40 | } 41 | 42 | // Data is now the file metadata for every page that directly OR indirectly links to the current page. 43 | let data = dv.array(Array.from(pages)).map(p => dv.page(p)); 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/docs/api/intro.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The Dataview JavaScript API allows for executing arbitrary JavaScript with access to the dataview indices and query 4 | engine, which is good for complex views or interop with other plugins. The API comes in two flavors: plugin facing, and 5 | user facing (or 'inline API usage'). 6 | 7 | ## Inline Access 8 | 9 | You can create a "DataviewJS" block via: 10 | 11 | ~~~ 12 | ```dataviewjs 13 | dv.pages("#thing")... 14 | ``` 15 | ~~~ 16 | 17 | Code executed in such codeblocks have access to the `dv` variable, which provides the entirety of the codeblock-relevant 18 | dataview API (like `dv.table()`, `dv.pages()`, and so on). For more information, check out the [codeblock API 19 | reference](code-reference.md). 20 | 21 | ## Plugin Access 22 | 23 | You can access the Dataview Plugin API (from other plugins or the console) through `app.plugins.plugins.dataview.api`; 24 | this API is similar to the codeblock reference, with slightly different arguments due to the lack of an implicit file 25 | to execute the queries in. For more information, check out the [Plugin API reference](code-reference.md). 26 | -------------------------------------------------------------------------------- /docs/docs/assets/books-by-genre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksmithgu/obsidian-dataview/5ad0994ff384cbb797de382e7edff2388141b73a/docs/docs/assets/books-by-genre.png -------------------------------------------------------------------------------- /docs/docs/assets/calendar_query_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksmithgu/obsidian-dataview/5ad0994ff384cbb797de382e7edff2388141b73a/docs/docs/assets/calendar_query_type.png -------------------------------------------------------------------------------- /docs/docs/assets/daily-retro-example-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksmithgu/obsidian-dataview/5ad0994ff384cbb797de382e7edff2388141b73a/docs/docs/assets/daily-retro-example-table.png -------------------------------------------------------------------------------- /docs/docs/assets/file-path-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksmithgu/obsidian-dataview/5ad0994ff384cbb797de382e7edff2388141b73a/docs/docs/assets/file-path-list.png -------------------------------------------------------------------------------- /docs/docs/assets/file-tags-indented-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksmithgu/obsidian-dataview/5ad0994ff384cbb797de382e7edff2388141b73a/docs/docs/assets/file-tags-indented-list.png -------------------------------------------------------------------------------- /docs/docs/assets/flatten-authors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksmithgu/obsidian-dataview/5ad0994ff384cbb797de382e7edff2388141b73a/docs/docs/assets/flatten-authors.png -------------------------------------------------------------------------------- /docs/docs/assets/game-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksmithgu/obsidian-dataview/5ad0994ff384cbb797de382e7edff2388141b73a/docs/docs/assets/game-list.png -------------------------------------------------------------------------------- /docs/docs/assets/game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksmithgu/obsidian-dataview/5ad0994ff384cbb797de382e7edff2388141b73a/docs/docs/assets/game.png -------------------------------------------------------------------------------- /docs/docs/assets/grouped-book-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksmithgu/obsidian-dataview/5ad0994ff384cbb797de382e7edff2388141b73a/docs/docs/assets/grouped-book-example.png -------------------------------------------------------------------------------- /docs/docs/assets/obsidian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksmithgu/obsidian-dataview/5ad0994ff384cbb797de382e7edff2388141b73a/docs/docs/assets/obsidian.png -------------------------------------------------------------------------------- /docs/docs/assets/project-task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacksmithgu/obsidian-dataview/5ad0994ff384cbb797de382e7edff2388141b73a/docs/docs/assets/project-task.png -------------------------------------------------------------------------------- /docs/docs/friends.md: -------------------------------------------------------------------------------- 1 | # Friends of Dataview 2 | 3 | A list of plugins which may be helpful for Dataview related workflows: 4 | 5 | - [MetaEdit](https://github.com/chhoumann/MetaEdit) - Add or update yaml properties and Dataview fields easily 6 | 7 | Another non-exhaustive list of plugins which use Dataview for some of the heavy-lifting required for their features: 8 | 9 | - [Kanban](https://github.com/mgmeyers/obsidian-kanban) - Create markdown-backed Kanban boards in Obsidian 10 | - [Breadcrumbs](http://publish.obsidian.md/breadcrumbs-docs) - Gives you a way to visualize a custom-built hierarchy in your Obsidian vault 11 | - [Supercharged Links](https://github.com/mdelobelle/obsidian_supercharged_links) - Allows you to style links in your Obsidian vault based on note metadata 12 | 13 | A full list can be found using GitHub's [Dependents](https://github.com/blacksmithgu/obsidian-dataview/network/dependents) feature. 14 | -------------------------------------------------------------------------------- /docs/docs/queries/data-commands.md: -------------------------------------------------------------------------------- 1 | # Data Commands 2 | 3 | The different commands that dataview queries can be made up of. Commands are 4 | executed in order, and you can have duplicate commands (so multiple `WHERE` 5 | blocks or multiple `GROUP BY` blocks, for example). 6 | 7 | ## FROM 8 | 9 | The `FROM` statement determines what pages will initially be collected and passed onto the other commands for further 10 | filtering. You can select from any [source](../reference/sources.md), which currently means by folder, by tag, or by incoming/outgoing links. 11 | 12 | - **Tags**: To select from a tag (and all its subtags), use `FROM #tag`. 13 | - **Folders**: To select from a folder (and all its subfolders), use `FROM "folder"`. 14 | - **Single Files**: To select from a single file, use `FROM "path/to/file"`. 15 | - **Links**: You can either select links TO a file, or all links FROM a file. 16 | - To obtain all pages which link TO `[[note]]`, use `FROM [[note]]`. 17 | - To obtain all pages which link FROM `[[note]]` (i.e., all the links in that file), use `FROM outgoing([[note]])`. 18 | 19 | You can compose these filters in order to get more advanced sources using `and` and `or`. 20 | 21 | - For example, `#tag and "folder"` will return all pages in `folder` and with `#tag`. 22 | - `[[Food]] or [[Exercise]]` will give any pages which link to `[[Food]]` OR `[[Exercise]]`. 23 | 24 | You can also "negate" sources to obtain anything that does NOT match a source using `-`: 25 | 26 | - `-#tag` will exclude files which have the given tag. 27 | - `#tag and -"folder"` will only include files tagged `#tag` which are NOT in `"folder"`. 28 | 29 | ## WHERE 30 | 31 | Filter pages on fields. Only pages where the clause evaluates to `true` will be yielded. 32 | 33 | ``` 34 | WHERE 35 | ``` 36 | 37 | 1. Obtain all files which were modified in the last 24 hours: 38 | 39 | ```sql 40 | LIST WHERE file.mtime >= date(today) - dur(1 day) 41 | ``` 42 | 43 | 2. Find all projects which are not marked complete and are more than a month old: 44 | 45 | ```sql 46 | LIST FROM #projects 47 | WHERE !completed AND file.ctime <= date(today) - dur(1 month) 48 | ``` 49 | 50 | ## SORT 51 | 52 | Sorts all results by one or more fields. 53 | 54 | ``` 55 | SORT date [ASCENDING/DESCENDING/ASC/DESC] 56 | ``` 57 | 58 | You can also give multiple fields to sort by. Sorting will be done based on the first field. Then, if a tie occurs, the second field will be used to sort the tied fields. If there is still a tie, the third sort will resolve it, and so on. 59 | 60 | ``` 61 | SORT field1 [ASCENDING/DESCENDING/ASC/DESC], ..., fieldN [ASC/DESC] 62 | ``` 63 | 64 | ## GROUP BY 65 | 66 | Group all results on a field. Yields one row per unique field value, which has 2 properties: one corresponding to the field being grouped on, and a `rows` array field which contains all of the pages that matched. 67 | 68 | ``` 69 | GROUP BY field 70 | GROUP BY (computed_field) AS name 71 | ``` 72 | 73 | In order to make working with the `rows` array easier, Dataview supports field "swizzling". If you want the field `test` from every object in the `rows` array, then `rows.test` will automatically fetch the `test` field from every object in `rows`, yielding a new array. 74 | You can then apply aggregation operators like `sum()` or `flat()` over the resulting array. 75 | 76 | ## FLATTEN 77 | 78 | Flatten an array in every row, yielding one result row per entry in the array. 79 | 80 | ``` 81 | FLATTEN field 82 | FLATTEN (computed_field) AS name 83 | ``` 84 | 85 | For example, flatten the `authors` field in each literature note to give one row per author: 86 | 87 | === "Query" 88 | ```sql 89 | TABLE authors FROM #LiteratureNote 90 | FLATTEN authors 91 | ``` 92 | === "Output" 93 | |File|authors| 94 | |-|-| 95 | |stegEnvironmentalPsychologyIntroduction2018 SN|Steg, L.| 96 | |stegEnvironmentalPsychologyIntroduction2018 SN|Van den Berg, A. E.| 97 | |stegEnvironmentalPsychologyIntroduction2018 SN|De Groot, J. I. M.| 98 | |Soap Dragons SN|Robert Lamb| 99 | |Soap Dragons SN|Joe McCormick| 100 | |smithPainAssaultSelf2007 SN|Jonathan A. Smith| 101 | |smithPainAssaultSelf2007 SN|Mike Osborn| 102 | 103 | A good use of this would be when there is a deeply nested list that you want to use more easily. 104 | For example, `file.lists` or `file.tasks`. 105 | Note the simpler query though the end results are slightly different (grouped vs non-grouped). 106 | You can use a `GROUP BY file.link` to achieve identical results but would need to use `rows.T.text` as described earlier. 107 | 108 | ``` 109 | table T.text as "Task Text" 110 | from "Scratchpad" 111 | flatten file.tasks as T 112 | where T.text 113 | ``` 114 | 115 | ``` 116 | table filter(file.tasks.text, (t) => t) as "Task Text" 117 | from "Scratchpad" 118 | where file.tasks.text 119 | ``` 120 | 121 | `FLATTEN` makes it easier to operate on nested lists since you can then use simpler where conditions on them as opposed to using functions like `map()` or `filter()`. 122 | 123 | ## LIMIT 124 | 125 | Restrict the results to at most N values. 126 | 127 | ``` 128 | LIMIT 5 129 | ``` 130 | 131 | Commands are processed in the order they are written, so the following sorts the results *after* they have already been limited: 132 | 133 | ``` 134 | LIMIT 5 135 | SORT date ASCENDING 136 | ``` 137 | -------------------------------------------------------------------------------- /docs/docs/queries/differences-to-sql.md: -------------------------------------------------------------------------------- 1 | 9 | # Dataview Query Language (DQL) and SQL 10 | 11 | If you are familiar with SQL and experienced in writing SQL queries, you might approach writing a DQL query in a similar way. However, DQL is significantly different from SQL. 12 | 13 | A DQL query is **executed from top to bottom**, line-by-line. It is more like a computer program than a typical SQL query. 14 | 15 | When a line is evaluated, it produces a result set and **passes the whole set on to the next DQL line** which will manipulate the set that it received from the previous line. This is why in DQL it is possible, for example, to have multiple WHERE clauses. But in DQL it is not a 'clause' but a 'data command'. Every line of a DQL query (except the 1st and 2nd lines) is a 'data command'. 16 | 17 | ## Anatomy of a DQL query 18 | 19 | Instead of starting with SELECT, a DQL query starts with a word determining the Query Type, which determines how your final result will be rendered on screen (a table, a list, a task list, or a calendar). Then follows the list of fields, which is actually very similar to the column list you put after a SELECT statement. 20 | 21 | The next line starts with FROM which is not followed by a table name but by a complex expression, similar to an SQL WHERE clause. Here you can filter on many things, like tags in files, file names, path names, etc. In the background, this command already produces a result set which will be our initial set for further data manipulation by 'data commands' on subsequent lines. 22 | 23 | You can have as many following lines as you want. Each will start with a [data command](data-commands.md) and will re-shape the result set it received from the previous line. For example: 24 | 25 | - The WHERE data command will only keep those lines from the result set which match a given condition. This means that, unless all data in the result set matches the condition, this command will pass on a smaller result set to the next line than it received from the previous line. Unlike in SQL, you can have as many WHERE commands as you like. 26 | - The FLATTEN data command is not found in common SQL but in DQL you can use it to reduce the depth of the result set. 27 | - DQL, similarly to SQL, has a GROUP BY command but this can also be used multiple times, which is not possible in common SQL. You can even do several SORT or GROUP BY commands one after the other. 28 | -------------------------------------------------------------------------------- /docs/docs/queries/dql-js-inline.md: -------------------------------------------------------------------------------- 1 | # DQL, JS and Inlines 2 | 3 | Once you've added [useful data to relevant pages](../annotation/add-metadata.md), you'll want to actually display it somewhere or operate on it. Dataview 4 | allows this in four different ways, all of which are written in codeblocks directly in your Markdown and live-reloaded 5 | when your vault changes. 6 | 7 | ## Dataview Query Language (DQL) 8 | 9 | The [**Dataview Query Language**](structure.md) (for short **DQL**) is a SQL-like language and Dataviews core functionality. It supports [four Query Types](./query-types.md) to produce different outputs, [data commands](./data-commands.md) to refine, resort or group your result and [plentiful functions](../reference/functions.md) which allow numerous operations and adjustments to achieve your wanted output. 10 | 11 | !!! warning Differences to SQL 12 | If you are familiar with SQL, please read [Differences to SQL](differences-to-sql.md) to avoid confusing DQL with SQL. 13 | 14 | You create a **DQL** query with a codeblock that uses `dataview` as type: 15 | 16 | ~~~ 17 | ```dataview 18 | TABLE rating AS "Rating", summary AS "Summary" FROM #games 19 | SORT rating DESC 20 | ``` 21 | ~~~ 22 | 23 | !!! attention "Use backticks" 24 | A valid codeblock needs to use backticks (\`) on start and end (three each). Do not confuse the backtick with the similar looking apostrophe ' ! 25 | 26 | Find a explanation how to write a DQL Query under the [query language 27 | reference](structure.md). If you learn better by example, take a look at the [query examples](../resources/examples.md). 28 | 29 | ## Inline DQL 30 | 31 | A Inline DQL uses a inline block format instead of a code block and a configurable prefix to mark this inline code block as a DQL block. 32 | 33 | ~~~ 34 | `= this.file.name` 35 | ~~~ 36 | 37 | !!! info "Change of DQL prefix" 38 | You can change the `=` to another token (like `dv:` or `~`) in Dataviews' settings under "Codeblock Settings" > "Inline Query Prefix" 39 | 40 | Inline DQL Queries display **exactly one value** somewhere in the middle of your note. They seamlessly blend into the content of your note: 41 | 42 | ~~~markdown 43 | Today is `= date(today)` - `= [[exams]].deadline - date(today)` until exams! 44 | ~~~ 45 | 46 | would, for example, render to 47 | 48 | ~~~markdown 49 | Today is November 07, 2022 - 2 months, 5 days until exams! 50 | ~~~ 51 | 52 | **Inline DQL** queries always display exactly one value, not a list (or table) of values. You can access the properties of the **current page** via prefix `this.` or a different page via `[[linkToPage]].`. 53 | 54 | ~~~markdown 55 | `= this.file.name` 56 | `= this.file.mtime` 57 | `= this.someMetadataField` 58 | `= [[secondPage]].file.name` 59 | `= [[secondPage]].file.mtime` 60 | `= [[secondPage]].someMetadataField` 61 | ~~~ 62 | 63 | You can use everything available as [expressions](../reference/expressions.md) and [literals](../reference/literals.md) in an Inline DQL Query, including [functions](../reference/functions.md). Query Types and Data Commands, on the other hand, are **not available in Inlines.** 64 | 65 | ~~~markdown 66 | Assignment due in `= this.due - date(today)` 67 | Final paper due in `= [[Computer Science Theory]].due - date(today)` 68 | 69 | 🏃‍♂️ Goal reached? `= choice(this.steps > 10000, "YES!", "**No**, get moving!")` 70 | 71 | You have `= length(filter(link(dateformat(date(today), "yyyy-MM-dd")).file.tasks, (t) => !t.completed))` tasks to do. `= choice(date(today).weekday > 5, "Take it easy!", "Time to get work done!")` 72 | ~~~ 73 | 74 | ## Dataview JS 75 | 76 | The dataview [JavaScript API](../api/intro.md) gives you the full power of JavaScript and provides a DSL for pulling 77 | Dataview data and executing queries, allowing you to create arbitrarily complex queries and views. Similar to the query 78 | language, you create Dataview JS blocks via a `dataviewjs`-annotated codeblock: 79 | 80 | ~~~java 81 | ```dataviewjs 82 | let pages = dv.pages("#books and -#books/finished").where(b => b.rating >= 7); 83 | for (let group of pages.groupBy(b => b.genre)) { 84 | dv.header(3, group.key); 85 | dv.list(group.rows.file.name); 86 | } 87 | ``` 88 | ~~~ 89 | 90 | Inside of a JS dataview block, you have access to the full dataview API via the `dv` variable. For an explanation of 91 | what you can do with it, see the [API documentation](../api/code-reference.md), or the [API 92 | examples](../api/code-examples.md). 93 | 94 | !!! attention "Advanced usage" 95 | Writing Javascript queries is a advanced technique that requires understanding in programming and JS. Please be aware that JS Queries have access to your file system and be cautious when using other peoples' JS Queries, especially when they are not publicly shared through the Obsidian Community. 96 | 97 | ## Inline Dataview JS 98 | 99 | Similar to the query language, you can write JS inline queries, which let you embed a computed JS value directly. You 100 | create JS inline queries via inline code blocks: 101 | 102 | ``` 103 | `$= dv.current().file.mtime` 104 | ``` 105 | 106 | In inline DataviewJS, you have access to the `dv` variable, as in `dataviewjs` codeblocks, and can make all of the same calls. The result 107 | should be something which evaluates to a JavaScript value, which Dataview will automatically render appropriately. 108 | 109 | Unlike Inline DQL queries, Inline JS queries do have access to everything a Dataview JS Query has available and can hence query and output multiple pages. 110 | 111 | !!! info "Change of Inline JS prefix" 112 | You can change the `$=` to another token (like `dvjs:` or `$~`) in Dataviews' settings under "Codeblock Settings" > "Javascript Inline Query Prefix" 113 | -------------------------------------------------------------------------------- /docs/docs/reference/literals.md: -------------------------------------------------------------------------------- 1 | # Literals 2 | 3 | Dataview query language *literals* are **expressions** which represent constant values like a text (`"Science"`) or a number (`2021`). They can be used as part as [functions](functions.md) or of [expressions like comparison](./expressions.md). Some examples of [Queries](../queries/structure.md) that use **literals**: 4 | 5 | ~~~ 6 | 7 | Literal (number) 2022 used in a comparison 8 | ```dataview 9 | LIST 10 | WHERE file.day.year = 2022 11 | ``` 12 | 13 | Literal (text) "Math" used in a function call 14 | ```dataview 15 | LIST 16 | WHERE contains(file.name, "Math") 17 | ``` 18 | 19 | Literal (link) [[Study MOC]] used as a source 20 | ```dataview 21 | LIST 22 | FROM [[Study MOC]] 23 | ``` 24 | 25 | Literal (date) date(yesterday) used in a comparison 26 | ```dataview 27 | TASK 28 | WHERE !completed AND file.day = date(yesterday) 29 | ``` 30 | 31 | Literal (duration) dur(2 days) used in a comparison 32 | ```dataview 33 | LIST 34 | WHERE end - start > dur(2 days) 35 | ``` 36 | ~~~ 37 | 38 | !!! summary "Literals" 39 | Literals are **static values** that can be used as part of the Dataview Query Language (DQL), i.e. for comparisons. 40 | 41 | The following is an extensive, but non-exhaustive list of possible literals in DQL. 42 | 43 | ### General 44 | Literal|Description 45 | -|- 46 | `0`|The number zero 47 | `1337`|The positive number 1337 48 | `-200`| The negative number -200 49 | `"The quick brown fox jumps over the lazy dog"`| Text (sometimes referred to as "string") 50 | `[[Science]]`|A link to the file named "Science" 51 | `[[]]`| A link to the current file 52 | `[1, 2, 3]`|A list of numbers 1, 2, and 3 53 | `[[1, 2],[3, 4]]`|A list of list [1, 2] and [3, 4] 54 | `{ a: 1, b: 2 }`| An object with keys a and b, whereas a has value 1, b 2. | 55 | `date(2021-07-14)`| A date (read more below) | 56 | `dur(2 days 4 hours)` | A duration (read more below) | 57 | 58 | !!! attention "Literals as field values" 59 | Literals are only interpreted this way when used inside a Query, not when used as a meta data value. For possible values and their data types for fields, please refer to [Types of Metadata](../annotation/types-of-metadata.md). 60 | 61 | ### Dates 62 | 63 | Whenever you use a [field value in Date ISO format](../annotation/types-of-metadata.md#date), you'll need to compare these fields against date objects. Dataview provides some shorthands for common use cases like tomorrow, start of current week etc. Please note that `date()` is also a [function](functions.md#dateany), which can be called on **text** to extract dates. 64 | 65 | Literal|Description 66 | -|- 67 | `date(2021-11-11)`|A date, November 11th, 2021 68 | `date(2021-09-20T20:17)`| A date, September 20th, 2021 at 20:17 69 | `date(today)`|A date representing the current date 70 | `date(now)`|A date representing the current date and time 71 | `date(tomorrow)`|A date representing tomorrow's date 72 | `date(yesterday)`|A date representing yesterday's date 73 | `date(sow)`|A date representing the start of the current week 74 | `date(eow)`|A date representing the end of the current week 75 | `date(som)`|A date representing the start of the current month 76 | `date(eom)`|A date representing the end of the current month 77 | `date(soy)`|A date representing the start of the current year 78 | `date(eoy)`|A date representing the end of the current year 79 | 80 | ### Durations 81 | 82 | Durations are representatives of a time span. You can either [define them directly](../annotation/types-of-metadata.md#duration) or create them due to [calculating with dates](../annotation/types-of-metadata.md#duration), and use these for i.e. comparisons. 83 | 84 | #### Seconds 85 | Literal|Description 86 | -|- 87 | `dur(1 s)`|one second 88 | `dur(3 s)`|three seconds 89 | `dur(1 sec)`|one second 90 | `dur(3 secs)`|three seconds 91 | `dur(1 second)`|one second 92 | `dur(3 seconds)`|three seconds 93 | 94 | #### Minutes 95 | Literal|Description 96 | -|- 97 | `dur(1 m)`|one minute 98 | `dur(3 m)`|three minutes 99 | `dur(1 min)`|one minute 100 | `dur(3 mins)`|three minutes 101 | `dur(1 minute)`|one minute 102 | `dur(3 minutes)`|three minutes 103 | 104 | #### Hours 105 | Literal|Description 106 | -|- 107 | `dur(1 h)`|one hour 108 | `dur(3 h)`|three hours 109 | `dur(1 hr)`|one hour 110 | `dur(3 hrs)`|three hours 111 | `dur(1 hour)`|one hour 112 | `dur(3 hours)`|three hours 113 | 114 | #### Days 115 | Literal|Description 116 | -|- 117 | `dur(1 d)`|one day 118 | `dur(3 d)`|three days 119 | `dur(1 day)`|one day 120 | `dur(3 days)`|three days 121 | 122 | #### Weeks 123 | Literal|Description 124 | -|- 125 | `dur(1 w)`|one week 126 | `dur(3 w)`|three weeks 127 | `dur(1 wk)`|one week 128 | `dur(3 wks)`|three weeks 129 | `dur(1 week)`|one week 130 | `dur(3 weeks)`|three weeks 131 | 132 | #### Months 133 | Literal|Description 134 | -|- 135 | `dur(1 mo)`|one month 136 | `dur(3 mo)`|three month 137 | `dur(1 month)`|one month 138 | `dur(3 months)`|three months 139 | 140 | #### Years 141 | Literal|Description 142 | -|- 143 | `dur(1 yr)`|one year 144 | `dur(3 yrs)`|three years 145 | `dur(1 year)`|one year 146 | `dur(3 years)`|three years 147 | 148 | #### Combinations 149 | Literal|Description 150 | -|- 151 | `dur(1 s, 2 m, 3 h)`|three hours, two minutes, and one second 152 | `dur(1 s 2 m 3 h)`|three hours, two minutes, and one second 153 | `dur(1s 2m 3h)`|three hours, two minutes, and one second 154 | `dur(1second 2min 3h)`|three hours, two minutes, and one second 155 | -------------------------------------------------------------------------------- /docs/docs/reference/sources.md: -------------------------------------------------------------------------------- 1 | # Sources 2 | 3 | A dataview **source** is something that identifies a set of files, tasks, or other data. Sources are indexed internally by 4 | Dataview, so they are fast to query. The most prominent use of sources is the [FROM data command](../../queries/data-commands.md#from). They are also used in various JavaScript API query calls. 5 | 6 | ## Types of Sources 7 | 8 | Dataview currently supports **four source types**. 9 | 10 | ### Tags 11 | 12 | Sources of the form `#tag`. These match all files / sections / tasks with the given tag. 13 | 14 | ~~~ 15 | ```dataview 16 | LIST 17 | FROM #homework 18 | ``` 19 | ~~~ 20 | 21 | ### Folders 22 | 23 | Sources of the form `"folder"`. These match all files / sections / tasks contained in the given folder and its sub folders. The full vault path is expected instead of just the folder name. Note that trailing slashes are not supported, i.e. `"Path/To/Folder/"` will not work but `"Path/To/Folder"` will. 24 | 25 | ~~~ 26 | ```dataview 27 | TABLE file.ctime, status 28 | FROM "projects/brainstorming" 29 | ``` 30 | ~~~ 31 | 32 | 33 | ### Specific Files 34 | 35 | You can select from a specific file by specifying it's full path: `"folder/File"`. 36 | 37 | - If you have both a file and a folder with the exact same path, Dataview will prefer the folder. You can force it to read from the file by specifying an extension: `folder/File.md`. 38 | 39 | ~~~ 40 | ```dataview 41 | LIST WITHOUT ID next-in-line 42 | FROM "30 Hobbies/Games/Dashboard" 43 | ``` 44 | ~~~ 45 | 46 | 47 | ### Links 48 | 49 | You can either select links **to** a file, or all links **from** a file. 50 | 51 | - To obtain all pages which link **to** `[[note]]`, use `[[note]]`. 52 | - To obtain all pages which link **from** `[[note]]` (i.e., all the links in that file), use `outgoing([[note]])`. 53 | - You can implicitly reference the current file via `[[#]]` or `[[]]`, i.e. `[[]]` lets you query from all files linking to the current file. 54 | 55 | ~~~ 56 | ```dataview 57 | LIST 58 | FROM [[]] 59 | ``` 60 | 61 | ```dataview 62 | LIST 63 | FROM outgoing([[Dashboard]]) 64 | ``` 65 | ~~~ 66 | 67 | 68 | ## Combining Sources 69 | 70 | You can compose these filters in order to get more advanced sources using `and` and `or`. 71 | 72 | - For example, `#tag and "folder"` will return all pages in `folder` and with `#tag`. 73 | - Querying from `#food and !#fastfood` will only return pages that contain `#food` but does not contain `#fastfood`. 74 | - `[[Food]] or [[Exercise]]` will give any pages which link to `[[Food]]` OR `[[Exercise]]`. 75 | 76 | If you have complex queries where grouping or precedence matters, you can use parenthesis to logically group them: 77 | 78 | - `#tag and ("folder" or #other-tag)` 79 | - `(#tag1 or #tag2) and (#tag3 or #tag4)` 80 | 81 | 82 | -------------------------------------------------------------------------------- /docs/docs/resources/develop-against-dataview.md: -------------------------------------------------------------------------------- 1 | # Developing Against Dataview 2 | 3 | Dataview includes a high-level plugin-facing API as well as TypeScript definitions and a utility library; to install it, 4 | simply use: 5 | 6 | ```bash 7 | npm install -D obsidian-dataview 8 | ``` 9 | 10 | To verify that it is the correct version installed, do `npm list obsidian-dataview`. If that fails to report the latest version, which currently is 0.5.64, you can do: 11 | 12 | ```bash 13 | npm install obsidian-dataview@0.5.64 14 | ``` 15 | 16 | **Note**: If [Git](http://git-scm.com/) is not already installed on your local system, you will need to install it first. You may need to restart your device to complete the Git installation before you can install the Dataview API. 17 | 18 | ##### Accessing the Dataview API 19 | 20 | You can use the `getAPI()` function to obtain the Dataview Plugin API; this returns a `DataviewApi` object which 21 | provides various utilities, including rendering dataviews, checking dataview's version, hooking into the dataview event 22 | life cycle, and querying dataview metadata. 23 | 24 | ```ts 25 | import { getAPI } from "obsidian-dataview"; 26 | 27 | const api = getAPI(); 28 | ``` 29 | 30 | For full API definitions available, check 31 | [index.ts](https://github.com/blacksmithgu/obsidian-dataview/blob/master/src/index.ts) or the plugin API definition [plugin-api.ts](https://github.com/blacksmithgu/obsidian-dataview/blob/master/src/api/plugin-api.ts). 32 | 33 | ##### Binding to Dataview Events 34 | 35 | You can bind to dataview metadata events, which fire on all file updates and changes, via: 36 | 37 | 38 | ```ts 39 | plugin.registerEvent(plugin.app.metadataCache.on("dataview:index-ready", () => { 40 | ... 41 | }); 42 | 43 | plugin.registerEvent(plugin.app.metadataCache.on("dataview:metadata-change", 44 | (type, file, oldPath?) => { ... })); 45 | ``` 46 | 47 | For all events hooked on MetadataCache, check [index.ts](https://github.com/blacksmithgu/obsidian-dataview/blob/master/src/index.ts). 48 | 49 | ##### Value Utilities 50 | 51 | You can access various type utilities which let you check the types of objects and compare them via `Values`: 52 | 53 | ~~~ts 54 | import { getAPI, Values } from "obsidian-dataview"; 55 | 56 | const field = getAPI(plugin.app)?.page('sample.md').field; 57 | if (!field) return; 58 | 59 | if (Values.isHtml(field)) // do something 60 | else if (Values.isLink(field)) // do something 61 | // ... 62 | ~~~ 63 | -------------------------------------------------------------------------------- /docs/docs/resources/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | A small collection of simple usages of the dataview query language. 4 | 5 | --- 6 | 7 | Show all games in the games folder, sorted by rating, with some metadata: 8 | 9 | === "Query" 10 | ```sql 11 | TABLE 12 | time-played AS "Time Played", 13 | length AS "Length", 14 | rating AS "Rating" 15 | FROM "games" 16 | SORT rating DESC 17 | ``` 18 | === "Output" 19 | |File|Time Played|Length|Rating| 20 | |-|-|-|-| 21 | |[Outer Wilds](#)|November 19th - 21st, 2020|15h|9.5| 22 | |[Minecraft](#)|All the time.|2000h|9.5| 23 | |[Pillars of Eternity 2](#)|August - October 2019|100h|9| 24 | 25 | --- 26 | 27 | List games which are MOBAs or CRPGs. 28 | 29 | === "Query" 30 | ``` sql 31 | LIST FROM #games/mobas OR #games/crpg 32 | ``` 33 | === "Output" 34 | - [League of Legends](#) 35 | - [Pillars of Eternity 2](#) 36 | 37 | --- 38 | 39 | List all tasks in un-completed projects: 40 | 41 | === "Query" 42 | ``` sql 43 | TASK FROM "dataview" 44 | ``` 45 | === "Output" 46 | [dataview/Project A](#) 47 | 48 | - [ ] I am a task. 49 | - [ ] I am another task. 50 | 51 | [dataview/Project A](#) 52 | 53 | - [ ] I could be a task, though who knows. 54 | - [X] Determine if this is a task. 55 | - [X] I'm a finished task. 56 | 57 | --- 58 | 59 | List all of the files in the `books` folder, sorted by the last time you modified the file: 60 | 61 | === "Query" 62 | ```sql 63 | TABLE file.mtime AS "Last Modified" 64 | FROM "books" 65 | SORT file.mtime DESC 66 | ``` 67 | === "Output" 68 | |File|Last Modified| 69 | |-|-| 70 | |[Atomic Habits](#)|11:06 PM - August 07, 2021| 71 | |[Can't Hurt Me](#)|10:58 PM - August 07, 2021| 72 | |[Deep Work](#)|10:52 PM - August 07, 2021| 73 | 74 | --- 75 | 76 | List all files which have a date in their title (of the form `yyyy-mm-dd`), and list them by date order. 77 | 78 | === "Query" 79 | ```sql 80 | LIST file.day WHERE file.day 81 | SORT file.day DESC 82 | ``` 83 | === "Output" 84 | - [2021-08-07](#): August 07, 2021 85 | - [2020-08-10](#): August 10, 2020 86 | -------------------------------------------------------------------------------- /docs/docs/resources/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | A collection of frequently asked questions for Dataview queries and the expression language. 4 | 5 | ### How do I use fields with the same name as keywords (like "from", "where")? 6 | 7 | Dataview provides a special "fake" field called `row` which can be indexed into to obtain fields which conflict with 8 | Dataview keywords: 9 | 10 | ```javascript 11 | row.from /* Same as "from" */ 12 | row.where /* Same as "where" */ 13 | ``` 14 | 15 | 16 | ### How do I access fields with spaces in the name? 17 | 18 | There are two ways: 19 | 20 | 1. Use the normalized Dataview name for such a field - just convert the name to lowercase and replace whitespace with 21 | dashes ("-"). Something like `Field With Space In It` becomes `field-with-space-in-it`. 22 | 2. Use the implicit `row` field: 23 | ```javascript 24 | row["Field With Space In It"] 25 | ``` 26 | 27 | ### Do you have a list of resources to learn from? 28 | 29 | Yes! Please see the [Resources](../resources/resources-and-support.md) page. 30 | 31 | ### Can I save the result of a query for reusability? 32 | 33 | You can write reusable Javascript Queries with the [dv.view](../api/code-reference.md#dvviewpath-input) function. In DQL, beside the possibility of writing your Query inside a Template and using this template (either with the [Core Plugin Templates](https://help.obsidian.md/Plugins/Templates) or the popular Community Plugin [Templater](https://obsidian.md/plugins?id=templater-obsidian)), you can **save calculations in metadata fields via [Inline DQL](../queries/dql-js-inline.md#inline-dql)**, for example: 34 | 35 | ```markdown 36 | start:: 07h00m 37 | end:: 18h00m 38 | pause:: 01h30m 39 | duration:: `= this.end - this.start - this.pause` 40 | ``` 41 | 42 | You can list the value (9h 30m in our example) then i.e. in a TABLE without needing to repeat the calculation: 43 | 44 | ~~~markdown 45 | ```dataview 46 | TABLE start, end, duration 47 | WHERE duration 48 | ``` 49 | ~~~ 50 | 51 | Gives you 52 | 53 | | File (1) | start| end| duration| 54 | | ---- | ----- | ------ | ----- | 55 | | Example | 7 hours | 18 hours| 9 hours, 30 minutes | 56 | 57 | **But storing a Inline DQL in a field comes with a limitation**: While the value that gets displayed in the result is the calculated one, **the saved value inside your metadata field is still your Inline DQL calculation**. The value is literally `= this.end - this.start - this.pause`. This means you cannot filter for the Inlines' result like: 58 | 59 | ~~~markdown 60 | ```dataview 61 | TABLE start, end, duration 62 | WHERE duration > dur("10h") 63 | ``` 64 | ~~~ 65 | 66 | This will give you back the example page, even though the result doesn't fulfill the `WHERE` clause, because the value you are comparing against is `= this.end - this.start - this.pause` and is not a duration. 67 | 68 | ### How can I hide the result count on TABLE Queries? 69 | 70 | With Dataview 0.5.52, you can hide the result count on TABLE and TASK Queries via a setting. Go to Dataview's settings -> Display result count. 71 | 72 | ### How can I style my queries? 73 | 74 | You can use [CSS Snippets](https://help.obsidian.md/Extending+Obsidian/CSS+snippets) to define custom styling in general for Obsidian. So if you define `cssclasses: myTable` as a property, and enable the snippet below you could set the background color of a table from dataview. Similar to target the outer <ul> of a `TASK` or `LIST` query, you could use the `ul.contains-task-list` or `ul.list-view-ul` respectively. 75 | 76 | ```css 77 | .myTable dataview.table { 78 | background-color: green 79 | } 80 | ``` 81 | 82 | In general there are no unique ID's given to a specific table on a page, so the mentioned targeting applies to any note having that `cssclasses` defined and **all** tables on that page. Currently you can't target a specific table using an ordinary query, but if you're using javascript, you can add the class `clsname` directly to your query result by doing: 83 | 84 | ```js 85 | dv.container.className += ' clsname' 86 | ``` 87 | 88 | However, there is a trick to target any table within Obsidian using tags like in the example below, and that would apply to any table having that tag tag within it. This would apply to both manually and dataview generated tables. To make the snippet below work add the tag `#myId` _anywhere_ within your table output. 89 | 90 | ```css 91 | [href="#myId"] { 92 | display: none; /* Hides the tag from the table view */ 93 | } 94 | 95 | table:has([href="#myId"]) { 96 | /* Style your table as you like */ 97 | background-color: #262626; 98 | & tr:nth-child(even) td:first-child{ 99 | background-color: #3f3f3f; 100 | } 101 | } 102 | ``` 103 | 104 | Which would end up having a grey background on the entire table, and the first cell in every even row a different variant of grey. **Disclaimer:** We're not style gurus, so this is just an example to show some of the syntax needed for styling different parts of a table. 105 | 106 | Furthermore, in [Style dataview table columns](https://s-blu.github.io/obsidian_dataview_example_vault/20%20Dataview%20Queries/Style%20dataview%20table%20columns/) @s-blu describes an alternate trick using `` to style various parts of table cells (and columns). 107 | -------------------------------------------------------------------------------- /docs/docs/resources/resources-and-support.md: -------------------------------------------------------------------------------- 1 | # Other Resources 2 | 3 | There is a bit of a learning curve to getting started with Dataview. 4 | This page is a collection of resources that will help you get started. 5 | Dataview gets new features and fixes fairly frequently so please account for these resources being slightly out of date. 6 | Feel free to contribute directly to this list, documentation, or even reach out to the authors of the original sources for updates. 7 | 8 | ## Resources 9 | 10 | ### The Obsidian Hub 11 | 12 | - SkepticMystic's [Introduction to Dataview](https://publish.obsidian.md/hub/04+-+Guides%2C+Workflows%2C+%26+Courses/Community+Talks/YT+-+An+Introduction+to+Dataview) supplemented by [a textual guide](https://publish.obsidian.md/hub/04+-+Guides%2C+Workflows%2C+%26+Courses/Guides/An+Introduction+to+Dataview) 13 | 14 | ### YouTube videos 15 | 16 | - SkepticMystic's aforementioned community talk 17 | - [Dataview Plugin: How To Use This Powerful Obsidian Plugin (With Examples) by Filipe Donadio](https://www.youtube.com/watch?v=7kFEl7Ovsr8) 18 | - [Automate Your Vault With Dataview - How To Use Dataview in Obsidian by FromSergio](https://www.youtube.com/watch?v=8yjNuiSBSAM) 19 | - [How to use the Obsidian Dataview plugin by Nicole van der Hoeven](https://www.youtube.com/watch?v=JTObSymEvWA) 20 | - [Intro to Dataview Plugin - Obsidian Community Talk](https://www.youtube.com/watch?v=lclif6l9UgQ) 21 | 22 | ### Example Vault 23 | 24 | - @s-blu has very kindly put together [a vault of example queries](https://github.com/s-blu/obsidian_dataview_example_vault/) that you can use as a playground of sorts. 25 | 26 | ### Blog Posts 27 | 28 | - [Obsidian Dataview For Beginners: A checklist to help fix your dataview queries](https://denisetodd.medium.com/obsidian-dataview-for-beginners-a-checklist-to-help-fix-your-dataview-queries-11acc57f1e48) 29 | 30 | ### GitHub Discussion 31 | 32 | The GitHub repository has a fairly active [Discussions Page](https://github.com/blacksmithgu/obsidian-dataview/discussions/) with dozens of answered questions. 33 | 34 | ### Obsidian Forums 35 | 36 | The [Obsidian Forums](https://forum.obsidian.md/) have a wealth of questions and answers and other interesting tidbits as well. 37 | Try searching the forums for an answer, especially if it seems like a beginner question. 38 | 39 | ### Discord 40 | 41 | The [Obsidian Members Group Discord server](https://obsidian.md/community) has a `#dataview` channel. 42 | Once again, more likely than not, your question has been asked before so try searching the thread though it is known that Discord's search can be spotty. 43 | In case you don't find anything satisfactory, 44 | This is the closest you'll get to real-time support but there are no guarantees of instant replies. 45 | There are many helpful people though so don't be afraid to ask. 46 | 47 | ## Support 48 | 49 | Where do you go when you have questions? 50 | Here's what we recommend you try. 51 | 52 | 1. Search GitHub Discussions and the Obsidian Forums for your question. 53 | 2. Search through Discord for possible solutions. 54 | 3. Depending on the complexity of the your question: 55 | - If you need close to synchronous communication, use Discord. Note that we are a community of volunteers and there may be delays in responding. 56 | - If you expect your problem needs time over multiple days and asynchronous communication, open a GitHub discussion. 57 | 4. If you found a bug, please report it in [the repo's issues](https://github.com/blacksmithgu/obsidian-dataview/issues). 58 | -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Dataview 2 | repo_url: https://github.com/blacksmithgu/obsidian-dataview 3 | edit_uri: edit/master/docs/docs/ 4 | markdown_extensions: 5 | - abbr 6 | - admonition 7 | - pymdownx.highlight 8 | - pymdownx.superfences 9 | - pymdownx.tabbed 10 | - pymdownx.tasklist 11 | theme: 12 | name: material 13 | palette: 14 | primary: deep purple 15 | logo: assets/obsidian.png 16 | favicon: assets/obsidian.png 17 | custom_dir: overrides 18 | nav: 19 | - Overview: 'index.md' 20 | - Metadata: 21 | - Adding Metadata: 'annotation/add-metadata.md' 22 | - Data Types: 'annotation/types-of-metadata.md' 23 | - Metadata on Pages: 'annotation/metadata-pages.md' 24 | - Metadata on Tasks and Lists: 'annotation/metadata-tasks.md' 25 | - DQL, JS and Inlines: 'queries/dql-js-inline.md' 26 | - Query Language Reference: 27 | - Structure of a Query: 'queries/structure.md' 28 | - Query Types: 'queries/query-types.md' 29 | - Data Commands: 'queries/data-commands.md' 30 | - Differences to SQL: 'queries/differences-to-sql.md' 31 | - Sources: 'reference/sources.md' 32 | - Expressions: 'reference/expressions.md' 33 | - Literals: 'reference/literals.md' 34 | - Functions: 'reference/functions.md' 35 | - JavaScript Reference: 36 | - Overview: 'api/intro.md' 37 | - Data Arrays: 'api/data-array.md' 38 | - Codeblock Reference: 'api/code-reference.md' 39 | - Codeblock Examples: 'api/code-examples.md' 40 | - FAQ and Resources: 41 | - Frequently Asked Questions: 'resources/faq.md' 42 | - Examples: 'resources/examples.md' 43 | - Developing Against Dataview: 'resources/develop-against-dataview.md' 44 | - Resources and Support: 'resources/resources-and-support.md' 45 | - Friends of Dataview: 'friends.md' 46 | - Changelog: 'changelog.md' 47 | plugins: 48 | - search 49 | - redirects: 50 | redirect_maps: 51 | docs/where-data-comes-from.md: annotation/add-metadata.md 52 | docs/creating-queries.md: queries/dql-js-inline.md 53 | docs/query/queries.md: queries/structure.md 54 | docs/query/expressions.md: reference/expressions.md 55 | docs/query/sources.md: reference/sources.md 56 | docs/query/functions.md: reference/functions.md 57 | docs/query/examples.md: resources/examples.md 58 | docs/api/intro.md: api/intro.md 59 | docs/api/data-array.md: api/data-array.md 60 | docs/api/code-reference.md: api/code-reference.md 61 | docs/api/code-examples.md: api/code-examples.md 62 | data-queries.md: queries/dql-js-inline.md 63 | query/queries.md: queries/structure.md 64 | data-annotation.md: annotation/add-metadata.md 65 | resources-and-support.md: resources/resources-and-support.md 66 | plugin/develop-against-dataview.md: resources/develop-against-dataview.md 67 | reference/faq.md: resources/faq.md 68 | reference/examples.md: resources/examples.md 69 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "jsdom", 4 | moduleDirectories: ["node_modules", "src"], 5 | }; 6 | -------------------------------------------------------------------------------- /manifest-beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dataview", 3 | "name": "Dataview", 4 | "version": "0.5.70", 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 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dataview", 3 | "name": "Dataview", 4 | "version": "0.5.68", 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-dataview", 3 | "version": "0.5.70", 4 | "description": "Advanced data views for Obsidian.md.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*" 9 | ], 10 | "scripts": { 11 | "bdd": "npx jest -i --watch --no-cache", 12 | "build": "npx rollup --config rollup.config.js --environment BUILD:production", 13 | "check-format": "npx prettier --check src", 14 | "dev": "npx rollup --config rollup.config.js -w", 15 | "format": "npx prettier --write src", 16 | "lib": "npx rollup --config rollup.config.js --environment BUILD:lib", 17 | "test": "npx jest" 18 | }, 19 | "keywords": [ 20 | "obsidian", 21 | "dataview" 22 | ], 23 | "author": "Michael Brenan", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@rollup/plugin-commonjs": "^25.0.4", 27 | "@rollup/plugin-node-resolve": "^15.2.1", 28 | "@rollup/plugin-typescript": "^11.1.3", 29 | "@types/jest": "^27.0.1", 30 | "@types/luxon": "^3.2.0", 31 | "@types/node": "^16.7.13", 32 | "@types/papaparse": "^5.2.6", 33 | "@types/parsimmon": "^1.10.6", 34 | "@zerollup/ts-transform-paths": "^1.7.18", 35 | "compare-versions": "^4.1.1", 36 | "jest": "^29.7.0", 37 | "jest-environment-jsdom": "^29.7.0", 38 | "obsidian": "^1.4.0", 39 | "prettier": "2.3.2", 40 | "rollup": "^2.79.1", 41 | "rollup-jest": "^3.1.0", 42 | "rollup-plugin-copy": "^3.5.0", 43 | "rollup-plugin-typescript2": "^0.35.0", 44 | "rollup-plugin-web-worker-loader": "^1.6.1", 45 | "ts-jest": "^29.1.1", 46 | "tslib": "^2.6.2", 47 | "typescript": "^5.2.2" 48 | }, 49 | "dependencies": { 50 | "@codemirror/language": "https://github.com/lishid/cm-language", 51 | "@codemirror/state": "^6.0.1", 52 | "@codemirror/view": "^6.0.1", 53 | "emoji-regex": "^10.0.0", 54 | "localforage": "^1.10.0", 55 | "luxon": "^3.2.0", 56 | "obsidian-calendar-ui": "^0.3.12", 57 | "papaparse": "^5.3.1", 58 | "parsimmon": "^1.18.0", 59 | "preact": "^10.6.5", 60 | "remove-markdown": "^0.5.5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import webWorker from "rollup-plugin-web-worker-loader"; 4 | import copy from "rollup-plugin-copy"; 5 | import typescript2 from "rollup-plugin-typescript2"; 6 | 7 | const BASE_CONFIG = { 8 | input: "src/main.ts", 9 | external: ["obsidian", "@codemirror/view", "@codemirror/state", "@codemirror/language"], 10 | onwarn: (warning, warn) => { 11 | // Sorry rollup, but we're using eval... 12 | if (/Use of eval is strongly discouraged/.test(warning.message)) return; 13 | warn(warning); 14 | }, 15 | }; 16 | 17 | const getRollupPlugins = (tsconfig, ...plugins) => 18 | [ 19 | typescript2(tsconfig), 20 | nodeResolve({ browser: true }), 21 | commonjs(), 22 | webWorker({ inline: true, forceInline: true, targetPlatform: "browser" }), 23 | ].concat(plugins); 24 | 25 | const DEV_PLUGIN_CONFIG = { 26 | ...BASE_CONFIG, 27 | output: { 28 | dir: "test-vault/.obsidian/plugins/dataview", 29 | sourcemap: "inline", 30 | format: "cjs", 31 | exports: "default", 32 | name: "Dataview (Development)", 33 | }, 34 | plugins: getRollupPlugins( 35 | undefined, 36 | copy({ 37 | targets: [ 38 | { src: "manifest.json", dest: "test-vault/.obsidian/plugins/dataview/" }, 39 | { src: "styles.css", dest: "test-vault/.obsidian/plugins/dataview/" }, 40 | ], 41 | }) 42 | ), 43 | }; 44 | 45 | const PROD_PLUGIN_CONFIG = { 46 | ...BASE_CONFIG, 47 | output: { 48 | dir: "build", 49 | sourcemap: "inline", 50 | sourcemapExcludeSources: true, 51 | format: "cjs", 52 | exports: "default", 53 | name: "Dataview (Production)", 54 | }, 55 | plugins: getRollupPlugins(), 56 | }; 57 | 58 | const LIBRARY_CONFIG = { 59 | ...BASE_CONFIG, 60 | input: "src/index.ts", 61 | output: { 62 | dir: "lib", 63 | sourcemap: true, 64 | format: "cjs", 65 | name: "Dataview (Library)", 66 | }, 67 | plugins: getRollupPlugins( 68 | { tsconfig: "tsconfig-lib.json" }, 69 | copy({ targets: [{ src: "src/typings/*.d.ts", dest: "lib/typings" }] }) 70 | ), 71 | }; 72 | 73 | let configs = []; 74 | if (process.env.BUILD === "lib") { 75 | // Library build, only library code. 76 | configs.push(LIBRARY_CONFIG); 77 | } else if (process.env.BUILD === "production") { 78 | // Production build, build library and main plugin. 79 | configs.push(LIBRARY_CONFIG, PROD_PLUGIN_CONFIG); 80 | } else if (process.env.BUILD === "dev") { 81 | // Dev build, only build the plugin. 82 | configs.push(DEV_PLUGIN_CONFIG); 83 | } else { 84 | // Default to the dev build. 85 | configs.push(DEV_PLUGIN_CONFIG); 86 | } 87 | 88 | export default configs; 89 | -------------------------------------------------------------------------------- /scripts/beta-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Automatically update versions in files and create an autorelease. 3 | # Requires the github CLI, and the jq command 4 | 5 | EXIT="" 6 | NEW_VERSION=$1 7 | 8 | if [ -z "$EDITOR" ]; then 9 | echo "Specify which editor to use in EDITOR, or by doing: EDITOR=vi ./scripts/beta-release 0.x.y" 10 | EXIT=1 11 | fi 12 | 13 | if ! command -v jq 2>&1 >/dev/null; then 14 | echo "This script relies on the 'jq' command, please install it" 15 | EXIT=1 16 | fi 17 | 18 | if ! command -v gh 2>&1 >/dev/null; then 19 | echo "This script relies on the 'gh' command from the Github CLI package, please install it" 20 | EXIT=1 21 | fi 22 | 23 | if [ $EXIT ]; then 24 | exit $EXIT 25 | fi 26 | 27 | if [ -z "$NEW_VERSION" ]; then 28 | NEW_VERSION=$(jq -r ".version" manifest-beta.json | awk -F. -v OFS=. '{$NF += 1 ; print}') 29 | fi 30 | 31 | if [ -z "$NEW_VERSION" ]; then 32 | echo "Auto-generating next version number failed, please specify next version : ./scripts/beta-release 0.x.y" 33 | exit 1 34 | fi 35 | 36 | echo "Releasing beta version '${NEW_VERSION}'" 37 | 38 | # Let users edit release-notes.txt for release notes. 39 | rm -f release-notes.md 40 | touch release-notes.md 41 | echo -e "# ${NEW_VERSION} (Beta)\n\n" >> release-notes.md 42 | $EDITOR release-notes.md 43 | 44 | # Append release notes to CHANGELOG.md. 45 | mv CHANGELOG.md CHANGELOG-TEMP.md 46 | cp release-notes.md CHANGELOG.md 47 | echo -e "\n---\n" >> CHANGELOG.md 48 | cat CHANGELOG-TEMP.md >> CHANGELOG.md 49 | rm -f CHANGELOG-TEMP.md 50 | 51 | # Overwrite the documentation changelog. 52 | cp -f CHANGELOG.md docs/docs/changelog.md 53 | 54 | # Delete old files if they exist 55 | rm -f package.tmp.json 56 | rm -f manifest-beta.tmp.json 57 | rm -f versions.tmp.json 58 | 59 | # Rewrite versions in relevant files. 60 | jq ".version=\"${NEW_VERSION}\"" package.json > package.tmp.json && mv package.tmp.json package.json 61 | jq ".version=\"${NEW_VERSION}\"" manifest-beta.json > manifest-beta.tmp.json && mv manifest-beta.tmp.json manifest-beta.json 62 | jq ". + {\"${NEW_VERSION}\": \"0.13.11\"}" versions.json > versions.tmp.json && mv versions.tmp.json versions.json 63 | 64 | # Create commit & commit. 65 | git commit -a -m "Auto-release beta ${NEW_VERSION}" 66 | git push 67 | 68 | # Rebuild the project to prepare for a release. 69 | npm run build 70 | 71 | # release api 72 | npm publish --tag beta --access public 73 | 74 | # And do a github release. 75 | gh release create "${NEW_VERSION}" --pre-release build/main.js styles.css manifest.json --title "${NEW_VERSION}" --notes-file release-notes.md 76 | rm -f release-notes.md 77 | -------------------------------------------------------------------------------- /scripts/install-built: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Builds dataview and allows you to provide a path to the vault that it should be installed in. 3 | # Useful for when you want to dry-run the plugin in a vault other than the test vault. 4 | 5 | VAULT="$1" 6 | TARGET="$VAULT/.obsidian/plugins/dataview/" 7 | mkdir -p "$TARGET" 8 | cp -f build/main.js styles.css "$TARGET" 9 | cp -f manifest-beta.json "$TARGET/manifest.json" 10 | echo Installed plugin files to "$TARGET" 11 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Automatically update versions in files and create an autorelease. 3 | # Requires the github CLI and jq (if you don't update the version manually). 4 | EXIT="" 5 | NEW_VERSION=$1 6 | 7 | if [ -z "$EDITOR" ]; then 8 | echo "Specify which editor to use in EDITOR, or by doing: EDITOR=vi ./scripts/release 0.x.y" 9 | EXIT=1 10 | fi 11 | 12 | if ! command -v jq 2>&1 >/dev/null; then 13 | echo "This script relies on the 'jq' command, please install it" 14 | EXIT=1 15 | fi 16 | 17 | if ! command -v gh 2>&1 >/dev/null; then 18 | echo "This script relies on the 'gh' command from the Github CLI package, please install it" 19 | EXIT=1 20 | fi 21 | 22 | if [ $EXIT ]; then 23 | exit $EXIT 24 | fi 25 | 26 | if [ -z "$NEW_VERSION" ]; then 27 | NEW_VERSION=$(jq -r ".version" manifest-beta.json | awk -F. -v OFS=. '{$NF += 1 ; print}') 28 | fi 29 | 30 | if [ -z "$NEW_VERSION" ]; then 31 | echo "Auto-generating next version number failed, please specify next version : ./scripts/release 0.x.y" 32 | exit 1 33 | fi 34 | 35 | # Let users edit release-notes.txt for release notes. 36 | rm -f release-notes.md 37 | touch release-notes.md 38 | echo -e "# ${NEW_VERSION}\n\n" >> release-notes.md 39 | $EDITOR release-notes.md 40 | 41 | # Append release notes to CHANGELOG.md. 42 | mv CHANGELOG.md CHANGELOG-TEMP.md 43 | cp release-notes.md CHANGELOG.md 44 | echo -e "\n---\n" >> CHANGELOG.md 45 | cat CHANGELOG-TEMP.md >> CHANGELOG.md 46 | rm -f CHANGELOG-TEMP.md 47 | 48 | # Overwrite the documentation changelog. 49 | cp -f CHANGELOG.md docs/docs/changelog.md 50 | 51 | # Delete old files if they exist 52 | rm -f package.tmp.json 53 | rm -f manifest.tmp.json 54 | rm -f manifest-beta.tmp.json 55 | rm -f versions.tmp.json 56 | 57 | # Rewrite versions in relevant files. 58 | jq ".version=\"${NEW_VERSION}\"" package.json > package.tmp.json && mv package.tmp.json package.json 59 | jq ".version=\"${NEW_VERSION}\"" manifest.json > manifest.tmp.json && mv manifest.tmp.json manifest.json 60 | jq ".version=\"${NEW_VERSION}\"" manifest-beta.json > manifest-beta.tmp.json && mv manifest-beta.tmp.json manifest-beta.json 61 | jq ". + {\"${NEW_VERSION}\": \"0.12.0\"}" versions.json > versions.tmp.json && mv versions.tmp.json versions.json 62 | 63 | # Overwrite the beta manifest as well. 64 | cp manifest.json manifest-beta.json 65 | 66 | # Create commit & commit. 67 | git commit -a -m "Auto-release ${NEW_VERSION}" 68 | git push 69 | 70 | # Rebuild the project to prepare for a release. 71 | npm run build 72 | 73 | # release api 74 | npm publish --access public 75 | 76 | # And do a github release. 77 | gh release create "${NEW_VERSION}" build/main.js styles.css manifest.json --title "${NEW_VERSION}" --notes-file release-notes.md 78 | rm -f release-notes.md 79 | -------------------------------------------------------------------------------- /src/api/extensions.ts: -------------------------------------------------------------------------------- 1 | import { STask } from "data-model/serialized/markdown"; 2 | 3 | /** A general function for deciding how to check a task given it's current state. */ 4 | export type TaskStatusSelector = (task: STask) => Promise; 5 | 6 | /** 7 | * A dataview extension; allows for registering new functions, altering views, and altering some more 8 | * advanced dataview behavior. 9 | **/ 10 | export class Extension { 11 | /** All registered task status selectors for this extension. */ 12 | public taskStatusSelectors: Record; 13 | 14 | public constructor(public plugin: string) { 15 | this.taskStatusSelectors = {}; 16 | } 17 | 18 | /** Register a task status selector under the given name. */ 19 | public taskStatusSelector(name: string, selector: TaskStatusSelector): Extension { 20 | this.taskStatusSelectors[name] = selector; 21 | return this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/api/result.ts: -------------------------------------------------------------------------------- 1 | /** Functional return type for error handling. */ 2 | export class Success { 3 | public successful: true; 4 | 5 | public constructor(public value: T) { 6 | this.successful = true; 7 | } 8 | 9 | public map(f: (a: T) => U): Result { 10 | return new Success(f(this.value)); 11 | } 12 | 13 | public flatMap(f: (a: T) => Result): Result { 14 | return f(this.value); 15 | } 16 | 17 | public mapErr(f: (e: E) => U): Result { 18 | return this as any as Result; 19 | } 20 | 21 | public bimap(succ: (a: T) => T2, _fail: (b: E) => E2): Result { 22 | return this.map(succ) as any; 23 | } 24 | 25 | public orElse(_value: T): T { 26 | return this.value; 27 | } 28 | 29 | public cast(): Result { 30 | return this as any; 31 | } 32 | 33 | public orElseThrow(_message?: (e: E) => string): T { 34 | return this.value; 35 | } 36 | } 37 | 38 | /** Functional return type for error handling. */ 39 | export class Failure { 40 | public successful: false; 41 | 42 | public constructor(public error: E) { 43 | this.successful = false; 44 | } 45 | 46 | public map(_f: (a: T) => U): Result { 47 | return this as any as Failure; 48 | } 49 | 50 | public flatMap(_f: (a: T) => Result): Result { 51 | return this as any as Failure; 52 | } 53 | 54 | public mapErr(f: (e: E) => U): Result { 55 | return new Failure(f(this.error)); 56 | } 57 | 58 | public bimap(_succ: (a: T) => T2, fail: (b: E) => E2): Result { 59 | return this.mapErr(fail) as any; 60 | } 61 | 62 | public orElse(value: T): T { 63 | return value; 64 | } 65 | 66 | public cast(): Result { 67 | return this as any; 68 | } 69 | 70 | public orElseThrow(message?: (e: E) => string): T { 71 | if (message) throw new Error(message(this.error)); 72 | else throw new Error("" + this.error); 73 | } 74 | } 75 | 76 | export type Result = Success | Failure; 77 | 78 | /** Monadic 'Result' type which encapsulates whether a procedure succeeded or failed, as well as it's return value. */ 79 | export namespace Result { 80 | /** Construct a new success result wrapping the given value. */ 81 | export function success(value: T): Result { 82 | return new Success(value); 83 | } 84 | 85 | /** Construct a new failure value wrapping the given error. */ 86 | export function failure(error: E): Result { 87 | return new Failure(error); 88 | } 89 | 90 | /** Join two results with a bi-function and return a new result. */ 91 | export function flatMap2( 92 | first: Result, 93 | second: Result, 94 | f: (a: T1, b: T2) => Result 95 | ): Result { 96 | if (first.successful) { 97 | if (second.successful) return f(first.value, second.value); 98 | else return failure(second.error); 99 | } else { 100 | return failure(first.error); 101 | } 102 | } 103 | 104 | /** Join two results with a bi-function and return a new result. */ 105 | export function map2( 106 | first: Result, 107 | second: Result, 108 | f: (a: T1, b: T2) => O 109 | ): Result { 110 | return flatMap2(first, second, (a, b) => success(f(a, b))); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/data-import/common.ts: -------------------------------------------------------------------------------- 1 | /** Common utilities for extracting tags, links, and other business from metadata. */ 2 | 3 | import { EXPRESSION } from "expression/parse"; 4 | 5 | const POTENTIAL_TAG_MATCHER = /#[^\s,;\.:!\?'"`()\[\]\{\}]+/giu; 6 | 7 | /** Extract all tags from the given source string. */ 8 | export function extractTags(source: string): Set { 9 | let result = new Set(); 10 | 11 | let matches = source.matchAll(POTENTIAL_TAG_MATCHER); 12 | for (let match of matches) { 13 | let parsed = EXPRESSION.tag.parse(match[0]); 14 | if (parsed.status) result.add(parsed.value); 15 | } 16 | 17 | return result; 18 | } 19 | -------------------------------------------------------------------------------- /src/data-import/csv.ts: -------------------------------------------------------------------------------- 1 | import { canonicalizeVarName } from "util/normalize"; 2 | import { DataObject } from "data-model/value"; 3 | import * as Papa from "papaparse"; 4 | import { parseFrontmatter } from "data-import/markdown-file"; 5 | 6 | /** Parse a CSV file into a collection of data rows. */ 7 | export function parseCsv(content: string): DataObject[] { 8 | let parsed = Papa.parse(content, { 9 | header: true, 10 | skipEmptyLines: true, 11 | comments: "#", 12 | dynamicTyping: true, 13 | }); 14 | 15 | let rows = []; 16 | for (let parsedRow of parsed.data) { 17 | let fields = parseFrontmatter(parsedRow) as DataObject; 18 | let result: DataObject = {}; 19 | 20 | for (let [key, value] of Object.entries(fields)) { 21 | result[key] = value; 22 | result[canonicalizeVarName(key)] = value; 23 | } 24 | 25 | rows.push(result); 26 | } 27 | 28 | return rows; 29 | } 30 | -------------------------------------------------------------------------------- /src/data-import/persister.ts: -------------------------------------------------------------------------------- 1 | import { PageMetadata } from "data-model/markdown"; 2 | import { Transferable } from "data-model/transferable"; 3 | import localforage from "localforage"; 4 | 5 | /** A piece of data that has been cached for a specific version and time. */ 6 | export interface Cached { 7 | /** The version of Dataview that the data was written to cache with. */ 8 | version: string; 9 | /** The time that the data was written to cache. */ 10 | time: number; 11 | /** The data that was cached. */ 12 | data: T; 13 | } 14 | 15 | /** Simpler wrapper for a file-backed cache for arbitrary metadata. */ 16 | export class LocalStorageCache { 17 | public persister: LocalForage; 18 | 19 | public constructor(public appId: string, public version: string) { 20 | this.persister = localforage.createInstance({ 21 | name: "dataview/cache/" + appId, 22 | driver: [localforage.INDEXEDDB], 23 | description: "Cache metadata about files and sections in the dataview index.", 24 | }); 25 | } 26 | 27 | /** Drop the entire cache instance and re-create a new fresh instance. */ 28 | public async recreate() { 29 | await localforage.dropInstance({ name: "dataview/cache/" + this.appId }); 30 | 31 | this.persister = localforage.createInstance({ 32 | name: "dataview/cache/" + this.appId, 33 | driver: [localforage.INDEXEDDB], 34 | description: "Cache metadata about files and sections in the dataview index.", 35 | }); 36 | } 37 | 38 | /** Load file metadata by path. */ 39 | public async loadFile(path: string): Promise> | null | undefined> { 40 | return this.persister.getItem(this.fileKey(path)).then(raw => { 41 | let result = raw as any as Cached>; 42 | if (result) result.data = Transferable.value(result.data); 43 | return result; 44 | }); 45 | } 46 | 47 | /** Store file metadata by path. */ 48 | public async storeFile(path: string, data: Partial): Promise { 49 | await this.persister.setItem(this.fileKey(path), { 50 | version: this.version, 51 | time: Date.now(), 52 | data: Transferable.transferable(data), 53 | }); 54 | } 55 | 56 | /** Drop old file keys that no longer exist. */ 57 | public async synchronize(existing: string[] | Set): Promise> { 58 | let keys = new Set(await this.allFiles()); 59 | for (let exist of existing) keys.delete(exist); 60 | 61 | // Any keys remaining after deleting existing keys are non-existent keys that should be cleared from cache. 62 | for (let key of keys) await this.persister.removeItem(this.fileKey(key)); 63 | 64 | return keys; 65 | } 66 | 67 | /** Obtain a list of all metadata keys. */ 68 | public async allKeys(): Promise { 69 | return this.persister.keys(); 70 | } 71 | 72 | /** Obtain a list of all persisted files. */ 73 | public async allFiles(): Promise { 74 | let keys = await this.allKeys(); 75 | return keys.filter(k => k.startsWith("file:")).map(k => k.substring(5)); 76 | } 77 | 78 | public fileKey(path: string): string { 79 | return "file:" + path; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/data-import/web-worker/import-entry.ts: -------------------------------------------------------------------------------- 1 | /** Entry-point script used by the index as a web worker. */ 2 | import { runImport } from "data-import/web-worker/import-impl"; 3 | import { Transferable } from "data-model/transferable"; 4 | import { CachedMetadata, FileStats } from "obsidian"; 5 | 6 | /** An import which can fail and raise an exception, which will be caught by the handler. */ 7 | function failableImport(path: string, contents: string, stat: FileStats, metadata?: CachedMetadata) { 8 | if (metadata === undefined || metadata === null) { 9 | throw Error(`Cannot index file, since it has no Obsidian file metadata.`); 10 | } 11 | 12 | return runImport(path, contents, stat, metadata); 13 | } 14 | 15 | onmessage = async evt => { 16 | try { 17 | let { path, contents, stat, metadata } = evt.data; 18 | let result = failableImport(path, contents, stat, metadata); 19 | (postMessage as any)({ path: evt.data.path, result: Transferable.transferable(result) }); 20 | } catch (error) { 21 | console.log(error); 22 | (postMessage as any)({ 23 | path: evt.data.path, 24 | result: { 25 | $error: `Failed to index file: ${evt.data.path}: ${error}`, 26 | }, 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/data-import/web-worker/import-impl.ts: -------------------------------------------------------------------------------- 1 | /** Actual import implementation backend. This must remain separate from `import-entry` since it is used without web workers. */ 2 | import { parsePage } from "data-import/markdown-file"; 3 | import { PageMetadata } from "data-model/markdown"; 4 | import { CachedMetadata, FileStats } from "obsidian"; 5 | 6 | export function runImport( 7 | path: string, 8 | contents: string, 9 | stats: FileStats, 10 | metadata: CachedMetadata 11 | ): Partial { 12 | return parsePage(path, contents, stats, metadata); 13 | } 14 | -------------------------------------------------------------------------------- /src/data-import/web-worker/import-manager.ts: -------------------------------------------------------------------------------- 1 | /** Controls and creates Dataview file importers, allowing for asynchronous loading and parsing of files. */ 2 | 3 | import { Transferable } from "data-model/transferable"; 4 | import DataviewImportWorker from "web-worker:./import-entry.ts"; 5 | import { Component, MetadataCache, TFile, Vault } from "obsidian"; 6 | 7 | /** Callback when a file is resolved. */ 8 | type FileCallback = (p: any) => void; 9 | 10 | /** Multi-threaded file parser which debounces rapid file requests automatically. */ 11 | export class FileImporter extends Component { 12 | /* Background workers which do the actual file parsing. */ 13 | workers: Worker[]; 14 | /** Tracks which workers are actively parsing a file, to make sure we properly delegate results. */ 15 | busy: boolean[]; 16 | 17 | /** List of files which have been queued for a reload. */ 18 | reloadQueue: TFile[]; 19 | /** Fast-access set which holds the list of files queued to be reloaded; used for debouncing. */ 20 | reloadSet: Set; 21 | /** Paths -> promises for file reloads which have not yet been queued. */ 22 | callbacks: Map; 23 | 24 | public constructor(public numWorkers: number, public vault: Vault, public metadataCache: MetadataCache) { 25 | super(); 26 | this.workers = []; 27 | this.busy = []; 28 | 29 | this.reloadQueue = []; 30 | this.reloadSet = new Set(); 31 | this.callbacks = new Map(); 32 | 33 | for (let index = 0; index < numWorkers; index++) { 34 | let worker = new DataviewImportWorker({ name: "Dataview Indexer " + (index + 1) }); 35 | 36 | worker.onmessage = evt => this.finish(evt.data.path, Transferable.value(evt.data.result), index); 37 | this.workers.push(worker); 38 | this.register(() => worker.terminate()); 39 | this.busy.push(false); 40 | } 41 | } 42 | 43 | /** 44 | * Queue the given file for reloading. Multiple reload requests for the same file in a short time period will be de-bounced 45 | * and all be resolved by a single actual file reload. 46 | */ 47 | public reload(file: TFile): Promise { 48 | let promise: Promise = new Promise((resolve, reject) => { 49 | if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]); 50 | else this.callbacks.set(file.path, [[resolve, reject]]); 51 | }); 52 | 53 | // De-bounce repeated requests for the same file. 54 | if (this.reloadSet.has(file.path)) return promise; 55 | this.reloadSet.add(file.path); 56 | 57 | // Immediately run this task if there are available workers; otherwise, add it to the queue. 58 | let workerId = this.nextAvailableWorker(); 59 | if (workerId !== undefined) { 60 | this.send(file, workerId); 61 | } else { 62 | this.reloadQueue.push(file); 63 | } 64 | 65 | return promise; 66 | } 67 | 68 | /** Finish the parsing of a file, potentially queueing a new file. */ 69 | private finish(path: string, data: any, index: number) { 70 | // Cache the callbacks before we do book-keeping. 71 | let calls = ([] as [FileCallback, FileCallback][]).concat(this.callbacks.get(path) ?? []); 72 | 73 | // Book-keeping to clear metadata & allow the file to be re-loaded again. 74 | this.reloadSet.delete(path); 75 | this.callbacks.delete(path); 76 | 77 | // Notify the queue this file is available for new work. 78 | this.busy[index] = false; 79 | 80 | // Queue a new job onto this worker. 81 | let job = this.reloadQueue.shift(); 82 | if (job !== undefined) this.send(job, index); 83 | 84 | // Resolve promises to let users know this file has finished. 85 | if ("$error" in data) { 86 | for (let [_, reject] of calls) reject(data["$error"]); 87 | } else { 88 | for (let [callback, _] of calls) callback(data); 89 | } 90 | } 91 | 92 | /** Send a new task to the given worker ID. */ 93 | private send(file: TFile, workerId: number) { 94 | this.busy[workerId] = true; 95 | 96 | this.vault.cachedRead(file).then(c => 97 | this.workers[workerId].postMessage({ 98 | path: file.path, 99 | contents: c, 100 | stat: file.stat, 101 | metadata: this.metadataCache.getFileCache(file), 102 | }) 103 | ); 104 | } 105 | 106 | /** Find the next available, non-busy worker; return undefined if all workers are busy. */ 107 | private nextAvailableWorker(): number | undefined { 108 | let index = this.busy.indexOf(false); 109 | return index == -1 ? undefined : index; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/data-index/resolver.ts: -------------------------------------------------------------------------------- 1 | /** Collect data matching a source query. */ 2 | 3 | import { FullIndex, PathFilters } from "data-index/index"; 4 | import { Result } from "api/result"; 5 | import { Source } from "./source"; 6 | import { DataObject, Link, Literal } from "../data-model/value"; 7 | 8 | /** A data row which has an ID and associated data (like page link / page data). */ 9 | export type Datarow = { id: Literal; data: T }; 10 | 11 | /** Find source paths which match the given source. */ 12 | export function matchingSourcePaths( 13 | source: Source, 14 | index: FullIndex, 15 | originFile: string = "" 16 | ): Result, string> { 17 | switch (source.type) { 18 | case "empty": 19 | return Result.success(new Set()); 20 | case "tag": 21 | return Result.success(index.tags.getInverse(source.tag)); 22 | case "csv": 23 | return Result.success(new Set([index.prefix.resolveRelative(source.path, originFile)])); 24 | case "folder": 25 | // Prefer loading from the folder at the given path. 26 | if (index.prefix.nodeExists(source.folder)) 27 | return Result.success(index.prefix.get(source.folder, PathFilters.markdown)); 28 | 29 | // But allow for loading individual files if they exist. 30 | if (index.prefix.pathExists(source.folder)) return Result.success(new Set([source.folder])); 31 | else if (index.prefix.pathExists(source.folder + ".md")) 32 | return Result.success(new Set([source.folder + ".md"])); 33 | 34 | // For backwards-compat, return an empty result even if the folder does not exist. 35 | return Result.success(new Set()); 36 | case "link": 37 | let fullPath = index.metadataCache.getFirstLinkpathDest(source.file, originFile)?.path; 38 | if (!fullPath) { 39 | // Look in links which includes unresolved links 40 | return Result.success(index.links.getInverse(source.file)); 41 | } 42 | 43 | if (source.direction === "incoming") { 44 | // To find all incoming links (i.e., things that link to this), use the index that Obsidian provides. 45 | // TODO: Use an actual index so this isn't a fullscan. 46 | let resolved = index.metadataCache.resolvedLinks; 47 | let incoming = new Set(); 48 | 49 | for (let [key, value] of Object.entries(resolved)) { 50 | if (fullPath in value) incoming.add(key); 51 | } 52 | 53 | return Result.success(incoming); 54 | } else { 55 | let resolved = index.metadataCache.resolvedLinks; 56 | if (!(fullPath in resolved)) 57 | return Result.failure(`Could not find file "${source.file}" during link lookup - does it exist?`); 58 | 59 | return Result.success(new Set(Object.keys(index.metadataCache.resolvedLinks[fullPath]))); 60 | } 61 | case "binaryop": 62 | return Result.flatMap2( 63 | matchingSourcePaths(source.left, index, originFile), 64 | matchingSourcePaths(source.right, index, originFile), 65 | (left, right) => { 66 | if (source.op == "&") { 67 | let result = new Set(); 68 | for (let elem of right) { 69 | if (left.has(elem)) result.add(elem); 70 | } 71 | 72 | return Result.success(result); 73 | } else if (source.op == "|") { 74 | let result = new Set(left); 75 | for (let elem of right) result.add(elem); 76 | return Result.success(result); 77 | } else { 78 | return Result.failure(`Unrecognized operator '${source.op}'.`); 79 | } 80 | } 81 | ); 82 | case "negate": 83 | return matchingSourcePaths(source.child, index, originFile).map(child => { 84 | // TODO: This is obviously very inefficient. Can be improved by complicating the 85 | // return type of this function & optimizing 'and' / 'or'. 86 | let allFiles = new Set(index.vault.getMarkdownFiles().map(f => f.path)); 87 | child.forEach(f => allFiles.delete(f)); 88 | return allFiles; 89 | }); 90 | } 91 | } 92 | 93 | /** Convert a path to the data for that path; usually markdown pages, but could also be other file types (like CSV). */ 94 | export async function resolvePathData(path: string, index: FullIndex): Promise[], string>> { 95 | if (PathFilters.csv(path)) return resolveCsvData(path, index); 96 | else return resolveMarkdownData(path, index); 97 | } 98 | 99 | // TODO: We shouldn't be doing path normalization here relative to an origin file, 100 | /** Convert a CSV path to the data in the CSV (in dataview format). */ 101 | export async function resolveCsvData(path: string, index: FullIndex): Promise[], string>> { 102 | let rawData = await index.csv.get(path); 103 | return rawData.map(rows => { 104 | return rows.map((row, index) => { 105 | return { 106 | id: `${path}#${index}`, 107 | data: row, 108 | }; 109 | }); 110 | }); 111 | } 112 | 113 | /** Convert a path pointing to a markdown page, into the associated metadata. */ 114 | export function resolveMarkdownData(path: string, index: FullIndex): Result[], string> { 115 | let page = index.pages.get(path); 116 | if (!page) return Result.success([]); 117 | 118 | return Result.success([ 119 | { 120 | id: Link.file(path), 121 | data: page.serialize(index), 122 | }, 123 | ]); 124 | } 125 | 126 | /** Resolve a source to the collection of data rows that it matches. */ 127 | export async function resolveSource( 128 | source: Source, 129 | index: FullIndex, 130 | originFile: string = "" 131 | ): Promise[], string>> { 132 | let paths = matchingSourcePaths(source, index, originFile); 133 | if (!paths.successful) return Result.failure(paths.error); 134 | 135 | let result = []; 136 | for (let path of paths.value) { 137 | let resolved = await resolvePathData(path, index); 138 | if (!resolved.successful) return resolved; 139 | 140 | for (let val of resolved.value) result.push(val); 141 | } 142 | 143 | return Result.success(result); 144 | } 145 | -------------------------------------------------------------------------------- /src/data-index/source.ts: -------------------------------------------------------------------------------- 1 | /** AST implementation for queries over data sources. */ 2 | 3 | /** The source of files for a query. */ 4 | export type Source = TagSource | CsvSource | FolderSource | LinkSource | EmptySource | NegatedSource | BinaryOpSource; 5 | /** Valid operations for combining sources. */ 6 | export type SourceOp = "&" | "|"; 7 | 8 | /** A tag as a source of data. */ 9 | export interface TagSource { 10 | type: "tag"; 11 | /** The tag to source from. */ 12 | tag: string; 13 | } 14 | 15 | /** A csv as a source of data. */ 16 | export interface CsvSource { 17 | type: "csv"; 18 | /** The path to the CSV file. */ 19 | path: string; 20 | } 21 | 22 | /** A folder prefix as a source of data. */ 23 | export interface FolderSource { 24 | type: "folder"; 25 | /** The folder prefix to source from. */ 26 | folder: string; 27 | } 28 | 29 | /** Either incoming or outgoing links to a given file. */ 30 | export interface LinkSource { 31 | type: "link"; 32 | /** The file to look for links to/from. */ 33 | file: string; 34 | /** 35 | * The direction to look - if incoming, then all files linking to the target file. If outgoing, then all files 36 | * which the file links to. 37 | */ 38 | direction: "incoming" | "outgoing"; 39 | } 40 | 41 | /** A source which is everything EXCEPT the files returned by the given source. */ 42 | export interface NegatedSource { 43 | type: "negate"; 44 | /** The source to negate. */ 45 | child: Source; 46 | } 47 | 48 | /** A source which yields nothing. */ 49 | export interface EmptySource { 50 | type: "empty"; 51 | } 52 | 53 | /** A source made by combining subsources with a logical operators. */ 54 | export interface BinaryOpSource { 55 | type: "binaryop"; 56 | op: SourceOp; 57 | left: Source; 58 | right: Source; 59 | } 60 | 61 | /** Utility functions for creating and manipulating sources. */ 62 | export namespace Sources { 63 | /** Create a source which searches from a tag. */ 64 | export function tag(tag: string): TagSource { 65 | return { type: "tag", tag }; 66 | } 67 | 68 | /** Create a source which fetches from a CSV file. */ 69 | export function csv(path: string): CsvSource { 70 | return { type: "csv", path }; 71 | } 72 | 73 | /** Create a source which searches for files under a folder prefix. */ 74 | export function folder(prefix: string): FolderSource { 75 | return { type: "folder", folder: prefix }; 76 | } 77 | 78 | /** Create a source which searches for files which link to/from a given file. */ 79 | export function link(file: string, incoming: boolean): LinkSource { 80 | return { type: "link", file, direction: incoming ? "incoming" : "outgoing" }; 81 | } 82 | 83 | /** Create a source which joins two sources by a logical operator (and/or). */ 84 | export function binaryOp(left: Source, op: SourceOp, right: Source): Source { 85 | return { type: "binaryop", left, op, right }; 86 | } 87 | 88 | /** Create a source which takes the intersection of two sources. */ 89 | export function and(left: Source, right: Source): Source { 90 | return { type: "binaryop", left, op: "&", right }; 91 | } 92 | 93 | /** Create a source which takes the union of two sources. */ 94 | export function or(left: Source, right: Source): Source { 95 | return { type: "binaryop", left, op: "|", right }; 96 | } 97 | 98 | /** Create a source which negates the underlying source. */ 99 | export function negate(child: Source): NegatedSource { 100 | return { type: "negate", child }; 101 | } 102 | 103 | export function empty(): EmptySource { 104 | return { type: "empty" }; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/data-model/serialized/markdown.ts: -------------------------------------------------------------------------------- 1 | /** Serialized / API facing data types for Dataview objects. */ 2 | 3 | import { Link, Literal } from "data-model/value"; 4 | import { DateTime } from "luxon"; 5 | import { Pos } from "obsidian"; 6 | 7 | export interface SMarkdownPage { 8 | file: { 9 | path: string; 10 | folder: string; 11 | name: string; 12 | link: Link; 13 | outlinks: Link[]; 14 | inlinks: Link[]; 15 | etags: string[]; 16 | tags: string[]; 17 | aliases: string[]; 18 | lists: SListItem[]; 19 | tasks: STask[]; 20 | ctime: DateTime; 21 | cday: DateTime; 22 | mtime: DateTime; 23 | mday: DateTime; 24 | size: number; 25 | ext: string; 26 | starred: boolean; 27 | 28 | day?: DateTime; 29 | }; 30 | 31 | /** Additional fields added by field data. */ 32 | [key: string]: any; 33 | } 34 | 35 | //////////////////////// 36 | // <-- List Items --> // 37 | //////////////////////// 38 | 39 | /** A serialized list item. */ 40 | export type SListItem = SListEntry | STask; 41 | 42 | /** Shared data between list items. */ 43 | export interface SListItemBase { 44 | /** The symbol used to start this list item, like '1.' or '1)' or '*'. */ 45 | symbol: string; 46 | /** A link to the closest thing to this list item (a block, a section, or a file). */ 47 | link: Link; 48 | /** The section that contains this list item. */ 49 | section: Link; 50 | /** The path of the file that contains this item. */ 51 | path: string; 52 | 53 | /** The line this item starts on. */ 54 | line: number; 55 | /** The number of lines this item spans. */ 56 | lineCount: number; 57 | /** The internal Obsidian tracker of the exact position of this line. */ 58 | position: Pos; 59 | /** The line number of the list that this item is part of. */ 60 | list: number; 61 | /** If present, the block ID for this item. */ 62 | blockId?: string; 63 | /** The line number of the parent item to this list, if relevant. */ 64 | parent?: number; 65 | /** The children elements of this list item. */ 66 | children: SListItem[]; 67 | /** Links contained inside this list item. */ 68 | outlinks: Link[]; 69 | 70 | /** The raw text of this item. */ 71 | text: string; 72 | /** 73 | * If present, overrides 'text' when rendered in task views. You should not mutate 'text' since it is used to 74 | * validate a list item when editing it. 75 | */ 76 | visual?: string; 77 | /** Whether this item has any metadata annotations on it. */ 78 | annotated?: boolean; 79 | 80 | /** Any tags present in this task. */ 81 | tags: string[]; 82 | 83 | /** @deprecated use 'children' instead. */ 84 | subtasks: SListItem[]; 85 | /** @deprecated use 'task' instead. */ 86 | real: boolean; 87 | /** @deprecated use 'section' instead. */ 88 | header: Link; 89 | 90 | /** Additional fields added by annotations. */ 91 | [key: string]: any; 92 | } 93 | 94 | /** A serialized list item as seen by users; this is not a task. */ 95 | export interface SListEntry extends SListItemBase { 96 | task: false; 97 | } 98 | 99 | /** A serialized task. */ 100 | export interface STask extends SListItemBase { 101 | task: true; 102 | /** The status of this task, the text between the brackets ('[ ]'). Will be a space if the task is currently unchecked. */ 103 | status: string; 104 | /** Indicates whether the task has any value other than empty space. */ 105 | checked: boolean; 106 | /** Indicates whether the task explicitly has been marked "completed" ('x' or 'X'). */ 107 | completed: boolean; 108 | /** Indicates whether the task and ALL subtasks have been completed. */ 109 | fullyCompleted: boolean; 110 | 111 | /** If present, then the time that this task was created. */ 112 | created?: Literal; 113 | /** If present, then the time that this task was due. */ 114 | due?: Literal; 115 | /** If present, then the time that this task was completed. */ 116 | completion?: Literal; 117 | /** If present, then the day that this task can be started. */ 118 | start?: Literal; 119 | /** If present, then the day that work on this task is scheduled. */ 120 | scheduled?: Literal; 121 | } 122 | -------------------------------------------------------------------------------- /src/data-model/transferable.ts: -------------------------------------------------------------------------------- 1 | import { Link, Values } from "data-model/value"; 2 | import { DateTime, Duration, SystemZone } from "luxon"; 3 | 4 | /** Simplifies passing dataview values across the JS web worker barrier. */ 5 | export namespace Transferable { 6 | /** Convert a literal value to a serializer-friendly transferable value. */ 7 | export function transferable(value: any): any { 8 | // Handle simple universal types first. 9 | if (value instanceof Map) { 10 | let copied = new Map(); 11 | for (let [key, val] of value.entries()) copied.set(transferable(key), transferable(val)); 12 | return copied; 13 | } else if (value instanceof Set) { 14 | let copied = new Set(); 15 | for (let val of value) copied.add(transferable(val)); 16 | return copied; 17 | } 18 | 19 | let wrapped = Values.wrapValue(value); 20 | if (wrapped === undefined) throw Error("Unrecognized transferable value: " + value); 21 | 22 | switch (wrapped.type) { 23 | case "null": 24 | case "number": 25 | case "string": 26 | case "boolean": 27 | return wrapped.value; 28 | case "date": 29 | return { 30 | "___transfer-type": "date", 31 | value: transferable(wrapped.value.toObject()), 32 | options: { 33 | zone: wrapped.value.zone.equals(SystemZone.instance) ? undefined : wrapped.value.zoneName, 34 | }, 35 | }; 36 | case "duration": 37 | return { "___transfer-type": "duration", value: transferable(wrapped.value.toObject()) }; 38 | case "array": 39 | return wrapped.value.map(v => transferable(v)); 40 | case "link": 41 | return { "___transfer-type": "link", value: transferable(wrapped.value.toObject()) }; 42 | case "object": 43 | let result: Record = {}; 44 | for (let [key, value] of Object.entries(wrapped.value)) result[key] = transferable(value); 45 | return result; 46 | } 47 | } 48 | 49 | /** Convert a transferable value back to a literal value we can work with. */ 50 | export function value(transferable: any): any { 51 | if (transferable === null) { 52 | return null; 53 | } else if (transferable === undefined) { 54 | return undefined; 55 | } else if (transferable instanceof Map) { 56 | let real = new Map(); 57 | for (let [key, val] of transferable.entries()) real.set(value(key), value(val)); 58 | return real; 59 | } else if (transferable instanceof Set) { 60 | let real = new Set(); 61 | for (let val of transferable) real.add(value(val)); 62 | return real; 63 | } else if (Array.isArray(transferable)) { 64 | return transferable.map(v => value(v)); 65 | } else if (typeof transferable === "object") { 66 | if ("___transfer-type" in transferable) { 67 | switch (transferable["___transfer-type"]) { 68 | case "date": 69 | let dateOpts = value(transferable.options); 70 | let dateData = value(transferable.value) as any; 71 | 72 | return DateTime.fromObject(dateData, { zone: dateOpts.zone }); 73 | case "duration": 74 | return Duration.fromObject(value(transferable.value)); 75 | case "link": 76 | return Link.fromObject(value(transferable.value)); 77 | default: 78 | throw Error(`Unrecognized transfer type '${transferable["___transfer-type"]}'`); 79 | } 80 | } 81 | 82 | let result: Record = {}; 83 | for (let [key, val] of Object.entries(transferable)) result[key] = value(val); 84 | return result; 85 | } 86 | 87 | return transferable; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/expression/field.ts: -------------------------------------------------------------------------------- 1 | /** Defines the AST for a field which can be evaluated. */ 2 | import { Literal } from "data-model/value"; 3 | 4 | /** Comparison operators which yield true/false. */ 5 | export type CompareOp = ">" | ">=" | "<=" | "<" | "=" | "!="; 6 | /** Arithmetic operators which yield numbers and other values. */ 7 | export type ArithmeticOp = "+" | "-" | "*" | "/" | "%" | "&" | "|"; 8 | /** All valid binary operators. */ 9 | export type BinaryOp = CompareOp | ArithmeticOp; 10 | /** A (potentially computed) field to select or compare against. */ 11 | export type Field = 12 | | BinaryOpField 13 | | VariableField 14 | | LiteralField 15 | | FunctionField 16 | | IndexField 17 | | NegatedField 18 | | LambdaField 19 | | ObjectField 20 | | ListField; 21 | 22 | /** Literal representation of some field type. */ 23 | export interface LiteralField { 24 | type: "literal"; 25 | value: Literal; 26 | } 27 | 28 | /** A variable field for a variable with a given name. */ 29 | export interface VariableField { 30 | type: "variable"; 31 | name: string; 32 | } 33 | 34 | /** A list, which is an ordered collection of fields. */ 35 | export interface ListField { 36 | type: "list"; 37 | values: Field[]; 38 | } 39 | 40 | /** An object, which is a mapping of name to field. */ 41 | export interface ObjectField { 42 | type: "object"; 43 | values: Record; 44 | } 45 | 46 | /** A binary operator field which combines two subnodes somehow. */ 47 | export interface BinaryOpField { 48 | type: "binaryop"; 49 | left: Field; 50 | right: Field; 51 | op: BinaryOp; 52 | } 53 | 54 | /** A function field which calls a function on 0 or more arguments. */ 55 | export interface FunctionField { 56 | type: "function"; 57 | /** Either the name of the function being called, or a Function object. */ 58 | func: Field; 59 | /** The arguments being passed to the function. */ 60 | arguments: Field[]; 61 | } 62 | 63 | export interface LambdaField { 64 | type: "lambda"; 65 | /** An ordered list of named arguments. */ 66 | arguments: string[]; 67 | /** The field which should be evaluated with the arguments in context. */ 68 | value: Field; 69 | } 70 | 71 | /** A field which indexes a variable into another variable. */ 72 | export interface IndexField { 73 | type: "index"; 74 | /** The field to index into. */ 75 | object: Field; 76 | /** The index. */ 77 | index: Field; 78 | } 79 | 80 | /** A field which negates the value of the original field. */ 81 | export interface NegatedField { 82 | type: "negated"; 83 | /** The child field to negated. */ 84 | child: Field; 85 | } 86 | 87 | /** Utility methods for creating & comparing fields. */ 88 | export namespace Fields { 89 | export function variable(name: string): VariableField { 90 | return { type: "variable", name }; 91 | } 92 | 93 | export function literal(value: Literal): LiteralField { 94 | return { type: "literal", value }; 95 | } 96 | 97 | export function binaryOp(left: Field, op: BinaryOp, right: Field): Field { 98 | return { type: "binaryop", left, op, right } as BinaryOpField; 99 | } 100 | 101 | export function index(obj: Field, index: Field): IndexField { 102 | return { type: "index", object: obj, index }; 103 | } 104 | 105 | /** Converts a string in dot-notation-format into a variable which indexes. */ 106 | export function indexVariable(name: string): Field { 107 | let parts = name.split("."); 108 | let result: Field = Fields.variable(parts[0]); 109 | for (let index = 1; index < parts.length; index++) { 110 | result = Fields.index(result, Fields.literal(parts[index])); 111 | } 112 | 113 | return result; 114 | } 115 | 116 | export function lambda(args: string[], value: Field): LambdaField { 117 | return { type: "lambda", arguments: args, value }; 118 | } 119 | 120 | export function func(func: Field, args: Field[]): FunctionField { 121 | return { type: "function", func, arguments: args }; 122 | } 123 | 124 | export function list(values: Field[]): ListField { 125 | return { type: "list", values }; 126 | } 127 | 128 | export function object(values: Record): ObjectField { 129 | return { type: "object", values }; 130 | } 131 | 132 | export function negate(child: Field): NegatedField { 133 | return { type: "negated", child }; 134 | } 135 | 136 | export function isCompareOp(op: BinaryOp): op is CompareOp { 137 | return op == "<=" || op == "<" || op == ">" || op == ">=" || op == "!=" || op == "="; 138 | } 139 | 140 | export const NULL = Fields.literal(null); 141 | } 142 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Basic API type. 2 | export type { DataviewApi } from "api/plugin-api"; 3 | export type { DataviewInlineApi, DataviewInlineIOApi } from "api/inline-api"; 4 | 5 | // Core Dataview types. 6 | export type { DateTime, Duration } from "luxon"; 7 | export type { 8 | Link, 9 | DataObject, 10 | LiteralType, 11 | Literal, 12 | LiteralRepr, 13 | WrappedLiteral, 14 | LiteralWrapper, 15 | Widget, 16 | } from "data-model/value"; 17 | 18 | export type { Result, Success, Failure } from "api/result"; 19 | export type { DataArray } from "api/data-array"; 20 | 21 | // Dataview Index. 22 | export type { ListItem, PageMetadata } from "data-model/markdown"; 23 | export type { FullIndex, PrefixIndex, IndexMap } from "data-index/index"; 24 | 25 | // Serialized types which describe all outputs of serialization. 26 | export type { SMarkdownPage, SListEntry, STask } from "data-model/serialized/markdown"; 27 | 28 | // Useful utilities for directly using dataview parsers. 29 | export { 30 | DURATION_TYPES, 31 | DATE_SHORTHANDS, 32 | KEYWORDS, 33 | ExpressionLanguage, 34 | EXPRESSION, 35 | parseField, 36 | } from "expression/parse"; 37 | export { QUERY_LANGUAGE } from "query/parse"; 38 | export { Query } from "query/query"; 39 | 40 | //////////////////// 41 | // Implementation // 42 | //////////////////// 43 | 44 | import type { DataviewApi } from "api/plugin-api"; 45 | 46 | import "obsidian"; 47 | import type { App } from "obsidian"; 48 | 49 | // Utility functions. 50 | /** 51 | * Get the current Dataview API from the app if provided; if not, it is inferred from the global API object installed 52 | * on the window. 53 | */ 54 | export const getAPI = (app?: App): DataviewApi | undefined => { 55 | if (app) return app.plugins.plugins.dataview?.api; 56 | else return window.DataviewAPI; 57 | }; 58 | 59 | /** Determine if Dataview is enabled in the given application. */ 60 | export const isPluginEnabled = (app: App) => app.plugins.enabledPlugins.has("dataview"); 61 | -------------------------------------------------------------------------------- /src/query/query.ts: -------------------------------------------------------------------------------- 1 | /** Provides an AST for complex queries. */ 2 | import { Source } from "data-index/source"; 3 | import { Field } from "expression/field"; 4 | 5 | /** The supported query types (corresponding to view types). */ 6 | export type QueryType = "list" | "table" | "task" | "calendar"; 7 | 8 | /** A single-line comment. */ 9 | export type Comment = string; 10 | 11 | /** Fields used in the query portion. */ 12 | export interface NamedField { 13 | /** The effective name of this field. */ 14 | name: string; 15 | /** The value of this field. */ 16 | field: Field; 17 | } 18 | 19 | /** A query sort by field, for determining sort order. */ 20 | export interface QuerySortBy { 21 | /** The field to sort on. */ 22 | field: Field; 23 | /** The direction to sort in. */ 24 | direction: "ascending" | "descending"; 25 | } 26 | 27 | /** Utility functions for quickly creating fields. */ 28 | export namespace QueryFields { 29 | export function named(name: string, field: Field): NamedField { 30 | return { name, field } as NamedField; 31 | } 32 | 33 | export function sortBy(field: Field, dir: "ascending" | "descending"): QuerySortBy { 34 | return { field, direction: dir }; 35 | } 36 | } 37 | 38 | ////////////////////// 39 | // Query Definition // 40 | ////////////////////// 41 | 42 | /** A query which should render a list of elements. */ 43 | export interface ListQuery { 44 | type: "list"; 45 | /** What should be rendered in the list. */ 46 | format?: Field; 47 | /** If true, show the default DI field; otherwise, don't. */ 48 | showId: boolean; 49 | } 50 | 51 | /** A query which renders a table of elements. */ 52 | export interface TableQuery { 53 | type: "table"; 54 | /** The fields (computed or otherwise) to select. */ 55 | fields: NamedField[]; 56 | /** If true, show the default ID field; otherwise, don't. */ 57 | showId: boolean; 58 | } 59 | 60 | /** A query which renders a collection of tasks. */ 61 | export interface TaskQuery { 62 | type: "task"; 63 | } 64 | 65 | /** A query which renders a collection of notes in a calendar view. */ 66 | export interface CalendarQuery { 67 | type: "calendar"; 68 | /** The date field that we'll be grouping notes by for the calendar view */ 69 | field: NamedField; 70 | } 71 | 72 | export type QueryHeader = ListQuery | TableQuery | TaskQuery | CalendarQuery; 73 | 74 | /** A step which only retains rows whose 'clause' field is truthy. */ 75 | export interface WhereStep { 76 | type: "where"; 77 | clause: Field; 78 | } 79 | 80 | /** A step which sorts all current rows by the given list of sorts. */ 81 | export interface SortByStep { 82 | type: "sort"; 83 | fields: QuerySortBy[]; 84 | } 85 | 86 | /** A step which truncates the number of rows to the given amount. */ 87 | export interface LimitStep { 88 | type: "limit"; 89 | amount: Field; 90 | } 91 | 92 | /** A step which flattens rows into multiple child rows. */ 93 | export interface FlattenStep { 94 | type: "flatten"; 95 | field: NamedField; 96 | } 97 | 98 | /** A step which groups rows into groups by the given field. */ 99 | export interface GroupStep { 100 | type: "group"; 101 | field: NamedField; 102 | } 103 | 104 | /** A virtual step which extracts an array of values from each row. */ 105 | export interface ExtractStep { 106 | type: "extract"; 107 | fields: Record; 108 | } 109 | 110 | export type QueryOperation = WhereStep | SortByStep | LimitStep | FlattenStep | GroupStep | ExtractStep; 111 | 112 | /** 113 | * A query over the Obsidian database. Queries have a specific and deterministic execution order: 114 | */ 115 | export interface Query { 116 | /** The view type to render this query in. */ 117 | header: QueryHeader; 118 | /** The source that file candidates will come from. */ 119 | source: Source; 120 | /** The operations to apply to the data to produce the final result that will be rendered. */ 121 | operations: QueryOperation[]; 122 | } 123 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | //////////////////// 2 | // Query Settings // 3 | //////////////////// 4 | 5 | export interface QuerySettings { 6 | /** What to render 'null' as in tables. Defaults to '-'. */ 7 | renderNullAs: string; 8 | /** If enabled, tasks in Dataview views will automatically have their completion date appended when they are checked. */ 9 | taskCompletionTracking: boolean; 10 | /** If enabled, automatic completions will use emoji shorthand ✅ YYYY-MM-DD instead of [completion:: date]. */ 11 | taskCompletionUseEmojiShorthand: boolean; 12 | /** The name of the inline field to be added as a task's completion when checked. Only used if completionTracking is enabled and emojiShorthand is not. */ 13 | taskCompletionText: string; 14 | /** Date format of the task's completion timestamp. Only used if completionTracking is enabled and emojiShorthand is not. */ 15 | taskCompletionDateFormat: string; 16 | /** Whether or not subtasks should be recursively completed in addition to their parent task. */ 17 | recursiveSubTaskCompletion: boolean; 18 | /** If true, render a modal which shows no results were returned. */ 19 | warnOnEmptyResult: boolean; 20 | /** Whether or not automatic view refreshing is enabled. */ 21 | refreshEnabled: boolean; 22 | /** The interval that views are refreshed, by default. */ 23 | refreshInterval: number; 24 | /** The default format that dates are rendered in (using luxon's moment-like formatting). */ 25 | defaultDateFormat: string; 26 | /** The default format that date-times are rendered in (using luxon's moment-like formatting). */ 27 | defaultDateTimeFormat: string; 28 | /** Maximum depth that objects will be expanded when being rendered recursively. */ 29 | maxRecursiveRenderDepth: number; 30 | /** The name of the default ID field ('File'). */ 31 | tableIdColumnName: string; 32 | /** The name of default ID fields on grouped data ('Group'). */ 33 | tableGroupColumnName: string; 34 | /** Include the result count as part of the output. */ 35 | showResultCount: boolean; 36 | } 37 | 38 | export const DEFAULT_QUERY_SETTINGS: QuerySettings = { 39 | renderNullAs: "\\-", 40 | taskCompletionTracking: false, 41 | taskCompletionUseEmojiShorthand: false, 42 | taskCompletionText: "completion", 43 | taskCompletionDateFormat: "yyyy-MM-dd", 44 | recursiveSubTaskCompletion: false, 45 | warnOnEmptyResult: true, 46 | refreshEnabled: true, 47 | refreshInterval: 2500, 48 | defaultDateFormat: "MMMM dd, yyyy", 49 | defaultDateTimeFormat: "h:mm a - MMMM dd, yyyy", 50 | maxRecursiveRenderDepth: 4, 51 | 52 | tableIdColumnName: "File", 53 | tableGroupColumnName: "Group", 54 | showResultCount: true, 55 | }; 56 | 57 | ///////////////////// 58 | // Export Settings // 59 | ///////////////////// 60 | 61 | export interface ExportSettings { 62 | /** Whether or not HTML should be used for formatting in exports. */ 63 | allowHtml: boolean; 64 | } 65 | 66 | export const DEFAULT_EXPORT_SETTINGS: ExportSettings = { 67 | allowHtml: true, 68 | }; 69 | 70 | /////////////////////////////// 71 | // General Dataview Settings // 72 | /////////////////////////////// 73 | 74 | export interface DataviewSettings extends QuerySettings, ExportSettings { 75 | /** The prefix for inline queries by default. */ 76 | inlineQueryPrefix: string; 77 | /** The prefix for inline JS queries by default. */ 78 | inlineJsQueryPrefix: string; 79 | /** If true, inline queries are also evaluated in full codeblocks. */ 80 | inlineQueriesInCodeblocks: boolean; 81 | /** Enable or disable executing DataviewJS queries. */ 82 | enableDataviewJs: boolean; 83 | /** Enable or disable regular inline queries. */ 84 | enableInlineDataview: boolean; 85 | /** Enable or disable executing inline DataviewJS queries. */ 86 | enableInlineDataviewJs: boolean; 87 | /** Enable or disable rendering inline fields prettily in Reading View. */ 88 | prettyRenderInlineFields: boolean; 89 | /** Enable or disable rendering inline fields prettily in Live Preview. */ 90 | prettyRenderInlineFieldsInLivePreview: boolean; 91 | /** The keyword for DataviewJS blocks. */ 92 | dataviewJsKeyword: string; 93 | } 94 | 95 | /** Default settings for dataview on install. */ 96 | export const DEFAULT_SETTINGS: DataviewSettings = { 97 | ...DEFAULT_QUERY_SETTINGS, 98 | ...DEFAULT_EXPORT_SETTINGS, 99 | ...{ 100 | inlineQueryPrefix: "=", 101 | inlineJsQueryPrefix: "$=", 102 | inlineQueriesInCodeblocks: true, 103 | enableInlineDataview: true, 104 | enableDataviewJs: false, 105 | enableInlineDataviewJs: false, 106 | prettyRenderInlineFields: true, 107 | prettyRenderInlineFieldsInLivePreview: true, 108 | dataviewJsKeyword: "dataviewjs", 109 | }, 110 | }; 111 | -------------------------------------------------------------------------------- /src/test/common.ts: -------------------------------------------------------------------------------- 1 | import { Literal } from "data-model/value"; 2 | import { Context, LinkHandler } from "expression/context"; 3 | import { EXPRESSION } from "expression/parse"; 4 | import { DEFAULT_QUERY_SETTINGS } from "settings"; 5 | 6 | /** Expect that the given dataview expression resolves to the given value. */ 7 | export function expectEvals(text: string, result: Literal) { 8 | expect(parseEval(text)).toEqual(result); 9 | } 10 | 11 | /** Parse a field expression and evaluate it in the simple context. */ 12 | export function parseEval(text: string): Literal { 13 | let field = EXPRESSION.field.tryParse(text); 14 | return simpleContext().tryEvaluate(field); 15 | } 16 | 17 | /** Create a trivial link handler which never resolves links. */ 18 | export function simpleLinkHandler(): LinkHandler { 19 | return { 20 | resolve: path => null, 21 | normalize: path => path, 22 | exists: path => true, 23 | }; 24 | } 25 | 26 | /** Create a trivial context good for evaluations that do not depend on links. */ 27 | export function simpleContext(): Context { 28 | return new Context(simpleLinkHandler(), DEFAULT_QUERY_SETTINGS); 29 | } 30 | -------------------------------------------------------------------------------- /src/test/data/index-map.test.ts: -------------------------------------------------------------------------------- 1 | import { IndexMap } from "data-index/index"; 2 | 3 | test("Simple Set/Get", () => { 4 | let index = new IndexMap(); 5 | index.set("test", new Set(["one", "two"])); 6 | index.set("test2", new Set(["two"])); 7 | 8 | expect(index.get("test")).toEqual(new Set(["one", "two"])); 9 | expect(index.get("test2")).toEqual(new Set(["two"])); 10 | }); 11 | 12 | test("Inverted Get", () => { 13 | let index = new IndexMap(); 14 | index.set("test", new Set(["a", "b", "c"])); 15 | index.set("test2", new Set(["a", "c"])); 16 | index.set("test3", new Set([])); 17 | 18 | expect(index.getInverse("a")).toEqual(new Set(["test", "test2"])); 19 | expect(index.getInverse("b")).toEqual(new Set(["test"])); 20 | expect(index.getInverse("")).toEqual(new Set()); 21 | 22 | index.set("test", new Set(["a", "c"])); 23 | expect(index.getInverse("b")).toEqual(new Set([])); 24 | expect(index.getInverse("a")).toEqual(new Set(["test", "test2"])); 25 | expect(index.getInverse("c")).toEqual(new Set(["test", "test2"])); 26 | 27 | index.set("test", new Set([])); 28 | expect(index.getInverse("a")).toEqual(new Set(["test2"])); 29 | expect(index.getInverse("c")).toEqual(new Set(["test2"])); 30 | }); 31 | -------------------------------------------------------------------------------- /src/test/data/transferable.test.ts: -------------------------------------------------------------------------------- 1 | import { Transferable } from "data-model/transferable"; 2 | import { Link } from "data-model/value"; 3 | import { DateTime, Duration } from "luxon"; 4 | 5 | describe("Literals", () => { 6 | test("String", () => checkRoundTrip("hello")); 7 | test("Number", () => checkRoundTrip(18)); 8 | test("Boolean", () => checkRoundTrip(true)); 9 | test("Null", () => checkRoundTrip(null)); 10 | }); 11 | 12 | test("Date", () => expect(roundTrip(DateTime.fromObject({ year: 1982, month: 5, day: 25 })).day).toEqual(25)); 13 | test("Duration", () => expect(roundTrip(Duration.fromMillis(10000)).toMillis()).toEqual(10000)); 14 | test("Link", () => expect(roundTrip(Link.file("hello"))).toEqual(Link.file("hello"))); 15 | 16 | test("Full Date", () => { 17 | let date = DateTime.fromObject({ year: 1982, month: 5, day: 19 }, { zone: "UTC+8" }); 18 | expect(roundTrip(date).equals(date)).toBeTruthy(); 19 | }); 20 | 21 | /** Run a value through the transferable converter and back again. */ 22 | function roundTrip(value: T): T { 23 | return Transferable.value(Transferable.transferable(value)); 24 | } 25 | 26 | function checkRoundTrip(value: any) { 27 | expect(roundTrip(value)).toEqual(value); 28 | } 29 | -------------------------------------------------------------------------------- /src/test/data/values.test.ts: -------------------------------------------------------------------------------- 1 | import { Values, Link } from "data-model/value"; 2 | 3 | describe("Links", () => { 4 | describe("Comparisons", () => { 5 | test("Same File", () => expect(Link.file("test").equals(Link.file("test"))).toBeTruthy()); 6 | test("Different File", () => expect(Link.file("test").equals(Link.file("test2"))).toBeFalsy()); 7 | test("Different Subpath", () => expect(Link.file("test").equals(Link.header("test", "Hello"))).toBeFalsy()); 8 | test("Different Subpath Type", () => 9 | expect(Link.header("test", "abc").equals(Link.block("test", "abc"))).toBeFalsy()); 10 | }); 11 | 12 | describe("General Comparisons", () => { 13 | test("Same File", () => expect(Values.compareValue(Link.file("test"), Link.file("test"))).toBe(0)); 14 | test("Different File", () => 15 | expect(Values.compareValue(Link.file("test"), Link.file("test2"))).toBeLessThan(0)); 16 | test("Different Subpath", () => 17 | expect(Values.compareValue(Link.file("test"), Link.header("test", "Hello"))).toBeLessThan(0)); 18 | test("Different Subpath Type", () => 19 | expect(Values.compareValue(Link.header("test", "abc"), Link.block("test", "abc"))).toBeTruthy()); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/test/function/aggregation.test.ts: -------------------------------------------------------------------------------- 1 | import { expectEvals } from "test/common"; 2 | 3 | describe("map()", () => { 4 | test("empty list", () => expectEvals("map([], (k) => 6)", [])); 5 | test("number list", () => expectEvals("map([1, 2, 3], (k) => k + 4)", [5, 6, 7])); 6 | test("string list", () => expectEvals('map(["a", "be", "ced"], (k) => length(k))', [1, 2, 3])); 7 | }); 8 | 9 | describe("filter()", () => { 10 | test("empty list", () => expectEvals("filter(list(), (k) => true)", [])); 11 | test("number list", () => expectEvals("filter(list(1, 2, 3), (k) => k >= 2)", [2, 3])); 12 | }); 13 | 14 | describe("unique()", () => { 15 | test("empty", () => expectEvals("unique([])", [])); 16 | test("single", () => expectEvals("unique([1])", [1])); 17 | test("multiple unique", () => expectEvals("unique([1, 1, 1])", [1])); 18 | test("multiple same", () => expectEvals("unique([1, 3, 7, 3, 1])", [1, 3, 7])); 19 | }); 20 | 21 | describe("min()", () => { 22 | test("empty", () => expectEvals("min()", null)); 23 | test("single", () => expectEvals("min(6)", 6)); 24 | test("multiple", () => expectEvals("min(6, 9, 12)", 6)); 25 | test("list empty", () => expectEvals("min([])", null)); 26 | test("list multiple", () => expectEvals("min([1, 2, 3])", 1)); 27 | }); 28 | 29 | describe("minby()", () => { 30 | test("empty", () => expectEvals("minby([], (k) => k)", null)); 31 | test("single", () => expectEvals("minby([1], (k) => k)", 1)); 32 | test("multiple", () => expectEvals("minby([1, 2, 3], (k) => 0 - k)", 3)); 33 | }); 34 | 35 | describe("max()", () => { 36 | test("empty", () => expectEvals("max()", null)); 37 | test("single", () => expectEvals("max(6)", 6)); 38 | test("multiple", () => expectEvals("max(6, 9, 12)", 12)); 39 | test("list empty", () => expectEvals("max([])", null)); 40 | test("list multiple", () => expectEvals("max([1, 2, 3])", 3)); 41 | }); 42 | 43 | describe("maxby()", () => { 44 | test("empty", () => expectEvals("maxby([], (k) => k)", null)); 45 | test("single", () => expectEvals("maxby([1], (k) => k)", 1)); 46 | test("multiple", () => expectEvals("maxby([1, 2, 3], (k) => 0 - k)", 1)); 47 | }); 48 | 49 | describe("sum()", () => { 50 | test("number list", () => expectEvals("sum([2, 3, 1])", 6)); 51 | test("string list", () => expectEvals('sum(["a", "b", "c"])', "abc")); 52 | test("empty list", () => expectEvals("sum([])", null)); 53 | }); 54 | 55 | describe("average()", () => { 56 | test("number list", () => expectEvals("average([2, 3, 1])", 2)); 57 | test("number list", () => expectEvals("average(nonnull([2, 3, null, 1]))", 2)); 58 | test("empty list", () => expectEvals("average([])", null)); 59 | }); 60 | 61 | describe("any()", () => { 62 | test("true, false", () => expectEvals("any(true, false)", true)); 63 | test("[true, false]", () => expectEvals("any(list(true, false))", true)); 64 | }); 65 | 66 | describe("all()", () => { 67 | test("true, false", () => expectEvals("all(true, false)", false)); 68 | test("true, [false]", () => expectEvals("all(true, list(false))", true)); 69 | test("[true, false]", () => expectEvals("all(list(true, false))", false)); 70 | 71 | test("vectorized", () => { 72 | expectEvals('all(regexmatch("a+", list("a", "aaaa")))', true); 73 | expectEvals('all(regexmatch("a+", list("a", "aaab")))', false); 74 | expectEvals('any(regexmatch("a+", list("a", "aaab")))', true); 75 | 76 | expectEvals('all(regextest("a+", list("a", "aaaa")))', true); 77 | expectEvals('all(regextest("a+", list("a", "aaab")))', true); 78 | expectEvals('any(regextest("a+", list("a", "aaab")))', true); 79 | }); 80 | }); 81 | 82 | describe("nonnull()", () => { 83 | test("empty", () => expectEvals("nonnull([])", [])); 84 | test("[null, false]", () => expectEvals("nonnull([null, false])", [false])); 85 | }); 86 | 87 | describe("firstvalue()", () => { 88 | test("empty", () => expectEvals("firstvalue([])", null)); 89 | test("null", () => expectEvals("firstvalue(null)", null)); 90 | test("[1, 2, 3]", () => expectEvals("firstvalue([1, 2, 3])", 1)); 91 | test("[null, 1, 2]", () => expectEvals("firstvalue([null, 1, 2])", 1)); 92 | }); 93 | -------------------------------------------------------------------------------- /src/test/function/coerce.test.ts: -------------------------------------------------------------------------------- 1 | import { expectEvals, parseEval } from "test/common"; 2 | 3 | test("number()", () => { 4 | expect(parseEval('number("hmm")')).toEqual(null); 5 | expect(parseEval("number(34)")).toEqual(34); 6 | expect(parseEval('number("34")')).toEqual(34); 7 | expect(parseEval('number("17 years")')).toEqual(17); 8 | expect(parseEval('number("-19")')).toEqual(-19); 9 | }); 10 | 11 | describe("string()", () => { 12 | test("number", () => expect(parseEval(`string(18)`)).toEqual("18")); 13 | }); 14 | 15 | test("list()", () => { 16 | expectEvals("list(1, 2, 3)", [1, 2, 3]); 17 | expectEvals("list()", []); 18 | }); 19 | 20 | test("object()", () => { 21 | expect(parseEval("object()")).toEqual({}); 22 | expect(parseEval('object("hello", 1)')).toEqual({ hello: 1 }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/test/function/constructors.test.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from "luxon"; 2 | import { parseEval } from "test/common"; 3 | 4 | describe("dur()", () => { 5 | test("8 minutes", () => expect(parseEval(`dur("8 minutes")`)).toEqual(Duration.fromObject({ minutes: 8 }))); 6 | test("3 hrs", () => expect(parseEval(`dur("3 hrs")`)).toEqual(Duration.fromObject({ hours: 3 }))); 7 | test("2 days, 6 minutes", () => 8 | expect(parseEval(`dur("2 days, 6 minutes")`)).toEqual(Duration.fromObject({ days: 2, minutes: 6 }))); 9 | }); 10 | 11 | describe("typeof()", () => { 12 | test("string", () => expect(parseEval(`typeof("nice")`)).toEqual("string")); 13 | test("object", () => expect(parseEval(`typeof({a: 1, b: 2})`)).toEqual("object")); 14 | test("array", () => expect(parseEval(`typeof(["nice"])`)).toEqual("array")); 15 | test("number", () => expect(parseEval(`typeof(18.4)`)).toEqual("number")); 16 | }); 17 | -------------------------------------------------------------------------------- /src/test/function/eval.test.ts: -------------------------------------------------------------------------------- 1 | /** Various tests for evaluating fields in context. */ 2 | 3 | import { EXPRESSION } from "expression/parse"; 4 | import { Context, LinkHandler } from "expression/context"; 5 | import { Duration } from "luxon"; 6 | import { Fields } from "expression/field"; 7 | import { Literal, Link } from "data-model/value"; 8 | import { DEFAULT_QUERY_SETTINGS } from "settings"; 9 | 10 | // <-- Numeric Operations --> 11 | 12 | test("Evaluate simple numeric operations", () => { 13 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(2), "+", Fields.literal(4)))).toEqual(6); 14 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(2), "-", Fields.literal(4)))).toEqual(-2); 15 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(2), "*", Fields.literal(4)))).toEqual(8); 16 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(8), "/", Fields.literal(4)))).toEqual(2); 17 | }); 18 | 19 | test("Evaluate numeric comparisons", () => { 20 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(8), "<", Fields.literal(4)))).toEqual(false); 21 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(-2), "=", Fields.literal(-2)))).toEqual(true); 22 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(-2), ">=", Fields.literal(-8)))).toEqual(true); 23 | }); 24 | 25 | test("Evaluate complex numeric operations", () => { 26 | expect(parseEval("12 + 8 - 4 / 2")).toEqual(18); 27 | expect(parseEval("16 / 8 / 2")).toEqual(1); 28 | expect(parseEval("39 / 3 <= 14")).toEqual(true); 29 | }); 30 | 31 | // <-- String Operations --> 32 | 33 | test("Evaluate simple string operations", () => { 34 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal("a"), "+", Fields.literal("b")))).toEqual("ab"); 35 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal("a"), "+", Fields.literal(12)))).toEqual("a12"); 36 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal("a"), "*", Fields.literal(6)))).toEqual("aaaaaa"); 37 | }); 38 | 39 | test("Evaluate string comparisons", () => { 40 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal("abc"), "<", Fields.literal("abd")))).toEqual( 41 | true 42 | ); 43 | expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal("xyz"), "=", Fields.literal("xyz")))).toEqual( 44 | true 45 | ); 46 | }); 47 | 48 | // <-- Date Operations --> 49 | 50 | test("Evaluate date comparisons", () => { 51 | expect(parseEval("date(2021-01-14) = date(2021-01-14)")).toEqual(true); 52 | expect(parseEval("contains(list(date(2020-01-01)), date(2020-01-01))")).toEqual(true); 53 | }); 54 | 55 | test("Evaluate date subtraction", () => { 56 | let duration = parseEval("date(2021-05-04) - date(1997-05-17)") as Duration; 57 | expect(duration.years).toEqual(23); 58 | }); 59 | 60 | // <-- Field resolution --> 61 | 62 | test("Evaluate simple field resolution", () => { 63 | let context = simpleContext().set("a", 18).set("b", "hello"); 64 | expect(context.get("a")).toEqual(18); 65 | expect(context.get("b")).toEqual("hello"); 66 | expect(context.get("c")).toEqual(null); 67 | }); 68 | 69 | test("Evaluate simple object resolution", () => { 70 | let object = { inner: { final: 6 } }; 71 | let context = simpleContext().set("obj", object); 72 | 73 | expect(context.tryEvaluate(Fields.indexVariable("obj.inner"))).toEqual(object.inner); 74 | expect(context.tryEvaluate(Fields.indexVariable("obj.inner.final"))).toEqual(object.inner.final); 75 | }); 76 | 77 | test("Evaluate simple link resolution", () => { 78 | let object = { inner: { final: 6 } }; 79 | let context = new Context( 80 | { resolve: path => object, normalize: path => path, exists: path => false }, 81 | DEFAULT_QUERY_SETTINGS 82 | ).set("link", Link.file("test", false)); 83 | expect(context.tryEvaluate(Fields.indexVariable("link.inner"))).toEqual(object.inner); 84 | expect(context.tryEvaluate(Fields.indexVariable("link.inner.final"))).toEqual(object.inner.final); 85 | }); 86 | 87 | describe("Immediately Invoked Lambdas", () => { 88 | test("Addition", () => expect(parseEval("((a, b) => a + b)(1, 2)")).toEqual(3)); 89 | test("Negation", () => expect(parseEval("((v) => 0-v)(6)")).toEqual(-6)); 90 | test("Curried", () => expect(parseEval("((a) => (b) => a + b)(1)(2)")).toEqual(3)); 91 | test("In Argument", () => expect(parseEval("((a) => 1 + a)(((a) => 2)(3))")).toEqual(3)); 92 | }); 93 | 94 | describe("Immediately Indexed Objects", () => { 95 | test("Empty", () => expect(parseEval('{ a: 1, b: 2 }["c"]')).toEqual(null)); 96 | test("Single", () => expect(parseEval('{ a: 1, b: 2 }["a"]')).toEqual(1)); 97 | test("Nested", () => expect(parseEval('{ a: 1, b: { c: 4 } }["b"]["c"]')).toEqual(4)); 98 | }); 99 | 100 | /** Parse a field expression and evaluate it in the simple context. */ 101 | function parseEval(text: string): Literal { 102 | let field = EXPRESSION.field.tryParse(text); 103 | return simpleContext().tryEvaluate(field); 104 | } 105 | 106 | /** Create a trivial link handler which never resolves links. */ 107 | function simpleLinkHandler(): LinkHandler { 108 | return { 109 | resolve: path => null, 110 | normalize: path => path, 111 | exists: path => true, 112 | }; 113 | } 114 | 115 | /** Create a trivial context good for evaluations that do not depend on links. */ 116 | function simpleContext(): Context { 117 | return new Context(simpleLinkHandler(), DEFAULT_QUERY_SETTINGS); 118 | } 119 | -------------------------------------------------------------------------------- /src/test/function/meta.test.ts: -------------------------------------------------------------------------------- 1 | import { parseEval } from "test/common"; 2 | 3 | test("Evaluate meta(link).display", () => { 4 | expect(parseEval(`meta([[2021-11-01|Displayed link text]]).display`)).toEqual("Displayed link text"); 5 | expect(parseEval(`meta([[2021-11-01]]).display`)).toBeNull; 6 | }); 7 | 8 | test("Evaluate meta(link).path", () => { 9 | expect(parseEval(`meta([[My Project#Next Actions]]).path`)).toEqual("My Project"); 10 | expect(parseEval(`meta([[My Project#^9bcbe8]]).path`)).toEqual("My Project"); 11 | expect(parseEval(`meta([[My Project]]).path`)).toEqual("My Project"); 12 | }); 13 | 14 | test("Evaluate meta(link).subpath", () => { 15 | expect(parseEval(`meta([[My Project#Next Actions]]).subpath`)).toEqual("Next Actions"); 16 | expect(parseEval(`meta([[My Project#^9bcbe8]]).subpath`)).toEqual("9bcbe8"); 17 | expect(parseEval(`meta([[My Project]]).subpath`)).toBeNull; 18 | }); 19 | 20 | test("Evaluate meta(link).type", () => { 21 | expect(parseEval(`meta([[My Project]]).type`)).toEqual("file"); 22 | expect(parseEval(`meta([[My Project#Next Actions]]).type`)).toEqual("header"); 23 | expect(parseEval(`meta([[My Project#^9bcbe8]]).type`)).toEqual("block"); 24 | }); 25 | -------------------------------------------------------------------------------- /src/test/function/vectorization.test.ts: -------------------------------------------------------------------------------- 1 | import { Literal } from "data-model/value"; 2 | import { parseEval } from "test/common"; 3 | 4 | describe("Single List Argument", () => { 5 | test("replace(list, string, string)", () => check('replace(list("yes", "re"), "e", "a")', ["yas", "ra"])); 6 | 7 | test("lower(list)", () => check('lower(["YES", "nO"])', ["yes", "no"])); 8 | test("upper(list)", () => check('upper(["okay", "yep", "1"])', ["OKAY", "YEP", "1"])); 9 | }); 10 | 11 | describe("Multi-List Arguments", () => { 12 | test("replace(list, list, string)", () => check('replace(["a", "b", "c"], ["a", "b", "c"], "d")', ["d", "d", "d"])); 13 | test("replace(list, string, list)", () => check('replace(["a", "b", "c"], "a", ["d", "e", "f"])', ["d", "b", "c"])); 14 | test("replace(list, list, list)", () => 15 | check('replace(["a", "b", "c"], ["a", "b", "c"], ["x", "y", "z"])', ["x", "y", "z"])); 16 | }); 17 | 18 | function check(statement: string, result: Literal) { 19 | expect(parseEval(statement)).toEqual(result); 20 | } 21 | -------------------------------------------------------------------------------- /src/test/markdown/parse.file.test.ts: -------------------------------------------------------------------------------- 1 | import { extractTags } from "data-import/markdown-file"; 2 | import * as common from "data-import/common"; 3 | import { FrontMatterCache } from "obsidian"; 4 | 5 | describe("Frontmatter Tags", () => { 6 | test("Empty", () => expect(extractTags({} as FrontMatterCache)).toEqual([])); 7 | test("No Tags", () => expect(extractTags({ a: 1, b: 2 } as any as FrontMatterCache)).toEqual([])); 8 | test("One Tag", () => expect(extractTags({ tag: "hello" } as any as FrontMatterCache)).toEqual(["#hello"])); 9 | test("Two Tag", () => 10 | expect(extractTags({ tag: ["hello", "goodbye"] } as any as FrontMatterCache)).toEqual(["#hello", "#goodbye"])); 11 | test("Two Tag String", () => 12 | expect(extractTags({ tag: "hello goodbye" } as any as FrontMatterCache)).toEqual(["#hello", "#goodbye"])); 13 | test("Two Tag String Comma", () => 14 | expect(extractTags({ tag: "hello, goodbye" } as any as FrontMatterCache)).toEqual(["#hello", "#goodbye"])); 15 | }); 16 | 17 | describe("Task Tags", () => { 18 | test("Empty", () => expect(common.extractTags("hello")).toEqual(new Set([]))); 19 | test("One Tag", () => expect(common.extractTags("and text #hello")).toEqual(new Set(["#hello"]))); 20 | test("Two Tags", () => 21 | expect(common.extractTags("#and/thing text #hello")).toEqual(new Set(["#and/thing", "#hello"]))); 22 | test("Comma Delimited", () => 23 | expect(common.extractTags("#one,#two, #three")).toEqual(new Set(["#one", "#two", "#three"]))); 24 | test("Semicolon Delimited", () => 25 | expect(common.extractTags("#one;;;#two; #three")).toEqual(new Set(["#one", "#two", "#three"]))); 26 | test("Parenthesis", () => 27 | expect(common.extractTags("[#one]]#two;;#four()() #three")).toEqual( 28 | new Set(["#one", "#two", "#four", "#three"]) 29 | )); 30 | }); 31 | -------------------------------------------------------------------------------- /src/test/util/normalize.test.ts: -------------------------------------------------------------------------------- 1 | import { canonicalizeVarName, normalizeHeaderForLink } from "util/normalize"; 2 | 3 | describe("Header Normalization", () => { 4 | test("Link", () => expect(normalizeHeaderForLink("Header [[Outer Wilds]] ")).toEqual("Header Outer Wilds")); 5 | test("Dash", () => expect(normalizeHeaderForLink("Header - More")).toEqual("Header - More")); 6 | test("Underscore", () => expect(normalizeHeaderForLink("Header _ More _")).toEqual("Header _ More _")); 7 | test("Link with Display", () => 8 | expect(normalizeHeaderForLink("Header [[Outer Wilds|Thing]] ")).toEqual("Header Outer Wilds Thing")); 9 | test("Markup", () => expect(normalizeHeaderForLink("**Header** *Value")).toEqual("Header Value")); 10 | test("Emoji", () => 11 | expect(normalizeHeaderForLink("Header 📷 [[Outer Wilds]] ")).toEqual("Header 📷 Outer Wilds")); 12 | }); 13 | 14 | describe("Variable Canonicalization", () => { 15 | test("Idempotent", () => expect(canonicalizeVarName("test")).toEqual("test")); 16 | test("Idempotent 2", () => expect(canonicalizeVarName("property")).toEqual("property")); 17 | test("Space", () => expect(canonicalizeVarName("test thing")).toEqual("test-thing")); 18 | test("Multiple Space", () => expect(canonicalizeVarName("This is test")).toEqual("this-is-test")); 19 | test("Number", () => expect(canonicalizeVarName("test thing 3")).toEqual("test-thing-3")); 20 | test("Punctuation", () => expect(canonicalizeVarName("This is a Test.")).toEqual("this-is-a-test")); 21 | test("Dash", () => expect(canonicalizeVarName("Yes-sir")).toEqual("yes-sir")); 22 | test("Emoji", () => expect(canonicalizeVarName("📷")).toEqual("📷")); 23 | test("Статус", () => expect(canonicalizeVarName("Статус")).toEqual("статус")); 24 | }); 25 | -------------------------------------------------------------------------------- /src/test/util/paths.test.ts: -------------------------------------------------------------------------------- 1 | import { getFileTitle, getParentFolder } from "util/normalize"; 2 | 3 | describe("getFileTitle()", () => { 4 | test("empty", () => expect(getFileTitle("")).toEqual("")); 5 | 6 | test("root getFileTitle()", () => { 7 | expect(getFileTitle("yes.md")).toEqual("yes"); 8 | expect(getFileTitle("yes")).toEqual("yes"); 9 | }); 10 | 11 | test("folder getFileTitle()", () => { 12 | expect(getFileTitle("ok/yes.md")).toEqual("yes"); 13 | expect(getFileTitle("/yes")).toEqual("yes"); 14 | }); 15 | }); 16 | 17 | test("empty getParentFolder()", () => { 18 | expect(getParentFolder("")).toEqual(""); 19 | }); 20 | 21 | test("root getParentFolder()", () => { 22 | expect(getParentFolder("yes")).toEqual(""); 23 | expect(getParentFolder("maybe")).toEqual(""); 24 | }); 25 | 26 | test("folder getParentFolder()", () => { 27 | expect(getParentFolder("ok/yes")).toEqual("ok"); 28 | expect(getParentFolder("no/maybe")).toEqual("no"); 29 | expect(getParentFolder("/maybe")).toEqual(""); 30 | }); 31 | 32 | test("nested folder getParentFolder()", () => { 33 | expect(getParentFolder("a/b/c.md")).toEqual("a/b"); 34 | expect(getParentFolder("hello/yes/no/maybe.md")).toEqual("hello/yes/no"); 35 | expect(getParentFolder("hello/yes/no/")).toEqual("hello/yes/no"); 36 | }); 37 | -------------------------------------------------------------------------------- /src/typings/obsidian-ex.d.ts: -------------------------------------------------------------------------------- 1 | import type { DataviewApi } from "api/plugin-api"; 2 | import "obsidian"; 3 | import { EditorView } from "@codemirror/view"; 4 | 5 | declare module "obsidian" { 6 | interface MetadataCache { 7 | trigger(...args: Parameters): void; 8 | trigger(name: string, ...data: any[]): void; 9 | } 10 | 11 | interface App { 12 | appId?: string; 13 | plugins: { 14 | enabledPlugins: Set; 15 | plugins: { 16 | dataview?: { 17 | api: DataviewApi; 18 | }; 19 | }; 20 | }; 21 | } 22 | 23 | interface Workspace { 24 | /** Sent to rendered dataview components to tell them to possibly refresh */ 25 | on(name: "dataview:refresh-views", callback: () => void, ctx?: any): EventRef; 26 | } 27 | 28 | interface Editor { 29 | /** 30 | * CodeMirror editor instance 31 | */ 32 | cm?: EditorView; 33 | } 34 | } 35 | 36 | declare global { 37 | interface Window { 38 | DataviewAPI?: DataviewApi; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/typings/workers.d.ts: -------------------------------------------------------------------------------- 1 | declare module "web-worker:*" { 2 | const WorkerFactory: new (options: any) => Worker; 3 | export default WorkerFactory; 4 | } 5 | -------------------------------------------------------------------------------- /src/ui/export/markdown.ts: -------------------------------------------------------------------------------- 1 | import { SListItem } from "data-model/serialized/markdown"; 2 | import { Grouping, Groupings, Literal, Values, Widgets } from "data-model/value"; 3 | import { DEFAULT_SETTINGS, ExportSettings, QuerySettings } from "settings"; 4 | import { nestItems } from "ui/views/task-view"; 5 | 6 | //////////// 7 | // Tables // 8 | //////////// 9 | 10 | /** Render a table of literals to Markdown. */ 11 | export function markdownTable( 12 | headers: string[], 13 | values: Literal[][], 14 | settings?: QuerySettings & ExportSettings 15 | ): string { 16 | if (values.length > 0 && headers.length != values[0].length) 17 | throw new Error( 18 | `The number of headers (${headers.length}) must match the number of columns (${values[0].length})` 19 | ); 20 | 21 | settings = settings ?? DEFAULT_SETTINGS; 22 | 23 | const mvalues: string[][] = []; 24 | const maxLengths: number[] = Array.from(headers, v => escapeTable(v).length); 25 | 26 | // Pre-construct the table in memory so we can size columns. 27 | for (let row = 0; row < values.length; row++) { 28 | const current: string[] = []; 29 | for (let col = 0; col < values[row].length; col++) { 30 | const text = tableLiteral(values[row][col], settings.allowHtml, settings); 31 | 32 | current.push(text); 33 | maxLengths[col] = Math.max(maxLengths[col], text.length); 34 | } 35 | mvalues.push(current); 36 | } 37 | 38 | // Then construct the actual table... 39 | // Append the header fields first. 40 | let table = `| ${headers.map((v, i) => padright(escapeTable(v), " ", maxLengths[i])).join(" | ")} |\n`; 41 | // Then the separating column. 42 | table += `| ${maxLengths.map(i => padright("", "-", i)).join(" | ")} |\n`; 43 | // Then the data columns. 44 | for (let row = 0; row < values.length; row++) { 45 | table += `| ${mvalues[row].map((v, i) => padright(v, " ", maxLengths[i])).join(" | ")} |\n`; 46 | } 47 | 48 | return table; 49 | } 50 | 51 | /** Convert a value to a Markdown-friendly string. */ 52 | function tableLiteral(value: Literal, allowHtml: boolean = true, settings?: QuerySettings): string { 53 | return escapeTable(rawTableLiteral(value, allowHtml, settings)); 54 | } 55 | 56 | /** Convert a value to a Markdown-friendly string; does not do escaping. */ 57 | function rawTableLiteral(value: Literal, allowHtml: boolean = true, settings?: QuerySettings): string { 58 | if (!allowHtml) return Values.toString(value, settings); 59 | 60 | if (Values.isArray(value)) { 61 | return `
    ${value.map(v => "
  • " + tableLiteral(v, allowHtml, settings) + "
  • ").join("")}
`; 62 | } else if (Values.isObject(value)) { 63 | const inner = Object.entries(value) 64 | .map(([k, v]) => { 65 | return `
  • ${tableLiteral(k, allowHtml, settings)}: ${tableLiteral( 66 | v, 67 | allowHtml, 68 | settings 69 | )}
  • `; 70 | }) 71 | .join(""); 72 | 73 | return `
      ${inner}
    `; 74 | } else { 75 | return Values.toString(value, settings); 76 | } 77 | } 78 | 79 | /** Don't need to import a library for this one... */ 80 | function padright(text: string, padding: string, length: number): string { 81 | if (text.length >= length) return text; 82 | return text + padding.repeat(length - text.length); 83 | } 84 | 85 | /** Escape bars inside table content to prevent it from messing up table rows. */ 86 | function escapeTable(text: string): string { 87 | return text.split(/(?!\\)\|/i).join("\\|"); 88 | } 89 | 90 | /////////// 91 | // Lists // 92 | /////////// 93 | 94 | /** Render a list of literal elements to a markdown list. */ 95 | export function markdownList(values: Literal[], settings?: QuerySettings & ExportSettings): string { 96 | return markdownListRec(values, settings, 0); 97 | } 98 | 99 | /** Internal recursive function which renders markdown lists. */ 100 | function markdownListRec(input: Literal, settings?: QuerySettings & ExportSettings, depth: number = 0): string { 101 | if (Values.isArray(input)) { 102 | let result = depth == 0 ? "" : "\n"; 103 | for (let value of input) { 104 | result += " ".repeat(depth) + "- "; 105 | result += markdownListRec(value, settings, depth); 106 | result += "\n"; 107 | } 108 | 109 | return result; 110 | } else if (Values.isObject(input)) { 111 | let result = depth == 0 ? "" : "\n"; 112 | for (let [key, value] of Object.entries(input)) { 113 | result += " ".repeat(depth) + "- "; 114 | result += Values.toString(key) + ": "; 115 | result += markdownListRec(value, settings, depth); 116 | result += "\n"; 117 | } 118 | 119 | return result; 120 | } else if (Values.isWidget(input) && Widgets.isListPair(input)) { 121 | return `${Values.toString(input.key)}: ${markdownListRec(input.value, settings, depth + 1)}`; 122 | } 123 | 124 | return Values.toString(input); 125 | } 126 | 127 | /////////// 128 | // Tasks // 129 | /////////// 130 | 131 | /** Render the result of a task query to markdown. */ 132 | export function markdownTaskList( 133 | tasks: Grouping, 134 | settings?: QuerySettings & ExportSettings, 135 | depth: number = 0 136 | ): string { 137 | if (Groupings.isGrouping(tasks)) { 138 | let result = ""; 139 | for (let element of tasks) { 140 | result += "#".repeat(depth + 1) + " " + Values.toString(element.key) + "\n\n"; 141 | result += markdownTaskList(element.rows, settings, depth + 1); 142 | } 143 | return result; 144 | } else { 145 | // Remove task line duplicates if present to match `taskList()` behavior. 146 | const [dedupTasks, _] = nestItems(tasks); 147 | 148 | let result = ""; 149 | for (let element of dedupTasks) { 150 | result += " ".repeat(depth) + "- "; 151 | 152 | if (element.task) { 153 | result += `[${element.status}] ${(element.visual ?? element.text).split("\n").join(" ")}\n`; 154 | } else { 155 | result += `${(element.visual ?? element.text).split("\n").join(" ")}\n`; 156 | } 157 | 158 | result += markdownTaskList(element.children, settings, depth + 1); 159 | } 160 | 161 | return result; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/ui/refreshable-view.ts: -------------------------------------------------------------------------------- 1 | import { FullIndex } from "data-index"; 2 | import { App, MarkdownRenderChild } from "obsidian"; 3 | import { DataviewSettings } from "settings"; 4 | 5 | /** Generic code for embedded Dataviews. */ 6 | export abstract class DataviewRefreshableRenderer extends MarkdownRenderChild { 7 | private lastReload: number; 8 | 9 | public constructor( 10 | public container: HTMLElement, 11 | public index: FullIndex, 12 | public app: App, 13 | public settings: DataviewSettings 14 | ) { 15 | super(container); 16 | this.lastReload = 0; 17 | } 18 | 19 | abstract render(): Promise; 20 | 21 | onload() { 22 | this.render(); 23 | this.lastReload = this.index.revision; 24 | // Refresh after index changes stop. 25 | this.registerEvent(this.app.workspace.on("dataview:refresh-views", this.maybeRefresh)); 26 | // ...or when the DOM is shown (sidebar expands, tab selected, nodes scrolled into view). 27 | this.register(this.container.onNodeInserted(this.maybeRefresh)); 28 | } 29 | 30 | maybeRefresh = () => { 31 | // If the index revision has changed recently, then queue a reload. 32 | // But only if we're mounted in the DOM and auto-refreshing is active. 33 | if (this.lastReload != this.index.revision && this.container.isShown() && this.settings.refreshEnabled) { 34 | this.lastReload = this.index.revision; 35 | this.render(); 36 | } 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/ui/views/calendar-view.ts: -------------------------------------------------------------------------------- 1 | import { FullIndex } from "data-index"; 2 | import { Link } from "index"; 3 | import { App } from "obsidian"; 4 | import { Calendar, ICalendarSource, IDayMetadata, IDot } from "obsidian-calendar-ui"; 5 | import { executeCalendar } from "query/engine"; 6 | import { Query } from "query/query"; 7 | import { DataviewSettings } from "settings"; 8 | import { renderErrorPre } from "ui/render"; 9 | import { DataviewRefreshableRenderer } from "ui/refreshable-view"; 10 | import { asyncTryOrPropagate } from "util/normalize"; 11 | import type { Moment } from "moment"; 12 | 13 | // CalendarFile is a representation of a particular file, displayed in the calendar view. 14 | // It'll be represented in the calendar as a dot. 15 | interface CalendarFile extends IDot { 16 | link: Link; 17 | } 18 | 19 | export class DataviewCalendarRenderer extends DataviewRefreshableRenderer { 20 | private calendar: Calendar; 21 | constructor( 22 | public query: Query, 23 | public container: HTMLElement, 24 | public index: FullIndex, 25 | public origin: string, 26 | public settings: DataviewSettings, 27 | public app: App 28 | ) { 29 | super(container, index, app, settings); 30 | } 31 | 32 | async render() { 33 | this.container.innerHTML = ""; 34 | let maybeResult = await asyncTryOrPropagate(() => 35 | executeCalendar(this.query, this.index, this.origin, this.settings) 36 | ); 37 | if (!maybeResult.successful) { 38 | renderErrorPre(this.container, "Dataview: " + maybeResult.error); 39 | return; 40 | } else if (maybeResult.value.data.length == 0 && this.settings.warnOnEmptyResult) { 41 | renderErrorPre(this.container, "Dataview: Query returned 0 results."); 42 | return; 43 | } 44 | let dateMap = new Map(); 45 | for (let data of maybeResult.value.data) { 46 | const dot = { 47 | color: "default", 48 | className: "note", 49 | isFilled: true, 50 | link: data.link, 51 | }; 52 | const d = data.date.toFormat("yyyyLLdd"); 53 | if (!dateMap.has(d)) { 54 | dateMap.set(d, [dot]); 55 | } else { 56 | dateMap.get(d)?.push(dot); 57 | } 58 | } 59 | 60 | const querySource: ICalendarSource = { 61 | getDailyMetadata: async (date: Moment): Promise => { 62 | return { 63 | dots: dateMap.get(date.format("YYYYMMDD")) || [], 64 | }; 65 | }, 66 | }; 67 | 68 | const sources: ICalendarSource[] = [querySource]; 69 | const renderer = this; 70 | this.calendar = new Calendar({ 71 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 72 | target: (this as any).container, 73 | props: { 74 | onHoverDay(date: Moment, targetEl: EventTarget): void { 75 | const vals = dateMap.get(date.format("YYYYMMDD")); 76 | if (!vals || vals.length == 0) { 77 | return; 78 | } 79 | if (vals?.length == 0) { 80 | return; 81 | } 82 | 83 | renderer.app.workspace.trigger("link-hover", {}, targetEl, vals[0].link.path, vals[0].link.path); 84 | }, 85 | onClickDay: async date => { 86 | const vals = dateMap.get(date.format("YYYYMMDD")); 87 | if (!vals || vals.length == 0) { 88 | return; 89 | } 90 | if (vals?.length == 0) { 91 | return; 92 | } 93 | const file = renderer.app.metadataCache.getFirstLinkpathDest(vals[0].link.path, ""); 94 | if (file == null) { 95 | return; 96 | } 97 | const leaf = renderer.app.workspace.getUnpinnedLeaf(); 98 | await leaf.openFile(file, { active: true }); 99 | }, 100 | showWeekNums: false, 101 | sources, 102 | }, 103 | }); 104 | } 105 | 106 | onClose(): Promise { 107 | if (this.calendar) { 108 | this.calendar.$destroy(); 109 | } 110 | return Promise.resolve(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/ui/views/inline-field.tsx: -------------------------------------------------------------------------------- 1 | import { InlineField, extractInlineFields, parseInlineValue } from "data-import/inline-field"; 2 | import { MarkdownPostProcessorContext, MarkdownRenderChild } from "obsidian"; 3 | import { h, render } from "preact"; 4 | import { DataviewContext, DataviewInit, Lit } from "ui/markdown"; 5 | import { canonicalizeVarName } from "util/normalize"; 6 | 7 | /** Replaces raw textual inline fields in text containers with pretty HTML equivalents. */ 8 | export async function replaceInlineFields(ctx: MarkdownPostProcessorContext, init: DataviewInit): Promise { 9 | const inlineFields = extractInlineFields(init.container.innerHTML); 10 | if (inlineFields.length == 0) return; 11 | 12 | const component = new MarkdownRenderChild(init.container); 13 | ctx.addChild(component); 14 | 15 | // Iterate through the raw HTML and replace inline field matches with corresponding rendered values. 16 | let result = init.container.innerHTML; 17 | for (let x = inlineFields.length - 1; x >= 0; x--) { 18 | let field = inlineFields[x]; 19 | let renderContainer = document.createElement("span"); 20 | renderContainer.addClasses(["dataview", "inline-field"]); 21 | 22 | // Block inline fields render the key, parenthesis ones do not. 23 | if (field.wrapping == "[") { 24 | const key = renderContainer.createSpan({ 25 | cls: ["dataview", "inline-field-key"], 26 | attr: { 27 | "data-dv-key": field.key, 28 | "data-dv-norm-key": canonicalizeVarName(field.key), 29 | }, 30 | }); 31 | 32 | // Explicitly set the inner HTML to respect any key formatting that we should carry over. 33 | key.innerHTML = field.key; 34 | 35 | renderContainer.createSpan({ 36 | cls: ["dataview", "inline-field-value"], 37 | attr: { id: "dataview-inline-field-" + x }, 38 | }); 39 | } else { 40 | renderContainer.createSpan({ 41 | cls: ["dataview", "inline-field-standalone-value"], 42 | attr: { 43 | id: "dataview-inline-field-" + x, 44 | "data-dv-key": field.key, 45 | "data-dv-norm-key": canonicalizeVarName(field.key), 46 | }, 47 | }); 48 | } 49 | 50 | result = result.slice(0, field.start) + renderContainer.outerHTML + result.slice(field.end); 51 | } 52 | 53 | // Use a