[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n# Unix-style newlines with a newline ending every file\n[*]\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n# Matches multiple files with brace expansion notation\n# Set default charset\n[*.{ts,js}]\ncharset = utf-8\nindent_style = space\nindent_size = 4"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [\"blacksmithgu\"]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report (desktop)\ndescription: \"File a bug report for Dataview on desktop Obsidian\"\ntitle: \"Bug report\"\nlabels: [\"bug\", \"needs-investigation\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: Also tell us, what did you expect to happen?\n      placeholder: Tell us what you see!\n    validations:\n      required: true\n  - type: textarea\n    id: dql\n    attributes:\n      label: DQL\n      description: If applicable, provide the query where the bug occurred\n      placeholder: |\n        ```dataview\n        LIST FROM #example\n        ```\n  - type: textarea\n    id: js\n    attributes:\n      label: JS\n      description: If applicable, provide the javascript where the bug occurred\n      render: js\n  - type: input \n    id: version\n    attributes:\n      label: Dataview Version\n      description: What version of Dataview are you running?\n      placeholder: 0.4.20\n    validations:\n      required: true\n  - type: input \n    id: obsidian-version \n    attributes:\n      label: Obsidian Version\n      placeholder: 0.12.19\n    validations:\n      required: true\n  - type: dropdown\n    id: os\n    attributes:\n      label: OS\n      options:\n         - Windows\n         - MacOS\n         - Linux\n    validations:\n      required: true\n         \n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report_mobile.yml",
    "content": "name: Bug Report (mobile)\ndescription: \"File a bug report for Dataview on mobile Obsidian\"\ntitle: \"Bug report\"\nlabels: [\"bug\", \"needs-investigation\", \"mobile\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: Also tell us, what did you expect to happen?\n      placeholder: Tell us what you see!\n    validations:\n      required: true\n  - type: textarea\n    id: dql\n    attributes:\n      label: DQL\n      description: If applicable, provide the query where the bug occurred\n      placeholder: |\n        ```dataview\n        LIST FROM #example\n        ```\n  - type: textarea\n    id: js\n    attributes:\n      label: JS\n      description: If applicable, provide the javascript where the bug occurred\n      render: js\n  - type: input \n    id: version\n    attributes:\n      label: Dataview Version\n      description: What version of Dataview are you running?\n      placeholder: 0.4.20\n    validations:\n      required: true\n  - type: input \n    id: obsidian-version \n    attributes:\n      label: Obsidian Version\n      placeholder: 0.12.19\n    validations:\n      required: true\n  - type: input\n    id: device\n    attributes:\n      label: Device\n      description: What device are you running Obsidian on? Please provide the full model (version, year, etc.)\n      placeholder: iPhone 6\n    validations:\n      required: true\n  - type: input\n    id: os\n    attributes:\n      label: OS\n      description: What OS are you running Obsidian on? Please provide the full OS version.\n      placeholder: iOS 8.1\n    validations:\n      required: true\n         \n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Dataview Discussions\n    url: https://github.com/blacksmithgu/obsidian-dataview/discussions\n    about: Please ask and answer questions here.\n  - name: Obsidian Discord\n    url: https://obsidian.md/community\n    about: \"Check out the #dataview channel under the Plugins section.\"\n  - name: Dataview Snippet Showcase\n    url: https://forum.obsidian.md/t/dataview-plugin-snippet-showcase\n    about: Show off your Dataview snippets here!\n  - name: DataviewJS Snippet Showcase\n    url: https://forum.obsidian.md/t/dataviewjs-snippet-showcase\n    about: Show off your DataviewJS snippets here!\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation.md",
    "content": "---\nname: Documentation\nabout: Suggest improvements to documentation\ntitle: ''\nlabels: documentation\n\n---\n\n**Please provide a link to the documentation page and section**\n\n**Describe the problem**\nA clear and concise description of what is unclear about the documentation\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen. Feel free to suggest wording, full sentences, etc.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature-request\n\n---\n\n[//]: # (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)\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/documentation.yml",
    "content": "name: docs\non:\n  release:\n    types:\n      - published\n  workflow_dispatch:\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-python@v4\n        with:\n          python-version: 3.x\n      - run: |\n          pip install mkdocs-material mkdocs-redirects\n          cd docs\n          mkdocs gh-deploy --force\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Build and test project\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  format:\n    runs-on: ubuntu-latest\n    name: Check code formatting\n    steps:\n      - uses: actions/checkout@v3\n      - name: Setup node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 16\n      - run: npm install\n      - run: npm run check-format\n  build:\n    runs-on: ubuntu-latest\n    name: Build project\n    steps:\n      - uses: actions/checkout@v3\n      - name: Setup node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 16\n      - run: npm install\n      - run: npm run build\n  test:\n    runs-on: ubuntu-latest\n    name: Test project\n    steps:\n      - uses: actions/checkout@v3\n      - name: Setup node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 16\n      - run: npm install\n      - run: npm run test\n"
  },
  {
    "path": ".gitignore",
    "content": "# Intellij\n*.iml\n.idea\n\n# VSCode\n.vscode\n.history/\n\n# npm\nnode_modules\n\n# build\nbuild/\n\n# Ignore .obsidian\n# No one will commit these files, they just spam 'git status'\ntest-vault/.obsidian/*\n# Tells obsidian what plugins are enabled\n!test-vault/.obsidian/community-plugins.json\n# This plugin should still be tracked by git.\n# It might need updated at some point\n!test-vault/.obsidian/hot-reload-master/*\n\n# Don't track this folder. For random things to try.\n# If its import to test though, add it somewhere where other\n# people can test it too.\ntest-vault/untracked/*\n!test-vault/untracked/README.md\n\nlib\nyarn.lock\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n    \"tabWidth\": 4,\n    \"semi\": true,\n    \"embeddedLanguageFormatting\": \"off\",\n    \"parser\": \"typescript\",\n    \"printWidth\": 120,\n    \"arrowParens\": \"avoid\"\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 0.5.70 (Beta)\n\nStill attempting to fix #2557, github is acting up.\n\n\n---\n\n# 0.5.69 (Beta)\n\nAttempting to fix #2557, but uncertain to any side effects.\n\n\n---\n\n# 0.5.68\n\n- Many fixes to the documentation\n- #2318 & co: Various fixes related to _live preview_ rendering of lists\n- New/documented functions for `unique()`, `display()`, `firstvalue()`\n- Added DOM information related to standalone inline fields\n\nThis is the first release done by @holroy, so thanks to him for further developing of _Dataview_. Thank you also to all the people having contributed through PRs and issues.\n\n---\n\n# 0.5.67\n\nIncludes several documentation fixes and several community-contributed bug fixes.\n\n- @reply2za: Fixed inline rendering in the reading view.\n- @carlesalbasboix: Adds sum(), avg(), min(), and max() to data arrays.\n- @mnaoumov: Adds code mirror configuration which code highlights dataviewjs!\n\n---\n\n# 0.5.66\n\nBugfix for version comparisons to fix some other plugins having broken interactions with Dataview.\n\n---\n\n# 0.5.65\n\nA maintenance update which fixes some issues with rendering embeds in Dataviews and adds a few new functions.\n\n- Adds the `hash()` function for generating consistent uniformly-distributed values given arbitrary inputs. Primarily useful for creating \"random\" views which remain consistent across page refreshes. Thanks to @holroy.\n- Adds the `slice()` function for slicing arrays, similar to Javascript's `Array.slice`. Thanks to @holroy.\n- Fixes several issues with rendering embeds inside dataviews. Thanks to @GottZ.\n- Several documentation improvements around tasks - thanks to @holroy and @RaviOnline.\n\n---\n\n# 0.5.64\n\nMore bug fixes for inline field rendering.\n\n\n---\n\n# 0.5.63\n\n- More bugfixes from @RyotaUshio for rendering Markdown paragraphs and other blocks in DataviewJS.\n\n---\n\n# 0.5.62\n\nSeveral more inline field fixes from @RyotaUshio, including more configuration options, fixing inline fields being rendered inside codeblocks, and more. Thanks!\n\n---\n\n# 0.5.61\n\n- @RyotaUshio: Fix several bugs related to the new inline field rendering, including source mode and fixing date formatting.\n\n---\n\n# 0.5.60\n\n- @RyotaUshio: Add explicit rendering of inline fields in live preview. They are much more visually distinct now!\n- @MarioRicalde: Adds `PluginApi#evaluateInline(expression, path)` to the plugin API, which evaluate expressions as if you were on the given page.\n\n---\n\n# 0.5.59\n\n- Fix an issue with the plugin failing to run on iOS due to an esoteric regex issue.\n\n---\n\n# 0.5.58\n\n- Negative durations will now be properly rendered.\n\n---\n\n# 0.5.57\n\nMaintenance patch which bumps many internal dependency versions and which includes approximately ~20 community-contributed PRs which add some new functions, fix some Dataview interactions with properties, and more!\n\n---\n\n# Unreleased\n\n- DQL: Adds new `durationformat(duration, string)` function.\n- DQL: New math rounding functions, `trunc(number)`, `floor(number)`, `ceil(number)`.\n\n# 0.5.56\n\n- Includes some performance fixes on recent versions of Obsidian 1.3+ due to some API changes. Thanks @kometenstaub.\n- Documentation cleanups and improvements by @mocsa, @protofarer, @seanlzx, and @somidad.\n- Adds the new `flat(array)` method for flattening nested arrays, as well as parsing dates using arbitrary formats using\n  `date(text, \"format\")`. Thanks @holroy!\n\n---\n\n# 0.5.55\n\n- Durations are now internationalized using luxon's new internationalization support.\n- Dataviews should now properly render inside Canvas and some other contexts. Thanks @GamerGirlandCo!\n\n---\n\n# 0.5.54\n\n- Regular list items are now also clickable in task views, not just task lines! Thanks to @LilaRest.\n\n---\n\n# 0.5.53\n\n- Fix some documentation issues causing docs to not be updated.\n\n---\n\n# 0.5.52\n\nSubstantial documentation improvements thanks to @s-blu and @AB1908!\n\n- For people concerned about dataviewjs code execution from copy-pasting, @eyuelt has made it possible to change the\n  dataviewjs codeblock prefix.\n- @sohanglal has added some documentation for `task.visual` for changing the visual text of a task.\n- @Chouffy and @Daryl-Horton have fixed some bad documentation links!\n- @vrtmrz swapped the regex used for parsing tags to better match Obsidian's own parser.\n- @alexfertel has added `regextest`, which allows for matching parts of a string instead of the whole thing.\n- @iamrecursion has added more metadata to file links, so they now include section metadata. This may cause some slight\n  visual changes in link views.\n\n---\n\n# 0.5.51 (Beta)\n\n- Allow disabling regular Dataview inline queries via configuration option.\n\n---\n\n# 0.5.50 (Beta)\n\n- Expose dataview EXPRESSION and QUERY parsing to the dataview npm plugin, so others can parse dataview ASTs.\n- Fix documentation issue with `join`.\n\n---\n\n# 0.5.49 (Beta)\n\n- Add the `average` function to compute averages of lists (`average([list of things])`).\n- Added documentation for `average`, `min`, `max`, `minby`, and `maxby` functions.\n- Fixed the broken `nonnull` function and documented it.\n\n---\n\n# 0.5.48 (Beta)\n\nWe're back to more regular beta releases while I trial out new functionality!\n\n- Fixed broken list behavior for `dv.markdownTaskList`.\n- @GamerGirlandCo: Better handling of block IDs when checking off tasks!\n- @s-blu and @AB1908: Lots of big documentation upgrades! Nice!\n- @leoccyao: More block ID task checking fixes. Should work after this one.\n- Add expression/query parsing to the dataview NPM package.\n- @charleshan: Fix a missing header level in the dataview `dv.header` example.\n\n---\n\n# 0.5.47\n\nImproves `date + duration` behavior when either the date or duration are null.\n\n---\n\n# 0.5.46\n\n- Fix #1412: Fix bad `file.cday` and `file.ctime` comparisons due to wrong timezone being set. Ugh.\n\n---\n\n# 0.5.45\n\n- #1400: Properly use the group by field for the group name.\n- Fix bad table highlighting in some themes.\n\n---\n\n# 0.5.44\n\n- #1404: Fixed dates in non-local timezones parsing incorrectly.\n- Fixed some build non-determinism issues.\n- Swapped to pull requests for adding new functionality, and added some more internal tests.\n\n---\n\n# 0.5.43\n\n- Fix #1366: Better handling of calendar emoji (used as due dates in tasks).\n\n---\n\n# 0.5.42\n\nIt's been over a month since the last release! Anyway, this release bundles several nice user-contributed features:\n\n- @AB1908: Tag queries are now case insensitive.\n- @AB1908: Shift-clicking a link/task to open in a new tab now works properly on Mac.\n- @AB1908: Numerous documentation fixes for clarity and more examples.\n- @AnnaKornfeldSimpson: Additional emoji shorthands for more task fields (finished, due).\n- @ooker777: Documentation improvements for some DataviewJS functions, and the ability to use inline emoji for the\n  completion tracking feature.\n- @mt-krainski: Custom date formats for task completions.\n- @gentlegiantJGC: Better support for nested inline fields (i.e., less crashy).\n\n---\n\n# 0.5.41\n\n- Fix a bad regex doing escaping in markdown tables.\n- Improve async documentation.\n\n---\n\n# 0.5.40\n\nAdds some more documentation about the new markdown functionality.\n\n---\n\n# 0.5.39\n\n- Fixed an issue where checking a task in a task view would check the wrong box visually.\n- Added experimental plugin APIs for querying dataview directly as markdown, and converting dataview results to properly\n  formatted markdown.\n\n---\n\n# 0.5.38\n\n- Some minor documentation improvements.\n- Fix an issue with inline fields rendering out of order. That was a weird bug.\n\n---\n\n# 0.5.37\n\nFixes inline field rendering to once again work for highlighting/links, as well as some other rendering quirks with\ninline queries in codeblocks.\n\n---\n\n# 0.5.36\n\n- Fix a bug when checking if an element is an HTMLElement.\n- Properly include the nice improvements to the file count in tables and lists.\n\n---\n\n# 0.5.35\n\n- Fix #1196, #1176: Re-enable HTML values. This was never a featured I advertised since it was just for some internal\n  hackery, but it appears people just discovered it in DataviewJS queries.\n- Improved initial time to popular queries that use `file.starred`.\n\n---\n\n# 0.5.34\n\n- Fix #1174: Fix indexing with a variable.\n- Fix an issue with the experimental calendar view.\n\n---\n\n# 0.5.33\n\n- Fix a bug with inline views that was introduced in 0.5.32.\n\n---\n\n# 0.5.32\n\nThe Dataview API has been noticeably revamped - there are now approximately twice as many functions available on the\nplugin API as there were before, and some additional utilities have been added to both the plugin and inline API. I\nwill be finishing up the associated new \"extension\" functionality shortly, which will allow:\n\n1. For custom Dataview + DataviewJS functions to be added via plugins.\n2. For custom renderable objects (progress bars, embedded task lists, embedded tables) to be added to any Dataview view via plugins.\n3. For plugins to provide alternative behavior for some dataview functionality (such as integrating task plugins with\n   the dataview task query).\n   \nAs part of the API revamp, it is now possible to programmatically execute Dataview and DataviewJS queries - either for\nusing the existing Dataview query language in your own plugin, or for embedding dataview. The Dataview npm library also\nnow exposes many useful internal Dataview types, including the AST structure for all dataview queries.\n\nI am hoping that cleaning up the Dataview API and making it much more extensible will allow for Dataview [to](to) integrate\nmuch better with existing plugins, and to provide the full power of the in-memory index for plugins. I have been very\ncarefully watching index performance in recent weeks to ensure smooth frontend performance for anyone using the API\n(with a goal of <10ms for most queries).\n\n---\n\n# 0.5.31\n\nTasks now have an `outlinks` list field which includes all links in the task; this can be used for finding tasks with\nlinks in them.\n\n---\n\n# 0.5.30\n\n- Added the `typeof(any)` function in Dataview, which obtains the type of any value for comparison:\n```javascript\ntypeof(\"text\") = \"string\"\ntypeof(1) = \"number\"\ntypeof([1, 2, 3]) = \"array\"\n```\n\n- Added the modulo operator (`%`) for doing integer division remainder. I.e., `14 % 2 = 0` and `14 % 3 = 2`.\n- Fixed some minor spacing issues with lists in tables.\n\n---\n\n# 0.5.29\n\nFix another subtle incompatibility between 0.4.26 and 0.5.29 - if you frequently used empty inline fields (like\n`Key::` with no value), the 0.5+ behavior is now the same as 0.4 behavior and will map such fields to null instead of an\nempty string.\n\nThis may fix a broad variety of \"subtly wrong\" queries that you may have seen after the upgrade.\n\n---\n\n# 0.5.28\n\n- Fix a bug with some more string concatenations and null handling.\n\n---\n\n# 0.5.27\n\nMore performance + correctness bugfixes.\n\n- The parser has been made a little more robust to prevent major indexing issues (or at least recover from them\n  quickly).\n- Several new strange tag variants are now supported.\n- Markdown links are now properly indexed again.\n\nSome DataviewJS performance issues should be resolved now, especially for external plugins using Dataview. This fix\ndoes involve a slight API break w.r.t. what types are wrapped into Dataview Arrays (which provide functions like\n`.where()`). Generally, only Dataview-provided implicits are wrapped in data arrays now; frontmatter and inline fields\nare always now regular JS arrays - use `dv.array()` to explicitly make a data array if you want the advanced querying.\n\n---\n\n# 0.5.26\n\nMore small bugfixes:\n\n- Fix a few small link rendering issues.\n- Tag extraction from tasks now handles punctuation properly.\n- Upgrade luxon (which is embedded in DataviewJS) to 2.4.0.\n\n---\n\n# 0.5.25\n\n- Fix #1147: Fix there being a `#null` tag for files with an empty `tag` or `tags` frontmatter.\n\n---\n\n# 0.5.24\n\nSeveral bugfixes:\n\n- Nulls are now sorted first rather than last; it's generally good practice to explicitly check for nulls in your\n  queries to avoid strange behavior.\n- Dataview now properly parses space-delimited tags (like `tags: abc def ghi`).\n- Dataview now supports dropping the entire file cache in case of bugs.\n\n---\n\n# 0.5.23\n\n- Fix #1140: Force API objects to be arrays if they are iterables.\n\n---\n\n# 0.5.22\n\n- Fix #1135: Use 'x' instead of 'X' for checkboxes.\n\n---\n\n# 0.5.21\n\nA long-overdue swap from the beta branch to the stable branch. The beta branch should not include any (intended) breaking\nchanges, and has some nice performance improvements that come along with it! Here are the major changes:\n\n- Most views now use React and no longer flicker when updating; this is not the case yet for DataviewJS, which will be\n  getting equivalent treatment in the future.\n- Dataview now caches metadata, so Dataview loads are very fast after the first time you open your vault. Dataview still\n  needs to visit every file when you update the plugin version, so that should be the only times you experience slower\n  load times.\n- A brand new task view backend and query which allows you to filter per-task, rather than per-page! Check the\n  documentation for details, but this broadly means `WHERE` statements now use task properties instead of page\n  properties.\n- Some additional metadata is now available for use - `file.starred`, `file.lists`, and more metadata in\n  `file.tasks`.\n\nThere have been some moderate documentation touch-ups to keep things up to date; I'm still working on a walkthrough for\ncommon Dataview use cases. This review also includes about ~30-40 bugfixes; some new bugs may arise due to internal\nchanges, so please flag them if you encounter them.\n\n---\n\n# 0.5.20 (Beta)\n\nSlight fix to hopefully improve some strange reported cases of bad indexing at startup.\n\n---\n\n# 0.5.19 (Beta)\n\nDataview now uses IndexedDB to cache file metadata, reducing startup time to virtually nothing if you've opened the\nvault before; if you have a small vault (<1000 notes), you may notice a slight improvement, but large vaults and mobile\ndevices will notice a very significant performance improvement to \"first valid paint\". Some other performance parameters\nhave been tuned to hopefully make the default experience better.\n\nA few small bugs related to rendering have also been squashed, including an issue with images being scaled wrongly.\n\n---\n\n# 0.5.18 (Beta)\n\n- Tasks in task views now support alternative task status characters like '!' and '/'; thanks @ebullient.\n- A few documentation nit fixes.\n- Added `DataArray#sortInPlace` for a more efficient mutable sort for niche use cases.\n\n---\n\n# 0.5.17 (Beta)\n\n- Improved behavior when clicking on tasks in the task view; will now properly scroll to the relevant line in long\n  files!\n- Fixed a bug with incorrect counts being displayed in task views.\n- Added `tags` as a field available on task items, so you can now do things like `TASK WHERE contains(tags, \"#tag\")`.\n\n---\n\n# 0.5.16 (Beta)\n\nDataview now tracks initialization and will report when all files have been indexed in the console; you can\nprogrammatically see this via `dataview:index-ready`, or by checking `api.index.initialized`.\n\n---\n\n# 0.5.15 (Beta)\n\n- Add hover highlights to tables to make seeing rows a little easier.\n- Tables and task lists now include counts of the number of results in the headers.\n- Further improved task selection in the task view.\n\n---\n\n# 0.5.14 (Beta)\n\n- Fix task highlighting when not grouping.\n- Remove some spurious console logging.\n- Slightly improve task highlighting behavior when clicking on a task.\n\n---\n\n# 0.5.13 (Beta)\n\nSeveral smaller bugfixes!\n\n- Fix #997: Use the group by field name in the table name.\n- Prevent tons of errors if you incorrectly set the inline query prefix.\n\n---\n\n# 0.5.12 (Beta)\n\nImprove error messages for queries somewhat and get rid of some ugly output.\n\n---\n\n# 0.5.11 (Beta)\n\nAdd detection of tasks inside of block quotes, as well as correctly implement automatic checking and unchecking of these\ntasks.\n\n---\n\n# 0.5.10 (Beta)\n\nAdds the `Dataview: Force Refresh Views` Command (accessible via the Ctrl+P command view) to force current views to\nrefresh immediately.\n\n---\n\n# 0.5.9 (Beta)\n\nAnother fix for due-date related emoji in tasks. I hate emoji.\n\n---\n\n# 0.5.8 (Beta)\n\nFix some issues with infinite loops of tasks due to bad Obsidian metadata (potentially due to being out of date?).\n\n---\n\n# 0.5.7 (Beta)\n\nFix issues with parsing '🗓️2021-08-29' due-date annotations on tasks, as well as an issue with properly extracting\ndue/completed/completed times for use in queries.\n\n---\n\n# 0.5.6 (Beta)\n\nProper release of 0.5.5 plus one additional small improvement:\n\n- Add `duration * number` and `duration / number` operations for manipulation durations numerically.\n\n---\n\n# 0.5.5 (Beta)\n\nMore small features:\n\n- Fix issues with task sorting not doing anything. Sort away!\n- Table headers can now be arbitrary markdown. So you can put things like links in your headers: `TABLE (1 + 2) AS\n  \"[[File]]\".\n- You can now specify the size of an image embed by providing WxH in it's display property: `![[image.png|50x50]]`.\n\n---\n\n# 0.5.4 (Beta)\n\nImproved image rendering for some link types, and adds the `embed(link)` and `embed(link, false)` options to convert\nlinks to/from their embedded equivalents.\n\n---\n\n# 0.5.3 (Beta)\n\nIterative beta which adds a few nice QoL features and fixes some more bugs:\n\n- Internally swapped to a React-based renderer; this should not have a noticeable perf or usability impact, but makes it\n  easier for me to implement complex table/list behaviors.\n- Naming your fields with `AS \"Name\"` is now optional; Dataview will infer the name from the expression automatically.\n  For example, `TABLE 8 + 4, 3 + 6 FROM ...` is now a valid table expression, and the columns will be named `8 + 4` and\n  `3 + 6` respectively.\n- Some issues with array and object rendering were corrected.\n- Error messages on empty dataview results were improved and now show up for all views.\n\nInline images are now rendered correctly in Dataview tables and lists - no more hacky `app://local/` shenanigans!\n\n---\n\n# 0.5.2 (Beta)\n\n- Fix #971: Objects now work properly inside DataviewQL evaluation.\n\n---\n\n# 0.5.1 (Beta)\n\n- Temporarily revert the new task metadata behavior: inline fields in sublists of tasks are added to the page, instead\n  of the task. This behavior is not good, but is compatible with legacy usages of task metadata, which should not break\n  some existing queries.\n    - This behavior will be removed in the future behind a flag.\n- Added the 'visual' field to tasks - if set, tasks render 'visual' instead of their regular text.\n- Fixed `DataArray#mutate()`.\n\n---\n\n# 0.5.0 (Beta)\n\nRe-release of broken release 0.4.23, now hopefully with fixes that make it work on (most) machines. I'll be doing beta\nreleases for a little while until I can confirm the new version is stable; use BRAT\n(https://github.com/TfTHacker/obsidian42-brat) to easily track Dataview beta versions if you are interested in cutting\nedge features.\n\n---\n\n# 0.4.25\n\nFix #867: Create a container div per taskList to allow for multiple task views.\n\n---\n\n# 0.4.24\n\nRe-release of 0.4.23f since Obsidian does not automatically update between non-semver versions.\n\n---\n\n# 0.4.23f\n\nRemove some code which attempted to make tag queries case-insensitive; I'll reimplement this more generally later (it\nconflicts with existing queries which check tags via `contains(file.tags, \"#Tag\")` and similar).\n\n---\n\n# 0.4.23e\n\nMore task bugfixes / improvements, and a fix that caused task metadata to be duplicated.\n\n---\n\n# 0.4.23d\n\nMore inline field list parsing bug fixes. Hopefully we're back to a nice working order!\n\n---\n\n# 0.4.23c\n\nBugfix which adds support for '1)' style lists, as well as a very annoying null issue due to JavaScript being a very\nsad, very sad language.\n\n---\n\n# 0.4.23b\n\nBugfix for bad inlink/outlink computations; links were not being normalized properly so reverse lookups were not\nworking.\n\n---\n\n# 0.4.23\n\nThe Task Update! This release reworks how dataview handles tasks and list items so that they should be much more\nintuitive to use and interact with:\n\n1. **Subtask Support**: Queries now search over all list items, instead of only over root elements. This should make\n   task filtering much more usable, especially if you tend to put tasks under other list items or care specifically\n   about subtasks.\n2. **Multiline Support**: Dataview now understands multi-line tasks and renders/updates them correctly.\n3. **Immediately Navigate to Task**: The new task view, aside from looking a little cleaner than previous views, now\n   immediately navigates to the task in it's original file on click and selects it.\n4. **Grouping Support**: For DataviewJS users, `dv.taskList` now supports grouping (as produced by `groupBy` and the new\n   `groupIn`) natively.\n\nFor DataviewJS users, the task and list representation has changed: `file.tasks` (and the new `file.lists`) contain\nevery single task (including subtasks) in the file, instead of only the root elements. You can return to previous\nbehavior by filtering out tasks with a non-null parent - i.e., `file.tasks.where(task => !task.parent)`. `dv.taskList`\nwill intelligently deal with properly nesting and de-duplicating tasks, so just filter to the tasks you want to render and\nthe API will do the rest.\n\nThis release also includes general backend improvements as we prepare for live-editing in Dataview views, as well as\nseveral community-contributed API improvements:\n\n- `DataArray#groupIn`: For grouping already grouped data, you can now use `array.groupIn(v => ...)`, which will group\n  the innermost (original) data in the array instead of the top level groups. This allows for more easily grouping\n  recursively, such as `dv.pages().groupBy(page => page.file.folder).groupIn(page => page.title)` producing a grouping\n  of folders, then page titles.\n- `substring(string, start[, end])`: The last major missing string function is now available! Take slices of strings.\n- Improved `dv.el()` and other HTML functions - thanks @vitaly.\n- null and undefined entries sort at the end instead of the beginning by default; sorry to those whose code sorts wrong\n  because of this, but it is a better default for most people's use cases.\n- All links are now properly normalized to their full paths, fixing many link comparison edge cases in DataviewJS.\n\nDocumentation additions for the new task functionality will be coming out in the next few days. The next release 0.4.24\nis currently targeting expanded `FROM` query support, basic table view improvements, and general exporting functionality\nfor Dataview. See you then!\n\n---\n\n# 0.4.22\n\nThe @pjeby update! This includes several performance improvements suggested by @pjeby to dramatically improve background\nDataview performance as well as reduce some memory pressure. It also includes some minor bug-fixes and preliminary\nfunctionality:\n\n- Target ES2018 for better Promise support\n- Allow parsing shorthands in `dv.date()`.\n- Add additional metadata to inline field rendering which can be styled.\n- Cleanup events & workers on plugin uninstall, improving the Dataview uninstall/disable/reload experience.\n- Add preliminary `CALENDAR` queries - rendering similar to the obsidian-calendar plugin, see the documentation!\n\nDataview should perform much better on startup and when you have lots of tabs open - thanks again to @pjeby.\n\n---\n\n# 0.4.21\n\nBugfix release which primarily fixes issues that Dataview had with the live preview mode in upcoming Obsidian versions;\nDataview live preview should now be functional. Also includes a number of smaller bugfixes.\n\n- Fix #646: Add `date(yesterday)` to create a date 24 hours ago.\n- Fix #618: Luxon is now available on the dataview API (`dv.luxon`).\n- Fix #510: Add `dv.duration()` for parsing durations.\n- Fix #647: All HTML functions in the DataviewJS API now return their rendered objects.\n- Fix #652: Fix parsing of invalid dates.\n- Fix #629: Fix block link parsing.\n- Fix #601: Timezones are now rendered properly and parsed properly in Dataview dates.\n- PR #637: Add `meta(link)` which allows you to access various metadata about a link itself.\n- Various minor null safety fixes.\n- Dataview now reports it's exact version and build time in logs.\n\n---\n\n# 0.4.20\n\nSome feature work (mostly by other contributors) while I while away at section metadata. May also fix a few bugs!\n\n- Fix #448: You can now use the \"Task Completion Tracking\" option to automatically add completion metadata to tasks\n  which are checked/unchecked through Dataview. Thanks to @sheeley.\n- Add a search bar to documentation. Thanks to @tzhou.\n- Add new date expressions for the start of the week (`date(sow)`), and the end of the week (`date(eow)`). Thanks\n  @Jeamee and @v_mujunma.\n\nSmall minor bugfix / security releases may follow in the near future; otherwise, the next major release will include\nsection and object metadata.\n\n---\n\n# 0.4.19\n\nBugfix release which corrects emoji parsing & localization issues.\n\n- Add `DataArray#into`, which lets you index into objects without flattening.\n- Renamed 'header' to 'section' in task metadata; 'header' will remain around for a few major releases to let people\n  naturally migrate.\n- Fix #487: You no longer need spaces around '*' in expressions.\n- Fix #559: Fix unicode issues in variable canonicalization which was causing problems with non-Latin inline field\n  keys.\n\n## Duration Parsing\n\nYou can now include multiple units in durations: `dur(8 minutes, 4 seconds)` or `dur(2yr8mo12d)`. You can separate\ndurations by commas, or use the abbreviated syntax with/without spaces.\n\n---\n\n# 0.4.18\n\nBugfix release which fixes bad inline field highlighting if '[' and '(' are mixed on the same line in particular orders.\n\n---\n\n# 0.4.17\n\nMinor feature release to patch up more implementation holes.\n\n## Single File Queries\n\nYou can now query from a specific file (instead of just folders and tags) by specifying the full file path:\n\n```\nTASK FROM \"dataview/Test\"\n...\n```\n\nThis is primarily useful for task queries, but will soon be useful for section and object queries in the near future as\nwell.\n\n## Better Inline Field Highlighting\n\nThe CSS for inline field highlighting has been fixed and some compatibility issues improved, so it should work on all\nthemes now instead of only a few.\n\n## dv.el()\n\nDataviewJS now has `dv.el()`, which is like existing functions like `dv.paragraph` and `dv.span` but can create any\nHTML element type; for example:\n\n```js\ndv.el(\"b\", \"Text!\");\ndv.el(\"i\", 18);\n```\n\n---\n\n# 0.4.16\n\nSmall performance release which substantially reduces the impact Dataview has on vault loading times (by spreading out\nfile loading). The Dataview Index is now also eagerly initialized, so plugin consumers of the API can immediately start\nusing it instead of waiting for the `dataview:api-ready` event.\n\n---\n\n# 0.4.15\n\nA simple fix for #537 which properly 'awaits' value rendering in `dv.view()`. Fixes issues with values rendering out of\norder.\n\n---\n\n# 0.4.14\n\nSmall bugfix release.\n\n- Fixes inline field evaluation when using the new fancy highlighting.\n- You can now configure whether task links should show up at the beginning or end of the task (or just disable them)\n  in the \"Task Link Location\" setting.\n- Most setting updates will immediately be applied to existing Dataviews.\n\n---\n\n# 0.4.13\n\nBugfix release which adds fancy rendering to inline-inline fields and includes a few bugfixes.\n\n## Pretty Inline Fields\n\nInline fields of the form `[key:: value]` will now be rendered with fancy new HTML! By default, they are rendered with\nboth the key and value. You can only render the value using parenthesis instead: `(key:: value)`. You can disable\nthis feature in the configuration.\n\nFull-line inline fields (that Dataview has supported for a long time) will gain similar rendering support soon; in the\nmeanwhile, give the new syntax a try!\n\n### Task Linking\n\nTasks now render with a link to the page/section that they are defined in, making `GROUP BY` and custom task\nediting easier to do:\n\n- [ ] A Task. 🔗\n- [ ] Another Task. 🔗\n    - [ ] Some Random Subtask. 🔗\n\nYou can configure the symbol for the link or disable it altogether.\n\n### Improving DataviewJS Posture\n\nI am currently actively looking into improving DataviewJS sandboxing and general security posture. As a first small step\nin this, I have made DataviewJS opt-in instead of opt-out, and added a separate control for Inline DataviewJS. You may\nneed to re-enable it in your settings if you use it.\n\nMore improvements and better JavaScript sandboxing will follow.\n\n---\n\n# 0.4.12-hotfix1\n\nRe-release of 0.4.12 that fixes an important indexing issue.\n\n- Fix #505: Use `completion` instead of `completed` when setting task completion time.\n- Fix #509: Add `startswith` / `endswith` string functions.\n- Fix #488: Add `padleft` and `padright`, and `string`.\n- Fix #506, #512: Fix date comparisons due to a bizarre date zone issue.\n\n---\n\n# 0.4.12\n\nBugfix release following up 0.4.11 which includes a few minor function additions.\n\n- Fix #512: Strange zone issue causing dates to not be equal.\n- Fix #506: Same as #512.\n- Fix #488: Add `padleft` / `padright` functions.\n- Fix #509: Add `startswith` and `endswith` functions.\n- Fix #505: Correctly read completion dates for tasks from `completion`.\n\nThis release also includes improved testing thanks to mocking Obsidian plugin APIs!\n\n---\n\n# 0.4.11\n\nFixes task behavior and adds \"truly inline\" fields!\n\n## Improved Task Behavior\n\nTask queries are now much improved from their primitive foundations - you can now filter, sort, and group them! The FROM\nblock is still page-based, sadly, though you can simply use `WHERE` instead if desired. For example, you can now access\ntask fields like `text`, `line`, or `completed`:\n\n```\nTASK WHERE contains(text, \"#tag\")\nWHERE !completed\nGROUP BY file.folder\n```\n\nThe full list of all available task metadata can be found\n[here](https://blacksmithgu.github.io/obsidian-dataview/data-annotation/#tasks); tasks include all the information\nneeded to uniquely identify them, and automatically inherit all of the metadata from their parent file as well (so you\ncan access `file.name`, for example). You can also annotate tasks with inline fields, as described in the section below.\n\nThere is some additional UX work to be done - primarily on more easily allowing you to navigate to where the task is\ndefined, as well as render tasks in views other than the `TASK` view.  The semantics of how grouping works (to make it\nmore intuitive/useful than it currently is) will likely also be revisited.\n\n## Inline Inline Fields\n\nEarly support for truly inline fields have been added, where you can add metadata in the middle of a sentence. It looks\nsimilar to existing inline field syntax, but with brackets or parenthesis:\n\n```\nI would rate this a [rating:: 6]. It was (thoughts:: acceptable).\n```\n\nImproved rendering for all inline fields is coming in an upcoming update to improve the visual look of these inline\nfields.\n\n\n## Issues\n\n- Fix #496: Fix task `SORT` functionality to do something.\n- Fix #492: Tasks now properly annotated with parent file information.\n- Fix #498: Fix task checking/unchecking logic (which broke due to a change in the task regex...).\n\n---\n\n# Initial\n\nStart of the automatic changelog.\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "MIT License\n\nCopyright (c) 2021 Michael Brenan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Obsidian Dataview\n\nTreat your [Obsidian Vault](https://obsidian.md/) as a database which you can query from. Provides a JavaScript API and\npipeline-based query language for filtering, sorting, and extracting data from Markdown pages. See the Examples section\nbelow for some quick examples, or the full [reference](https://blacksmithgu.github.io/obsidian-dataview/) for all the details.\n\n## Examples\n\nShow all games in the game folder, sorted by rating, with some metadata:\n\n~~~markdown\n```dataview\ntable time-played, length, rating\nfrom \"games\"\nsort rating desc\n```\n~~~\n\n![Game Example](docs/docs/assets/game.png)\n\n---\n\nList games which are MOBAs or CRPGs.\n\n~~~markdown\n```dataview\nlist from #game/moba or #game/crpg\n```\n~~~\n\n![Game List](docs/docs/assets/game-list.png)\n\n---\n\nList all markdown [tasks](https://blacksmithgu.github.io/obsidian-dataview/data-annotation/#tasks) in un-completed projects:\n\n~~~markdown\n```dataview\ntask from #projects/active\n```\n~~~\n\n![Task List](docs/docs/assets/project-task.png)\n\n---\n\nShow all files in the `books` folder that you read in 2021, grouped by genre and sorted by rating:\n\n~~~markdown\n```dataviewjs\nfor (let group of dv.pages(\"#book\").where(p => p[\"time-read\"].year == 2021).groupBy(p => p.genre)) {\n\tdv.header(3, group.key);\n\tdv.table([\"Name\", \"Time Read\", \"Rating\"],\n\t\tgroup.rows\n\t\t\t.sort(k => k.rating, 'desc')\n\t\t\t.map(k => [k.file.link, k[\"time-read\"], k.rating]))\n}\n```\n~~~\n\n![Books By Genre](docs/docs/assets/books-by-genre.png)\n\n## Usage\n\nFor a full description of all features, instructions, and examples, see the [reference](https://blacksmithgu.github.io/obsidian-dataview/). For a more brief outline, let us examine the two major aspects of Dataview: *data* and *querying*.\n\n#### **Data**\n\nDataview generates *data* from your vault by pulling\ninformation from **Markdown frontmatter** and **Inline fields**.\n\n- Markdown frontmatter is arbitrary YAML enclosed by `---` at the top of a markdown document which can store metadata\n  about that document.\n- Inline fields are a Dataview feature which allow you to write metadata directly inline in your markdown document via\n  `Key:: Value` syntax.\n\nExamples of both are shown below:\n\n```yaml\n---\nalias: \"document\"\nlast-reviewed: 2021-08-17\nthoughts:\n  rating: 8\n  reviewable: false\n---\n```\n```markdown\n# Markdown Page\n\nBasic Field:: Value\n**Bold Field**:: Nice!\nYou can also write [field:: inline fields]; multiple [field2:: on the same line].\nIf you want to hide the (field3:: key), you can do that too.\n```\n\n#### **Querying**\n\nOnce you've annotated documents and the like with metadata, you can then query it using any of Dataview's four query\nmodes:\n\n1. **Dataview Query Language (DQL)**: A pipeline-based, vaguely SQL-looking expression language which can support basic\n   use cases. See the [documentation](https://blacksmithgu.github.io/obsidian-dataview/query/queries/) for details.\n\n   ~~~markdown\n   ```dataview\n   TABLE file.name AS \"File\", rating AS \"Rating\" FROM #book\n   ```\n   ~~~\n\n2. **Inline Expressions**: DQL expressions which you can embed directly inside markdown and which will be evaluated in\n   preview mode. See the [documentation](https://blacksmithgu.github.io/obsidian-dataview/reference/expressions/) for\n   allowable queries.\n\n   ```markdown\n   We are on page `= this.file.name`.\n   ```\n\n3. **DataviewJS**: A high-powered JavaScript API which gives full access to the Dataview index and some convenient\n   rendering utilities. Highly recommended if you know JavaScript, since this is far more powerful than the query\n   language. Check the [documentation](https://blacksmithgu.github.io/obsidian-dataview/api/intro/) for more details.\n\n   ~~~markdown\n   ```dataviewjs\n   dv.taskList(dv.pages().file.tasks.where(t => !t.completed));\n   ```\n   ~~~\n\n4. **Inline JS Expressions**: The JavaScript equivalent to inline expressions, which allow you to execute arbitrary JS\n   inline:\n\n   ~~~markdown\n   This page was last modified at `$= dv.current().file.mtime`.\n   ~~~\n\n#### JavaScript Queries: Security Note\n\nJavaScript queries are very powerful, but they run at the same level of access as any other Obsidian plugin. This means\nthey can potentially rewrite, create, or delete files, as well as make network calls. You should generally write\nJavaScript queries yourself or use scripts that you understand or that come from reputable sources. Regular Dataview\nqueries are sandboxed and cannot make negative changes to your vault (in exchange for being much more limited).\n\n## Contributing\n\nContributions via bug reports, bug fixes, documentation, and general improvements are always welcome. For more major\nfeature work, make an issue about the feature idea / reach out to me so we can judge feasibility and how best to\nimplement it.\n\n#### Local Development\n\nThe codebase is written in TypeScript and uses `rollup` / `node` for compilation; for a first time set up, all you\nshould need to do is pull, install, and build:\n\n```console\nfoo@bar:~$ git clone git@github.com:blacksmithgu/obsidian-dataview.git\nfoo@bar:~$ cd obsidian-dataview\nfoo@bar:~/obsidian-dataview$ npm install\nfoo@bar:~/obsidian-dataview$ npm run dev\n```\n\nThis will install libraries, build dataview, and deploy it to `test-vault`, which you can then open in Obsidian. This\nwill also put `rollup` in watch mode, so any changes to the code will be re-compiled and the test vault will automatically\nreload itself.\n\n#### Preparing for creating pull requests\n\nIf you plan on doing pull request, we would also recommend to do the following in advance of creating the pull request:\n\n```console\nfoo@bar:~$ npm run dev\nfoo@bar:~$ npm run check-format\nfoo@bar:~$ npm run format\nfoo@bar:~$ npm run test\n```\n\nThe third step of `npm run format` is only needed if the format check reports some issue.\n\n#### Installing to Other Vaults\n\nIf you want to dogfood dataview in your real vault, you can build and install manually. Dataview is predominantly a\nread-only store, so this should be safe, but watch out if you are adjusting functionality that performs file edits!\n\n```console\nfoo@bar:~/obsidian-dataview$ npm run build\nfoo@bar:~/obsidian-dataview$ ./scripts/install-built path/to/your/vault\n```\n\n#### Building Documentation\n\nWe use `MkDocs` for documentation (found in `docs/`). You'll need to have python and pip to run it locally:\n\n```console\nfoo@bar:~/obsidian-dataview$ pip3 install mkdocs mkdocs-material mkdocs-redirects\nfoo@bar:~/obsidian-dataview$ cd docs\nfoo@bar:~/obsidian-dataview/docs$ mkdocs serve\n```\n\nThis will start a local web server rendering the documentation in `docs/docs`, which will live-reload on change.\nDocumentation changes are automatically pushed to `blacksmithgu.github.io/obsidian-dataview` once they are merged\nto the main branch.\n\n#### Using Dataview Types In Your Own Plugin\n\nDataview publishes TypeScript typings for all of its APIs onto NPM (as `blacksmithgu/obsidian-dataview`). For\ninstructions on how to set up development using Dataview, see [setup instructions](https://blacksmithgu.github.io/obsidian-dataview/plugin/develop-against-dataview/).\n\n## Support\n\nHave you found the Dataview plugin helpful, and want to support it? I accept donations which go towards future\ndevelopment efforts. I generally do not accept payment for bug bounties/feature requests, as financial incentives add\nstress/expectations which I want to avoid for a hobby project!\n\nSupport @blacksmithgu:  \n[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate?business=Y9SKV24R5A8BQ&item_name=Open+source+software+development&currency_code=USD)\n\nSupport @holroy:  \n<a href=\"https://www.buymeacoffee.com/holroy\" target=\"_blank\"><img src=\"https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png\" alt=\"Buy Me A Coffee\" style=\"height: 40px !important;width: 175px !important;\" ></a>\n"
  },
  {
    "path": "__mocks__/data-import/web-worker/import-manager.ts",
    "content": "/** A mock for `FileImporter` which runs on the same thread. */\n\nimport { runImport } from \"data-import/web-worker/import-impl\";\nimport { CachedMetadata, MetadataCache, TFile, Vault } from \"obsidian\";\n\nexport class FileImporter {\n    public constructor(public numWorkers: number, public vault: Vault, public metadataCache: MetadataCache) {}\n\n    public async reload<T>(file: TFile): Promise<T> {\n        let contents = await this.vault.read(file);\n        let metadata = await this.metadataCache.getFileCache(file);\n        return runImport(file.path, contents, file.stat, metadata as CachedMetadata) as any as T;\n    }\n}\n"
  },
  {
    "path": "__mocks__/obsidian.ts",
    "content": "import EventEmitter from \"events\";\n\n/** Basic obsidian abstraction for any file or folder in a vault. */\nexport abstract class TAbstractFile {\n    /**\n     * @public\n     */\n    vault: Vault;\n    /**\n     * @public\n     */\n    path: string;\n    /**\n     * @public\n     */\n    name: string;\n    /**\n     * @public\n     */\n    parent: TFolder;\n}\n\n/** Tracks file created/modified time as well as file system size. */\nexport interface FileStats {\n    /** @public */\n    ctime: number;\n    /** @public */\n    mtime: number;\n    /** @public */\n    size: number;\n}\n\n/** A regular file in the vault. */\nexport class TFile extends TAbstractFile {\n    stat: FileStats;\n    basename: string;\n    extension: string;\n}\n\n/** A folder in the vault. */\nexport class TFolder extends TAbstractFile {\n    children: TAbstractFile[];\n\n    isRoot(): boolean {\n        return false;\n    }\n}\n\nexport class Vault extends EventEmitter {\n    getFiles() {\n        return [];\n    }\n    trigger(name: string, ...data: any[]): void {\n        this.emit(name, ...data);\n    }\n}\n\nexport class Component {\n    registerEvent() {}\n}\n"
  },
  {
    "path": "cspell.json",
    "content": "{\n  \"version\": \"0.2\",\n  \"ignorePaths\": [],\n  \"dictionaryDefinitions\": [],\n  \"dictionaries\": [],\n  \"words\": [\n    \"aaab\",\n    \"akey\",\n    \"alexfertel\",\n    \"AnnaKornfeldSimpson\",\n    \"apng\",\n    \"artisticat\",\n    \"autorelease\",\n    \"bcbe\",\n    \"bimap\",\n    \"binaryop\",\n    \"bkey\",\n    \"blacksmithgu\",\n    \"Brenan\",\n    \"bryc\",\n    \"callouts\",\n    \"canonicalization\",\n    \"canonicalize\",\n    \"CANONICALIZER\",\n    \"canonicalizes\",\n    \"canonicalizing\",\n    \"carlesalbasboix\",\n    \"cday\",\n    \"charleshan\",\n    \"Cheatsheet\",\n    \"Cheatsheets\",\n    \"Chouffy\",\n    \"clsname\",\n    \"codemirror\",\n    \"combinators\",\n    \"compday\",\n    \"comptime\",\n    \"concat\",\n    \"containsword\",\n    \"crashy\",\n    \"cres\",\n    \"crog\",\n    \"crpg\",\n    \"cssclasses\",\n    \"currencyformat\",\n    \"cyrb\",\n    \"dailys\",\n    \"datapoints\",\n    \"Datarow\",\n    \"Dataview\",\n    \"dataviewjs\",\n    \"Dataviews\",\n    \"datefield\",\n    \"ddd\",\n    \"DDTHH\",\n    \"début\",\n    \"dedup\",\n    \"dformat\",\n    \"dogfood\",\n    \"Donadio\",\n    \"dtformat\",\n    \"duedate\",\n    \"dueday\",\n    \"duetime\",\n    \"durationformat\",\n    \"dvjs\",\n    \"econtains\",\n    \"elink\",\n    \"embeddable\",\n    \"endswith\",\n    \"errorbox\",\n    \"etags\",\n    \"Evals\",\n    \"eyuelt\",\n    \"failable\",\n    \"fastfood\",\n    \"fdefault\",\n    \"fileset\",\n    \"Filetext\",\n    \"Filipe\",\n    \"frontmatter\",\n    \"fullscan\",\n    \"functionname\",\n    \"GamerGirlandCo\",\n    \"gdhjg\",\n    \"gentlegiantJGC\",\n    \"Gott\",\n    \"Groot\",\n    \"helloxx\",\n    \"Hoeven\",\n    \"holroy\",\n    \"iamrecursion\",\n    \"icontains\",\n    \"iden\",\n    \"ifield\",\n    \"iitem\",\n    \"implicits\",\n    \"INDEXEDDB\",\n    \"Ingrouped\",\n    \"Inlines\",\n    \"inlink\",\n    \"inlinks\",\n    \"Jeamee\",\n    \"jfif\",\n    \"Kanban\",\n    \"kometenstaub\",\n    \"ldefault\",\n    \"leoccyao\",\n    \"lezer\",\n    \"Linkpath\",\n    \"localforage\",\n    \"localtime\",\n    \"longkeyidontneedwhenreading\",\n    \"lres\",\n    \"luxon\",\n    \"lwrap\",\n    \"MarioRicalde\",\n    \"matchreg\",\n    \"maxby\",\n    \"mday\",\n    \"meello\",\n    \"meep\",\n    \"Millis\",\n    \"minby\",\n    \"mkdocs\",\n    \"mnaoumov\",\n    \"moba\",\n    \"mobas\",\n    \"mocsa\",\n    \"mt-krainski\",\n    \"mvalues\",\n    \"nestedfield\",\n    \"nonnull\",\n    \"noopener\",\n    \"Nums\",\n    \"offref\",\n    \"onwarn\",\n    \"ooker777\",\n    \"outlink\",\n    \"outlinks\",\n    \"padleft\",\n    \"padright\",\n    \"Pagerow\",\n    \"papaparse\",\n    \"parsimmon\",\n    \"pathlike\",\n    \"pjeby\",\n    \"pjepg\",\n    \"pleh\",\n    \"preact\",\n    \"protofarer\",\n    \"proxied\",\n    \"pymdownx\",\n    \"RaviOnline\",\n    \"rawlink\",\n    \"recurkey\",\n    \"Refreshable\",\n    \"REGEXES\",\n    \"regexmatch\",\n    \"regexreplace\",\n    \"regextest\",\n    \"renderable\",\n    \"Repr\",\n    \"RyotaUshio\",\n    \"sandboxed\",\n    \"sandboxing\",\n    \"seanlzx\",\n    \"sheeley\",\n    \"sohanglal\",\n    \"somemetadata\",\n    \"somidad\",\n    \"spoopy\",\n    \"Stardew\",\n    \"startswith\",\n    \"steamid\",\n    \"steg\",\n    \"striptime\",\n    \"subargs\",\n    \"subcontainer\",\n    \"subeval\",\n    \"subfolders\",\n    \"sublists\",\n    \"Subsettings\",\n    \"subsources\",\n    \"subtag\",\n    \"Subtags\",\n    \"subvalue\",\n    \"subword\",\n    \"succ\",\n    \"superfences\",\n    \"tasklist\",\n    \"tcopy\",\n    \"Templater\",\n    \"Testcase\",\n    \"toolbelt\",\n    \"trunc\",\n    \"typeof\",\n    \"tzhou\",\n    \"unindented\",\n    \"unrendered\",\n    \"v_mujunma\",\n    \"vals\",\n    \"vararg\",\n    \"varargs\",\n    \"vectorize\",\n    \"vitaly\",\n    \"vpos\",\n    \"vrtmrz\",\n    \"weekyear\",\n    \"whitespaces\",\n    \"xxhello\",\n    \"ymdh\",\n    \"ymdhm\",\n    \"ymdhms\",\n    \"zerollup\",\n    \"Статус\"\n  ],\n  \"ignoreWords\": [],\n  \"import\": [],\n  \"enabled\": true\n}\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "site/\n"
  },
  {
    "path": "docs/docs/annotation/add-metadata.md",
    "content": "# Adding Metadata to your Pages\n\nDataview cannot query all content of your vault. In order to be able to search, filter and display content, this content needs to be **indexed**. Some content is indexed automatically, like bullet points or task lists - so called **Implicit fields**, more on that below - and other data needs to be saved in a metadata **field** to be accessible through dataview. \n\n## What is a \"field\"?\n\nA metadata field is a pair of a **key** and a **value**. The _value_ of a field has a data type (more on that [here](./types-of-metadata.md)) that determines how this field will behave when querying it. \n\nYou can add any number of fields to a **note**, a **list item** or a **task**. \n\n## How do I add fields?\n\nYou can add fields to a **note** in three different ways. How a field look like depends on the way you add it.\n\nOn **tasks or list items**, you will have YAML Frontmatter information available, but won't be able to add them to a specific list item. If you want to add metadata to one list item or task only, use [Inline Fields](#inline-fields).\n\n### Frontmatter\n\nFrontmatter is a common Markdown extension which allows for YAML metadata to be added to the top of a page. It is natively supported by Obsidian and explained in its [official documentation](https://help.obsidian.md/Advanced+topics/YAML+front+matter). All YAML Frontmatter fields will be automatically available as Dataview fields.\n\n```yaml\n    ---\n    alias: \"document\"\n    last-reviewed: 2021-08-17\n    thoughts:\n      rating: 8\n      reviewable: false\n    ---\n```\n\nWith this your note has metadata fields named `alias`, `last-reviewed`, and `thoughts`. Each of these have different **data types**:\n\n- `alias` is a [text](types-of-metadata.md#text), because its wrapped in \"\"\n- `last-reviewed` is a [date](types-of-metadata.md#date), because it follows the ISO date format\n- `thoughts` is a [object](types-of-metadata.md#object) field, because it uses the YAML Frontmatter object syntax\n\nYou could query for this note with the following query:\n\n~~~\n```dataview\nLIST\nWHERE thoughts.rating = 8\n```\n~~~\n\n### Inline Fields\n\nFor those wanting a more natural-looking annotation, Dataview supports \"inline\" fields via a `Key:: Value` syntax that you can use everywhere in your file. This allows you to write your queryable data right where you need it - for example in the middle of a sentence. \n\nIf your inline field has an own line, without any content beforehand, you can write it like this: \n\n```markdown\n# Markdown Page\n\nBasic Field:: Some random Value\n**Bold Field**:: Nice!\n```\n\nAll content after the `::` is the value of the field until the next line break.\n\n!!! hint \"Mind the `::`\"\n    Note that you need to use a double colon `::` between key and value when using inline fields, contrary to YAML Frontmatter fields where one colon is enough. \n\nIf you want to embed metadata inside sentences, or multiple fields on the same line, you can use the bracket syntax and wrap your field in square brackets:\n\n```markdown\nI would rate this a [rating:: 9]! It was [mood:: acceptable].\n```\n\n!!! info \"Fields on list items and tasks\"\n    When you want to annotate a list item, e.g. a task, with metadata, you always need to use the bracket syntax (because the field is not the only information in this line)\n    ```markdown\n    - [ ] Send an mail to David about the deadline [due:: 2022-04-05].\n    ```\n    Bracketed inline fields are the only way to explicitly add fields to specific list items, YAML frontmatter always applies to the whole page (but is also available in context of list items.)\n\nThere is also the alternative parenthesis syntax, which hides the key when\nrendered in Reader mode:\n\n```markdown\nThis will not show the (longKeyIDontNeedWhenReading:: key).\n```\n\nwill render to:\n\n```markdown\nThis will not show the key.\n```\n\nYou can use YAML Frontmatter and Inline fields with all syntax variants together in one file. You do not need to decide for one and can mix them to fit your workflow.\n\n## Field names\n\nImagine you used all the examples for Inline fields you see above in one note, then following metadata would be available to you:\n\n| Metadata Key | Sanitized Metadata key | Value | Data Type of Value |\n| ----------- | ------------------------|----------- | ----------- |\n| `Basic Field` | `basic-field`  | Some random Value | Text |\n| `Bold Field` | `bold-field`  | Nice! | Text |\n| `rating` | - | 9 | Number |\n| `mood` | - | acceptable | Text |\n| `due` | - | Date Object for 2022-04-05 | Date |\n| `longKeyIDontNeedWhenReading` | `longkeyidontneedwhenreading` | key | Text |\n\nLike you can see in the table, if you are using **spaces or capitalized letters** in your metadata key name, dataview will provide you with a **sanitized version** of the key. \n\n**Keys with spaces** cannot be used in a query as-is. You have two possibilities here: Either use the sanitized name, that is always all lowercase and with dashes instead of spaces or use the **row** variable syntax. Find out more [in the FAQ](../resources/faq.md).\n\n**Keys with capitalized letters** can be used as-is, if you wish. The sanitized version allows you to query for a key independent of its capitalization and makes it easier to use: You can query the same field that's, for example, in one file named `someMetadata` and in another `someMetaData` when using the sanitized key `somemetadata`. \n\nIn addition, the **bold field key is missing its formatting tokens**. Even though the `**` used to make it appear bold are part of the key name in the file, they are left out when indexing your note. The same goes for all other built-in formatting, like strike through or italic. This means formatted keys can only be queried without their formatting. This allows you to format the key in context of the note without worrying that you might create different keys for the same type of information. \n\n### Usage of emojis and non-latin characters\n\nYou are not limited to latin characters when naming your metadata fields. You can use all characters available in UTF-8:\n\n```markdown\nNoël:: Un jeu de console\nクリスマス:: 家庭用ゲーム機\n[🎅:: a console game]\n[xmas🎄:: a console game]\n```\n\n**Using emojis as metadata keys** is possible, but it comes with some limitations. When using emojis in field names, you need to put them into square brackets so that dataview recognize them correctly. \nAlso, please be aware when switching the OS (i.e. from Windows to Android), the same emoji could use another character code and you might not find your metadata when querying it.\n\n!!! info \"Task Field Shorthands\"\n    An exception to this are the [shorthand syntax](./metadata-tasks.md#field-shorthands) in Tasks. You can use shorthands without bracketing. Please mind though that this only counts for listed shorthands - every other field (if with emojis or not) need to use the `[key:: value]` syntax.\n\n## Implicit fields\n\nEven if you do not add any metadata explicitly to your note, dataview provides you with a big amount of indexed data out of the box. Some examples for implicit fields are:\n\n- day the file was created (`file.cday`)\n- links in the file (`file.outlinks`)\n- tags in the file (`file.etags`)\n- all list items in the file (`file.lists` and `file.tasks`)\n\nand many more. Available implicit fields differ depending if you look at a page or a list item. Find the full list of available implicit fields on [Metadata on pages](metadata-pages.md) and [Metadata on Tasks and Lists](metadata-tasks.md).\n"
  },
  {
    "path": "docs/docs/annotation/metadata-pages.md",
    "content": "# Metadata on Pages\n\nYou 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).\n\n## Implicit Fields\n\nDataview 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:\n\n| Field Name | Data Type | Description |\n| --------------------- | --------- | ----------- |\n| `file.name` | Text | The file name as seen in Obsidians sidebar. |\n| `file.folder` | Text | The path of the folder this file belongs to. |\n| `file.path` | Text | The full file path, including the files name. |\n| `file.ext` | Text | The extension of the file type; generally `md`. |\n| `file.link` | Link | A link to the file. |\n| `file.size` | Number | The size (in bytes) of the file. |\n| `file.ctime` | Date with Time | The date that the file was created. |\n| `file.cday` | Date | The date that the file was created. |\n| `file.mtime` | Date with Time | The date that the file was last modified. |\n| `file.mday` | Date | The date that the file was last modified. |\n| `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]`. |\n| `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]` |\n| `file.inlinks` | List | A list of all incoming links to this file, meaning all files that contain a link to this file. |\n| `file.outlinks` | List | A list of all outgoing links from this file, meaning all links the file contains. |\n| `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). |\n| `file.tasks` | List | A list of all tasks (I.e., `| [ ] some task`) in this file. |\n| `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. |\n| `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. |\n| `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. |\n| `file.starred` | Boolean | If this file has been bookmarked via the Obsidian Core Plugin \"Bookmarks\". |\n\n## Example page\n\nThis is a small Markdown page which includes both user-defined ways to add metadata:\n\n```markdown\n---\ngenre: \"action\"\nreviewed: false\n---\n# Movie X\n#movies\n\n**Thoughts**:: It was decent.\n**Rating**:: 6\n\n[mood:: okay] | [length:: 2 hours]\n```\n\nIn addition to the values you see here, the page has also all keys listed above available.\n\n### Example Query\n\nYou can query part of the above information with following query, for example:\n\n~~~yaml\n```dataview\nTABLE file.ctime, length, rating, reviewed\nFROM #movies\n```\n~~~\n"
  },
  {
    "path": "docs/docs/annotation/metadata-tasks.md",
    "content": "# Metadata on Tasks and Lists\n\nJust 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):\n\n```markdown\n- [ ] Hello, this is some [metadata:: value]!\n- [X] I finished this on [completion:: 2021-08-15].\n```\n\nTasks and list items are the same data wise, so all your bullet points have all the information described here available, too.\n\n## Field Shorthands\n\nThe [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.\n\n=== \"Example\"\n\n\n=== \"Example\"\n    - [ ] Due this Saturday 🗓️2021-08-29\n    - [x] Completed last Saturday ✅2021-08-22\n    - [ ] I made this on ➕1990-06-14\n    - [ ] Task I can start this weekend 🛫2021-08-29\n    - [x] Task I finished ahead of schedule ⏳2021-08-29 ✅2021-08-22\n\nThere 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:\n\n| Field name | Short hand syntax |\n| ---------- | ----------------- |\n| due | `🗓️YYYY-MM-DD` |\n| completion |  `✅YYYY-MM-DD` |\n| created | `➕YYYY-MM-DD` |\n| start | `🛫YYYY-MM-DD` |\n| scheduled | `⏳YYYY-MM-DD` |\n\nThis means if you want to query for all tasks that are completed 2021-08-22, you'll write:\n\n~~~markdown\n```dataview\nTASK\nWHERE completion = date(\"2021-08-22\")\n```\n~~~\n\nWhich will list both variants - shorthands and textual annotation:\n\n```markdown\n- [x] Completed last Saturday ✅2021-08-22\n- [x] Some Done Task [completion:: 2021-08-22]\n```\n\n## Implicit Fields\n\nAs with pages, Dataview adds a number of implicit fields to each task or list item:\n\n!!! info \"Inheritance of Fields\"\n    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.\n\n\n| Field name | Data Type | Description |\n| ---------- | --------- | ----------- |\n| `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. |\n| `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. |\n| `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. |\n| `fullyCompleted` |  Boolean  | Whether or not this task and **all** of its subtasks are completed. |\n| `text` |  Text  | The plain text of this task, including any metadata field annotations. |\n| `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). |\n| `line` |  Number  | The line of the file this task shows up on. |\n| `lineCount` |  Number  | The number of Markdown lines that this task takes up. |\n| `path` |  Text  | The full path of the file this task is in. Equals to `file.path` for [pages](./metadata-pages.md). |\n| `section` | Link |  Link to the section this task is contained in. |\n| `tags` | List  | Any tags inside the task text. |\n| `outlinks` | List |  Any links defined in this task. |\n| `link` | Link  |  Link to the closest linkable block near this task; useful for making links which go to the task. |\n| `children` | List  | Any subtasks or sublists of this task. |\n| `task` | Boolean  | If true, this is a task; otherwise, it is a regular list element. |\n| `annotated` | Boolean  | True if the task text contains any metadata fields, false otherwise. |\n| `parent` | Number |  The line number of the task above this task, if present; will be null if this is a root-level task. |\n| `blockId` | Text | The block ID of this task / list element, if one has been defined with the `^blockId` syntax; otherwise null. |\n\nWith usage of the [shorthand syntax](#field-shorthands), following additional properties may be available:\n\n- `completion`: The date a task was completed.\n- `due`: The date a task is due, if it has one.\n- `created`: The date a task was created.\n- `start`: The date a task can be started.\n- `scheduled`: The date a task is scheduled to work on.\n\n### Accessing Implicit Fields in Queries\n\nIf 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:\n\n~~~markdown\n```dataview\nTASK\nWHERE !fullyCompleted\n```\n~~~\n\nFor 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:\n\n~~~markdown\n```dataview\nLIST\nWHERE any(file.tasks, (t) => !t.fullyCompleted)\n```\n~~~\n\nThis 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.\n"
  },
  {
    "path": "docs/docs/annotation/types-of-metadata.md",
    "content": "# Field Types\n\nAll fields in dataview have a **type**, which determines how dataview will render, sort, and operate on that field.\nRead more about how to create fields on [\"Adding metadata\"](add-metadata.md) and which information you have automatically available on [metadata on pages](./metadata-pages.md) and [metadata on tasks and lists](./metadata-tasks.md).\n\n## Why does the type matter?\n\nDataview provides [functions](../reference/functions.md) you can use to modify your metadata and allows you to write all sorts of complex queries. Specific functions need specific data types to work correctly. That means the data type of your field determines which functions you can use on these fields and how the functions behave. Furthermore, depending on the type, the output dataview renders can be different.\n\nMost of the time you do not need to worry too much about the type of your fields, but if you want to perform calculations and other magical operations on your data, you should be aware of them.\n\n!!! example \"Different rendering based on type\"\n    If you have this file:\n    ~~~yaml\n    date1:: 2021-02-26T15:15\n    date2:: 2021-04-17 18:00\n\n    ```dataview\n    TABLE date1, date2\n    WHERE file = this.file\n    ```\n    ~~~\n\n    You'll see the following output (depending on your Date + Time Format Setting for dataview): \n\n    | File (1) | date1 | date2 |\n    | -------- | ----- | ----- |\n    | Untitled 2 | 3:15 PM - February 26, 2021 | 2021-04-17 18:00 |\n\n    `date1` is recognized as a **Date** while `date2` is a normal **Text** to dataview, that's why `date1` is parsed differently for you. Find out more on [Dates below](#date). \n\n## Available Field Types\n\nDataview knows several field types to cover common use cases.\n\n### Text\n\nThe default catch-all. If a field doesn't match a more specific type, it is plain text.\n\n```markdown\nExample:: This is some normal text.\n```\n\n!!! hint \"Multiline text\"\n    Multiline text as a value is only possible via YAML Frontmatter and the pipe operator:\n    ```yaml\n    ---\n    poem: |\n      Because I could not stop for Death,\n      He kindly stopped for me;\n      The carriage held but just ourselves\n      And Immortality.\n    author: \"[[Emily Dickinson]]\"\n    title: \"Because I could not stop for Death\"\n    ---\n    ```\n    For inline fields, a line break means the end of the value. \n\n### Number\n\nNumbers like '6' and '3.6'.\n```markdown\nExample:: 6\nExample:: 2.4\nExample:: -80\n```\n\nIn YAML Frontmatter, you write a number without surrounding quotes: \n\n```yaml\n---\nrating: 8\ndescription: \"A nice little horror movie\"\n---\n```\n\n### Boolean\n\nBoolean only knows two values: true or false, as the programming concept.\n\n```markdown\nExample:: true\nExample:: false\n```\n\n### Date\n\nText that matches the ISO8601 notation will be automatically transformed into a date object. [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) follows the format `YYYY-MM[-DDTHH:mm:ss.nnn+ZZ]`. Everything after the month is optional.\n    \n```markdown\nExample:: 2021-04 \nExample:: 2021-04-18\nExample:: 2021-04-18T04:19:35.000\nExample:: 2021-04-18T04:19:35.000+06:30\n```\n\nWhen querying for these dates, you can access properties that give you a certain portion of your date back:\n\n- field.year\n- field.month\n- field.weekyear\n- field.week\n- field.weekday\n- field.day\n- field.hour\n- field.minute\n- field.second\n- field.millisecond\n\nFor example, if you're interested in which month your date lies, you can access it via `datefield.month`:\n\n~~~markdown\nbirthday:: 2001-06-11\n\n```dataview\nLIST birthday\nWHERE birthday.month = date(now).month\n```\n~~~\n\ngives you back all birthdays happening this month. Curious about `date(now)`? Read more about it under [literals](../reference/literals.md#dates).\n\n!!! info \"Displaying of date objects\"\n    Dataview renders date objects in a human readable format, i.e. `3:15 PM - February 26, 2021`. You can adjust how this format looks like in Dataview's Setting under \"General\" with \"Date Format\" and \"Date + Time Format\". If you want to adjust the format in a specific query only, use the [dateformat function](../reference/functions.md#dateformatdatedatetime-string).\n\n### Duration\n\nDurations are text of the form `<time> <unit>`, like `6 hours` or `4 minutes`. Common English abbreviations like\n  `6hrs` or `2m` are accepted. You can specify multiple units in one field, i.e. `6hr 4min`, optionally with comma separator: `6 hours, 4 minutes`\n\n```markdown\nExample:: 7 hours\nExample:: 16days\nExample:: 4min\nExample:: 6hr7min\nExample:: 9 years, 8 months, 4 days, 16 hours, 2 minutes\nExample:: 9 yrs 8 min\n```\n\nFind the complete list of values that are recognized as a duration on [literals](../reference/literals.md#durations). \n\n!!! hint \"Calculations with dates and durations\"\n    Date and Duration types are compatible with each other. This means you can, for example, add durations to a date to produce a new date:\n    ~~~markdown\n    departure:: 2022-10-07T15:15\n    length of travel:: 1 day, 3 hours\n\n    **Arrival**: `= this.departure + this.length-of-travel`\n    ~~~\n\n    and you get back a duration when calculating with dates:\n    ~~~markdown\n    release-date:: 2023-02-14T12:00\n      \n    `= this.release-date - date(now)` until release!!\n    ~~~\n\n    Curious about `date(now)`? Read more about it under [literals](../reference/literals.md#dates).\n\n### Link\n\nObsidian links like `[[Page]]` or `[[Page|Page Display]]`.\n\n```markdown\nExample:: [[A Page]]\nExample:: [[Some Other Page|Render Text]]\n```\n\n!!! info \"Links in YAML Frontmatter\"\n    If you reference a link in frontmatter, you need to quote it, as so: `key: \"[[Link]]\"`. This is default Obsidian-supported behavior. Unquoted links lead to a invalid YAML frontmatter that cannot be parsed anymore. \n    ```yaml\n    ---\n    parent: \"[[parentPage]]\"\n    ---\n    ```\n    Please be aware that this is only a link for dataview, but not for Obsidian anymore - that means it won't show up in the outgoing links, won't be displayed on graph view and won't be updated on i.e. a rename.\n\n### List\n\nLists are multi-value fields. In YAML, these are defined as normal YAML lists: \n```yaml\n---\nkey3: [one, two, three]\nkey4:\n - four\n - five\n - six\n---\n```\n\nIn inline fields, they are comma-separated lists values:\n\n```markdown\nExample1:: 1, 2, 3\nExample2:: \"yes\", \"or\", \"no\"\n```\n\nPlease be aware that in Inline fields, you need to wrap **text values into quotes** to be recognized as a list (see `Example2`). `yes, or, no` is recognized as plain text.\n\n!!! info \"Duplicated metadata keys in the same file lead to lists\"\n    If you're using a metadata key twice or more in the same note, dataview will collect all values and give you a list. For example\n    ~~~markdown\n    grocery:: flour\n    [...]\n    grocery:: soap\n\n    ```dataview\n    LIST grocery\n    WHERE file = this.file\n    ```\n    ~~~\n    will give you a **list** out of `flour` and `soap` back.\n\n!!! hint \"Arrays are lists\"\n    In some places of this documentation, you'll read the term \"array\". Array is the term for lists in Javascript - Lists and Arrays are the same. A function that needs an array as argument needs a list as argument.\n\n### Object\n\nObjects are a map of multiple fields under one parent field. These can only be defined in YAML frontmatter, using the YAML object syntax:\n```yaml\n---\nobj:\n  key1: \"Val\"\n  key2: 3\n  key3: \n    - \"List1\"\n    - \"List2\"\n    - \"List3\"\n---\n```\n\n  In queries, you can then access these child values via `obj.key1` etc:\n\n~~~markdown\n```dataview\nTABLE obj.key1, obj.key2, obj.key3\nWHERE file = this.file\n```\n~~~\n"
  },
  {
    "path": "docs/docs/api/code-examples.md",
    "content": "# Codeblock Examples\n\n## Grouped Books\n\nGroup your books by genre, then create a table for each sorted by rating via a straightforward usage of\nthe dataview rendering API:\n\n```js\nfor (let group of dv.pages(\"#book\").groupBy(p => p.genre)) {\n\tdv.header(3, group.key);\n\tdv.table([\"Name\", \"Time Read\", \"Rating\"],\n\t\tgroup.rows\n\t\t\t.sort(k => k.rating, 'desc')\n\t\t\t.map(k => [k.file.link, k[\"time-read\"], k.rating]))\n}\n```\n\n![Grouped Books Example](../assets/grouped-book-example.png)\n\n## Find All Direct And Indirectly Linked Pages\n\nUse a simple set + stack depth first search to find all notes linked to the current note, or a note of your choosing:\n\n```js\nlet page = dv.current().file.path;\nlet pages = new Set();\n\nlet stack = [page];\nwhile (stack.length > 0) {\n\tlet elem = stack.pop();\n\tlet meta = dv.page(elem);\n\tif (!meta) continue;\n\n\tfor (let inlink of meta.file.inlinks.concat(meta.file.outlinks).array()) {\n\t\tconsole.log(inlink);\n\t\tif (pages.has(inlink.path)) continue;\n\t\tpages.add(inlink.path);\n\t\tstack.push(inlink.path);\n\t}\n}\n\n// Data is now the file metadata for every page that directly OR indirectly links to the current page.\nlet data = dv.array(Array.from(pages)).map(p => dv.page(p));\n```\n"
  },
  {
    "path": "docs/docs/api/code-reference.md",
    "content": "# Codeblock Reference\n\nDataview JavaScript Codeblocks are created using the `dataviewjs` language specification for a codeblock:\n\n~~~\n```dataviewjs\ndv.table([], ...)\n```\n~~~\n\nThe API is available through the implicitly provided `dv` (or `dataview`) variable, through which you can query for\ninformation, render HTML, and configure the view.\n\nAsynchronous API calls are marked with `⌛`.\n\n## Query\nQuery methods allow you to query the Dataview index for page metadata; to render this data, use the methods in the [render section](#render).\n\n### `dv.current()`\n\nGet page information (via `dv.page()`) for the page the script is currently executing on.\n\n### `dv.pages(source)`\n\nTake a single string argument, `source`, which is the same form as a [query language source](../reference/sources.md).\nReturn a [data array](data-array.md) of page objects, which are plain objects with all of the page fields as\nvalues.\n\n```js\ndv.pages() => all pages in your vault\ndv.pages(\"#books\") => all pages with tag 'books'\ndv.pages('\"folder\"') => all pages from folder \"folder\"\ndv.pages(\"#yes or -#no\") => all pages with tag #yes, or which DON'T have tag #no\ndv.pages('\"folder\" or #tag') => all pages with tag #tag, or from folder \"folder\"\n```\n\nNote that folders need to be double-quoted inside the string (i.e., `dv.pages(\"folder\")` does not work, but\n`dv.pages('\"folder\"')` does) - this is to exactly match how sources are written in the query language.\n\n### `dv.pagePaths(source)`\n\nAs with `dv.pages`, but just returns a [data array](data-array.md) of paths of pages that match the given source.\n\n```js\ndv.pagePaths(\"#books\") => the paths of pages with tag 'books'\n```\n\n### `dv.page(path)`\n\nMap a simple path or link to the full page object, which includes all of the pages fields. Automatically does link resolution,\nand will figure out the extension automatically if not present.\n\n```js\ndv.page(\"Index\") => The page object for /Index\ndv.page(\"books/The Raisin.md\") => The page object for /books/The Raisin.md\n```\n\n## Render\n\n### `dv.el(element, text)`\n\nRender arbitrary text in the given html element.\n```js\ndv.el(\"b\", \"This is some bold text\");\n```\n\nYou can specify custom classes to add to the element via `cls`, and additional attributes via `attr`:\n\n```js\ndv.el(\"b\", \"This is some text\", { cls: \"dataview dataview-class\", attr: { alt: \"Nice!\" } });\n```\n\n### `dv.header(level, text)`\n\nRender a header of level 1 - 6 with the given text.\n\n```js\ndv.header(1, \"Big!\");\ndv.header(6, \"Tiny\");\n```\n\n### `dv.paragraph(text)`\n\nRender arbitrary text in a paragraph.\n\n```js\ndv.paragraph(\"This is some text\");\n```\n\n### `dv.span(text)`\n\nRender arbitrary text in a span (no padding above/below, unlike a paragraph).\n\n```js\ndv.span(\"This is some text\");\n```\n\n### `dv.execute(source)`\n\nExecute an arbitrary dataview query and embed the view into the current page.\n\n```js\ndv.execute(\"LIST FROM #tag\");\ndv.execute(\"TABLE field1, field2 FROM #thing\");\n```\n\n### `dv.executeJs(source)`\n\nExecute an arbitrary DataviewJS query and embed the view into the current page.\n\n```js\ndv.executeJs(\"dv.list([1, 2, 3])\");\n```\n\n### `dv.view(path, input)`\n\nComplex function which allows for custom views. Will attempt to load a JavaScript file at the given path, passing it\n`dv` and `input` and allowing it to execute. This allows for you to re-use custom view code across multiple pages. Note\nthat this is an asynchronous function since it involves file I/O - make sure to `await` the result!\n\n\n```js\nawait dv.view(\"views/custom\", { arg1: ..., arg2: ... });\n```\n\nIf you want to also include custom CSS in your view, you can instead pass a path to a folder containing `view.js` and\n`view.css`; the CSS will be added to the view automatically:\n\n```\nviews/custom\n -> view.js\n -> view.css\n```\n\nView scripts have access to the `dv` object (the API object), and an `input` object which is exactly whatever the second\nargument of `dv.view()` was.\n\nBear in mind, `dv.view()` cannot read from directories starting with a dot, like `.views`. Example of an incorrect usage:\n\n```js\nawait dv.view(\".views/view1\", { arg1: 'a', arg2: 'b' });\n```\nAttempting this will yield the following exception:\n\n```\nDataview: custom view not found for '.views/view1/view.js' or '.views/view1.js'.\n```\n\nAlso note, directory paths always originate from the vault root.\n\n#### Example\nIn this example, we have a custom script file named `view1.js` in the `scripts` directory. \n\n**File:** `scripts/view1.js`\n```js\nconsole.log(`Loading view1`);\n\nfunction foo(...args) {\n  console.log('foo is called with args', ...args);\n}\nfoo(input)\n```\n\nAnd we have an Obsidian document located under `projects`. We'll call `dv.view()` from this document using the `scripts/view1.js` path.\n\n**Document:** `projects/customViews.md`\n```js\nawait dv.view(\"scripts/view1\", { arg1: 'a', arg2: 'b' }) \n```\n\nWhen the above script is executed, it will print the following:\n\n```\nLoading view1\nfoo is called with args {arg1: 'a', arg2: 'b'}\n```\n\n## Dataviews\n\n### `dv.list(elements)`\n\nRender a dataview list of elements; accept both vanilla arrays and data arrays.\n\n```js\ndv.list([1, 2, 3]) => list of 1, 2, 3\ndv.list(dv.pages().file.name) => list of all file names\ndv.list(dv.pages().file.link) => list of all file links\ndv.list(dv.pages(\"#book\").where(p => p.rating > 7)) => list of all books with rating greater than 7\n```\n\n### `dv.taskList(tasks, groupByFile)`\n\nRender a dataview list of `Task` objects, as obtained by `page.file.tasks`. By default, this view will automatically\ngroup the tasks by their origin file. If you provide `false` as a second argument explicitly, it will instead render them\nas a single unified list.\n\n```js\n// List all tasks from pages marked '#project'\ndv.taskList(dv.pages(\"#project\").file.tasks)\n\n// List all *uncompleted* tasks from pages marked #project\ndv.taskList(dv.pages(\"#project\").file.tasks\n    .where(t => !t.completed))\n\n// List all tasks tagged with '#tag' from pages marked #project\ndv.taskList(dv.pages(\"#project\").file.tasks\n    .where(t => t.text.includes(\"#tag\")))\n\n// List all tasks from pages marked '#project', without grouping.\ndv.taskList(dv.pages(\"#project\").file.tasks, false)\n```\n\n### `dv.table(headers, elements)`\n\nRenders a dataview table. `headers` is an array of column headers. `elements` is an array of rows. Each row is itself an array of columns. Inside a row, every column which is an array will be rendered with bullet points.\n\n```js\ndv.table(\n\t[\"Col1\", \"Col2\", \"Col3\"],\n\t\t[\n\t\t\t[\"Row1\", \"Dummy\", \"Dummy\"],\n\t\t\t[\"Row2\", \n\t\t\t\t[\"Bullet1\",\n\t\t\t\t \"Bullet2\",\n\t\t\t\t \"Bullet3\"],\n\t\t\t \"Dummy\"],\n\t\t\t[\"Row3\", \"Dummy\", \"Dummy\"]\n\t\t]\n\t);\n```\n\nAn example of how to render a simple table of book info sorted by rating.\n\n```js\ndv.table([\"File\", \"Genre\", \"Time Read\", \"Rating\"], dv.pages(\"#book\")\n    .sort(b => b.rating)\n    .map(b => [b.file.link, b.genre, b[\"time-read\"], b.rating]))\n```\n\n## Markdown Dataviews\n\nFunctions which render to plain Markdown strings which you can then render or manipulate as desired.\n\n### `dv.markdownTable(headers, values)`\n\nEquivalent to `dv.table()`, which renders a table with the given list of headers and 2D array of elements, but\nreturns plain Markdown.\n\n```js\n// Render a simple table of book info sorted by rating.\nconst table = dv.markdownTable([\"File\", \"Genre\", \"Time Read\", \"Rating\"], dv.pages(\"#book\")\n    .sort(b => b.rating)\n    .map(b => [b.file.link, b.genre, b[\"time-read\"], b.rating]))\n\ndv.paragraph(table);\n```\n\n### `dv.markdownList(values)`\n\nEquivalent to `dv.list()`, which renders a list of the given elements, but returns plain Markdown.\n\n```js\nconst markdown = dv.markdownList([1, 2, 3]);\ndv.paragraph(markdown);\n```\n\n### `dv.markdownTaskList(tasks)`\n\nEquivalent to `dv.taskList()`, which renders a task list, but returns plain Markdown.\n\n```js\nconst markdown = dv.markdownTaskList(dv.pages(\"#project\").file.tasks);\ndv.paragraph(markdown);\n```\n \n## Utility\n\n### `dv.array(value)`\n\nConvert a given value or array into a Dataview [data array](data-array.md). If the value is already a data array, returns\nit unchanged.\n\n```js\ndv.array([1, 2, 3]) => dataview data array [1, 2, 3]\n```\n\n### `dv.isArray(value)`\n\nReturns true if the given value is an array or dataview array.\n\n```js\ndv.isArray(dv.array([1, 2, 3])) => true\ndv.isArray([1, 2, 3]) => true\ndv.isArray({ x: 1 }) => false\n```\n\n### `dv.fileLink(path, [embed?], [display-name])`\n\nConverts a textual path into a Dataview `Link` object; you can optionally also specify if the link is embedded as well\nas it's display name.\n\n```js\ndv.fileLink(\"2021-08-08\") => link to file named \"2021-08-08\"\ndv.fileLink(\"book/The Raisin\", true) => embed link to \"The Raisin\"\ndv.fileLink(\"Test\", false, \"Test File\") => link to file \"Test\" with display name \"Test File\"\n```\n\n### `dv.sectionLink(path, section, [embed?], [display?])`\n\nConverts a textual path + section name into a Dataview `Link` object; you can optionally also specify if the link is embedded and\nit's display name.\n\n```js\ndv.sectionLink(\"Index\", \"Books\") => [[Index#Books]]\ndv.sectionLink(\"Index\", \"Books\", false, \"My Books\") => [[Index#Books|My Books]]\n```\n\n### `dv.blockLink(path, blockId, [embed?], [display?])`\n\nConverts a textual path + block ID into a Dataview `Link` object; you\ncan optionally also specify if the link is embedded and it's display name.\n\n```js\ndv.blockLink(\"Notes\", \"12gdhjg3\") => [[Index#^12gdhjg3]]\n```\n\n### `dv.date(text)`\n\nCoerce text and links to luxon `DateTime`; if provided with a `DateTime`, return it unchanged.\n\n```js\ndv.date(\"2021-08-08\") => DateTime for August 8th, 2021\ndv.date(dv.fileLink(\"2021-08-07\")) => dateTime for August 8th, 2021\n```\n\n### `dv.duration(text)`\n\nCoerce text to a luxon `Duration`; uses the same parsing rules as Dataview duration types.\n\n```js\ndv.duration(\"8 minutes\") => Duration { 8 minutes }\ndv.duration(\"9 hours, 2 minutes, 3 seconds\") => Duration { 9 hours, 2 minutes, 3 seconds }\n```\n\n### `dv.compare(a, b)`\n\nCompare two arbitrary JavaScript values according to dataview's default comparison rules; useful if you are writing a\ncustom comparator and want to fall back to the default behavior. Returns a negative value if `a < b`, 0 if `a = b`, and\na positive value if `a > b`.\n\n```js\ndv.compare(1, 2) = -1\ndv.compare(\"yes\", \"no\") = 1\ndv.compare({ what: 0 }, { what: 0 }) = 0\n```\n\n### `dv.equal(a, b)`\n\nCompare two arbitrary JavaScript values and return true if they are equal according to Dataview's default comparison\nrules.\n\n```js\ndv.equal(1, 2) = false\ndv.equal(1, 1) = true\n```\n\n### `dv.clone(value)`\n\nDeep clone any Dataview value, including dates, arrays, and links.\n\n```js\ndv.clone(1) = 1\ndv.clone({ a: 1 }) = { a: 1 }\n```\n\n### `dv.parse(value)`\n\nParse an arbitrary string object into a complex Dataview type\n(mainly supporting links, dates, and durations).\n\n```js\ndv.parse(\"[[A]]\") = Link { path: A }\ndv.parse(\"2020-08-14\") = DateTime { 2020-08-14 }\ndv.parse(\"9 seconds\") = Duration { 9 seconds }\n```\n\n## File I/O\n\nThese utility methods are all contained in the `dv.io` sub-API, and are all *asynchronous* (marked by ⌛).\n\n### ⌛ `dv.io.csv(path, [origin-file])`\n\nLoad a CSV from the given path (a link or string). Relative paths will be resolved relative to the optional origin file (defaulting to the current file if not provided). Return a dataview array, each element containing an object of the CSV values; if the file does not exist, return `undefined`.\n\n```js\nawait dv.io.csv(\"hello.csv\") => [{ column1: ..., column2: ...}, ...]\n```\n\n### ⌛ `dv.io.load(path, [origin-file])`\n\nLoad the contents of the given path (a link or string) asynchronously. Relative paths will be resolved relative to the\noptional origin file (defaulting to the current file if not provided). Returns the string contents of the file, or\n`undefined` if the file does not exist.\n\n```js\nawait dv.io.load(\"File\") => \"# File\\nThis is an example file...\"\n```\n\n### `dv.io.normalize(path, [origin-file])`\n\nConvert a relative link or path into an absolute path. If `origin-file` is provided, then the resolution is doing as if\nyou were resolving the link from that file; if not, the path is resolved relative to the current file.\n\n```js\ndv.io.normalize(\"Test\") => \"dataview/test/Test.md\", if inside \"dataview/test\"\ndv.io.normalize(\"Test\", \"dataview/test2/Index.md\") => \"dataview/test2/Test.md\", irrespective of the current file\n```\n\n## Query Evaluation\n\n### ⌛ `dv.query(source, [file, settings])`\n\nExecute a Dataview query and return the results as a structured return.\nThe return type of this function varies by the query type being executed,\nthough will always be an object with a `type` denoting the return type. This version of `query` returns a result type - you may want `tryQuery`, which instead throws an error on failed query execution.\n\n```javascript\nawait dv.query(\"LIST FROM #tag\") =>\n    { successful: true, value: { type: \"list\", values: [value1, value2, ...] } }\n\nawait dv.query(`TABLE WITHOUT ID file.name, value FROM \"path\"`) =>\n    { successful: true, value: { type: \"table\", headers: [\"file.name\", \"value\"], values: [[\"A\", 1], [\"B\", 2]] } }\n\nawait dv.query(\"TASK WHERE due\") =>\n    { successful: true, value: { type: \"task\", values: [task1, task2, ...] } }\n```\n\n`dv.query` accepts two additional, optional arguments:\n1. `file`: The file path to resolve the query from (in case of references to `this`). Defaults to the current file.\n2. `settings`: Execution settings for running the query. This is largely an advanced use case (where I recommend you\n   directly check the API implementation to see all available options).\n\n### ⌛ `dv.tryQuery(source, [file, settings])`\n\nExactly the same as `dv.query`, but more convenient in short scripts as\nexecution failures will be raised as JavaScript exceptions instead of a\nresult type.\n\n### ⌛ `dv.queryMarkdown(source, [file], [settings])`\n\nEquivalent to `dv.query()`, but returns rendered Markdown.\n\n```js\nawait dv.queryMarkdown(\"LIST FROM #tag\") =>\n    { successful: true, value: { \"- [[Page 1]]\\n- [[Page 2]]\" } }\n```\n\n### ⌛ `dv.tryQueryMarkdown(source, [file], [settings])`\n\nExactly the same as `dv.queryMarkdown()`, but throws an error on parse failure.\n\n### `dv.tryEvaluate(expression, [context])`\n\nEvaluate an arbitrary dataview expression (like `2 + 2` or `link(\"text\")` or `x * 9`); throws an `Error` on parse or\nevaluation failure. `this` is an always-available implicit variable which refers to the current file.\n\n```js\ndv.tryEvaluate(\"2 + 2\") => 4\ndv.tryEvaluate(\"x + 2\", {x: 3}) => 5\ndv.tryEvaluate(\"length(this.file.tasks)\") => number of tasks in the current file\n```\n\n### `dv.evaluate(expression, [context])`\n\nEvaluate an arbitrary dataview expression (like `2 + 2` or `link(\"text\")` or `x * 9`), returning a `Result` object of\nthe result. You can unwrap the result type by checking `result.successful` (and then fetching either `result.value`\nor `result.error`). If you want a simpler API that throws an error on a failed evaluation, use `dv.tryEvaluate`.\n\n```js\ndv.evaluate(\"2 + 2\") => Successful { value: 4 }\ndv.evaluate(\"2 +\") => Failure { error: \"Failed to parse ... \" }\n```\n"
  },
  {
    "path": "docs/docs/api/data-array.md",
    "content": "# Data Arrays\n\nThe general representation of result lists in Dataview is the `DataArray`, which is a [proxied](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) version of the JavaScript array with expanded\nfunctionality. Data arrays support indexing and iteration (via `for` and `for ... of` loops), like normal arrays do, but\nalso include many data manipulation operators like `sort`, `groupBy`, `distinct`, `where`, and so on to make\nmanipulating tabular data easier.\n\n## Creation\n\nData arrays are returned by most Dataview APIs that can return multiple results, such as `dv.pages()`. You can also\nexplicitly convert a normal JavaScript array into a Dataview array using `dv.array(<array>)`. If you want to convert a\nData array back to a normal array, use `DataArray#array()`.\n\n## Indexing and Swizzling\n\nData arrays support regular indexing just like normal arrays (like `array[0]`), but importantly, they also support\nquery-language-style \"swizzling\": if you index into a data array with a field name (like `array.field`), it\nautomatically maps every element in the array to `field`, flattening `field` if it itself is also an array.\n\nFor example, `dv.pages().file.name` will return a data array of all file names in your vault;\n`dv.pages(\"#books\").genres` will return a flattened list of all genres in your books.\n\n## Raw Interface\n\nThe full interface for the data array implementation is provided below for reference:\n\n```ts\n/** A function which maps an array element to some value. */\nexport type ArrayFunc<T, O> = (elem: T, index: number, arr: T[]) => O;\n\n/** A function which compares two types. */\nexport type ArrayComparator<T> = (a: T, b: T) => number;\n\n/**\n * Proxied interface which allows manipulating array-based data. All functions on a data array produce a NEW array\n * (i.e., the arrays are immutable).\n */\nexport interface DataArray<T> {\n    /** The total number of elements in the array. */\n    length: number;\n\n    /** Filter the data array down to just elements which match the given predicate. */\n    where(predicate: ArrayFunc<T, boolean>): DataArray<T>;\n    /** Alias for 'where' for people who want array semantics. */\n    filter(predicate: ArrayFunc<T, boolean>): DataArray<T>;\n\n    /** Map elements in the data array by applying a function to each. */\n    map<U>(f: ArrayFunc<T, U>): DataArray<U>;\n    /** Map elements in the data array by applying a function to each, then flatten the results to produce a new array. */\n    flatMap<U>(f: ArrayFunc<T, U[]>): DataArray<U>;\n    /** Mutably change each value in the array, returning the same array which you can further chain off of. */\n    mutate(f: ArrayFunc<T, any>): DataArray<any>;\n\n    /** Limit the total number of entries in the array to the given value. */\n    limit(count: number): DataArray<T>;\n    /**\n     * Take a slice of the array. If `start` is undefined, it is assumed to be 0; if `end` is undefined, it is assumed\n     * to be the end of the array.\n     */\n    slice(start?: number, end?: number): DataArray<T>;\n    /** Concatenate the values in this data array with those of another iterable / data array / array. */\n    concat(other: Iterable<T>): DataArray<T>;\n\n    /** Return the first index of the given (optionally starting the search) */\n    indexOf(element: T, fromIndex?: number): number;\n    /** Return the first element that satisfies the given predicate. */\n    find(pred: ArrayFunc<T, boolean>): T | undefined;\n    /** Find the index of the first element that satisfies the given predicate. Returns -1 if nothing was found. */\n    findIndex(pred: ArrayFunc<T, boolean>, fromIndex?: number): number;\n    /** Returns true if the array contains the given element, and false otherwise. */\n    includes(element: T): boolean;\n\n    /**\n     * Return a string obtained by converting each element in the array to a string, and joining it with the\n     * given separator (which defaults to ', ').\n     */\n    join(sep?: string): string;\n\n    /**\n     * Return a sorted array sorted by the given key; an optional comparator can be provided, which will\n     * be used to compare the keys in lieu of the default dataview comparator.\n     */\n    sort<U>(key: ArrayFunc<T, U>, direction?: \"asc\" | \"desc\", comparator?: ArrayComparator<U>): DataArray<T>;\n\n    /**\n     * Return an array where elements are grouped by the given key; the resulting array will have objects of the form\n     * { key: <key value>, rows: DataArray }.\n     */\n    groupBy<U>(key: ArrayFunc<T, U>, comparator?: ArrayComparator<U>): DataArray<{ key: U; rows: DataArray<T> }>;\n\n    /**\n     * Return distinct entries. If a key is provided, then rows with distinct keys are returned.\n     */\n    distinct<U>(key?: ArrayFunc<T, U>, comparator?: ArrayComparator<U>): DataArray<T>;\n\n    /** Return true if the predicate is true for all values. */\n    every(f: ArrayFunc<T, boolean>): boolean;\n    /** Return true if the predicate is true for at least one value. */\n    some(f: ArrayFunc<T, boolean>): boolean;\n    /** Return true if the predicate is FALSE for all values. */\n    none(f: ArrayFunc<T, boolean>): boolean;\n\n    /** Return the first element in the data array. Returns undefined if the array is empty. */\n    first(): T;\n    /** Return the last element in the data array. Returns undefined if the array is empty. */\n    last(): T;\n\n    /** Map every element in this data array to the given key, and then flatten it.*/\n    to(key: string): DataArray<any>;\n    /**\n     * Recursively expand the given key, flattening a tree structure based on the key into a flat array. Useful for handling\n     * hierarchical data like tasks with 'subtasks'.\n     */\n    expand(key: string): DataArray<any>;\n\n    /** Run a lambda on each element in the array. */\n    forEach(f: ArrayFunc<T, void>): void;\n\n    /** Calculate the sum of the elements in the array. */\n    sum(): number;\n\n    /** Calculate the average of the elements in the array. */\n    avg(): number;\n\n    /** Calculate the minimum of the elements in the array. */\n    min(): number;\n\n    /** Calculate the maximum of the elements in the array. */\n    max(): number;\n\n    /** Convert this to a plain javascript array. */\n    array(): T[];\n\n    /** Allow iterating directly over the array. */\n    [Symbol.iterator](): Iterator<T>;\n\n    /** Map indexes to values. */\n    [index: number]: any;\n    /** Automatic flattening of fields. Equivalent to implicitly calling `array.to(\"field\")` */\n    [field: string]: any;\n}\n```\n"
  },
  {
    "path": "docs/docs/api/intro.md",
    "content": "# Overview\n\nThe Dataview JavaScript API allows for executing arbitrary JavaScript with access to the dataview indices and query\nengine, which is good for complex views or interop with other plugins. The API comes in two flavors: plugin facing, and\nuser facing (or 'inline API usage').\n\n## Inline Access\n\nYou can create a \"DataviewJS\" block via:\n\n~~~\n```dataviewjs\ndv.pages(\"#thing\")...\n```\n~~~\n\nCode executed in such codeblocks have access to the `dv` variable, which provides the entirety of the codeblock-relevant\ndataview API (like `dv.table()`, `dv.pages()`, and so on). For more information, check out the [codeblock API\nreference](code-reference.md).\n\n## Plugin Access\n\nYou can access the Dataview Plugin API (from other plugins or the console) through `app.plugins.plugins.dataview.api`;\nthis API is similar to the codeblock reference, with slightly different arguments due to the lack of an implicit file\nto execute the queries in. For more information, check out the [Plugin API reference](code-reference.md).\n"
  },
  {
    "path": "docs/docs/changelog.md",
    "content": "# 0.5.70 (Beta)\n\nStill attempting to fix #2557, github is acting up.\n\n\n---\n\n# 0.5.69 (Beta)\n\nAttempting to fix #2557, but uncertain to any side effects.\n\n\n---\n\n# 0.5.68\n\n- Many fixes to the documentation\n- #2318 & co: Various fixes related to _live preview_ rendering of lists\n- New/documented functions for `unique()`, `display()`, `firstvalue()`\n- Added DOM information related to standalone inline fields\n\nThis is the first release done by @holroy, so thanks to him for further developing of _Dataview_. Thank you also to all the people having contributed through PRs and issues.\n\n---\n\n# 0.5.67\n\nIncludes several documentation fixes and several community-contributed bug fixes.\n\n- @reply2za: Fixed inline rendering in the reading view.\n- @carlesalbasboix: Adds sum(), avg(), min(), and max() to data arrays.\n- @mnaoumov: Adds code mirror configuration which code highlights dataviewjs!\n\n---\n\n# 0.5.66\n\nBugfix for version comparisons to fix some other plugins having broken interactions with Dataview.\n\n---\n\n# 0.5.65\n\nA maintenance update which fixes some issues with rendering embeds in Dataviews and adds a few new functions.\n\n- Adds the `hash()` function for generating consistent uniformly-distributed values given arbitrary inputs. Primarily useful for creating \"random\" views which remain consistent across page refreshes. Thanks to @holroy.\n- Adds the `slice()` function for slicing arrays, similar to Javascript's `Array.slice`. Thanks to @holroy.\n- Fixes several issues with rendering embeds inside dataviews. Thanks to @GottZ.\n- Several documentation improvements around tasks - thanks to @holroy and @RaviOnline.\n\n---\n\n# 0.5.64\n\nMore bug fixes for inline field rendering.\n\n\n---\n\n# 0.5.63\n\n- More bugfixes from @RyotaUshio for rendering Markdown paragraphs and other blocks in DataviewJS.\n\n---\n\n# 0.5.62\n\nSeveral more inline field fixes from @RyotaUshio, including more configuration options, fixing inline fields being rendered inside codeblocks, and more. Thanks!\n\n---\n\n# 0.5.61\n\n- @RyotaUshio: Fix several bugs related to the new inline field rendering, including source mode and fixing date formatting.\n\n---\n\n# 0.5.60\n\n- @RyotaUshio: Add explicit rendering of inline fields in live preview. They are much more visually distinct now!\n- @MarioRicalde: Adds `PluginApi#evaluateInline(expression, path)` to the plugin API, which evaluate expressions as if you were on the given page.\n\n---\n\n# 0.5.59\n\n- Fix an issue with the plugin failing to run on iOS due to an esoteric regex issue.\n\n---\n\n# 0.5.58\n\n- Negative durations will now be properly rendered.\n\n---\n\n# 0.5.57\n\nMaintenance patch which bumps many internal dependency versions and which includes approximately ~20 community-contributed PRs which add some new functions, fix some Dataview interactions with properties, and more!\n\n---\n\n# Unreleased\n\n- DQL: Adds new `durationformat(duration, string)` function.\n- DQL: New math rounding functions, `trunc(number)`, `floor(number)`, `ceil(number)`.\n\n# 0.5.56\n\n- Includes some performance fixes on recent versions of Obsidian 1.3+ due to some API changes. Thanks @kometenstaub.\n- Documentation cleanups and improvements by @mocsa, @protofarer, @seanlzx, and @somidad.\n- Adds the new `flat(array)` method for flattening nested arrays, as well as parsing dates using arbitrary formats using\n  `date(text, \"format\")`. Thanks @holroy!\n\n---\n\n# 0.5.55\n\n- Durations are now internationalized using luxon's new internationalization support.\n- Dataviews should now properly render inside Canvas and some other contexts. Thanks @GamerGirlandCo!\n\n---\n\n# 0.5.54\n\n- Regular list items are now also clickable in task views, not just task lines! Thanks to @LilaRest.\n\n---\n\n# 0.5.53\n\n- Fix some documentation issues causing docs to not be updated.\n\n---\n\n# 0.5.52\n\nSubstantial documentation improvements thanks to @s-blu and @AB1908!\n\n- For people concerned about dataviewjs code execution from copy-pasting, @eyuelt has made it possible to change the\n  dataviewjs codeblock prefix.\n- @sohanglal has added some documentation for `task.visual` for changing the visual text of a task.\n- @Chouffy and @Daryl-Horton have fixed some bad documentation links!\n- @vrtmrz swapped the regex used for parsing tags to better match Obsidian's own parser.\n- @alexfertel has added `regextest`, which allows for matching parts of a string instead of the whole thing.\n- @iamrecursion has added more metadata to file links, so they now include section metadata. This may cause some slight\n  visual changes in link views.\n\n---\n\n# 0.5.51 (Beta)\n\n- Allow disabling regular Dataview inline queries via configuration option.\n\n---\n\n# 0.5.50 (Beta)\n\n- Expose dataview EXPRESSION and QUERY parsing to the dataview npm plugin, so others can parse dataview ASTs.\n- Fix documentation issue with `join`.\n\n---\n\n# 0.5.49 (Beta)\n\n- Add the `average` function to compute averages of lists (`average([list of things])`).\n- Added documentation for `average`, `min`, `max`, `minby`, and `maxby` functions.\n- Fixed the broken `nonnull` function and documented it.\n\n---\n\n# 0.5.48 (Beta)\n\nWe're back to more regular beta releases while I trial out new functionality!\n\n- Fixed broken list behavior for `dv.markdownTaskList`.\n- @GamerGirlandCo: Better handling of block IDs when checking off tasks!\n- @s-blu and @AB1908: Lots of big documentation upgrades! Nice!\n- @leoccyao: More block ID task checking fixes. Should work after this one.\n- Add expression/query parsing to the dataview NPM package.\n- @charleshan: Fix a missing header level in the dataview `dv.header` example.\n\n---\n\n# 0.5.47\n\nImproves `date + duration` behavior when either the date or duration are null.\n\n---\n\n# 0.5.46\n\n- Fix #1412: Fix bad `file.cday` and `file.ctime` comparisons due to wrong timezone being set. Ugh.\n\n---\n\n# 0.5.45\n\n- #1400: Properly use the group by field for the group name.\n- Fix bad table highlighting in some themes.\n\n---\n\n# 0.5.44\n\n- #1404: Fixed dates in non-local timezones parsing incorrectly.\n- Fixed some build non-determinism issues.\n- Swapped to pull requests for adding new functionality, and added some more internal tests.\n\n---\n\n# 0.5.43\n\n- Fix #1366: Better handling of calendar emoji (used as due dates in tasks).\n\n---\n\n# 0.5.42\n\nIt's been over a month since the last release! Anyway, this release bundles several nice user-contributed features:\n\n- @AB1908: Tag queries are now case insensitive.\n- @AB1908: Shift-clicking a link/task to open in a new tab now works properly on Mac.\n- @AB1908: Numerous documentation fixes for clarity and more examples.\n- @AnnaKornfeldSimpson: Additional emoji shorthands for more task fields (finished, due).\n- @ooker777: Documentation improvements for some DataviewJS functions, and the ability to use inline emoji for the\n  completion tracking feature.\n- @mt-krainski: Custom date formats for task completions.\n- @gentlegiantJGC: Better support for nested inline fields (i.e., less crashy).\n\n---\n\n# 0.5.41\n\n- Fix a bad regex doing escaping in markdown tables.\n- Improve async documentation.\n\n---\n\n# 0.5.40\n\nAdds some more documentation about the new markdown functionality.\n\n---\n\n# 0.5.39\n\n- Fixed an issue where checking a task in a task view would check the wrong box visually.\n- Added experimental plugin APIs for querying dataview directly as markdown, and converting dataview results to properly\n  formatted markdown.\n\n---\n\n# 0.5.38\n\n- Some minor documentation improvements.\n- Fix an issue with inline fields rendering out of order. That was a weird bug.\n\n---\n\n# 0.5.37\n\nFixes inline field rendering to once again work for highlighting/links, as well as some other rendering quirks with\ninline queries in codeblocks.\n\n---\n\n# 0.5.36\n\n- Fix a bug when checking if an element is an HTMLElement.\n- Properly include the nice improvements to the file count in tables and lists.\n\n---\n\n# 0.5.35\n\n- Fix #1196, #1176: Re-enable HTML values. This was never a featured I advertised since it was just for some internal\n  hackery, but it appears people just discovered it in DataviewJS queries.\n- Improved initial time to popular queries that use `file.starred`.\n\n---\n\n# 0.5.34\n\n- Fix #1174: Fix indexing with a variable.\n- Fix an issue with the experimental calendar view.\n\n---\n\n# 0.5.33\n\n- Fix a bug with inline views that was introduced in 0.5.32.\n\n---\n\n# 0.5.32\n\nThe Dataview API has been noticeably revamped - there are now approximately twice as many functions available on the\nplugin API as there were before, and some additional utilities have been added to both the plugin and inline API. I\nwill be finishing up the associated new \"extension\" functionality shortly, which will allow:\n\n1. For custom Dataview + DataviewJS functions to be added via plugins.\n2. For custom renderable objects (progress bars, embedded task lists, embedded tables) to be added to any Dataview view via plugins.\n3. For plugins to provide alternative behavior for some dataview functionality (such as integrating task plugins with\n   the dataview task query).\n   \nAs part of the API revamp, it is now possible to programmatically execute Dataview and DataviewJS queries - either for\nusing the existing Dataview query language in your own plugin, or for embedding dataview. The Dataview npm library also\nnow exposes many useful internal Dataview types, including the AST structure for all dataview queries.\n\nI am hoping that cleaning up the Dataview API and making it much more extensible will allow for Dataview [to](to) integrate\nmuch better with existing plugins, and to provide the full power of the in-memory index for plugins. I have been very\ncarefully watching index performance in recent weeks to ensure smooth frontend performance for anyone using the API\n(with a goal of <10ms for most queries).\n\n---\n\n# 0.5.31\n\nTasks now have an `outlinks` list field which includes all links in the task; this can be used for finding tasks with\nlinks in them.\n\n---\n\n# 0.5.30\n\n- Added the `typeof(any)` function in Dataview, which obtains the type of any value for comparison:\n```javascript\ntypeof(\"text\") = \"string\"\ntypeof(1) = \"number\"\ntypeof([1, 2, 3]) = \"array\"\n```\n\n- Added the modulo operator (`%`) for doing integer division remainder. I.e., `14 % 2 = 0` and `14 % 3 = 2`.\n- Fixed some minor spacing issues with lists in tables.\n\n---\n\n# 0.5.29\n\nFix another subtle incompatibility between 0.4.26 and 0.5.29 - if you frequently used empty inline fields (like\n`Key::` with no value), the 0.5+ behavior is now the same as 0.4 behavior and will map such fields to null instead of an\nempty string.\n\nThis may fix a broad variety of \"subtly wrong\" queries that you may have seen after the upgrade.\n\n---\n\n# 0.5.28\n\n- Fix a bug with some more string concatenations and null handling.\n\n---\n\n# 0.5.27\n\nMore performance + correctness bugfixes.\n\n- The parser has been made a little more robust to prevent major indexing issues (or at least recover from them\n  quickly).\n- Several new strange tag variants are now supported.\n- Markdown links are now properly indexed again.\n\nSome DataviewJS performance issues should be resolved now, especially for external plugins using Dataview. This fix\ndoes involve a slight API break w.r.t. what types are wrapped into Dataview Arrays (which provide functions like\n`.where()`). Generally, only Dataview-provided implicits are wrapped in data arrays now; frontmatter and inline fields\nare always now regular JS arrays - use `dv.array()` to explicitly make a data array if you want the advanced querying.\n\n---\n\n# 0.5.26\n\nMore small bugfixes:\n\n- Fix a few small link rendering issues.\n- Tag extraction from tasks now handles punctuation properly.\n- Upgrade luxon (which is embedded in DataviewJS) to 2.4.0.\n\n---\n\n# 0.5.25\n\n- Fix #1147: Fix there being a `#null` tag for files with an empty `tag` or `tags` frontmatter.\n\n---\n\n# 0.5.24\n\nSeveral bugfixes:\n\n- Nulls are now sorted first rather than last; it's generally good practice to explicitly check for nulls in your\n  queries to avoid strange behavior.\n- Dataview now properly parses space-delimited tags (like `tags: abc def ghi`).\n- Dataview now supports dropping the entire file cache in case of bugs.\n\n---\n\n# 0.5.23\n\n- Fix #1140: Force API objects to be arrays if they are iterables.\n\n---\n\n# 0.5.22\n\n- Fix #1135: Use 'x' instead of 'X' for checkboxes.\n\n---\n\n# 0.5.21\n\nA long-overdue swap from the beta branch to the stable branch. The beta branch should not include any (intended) breaking\nchanges, and has some nice performance improvements that come along with it! Here are the major changes:\n\n- Most views now use React and no longer flicker when updating; this is not the case yet for DataviewJS, which will be\n  getting equivalent treatment in the future.\n- Dataview now caches metadata, so Dataview loads are very fast after the first time you open your vault. Dataview still\n  needs to visit every file when you update the plugin version, so that should be the only times you experience slower\n  load times.\n- A brand new task view backend and query which allows you to filter per-task, rather than per-page! Check the\n  documentation for details, but this broadly means `WHERE` statements now use task properties instead of page\n  properties.\n- Some additional metadata is now available for use - `file.starred`, `file.lists`, and more metadata in\n  `file.tasks`.\n\nThere have been some moderate documentation touch-ups to keep things up to date; I'm still working on a walkthrough for\ncommon Dataview use cases. This review also includes about ~30-40 bugfixes; some new bugs may arise due to internal\nchanges, so please flag them if you encounter them.\n\n---\n\n# 0.5.20 (Beta)\n\nSlight fix to hopefully improve some strange reported cases of bad indexing at startup.\n\n---\n\n# 0.5.19 (Beta)\n\nDataview now uses IndexedDB to cache file metadata, reducing startup time to virtually nothing if you've opened the\nvault before; if you have a small vault (<1000 notes), you may notice a slight improvement, but large vaults and mobile\ndevices will notice a very significant performance improvement to \"first valid paint\". Some other performance parameters\nhave been tuned to hopefully make the default experience better.\n\nA few small bugs related to rendering have also been squashed, including an issue with images being scaled wrongly.\n\n---\n\n# 0.5.18 (Beta)\n\n- Tasks in task views now support alternative task status characters like '!' and '/'; thanks @ebullient.\n- A few documentation nit fixes.\n- Added `DataArray#sortInPlace` for a more efficient mutable sort for niche use cases.\n\n---\n\n# 0.5.17 (Beta)\n\n- Improved behavior when clicking on tasks in the task view; will now properly scroll to the relevant line in long\n  files!\n- Fixed a bug with incorrect counts being displayed in task views.\n- Added `tags` as a field available on task items, so you can now do things like `TASK WHERE contains(tags, \"#tag\")`.\n\n---\n\n# 0.5.16 (Beta)\n\nDataview now tracks initialization and will report when all files have been indexed in the console; you can\nprogrammatically see this via `dataview:index-ready`, or by checking `api.index.initialized`.\n\n---\n\n# 0.5.15 (Beta)\n\n- Add hover highlights to tables to make seeing rows a little easier.\n- Tables and task lists now include counts of the number of results in the headers.\n- Further improved task selection in the task view.\n\n---\n\n# 0.5.14 (Beta)\n\n- Fix task highlighting when not grouping.\n- Remove some spurious console logging.\n- Slightly improve task highlighting behavior when clicking on a task.\n\n---\n\n# 0.5.13 (Beta)\n\nSeveral smaller bugfixes!\n\n- Fix #997: Use the group by field name in the table name.\n- Prevent tons of errors if you incorrectly set the inline query prefix.\n\n---\n\n# 0.5.12 (Beta)\n\nImprove error messages for queries somewhat and get rid of some ugly output.\n\n---\n\n# 0.5.11 (Beta)\n\nAdd detection of tasks inside of block quotes, as well as correctly implement automatic checking and unchecking of these\ntasks.\n\n---\n\n# 0.5.10 (Beta)\n\nAdds the `Dataview: Force Refresh Views` Command (accessible via the Ctrl+P command view) to force current views to\nrefresh immediately.\n\n---\n\n# 0.5.9 (Beta)\n\nAnother fix for due-date related emoji in tasks. I hate emoji.\n\n---\n\n# 0.5.8 (Beta)\n\nFix some issues with infinite loops of tasks due to bad Obsidian metadata (potentially due to being out of date?).\n\n---\n\n# 0.5.7 (Beta)\n\nFix issues with parsing '🗓️2021-08-29' due-date annotations on tasks, as well as an issue with properly extracting\ndue/completed/completed times for use in queries.\n\n---\n\n# 0.5.6 (Beta)\n\nProper release of 0.5.5 plus one additional small improvement:\n\n- Add `duration * number` and `duration / number` operations for manipulation durations numerically.\n\n---\n\n# 0.5.5 (Beta)\n\nMore small features:\n\n- Fix issues with task sorting not doing anything. Sort away!\n- Table headers can now be arbitrary markdown. So you can put things like links in your headers: `TABLE (1 + 2) AS\n  \"[[File]]\".\n- You can now specify the size of an image embed by providing WxH in it's display property: `![[image.png|50x50]]`.\n\n---\n\n# 0.5.4 (Beta)\n\nImproved image rendering for some link types, and adds the `embed(link)` and `embed(link, false)` options to convert\nlinks to/from their embedded equivalents.\n\n---\n\n# 0.5.3 (Beta)\n\nIterative beta which adds a few nice QoL features and fixes some more bugs:\n\n- Internally swapped to a React-based renderer; this should not have a noticeable perf or usability impact, but makes it\n  easier for me to implement complex table/list behaviors.\n- Naming your fields with `AS \"Name\"` is now optional; Dataview will infer the name from the expression automatically.\n  For example, `TABLE 8 + 4, 3 + 6 FROM ...` is now a valid table expression, and the columns will be named `8 + 4` and\n  `3 + 6` respectively.\n- Some issues with array and object rendering were corrected.\n- Error messages on empty dataview results were improved and now show up for all views.\n\nInline images are now rendered correctly in Dataview tables and lists - no more hacky `app://local/` shenanigans!\n\n---\n\n# 0.5.2 (Beta)\n\n- Fix #971: Objects now work properly inside DataviewQL evaluation.\n\n---\n\n# 0.5.1 (Beta)\n\n- Temporarily revert the new task metadata behavior: inline fields in sublists of tasks are added to the page, instead\n  of the task. This behavior is not good, but is compatible with legacy usages of task metadata, which should not break\n  some existing queries.\n    - This behavior will be removed in the future behind a flag.\n- Added the 'visual' field to tasks - if set, tasks render 'visual' instead of their regular text.\n- Fixed `DataArray#mutate()`.\n\n---\n\n# 0.5.0 (Beta)\n\nRe-release of broken release 0.4.23, now hopefully with fixes that make it work on (most) machines. I'll be doing beta\nreleases for a little while until I can confirm the new version is stable; use BRAT\n(https://github.com/TfTHacker/obsidian42-brat) to easily track Dataview beta versions if you are interested in cutting\nedge features.\n\n---\n\n# 0.4.25\n\nFix #867: Create a container div per taskList to allow for multiple task views.\n\n---\n\n# 0.4.24\n\nRe-release of 0.4.23f since Obsidian does not automatically update between non-semver versions.\n\n---\n\n# 0.4.23f\n\nRemove some code which attempted to make tag queries case-insensitive; I'll reimplement this more generally later (it\nconflicts with existing queries which check tags via `contains(file.tags, \"#Tag\")` and similar).\n\n---\n\n# 0.4.23e\n\nMore task bugfixes / improvements, and a fix that caused task metadata to be duplicated.\n\n---\n\n# 0.4.23d\n\nMore inline field list parsing bug fixes. Hopefully we're back to a nice working order!\n\n---\n\n# 0.4.23c\n\nBugfix which adds support for '1)' style lists, as well as a very annoying null issue due to JavaScript being a very\nsad, very sad language.\n\n---\n\n# 0.4.23b\n\nBugfix for bad inlink/outlink computations; links were not being normalized properly so reverse lookups were not\nworking.\n\n---\n\n# 0.4.23\n\nThe Task Update! This release reworks how dataview handles tasks and list items so that they should be much more\nintuitive to use and interact with:\n\n1. **Subtask Support**: Queries now search over all list items, instead of only over root elements. This should make\n   task filtering much more usable, especially if you tend to put tasks under other list items or care specifically\n   about subtasks.\n2. **Multiline Support**: Dataview now understands multi-line tasks and renders/updates them correctly.\n3. **Immediately Navigate to Task**: The new task view, aside from looking a little cleaner than previous views, now\n   immediately navigates to the task in it's original file on click and selects it.\n4. **Grouping Support**: For DataviewJS users, `dv.taskList` now supports grouping (as produced by `groupBy` and the new\n   `groupIn`) natively.\n\nFor DataviewJS users, the task and list representation has changed: `file.tasks` (and the new `file.lists`) contain\nevery single task (including subtasks) in the file, instead of only the root elements. You can return to previous\nbehavior by filtering out tasks with a non-null parent - i.e., `file.tasks.where(task => !task.parent)`. `dv.taskList`\nwill intelligently deal with properly nesting and de-duplicating tasks, so just filter to the tasks you want to render and\nthe API will do the rest.\n\nThis release also includes general backend improvements as we prepare for live-editing in Dataview views, as well as\nseveral community-contributed API improvements:\n\n- `DataArray#groupIn`: For grouping already grouped data, you can now use `array.groupIn(v => ...)`, which will group\n  the innermost (original) data in the array instead of the top level groups. This allows for more easily grouping\n  recursively, such as `dv.pages().groupBy(page => page.file.folder).groupIn(page => page.title)` producing a grouping\n  of folders, then page titles.\n- `substring(string, start[, end])`: The last major missing string function is now available! Take slices of strings.\n- Improved `dv.el()` and other HTML functions - thanks @vitaly.\n- null and undefined entries sort at the end instead of the beginning by default; sorry to those whose code sorts wrong\n  because of this, but it is a better default for most people's use cases.\n- All links are now properly normalized to their full paths, fixing many link comparison edge cases in DataviewJS.\n\nDocumentation additions for the new task functionality will be coming out in the next few days. The next release 0.4.24\nis currently targeting expanded `FROM` query support, basic table view improvements, and general exporting functionality\nfor Dataview. See you then!\n\n---\n\n# 0.4.22\n\nThe @pjeby update! This includes several performance improvements suggested by @pjeby to dramatically improve background\nDataview performance as well as reduce some memory pressure. It also includes some minor bug-fixes and preliminary\nfunctionality:\n\n- Target ES2018 for better Promise support\n- Allow parsing shorthands in `dv.date()`.\n- Add additional metadata to inline field rendering which can be styled.\n- Cleanup events & workers on plugin uninstall, improving the Dataview uninstall/disable/reload experience.\n- Add preliminary `CALENDAR` queries - rendering similar to the obsidian-calendar plugin, see the documentation!\n\nDataview should perform much better on startup and when you have lots of tabs open - thanks again to @pjeby.\n\n---\n\n# 0.4.21\n\nBugfix release which primarily fixes issues that Dataview had with the live preview mode in upcoming Obsidian versions;\nDataview live preview should now be functional. Also includes a number of smaller bugfixes.\n\n- Fix #646: Add `date(yesterday)` to create a date 24 hours ago.\n- Fix #618: Luxon is now available on the dataview API (`dv.luxon`).\n- Fix #510: Add `dv.duration()` for parsing durations.\n- Fix #647: All HTML functions in the DataviewJS API now return their rendered objects.\n- Fix #652: Fix parsing of invalid dates.\n- Fix #629: Fix block link parsing.\n- Fix #601: Timezones are now rendered properly and parsed properly in Dataview dates.\n- PR #637: Add `meta(link)` which allows you to access various metadata about a link itself.\n- Various minor null safety fixes.\n- Dataview now reports it's exact version and build time in logs.\n\n---\n\n# 0.4.20\n\nSome feature work (mostly by other contributors) while I while away at section metadata. May also fix a few bugs!\n\n- Fix #448: You can now use the \"Task Completion Tracking\" option to automatically add completion metadata to tasks\n  which are checked/unchecked through Dataview. Thanks to @sheeley.\n- Add a search bar to documentation. Thanks to @tzhou.\n- Add new date expressions for the start of the week (`date(sow)`), and the end of the week (`date(eow)`). Thanks\n  @Jeamee and @v_mujunma.\n\nSmall minor bugfix / security releases may follow in the near future; otherwise, the next major release will include\nsection and object metadata.\n\n---\n\n# 0.4.19\n\nBugfix release which corrects emoji parsing & localization issues.\n\n- Add `DataArray#into`, which lets you index into objects without flattening.\n- Renamed 'header' to 'section' in task metadata; 'header' will remain around for a few major releases to let people\n  naturally migrate.\n- Fix #487: You no longer need spaces around '*' in expressions.\n- Fix #559: Fix unicode issues in variable canonicalization which was causing problems with non-Latin inline field\n  keys.\n\n## Duration Parsing\n\nYou can now include multiple units in durations: `dur(8 minutes, 4 seconds)` or `dur(2yr8mo12d)`. You can separate\ndurations by commas, or use the abbreviated syntax with/without spaces.\n\n---\n\n# 0.4.18\n\nBugfix release which fixes bad inline field highlighting if '[' and '(' are mixed on the same line in particular orders.\n\n---\n\n# 0.4.17\n\nMinor feature release to patch up more implementation holes.\n\n## Single File Queries\n\nYou can now query from a specific file (instead of just folders and tags) by specifying the full file path:\n\n```\nTASK FROM \"dataview/Test\"\n...\n```\n\nThis is primarily useful for task queries, but will soon be useful for section and object queries in the near future as\nwell.\n\n## Better Inline Field Highlighting\n\nThe CSS for inline field highlighting has been fixed and some compatibility issues improved, so it should work on all\nthemes now instead of only a few.\n\n## dv.el()\n\nDataviewJS now has `dv.el()`, which is like existing functions like `dv.paragraph` and `dv.span` but can create any\nHTML element type; for example:\n\n```js\ndv.el(\"b\", \"Text!\");\ndv.el(\"i\", 18);\n```\n\n---\n\n# 0.4.16\n\nSmall performance release which substantially reduces the impact Dataview has on vault loading times (by spreading out\nfile loading). The Dataview Index is now also eagerly initialized, so plugin consumers of the API can immediately start\nusing it instead of waiting for the `dataview:api-ready` event.\n\n---\n\n# 0.4.15\n\nA simple fix for #537 which properly 'awaits' value rendering in `dv.view()`. Fixes issues with values rendering out of\norder.\n\n---\n\n# 0.4.14\n\nSmall bugfix release.\n\n- Fixes inline field evaluation when using the new fancy highlighting.\n- You can now configure whether task links should show up at the beginning or end of the task (or just disable them)\n  in the \"Task Link Location\" setting.\n- Most setting updates will immediately be applied to existing Dataviews.\n\n---\n\n# 0.4.13\n\nBugfix release which adds fancy rendering to inline-inline fields and includes a few bugfixes.\n\n## Pretty Inline Fields\n\nInline fields of the form `[key:: value]` will now be rendered with fancy new HTML! By default, they are rendered with\nboth the key and value. You can only render the value using parenthesis instead: `(key:: value)`. You can disable\nthis feature in the configuration.\n\nFull-line inline fields (that Dataview has supported for a long time) will gain similar rendering support soon; in the\nmeanwhile, give the new syntax a try!\n\n### Task Linking\n\nTasks now render with a link to the page/section that they are defined in, making `GROUP BY` and custom task\nediting easier to do:\n\n- [ ] A Task. 🔗\n- [ ] Another Task. 🔗\n    - [ ] Some Random Subtask. 🔗\n\nYou can configure the symbol for the link or disable it altogether.\n\n### Improving DataviewJS Posture\n\nI am currently actively looking into improving DataviewJS sandboxing and general security posture. As a first small step\nin this, I have made DataviewJS opt-in instead of opt-out, and added a separate control for Inline DataviewJS. You may\nneed to re-enable it in your settings if you use it.\n\nMore improvements and better JavaScript sandboxing will follow.\n\n---\n\n# 0.4.12-hotfix1\n\nRe-release of 0.4.12 that fixes an important indexing issue.\n\n- Fix #505: Use `completion` instead of `completed` when setting task completion time.\n- Fix #509: Add `startswith` / `endswith` string functions.\n- Fix #488: Add `padleft` and `padright`, and `string`.\n- Fix #506, #512: Fix date comparisons due to a bizarre date zone issue.\n\n---\n\n# 0.4.12\n\nBugfix release following up 0.4.11 which includes a few minor function additions.\n\n- Fix #512: Strange zone issue causing dates to not be equal.\n- Fix #506: Same as #512.\n- Fix #488: Add `padleft` / `padright` functions.\n- Fix #509: Add `startswith` and `endswith` functions.\n- Fix #505: Correctly read completion dates for tasks from `completion`.\n\nThis release also includes improved testing thanks to mocking Obsidian plugin APIs!\n\n---\n\n# 0.4.11\n\nFixes task behavior and adds \"truly inline\" fields!\n\n## Improved Task Behavior\n\nTask queries are now much improved from their primitive foundations - you can now filter, sort, and group them! The FROM\nblock is still page-based, sadly, though you can simply use `WHERE` instead if desired. For example, you can now access\ntask fields like `text`, `line`, or `completed`:\n\n```\nTASK WHERE contains(text, \"#tag\")\nWHERE !completed\nGROUP BY file.folder\n```\n\nThe full list of all available task metadata can be found\n[here](https://blacksmithgu.github.io/obsidian-dataview/data-annotation/#tasks); tasks include all the information\nneeded to uniquely identify them, and automatically inherit all of the metadata from their parent file as well (so you\ncan access `file.name`, for example). You can also annotate tasks with inline fields, as described in the section below.\n\nThere is some additional UX work to be done - primarily on more easily allowing you to navigate to where the task is\ndefined, as well as render tasks in views other than the `TASK` view.  The semantics of how grouping works (to make it\nmore intuitive/useful than it currently is) will likely also be revisited.\n\n## Inline Inline Fields\n\nEarly support for truly inline fields have been added, where you can add metadata in the middle of a sentence. It looks\nsimilar to existing inline field syntax, but with brackets or parenthesis:\n\n```\nI would rate this a [rating:: 6]. It was (thoughts:: acceptable).\n```\n\nImproved rendering for all inline fields is coming in an upcoming update to improve the visual look of these inline\nfields.\n\n\n## Issues\n\n- Fix #496: Fix task `SORT` functionality to do something.\n- Fix #492: Tasks now properly annotated with parent file information.\n- Fix #498: Fix task checking/unchecking logic (which broke due to a change in the task regex...).\n\n---\n\n# Initial\n\nStart of the automatic changelog.\n"
  },
  {
    "path": "docs/docs/friends.md",
    "content": "# Friends of Dataview\n\nA list of plugins which may be helpful for Dataview related workflows:\n\n - [MetaEdit](https://github.com/chhoumann/MetaEdit) - Add or update yaml properties and Dataview fields easily\n\nAnother non-exhaustive list of plugins which use Dataview for some of the heavy-lifting required for their features:\n\n - [Kanban](https://github.com/mgmeyers/obsidian-kanban) - Create markdown-backed Kanban boards in Obsidian\n - [Breadcrumbs](http://publish.obsidian.md/breadcrumbs-docs) - Gives you a way to visualize a custom-built hierarchy in your Obsidian vault\n - [Supercharged Links](https://github.com/mdelobelle/obsidian_supercharged_links) - Allows you to style links in your Obsidian vault based on note metadata\n\nA full list can be found using GitHub's [Dependents](https://github.com/blacksmithgu/obsidian-dataview/network/dependents) feature.\n"
  },
  {
    "path": "docs/docs/index.md",
    "content": "# Overview\n\nDataview is a live index and query engine over your personal knowledge base. You can [**add metadata**](annotation/add-metadata.md) to your notes and **query** them with the [**Dataview Query Language**](queries/structure.md) to list, filter, sort or group your data. Dataview keeps your queries always up to date and makes data aggregation a breeze.\n\nYou could\n\n- Track your sleep by recording it in daily notes, and automatically create weekly tables of your sleep schedule.\n- Automatically collect links to books in your notes, and render them all sorted by rating.\n- Automatically collect pages associated with today's date and show them in your daily note.\n- Find pages with no tags for follow-up, or show pretty views of specifically-tagged pages.\n- Create dynamic views which show upcoming birthdays or events recorded in your notes\n\nand many more things.\n\n!!! hint \"Dataview gives you a fast way to search, display and operate on indexed data in your vault!\"\n\nDataview is highly generic and high performance, scaling up to hundreds of thousands of annotated notes without issue. \n\nIf the built in [query language](queries/structure.md) is insufficient for your purpose, you can run arbitrary\nJavaScript against the [dataview API](api/intro.md) and build whatever utility you might need yourself, right in your notes.\n\n!!! info \"Dataview is about displaying, not editing\"\n    Dataview is meant for displaying and calculating data. It is not meant to edit your notes/metadata and will always leave them untouched (... except if you're checking a [Task](queries/query-types.md#task) through Dataview.)\n\n## How to Use Dataview\n\nDataview consists of two big building blocks: **Data Indexing** and **Data Querying**. \n\n!!! info \"More details on the linked documentation pages\"\n    The following sections should give you a general overview about what you can do with dataview and how. Be sure to visit the linked pages to find out more about the individual parts.\n\n### Data Indexing\n\nDataview operates on metadata in your Markdown files. It cannot read everything in your vault, but only specific data. Some of your content, like tags and bullet points (including tasks), are [available automatically](annotation/add-metadata.md#implicit-fields) in Dataview. You can add other data through **fields**, either on top of your file [per YAML Frontmatter](annotation/add-metadata.md#frontmatter) or in the middle of your content with [Inline Fields](annotation/add-metadata.md#inline-fields) via the `[key:: value]` syntax. Dataview _indexes_ these data to make it available for you to query. \n\n!!! hint \"Dataview indexes [certain information](annotation/add-metadata.md#implicit-fields) like tags and list items and the data you add via fields. Only indexed data is available in a Dataview query!\"\n\nFor example, a file might look like this:\n\n```markdown\n---\nauthor: \"Edgar Allan Poe\"\npublished: 1845\ntags: poems\n---\n\n# The Raven\n\nOnce upon a midnight dreary, while I pondered, weak and weary,\nOver many a quaint and curious volume of forgotten lore—\n```\n\nOr like this:\n\n```markdown\n#poems\n\n# The Raven\n\nFrom [author:: Edgar Allan Poe], written in (published:: 1845)\n\nOnce upon a midnight dreary, while I pondered, weak and weary,\nOver many a quaint and curious volume of forgotten lore—\n```\n\nIn terms of indexed metadata (or what you can query), they are identical, and only differ in their annotation style. How you want to [annotate your  metadata](annotation/add-metadata.md) is up to you and your personal preference. With this file, you'd have the **metadata field** `author` available and everything Dataview provides you [automatically as implicit fields](annotation/metadata-pages.md), like the tag or note title. \n\n!!! attention \"Data needs to be indexed\"\n    In the above example, you _do not_ have the poem itself available in Dataview: It is a paragraph, not a metadata field and not something Dataview indexes automatically. It is not part of Dataviews index, so you won't be able to query it.\n\n### Data Querying\n\nYou can access **indexed data** with the help of **Queries**.\n\nThere are **three different ways** you can write a Query: With help of the [Dataview Query Language](queries/dql-js-inline.md#dataview-query-language-dql), as an [inline statement](queries/dql-js-inline.md#inline-dql) or in the most flexible but most complex way: as a [Javascript Query](queries/dql-js-inline.md#dataview-js). \n\nThe **Dataview Query Language** (**DQL**) gives you a broad and powerful toolbelt to query, display and operate on your data. An [**inline query**](queries/dql-js-inline.md#inline-dql) gives you the possibility to display exactly one indexed value anywhere in your note. You can also do calculations this way. With **DQL** at your hands, you'll be probably fine without any Javascript through your data journey.\n\nA DQL Query consists of several parts:\n\n- Exactly one [**Query Type**](queries/query-types.md) that determines what your Query Output looks like\n- None or one [**FROM statement**](queries/data-commands.md#from) to pick a specific tag or folder (or another [source](reference/sources.md)) to look at\n- None to multiple [**other Data Commands**](queries/data-commands.md) that help you filter, group and sort your wanted output\n\nFor example, a Query can look like this:\n\n~~~markdown\n```dataview\nLIST\n```\n~~~\n\nwhich list all files in your vault. \n\n!!! info \"Everything but the Query Type is optional\"\n    The only thing you need for a valid DQL Query is the Query Type (and on [CALENDAR](queries/query-types.md#calendar)s, a date field.)\n\n A more restricted Query might look like this:\n\n~~~markdown\n```dataview\nLIST\nFROM #poems\nWHERE author = \"Edgar Allan Poe\"\n```\n~~~\n\nwhich lists all files in your vault that have the tag `#poems` and a [field](annotation/add-metadata.md) named `author` with the value `Edgar Allan Poe`. This query would find our example page from above. \n\n`LIST` is only one out of four [Query Types](queries/query-types.md) you can use. For example, with a `TABLE`, we could add some more information to our output: \n\n\n~~~markdown\n```dataview\nTABLE author, published, file.inlinks AS \"Mentions\"\nFROM #poems\n```\n~~~\n\nThis'll give you back a result like:\n\n| File (3) |\tauthor |\tpublished\t| Mentions |\n| -------- | ------- | ---------- | -------- |\n| The Bells |\tEdgar Allan Poe |\t1849 |  |\t\n| The New Colossus |\tEmma Lazarus | 1883\t| - [[Favorite Poems]] |\t\n| The Raven |\tEdgar Allan Poe |\t1845 | - [[Favorite Poems]] |\t\n\nThat's not where the capabilities of dataview end, though. You can also **operate on your data** with help of [**functions**](reference/functions.md). Mind that these operations are only made inside your query - your **data in your files stays untouched**.\n\n~~~markdown\n```dataview\nTABLE author, date(now).year - published AS \"Age in Yrs\", length(file.inlinks) AS \"Counts of Mentions\"\nFROM #poems\n```\n~~~\n\ngives you back\n\n| File (3) |\tauthor |\tAge in Yrs\t| Count of Mentions |\n| -------- | ------- | ---------- | -------- |\n| The Bells\t|  Edgar Allan Poe |\t173 | 0 |\n| The New Colossus\t| Emma Lazarus |\t139 |\t1 |\n| The Raven |\tEdgar Allan Poe |\t177 | 1 |\t\n\n!!! info \"Find more examples [here](resources/examples.md).\"\n\nAs you can see, dataview doesn't only allow you to aggregate your data swiftly and always up to date, it also can help you with operations to give you new insights on your dataset. Browse through the documentation to find out more on how to interact with your data.\n\n Have fun exploring your vault in new ways! \n\n## Resources and Help\n\nThis documentation is not the only place that can help you out on your data journey. Take a look at [Resources and Support](./resources/resources-and-support.md) for a list of helpful pages and videos.\n"
  },
  {
    "path": "docs/docs/queries/data-commands.md",
    "content": "# Data Commands\n\nThe different commands that dataview queries can be made up of. Commands are\nexecuted in order, and you can have duplicate commands (so multiple `WHERE`\nblocks or multiple `GROUP BY` blocks, for example).\n\n## FROM\n\nThe `FROM` statement determines what pages will initially be collected and passed onto the other commands for further\nfiltering. You can select from any [source](../reference/sources.md), which currently means by folder, by tag, or by incoming/outgoing links.\n\n- **Tags**: To select from a tag (and all its subtags), use `FROM #tag`.\n- **Folders**: To select from a folder (and all its subfolders), use `FROM \"folder\"`.\n- **Single Files**: To select from a single file, use `FROM \"path/to/file\"`.\n- **Links**: You can either select links TO a file, or all links FROM a file.\n  - To obtain all pages which link TO `[[note]]`, use `FROM [[note]]`.\n  - To obtain all pages which link FROM `[[note]]` (i.e., all the links in that file), use `FROM outgoing([[note]])`.\n\nYou can compose these filters in order to get more advanced sources using `and` and `or`.\n\n- For example, `#tag and \"folder\"` will return all pages in `folder` and with `#tag`.\n- `[[Food]] or [[Exercise]]` will give any pages which link to `[[Food]]` OR `[[Exercise]]`.\n\nYou can also \"negate\" sources to obtain anything that does NOT match a source using `-`:\n\n- `-#tag` will exclude files which have the given tag.\n- `#tag and -\"folder\"` will only include files tagged `#tag` which are NOT in `\"folder\"`.\n\n## WHERE\n\nFilter pages on fields. Only pages where the clause evaluates to `true` will be yielded.\n\n```\nWHERE <clause>\n```\n\n1. Obtain all files which were modified in the last 24 hours:\n\n    ```sql\n    LIST WHERE file.mtime >= date(today) - dur(1 day)\n    ```\n\n2. Find all projects which are not marked complete and are more than a month old:\n\n    ```sql\n    LIST FROM #projects\n    WHERE !completed AND file.ctime <= date(today) - dur(1 month)\n    ```\n\n## SORT\n\nSorts all results by one or more fields.\n\n```\nSORT date [ASCENDING/DESCENDING/ASC/DESC]\n```\n\nYou 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.\n\n```\nSORT field1 [ASCENDING/DESCENDING/ASC/DESC], ..., fieldN [ASC/DESC]\n```\n\n## GROUP BY\n\nGroup 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.\n\n```\nGROUP BY field\nGROUP BY (computed_field) AS name\n```\n\nIn 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.\nYou can then apply aggregation operators like `sum()` or `flat()` over the resulting array.\n\n## FLATTEN\n\nFlatten an array in every row, yielding one result row per entry in the array.\n\n```\nFLATTEN field\nFLATTEN (computed_field) AS name\n```\n\nFor example, flatten the `authors` field in each literature note to give one row per author:\n\n=== \"Query\"\n    ```sql\n    TABLE authors FROM #LiteratureNote\n    FLATTEN authors\n    ```\n=== \"Output\"\n    |File|authors|\n    |-|-|\n    |stegEnvironmentalPsychologyIntroduction2018 SN|Steg, L.|\n    |stegEnvironmentalPsychologyIntroduction2018 SN|Van den Berg, A. E.|\n    |stegEnvironmentalPsychologyIntroduction2018 SN|De Groot, J. I. M.|\n    |Soap Dragons SN|Robert Lamb|\n    |Soap Dragons SN|Joe McCormick|\n    |smithPainAssaultSelf2007 SN|Jonathan A. Smith|\n    |smithPainAssaultSelf2007 SN|Mike Osborn|\n\nA good use of this would be when there is a deeply nested list that you want to use more easily.\nFor example, `file.lists` or `file.tasks`.\nNote the simpler query though the end results are slightly different (grouped vs non-grouped).\nYou can use a `GROUP BY file.link` to achieve identical results but would need to use `rows.T.text` as described earlier.\n\n```\ntable T.text as \"Task Text\"\nfrom \"Scratchpad\"\nflatten file.tasks as T\nwhere T.text\n```\n\n```\ntable filter(file.tasks.text, (t) => t) as \"Task Text\"\nfrom \"Scratchpad\"\nwhere file.tasks.text\n```\n\n`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()`.\n\n## LIMIT\n\nRestrict the results to at most N values.\n\n```\nLIMIT 5\n```\n\nCommands are processed in the order they are written, so the following sorts the results *after* they have already been limited:\n\n```\nLIMIT 5\nSORT date ASCENDING\n```\n"
  },
  {
    "path": "docs/docs/queries/differences-to-sql.md",
    "content": "<!--\n * @Author: chinesehamburger 2576226012@qq.com\n * @Date: 2024-12-12 14:24:45\n * @LastEditors: chinesehamburger 2576226012@qq.com\n * @LastEditTime: 2024-12-13 16:40:43\n * @FilePath: \\obsidian-dataview\\docs\\docs\\queries\\differences-to-sql.md\n * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE\n-->\n# Dataview Query Language (DQL) and SQL\n\nIf 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.\n\nA DQL query is **executed from top to bottom**, line-by-line. It is more like a computer program than a typical SQL query.\n\nWhen 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'.\n\n## Anatomy of a DQL query\n\nInstead 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.\n\nThe 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.\n\nYou 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:\n\n- 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.\n- 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.\n- 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.\n"
  },
  {
    "path": "docs/docs/queries/dql-js-inline.md",
    "content": "# DQL, JS and Inlines\n\nOnce 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\nallows this in four different ways, all of which are written in codeblocks directly in your Markdown and live-reloaded\nwhen your vault changes.\n\n## Dataview Query Language (DQL)\n\nThe [**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.\n\n!!! warning Differences to SQL\n    If you are familiar with SQL, please read [Differences to SQL](differences-to-sql.md) to avoid confusing DQL with SQL.\n\nYou create a **DQL** query with a codeblock that uses `dataview` as type:\n\n~~~\n```dataview\nTABLE rating AS \"Rating\", summary AS \"Summary\" FROM #games\nSORT rating DESC\n```\n~~~\n\n!!! attention \"Use backticks\"\n    A valid codeblock needs to use backticks (\\`) on start and end (three each). Do not confuse the backtick with the similar looking apostrophe ' !\n\nFind a explanation how to write a DQL Query under the [query language\nreference](structure.md). If you learn better by example, take a look at the [query examples](../resources/examples.md).\n\n## Inline DQL\n\nA 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.\n\n~~~\n`= this.file.name`\n~~~\n\n!!! info \"Change of DQL prefix\"\n    You can change the `=` to another token (like `dv:` or `~`) in Dataviews' settings under \"Codeblock Settings\" > \"Inline Query Prefix\"\n\nInline DQL Queries display **exactly one value** somewhere in the middle of your note. They seamlessly blend into the content of your note:\n\n~~~markdown\nToday is `= date(today)` - `= [[exams]].deadline - date(today)` until exams!\n~~~\n\nwould, for example, render to\n\n~~~markdown\nToday is November 07, 2022 - 2 months, 5 days until exams!\n~~~\n\n**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]].`.\n\n~~~markdown\n`= this.file.name`\n`= this.file.mtime`\n`= this.someMetadataField`\n`= [[secondPage]].file.name`\n`= [[secondPage]].file.mtime`\n`= [[secondPage]].someMetadataField`\n~~~\n\nYou 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.**\n\n~~~markdown\nAssignment due in `= this.due - date(today)`\nFinal paper due in `= [[Computer Science Theory]].due - date(today)`\n\n🏃‍♂️ Goal reached? `= choice(this.steps > 10000, \"YES!\", \"**No**, get moving!\")`\n\nYou 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!\")`\n~~~\n\n## Dataview JS\n\nThe dataview [JavaScript API](../api/intro.md) gives you the full power of JavaScript and provides a DSL for pulling\nDataview data and executing queries, allowing you to create arbitrarily complex queries and views. Similar to the query\nlanguage, you create Dataview JS blocks via a `dataviewjs`-annotated codeblock:\n\n~~~java\n```dataviewjs\nlet pages = dv.pages(\"#books and -#books/finished\").where(b => b.rating >= 7);\nfor (let group of pages.groupBy(b => b.genre)) {\n   dv.header(3, group.key);\n   dv.list(group.rows.file.name);\n}\n```\n~~~\n\nInside of a JS dataview block, you have access to the full dataview API via the `dv` variable. For an explanation of\nwhat you can do with it, see the [API documentation](../api/code-reference.md), or the [API\nexamples](../api/code-examples.md).\n\n!!! attention \"Advanced usage\"\n    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.\n\n## Inline Dataview JS\n\nSimilar to the query language, you can write JS inline queries, which let you embed a computed JS value directly. You\ncreate JS inline queries via inline code blocks:\n\n```\n`$= dv.current().file.mtime`\n```\n\nIn inline DataviewJS, you have access to the `dv` variable, as in `dataviewjs` codeblocks, and can make all of the same calls. The result\nshould be something which evaluates to a JavaScript value, which Dataview will automatically render appropriately.\n\nUnlike 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.\n\n!!! info \"Change of Inline JS prefix\"\n    You can change the `$=` to another token (like `dvjs:` or `$~`) in Dataviews' settings under \"Codeblock Settings\" > \"Javascript Inline Query Prefix\"\n"
  },
  {
    "path": "docs/docs/queries/query-types.md",
    "content": "# Query Types\n\nThe **Query Type** determines how the output of your dataview query looks like. It is the **first and only mandatory** specification you give to a dataview query. There are four available: `LIST`, `TABLE`, `TASK` and `CALENDAR`.\n\nThe Query Type also determines which **information level** a query is executed on. `LIST`, `TABLE` and `CALENDAR` operate at **page level** whereas `TASK` queries operate at the `file.tasks` level. More on that in the `TASK` Query Type.\n\nYou can combine **every Query Type with all available [Data Commands](data-commands.md)** to refine your result set. Read more about the interconnection between Query Types and Data Commands on [How to Use Dataview](../index.md#how-to-use-dataview) and the [structure page](structure.md).\n\n!!! summary \"Query Type\"\n    The Query Type determines the output format of a query. It's the only mandatory information for a query.\n\n## LIST\n\n`LIST` queries output a bullet point list consisting of your file links or the group name, if you decided to [group](data-commands.md#group-by). You can specify up to **one additional information** to output alongside your file or group information.\n\n!!! summary \"Query Type `LIST`\"\n    `LIST` outputs a bullet point list of page links or Group keys. You can specify one additional information to show for each result.\n\nThe simplest LIST query outputs a bullet point list of all files in your vault:\n\n~~~\n```dataview\nLIST\n```\n~~~\n\n**Output**\n\n\n- [Classic Cheesecake](#)\n- [Git Basics](#)\n- [How to fix Git Cheatsheet](#)\n- [League of Legends](#)\n- [Pillars of Eternity 2](#)\n- [Stardew Valley](#)\n- [Dashboard](#)\n\n\nbut you can, of course, use [data commands](data-commands.md) to restrict which pages you want to have listed:\n\n~~~\n```dataview\nLIST\nFROM #games/mobas OR #games/crpg\n```\n~~~\n\n**Output**\n\n- [League of Legends](#)\n- [Pillars of Eternity 2](#)\n\n### Output an additional information\n\nTo add a **additional information** to your query, specify it right after the `LIST` command and before possibly available data commands:\n\n~~~\n```dataview\nLIST file.folder\n```\n~~~\n\n**Output**\n\n\n- [Classic Cheesecake](#): Baking/Recipes\n- [Git Basics](#): Coding\n- [How to fix Git Cheatsheet](#): Coding/Cheatsheets\n- [League of Legends](#): Games\n- [Pillars of Eternity 2](#): Games\n- [Stardew Valley](#): Games/finished\n- [Dashboard](#):\n\nYou can only add **one** additional information, not multiple. But you can **specify a computed value** instead of a plain meta data field, which can contain information of multiple fields:\n\n~~~\n```dataview\nLIST \"File Path: \" + file.folder + \" _(created: \" + file.cday + \")_\"\nFROM \"Games\"\n```\n~~~\n\n**Output**\n\n- [League of Legends](#): File Path: Games _(created: May 13, 2021)_\n- [Pillars of Eternity 2](#): File Path: Games _(created: February 02, 2022)_\n- [Stardew Valley](#): File Path: Games/finished _(created: April 04, 2021)_\n\n### Grouping\n\nA **grouped list** shows their group keys, and only the group keys, by default:\n\n~~~\n```dataview\nLIST\nGROUP BY type\n```\n~~~\n\n**Output**\n\n- game\n- knowledge\n- moc\n- recipe\n- summary\n\nA common use-case on grouped `LIST` queries is to add the file links to the output by specifying them as the additional information:\n\n~~~\n```dataview\nLIST rows.file.link\nGROUP BY type\n```\n~~~\n\n- game:\n    - [Stardew Valley](#)\n    - [League of Legends](#)\n    - [Pillars of Eternity 2](#)\n- knowledge:\n    - [Git Basics](#)\n- moc:\n    - [Dashboard](#)\n- recipe:\n    - [Classic Cheesecake](#)\n- summary:\n    - [How to fix Git Cheatsheet](#)\n\n### LIST WITHOUT ID\n\nIf you don't want the file name or group key included in the list view, you can use `LIST WITHOUT ID`. `LIST WITHOUT ID` works the same as `LIST`, but it does not output the file link or group name if you add an additional information.\n\n~~~\n```dataview\nLIST WITHOUT ID\n```\n~~~\n\n**Output**\n\n\n- [Classic Cheesecake](#)\n- [Git Basics](#)\n- [How to fix Git Cheatsheet](#)\n- [League of Legends](#)\n- [Pillars of Eternity 2](#)\n- [Stardew Valley](#)\n- [Dashboard](#)\n\nIt's the same as `LIST`, because it does not contain an additional information!\n\n~~~\n```dataview\nLIST WITHOUT ID type\n```\n~~~\n\n**Output**\n\n- moc\n- recipe\n- summary\n- knowledge\n- game\n- game\n- game\n\n`LIST WITHOUT ID` can be handy if you want to output computed values, for example.\n\n~~~\n```dataview\nLIST WITHOUT ID length(rows) + \" pages of type \" + key\nGROUP BY type\n```\n~~~\n\n**Output**\n\n- 3 pages of type game\n- 1 pages of type knowledge\n- 1 pages of type moc\n- 1 pages of type recipe\n- 1 pages of type summary\n\n## TABLE\n\nThe `TABLE` query types outputs page data as a tabular view. You can add zero to multiple meta data fields to your `TABLE` query by adding them as a **comma separated list**. You can not only use plain meta data fields as columns, but specify **calculations** as well. Optionally, you can specify a **table header** via the `AS <header>` syntax. Like all other query types, you can refine your result set for your query with [data commands](data-commands.md).\n\n!!! summary \"`TABLE` Query Type\"\n    `TABLE` queries render a tabular view of any number of meta data values or calculations. It is possible to specify column headers via `AS <header>`.\n\n~~~\n```dataview\nTABLE\n```\n~~~\n\n**Output**\n\n| File (7) |\n| ----- |\n| [Classic Cheesecake](#) |\n| [Git Basics](#) |\n| [How to fix Git Cheatsheet](#) |\n| [League of Legends](#) |\n| [Pillars of Eternity 2](#) |\n| [Stardew Valley](#) |\n| [Dashboard](#) |\n\n!!! hint \"Changing the first column header name\"\n    You can change the name of the first column header (by default \"File\" or \"Group\") via the Dataview Settings under Table Settings -> Primary Column Name / Group Column Name.\n    If you want to change the name only for one specific `TABLE` query, have a look at `TABLE WITHOUT ID`.\n\n!!! info \"Disabling Result count\"\n    The first column always shows the result count. If you do not want to get it displayed, you can disable it in Dataview's settings (\"Display result count\", available since 0.5.52).\n\nOf course, a `TABLE` is made for specifying one to multiple additional information:\n\n~~~\n```dataview\nTABLE started, file.folder, file.etags\nFROM #games\n```\n~~~\n\n**Output**\n\n| File (3) | started | file.folder | file.etags |\n| --- | --- | --- | --- |\n| [League of Legends](#)  | \tMay 16, 2021 | \tGames\t | - #games/moba  |\n| [Pillars of Eternity 2](#)  | \tApril 21, 2022 | \tGames\t | - #games/crpg |\n| [Stardew Valley](#) | \tApril 04, 2021 | \tGames/finished\t |  - #games/simulation |\n\n!!! hint \"Implicit fields\"\n    Curious about `file.folder` and `file.etags`? Learn more about [implicit fields on pages](../annotation/metadata-pages.md).\n\n### Custom Column Headers\n\nYou can specify **custom headings** for your columns by using the `AS` syntax:\n\n~~~\n```dataview\nTABLE started, file.folder AS Path, file.etags AS \"File Tags\"\nFROM #games\n```\n~~~\n\n**Output**\n\n| File (3) | started | Path | File Tags |\n| --- | --- | --- | --- |\n| [League of Legends](#) | \tMay 16, 2021 | \tGames\t | - #games/moba  |\n| [Pillars of Eternity 2](#)  | \tApril 21, 2022 | \tGames\t | - #games/crpg |\n| [Stardew Valley](#) | \tApril 04, 2021 | \tGames/finished\t |  - #games/simulation |\n\n!!! info \"Custom headers with spaces\"\n    If you want to use a custom header with spaces, like `File Tags`, you need to wrap it into double quotes: `\"File Tags\"`.\n\nThis is especially useful when you want to use **calculations or expressions as column values**:\n\n~~~\n```dataview\nTABLE\ndefault(finished, date(today)) - started AS \"Played for\",\nfile.folder AS Path,\nfile.etags AS \"File Tags\"\nFROM #games\n```\n~~~\n\n**Output**\n\n| File (3) | Played for | Path | File Tags |\n| --- | --- | --- | --- |\n| [League of Legends](#) | \t1 years, 6 months, 1 weeks | \tGames\t | - #games/moba  |\n| [Pillars of Eternity 2](#)  | \t7 months, 2 days | \tGames\t | - #games/crpg |\n| [Stardew Valley](#) | \t4 months, 3 weeks, 3 days | \tGames/finished\t |  - #games/simulation |\n\n!!! hint \"Calculations and expressions\"\n    Learn more about the capability of computing expressions and calculations under [expressions](../reference/expressions.md) and [functions](../reference/functions.md).\n\n### TABLE WITHOUT ID\n\nIf you don't want the first column (\"File\" or \"Group\" by default), you can use `TABLE WITHOUT ID`. `TABLE WITHOUT ID` works the same as `TABLE`, but it does not output the file link or group name as a first column if you add additional information.\n\nYou can use this if you, for example, output another identifying value:\n\n~~~\n```dataview\nTABLE WITHOUT ID\nsteamid,\nfile.etags AS \"File Tags\"\nFROM #games\n```\n~~~\n\n**Output**\n\n| steamid (3)  | File Tags |\n| --- | --- |\n| 560130 |  - #games/crog  |\n| - |  - #games/moba |\n| 413150 |   - #games/simulation |\n\nAlso, you can use `TABLE WITHOUT ID` if you want to **rename the first column for one specific query**.\n\n~~~\n```dataview\nTABLE WITHOUT ID\nfile.link AS \"Game\",\nfile.etags AS \"File Tags\"\nFROM #games\n```\n~~~\n\n**Output**\n\n| Game (3) | File Tags |\n| --- | --- |\n| [League of Legends](#) |  - #games/moba  |\n| [Pillars of Eternity 2](#)  | - #games/crpg |\n| [Stardew Valley](#) |  - #games/simulation |\n\n!!! info \"Renaming the first column in general\"\n    If you want to rename the first column in all cases, change the name in Dataviews settings under Table Settings.\n\n## TASK\n\nThe `TASK` Query outputs **an interactive list of all tasks in your vault** that match the given [data commands](data-commands.md) (if any). `TASK` queries are special compared to the other Query Types because they do give back **Tasks as results and not pages**. This implies that all [data commands](data-commands.md) operate on **Task level** and makes it possible to granularly filter your tasks i.e. for their status or meta data specified on the task itself.\n\nAlso, `TASK` Queries are the only possibility to **manipulate your files through DQL**. Normally, Dataview does not touch the content of your files; however, if you check a task through a dataview query, it'll get **checked in its original file, too**. In the Dataview Settings under \"Task Settings\", you can opt-in to automatically set a `completion` meta data field when checking a task in dataview. Mind though that this only works if you check the task inside a dataview block.\n\n!!! summary \"`TASK` Query Type\"\n    `TASK` queries render an interactive list of all tasks in your vault. `TASK` Queries are executed on **task level**, not page level, allowing for task-specific filtering. This is the only command in dataview that modifies your original files if interacted with.\n\n~~~\n```dataview\nTASK\n```\n~~~\n\n**Output**\n\n- [ ] Buy new shoes #shopping\n- [ ] Mail Paul about training schedule\n- [ ] Finish the math assignment\n    - [x] Finish Paper 1 [due:: 2022-08-13]\n    - [ ] Read again through chapter 3 [due:: 2022-09-01]\n    - [x] Write a cheatsheet [due:: 2022-08-02]\n    - [ ] Write a summary of chapter 1-4 [due:: 2022-09-12]\n- [x] Hand in physics\n- [ ] Get new pillows for mom #shopping\n- [x] Buy some working pencils #shopping\n\nYou can use [data commands](data-commands.md) like for all other Query Types. Data Commands are executed on task level, making [implicit fields on tasks](../annotation/metadata-tasks.md) directly available.\n\n~~~\n```dataview\nTASK\nWHERE !completed AND contains(tags, \"#shopping\")\n```\n~~~\n\n**Output**\n\n- [ ] Buy new shoes #shopping\n- [ ] Get new pillows for mom #shopping\n\nA common use case for tasks is to **group tasks by their originating file**:\n\n~~~\n```dataview\nTASK\nWHERE !completed\nGROUP BY file.link\n```\n~~~\n\n**Output**\n\n[2022-07-30](#) (1)\n\n- [ ] Finish the math assignment\n    - [ ] Read again through chapter 3 [due:: 2022-09-01]\n    - [ ] Write a summary of chapter 1-4 [due:: 2022-09-12]\n\n[2022-09-21](#) (2)\n\n- [ ] Buy new shoes #shopping\n- [ ] Mail Paul about training schedule\n\n[2022-09-27](#) (1)\n\n- [ ] Get new pillows for mom #shopping\n\n!!! hint \"Counting tasks with subtask\"\n    Noticing the (1) on the header of `2022-07-30`? Child tasks belong to their parent task and are not counted separately. Also, they **behave differently** on filtering.\n\n### Child Tasks\n\nA task is considered a **child task** if it is **indented by a tab** and is below an unindented task.\n\n- [ ] clean up the house\n\t- [ ] kitchen\n\t- [x] living room\n\t- [ ] Bedroom [urgent:: true]\n\n\n!!! info \"Children of a bullet point item\"\n    While indented tasks under a bulleted list item are, strictly speaking, also child tasks, Dataview will handle them like normal tasks in most cases.\n\nChild Tasks **belong to their parent**. This means if you're querying for tasks, you'll get child tasks as part of their parent back.\n\n~~~\n```dataview\nTASK\n```\n~~~\n\n**Output**\n\n- [ ] clean up the house\n\t- [ ] kitchen\n\t- [x] living room\n\t- [ ] Bedroom [urgent:: true]\n- [ ] Call the insurance about the car\n- [x] Find out the transaction number\n\nThis specifically means that child task will be part of your result set **as long as the parent matches the query** - even if the child task itself doesn't.\n\n~~~\n```dataview\nTASK\nWHERE !completed\n```\n~~~\n\n**Output**\n\n- [ ] clean up the house\n\t- [ ] kitchen\n\t- [x] living room\n\t- [ ] Bedroom [urgent:: true]\n- [ ] Call the insurance about the car\n\nHere, `living room` does **not match** the query, but is included anyway, because its parent `clean up the house` does match.\n\nMind that you'll get individual children tasks back, if the child matches your predicate but the parent doesn't:\n\n~~~\n```dataview\nTASK\nWHERE urgent\n```\n~~~\n\n**Output**\n\n- [ ] Bedroom [urgent:: true]\n\n## CALENDAR\n\nThe `CALENDAR` Query outputs a monthly based calendar where every result is depicted as a dot on it referring date. The `CALENDAR` is the only Query Type that requires an additional information. This additional information needs to be a [date](../annotation/types-of-metadata.md#date) (or unset) on all queried pages.\n\n!!! summary \"`CALENDAR` Query Type\"\n    The `CALENDAR` Query Types renders a calendar view where every result is represented by a dot on the given meta data field date.\n\n\n\n~~~\n```dataview\nCALENDAR file.ctime\n```\n~~~\n\n**Output**\n\n![](../assets/calendar_query_type.png)\n\nWhile it is possible to use `SORT` and `GROUP BY` in combination with `CALENDAR`, it has **no effect**. Additionally, the calendar query does not render if the given meta data field contains something else than a valid [date](../annotation/types-of-metadata.md#date) (but the field can be empty). To make sure you're only taking valid pages into account, you can filter for valid meta data values:\n\n~~~\n```dataview\nCALENDAR due\nWHERE typeof(due) = \"date\"\n```\n~~~\n"
  },
  {
    "path": "docs/docs/queries/structure.md",
    "content": "# Structure of a Query\n\nDataview offers [multiple ways](dql-js-inline.md) to write queries and the syntax differs for each.\n\nThis page provides information on how to write a **Dataview Query Language** (**DQL**) query. If you're interested in how to write Inline Queries, refer to the [inline section on DQL, JS and Inlines](dql-js-inline.md#inline-dql). You'll find more information about **Javascript Queries** on the [Javascript Reference](../api/intro.md).\n\n**DQL** is a SQL like query language for creating different views or calculations on your data. It\nsupports:\n\n- Choosing an **output format** of your output (the [Query Type](./query-types.md))\n- Fetch pages **from a certain [source](../reference/sources.md)**, i.e. a tag, folder or link\n- **Filtering pages/data** by simple operations on fields, like comparison, existence checks, and so on\n- **Transforming fields** for displaying, i.e. with calculations or splitting up multi-value fields\n- **Sorting** results based on fields\n- **Grouping** results based on fields\n- **Limiting** your result count\n\n!!! warning Differences to SQL\n    If you are familiar with SQL, please read [Differences to SQL](differences-to-sql.md) to avoid confusing DQL with SQL.\n\nLet's have a look at how we can put DQL to use.\n\n## General Format of a DQL Query\n\nEvery query follows the same structure and consists of\n\n- exactly one **Query Type** with zero, one or many [fields](../annotation/add-metadata.md), depending on query type\n- zero or one **FROM** data commands with one to many [sources](../reference/sources.md)\n- zero to many other **data commands** with one to many [expressions](../reference/expressions.md) and/or other infos depending on the data command\n\nAt a high level, a query conforms to the following pattern:\n\n~~~\n```dataview\n<QUERY-TYPE> <fields>\nFROM <source>\n<DATA-COMMAND> <expression>\n<DATA-COMMAND> <expression>\n          ...\n```\n~~~\n\n!!! hint \"Only the Query Type is mandatory.\"\n\nThe following sections will explain the theory in further detail.\n\n## Choose a Output Format\n\nThe output format of a query is determined by its **Query Type**. There are four available:\n\n1. **TABLE**: A table of results with one row per result and one or many columns of field data.\n2. **LIST**: A bullet point list of pages which match the query. You can output one field for each page alongside their file links.\n3. **TASK**: An interactive task list of tasks that match the given query.\n4. **CALENDAR**: A calendar view displaying each hit via a dot on its referred date.\n\nThe Query Type is the **only mandatory command in a query**. Everything else is optional.\n\n!!! attention \"Possibly memory intense examples\"\n    Depending on the size of your vault, executing the following examples can take long and even freeze Obsidian in extreme cases. It's recommended that you specify a `FROM` to restrict the query execution to a specific subset of your vaults' files. See next section.\n\n~~~\nLists all pages in your vault as a bullet point list\n```dataview\nLIST\n```\n\nLists all tasks (completed or not) in your vault\n```dataview\nTASK\n```\n\nRenders a Calendar view where each page is represented as a dot on its creation date.\n```dataview\nCALENDAR file.cday\n```\n\nShows a table with all pages of your vault, their field value of due, the files' tags and an average of the values of multi-value field working-hours\n```dataview\nTABLE due, file.tags AS \"tags\", average(working-hours)\n```\n~~~\n\n!!! info \"Read more about the available Query Types and how to use them [here](./query-types.md).\"\n\n## Choose your source\n\nAdditionally to the Query Types, you have several **Data Commands** available that help you restrict, refine, sort or group your query. One of these query commands is the **FROM** statement. `FROM` takes a [source](../reference/sources.md) or a combination of [sources](../reference/sources.md) as an argument and restricts the query to a set of pages that match your source.\n\nIt behaves differently from the other Data Commands: You can add **zero or one** `FROM` data command to your query, right after your Query Type. You cannot add multiple FROM statements and you cannot add it after other Data Commands.\n\n~~~\nLists all pages inside the folder Books and its sub folders\n```dataview\nLIST\nFROM \"Books\"\n```\n\nLists all pages that include the tag #status/open or #status/wip\n```dataview\nLIST\nFROM #status/open OR #status/wip\n```\n\nLists all pages that have either the tag #assignment and are inside folder \"30 School\" (or its sub folders), or are inside folder \"30 School/32 Homeworks\" and are linked on the page School Dashboard Current To Dos\n```dataview\nLIST\nFROM (#assignment AND \"30 School\") OR (\"30 School/32 Homeworks\" AND outgoing([[School Dashboard Current To Dos]]))\n```\n\n~~~\n\n!!! info \"Read more about `FROM` [here](./data-commands.md#from).\"\n\n## Filter, sort, group or limit results\n\nIn addition to the Query Types and the **Data command** `FROM` that's explained above, you have several other **Data Commands** available that help you restrict, refine, sort or group your query results.\n\nAll data commands except the `FROM` command can be used **multiple times in any order** (as long as they come after the Query Type and `FROM`, if `FROM` is used at all). They'll be executed in the order they are written.\n\nAvailable are:\n\n1. **FROM** like explained [above](#choose-your-source).\n2. **WHERE**: Filter notes based on information **inside** notes, the meta data fields.\n3. **SORT**: Sorts your results depending on a field and a direction.\n4. **GROUP BY**: Bundles up several results into one result row per group.\n5. **LIMIT**: Limits the result count of your query to the given number.\n6. **FLATTEN**: Splits up one result into multiple results based on a field or calculation.\n\n~~~\n\nLists all pages that have a metadata field `due` and where `due` is before today\n```dataview\nLIST\nWHERE due AND due < date(today)\n```\n\nLists the 10 most recently created pages in your vault that have the tag #status/open\n```dataview\nLIST\nFROM #status/open\nSORT file.ctime DESC\nLIMIT 10\n```\n\nLists the 10 oldest and incomplete tasks of your vault as an interactive task list, grouped by their containing file and sorted from oldest to newest file.\n```dataview\nTASK\nWHERE !completed\nSORT created ASC\nLIMIT 10\nGROUP BY file.link\nSORT rows.file.ctime ASC\n```\n\n~~~\n\n!!! info \"Find out more about available [data commands](./data-commands.md).\"\n\n## Examples\n\nFollowing are some example queries. Find more examples [here](../resources/examples.md).\n\n~~~\n```dataview\nTASK\n```\n~~~\n\n~~~\n```dataview\nTABLE recipe-type AS \"type\", portions, length\nFROM #recipes\n```\n~~~\n\n~~~\n```dataview\nLIST\nFROM #assignments\nWHERE status = \"open\"\n```\n~~~\n\n~~~\n```dataview\nTABLE file.ctime, appointment.type, appointment.time, follow-ups\nFROM \"30 Protocols/32 Management\"\nWHERE follow-ups\nSORT appointment.time\n```\n~~~\n\n~~~\n```dataview\nTABLE L.text AS \"My lists\"\nFROM \"dailys\"\nFLATTEN file.lists AS L\nWHERE contains(L.author, \"Surname\")\n```\n~~~\n\n~~~\n```dataview\nLIST rows.c\nWHERE typeof(contacts) = \"array\" AND contains(contacts, [[Mr. L]])\nSORT length(contacts)\nFLATTEN contacts as c\nSORT link(c).age ASC\n```\n~~~\n"
  },
  {
    "path": "docs/docs/reference/expressions.md",
    "content": "# Expressions\n\nDataview query language **expressions** are anything that yields a value:\n\n- all [fields](../annotation/add-metadata.md)\n- all [literals](./literals.md) \n- and computed values, like `field - 9` of [function calls](./functions.md). \n\nBasically, everything that is not a [Query Type](../queries/query-types.md), nor a [data command](../queries/data-commands.md) is an expression.\n\nFor a very high level summary, following is considered an **expression** in DQL:\n\n```\n# Literals\n1                   (number)\ntrue/false          (boolean)\n\"text\"              (text)\ndate(2021-04-18)    (date)\ndur(1 day)          (duration)\n[[Link]]            (link)\n[1, 2, 3]           (list)\n{ a: 1, b: 2 }      (object)\n\n# Lambdas\n(x1, x2) => ...     (lambda)\n\n# References\nfield               (directly refer to a field)\nsimple-field        (refer to fields with spaces/punctuation in them like \"Simple Field!\")\na.b                 (if a is an object, retrieve field named 'b')\na[expr]             (if a is an object or array, retrieve field with name specified by expression 'expr')\nf(a, b, ...)        (call a function called `f` on arguments a, b, ...)\n\n# Arithmetic\na + b               (addition)\na - b               (subtraction)\na * b               (multiplication)\na / b               (division)\na % b               (modulo / remainder of division)\n\n# Comparison\na > b               (check if a is greater than b)\na < b               (check if a is less than b)\na = b               (check if a equals b)\na != b              (check if a does not equal b)\na <= b              (check if a is less than or equal to b)\na >= b              (check if a is greater than or equal to b)\n\n# Strings\n\na + b               (string concatenation)\na * num             (repeat string <num> times)\n\n# Special Operations\n[[Link]].value      (fetch `value` from page `Link`)\n```\n\nMore detailed explanations of each follow.\n\n## Expression Types\n\n### Fields as Expressions\n\nThe simplest expression is one that just directly refers to a field. If you have a field called \"duedate\", then you can\nrefer to it directly by name - `duedate`. \n\n~~~\n```dataview\nTABLE duedate, class, field-with-space\n```\n~~~\n\n!!! info \"Field names with spaces and punctuations\"\n    If the field name has spaces, punctuation, or other non-letter/number characters, then you can refer to it using Dataview's simplified name, which is all lower case with spaces replaced with \"-\". For example, `this is a field` becomes `this-is-a-field`; `Hello!` becomes `hello`, and so on. Read more under [Field names](../annotation/add-metadata.md#field-names)\n\n### Literals\n\nConstant values - things like `1` or `\"hello\"` or `date(som)` (\"start of month\"). There are literals for each data type\nthat dataview supports; read more about them [here](./literals.md).\n\n~~~\n```dataview\nLIST\nWHERE file.name = \"Scribble\"\n```\n~~~\n\n### Arithmetic\n\nYou can use standard arithmetic operators to combine fields: addition (`+`), subtraction (`-`), multiplication (`*`),\nand division (`/`). For example `field1 + field2` is an expression which computes the sum of the two fields.\n\n~~~\n```dataview\nTABLE start, end, (end - start) - dur(8 h) AS \"Overtime\" \nFROM #work\n```\n\n```dataview\nTABLE hrs / 24 AS \"days\"\nFROM \"30 Projects\"\n```\n~~~\n\n### Comparisons\n\nYou can compare most values using the various comparison operators: `<`, `>`, `<=`, `>=`, `=`, `!=`. This yields a\nboolean true or false value which can be used in `WHERE` blocks in queries.\n\n~~~\n```dataview\nLIST\nFROM \"Games\"\nWHERE price > 10\n```\n\n```dataview\nTASK\nWHERE due <= date(today)\n```\n\n```dataview\nLIST\nFROM #homework\nWHERE status != \"done\"\n```\n~~~\n\n!!! hint \"Comparing different types\"\n    Comparing different [data types](../annotation/types-of-metadata.md) with each other can lead to unexpected results. Take the second example: If `due` is not set (neither on page nor task level), it is `null` and `null <= date(today)` returns true, including tasks without any due date. If this is not wanted, add a type check to make sure you're always comparing the same types:\n    ~~~\n    ```dataview\n    TASK\n    WHERE typeof(due) = \"date\" AND due <= date(today)\n    ```\n    ~~~\n    Most often, it is sufficient to check if the meta data is available via `WHERE due AND due <= date(today)`, but checking the type is the safer way to get foreseeable results. \n\n### List/Object Indexing\n\nYou can retrieve data from [lists/arrays](../annotation/types-of-metadata.md#list) via the index operator `list[<index>]`, where `<index>` is any computed expression.\nLists are 0-indexed, so the first element is index 0, the second element is index 1, and so on.\nFor example `list(\"A\", \"B\", \"C\")[0] = \"A\"`.\n\nA similar notation style works for [objects](../annotation/types-of-metadata.md#object).\nYou can use `field[\"nestedfield\"]` to reference fields inside an object or otherwise similarly nested.\nFor example, in the YAML defined below, we can reference `previous` via `episode_metadata[\"previous\"]`.\n```yaml\n---\naliases:\n  - \"ABC\"\ncurrent_episode: \"S01E03\"\nepisode_metadata:\n  previous: \"S01E02\"\n  next: \"S01E04\"\n---\n```\n\nYou can also retrieve data from objects (which map text to data values) also using the index operator, where indexes are now strings/text instead of numbers.\nYou can also use the shorthand `object.<name>`, where `<name>` is the name of the value to retrieve.\nFor the previous frontmatter example, we could also use `episode_metadata.previous` to obtain the same value.\n\nIndex expressions also work on objects which have fields that are not directly supported by the query language.\nA good example is `where`, since it is a keyword.\nIf your frontmatter/metadata contains a field `where`, you can reference it via the `row` syntax: `row[\"where\"]`.\nSee the [note in the FAQ](../resources/faq.md#how-do-i-use-fields-with-the-same-name-as-keywords-like-from-where) and [the corresponding issue](https://github.com/blacksmithgu/obsidian-dataview/issues/1164) for further information.\n\n~~~\n```dataview\nTABLE id, episode_metadata.next, aliases[0]\n```\n~~~\n\n### Function Calls\n\nDataview supports various functions for manipulating data, which are described in full in the [functions\ndocumentation](functions.md). They have the general syntax `function(arg1, arg2, ...)` - i.e., `lower(file.name)` or\n`regexmatch(\"A.+\", file.folder)`.\n\n~~~\n```dataview\nLIST\nWHERE contains(file.name, \"WIP\")\n```\n\n```dataview\nLIST\nWHERE string(file.day.year) = split(this.file.name, \"-W\")[0]\n```\n~~~\n\n### Lambdas\n\nLambdas are advanced literals which let you define a function that takes some number of inputs, and produces an output.\nThey have the general form:\n\n```\n(arg1, arg2, arg3, ...) => <expression using args>\n```\n\nLambdas are used in several advanced operators like `reduce` and `map` to allow for complex transformations of data. A\nfew examples:\n\n```\n(x) => x.field                  (return field of x, often used for map)\n(x, y) => x + y                 (sum x and y)\n(x) => 2 * x                    (double x)\n(value) => length(value) = 4    (return true if value is length 4)\n```\n\n~~~\n```dataview\nCALENDAR file.day\nFLATTEN all(map(file.tasks, (x) => x.completed)) AS \"allCompleted\"\nWHERE !allCompleted\n```\n~~~\n\n---\n\n## Type-specific Interactions & Values\n\nMost dataview types have special interactions with operators, or have additional fields that can be retrieved using the\nindex operator. This is true for [dates](../annotation/types-of-metadata.md#date) and [durations](../annotation/types-of-metadata.md#duration) and as well for links. Read more about date and durations on their respective section in [Types of Metadata](../annotation/types-of-metadata.md).\n\n### Links\n\nYou can \"index through\" a link to get values on the corresponding page. For example `[[Assignment Math]].duedate` would get the value\n`duedate` from page `Assignment Math`.\n\n!!! note \"Link Indexing in Expressions\"\n    If your link is a field that you defined in an inline field or in front-matter, like `Class:: [[Math]]` and you want to get the field `timetable`, then you\n    index into it by writing `Class.timetable`.\n    Using `[[Class]].timetable` would look up the page literally called `Class`, and not `Math`!\n"
  },
  {
    "path": "docs/docs/reference/functions.md",
    "content": "# Functions\n\nDataview functions provide more advanced ways to manipulate data. You can use functions **in [data commands](../queries/data-commands.md)** (except FROM) to filter or group or use them **as [additional information](../queries/query-types.md)** like TABLE columns or extra output for LIST queries to see your data in a new light.\n\n## How functions work\n\nFunctions are another form of [expression](expressions.md) and can be used everywhere you can use an expression. A function always gives you back a new value and follows this format:\n\n```\nfunctionname(parameter1, parameter2)\n```\n\nParameters are again [expressions](expressions.md) and you can use literals, meta data fields, or even another function as parameter. You'll find out which [data type](../annotation/types-of-metadata.md) your parameters need to have on the documentation of this page. Pay attention to the information inside the function brackets. Parameters in square brackets, i.e. `link(path, [display])` means they are *optional* and can be omitted. Find out more about the default behavior of each function on their explanation.\n\n## Calling functions on lists of values\n\nMost functions can be applied either to single values (like `number`, `string`, `date`, etc.) OR to lists of those\nvalues. If a function is applied to a list, it also returns a list after the function is applied to each element\nin the list. For example:\n\n```js\nlower(\"YES\") = \"yes\"\nlower([\"YES\", \"NO\"]) = [\"yes\", \"no\"]\n\nreplace(\"yes\", \"e\", \"a\") = \"yas\"\nreplace([\"yes\", \"ree\"], \"e\", \"a\") = [\"yas\", \"raa\"]\n```\n\nThis so-called \"function vectorization\" will not be mentioned explicitly on the following definitions and is possible for a wide range of these functionalities implicitly.\n\n## Constructors\n\nConstructors which create values.\n\n### `object(key1, value1, ...)`\n\nCreates a new object with the given keys and values. Keys and values should alternate in the call, and keys should\nalways be strings/text.\n\n```js\nobject() => empty object\nobject(\"a\", 6) => object which maps \"a\" to 6\nobject(\"a\", 4, \"c\", \"yes\") => object which maps a to 4, and c to \"yes\"\n```\n\n### `list(value1, value2, ...)`\n\nCreates a new list with the given values in it. `array` can be used an alias for `list`.\n\n```js\nlist() => empty list\nlist(1, 2, 3) => list with 1, 2, and 3\narray(\"a\", \"b\", \"c\") => list with \"a\", \"b\", and \"c\"\n```\n\n### `date(any)`\n\nParses a date from the provided string, date, or link object, if possible, returning null otherwise.\n\n```js\ndate(\"2020-04-18\") = <date object representing April 18th, 2020>\ndate([[2021-04-16]]) = <date object for the given page, referring to file.day>\n```\n\n### `date(text, format)`\n\nParses a date from text to luxon `DateTime` with the specified format. Note localized formats might not work.\nUses [Luxon tokens](https://moment.github.io/luxon/#/formatting?id=table-of-tokens).\n\n```js\ndate(\"12/31/2022\", \"MM/dd/yyyy\") => DateTime for December 31th, 2022\ndate(\"210313\", \"yyMMdd\") => DateTime for March 13th, 2021\ndate(\"946778645000\", \"x\") => DateTime for \"2000-01-02T03:04:05\"\n```\n\n### `dur(any)`\n\nParses a duration from the provided string or duration, returning null on failure.\n\n```js\ndur(8 minutes) = <8 minutes>\ndur(\"8 minutes, 4 seconds\") = <8 minutes, 4 seconds>\ndur(dur(8 minutes)) = dur(8 minutes) = <8 minutes>\n```\n\n### `number(string)`\n\nPulls the first number out of the given string, returning it if possible. Returns null if there are no numbers in the\nstring.\n\n```js\nnumber(\"18 years\") = 18\nnumber(34) = 34\nnumber(\"hmm\") = null\n```\n\n### `string(any)`\n\nConverts any value into a \"reasonable\" string representation. This sometimes produces less pretty results than just directly using\nthe value in a query - it is mostly useful for coercing dates, durations, numbers, and so on into strings for\nmanipulation.\n\n```js\nstring(18) = \"18\"\nstring(dur(8 hours)) = \"8 hours\"\nstring(date(2021-08-15)) = \"August 15th, 2021\"\n```\n\n### `link(path, [display])`\n\nConstruct a link object from the given file path or name. If provided with two arguments, the second argument is the\ndisplay name for the link.\n\n```js\nlink(\"Hello\") => link to page named 'Hello'\nlink(\"Hello\", \"Goodbye\") => link to page named 'Hello', displays as 'Goodbye'\n```\n\n### `embed(link, [embed?])`\n\nConvert a link object into an embedded link; support for embedded links is somewhat spotty in Dataview views, though\nembedding of images should work.\n\n```js\nembed(link(\"Hello.png\")) => embedded link to the \"Hello.png\" image, which will render as an actual image.\n```\n\n### `elink(url, [display])`\n\nConstruct a link to an external url (like `www.google.com`). If provided with two arguments, the second\nargument is the display name for the link.\n\n```js\nelink(\"www.google.com\") => link element to google.com\nelink(\"www.google.com\", \"Google\") => link element to google.com, displays as \"Google\"\n```\n\n### `typeof(any)`\n\nGet the type of any object for inspection. Can be used in conjunction with other operators to change behavior based on type.\n\n```js\ntypeof(8) => \"number\"\ntypeof(\"text\") => \"string\"\ntypeof([1, 2, 3]) => \"array\"\ntypeof({ a: 1, b: 2 }) => \"object\"\ntypeof(date(2020-01-01)) => \"date\"\ntypeof(dur(8 minutes)) => \"duration\"\n```\n\n---\n\n## Numeric Operations\n\n### `round(number, [digits])`\n\nRound a number to a given number of digits. If the second argument is not specified, rounds to the nearest whole number;\notherwise, rounds to the given number of digits.\n\n```js\nround(16.555555) = 17\nround(16.555555, 2) = 16.56\n```\n\n### `trunc(number)`\n\nTruncates (\"cuts off\") the decimal point from a number.\n\n```js\ntrunc(12.937) = 12\ntrunc(-93.33333) = -93\ntrunc(-0.837764) = 0\n```\n\n### `floor(number)`\n\nAlways rounds down and returns the largest integer less than or equal to a given number.\nThis means that negative numbers become more negative.\n\n```js\nfloor(12.937) = 12\nfloor(-93.33333) = -94\nfloor(-0.837764) = -1\n```\n\n### `ceil(number)`\n\nAlways rounds up and returns the smallest integer greater than or equal to a given number.\nThis means negative numbers become less negative.\n\n```js\nceil(12.937) = 13\nceil(-93.33333) = -93\nceil(-0.837764) = 0\n```\n\n### `min(a, b, ..)`\n\nCompute the minimum value of a list of arguments, or an array.\n\n```js\nmin(1, 2, 3) = 1\nmin([1, 2, 3]) = 1\n\nmin(\"a\", \"ab\", \"abc\") = \"a\"\n```\n\n### `max(a, b, ...)`\n\nCompute the maximum value of a list of arguments, or an array.\n\n```js\nmax(1, 2, 3) = 3\nmax([1, 2, 3]) = 3\n\nmax(\"a\", \"ab\", \"abc\") = \"abc\"\n```\n\n### `sum(array)`\n\nSums all numeric values in the array. If you have null values in your sum, you can eliminate them via the `nonnull` function.\n\n```js\nsum([1, 2, 3]) = 6\nsum([]) = null\n\nsum(nonnull([null, 1, 8])) = 9\n```\n\n### `product(array)`\n\nCalculates the product of a list of numbers. If you have null values in your average, you can eliminate them via the `nonnull` function.\n\n```js\nproduct([1,2,3]) = 6\nproduct([]) = null\n\nproduct(nonnull([null, 1, 2, 4])) = 8\n```\n\n### `reduce(array, operand)`\n\nA generic function to reduce a list into a single value, valid operands are `\"+\"`, `\"-\"`, `\"*\"`, `\"/\"` and the boolean operands `\"&\"` and `\"|\"`. Note that using `\"+\"` and `\"*\"` equals the `sum()` and `product()` functions, and using `\"&\"` and `\"|\"` matches `all()` and `any()`.\n\n```js\nreduce([100, 20, 3], \"-\") = 77\nreduce([200, 10, 2], \"/\") = 10\nreduce(values, \"*\") = Multiplies every element of values, same as product(values)\nreduce(values, this.operand) = Applies the local field operand to each of the values\nreduce([\"⭐\", 3], \"*\") = \"⭐⭐⭐\", same as \"⭐\" * 3\n\nreduce([1]), \"+\") = 1, has the side effect of reducing the list into a single element\n```\n\n### `average(array)`\n\nComputes the numeric average of numeric values. If you have null values in your average, you can eliminate them via the\n`nonnull` function.\n\n```js\naverage([1, 2, 3]) = 2\naverage([]) = null\n\naverage(nonnull([null, 1, 2])) = 1.5\n```\n\n### `minby(array, function)`\n\nCompute the minimum value of an array, using the provided function.\n\n```js\nminby([1, 2, 3], (k) => k) = 1\nminby([1, 2, 3], (k) => 0 - k) => 3\n\nminby(this.file.tasks, (k) => k.due) => (earliest due)\n```\n\n### `maxby(array, function)`\n\nCompute the maximum value of an array, using the provided function.\n\n```js\nmaxby([1, 2, 3], (k) => k) = 3\nmaxby([1, 2, 3], (k) => 0 - k) => 1\n\nmaxby(this.file.tasks, (k) => k.due) => (latest due)\n```\n\n--\n\n## Objects, Arrays, and String Operations\n\nOperations that manipulate values inside of container objects.\n\n### `contains()` and friends\n\nFor a quick summary, here are some examples:\n\n```js\ncontains(\"Hello\", \"Lo\") = false\ncontains(\"Hello\", \"lo\") = true\n\nicontains(\"Hello\", \"Lo\") = true\nicontains(\"Hello\", \"lo\") = true\n\necontains(\"Hello\", \"Lo\") = false\necontains(\"Hello\", \"lo\") = true\necontains([\"this\",\"is\",\"example\"], \"ex\") = false\necontains([\"this\",\"is\",\"example\"], \"is\") = true\n```\n\n#### `contains(object|list|string, value)`\n\nChecks if the given container type has the given value in it. This function behave slightly differently based on whether\nthe first argument is an object, a list, or a string.\nThis function is case-sensitive.\n\n- For objects, checks if the object has a key with the given name. For example,\n    ```\n    contains(file, \"ctime\") = true\n    contains(file, \"day\") = true (if file has a date in its title, false otherwise)\n    ```\n- For lists, checks if any of the array elements equals the given value. For example,\n    ```\n    contains(list(1, 2, 3), 3) = true\n    contains(list(), 1) = false\n    ```\n- For strings, checks if the given value is a substring (i.e., inside) the string.\n    ```\n    contains(\"hello\", \"lo\") = true\n    contains(\"yes\", \"no\") = false\n    ```\n\n#### `icontains(object|list|string, value)`\n\nCase insensitive version of `contains()`.\n\n#### `econtains(object|list|string, value)`\n\n\"Exact contains\" checks if the exact match is found in the string/list.\nThis function is case sensitive.\n\n- For strings, it behaves exactly like [`contains()`](#containsobjectliststring-value).\n    ```\n    econtains(\"Hello\", \"Lo\") = false\n    econtains(\"Hello\", \"lo\") = true\n    ```\n\n- For lists, it checks if the exact word is in the list.\n    ```\n    econtains([\"These\", \"are\", \"words\"], \"word\") = false\n    econtains([\"These\", \"are\", \"words\"], \"words\") = true\n    ```\n\n- For objects, it checks if the exact key name is present in the object. It does **not** do recursive checks.\n    ```\n    econtains({key:\"value\", pairs:\"here\"}, \"here\") = false\n    econtains({key:\"value\", pairs:\"here\"}, \"key\") = true\n    econtains({key:\"value\", recur:{recurkey: \"val\"}}, \"value\") = false\n    econtains({key:\"value\", recur:{recurkey: \"val\"}}, \"Recur\") = false\n    econtains({key:\"value\", recur:{recurkey: \"val\"}}, \"recurkey\") = false\n    ```\n\n### `containsword(list|string, value)`\n\nChecks if `value` has an exact word match in `string` or `list`.\nThis is case insensitive.\nThe outputs are different for different types of input, see examples.\n\n- For strings, it checks if the word is present in the given string.\n    ```\n    containsword(\"word\", \"word\") = true\n    containsword(\"word\", \"Word\") = true\n    containsword(\"words\", \"Word\") = false\n    containsword(\"Hello there!\", \"hello\") = true\n    containsword(\"Hello there!\", \"HeLLo\") = true\n    containsword(\"Hello there chaps!\", \"chap\") = false\n    containsword(\"Hello there chaps!\", \"chaps\") = true\n    ```\n\n- For lists, it returns a list of booleans indicating if the word's exact case insensitive match was found.\n    ```\n    containsword([\"I have no words.\", \"words\"], \"Word\") = [false, false]\n    containsword([\"word\", \"Words\"], \"Word\") = [true, false]\n    containsword([\"Word\", \"Words in word\"], \"WORD\") = [true, true]\n    ```\n\n### `extract(object, key1, key2, ...)`\n\nPulls multiple fields out of an object, creating a new object with just those fields.\n\n```\nextract(file, \"ctime\", \"mtime\") = object(\"ctime\", file.ctime, \"mtime\", file.mtime)\nextract(object(\"test\", 1)) = object()\n```\n\n### `sort(list)`\n\nSorts a list, returning a new list in sorted order.\n\n```\nsort(list(3, 2, 1)) = list(1, 2, 3)\nsort(list(\"a\", \"b\", \"aa\")) = list(\"a\", \"aa\", \"b\")\n```\n\n### `reverse(list)`\n\nReverses a list, returning a new list in reversed order.\n\n```\nreverse(list(1, 2, 3)) = list(3, 2, 1)\nreverse(list(\"a\", \"b\", \"c\")) = list(\"c\", \"b\", \"a\")\n```\n\n### `length(object|array)`\n\nReturns the number of fields in an object, or the number of entries in an array.\n\n```\nlength([]) = 0\nlength([1, 2, 3]) = 3\nlength(object(\"hello\", 1, \"goodbye\", 2)) = 2\n```\n\n### `nonnull(array)`\n\nReturn a new array with all null values removed.\n\n```\nnonnull([]) = []\nnonnull([null, false]) = [false]\nnonnull([1, 2, 3]) = [1, 2, 3]\n```\n\n### `firstvalue(array)`\n\nReturn the first non-null value from the array, as a single element. This can be used to pick the first defined field in the children of a task/list item, like in `firstvalue(children.myField)`.\n\n```js\nfirstvalue([null, 1, 2]) => 1\nfirstvalue(children.myField) => If children.myField equals [null, null, \"myValue\", null], it would return \"myValue\"\n```\n\n### `all(array)`\n\nReturns `true` only if ALL values in the array are truthy. You can also pass multiple arguments to this function, in\nwhich case it returns `true` only if all arguments are truthy.\n\n```\nall([1, 2, 3]) = true\nall([true, false]) = false\nall(true, false) = false\nall(true, true, true) = true\n```\n\nYou can pass a function as second argument to return only true if all elements in the array matches the predicate.\n\n```\nall([1, 2, 3], (x) => x > 0) = true\nall([1, 2, 3], (x) => x > 1) = false\nall([\"apple\", \"pie\", 3], (x) => typeof(x) = \"string\") = false\n```\n\n### `any(array)`\n\nReturns `true` if ANY of the values in the array are truthy. You can also pass multiple arguments to this function, in\nwhich case it returns `true` if any of the arguments are truthy.\n\n```\nany(list(1, 2, 3)) = true\nany(list(true, false)) = true\nany(list(false, false, false)) = false\nany(true, false) = true\nany(false, false) = false\n```\n\nYou can pass a function as second argument to return only true if any element in the array matches the predicate.\n\n```\nany(list(1, 2, 3), (x) => x > 2) = true\nany(list(1, 2, 3), (x) => x = 0) = false\n```\n\n### `none(array)`\n\nReturns `true` if NONE of the values in the array are truthy.\n\n```\nnone([]) = true\nnone([false, false]) = true\nnone([false, true]) = false\nnone([1, 2, 3]) = false\n```\n\nYou can pass a function as second argument to return only true if none of the elements in the array matches the predicate.\n\n```\nnone([1, 2, 3], (x) => x = 0) = true\nnone([true, true], (x) => x = false) = true\nnone([\"Apple\", \"Pi\", \"Banana\"], (x) => startswith(x, \"A\")) = false\n```\n\n### `join(array, [delimiter])`\n\nJoins elements in an array into a single string (i.e., rendering them all on the same line). If provided with a second\nargument, then each element will be separated by the given separator.\n\n```\njoin(list(1, 2, 3)) = \"1, 2, 3\"\njoin(list(1, 2, 3), \" \") = \"1 2 3\"\njoin(6) = \"6\"\njoin(list()) = \"\"\n```\n\n### `filter(array, predicate)`\n\nFilters elements in an array according to the predicate, returning a new list of the elements which matched.\n\n```js\nfilter([1, 2, 3], (x) => x >= 2) = [2, 3]\nfilter([\"yes\", \"no\", \"yas\"], (x) => startswith(x, \"y\")) = [\"yes\", \"yas\"]\n```\n\n### `unique(array)`\n\nCreates a new array with only unique values. \n\n```js\nunique([1, 3, 7, 3, 1]) => [1, 3, 7]\n```\n\n### `map(array, func)`\n\nApplies the function to each element in the array, returning a list of the mapped results.\n\n```js\nmap([1, 2, 3], (x) => x + 2) = [3, 4, 5]\nmap([\"yes\", \"no\"], (x) => x + \"?\") = [\"yes?\", \"no?\"]\n```\n\n### `flat(array, [depth])`\n\nConcatenates sub-levels of the array to the desired depth. Default is 1 level, but it can\nconcatenate multiple levels. E.g. Can be used to reduce array depth on `rows` lists after\ndoing `GROUP BY`.\n\n```js\nflat(list(1, 2, 3, list(4, 5), 6)) => list(1, 2, 3, 4, 5, 6)\nflat(list(1, list(21, 22), list(list (311, 312, 313))), 4) => list(1, 21, 22, 311, 312, 313)\nflat(rows.file.outlinks)) => All the file outlinks at first level in output\n```\n\n### `slice(array, [start, [end]])`\n\nReturns a shallow copy of a portion of an array into a new array object selected from `start`\nto `end` (`end` not included) where `start` and `end` represents the index of items in that array.\n\n```js\nslice([1, 2, 3, 4, 5], 3) = [4, 5] => All items from given position, 0 as first\nslice([\"ant\", \"bison\", \"camel\", \"duck\", \"elephant\"], 0, 2) = [\"ant\", \"bison\"] => First two items\nslice([1, 2, 3, 4, 5], -2) = [4, 5] => counts from the end, last two items\nslice(someArray) => a copy of someArray\n```\n\n---\n\n## String Operations\n\n### `regextest(pattern, string)`\n\nChecks if the given regex pattern can be found in the string (using the JavaScript regex engine).\n\n```js\nregextest(\"\\w+\", \"hello\") = true\nregextest(\".\", \"a\") = true\nregextest(\"yes|no\", \"maybe\") = false\nregextest(\"what\", \"what's up dog?\") = true\n```\n\n### `regexmatch(pattern, string)`\n\nChecks if the given regex pattern matches the *entire* string, using the JavaScript regex engine.\nThis differs from `regextest` in that regextest can match just parts of the text.\n\n```js\nregexmatch(\"\\w+\", \"hello\") = true\nregexmatch(\".\", \"a\") = true\nregexmatch(\"yes|no\", \"maybe\") = false\nregexmatch(\"what\", \"what's up dog?\") = false\n```\n\n### `regexreplace(string, pattern, replacement)`\n\nReplaces all instances where the *regex* `pattern` matches in `string`, with `replacement`. This uses the JavaScript\nreplace method under the hood, so you can use special characters like `$1` to refer to the first capture group, and so on.\n\n```js\nregexreplace(\"yes\", \"[ys]\", \"a\") = \"aea\"\nregexreplace(\"Suite 1000\", \"\\d+\", \"-\") = \"Suite -\"\n```\n\n### `replace(string, pattern, replacement)`\n\nReplace all instances of `pattern` in `string` with `replacement`.\n\n```js\nreplace(\"what\", \"wh\", \"h\") = \"hat\"\nreplace(\"The big dog chased the big cat.\", \"big\", \"small\") = \"The small dog chased the small cat.\"\nreplace(\"test\", \"test\", \"no\") = \"no\"\n```\n\n### `lower(string)`\n\nConvert a string to all lower case.\n\n```js\nlower(\"Test\") = \"test\"\nlower(\"TEST\") = \"test\"\n```\n\n### `upper(string)`\n\nConvert a string to all upper case.\n\n```js\nupper(\"Test\") = \"TEST\"\nupper(\"test\") = \"TEST\"\n```\n\n### `split(string, delimiter, [limit])`\n\nSplit a string on the given delimiter string. If a third argument is provided, it limits the number of splits that occur. The delimiter string is interpreted as a regular expression. If there are capture groups in the delimiter, matches are spliced into the result array, and non-matching captures are empty strings.\n\n\n```js\nsplit(\"hello world\", \" \") = list(\"hello\", \"world\")\nsplit(\"hello  world\", \"\\s\") = list(\"hello\", \"world\")\nsplit(\"hello there world\", \" \", 2) = list(\"hello\", \"there\")\nsplit(\"hello there world\", \"(t?here)\") = list(\"hello \", \"there\", \" world\")\nsplit(\"hello there world\", \"( )(x)?\") = list(\"hello\", \" \", \"\", \"there\", \" \", \"\", \"world\")\n```\n\n### `startswith(string, prefix)`\n\nChecks if a string starts with the given prefix.\n\n```js\nstartswith(\"yes\", \"ye\") = true\nstartswith(\"path/to/something\", \"path/\") = true\nstartswith(\"yes\", \"no\") = false\n```\n\n### `endswith(string, suffix)`\n\nChecks if a string ends with the given suffix.\n\n```js\nendswith(\"yes\", \"es\") = true\nendswith(\"path/to/something\", \"something\") = true\nendswith(\"yes\", \"ye\") = false\n```\n\n### `padleft(string, length, [padding])`\n\nPads a string up to the desired length by adding padding on the left side. If you omit the padding character, spaces\nwill be used by default.\n\n```js\npadleft(\"hello\", 7) = \"  hello\"\npadleft(\"yes\", 5, \"!\") = \"!!yes\"\n```\n\n### `padright(string, length, [padding])`\n\nEquivalent to `padleft`, but pads to the right instead.\n\n```js\npadright(\"hello\", 7) = \"hello  \"\npadright(\"yes\", 5, \"!\") = \"yes!!\"\n```\n\n### `substring(string, start, [end])`\n\nTake a slice of a string, starting at `start` and ending at `end` (or the end of the string if unspecified).\n\n```js\nsubstring(\"hello\", 0, 2) = \"he\"\nsubstring(\"hello\", 2, 4) = \"ll\"\nsubstring(\"hello\", 2) = \"llo\"\nsubstring(\"hello\", 0) = \"hello\"\n```\n\n### `truncate(string, length, [suffix])`\n\nTruncate a string to be at most the given length, including the `suffix` (which defaults to `...`). Generally useful\nto cut off long text in tables.\n\n```js\ntruncate(\"Hello there!\", 8) = \"Hello...\"\ntruncate(\"Hello there!\", 8, \"/\") = \"Hello t/\"\ntruncate(\"Hello there!\", 10) = \"Hello t...\"\ntruncate(\"Hello there!\", 10, \"!\") = \"Hello the!\"\ntruncate(\"Hello there!\", 20) = \"Hello there!\"\n```\n\n## Utility Functions\n\n### `default(field, value)`\n\nIf `field` is null, return `value`; otherwise return `field`. Useful for replacing null values with defaults. For example, to show projects which haven't been completed yet, use `\"incomplete\"` as their default value:\n\n```js\ndefault(dateCompleted, \"incomplete\")\n```\n\nDefault is vectorized in both arguments; if you need to use default explicitly on a list argument, use `ldefault`, which\nis the same as default but is not vectorized.\n\n```js\ndefault(list(1, 2, null), 3) = list(1, 2, 3)\nldefault(list(1, 2, null), 3) = list(1, 2, null)\n```\n\n### `display()`\n\nDisplay function converts the input into a string representation while trying to\npreserve the display property of data types.\nThis means that links and urls will be replaced by their display value.\n\n\n```js\ndisplay(\"Hello World\") = \"Hello World\"\ndisplay(\"**Hello** World\") = \"Hello World\"\ndisplay(\"[Hello](https://example.com) [[World]]\") = \"Hello World\"\ndisplay(link(\"path/to/file.md\")) = \"file\"\ndisplay(link(\"path/to/file.md\", \"displayname\")) = \"displayname\"\ndisplay(date(\"2024-11-18\")) = \"November 18, 2024\"\ndisplay(list(\"Hello\", \"World\")) = \"Hello, World\"\n```\n\n### `choice(bool, left, right)`\n\nA primitive if statement - if the first argument is truthy, returns left; otherwise, returns right.\n\n```js\nchoice(true, \"yes\", \"no\") = \"yes\"\nchoice(false, \"yes\", \"no\") = \"no\"\nchoice(x > 4, y, z) = y if x > 4, else z\n```\n\n### `hash(seed, [text], [variant])`\n\nGenerate a hash based on the `seed`, and the optional extra `text` or a variant `number`. The function\ngenerates a fixed number based on the combination of these parameters, which can be used to randomize\nthe sort order of files or lists/tasks. If you choose a `seed` based on a date, i.e. \"2024-03-17\",\nor another timestamp, i.e. \"2024-03-17 19:13\", you can make the \"randomness\" be fixed\nrelated to that timestamp. `variant` is a number, which in some cases is needed to make the combination of\n`text` and `variant` become unique.\n\n```js\nhash(dateformat(date(today), \"YYYY-MM-DD\"), file.name) = ... A unique value for a given date in time\nhash(dateformat(date(today), \"YYYY-MM-DD\"), file.name, position.start.line) = ... A unique \"random\" value in a TASK query\n```\n\nThis function can be used in a `SORT` statement to randomize the order. If you're using a `TASK` query,\nsince the file name could be the same for multiple tasks, you can add some number like the starting line\nnumber (as shown above) to make it a unique combination. If using something like `FLATTEN file.lists as item`,\nthe similar addition would be to do `item.position.start.line` as the last parameter.\n\n### `striptime(date)`\n\nStrip the time component of a date, leaving only the year, month, and day. Good for date comparisons if you don't care\nabout the time.\n\n```js\nstriptime(file.ctime) = file.cday\nstriptime(file.mtime) = file.mday\n```\n\n### `dateformat(date|datetime, string)`\n\nFormat a Dataview date using a formatting string. Uses [Luxon tokens](https://moment.github.io/luxon/#/formatting?id=table-of-tokens).\n\n```js\ndateformat(file.ctime,\"yyyy-MM-dd\") = \"2022-01-05\"\ndateformat(file.ctime,\"HH:mm:ss\") = \"12:18:04\"\ndateformat(date(now),\"x\") = \"1407287224054\"\ndateformat(file.mtime,\"ffff\") = \"Wednesday, August 6, 2014, 1:07 PM Eastern Daylight Time\"\n```\n\n**Note:** `dateformat()` returns a string, not a date, so you can't compare it against the result from a call to `date()` or a variable like `file.day` which already is a date. To make those comparisons you can format both arguments.\n\n### `durationformat(duration, string)`\n\nFormat a Dataview duration using a formatting string.\nAnything inside single quotes will not be treated as a token and\ninstead will be shown in the output as written. See examples.\n\nYou may use these tokens:\n\n- `S` for milliseconds\n- `s` for seconds\n- `m` for minutes\n- `h` for hours\n- `d` for days\n- `w` for weeks\n- `M` for months\n- `y` for years\n\n```js\ndurationformat(dur(\"3 days 7 hours 43 seconds\"), \"ddd'd' hh'h' ss's'\") = \"003d 07h 43s\"\ndurationformat(dur(\"365 days 5 hours 49 minutes\"), \"yyyy ddd hh mm ss\") = \"0001 000 05 49 00\"\ndurationformat(dur(\"2000 years\"), \"M months\") = \"24000 months\"\ndurationformat(dur(\"14d\"), \"s 'seconds'\") = \"1209600 seconds\"\n```\n\n### `currencyformat(number, [currency])`\n\nPresents the number depending on your current locale, according to the `currency` code, from [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes).\n\n```\nnumber = 123456.789\ncurrencyformat(number, \"EUR\") =  €123,456.79  in locale: en_US)\ncurrencyformat(number, \"EUR\") =  123.456,79 € in locale: de_DE)\ncurrencyformat(number, \"EUR\") =  € 123 456,79 in locale: nb)\n```\n\n### `localtime(date)`\n\nConverts a date in a fixed timezone to a date in the current timezone.\n\n### `meta(link)`\n\nGet an object containing metadata of a link. When you access a property on a link what you get back is the property\nvalue from the linked file. The `meta` function makes it possible to access properties of the link itself.\n\nThere are several properties on the object returned by `meta`:\n\n#### `meta(link).display`\n\nGet the display text of a link, or null if the link does not have defined display text.\n\n```js\nmeta([[2021-11-01|Displayed link text]]).display = \"Displayed link text\"\nmeta([[2021-11-01]]).display = null\n```\n\n#### `meta(link).embed`\n\nTrue or false depending on whether the link is an embed. Those are links that begin with an exclamation mark, like\n`![[Some Link]]`.\n\n#### `meta(link).path`\n\nGet the path portion of a link.\n\n```js\nmeta([[My Project]]).path = \"My Project\"\nmeta([[My Project#Next Actions]]).path = \"My Project\"\nmeta([[My Project#^9bcbe8]]).path = \"My Project\"\n```\n\n#### `meta(link).subpath`\n\nGet the subpath of a link. For links to a heading within a file the subpath will be the text of the heading. For links\nto a block the subpath will be the block ID. If neither of those cases applies then the subpath will be null.\n\n```js\nmeta([[My Project#Next Actions]]).subpath = \"Next Actions\"\nmeta([[My Project#^9bcbe8]]).subpath = \"9bcbe8\"\nmeta([[My Project]]).subpath = null\n```\n\nThis can be used to select tasks under specific headings.\n\n````\n```dataview\ntask\nwhere meta(section).subpath = \"Next Actions\"\n```\n````\n\n#### `meta(link).type`\n\nHas the value \"file\", \"header\", or \"block\" depending on whether the link links to an entire file, a heading within\na file, or to a block within a file.\n\n```js\nmeta([[My Project]]).type = \"file\"\nmeta([[My Project#Next Actions]]).type = \"header\"\nmeta([[My Project#^9bcbe8]]).type = \"block\"\n```\n"
  },
  {
    "path": "docs/docs/reference/literals.md",
    "content": "# Literals\n\nDataview 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**:\n\n~~~\n\nLiteral (number) 2022 used in a comparison\n```dataview\nLIST\nWHERE file.day.year = 2022\n```\n\nLiteral (text) \"Math\" used in a function call\n```dataview\nLIST\nWHERE contains(file.name, \"Math\")\n```\n\nLiteral (link) [[Study MOC]] used as a source\n```dataview\nLIST\nFROM [[Study MOC]]\n```\n\nLiteral (date) date(yesterday) used in a comparison\n```dataview\nTASK\nWHERE !completed AND file.day = date(yesterday)\n```\n\nLiteral (duration) dur(2 days) used in a comparison\n```dataview\nLIST\nWHERE end - start > dur(2 days)\n```\n~~~\n\n!!! summary \"Literals\"\n    Literals are **static values** that can be used as part of the Dataview Query Language (DQL), i.e. for comparisons.\n\nThe following is an extensive, but non-exhaustive list of possible literals in DQL.\n\n### General\nLiteral|Description\n-|-\n`0`|The number zero\n`1337`|The positive number 1337\n`-200`| The negative number -200\n`\"The quick brown fox jumps over the lazy dog\"`| Text (sometimes referred to as \"string\")\n`[[Science]]`|A link to the file named \"Science\"\n`[[]]`| A link to the current file\n`[1, 2, 3]`|A list of numbers 1, 2, and 3\n`[[1, 2],[3, 4]]`|A list of list [1, 2] and [3, 4]\n`{ a: 1, b: 2 }`| An object with keys a and b, whereas a has value 1, b 2. |\n`date(2021-07-14)`| A date (read more below) |\n`dur(2 days 4 hours)` | A duration (read more below) | \n\n!!! attention \"Literals as field values\"\n    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).\n\n### Dates\n\nWhenever 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.\n\nLiteral|Description\n-|-\n`date(2021-11-11)`|A date, November 11th, 2021\n`date(2021-09-20T20:17)`| A date, September 20th, 2021 at 20:17\n`date(today)`|A date representing the current date\n`date(now)`|A date representing the current date and time\n`date(tomorrow)`|A date representing tomorrow's date\n`date(yesterday)`|A date representing yesterday's date\n`date(sow)`|A date representing the start of the current week\n`date(eow)`|A date representing the end of the current week\n`date(som)`|A date representing the start of the current month\n`date(eom)`|A date representing the end of the current month\n`date(soy)`|A date representing the start of the current year\n`date(eoy)`|A date representing the end of the current year\n\n### Durations\n\nDurations 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.\n\n#### Seconds\nLiteral|Description\n-|-\n`dur(1 s)`|one second\n`dur(3 s)`|three seconds\n`dur(1 sec)`|one second\n`dur(3 secs)`|three seconds\n`dur(1 second)`|one second\n`dur(3 seconds)`|three seconds\n\n#### Minutes\nLiteral|Description\n-|-\n`dur(1 m)`|one minute\n`dur(3 m)`|three minutes\n`dur(1 min)`|one minute\n`dur(3 mins)`|three minutes\n`dur(1 minute)`|one minute\n`dur(3 minutes)`|three minutes\n\n#### Hours\nLiteral|Description\n-|-\n`dur(1 h)`|one hour\n`dur(3 h)`|three hours\n`dur(1 hr)`|one hour\n`dur(3 hrs)`|three hours\n`dur(1 hour)`|one hour\n`dur(3 hours)`|three hours\n\n#### Days\nLiteral|Description\n-|-\n`dur(1 d)`|one day\n`dur(3 d)`|three days\n`dur(1 day)`|one day\n`dur(3 days)`|three days\n\n#### Weeks\nLiteral|Description\n-|-\n`dur(1 w)`|one week\n`dur(3 w)`|three weeks\n`dur(1 wk)`|one week\n`dur(3 wks)`|three weeks\n`dur(1 week)`|one week\n`dur(3 weeks)`|three weeks\n\n#### Months\nLiteral|Description\n-|-\n`dur(1 mo)`|one month\n`dur(3 mo)`|three month\n`dur(1 month)`|one month\n`dur(3 months)`|three months\n\n#### Years\nLiteral|Description\n-|-\n`dur(1 yr)`|one year\n`dur(3 yrs)`|three years\n`dur(1 year)`|one year\n`dur(3 years)`|three years\n\n#### Combinations\nLiteral|Description\n-|-\n`dur(1 s, 2 m, 3 h)`|three hours, two minutes, and one second\n`dur(1 s 2 m 3 h)`|three hours, two minutes, and one second\n`dur(1s 2m 3h)`|three hours, two minutes, and one second\n`dur(1second 2min 3h)`|three hours, two minutes, and one second\n"
  },
  {
    "path": "docs/docs/reference/sources.md",
    "content": "# Sources\n\nA dataview **source** is something that identifies a set of files, tasks, or other data. Sources are indexed internally by\nDataview, 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.\n\n## Types of Sources\n\nDataview currently supports **four source types**.\n\n### Tags\n\nSources of the form `#tag`. These match all files / sections / tasks with the given tag.\n\n~~~\n```dataview\nLIST\nFROM #homework\n```\n~~~\n\n### Folders\n\nSources 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.\n\n~~~\n```dataview\nTABLE file.ctime, status\nFROM \"projects/brainstorming\"\n```\n~~~\n\n\n### Specific Files\n\nYou can select from a specific file by specifying it's full path: `\"folder/File\"`.\n\n- 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`.\n\n~~~\n```dataview\nLIST WITHOUT ID next-in-line\nFROM \"30 Hobbies/Games/Dashboard\"\n```\n~~~\n\n\n### Links\n\n You can either select links **to** a file, or all links **from** a file.\n \n- To obtain all pages which link **to** `[[note]]`, use `[[note]]`.\n- To obtain all pages which link **from** `[[note]]` (i.e., all the links in that file), use `outgoing([[note]])`.\n- You can implicitly reference the current file via `[[#]]` or `[[]]`, i.e. `[[]]` lets you query from all files linking to the current file.\n\n~~~\n```dataview\nLIST\nFROM [[]]\n```\n\n```dataview\nLIST\nFROM outgoing([[Dashboard]])\n```\n~~~\n\n\n## Combining Sources\n\nYou can compose these filters in order to get more advanced sources using `and` and `or`.\n\n- For example, `#tag and \"folder\"` will return all pages in `folder` and with `#tag`.\n- Querying from `#food and !#fastfood` will only return pages that contain `#food` but does not contain `#fastfood`.\n- `[[Food]] or [[Exercise]]` will give any pages which link to `[[Food]]` OR `[[Exercise]]`.\n\nIf you have complex queries where grouping or precedence matters, you can use parenthesis to logically group them:\n\n- `#tag and (\"folder\" or #other-tag)`\n- `(#tag1 or #tag2) and (#tag3 or #tag4)`\n\n\n"
  },
  {
    "path": "docs/docs/resources/develop-against-dataview.md",
    "content": "# Developing Against Dataview\n\nDataview includes a high-level plugin-facing API as well as TypeScript definitions and a utility library; to install it,\nsimply use:\n\n```bash\nnpm install -D obsidian-dataview\n```\n\nTo 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:\n\n```bash\nnpm install obsidian-dataview@0.5.64\n```\n\n**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.\n\n##### Accessing the Dataview API\n\nYou can use the `getAPI()` function to obtain the Dataview Plugin API; this returns a `DataviewApi` object which\nprovides various utilities, including rendering dataviews, checking dataview's version, hooking into the dataview event\nlife cycle, and querying dataview metadata.\n\n```ts\nimport { getAPI } from \"obsidian-dataview\";\n\nconst api = getAPI();\n```\n\nFor full API definitions available, check\n[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).\n\n##### Binding to Dataview Events\n\nYou can bind to dataview metadata events, which fire on all file updates and changes, via:\n\n\n```ts\nplugin.registerEvent(plugin.app.metadataCache.on(\"dataview:index-ready\", () => {\n    ...\n});\n\nplugin.registerEvent(plugin.app.metadataCache.on(\"dataview:metadata-change\",\n    (type, file, oldPath?) => { ... }));\n```\n\nFor all events hooked on MetadataCache, check [index.ts](https://github.com/blacksmithgu/obsidian-dataview/blob/master/src/index.ts).\n\n##### Value Utilities\n\nYou can access various type utilities which let you check the types of objects and compare them via `Values`:\n\n~~~ts\nimport { getAPI, Values } from \"obsidian-dataview\";\n\nconst field = getAPI(plugin.app)?.page('sample.md').field;\nif (!field) return;\n\nif (Values.isHtml(field)) // do something\nelse if (Values.isLink(field)) // do something\n// ...\n~~~\n"
  },
  {
    "path": "docs/docs/resources/examples.md",
    "content": "# Examples\n\nA small collection of simple usages of the dataview query language.\n\n---\n\nShow all games in the games folder, sorted by rating, with some metadata:\n\n=== \"Query\"\n    ```sql\n    TABLE\n      time-played AS \"Time Played\",\n      length AS \"Length\",\n      rating AS \"Rating\"\n    FROM \"games\"\n    SORT rating DESC\n    ```\n=== \"Output\"\n    |File|Time Played|Length|Rating|\n    |-|-|-|-|\n    |[Outer Wilds](#)|November 19th - 21st, 2020|15h|9.5|\n    |[Minecraft](#)|All the time.|2000h|9.5|\n    |[Pillars of Eternity 2](#)|August - October 2019|100h|9|\n\n---\n\nList games which are MOBAs or CRPGs.\n\n=== \"Query\"\n    ``` sql\n    LIST FROM #games/mobas OR #games/crpg\n    ```\n=== \"Output\"\n    - [League of Legends](#)\n    - [Pillars of Eternity 2](#)\n\n---\n\nList all tasks in un-completed projects:\n\n=== \"Query\"\n    ``` sql\n    TASK FROM \"dataview\"\n    ```\n=== \"Output\"\n    [dataview/Project A](#)\n\n    - [ ] I am a task.\n    - [ ] I am another task.\n\n    [dataview/Project A](#)\n\n    - [ ] I could be a task, though who knows.\n        - [X] Determine if this is a task.\n    - [X] I'm a finished task.\n\n---\n\nList all of the files in the `books` folder, sorted by the last time you modified the file:\n\n=== \"Query\"\n    ```sql\n    TABLE file.mtime AS \"Last Modified\"\n    FROM \"books\"\n    SORT file.mtime DESC\n    ```\n=== \"Output\"\n    |File|Last Modified|\n    |-|-|\n    |[Atomic Habits](#)|11:06 PM - August 07, 2021|\n    |[Can't Hurt Me](#)|10:58 PM - August 07, 2021|\n    |[Deep Work](#)|10:52 PM - August 07, 2021|\n\n---\n\nList all files which have a date in their title (of the form `yyyy-mm-dd`), and list them by date order.\n\n=== \"Query\"\n    ```sql\n    LIST file.day WHERE file.day\n    SORT file.day DESC\n    ```\n=== \"Output\"\n    - [2021-08-07](#): August 07, 2021\n    - [2020-08-10](#): August 10, 2020\n"
  },
  {
    "path": "docs/docs/resources/faq.md",
    "content": "# Frequently Asked Questions\n\nA collection of frequently asked questions for Dataview queries and the expression language.\n\n### How do I use fields with the same name as keywords (like \"from\", \"where\")?\n\nDataview provides a special \"fake\" field called `row` which can be indexed into to obtain fields which conflict with\nDataview keywords:\n\n```javascript\nrow.from /* Same as \"from\" */\nrow.where /* Same as \"where\" */\n```\n\n\n### How do I access fields with spaces in the name?\n\nThere are two ways:\n\n1. Use the normalized Dataview name for such a field - just convert the name to lowercase and replace whitespace with\n   dashes (\"-\"). Something like `Field With Space In It` becomes `field-with-space-in-it`.\n2. Use the implicit `row` field:\n    ```javascript\n    row[\"Field With Space In It\"]\n    ```\n\n### Do you have a list of resources to learn from?\n\nYes! Please see the [Resources](../resources/resources-and-support.md) page.\n\n### Can I save the result of a query for reusability?\n\nYou 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:\n\n```markdown\nstart:: 07h00m\nend:: 18h00m\npause:: 01h30m\nduration:: `= this.end - this.start - this.pause`\n```\n\nYou can list the value (9h 30m in our example) then i.e. in a TABLE without needing to repeat the calculation:\n\n~~~markdown\n```dataview\nTABLE start, end, duration\nWHERE duration\n```\n~~~\n\nGives you\n\n| File (1)\t| start| \tend| \tduration|\n| ---- | ----- | ------ |  ----- |\n| Example | 7 hours\t| 18 hours| \t9 hours, 30 minutes |\n\n**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:\n\n~~~markdown\n```dataview\nTABLE start, end, duration\nWHERE duration > dur(\"10h\")\n```\n~~~\n\nThis 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.\n\n### How can I hide the result count on TABLE Queries?\n\nWith 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.\n\n### How can I style my queries?\n\nYou 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 &lt;ul&gt; of a `TASK` or `LIST` query, you could use the `ul.contains-task-list` or `ul.list-view-ul` respectively.\n\n```css\n.myTable dataview.table {\n    background-color: green\n}\n```\n\nIn 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:\n\n```js\ndv.container.className += ' clsname'\n```\n\nHowever, 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.\n\n```css\n[href=\"#myId\"] {\n    display: none; /* Hides the tag from the table view */\n}\n\ntable:has([href=\"#myId\"]) {\n   /* Style your table as you like */\n  background-color: #262626;\n  & tr:nth-child(even) td:first-child{\n    background-color: #3f3f3f;\n  }\n}\n```\n\nWhich 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.\n\nFurthermore, 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 `<span>` to style various parts of table cells (and columns).\n"
  },
  {
    "path": "docs/docs/resources/resources-and-support.md",
    "content": "# Other Resources\n\nThere is a bit of a learning curve to getting started with Dataview.\nThis page is a collection of resources that will help you get started.\nDataview gets new features and fixes fairly frequently so please account for these resources being slightly out of date.\nFeel free to contribute directly to this list, documentation, or even reach out to the authors of the original sources for updates.\n\n## Resources\n\n### The Obsidian Hub\n\n- 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)\n\n### YouTube videos\n\n- SkepticMystic's aforementioned community talk\n- [Dataview Plugin: How To Use This Powerful Obsidian Plugin (With Examples) by Filipe Donadio](https://www.youtube.com/watch?v=7kFEl7Ovsr8)\n- [Automate Your Vault With Dataview - How To Use Dataview in Obsidian by FromSergio](https://www.youtube.com/watch?v=8yjNuiSBSAM)\n- [How to use the Obsidian Dataview plugin by Nicole van der Hoeven](https://www.youtube.com/watch?v=JTObSymEvWA)\n- [Intro to Dataview Plugin - Obsidian Community Talk](https://www.youtube.com/watch?v=lclif6l9UgQ)\n\n### Example Vault\n\n- @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.\n\n### Blog Posts\n\n- [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)\n\n### GitHub Discussion\n\nThe GitHub repository has a fairly active [Discussions Page](https://github.com/blacksmithgu/obsidian-dataview/discussions/) with dozens of answered questions.\n\n### Obsidian Forums\n\nThe [Obsidian Forums](https://forum.obsidian.md/) have a wealth of questions and answers and other interesting tidbits as well.\nTry searching the forums for an answer, especially if it seems like a beginner question.\n\n### Discord\n\nThe [Obsidian Members Group Discord server](https://obsidian.md/community) has a `#dataview` channel.\nOnce 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.\nIn case you don't find anything satisfactory, \nThis is the closest you'll get to real-time support but there are no guarantees of instant replies.\nThere are many helpful people though so don't be afraid to ask.\n\n## Support\n\nWhere do you go when you have questions?\nHere's what we recommend you try.\n\n1. Search GitHub Discussions and the Obsidian Forums for your question.\n2. Search through Discord for possible solutions.\n3. Depending on the complexity of the your question:\n  - If you need close to synchronous communication, use Discord. Note that we are a community of volunteers and there may be delays in responding.\n  - If you expect your problem needs time over multiple days and asynchronous communication, open a GitHub discussion.\n4. If you found a bug, please report it in [the repo's issues](https://github.com/blacksmithgu/obsidian-dataview/issues).\n"
  },
  {
    "path": "docs/mkdocs.yml",
    "content": "site_name: Dataview\nrepo_url: https://github.com/blacksmithgu/obsidian-dataview\nedit_uri: edit/master/docs/docs/\nmarkdown_extensions:\n  - abbr\n  - admonition\n  - pymdownx.highlight\n  - pymdownx.superfences\n  - pymdownx.tabbed\n  - pymdownx.tasklist\ntheme:\n  name: material\n  palette:\n    primary: deep purple\n  logo: assets/obsidian.png\n  favicon: assets/obsidian.png\n  custom_dir: overrides\nnav:\n  - Overview: 'index.md'\n  - Metadata:\n    - Adding Metadata: 'annotation/add-metadata.md'\n    - Data Types: 'annotation/types-of-metadata.md'\n    - Metadata on Pages: 'annotation/metadata-pages.md'\n    - Metadata on Tasks and Lists: 'annotation/metadata-tasks.md'\n  - DQL, JS and Inlines: 'queries/dql-js-inline.md'\n  - Query Language Reference:\n    - Structure of a Query: 'queries/structure.md'\n    - Query Types: 'queries/query-types.md'\n    - Data Commands: 'queries/data-commands.md'\n    - Differences to SQL: 'queries/differences-to-sql.md'\n    - Sources: 'reference/sources.md'\n    - Expressions: 'reference/expressions.md'\n    - Literals: 'reference/literals.md'\n    - Functions: 'reference/functions.md'\n  - JavaScript Reference:\n    - Overview: 'api/intro.md'\n    - Data Arrays: 'api/data-array.md'\n    - Codeblock Reference: 'api/code-reference.md'\n    - Codeblock Examples: 'api/code-examples.md'\n  - FAQ and Resources:\n    - Frequently Asked Questions: 'resources/faq.md'\n    - Examples: 'resources/examples.md'\n    - Developing Against Dataview: 'resources/develop-against-dataview.md'\n    - Resources and Support: 'resources/resources-and-support.md'\n  - Friends of Dataview: 'friends.md'\n  - Changelog: 'changelog.md'\nplugins:\n  - search\n  - redirects:\n      redirect_maps:\n        docs/where-data-comes-from.md: annotation/add-metadata.md\n        docs/creating-queries.md: queries/dql-js-inline.md\n        docs/query/queries.md: queries/structure.md\n        docs/query/expressions.md: reference/expressions.md\n        docs/query/sources.md: reference/sources.md\n        docs/query/functions.md: reference/functions.md\n        docs/query/examples.md: resources/examples.md\n        docs/api/intro.md: api/intro.md\n        docs/api/data-array.md: api/data-array.md\n        docs/api/code-reference.md: api/code-reference.md\n        docs/api/code-examples.md: api/code-examples.md\n        data-queries.md: queries/dql-js-inline.md\n        query/queries.md: queries/structure.md\n        data-annotation.md: annotation/add-metadata.md\n        resources-and-support.md: resources/resources-and-support.md\n        plugin/develop-against-dataview.md: resources/develop-against-dataview.md\n        reference/faq.md: resources/faq.md\n        reference/examples.md: resources/examples.md\n"
  },
  {
    "path": "docs/overrides/main.html",
    "content": "{% extends \"base.html\" %}\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n    preset: \"ts-jest\",\n    testEnvironment: \"jsdom\",\n    moduleDirectories: [\"node_modules\", \"src\"],\n};\n"
  },
  {
    "path": "manifest-beta.json",
    "content": "{\n  \"id\": \"dataview\",\n  \"name\": \"Dataview\",\n  \"version\": \"0.5.70\",\n  \"minAppVersion\": \"0.13.11\",\n  \"description\": \"Complex data views for the data-obsessed.\",\n  \"author\": \"Michael Brenan <blacksmithgu@gmail.com>\",\n  \"authorUrl\": \"https://github.com/blacksmithgu\",\n  \"helpUrl\": \"https://blacksmithgu.github.io/obsidian-dataview/\",\n  \"isDesktopOnly\": false\n}\n"
  },
  {
    "path": "manifest.json",
    "content": "{\n  \"id\": \"dataview\",\n  \"name\": \"Dataview\",\n  \"version\": \"0.5.68\",\n  \"minAppVersion\": \"0.13.11\",\n  \"description\": \"Complex data views for the data-obsessed.\",\n  \"author\": \"Michael Brenan <blacksmithgu@gmail.com>\",\n  \"authorUrl\": \"https://github.com/blacksmithgu\",\n  \"helpUrl\": \"https://blacksmithgu.github.io/obsidian-dataview/\",\n  \"isDesktopOnly\": false\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"obsidian-dataview\",\n  \"version\": \"0.5.70\",\n  \"description\": \"Advanced data views for Obsidian.md.\",\n  \"main\": \"lib/index.js\",\n  \"types\": \"lib/index.d.ts\",\n  \"files\": [\n    \"lib/**/*\"\n  ],\n  \"scripts\": {\n    \"bdd\": \"npx jest -i --watch --no-cache\",\n    \"build\": \"npx rollup --config rollup.config.js --environment BUILD:production\",\n    \"check-format\": \"npx prettier --check src\",\n    \"dev\": \"npx rollup --config rollup.config.js -w\",\n    \"format\": \"npx prettier --write src\",\n    \"lib\": \"npx rollup --config rollup.config.js --environment BUILD:lib\",\n    \"test\": \"npx jest\"\n  },\n  \"keywords\": [\n    \"obsidian\",\n    \"dataview\"\n  ],\n  \"author\": \"Michael Brenan\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^25.0.4\",\n    \"@rollup/plugin-node-resolve\": \"^15.2.1\",\n    \"@rollup/plugin-typescript\": \"^11.1.3\",\n    \"@types/jest\": \"^27.0.1\",\n    \"@types/luxon\": \"^3.2.0\",\n    \"@types/node\": \"^16.7.13\",\n    \"@types/papaparse\": \"^5.2.6\",\n    \"@types/parsimmon\": \"^1.10.6\",\n    \"@zerollup/ts-transform-paths\": \"^1.7.18\",\n    \"compare-versions\": \"^4.1.1\",\n    \"jest\": \"^29.7.0\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"obsidian\": \"^1.4.0\",\n    \"prettier\": \"2.3.2\",\n    \"rollup\": \"^2.79.1\",\n    \"rollup-jest\": \"^3.1.0\",\n    \"rollup-plugin-copy\": \"^3.5.0\",\n    \"rollup-plugin-typescript2\": \"^0.35.0\",\n    \"rollup-plugin-web-worker-loader\": \"^1.6.1\",\n    \"ts-jest\": \"^29.1.1\",\n    \"tslib\": \"^2.6.2\",\n    \"typescript\": \"^5.2.2\"\n  },\n  \"dependencies\": {\n    \"@codemirror/language\": \"https://github.com/lishid/cm-language\",\n    \"@codemirror/state\": \"^6.0.1\",\n    \"@codemirror/view\": \"^6.0.1\",\n    \"emoji-regex\": \"^10.0.0\",\n    \"localforage\": \"^1.10.0\",\n    \"luxon\": \"^3.2.0\",\n    \"obsidian-calendar-ui\": \"^0.3.12\",\n    \"papaparse\": \"^5.3.1\",\n    \"parsimmon\": \"^1.18.0\",\n    \"preact\": \"^10.6.5\",\n    \"remove-markdown\": \"^0.5.5\"\n  }\n}\n"
  },
  {
    "path": "rollup.config.js",
    "content": "import nodeResolve from \"@rollup/plugin-node-resolve\";\nimport commonjs from \"@rollup/plugin-commonjs\";\nimport webWorker from \"rollup-plugin-web-worker-loader\";\nimport copy from \"rollup-plugin-copy\";\nimport typescript2 from \"rollup-plugin-typescript2\";\n\nconst BASE_CONFIG = {\n    input: \"src/main.ts\",\n    external: [\"obsidian\", \"@codemirror/view\", \"@codemirror/state\", \"@codemirror/language\"],\n    onwarn: (warning, warn) => {\n        // Sorry rollup, but we're using eval...\n        if (/Use of eval is strongly discouraged/.test(warning.message)) return;\n        warn(warning);\n    },\n};\n\nconst getRollupPlugins = (tsconfig, ...plugins) =>\n    [\n        typescript2(tsconfig),\n        nodeResolve({ browser: true }),\n        commonjs(),\n        webWorker({ inline: true, forceInline: true, targetPlatform: \"browser\" }),\n    ].concat(plugins);\n\nconst DEV_PLUGIN_CONFIG = {\n    ...BASE_CONFIG,\n    output: {\n        dir: \"test-vault/.obsidian/plugins/dataview\",\n        sourcemap: \"inline\",\n        format: \"cjs\",\n        exports: \"default\",\n        name: \"Dataview (Development)\",\n    },\n    plugins: getRollupPlugins(\n        undefined,\n        copy({\n            targets: [\n                { src: \"manifest.json\", dest: \"test-vault/.obsidian/plugins/dataview/\" },\n                { src: \"styles.css\", dest: \"test-vault/.obsidian/plugins/dataview/\" },\n            ],\n        })\n    ),\n};\n\nconst PROD_PLUGIN_CONFIG = {\n    ...BASE_CONFIG,\n    output: {\n        dir: \"build\",\n        sourcemap: \"inline\",\n        sourcemapExcludeSources: true,\n        format: \"cjs\",\n        exports: \"default\",\n        name: \"Dataview (Production)\",\n    },\n    plugins: getRollupPlugins(),\n};\n\nconst LIBRARY_CONFIG = {\n    ...BASE_CONFIG,\n    input: \"src/index.ts\",\n    output: {\n        dir: \"lib\",\n        sourcemap: true,\n        format: \"cjs\",\n        name: \"Dataview (Library)\",\n    },\n    plugins: getRollupPlugins(\n        { tsconfig: \"tsconfig-lib.json\" },\n        copy({ targets: [{ src: \"src/typings/*.d.ts\", dest: \"lib/typings\" }] })\n    ),\n};\n\nlet configs = [];\nif (process.env.BUILD === \"lib\") {\n    // Library build, only library code.\n    configs.push(LIBRARY_CONFIG);\n} else if (process.env.BUILD === \"production\") {\n    // Production build, build library and main plugin.\n    configs.push(LIBRARY_CONFIG, PROD_PLUGIN_CONFIG);\n} else if (process.env.BUILD === \"dev\") {\n    // Dev build, only build the plugin.\n    configs.push(DEV_PLUGIN_CONFIG);\n} else {\n    // Default to the dev build.\n    configs.push(DEV_PLUGIN_CONFIG);\n}\n\nexport default configs;\n"
  },
  {
    "path": "scripts/beta-release",
    "content": "#!/usr/bin/env bash\n# Automatically update versions in files and create an autorelease.\n# Requires the github CLI, and the jq command\n\nEXIT=\"\"\nNEW_VERSION=$1\n\nif [ -z \"$EDITOR\" ]; then\n  echo \"Specify which editor to use in EDITOR, or by doing: EDITOR=vi ./scripts/beta-release 0.x.y\"\n  EXIT=1\nfi\n\nif ! command -v jq 2>&1 >/dev/null; then\n  echo \"This script relies on the 'jq' command, please install it\"\n  EXIT=1\nfi\n\nif ! command -v gh 2>&1 >/dev/null; then\n  echo \"This script relies on the 'gh' command from the Github CLI package, please install it\"\n  EXIT=1\nfi\n\nif [ $EXIT ]; then\n  exit $EXIT\nfi\n\nif [ -z \"$NEW_VERSION\" ]; then\n  NEW_VERSION=$(jq -r \".version\" manifest-beta.json | awk -F. -v OFS=. '{$NF += 1 ; print}')\nfi\n\nif [ -z \"$NEW_VERSION\" ]; then\n  echo \"Auto-generating next version number failed, please specify next version : ./scripts/beta-release 0.x.y\"\n  exit 1\nfi\n\necho \"Releasing beta version '${NEW_VERSION}'\"\n\n# Let users edit release-notes.txt for release notes.\nrm -f release-notes.md\ntouch release-notes.md\necho -e \"# ${NEW_VERSION} (Beta)\\n\\n\" >> release-notes.md\n$EDITOR release-notes.md\n\n# Append release notes to CHANGELOG.md.\nmv CHANGELOG.md CHANGELOG-TEMP.md\ncp release-notes.md CHANGELOG.md\necho -e \"\\n---\\n\" >> CHANGELOG.md\ncat CHANGELOG-TEMP.md >> CHANGELOG.md\nrm -f CHANGELOG-TEMP.md\n\n# Overwrite the documentation changelog.\ncp -f CHANGELOG.md docs/docs/changelog.md\n\n# Delete old files if they exist\nrm -f package.tmp.json\nrm -f manifest-beta.tmp.json\nrm -f versions.tmp.json\n\n# Rewrite versions in relevant files.\njq \".version=\\\"${NEW_VERSION}\\\"\" package.json > package.tmp.json && mv package.tmp.json package.json\njq \".version=\\\"${NEW_VERSION}\\\"\" manifest-beta.json > manifest-beta.tmp.json && mv manifest-beta.tmp.json manifest-beta.json\njq \". + {\\\"${NEW_VERSION}\\\": \\\"0.13.11\\\"}\" versions.json > versions.tmp.json && mv versions.tmp.json versions.json\n\n# Create commit & commit.\ngit commit -a -m \"Auto-release beta ${NEW_VERSION}\"\ngit push\n\n# Rebuild the project to prepare for a release.\nnpm run build\n\n# release api\nnpm publish --tag beta --access public\n\n# And do a github release.\ngh release create \"${NEW_VERSION}\" --pre-release build/main.js styles.css manifest.json --title \"${NEW_VERSION}\" --notes-file release-notes.md\nrm -f release-notes.md\n"
  },
  {
    "path": "scripts/install-built",
    "content": "#!/usr/bin/env bash\n# Builds dataview and allows you to provide a path to the vault that it should be installed in.\n# Useful for when you want to dry-run the plugin in a vault other than the test vault.\n\nVAULT=\"$1\"\nTARGET=\"$VAULT/.obsidian/plugins/dataview/\"\nmkdir -p \"$TARGET\"\ncp -f build/main.js styles.css \"$TARGET\"\ncp -f manifest-beta.json \"$TARGET/manifest.json\"\necho Installed plugin files to \"$TARGET\"\n"
  },
  {
    "path": "scripts/release",
    "content": "#!/usr/bin/env bash\n# Automatically update versions in files and create an autorelease.\n# Requires the github CLI and jq (if you don't update the version manually).\nEXIT=\"\"\nNEW_VERSION=$1\n\nif [ -z \"$EDITOR\" ]; then\n  echo \"Specify which editor to use in EDITOR, or by doing: EDITOR=vi ./scripts/release 0.x.y\"\n  EXIT=1\nfi\n\nif ! command -v jq 2>&1 >/dev/null; then\n  echo \"This script relies on the 'jq' command, please install it\"\n  EXIT=1\nfi\n\nif ! command -v gh 2>&1 >/dev/null; then\n  echo \"This script relies on the 'gh' command from the Github CLI package, please install it\"\n  EXIT=1\nfi\n\nif [ $EXIT ]; then\n  exit $EXIT\nfi\n\nif [ -z \"$NEW_VERSION\" ]; then\n  NEW_VERSION=$(jq -r \".version\" manifest-beta.json | awk -F. -v OFS=. '{$NF += 1 ; print}')\nfi\n\nif [ -z \"$NEW_VERSION\" ]; then\n  echo \"Auto-generating next version number failed, please specify next version : ./scripts/release 0.x.y\"\n  exit 1\nfi\n\n# Let users edit release-notes.txt for release notes.\nrm -f release-notes.md\ntouch release-notes.md\necho -e \"# ${NEW_VERSION}\\n\\n\" >> release-notes.md\n$EDITOR release-notes.md\n\n# Append release notes to CHANGELOG.md.\nmv CHANGELOG.md CHANGELOG-TEMP.md\ncp release-notes.md CHANGELOG.md\necho -e \"\\n---\\n\" >> CHANGELOG.md\ncat CHANGELOG-TEMP.md >> CHANGELOG.md\nrm -f CHANGELOG-TEMP.md\n\n# Overwrite the documentation changelog.\ncp -f CHANGELOG.md docs/docs/changelog.md\n\n# Delete old files if they exist\nrm -f package.tmp.json\nrm -f manifest.tmp.json\nrm -f manifest-beta.tmp.json\nrm -f versions.tmp.json\n\n# Rewrite versions in relevant files.\njq \".version=\\\"${NEW_VERSION}\\\"\" package.json > package.tmp.json && mv package.tmp.json package.json\njq \".version=\\\"${NEW_VERSION}\\\"\" manifest.json > manifest.tmp.json && mv manifest.tmp.json manifest.json\njq \".version=\\\"${NEW_VERSION}\\\"\" manifest-beta.json > manifest-beta.tmp.json && mv manifest-beta.tmp.json manifest-beta.json\njq \". + {\\\"${NEW_VERSION}\\\": \\\"0.12.0\\\"}\" versions.json > versions.tmp.json && mv versions.tmp.json versions.json\n\n# Overwrite the beta manifest as well.\ncp manifest.json manifest-beta.json\n\n# Create commit & commit.\ngit commit -a -m \"Auto-release ${NEW_VERSION}\"\ngit push\n\n# Rebuild the project to prepare for a release.\nnpm run build\n\n# release api\nnpm publish --access public\n\n# And do a github release.\ngh release create \"${NEW_VERSION}\" build/main.js styles.css manifest.json --title \"${NEW_VERSION}\" --notes-file release-notes.md\nrm -f release-notes.md\n"
  },
  {
    "path": "src/api/data-array.ts",
    "content": "import { Groupings, Values } from \"data-model/value\";\nimport { QuerySettings } from \"settings\";\n\n/** A function which maps an array element to some value. */\nexport type ArrayFunc<T, O> = (elem: T, index: number, arr: T[]) => O;\n\n/** A function which compares two types. */\nexport type ArrayComparator<T> = (a: T, b: T) => number;\n\n/** Finds the value of the lowest value type in a grouping. */\nexport type LowestKey<T> = T extends { key: any; rows: any } ? LowestKey<T[\"rows\"][0]> : T;\n\n/** A ridiculous type which properly types the result of the 'groupIn' command. */\nexport type Ingrouped<U, T> = T extends { key: any; rows: any }\n    ? { key: T[\"key\"]; rows: Ingrouped<U, T[\"rows\"][0]> }\n    : { key: U; rows: T[] };\n\n/**\n * Proxied interface which allows manipulating array-based data. All functions on a data array produce a NEW array\n * (i.e., the arrays are immutable).\n */\nexport interface DataArray<T> {\n    /** The total number of elements in the array. */\n    length: number;\n\n    /** Filter the data array down to just elements which match the given predicate. */\n    where(predicate: ArrayFunc<T, boolean>): DataArray<T>;\n    /** Alias for 'where' for people who want array semantics. */\n    filter(predicate: ArrayFunc<T, boolean>): DataArray<T>;\n\n    /** Map elements in the data array by applying a function to each. */\n    map<U>(f: ArrayFunc<T, U>): DataArray<U>;\n    /** Map elements in the data array by applying a function to each, then flatten the results to produce a new array. */\n    flatMap<U>(f: ArrayFunc<T, U[]>): DataArray<U>;\n    /** Mutably change each value in the array, returning the same array which you can further chain off of. */\n    mutate(f: ArrayFunc<T, void>): DataArray<T>;\n\n    /** Limit the total number of entries in the array to the given value. */\n    limit(count: number): DataArray<T>;\n    /**\n     * Take a slice of the array. If `start` is undefined, it is assumed to be 0; if `end` is undefined, it is assumed\n     * to be the end of the array.\n     */\n    slice(start?: number, end?: number): DataArray<T>;\n    /** Concatenate the values in this data array with those of another iterable / data array / array. */\n    concat(other: Iterable<T>): DataArray<T>;\n\n    /** Return the first index of the given (optionally starting the search) */\n    indexOf(element: T, fromIndex?: number): number;\n    /** Return the first element that satisfies the given predicate. */\n    find(pred: ArrayFunc<T, boolean>): T | undefined;\n    /** Find the index of the first element that satisfies the given predicate. Returns -1 if nothing was found. */\n    findIndex(pred: ArrayFunc<T, boolean>, fromIndex?: number): number;\n    /** Returns true if the array contains the given element, and false otherwise. */\n    includes(element: T): boolean;\n\n    /**\n     * Return a string obtained by converting each element in the array to a string, and joining it with the\n     * given separator (which defaults to ', ').\n     */\n    join(sep?: string): string;\n\n    /**\n     * Return a sorted array sorted by the given key; an optional comparator can be provided, which will\n     * be used to compare the keys in lieu of the default dataview comparator.\n     */\n    sort<U>(key: ArrayFunc<T, U>, direction?: \"asc\" | \"desc\", comparator?: ArrayComparator<U>): DataArray<T>;\n\n    /**\n     * Mutably modify the current array with an in place sort; this is less flexible than a regular sort in exchange\n     * for being a little more performant. Only use this is performance is a serious consideration.\n     */\n    sortInPlace<U>(key: (v: T) => U, direction?: \"asc\" | \"desc\", comparator?: ArrayComparator<U>): DataArray<T>;\n\n    /**\n     * Return an array where elements are grouped by the given key; the resulting array will have objects of the form\n     * { key: <key value>, rows: DataArray }.\n     */\n    groupBy<U>(key: ArrayFunc<T, U>, comparator?: ArrayComparator<U>): DataArray<{ key: U; rows: DataArray<T> }>;\n\n    /**\n     * If the array is not grouped, groups it as `groupBy` does; otherwise, groups the elements inside each current\n     * group. This allows for top-down recursive grouping which may be easier than bottom-up grouping.\n     */\n    groupIn<U>(key: ArrayFunc<LowestKey<T>, U>, comparator?: ArrayComparator<U>): DataArray<Ingrouped<U, T>>;\n\n    /**\n     * Return distinct entries. If a key is provided, then rows with distinct keys are returned.\n     */\n    distinct<U>(key?: ArrayFunc<T, U>, comparator?: ArrayComparator<U>): DataArray<T>;\n\n    /** Return true if the predicate is true for all values. */\n    every(f: ArrayFunc<T, boolean>): boolean;\n    /** Return true if the predicate is true for at least one value. */\n    some(f: ArrayFunc<T, boolean>): boolean;\n    /** Return true if the predicate is FALSE for all values. */\n    none(f: ArrayFunc<T, boolean>): boolean;\n\n    /** Return the first element in the data array. Returns undefined if the array is empty. */\n    first(): T;\n    /** Return the last element in the data array. Returns undefined if the array is empty. */\n    last(): T;\n\n    /** Map every element in this data array to the given key, and then flatten it.*/\n    to(key: string): DataArray<any>;\n    /** Map every element in this data array to the given key; unlike to(), does not flatten the result. */\n    into(key: string): DataArray<any>;\n\n    /**\n     * Recursively expand the given key, flattening a tree structure based on the key into a flat array. Useful for handling\n     * hierarchical data like tasks with 'subtasks'.\n     */\n    expand(key: string): DataArray<any>;\n\n    /** Run a lambda on each element in the array. */\n    forEach(f: ArrayFunc<T, void>): void;\n\n    /** Calculate the sum of the elements in the array. */\n    sum(): number;\n\n    /** Calculate the average of the elements in the array. */\n    avg(): number;\n\n    /** Calculate the minimum of the elements in the array. */\n    min(): number;\n\n    /** Calculate the maximum of the elements in the array. */\n    max(): number;\n\n    /** Convert this to a plain javascript array. */\n    array(): T[];\n\n    /** Allow iterating directly over the array. */\n    [Symbol.iterator](): Iterator<T>;\n\n    /** Map indexes to values. */\n    [index: number]: any;\n    /** Automatic flattening of fields. Equivalent to implicitly calling `array.to(\"field\")` */\n    [field: string]: any;\n}\n\n/** Implementation of DataArray, minus the dynamic variable access, which is implemented via proxy. */\nclass DataArrayImpl<T> implements DataArray<T> {\n    private static ARRAY_FUNCTIONS: Set<string> = new Set([\n        \"where\",\n        \"filter\",\n        \"map\",\n        \"flatMap\",\n        \"mutate\",\n        \"slice\",\n        \"concat\",\n        \"indexOf\",\n        \"limit\",\n        \"find\",\n        \"findIndex\",\n        \"includes\",\n        \"join\",\n        \"sort\",\n        \"sortInPlace\",\n        \"groupBy\",\n        \"groupIn\",\n        \"distinct\",\n        \"every\",\n        \"some\",\n        \"none\",\n        \"first\",\n        \"last\",\n        \"to\",\n        \"into\",\n        \"lwrap\",\n        \"expand\",\n        \"forEach\",\n        \"length\",\n        \"values\",\n        \"array\",\n        \"defaultComparator\",\n        \"toString\",\n        \"settings\",\n        \"sum\",\n        \"avg\",\n        \"min\",\n        \"max\",\n    ]);\n\n    private static ARRAY_PROXY: ProxyHandler<DataArrayImpl<any>> = {\n        get: function (target, prop, receiver) {\n            if (typeof prop === \"symbol\") return (target as any)[prop];\n            else if (typeof prop === \"number\") return target.values[prop];\n            else if (prop === \"constructor\") return target.values.constructor;\n            else if (!isNaN(parseInt(prop))) return target.values[parseInt(prop)];\n            else if (DataArrayImpl.ARRAY_FUNCTIONS.has(prop.toString())) return target[prop.toString()];\n\n            return target.to(prop);\n        },\n    };\n\n    public static wrap<T>(\n        arr: T[],\n        settings: QuerySettings,\n        defaultComparator: ArrayComparator<any> = Values.compareValue\n    ): DataArray<T> {\n        return new Proxy<DataArrayImpl<T>>(\n            new DataArrayImpl<T>(arr, settings, defaultComparator),\n            DataArrayImpl.ARRAY_PROXY\n        );\n    }\n\n    public length: number;\n    [key: string]: any;\n\n    private constructor(\n        public values: any[],\n        public settings: QuerySettings,\n        public defaultComparator: ArrayComparator<any> = Values.compareValue\n    ) {\n        this.length = values.length;\n    }\n\n    private lwrap<U>(values: U[]): DataArray<U> {\n        return DataArrayImpl.wrap(values, this.settings, this.defaultComparator);\n    }\n\n    public where(predicate: ArrayFunc<T, boolean>): DataArray<T> {\n        return this.lwrap(this.values.filter(predicate));\n    }\n\n    public filter(predicate: ArrayFunc<T, boolean>): DataArray<T> {\n        return this.where(predicate);\n    }\n\n    public map<U>(f: ArrayFunc<T, U>): DataArray<U> {\n        return this.lwrap(this.values.map(f));\n    }\n\n    public flatMap<U>(f: ArrayFunc<T, U[]>): DataArray<U> {\n        let result = [];\n        for (let index = 0; index < this.length; index++) {\n            let value = f(this.values[index], index, this.values);\n            if (!value || value.length == 0) continue;\n\n            for (let r of value) result.push(r);\n        }\n\n        return this.lwrap(result);\n    }\n\n    public mutate(f: ArrayFunc<T, void>): DataArray<T> {\n        for (let index = 0; index < this.values.length; index++) {\n            f(this.values[index], index, this.values);\n        }\n\n        return this as any;\n    }\n\n    public limit(count: number): DataArray<T> {\n        return this.lwrap(this.values.slice(0, count));\n    }\n\n    public slice(start?: number, end?: number): DataArray<T> {\n        return this.lwrap(this.values.slice(start, end));\n    }\n\n    public concat(other: DataArray<T>): DataArray<T> {\n        return this.lwrap(this.values.concat(other.values));\n    }\n\n    /** Return the first index of the given (optionally starting the search) */\n    public indexOf(element: T, fromIndex?: number): number {\n        return this.findIndex(e => this.defaultComparator(e, element) == 0, fromIndex);\n    }\n\n    /** Return the first element that satisfies the given predicate. */\n    public find(pred: ArrayFunc<T, boolean>): T | undefined {\n        let index = this.findIndex(pred);\n        if (index == -1) return undefined;\n        else return this.values[index];\n    }\n\n    public findIndex(pred: ArrayFunc<T, boolean>, fromIndex?: number): number {\n        for (let index = fromIndex ?? 0; index < this.length; index++) {\n            if (pred(this.values[index], index, this.values)) return index;\n        }\n\n        return -1;\n    }\n\n    public includes(element: T): boolean {\n        return this.indexOf(element, 0) != -1;\n    }\n\n    public join(sep?: string): string {\n        return this.map(s => Values.toString(s, this.settings))\n            .array()\n            .join(sep ?? \", \");\n    }\n\n    public sort<U>(key?: ArrayFunc<T, U>, direction?: \"asc\" | \"desc\", comparator?: ArrayComparator<U>): DataArray<T> {\n        if (this.values.length == 0) return this;\n        let realComparator = comparator ?? this.defaultComparator;\n        let realKey = key ?? ((l: T) => l as any as U);\n\n        // Associate each entry with it's index for the key function, and then do a normal sort.\n        let copy = ([] as any[]).concat(this.array()).map((elem, index) => {\n            return { index: index, value: elem };\n        });\n        copy.sort((a, b) => {\n            let aKey = realKey(a.value, a.index, this.values);\n            let bKey = realKey(b.value, b.index, this.values);\n            return direction === \"desc\" ? -realComparator(aKey, bKey) : realComparator(aKey, bKey);\n        });\n\n        return this.lwrap(copy.map(e => e.value));\n    }\n\n    public sortInPlace<U>(\n        key?: (value: T) => U,\n        direction?: \"asc\" | \"desc\",\n        comparator?: ArrayComparator<U>\n    ): DataArray<T> {\n        if (this.values.length == 0) return this;\n        let realComparator = comparator ?? this.defaultComparator;\n        let realKey = key ?? ((l: T) => l as any as U);\n\n        this.values.sort((a, b) => {\n            let aKey = realKey(a);\n            let bKey = realKey(b);\n\n            return direction == \"desc\" ? -realComparator(aKey, bKey) : realComparator(aKey, bKey);\n        });\n\n        return this;\n    }\n\n    public groupBy<U>(\n        key: ArrayFunc<T, U>,\n        comparator?: ArrayComparator<U>\n    ): DataArray<{ key: U; rows: DataArray<T> }> {\n        if (this.values.length == 0) return this.lwrap([]);\n\n        // JavaScript sucks and we can't make hash maps over arbitrary types (only strings/ints), so\n        // we do a poor man algorithm where we SORT, followed by grouping.\n        let intermediate = this.sort(key, \"asc\", comparator);\n        comparator = comparator ?? this.defaultComparator;\n\n        let result: { key: U; rows: DataArray<T> }[] = [];\n        let currentRow = [intermediate[0]];\n        let current = key(intermediate[0], 0, intermediate.values);\n        for (let index = 1; index < intermediate.length; index++) {\n            let newKey = key(intermediate[index], index, intermediate.values);\n            if (comparator(current, newKey) != 0) {\n                result.push({ key: current, rows: this.lwrap(currentRow) });\n                current = newKey;\n                currentRow = [intermediate[index]];\n            } else {\n                currentRow.push(intermediate[index]);\n            }\n        }\n        result.push({ key: current, rows: this.lwrap(currentRow) });\n\n        return this.lwrap(result);\n    }\n\n    public groupIn<U>(key: ArrayFunc<LowestKey<T>, U>, comparator?: ArrayComparator<U>): DataArray<Ingrouped<U, T>> {\n        if (Groupings.isGrouping(this.values)) {\n            return this.map(v => {\n                return {\n                    key: (v as any).key,\n                    rows: DataArray.wrap((v as any).rows, this.settings).groupIn(key as any, comparator as any),\n                } as any;\n            });\n        } else {\n            return this.groupBy(key as any, comparator) as any;\n        }\n    }\n\n    public distinct<U>(key?: ArrayFunc<T, U>, comparator?: ArrayComparator<U>): DataArray<T> {\n        if (this.values.length == 0) return this;\n        let realKey = key ?? (x => x as any as U);\n\n        // For similar reasons to groupBy, do a sort and take the first element of each block.\n        let intermediate = this.map((x, index) => {\n            return { key: realKey(x, index, this.values), value: x };\n        }).sort(x => x.key, \"asc\", comparator);\n        comparator = comparator ?? this.defaultComparator;\n\n        let result: T[] = [intermediate[0].value];\n        for (let index = 1; index < intermediate.length; index++) {\n            if (comparator(intermediate[index - 1].key, intermediate[index].key) != 0) {\n                result.push(intermediate[index].value);\n            }\n        }\n\n        return this.lwrap(result);\n    }\n\n    public every(f: ArrayFunc<T, boolean>): boolean {\n        return this.values.every(f);\n    }\n\n    public some(f: ArrayFunc<T, boolean>): boolean {\n        return this.values.some(f);\n    }\n\n    public none(f: ArrayFunc<T, boolean>): boolean {\n        return this.values.every((v, i, a) => !f(v, i, a));\n    }\n\n    public first(): T {\n        return this.values.length > 0 ? this.values[0] : undefined;\n    }\n    public last(): T {\n        return this.values.length > 0 ? this.values[this.values.length - 1] : undefined;\n    }\n\n    public to(key: string): DataArray<any> {\n        let result: any[] = [];\n        for (let child of this.values) {\n            let value = child[key];\n            if (value === undefined || value === null) continue;\n\n            if (Array.isArray(value) || DataArray.isDataArray(value)) value.forEach(v => result.push(v));\n            else result.push(value);\n        }\n\n        return this.lwrap(result);\n    }\n\n    public into(key: string): DataArray<any> {\n        let result: any[] = [];\n        for (let child of this.values) {\n            let value = child[key];\n            if (value === undefined || value === null) continue;\n\n            result.push(value);\n        }\n\n        return this.lwrap(result);\n    }\n\n    public expand(key: string): DataArray<any> {\n        let result = [];\n        let queue: any[] = ([] as any[]).concat(this.values);\n\n        while (queue.length > 0) {\n            let next = queue.pop();\n            let value = next[key];\n\n            if (value === undefined || value === null) continue;\n            if (Array.isArray(value)) value.forEach(v => queue.push(v));\n            else if (value instanceof DataArrayImpl) value.forEach(v => queue.push(v));\n            else queue.push(value);\n\n            result.push(next);\n        }\n\n        return this.lwrap(result);\n    }\n\n    public forEach(f: ArrayFunc<T, void>) {\n        for (let index = 0; index < this.values.length; index++) {\n            f(this.values[index], index, this.values);\n        }\n    }\n\n    public sum() {\n        return this.values.reduce((a, b) => a + b, 0);\n    }\n\n    public avg() {\n        return this.sum() / this.values.length;\n    }\n\n    public min() {\n        return Math.min(...this.values);\n    }\n\n    public max() {\n        return Math.max(...this.values);\n    }\n\n    public array(): T[] {\n        return ([] as any[]).concat(this.values);\n    }\n\n    public [Symbol.iterator](): Iterator<T> {\n        return this.values[Symbol.iterator]();\n    }\n\n    public toString(): string {\n        return \"[\" + this.values.join(\", \") + \"]\";\n    }\n}\n\n/** Provides utility functions for generating data arrays. */\nexport namespace DataArray {\n    /** Create a new Dataview data array. */\n    export function wrap<T>(raw: T[] | DataArray<T>, settings: QuerySettings): DataArray<T> {\n        if (isDataArray(raw)) return raw;\n        return DataArrayImpl.wrap(raw, settings);\n    }\n\n    /** Create a new DataArray from an iterable object. */\n    export function from<T>(raw: Iterable<T>, settings: QuerySettings): DataArray<T> {\n        if (isDataArray(raw)) return raw;\n\n        let data = [];\n        for (let elem of raw) data.push(elem);\n        return DataArrayImpl.wrap(data, settings);\n    }\n\n    /** Return true if the given object is a data array. */\n    export function isDataArray(obj: any): obj is DataArray<any> {\n        return obj instanceof DataArrayImpl;\n    }\n}\n\n// A scary looking polyfill, sure, but it fixes up data array/array interop for us.\nconst oldArrayIsArray = Array.isArray;\nArray.isArray = (arg): arg is any[] => {\n    return oldArrayIsArray(arg) || DataArray.isDataArray(arg);\n};\n"
  },
  {
    "path": "src/api/extensions.ts",
    "content": "import { STask } from \"data-model/serialized/markdown\";\n\n/** A general function for deciding how to check a task given it's current state. */\nexport type TaskStatusSelector = (task: STask) => Promise<string>;\n\n/**\n * A dataview extension; allows for registering new functions, altering views, and altering some more\n * advanced dataview behavior.\n **/\nexport class Extension {\n    /** All registered task status selectors for this extension. */\n    public taskStatusSelectors: Record<string, TaskStatusSelector>;\n\n    public constructor(public plugin: string) {\n        this.taskStatusSelectors = {};\n    }\n\n    /** Register a task status selector under the given name. */\n    public taskStatusSelector(name: string, selector: TaskStatusSelector): Extension {\n        this.taskStatusSelectors[name] = selector;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/api/inline-api.ts",
    "content": "/** Fancy wrappers for the JavaScript API, used both by external plugins AND by the dataview javascript view. */\n\nimport { App, Component } from \"obsidian\";\nimport { FullIndex } from \"data-index\";\nimport { renderValue, renderErrorPre } from \"ui/render\";\nimport type { DataviewApi, DataviewIOApi, QueryApiSettings, QueryResult } from \"api/plugin-api\";\nimport { DataviewSettings, ExportSettings } from \"settings\";\nimport { DataObject, Grouping, Link, Literal, Values, Widgets } from \"data-model/value\";\nimport { BoundFunctionImpl, DEFAULT_FUNCTIONS, Functions } from \"expression/functions\";\nimport { Context } from \"expression/context\";\nimport { defaultLinkHandler } from \"query/engine\";\nimport { DateTime, Duration } from \"luxon\";\nimport * as Luxon from \"luxon\";\nimport { DataArray } from \"./data-array\";\nimport { SListItem } from \"data-model/serialized/markdown\";\nimport { EXPRESSION } from \"expression/parse\";\nimport { Result } from \"api/result\";\n\n/** Asynchronous API calls related to file / system IO. */\nexport class DataviewInlineIOApi {\n    public constructor(public api: DataviewIOApi, public currentFile: string) {}\n\n    /** Load the contents of a CSV asynchronously, returning a data array of rows (or undefined if it does not exist). */\n    public async csv(path: string, originFile?: string): Promise<DataArray<DataObject> | undefined> {\n        return this.api.csv(path, originFile || this.currentFile);\n    }\n\n    /** Asynchronously load the contents of any link or path in an Obsidian vault. */\n    public async load(path: Link | string, originFile?: string): Promise<string | undefined> {\n        return this.api.load(path, originFile || this.currentFile);\n    }\n\n    /** Normalize a link or path relative to an optional origin file. Returns a textual fully-qualified-path. */\n    public normalize(path: Link | string, originFile?: string): string {\n        return this.api.normalize(path, originFile || this.currentFile);\n    }\n}\n\nexport class DataviewInlineApi {\n    /**\n     * The raw dataview indices, which track file <-> metadata relations. Use these if the intuitive API does not support\n     * your use case.\n     */\n    public index: FullIndex;\n\n    /** The component that handles the lifetime of this view. Use it if you are adding custom event handlers/components. */\n    public component: Component;\n\n    /** The path to the current file this script is running in. */\n    public currentFilePath: string;\n\n    /**\n     * The container which holds the output of this view. You can directly append fields to this, if you wish, though\n     * the rendering API is likely to be easier for straight-forward purposes.\n     */\n    public container: HTMLElement;\n\n    /** Directly access the Obsidian app object, such as for reaching out to other plugins. */\n    public app: App;\n\n    /** The general plugin API which much of this inline API delegates to. */\n    public api: DataviewApi;\n\n    /** Settings which determine defaults, incl. many rendering options. */\n    public settings: DataviewSettings;\n\n    /** Evaluation context which expressions can be evaluated in. */\n    public evaluationContext: Context;\n\n    /** Value utilities which allow for type-checking and comparisons. */\n    public value = Values;\n\n    /** Widget utility functions for creating built-in widgets. */\n    public widget = Widgets;\n\n    /** IO utilities which are largely asynchronous. */\n    public io: DataviewInlineIOApi;\n\n    /** Re-exporting of luxon for people who can't easily require it. Sorry! */\n    public luxon = Luxon;\n\n    /** Dataview functions which can be called from DataviewJS. */\n    public func: Record<string, BoundFunctionImpl>;\n\n    constructor(api: DataviewApi, component: Component, container: HTMLElement, currentFilePath: string) {\n        this.index = api.index;\n        this.app = api.app;\n        this.settings = api.settings;\n\n        this.component = component;\n        this.container = container;\n        this.currentFilePath = currentFilePath;\n\n        this.api = api;\n        this.io = new DataviewInlineIOApi(this.api.io, this.currentFilePath);\n\n        // Set up the evaluation context with variables from the current file.\n        let fileMeta = this.index.pages.get(this.currentFilePath)?.serialize(this.index) ?? {};\n        this.evaluationContext = new Context(defaultLinkHandler(this.index, this.currentFilePath), this.settings, {\n            this: fileMeta,\n        });\n\n        this.func = Functions.bindAll(DEFAULT_FUNCTIONS, this.evaluationContext);\n    }\n\n    /////////////////////////////\n    // Index + Data Collection //\n    /////////////////////////////\n\n    /** Return an array of paths (as strings) corresponding to pages which match the query. */\n    public pagePaths(query?: string): DataArray<string> {\n        return this.api.pagePaths(query, this.currentFilePath);\n    }\n\n    /** Map a page path to the actual data contained within that page. */\n    public page(path: string | Link): DataObject | undefined {\n        return this.api.page(path, this.currentFilePath);\n    }\n\n    /** Return an array of page objects corresponding to pages which match the query. */\n    public pages(query?: string): DataArray<any> {\n        return this.api.pages(query, this.currentFilePath);\n    }\n\n    /** Return the information about the current page. */\n    public current(): Record<string, any> | undefined {\n        return this.page(this.currentFilePath);\n    }\n\n    ///////////////////////////////\n    // Dataview Query Evaluation //\n    ///////////////////////////////\n\n    /** Execute a Dataview query, returning the results in programmatic form. */\n    public async query(\n        source: string,\n        originFile?: string,\n        settings?: QueryApiSettings\n    ): Promise<Result<QueryResult, string>> {\n        return this.api.query(source, originFile ?? this.currentFilePath, settings);\n    }\n\n    /** Error-throwing version of {@link query}. */\n    public async tryQuery(source: string, originFile?: string, settings?: QueryApiSettings): Promise<QueryResult> {\n        return this.api.tryQuery(source, originFile ?? this.currentFilePath, settings);\n    }\n\n    /** Execute a Dataview query, returning the results in Markdown. */\n    public async queryMarkdown(\n        source: string,\n        originFile?: string,\n        settings?: QueryApiSettings\n    ): Promise<Result<string, string>> {\n        return this.api.queryMarkdown(source, originFile ?? this.currentFilePath, settings);\n    }\n\n    /** Error-throwing version of {@link queryMarkdown}. */\n    public async tryQueryMarkdown(source: string, originFile?: string, settings?: QueryApiSettings): Promise<string> {\n        return this.api.tryQueryMarkdown(source, originFile ?? this.currentFilePath, settings);\n    }\n\n    /**\n     * Evaluate a dataview expression (like '2 + 2' or 'link(\"hello\")'), returning the evaluated result.\n     * This takes an optional second argument which provides definitions for variables, such as:\n     *\n     * ```\n     * dv.evaluate(\"x + 6\", { x: 2 }) = 8\n     * dv.evaluate('link(target)', { target: \"Okay\" }) = [[Okay]]\n     * ```\n     *\n     * Note that `this` is implicitly available and refers to the current file.\n     *\n     * This method returns a Result type instead of throwing an error; you can check the result of the\n     * execution via `result.successful` and obtain `result.value` or `result.error` accordingly. If\n     * you'd rather this method throw on an error, use `dv.tryEvaluate`.\n     */\n    public evaluate(expression: string, context?: DataObject): Result<Literal, string> {\n        let field = EXPRESSION.field.parse(expression);\n        if (!field.status) return Result.failure(`Failed to parse expression \"${expression}\"`);\n\n        return this.evaluationContext.evaluate(field.value, context);\n    }\n\n    /** Error-throwing version of `dv.evaluate`. */\n    public tryEvaluate(expression: string, context?: DataObject): Literal {\n        return this.evaluate(expression, context).orElseThrow();\n    }\n\n    /** Execute a Dataview query and embed it into the current view. */\n    public async execute(source: string) {\n        this.api.execute(source, this.container, this.component, this.currentFilePath);\n    }\n\n    /** Execute a DataviewJS query and embed it into the current view. */\n    public async executeJs(code: string) {\n        this.api.executeJs(code, this.container, this.component, this.currentFilePath);\n    }\n\n    /////////////\n    // Utility //\n    /////////////\n\n    /**\n     * Convert an input element or array into a Dataview data-array. If the input is already a data array,\n     * it is returned unchanged.\n     */\n    public array(raw: any): DataArray<any> {\n        return this.api.array(raw);\n    }\n\n    /** Return true if the given value is a javascript array OR a dataview data array. */\n    public isArray(raw: any): raw is DataArray<any> | Array<any> {\n        return this.api.isArray(raw);\n    }\n\n    /** Return true if the given value is a dataview data array; this returns FALSE for plain JS arrays. */\n    public isDataArray(raw: unknown): raw is DataArray<any> {\n        return DataArray.isDataArray(raw);\n    }\n\n    /** Create a dataview file link to the given path. */\n    public fileLink(path: string, embed: boolean = false, display?: string): Link {\n        return Link.file(path, embed, display);\n    }\n\n    /** Create a dataview section link to the given path. */\n    public sectionLink(path: string, section: string, embed: boolean = false, display?: string): Link {\n        return Link.header(path, section, embed, display);\n    }\n\n    /** Create a dataview block link to the given path. */\n    public blockLink(path: string, blockId: string, embed: boolean = false, display?: string): Link {\n        return Link.block(path, blockId, embed, display);\n    }\n\n    /** Attempt to extract a date from a string, link or date. */\n    public date(pathlike: string | Link | DateTime): DateTime | null {\n        return this.api.date(pathlike);\n    }\n\n    /** Attempt to extract a duration from a string or duration. */\n    public duration(dur: string | Duration): Duration | null {\n        return this.api.duration(dur);\n    }\n\n    /** Parse a raw textual value into a complex Dataview type, if possible. */\n    public parse(value: string): Literal {\n        return this.api.parse(value);\n    }\n\n    /** Convert a basic JS type into a Dataview type by parsing dates, links, durations, and so on. */\n    public literal(value: any): Literal {\n        return this.api.literal(value);\n    }\n\n    /** Deep clone the given literal, returning a new literal which is independent of the original. */\n    public clone(value: Literal): Literal {\n        return Values.deepCopy(value);\n    }\n\n    /**\n     * Compare two arbitrary JavaScript values using Dataview's default comparison rules. Returns a negative value if\n     * a < b, 0 if a = b, and a positive value if a > b.\n     */\n    public compare(a: any, b: any): number {\n        return Values.compareValue(a, b);\n    }\n\n    /** Return true if the two given JavaScript values are equal using Dataview's default comparison rules. */\n    public equal(a: any, b: any): boolean {\n        return this.compare(a, b) == 0;\n    }\n\n    /////////////////////////\n    // Rendering Functions //\n    /////////////////////////\n\n    /** Render an HTML element, containing arbitrary text. */\n    public el<K extends keyof HTMLElementTagNameMap>(\n        el: K,\n        text: any,\n        { container = this.container, ...options }: DomElementInfo & { container?: HTMLElement } = {}\n    ): HTMLElementTagNameMap[K] {\n        let wrapped = Values.wrapValue(text);\n\n        if (wrapped === null || wrapped === undefined) {\n            return container.createEl(el, Object.assign({ text }, options));\n        }\n\n        let _el = container.createEl(el, options);\n        renderValue(this.app, wrapped.value, _el, this.currentFilePath, this.component, this.settings, true);\n        return _el;\n    }\n\n    /** Render an HTML header; the level can be anything from 1 - 6. */\n    public header(level: number, text: any, options?: DomElementInfo): HTMLHeadingElement {\n        let header = { 1: \"h1\", 2: \"h2\", 3: \"h3\", 4: \"h4\", 5: \"h5\", 6: \"h6\" }[level];\n        if (!header) throw Error(`Unrecognized level '${level}' (expected 1, 2, 3, 4, 5, or 6)`);\n\n        return this.el(header as keyof HTMLElementTagNameMap, text, options) as HTMLHeadingElement;\n    }\n\n    /** Render an HTML paragraph, containing arbitrary text. */\n    public paragraph(text: any, options?: DomElementInfo): HTMLParagraphElement {\n        return this.el(\"p\", text, options);\n    }\n\n    /** Render an inline span, containing arbitrary text. */\n    public span(text: any, options?: DomElementInfo): HTMLSpanElement {\n        return this.el(\"span\", text, options);\n    }\n\n    /**\n     * Render HTML from the output of a template \"view\" saved as a file in the vault.\n     * Takes a filename and arbitrary input data.\n     */\n    public async view(viewName: string, input: any) {\n        // Look for `${viewName}.js` first, then for `${viewName}/view.js`.\n        const simpleViewPath = `${viewName}.js`;\n        const complexViewPath = `${viewName}/view.js`;\n        let checkForCss = false;\n        let cssElement = undefined;\n        let viewFile = this.app.metadataCache.getFirstLinkpathDest(simpleViewPath, this.currentFilePath);\n        if (!viewFile) {\n            viewFile = this.app.metadataCache.getFirstLinkpathDest(complexViewPath, this.currentFilePath);\n            checkForCss = true;\n        }\n\n        if (!viewFile) {\n            renderErrorPre(\n                this.container,\n                `Dataview: custom view not found for '${simpleViewPath}' or '${complexViewPath}'.`\n            );\n            return;\n        }\n\n        if (checkForCss) {\n            // Check for optional CSS.\n            let cssFile = this.app.metadataCache.getFirstLinkpathDest(`${viewName}/view.css`, this.currentFilePath);\n            if (cssFile) {\n                let cssContents = await this.app.vault.read(cssFile);\n                cssContents += `\\n/*# sourceURL=${location.origin}/${cssFile.path} */`;\n                cssElement = this.container.createEl(\"style\", { text: cssContents, attr: { scope: \" \" } });\n            }\n        }\n\n        let contents = await this.app.vault.read(viewFile);\n        if (contents.contains(\"await\")) contents = \"(async () => { \" + contents + \" })()\";\n        contents += `\\n//# sourceURL=${viewFile.path}`;\n        let func = new Function(\"dv\", \"input\", contents);\n\n        try {\n            // This may directly render, in which case it will likely return undefined or null.\n            let result = await Promise.resolve(func(this, input));\n            if (result)\n                await renderValue(\n                    this.app,\n                    result as any,\n                    this.container,\n                    this.currentFilePath,\n                    this.component,\n                    this.settings,\n                    true\n                );\n        } catch (ex) {\n            if (cssElement) this.container.removeChild(cssElement);\n            renderErrorPre(this.container, `Dataview: Failed to execute view '${viewFile.path}'.\\n\\n${ex}`);\n        }\n    }\n\n    /** Render a dataview list of the given values. */\n    public list(values?: any[] | DataArray<any>) {\n        return this.api.list(values, this.container, this.component, this.currentFilePath);\n    }\n\n    /** Render a dataview table with the given headers, and the 2D array of values. */\n    public table(headers: string[], values?: any[][] | DataArray<any>) {\n        return this.api.table(headers, values, this.container, this.component, this.currentFilePath);\n    }\n\n    /** Render a dataview task view with the given tasks. */\n    public taskList(tasks: Grouping<SListItem>, groupByFile: boolean = true) {\n        return this.api.taskList(tasks, groupByFile, this.container, this.component, this.currentFilePath);\n    }\n\n    ////////////////////////\n    // Markdown Rendering //\n    ////////////////////////\n\n    /** Render a table directly to markdown, returning the markdown. */\n    public markdownTable(\n        headers: string[],\n        values?: any[][] | DataArray<any>,\n        settings?: Partial<ExportSettings>\n    ): string {\n        return this.api.markdownTable(headers, values, settings);\n    }\n\n    /** Render a list directly to markdown, returning the markdown. */\n    public markdownList(values?: any[] | DataArray<any> | undefined, settings?: Partial<ExportSettings>) {\n        return this.api.markdownList(values, settings);\n    }\n\n    /** Render at ask list directly to markdown, returning the markdown. */\n    public markdownTaskList(values: Grouping<SListItem>, settings?: Partial<ExportSettings>) {\n        return this.api.markdownTaskList(values, settings);\n    }\n}\n\n/**\n * Evaluate a script where 'this' for the script is set to the given context. Allows you to define global variables.\n */\nexport function evalInContext(script: string, context: any): any {\n    return function () {\n        return eval(script);\n    }.call(context);\n}\n\n/**\n * Evaluate a script possibly asynchronously, if the script contains `async/await` blocks.\n */\nexport async function asyncEvalInContext(script: string, context: any): Promise<any> {\n    if (script.includes(\"await\")) {\n        return evalInContext(\"(async () => { \" + script + \" })()\", context) as Promise<any>;\n    } else {\n        return Promise.resolve(evalInContext(script, context));\n    }\n}\n"
  },
  {
    "path": "src/api/plugin-api.ts",
    "content": "/** The general, externally accessible plugin API (available at `app.plugins.plugins.dataview.api` or as global `DataviewAPI`). */\n\nimport { App, Component, MarkdownPostProcessorContext, TFile } from \"obsidian\";\nimport { FullIndex } from \"data-index/index\";\nimport { matchingSourcePaths } from \"data-index/resolver\";\nimport { Sources } from \"data-index/source\";\nimport { DataObject, Grouping, Groupings, Link, Literal, Values, Widgets } from \"data-model/value\";\nimport { EXPRESSION } from \"expression/parse\";\nimport { renderCodeBlock, renderErrorPre, renderValue } from \"ui/render\";\nimport { DataArray } from \"./data-array\";\nimport { BoundFunctionImpl, DEFAULT_FUNCTIONS, Functions } from \"expression/functions\";\nimport { Context } from \"expression/context\";\nimport {\n    defaultLinkHandler,\n    executeCalendar,\n    executeInline,\n    executeList,\n    executeTable,\n    executeTask,\n    IdentifierMeaning,\n} from \"query/engine\";\nimport { DateTime, Duration } from \"luxon\";\nimport * as Luxon from \"luxon\";\nimport { compare, CompareOperator, satisfies } from \"compare-versions\";\nimport { DataviewSettings, ExportSettings } from \"settings\";\nimport { parseFrontmatter } from \"data-import/markdown-file\";\nimport { SListItem, SMarkdownPage } from \"data-model/serialized/markdown\";\nimport { createFixedTaskView, createTaskView, nestGroups } from \"ui/views/task-view\";\nimport { createFixedListView, createListView } from \"ui/views/list-view\";\nimport { createFixedTableView, createTableView } from \"ui/views/table-view\";\nimport { Result } from \"api/result\";\nimport { parseQuery } from \"query/parse\";\nimport { tryOrPropagate } from \"util/normalize\";\nimport { Query } from \"query/query\";\nimport { DataviewCalendarRenderer } from \"ui/views/calendar-view\";\nimport { DataviewJSRenderer } from \"ui/views/js-view\";\nimport { markdownList, markdownTable, markdownTaskList } from \"ui/export/markdown\";\n\n/** Asynchronous API calls related to file / system IO. */\nexport class DataviewIOApi {\n    public constructor(public api: DataviewApi) {}\n\n    /** Load the contents of a CSV asynchronously, returning a data array of rows (or undefined if it does not exist). */\n    public async csv(path: Link | string, originFile?: string): Promise<DataArray<DataObject> | undefined> {\n        if (!Values.isLink(path) && !Values.isString(path)) {\n            throw Error(`dv.io.csv only handles string or link paths; was provided type '${typeof path}'.`);\n        }\n\n        let data = await this.api.index.csv.get(this.normalize(path, originFile));\n        if (data.successful) return DataArray.from(data.value, this.api.settings);\n        else throw Error(`Could not find CSV for path '${path}' (relative to origin '${originFile ?? \"/\"}')`);\n    }\n\n    /** Asynchronously load the contents of any link or path in an Obsidian vault. */\n    public async load(path: Link | string, originFile?: string): Promise<string | undefined> {\n        if (!Values.isLink(path) && !Values.isString(path)) {\n            throw Error(`dv.io.load only handles string or link paths; was provided type '${typeof path}'.`);\n        }\n\n        let existingFile = this.api.index.vault.getAbstractFileByPath(this.normalize(path, originFile));\n        if (!existingFile || !(existingFile instanceof TFile)) return undefined;\n\n        return this.api.index.vault.cachedRead(existingFile);\n    }\n\n    /** Normalize a link or path relative to an optional origin file. Returns a textual fully-qualified-path. */\n    public normalize(path: Link | string, originFile?: string): string {\n        let realPath;\n        if (Values.isLink(path)) realPath = path.path;\n        else realPath = path;\n\n        return this.api.index.prefix.resolveRelative(realPath, originFile);\n    }\n}\n\n/** Global API for accessing the Dataview API, executing dataview queries, and  */\nexport class DataviewApi {\n    /** Evaluation context which expressions can be evaluated in. */\n    public evaluationContext: Context;\n    /** IO API which supports asynchronous loading of data directly. */\n    public io: DataviewIOApi;\n    /** Dataview functions which can be called from DataviewJS. */\n    public func: Record<string, BoundFunctionImpl>;\n    /** Value utility functions for comparisons and type-checking. */\n    public value = Values;\n    /** Widget utility functions for creating built-in widgets. */\n    public widget = Widgets;\n    /** Re-exporting of luxon for people who can't easily require it. Sorry! */\n    public luxon = Luxon;\n\n    public constructor(\n        public app: App,\n        public index: FullIndex,\n        public settings: DataviewSettings,\n        private verNum: string\n    ) {\n        this.evaluationContext = new Context(defaultLinkHandler(index, \"\"), settings);\n        this.func = Functions.bindAll(DEFAULT_FUNCTIONS, this.evaluationContext);\n        this.io = new DataviewIOApi(this);\n    }\n\n    /** Utilities to check the current Dataview version and compare it to SemVer version ranges. */\n    public version: {\n        current: string;\n        compare: (op: CompareOperator, ver: string) => boolean;\n        satisfies: (range: string) => boolean;\n    } = (() => {\n        const self = this;\n        return {\n            get current() {\n                return self.verNum;\n            },\n            compare: (op: CompareOperator, ver: string) => compare(this.verNum, ver, op),\n            satisfies: (range: string) => satisfies(this.verNum, range),\n        };\n    })();\n\n    /////////////////////////////\n    // Index + Data Collection //\n    /////////////////////////////\n\n    /** Return an array of paths (as strings) corresponding to pages which match the query. */\n    public pagePaths(query?: string, originFile?: string): DataArray<string> {\n        let source;\n        try {\n            if (!query || query.trim() === \"\") source = Sources.folder(\"\");\n            else source = EXPRESSION.source.tryParse(query);\n        } catch (ex) {\n            throw new Error(`Failed to parse query in 'pagePaths': ${ex}`);\n        }\n\n        return matchingSourcePaths(source, this.index, originFile)\n            .map(s => DataArray.from(s, this.settings))\n            .orElseThrow();\n    }\n\n    /** Map a page path to the actual data contained within that page. */\n    public page(path: string | Link, originFile?: string): Record<string, Literal> | undefined {\n        if (!(typeof path === \"string\") && !Values.isLink(path)) {\n            throw Error(\"dv.page only handles string and link paths; was provided type '\" + typeof path + \"'\");\n        }\n\n        let rawPath = path instanceof Link ? path.path : path;\n        let normPath = this.app.metadataCache.getFirstLinkpathDest(rawPath, originFile ?? \"\");\n        if (!normPath) return undefined;\n\n        let pageObject = this.index.pages.get(normPath.path);\n        if (!pageObject) return undefined;\n\n        return this._addDataArrays(pageObject.serialize(this.index));\n    }\n\n    /** Return an array of page objects corresponding to pages which match the source query. */\n    public pages(query?: string, originFile?: string): DataArray<Record<string, Literal>> {\n        return this.pagePaths(query, originFile).flatMap(p => {\n            let res = this.page(p, originFile);\n            return res ? [res] : [];\n        });\n    }\n\n    /** Remaps important metadata to add data arrays.  */\n    private _addDataArrays(pageObject: SMarkdownPage): SMarkdownPage {\n        // Remap the \"file\" metadata entries to be data arrays.\n        for (let [key, value] of Object.entries(pageObject.file)) {\n            if (Array.isArray(value)) (pageObject.file as any)[key] = DataArray.wrap<any>(value, this.settings);\n        }\n\n        return pageObject;\n    }\n\n    /////////////\n    // Utility //\n    /////////////\n\n    /**\n     * Convert an input element or array into a Dataview data-array. If the input is already a data array,\n     * it is returned unchanged.\n     */\n    public array(raw: unknown): DataArray<any> {\n        if (DataArray.isDataArray(raw)) return raw;\n        if (Array.isArray(raw)) return DataArray.wrap(raw, this.settings);\n        return DataArray.wrap([raw], this.settings);\n    }\n\n    /** Return true if the given value is a javascript array OR a dataview data array. */\n    public isArray(raw: unknown): raw is DataArray<any> | Array<any> {\n        return DataArray.isDataArray(raw) || Array.isArray(raw);\n    }\n\n    /** Return true if the given value is a dataview data array; this returns FALSE for plain JS arrays. */\n    public isDataArray(raw: unknown): raw is DataArray<any> {\n        return DataArray.isDataArray(raw);\n    }\n\n    /** Create a dataview file link to the given path. */\n    public fileLink(path: string, embed: boolean = false, display?: string) {\n        return Link.file(path, embed, display);\n    }\n\n    /** Create a dataview section link to the given path. */\n    public sectionLink(path: string, section: string, embed: boolean = false, display?: string): Link {\n        return Link.header(path, section, embed, display);\n    }\n\n    /** Create a dataview block link to the given path. */\n    public blockLink(path: string, blockId: string, embed: boolean = false, display?: string): Link {\n        return Link.block(path, blockId, embed, display);\n    }\n\n    /** Attempt to extract a date from a string, link or date. */\n    public date(pathlike: string | Link | DateTime): DateTime | null {\n        return this.func.date(pathlike) as DateTime | null;\n    }\n\n    /** Attempt to extract a duration from a string or duration. */\n    public duration(str: string | Duration): Duration | null {\n        return this.func.dur(str) as Duration | null;\n    }\n\n    /** Parse a raw textual value into a complex Dataview type, if possible. */\n    public parse(value: string): Literal {\n        let raw = EXPRESSION.inlineField.parse(value);\n        if (raw.status) return raw.value;\n        else return value;\n    }\n\n    /** Convert a basic JS type into a Dataview type by parsing dates, links, durations, and so on. */\n    public literal(value: any): Literal {\n        return parseFrontmatter(value);\n    }\n\n    /** Deep clone the given literal, returning a new literal which is independent of the original. */\n    public clone(value: Literal): Literal {\n        return Values.deepCopy(value);\n    }\n\n    /**\n     * Compare two arbitrary JavaScript values using Dataview's default comparison rules. Returns a negative value if\n     * a < b, 0 if a = b, and a positive value if a > b.\n     */\n    public compare(a: any, b: any): number {\n        return Values.compareValue(a, b, this.evaluationContext.linkHandler.normalize);\n    }\n\n    /** Return true if the two given JavaScript values are equal using Dataview's default comparison rules. */\n    public equal(a: any, b: any): boolean {\n        return this.compare(a, b) == 0;\n    }\n\n    ///////////////////////////////\n    // Dataview Query Evaluation //\n    ///////////////////////////////\n\n    /**\n     * Execute an arbitrary Dataview query, returning a query result which:\n     *\n     * 1. Indicates the type of query,\n     * 2. Includes the raw AST of the parsed query.\n     * 3. Includes the output in the form relevant to that query type.\n     *\n     * List queries will return a list of objects ({ id, value }); table queries return a header array\n     * and a 2D array of values; and task arrays return a Grouping<Task> type which allows for recursive\n     * task nesting.\n     */\n    public async query(\n        source: string | Query,\n        originFile?: string,\n        settings?: QueryApiSettings\n    ): Promise<Result<QueryResult, string>> {\n        const query = typeof source === \"string\" ? parseQuery(source) : Result.success<Query, string>(source);\n        if (!query.successful) return query.cast();\n\n        const header = query.value.header;\n        switch (header.type) {\n            case \"calendar\":\n                const cres = await executeCalendar(query.value, this.index, originFile ?? \"\", this.settings);\n                if (!cres.successful) return cres.cast();\n\n                return Result.success({ type: \"calendar\", values: cres.value.data });\n            case \"task\":\n                const tasks = await executeTask(query.value, originFile ?? \"\", this.index, this.settings);\n                if (!tasks.successful) return tasks.cast();\n\n                return Result.success({ type: \"task\", values: tasks.value.tasks });\n            case \"list\":\n                if (settings?.forceId !== undefined) header.showId = settings.forceId;\n\n                const lres = await executeList(query.value, this.index, originFile ?? \"\", this.settings);\n                if (!lres.successful) return lres.cast();\n\n                // TODO: WITHOUT ID probably shouldn't exist, or should be moved to the engine itself.\n                // For now, until I fix it up in an upcoming refactor, we re-implement the behavior here.\n\n                return Result.success({\n                    type: \"list\",\n                    values: lres.value.data,\n                    primaryMeaning: lres.value.primaryMeaning,\n                });\n            case \"table\":\n                if (settings?.forceId !== undefined) header.showId = settings.forceId;\n\n                const tres = await executeTable(query.value, this.index, originFile ?? \"\", this.settings);\n                if (!tres.successful) return tres.cast();\n\n                return Result.success({\n                    type: \"table\",\n                    values: tres.value.data,\n                    headers: tres.value.names,\n                    idMeaning: tres.value.idMeaning,\n                });\n        }\n    }\n\n    /** Error-throwing version of {@link query}. */\n    public async tryQuery(source: string, originFile?: string, settings?: QueryApiSettings): Promise<QueryResult> {\n        return (await this.query(source, originFile, settings)).orElseThrow();\n    }\n\n    /** Execute an arbitrary dataview query, returning the results in well-formatted markdown. */\n    public async queryMarkdown(\n        source: string | Query,\n        originFile?: string,\n        settings?: Partial<QueryApiSettings & ExportSettings>\n    ): Promise<Result<string, string>> {\n        const result = await this.query(source, originFile, settings);\n        if (!result.successful) return result.cast();\n\n        switch (result.value.type) {\n            case \"list\":\n                return Result.success(this.markdownList(result.value.values, settings));\n            case \"table\":\n                return Result.success(this.markdownTable(result.value.headers, result.value.values, settings));\n            case \"task\":\n                return Result.success(this.markdownTaskList(result.value.values, settings));\n            case \"calendar\":\n                return Result.failure(\"Cannot render calendar queries to markdown.\");\n        }\n    }\n\n    /** Error-throwing version of {@link queryMarkdown}. */\n    public async tryQueryMarkdown(\n        source: string | Query,\n        originFile?: string,\n        settings?: Partial<QueryApiSettings & ExportSettings>\n    ): Promise<string> {\n        return (await this.queryMarkdown(source, originFile, settings)).orElseThrow();\n    }\n\n    /**\n     * Evaluate a dataview expression (like '2 + 2' or 'link(\"hello\")'), returning the evaluated result.\n     * This takes an optional second argument which provides definitions for variables, such as:\n     *\n     * ```\n     * dv.evaluate(\"x + 6\", { x: 2 }) = 8\n     * dv.evaluate('link(target)', { target: \"Okay\" }) = [[Okay]]\n     * ```\n     *\n     * This method returns a Result type instead of throwing an error; you can check the result of the\n     * execution via `result.successful` and obtain `result.value` or `result.error` accordingly. If\n     * you'd rather this method throw on an error, use `dv.tryEvaluate`.\n     */\n    public evaluate(expression: string, context?: DataObject, originFile?: string): Result<Literal, string> {\n        let field = EXPRESSION.field.parse(expression);\n        if (!field.status) return Result.failure(`Failed to parse expression \"${expression}\"`);\n\n        let evaluationContext = originFile\n            ? new Context(defaultLinkHandler(this.index, originFile), this.settings)\n            : this.evaluationContext;\n\n        return evaluationContext.evaluate(field.value, context);\n    }\n\n    /** Error-throwing version of `dv.evaluate`. */\n    public tryEvaluate(expression: string, context?: DataObject, originFile?: string): Literal {\n        return this.evaluate(expression, context, originFile).orElseThrow();\n    }\n\n    /** Evaluate an expression in the context of the given file. */\n    public evaluateInline(expression: string, origin: string): Result<Literal, string> {\n        let field = EXPRESSION.field.parse(expression);\n        if (!field.status) return Result.failure(`Failed to parse expression \"${expression}\"`);\n\n        return executeInline(field.value, origin, this.index, this.settings);\n    }\n\n    ///////////////\n    // Rendering //\n    ///////////////\n\n    /**\n     * Execute the given query, rendering results into the given container using the components lifecycle.\n     * Your component should be a *real* component which calls onload() on it's child components at some point,\n     * or a MarkdownPostProcessorContext!\n     *\n     * Note that views made in this way are live updating and will automatically clean themselves up when\n     * the component is unloaded or the container is removed.\n     */\n    public async execute(\n        source: string,\n        container: HTMLElement,\n        component: Component | MarkdownPostProcessorContext,\n        filePath: string\n    ) {\n        if (isDataviewDisabled(filePath)) {\n            renderCodeBlock(container, source);\n            return;\n        }\n\n        let maybeQuery = tryOrPropagate(() => parseQuery(source));\n\n        // In case of parse error, just render the error.\n        if (!maybeQuery.successful) {\n            renderErrorPre(container, \"Dataview: \" + maybeQuery.error);\n            return;\n        }\n\n        let query = maybeQuery.value;\n        let init = { app: this.app, settings: this.settings, index: this.index, container };\n        let childComponent;\n        switch (query.header.type) {\n            case \"task\":\n                childComponent = createTaskView(init, query as Query, filePath);\n                component.addChild(childComponent);\n                break;\n            case \"list\":\n                childComponent = createListView(init, query as Query, filePath);\n                component.addChild(childComponent);\n\n                break;\n            case \"table\":\n                childComponent = createTableView(init, query as Query, filePath);\n\n                component.addChild(childComponent);\n                break;\n            case \"calendar\":\n                childComponent = new DataviewCalendarRenderer(\n                    query as Query,\n                    container,\n                    this.index,\n                    filePath,\n                    this.settings,\n                    this.app\n                );\n\n                component.addChild(childComponent);\n                break;\n        }\n        childComponent.load();\n    }\n\n    /**\n     * Execute the given DataviewJS query, rendering results into the given container using the components lifecycle.\n     * See {@link execute} for general rendering semantics.\n     */\n    public async executeJs(\n        code: string,\n        container: HTMLElement,\n        component: Component | MarkdownPostProcessorContext,\n        filePath: string\n    ) {\n        if (isDataviewDisabled(filePath)) {\n            renderCodeBlock(container, code, \"javascript\");\n            return;\n        }\n        const renderer = new DataviewJSRenderer(this, code, container, filePath);\n        renderer.load();\n        component.addChild(renderer);\n    }\n\n    /** Render a dataview list of the given values. */\n    public async list(\n        values: any[] | DataArray<any> | undefined,\n        container: HTMLElement,\n        component: Component,\n        filePath: string\n    ) {\n        if (!values) return;\n        if (values !== undefined && values !== null && !Array.isArray(values) && !DataArray.isDataArray(values))\n            values = Array.from(values);\n\n        // Append a child div, since React will keep re-rendering otherwise.\n        let subcontainer = container.createEl(\"div\");\n        component.addChild(\n            createFixedListView(\n                { app: this.app, settings: this.settings, index: this.index, container: subcontainer },\n                values as Literal[],\n                filePath\n            )\n        );\n    }\n\n    /** Render a dataview table with the given headers, and the 2D array of values. */\n    public async table(\n        headers: string[],\n        values: any[][] | DataArray<any> | undefined,\n        container: HTMLElement,\n        component: Component,\n        filePath: string\n    ) {\n        if (!headers) headers = [];\n        if (!values) values = [];\n        if (!Array.isArray(headers) && !DataArray.isDataArray(headers)) headers = Array.from(headers);\n\n        // Append a child div, since React will keep re-rendering otherwise.\n        let subcontainer = container.createEl(\"div\");\n        component.addChild(\n            createFixedTableView(\n                { app: this.app, settings: this.settings, index: this.index, container: subcontainer },\n                headers,\n                values as Literal[][],\n                filePath\n            )\n        );\n    }\n\n    /** Render a dataview task view with the given tasks. */\n    public async taskList(\n        tasks: Grouping<SListItem>,\n        groupByFile: boolean = true,\n        container: HTMLElement,\n        component: Component,\n        filePath: string = \"\"\n    ) {\n        let groupedTasks =\n            !Groupings.isGrouping(tasks) && groupByFile ? this.array(tasks).groupBy(t => Link.file(t.path)) : tasks;\n\n        // Append a child div, since React will override several task lists otherwise.\n        let taskContainer = container.createEl(\"div\");\n        component.addChild(\n            createFixedTaskView(\n                { app: this.app, settings: this.settings, index: this.index, container: taskContainer },\n                groupedTasks as Grouping<SListItem>,\n                filePath\n            )\n        );\n    }\n\n    /** Render an arbitrary value into a container. */\n    public async renderValue(\n        value: any,\n        container: HTMLElement,\n        component: Component,\n        filePath: string,\n        inline: boolean = false\n    ) {\n        return renderValue(this.app, value as Literal, container, filePath, component, this.settings, inline);\n    }\n\n    /////////////////\n    // Data Export //\n    /////////////////\n\n    /** Render data to a markdown table. */\n    public markdownTable(\n        headers: string[] | undefined,\n        values: any[][] | DataArray<any> | undefined,\n        settings?: Partial<ExportSettings>\n    ): string {\n        if (!headers) headers = [];\n        if (!values) values = [];\n\n        const combined = Object.assign({}, this.settings, settings);\n        return markdownTable(headers, values as any[][], combined);\n    }\n\n    /** Render data to a markdown list. */\n    public markdownList(values: any[] | DataArray<any> | undefined, settings?: Partial<ExportSettings>): string {\n        if (!values) values = [];\n\n        const combined = Object.assign({}, this.settings, settings);\n        return markdownList(values as any[], combined);\n    }\n\n    /** Render tasks or list items to a markdown task list. */\n    public markdownTaskList(values: Grouping<SListItem>, settings?: Partial<ExportSettings>): string {\n        if (!values) values = [];\n\n        const sparse = nestGroups(values);\n        const combined = Object.assign({}, this.settings, settings);\n        return markdownTaskList(sparse as any[], combined);\n    }\n}\n\n/** The result of executing a table query. */\nexport type TableResult = { type: \"table\"; headers: string[]; values: Literal[][]; idMeaning: IdentifierMeaning };\n/** The result of executing a list query. */\nexport type ListResult = { type: \"list\"; values: Literal[]; primaryMeaning: IdentifierMeaning };\n/** The result of executing a task query. */\nexport type TaskResult = { type: \"task\"; values: Grouping<SListItem> };\n/** The result of executing a calendar query. */\nexport type CalendarResult = {\n    type: \"calendar\";\n    values: {\n        date: DateTime;\n        link: Link;\n        value?: Literal[];\n    }[];\n};\n\n/** The result of executing a query of some sort. */\nexport type QueryResult = TableResult | ListResult | TaskResult | CalendarResult;\n\n/** Settings when querying the dataview API. */\nexport type QueryApiSettings = {\n    /** If present, then this forces queries to include/exclude the implicit id field (such as with `WITHOUT ID`). */\n    forceId?: boolean;\n};\n\n/** Determines if source-path has a `?no-dataview` annotation that disables dataview. */\nexport function isDataviewDisabled(sourcePath: string): boolean {\n    if (!sourcePath) return false;\n\n    let questionLocation = sourcePath.lastIndexOf(\"?\");\n    if (questionLocation == -1) return false;\n\n    return sourcePath.substring(questionLocation).contains(\"no-dataview\");\n}\n"
  },
  {
    "path": "src/api/result.ts",
    "content": "/** Functional return type for error handling. */\nexport class Success<T, E> {\n    public successful: true;\n\n    public constructor(public value: T) {\n        this.successful = true;\n    }\n\n    public map<U>(f: (a: T) => U): Result<U, E> {\n        return new Success(f(this.value));\n    }\n\n    public flatMap<U>(f: (a: T) => Result<U, E>): Result<U, E> {\n        return f(this.value);\n    }\n\n    public mapErr<U>(f: (e: E) => U): Result<T, U> {\n        return this as any as Result<T, U>;\n    }\n\n    public bimap<T2, E2>(succ: (a: T) => T2, _fail: (b: E) => E2): Result<T2, E2> {\n        return this.map(succ) as any;\n    }\n\n    public orElse(_value: T): T {\n        return this.value;\n    }\n\n    public cast<U>(): Result<U, E> {\n        return this as any;\n    }\n\n    public orElseThrow(_message?: (e: E) => string): T {\n        return this.value;\n    }\n}\n\n/** Functional return type for error handling. */\nexport class Failure<T, E> {\n    public successful: false;\n\n    public constructor(public error: E) {\n        this.successful = false;\n    }\n\n    public map<U>(_f: (a: T) => U): Result<U, E> {\n        return this as any as Failure<U, E>;\n    }\n\n    public flatMap<U>(_f: (a: T) => Result<U, E>): Result<U, E> {\n        return this as any as Failure<U, E>;\n    }\n\n    public mapErr<U>(f: (e: E) => U): Result<T, U> {\n        return new Failure(f(this.error));\n    }\n\n    public bimap<T2, E2>(_succ: (a: T) => T2, fail: (b: E) => E2): Result<T2, E2> {\n        return this.mapErr(fail) as any;\n    }\n\n    public orElse(value: T): T {\n        return value;\n    }\n\n    public cast<U>(): Result<U, E> {\n        return this as any;\n    }\n\n    public orElseThrow(message?: (e: E) => string): T {\n        if (message) throw new Error(message(this.error));\n        else throw new Error(\"\" + this.error);\n    }\n}\n\nexport type Result<T, E> = Success<T, E> | Failure<T, E>;\n\n/** Monadic 'Result' type which encapsulates whether a procedure succeeded or failed, as well as it's return value. */\nexport namespace Result {\n    /** Construct a new success result wrapping the given value. */\n    export function success<T, E>(value: T): Result<T, E> {\n        return new Success(value);\n    }\n\n    /** Construct a new failure value wrapping the given error. */\n    export function failure<T, E>(error: E): Result<T, E> {\n        return new Failure(error);\n    }\n\n    /** Join two results with a bi-function and return a new result. */\n    export function flatMap2<T1, T2, O, E>(\n        first: Result<T1, E>,\n        second: Result<T2, E>,\n        f: (a: T1, b: T2) => Result<O, E>\n    ): Result<O, E> {\n        if (first.successful) {\n            if (second.successful) return f(first.value, second.value);\n            else return failure(second.error);\n        } else {\n            return failure(first.error);\n        }\n    }\n\n    /** Join two results with a bi-function and return a new result. */\n    export function map2<T1, T2, O, E>(\n        first: Result<T1, E>,\n        second: Result<T2, E>,\n        f: (a: T1, b: T2) => O\n    ): Result<O, E> {\n        return flatMap2(first, second, (a, b) => success(f(a, b)));\n    }\n}\n"
  },
  {
    "path": "src/data-import/common.ts",
    "content": "/** Common utilities for extracting tags, links, and other business from metadata. */\n\nimport { EXPRESSION } from \"expression/parse\";\n\nconst POTENTIAL_TAG_MATCHER = /#[^\\s,;\\.:!\\?'\"`()\\[\\]\\{\\}]+/giu;\n\n/** Extract all tags from the given source string. */\nexport function extractTags(source: string): Set<string> {\n    let result = new Set<string>();\n\n    let matches = source.matchAll(POTENTIAL_TAG_MATCHER);\n    for (let match of matches) {\n        let parsed = EXPRESSION.tag.parse(match[0]);\n        if (parsed.status) result.add(parsed.value);\n    }\n\n    return result;\n}\n"
  },
  {
    "path": "src/data-import/csv.ts",
    "content": "import { canonicalizeVarName } from \"util/normalize\";\nimport { DataObject } from \"data-model/value\";\nimport * as Papa from \"papaparse\";\nimport { parseFrontmatter } from \"data-import/markdown-file\";\n\n/** Parse a CSV file into a collection of data rows. */\nexport function parseCsv(content: string): DataObject[] {\n    let parsed = Papa.parse(content, {\n        header: true,\n        skipEmptyLines: true,\n        comments: \"#\",\n        dynamicTyping: true,\n    });\n\n    let rows = [];\n    for (let parsedRow of parsed.data) {\n        let fields = parseFrontmatter(parsedRow) as DataObject;\n        let result: DataObject = {};\n\n        for (let [key, value] of Object.entries(fields)) {\n            result[key] = value;\n            result[canonicalizeVarName(key)] = value;\n        }\n\n        rows.push(result);\n    }\n\n    return rows;\n}\n"
  },
  {
    "path": "src/data-import/inline-field.ts",
    "content": "/** Parse inline fields and other embedded metadata in a line. */\n\nimport { EXPRESSION } from \"expression/parse\";\nimport { Literal } from \"data-model/value\";\nimport * as P from \"parsimmon\";\nimport emojiRegex from \"emoji-regex\";\n\n/** A parsed inline field. */\nexport interface InlineField {\n    /** The raw parsed key. */\n    key: string;\n    /** The raw value of the field. */\n    value: string;\n    /** The start column of the field. */\n    start: number;\n    /** The start column of the *value* for the field. */\n    startValue: number;\n    /** The end column of the field. */\n    end: number;\n    /** If this inline field was defined via a wrapping ('[' or '('), then the wrapping that was used. */\n    wrapping?: string;\n}\n\n/** The wrapper characters that can be used to define an inline field. */\nexport const INLINE_FIELD_WRAPPERS: Readonly<Record<string, string>> = Object.freeze({\n    \"[\": \"]\",\n    \"(\": \")\",\n});\n\n/**\n * Find a matching closing bracket that occurs at or after `start`, respecting nesting and escapes. If found,\n * returns the value contained within and the string index after the end of the value.\n */\nfunction findClosing(\n    line: string,\n    start: number,\n    open: string,\n    close: string\n): { value: string; endIndex: number } | undefined {\n    let nesting = 0;\n    let escaped = false;\n    for (let index = start; index < line.length; index++) {\n        let char = line.charAt(index);\n\n        // Allows for double escapes like '\\\\' to be rendered normally.\n        if (char == \"\\\\\") {\n            escaped = !escaped;\n            continue;\n        }\n\n        // If escaped, ignore the next character for computing nesting, regardless of what it is.\n        if (escaped) {\n            escaped = false;\n            continue;\n        }\n\n        if (char == open) nesting++;\n        else if (char == close) nesting--;\n\n        // Only occurs if we are on a close character and there is no more nesting.\n        if (nesting < 0) return { value: line.substring(start, index).trim(), endIndex: index + 1 };\n\n        escaped = false;\n    }\n\n    return undefined;\n}\n\n/** Find the '::' separator in an inline field. */\nfunction findSeparator(line: string, start: number): { key: string; valueIndex: number } | undefined {\n    let sep = line.indexOf(\"::\", start);\n    if (sep < 0) return undefined;\n\n    return { key: line.substring(start, sep).trim(), valueIndex: sep + 2 };\n}\n\n/** Try to completely parse an inline field starting at the given position. Assumes `start` is on a wrapping character. */\nfunction findSpecificInlineField(line: string, start: number): InlineField | undefined {\n    let open = line.charAt(start);\n\n    let key = findSeparator(line, start + 1);\n    if (key === undefined) return undefined;\n\n    // Fail the match if we find any separator characters (not allowed in keys).\n    for (let sep of Object.keys(INLINE_FIELD_WRAPPERS).concat(Object.values(INLINE_FIELD_WRAPPERS))) {\n        if (key.key.includes(sep)) return undefined;\n    }\n\n    let value = findClosing(line, key.valueIndex, open, INLINE_FIELD_WRAPPERS[open]);\n    if (value === undefined) return undefined;\n\n    return {\n        key: key.key,\n        value: value.value,\n        start: start,\n        startValue: key.valueIndex,\n        end: value.endIndex,\n        wrapping: open,\n    };\n}\n\n/** Parse a textual inline field value into something we can work with. */\nexport function parseInlineValue(value: string): Literal {\n    // Empty inline values (i.e., no text) should map to null to match long-term Dataview semantics.\n    // Null is also a more universal type to deal with than strings, since all functions accept nulls.\n    if (value.trim() == \"\") return null;\n\n    // The stripped literal field parser understands all of the non-array/non-object fields and can parse them for us.\n    // Inline field objects are not currently supported; inline array objects have to be handled by the parser\n    // separately.\n    let inline = EXPRESSION.inlineField.parse(value);\n    if (inline.status) return inline.value;\n    else return value;\n}\n\n/** Extracts inline fields of the form '[key:: value]' from a line of text. This is done in a relatively\n * \"robust\" way to avoid failing due to bad nesting or other interfering Markdown symbols:\n *\n * - Look for any wrappers ('[' and '(') in the line, trying to parse whatever comes after it as an inline key::.\n * - If successful, scan until you find a matching end bracket, and parse whatever remains as an inline value.\n */\nexport function extractInlineFields(line: string, includeTaskFields: boolean = false): InlineField[] {\n    let fields: InlineField[] = [];\n    for (let wrapper of Object.keys(INLINE_FIELD_WRAPPERS)) {\n        let foundIndex = line.indexOf(wrapper);\n        while (foundIndex >= 0) {\n            let parsedField = findSpecificInlineField(line, foundIndex);\n            if (!parsedField) {\n                foundIndex = line.indexOf(wrapper, foundIndex + 1);\n                continue;\n            }\n\n            fields.push(parsedField);\n            foundIndex = line.indexOf(wrapper, parsedField.end);\n        }\n    }\n\n    if (includeTaskFields) fields = fields.concat(extractSpecialTaskFields(line));\n\n    fields.sort((a, b) => a.start - b.start);\n\n    let filteredFields: InlineField[] = [];\n    for (let i = 0; i < fields.length; i++) {\n        if (i == 0 || filteredFields[filteredFields.length - 1].end < fields[i].start) {\n            filteredFields.push(fields[i]);\n        }\n    }\n    return filteredFields;\n}\n\n/** Validates that a raw field name has a valid form. */\nconst FULL_LINE_KEY_PART: P.Parser<string> = P.alt(\n    P.regexp(new RegExp(emojiRegex(), \"u\")),\n    P.regexp(/[0-9\\p{Letter}\\w\\s_/-]+/u)\n)\n    .many()\n    .map(parts => parts.join(\"\"));\n\nconst FULL_LINE_KEY_PARSER: P.Parser<string> = P.regexp(/[^0-9\\w\\p{Letter}]*/u)\n    .then(FULL_LINE_KEY_PART)\n    .skip(P.regexp(/[_\\*~`]*/u));\n\n/** Attempt to extract a full-line field (Key:: Value consuming the entire content line). */\nexport function extractFullLineField(text: string): InlineField | undefined {\n    let sep = findSeparator(text, 0);\n    if (!sep) return undefined;\n\n    // We need to post-process the key to drop unnecessary opening annotations as well as\n    // drop surrounding Markdown.\n    let realKey = FULL_LINE_KEY_PARSER.parse(sep.key);\n    if (!realKey.status) return undefined;\n\n    return {\n        key: realKey.value,\n        value: text.substring(sep.valueIndex).trim(),\n        start: 0,\n        startValue: sep.valueIndex,\n        end: text.length,\n    };\n}\n\nexport const CREATED_DATE_REGEX = /\\u{2795}\\s*(\\d{4}-\\d{2}-\\d{2})/u;\nexport const DUE_DATE_REGEX = /(?:\\u{1F4C5}|\\u{1F4C6}|\\u{1F5D3}\\u{FE0F}?)\\s*(\\d{4}-\\d{2}-\\d{2})/u;\nexport const DONE_DATE_REGEX = /\\u{2705}\\s*(\\d{4}-\\d{2}-\\d{2})/u;\nexport const SCHEDULED_DATE_REGEX = /[\\u{23F3}\\u{231B}]\\s*(\\d{4}-\\d{2}-\\d{2})/u;\nexport const START_DATE_REGEX = /\\u{1F6EB}\\s*(\\d{4}-\\d{2}-\\d{2})/u;\n\nexport const EMOJI_REGEXES = [\n    { regex: CREATED_DATE_REGEX, key: \"created\" },\n    { regex: START_DATE_REGEX, key: \"start\" },\n    { regex: SCHEDULED_DATE_REGEX, key: \"scheduled\" },\n    { regex: DUE_DATE_REGEX, key: \"due\" },\n    { regex: DONE_DATE_REGEX, key: \"completion\" },\n];\n\n/** Parse special completed/due/done task fields which are marked via emoji. */\nfunction extractSpecialTaskFields(line: string): InlineField[] {\n    let results: InlineField[] = [];\n\n    for (let { regex, key } of EMOJI_REGEXES) {\n        const match = regex.exec(line);\n        if (!match) continue;\n\n        results.push({\n            key,\n            value: match[1],\n            start: match.index,\n            startValue: match.index + 1,\n            end: match.index + match[0].length,\n            wrapping: \"emoji-shorthand\",\n        });\n    }\n\n    return results;\n}\n\n/** Sets or replaces the value of an inline field; if the value is 'undefined', deletes the key. */\nexport function setInlineField(source: string, key: string, value?: string): string {\n    let existing = extractInlineFields(source);\n    let existingKeys = existing.filter(f => f.key == key);\n\n    // Don't do anything if there are duplicate keys OR the key already doesn't exist.\n    if (existingKeys.length > 2 || (existingKeys.length == 0 && !value)) return source;\n    let existingKey = existingKeys[0];\n\n    let annotation = value ? `[${key}:: ${value}]` : \"\";\n    if (existingKey) {\n        let prefix = source.substring(0, existingKey.start);\n        let suffix = source.substring(existingKey.end);\n\n        if (annotation) return `${prefix}${annotation}${suffix}`;\n        else return `${prefix}${suffix.trimStart()}`;\n    } else if (annotation) {\n        return `${source.trimEnd()} ${annotation}`;\n    }\n\n    return source;\n}\n\nexport function setEmojiShorthandCompletionField(source: string, value?: string): string {\n    const existing = extractInlineFields(source, true);\n    const existingKeys = existing.filter(f => f.key === \"completion\" && f.wrapping === \"emoji-shorthand\");\n\n    // Don't do anything if there are duplicate keys OR the key already doesn't exist.\n    if (existingKeys.length > 2 || (existingKeys.length == 0 && !value)) return source;\n\n    /* No wrapper, add own spacing at start */\n    const annotation = value ? ` ✅ ${value}` : \"\";\n    let existingKey = existingKeys[0];\n    if (existingKey) {\n        const prefix = source.substring(0, existingKey.start);\n        const suffix = source.substring(existingKey.end);\n        return `${prefix.trimEnd()}${annotation}${suffix}`;\n    } else {\n        return `${source.trimEnd()}${annotation}`;\n    }\n}\n"
  },
  {
    "path": "src/data-import/markdown-file.ts",
    "content": "/** Importer for markdown documents. */\n\nimport { extractFullLineField, extractInlineFields, parseInlineValue, InlineField } from \"data-import/inline-field\";\nimport { ListItem, PageMetadata } from \"data-model/markdown\";\nimport { Literal, Link, Values } from \"data-model/value\";\nimport { EXPRESSION } from \"expression/parse\";\nimport { DateTime } from \"luxon\";\nimport { CachedMetadata, FileStats, FrontMatterCache, HeadingCache } from \"obsidian\";\nimport { canonicalizeVarName, extractDate, getFileTitle } from \"util/normalize\";\nimport * as common from \"data-import/common\";\n\n/** Extract markdown metadata from the given Obsidian markdown file. */\nexport function parsePage(path: string, contents: string, stat: FileStats, metadata: CachedMetadata): PageMetadata {\n    let tags = new Set<string>();\n    let aliases = new Set<string>();\n    let fields = new Map<string, Literal[]>();\n    let links: Link[] = [];\n\n    // File tags, including front-matter and in-file tags.\n    (metadata.tags || []).forEach(t => tags.add(t.tag.startsWith(\"#\") ? t.tag : \"#\" + t.tag));\n\n    // Front-matter file tags, aliases, AND frontmatter properties.\n    if (metadata.frontmatter) {\n        for (let tag of extractTags(metadata.frontmatter)) {\n            if (!tag.startsWith(\"#\")) tag = \"#\" + tag;\n            tags.add(tag);\n        }\n\n        for (let alias of extractAliases(metadata.frontmatter) || []) aliases.add(alias);\n\n        let frontFields = parseFrontmatter(metadata.frontmatter) as Record<string, Literal>;\n        for (let [key, value] of Object.entries(frontFields)) {\n            if (key == \"position\") continue;\n            addInlineField(key, value, fields);\n        }\n    }\n\n    // Add frontmatter links to links.\n    if (metadata.frontmatterLinks) {\n        for (let rawLink of metadata.frontmatterLinks || []) {\n            const link = Link.infer(rawLink.link, false, rawLink.displayText);\n            links.push(link);\n        }\n    }\n\n    // Links in metadata.\n    const linksByLine: Record<number, Link[]> = {};\n    for (let rawLink of metadata.links || []) {\n        const link = Link.infer(rawLink.link, false, rawLink.displayText);\n        const line = rawLink.position.start.line;\n\n        links.push(link);\n        if (!(line in linksByLine)) linksByLine[line] = [link];\n        else linksByLine[line].push(link);\n    }\n\n    // Embed Links in metadata.\n    for (let rawEmbed of metadata.embeds || []) {\n        const link = Link.infer(rawEmbed.link, true, rawEmbed.displayText);\n        const line = rawEmbed.position.start.line;\n\n        links.push(link);\n        if (!(line in linksByLine)) linksByLine[line] = [link];\n        else linksByLine[line].push(link);\n    }\n\n    // Merge frontmatter fields with parsed fields.\n    let markdownData = parseMarkdown(path, contents.split(\"\\n\"), metadata, linksByLine);\n    mergeFieldGroups(fields, markdownData.fields);\n\n    // Strip \"position\" from frontmatter since it is Obsidian determined.\n    const frontmatter = metadata.frontmatter || ({} as Record<string, any>);\n    if (frontmatter && \"position\" in frontmatter) delete frontmatter[\"position\"];\n\n    return new PageMetadata(path, {\n        tags,\n        aliases,\n        links,\n        lists: markdownData.lists,\n        fields: finalizeInlineFields(fields),\n        frontmatter: frontmatter,\n        ctime: DateTime.fromMillis(stat.ctime),\n        mtime: DateTime.fromMillis(stat.mtime),\n        size: stat.size,\n        day: findDate(path, fields),\n    });\n}\n\n/** Extract tags intelligently from frontmatter. Handles arrays, numbers, and strings. */\nexport function extractTags(metadata: FrontMatterCache): string[] {\n    let tagKeys = Object.keys(metadata).filter(t => t.toLowerCase() == \"tags\" || t.toLowerCase() == \"tag\");\n\n    return tagKeys\n        .map(k => splitFrontmatterTagOrAlias(metadata[k], /[,\\s]+/))\n        .reduce((p, c) => p.concat(c), [])\n        .map(str => (str.startsWith(\"#\") ? str : \"#\" + str));\n}\n\n/** Extract aliases intelligently from frontmatter. Handles arrays, numbers, and strings.  */\nexport function extractAliases(metadata: FrontMatterCache): string[] {\n    let aliasKeys = Object.keys(metadata).filter(t => t.toLowerCase() == \"alias\" || t.toLowerCase() == \"aliases\");\n\n    const result: string[] = [];\n    for (let key of aliasKeys) {\n        const value = metadata[key];\n        if (!value) continue;\n\n        if (Array.isArray(value)) result.push(...value.map(v => (\"\" + v).trim()));\n        else result.push(...splitFrontmatterTagOrAlias(value, /,/));\n    }\n\n    return result;\n}\n\n/** Split a frontmatter list into separate elements; handles actual lists, comma separated lists, and single elements. */\nexport function splitFrontmatterTagOrAlias(data: any, on: RegExp): string[] {\n    if (data == null || data == undefined) return [];\n    if (Array.isArray(data)) {\n        return data\n            .filter(s => !!s)\n            .map(s => splitFrontmatterTagOrAlias(s, on))\n            .reduce((p, c) => p.concat(c), []);\n    }\n\n    // Force to a string to handle numbers and so on.\n    return (\"\" + data)\n        .split(on)\n        .filter(t => !!t)\n        .map(t => t.trim())\n        .filter(t => t.length > 0);\n}\n\n/** Parse raw (newline-delimited) markdown, returning inline fields, list items, and other metadata. */\nexport function parseMarkdown(\n    path: string,\n    contents: string[],\n    metadata: CachedMetadata,\n    linksByLine: Record<number, Link[]>\n): { fields: Map<string, Literal[]>; lists: ListItem[] } {\n    let fields: Map<string, Literal[]> = new Map();\n\n    // Extract task data and append the global data extracted from them to our fields.\n    let [lists, extraData] = parseLists(path, contents, metadata, linksByLine);\n    for (let [key, values] of extraData.entries()) {\n        if (!fields.has(key)) fields.set(key, values);\n        else fields.set(key, fields.get(key)!!.concat(values));\n    }\n\n    // The Obsidian metadata cache will track list elements inside of other element groups (like annotations and\n    // callouts)... this means we might see metadata twice, so skip them now. Very annoying.\n    const listLinesToSkip = new Set<number>();\n    for (const line of lists) {\n        for (let i = 0; i < line.lineCount; i++) listLinesToSkip.add(line.line + i);\n    }\n\n    // Only parse heading and paragraph elements for inline fields; we will parse list metadata separately.\n    for (let section of metadata.sections || []) {\n        if (section.type == \"list\" || section.type == \"ruling\") continue;\n\n        for (let lineno = section.position.start.line; lineno <= section.position.end.line; lineno++) {\n            let line = contents[lineno];\n            if (line == undefined || line == null) continue;\n            if (listLinesToSkip.has(lineno)) continue;\n\n            // Fast bail-out for lines that are too long or do not contain '::'.\n            if (line.length > 32768 || !line.includes(\"::\")) continue;\n            line = line.trim();\n\n            let inlineFields = extractInlineFields(line);\n            if (inlineFields.length > 0) {\n                for (let ifield of inlineFields) addRawInlineField(ifield, fields);\n            } else {\n                let fullLine = extractFullLineField(line);\n                if (fullLine) addRawInlineField(fullLine, fields);\n            }\n        }\n    }\n\n    return { fields, lists };\n}\n\n// TODO: Consider using an actual parser in lieu of a more expensive regex.\nexport const LIST_ITEM_REGEX = /^[\\s>]*(\\d+\\.|\\d+\\)|\\*|-|\\+)\\s*(\\[.{0,1}\\])?\\s*(.*)$/mu;\n\n/**\n * Parse list items from the page + metadata. This requires some additional parsing above whatever Obsidian provides,\n * since Obsidian only gives line numbers.\n */\nexport function parseLists(\n    path: string,\n    content: string[],\n    metadata: CachedMetadata,\n    linksByLine: Record<number, Link[]>\n): [ListItem[], Map<string, Literal[]>] {\n    let cache: Record<number, ListItem> = {};\n\n    // Place all of the values in the cache before resolving children & metadata relationships.\n    for (let rawElement of metadata.listItems || []) {\n        // Match on the first line to get the symbol and first line of text.\n        let rawMatch = LIST_ITEM_REGEX.exec(content[rawElement.position.start.line]);\n        if (!rawMatch) continue;\n\n        // And then strip unnecessary spacing from the remaining lines.\n        let textParts = [rawMatch[3]]\n            .concat(content.slice(rawElement.position.start.line + 1, rawElement.position.end.line + 1))\n            .map(t => t.trim());\n        let textWithNewline = textParts.join(\"\\n\");\n        let textNoNewline = textParts.join(\" \");\n\n        // Find the list that we are a part of by line.\n        let containingListId = (metadata.sections || []).findIndex(\n            s =>\n                s.type == \"list\" &&\n                s.position.start.line <= rawElement.position.start.line &&\n                s.position.end.line >= rawElement.position.start.line\n        );\n\n        // Find the section we belong to as well.\n        let sectionName = findPreviousHeader(rawElement.position.start.line, metadata.headings || []);\n        let sectionLink = sectionName === undefined ? Link.file(path) : Link.header(path, sectionName);\n        let closestLink = rawElement.id === undefined ? sectionLink : Link.block(path, rawElement.id);\n\n        // Gather any links that occur on the same lines as the task.\n        const links = [];\n        for (let line = rawElement.position.start.line; line <= rawElement.position.end.line; line++) {\n            if (linksByLine[line]) links.push(...linksByLine[line]);\n        }\n\n        // Construct universal information about this element (before tasks).\n        let item = new ListItem({\n            symbol: rawMatch[1],\n            link: closestLink,\n            links: links,\n            section: sectionLink,\n            text: textWithNewline,\n            tags: common.extractTags(textNoNewline),\n            line: rawElement.position.start.line,\n            lineCount: rawElement.position.end.line - rawElement.position.start.line + 1,\n            list: containingListId == -1 ? -1 : (metadata.sections || [])[containingListId].position.start.line,\n            position: rawElement.position,\n            children: [],\n            blockId: rawElement.id,\n        });\n\n        if (rawElement.parent >= 0 && rawElement.parent != item.line) item.parent = rawElement.parent;\n\n        // Set up the basic task information for now, though we have to recompute `fullyComputed` later.\n        if (rawElement.task) {\n            item.task = {\n                status: rawElement.task,\n                checked: rawElement.task != \"\" && rawElement.task != \" \",\n                completed: rawElement.task == \"X\" || rawElement.task == \"x\",\n                fullyCompleted: rawElement.task == \"X\" || rawElement.task == \"x\",\n            };\n        }\n\n        // Extract inline fields; extract full-line fields only if we are NOT a task.\n        item.fields = new Map<string, Literal[]>();\n        for (let element of extractInlineFields(textNoNewline, true)) addRawInlineField(element, item.fields);\n\n        if (!rawElement.task && item.fields.size == 0) {\n            let fullLine = extractFullLineField(textNoNewline);\n            if (fullLine) addRawInlineField(fullLine, item.fields);\n        }\n\n        cache[item.line] = item;\n    }\n\n    // Tree updating passes. Update child lists. Propagate metadata up to parent tasks. Update task `fullyCompleted`.\n    let literals: Map<string, Literal[]> = new Map();\n    for (let listItem of Object.values(cache)) {\n        // Pass 1: Update child lists.\n        if (listItem.parent !== undefined && listItem.parent in cache) {\n            let parent = cache[listItem.parent!!];\n            parent.children.push(listItem.line);\n        }\n\n        // Pass 2: Propagate metadata up to the parent task or root element.\n        if (!listItem.task) {\n            mergeFieldGroups(literals, listItem.fields);\n\n            // TODO (blacksmithgu): The below code properly Propagates metadata up to the nearest task, which is the\n            // more intuitive behavior. For now, though, we will keep the existing logic.\n            /*\n            let root: ListItem | undefined = listItem;\n            while (!!root && !root.task) root = cache[root.parent ?? -1];\n\n            // If the root is null, append this metadata to the root; otherwise, append to the task.\n            mergeFieldGroups(root === undefined || root == null ? literals : root.fields, listItem.fields);\n            */\n        }\n\n        // Pass 3: Propagate `fullyCompleted` up the task tree. This is a little less efficient than just doing a simple\n        // DFS using the children IDs, but it's probably fine.\n        if (listItem.task) {\n            let curr: ListItem | undefined = listItem;\n            while (!!curr) {\n                if (curr.task) curr.task.fullyCompleted = curr.task.fullyCompleted && listItem.task.completed;\n                curr = cache[curr.parent ?? -1];\n            }\n        }\n    }\n\n    return [Object.values(cache), literals];\n}\n\n/** Attempt to find a date associated with the given page from metadata or filenames. */\nfunction findDate(file: string, fields: Map<string, Literal>): DateTime | undefined {\n    for (let key of fields.keys()) {\n        if (!(key.toLocaleLowerCase() == \"date\" || key.toLocaleLowerCase() == \"day\")) continue;\n\n        let value = fields.get(key) as Literal;\n        if (Values.isDate(value)) {\n            return value;\n        } else if (Values.isArray(value) && value.length > 0 && Values.isDate(value[0])) {\n            return value[0];\n        } else if (Values.isLink(value)) {\n            let date = extractDate(value.path) ?? extractDate(value.subpath ?? \"\") ?? extractDate(value.display ?? \"\");\n            if (date) return date;\n        }\n    }\n\n    return extractDate(getFileTitle(file));\n}\n\n/** Recursively convert frontmatter into fields. We have to dance around YAML structure. */\nexport function parseFrontmatter(value: any): Literal {\n    if (value == null) {\n        return null;\n    } else if (typeof value === \"object\") {\n        if (Array.isArray(value)) {\n            let result = [];\n            for (let child of value as Array<any>) {\n                result.push(parseFrontmatter(child));\n            }\n\n            return result;\n        } else if (value instanceof Date) {\n            let dateParse = DateTime.fromJSDate(value);\n            return dateParse;\n        } else {\n            let object = value as Record<string, any>;\n            let result: Record<string, Literal> = {};\n            for (let key in object) {\n                result[key] = parseFrontmatter(object[key]);\n            }\n\n            return result;\n        }\n    } else if (typeof value === \"number\") {\n        return value;\n    } else if (typeof value === \"boolean\") {\n        return value;\n    } else if (typeof value === \"string\") {\n        let dateParse = EXPRESSION.date.parse(value);\n        if (dateParse.status) return dateParse.value;\n\n        let durationParse = EXPRESSION.duration.parse(value);\n        if (durationParse.status) return durationParse.value;\n\n        let linkParse = EXPRESSION.embedLink.parse(value);\n        if (linkParse.status) return linkParse.value;\n\n        return value;\n    }\n\n    // Backup if we don't understand the type.\n    return null;\n}\n\n/** Add a parsed inline field to the output map. */\nexport function addRawInlineField(field: InlineField, output: Map<string, Literal[]>) {\n    addInlineField(field.key, parseInlineValue(field.value), output);\n}\n\n/** Add a raw inline field to an output map, canonicalizing as needed. */\nexport function addInlineField(key: string, value: Literal, output: Map<string, Literal[]>) {\n    if (!output.has(key)) output.set(key, [value]);\n    else output.get(key)?.push(value);\n}\n\n/** Given a raw list of inline field values, add normalized keys and squash them. */\nexport function finalizeInlineFields(fields: Map<string, Literal[]>): Map<string, Literal> {\n    // Compute unique normalized keys (that do not overlap w/ the fields).\n    let normalized = new Map<string, Literal[]>();\n    for (let [key, values] of fields.entries()) {\n        let normKey = canonicalizeVarName(key);\n        if (normKey == \"\" || fields.has(normKey)) continue;\n\n        if (!normalized.has(normKey)) normalized.set(normKey, values);\n        else normalized.set(normKey, normalized.get(normKey)!!.concat(values));\n    }\n\n    // Combine normalized + normal keys.\n    let interim = new Map<string, Literal[]>();\n    mergeFieldGroups(interim, fields);\n    mergeFieldGroups(interim, normalized);\n\n    // And then flatten them.\n    let result = new Map<string, Literal>();\n    for (let [key, value] of interim.entries()) {\n        if (value.length == 1) result.set(key, value[0]);\n        else result.set(key, value);\n    }\n\n    return result;\n}\n\n/** Copy all fields of 'source' into 'target'. */\nexport function mergeFieldGroups(target: Map<string, Literal[]>, source: Map<string, Literal[]>) {\n    for (let key of source.keys()) {\n        if (!target.has(key)) target.set(key, source.get(key)!!);\n        else target.set(key, target.get(key)!!.concat(source.get(key)!!));\n    }\n}\n\n/** Find the header that is most immediately above the given line number. */\nexport function findPreviousHeader(line: number, headers: HeadingCache[]): string | undefined {\n    if (headers.length == 0) return undefined;\n    if (headers[0].position.start.line > line) return undefined;\n\n    let index = headers.length - 1;\n    while (index >= 0 && headers[index].position.start.line > line) index--;\n\n    return headers[index].heading;\n}\n"
  },
  {
    "path": "src/data-import/persister.ts",
    "content": "import { PageMetadata } from \"data-model/markdown\";\nimport { Transferable } from \"data-model/transferable\";\nimport localforage from \"localforage\";\n\n/** A piece of data that has been cached for a specific version and time. */\nexport interface Cached<T> {\n    /** The version of Dataview that the data was written to cache with. */\n    version: string;\n    /** The time that the data was written to cache. */\n    time: number;\n    /** The data that was cached. */\n    data: T;\n}\n\n/** Simpler wrapper for a file-backed cache for arbitrary metadata. */\nexport class LocalStorageCache {\n    public persister: LocalForage;\n\n    public constructor(public appId: string, public version: string) {\n        this.persister = localforage.createInstance({\n            name: \"dataview/cache/\" + appId,\n            driver: [localforage.INDEXEDDB],\n            description: \"Cache metadata about files and sections in the dataview index.\",\n        });\n    }\n\n    /** Drop the entire cache instance and re-create a new fresh instance. */\n    public async recreate() {\n        await localforage.dropInstance({ name: \"dataview/cache/\" + this.appId });\n\n        this.persister = localforage.createInstance({\n            name: \"dataview/cache/\" + this.appId,\n            driver: [localforage.INDEXEDDB],\n            description: \"Cache metadata about files and sections in the dataview index.\",\n        });\n    }\n\n    /** Load file metadata by path. */\n    public async loadFile(path: string): Promise<Cached<Partial<PageMetadata>> | null | undefined> {\n        return this.persister.getItem(this.fileKey(path)).then(raw => {\n            let result = raw as any as Cached<Partial<PageMetadata>>;\n            if (result) result.data = Transferable.value(result.data);\n            return result;\n        });\n    }\n\n    /** Store file metadata by path. */\n    public async storeFile(path: string, data: Partial<PageMetadata>): Promise<void> {\n        await this.persister.setItem(this.fileKey(path), {\n            version: this.version,\n            time: Date.now(),\n            data: Transferable.transferable(data),\n        });\n    }\n\n    /** Drop old file keys that no longer exist. */\n    public async synchronize(existing: string[] | Set<string>): Promise<Set<string>> {\n        let keys = new Set(await this.allFiles());\n        for (let exist of existing) keys.delete(exist);\n\n        // Any keys remaining after deleting existing keys are non-existent keys that should be cleared from cache.\n        for (let key of keys) await this.persister.removeItem(this.fileKey(key));\n\n        return keys;\n    }\n\n    /** Obtain a list of all metadata keys. */\n    public async allKeys(): Promise<string[]> {\n        return this.persister.keys();\n    }\n\n    /** Obtain a list of all persisted files. */\n    public async allFiles(): Promise<string[]> {\n        let keys = await this.allKeys();\n        return keys.filter(k => k.startsWith(\"file:\")).map(k => k.substring(5));\n    }\n\n    public fileKey(path: string): string {\n        return \"file:\" + path;\n    }\n}\n"
  },
  {
    "path": "src/data-import/web-worker/import-entry.ts",
    "content": "/** Entry-point script used by the index as a web worker. */\nimport { runImport } from \"data-import/web-worker/import-impl\";\nimport { Transferable } from \"data-model/transferable\";\nimport { CachedMetadata, FileStats } from \"obsidian\";\n\n/** An import which can fail and raise an exception, which will be caught by the handler. */\nfunction failableImport(path: string, contents: string, stat: FileStats, metadata?: CachedMetadata) {\n    if (metadata === undefined || metadata === null) {\n        throw Error(`Cannot index file, since it has no Obsidian file metadata.`);\n    }\n\n    return runImport(path, contents, stat, metadata);\n}\n\nonmessage = async evt => {\n    try {\n        let { path, contents, stat, metadata } = evt.data;\n        let result = failableImport(path, contents, stat, metadata);\n        (postMessage as any)({ path: evt.data.path, result: Transferable.transferable(result) });\n    } catch (error) {\n        console.log(error);\n        (postMessage as any)({\n            path: evt.data.path,\n            result: {\n                $error: `Failed to index file: ${evt.data.path}: ${error}`,\n            },\n        });\n    }\n};\n"
  },
  {
    "path": "src/data-import/web-worker/import-impl.ts",
    "content": "/** Actual import implementation backend. This must remain separate from `import-entry` since it is used without web workers. */\nimport { parsePage } from \"data-import/markdown-file\";\nimport { PageMetadata } from \"data-model/markdown\";\nimport { CachedMetadata, FileStats } from \"obsidian\";\n\nexport function runImport(\n    path: string,\n    contents: string,\n    stats: FileStats,\n    metadata: CachedMetadata\n): Partial<PageMetadata> {\n    return parsePage(path, contents, stats, metadata);\n}\n"
  },
  {
    "path": "src/data-import/web-worker/import-manager.ts",
    "content": "/** Controls and creates Dataview file importers, allowing for asynchronous loading and parsing of files. */\n\nimport { Transferable } from \"data-model/transferable\";\nimport DataviewImportWorker from \"web-worker:./import-entry.ts\";\nimport { Component, MetadataCache, TFile, Vault } from \"obsidian\";\n\n/** Callback when a file is resolved. */\ntype FileCallback = (p: any) => void;\n\n/** Multi-threaded file parser which debounces rapid file requests automatically. */\nexport class FileImporter extends Component {\n    /* Background workers which do the actual file parsing. */\n    workers: Worker[];\n    /** Tracks which workers are actively parsing a file, to make sure we properly delegate results. */\n    busy: boolean[];\n\n    /** List of files which have been queued for a reload. */\n    reloadQueue: TFile[];\n    /** Fast-access set which holds the list of files queued to be reloaded; used for debouncing. */\n    reloadSet: Set<string>;\n    /** Paths -> promises for file reloads which have not yet been queued. */\n    callbacks: Map<string, [FileCallback, FileCallback][]>;\n\n    public constructor(public numWorkers: number, public vault: Vault, public metadataCache: MetadataCache) {\n        super();\n        this.workers = [];\n        this.busy = [];\n\n        this.reloadQueue = [];\n        this.reloadSet = new Set();\n        this.callbacks = new Map();\n\n        for (let index = 0; index < numWorkers; index++) {\n            let worker = new DataviewImportWorker({ name: \"Dataview Indexer \" + (index + 1) });\n\n            worker.onmessage = evt => this.finish(evt.data.path, Transferable.value(evt.data.result), index);\n            this.workers.push(worker);\n            this.register(() => worker.terminate());\n            this.busy.push(false);\n        }\n    }\n\n    /**\n     * Queue the given file for reloading. Multiple reload requests for the same file in a short time period will be de-bounced\n     * and all be resolved by a single actual file reload.\n     */\n    public reload<T>(file: TFile): Promise<T> {\n        let promise: Promise<T> = new Promise((resolve, reject) => {\n            if (this.callbacks.has(file.path)) this.callbacks.get(file.path)?.push([resolve, reject]);\n            else this.callbacks.set(file.path, [[resolve, reject]]);\n        });\n\n        // De-bounce repeated requests for the same file.\n        if (this.reloadSet.has(file.path)) return promise;\n        this.reloadSet.add(file.path);\n\n        // Immediately run this task if there are available workers; otherwise, add it to the queue.\n        let workerId = this.nextAvailableWorker();\n        if (workerId !== undefined) {\n            this.send(file, workerId);\n        } else {\n            this.reloadQueue.push(file);\n        }\n\n        return promise;\n    }\n\n    /** Finish the parsing of a file, potentially queueing a new file. */\n    private finish(path: string, data: any, index: number) {\n        // Cache the callbacks before we do book-keeping.\n        let calls = ([] as [FileCallback, FileCallback][]).concat(this.callbacks.get(path) ?? []);\n\n        // Book-keeping to clear metadata & allow the file to be re-loaded again.\n        this.reloadSet.delete(path);\n        this.callbacks.delete(path);\n\n        // Notify the queue this file is available for new work.\n        this.busy[index] = false;\n\n        // Queue a new job onto this worker.\n        let job = this.reloadQueue.shift();\n        if (job !== undefined) this.send(job, index);\n\n        // Resolve promises to let users know this file has finished.\n        if (\"$error\" in data) {\n            for (let [_, reject] of calls) reject(data[\"$error\"]);\n        } else {\n            for (let [callback, _] of calls) callback(data);\n        }\n    }\n\n    /** Send a new task to the given worker ID. */\n    private send(file: TFile, workerId: number) {\n        this.busy[workerId] = true;\n\n        this.vault.cachedRead(file).then(c =>\n            this.workers[workerId].postMessage({\n                path: file.path,\n                contents: c,\n                stat: file.stat,\n                metadata: this.metadataCache.getFileCache(file),\n            })\n        );\n    }\n\n    /** Find the next available, non-busy worker; return undefined if all workers are busy. */\n    private nextAvailableWorker(): number | undefined {\n        let index = this.busy.indexOf(false);\n        return index == -1 ? undefined : index;\n    }\n}\n"
  },
  {
    "path": "src/data-index/index.ts",
    "content": "/** Stores various indices on all files in the vault to make dataview generation fast. */\nimport { Result } from \"api/result\";\nimport { parseCsv } from \"data-import/csv\";\nimport { LocalStorageCache } from \"data-import/persister\";\nimport { FileImporter } from \"data-import/web-worker/import-manager\";\nimport { PageMetadata } from \"data-model/markdown\";\nimport { DataObject } from \"data-model/value\";\nimport { DateTime } from \"luxon\";\nimport { App, Component, MetadataCache, TAbstractFile, TFile, TFolder, Vault } from \"obsidian\";\nimport { getParentFolder, setsEqual } from \"util/normalize\";\n\n/** Aggregate index which has several sub-indices and will initialize all of them. */\nexport class FullIndex extends Component {\n    /** Generate a full index from the given vault. */\n    public static create(app: App, indexVersion: string, onChange: () => void): FullIndex {\n        return new FullIndex(app, indexVersion, onChange);\n    }\n\n    /** Whether all files in the vault have been indexed at least once. */\n    public initialized: boolean;\n\n    /** I/O access to the Obsidian vault contents. */\n    public vault: Vault;\n    /** Access to in-memory metadata, useful for parsing and metadata lookups. */\n    public metadataCache: MetadataCache;\n    /** Persistent IndexedDB backing store, used for faster startup. */\n    public persister: LocalStorageCache;\n\n    /* Maps path -> markdown metadata for all markdown pages. */\n    public pages: Map<string, PageMetadata>;\n\n    /** Map files -> tags in that file, and tags -> files. This version includes subtags. */\n    public tags: ValueCaseInsensitiveIndexMap;\n    /** Map files -> exact tags in that file, and tags -> files. This version does not automatically add subtags. */\n    public etags: ValueCaseInsensitiveIndexMap;\n    /** Map files -> linked files in that file, and linked file -> files that link to it. */\n    public links: IndexMap;\n    /** Search files by path prefix. */\n    public prefix: PrefixIndex;\n    /** Allows for efficient lookups of whether a file is starred or not. */\n    public starred: StarredCache;\n    /** Caches data in CSV files. */\n    // TODO: CSV parsing should be done by a worker thread asynchronously to avoid frontend stalls.\n    public csv: CsvCache;\n\n    /**\n     * The current \"revision\" of the index, which monotonically increases for every index change. Use this to determine\n     * if you are up to date.\n     */\n    public revision: number;\n\n    /** Asynchronously parses files in the background using web workers. */\n    public importer: FileImporter;\n\n    /** Construct a new index using the app data and a current data version. */\n    private constructor(public app: App, public indexVersion: string, public onChange: () => void) {\n        super();\n\n        this.initialized = false;\n\n        this.vault = app.vault;\n        this.metadataCache = app.metadataCache;\n\n        this.pages = new Map();\n        this.tags = new ValueCaseInsensitiveIndexMap();\n        this.etags = new ValueCaseInsensitiveIndexMap();\n        this.links = new IndexMap();\n        this.revision = 0;\n\n        // Caches metadata via durable storage to speed up cache initialization when Obsidian restarts.\n        this.persister = new LocalStorageCache(app.appId || \"shared\", indexVersion);\n\n        // Handles asynchronous reloading of files on web workers.\n        this.addChild((this.importer = new FileImporter(2, this.vault, this.metadataCache)));\n        // Prefix listens to file creation/deletion/rename, and not modifies, so we let it set up it's own listeners.\n        this.addChild((this.prefix = PrefixIndex.create(this.vault, () => this.touch())));\n        // The CSV cache also needs to listen to filesystem events for cache invalidation.\n        this.addChild((this.csv = new CsvCache(this.vault)));\n        // The starred cache fetches starred entries semi-regularly via an interval.\n        this.addChild((this.starred = new StarredCache(this.app, () => this.touch())));\n    }\n\n    /** Trigger a metadata event on the metadata cache. */\n    private trigger(...args: any[]): void {\n        this.metadataCache.trigger(\"dataview:metadata-change\", ...args);\n    }\n\n    /** \"Touch\" the index, incrementing the revision number and causing downstream views to reload. */\n    public touch() {\n        this.revision += 1;\n        this.onChange();\n    }\n\n    /** Runs through the whole vault to set up initial file metadata. */\n    public initialize() {\n        // The metadata cache is updated on initial file index and file loads.\n        this.registerEvent(this.metadataCache.on(\"resolve\", file => this.reload(file)));\n\n        // Renames do not set off the metadata cache; catch these explicitly.\n        this.registerEvent(this.vault.on(\"rename\", this.rename, this));\n\n        // File creation does cause a metadata change, but deletes do not. Clear the caches for this.\n        this.registerEvent(\n            this.vault.on(\"delete\", af => {\n                if (!(af instanceof TFile) || !PathFilters.markdown(af.path)) return;\n                let file = af as TFile;\n\n                this.pages.delete(file.path);\n                this.tags.delete(file.path);\n                this.etags.delete(file.path);\n                this.links.delete(file.path);\n\n                this.touch();\n                this.trigger(\"delete\", file);\n            })\n        );\n\n        // Asynchronously initialize actual content in the background.\n        this._initialize(this.vault.getMarkdownFiles());\n    }\n\n    /** Drops the local storage cache and re-indexes all files; this should generally be used if you expect cache issues. */\n    public async reinitialize() {\n        await this.persister.recreate();\n\n        const files = this.vault.getMarkdownFiles();\n        const start = Date.now();\n        let promises = files.map(file => this.reload(file));\n\n        await Promise.all(promises);\n        console.log(`Dataview: re-initialized index with ${files.length} files (${(Date.now() - start) / 1000.0}s)`);\n    }\n\n    /** Internal asynchronous initializer. */\n    private async _initialize(files: TFile[]) {\n        let reloadStart = Date.now();\n        let promises = files.map(l => this.reload(l));\n        let results = await Promise.all(promises);\n\n        let cached = 0,\n            skipped = 0;\n        for (let item of results) {\n            if (item.skipped) {\n                skipped += 1;\n                continue;\n            }\n\n            if (item.cached) cached += 1;\n        }\n\n        this.initialized = true;\n        this.metadataCache.trigger(\"dataview:index-ready\");\n        console.log(\n            `Dataview: all ${files.length} files have been indexed in ${\n                (Date.now() - reloadStart) / 1000.0\n            }s (${cached} cached, ${skipped} skipped).`\n        );\n\n        // Drop keys for files which do not exist anymore.\n        let remaining = await this.persister.synchronize(files.map(l => l.path));\n        if (remaining.size > 0) {\n            console.log(`Dataview: Dropped cache entries for ${remaining.size} deleted files.`);\n        }\n    }\n\n    public rename(file: TAbstractFile, oldPath: string) {\n        if (!(file instanceof TFile) || !PathFilters.markdown(file.path)) return;\n\n        if (this.pages.has(oldPath)) {\n            const oldMeta = this.pages.get(oldPath);\n            this.pages.delete(oldPath);\n            if (oldMeta) {\n                oldMeta.path = file.path;\n                this.pages.set(file.path, oldMeta);\n            }\n        }\n\n        this.tags.rename(oldPath, file.path);\n        this.links.rename(oldPath, file.path);\n        this.etags.rename(oldPath, file.path);\n\n        this.touch();\n        this.trigger(\"rename\", file, oldPath);\n    }\n\n    /** Queue a file for reloading; this is done asynchronously in the background and may take a few seconds. */\n    public async reload(file: TFile): Promise<{ cached: boolean; skipped: boolean }> {\n        if (!PathFilters.markdown(file.path)) return { cached: false, skipped: true };\n\n        // The first load of a file is attempted from persisted cache; subsequent loads just use the importer.\n        if (this.pages.has(file.path) || this.initialized) {\n            await this.import(file);\n            return { cached: false, skipped: false };\n        } else {\n            // Check the cache for the latest data; if it is out of date or non-existent, then reload.\n            return this.persister.loadFile(file.path).then(async cached => {\n                if (!cached || cached.time < file.stat.mtime || cached.version != this.indexVersion) {\n                    // This cache value is out of data, reload via the importer and update the cache.\n                    // We will skip files with no active file metadata - they will be caught by a later reload\n                    // via the 'resolve' metadata event.\n                    let fileCache = this.metadataCache.getFileCache(file);\n                    if (fileCache === undefined || fileCache === null) return { cached: false, skipped: true };\n\n                    await this.import(file);\n                    return { cached: false, skipped: false };\n                } else {\n                    // Use the cached data since it is up to date and on the same version.\n                    this.finish(file, cached.data);\n                    return { cached: true, skipped: false };\n                }\n            });\n        }\n    }\n\n    /** Import a file directly from disk, skipping the cache. */\n    private async import(file: TFile): Promise<void> {\n        return this.importer.reload<Partial<PageMetadata>>(file).then(r => {\n            this.finish(file, r);\n            this.persister.storeFile(file.path, r);\n        });\n    }\n\n    /** Finish the reloading of file metadata by adding it to in memory indexes. */\n    private finish(file: TFile, parsed: Partial<PageMetadata>) {\n        let meta = PageMetadata.canonicalize(parsed, link => {\n            let realPath = this.metadataCache.getFirstLinkpathDest(link.path, file.path);\n            if (realPath) return link.withPath(realPath.path);\n            else return link;\n        });\n\n        this.pages.set(file.path, meta);\n        this.tags.set(file.path, meta.fullTags());\n        this.etags.set(file.path, meta.tags);\n        this.links.set(file.path, new Set<string>(meta.links.map(l => l.path)));\n\n        this.touch();\n        this.trigger(\"update\", file);\n    }\n}\n\n/** Indexes files by their full prefix - essentially a simple prefix tree. */\nexport class PrefixIndex extends Component {\n    public static create(vault: Vault, updateRevision: () => void): PrefixIndex {\n        return new PrefixIndex(vault, updateRevision);\n    }\n\n    constructor(public vault: Vault, public updateRevision: () => void) {\n        super();\n    }\n\n    private *walk(folder: TFolder, filter?: (path: string) => boolean): Generator<string> {\n        for (const file of folder.children) {\n            if (file instanceof TFolder) {\n                yield* this.walk(file, filter);\n            } else if (filter ? filter(file.path) : true) {\n                yield file.path;\n            }\n        }\n    }\n\n    /** Get the list of all files under the given path. */\n    public get(prefix: string, filter?: (path: string) => boolean): Set<string> {\n        let folder = this.vault.getAbstractFileByPath(prefix || \"/\");\n        return new Set(folder instanceof TFolder ? this.walk(folder, filter) : []);\n    }\n\n    /** Determines if the given path exists in the prefix index. */\n    public pathExists(path: string): boolean {\n        return this.vault.getAbstractFileByPath(path || \"/\") != null;\n    }\n\n    /** Determines if the given prefix exists in the prefix index. */\n    public nodeExists(prefix: string): boolean {\n        return this.vault.getAbstractFileByPath(prefix || \"/\") instanceof TFolder;\n    }\n\n    /**\n     * Use the in-memory prefix index to convert a relative path to an absolute one.\n     */\n    public resolveRelative(path: string, origin?: string): string {\n        if (!origin) return path;\n        else if (path.startsWith(\"/\")) return path.substring(1);\n\n        let relativePath = getParentFolder(origin) + \"/\" + path;\n        if (this.pathExists(relativePath)) return relativePath;\n        else return path;\n    }\n}\n\n/** Simple path filters which filter file types. */\nexport namespace PathFilters {\n    export function csv(path: string): boolean {\n        return path.toLowerCase().endsWith(\".csv\");\n    }\n\n    export function markdown(path: string): boolean {\n        let lcPath = path.toLowerCase();\n        return lcPath.endsWith(\".md\") || lcPath.endsWith(\".markdown\");\n    }\n}\n\n/**\n * Caches in-use CSVs to make high-frequency reloads (such as actively looking at a document\n * that uses CSV) fast.\n */\nexport class CsvCache extends Component {\n    public static CACHE_EXPIRY_SECONDS: number = 5 * 60;\n\n    // Cache of loaded CSVs; old entries will periodically be removed\n    cache: Map<string, { data: DataObject[]; loadTime: DateTime }>;\n    // Periodic job which clears out the cache based on time.\n    cacheClearInterval: number;\n\n    public constructor(public vault: Vault) {\n        super();\n\n        this.cache = new Map();\n\n        // Force-flush the cache on CSV file deletions or modifications.\n        this.registerEvent(\n            this.vault.on(\"modify\", file => {\n                if (file instanceof TFile && PathFilters.csv(file.path)) this.cache.delete(file.path);\n            })\n        );\n\n        this.registerEvent(\n            this.vault.on(\"delete\", file => {\n                if (file instanceof TFile && PathFilters.csv(file.path)) this.cache.delete(file.path);\n            })\n        );\n    }\n\n    /** Load a CSV file from the cache, doing a fresh load if it has not been loaded. */\n    public async get(path: string): Promise<Result<DataObject[], string>> {\n        // Clear old entries on every fresh load, since the path being loaded may be stale.\n        this.clearOldEntries();\n\n        let existing = this.cache.get(path);\n        if (existing) return Result.success(existing.data);\n        else {\n            let value = await this.loadInternal(path);\n            if (value.successful) this.cache.set(path, { data: value.value, loadTime: DateTime.now() });\n            return value;\n        }\n    }\n\n    /** Do the actual raw loading of a CSV path (which is either local or an HTTP request). */\n    private async loadInternal(path: string): Promise<Result<DataObject[], string>> {\n        // Allow http://, https://, and file:// prefixes which use AJAX.\n        if (path.startsWith(\"http://\") || path.startsWith(\"https://\") || path.startsWith(\"file://\")) {\n            try {\n                let result = await fetch(path, {\n                    method: \"GET\",\n                    mode: \"no-cors\",\n                    redirect: \"follow\",\n                });\n\n                return Result.success(parseCsv(await result.text()));\n            } catch (ex) {\n                return Result.failure(\"\" + ex + \"\\n\\n\" + ex.stack);\n            }\n        }\n\n        // Otherwise, assume it is a fully-qualified file path.\n        try {\n            let fileData = await this.vault.adapter.read(path);\n            return Result.success(parseCsv(fileData));\n        } catch (ex) {\n            return Result.failure(`Failed to load data from path '${path}'.`);\n        }\n    }\n\n    /** Clear old entries in the cache (as measured by insertion time). */\n    private clearOldEntries() {\n        let currentTime = DateTime.now();\n        let keysToRemove = new Set<string>();\n        for (let [key, value] of this.cache.entries()) {\n            let entryAge = Math.abs(currentTime.diff(value.loadTime, \"seconds\").seconds);\n            if (entryAge > CsvCache.CACHE_EXPIRY_SECONDS) keysToRemove.add(key);\n        }\n\n        keysToRemove.forEach(key => this.cache.delete(key));\n    }\n}\n\nexport type StarredEntry =\n    | { type: \"group\"; items: StarredEntry[]; title: string }\n    | { type: \"file\"; path: string; title: string }\n    | { type: \"folder\" }\n    | { type: \"query\" };\n\n/** Optional connector to the Obsidian 'Starred' plugin which allows for efficiently querying if a file is starred or not. */\nexport class StarredCache extends Component {\n    /** Initial delay before checking the cache; we need to wait for it to asynchronously load the initial stars. */\n    public static INITIAL_DELAY = 4 * 1_000;\n    /** How frequently to check for star updates. */\n    public static REFRESH_INTERVAL = 30 * 1_000;\n\n    /** Set of all starred file paths. */\n    private stars: Set<string>;\n\n    public constructor(public app: App, public onUpdate: () => void) {\n        super();\n\n        this.stars = StarredCache.fetch(this.app);\n        this.registerInterval(window.setInterval(() => this.reload(), StarredCache.REFRESH_INTERVAL));\n\n        const initialHandler = window.setTimeout(() => this.reload(), StarredCache.INITIAL_DELAY);\n        this.register(() => window.clearTimeout(initialHandler));\n    }\n\n    /** Determines if the given path is starred. */\n    public starred(path: string): boolean {\n        return this.stars.has(path);\n    }\n\n    private reload() {\n        let newStars = StarredCache.fetch(this.app);\n        if (!setsEqual(this.stars, newStars)) {\n            this.stars = newStars;\n            this.onUpdate();\n        }\n    }\n\n    /** Fetch all starred files from the stars plugin, if present. */\n    private static fetch(app: App): Set<string> {\n        let items = (app as any)?.internalPlugins?.plugins?.bookmarks?.instance?.items as StarredEntry[];\n        if (items == undefined) return new Set();\n\n        // Retrieve all grouped (nested) items, returning a flat array\n        const flattenItems = (items: StarredEntry[]): StarredEntry[] => {\n            let children: StarredEntry[] = [];\n\n            return items\n                .map(i => {\n                    if (i.type == \"group\" && i.items && i.items.length) {\n                        children = [...children, ...i.items];\n                    }\n                    return i;\n                })\n                .concat(children.length ? flattenItems(children) : children);\n        };\n\n        items = flattenItems(items);\n\n        return new Set(\n            items.filter((l): l is { type: \"file\"; path: string; title: string } => l.type === \"file\").map(l => l.path)\n        );\n    }\n}\n\n/** A generic index which indexes variables of the form key -> value[], allowing both forward and reverse lookups. */\nexport class IndexMap {\n    /** Maps key -> values for that key. */\n    map: Map<string, Set<string>>;\n    /** Cached inverse map; maps value -> keys that reference that value. */\n    invMap: Map<string, Set<string>>;\n\n    /** Create a new, empty index map. */\n    public constructor() {\n        this.map = new Map();\n        this.invMap = new Map();\n    }\n\n    /** Returns all values for the given key. */\n    public get(key: string): Set<string> {\n        let result = this.map.get(key);\n        if (result) {\n            return new Set(result);\n        } else {\n            return new Set();\n        }\n    }\n\n    /** Returns all keys that reference the given key. Mutating the returned set is not allowed. */\n    public getInverse(value: string): Readonly<Set<string>> {\n        return this.invMap.get(value) || IndexMap.EMPTY_SET;\n    }\n\n    /** Sets the key to the given values; this will delete the old mapping for the key if one was present. */\n    public set(key: string, values: Set<string>): this {\n        if (!values.size) {\n            // no need to store if no values\n            this.delete(key);\n            return this;\n        }\n        let oldValues = this.map.get(key);\n        if (oldValues) {\n            for (let value of oldValues) {\n                // Only delete the ones we're not adding back\n                if (!values.has(key)) this.invMap.get(value)?.delete(key);\n            }\n        }\n        this.map.set(key, values);\n        for (let value of values) {\n            if (!this.invMap.has(value)) this.invMap.set(value, new Set([key]));\n            else this.invMap.get(value)?.add(key);\n        }\n        return this;\n    }\n\n    /** Clears all values for the given key so they can be re-added. */\n    public delete(key: string): boolean {\n        let oldValues = this.map.get(key);\n        if (!oldValues) return false;\n\n        this.map.delete(key);\n        for (let value of oldValues) {\n            this.invMap.get(value)?.delete(key);\n        }\n\n        return true;\n    }\n\n    /** Rename all references to the given key to a new value. */\n    public rename(oldKey: string, newKey: string): boolean {\n        let oldValues = this.map.get(oldKey);\n        if (!oldValues) return false;\n\n        this.delete(oldKey);\n        this.set(newKey, oldValues);\n        return true;\n    }\n\n    /** Clear the entire index. */\n    public clear() {\n        this.map.clear();\n        this.invMap.clear();\n    }\n\n    static EMPTY_SET: Readonly<Set<string>> = Object.freeze(new Set<string>());\n}\n\n/** Index map wrapper which is case-insensitive in the key. */\nexport class ValueCaseInsensitiveIndexMap {\n    /** Create a new, empty case insensitive index map. */\n    public constructor(public delegate: IndexMap = new IndexMap()) {}\n\n    /** Returns all values for the given key. */\n    public get(key: string): Set<string> {\n        return this.delegate.get(key);\n    }\n\n    /** Returns all keys that reference the given value. Mutating the returned set is not allowed. */\n    public getInverse(value: string): Readonly<Set<string>> {\n        return this.delegate.getInverse(value.toLocaleLowerCase());\n    }\n\n    /** Sets the key to the given values; this will delete the old mapping for the key if one was present. */\n    public set(key: string, values: Set<string>): this {\n        this.delegate.set(key, new Set(Array.from(values).map(v => v.toLocaleLowerCase())));\n        return this;\n    }\n\n    /** Clears all values for the given key so they can be re-added. */\n    public delete(key: string): boolean {\n        return this.delegate.delete(key);\n    }\n\n    /** Rename all references to the given key to a new value. */\n    public rename(oldKey: string, newKey: string): boolean {\n        return this.delegate.rename(oldKey, newKey);\n    }\n\n    /** Clear the entire index. */\n    public clear() {\n        this.delegate.clear();\n    }\n}\n"
  },
  {
    "path": "src/data-index/resolver.ts",
    "content": "/** Collect data matching a source query. */\n\nimport { FullIndex, PathFilters } from \"data-index/index\";\nimport { Result } from \"api/result\";\nimport { Source } from \"./source\";\nimport { DataObject, Link, Literal } from \"../data-model/value\";\n\n/** A data row which has an ID and associated data (like page link / page data). */\nexport type Datarow<T> = { id: Literal; data: T };\n\n/** Find source paths which match the given source. */\nexport function matchingSourcePaths(\n    source: Source,\n    index: FullIndex,\n    originFile: string = \"\"\n): Result<Set<string>, string> {\n    switch (source.type) {\n        case \"empty\":\n            return Result.success(new Set<string>());\n        case \"tag\":\n            return Result.success(index.tags.getInverse(source.tag));\n        case \"csv\":\n            return Result.success(new Set<string>([index.prefix.resolveRelative(source.path, originFile)]));\n        case \"folder\":\n            // Prefer loading from the folder at the given path.\n            if (index.prefix.nodeExists(source.folder))\n                return Result.success(index.prefix.get(source.folder, PathFilters.markdown));\n\n            // But allow for loading individual files if they exist.\n            if (index.prefix.pathExists(source.folder)) return Result.success(new Set([source.folder]));\n            else if (index.prefix.pathExists(source.folder + \".md\"))\n                return Result.success(new Set([source.folder + \".md\"]));\n\n            // For backwards-compat, return an empty result even if the folder does not exist.\n            return Result.success(new Set());\n        case \"link\":\n            let fullPath = index.metadataCache.getFirstLinkpathDest(source.file, originFile)?.path;\n            if (!fullPath) {\n                // Look in links which includes unresolved links\n                return Result.success(index.links.getInverse(source.file));\n            }\n\n            if (source.direction === \"incoming\") {\n                // To find all incoming links (i.e., things that link to this), use the index that Obsidian provides.\n                // TODO: Use an actual index so this isn't a fullscan.\n                let resolved = index.metadataCache.resolvedLinks;\n                let incoming = new Set<string>();\n\n                for (let [key, value] of Object.entries(resolved)) {\n                    if (fullPath in value) incoming.add(key);\n                }\n\n                return Result.success(incoming);\n            } else {\n                let resolved = index.metadataCache.resolvedLinks;\n                if (!(fullPath in resolved))\n                    return Result.failure(`Could not find file \"${source.file}\" during link lookup - does it exist?`);\n\n                return Result.success(new Set<string>(Object.keys(index.metadataCache.resolvedLinks[fullPath])));\n            }\n        case \"binaryop\":\n            return Result.flatMap2(\n                matchingSourcePaths(source.left, index, originFile),\n                matchingSourcePaths(source.right, index, originFile),\n                (left, right) => {\n                    if (source.op == \"&\") {\n                        let result = new Set<string>();\n                        for (let elem of right) {\n                            if (left.has(elem)) result.add(elem);\n                        }\n\n                        return Result.success(result);\n                    } else if (source.op == \"|\") {\n                        let result = new Set(left);\n                        for (let elem of right) result.add(elem);\n                        return Result.success(result);\n                    } else {\n                        return Result.failure(`Unrecognized operator '${source.op}'.`);\n                    }\n                }\n            );\n        case \"negate\":\n            return matchingSourcePaths(source.child, index, originFile).map(child => {\n                // TODO: This is obviously very inefficient. Can be improved by complicating the\n                // return type of this function & optimizing 'and' / 'or'.\n                let allFiles = new Set<string>(index.vault.getMarkdownFiles().map(f => f.path));\n                child.forEach(f => allFiles.delete(f));\n                return allFiles;\n            });\n    }\n}\n\n/** Convert a path to the data for that path; usually markdown pages, but could also be other file types (like CSV).  */\nexport async function resolvePathData(path: string, index: FullIndex): Promise<Result<Datarow<DataObject>[], string>> {\n    if (PathFilters.csv(path)) return resolveCsvData(path, index);\n    else return resolveMarkdownData(path, index);\n}\n\n// TODO: We shouldn't be doing path normalization here relative to an origin file,\n/** Convert a CSV path to the data in the CSV (in dataview format). */\nexport async function resolveCsvData(path: string, index: FullIndex): Promise<Result<Datarow<DataObject>[], string>> {\n    let rawData = await index.csv.get(path);\n    return rawData.map(rows => {\n        return rows.map((row, index) => {\n            return {\n                id: `${path}#${index}`,\n                data: row,\n            };\n        });\n    });\n}\n\n/** Convert a path pointing to a markdown page, into the associated metadata. */\nexport function resolveMarkdownData(path: string, index: FullIndex): Result<Datarow<DataObject>[], string> {\n    let page = index.pages.get(path);\n    if (!page) return Result.success([]);\n\n    return Result.success([\n        {\n            id: Link.file(path),\n            data: page.serialize(index),\n        },\n    ]);\n}\n\n/** Resolve a source to the collection of data rows that it matches. */\nexport async function resolveSource(\n    source: Source,\n    index: FullIndex,\n    originFile: string = \"\"\n): Promise<Result<Datarow<DataObject>[], string>> {\n    let paths = matchingSourcePaths(source, index, originFile);\n    if (!paths.successful) return Result.failure(paths.error);\n\n    let result = [];\n    for (let path of paths.value) {\n        let resolved = await resolvePathData(path, index);\n        if (!resolved.successful) return resolved;\n\n        for (let val of resolved.value) result.push(val);\n    }\n\n    return Result.success(result);\n}\n"
  },
  {
    "path": "src/data-index/source.ts",
    "content": "/** AST implementation for queries over data sources. */\n\n/** The source of files for a query. */\nexport type Source = TagSource | CsvSource | FolderSource | LinkSource | EmptySource | NegatedSource | BinaryOpSource;\n/** Valid operations for combining sources. */\nexport type SourceOp = \"&\" | \"|\";\n\n/** A tag as a source of data. */\nexport interface TagSource {\n    type: \"tag\";\n    /** The tag to source from. */\n    tag: string;\n}\n\n/** A csv as a source of data. */\nexport interface CsvSource {\n    type: \"csv\";\n    /** The path to the CSV file. */\n    path: string;\n}\n\n/** A folder prefix as a source of data. */\nexport interface FolderSource {\n    type: \"folder\";\n    /** The folder prefix to source from. */\n    folder: string;\n}\n\n/** Either incoming or outgoing links to a given file. */\nexport interface LinkSource {\n    type: \"link\";\n    /** The file to look for links to/from.  */\n    file: string;\n    /**\n     * The direction to look - if incoming, then all files linking to the target file. If outgoing, then all files\n     * which the file links to.\n     */\n    direction: \"incoming\" | \"outgoing\";\n}\n\n/** A source which is everything EXCEPT the files returned by the given source. */\nexport interface NegatedSource {\n    type: \"negate\";\n    /** The source to negate. */\n    child: Source;\n}\n\n/** A source which yields nothing. */\nexport interface EmptySource {\n    type: \"empty\";\n}\n\n/** A source made by combining subsources with a logical operators. */\nexport interface BinaryOpSource {\n    type: \"binaryop\";\n    op: SourceOp;\n    left: Source;\n    right: Source;\n}\n\n/** Utility functions for creating and manipulating sources. */\nexport namespace Sources {\n    /** Create a source which searches from a tag. */\n    export function tag(tag: string): TagSource {\n        return { type: \"tag\", tag };\n    }\n\n    /** Create a source which fetches from a CSV file. */\n    export function csv(path: string): CsvSource {\n        return { type: \"csv\", path };\n    }\n\n    /** Create a source which searches for files under a folder prefix. */\n    export function folder(prefix: string): FolderSource {\n        return { type: \"folder\", folder: prefix };\n    }\n\n    /** Create a source which searches for files which link to/from a given file. */\n    export function link(file: string, incoming: boolean): LinkSource {\n        return { type: \"link\", file, direction: incoming ? \"incoming\" : \"outgoing\" };\n    }\n\n    /** Create a source which joins two sources by a logical operator (and/or). */\n    export function binaryOp(left: Source, op: SourceOp, right: Source): Source {\n        return { type: \"binaryop\", left, op, right };\n    }\n\n    /** Create a source which takes the intersection of two sources. */\n    export function and(left: Source, right: Source): Source {\n        return { type: \"binaryop\", left, op: \"&\", right };\n    }\n\n    /** Create a source which takes the union of two sources. */\n    export function or(left: Source, right: Source): Source {\n        return { type: \"binaryop\", left, op: \"|\", right };\n    }\n\n    /** Create a source which negates the underlying source. */\n    export function negate(child: Source): NegatedSource {\n        return { type: \"negate\", child };\n    }\n\n    export function empty(): EmptySource {\n        return { type: \"empty\" };\n    }\n}\n"
  },
  {
    "path": "src/data-model/markdown.ts",
    "content": "import { extractSubtags, getExtension, getFileTitle, getParentFolder, stripTime } from \"util/normalize\";\nimport { DateTime } from \"luxon\";\nimport type { FullIndex } from \"data-index/index\";\nimport { Literal, Link, Values } from \"data-model/value\";\nimport { DataObject } from \"index\";\nimport { SListItem, SMarkdownPage } from \"data-model/serialized/markdown\";\nimport { Pos } from \"obsidian\";\n\n/** All extracted markdown file metadata obtained from a file. */\nexport class PageMetadata {\n    /** The path this file exists at. */\n    public path: string;\n    /** Obsidian-provided date this page was created. */\n    public ctime: DateTime;\n    /** Obsidian-provided date this page was modified. */\n    public mtime: DateTime;\n    /** Obsidian-provided size of this page in bytes. */\n    public size: number;\n    /** The day associated with this page, if relevant. */\n    public day?: DateTime;\n    /** The first H1/H2 header in the file. May not exist. */\n    public title?: string;\n    /** All of the fields contained in this markdown file - both frontmatter AND in-file links. */\n    public fields: Map<string, Literal>;\n    /** All of the exact tags (prefixed with '#') in this file overall. */\n    public tags: Set<string>;\n    /** All of the aliases defined for this file. */\n    public aliases: Set<string>;\n    /** All OUTGOING links (including embeds, header + block links) in this file. */\n    public links: Link[];\n    /** All list items contained within this page. Filter for tasks to get just tasks. */\n    public lists: ListItem[];\n    /** The raw frontmatter for this document. */\n    public frontmatter: Record<string, Literal>;\n\n    public constructor(path: string, init?: Partial<PageMetadata>) {\n        this.path = path;\n        this.fields = new Map<string, Literal>();\n        this.frontmatter = {};\n        this.tags = new Set<string>();\n        this.aliases = new Set<string>();\n        this.links = [];\n\n        Object.assign(this, init);\n\n        this.lists = (this.lists || []).map(l => new ListItem(l));\n    }\n\n    /** Canonicalize raw links and other data in partial data with normalizers, returning a completed object. */\n    public static canonicalize(data: Partial<PageMetadata>, linkNormalizer: (link: Link) => Link): PageMetadata {\n        // Mutate the data for now, which is probably a bad idea but... all well.\n        if (data.frontmatter) {\n            data.frontmatter = Values.mapLeaves(data.frontmatter, t =>\n                Values.isLink(t) ? linkNormalizer(t) : t\n            ) as DataObject;\n        }\n\n        if (data.fields) {\n            for (let [key, value] of data.fields.entries()) {\n                data.fields.set(\n                    key,\n                    Values.mapLeaves(value, t => (Values.isLink(t) ? linkNormalizer(t) : t))\n                );\n            }\n        }\n\n        if (data.lists) {\n            for (let item of data.lists) {\n                for (let [key, value] of item.fields.entries()) {\n                    item.fields.set(\n                        key,\n                        value.map(x => Values.mapLeaves(x, t => (Values.isLink(t) ? linkNormalizer(t) : t)))\n                    );\n                }\n            }\n        }\n\n        if (data.links) {\n            data.links = data.links.map(l => linkNormalizer(l));\n        }\n\n        // This is pretty ugly, but it's not possible to normalize on the worker thread that does parsing.\n        // The best way to improve this is to instead just canonicalize the entire data object; I can try to\n        // optimize `Values.mapLeaves` to only mutate if it actually changes things.\n        return new PageMetadata(data.path!!, data);\n    }\n\n    /** The name (based on path) of this file. */\n    public name(): string {\n        return getFileTitle(this.path);\n    }\n\n    /** The containing folder (based on path) of this file. */\n    public folder(): string {\n        return getParentFolder(this.path);\n    }\n\n    /** The extension of this file (likely 'md'). */\n    public extension(): string {\n        return getExtension(this.path);\n    }\n\n    /** Return a set of tags AND all of their parent tags (so #hello/yes would become #hello, #hello/yes). */\n    public fullTags(): Set<string> {\n        let result = new Set<string>();\n        for (let tag of this.tags) {\n            for (let subtag of extractSubtags(tag)) result.add(subtag);\n        }\n\n        return result;\n    }\n\n    /** Convert all links in this file to file links. */\n    public fileLinks(): Link[] {\n        // We want to make them distinct, but where links are not raw links we\n        // now keep the additional metadata.\n        let distinctLinks = new Set<Link>(this.links);\n        return Array.from(distinctLinks);\n    }\n\n    /** Map this metadata to a full object; uses the index for additional data lookups.  */\n    public serialize(index: FullIndex, cache?: ListSerializationCache): SMarkdownPage {\n        // Convert list items via the canonicalization cache.\n        let realCache = cache ?? new ListSerializationCache(this.lists);\n\n        let result: any = {\n            file: {\n                path: this.path,\n                folder: this.folder(),\n                name: this.name(),\n                link: Link.file(this.path),\n                outlinks: this.fileLinks(),\n                inlinks: Array.from(index.links.getInverse(this.path)).map(l => Link.file(l)),\n                etags: Array.from(this.tags),\n                tags: Array.from(this.fullTags()),\n                aliases: Array.from(this.aliases),\n                lists: this.lists.map(l => realCache.get(l.line)),\n                tasks: this.lists.filter(l => !!l.task).map(l => realCache.get(l.line)),\n                ctime: this.ctime,\n                cday: stripTime(this.ctime),\n                mtime: this.mtime,\n                mday: stripTime(this.mtime),\n                size: this.size,\n                starred: index.starred.starred(this.path),\n                frontmatter: Values.deepCopy(this.frontmatter),\n                ext: this.extension(),\n            },\n        };\n\n        // Add the current day if present.\n        if (this.day) result.file.day = this.day;\n\n        // Then append the computed fields.\n        for (let [key, value] of this.fields.entries()) {\n            if (key in result) continue; // Don't allow fields to override existing keys.\n            result[key] = value;\n        }\n\n        return result;\n    }\n}\n\n/** A list item inside of a list. */\nexport class ListItem {\n    /** The symbol ('*', '-', '1.') used to define this list item. */\n    symbol: string;\n    /** A link which points to this task, or to the closest block that this task is contained in. */\n    link: Link;\n    /** A link to the section that contains this list element; could be a file if this is not in a section. */\n    section: Link;\n    /** The text of this list item. This may be multiple lines of markdown. */\n    text: string;\n    /** The line that this list item starts on in the file. */\n    line: number;\n    /** The number of lines that define this list item. */\n    lineCount: number;\n    /** The line number for the first list item in the list this item belongs to. */\n    list: number;\n    /** Any links contained within this list item. */\n    links: Link[];\n    /** The tags contained within this list item. */\n    tags: Set<string>;\n    /** The raw Obsidian-provided position for where this task is. */\n    position: Pos;\n    /** The line number of the parent list item, if present; if this is undefined, this is a root item. */\n    parent?: number;\n    /** The line numbers of children of this list item. */\n    children: number[];\n    /** The block ID for this item, if one is present. */\n    blockId?: string;\n    /** Any fields defined in this list item. For tasks, this includes fields underneath the task. */\n    fields: Map<string, Literal[]>;\n\n    task?: {\n        /** The text in between the brackets of the '[ ]' task indicator ('[X]' would yield 'X', for example.) */\n        status: string;\n        /** Whether or not this task has been checked in any way (it's status is not empty/space). */\n        checked: boolean;\n        /** Whether or not this task was completed; derived from 'status' by checking if the field 'X' or 'x'. */\n        completed: boolean;\n        /** Whether or not this task and all of it's subtasks are completed. */\n        fullyCompleted: boolean;\n    };\n\n    public constructor(init?: Partial<ListItem>) {\n        Object.assign(this, init);\n\n        this.fields = this.fields || new Map();\n        this.tags = this.tags || new Set();\n        this.children = this.children || [];\n        this.links = this.links || [];\n    }\n\n    public id(): string {\n        return `${this.file().path}-${this.line}`;\n    }\n\n    public file(): Link {\n        return this.link.toFile();\n    }\n\n    public markdown(): string {\n        if (this.task) return `${this.symbol} [${this.task.completed ? \"x\" : \" \"}] ${this.text}`;\n        else return `${this.symbol} ${this.text}`;\n    }\n\n    public created(): Literal | undefined {\n        return (this.fields.get(\"created\") ?? this.fields.get(\"ctime\") ?? this.fields.get(\"cday\"))?.[0];\n    }\n\n    public due(): Literal | undefined {\n        return (this.fields.get(\"due\") ?? this.fields.get(\"duetime\") ?? this.fields.get(\"dueday\"))?.[0];\n    }\n\n    public completed(): Literal | undefined {\n        return (this.fields.get(\"completed\") ??\n            this.fields.get(\"completion\") ??\n            this.fields.get(\"comptime\") ??\n            this.fields.get(\"compday\"))?.[0];\n    }\n\n    public start(): Literal | undefined {\n        return this.fields.get(\"start\")?.[0];\n    }\n\n    public scheduled(): Literal | undefined {\n        return this.fields.get(\"scheduled\")?.[0];\n    }\n\n    /** Create an API-friendly copy of this list item. De-duplication is done via the provided cache. */\n    public serialize(cache: ListSerializationCache): SListItem {\n        // Map children to their serialized/de-duplicated equivalents right away.\n        let children = this.children.map(l => cache.get(l)).filter((l): l is SListItem => l !== undefined);\n\n        let result: DataObject = {\n            symbol: this.symbol,\n            link: this.link,\n            section: this.section,\n            text: this.text,\n            tags: Array.from(this.tags),\n            line: this.line,\n            lineCount: this.lineCount,\n            list: this.list,\n            outlinks: Array.from(this.links),\n            path: this.link.path,\n            children: children,\n            task: !!this.task,\n            annotated: this.fields.size > 0,\n            position: Values.deepCopy(this.position as any),\n\n            subtasks: children, // @deprecated, use 'item.children' instead.\n            real: !!this.task, // @deprecated, use 'item.task' instead.\n            header: this.section, // @deprecated, use 'item.section' instead.\n        };\n\n        if (this.parent || this.parent === 0) result.parent = this.parent;\n        if (this.blockId) result.blockId = this.blockId;\n\n        addFields(this.fields, result);\n\n        if (this.task) {\n            result.status = this.task.status;\n            result.checked = this.task.checked;\n            result.completed = this.task.completed;\n            result.fullyCompleted = this.task.fullyCompleted;\n\n            let created = this.created(),\n                due = this.due(),\n                completed = this.completed(),\n                start = this.start(),\n                scheduled = this.scheduled();\n\n            if (created) result.created = Values.deepCopy(created);\n            if (due) result.due = Values.deepCopy(due);\n            if (completed) result.completion = Values.deepCopy(completed);\n            if (start) result.start = Values.deepCopy(start);\n            if (scheduled) result.scheduled = Values.deepCopy(scheduled);\n        }\n\n        return result as SListItem;\n    }\n}\n\n//////////////////////////////////////////\n// Conversion / Serialization Utilities //\n//////////////////////////////////////////\n\n/** De-duplicates list items across section metadata and page metadata. */\nexport class ListSerializationCache {\n    public listItems: Record<number, ListItem>;\n    public cache: Record<number, SListItem>;\n    public seen: Set<number>;\n\n    public constructor(listItems: ListItem[]) {\n        this.listItems = {};\n        this.cache = {};\n        this.seen = new Set();\n\n        for (let item of listItems) this.listItems[item.line] = item;\n    }\n\n    public get(lineno: number): SListItem | undefined {\n        if (lineno in this.cache) return this.cache[lineno];\n        else if (this.seen.has(lineno)) {\n            console.log(\n                `Dataview: Encountered a circular list (line number ${lineno}; children ${this.listItems[\n                    lineno\n                ].children.join(\", \")})`\n            );\n            return undefined;\n        }\n\n        this.seen.add(lineno);\n        let result = this.listItems[lineno].serialize(this);\n        this.cache[lineno] = result;\n        return result;\n    }\n}\n\nexport function addFields(fields: Map<string, Literal[]>, target: DataObject): DataObject {\n    for (let [key, values] of fields.entries()) {\n        if (key in target) continue;\n        target[key] = values.length == 1 ? values[0] : values;\n    }\n\n    return target;\n}\n"
  },
  {
    "path": "src/data-model/serialized/markdown.ts",
    "content": "/** Serialized / API facing data types for Dataview objects. */\n\nimport { Link, Literal } from \"data-model/value\";\nimport { DateTime } from \"luxon\";\nimport { Pos } from \"obsidian\";\n\nexport interface SMarkdownPage {\n    file: {\n        path: string;\n        folder: string;\n        name: string;\n        link: Link;\n        outlinks: Link[];\n        inlinks: Link[];\n        etags: string[];\n        tags: string[];\n        aliases: string[];\n        lists: SListItem[];\n        tasks: STask[];\n        ctime: DateTime;\n        cday: DateTime;\n        mtime: DateTime;\n        mday: DateTime;\n        size: number;\n        ext: string;\n        starred: boolean;\n\n        day?: DateTime;\n    };\n\n    /** Additional fields added by field data. */\n    [key: string]: any;\n}\n\n////////////////////////\n// <-- List Items --> //\n////////////////////////\n\n/** A serialized list item. */\nexport type SListItem = SListEntry | STask;\n\n/** Shared data between list items. */\nexport interface SListItemBase {\n    /** The symbol used to start this list item, like '1.' or '1)' or '*'. */\n    symbol: string;\n    /** A link to the closest thing to this list item (a block, a section, or a file). */\n    link: Link;\n    /** The section that contains this list item. */\n    section: Link;\n    /** The path of the file that contains this item. */\n    path: string;\n\n    /** The line this item starts on. */\n    line: number;\n    /** The number of lines this item spans. */\n    lineCount: number;\n    /** The internal Obsidian tracker of the exact position of this line. */\n    position: Pos;\n    /** The line number of the list that this item is part of. */\n    list: number;\n    /** If present, the block ID for this item. */\n    blockId?: string;\n    /** The line number of the parent item to this list, if relevant. */\n    parent?: number;\n    /** The children elements of this list item. */\n    children: SListItem[];\n    /** Links contained inside this list item. */\n    outlinks: Link[];\n\n    /** The raw text of this item. */\n    text: string;\n    /**\n     * If present, overrides 'text' when rendered in task views. You should not mutate 'text' since it is used to\n     * validate a list item when editing it.\n     */\n    visual?: string;\n    /** Whether this item has any metadata annotations on it. */\n    annotated?: boolean;\n\n    /** Any tags present in this task. */\n    tags: string[];\n\n    /** @deprecated use 'children' instead. */\n    subtasks: SListItem[];\n    /** @deprecated use 'task' instead. */\n    real: boolean;\n    /** @deprecated use 'section' instead. */\n    header: Link;\n\n    /** Additional fields added by annotations. */\n    [key: string]: any;\n}\n\n/** A serialized list item as seen by users; this is not a task. */\nexport interface SListEntry extends SListItemBase {\n    task: false;\n}\n\n/** A serialized task. */\nexport interface STask extends SListItemBase {\n    task: true;\n    /** The status of this task, the text between the brackets ('[ ]'). Will be a space if the task is currently unchecked. */\n    status: string;\n    /** Indicates whether the task has any value other than empty space. */\n    checked: boolean;\n    /** Indicates whether the task explicitly has been marked \"completed\" ('x' or 'X'). */\n    completed: boolean;\n    /** Indicates whether the task and ALL subtasks have been completed. */\n    fullyCompleted: boolean;\n\n    /** If present, then the time that this task was created. */\n    created?: Literal;\n    /** If present, then the time that this task was due. */\n    due?: Literal;\n    /** If present, then the time that this task was completed. */\n    completion?: Literal;\n    /** If present, then the day that this task can be started. */\n    start?: Literal;\n    /** If present, then the day that work on this task is scheduled. */\n    scheduled?: Literal;\n}\n"
  },
  {
    "path": "src/data-model/transferable.ts",
    "content": "import { Link, Values } from \"data-model/value\";\nimport { DateTime, Duration, SystemZone } from \"luxon\";\n\n/** Simplifies passing dataview values across the JS web worker barrier. */\nexport namespace Transferable {\n    /** Convert a literal value to a serializer-friendly transferable value. */\n    export function transferable(value: any): any {\n        // Handle simple universal types first.\n        if (value instanceof Map) {\n            let copied = new Map();\n            for (let [key, val] of value.entries()) copied.set(transferable(key), transferable(val));\n            return copied;\n        } else if (value instanceof Set) {\n            let copied = new Set();\n            for (let val of value) copied.add(transferable(val));\n            return copied;\n        }\n\n        let wrapped = Values.wrapValue(value);\n        if (wrapped === undefined) throw Error(\"Unrecognized transferable value: \" + value);\n\n        switch (wrapped.type) {\n            case \"null\":\n            case \"number\":\n            case \"string\":\n            case \"boolean\":\n                return wrapped.value;\n            case \"date\":\n                return {\n                    \"___transfer-type\": \"date\",\n                    value: transferable(wrapped.value.toObject()),\n                    options: {\n                        zone: wrapped.value.zone.equals(SystemZone.instance) ? undefined : wrapped.value.zoneName,\n                    },\n                };\n            case \"duration\":\n                return { \"___transfer-type\": \"duration\", value: transferable(wrapped.value.toObject()) };\n            case \"array\":\n                return wrapped.value.map(v => transferable(v));\n            case \"link\":\n                return { \"___transfer-type\": \"link\", value: transferable(wrapped.value.toObject()) };\n            case \"object\":\n                let result: Record<string, any> = {};\n                for (let [key, value] of Object.entries(wrapped.value)) result[key] = transferable(value);\n                return result;\n        }\n    }\n\n    /** Convert a transferable value back to a literal value we can work with. */\n    export function value(transferable: any): any {\n        if (transferable === null) {\n            return null;\n        } else if (transferable === undefined) {\n            return undefined;\n        } else if (transferable instanceof Map) {\n            let real = new Map();\n            for (let [key, val] of transferable.entries()) real.set(value(key), value(val));\n            return real;\n        } else if (transferable instanceof Set) {\n            let real = new Set();\n            for (let val of transferable) real.add(value(val));\n            return real;\n        } else if (Array.isArray(transferable)) {\n            return transferable.map(v => value(v));\n        } else if (typeof transferable === \"object\") {\n            if (\"___transfer-type\" in transferable) {\n                switch (transferable[\"___transfer-type\"]) {\n                    case \"date\":\n                        let dateOpts = value(transferable.options);\n                        let dateData = value(transferable.value) as any;\n\n                        return DateTime.fromObject(dateData, { zone: dateOpts.zone });\n                    case \"duration\":\n                        return Duration.fromObject(value(transferable.value));\n                    case \"link\":\n                        return Link.fromObject(value(transferable.value));\n                    default:\n                        throw Error(`Unrecognized transfer type '${transferable[\"___transfer-type\"]}'`);\n                }\n            }\n\n            let result: Record<string, any> = {};\n            for (let [key, val] of Object.entries(transferable)) result[key] = value(val);\n            return result;\n        }\n\n        return transferable;\n    }\n}\n"
  },
  {
    "path": "src/data-model/value.ts",
    "content": "import { DateTime, Duration } from \"luxon\";\nimport { DEFAULT_QUERY_SETTINGS, QuerySettings } from \"settings\";\nimport { getFileTitle, normalizeHeaderForLink, renderMinimalDuration } from \"util/normalize\";\n\n/** Shorthand for a mapping from keys to values. */\nexport type DataObject = { [key: string]: Literal };\n/** The literal types supported by the query engine. */\nexport type LiteralType =\n    | \"boolean\"\n    | \"number\"\n    | \"string\"\n    | \"date\"\n    | \"duration\"\n    | \"link\"\n    | \"array\"\n    | \"object\"\n    | \"function\"\n    | \"null\"\n    | \"html\"\n    | \"widget\";\n/** The raw values that a literal can take on. */\nexport type Literal =\n    | boolean\n    | number\n    | string\n    | DateTime\n    | Duration\n    | Link\n    | Array<Literal>\n    | DataObject\n    | Function\n    | null\n    | HTMLElement\n    | Widget;\n\n/** A grouping on a type which supports recursively-nested groups. */\nexport type GroupElement<T> = { key: Literal; rows: Grouping<T> };\nexport type Grouping<T> = T[] | GroupElement<T>[];\n\n/** Maps the string type to it's external, API-facing representation. */\nexport type LiteralRepr<T extends LiteralType> = T extends \"boolean\"\n    ? boolean\n    : T extends \"number\"\n    ? number\n    : T extends \"string\"\n    ? string\n    : T extends \"duration\"\n    ? Duration\n    : T extends \"date\"\n    ? DateTime\n    : T extends \"null\"\n    ? null\n    : T extends \"link\"\n    ? Link\n    : T extends \"array\"\n    ? Array<Literal>\n    : T extends \"object\"\n    ? Record<string, Literal>\n    : T extends \"function\"\n    ? Function\n    : T extends \"html\"\n    ? HTMLElement\n    : T extends \"widget\"\n    ? Widget\n    : any;\n\n/** A wrapped literal value which can be switched on. */\nexport type WrappedLiteral =\n    | LiteralWrapper<\"string\">\n    | LiteralWrapper<\"number\">\n    | LiteralWrapper<\"boolean\">\n    | LiteralWrapper<\"date\">\n    | LiteralWrapper<\"duration\">\n    | LiteralWrapper<\"link\">\n    | LiteralWrapper<\"array\">\n    | LiteralWrapper<\"object\">\n    | LiteralWrapper<\"html\">\n    | LiteralWrapper<\"widget\">\n    | LiteralWrapper<\"function\">\n    | LiteralWrapper<\"null\">;\n\nexport interface LiteralWrapper<T extends LiteralType> {\n    type: T;\n    value: LiteralRepr<T>;\n}\n\nexport namespace Values {\n    /** Convert an arbitrary value into a reasonable, Markdown-friendly string if possible. */\n    export function toString(\n        field: any,\n        setting: QuerySettings = DEFAULT_QUERY_SETTINGS,\n        recursive: boolean = false\n    ): string {\n        let wrapped = wrapValue(field);\n        if (!wrapped) return setting.renderNullAs;\n\n        switch (wrapped.type) {\n            case \"null\":\n                return setting.renderNullAs;\n            case \"string\":\n                return wrapped.value;\n            case \"number\":\n            case \"boolean\":\n                return \"\" + wrapped.value;\n            case \"html\":\n                return wrapped.value.outerHTML;\n            case \"widget\":\n                return wrapped.value.markdown();\n            case \"link\":\n                return wrapped.value.markdown();\n            case \"function\":\n                return \"<function>\";\n            case \"array\":\n                let result = \"\";\n                if (recursive) result += \"[\";\n                result += wrapped.value.map(f => toString(f, setting, true)).join(\", \");\n                if (recursive) result += \"]\";\n                return result;\n            case \"object\":\n                return (\n                    \"{ \" +\n                    Object.entries(wrapped.value)\n                        .map(e => e[0] + \": \" + toString(e[1], setting, true))\n                        .join(\", \") +\n                    \" }\"\n                );\n            case \"date\":\n                if (wrapped.value.second == 0 && wrapped.value.hour == 0 && wrapped.value.minute == 0) {\n                    return wrapped.value.toFormat(setting.defaultDateFormat);\n                }\n\n                return wrapped.value.toFormat(setting.defaultDateTimeFormat);\n            case \"duration\":\n                return renderMinimalDuration(wrapped.value);\n        }\n    }\n\n    /** Wrap a literal value so you can switch on it easily. */\n    export function wrapValue(val: Literal): WrappedLiteral | undefined {\n        if (isNull(val)) return { type: \"null\", value: val };\n        else if (isNumber(val)) return { type: \"number\", value: val };\n        else if (isString(val)) return { type: \"string\", value: val };\n        else if (isBoolean(val)) return { type: \"boolean\", value: val };\n        else if (isDuration(val)) return { type: \"duration\", value: val };\n        else if (isDate(val)) return { type: \"date\", value: val };\n        else if (isWidget(val)) return { type: \"widget\", value: val };\n        else if (isArray(val)) return { type: \"array\", value: val };\n        else if (isLink(val)) return { type: \"link\", value: val };\n        else if (isFunction(val)) return { type: \"function\", value: val };\n        else if (isHtml(val)) return { type: \"html\", value: val };\n        else if (isObject(val)) return { type: \"object\", value: val };\n        else return undefined;\n    }\n\n    /** Recursively map complex objects at the leaves. */\n    export function mapLeaves(val: Literal, func: (t: Literal) => Literal): Literal {\n        if (isObject(val)) {\n            let result: DataObject = {};\n            for (let [key, value] of Object.entries(val)) result[key] = mapLeaves(value, func);\n            return result;\n        } else if (isArray(val)) {\n            let result: Literal[] = [];\n            for (let value of val) result.push(mapLeaves(value, func));\n            return result;\n        } else {\n            return func(val);\n        }\n    }\n\n    /** Compare two arbitrary JavaScript values. Produces a total ordering over ANY possible dataview value. */\n    export function compareValue(val1: Literal, val2: Literal, linkNormalizer?: (link: string) => string): number {\n        // Handle undefined/nulls first.\n        if (val1 === undefined) val1 = null;\n        if (val2 === undefined) val2 = null;\n        if (val1 === null && val2 === null) return 0;\n        else if (val1 === null) return -1;\n        else if (val2 === null) return 1;\n\n        // A non-null value now which we can wrap & compare on.\n        let wrap1 = wrapValue(val1);\n        let wrap2 = wrapValue(val2);\n\n        if (wrap1 === undefined && wrap2 === undefined) return 0;\n        else if (wrap1 === undefined) return -1;\n        else if (wrap2 === undefined) return 1;\n\n        // Short-circuit on different types or on reference equality.\n        if (wrap1.type != wrap2.type) return wrap1.type.localeCompare(wrap2.type);\n        if (wrap1.value === wrap2.value) return 0;\n\n        switch (wrap1.type) {\n            case \"string\":\n                return wrap1.value.localeCompare(wrap2.value as string);\n            case \"number\":\n                if (wrap1.value < (wrap2.value as number)) return -1;\n                else if (wrap1.value == (wrap2.value as number)) return 0;\n                return 1;\n            case \"null\":\n                return 0;\n            case \"boolean\":\n                if (wrap1.value == wrap2.value) return 0;\n                else return wrap1.value ? 1 : -1;\n            case \"link\":\n                let link1 = wrap1.value;\n                let link2 = wrap2.value as Link;\n                let normalize = linkNormalizer ?? ((x: string) => x);\n\n                // We can't compare by file name or display, since that would break link equality. Compare by path.\n                let pathCompare = normalize(link1.path).localeCompare(normalize(link2.path));\n                if (pathCompare != 0) return pathCompare;\n\n                // Then compare by type.\n                let typeCompare = link1.type.localeCompare(link2.type);\n                if (typeCompare != 0) return typeCompare;\n\n                // Then compare by subpath existence.\n                if (link1.subpath && !link2.subpath) return 1;\n                if (!link1.subpath && link2.subpath) return -1;\n                if (!link1.subpath && !link2.subpath) return 0;\n\n                // Since both have a subpath, compare by subpath.\n                return (link1.subpath ?? \"\").localeCompare(link2.subpath ?? \"\");\n            case \"date\":\n                return wrap1.value < (wrap2.value as DateTime)\n                    ? -1\n                    : wrap1.value.equals(wrap2.value as DateTime)\n                    ? 0\n                    : 1;\n            case \"duration\":\n                return wrap1.value < (wrap2.value as Duration)\n                    ? -1\n                    : wrap1.value.equals(wrap2.value as Duration)\n                    ? 0\n                    : 1;\n            case \"array\":\n                let f1 = wrap1.value;\n                let f2 = wrap2.value as any[];\n                for (let index = 0; index < Math.min(f1.length, f2.length); index++) {\n                    let comp = compareValue(f1[index], f2[index]);\n                    if (comp != 0) return comp;\n                }\n                return f1.length - f2.length;\n            case \"object\":\n                let o1 = wrap1.value;\n                let o2 = wrap2.value as Record<string, any>;\n                let k1 = Array.from(Object.keys(o1));\n                let k2 = Array.from(Object.keys(o2));\n                k1.sort();\n                k2.sort();\n\n                let keyCompare = compareValue(k1, k2);\n                if (keyCompare != 0) return keyCompare;\n\n                for (let key of k1) {\n                    let comp = compareValue(o1[key], o2[key]);\n                    if (comp != 0) return comp;\n                }\n\n                return 0;\n            case \"widget\":\n            case \"html\":\n            case \"function\":\n                return 0;\n        }\n    }\n\n    /** Find the corresponding Dataview type for an arbitrary value. */\n    export function typeOf(val: any): LiteralType | undefined {\n        return wrapValue(val)?.type;\n    }\n\n    /** Determine if the given value is \"truthy\" (i.e., is non-null and has data in it). */\n    export function isTruthy(field: Literal): boolean {\n        let wrapped = wrapValue(field);\n        if (!wrapped) return false;\n\n        switch (wrapped.type) {\n            case \"number\":\n                return wrapped.value != 0;\n            case \"string\":\n                return wrapped.value.length > 0;\n            case \"boolean\":\n                return wrapped.value;\n            case \"link\":\n                return !!wrapped.value.path;\n            case \"date\":\n                return wrapped.value.toMillis() != 0;\n            case \"duration\":\n                return wrapped.value.as(\"seconds\") != 0;\n            case \"object\":\n                return Object.keys(wrapped.value).length > 0;\n            case \"array\":\n                return wrapped.value.length > 0;\n            case \"null\":\n                return false;\n            case \"html\":\n            case \"widget\":\n            case \"function\":\n                return true;\n        }\n    }\n\n    /** Deep copy a field. */\n    export function deepCopy<T extends Literal>(field: T): T {\n        if (field === null || field === undefined) return field;\n\n        if (Values.isArray(field)) {\n            return ([] as Literal[]).concat(field.map(v => deepCopy(v))) as T;\n        } else if (Values.isObject(field)) {\n            let result: Record<string, Literal> = {};\n            for (let [key, value] of Object.entries(field)) result[key] = deepCopy(value);\n            return result as T;\n        } else {\n            return field;\n        }\n    }\n\n    export function isString(val: any): val is string {\n        return typeof val == \"string\";\n    }\n\n    export function isNumber(val: any): val is number {\n        return typeof val == \"number\";\n    }\n\n    export function isDate(val: any): val is DateTime {\n        return val instanceof DateTime;\n    }\n\n    export function isDuration(val: any): val is Duration {\n        return val instanceof Duration;\n    }\n\n    export function isNull(val: any): val is null | undefined {\n        return val === null || val === undefined;\n    }\n\n    export function isArray(val: any): val is any[] {\n        return Array.isArray(val);\n    }\n\n    export function isBoolean(val: any): val is boolean {\n        return typeof val === \"boolean\";\n    }\n\n    export function isLink(val: any): val is Link {\n        return val instanceof Link;\n    }\n\n    export function isWidget(val: any): val is Widget {\n        return val instanceof Widget;\n    }\n\n    export function isHtml(val: any): val is HTMLElement {\n        if (typeof HTMLElement !== \"undefined\") {\n            return val instanceof HTMLElement;\n        } else {\n            return false;\n        }\n    }\n\n    /** Checks if the given value is an object (and not any other dataview-recognized object-like type). */\n    export function isObject(val: any): val is Record<string, any> {\n        return (\n            typeof val == \"object\" &&\n            !isHtml(val) &&\n            !isWidget(val) &&\n            !isArray(val) &&\n            !isDuration(val) &&\n            !isDate(val) &&\n            !isLink(val) &&\n            val !== undefined &&\n            !isNull(val)\n        );\n    }\n\n    export function isFunction(val: any): val is Function {\n        return typeof val == \"function\";\n    }\n}\n\n///////////////\n// Groupings //\n///////////////\n\nexport namespace Groupings {\n    /** Determines if the given group entry is a standalone value, or a grouping of sub-entries. */\n    export function isElementGroup<T>(entry: T | GroupElement<T>): entry is GroupElement<T> {\n        return Values.isObject(entry) && Object.keys(entry).length == 2 && \"key\" in entry && \"rows\" in entry;\n    }\n\n    /** Determines if the given array is a grouping array. */\n    export function isGrouping<T>(entry: Grouping<T>): entry is GroupElement<T>[] {\n        for (let element of entry) if (!isElementGroup(element)) return false;\n\n        return true;\n    }\n\n    /** Count the total number of elements in a recursive grouping. */\n    export function count<T>(elements: Grouping<T>): number {\n        if (isGrouping(elements)) {\n            let result = 0;\n            for (let subgroup of elements) result += count(subgroup.rows);\n            return result;\n        } else {\n            return elements.length;\n        }\n    }\n}\n\n//////////\n// LINK //\n//////////\n\n/** The Obsidian 'link', used for uniquely describing a file, header, or block. */\nexport class Link {\n    /** The file path this link points to. */\n    public path: string;\n    /** The display name associated with the link. */\n    public display?: string;\n    /** The block ID or header this link points to within a file, if relevant. */\n    public subpath?: string;\n    /** Is this link an embedded link (!)? */\n    public embed: boolean;\n    /** The type of this link, which determines what 'subpath' refers to, if anything. */\n    public type: \"file\" | \"header\" | \"block\";\n\n    /** Create a link to a specific file. */\n    public static file(path: string, embed: boolean = false, display?: string) {\n        return new Link({\n            path,\n            embed,\n            display,\n            subpath: undefined,\n            type: \"file\",\n        });\n    }\n\n    public static infer(linkpath: string, embed: boolean = false, display?: string) {\n        if (linkpath.includes(\"#^\")) {\n            let split = linkpath.split(\"#^\");\n            return Link.block(split[0], split[1], embed, display);\n        } else if (linkpath.includes(\"#\")) {\n            let split = linkpath.split(\"#\");\n            return Link.header(split[0], split[1], embed, display);\n        } else return Link.file(linkpath, embed, display);\n    }\n\n    /** Create a link to a specific file and header in that file. */\n    public static header(path: string, header: string, embed?: boolean, display?: string) {\n        // Headers need to be normalized to alpha-numeric & with extra spacing removed.\n        return new Link({\n            path,\n            embed,\n            display,\n            subpath: normalizeHeaderForLink(header),\n            type: \"header\",\n        });\n    }\n\n    /** Create a link to a specific file and block in that file. */\n    public static block(path: string, blockId: string, embed?: boolean, display?: string) {\n        return new Link({\n            path,\n            embed,\n            display,\n            subpath: blockId,\n            type: \"block\",\n        });\n    }\n\n    public static fromObject(object: Record<string, any>) {\n        return new Link(object);\n    }\n\n    private constructor(fields: Partial<Link>) {\n        Object.assign(this, fields);\n    }\n\n    /** Checks for link equality (i.e., that the links are pointing to the same exact location). */\n    public equals(other: Link): boolean {\n        if (other == undefined || other == null) return false;\n\n        return this.path == other.path && this.type == other.type && this.subpath == other.subpath;\n    }\n\n    /** Convert this link to it's markdown representation. */\n    public toString(): string {\n        return this.markdown();\n    }\n\n    /** Convert this link to a raw object which is serialization-friendly. */\n    public toObject(): Record<string, any> {\n        return { path: this.path, type: this.type, subpath: this.subpath, display: this.display, embed: this.embed };\n    }\n\n    /** Update this link with a new path. */\n    //@ts-ignore; error appeared after updating Obsidian to 0.15.4; it also updated other packages but didn't say which\n    public withPath(path: string) {\n        return new Link(Object.assign({}, this, { path }));\n    }\n\n    /** Return a new link which points to the same location but with a new display value. */\n    public withDisplay(display?: string) {\n        return new Link(Object.assign({}, this, { display }));\n    }\n\n    /** Convert a file link into a link to a specific header. */\n    public withHeader(header: string) {\n        return Link.header(this.path, header, this.embed, this.display);\n    }\n\n    /** Convert any link into a link to its file. */\n    public toFile() {\n        return Link.file(this.path, this.embed, this.display);\n    }\n\n    /** Convert this link into an embedded link. */\n    public toEmbed(): Link {\n        if (this.embed) {\n            return this;\n        } else {\n            let link = new Link(this);\n            link.embed = true;\n            return link;\n        }\n    }\n\n    /** Convert this link into a non-embedded link. */\n    public fromEmbed(): Link {\n        if (!this.embed) {\n            return this;\n        } else {\n            let link = new Link(this);\n            link.embed = false;\n            return link;\n        }\n    }\n\n    /** Convert this link to markdown so it can be rendered. */\n    public markdown(): string {\n        let result = (this.embed ? \"!\" : \"\") + \"[[\" + this.obsidianLink();\n\n        if (this.display) {\n            result += \"|\" + this.display;\n        } else {\n            result += \"|\" + getFileTitle(this.path);\n            if (this.type == \"header\" || this.type == \"block\") result += \" > \" + this.subpath;\n        }\n\n        result += \"]]\";\n        return result;\n    }\n\n    /** Convert the inner part of the link to something that Obsidian can open / understand. */\n    public obsidianLink(): string {\n        const escaped = this.path.replaceAll(\"|\", \"\\\\|\");\n        if (this.type == \"header\") return escaped + \"#\" + this.subpath?.replaceAll(\"|\", \"\\\\|\");\n        if (this.type == \"block\") return escaped + \"#^\" + this.subpath?.replaceAll(\"|\", \"\\\\|\");\n        else return escaped;\n    }\n\n    /** The stripped name of the file this link points to. */\n    public fileName(): string {\n        return getFileTitle(this.path).replace(\".md\", \"\");\n    }\n}\n\n/////////////////\n// WIDGET BASE //\n/////////////////\n\n/**\n * A trivial base class which just defines the '$widget' identifier type. Subtypes of\n * widget are responsible for adding whatever metadata is relevant. If you want your widget\n * to have rendering functionality (which you probably do), you should extend `RenderWidget`.\n */\nexport abstract class Widget {\n    public constructor(public $widget: string) {}\n\n    /**\n     * Attempt to render this widget in markdown, if possible; if markdown is not possible,\n     * then this will attempt to render as HTML. Note that many widgets have interactive\n     * components or difficult functional components and the `markdown` function can simply\n     * return a placeholder in this case (such as `<function>` or `<task-list>`).\n     */\n    public abstract markdown(): string;\n}\n\n/** A trivial widget which renders a (key, value) pair, and allows accessing the key and value. */\nexport class ListPairWidget extends Widget {\n    public constructor(public key: Literal, public value: Literal) {\n        super(\"dataview:list-pair\");\n    }\n\n    public override markdown(): string {\n        return `${Values.toString(this.key)}: ${Values.toString(this.value)}`;\n    }\n}\n\n/** A simple widget which renders an external link. */\nexport class ExternalLinkWidget extends Widget {\n    public constructor(public url: string, public display?: string) {\n        super(\"dataview:external-link\");\n    }\n\n    public override markdown(): string {\n        return `[${this.display ?? this.url}](${this.url})`;\n    }\n}\n\nexport namespace Widgets {\n    /** Create a list pair widget matching the given key and value. */\n    export function listPair(key: Literal, value: Literal): ListPairWidget {\n        return new ListPairWidget(key, value);\n    }\n\n    /** Create an external link widget which renders an external Obsidian link. */\n    export function externalLink(url: string, display?: string): ExternalLinkWidget {\n        return new ExternalLinkWidget(url, display);\n    }\n\n    /** Checks if the given widget is a list pair widget. */\n    export function isListPair(widget: Widget): widget is ListPairWidget {\n        return widget.$widget === \"dataview:list-pair\";\n    }\n\n    export function isExternalLink(widget: Widget): widget is ExternalLinkWidget {\n        return widget.$widget === \"dataview:external-link\";\n    }\n\n    /** Determines if the given widget is any kind of built-in widget with special rendering handling. */\n    export function isBuiltin(widget: Widget): boolean {\n        return isListPair(widget) || isExternalLink(widget);\n    }\n}\n"
  },
  {
    "path": "src/expression/binaryop.ts",
    "content": "/** Provides a global dispatch table for evaluating binary operators, including comparison. */\nimport { LiteralRepr, LiteralType, Literal, Values } from \"data-model/value\";\nimport { normalizeDuration } from \"util/normalize\";\nimport { Result } from \"api/result\";\nimport { BinaryOp } from \"expression/field\";\nimport type { Context } from \"expression/context\";\n\n/** A literal type or a catch-all '*'. */\nexport type LiteralTypeOrAll = LiteralType | \"*\";\n\n/** Maps a literal type or the catch-all '*'. */\nexport type LiteralReprAll<T extends LiteralTypeOrAll> = T extends \"*\"\n    ? Literal\n    : T extends LiteralType\n    ? LiteralRepr<T>\n    : any;\n\n/** An implementation for a binary operator. */\nexport type BinaryOpImpl<A extends Literal, B extends Literal> = (first: A, second: B, ctx: Context) => Literal;\n/** An implementation of a comparator (returning a number) which then automatically defines all of the comparison operators. */\nexport type CompareImpl<T extends Literal> = (first: T, second: T, ctx: Context) => number;\n\n/** Provides implementations for binary operators on two types using a registry. */\nexport class BinaryOpHandler {\n    private map: Map<string, BinaryOpImpl<any, any>>;\n\n    public static create() {\n        return new BinaryOpHandler();\n    }\n\n    public constructor() {\n        this.map = new Map();\n    }\n\n    public register<T extends LiteralTypeOrAll, U extends LiteralTypeOrAll>(\n        left: T,\n        op: BinaryOp,\n        right: U,\n        func: BinaryOpImpl<LiteralReprAll<T>, LiteralReprAll<U>>\n    ): BinaryOpHandler {\n        this.map.set(BinaryOpHandler.repr(op, left, right), func);\n        return this;\n    }\n\n    public registerComm<T extends LiteralTypeOrAll, U extends LiteralTypeOrAll>(\n        left: T,\n        op: BinaryOp,\n        right: U,\n        func: BinaryOpImpl<LiteralReprAll<T>, LiteralReprAll<U>>\n    ): BinaryOpHandler {\n        return this.register(left, op, right, func).register(right, op, left, (a, b, ctx) => func(b, a, ctx));\n    }\n\n    /** Implement a comparison function. */\n    public compare<T extends LiteralTypeOrAll>(type: T, compare: CompareImpl<LiteralReprAll<T>>): BinaryOpHandler {\n        return this.register(type, \"<\", type, (a, b, ctx) => compare(a, b, ctx) < 0)\n            .register(type, \"<=\", type, (a, b, ctx) => compare(a, b, ctx) <= 0)\n            .register(type, \">\", type, (a, b, ctx) => compare(a, b, ctx) > 0)\n            .register(type, \">=\", type, (a, b, ctx) => compare(a, b, ctx) >= 0)\n            .register(type, \"=\", type, (a, b, ctx) => compare(a, b, ctx) == 0)\n            .register(type, \"!=\", type, (a, b, ctx) => compare(a, b, ctx) != 0);\n    }\n\n    /** Attempt to evaluate the given binary operator on the two literal fields. */\n    public evaluate(op: BinaryOp, left: Literal, right: Literal, ctx: Context): Result<Literal, string> {\n        let leftType = Values.typeOf(left);\n        let rightType = Values.typeOf(right);\n        if (!leftType) return Result.failure(`Unrecognized value '${left}'`);\n        else if (!rightType) return Result.failure(`Unrecognized value '${right}'`);\n\n        let handler = this.map.get(BinaryOpHandler.repr(op, leftType, rightType));\n        if (handler) return Result.success(handler(left, right, ctx));\n\n        // Right-'*' fallback:\n        let handler2 = this.map.get(BinaryOpHandler.repr(op, leftType, \"*\"));\n        if (handler2) return Result.success(handler2(left, right, ctx));\n\n        // Left-'*' fallback:\n        let handler3 = this.map.get(BinaryOpHandler.repr(op, \"*\", rightType));\n        if (handler3) return Result.success(handler3(left, right, ctx));\n\n        // Double '*' fallback.\n        let handler4 = this.map.get(BinaryOpHandler.repr(op, \"*\", \"*\"));\n        if (handler4) return Result.success(handler4(left, right, ctx));\n\n        return Result.failure(`No implementation found for '${leftType} ${op} ${rightType}'`);\n    }\n\n    /** Create a string representation of the given triplet for unique lookup in the map. */\n    public static repr(op: BinaryOp, left: LiteralTypeOrAll, right: LiteralTypeOrAll) {\n        return `${left},${op},${right}`;\n    }\n}\n\n/** Configure and create a binary OP handler with the given parameters. */\nexport function createBinaryOps(linkNormalizer: (x: string) => string): BinaryOpHandler {\n    return (\n        BinaryOpHandler.create()\n            // TODO: Consider not using a universal comparison function.\n            .compare(\"*\", (a, b) => Values.compareValue(a, b, linkNormalizer))\n            // Global boolean operations.\n            .register(\"*\", \"&\", \"*\", (a, b) => Values.isTruthy(a) && Values.isTruthy(b))\n            .register(\"*\", \"|\", \"*\", (a, b) => Values.isTruthy(a) || Values.isTruthy(b))\n            // Number implementations.\n            .register(\"number\", \"+\", \"number\", (a, b) => a + b)\n            .register(\"number\", \"-\", \"number\", (a, b) => a - b)\n            .register(\"number\", \"*\", \"number\", (a, b) => a * b)\n            .register(\"number\", \"/\", \"number\", (a, b) => a / b)\n            .register(\"number\", \"%\", \"number\", (a, b) => a % b)\n            // String implementations.\n            .register(\"string\", \"+\", \"*\", (a, b, ctx) => a + Values.toString(b, ctx.settings))\n            .register(\"*\", \"+\", \"string\", (a, b, ctx) => Values.toString(a, ctx.settings) + b)\n            .registerComm(\"string\", \"*\", \"number\", (a, b) => (b < 0 ? \"\" : a.repeat(b)))\n            // Date Operations.\n            .register(\"date\", \"-\", \"date\", (a, b) => {\n                return normalizeDuration(\n                    a.diff(b, [\"years\", \"months\", \"days\", \"hours\", \"minutes\", \"seconds\", \"milliseconds\"])\n                );\n            })\n            .register(\"date\", \"-\", \"duration\", (a, b) => a.minus(b))\n            .registerComm(\"date\", \"+\", \"duration\", (a, b) => a.plus(b))\n            // Duration Operations.\n            .register(\"duration\", \"+\", \"duration\", (a, b) => normalizeDuration(a.plus(b)))\n            .register(\"duration\", \"-\", \"duration\", (a, b) => normalizeDuration(a.minus(b)))\n            .register(\"duration\", \"/\", \"number\", (a, b) => normalizeDuration(a.mapUnits(x => x / b)))\n            .registerComm(\"duration\", \"*\", \"number\", (a, b) => normalizeDuration(a.mapUnits(x => x * b)))\n            // Array operations.\n            .register(\"array\", \"+\", \"array\", (a, b) => ([] as Literal[]).concat(a).concat(b))\n            // Object operations.\n            .register(\"object\", \"+\", \"object\", (a, b) => Object.assign({}, a, b))\n            // Null handling operators.\n            .register(\"null\", \"+\", \"null\", (_a, _b) => null)\n            .register(\"null\", \"-\", \"null\", (_a, _b) => null)\n            .register(\"null\", \"*\", \"null\", (_a, _b) => null)\n            .register(\"null\", \"/\", \"null\", (_a, _b) => null)\n            .register(\"null\", \"%\", \"null\", (_a, _b) => null)\n            .register(\"date\", \"+\", \"null\", (_a, _b) => null)\n            .register(\"null\", \"+\", \"date\", (_a, _b) => null)\n            .register(\"date\", \"-\", \"null\", (_a, _b) => null)\n            .register(\"null\", \"-\", \"date\", (_a, _b) => null)\n    );\n}\n"
  },
  {
    "path": "src/expression/context.ts",
    "content": "/** Core implementation of the query language evaluation engine. */\n\nimport { DataObject, Literal, Values } from \"data-model/value\";\nimport { Result } from \"api/result\";\nimport { BinaryOpHandler, createBinaryOps } from \"./binaryop\";\nimport { Field, Fields } from \"./field\";\nimport { DEFAULT_FUNCTIONS, FunctionImpl } from \"./functions\";\nimport { QuerySettings } from \"settings\";\n\n/** Handles link resolution and normalization inside of a context. */\nexport interface LinkHandler {\n    /** Resolve a link to the metadata it contains. */\n    resolve(path: string): Record<string, Literal> | null;\n    /**\n     * Normalize a link to it's fully-qualified path for comparison purposes.\n     * If the path does not exist, returns it unchanged.\n     */\n    normalize(path: string): string;\n    /** Return true if the given path actually exists, false otherwise. */\n    exists(path: string): boolean;\n}\n\n/**\n * Evaluation context that expressions can be evaluated in. Includes global state, as well as available functions and a handler\n * for binary operators.\n */\nexport class Context {\n    /**\n     * Create a new context with the given namespace of globals, as well as optionally with custom binary operator, function,\n     * and link handlers.\n     */\n    public constructor(\n        public linkHandler: LinkHandler,\n        public settings: QuerySettings,\n        public globals: Record<string, Literal> = {},\n        public binaryOps: BinaryOpHandler = createBinaryOps(linkHandler.normalize),\n        public functions: Record<string, FunctionImpl> = DEFAULT_FUNCTIONS\n    ) {}\n\n    /** Set a global value in this context. */\n    public set(name: string, value: Literal): Context {\n        this.globals[name] = value;\n        return this;\n    }\n\n    /** Get the value of a global variable by name. Returns null if not present. */\n    public get(name: string): Literal {\n        return this.globals[name] ?? null;\n    }\n\n    /** Try to evaluate an arbitrary field in this context, raising an exception on failure. */\n    public tryEvaluate(field: Field, data: Record<string, Literal> = {}): Literal {\n        return this.evaluate(field, data).orElseThrow();\n    }\n\n    /** Evaluate an arbitrary field in this context. */\n    public evaluate(field: Field, data: Record<string, Literal> = {}): Result<Literal, string> {\n        switch (field.type) {\n            case \"literal\":\n                return Result.success(field.value);\n            case \"variable\":\n                if (field.name in data) return Result.success(data[field.name]);\n                else if (field.name in this.globals) return Result.success(this.globals[field.name]);\n                else return Result.success(null);\n            case \"negated\":\n                return this.evaluate(field.child, data).map(s => !Values.isTruthy(s));\n            case \"binaryop\":\n                return Result.flatMap2(this.evaluate(field.left, data), this.evaluate(field.right, data), (a, b) =>\n                    this.binaryOps.evaluate(field.op, a, b, this)\n                );\n            case \"list\":\n                let result = [];\n                for (let child of field.values) {\n                    let subeval = this.evaluate(child, data);\n                    if (!subeval.successful) return subeval;\n                    result.push(subeval.value);\n                }\n                return Result.success(result);\n            case \"object\":\n                let objResult: DataObject = {};\n                for (let [key, child] of Object.entries(field.values)) {\n                    let subeval = this.evaluate(child, data);\n                    if (!subeval.successful) return subeval;\n                    objResult[key] = subeval.value;\n                }\n                return Result.success(objResult);\n            case \"lambda\":\n                // Just relying on JS to capture 'data' for us implicitly; unsure\n                // if this is correct thing to do. Could cause weird behaviors.\n                return Result.success((ctx: Context, ...args: Literal[]) => {\n                    let copy: Record<string, Literal> = Object.assign({}, data);\n                    for (let arg = 0; arg < Math.min(args.length, field.arguments.length); arg++) {\n                        copy[field.arguments[arg]] = args[arg];\n                    }\n\n                    return ctx.evaluate(field.value, copy).orElseThrow();\n                });\n            case \"function\":\n                let rawFunc =\n                    field.func.type == \"variable\"\n                        ? Result.success<string, string>(field.func.name)\n                        : this.evaluate(field.func, data);\n                if (!rawFunc.successful) return rawFunc;\n                let func = rawFunc.value;\n\n                let args: Literal[] = [];\n                for (let arg of field.arguments) {\n                    let resolved = this.evaluate(arg, data);\n                    if (!resolved.successful) return resolved;\n                    args.push(resolved.value);\n                }\n\n                let call: FunctionImpl;\n                if (Values.isFunction(func)) call = func as FunctionImpl;\n                else if (Values.isString(func) && func in this.functions) call = this.functions[func];\n                else if (Values.isString(func)) return Result.failure(`Unrecognized function name '${func}'`);\n                else return Result.failure(`Cannot call type '${Values.typeOf(func)}' as a function`);\n\n                try {\n                    return Result.success(call(this, ...args));\n                } catch (e) {\n                    return Result.failure(e.message);\n                }\n            case \"index\":\n                // TODO: Will move this out to an 'primitives' module and add more content to it.\n                let literalIndex = this.evaluate(field.index, data);\n                let checkedIndex: Result<string | number | null, string> = literalIndex.flatMap(s =>\n                    Values.isString(s) || Values.isNumber(s) || Values.isNull(s)\n                        ? Result.success<string | number | null, string>(s)\n                        : Result.failure(\"Can only index with a string or number\")\n                );\n                if (!checkedIndex.successful) return checkedIndex;\n\n                let index = checkedIndex.value;\n                if (Values.isNull(index)) return Result.success(null);\n\n                let checkedObject =\n                    field.object.type == \"variable\" && field.object.name == \"row\"\n                        ? Result.success<Literal, string>(Object.assign({}, this.globals, data))\n                        : this.evaluate(field.object, data);\n                if (!checkedObject.successful) return checkedObject;\n\n                let object = Values.wrapValue(checkedObject.value);\n                if (!object) return Result.failure(\"Unrecognized object to index into: \" + object);\n\n                switch (object.type) {\n                    case \"object\":\n                        if (!Values.isString(index))\n                            return Result.failure('can only index into objects with strings (a.b or a[\"b\"])');\n                        return Result.success(object.value[index] ?? null);\n                    case \"link\":\n                        if (!Values.isString(index))\n                            return Result.failure('can only index into links with strings (a.b or a[\"b\"])');\n                        let linkValue = this.linkHandler.resolve(object.value.path);\n                        if (Values.isNull(linkValue)) return Result.success(null);\n                        return Result.success(linkValue[index] ?? null);\n                    case \"array\":\n                        if (Values.isNumber(index)) {\n                            if (index >= object.value.length || index < 0) return Result.success(null);\n                            else return Result.success(object.value[index]);\n                        } else if (Values.isString(index)) {\n                            let result: Literal[] = [];\n                            for (let value of object.value) {\n                                let next = this.evaluate(Fields.index(Fields.literal(value), Fields.literal(index)));\n                                if (!next.successful) continue;\n                                result.push(next.value);\n                            }\n                            return Result.success(result);\n                        } else {\n                            return Result.failure(\n                                \"Array indexing requires either a number (to get a specific element), or a string (to map all elements inside the array)\"\n                            );\n                        }\n                    case \"string\":\n                        if (!Values.isNumber(index))\n                            return Result.failure(\"string indexing requires a numeric index (string[index])\");\n                        if (index >= object.value.length || index < 0) return Result.success(null);\n                        return Result.success(object.value[index]);\n                    case \"date\":\n                        if (!Values.isString(index))\n                            return Result.failure(\"date indexing requires a string representing the unit\");\n                        switch (index) {\n                            case \"year\":\n                                return Result.success(object.value.year);\n                            case \"month\":\n                                return Result.success(object.value.month);\n                            case \"weekyear\":\n                                return Result.success(object.value.weekNumber);\n                            case \"week\":\n                                return Result.success(Math.floor(object.value.day / 7) + 1);\n                            case \"weekday\":\n                                return Result.success(object.value.weekday);\n                            case \"day\":\n                                return Result.success(object.value.day);\n                            case \"hour\":\n                                return Result.success(object.value.hour);\n                            case \"minute\":\n                                return Result.success(object.value.minute);\n                            case \"second\":\n                                return Result.success(object.value.second);\n                            case \"millisecond\":\n                                return Result.success(object.value.millisecond);\n                            default:\n                                return Result.success(null);\n                        }\n                    case \"duration\":\n                        if (!Values.isString(index))\n                            return Result.failure(\"duration indexing requires a string representing the unit\");\n                        switch (index) {\n                            case \"year\":\n                            case \"years\":\n                                return Result.success(object.value.shiftTo(\"years\").years);\n                            case \"month\":\n                            case \"months\":\n                                return Result.success(object.value.shiftTo(\"months\").months);\n                            case \"weeks\":\n                                return Result.success(object.value.shiftTo(\"weeks\").weeks);\n                            case \"day\":\n                            case \"days\":\n                                return Result.success(object.value.shiftTo(\"days\").days);\n                            case \"hour\":\n                            case \"hours\":\n                                return Result.success(object.value.shiftTo(\"hours\").hours);\n                            case \"minute\":\n                            case \"minutes\":\n                                return Result.success(object.value.shiftTo(\"minutes\").minutes);\n                            case \"second\":\n                            case \"seconds\":\n                                return Result.success(object.value.shiftTo(\"seconds\").seconds);\n                            case \"millisecond\":\n                            case \"milliseconds\":\n                                return Result.success(object.value.shiftTo(\"milliseconds\").milliseconds);\n                            default:\n                                return Result.success(null);\n                        }\n                    default:\n                        return Result.success(null);\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "src/expression/field.ts",
    "content": "/** Defines the AST for a field which can be evaluated. */\nimport { Literal } from \"data-model/value\";\n\n/** Comparison operators which yield true/false. */\nexport type CompareOp = \">\" | \">=\" | \"<=\" | \"<\" | \"=\" | \"!=\";\n/** Arithmetic operators which yield numbers and other values. */\nexport type ArithmeticOp = \"+\" | \"-\" | \"*\" | \"/\" | \"%\" | \"&\" | \"|\";\n/** All valid binary operators. */\nexport type BinaryOp = CompareOp | ArithmeticOp;\n/** A (potentially computed) field to select or compare against. */\nexport type Field =\n    | BinaryOpField\n    | VariableField\n    | LiteralField\n    | FunctionField\n    | IndexField\n    | NegatedField\n    | LambdaField\n    | ObjectField\n    | ListField;\n\n/** Literal representation of some field type. */\nexport interface LiteralField {\n    type: \"literal\";\n    value: Literal;\n}\n\n/** A variable field for a variable with a given name. */\nexport interface VariableField {\n    type: \"variable\";\n    name: string;\n}\n\n/** A list, which is an ordered collection of fields. */\nexport interface ListField {\n    type: \"list\";\n    values: Field[];\n}\n\n/** An object, which is a mapping of name to field. */\nexport interface ObjectField {\n    type: \"object\";\n    values: Record<string, Field>;\n}\n\n/** A binary operator field which combines two subnodes somehow. */\nexport interface BinaryOpField {\n    type: \"binaryop\";\n    left: Field;\n    right: Field;\n    op: BinaryOp;\n}\n\n/** A function field which calls a function on 0 or more arguments. */\nexport interface FunctionField {\n    type: \"function\";\n    /** Either the name of the function being called, or a Function object. */\n    func: Field;\n    /** The arguments being passed to the function. */\n    arguments: Field[];\n}\n\nexport interface LambdaField {\n    type: \"lambda\";\n    /** An ordered list of named arguments. */\n    arguments: string[];\n    /** The field which should be evaluated with the arguments in context. */\n    value: Field;\n}\n\n/** A field which indexes a variable into another variable. */\nexport interface IndexField {\n    type: \"index\";\n    /** The field to index into. */\n    object: Field;\n    /** The index. */\n    index: Field;\n}\n\n/** A field which negates the value of the original field. */\nexport interface NegatedField {\n    type: \"negated\";\n    /** The child field to negated. */\n    child: Field;\n}\n\n/** Utility methods for creating & comparing fields. */\nexport namespace Fields {\n    export function variable(name: string): VariableField {\n        return { type: \"variable\", name };\n    }\n\n    export function literal(value: Literal): LiteralField {\n        return { type: \"literal\", value };\n    }\n\n    export function binaryOp(left: Field, op: BinaryOp, right: Field): Field {\n        return { type: \"binaryop\", left, op, right } as BinaryOpField;\n    }\n\n    export function index(obj: Field, index: Field): IndexField {\n        return { type: \"index\", object: obj, index };\n    }\n\n    /** Converts a string in dot-notation-format into a variable which indexes. */\n    export function indexVariable(name: string): Field {\n        let parts = name.split(\".\");\n        let result: Field = Fields.variable(parts[0]);\n        for (let index = 1; index < parts.length; index++) {\n            result = Fields.index(result, Fields.literal(parts[index]));\n        }\n\n        return result;\n    }\n\n    export function lambda(args: string[], value: Field): LambdaField {\n        return { type: \"lambda\", arguments: args, value };\n    }\n\n    export function func(func: Field, args: Field[]): FunctionField {\n        return { type: \"function\", func, arguments: args };\n    }\n\n    export function list(values: Field[]): ListField {\n        return { type: \"list\", values };\n    }\n\n    export function object(values: Record<string, Field>): ObjectField {\n        return { type: \"object\", values };\n    }\n\n    export function negate(child: Field): NegatedField {\n        return { type: \"negated\", child };\n    }\n\n    export function isCompareOp(op: BinaryOp): op is CompareOp {\n        return op == \"<=\" || op == \"<\" || op == \">\" || op == \">=\" || op == \"!=\" || op == \"=\";\n    }\n\n    export const NULL = Fields.literal(null);\n}\n"
  },
  {
    "path": "src/expression/functions.ts",
    "content": "/** Default function implementations for the expression evaluator. */\n\nimport { DateTime } from \"luxon\";\nimport { LiteralType, Link, Literal, Values, Widgets } from \"data-model/value\";\nimport { currentLocale } from \"util/locale\";\nimport { LiteralReprAll, LiteralTypeOrAll } from \"./binaryop\";\nimport { Context } from \"./context\";\nimport { Fields } from \"./field\";\nimport { EXPRESSION } from \"./parse\";\nimport { escapeRegex, normalizeMarkdown } from \"util/normalize\";\nimport { DataArray } from \"api/data-array\";\nimport { cyrb53 } from \"util/hash\";\n\n/**\n * A function implementation which takes in a function context and a variable number of arguments. Throws an error if an\n * invalid number/type of arguments are passed.\n */\nexport type FunctionImpl = (context: Context, ...rest: Literal[]) => Literal;\n/** A \"bound\" function implementation which has already had a function context passed to it. */\nexport type BoundFunctionImpl = (...args: Literal[]) => Literal;\n\n/** A function variant used in the function builder which holds the argument types. */\ninterface FunctionVariant {\n    args: LiteralTypeOrAll[];\n    varargs: boolean;\n    /** The implementing function for this specific variant. */\n    impl: FunctionImpl;\n}\n\n/**\n * Allows for the creation of functions that check the number and type of their arguments, and dispatch\n * to different implementations based on the types of the inputs.\n */\nexport class FunctionBuilder {\n    variants: FunctionVariant[];\n    vectorized: Record<number, number[]>;\n\n    public constructor(public name: string) {\n        this.variants = [];\n        this.vectorized = {};\n    }\n\n    /** Add a general function variant which accepts any number of arguments of any type. */\n    public vararg(impl: FunctionImpl): FunctionBuilder {\n        this.variants.push({ args: [], varargs: true, impl });\n        return this;\n    }\n\n    /** Add a function variant which takes in a single argument. */\n    public add1<T extends LiteralTypeOrAll>(\n        argType: T,\n        impl: (a: LiteralReprAll<T>, context: Context) => Literal\n    ): FunctionBuilder {\n        this.variants.push({\n            args: [argType],\n            varargs: false,\n            impl: (c, ...rest) => impl(rest[0] as LiteralReprAll<T>, c),\n        });\n        return this;\n    }\n\n    /** Add a function variant which takes in two typed arguments. */\n    public add2<T extends LiteralTypeOrAll, U extends LiteralTypeOrAll>(\n        arg1: T,\n        arg2: U,\n        impl: (a: LiteralReprAll<T>, b: LiteralReprAll<U>, context: Context) => Literal\n    ): FunctionBuilder {\n        this.variants.push({\n            args: [arg1, arg2],\n            varargs: false,\n            impl: (c, ...rest) => impl(rest[0] as LiteralReprAll<T>, rest[1] as LiteralReprAll<U>, c),\n        });\n        return this;\n    }\n\n    /** Add a function variant which takes in three typed arguments. */\n    public add3<T extends LiteralTypeOrAll, U extends LiteralTypeOrAll, V extends LiteralTypeOrAll>(\n        arg1: T,\n        arg2: U,\n        arg3: V,\n        impl: (a: LiteralReprAll<T>, b: LiteralReprAll<U>, c: LiteralReprAll<V>, context: Context) => Literal\n    ): FunctionBuilder {\n        this.variants.push({\n            args: [arg1, arg2, arg3],\n            varargs: false,\n            impl: (c, ...rest) =>\n                impl(rest[0] as LiteralReprAll<T>, rest[1] as LiteralReprAll<U>, rest[2] as LiteralReprAll<V>, c),\n        });\n        return this;\n    }\n\n    /** Add vectorized variants which accept the given number of arguments and delegate. */\n    public vectorize(numArgs: number, positions: number[]): FunctionBuilder {\n        this.vectorized[numArgs] = positions;\n        return this;\n    }\n\n    /** Return a function which checks the number and type of arguments, passing them on to the first matching variant. */\n    public build(): FunctionImpl {\n        let self: FunctionImpl = (context: Context, ...args: Literal[]) => {\n            let types: LiteralType[] = [];\n            for (let arg of args) {\n                let argType = Values.typeOf(arg);\n                if (!argType) throw Error(`Unrecognized argument type for argument '${arg}'`);\n                types.push(argType);\n            }\n\n            // Handle vectorization, possibly in multiple fields.\n            if (this.vectorized[types.length]) {\n                let vectorizedPositions = this.vectorized[types.length].filter(k => types[k] == \"array\");\n                if (vectorizedPositions.length > 0) {\n                    let minLength = vectorizedPositions\n                        .map(p => (args[p] as any[]).length)\n                        .reduce((p, c) => Math.min(p, c));\n\n                    // Call the subfunction for each element in the longest array.\n                    // If you call a vectorized function with different-length arrays,\n                    // the output is limited by the length of the shortest array.\n                    let result = [];\n                    for (let vpos = 0; vpos < minLength; vpos++) {\n                        let subargs = [];\n                        for (let index = 0; index < args.length; index++) {\n                            if (vectorizedPositions.includes(index)) {\n                                let arr = args[index] as any[];\n                                subargs.push(arr[vpos]);\n                            } else {\n                                subargs.push(args[index]);\n                            }\n                        }\n\n                        result.push(self(context, ...subargs));\n                    }\n\n                    return result;\n                }\n            }\n\n            outer: for (let variant of this.variants) {\n                if (variant.varargs) return variant.impl(context, ...args);\n                if (variant.args.length != types.length) continue;\n\n                for (let index = 0; index < variant.args.length; index++) {\n                    if (variant.args[index] != \"*\" && variant.args[index] != types[index]) continue outer;\n                }\n\n                return variant.impl(context, ...args);\n            }\n\n            throw Error(`No implementation of '${this.name}' found for arguments: ${types.join(\", \")}`);\n        };\n\n        return self;\n    }\n}\n\n/** Utilities for managing function implementations. */\nexport namespace Functions {\n    /** Bind a context to a function implementation, yielding a function which does not need the context argument. */\n    export function bind(func: FunctionImpl, context: Context): BoundFunctionImpl {\n        return (...args: Literal[]) => func(context, ...args);\n    }\n\n    /** Bind a context to all functions in the given map, yielding a new map of bound functions. */\n    export function bindAll(funcs: Record<string, FunctionImpl>, context: Context): Record<string, BoundFunctionImpl> {\n        let result: Record<string, BoundFunctionImpl> = {};\n        for (let [key, func] of Object.entries(funcs)) {\n            result[key] = Functions.bind(func, context);\n        }\n\n        return result;\n    }\n}\n\n/**\n * Collection of all defined functions; defined here so that they can be called from within dataview,\n * and test code.\n */\nexport namespace DefaultFunctions {\n    export const typeOf = new FunctionBuilder(\"type\")\n        .add1(\"array\", _ => \"array\")\n        .add1(\"boolean\", _ => \"boolean\")\n        .add1(\"date\", _ => \"date\")\n        .add1(\"duration\", _ => \"duration\")\n        .add1(\"function\", _ => \"function\")\n        .add1(\"widget\", _ => \"widget\")\n        .add1(\"link\", _ => \"link\")\n        .add1(\"null\", _ => \"null\")\n        .add1(\"number\", _ => \"number\")\n        .add1(\"object\", _ => \"object\")\n        .add1(\"string\", _ => \"string\")\n        .add1(\"*\", _ => \"unknown\")\n        .build();\n\n    /** Compute the length of a data type. */\n    export const length = new FunctionBuilder(\"length\")\n        .add1(\"array\", a => a.length)\n        .add1(\"object\", a => Object.keys(a).length)\n        .add1(\"string\", a => a.length)\n        .add1(\"null\", _a => 0)\n        .build();\n\n    /** List constructor function. */\n    export const list: FunctionImpl = (_context, ...args) => args;\n\n    /** Object constructor function. */\n    export const object: FunctionImpl = (_context, ...args) => {\n        if (args.length % 2 != 0) throw Error(\"object() requires an even number of arguments\");\n        let result: Record<string, Literal> = {};\n        for (let index = 0; index < args.length; index += 2) {\n            let key = args[index];\n            if (!Values.isString(key)) throw Error(\"keys should be of type string for object(key1, value1, ...)\");\n            result[key] = args[index + 1];\n        }\n\n        return result;\n    };\n\n    /** Internal link constructor function. */\n    export const link: FunctionImpl = new FunctionBuilder(\"link\")\n        .add1(\"string\", (a, c) => Link.file(c.linkHandler.normalize(a), false))\n        .add1(\"link\", a => a)\n        .add1(\"null\", _a => null)\n        .vectorize(1, [0])\n        .add2(\"string\", \"string\", (t, d, c) => Link.file(c.linkHandler.normalize(t), false, d))\n        .add3(\"string\", \"string\", \"boolean\", (t, d, e, c) => Link.file(c.linkHandler.normalize(t), e, d))\n        .add2(\"link\", \"string\", (t, d) => t.withDisplay(d))\n        .add2(\"null\", \"*\", () => null)\n        .add2(\"*\", \"null\", (t, _n, c) => link(c, t))\n        .vectorize(2, [0, 1])\n        .build();\n\n    /** Embed and un-embed a link. */\n    export const embed: FunctionImpl = new FunctionBuilder(\"embed\")\n        .add1(\"link\", l => l.toEmbed())\n        .vectorize(1, [0])\n        .add2(\"link\", \"boolean\", (l, e, c) => (e ? l.toEmbed() : l.fromEmbed()))\n        .add1(\"null\", () => null)\n        .add2(\"null\", \"*\", () => null)\n        .add2(\"*\", \"null\", () => null)\n        .vectorize(2, [0, 1])\n        .build();\n\n    /** External link constructor function. */\n    export const elink: FunctionImpl = new FunctionBuilder(\"elink\")\n        .add2(\"string\", \"string\", (a, d) => Widgets.externalLink(a, d))\n        .add2(\"string\", \"null\", (s, _n, c) => elink(c, s, s))\n        .add2(\"null\", \"*\", () => null)\n        .vectorize(2, [0])\n        .add1(\"string\", (a, c) => elink(c, a, a))\n        .add1(\"null\", () => null)\n        .vectorize(1, [0])\n        .build();\n\n    /** Date constructor function. */\n    export const date = new FunctionBuilder(\"date\")\n        .add1(\"string\", str => {\n            let parsedDate = EXPRESSION.datePlus.parse(str);\n            if (parsedDate.status) return parsedDate.value;\n            else return null;\n        })\n        .add1(\"date\", d => d)\n        .add1(\"link\", (link, c) => {\n            // Try to parse from the display...\n            if (link.display) {\n                let parsedDate = EXPRESSION.date.parse(link.display);\n                if (parsedDate.status) return parsedDate.value;\n            }\n\n            // Then try to parse from the path...\n            let parsedDate = EXPRESSION.date.parse(link.path);\n            if (parsedDate.status) return parsedDate.value;\n\n            // Then pull it from the file.\n            let resolved = c.linkHandler.resolve(link.path);\n            if (resolved && (resolved as any)?.file?.day) {\n                return (resolved as any)?.file?.day;\n            }\n\n            return null;\n        })\n        .add2(\"string\", \"string\", (d, f) => {\n            if (f === \"x\" || f === \"X\") {\n                let match = NUMBER_REGEX.exec(d);\n                if (match) return DateTime.fromMillis(Number.parseInt(match[0]) * (f === \"X\" ? 1000 : 1));\n                else {\n                    throw Error(\"Not a number for format( (${ f }): ${ d }\");\n                }\n            } else {\n                let parsedDate = DateTime.fromFormat(d, f);\n                if (parsedDate.isValid) return parsedDate;\n                else {\n                    throw Error(`Can't handle format (${f}) on date string (${d})`);\n                }\n            }\n        })\n        .add2(\"null\", \"string\", () => null)\n        .add1(\"null\", () => null)\n        .vectorize(1, [0])\n        .build();\n\n    /** Duration constructor function. */\n    export const dur = new FunctionBuilder(\"dur\")\n        .add1(\"string\", str => {\n            let parsedDur = EXPRESSION.duration.parse(str.trim());\n            if (parsedDur.status) return parsedDur.value;\n            else return null;\n        })\n        .add1(\"duration\", d => d)\n        .add1(\"null\", d => d)\n        .vectorize(1, [0])\n        .build();\n\n    /** Format a date using a luxon/moment-style date format. */\n    export const dateformat = new FunctionBuilder(\"dateformat\")\n        .add2(\"date\", \"string\", (date, format) => date.toFormat(format, { locale: currentLocale() }))\n        .add2(\"null\", \"string\", (_nul, _format) => null)\n        .vectorize(2, [0])\n        .build();\n\n    export const durationformat = new FunctionBuilder(\"durationformat\")\n        .add2(\"duration\", \"string\", (dur, format) => dur.toFormat(format))\n        .add2(\"null\", \"string\", (_nul, _format) => null)\n        .vectorize(2, [0])\n        .build();\n\n    export const localtime = new FunctionBuilder(\"localtime\")\n        .add1(\"date\", d => d.toLocal())\n        .add1(\"null\", () => null)\n        .vectorize(1, [0])\n        .build();\n\n    const NUMBER_REGEX = /-?[0-9]+(\\.[0-9]+)?/;\n\n    /** Number constructor function. */\n    export const number = new FunctionBuilder(\"number\")\n        .add1(\"number\", a => a)\n        .add1(\"string\", str => {\n            let match = NUMBER_REGEX.exec(str);\n            if (match) return Number.parseFloat(match[0]);\n            else return null;\n        })\n        .add1(\"null\", () => null)\n        .vectorize(1, [0])\n        .build();\n\n    /** Format a number using a standard currency format. */\n    export const currencyformat = new FunctionBuilder(\"currencyformat\")\n        .add2(\"number\", \"string\", (num, format) =>\n            Intl.NumberFormat(currentLocale(), { style: \"currency\", currency: format }).format(num)\n        )\n        .add2(\"null\", \"string\", (_nul, _format) => null)\n        .add1(\"number\", num => Intl.NumberFormat(currentLocale(), { style: \"currency\", currency: \"USD\" }).format(num))\n        .add1(\"null\", () => null)\n        .vectorize(2, [0])\n        .build();\n\n    /**\n     * Convert any value to a reasonable internal string representation. Most useful for dates, strings, numbers, and\n     * so on.\n     */\n    export const string = new FunctionBuilder(\"string\").add1(\"*\", (a, ctx) => Values.toString(a, ctx.settings)).build();\n\n    export const round = new FunctionBuilder(\"round\")\n        .add1(\"number\", n => Math.round(n))\n        .add1(\"null\", () => null)\n        .vectorize(1, [0])\n        .add2(\"number\", \"number\", (n, p) => {\n            if (p <= 0) return Math.round(n);\n            return parseFloat(n.toFixed(p));\n        })\n        .add2(\"number\", \"null\", n => Math.round(n))\n        .add2(\"null\", \"*\", () => null)\n        .vectorize(2, [0])\n        .build();\n\n    export const trunc = new FunctionBuilder(\"trunc\")\n        .add1(\"number\", n => Math.trunc(n))\n        .add1(\"null\", () => null)\n        .vectorize(1, [0])\n        .build();\n\n    export const floor = new FunctionBuilder(\"floor\")\n        .add1(\"number\", n => Math.floor(n))\n        .add1(\"null\", () => null)\n        .vectorize(1, [0])\n        .build();\n\n    export const ceil = new FunctionBuilder(\"ceil\")\n        .add1(\"number\", n => Math.ceil(n))\n        .add1(\"null\", () => null)\n        .vectorize(1, [0])\n        .build();\n\n    export const min: FunctionImpl = new FunctionBuilder(\"min\")\n        .add2(\"*\", \"null\", (a, _n) => a)\n        .add2(\"null\", \"*\", (_n, a) => a)\n        .add2(\"*\", \"*\", (a, b, ctx) => (Values.compareValue(a, b, ctx.linkHandler.normalize) <= 0 ? a : b))\n        .add1(\"array\", (a, ctx) => min(ctx, ...a))\n        .vararg((ctx, ...args) => (args.length == 0 ? null : args.reduce((p, c) => min(ctx, p, c))))\n        .build();\n\n    export const max: FunctionImpl = new FunctionBuilder(\"max\")\n        .add2(\"*\", \"null\", (a, _n) => a)\n        .add2(\"null\", \"*\", (_n, a) => a)\n        .add2(\"*\", \"*\", (a, b, ctx) => (Values.compareValue(a, b, ctx.linkHandler.normalize) > 0 ? a : b))\n        .add1(\"array\", (a, ctx) => max(ctx, ...a))\n        .vararg((ctx, ...args) => (args.length == 0 ? null : args.reduce((p, c) => max(ctx, p, c))))\n        .build();\n\n    export const minby: FunctionImpl = new FunctionBuilder(\"minby\")\n        .add2(\"array\", \"function\", (arr, func, ctx) => {\n            if (arr.length == 0) return null;\n\n            let values = arr.map(v => {\n                return { value: v, mapped: func(ctx, v) };\n            });\n            let filtered = values.filter(v => !Values.isNull(v.mapped));\n            if (filtered.length == 0) return arr[0];\n\n            return filtered.reduce((p, c) => {\n                if (Values.compareValue(p.mapped, c.mapped, ctx.linkHandler.normalize) <= 0) return p;\n                else return c;\n            }).value;\n        })\n        .add2(\"null\", \"function\", (_arr, _func, _ctx) => null)\n        .build();\n\n    export const maxby: FunctionImpl = new FunctionBuilder(\"maxby\")\n        .add2(\"array\", \"function\", (arr, func, ctx) => {\n            if (arr.length == 0) return null;\n\n            let values = arr.map(v => {\n                return { value: v, mapped: func(ctx, v) };\n            });\n            let filtered = values.filter(v => !Values.isNull(v.mapped));\n            if (filtered.length == 0) return arr[0];\n\n            return filtered.reduce((p, c) => {\n                if (Values.compareValue(p.mapped, c.mapped, ctx.linkHandler.normalize) > 0) return p;\n                else return c;\n            }).value;\n        })\n        .add2(\"null\", \"function\", (_arr, _func, _ctx) => null)\n        .build();\n\n    export const striptime = new FunctionBuilder(\"striptime\")\n        .add1(\"date\", d => DateTime.fromObject({ year: d.year, month: d.month, day: d.day }))\n        .add1(\"null\", _n => null)\n        .vectorize(1, [0])\n        .build();\n\n    // Default contains, which looks through data structures recursively.\n    export const contains: FunctionImpl = new FunctionBuilder(\"contains\")\n        .add2(\"array\", \"*\", (l, elem, context) => l.some(e => contains(context, e, elem)))\n        .add2(\"string\", \"string\", (haystack, needle) => haystack.includes(needle))\n        .add2(\"object\", \"string\", (obj, key) => key in obj)\n        .add2(\"*\", \"*\", (elem1, elem2, context) =>\n            context.evaluate(Fields.binaryOp(Fields.literal(elem1), \"=\", Fields.literal(elem2))).orElseThrow()\n        )\n        .vectorize(2, [1])\n        .build();\n\n    // Case insensitive version of contains.\n    export const icontains: FunctionImpl = new FunctionBuilder(\"icontains\")\n        .add2(\"array\", \"*\", (l, elem, context) => l.some(e => icontains(context, e, elem)))\n        .add2(\"string\", \"string\", (haystack, needle) =>\n            haystack.toLocaleLowerCase().includes(needle.toLocaleLowerCase())\n        )\n        .add2(\"object\", \"string\", (obj, key) => key in obj)\n        .add2(\"*\", \"*\", (elem1, elem2, context) =>\n            context.evaluate(Fields.binaryOp(Fields.literal(elem1), \"=\", Fields.literal(elem2))).orElseThrow()\n        )\n        .vectorize(2, [1])\n        .build();\n\n    // \"exact\" contains, does not look recursively.\n    export const econtains: FunctionImpl = new FunctionBuilder(\"econtains\")\n        .add2(\"array\", \"*\", (l, elem, context) =>\n            l.some(e => context.evaluate(Fields.binaryOp(Fields.literal(elem), \"=\", Fields.literal(e))).orElseThrow())\n        )\n        .add2(\"string\", \"string\", (haystack, needle) => haystack.includes(needle))\n        .add2(\"object\", \"string\", (obj, key) => key in obj)\n        .add2(\"*\", \"*\", (elem1, elem2, context) =>\n            context.evaluate(Fields.binaryOp(Fields.literal(elem1), \"=\", Fields.literal(elem2))).orElseThrow()\n        )\n        .vectorize(2, [1])\n        .build();\n\n    // Case insensitive contains which looks for exact word matches (i.e., boundary-to-boundary match).\n    export const containsword: FunctionImpl = new FunctionBuilder(\"containsword\")\n        .add2(\n            \"string\",\n            \"string\",\n            (hay, needle) => !!hay.match(new RegExp(\".*\\\\b\" + escapeRegex(needle) + \"\\\\b.*\", \"i\"))\n        )\n        .add2(\"null\", \"*\", (_a, _b) => null)\n        .add2(\"*\", \"null\", (_a, _b) => null)\n        .vectorize(2, [0, 1])\n        .build();\n\n    /** Extract 0 or more keys from a given object via indexing. */\n    export const extract: FunctionImpl = (context: Context, ...args: Literal[]) => {\n        if (args.length == 0) return \"extract(object, key1, ...) requires at least 1 argument\";\n\n        // Manually handle vectorization in the first argument.\n        let object = args[0];\n        if (Values.isArray(object)) return object.map(v => extract(context, v, ...args.slice(1)));\n\n        let result: Record<string, Literal> = {};\n        for (let index = 1; index < args.length; index++) {\n            let key = args[index];\n            if (!Values.isString(key)) throw Error(\"extract(object, key1, ...) must be called with string keys\");\n\n            result[key] = context.evaluate(Fields.index(Fields.literal(object), Fields.literal(key))).orElseThrow();\n        }\n\n        return result;\n    };\n\n    // Reverse an array or string.\n    export const reverse = new FunctionBuilder(\"reverse\")\n        .add1(\"array\", l => {\n            let result = [];\n            for (let index = l.length - 1; index >= 0; index--) result.push(l[index]);\n            return result;\n        })\n        .add1(\"string\", l => {\n            let result = \"\";\n            for (let c = 0; c < l.length; c++) result += l[l.length - c - 1];\n            return result;\n        })\n        .add1(\"*\", e => e)\n        .build();\n\n    // Sort an array; if given two arguments, sorts by the key returned.\n    export const sort: FunctionImpl = new FunctionBuilder(\"sort\")\n        .add1(\"array\", (list, context) => sort(context, list, (_ctx: Context, a: Literal) => a))\n        .add2(\"array\", \"function\", (list, key, context) => {\n            let result = ([] as Literal[]).concat(list);\n            result.sort((a, b) => {\n                let akey = key(context, a);\n                let bkey = key(context, b);\n                let le = context\n                    .evaluate(Fields.binaryOp(Fields.literal(akey), \"<\", Fields.literal(bkey)))\n                    .orElseThrow();\n                if (Values.isTruthy(le)) return -1;\n\n                let eq = context\n                    .evaluate(Fields.binaryOp(Fields.literal(akey), \"=\", Fields.literal(bkey)))\n                    .orElseThrow();\n                if (Values.isTruthy(eq)) return 0;\n\n                return 1;\n            });\n            return result;\n        })\n        .add1(\"*\", e => e)\n        .build();\n\n    export const regextest = new FunctionBuilder(\"regextest\")\n        .add2(\"string\", \"string\", (pattern: string, field: string) => RegExp(pattern).test(field))\n        .add2(\"null\", \"*\", (_n, _a) => false)\n        .add2(\"*\", \"null\", (_a, _n) => false)\n        .vectorize(2, [0, 1])\n        .build();\n\n    export const regexmatch = new FunctionBuilder(\"regexmatch\")\n        .add2(\"string\", \"string\", (pattern: string, field: string) => {\n            if (!pattern.startsWith(\"^\") && !pattern.endsWith(\"$\")) pattern = \"^\" + pattern + \"$\";\n            return !!field.match(pattern);\n        })\n        .add2(\"null\", \"*\", (_n, _a) => false)\n        .add2(\"*\", \"null\", (_a, _n) => false)\n        .vectorize(2, [0, 1])\n        .build();\n\n    export const regexreplace = new FunctionBuilder(\"regexreplace\")\n        .add3(\"string\", \"string\", \"string\", (field: string, pat: string, rep: string) => {\n            try {\n                let reg = new RegExp(pat, \"g\");\n                return field.replace(reg, rep);\n            } catch (ex) {\n                throw Error(`Invalid regexp '${pat}' in regexreplace`);\n            }\n        })\n        .add3(\"null\", \"*\", \"*\", () => null)\n        .add3(\"*\", \"null\", \"*\", () => null)\n        .add3(\"*\", \"*\", \"null\", () => null)\n        .vectorize(3, [0, 1, 2])\n        .build();\n\n    export const lower = new FunctionBuilder(\"lower\")\n        .add1(\"string\", s => s.toLocaleLowerCase())\n        .add1(\"null\", () => null)\n        .vectorize(1, [0])\n        .build();\n\n    export const upper = new FunctionBuilder(\"upper\")\n        .add1(\"string\", s => s.toLocaleUpperCase())\n        .add1(\"null\", () => null)\n        .vectorize(1, [0])\n        .build();\n\n    export const replace = new FunctionBuilder(\"replace\")\n        .add3(\"string\", \"string\", \"string\", (str, pat, repr) => str.split(pat).join(repr))\n        .add3(\"null\", \"*\", \"*\", () => null)\n        .add3(\"*\", \"null\", \"*\", () => null)\n        .add3(\"*\", \"*\", \"null\", () => null)\n        .vectorize(3, [0, 1, 2])\n        .build();\n\n    // Ensure undefined matches turn into empty strings for split/2 and split/3.\n    const splitImpl = (str: string, delim: string, limit?: number): string[] =>\n        str.split(new RegExp(delim), limit).map(str => str || \"\");\n\n    /** Split a string on a given string. */\n    export const split: FunctionImpl = new FunctionBuilder(\"split\")\n        .add2(\"string\", \"string\", (string, splitter) => splitImpl(string, splitter))\n        .add3(\"string\", \"string\", \"number\", (string, splitter, limit) => splitImpl(string, splitter, limit))\n        .add2(\"null\", \"*\", () => null)\n        .add2(\"*\", \"null\", () => null)\n        .add3(\"*\", \"*\", \"null\", () => null)\n        .add3(\"*\", \"null\", \"*\", () => null)\n        .add3(\"null\", \"*\", \"*\", () => null)\n        .build();\n\n    export const startswith: FunctionImpl = new FunctionBuilder(\"startswith\")\n        .add2(\"string\", \"string\", (str, starting) => str.startsWith(starting))\n        .add2(\"null\", \"*\", () => null)\n        .add2(\"*\", \"null\", () => null)\n        .vectorize(2, [0, 1])\n        .build();\n\n    export const endswith: FunctionImpl = new FunctionBuilder(\"endswith\")\n        .add2(\"string\", \"string\", (str, ending) => str.endsWith(ending))\n        .add2(\"null\", \"*\", () => null)\n        .add2(\"*\", \"null\", () => null)\n        .vectorize(2, [0, 1])\n        .build();\n\n    export const padleft: FunctionImpl = new FunctionBuilder(\"padleft\")\n        .add2(\"string\", \"number\", (str, len) => str.padStart(len, \" \"))\n        .add3(\"string\", \"number\", \"string\", (str, len, padding) => str.padStart(len, padding))\n        .add2(\"null\", \"*\", () => null)\n        .add2(\"*\", \"null\", () => null)\n        .add3(\"null\", \"*\", \"*\", () => null)\n        .add3(\"*\", \"null\", \"*\", () => null)\n        .add3(\"*\", \"*\", \"null\", () => null)\n        .vectorize(2, [0, 1])\n        .vectorize(3, [0, 1, 2])\n        .build();\n\n    export const padright: FunctionImpl = new FunctionBuilder(\"padright\")\n        .add2(\"string\", \"number\", (str, len) => str.padEnd(len, \" \"))\n        .add3(\"string\", \"number\", \"string\", (str, len, padding) => str.padEnd(len, padding))\n        .add2(\"null\", \"*\", () => null)\n        .add2(\"*\", \"null\", () => null)\n        .add3(\"null\", \"*\", \"*\", () => null)\n        .add3(\"*\", \"null\", \"*\", () => null)\n        .add3(\"*\", \"*\", \"null\", () => null)\n        .vectorize(2, [0, 1])\n        .vectorize(3, [0, 1, 2])\n        .build();\n\n    export const substring: FunctionImpl = new FunctionBuilder(\"substring\")\n        .add2(\"string\", \"number\", (str, start) => str.substring(start))\n        .add3(\"string\", \"number\", \"number\", (str, start, end) => str.substring(start, end))\n        .add2(\"null\", \"*\", () => null)\n        .add2(\"*\", \"null\", () => null)\n        .add3(\"null\", \"*\", \"*\", () => null)\n        .add3(\"*\", \"null\", \"*\", () => null)\n        .add3(\"*\", \"*\", \"null\", () => null)\n        .vectorize(2, [0, 1])\n        .vectorize(3, [0, 1, 2])\n        .build();\n\n    export const truncate: FunctionImpl = new FunctionBuilder(\"truncate\")\n        .add3(\"string\", \"number\", \"string\", (str, length, suffix) => {\n            if (str.length > length - suffix.length) {\n                return str.substring(0, Math.max(0, length - suffix.length)) + suffix;\n            } else {\n                return str;\n            }\n        })\n        .add2(\"string\", \"number\", (str, length, ctx) => truncate(ctx, str, length, \"...\"))\n        .add2(\"null\", \"*\", () => null)\n        .add2(\"*\", \"null\", () => null)\n        .add3(\"null\", \"*\", \"*\", () => null)\n        .add3(\"*\", \"null\", \"*\", () => null)\n        .add3(\"*\", \"*\", \"null\", () => null)\n        .vectorize(2, [0, 1])\n        .vectorize(3, [0, 1, 2])\n        .build();\n\n    export const fdefault = new FunctionBuilder(\"default\")\n        .add2(\"*\", \"*\", (v, bk) => (Values.isNull(v) ? bk : v))\n        .vectorize(2, [0, 1])\n        .build();\n\n    export const ldefault = new FunctionBuilder(\"ldefault\")\n        .add2(\"*\", \"*\", (v, bk) => (Values.isNull(v) ? bk : v))\n        .build();\n\n    // Returns the display name of the element.\n    export const display = new FunctionBuilder(\"display\")\n        .add1(\"null\", (): Literal => \"\")\n        .add1(\"array\", (a: Literal[], ctx: Context): Literal => {\n            return a.map(e => display(ctx, e)).join(\", \");\n        })\n        .add1(\"string\", (str: string): Literal => normalizeMarkdown(str))\n        .add1(\"link\", (a: Link, ctx: Context): Literal => {\n            if (a.display) {\n                return display(ctx, a.display);\n            } else {\n                return Values.toString(a, ctx.settings).replace(/\\[\\[.*\\|(.*)\\]\\]/, \"$1\");\n            }\n        })\n        .add1(\"*\", (a: Literal, ctx: Context): Literal => {\n            return Values.toString(a, ctx.settings);\n        })\n        .build();\n\n    export const choice = new FunctionBuilder(\"choice\")\n        .add3(\"*\", \"*\", \"*\", (b, left, right) => (Values.isTruthy(b) ? left : right))\n        .vectorize(3, [0])\n        .build();\n\n    export const hash = new FunctionBuilder(\"hash\")\n        .add2(\"string\", \"number\", (seed, variant) => {\n            return cyrb53(seed, variant);\n        })\n        .add2(\"string\", \"string\", (seed, text) => {\n            return cyrb53(seed + text);\n        })\n        .add3(\"string\", \"string\", \"number\", (seed, text, variant) => {\n            return cyrb53(seed + text, variant);\n        })\n        .build();\n\n    export const reduce = new FunctionBuilder(\"reduce\")\n        .add2(\"array\", \"string\", (lis, op, context) => {\n            if (lis.length == 0) return null;\n\n            if (op != \"+\" && op != \"-\" && op != \"*\" && op != \"/\" && op != \"&\" && op != \"|\")\n                throw Error(\"reduce(array, op) supports '+', '-', '/', '*', '&', and '|'\");\n\n            let value = lis[0];\n            for (let index = 1; index < lis.length; index++) {\n                value = context\n                    .evaluate(Fields.binaryOp(Fields.literal(value), op, Fields.literal(lis[index])))\n                    .orElseThrow();\n            }\n\n            return value;\n        })\n        .add2(\"array\", \"function\", (lis, op, context) => {\n            if (lis.length == 0) return null;\n\n            let value = lis[0];\n            for (let index = 1; index < lis.length; index++) {\n                // Skip null values to reduce the pain of summing over fields that may or may not exist.\n                if (Values.isNull(lis[index])) continue;\n\n                value = op(context, value, lis[index]);\n            }\n\n            return value;\n        })\n        .add2(\"null\", \"*\", () => null)\n        .add2(\"*\", \"null\", () => null)\n        .vectorize(2, [1])\n        .build();\n\n    export const sum = new FunctionBuilder(\"sum\")\n        .add1(\"array\", (arr, c) => reduce(c, arr, \"+\"))\n        .add1(\"*\", e => e)\n        .build();\n\n    export const average = new FunctionBuilder(\"average\")\n        .add1(\"array\", (array, context) => {\n            if (array.length == 0) return null;\n\n            const add = sum(context, array);\n            if (add == null || add == undefined) return null;\n\n            return context\n                .evaluate(Fields.binaryOp(Fields.literal(add), \"/\", Fields.literal(array.length)))\n                .orElseThrow();\n        })\n        .add1(\"*\", e => e)\n        .build();\n\n    export const product = new FunctionBuilder(\"product\")\n        .add1(\"array\", (arr, c) => reduce(c, arr, \"*\"))\n        .add1(\"*\", e => e)\n        .build();\n\n    export const join: FunctionImpl = new FunctionBuilder(\"join\")\n        .add2(\"array\", \"string\", (arr, sep, ctx) => arr.map(e => Values.toString(e, ctx.settings)).join(sep))\n        .add2(\"array\", \"null\", (arr, _s, context) => join(context, arr, \", \"))\n        .add2(\"*\", \"string\", (elem, sep, ctx) => Values.toString(elem, ctx.settings))\n        .add1(\"array\", (arr, context) => join(context, arr, \", \"))\n        .add1(\"*\", (e, ctx) => Values.toString(e, ctx.settings))\n        .vectorize(2, [1])\n        .build();\n\n    export const any = new FunctionBuilder(\"any\")\n        .add1(\"array\", arr => arr.some(v => Values.isTruthy(v)))\n        .add2(\"array\", \"function\", (arr, f, ctx) => arr.some(v => Values.isTruthy(f(ctx, v))))\n        .vararg((_ctx, ...args) => args.some(v => Values.isTruthy(v)))\n        .build();\n\n    export const all = new FunctionBuilder(\"all\")\n        .add1(\"array\", arr => arr.every(v => Values.isTruthy(v)))\n        .add2(\"array\", \"function\", (arr, f, ctx) => arr.every(v => Values.isTruthy(f(ctx, v))))\n        .vararg((_ctx, ...args) => args.every(v => Values.isTruthy(v)))\n        .build();\n\n    export const none = new FunctionBuilder(\"all\")\n        .add1(\"array\", arr => !arr.some(v => Values.isTruthy(v)))\n        .add2(\"array\", \"function\", (arr, f, ctx) => !arr.some(v => Values.isTruthy(f(ctx, v))))\n        .vararg((_ctx, ...args) => !args.some(v => Values.isTruthy(v)))\n        .build();\n\n    export const filter = new FunctionBuilder(\"filter\")\n        .add2(\"array\", \"function\", (arr, f, ctx) => arr.filter(v => Values.isTruthy(f(ctx, v))))\n        .add2(\"null\", \"*\", () => null)\n        .build();\n\n    export const unique = new FunctionBuilder(\"unique\")\n        .add1(\"array\", (arr, ctx) => DataArray.wrap(arr, ctx.settings).distinct().array())\n        .add1(\"null\", () => null)\n        .build();\n\n    export const map = new FunctionBuilder(\"map\")\n        .add2(\"array\", \"function\", (arr, f, ctx) => arr.map(v => f(ctx, v)))\n        .add2(\"null\", \"*\", () => null)\n        .build();\n\n    export const nonnull = new FunctionBuilder(\"nonnull\")\n        .add1(\"array\", arr => arr.filter(v => Values.typeOf(v) != \"null\"))\n        .vararg((_ctx, ...args) => args.filter(v => Values.typeOf(v) != \"null\"))\n        .build();\n\n    /** Gets an object containing a link's own properties */\n    export const meta: FunctionImpl = new FunctionBuilder(\"meta\")\n        .add1(\"link\", link => ({\n            display: link.display ?? null,\n            embed: link.embed,\n            path: link.path,\n            subpath: link.subpath ?? null,\n            type: link.type,\n        }))\n        .build();\n\n    // Concatenates sub-array elements into a new array\n    export const flat = new FunctionBuilder(\"flat\")\n        .add1(\"array\", a => {\n            return a.flat();\n        })\n        .add2(\"array\", \"number\", (a, n) => {\n            // @ts-ignore\n            return a.flat(n);\n        })\n        .add1(\"null\", () => null)\n        .build();\n\n    // Slices the array into a new array\n    export const slice = new FunctionBuilder(\"slice\")\n        .add1(\"array\", a => {\n            return a.slice();\n        })\n        .add2(\"array\", \"number\", (a, start) => {\n            return a.slice(start);\n        })\n        .add3(\"array\", \"number\", \"number\", (a, start, end) => {\n            return a.slice(start, end);\n        })\n        .add1(\"null\", () => null)\n        .build();\n\n    // Returns the first non-null value from the array as a single element\n    export const firstvalue = new FunctionBuilder(\"firstvalue\")\n        .add1(\"array\", a => {\n            let nonnull = a.filter(v => Values.typeOf(v) != \"null\");\n            let res = nonnull.length != 0 ? nonnull[0] : null;\n            return res;\n        })\n        .add1(\"null\", () => null)\n        .build();\n}\n\n/** Default function implementations for the expression evaluator. */\n// Keep functions in same order as they're documented !!\nexport const DEFAULT_FUNCTIONS: Record<string, FunctionImpl> = {\n    // Constructors\n    object: DefaultFunctions.object,\n    list: DefaultFunctions.list,\n    array: DefaultFunctions.list,\n    date: DefaultFunctions.date,\n    dur: DefaultFunctions.dur,\n    number: DefaultFunctions.number,\n    string: DefaultFunctions.string,\n    link: DefaultFunctions.link,\n    embed: DefaultFunctions.embed,\n    elink: DefaultFunctions.elink,\n    typeof: DefaultFunctions.typeOf,\n\n    // Numeric Operations\n    round: DefaultFunctions.round,\n    trunc: DefaultFunctions.trunc,\n    floor: DefaultFunctions.floor,\n    ceil: DefaultFunctions.ceil,\n    min: DefaultFunctions.min,\n    max: DefaultFunctions.max,\n    sum: DefaultFunctions.sum,\n    product: DefaultFunctions.product,\n    average: DefaultFunctions.average,\n    minby: DefaultFunctions.minby,\n    maxby: DefaultFunctions.maxby,\n\n    // Object, Arrays, and String operations\n    contains: DefaultFunctions.contains,\n    icontains: DefaultFunctions.icontains,\n    econtains: DefaultFunctions.econtains,\n    containsword: DefaultFunctions.containsword,\n    extract: DefaultFunctions.extract,\n    sort: DefaultFunctions.sort,\n    reverse: DefaultFunctions.reverse,\n    length: DefaultFunctions.length,\n    nonnull: DefaultFunctions.nonnull,\n    firstvalue: DefaultFunctions.firstvalue,\n    all: DefaultFunctions.all,\n    any: DefaultFunctions.any,\n    none: DefaultFunctions.none,\n    join: DefaultFunctions.join,\n    filter: DefaultFunctions.filter,\n    map: DefaultFunctions.map,\n    flat: DefaultFunctions.flat,\n    slice: DefaultFunctions.slice,\n    unique: DefaultFunctions.unique,\n\n    reduce: DefaultFunctions.reduce,\n\n    // String Operations\n    regextest: DefaultFunctions.regextest,\n    regexmatch: DefaultFunctions.regexmatch,\n    regexreplace: DefaultFunctions.regexreplace,\n    replace: DefaultFunctions.replace,\n    lower: DefaultFunctions.lower,\n    upper: DefaultFunctions.upper,\n    split: DefaultFunctions.split,\n    startswith: DefaultFunctions.startswith,\n    endswith: DefaultFunctions.endswith,\n    padleft: DefaultFunctions.padleft,\n    padright: DefaultFunctions.padright,\n    substring: DefaultFunctions.substring,\n    truncate: DefaultFunctions.truncate,\n\n    // Utility Operations\n    default: DefaultFunctions.fdefault,\n    ldefault: DefaultFunctions.ldefault,\n    display: DefaultFunctions.display,\n    choice: DefaultFunctions.choice,\n    striptime: DefaultFunctions.striptime,\n    dateformat: DefaultFunctions.dateformat,\n    durationformat: DefaultFunctions.durationformat,\n    currencyformat: DefaultFunctions.currencyformat,\n    localtime: DefaultFunctions.localtime,\n    hash: DefaultFunctions.hash,\n    meta: DefaultFunctions.meta,\n};\n"
  },
  {
    "path": "src/expression/parse.ts",
    "content": "import { DateTime, Duration } from \"luxon\";\nimport { Literal, Link } from \"data-model/value\";\nimport * as P from \"parsimmon\";\nimport { BinaryOp, Field, Fields, LambdaField, ListField, LiteralField, ObjectField, VariableField } from \"./field\";\nimport { FolderSource, NegatedSource, Source, SourceOp, Sources, TagSource, CsvSource } from \"data-index/source\";\nimport { normalizeDuration } from \"util/normalize\";\nimport { Result } from \"api/result\";\nimport emojiRegex from \"emoji-regex\";\n\n/** Emoji regex without any additional flags. */\nconst EMOJI_REGEX = new RegExp(emojiRegex(), \"\");\n\n/** Provides a lookup table for unit durations of the given type. */\nexport const DURATION_TYPES = {\n    year: Duration.fromObject({ years: 1 }),\n    years: Duration.fromObject({ years: 1 }),\n    yr: Duration.fromObject({ years: 1 }),\n    yrs: Duration.fromObject({ years: 1 }),\n\n    month: Duration.fromObject({ months: 1 }),\n    months: Duration.fromObject({ months: 1 }),\n    mo: Duration.fromObject({ months: 1 }),\n    mos: Duration.fromObject({ months: 1 }),\n\n    week: Duration.fromObject({ weeks: 1 }),\n    weeks: Duration.fromObject({ weeks: 1 }),\n    wk: Duration.fromObject({ weeks: 1 }),\n    wks: Duration.fromObject({ weeks: 1 }),\n    w: Duration.fromObject({ weeks: 1 }),\n\n    day: Duration.fromObject({ days: 1 }),\n    days: Duration.fromObject({ days: 1 }),\n    d: Duration.fromObject({ days: 1 }),\n\n    hour: Duration.fromObject({ hours: 1 }),\n    hours: Duration.fromObject({ hours: 1 }),\n    hr: Duration.fromObject({ hours: 1 }),\n    hrs: Duration.fromObject({ hours: 1 }),\n    h: Duration.fromObject({ hours: 1 }),\n\n    minute: Duration.fromObject({ minutes: 1 }),\n    minutes: Duration.fromObject({ minutes: 1 }),\n    min: Duration.fromObject({ minutes: 1 }),\n    mins: Duration.fromObject({ minutes: 1 }),\n    m: Duration.fromObject({ minutes: 1 }),\n\n    second: Duration.fromObject({ seconds: 1 }),\n    seconds: Duration.fromObject({ seconds: 1 }),\n    sec: Duration.fromObject({ seconds: 1 }),\n    secs: Duration.fromObject({ seconds: 1 }),\n    s: Duration.fromObject({ seconds: 1 }),\n};\n\n/** Shorthand for common dates (relative to right now). */\nexport const DATE_SHORTHANDS = {\n    now: () => DateTime.local(),\n    today: () => DateTime.local().startOf(\"day\"),\n    yesterday: () =>\n        DateTime.local()\n            .startOf(\"day\")\n            .minus(Duration.fromObject({ days: 1 })),\n    tomorrow: () =>\n        DateTime.local()\n            .startOf(\"day\")\n            .plus(Duration.fromObject({ days: 1 })),\n    sow: () => DateTime.local().startOf(\"week\"),\n    \"start-of-week\": () => DateTime.local().startOf(\"week\"),\n    eow: () => DateTime.local().endOf(\"week\"),\n    \"end-of-week\": () => DateTime.local().endOf(\"week\"),\n    soy: () => DateTime.local().startOf(\"year\"),\n    \"start-of-year\": () => DateTime.local().startOf(\"year\"),\n    eoy: () => DateTime.local().endOf(\"year\"),\n    \"end-of-year\": () => DateTime.local().endOf(\"year\"),\n    som: () => DateTime.local().startOf(\"month\"),\n    \"start-of-month\": () => DateTime.local().startOf(\"month\"),\n    eom: () => DateTime.local().endOf(\"month\"),\n    \"end-of-month\": () => DateTime.local().endOf(\"month\"),\n};\n\n/**\n * Keywords which cannot be used as variables directly. Use `row.<thing>` if it is a variable you have defined and want\n * to access.\n */\nexport const KEYWORDS = [\"FROM\", \"WHERE\", \"LIMIT\", \"GROUP\", \"FLATTEN\"];\n\n///////////////\n// Utilities //\n///////////////\n\n/** Split on unescaped pipes in an inner link. */\nfunction splitOnUnescapedPipe(link: string): [string, string | undefined] {\n    let pipe = -1;\n    while ((pipe = link.indexOf(\"|\", pipe + 1)) >= 0) {\n        if (pipe > 0 && link[pipe - 1] == \"\\\\\") continue;\n        return [link.substring(0, pipe).replace(/\\\\\\|/g, \"|\"), link.substring(pipe + 1)];\n    }\n\n    return [link.replace(/\\\\\\|/g, \"|\"), undefined];\n}\n\n/** Attempt to parse the inside of a link to pull out display name, subpath, etc. */\nexport function parseInnerLink(rawlink: string): Link {\n    let [link, display] = splitOnUnescapedPipe(rawlink);\n    return Link.infer(link, false, display);\n}\n\n/** Create a left-associative binary parser which parses the given sub-element and separator. Handles whitespace. */\nexport function createBinaryParser<T, U>(\n    child: P.Parser<T>,\n    sep: P.Parser<U>,\n    combine: (a: T, b: U, c: T) => T\n): P.Parser<T> {\n    return P.seqMap(child, P.seq(P.optWhitespace, sep, P.optWhitespace, child).many(), (first, rest) => {\n        if (rest.length == 0) return first;\n\n        let node = combine(first, rest[0][1], rest[0][3]);\n        for (let index = 1; index < rest.length; index++) {\n            node = combine(node, rest[index][1], rest[index][3]);\n        }\n        return node;\n    });\n}\n\nexport function chainOpt<T>(base: P.Parser<T>, ...funcs: ((r: T) => P.Parser<T>)[]): P.Parser<T> {\n    return P.custom((success, failure) => {\n        return (input, i) => {\n            let result = (base as any)._(input, i);\n            if (!result.status) return result;\n\n            for (let func of funcs) {\n                let next = (func(result.value as T) as any)._(input, result.index);\n                if (!next.status) return result;\n\n                result = next;\n            }\n\n            return result;\n        };\n    });\n}\n\n////////////////////////\n// Expression Parsing //\n////////////////////////\n\nexport type PostfixFragment =\n    | { type: \"dot\"; field: string }\n    | { type: \"index\"; field: Field }\n    | { type: \"function\"; fields: Field[] };\n\nexport interface ExpressionLanguage {\n    number: number;\n    string: string;\n    escapeCharacter: string;\n    bool: boolean;\n    tag: string;\n    identifier: string;\n    link: Link;\n    embedLink: Link;\n    rootDate: DateTime;\n    dateShorthand: keyof typeof DATE_SHORTHANDS;\n    date: DateTime;\n    datePlus: DateTime;\n    durationType: keyof typeof DURATION_TYPES;\n    duration: Duration;\n    rawNull: string;\n\n    binaryPlusMinus: BinaryOp;\n    binaryMulDiv: BinaryOp;\n    binaryCompareOp: BinaryOp;\n    binaryBooleanOp: BinaryOp;\n\n    // Source-related parsers.\n    tagSource: TagSource;\n    csvSource: CsvSource;\n    folderSource: FolderSource;\n    parensSource: Source;\n    atomSource: Source;\n    linkIncomingSource: Source;\n    linkOutgoingSource: Source;\n    negateSource: NegatedSource;\n    binaryOpSource: Source;\n    source: Source;\n\n    // Field-related parsers.\n    variableField: VariableField;\n    numberField: LiteralField;\n    boolField: LiteralField;\n    stringField: LiteralField;\n    dateField: LiteralField;\n    durationField: LiteralField;\n    linkField: LiteralField;\n    nullField: LiteralField;\n\n    listField: ListField;\n    objectField: ObjectField;\n\n    atomInlineField: Literal;\n    inlineFieldList: Literal[];\n    inlineField: Literal;\n\n    negatedField: Field;\n    atomField: Field;\n    indexField: Field;\n    lambdaField: LambdaField;\n\n    // Postfix parsers for function calls & the like.\n    dotPostfix: PostfixFragment;\n    indexPostfix: PostfixFragment;\n    functionPostfix: PostfixFragment;\n\n    // Binary op parsers.\n    binaryMulDivField: Field;\n    binaryPlusMinusField: Field;\n    binaryCompareField: Field;\n    binaryBooleanField: Field;\n    binaryOpField: Field;\n    parensField: Field;\n    field: Field;\n}\n\nexport const EXPRESSION = P.createLanguage<ExpressionLanguage>({\n    // A floating point number; the decimal point is optional.\n    number: q =>\n        P.regexp(/-?[0-9]+(\\.[0-9]+)?/)\n            .map(str => Number.parseFloat(str))\n            .desc(\"number\"),\n\n    // A quote-surrounded string which supports escape characters ('\\').\n    string: q =>\n        P.string('\"')\n            .then(\n                P.alt(q.escapeCharacter, P.noneOf('\"\\\\'))\n                    .atLeast(0)\n                    .map(chars => chars.join(\"\"))\n            )\n            .skip(P.string('\"'))\n            .desc(\"string\"),\n\n    escapeCharacter: _ =>\n        P.string(\"\\\\\")\n            .then(P.any)\n            .map(escaped => {\n                // If we are escaping a backslash or a quote, pass in on in escaped form\n                if (escaped === '\"') return '\"';\n                if (escaped === \"\\\\\") return \"\\\\\";\n                else return \"\\\\\" + escaped;\n            }),\n\n    // A boolean true/false value.\n    bool: _ =>\n        P.regexp(/true|false|True|False/)\n            .map(str => str.toLowerCase() == \"true\")\n            .desc(\"boolean ('true' or 'false')\"),\n\n    // A tag of the form '#stuff/hello-there'.\n    tag: _ =>\n        P.seqMap(\n            P.string(\"#\"),\n            P.alt(P.regexp(/[^\\u2000-\\u206F\\u2E00-\\u2E7F'!\"#$%&()*+,.:;<=>?@^`{|}~\\[\\]\\\\\\s]/).desc(\"text\")).many(),\n            (start, rest) => start + rest.join(\"\")\n        ).desc(\"tag ('#hello/stuff')\"),\n\n    // A variable identifier, which is alphanumeric and must start with a letter or... emoji.\n    identifier: _ =>\n        P.seqMap(\n            P.alt(P.regexp(/\\p{Letter}/u), P.regexp(EMOJI_REGEX).desc(\"text\")),\n            P.alt(P.regexp(/[0-9\\p{Letter}_-]/u), P.regexp(EMOJI_REGEX).desc(\"text\")).many(),\n            (first, rest) => first + rest.join(\"\")\n        ).desc(\"variable identifier\"),\n\n    // An Obsidian link of the form [[<link>]].\n    link: _ =>\n        P.regexp(/\\[\\[([^\\[\\]]*?)\\]\\]/u, 1)\n            .map(linkInner => parseInnerLink(linkInner))\n            .desc(\"file link\"),\n\n    // An embeddable link which can start with '!'. This overlaps with the normal negation operator, so it is only\n    // provided for metadata parsing.\n    embedLink: q =>\n        P.seqMap(P.string(\"!\").atMost(1), q.link, (p, l) => {\n            if (p.length > 0) l.embed = true;\n            return l;\n        }).desc(\"file link\"),\n\n    // Binary plus or minus operator.\n    binaryPlusMinus: _ =>\n        P.regexp(/\\+|-/)\n            .map(str => str as BinaryOp)\n            .desc(\"'+' or '-'\"),\n\n    // Binary times or divide operator.\n    binaryMulDiv: _ =>\n        P.regexp(/\\*|\\/|%/)\n            .map(str => str as BinaryOp)\n            .desc(\"'*' or '/' or '%'\"),\n\n    // Binary comparison operator.\n    binaryCompareOp: _ =>\n        P.regexp(/>=|<=|!=|>|<|=/)\n            .map(str => str as BinaryOp)\n            .desc(\"'>=' or '<=' or '!=' or '=' or '>' or '<'\"),\n\n    // Binary boolean combination operator.\n    binaryBooleanOp: _ =>\n        P.regexp(/and|or|&|\\|/i)\n            .map(str => {\n                if (str.toLowerCase() == \"and\") return \"&\";\n                else if (str.toLowerCase() == \"or\") return \"|\";\n                else return str as BinaryOp;\n            })\n            .desc(\"'and' or 'or'\"),\n\n    // A date which can be YYYY-MM[-DDTHH:mm:ss].\n    rootDate: _ =>\n        P.seqMap(P.regexp(/\\d{4}/), P.string(\"-\"), P.regexp(/\\d{2}/), (year, _, month) => {\n            return DateTime.fromObject({ year: Number.parseInt(year), month: Number.parseInt(month) });\n        }).desc(\"date in format YYYY-MM[-DDTHH-MM-SS.MS]\"),\n    dateShorthand: _ =>\n        P.alt(\n            ...Object.keys(DATE_SHORTHANDS)\n                .sort((a, b) => b.length - a.length)\n                .map(P.string)\n        ) as P.Parser<keyof typeof DATE_SHORTHANDS>,\n    date: q =>\n        chainOpt<DateTime>(\n            q.rootDate,\n            (ym: DateTime) =>\n                P.seqMap(P.string(\"-\"), P.regexp(/\\d{2}/), (_, day) => ym.set({ day: Number.parseInt(day) })),\n            (ymd: DateTime) =>\n                P.seqMap(P.string(\"T\"), P.regexp(/\\d{2}/), (_, hour) => ymd.set({ hour: Number.parseInt(hour) })),\n            (ymdh: DateTime) =>\n                P.seqMap(P.string(\":\"), P.regexp(/\\d{2}/), (_, minute) =>\n                    ymdh.set({ minute: Number.parseInt(minute) })\n                ),\n            (ymdhm: DateTime) =>\n                P.seqMap(P.string(\":\"), P.regexp(/\\d{2}/), (_, second) =>\n                    ymdhm.set({ second: Number.parseInt(second) })\n                ),\n            (ymdhms: DateTime) =>\n                P.alt(\n                    P.seqMap(P.string(\".\"), P.regexp(/\\d{3}/), (_, millisecond) =>\n                        ymdhms.set({ millisecond: Number.parseInt(millisecond) })\n                    ),\n                    P.succeed(ymdhms) // pass\n                ),\n            (dt: DateTime) =>\n                P.alt(\n                    P.seqMap(P.string(\"+\").or(P.string(\"-\")), P.regexp(/\\d{1,2}(:\\d{2})?/), (pm, hr) =>\n                        dt.setZone(\"UTC\" + pm + hr, { keepLocalTime: true })\n                    ),\n                    P.seqMap(P.string(\"Z\"), () => dt.setZone(\"utc\", { keepLocalTime: true })),\n                    P.seqMap(P.string(\"[\"), P.regexp(/[0-9A-Za-z+-\\/]+/u), P.string(\"]\"), (_a, zone, _b) =>\n                        dt.setZone(zone, { keepLocalTime: true })\n                    )\n                )\n        )\n            .assert((dt: DateTime) => dt.isValid, \"valid date\")\n            .desc(\"date in format YYYY-MM[-DDTHH-MM-SS.MS]\"),\n\n    // A date, plus various shorthand times of day it could be.\n    datePlus: q =>\n        P.alt<DateTime>(\n            q.dateShorthand.map(d => DATE_SHORTHANDS[d]()),\n            q.date\n        ).desc(\"date in format YYYY-MM[-DDTHH-MM-SS.MS] or in shorthand\"),\n\n    // A duration of time.\n    durationType: _ =>\n        P.alt(\n            ...Object.keys(DURATION_TYPES)\n                .sort((a, b) => b.length - a.length)\n                .map(P.string)\n        ) as P.Parser<keyof typeof DURATION_TYPES>,\n    duration: q =>\n        P.seqMap(q.number, P.optWhitespace, q.durationType, (count, _, t) => DURATION_TYPES[t].mapUnits(x => x * count))\n            .sepBy1(P.string(\",\").trim(P.optWhitespace).or(P.optWhitespace))\n            .map(durations => durations.reduce((p, c) => p.plus(c)))\n            .desc(\"duration like 4hr2min\"),\n\n    // A raw null value.\n    rawNull: _ => P.string(\"null\"),\n\n    // Source parsing.\n    tagSource: q => q.tag.map(tag => Sources.tag(tag)),\n    csvSource: q =>\n        P.seqMap(P.string(\"csv(\").skip(P.optWhitespace), q.string, P.string(\")\"), (_1, path, _2) => Sources.csv(path)),\n    linkIncomingSource: q => q.link.map(link => Sources.link(link.path, true)),\n    linkOutgoingSource: q =>\n        P.seqMap(P.string(\"outgoing(\").skip(P.optWhitespace), q.link, P.string(\")\"), (_1, link, _2) =>\n            Sources.link(link.path, false)\n        ),\n    folderSource: q => q.string.map(str => Sources.folder(str)),\n    parensSource: q =>\n        P.seqMap(\n            P.string(\"(\"),\n            P.optWhitespace,\n            q.source,\n            P.optWhitespace,\n            P.string(\")\"),\n            (_1, _2, field, _3, _4) => field\n        ),\n    negateSource: q =>\n        P.seqMap(P.alt(P.string(\"-\"), P.string(\"!\")), q.atomSource, (_, source) => Sources.negate(source)),\n    atomSource: q =>\n        P.alt<Source>(\n            q.parensSource,\n            q.negateSource,\n            q.linkOutgoingSource,\n            q.linkIncomingSource,\n            q.folderSource,\n            q.tagSource,\n            q.csvSource\n        ),\n    binaryOpSource: q =>\n        createBinaryParser(\n            q.atomSource,\n            q.binaryBooleanOp.map(s => s as SourceOp),\n            Sources.binaryOp\n        ),\n    source: q => q.binaryOpSource,\n\n    // Field parsing.\n    variableField: q =>\n        q.identifier\n            .chain(r => {\n                if (KEYWORDS.includes(r.toUpperCase())) {\n                    return P.fail(\"Variable fields cannot be a keyword (\" + KEYWORDS.join(\" or \") + \")\");\n                } else {\n                    return P.succeed(Fields.variable(r));\n                }\n            })\n            .desc(\"variable\"),\n    numberField: q => q.number.map(val => Fields.literal(val)).desc(\"number\"),\n    stringField: q => q.string.map(val => Fields.literal(val)).desc(\"string\"),\n    boolField: q => q.bool.map(val => Fields.literal(val)).desc(\"boolean\"),\n    dateField: q =>\n        P.seqMap(\n            P.string(\"date(\"),\n            P.optWhitespace,\n            q.datePlus,\n            P.optWhitespace,\n            P.string(\")\"),\n            (prefix, _1, date, _2, postfix) => Fields.literal(date)\n        ).desc(\"date\"),\n    durationField: q =>\n        P.seqMap(\n            P.string(\"dur(\"),\n            P.optWhitespace,\n            q.duration,\n            P.optWhitespace,\n            P.string(\")\"),\n            (prefix, _1, dur, _2, postfix) => Fields.literal(dur)\n        ).desc(\"duration\"),\n    nullField: q => q.rawNull.map(_ => Fields.NULL),\n    linkField: q => q.link.map(f => Fields.literal(f)),\n    listField: q =>\n        q.field\n            .sepBy(P.string(\",\").trim(P.optWhitespace))\n            .wrap(P.string(\"[\").skip(P.optWhitespace), P.optWhitespace.then(P.string(\"]\")))\n            .map(l => Fields.list(l))\n            .desc(\"list ('[1, 2, 3]')\"),\n    objectField: q =>\n        P.seqMap(q.identifier.or(q.string), P.string(\":\").trim(P.optWhitespace), q.field, (name, _sep, value) => {\n            return { name, value };\n        })\n            .sepBy(P.string(\",\").trim(P.optWhitespace))\n            .wrap(P.string(\"{\").skip(P.optWhitespace), P.optWhitespace.then(P.string(\"}\")))\n            .map(vals => {\n                let res: Record<string, Field> = {};\n                for (let entry of vals) res[entry.name] = entry.value;\n                return Fields.object(res);\n            })\n            .desc(\"object ('{ a: 1, b: 2 }')\"),\n\n    atomInlineField: q =>\n        P.alt(\n            q.date,\n            q.duration.map(d => normalizeDuration(d)),\n            q.string,\n            q.tag,\n            q.embedLink,\n            q.bool,\n            q.number,\n            q.rawNull\n        ),\n    inlineFieldList: q => q.atomInlineField.sepBy(P.string(\",\").trim(P.optWhitespace).lookahead(q.atomInlineField)),\n    inlineField: q =>\n        P.alt(\n            P.seqMap(q.atomInlineField, P.string(\",\").trim(P.optWhitespace), q.inlineFieldList, (f, _s, l) =>\n                [f].concat(l)\n            ),\n            q.atomInlineField\n        ),\n\n    atomField: q =>\n        P.alt(\n            // Place embed links above negated fields as they are the special parser case '![[thing]]' and are generally unambiguous.\n            q.embedLink.map(l => Fields.literal(l)),\n            q.negatedField,\n            q.linkField,\n            q.listField,\n            q.objectField,\n            q.lambdaField,\n            q.parensField,\n            q.boolField,\n            q.numberField,\n            q.stringField,\n            q.dateField,\n            q.durationField,\n            q.nullField,\n            q.variableField\n        ),\n    indexField: q =>\n        P.seqMap(q.atomField, P.alt(q.dotPostfix, q.indexPostfix, q.functionPostfix).many(), (obj, postfixes) => {\n            let result = obj;\n            for (let post of postfixes) {\n                switch (post.type) {\n                    case \"dot\":\n                        result = Fields.index(result, Fields.literal(post.field));\n                        break;\n                    case \"index\":\n                        result = Fields.index(result, post.field);\n                        break;\n                    case \"function\":\n                        result = Fields.func(result, post.fields);\n                        break;\n                }\n            }\n\n            return result;\n        }),\n    negatedField: q => P.seqMap(P.string(\"!\"), q.indexField, (_, field) => Fields.negate(field)).desc(\"negated field\"),\n    parensField: q =>\n        P.seqMap(\n            P.string(\"(\"),\n            P.optWhitespace,\n            q.field,\n            P.optWhitespace,\n            P.string(\")\"),\n            (_1, _2, field, _3, _4) => field\n        ),\n    lambdaField: q =>\n        P.seqMap(\n            q.identifier\n                .sepBy(P.string(\",\").trim(P.optWhitespace))\n                .wrap(P.string(\"(\").trim(P.optWhitespace), P.string(\")\").trim(P.optWhitespace)),\n            P.string(\"=>\").trim(P.optWhitespace),\n            q.field,\n            (ident, _ignore, value) => {\n                return { type: \"lambda\", arguments: ident, value };\n            }\n        ),\n\n    dotPostfix: q =>\n        P.seqMap(P.string(\".\"), q.identifier, (_, field) => {\n            return { type: \"dot\", field: field };\n        }),\n    indexPostfix: q =>\n        P.seqMap(P.string(\"[\"), P.optWhitespace, q.field, P.optWhitespace, P.string(\"]\"), (_, _2, field, _3, _4) => {\n            return { type: \"index\", field };\n        }),\n    functionPostfix: q =>\n        P.seqMap(\n            P.string(\"(\"),\n            P.optWhitespace,\n            q.field.sepBy(P.string(\",\").trim(P.optWhitespace)),\n            P.optWhitespace,\n            P.string(\")\"),\n            (_, _1, fields, _2, _3) => {\n                return { type: \"function\", fields };\n            }\n        ),\n\n    // The precedence hierarchy of operators - multiply/divide, add/subtract, compare, and then boolean operations.\n    binaryMulDivField: q => createBinaryParser(q.indexField, q.binaryMulDiv, Fields.binaryOp),\n    binaryPlusMinusField: q => createBinaryParser(q.binaryMulDivField, q.binaryPlusMinus, Fields.binaryOp),\n    binaryCompareField: q => createBinaryParser(q.binaryPlusMinusField, q.binaryCompareOp, Fields.binaryOp),\n    binaryBooleanField: q => createBinaryParser(q.binaryCompareField, q.binaryBooleanOp, Fields.binaryOp),\n    binaryOpField: q => q.binaryBooleanField,\n\n    field: q => q.binaryOpField,\n});\n\n/**\n * Attempt to parse a field from the given text, returning a string error if the\n * parse failed.\n */\nexport function parseField(text: string): Result<Field, string> {\n    try {\n        return Result.success(EXPRESSION.field.tryParse(text));\n    } catch (error) {\n        return Result.failure(\"\" + error);\n    }\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "// Basic API type.\nexport type { DataviewApi } from \"api/plugin-api\";\nexport type { DataviewInlineApi, DataviewInlineIOApi } from \"api/inline-api\";\n\n// Core Dataview types.\nexport type { DateTime, Duration } from \"luxon\";\nexport type {\n    Link,\n    DataObject,\n    LiteralType,\n    Literal,\n    LiteralRepr,\n    WrappedLiteral,\n    LiteralWrapper,\n    Widget,\n} from \"data-model/value\";\n\nexport type { Result, Success, Failure } from \"api/result\";\nexport type { DataArray } from \"api/data-array\";\n\n// Dataview Index.\nexport type { ListItem, PageMetadata } from \"data-model/markdown\";\nexport type { FullIndex, PrefixIndex, IndexMap } from \"data-index/index\";\n\n// Serialized types which describe all outputs of serialization.\nexport type { SMarkdownPage, SListEntry, STask } from \"data-model/serialized/markdown\";\n\n// Useful utilities for directly using dataview parsers.\nexport {\n    DURATION_TYPES,\n    DATE_SHORTHANDS,\n    KEYWORDS,\n    ExpressionLanguage,\n    EXPRESSION,\n    parseField,\n} from \"expression/parse\";\nexport { QUERY_LANGUAGE } from \"query/parse\";\nexport { Query } from \"query/query\";\n\n////////////////////\n// Implementation //\n////////////////////\n\nimport type { DataviewApi } from \"api/plugin-api\";\n\nimport \"obsidian\";\nimport type { App } from \"obsidian\";\n\n// Utility functions.\n/**\n * Get the current Dataview API from the app if provided; if not, it is inferred from the global API object installed\n * on the window.\n */\nexport const getAPI = (app?: App): DataviewApi | undefined => {\n    if (app) return app.plugins.plugins.dataview?.api;\n    else return window.DataviewAPI;\n};\n\n/** Determine if Dataview is enabled in the given application. */\nexport const isPluginEnabled = (app: App) => app.plugins.enabledPlugins.has(\"dataview\");\n"
  },
  {
    "path": "src/main.ts",
    "content": "import {\n    App,\n    Component,\n    debounce,\n    MarkdownPostProcessorContext,\n    MarkdownView,\n    Plugin,\n    PluginSettingTab,\n    Setting,\n    WorkspaceLeaf,\n} from \"obsidian\";\nimport { renderErrorPre } from \"ui/render\";\nimport { FullIndex } from \"data-index/index\";\nimport { parseField } from \"expression/parse\";\nimport { tryOrPropagate } from \"util/normalize\";\nimport { DataviewApi, isDataviewDisabled } from \"api/plugin-api\";\nimport { DataviewSettings, DEFAULT_QUERY_SETTINGS, DEFAULT_SETTINGS } from \"settings\";\nimport { DataviewInlineRenderer } from \"ui/views/inline-view\";\nimport { DataviewInlineJSRenderer } from \"ui/views/js-view\";\nimport { currentLocale } from \"util/locale\";\nimport { DateTime } from \"luxon\";\nimport { DataviewInlineApi } from \"api/inline-api\";\nimport { replaceInlineFields } from \"ui/views/inline-field\";\nimport {\n    inlineFieldsField,\n    replaceInlineFieldsInLivePreview,\n    workspaceLayoutChangeEffect,\n} from \"./ui/views/inline-field-live-preview\";\nimport { DataviewInit } from \"ui/markdown\";\nimport { inlinePlugin } from \"./ui/lp-render\";\nimport { Extension } from \"@codemirror/state\";\n\nexport default class DataviewPlugin extends Plugin {\n    /** Plugin-wide default settings. */\n    public settings: DataviewSettings;\n\n    /** The index that stores all dataview data. */\n    public index: FullIndex;\n    /** External-facing plugin API. */\n    public api: DataviewApi;\n\n    /** CodeMirror 6 extensions that dataview installs. Tracked via array to allow for dynamic updates. */\n    private cmExtension: Extension[];\n\n    async onload() {\n        // Settings initialization; write defaults first time around.\n        this.settings = Object.assign(DEFAULT_SETTINGS, (await this.loadData()) ?? {});\n        this.addSettingTab(new GeneralSettingsTab(this.app, this));\n\n        this.index = this.addChild(\n            FullIndex.create(this.app, this.manifest.version, () => {\n                if (this.settings.refreshEnabled) this.debouncedRefresh();\n            })\n        );\n\n        // Set up automatic (intelligent) view refreshing that debounces.\n        this.updateRefreshSettings();\n\n        // From this point onwards the dataview API is fully functional (even if the index needs to do some background indexing).\n        this.api = new DataviewApi(this.app, this.index, this.settings, this.manifest.version);\n\n        // Register API to global window object.\n        (window[\"DataviewAPI\"] = this.api) && this.register(() => delete window[\"DataviewAPI\"]);\n\n        // Dataview query language code blocks.\n        this.registerPriorityCodeblockPostProcessor(\"dataview\", -100, async (source: string, el, ctx) =>\n            this.dataview(source, el, ctx, ctx.sourcePath)\n        );\n\n        // DataviewJS codeblocks.\n        this.registerPriorityCodeblockPostProcessor(\n            this.settings.dataviewJsKeyword,\n            -100,\n            async (source: string, el, ctx) => this.dataviewjs(source, el, ctx, ctx.sourcePath)\n        );\n\n        // Dataview inline queries.\n        this.registerPriorityMarkdownPostProcessor(-100, async (el, ctx) => {\n            // Allow for turning off inline queries.\n            if (!this.settings.enableInlineDataview || isDataviewDisabled(ctx.sourcePath)) return;\n\n            this.dataviewInline(el, ctx, ctx.sourcePath);\n        });\n\n        // Dataview inline-inline query fancy rendering. Runs at a low priority; should apply to Dataview views.\n        this.registerPriorityMarkdownPostProcessor(100, async (el, ctx) => {\n            // Allow for lame people to disable the pretty rendering.\n            if (!this.settings.prettyRenderInlineFields || isDataviewDisabled(ctx.sourcePath)) return;\n\n            // Handle p, header elements explicitly (opt-in rather than opt-out for now).\n            for (let p of el.findAllSelf(\"p,h1,h2,h3,h4,h5,h6,li,span,th,td\")) {\n                const init: DataviewInit = {\n                    app: this.app,\n                    index: this.index,\n                    settings: this.settings,\n                    container: p,\n                };\n\n                await replaceInlineFields(ctx, init);\n            }\n        });\n\n        // editor extensions\n        this.cmExtension = [];\n        this.registerEditorExtension(this.cmExtension);\n        this.updateEditorExtensions();\n\n        // Dataview \"force refresh\" operation.\n        this.addCommand({\n            id: \"dataview-force-refresh-views\",\n            name: \"Force refresh all views and blocks\",\n            callback: () => {\n                this.index.revision += 1;\n                this.app.workspace.trigger(\"dataview:refresh-views\");\n            },\n        });\n\n        this.addCommand({\n            id: \"dataview-drop-cache\",\n            name: \"Drop all cached file metadata\",\n            callback: () => {\n                this.index.reinitialize();\n            },\n        });\n\n        interface WorkspaceLeafRebuild extends WorkspaceLeaf {\n            rebuildView(): void;\n        }\n\n        this.addCommand({\n            id: \"dataview-rebuild-current-view\",\n            name: \"Rebuild current view\",\n            callback: () => {\n                const activeView: MarkdownView | null = this.app.workspace.getActiveViewOfType(MarkdownView);\n                if (activeView) {\n                    (activeView.leaf as WorkspaceLeafRebuild).rebuildView();\n                }\n            },\n        });\n\n        // Run index initialization, which actually traverses the vault to index files.\n        if (!this.app.workspace.layoutReady) {\n            this.app.workspace.onLayoutReady(async () => this.index.initialize());\n        } else {\n            this.index.initialize();\n        }\n\n        // Not required anymore, though holding onto it for backwards-compatibility.\n        this.app.metadataCache.trigger(\"dataview:api-ready\", this.api);\n        console.log(`Dataview: version ${this.manifest.version} (requires obsidian ${this.manifest.minAppVersion})`);\n\n        // Mainly intended to detect when the user switches between live preview and source mode.\n        this.registerEvent(\n            this.app.workspace.on(\"layout-change\", () => {\n                this.app.workspace.iterateAllLeaves(leaf => {\n                    if (leaf.view instanceof MarkdownView && leaf.view.editor.cm) {\n                        leaf.view.editor.cm.dispatch({\n                            effects: workspaceLayoutChangeEffect.of(null),\n                        });\n                    }\n                });\n            })\n        );\n\n        this.registerDataviewjsCodeHighlighting();\n        this.register(() => this.unregisterDataviewjsCodeHighlighting());\n    }\n\n    public registerDataviewjsCodeHighlighting(): void {\n        window.CodeMirror.defineMode(this.settings.dataviewJsKeyword, config =>\n            window.CodeMirror.getMode(config, \"javascript\")\n        );\n    }\n\n    public unregisterDataviewjsCodeHighlighting(): void {\n        window.CodeMirror.defineMode(this.settings.dataviewJsKeyword, config =>\n            window.CodeMirror.getMode(config, \"null\")\n        );\n    }\n\n    private debouncedRefresh: () => void = () => null;\n\n    private updateRefreshSettings() {\n        this.debouncedRefresh = debounce(\n            () => this.app.workspace.trigger(\"dataview:refresh-views\"),\n            this.settings.refreshInterval,\n            true\n        );\n    }\n\n    public onunload() {\n        console.log(`Dataview: version ${this.manifest.version} unloaded.`);\n    }\n\n    /** Register a markdown post processor with the given priority. */\n    public registerPriorityMarkdownPostProcessor(\n        priority: number,\n        processor: (el: HTMLElement, ctx: MarkdownPostProcessorContext) => Promise<void>\n    ) {\n        let registered = this.registerMarkdownPostProcessor(processor);\n        registered.sortOrder = priority;\n    }\n\n    /** Register a markdown codeblock post processor with the given priority. */\n    public registerPriorityCodeblockPostProcessor(\n        language: string,\n        priority: number,\n        processor: (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => Promise<void>\n    ) {\n        let registered = this.registerMarkdownCodeBlockProcessor(language, processor);\n        registered.sortOrder = priority;\n    }\n\n    public updateEditorExtensions() {\n        // Don't create a new array, keep the same reference\n        this.cmExtension.length = 0;\n        // editor extension for inline queries: enabled regardless of settings (enableInlineDataview/enableInlineDataviewJS)\n        this.cmExtension.push(inlinePlugin(this.app, this.index, this.settings, this.api));\n        // editor extension for rendering inline fields in live preview\n        if (this.settings.prettyRenderInlineFieldsInLivePreview) {\n            this.cmExtension.push(inlineFieldsField, replaceInlineFieldsInLivePreview(this.app, this.settings));\n        }\n        this.app.workspace.updateOptions();\n    }\n\n    /**\n     * Based on the source, generate a dataview view. This works by doing an initial parsing pass, and then adding\n     * a long-lived view object to the given component for life-cycle management.\n     */\n    public async dataview(\n        source: string,\n        el: HTMLElement,\n        component: Component | MarkdownPostProcessorContext,\n        sourcePath: string\n    ) {\n        el.style.overflowX = \"auto\";\n        this.api.execute(source, el, component, sourcePath);\n    }\n\n    /** Generate a DataviewJS view running the given source in the given element. */\n    public async dataviewjs(\n        source: string,\n        el: HTMLElement,\n        component: Component | MarkdownPostProcessorContext,\n        sourcePath: string\n    ) {\n        el.style.overflowX = \"auto\";\n        this.api.executeJs(source, el, component, sourcePath);\n    }\n\n    /** Render all dataview inline expressions in the given element. */\n    public async dataviewInline(\n        el: HTMLElement,\n        component: Component | MarkdownPostProcessorContext,\n        sourcePath: string\n    ) {\n        if (isDataviewDisabled(sourcePath)) return;\n\n        // Search for <code> blocks inside this element; for each one, look for things of the form `= ...`.\n        let codeblocks = el.querySelectorAll(\"code\");\n        for (let index = 0; index < codeblocks.length; index++) {\n            let codeblock = codeblocks.item(index);\n\n            // Skip code inside of pre elements if not explicitly enabled.\n            if (\n                codeblock.parentElement &&\n                codeblock.parentElement.nodeName.toLowerCase() == \"pre\" &&\n                !this.settings.inlineQueriesInCodeblocks\n            )\n                continue;\n\n            let text = codeblock.innerText.trim();\n            if (this.settings.inlineJsQueryPrefix.length > 0 && text.startsWith(this.settings.inlineJsQueryPrefix)) {\n                let code = text.substring(this.settings.inlineJsQueryPrefix.length).trim();\n                if (code.length == 0) continue;\n\n                component.addChild(new DataviewInlineJSRenderer(this.api, code, el, codeblock, sourcePath));\n            } else if (this.settings.inlineQueryPrefix.length > 0 && text.startsWith(this.settings.inlineQueryPrefix)) {\n                let potentialField = text.substring(this.settings.inlineQueryPrefix.length).trim();\n                if (potentialField.length == 0) continue;\n\n                let field = tryOrPropagate(() => parseField(potentialField));\n                if (!field.successful) {\n                    let errorBlock = el.createEl(\"div\");\n                    renderErrorPre(errorBlock, `Dataview (inline field '${potentialField}'): ${field.error}`);\n                } else {\n                    let fieldValue = field.value;\n                    component.addChild(\n                        new DataviewInlineRenderer(\n                            fieldValue,\n                            text,\n                            el,\n                            codeblock,\n                            this.index,\n                            sourcePath,\n                            this.settings,\n                            this.app\n                        )\n                    );\n                }\n            }\n        }\n    }\n\n    /** Update plugin settings. */\n    async updateSettings(settings: Partial<DataviewSettings>) {\n        Object.assign(this.settings, settings);\n        this.updateRefreshSettings();\n        await this.saveData(this.settings);\n    }\n\n    /** @deprecated Call the given callback when the dataview API has initialized. */\n    public withApi(callback: (api: DataviewApi) => void) {\n        callback(this.api);\n    }\n\n    /**\n     * Create an API element localized to the given path, with lifecycle management managed by the given component.\n     * The API will output results to the given HTML element.\n     */\n    public localApi(path: string, component: Component, el: HTMLElement): DataviewInlineApi {\n        return new DataviewInlineApi(this.api, component, el, path);\n    }\n}\n\n/** All of the dataview settings in a single, nice tab. */\nclass GeneralSettingsTab extends PluginSettingTab {\n    constructor(app: App, private plugin: DataviewPlugin) {\n        super(app, plugin);\n    }\n\n    public display(): void {\n        this.containerEl.empty();\n\n        new Setting(this.containerEl)\n            .setName(\"Enable inline queries\")\n            .setDesc(\"Enable or disable executing regular inline Dataview queries.\")\n            .addToggle(toggle =>\n                toggle\n                    .setValue(this.plugin.settings.enableInlineDataview)\n                    .onChange(async value => await this.plugin.updateSettings({ enableInlineDataview: value }))\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"Enable JavaScript queries\")\n            .setDesc(\"Enable or disable executing DataviewJS queries.\")\n            .addToggle(toggle =>\n                toggle\n                    .setValue(this.plugin.settings.enableDataviewJs)\n                    .onChange(async value => await this.plugin.updateSettings({ enableDataviewJs: value }))\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"Enable inline JavaScript queries\")\n            .setDesc(\n                \"Enable or disable executing inline DataviewJS queries. Requires that DataviewJS queries are enabled.\"\n            )\n            .addToggle(toggle =>\n                toggle\n                    .setValue(this.plugin.settings.enableInlineDataviewJs)\n                    .onChange(async value => await this.plugin.updateSettings({ enableInlineDataviewJs: value }))\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"Enable inline field highlighting in reading view\")\n            .setDesc(\"Enables or disables visual highlighting / pretty rendering for inline fields in reading view.\")\n            .addToggle(toggle =>\n                toggle\n                    .setValue(this.plugin.settings.prettyRenderInlineFields)\n                    .onChange(async value => await this.plugin.updateSettings({ prettyRenderInlineFields: value }))\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"Enable inline field highlighting in Live Preview\")\n            .setDesc(\"Enables or disables visual highlighting / pretty rendering for inline fields in Live Preview.\")\n            .addToggle(toggle =>\n                toggle.setValue(this.plugin.settings.prettyRenderInlineFieldsInLivePreview).onChange(async value => {\n                    await this.plugin.updateSettings({ prettyRenderInlineFieldsInLivePreview: value });\n                    this.plugin.updateEditorExtensions();\n                })\n            );\n\n        new Setting(this.containerEl).setName(\"Codeblocks\").setHeading();\n\n        new Setting(this.containerEl)\n            .setName(\"DataviewJS keyword\")\n            .setDesc(\n                \"Keyword for DataviewJS blocks. Defaults to 'dataviewjs'. Reload required for changes to take effect.\"\n            )\n            .addText(text =>\n                text\n                    .setPlaceholder(\"dataviewjs\")\n                    .setValue(this.plugin.settings.dataviewJsKeyword)\n                    .onChange(async value => {\n                        if (value.length == 0) return;\n                        this.plugin.unregisterDataviewjsCodeHighlighting();\n                        await this.plugin.updateSettings({ dataviewJsKeyword: value });\n                        this.plugin.registerDataviewjsCodeHighlighting();\n                    })\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"Inline query prefix\")\n            .setDesc(\"The prefix to inline queries (to mark them as Dataview queries). Defaults to '='.\")\n            .addText(text =>\n                text\n                    .setPlaceholder(\"=\")\n                    .setValue(this.plugin.settings.inlineQueryPrefix)\n                    .onChange(async value => {\n                        if (value.length == 0) return;\n\n                        await this.plugin.updateSettings({ inlineQueryPrefix: value });\n                    })\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"JavaScript inline query prefix\")\n            .setDesc(\"The prefix to JavaScript inline queries (to mark them as DataviewJS queries). Defaults to '$='.\")\n            .addText(text =>\n                text\n                    .setPlaceholder(\"$=\")\n                    .setValue(this.plugin.settings.inlineJsQueryPrefix)\n                    .onChange(async value => {\n                        if (value.length == 0) return;\n\n                        await this.plugin.updateSettings({ inlineJsQueryPrefix: value });\n                    })\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"Code block inline queries\")\n            .setDesc(\"If enabled, inline queries will also be evaluated inside full code blocks.\")\n            .addToggle(toggle =>\n                toggle\n                    .setValue(this.plugin.settings.inlineQueriesInCodeblocks)\n                    .onChange(async value => await this.plugin.updateSettings({ inlineQueriesInCodeblocks: value }))\n            );\n\n        new Setting(this.containerEl).setName(\"View\").setHeading();\n\n        new Setting(this.containerEl)\n            .setName(\"Display result count\")\n            .setDesc(\"If toggled off, the small number in the result header of TASK and TABLE queries will be hidden.\")\n            .addToggle(toggle =>\n                toggle.setValue(this.plugin.settings.showResultCount).onChange(async value => {\n                    await this.plugin.updateSettings({ showResultCount: value });\n                    this.plugin.index.touch();\n                })\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"Warn on empty result\")\n            .setDesc(\"If set, queries which return 0 results will render a warning message.\")\n            .addToggle(toggle =>\n                toggle.setValue(this.plugin.settings.warnOnEmptyResult).onChange(async value => {\n                    await this.plugin.updateSettings({ warnOnEmptyResult: value });\n                    this.plugin.index.touch();\n                })\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"Render null as\")\n            .setDesc(\"What null/non-existent should show up as in tables, by default. This supports Markdown notation.\")\n            .addText(text =>\n                text\n                    .setPlaceholder(\"-\")\n                    .setValue(this.plugin.settings.renderNullAs)\n                    .onChange(async value => {\n                        await this.plugin.updateSettings({ renderNullAs: value });\n                        this.plugin.index.touch();\n                    })\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"Automatic view refreshing\")\n            .setDesc(\n                \"If enabled, views will automatically refresh when files in your vault change; this can negatively affect\" +\n                    \" some functionality like embeds in views, so turn it off if such functionality is not working.\"\n            )\n            .addToggle(toggle =>\n                toggle.setValue(this.plugin.settings.refreshEnabled).onChange(async value => {\n                    await this.plugin.updateSettings({ refreshEnabled: value });\n                    this.plugin.index.touch();\n                })\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"Refresh interval\")\n            .setDesc(\"How long to wait (in milliseconds) for files to stop changing before updating views.\")\n            .addText(text =>\n                text\n                    .setPlaceholder(\"500\")\n                    .setValue(\"\" + this.plugin.settings.refreshInterval)\n                    .onChange(async value => {\n                        let parsed = parseInt(value);\n                        if (isNaN(parsed)) return;\n                        parsed = parsed < 100 ? 100 : parsed;\n                        await this.plugin.updateSettings({ refreshInterval: parsed });\n                    })\n            );\n\n        let dformat = new Setting(this.containerEl)\n            .setName(\"Date format\")\n            .setDesc(\n                \"The default date format (see Luxon date format options).\" +\n                    \" Currently: \" +\n                    DateTime.now().toFormat(this.plugin.settings.defaultDateFormat, { locale: currentLocale() })\n            )\n            .addText(text =>\n                text\n                    .setPlaceholder(DEFAULT_QUERY_SETTINGS.defaultDateFormat)\n                    .setValue(this.plugin.settings.defaultDateFormat)\n                    .onChange(async value => {\n                        dformat.setDesc(\n                            \"The default date format (see Luxon date format options).\" +\n                                \" Currently: \" +\n                                DateTime.now().toFormat(value, { locale: currentLocale() })\n                        );\n                        await this.plugin.updateSettings({ defaultDateFormat: value });\n\n                        this.plugin.index.touch();\n                    })\n            );\n\n        let dtformat = new Setting(this.containerEl)\n            .setName(\"Date + time format\")\n            .setDesc(\n                \"The default date and time format (see Luxon date format options).\" +\n                    \" Currently: \" +\n                    DateTime.now().toFormat(this.plugin.settings.defaultDateTimeFormat, { locale: currentLocale() })\n            )\n            .addText(text =>\n                text\n                    .setPlaceholder(DEFAULT_QUERY_SETTINGS.defaultDateTimeFormat)\n                    .setValue(this.plugin.settings.defaultDateTimeFormat)\n                    .onChange(async value => {\n                        dtformat.setDesc(\n                            \"The default date and time format (see Luxon date format options).\" +\n                                \" Currently: \" +\n                                DateTime.now().toFormat(value, { locale: currentLocale() })\n                        );\n                        await this.plugin.updateSettings({ defaultDateTimeFormat: value });\n\n                        this.plugin.index.touch();\n                    })\n            );\n\n        new Setting(this.containerEl).setName(\"Tables\").setHeading();\n\n        new Setting(this.containerEl)\n            .setName(\"Primary column name\")\n            .setDesc(\n                \"The name of the default ID column in tables; this is the auto-generated first column that links to the source file.\"\n            )\n            .addText(text =>\n                text\n                    .setPlaceholder(\"File\")\n                    .setValue(this.plugin.settings.tableIdColumnName)\n                    .onChange(async value => {\n                        await this.plugin.updateSettings({ tableIdColumnName: value });\n                        this.plugin.index.touch();\n                    })\n            );\n\n        new Setting(this.containerEl)\n            .setName(\"Grouped column name\")\n            .setDesc(\n                \"The name of the default ID column in tables, when the table is on grouped data; this is the auto-generated first column\" +\n                    \"that links to the source file/group.\"\n            )\n            .addText(text =>\n                text\n                    .setPlaceholder(\"Group\")\n                    .setValue(this.plugin.settings.tableGroupColumnName)\n                    .onChange(async value => {\n                        await this.plugin.updateSettings({ tableGroupColumnName: value });\n                        this.plugin.index.touch();\n                    })\n            );\n\n        new Setting(this.containerEl).setName(\"Tasks\").setHeading();\n\n        let taskCompletionSubsettingsEnabled = this.plugin.settings.taskCompletionTracking;\n        let taskCompletionInlineSubsettingsEnabled =\n            taskCompletionSubsettingsEnabled && !this.plugin.settings.taskCompletionUseEmojiShorthand;\n\n        new Setting(this.containerEl)\n            .setName(\"Automatic task completion tracking\")\n            .setDesc(\n                createFragment(el => {\n                    el.appendText(\n                        \"If enabled, Dataview will automatically append tasks with their completion date when they are checked in Dataview views.\"\n                    );\n                    el.createEl(\"br\");\n                    el.appendText(\n                        \"Example with default field name and date format: - [x] my task [completion:: 2022-01-01]\"\n                    );\n                })\n            )\n            .addToggle(toggle =>\n                toggle.setValue(this.plugin.settings.taskCompletionTracking).onChange(async value => {\n                    await this.plugin.updateSettings({ taskCompletionTracking: value });\n                    taskCompletionSubsettingsEnabled = value;\n                    this.display();\n                })\n            );\n\n        let taskEmojiShorthand = new Setting(this.containerEl)\n            .setName(\"Use emoji shorthand for completion\")\n            .setDisabled(!taskCompletionSubsettingsEnabled);\n        if (taskCompletionSubsettingsEnabled)\n            taskEmojiShorthand\n                .setDesc(\n                    createFragment(el => {\n                        el.appendText(\n                            'If enabled, will use emoji shorthand instead of inline field formatting to fill out implicit task field \"completion\".'\n                        );\n                        el.createEl(\"br\");\n                        el.appendText(\"Example: - [x] my task ✅ 2022-01-01\");\n                        el.createEl(\"br\");\n                        el.appendText(\n                            \"Disable this to customize the completion date format or field name, or to use Dataview inline field formatting.\"\n                        );\n                        el.createEl(\"br\");\n                        el.appendText('Only available when \"automatic task completion tracking\" is enabled.');\n                    })\n                )\n                .addToggle(toggle =>\n                    toggle.setValue(this.plugin.settings.taskCompletionUseEmojiShorthand).onChange(async value => {\n                        await this.plugin.updateSettings({ taskCompletionUseEmojiShorthand: value });\n                        taskCompletionInlineSubsettingsEnabled = taskCompletionSubsettingsEnabled && !value;\n                        this.display();\n                    })\n                );\n        else taskEmojiShorthand.setDesc('Only available when \"automatic task completion tracking\" is enabled.');\n\n        let taskFieldName = new Setting(this.containerEl)\n            .setName(\"Completion field name\")\n            .setDisabled(!taskCompletionInlineSubsettingsEnabled);\n        if (taskCompletionInlineSubsettingsEnabled)\n            taskFieldName\n                .setDesc(\n                    createFragment(el => {\n                        el.appendText(\n                            \"Text used as inline field key for task completion date when toggling a task's checkbox in a Dataview view.\"\n                        );\n                        el.createEl(\"br\");\n                        el.appendText(\n                            'Only available when \"automatic task completion tracking\" is enabled and \"use emoji shorthand for completion\" is disabled.'\n                        );\n                    })\n                )\n                .addText(text =>\n                    text.setValue(this.plugin.settings.taskCompletionText).onChange(async value => {\n                        await this.plugin.updateSettings({ taskCompletionText: value.trim() });\n                    })\n                );\n        else\n            taskFieldName.setDesc(\n                'Only available when \"automatic task completion tracking\" is enabled and \"use emoji shorthand for completion\" is disabled.'\n            );\n\n        let taskDtFormat = new Setting(this.containerEl)\n            .setName(\"Completion date format\")\n            .setDisabled(!taskCompletionInlineSubsettingsEnabled);\n        if (taskCompletionInlineSubsettingsEnabled) {\n            let descTextLines = [\n                \"Date-time format for task completion date when toggling a task's checkbox in a Dataview view (see Luxon date format options).\",\n                'Only available when \"automatic task completion tracking\" is enabled and \"use emoji shorthand for completion\" is disabled.',\n                \"Currently: \",\n            ];\n            taskDtFormat\n                .setDesc(\n                    createFragment(el => {\n                        el.appendText(descTextLines[0]);\n                        el.createEl(\"br\");\n                        el.appendText(descTextLines[1]);\n                        el.createEl(\"br\");\n                        el.appendText(\n                            descTextLines[2] +\n                                DateTime.now().toFormat(this.plugin.settings.taskCompletionDateFormat, {\n                                    locale: currentLocale(),\n                                })\n                        );\n                    })\n                )\n                .addText(text =>\n                    text\n                        .setPlaceholder(DEFAULT_SETTINGS.taskCompletionDateFormat)\n                        .setValue(this.plugin.settings.taskCompletionDateFormat)\n                        .onChange(async value => {\n                            taskDtFormat.setDesc(\n                                createFragment(el => {\n                                    el.appendText(descTextLines[0]);\n                                    el.createEl(\"br\");\n                                    el.appendText(descTextLines[1]);\n                                    el.createEl(\"br\");\n                                    el.appendText(\n                                        descTextLines[2] +\n                                            DateTime.now().toFormat(value.trim(), { locale: currentLocale() })\n                                    );\n                                })\n                            );\n                            await this.plugin.updateSettings({ taskCompletionDateFormat: value.trim() });\n                            this.plugin.index.touch();\n                        })\n                );\n        } else {\n            taskDtFormat.setDesc(\n                'Only available when \"automatic task completion tracking\" is enabled and \"use emoji shorthand for completion\" is disabled.'\n            );\n        }\n        new Setting(this.containerEl)\n            .setName(\"Recursive sub-task completion\")\n            // I gotta word this better :/\n            .setDesc(\"If enabled, completing a task in a Dataview will automatically complete its subtasks too.\")\n            .addToggle(toggle =>\n                toggle\n                    .setValue(this.plugin.settings.recursiveSubTaskCompletion)\n                    .onChange(async value => await this.plugin.updateSettings({ recursiveSubTaskCompletion: value }))\n            );\n    }\n}\n"
  },
  {
    "path": "src/query/engine.ts",
    "content": "/**\n * Takes a full query and a set of indices, and (hopefully quickly) returns all relevant files.\n */\nimport { FullIndex } from \"data-index/index\";\nimport { Context, LinkHandler } from \"expression/context\";\nimport { resolveSource, Datarow, matchingSourcePaths } from \"data-index/resolver\";\nimport { DataObject, Link, Literal, Values, Grouping, Widgets } from \"data-model/value\";\nimport { CalendarQuery, ListQuery, Query, QueryOperation, TableQuery } from \"query/query\";\nimport { Result } from \"api/result\";\nimport { Field, Fields } from \"expression/field\";\nimport { QuerySettings } from \"settings\";\nimport { DateTime } from \"luxon\";\nimport { SListItem } from \"data-model/serialized/markdown\";\n\nfunction iden<T>(x: T): T {\n    return x;\n}\n\n/** Operation diagnostics collected during the execution of each query step. */\nexport interface OperationDiagnostics {\n    timeMs: number;\n    incomingRows: number;\n    outgoingRows: number;\n    errors: { index: number; message: string }[];\n}\n\n/** The meaning of the 'id' field for a data row - i.e., where it came from. */\nexport type IdentifierMeaning = { type: \"group\"; name: string; on: IdentifierMeaning } | { type: \"path\" };\n\n/** A data row over an object. */\nexport type Pagerow = Datarow<DataObject>;\n/** An error during execution. */\nexport type ExecutionError = { index: number; message: string };\n\n/** The result of executing query operations over incoming data rows; includes timing and error information. */\nexport interface CoreExecution {\n    data: Pagerow[];\n    idMeaning: IdentifierMeaning;\n    timeMs: number;\n    ops: QueryOperation[];\n    diagnostics: OperationDiagnostics[];\n}\n\n/** Shared execution code which just takes in arbitrary data, runs operations over it, and returns it + per-row errors. */\nexport function executeCore(rows: Pagerow[], context: Context, ops: QueryOperation[]): Result<CoreExecution, string> {\n    let diagnostics = [];\n    let identMeaning: IdentifierMeaning = { type: \"path\" };\n    let startTime = Date.now();\n\n    for (let op of ops) {\n        let opStartTime = Date.now();\n        let incomingRows = rows.length;\n        let errors: { index: number; message: string }[] = [];\n\n        switch (op.type) {\n            case \"where\":\n                let whereResult: Pagerow[] = [];\n                for (let index = 0; index < rows.length; index++) {\n                    let row = rows[index];\n                    let value = context.evaluate(op.clause, row.data);\n                    if (!value.successful) errors.push({ index, message: value.error });\n                    else if (Values.isTruthy(value.value)) whereResult.push(row);\n                }\n\n                rows = whereResult;\n                break;\n            case \"sort\":\n                let sortFields = op.fields;\n                let taggedData: { data: Pagerow; fields: Literal[] }[] = [];\n                outer: for (let index = 0; index < rows.length; index++) {\n                    let row = rows[index];\n                    let rowSorts: Literal[] = [];\n                    for (let sIndex = 0; sIndex < sortFields.length; sIndex++) {\n                        let value = context.evaluate(sortFields[sIndex].field, row.data);\n                        if (!value.successful) {\n                            errors.push({ index, message: value.error });\n                            continue outer;\n                        }\n\n                        rowSorts.push(value.value);\n                    }\n\n                    taggedData.push({ data: row, fields: rowSorts });\n                }\n\n                // Sort rows by the sort fields, and then return the finished result.\n                taggedData.sort((a, b) => {\n                    for (let index = 0; index < sortFields.length; index++) {\n                        let factor = sortFields[index].direction === \"ascending\" ? 1 : -1;\n                        let le = context.binaryOps\n                            .evaluate(\"<\", a.fields[index], b.fields[index], context)\n                            .orElse(false);\n                        if (Values.isTruthy(le)) return factor * -1;\n\n                        let ge = context.binaryOps\n                            .evaluate(\">\", a.fields[index], b.fields[index], context)\n                            .orElse(false);\n                        if (Values.isTruthy(ge)) return factor * 1;\n                    }\n\n                    return 0;\n                });\n\n                rows = taggedData.map(v => v.data);\n                break;\n            case \"limit\":\n                let limiting = context.evaluate(op.amount);\n                if (!limiting.successful)\n                    return Result.failure(\"Failed to execute 'limit' statement: \" + limiting.error);\n                if (!Values.isNumber(limiting.value))\n                    return Result.failure(\n                        `Failed to execute 'limit' statement: limit should be a number, but got '${Values.typeOf(\n                            limiting.value\n                        )}' (${limiting.value})`\n                    );\n\n                rows = rows.slice(0, limiting.value);\n                break;\n            case \"group\":\n                let groupData: { data: Pagerow; key: Literal }[] = [];\n                for (let index = 0; index < rows.length; index++) {\n                    let value = context.evaluate(op.field.field, rows[index].data);\n                    if (!value.successful) {\n                        errors.push({ index, message: value.error });\n                        continue;\n                    }\n\n                    groupData.push({ data: rows[index], key: value.value });\n                }\n\n                // Sort by the key, which we will group on shortly.\n                groupData.sort((a, b) => {\n                    let le = context.binaryOps.evaluate(\"<\", a.key, b.key, context).orElse(false);\n                    if (Values.isTruthy(le)) return -1;\n\n                    let ge = context.binaryOps.evaluate(\">\", a.key, b.key, context).orElse(false);\n                    if (Values.isTruthy(ge)) return 1;\n\n                    return 0;\n                });\n\n                // Then walk through and find fields that are equal.\n                let finalGroupData: { key: Literal; rows: DataObject[]; [groupKey: string]: Literal }[] = [];\n                if (groupData.length > 0)\n                    finalGroupData.push({\n                        key: groupData[0].key,\n                        rows: [groupData[0].data.data],\n                        [op.field.name]: groupData[0].key,\n                    });\n\n                for (let index = 1; index < groupData.length; index++) {\n                    let curr = groupData[index],\n                        prev = groupData[index - 1];\n                    if (context.binaryOps.evaluate(\"=\", curr.key, prev.key, context).orElse(false)) {\n                        finalGroupData[finalGroupData.length - 1].rows.push(curr.data.data);\n                    } else {\n                        finalGroupData.push({\n                            key: curr.key,\n                            rows: [curr.data.data],\n                            [op.field.name]: curr.key,\n                        });\n                    }\n                }\n\n                rows = finalGroupData.map(d => {\n                    return { id: d.key, data: d };\n                });\n                identMeaning = { type: \"group\", name: op.field.name, on: identMeaning };\n                break;\n            case \"flatten\":\n                let flattenResult: Pagerow[] = [];\n                for (let index = 0; index < rows.length; index++) {\n                    let row = rows[index];\n                    let value = context.evaluate(op.field.field, row.data);\n                    if (!value.successful) {\n                        errors.push({ index, message: value.error });\n                        continue;\n                    }\n\n                    let datapoints = Values.isArray(value.value) ? value.value : [value.value];\n                    for (let v of datapoints) {\n                        let copy = Values.deepCopy(row);\n                        copy.data[op.field.name] = v;\n                        flattenResult.push(copy);\n                    }\n                }\n\n                rows = flattenResult;\n                if (identMeaning.type == \"group\" && identMeaning.name == op.field.name) identMeaning = identMeaning.on;\n                break;\n            default:\n                return Result.failure(\"Unrecognized query operation '\" + op.type + \"'\");\n        }\n\n        if (errors.length >= incomingRows && incomingRows > 0) {\n            return Result.failure(`Every row during operation '${op.type}' failed with an error; first ${Math.min(\n                3,\n                errors.length\n            )}:\\n\n                ${errors\n                    .slice(0, 3)\n                    .map(d => \"- \" + d.message)\n                    .join(\"\\n\")}`);\n        }\n\n        diagnostics.push({\n            incomingRows,\n            errors,\n            outgoingRows: rows.length,\n            timeMs: Date.now() - opStartTime,\n        });\n    }\n\n    return Result.success({\n        data: rows,\n        idMeaning: identMeaning,\n        ops,\n        diagnostics,\n        timeMs: Date.now() - startTime,\n    });\n}\n\n/** Expanded version of executeCore which adds an additional \"extraction\" step to the pipeline. */\nexport function executeCoreExtract(\n    rows: Pagerow[],\n    context: Context,\n    ops: QueryOperation[],\n    fields: Record<string, Field>\n): Result<CoreExecution, string> {\n    let internal = executeCore(rows, context, ops);\n    if (!internal.successful) return internal;\n\n    let core = internal.value;\n    let startTime = Date.now();\n    let errors: ExecutionError[] = [];\n    let res: Pagerow[] = [];\n\n    outer: for (let index = 0; index < core.data.length; index++) {\n        let page: Pagerow = { id: core.data[index].id, data: {} };\n        for (let [name, field] of Object.entries(fields)) {\n            let value = context.evaluate(field, core.data[index].data);\n            if (!value.successful) {\n                errors.push({ index: index, message: value.error });\n                continue outer;\n            }\n\n            page.data[name] = value.value;\n        }\n        res.push(page);\n    }\n\n    if (errors.length >= core.data.length && core.data.length > 0) {\n        return Result.failure(`Every row during final data extraction failed with an error; first ${Math.max(\n            errors.length,\n            3\n        )}:\\n\n            ${errors\n                .slice(0, 3)\n                .map(d => \"- \" + d.message)\n                .join(\"\\n\")}`);\n    }\n\n    let execTime = Date.now() - startTime;\n    return Result.success({\n        data: res,\n        idMeaning: core.idMeaning,\n        diagnostics: core.diagnostics.concat([\n            {\n                timeMs: execTime,\n                incomingRows: core.data.length,\n                outgoingRows: res.length,\n                errors,\n            },\n        ]),\n        ops: core.ops.concat([{ type: \"extract\", fields }]),\n        timeMs: core.timeMs + execTime,\n    });\n}\n\nexport interface ListExecution {\n    core: CoreExecution;\n    data: Literal[];\n    primaryMeaning: IdentifierMeaning;\n}\n\n/** Execute a list-based query, returning the final results. */\nexport async function executeList(\n    query: Query,\n    index: FullIndex,\n    origin: string,\n    settings: QuerySettings\n): Promise<Result<ListExecution, string>> {\n    // Start by collecting all of the files that match the 'from' queries.\n    let fileset = await resolveSource(query.source, index, origin);\n    if (!fileset.successful) return Result.failure(fileset.error);\n\n    // Extract information about the origin page to add to the root context.\n    let rootContext = new Context(defaultLinkHandler(index, origin), settings, {\n        this: index.pages.get(origin)?.serialize(index) ?? {},\n    });\n\n    let targetField = (query.header as ListQuery).format;\n    let showId = (query.header as ListQuery).showId;\n    let fields: Record<string, Field> = targetField ? { target: targetField } : {};\n\n    return executeCoreExtract(fileset.value, rootContext, query.operations, fields).map(core => {\n        let data: Literal[];\n        if (showId && targetField) {\n            data = core.data.map(p => Widgets.listPair(p.id, p.data[\"target\"] ?? null));\n        } else if (targetField) {\n            data = core.data.map(p => p.data[\"target\"] ?? null);\n        } else {\n            data = core.data.map(p => p.id);\n        }\n\n        return { primaryMeaning: core.idMeaning, core, data };\n    });\n}\n\n/** Result of executing a table query. */\nexport interface TableExecution {\n    core: CoreExecution;\n    names: string[];\n    data: Literal[][];\n    idMeaning: IdentifierMeaning;\n}\n\n/** Execute a table query. */\nexport async function executeTable(\n    query: Query,\n    index: FullIndex,\n    origin: string,\n    settings: QuerySettings\n): Promise<Result<TableExecution, string>> {\n    // Start by collecting all of the files that match the 'from' queries.\n    let fileset = await resolveSource(query.source, index, origin);\n    if (!fileset.successful) return Result.failure(fileset.error);\n\n    // Extract information about the origin page to add to the root context.\n    let rootContext = new Context(defaultLinkHandler(index, origin), settings, {\n        this: index.pages.get(origin)?.serialize(index) ?? {},\n    });\n\n    let targetFields = (query.header as TableQuery).fields;\n    let showId = (query.header as TableQuery).showId;\n    let fields: Record<string, Field> = {};\n    for (let field of targetFields) fields[field.name] = field.field;\n\n    return executeCoreExtract(fileset.value, rootContext, query.operations, fields).map(core => {\n        if (showId) {\n            const idName = core.idMeaning.type === \"group\" ? core.idMeaning.name : settings.tableIdColumnName;\n            let names = [idName].concat(targetFields.map(f => f.name));\n\n            let data = core.data.map(p => ([p.id] as Literal[]).concat(targetFields.map(f => p.data[f.name])));\n            return { core, names, data, idMeaning: core.idMeaning };\n        } else {\n            let names = targetFields.map(f => f.name);\n\n            let data = core.data.map(p => targetFields.map(f => p.data[f.name]));\n            return { core, names, data, idMeaning: core.idMeaning };\n        }\n    });\n}\n\n/** The result of executing a task query. */\nexport interface TaskExecution {\n    core: CoreExecution;\n    tasks: Grouping<SListItem>;\n}\n\n/** Maps a raw core execution result to a task grouping which is much easier to render. */\nfunction extractTaskGroupings(id: IdentifierMeaning, rows: DataObject[]): Grouping<SListItem> {\n    switch (id.type) {\n        case \"path\":\n            return rows as SListItem[];\n        case \"group\":\n            let key = id.name;\n            return rows.map(r =>\n                iden({\n                    key: r[key],\n                    rows: extractTaskGroupings(id.on, r.rows as DataObject[]),\n                })\n            );\n    }\n}\n\n/** Execute a task query, returning all matching tasks. */\nexport async function executeTask(\n    query: Query,\n    origin: string,\n    index: FullIndex,\n    settings: QuerySettings\n): Promise<Result<TaskExecution, string>> {\n    let fileset = matchingSourcePaths(query.source, index, origin);\n    if (!fileset.successful) return Result.failure(fileset.error);\n\n    // Collect tasks from pages which match.\n    let incomingTasks: Pagerow[] = [];\n    for (let path of fileset.value) {\n        let page = index.pages.get(path);\n        if (!page) continue;\n\n        let pageData = page.serialize(index);\n        let pageTasks = pageData.file.tasks.map(t => {\n            const tcopy = Values.deepCopy(t);\n\n            // Add page data to this copy.\n            for (let [key, value] of Object.entries(pageData)) {\n                if (key in tcopy) continue;\n                tcopy[key] = value;\n            }\n\n            return { id: `${pageData.path}#${t.line}`, data: tcopy };\n        });\n\n        for (let task of pageTasks) incomingTasks.push(task);\n    }\n\n    // Extract information about the origin page to add to the root context.\n    let rootContext = new Context(defaultLinkHandler(index, origin), settings, {\n        this: index.pages.get(origin)?.serialize(index) ?? {},\n    });\n\n    return executeCore(incomingTasks, rootContext, query.operations).map(core => {\n        return {\n            core,\n            tasks: extractTaskGroupings(\n                core.idMeaning,\n                core.data.map(r => r.data)\n            ),\n        };\n    });\n}\n\n/** Execute a single field inline a file, returning the evaluated result. */\nexport function executeInline(\n    field: Field,\n    origin: string,\n    index: FullIndex,\n    settings: QuerySettings\n): Result<Literal, string> {\n    return new Context(defaultLinkHandler(index, origin), settings, {\n        this: index.pages.get(origin)?.serialize(index) ?? {},\n    }).evaluate(field);\n}\n\n/** The default link resolver used when creating contexts. */\nexport function defaultLinkHandler(index: FullIndex, origin: string): LinkHandler {\n    return {\n        resolve: link => {\n            let realFile = index.metadataCache.getFirstLinkpathDest(link, origin);\n            if (!realFile) return null;\n\n            let realPage = index.pages.get(realFile.path);\n            if (!realPage) return null;\n\n            return realPage.serialize(index);\n        },\n        normalize: link => {\n            let realFile = index.metadataCache.getFirstLinkpathDest(link, origin);\n            return realFile?.path ?? link;\n        },\n        exists: link => {\n            let realFile = index.metadataCache.getFirstLinkpathDest(link, origin);\n            return !!realFile;\n        },\n    };\n}\n\n/** Execute a calendar-based query, returning the final results. */\nexport async function executeCalendar(\n    query: Query,\n    index: FullIndex,\n    origin: string,\n    settings: QuerySettings\n): Promise<Result<CalendarExecution, string>> {\n    // Start by collecting all of the files that match the 'from' queries.\n    let fileset = await resolveSource(query.source, index, origin);\n    if (!fileset.successful) return Result.failure(fileset.error);\n\n    // Extract information about the origin page to add to the root context.\n    let rootContext = new Context(defaultLinkHandler(index, origin), settings, {\n        this: index.pages.get(origin)?.serialize(index) ?? {},\n    });\n\n    let targetField = (query.header as CalendarQuery).field.field;\n    let fields: Record<string, Field> = {\n        target: targetField,\n        link: Fields.indexVariable(\"file.link\"),\n    };\n\n    return executeCoreExtract(fileset.value, rootContext, query.operations, fields).map(core => {\n        let data = core.data.map(p =>\n            iden({\n                date: p.data[\"target\"] as DateTime,\n                link: p.data[\"link\"] as Link,\n            })\n        );\n\n        return { core, data };\n    });\n}\n\nexport interface CalendarExecution {\n    core: CoreExecution;\n    data: { date: DateTime; link: Link; value?: Literal[] }[];\n}\n"
  },
  {
    "path": "src/query/parse.ts",
    "content": "import { EXPRESSION } from \"expression/parse\";\nimport * as P from \"parsimmon\";\nimport {\n    FlattenStep,\n    GroupStep,\n    LimitStep,\n    NamedField,\n    Query,\n    QueryFields,\n    QueryHeader,\n    QueryOperation,\n    QuerySortBy,\n    QueryType,\n    SortByStep,\n    WhereStep,\n    Comment,\n} from \"./query\";\nimport { Source, Sources } from \"data-index/source\";\nimport { DEFAULT_QUERY_SETTINGS } from \"settings\";\nimport { Result } from \"api/result\";\n\n///////////////////\n// Query Parsing //\n///////////////////\n\n/** Typings for the outputs of all of the parser combinators. */\ninterface QueryLanguageTypes {\n    queryType: QueryType;\n    comment: Comment;\n\n    explicitNamedField: NamedField;\n    namedField: NamedField;\n    sortField: QuerySortBy;\n\n    // Entire clauses in queries.\n    headerClause: QueryHeader;\n    fromClause: Source;\n    whereClause: WhereStep;\n    sortByClause: SortByStep;\n    limitClause: LimitStep;\n    flattenClause: FlattenStep;\n    groupByClause: GroupStep;\n    clause: QueryOperation;\n    query: Query;\n}\n\n/** Return a new parser which executes the underlying parser and returns it's raw string representation. */\nexport function captureRaw<T>(base: P.Parser<T>): P.Parser<[T, string]> {\n    return P.custom((success, failure) => {\n        return (input, i) => {\n            let result = (base as any)._(input, i);\n            if (!result.status) return result;\n\n            return Object.assign({}, result, { value: [result.value, input.substring(i, result.index)] });\n        };\n    });\n}\n\n/** Strip newlines and excess whitespace out of text. */\nfunction stripNewlines(text: string): string {\n    return text\n        .split(/[\\r\\n]+/)\n        .map(t => t.trim())\n        .join(\"\");\n}\n\n/** Given `parser`, return the parser that returns `if_eof()` if EOF is found,\n * otherwise `parser` preceded by (non-optional) whitespace */\nfunction precededByWhitespaceIfNotEof<T>(if_eof: (_: undefined) => T, parser: P.Parser<T>): P.Parser<T> {\n    return P.eof.map(if_eof).or(P.whitespace.then(parser));\n}\n\n/** A parsimmon-powered parser-combinator implementation of the query language. */\nexport const QUERY_LANGUAGE = P.createLanguage<QueryLanguageTypes>({\n    // Simple atom parsing, like words, identifiers, numbers.\n    queryType: q =>\n        P.alt<string>(P.regexp(/TABLE|LIST|TASK|CALENDAR/i))\n            .map(str => str.toLowerCase() as QueryType)\n            .desc(\"query type ('TABLE', 'LIST', 'TASK', or 'CALENDAR')\"),\n    explicitNamedField: q =>\n        P.seqMap(\n            EXPRESSION.field.skip(P.whitespace),\n            P.regexp(/AS/i).skip(P.whitespace),\n            EXPRESSION.identifier.or(EXPRESSION.string),\n            (field, _as, ident) => QueryFields.named(ident, field)\n        ),\n    comment: () =>\n        P.Parser((input, i) => {\n            // Parse a comment, which is a line starting with //.\n            let line = input.substring(i);\n            if (!line.startsWith(\"//\")) return P.makeFailure(i, \"Not a comment\");\n            // The comment ends at the end of the line.\n            line = line.split(\"\\n\")[0];\n            let comment = line.substring(2).trim();\n            return P.makeSuccess(i + line.length, comment);\n        }),\n    namedField: q =>\n        P.alt<NamedField>(\n            q.explicitNamedField,\n            captureRaw(EXPRESSION.field).map(([value, text]) => QueryFields.named(stripNewlines(text), value))\n        ),\n    sortField: q =>\n        P.seqMap(\n            EXPRESSION.field.skip(P.optWhitespace),\n            P.regexp(/ASCENDING|DESCENDING|ASC|DESC/i).atMost(1),\n            (field, dir) => {\n                let direction = dir.length == 0 ? \"ascending\" : dir[0].toLowerCase();\n                if (direction == \"desc\") direction = \"descending\";\n                if (direction == \"asc\") direction = \"ascending\";\n                return {\n                    field: field,\n                    direction: direction as \"ascending\" | \"descending\",\n                };\n            }\n        ),\n\n    headerClause: q =>\n        q.queryType\n            .chain(type => {\n                switch (type) {\n                    case \"table\": {\n                        return precededByWhitespaceIfNotEof(\n                            () => ({ type, fields: [], showId: true }),\n                            P.seqMap(\n                                P.regexp(/WITHOUT\\s+ID/i)\n                                    .skip(P.optWhitespace)\n                                    .atMost(1),\n                                P.sepBy(q.namedField, P.string(\",\").trim(P.optWhitespace)),\n                                (withoutId, fields) => {\n                                    return { type, fields, showId: withoutId.length == 0 };\n                                }\n                            )\n                        );\n                    }\n                    case \"list\":\n                        return precededByWhitespaceIfNotEof(\n                            () => ({ type, format: undefined, showId: true }),\n                            P.seqMap(\n                                P.regexp(/WITHOUT\\s+ID/i)\n                                    .skip(P.optWhitespace)\n                                    .atMost(1),\n                                EXPRESSION.field.atMost(1),\n                                (withoutId, format) => {\n                                    return {\n                                        type,\n                                        format: format.length == 1 ? format[0] : undefined,\n                                        showId: withoutId.length == 0,\n                                    };\n                                }\n                            )\n                        );\n                    case \"task\":\n                        return P.succeed({ type });\n                    case \"calendar\":\n                        return P.whitespace.then(\n                            P.seqMap(q.namedField, field => {\n                                return {\n                                    type,\n                                    showId: true,\n                                    field,\n                                } as QueryHeader;\n                            })\n                        );\n                    default:\n                        return P.fail(`Unrecognized query type '${type}'`);\n                }\n            })\n            .desc(\"TABLE or LIST or TASK or CALENDAR\"),\n    fromClause: q => P.seqMap(P.regexp(/FROM/i), P.whitespace, EXPRESSION.source, (_1, _2, source) => source),\n    whereClause: q =>\n        P.seqMap(P.regexp(/WHERE/i), P.whitespace, EXPRESSION.field, (where, _, field) => {\n            return { type: \"where\", clause: field } as WhereStep;\n        }).desc(\"WHERE <expression>\"),\n    sortByClause: q =>\n        P.seqMap(\n            P.regexp(/SORT/i),\n            P.whitespace,\n            q.sortField.sepBy1(P.string(\",\").trim(P.optWhitespace)),\n            (sort, _1, fields) => {\n                return { type: \"sort\", fields } as SortByStep;\n            }\n        ).desc(\"SORT field [ASC/DESC]\"),\n    limitClause: q =>\n        P.seqMap(P.regexp(/LIMIT/i), P.whitespace, EXPRESSION.field, (limit, _1, field) => {\n            return { type: \"limit\", amount: field } as LimitStep;\n        }).desc(\"LIMIT <value>\"),\n    flattenClause: q =>\n        P.seqMap(P.regexp(/FLATTEN/i).skip(P.whitespace), q.namedField, (_, field) => {\n            return { type: \"flatten\", field } as FlattenStep;\n        }).desc(\"FLATTEN <value> [AS <name>]\"),\n    groupByClause: q =>\n        P.seqMap(P.regexp(/GROUP BY/i).skip(P.whitespace), q.namedField, (_, field) => {\n            return { type: \"group\", field } as GroupStep;\n        }).desc(\"GROUP BY <value> [AS <name>]\"),\n    // Full query parsing.\n    clause: q => P.alt(q.fromClause, q.whereClause, q.sortByClause, q.limitClause, q.groupByClause, q.flattenClause),\n    query: q =>\n        P.seqMap(\n            q.headerClause.trim(optionalWhitespaceOrComment),\n            q.fromClause.trim(optionalWhitespaceOrComment).atMost(1),\n            q.clause.trim(optionalWhitespaceOrComment).many(),\n            (header, from, clauses) => {\n                return {\n                    header,\n                    source: from.length == 0 ? Sources.folder(\"\") : from[0],\n                    operations: clauses,\n                    settings: DEFAULT_QUERY_SETTINGS,\n                } as Query;\n            }\n        ),\n});\n\n/**\n * A parser for optional whitespace or comments. This is used to exclude whitespace and comments from other parsers.\n */\nconst optionalWhitespaceOrComment: P.Parser<string> = P.alt(P.whitespace, QUERY_LANGUAGE.comment)\n    .many() // Use many() since there may be zero whitespaces or comments.\n    // Transform the many to a single result.\n    .map(arr => arr.join(\"\"));\n\n/**\n * Attempt to parse a query from the given query text, returning a string error\n * if the parse failed.\n */\nexport function parseQuery(text: string): Result<Query, string> {\n    try {\n        let query = QUERY_LANGUAGE.query.tryParse(text);\n        return Result.success(query);\n    } catch (error) {\n        return Result.failure(\"\" + error);\n    }\n}\n"
  },
  {
    "path": "src/query/query.ts",
    "content": "/** Provides an AST for complex queries. */\nimport { Source } from \"data-index/source\";\nimport { Field } from \"expression/field\";\n\n/** The supported query types (corresponding to view types). */\nexport type QueryType = \"list\" | \"table\" | \"task\" | \"calendar\";\n\n/** A single-line comment. */\nexport type Comment = string;\n\n/** Fields used in the query portion. */\nexport interface NamedField {\n    /** The effective name of this field. */\n    name: string;\n    /** The value of this field. */\n    field: Field;\n}\n\n/** A query sort by field, for determining sort order. */\nexport interface QuerySortBy {\n    /** The field to sort on. */\n    field: Field;\n    /** The direction to sort in. */\n    direction: \"ascending\" | \"descending\";\n}\n\n/** Utility functions for quickly creating fields. */\nexport namespace QueryFields {\n    export function named(name: string, field: Field): NamedField {\n        return { name, field } as NamedField;\n    }\n\n    export function sortBy(field: Field, dir: \"ascending\" | \"descending\"): QuerySortBy {\n        return { field, direction: dir };\n    }\n}\n\n//////////////////////\n// Query Definition //\n//////////////////////\n\n/** A query which should render a list of elements. */\nexport interface ListQuery {\n    type: \"list\";\n    /** What should be rendered in the list. */\n    format?: Field;\n    /** If true, show the default DI field; otherwise, don't. */\n    showId: boolean;\n}\n\n/** A query which renders a table of elements. */\nexport interface TableQuery {\n    type: \"table\";\n    /** The fields (computed or otherwise) to select. */\n    fields: NamedField[];\n    /** If true, show the default ID field; otherwise, don't. */\n    showId: boolean;\n}\n\n/** A query which renders a collection of tasks. */\nexport interface TaskQuery {\n    type: \"task\";\n}\n\n/** A query which renders a collection of notes in a calendar view. */\nexport interface CalendarQuery {\n    type: \"calendar\";\n    /** The date field that we'll be grouping notes by for the calendar view */\n    field: NamedField;\n}\n\nexport type QueryHeader = ListQuery | TableQuery | TaskQuery | CalendarQuery;\n\n/** A step which only retains rows whose 'clause' field is truthy. */\nexport interface WhereStep {\n    type: \"where\";\n    clause: Field;\n}\n\n/** A step which sorts all current rows by the given list of sorts. */\nexport interface SortByStep {\n    type: \"sort\";\n    fields: QuerySortBy[];\n}\n\n/** A step which truncates the number of rows to the given amount. */\nexport interface LimitStep {\n    type: \"limit\";\n    amount: Field;\n}\n\n/** A step which flattens rows into multiple child rows. */\nexport interface FlattenStep {\n    type: \"flatten\";\n    field: NamedField;\n}\n\n/** A step which groups rows into groups by the given field. */\nexport interface GroupStep {\n    type: \"group\";\n    field: NamedField;\n}\n\n/** A virtual step which extracts an array of values from each row. */\nexport interface ExtractStep {\n    type: \"extract\";\n    fields: Record<string, Field>;\n}\n\nexport type QueryOperation = WhereStep | SortByStep | LimitStep | FlattenStep | GroupStep | ExtractStep;\n\n/**\n * A query over the Obsidian database. Queries have a specific and deterministic execution order:\n */\nexport interface Query {\n    /** The view type to render this query in. */\n    header: QueryHeader;\n    /** The source that file candidates will come from. */\n    source: Source;\n    /** The operations to apply to the data to produce the final result that will be rendered. */\n    operations: QueryOperation[];\n}\n"
  },
  {
    "path": "src/settings.ts",
    "content": "////////////////////\n// Query Settings //\n////////////////////\n\nexport interface QuerySettings {\n    /** What to render 'null' as in tables. Defaults to '-'. */\n    renderNullAs: string;\n    /** If enabled, tasks in Dataview views will automatically have their completion date appended when they are checked. */\n    taskCompletionTracking: boolean;\n    /** If enabled, automatic completions will use emoji shorthand ✅ YYYY-MM-DD instead of [completion:: date]. */\n    taskCompletionUseEmojiShorthand: boolean;\n    /** 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. */\n    taskCompletionText: string;\n    /** Date format of the task's completion timestamp. Only used if completionTracking is enabled and emojiShorthand is not. */\n    taskCompletionDateFormat: string;\n    /** Whether or not subtasks should be recursively completed in addition to their parent task. */\n    recursiveSubTaskCompletion: boolean;\n    /** If true, render a modal which shows no results were returned. */\n    warnOnEmptyResult: boolean;\n    /** Whether or not automatic view refreshing is enabled. */\n    refreshEnabled: boolean;\n    /** The interval that views are refreshed, by default. */\n    refreshInterval: number;\n    /** The default format that dates are rendered in (using luxon's moment-like formatting). */\n    defaultDateFormat: string;\n    /** The default format that date-times are rendered in (using luxon's moment-like formatting). */\n    defaultDateTimeFormat: string;\n    /** Maximum depth that objects will be expanded when being rendered recursively. */\n    maxRecursiveRenderDepth: number;\n    /** The name of the default ID field ('File'). */\n    tableIdColumnName: string;\n    /** The name of default ID fields on grouped data ('Group'). */\n    tableGroupColumnName: string;\n    /** Include the result count as part of the output. */\n    showResultCount: boolean;\n}\n\nexport const DEFAULT_QUERY_SETTINGS: QuerySettings = {\n    renderNullAs: \"\\\\-\",\n    taskCompletionTracking: false,\n    taskCompletionUseEmojiShorthand: false,\n    taskCompletionText: \"completion\",\n    taskCompletionDateFormat: \"yyyy-MM-dd\",\n    recursiveSubTaskCompletion: false,\n    warnOnEmptyResult: true,\n    refreshEnabled: true,\n    refreshInterval: 2500,\n    defaultDateFormat: \"MMMM dd, yyyy\",\n    defaultDateTimeFormat: \"h:mm a - MMMM dd, yyyy\",\n    maxRecursiveRenderDepth: 4,\n\n    tableIdColumnName: \"File\",\n    tableGroupColumnName: \"Group\",\n    showResultCount: true,\n};\n\n/////////////////////\n// Export Settings //\n/////////////////////\n\nexport interface ExportSettings {\n    /** Whether or not HTML should be used for formatting in exports. */\n    allowHtml: boolean;\n}\n\nexport const DEFAULT_EXPORT_SETTINGS: ExportSettings = {\n    allowHtml: true,\n};\n\n///////////////////////////////\n// General Dataview Settings //\n///////////////////////////////\n\nexport interface DataviewSettings extends QuerySettings, ExportSettings {\n    /** The prefix for inline queries by default. */\n    inlineQueryPrefix: string;\n    /** The prefix for inline JS queries by default. */\n    inlineJsQueryPrefix: string;\n    /** If true, inline queries are also evaluated in full codeblocks. */\n    inlineQueriesInCodeblocks: boolean;\n    /** Enable or disable executing DataviewJS queries. */\n    enableDataviewJs: boolean;\n    /** Enable or disable regular inline queries. */\n    enableInlineDataview: boolean;\n    /** Enable or disable executing inline DataviewJS queries. */\n    enableInlineDataviewJs: boolean;\n    /** Enable or disable rendering inline fields prettily in Reading View. */\n    prettyRenderInlineFields: boolean;\n    /** Enable or disable rendering inline fields prettily in Live Preview. */\n    prettyRenderInlineFieldsInLivePreview: boolean;\n    /** The keyword for DataviewJS blocks. */\n    dataviewJsKeyword: string;\n}\n\n/** Default settings for dataview on install. */\nexport const DEFAULT_SETTINGS: DataviewSettings = {\n    ...DEFAULT_QUERY_SETTINGS,\n    ...DEFAULT_EXPORT_SETTINGS,\n    ...{\n        inlineQueryPrefix: \"=\",\n        inlineJsQueryPrefix: \"$=\",\n        inlineQueriesInCodeblocks: true,\n        enableInlineDataview: true,\n        enableDataviewJs: false,\n        enableInlineDataviewJs: false,\n        prettyRenderInlineFields: true,\n        prettyRenderInlineFieldsInLivePreview: true,\n        dataviewJsKeyword: \"dataviewjs\",\n    },\n};\n"
  },
  {
    "path": "src/test/api/data-array.test.ts",
    "content": "import { DataArray } from \"api/data-array\";\nimport { DEFAULT_QUERY_SETTINGS } from \"settings\";\n\ndescribe(\"where\", () => {\n    test(\"true\", () =>\n        expect(\n            da([1, 2])\n                .where(x => true)\n                .array()\n        ).toEqual([1, 2]));\n    test(\"false\", () =>\n        expect(\n            da([1, 2])\n                .where(x => false)\n                .array()\n        ).toEqual([]));\n    test(\"number predicate\", () =>\n        expect(\n            da([1, 2, 3])\n                .where(x => x >= 2)\n                .array()\n        ).toEqual([2, 3]));\n});\n\ntest(\"filter\", () =>\n    expect(\n        da([1, 2])\n            .filter(x => true)\n            .array()\n    ).toEqual([1, 2]));\n\ndescribe(\"map\", () => {\n    test(\"identity\", () =>\n        expect(\n            da([1, 2, 3])\n                .map(x => x)\n                .array()\n        ).toEqual([1, 2, 3]));\n    test(\"number predicate\", () =>\n        expect(\n            da([1, 2, 3])\n                .map(x => x + 4)\n                .array()\n        ).toEqual([5, 6, 7]));\n});\n\ndescribe(\"flatMap\", () => {\n    test(\"identity\", () =>\n        expect(\n            da([1, 2, 3])\n                .flatMap(x => [x])\n                .array()\n        ).toEqual([1, 2, 3]));\n    test(\"number predicate\", () =>\n        expect(\n            da([1, 2, 3])\n                .flatMap(x => [x, x + 1])\n                .array()\n        ).toEqual([1, 2, 2, 3, 3, 4]));\n});\n\ntest(\"mutate\", () =>\n    expect(\n        da([1, 2])\n            .mutate(x => x)\n            .array()\n    ).toEqual([1, 2]));\n\ndescribe(\"limit\", () => {\n    test(\"zero\", () => expect(da([1, 2, 3]).limit(0).array()).toEqual([]));\n    test(\"one\", () => expect(da([1, 2, 3]).limit(1).array()).toEqual([1]));\n    test(\"huge\", () => expect(da([1, 2, 3]).limit(100).array()).toEqual([1, 2, 3]));\n});\n\ndescribe(\"slice\", () => {\n    test(\"zero\", () => expect(da([1, 2, 3, 4]).slice(0, 0).array()).toEqual([]));\n    test(\"one\", () => expect(da([1, 2, 3, 4]).slice(0, 1).array()).toEqual([1]));\n    test(\"middle\", () => expect(da([1, 2, 3, 4]).slice(1, 3).array()).toEqual([2, 3]));\n    test(\"huge\", () => expect(da([1, 2, 3, 4]).slice(1, 80).array()).toEqual([2, 3, 4]));\n});\n\ndescribe(\"concat\", () => {\n    test(\"numbers\", () =>\n        expect(\n            da([1, 2])\n                .concat(da([3, 4]))\n                .array()\n        ).toEqual([1, 2, 3, 4]));\n    test(\"empty\", () => expect(da([\"yes\"]).concat(da([])).array()).toEqual([\"yes\"]));\n});\n\ndescribe(\"indexOf\", () => {\n    test(\"exists\", () => expect(da([1, 2, 3]).indexOf(2)).toEqual(1));\n    test(\"not exists\", () => expect(da([1, 2, 3]).indexOf(4)).toEqual(-1));\n});\n\ndescribe(\"find\", () => {\n    test(\"true\", () => expect(da([1, 2, 3]).find(x => true)).toEqual(1));\n    test(\"false\", () => expect(da([1, 2, 3]).find(x => false)).toEqual(undefined));\n    test(\"number predicate\", () => expect(da([1, 2, 3]).find(x => x >= 2)).toEqual(2));\n});\n\ndescribe(\"findIndex\", () => {\n    test(\"true\", () => expect(da([1, 2, 3]).findIndex(x => true)).toEqual(0));\n    test(\"false\", () => expect(da([1, 2, 3]).findIndex(x => false)).toEqual(-1));\n    test(\"number predicate\", () => expect(da([1, 2, 3]).findIndex(x => x >= 2)).toEqual(1));\n});\n\ndescribe(\"includes\", () => {\n    test(\"exists\", () => expect(da([1, 2, 3]).includes(2)).toEqual(true));\n    test(\"not exists\", () => expect(da([1, 2, 3]).includes(4)).toEqual(false));\n});\n\ndescribe(\"join\", () => {\n    test(\"empty\", () => expect(da([]).join(\", \")).toEqual(\"\"));\n    test(\"one\", () => expect(da([1]).join(\", \")).toEqual(\"1\"));\n    test(\"multiple\", () => expect(da([1, 2, 3]).join(\", \")).toEqual(\"1, 2, 3\"));\n});\n\ndescribe(\"sort\", () => {\n    test(\"empty\", () =>\n        expect(\n            da([])\n                .sort(x => x)\n                .array()\n        ).toEqual([]));\n    test(\"single\", () =>\n        expect(\n            da([1])\n                .sort(x => x)\n                .array()\n        ).toEqual([1]));\n    test(\"numbers\", () =>\n        expect(\n            da([1, 4, -1])\n                .sort(x => x)\n                .array()\n        ).toEqual([-1, 1, 4]));\n    test(\"negated numbers\", () =>\n        expect(\n            da([1, 4, -1])\n                .sort(x => -x)\n                .array()\n        ).toEqual([4, 1, -1]));\n    test(\"reversed numbers\", () =>\n        expect(\n            da([1, 4, -1])\n                .sort(x => x, \"desc\")\n                .array()\n        ).toEqual([4, 1, -1]));\n    test(\"objects\", () =>\n        expect(\n            da([{ x: 1 }, { x: 4 }, { x: -6 }])\n                .sort(x => x.x)\n                .array()\n        ).toEqual([{ x: -6 }, { x: 1 }, { x: 4 }]));\n});\n\ndescribe(\"distinct\", () => {\n    test(\"empty\", () => expect(da([]).distinct().array()).toEqual([]));\n    test(\"single\", () => expect(da([1]).distinct().array()).toEqual([1]));\n    test(\"multiple unique\", () => expect(da([1, 1, 1]).distinct().array()).toEqual([1]));\n    test(\"multiple same\", () => expect(da([1, 3, 7, 3, 1]).distinct().array()).toEqual([1, 3, 7]));\n    test(\"objects\", () =>\n        expect(\n            da([{ x: 1 }, { x: 2 }, { x: 4 }])\n                .distinct(x => 1)\n                .array()\n        ).toEqual([{ x: 1 }]));\n});\n\ndescribe(\"first\", () => {\n    test(\"empty\", () => expect(da([]).first()).toEqual(undefined));\n    test(\"nonempty\", () => expect(da([1, 2, 3]).first()).toEqual(1));\n});\n\ndescribe(\"last\", () => {\n    test(\"empty\", () => expect(da([]).last()).toEqual(undefined));\n    test(\"nonempty\", () => expect(da([1, 2, 3]).last()).toEqual(3));\n});\n\ndescribe(\"sum\", () => {\n    test(\"empty\", () => expect(da([]).sum()).toEqual(0));\n    test(\"numbers\", () => expect(da([1, 10, 2]).sum()).toEqual(13));\n});\n\ndescribe(\"avg\", () => {\n    test(\"empty\", () => expect(da([]).avg()).toEqual(NaN));\n    test(\"numbers\", () => expect(da([5, 10, 15]).avg()).toEqual(10));\n});\n\ndescribe(\"min\", () => {\n    test(\"empty\", () => expect(da([]).min()).toEqual(Infinity));\n    test(\"numbers\", () => expect(da([14, 10, 15]).min()).toEqual(10));\n});\n\ndescribe(\"max\", () => {\n    test(\"empty\", () => expect(da([]).max()).toEqual(-Infinity));\n    test(\"numbers\", () => expect(da([14, 10, 15]).max()).toEqual(15));\n});\n\n/** Utility function for quickly creating a data array. */\nfunction da<T>(val: T[]): DataArray<T> {\n    return DataArray.wrap(val, DEFAULT_QUERY_SETTINGS);\n}\n"
  },
  {
    "path": "src/test/common.ts",
    "content": "import { Literal } from \"data-model/value\";\nimport { Context, LinkHandler } from \"expression/context\";\nimport { EXPRESSION } from \"expression/parse\";\nimport { DEFAULT_QUERY_SETTINGS } from \"settings\";\n\n/** Expect that the given dataview expression resolves to the given value. */\nexport function expectEvals(text: string, result: Literal) {\n    expect(parseEval(text)).toEqual(result);\n}\n\n/** Parse a field expression and evaluate it in the simple context. */\nexport function parseEval(text: string): Literal {\n    let field = EXPRESSION.field.tryParse(text);\n    return simpleContext().tryEvaluate(field);\n}\n\n/** Create a trivial link handler which never resolves links. */\nexport function simpleLinkHandler(): LinkHandler {\n    return {\n        resolve: path => null,\n        normalize: path => path,\n        exists: path => true,\n    };\n}\n\n/** Create a trivial context good for evaluations that do not depend on links. */\nexport function simpleContext(): Context {\n    return new Context(simpleLinkHandler(), DEFAULT_QUERY_SETTINGS);\n}\n"
  },
  {
    "path": "src/test/data/index-map.test.ts",
    "content": "import { IndexMap } from \"data-index/index\";\n\ntest(\"Simple Set/Get\", () => {\n    let index = new IndexMap();\n    index.set(\"test\", new Set([\"one\", \"two\"]));\n    index.set(\"test2\", new Set([\"two\"]));\n\n    expect(index.get(\"test\")).toEqual(new Set([\"one\", \"two\"]));\n    expect(index.get(\"test2\")).toEqual(new Set([\"two\"]));\n});\n\ntest(\"Inverted Get\", () => {\n    let index = new IndexMap();\n    index.set(\"test\", new Set([\"a\", \"b\", \"c\"]));\n    index.set(\"test2\", new Set([\"a\", \"c\"]));\n    index.set(\"test3\", new Set([]));\n\n    expect(index.getInverse(\"a\")).toEqual(new Set([\"test\", \"test2\"]));\n    expect(index.getInverse(\"b\")).toEqual(new Set([\"test\"]));\n    expect(index.getInverse(\"\")).toEqual(new Set());\n\n    index.set(\"test\", new Set([\"a\", \"c\"]));\n    expect(index.getInverse(\"b\")).toEqual(new Set([]));\n    expect(index.getInverse(\"a\")).toEqual(new Set([\"test\", \"test2\"]));\n    expect(index.getInverse(\"c\")).toEqual(new Set([\"test\", \"test2\"]));\n\n    index.set(\"test\", new Set([]));\n    expect(index.getInverse(\"a\")).toEqual(new Set([\"test2\"]));\n    expect(index.getInverse(\"c\")).toEqual(new Set([\"test2\"]));\n});\n"
  },
  {
    "path": "src/test/data/transferable.test.ts",
    "content": "import { Transferable } from \"data-model/transferable\";\nimport { Link } from \"data-model/value\";\nimport { DateTime, Duration } from \"luxon\";\n\ndescribe(\"Literals\", () => {\n    test(\"String\", () => checkRoundTrip(\"hello\"));\n    test(\"Number\", () => checkRoundTrip(18));\n    test(\"Boolean\", () => checkRoundTrip(true));\n    test(\"Null\", () => checkRoundTrip(null));\n});\n\ntest(\"Date\", () => expect(roundTrip(DateTime.fromObject({ year: 1982, month: 5, day: 25 })).day).toEqual(25));\ntest(\"Duration\", () => expect(roundTrip(Duration.fromMillis(10000)).toMillis()).toEqual(10000));\ntest(\"Link\", () => expect(roundTrip(Link.file(\"hello\"))).toEqual(Link.file(\"hello\")));\n\ntest(\"Full Date\", () => {\n    let date = DateTime.fromObject({ year: 1982, month: 5, day: 19 }, { zone: \"UTC+8\" });\n    expect(roundTrip(date).equals(date)).toBeTruthy();\n});\n\n/** Run a value through the transferable converter and back again. */\nfunction roundTrip<T>(value: T): T {\n    return Transferable.value(Transferable.transferable(value));\n}\n\nfunction checkRoundTrip(value: any) {\n    expect(roundTrip(value)).toEqual(value);\n}\n"
  },
  {
    "path": "src/test/data/values.test.ts",
    "content": "import { Values, Link } from \"data-model/value\";\n\ndescribe(\"Links\", () => {\n    describe(\"Comparisons\", () => {\n        test(\"Same File\", () => expect(Link.file(\"test\").equals(Link.file(\"test\"))).toBeTruthy());\n        test(\"Different File\", () => expect(Link.file(\"test\").equals(Link.file(\"test2\"))).toBeFalsy());\n        test(\"Different Subpath\", () => expect(Link.file(\"test\").equals(Link.header(\"test\", \"Hello\"))).toBeFalsy());\n        test(\"Different Subpath Type\", () =>\n            expect(Link.header(\"test\", \"abc\").equals(Link.block(\"test\", \"abc\"))).toBeFalsy());\n    });\n\n    describe(\"General Comparisons\", () => {\n        test(\"Same File\", () => expect(Values.compareValue(Link.file(\"test\"), Link.file(\"test\"))).toBe(0));\n        test(\"Different File\", () =>\n            expect(Values.compareValue(Link.file(\"test\"), Link.file(\"test2\"))).toBeLessThan(0));\n        test(\"Different Subpath\", () =>\n            expect(Values.compareValue(Link.file(\"test\"), Link.header(\"test\", \"Hello\"))).toBeLessThan(0));\n        test(\"Different Subpath Type\", () =>\n            expect(Values.compareValue(Link.header(\"test\", \"abc\"), Link.block(\"test\", \"abc\"))).toBeTruthy());\n    });\n});\n"
  },
  {
    "path": "src/test/function/aggregation.test.ts",
    "content": "import { expectEvals } from \"test/common\";\n\ndescribe(\"map()\", () => {\n    test(\"empty list\", () => expectEvals(\"map([], (k) => 6)\", []));\n    test(\"number list\", () => expectEvals(\"map([1, 2, 3], (k) => k + 4)\", [5, 6, 7]));\n    test(\"string list\", () => expectEvals('map([\"a\", \"be\", \"ced\"], (k) => length(k))', [1, 2, 3]));\n});\n\ndescribe(\"filter()\", () => {\n    test(\"empty list\", () => expectEvals(\"filter(list(), (k) => true)\", []));\n    test(\"number list\", () => expectEvals(\"filter(list(1, 2, 3), (k) => k >= 2)\", [2, 3]));\n});\n\ndescribe(\"unique()\", () => {\n    test(\"empty\", () => expectEvals(\"unique([])\", []));\n    test(\"single\", () => expectEvals(\"unique([1])\", [1]));\n    test(\"multiple unique\", () => expectEvals(\"unique([1, 1, 1])\", [1]));\n    test(\"multiple same\", () => expectEvals(\"unique([1, 3, 7, 3, 1])\", [1, 3, 7]));\n});\n\ndescribe(\"min()\", () => {\n    test(\"empty\", () => expectEvals(\"min()\", null));\n    test(\"single\", () => expectEvals(\"min(6)\", 6));\n    test(\"multiple\", () => expectEvals(\"min(6, 9, 12)\", 6));\n    test(\"list empty\", () => expectEvals(\"min([])\", null));\n    test(\"list multiple\", () => expectEvals(\"min([1, 2, 3])\", 1));\n});\n\ndescribe(\"minby()\", () => {\n    test(\"empty\", () => expectEvals(\"minby([], (k) => k)\", null));\n    test(\"single\", () => expectEvals(\"minby([1], (k) => k)\", 1));\n    test(\"multiple\", () => expectEvals(\"minby([1, 2, 3], (k) => 0 - k)\", 3));\n});\n\ndescribe(\"max()\", () => {\n    test(\"empty\", () => expectEvals(\"max()\", null));\n    test(\"single\", () => expectEvals(\"max(6)\", 6));\n    test(\"multiple\", () => expectEvals(\"max(6, 9, 12)\", 12));\n    test(\"list empty\", () => expectEvals(\"max([])\", null));\n    test(\"list multiple\", () => expectEvals(\"max([1, 2, 3])\", 3));\n});\n\ndescribe(\"maxby()\", () => {\n    test(\"empty\", () => expectEvals(\"maxby([], (k) => k)\", null));\n    test(\"single\", () => expectEvals(\"maxby([1], (k) => k)\", 1));\n    test(\"multiple\", () => expectEvals(\"maxby([1, 2, 3], (k) => 0 - k)\", 1));\n});\n\ndescribe(\"sum()\", () => {\n    test(\"number list\", () => expectEvals(\"sum([2, 3, 1])\", 6));\n    test(\"string list\", () => expectEvals('sum([\"a\", \"b\", \"c\"])', \"abc\"));\n    test(\"empty list\", () => expectEvals(\"sum([])\", null));\n});\n\ndescribe(\"average()\", () => {\n    test(\"number list\", () => expectEvals(\"average([2, 3, 1])\", 2));\n    test(\"number list\", () => expectEvals(\"average(nonnull([2, 3, null, 1]))\", 2));\n    test(\"empty list\", () => expectEvals(\"average([])\", null));\n});\n\ndescribe(\"any()\", () => {\n    test(\"true, false\", () => expectEvals(\"any(true, false)\", true));\n    test(\"[true, false]\", () => expectEvals(\"any(list(true, false))\", true));\n});\n\ndescribe(\"all()\", () => {\n    test(\"true, false\", () => expectEvals(\"all(true, false)\", false));\n    test(\"true, [false]\", () => expectEvals(\"all(true, list(false))\", true));\n    test(\"[true, false]\", () => expectEvals(\"all(list(true, false))\", false));\n\n    test(\"vectorized\", () => {\n        expectEvals('all(regexmatch(\"a+\", list(\"a\", \"aaaa\")))', true);\n        expectEvals('all(regexmatch(\"a+\", list(\"a\", \"aaab\")))', false);\n        expectEvals('any(regexmatch(\"a+\", list(\"a\", \"aaab\")))', true);\n\n        expectEvals('all(regextest(\"a+\", list(\"a\", \"aaaa\")))', true);\n        expectEvals('all(regextest(\"a+\", list(\"a\", \"aaab\")))', true);\n        expectEvals('any(regextest(\"a+\", list(\"a\", \"aaab\")))', true);\n    });\n});\n\ndescribe(\"nonnull()\", () => {\n    test(\"empty\", () => expectEvals(\"nonnull([])\", []));\n    test(\"[null, false]\", () => expectEvals(\"nonnull([null, false])\", [false]));\n});\n\ndescribe(\"firstvalue()\", () => {\n    test(\"empty\", () => expectEvals(\"firstvalue([])\", null));\n    test(\"null\", () => expectEvals(\"firstvalue(null)\", null));\n    test(\"[1, 2, 3]\", () => expectEvals(\"firstvalue([1, 2, 3])\", 1));\n    test(\"[null, 1, 2]\", () => expectEvals(\"firstvalue([null, 1, 2])\", 1));\n});\n"
  },
  {
    "path": "src/test/function/coerce.test.ts",
    "content": "import { expectEvals, parseEval } from \"test/common\";\n\ntest(\"number()\", () => {\n    expect(parseEval('number(\"hmm\")')).toEqual(null);\n    expect(parseEval(\"number(34)\")).toEqual(34);\n    expect(parseEval('number(\"34\")')).toEqual(34);\n    expect(parseEval('number(\"17 years\")')).toEqual(17);\n    expect(parseEval('number(\"-19\")')).toEqual(-19);\n});\n\ndescribe(\"string()\", () => {\n    test(\"number\", () => expect(parseEval(`string(18)`)).toEqual(\"18\"));\n});\n\ntest(\"list()\", () => {\n    expectEvals(\"list(1, 2, 3)\", [1, 2, 3]);\n    expectEvals(\"list()\", []);\n});\n\ntest(\"object()\", () => {\n    expect(parseEval(\"object()\")).toEqual({});\n    expect(parseEval('object(\"hello\", 1)')).toEqual({ hello: 1 });\n});\n"
  },
  {
    "path": "src/test/function/constructors.test.ts",
    "content": "import { Duration } from \"luxon\";\nimport { parseEval } from \"test/common\";\n\ndescribe(\"dur()\", () => {\n    test(\"8 minutes\", () => expect(parseEval(`dur(\"8 minutes\")`)).toEqual(Duration.fromObject({ minutes: 8 })));\n    test(\"3 hrs\", () => expect(parseEval(`dur(\"3 hrs\")`)).toEqual(Duration.fromObject({ hours: 3 })));\n    test(\"2 days, 6 minutes\", () =>\n        expect(parseEval(`dur(\"2 days, 6 minutes\")`)).toEqual(Duration.fromObject({ days: 2, minutes: 6 })));\n});\n\ndescribe(\"typeof()\", () => {\n    test(\"string\", () => expect(parseEval(`typeof(\"nice\")`)).toEqual(\"string\"));\n    test(\"object\", () => expect(parseEval(`typeof({a: 1, b: 2})`)).toEqual(\"object\"));\n    test(\"array\", () => expect(parseEval(`typeof([\"nice\"])`)).toEqual(\"array\"));\n    test(\"number\", () => expect(parseEval(`typeof(18.4)`)).toEqual(\"number\"));\n});\n"
  },
  {
    "path": "src/test/function/eval.test.ts",
    "content": "/** Various tests for evaluating fields in context. */\n\nimport { EXPRESSION } from \"expression/parse\";\nimport { Context, LinkHandler } from \"expression/context\";\nimport { Duration } from \"luxon\";\nimport { Fields } from \"expression/field\";\nimport { Literal, Link } from \"data-model/value\";\nimport { DEFAULT_QUERY_SETTINGS } from \"settings\";\n\n// <-- Numeric Operations -->\n\ntest(\"Evaluate simple numeric operations\", () => {\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(2), \"+\", Fields.literal(4)))).toEqual(6);\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(2), \"-\", Fields.literal(4)))).toEqual(-2);\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(2), \"*\", Fields.literal(4)))).toEqual(8);\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(8), \"/\", Fields.literal(4)))).toEqual(2);\n});\n\ntest(\"Evaluate numeric comparisons\", () => {\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(8), \"<\", Fields.literal(4)))).toEqual(false);\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(-2), \"=\", Fields.literal(-2)))).toEqual(true);\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(-2), \">=\", Fields.literal(-8)))).toEqual(true);\n});\n\ntest(\"Evaluate complex numeric operations\", () => {\n    expect(parseEval(\"12 + 8 - 4 / 2\")).toEqual(18);\n    expect(parseEval(\"16 / 8 / 2\")).toEqual(1);\n    expect(parseEval(\"39 / 3 <= 14\")).toEqual(true);\n});\n\n// <-- String Operations -->\n\ntest(\"Evaluate simple string operations\", () => {\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(\"a\"), \"+\", Fields.literal(\"b\")))).toEqual(\"ab\");\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(\"a\"), \"+\", Fields.literal(12)))).toEqual(\"a12\");\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(\"a\"), \"*\", Fields.literal(6)))).toEqual(\"aaaaaa\");\n});\n\ntest(\"Evaluate string comparisons\", () => {\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(\"abc\"), \"<\", Fields.literal(\"abd\")))).toEqual(\n        true\n    );\n    expect(simpleContext().tryEvaluate(Fields.binaryOp(Fields.literal(\"xyz\"), \"=\", Fields.literal(\"xyz\")))).toEqual(\n        true\n    );\n});\n\n// <-- Date Operations -->\n\ntest(\"Evaluate date comparisons\", () => {\n    expect(parseEval(\"date(2021-01-14) = date(2021-01-14)\")).toEqual(true);\n    expect(parseEval(\"contains(list(date(2020-01-01)), date(2020-01-01))\")).toEqual(true);\n});\n\ntest(\"Evaluate date subtraction\", () => {\n    let duration = parseEval(\"date(2021-05-04) - date(1997-05-17)\") as Duration;\n    expect(duration.years).toEqual(23);\n});\n\n// <-- Field resolution -->\n\ntest(\"Evaluate simple field resolution\", () => {\n    let context = simpleContext().set(\"a\", 18).set(\"b\", \"hello\");\n    expect(context.get(\"a\")).toEqual(18);\n    expect(context.get(\"b\")).toEqual(\"hello\");\n    expect(context.get(\"c\")).toEqual(null);\n});\n\ntest(\"Evaluate simple object resolution\", () => {\n    let object = { inner: { final: 6 } };\n    let context = simpleContext().set(\"obj\", object);\n\n    expect(context.tryEvaluate(Fields.indexVariable(\"obj.inner\"))).toEqual(object.inner);\n    expect(context.tryEvaluate(Fields.indexVariable(\"obj.inner.final\"))).toEqual(object.inner.final);\n});\n\ntest(\"Evaluate simple link resolution\", () => {\n    let object = { inner: { final: 6 } };\n    let context = new Context(\n        { resolve: path => object, normalize: path => path, exists: path => false },\n        DEFAULT_QUERY_SETTINGS\n    ).set(\"link\", Link.file(\"test\", false));\n    expect(context.tryEvaluate(Fields.indexVariable(\"link.inner\"))).toEqual(object.inner);\n    expect(context.tryEvaluate(Fields.indexVariable(\"link.inner.final\"))).toEqual(object.inner.final);\n});\n\ndescribe(\"Immediately Invoked Lambdas\", () => {\n    test(\"Addition\", () => expect(parseEval(\"((a, b) => a + b)(1, 2)\")).toEqual(3));\n    test(\"Negation\", () => expect(parseEval(\"((v) => 0-v)(6)\")).toEqual(-6));\n    test(\"Curried\", () => expect(parseEval(\"((a) => (b) => a + b)(1)(2)\")).toEqual(3));\n    test(\"In Argument\", () => expect(parseEval(\"((a) => 1 + a)(((a) => 2)(3))\")).toEqual(3));\n});\n\ndescribe(\"Immediately Indexed Objects\", () => {\n    test(\"Empty\", () => expect(parseEval('{ a: 1, b: 2 }[\"c\"]')).toEqual(null));\n    test(\"Single\", () => expect(parseEval('{ a: 1, b: 2 }[\"a\"]')).toEqual(1));\n    test(\"Nested\", () => expect(parseEval('{ a: 1, b: { c: 4 } }[\"b\"][\"c\"]')).toEqual(4));\n});\n\n/** Parse a field expression and evaluate it in the simple context. */\nfunction parseEval(text: string): Literal {\n    let field = EXPRESSION.field.tryParse(text);\n    return simpleContext().tryEvaluate(field);\n}\n\n/** Create a trivial link handler which never resolves links. */\nfunction simpleLinkHandler(): LinkHandler {\n    return {\n        resolve: path => null,\n        normalize: path => path,\n        exists: path => true,\n    };\n}\n\n/** Create a trivial context good for evaluations that do not depend on links. */\nfunction simpleContext(): Context {\n    return new Context(simpleLinkHandler(), DEFAULT_QUERY_SETTINGS);\n}\n"
  },
  {
    "path": "src/test/function/functions.test.ts",
    "content": "// <-- Functions -->\n// <-- Function vectorization -->\n\nimport { DateTime } from \"luxon\";\nimport { DefaultFunctions } from \"expression/functions\";\nimport { parseEval, simpleContext } from \"test/common\";\n\ntest(\"Evaluate lower(list)\", () => {\n    expect(parseEval('lower(list(\"A\", \"B\"))')).toEqual([\"a\", \"b\"]);\n});\n\n// <-- Length -->\n\ntest(\"Evaluate length(array)\", () => {\n    expect(DefaultFunctions.length(simpleContext(), [1, 2])).toEqual(2);\n    expect(DefaultFunctions.length(simpleContext(), [])).toEqual(0);\n});\n\ntest(\"Evaluate length(string)\", () => {\n    expect(DefaultFunctions.length(simpleContext(), \"hello\")).toEqual(5);\n    expect(DefaultFunctions.length(simpleContext(), \"no\")).toEqual(2);\n    expect(DefaultFunctions.length(simpleContext(), \"\")).toEqual(0);\n});\n\n// <-- contains() -->\n\ntest(\"Evaluate contains(object)\", () => {\n    expect(parseEval('contains(object(\"hello\", 1), \"hello\")')).toEqual(true);\n    expect(parseEval('contains(object(\"hello\", 1), \"no\")')).toEqual(false);\n});\n\ntest(\"Evaluate contains(array)\", () => {\n    expect(parseEval('contains(list(\"hello\", 1), \"hello\")')).toEqual(true);\n    expect(parseEval('contains(list(\"hello\", 1), 6)')).toEqual(false);\n});\n\ntest(\"Evaluate fuzzy contains(array)\", () => {\n    expect(parseEval(`contains(list(\"hello\"), \"he\")`)).toEqual(true);\n    expect(parseEval(`contains(list(\"hello\"), \"no\")`)).toEqual(false);\n});\n\ntest(\"Evaluate contains(string)\", () => {\n    expect(parseEval('contains(\"hello\", \"hello\")')).toEqual(true);\n    expect(parseEval('contains(\"meep\", \"me\")')).toEqual(true);\n    expect(parseEval('contains(\"hello\", \"xd\")')).toEqual(false);\n});\n\ntest(\"Evaluate non-fuzzy econtains(array)\", () => {\n    expect(parseEval(`econtains(list(\"hello\"), \"he\")`)).toEqual(false);\n    expect(parseEval(`econtains(list(\"hello\"), \"hello\")`)).toEqual(true);\n    expect(parseEval(`econtains(list(\"hello\", 19), 1)`)).toEqual(false);\n    expect(parseEval(`econtains(list(\"hello\", 19), 19)`)).toEqual(true);\n});\n\n// <-- reverse() -->\n\ntest(\"Evaluate reverse(list)\", () => {\n    expect(parseEval(\"reverse(list(1, 2, 3))\")).toEqual(parseEval(\"list(3, 2, 1)\"));\n    expect(parseEval('reverse(list(\"a\", \"b\", \"c\"))')).toEqual(parseEval('list(\"c\", \"b\", \"a\")'));\n});\n\n// <-- flat() -->\n\ntest(\"Evaluate flat()\", () => {\n    expect(parseEval(\"flat(list(1, 2, 3, list(11, 12)))\")).toEqual(parseEval(\"list(1,2,3,11,12)\"));\n    expect(parseEval(\"flat(list(1, list(21, list(221, 222)), 3, list(11, 12)))\")).toEqual(\n        parseEval(\"list(1,21,list(221,222),3,11,12)\")\n    ); // flat(...)\n    expect(parseEval(\"flat(list(1, list(2, list(3, list(4, list(5))))), 3)\")).toEqual(\n        parseEval(\"list(1,2,3,4,list(5))\")\n    ); // flat(..., 3)\n    expect(parseEval(\"flat(list(1, list(2, list(3, list(4, list(5))))), 10)\")).toEqual(parseEval(\"list(1,2,3,4,5)\")); // flat(..., 10)\n});\n\n// <-- slice() -->\n\ntest(\"Evaluate slice()\", () => {\n    expect(parseEval(\"slice(list(1, 2, 3, 4, 5), 3)\")).toEqual(parseEval(\"list(4, 5)\")); // slice(..., 3)\n    expect(parseEval(\"slice(list(1, 2, 3, 4, 5), 0, 2)\")).toEqual(parseEval(\"list(1, 2)\")); // slice(..., 0, 2)\n    expect(parseEval(\"slice(list(1, 2, 3, 4, 5), -2)\")).toEqual(parseEval(\"list(4, 5)\")); // slice(..., -2)\n    expect(parseEval(\"slice(list(1, 2, 3, 4, 5), -1, 1)\")).toEqual(parseEval(\"list()\")); // slice(..., -1, 1)\n    expect(parseEval(\"slice(list(1, 2, 3, 4, 5))\")).toEqual(parseEval(\"list(1, 2, 3, 4, 5)\")); // slice(...)\n    expect(parseEval('slice(list(date(\"2021-01-01\"), date(\"2022-02-02\"), date(\"2023-03-03\")), -2)')).toEqual([\n        DateTime.fromObject({ year: 2022, month: 2, day: 2 }),\n        DateTime.fromObject({ year: 2023, month: 3, day: 3 }),\n    ]); // slice(date list, -2)\n    expect(parseEval('slice([\"ant\", \"bison\", \"camel\", \"duck\", \"elephant\"], -3)')).toEqual([\n        \"camel\",\n        \"duck\",\n        \"elephant\",\n    ]); // slice(string list, -3)\n});\n\n// <-- sort() -->\n\ndescribe(\"sort()\", () => {\n    test(\"Evaluate sort(list)\", () => {\n        expect(parseEval(\"sort(list(2, 3, 1))\")).toEqual(parseEval(\"list(1, 2, 3)\"));\n        expect(parseEval('sort(list(\"a\", \"c\", \"b\"))')).toEqual(parseEval('list(\"a\", \"b\", \"c\")'));\n    });\n\n    test(\"Evaluate sort(list, func)\", () => {\n        expect(parseEval(\"sort(list(2, 3, 1), (k) => 0-k)\")).toEqual(parseEval(\"list(3, 2, 1)\"));\n    });\n});\n\n// <-- display() -->\n\ntest(\"Evaluate display()\", () => {\n    expect(parseEval('display(\"test\")')).toEqual(\"test\");\n    expect(parseEval('display(\"[displayname](http://example.com)\")')).toEqual(\"displayname\");\n    expect(parseEval('display(\"[[test]]\")')).toEqual(\"test\");\n    expect(parseEval('display(\"[[test|displayname]]\")')).toEqual(\"displayname\");\n    expect(parseEval('display(\"long [[test]] **with** [[test2|multiple]] [links](http://example.com)\")')).toEqual(\n        \"long test with multiple links\"\n    );\n    expect(parseEval(\"display(1)\")).toEqual(\"1\");\n    expect(parseEval(\"display(true)\")).toEqual(\"true\");\n    expect(parseEval(\"display(null)\")).toEqual(\"\");\n    expect(parseEval('display(date(\"2024-11-18\"))')).toEqual(\"November 18, 2024\");\n    expect(parseEval('display(dur(\"7 hours\"))')).toEqual(\"7 hours\");\n    expect(parseEval('display(link(\"path/to/file.md\"))')).toEqual(\"file\");\n    expect(parseEval('display(link(\"path/to/file.md\", \"displayname\"))')).toEqual(\"displayname\");\n    expect(parseEval('display(list(\"test\", 2, link(\"file.md\")))')).toEqual(\"test, 2, file\");\n});\n\n// <-- default() -->\n\ntest(\"Evaluate default()\", () => {\n    expect(parseEval(\"default(null, 1)\")).toEqual(1);\n    expect(parseEval(\"default(2, 1)\")).toEqual(2);\n    expect(parseEval(\"default(list(1, null, null), 2)\")).toEqual([1, 2, 2]);\n});\n\ntest(\"Evaluate ldefault()\", () => {\n    expect(parseEval(\"ldefault(null, 1)\")).toEqual(1);\n    expect(parseEval(\"ldefault(2, 1)\")).toEqual(2);\n});\n\n// <-- choice() -->\n\ntest(\"Evaluate choose()\", () => {\n    expect(parseEval(\"choice(true, 1, 2)\")).toEqual(1);\n    expect(parseEval(\"choice(false, 1, 2)\")).toEqual(2);\n});\n\ntest(\"Evaluate hash()\", () => {\n    expect(DefaultFunctions.hash(simpleContext(), \"2024-03-17\", \"\")).toEqual(3259376374957153);\n    expect(DefaultFunctions.hash(simpleContext(), \"2024-03-17\", 2)).toEqual(271608741894590);\n    expect(DefaultFunctions.hash(simpleContext(), \"2024-03-17\", \"Home\")).toEqual(3041844187830523);\n    expect(DefaultFunctions.hash(simpleContext(), \"2024-03-17\", \"note a1\", 21)).toEqual(1143088188331616);\n});\n\n// <-- extract() -->\n\ntest(\"Evaluate 1 field extract()\", () => {\n    let res = parseEval('extract(object(\"mtime\", 1), \"mtime\")');\n    expect(res).toEqual({ mtime: 1 });\n});\n\ntest(\"Evaluate 2 field extract()\", () => {\n    let res = parseEval('extract(object(\"mtime\", 1, \"yes\", \"hello\"), \"yes\", \"mtime\")');\n    expect(res).toEqual({\n        yes: \"hello\",\n        mtime: 1,\n    });\n});\n\n// <-- nonnull() -->\n\ntest(\"Evaluate nonnull()\", () => {\n    expect(DefaultFunctions.nonnull(simpleContext(), null, null, 1)).toEqual([1]);\n    expect(DefaultFunctions.nonnull(simpleContext(), \"yes\")).toEqual([\"yes\"]);\n});\n\n// <-- date() -->\n\ntest(\"Evaluate date()\", () => {\n    expect(parseEval(\"date([[2020-04-18]])\")).toEqual(DateTime.fromObject({ year: 2020, month: 4, day: 18 }));\n    expect(parseEval(\"date([[Place|2021-04]])\")).toEqual(DateTime.fromObject({ year: 2021, month: 4, day: 1 }));\n    expect(parseEval('date(\"12/31/2022\", \"MM/dd/yyyy\")')).toEqual(\n        DateTime.fromObject({ year: 2022, month: 12, day: 31 })\n    );\n    expect(parseEval('date(\"210313\", \"yyMMdd\")')).toEqual(DateTime.fromObject({ year: 2021, month: 3, day: 13 }));\n    expect(parseEval('date(\"946778645012\",\"x\")')).toEqual(DateTime.fromMillis(946778645012));\n    expect(parseEval('date(\"946778645\",\"X\")')).toEqual(DateTime.fromMillis(946778645000));\n    expect(DefaultFunctions.date(simpleContext(), null, \"MM/dd/yyyy\")).toEqual(null);\n});\n"
  },
  {
    "path": "src/test/function/meta.test.ts",
    "content": "import { parseEval } from \"test/common\";\n\ntest(\"Evaluate meta(link).display\", () => {\n    expect(parseEval(`meta([[2021-11-01|Displayed link text]]).display`)).toEqual(\"Displayed link text\");\n    expect(parseEval(`meta([[2021-11-01]]).display`)).toBeNull;\n});\n\ntest(\"Evaluate meta(link).path\", () => {\n    expect(parseEval(`meta([[My Project#Next Actions]]).path`)).toEqual(\"My Project\");\n    expect(parseEval(`meta([[My Project#^9bcbe8]]).path`)).toEqual(\"My Project\");\n    expect(parseEval(`meta([[My Project]]).path`)).toEqual(\"My Project\");\n});\n\ntest(\"Evaluate meta(link).subpath\", () => {\n    expect(parseEval(`meta([[My Project#Next Actions]]).subpath`)).toEqual(\"Next Actions\");\n    expect(parseEval(`meta([[My Project#^9bcbe8]]).subpath`)).toEqual(\"9bcbe8\");\n    expect(parseEval(`meta([[My Project]]).subpath`)).toBeNull;\n});\n\ntest(\"Evaluate meta(link).type\", () => {\n    expect(parseEval(`meta([[My Project]]).type`)).toEqual(\"file\");\n    expect(parseEval(`meta([[My Project#Next Actions]]).type`)).toEqual(\"header\");\n    expect(parseEval(`meta([[My Project#^9bcbe8]]).type`)).toEqual(\"block\");\n});\n"
  },
  {
    "path": "src/test/function/string.test.ts",
    "content": "// <-- regexreplace() -->\n\nimport { parseEval } from \"test/common\";\n\ndescribe(\"regexreplace()\", () => {\n    test(\"letter\", () => expect(parseEval('regexreplace(\"yes\", \"y\", \"no\")')).toEqual(\"noes\"));\n    test(\"full\", () => expect(parseEval('regexreplace(\"yes\", \"yes\", \"no\")')).toEqual(\"no\"));\n    test(\"regex\", () => expect(parseEval('regexreplace(\"yes\", \".+\", \"no\")')).toEqual(\"no\"));\n});\n\n// <-- containsword() -->\n\ndescribe(\"containsword()\", () => {\n    test(\"single word\", () => expect(parseEval('containsword(\"yes\", \"yes\")')).toEqual(true));\n    test(\"two word\", () => expect(parseEval('containsword(\"yes no\", \"no\")')).toEqual(true));\n    test(\"negative two word\", () => expect(parseEval('containsword(\"yes no\", \"maybe\")')).toEqual(false));\n    test(\"subword\", () => expect(parseEval('containsword(\"Hello there, chap!\", \"the\")')).toEqual(false));\n    test(\"punctuation\", () => expect(parseEval('containsword(\"Hello there, chap!\", \"there\")')).toEqual(true));\n    test(\"case insensitive\", () => expect(parseEval('containsword(\"Hello there, chap!\", \"hello\")')).toEqual(true));\n    test(\"case insensitive 2\", () => expect(parseEval('containsword(\"Hello there, chap!\", \"HELLO\")')).toEqual(true));\n});\n\n// <-- regextest() -->\n\ndescribe(\"regextest()\", () => {\n    test(\".+\", () => expect(parseEval('regextest(\".+\", \"stuff\")')).toEqual(true));\n    test(\".+ empty\", () => expect(parseEval('regextest(\".+\", \"\")')).toEqual(false));\n    test(\".* empty\", () => expect(parseEval('regextest(\".*\", \"\")')).toEqual(true));\n    test(\"word\", () => expect(parseEval('regextest(\"\\\\w+\", \"m3me\")')).toEqual(true));\n    test(\"whitespace\", () => expect(parseEval('regextest(\"\\\\s+\", \"  \")')).toEqual(true));\n    test(\"exact\", () => expect(parseEval('regextest(\"what\", \"what\")')).toEqual(true));\n    test(\"not exact\", () => expect(parseEval('regextest(\"what\", \"what are you doing?\")')).toEqual(true));\n    test(\"start & end\", () => expect(parseEval('regextest(\"^what$\", \"what\")')).toEqual(true));\n});\n\n// <-- regexmatch() -->\n\ndescribe(\"regexmatch()\", () => {\n    test(\".+\", () => expect(parseEval('regexmatch(\".+\", \"stuff\")')).toEqual(true));\n    test(\".+ empty\", () => expect(parseEval('regexmatch(\".+\", \"\")')).toEqual(false));\n    test(\".* empty\", () => expect(parseEval('regexmatch(\".*\", \"\")')).toEqual(true));\n    test(\"word\", () => expect(parseEval('regexmatch(\"\\\\w+\", \"m3me\")')).toEqual(true));\n    test(\"whitespace\", () => expect(parseEval('regexmatch(\"\\\\s+\", \"  \")')).toEqual(true));\n    test(\"exact\", () => expect(parseEval('regexmatch(\"what\", \"what\")')).toEqual(true));\n});\n\n// <-- replace() -- >\n\ndescribe(\"replace()\", () => {\n    test(\"letter\", () => expect(parseEval('replace(\"hello\", \"h\", \"me\")')).toEqual(\"meello\"));\n    test(\"full\", () => expect(parseEval('replace(\"meep\", \"meep\", \"pleh\")')).toEqual(\"pleh\"));\n    test(\"ignores regex\", () => expect(parseEval('replace(\"x.z\", \"x.\", \"z$\")')).toEqual(\"z$z\"));\n    test(\"ignores regex dot\", () => expect(parseEval('replace(\"x.z\", \".\", \"z\")')).toEqual(\"xzz\"));\n});\n\n// <-- lower/upper() -->\n\ndescribe(\"lower()\", () => {\n    test(\"idempotent\", () => expect(parseEval('lower(\"hello\")')).toEqual(\"hello\"));\n    test(\"Hello\", () => expect(parseEval('lower(\"Hello\")')).toEqual(\"hello\"));\n});\n\ndescribe(\"upper()\", () => {\n    test(\"Hello\", () => expect(parseEval('upper(\"Hello\")')).toEqual(\"HELLO\"));\n    test(\"hello\", () => expect(parseEval('upper(\"hello\")')).toEqual(\"HELLO\"));\n    test(\"idempotent\", () => expect(parseEval('upper(\"HELLO\")')).toEqual(\"HELLO\"));\n});\n\n// <-- split() -->\n\ntest(\"split(string, string)\", () => {\n    expect(parseEval(`split(\"hello world\", \" \")`)).toEqual(parseEval(`list(\"hello\", \"world\")`));\n    expect(parseEval(`split(\"hello world\", \"( )\")`)).toEqual(parseEval(`list(\"hello\", \" \", \"world\")`));\n    expect(parseEval(`split(\"hello  world\", \"\\\\s+\")`)).toEqual(parseEval(`list(\"hello\", \"world\")`));\n    expect(parseEval(`split(\"hello world\", \"x\")`)).toEqual(parseEval(`list(\"hello world\")`));\n    expect(parseEval(`split(\"hello world\", \"( )(x)?\")`)).toEqual(parseEval(`list(\"hello\", \" \", \"\", \"world\")`));\n    expect(parseEval(`split(\"hello there world\", \"(t?here)\")`)).toEqual(parseEval(`list(\"hello \", \"there\", \" world\")`));\n    expect(parseEval(`split(\"hello there world\", \"( )(x)?\")`)).toEqual(\n        parseEval(`list(\"hello\", \" \", \"\", \"there\", \" \", \"\", \"world\")`)\n    );\n});\n\ntest(\"split(string, string, limit)\", () => {\n    expect(parseEval(`split(\"hello world\", \" \", 0)`)).toEqual(parseEval(`list()`));\n    expect(parseEval(`split(\"hello world\", \" \", 1)`)).toEqual(parseEval(`list(\"hello\")`));\n    expect(parseEval(`split(\"hello world\", \" \", 3)`)).toEqual(parseEval(`list(\"hello\", \"world\")`));\n    expect(parseEval(`split(\"hello world\", \"( )\", 2)`)).toEqual(parseEval(`list(\"hello\", \" \")`));\n});\n\n// <-- startswith()/endswith() -->\n\ndescribe(\"startswith()\", () => {\n    test(\"(yes, ye)\", () => expect(parseEval(`startswith(\"yes\", \"ye\")`)).toEqual(true));\n    test(\"(Yes, ye)\", () => expect(parseEval(`startswith(\"Yes\", \"ye\")`)).toEqual(false));\n    test(\"(yes, no)\", () => expect(parseEval(`startswith(\"yes\", \"no\")`)).toEqual(false));\n});\n\ndescribe(\"endswith()\", () => {\n    test(\"(yes, ye)\", () => expect(parseEval(`endswith(\"yes\", \"ye\")`)).toEqual(false));\n    test(\"(yes, es)\", () => expect(parseEval(`endswith(\"yes\", \"es\")`)).toEqual(true));\n    test(\"(yes, no)\", () => expect(parseEval(`endswith(\"yes\", \"no\")`)).toEqual(false));\n});\n\n// <-- padleft()/padright() -->\n\ndescribe(\"padleft()\", () => {\n    test(\"(hello, 10)\", () => expect(parseEval(`padleft(\"hello\", 10)`)).toEqual(\"     hello\"));\n    test(\"(hello, 7)\", () => expect(parseEval(`padleft(\"hello\", 7)`)).toEqual(\"  hello\"));\n    test(\"(hello, 7, x)\", () => expect(parseEval(`padleft(\"hello\", 7, \"x\")`)).toEqual(\"xxhello\"));\n});\n\ndescribe(\"padright()\", () => {\n    test(\"(hello, 10)\", () => expect(parseEval(`padright(\"hello\", 10)`)).toEqual(\"hello     \"));\n    test(\"(hello, 7)\", () => expect(parseEval(`padright(\"hello\", 7)`)).toEqual(\"hello  \"));\n    test(\"(hello, 7, x)\", () => expect(parseEval(`padright(\"hello\", 7, \"x\")`)).toEqual(\"helloxx\"));\n    test(\"(, 1)\", () => expect(parseEval(`padright(\"\", 1)`)).toEqual(\" \"));\n});\n\ndescribe(\"substring()\", () => {\n    test(\"(yes, 1)\", () => expect(parseEval(`substring(\"yes\", 1)`)).toEqual(\"es\"));\n    test(\"(hello, 3)\", () => expect(parseEval(`substring(\"hello\", 3)`)).toEqual(\"lo\"));\n    test(\"(hello, 3, 20)\", () => expect(parseEval(`substring(\"hello\", 3, 20)`)).toEqual(\"lo\"));\n    test(\"(hello, 2, 4)\", () => expect(parseEval(`substring(\"hello\", 2, 4)`)).toEqual(\"ll\"));\n    test(\"(, 0)\", () => expect(parseEval(`substring(\"\", 0)`)).toEqual(\"\"));\n});\n\ndescribe(\"truncate()\", () => {\n    test(\"8\", () => check(`truncate(\"Hello There!\", 8)`).toEqual(\"Hello...\"));\n    test(\"10\", () => check(`truncate(\"Hello There!\", 10)`).toEqual(\"Hello T...\"));\n    test(\"10\", () => check(`truncate(\"Hello There!\", 10, \"!\")`).toEqual(\"Hello The!\"));\n});\n\nexport const check = (text: string) => expect(parseEval(text));\n"
  },
  {
    "path": "src/test/function/vectorization.test.ts",
    "content": "import { Literal } from \"data-model/value\";\nimport { parseEval } from \"test/common\";\n\ndescribe(\"Single List Argument\", () => {\n    test(\"replace(list, string, string)\", () => check('replace(list(\"yes\", \"re\"), \"e\", \"a\")', [\"yas\", \"ra\"]));\n\n    test(\"lower(list)\", () => check('lower([\"YES\", \"nO\"])', [\"yes\", \"no\"]));\n    test(\"upper(list)\", () => check('upper([\"okay\", \"yep\", \"1\"])', [\"OKAY\", \"YEP\", \"1\"]));\n});\n\ndescribe(\"Multi-List Arguments\", () => {\n    test(\"replace(list, list, string)\", () => check('replace([\"a\", \"b\", \"c\"], [\"a\", \"b\", \"c\"], \"d\")', [\"d\", \"d\", \"d\"]));\n    test(\"replace(list, string, list)\", () => check('replace([\"a\", \"b\", \"c\"], \"a\", [\"d\", \"e\", \"f\"])', [\"d\", \"b\", \"c\"]));\n    test(\"replace(list, list, list)\", () =>\n        check('replace([\"a\", \"b\", \"c\"], [\"a\", \"b\", \"c\"], [\"x\", \"y\", \"z\"])', [\"x\", \"y\", \"z\"]));\n});\n\nfunction check(statement: string, result: Literal) {\n    expect(parseEval(statement)).toEqual(result);\n}\n"
  },
  {
    "path": "src/test/markdown/parse.file.test.ts",
    "content": "import { extractTags } from \"data-import/markdown-file\";\nimport * as common from \"data-import/common\";\nimport { FrontMatterCache } from \"obsidian\";\n\ndescribe(\"Frontmatter Tags\", () => {\n    test(\"Empty\", () => expect(extractTags({} as FrontMatterCache)).toEqual([]));\n    test(\"No Tags\", () => expect(extractTags({ a: 1, b: 2 } as any as FrontMatterCache)).toEqual([]));\n    test(\"One Tag\", () => expect(extractTags({ tag: \"hello\" } as any as FrontMatterCache)).toEqual([\"#hello\"]));\n    test(\"Two Tag\", () =>\n        expect(extractTags({ tag: [\"hello\", \"goodbye\"] } as any as FrontMatterCache)).toEqual([\"#hello\", \"#goodbye\"]));\n    test(\"Two Tag String\", () =>\n        expect(extractTags({ tag: \"hello goodbye\" } as any as FrontMatterCache)).toEqual([\"#hello\", \"#goodbye\"]));\n    test(\"Two Tag String Comma\", () =>\n        expect(extractTags({ tag: \"hello, goodbye\" } as any as FrontMatterCache)).toEqual([\"#hello\", \"#goodbye\"]));\n});\n\ndescribe(\"Task Tags\", () => {\n    test(\"Empty\", () => expect(common.extractTags(\"hello\")).toEqual(new Set([])));\n    test(\"One Tag\", () => expect(common.extractTags(\"and text #hello\")).toEqual(new Set([\"#hello\"])));\n    test(\"Two Tags\", () =>\n        expect(common.extractTags(\"#and/thing text #hello\")).toEqual(new Set([\"#and/thing\", \"#hello\"])));\n    test(\"Comma Delimited\", () =>\n        expect(common.extractTags(\"#one,#two, #three\")).toEqual(new Set([\"#one\", \"#two\", \"#three\"])));\n    test(\"Semicolon Delimited\", () =>\n        expect(common.extractTags(\"#one;;;#two; #three\")).toEqual(new Set([\"#one\", \"#two\", \"#three\"])));\n    test(\"Parenthesis\", () =>\n        expect(common.extractTags(\"[#one]]#two;;#four()() #three\")).toEqual(\n            new Set([\"#one\", \"#two\", \"#four\", \"#three\"])\n        ));\n});\n"
  },
  {
    "path": "src/test/parse/parse.expression.test.ts",
    "content": "import { BinaryOpField, Fields, LiteralField } from \"expression/field\";\nimport { EXPRESSION } from \"expression/parse\";\nimport { DateTime, Duration } from \"luxon\";\nimport { Success } from \"parsimmon\";\nimport { Sources } from \"data-index/source\";\nimport { Values, Link } from \"data-model/value\";\n\n// <-- Integer Literals -->\n\ntest(\"Parse Integer Literal\", () => {\n    expect(EXPRESSION.number.parse(\"0no\").status).toBe(false);\n    expect(EXPRESSION.number.tryParse(\"123\")).toBe(123);\n    expect(EXPRESSION.number.tryParse(\"-123\")).toBe(-123);\n});\n\ntest(\"Parse Float Literal\", () => {\n    expect(EXPRESSION.number.tryParse(\"123.45\")).toBeCloseTo(123.45);\n    expect(EXPRESSION.number.tryParse(\"1000.0\")).toBeCloseTo(1000);\n    expect(EXPRESSION.number.tryParse(\"-123.18\")).toBe(-123.18);\n    expect(EXPRESSION.number.parse(\"123.0.0\").status).toBe(false);\n});\n\n// <-- String Literals -->\n\ndescribe(\"String Literals\", () => {\n    test(\"Parse String Literal\", () => {\n        expect(EXPRESSION.string.parse(`this won't work, no quotes`).status).toBe(false);\n        expect(EXPRESSION.string.tryParse(`\"hello\"`)).toBe(\"hello\");\n\n        expect(EXPRESSION.string.tryParse(`\"\\\\\"\"`)).toBe('\"');\n        expect(EXPRESSION.string.parse(`\"\\\\\\\\\"\"`).status).toBe(false);\n\n        // Test case which failed on old regex\n        expect(EXPRESSION.string.tryParse(`\"\\\\\\\\\\\\\"\"`)).toBe(`\\\\\"`);\n\n        // Testcase for escape in regex strings.\n        expect(EXPRESSION.string.tryParse('\"\\\\w+\"')).toBe(\"\\\\w+\");\n    });\n\n    test(\"Parse Empty String Literal\", () => {\n        expect(EXPRESSION.string.tryParse('\"\"')).toBe(\"\");\n    });\n\n    test(\"Parse String Escape\", () => {\n        expect(EXPRESSION.string.tryParse('\"\\\\\"\"')).toBe('\"');\n    });\n\n    test(\"Parse String Escape Escape\", () => {\n        expect(EXPRESSION.string.tryParse('\"\\\\\\\\\"')).toBe(\"\\\\\");\n    });\n\n    test(\"Parse Multiple Strings\", () => {\n        let result = EXPRESSION.field.tryParse('\"\" or \"yes\"') as BinaryOpField;\n        expect(result.type).toBe(\"binaryop\");\n\n        let left = result.left as LiteralField;\n        expect(left.type).toBe(\"literal\");\n        expect(left.value).toBe(\"\");\n\n        let right = result.right as LiteralField;\n        expect(right.type).toBe(\"literal\");\n        expect(right.value).toBe(\"yes\");\n    });\n\n    test(\"Parse emoji\", () => {\n        expect(EXPRESSION.string.tryParse('\"📷\"')).toEqual(\"📷\");\n        expect(EXPRESSION.string.tryParse('\"⚙️\"')).toEqual(\"⚙️\");\n    });\n\n    test(\"Parse string which includes emoji\", () => {\n        expect(EXPRESSION.string.tryParse('\"⚗️ KNOWLEDGE\"')).toEqual(\"⚗️ KNOWLEDGE\");\n    });\n});\n\n// <-- Booleans -->\n\ntest(\"Parse boolean literal\", () => {\n    expect(EXPRESSION.bool.tryParse(\"true\")).toBe(true);\n    expect(EXPRESSION.bool.tryParse(\"false\")).toBe(false);\n    expect(EXPRESSION.bool.parse(\"fal\").status).toBe(false);\n});\n\n// <-- Tags -->\n\ndescribe(\"Tag Literals\", () => {\n    test(\"Daily\", () => expect(EXPRESSION.tag.tryParse(\"#daily/2021/20/08\")).toEqual(\"#daily/2021/20/08\"));\n    test(\"Dashes\", () =>\n        expect(EXPRESSION.tag.tryParse(\"#hello-from-marketing/yes\")).toEqual(\"#hello-from-marketing/yes\"));\n\n    test(\"#📷\", () => expect(EXPRESSION.tag.tryParse(\"#📷\")).toEqual(\"#📷\"));\n    test(\"#🌱/🌿\", () => expect(EXPRESSION.tag.tryParse(\"#🌱/🌿\")).toEqual(\"#🌱/🌿\"));\n    test(\"#⚙️\", () => expect(EXPRESSION.tag.tryParse(\"#⚙️\")).toEqual(\"#⚙️\"));\n    test(\"#début\", () => expect(EXPRESSION.tag.tryParse(\"#début\")).toEqual(\"#début\"));\n});\n\n// <-- Identifiers -->\n\ndescribe(\"Identifiers\", () => {\n    test(\"lma0\", () => expect(EXPRESSION.identifier.tryParse(\"lma0\")).toEqual(\"lma0\"));\n    test(\"0no\", () => expect(EXPRESSION.identifier.parse(\"0no\").status).toBeFalsy());\n    test(\"a*b\", () => expect(EXPRESSION.identifier.parse(\"a*b\").status).toBeFalsy());\n    test(\"😊\", () => expect(EXPRESSION.identifier.tryParse(\"😊\")).toEqual(\"😊\"));\n    test(\"📷\", () => expect(EXPRESSION.identifier.tryParse(\"📷\")).toEqual(\"📷\"));\n});\n\n// <-- Dates -->\n\ntest(\"Parse Year-Month date\", () => {\n    let date = EXPRESSION.date.tryParse(\"2020-04\");\n    expect(date.year).toBe(2020);\n    expect(date.month).toBe(4);\n});\n\ntest(\"Parse Year-Month-Day date\", () => {\n    let date = EXPRESSION.date.tryParse(\"1984-08-15\");\n    expect(date.year).toBe(1984);\n    expect(date.month).toBe(8);\n    expect(date.day).toBe(15);\n});\n\ntest(\"Parse Year-Month-DayTHour:Minute:Second\", () => {\n    let date = EXPRESSION.date.tryParse(\"1984-08-15T12:42:59\");\n    expect(date.year).toBe(1984);\n    expect(date.month).toBe(8);\n    expect(date.day).toBe(15);\n    expect(date.hour).toBe(12);\n    expect(date.minute).toBe(42);\n    expect(date.second).toBe(59);\n});\n\ntest(\"Parse Year-Month-DayTHour:Minute:Second\", () => {\n    let date = EXPRESSION.date.tryParse(\"1984-08-15T12:42:59\");\n    expect(date.year).toBe(1984);\n    expect(date.month).toBe(8);\n    expect(date.day).toBe(15);\n    expect(date.hour).toBe(12);\n    expect(date.minute).toBe(42);\n    expect(date.second).toBe(59);\n});\n\ntest(\"Parse Year-Month-DayTHour:Minute:Second.Millisecond\", () => {\n    let date = EXPRESSION.date.tryParse(\"1984-08-15T12:42:59.123\");\n    expect(date.year).toBe(1984);\n    expect(date.month).toBe(8);\n    expect(date.day).toBe(15);\n    expect(date.hour).toBe(12);\n    expect(date.minute).toBe(42);\n    expect(date.second).toBe(59);\n    expect(date.millisecond).toBe(123);\n\n    let builtin = EXPRESSION.date.tryParse(new Date(\"1984-08-15T12:42:59.123\").toISOString());\n    // only seconds and milliseconds are inconsistent due to Javascript being bad with\n    // time zones, but the goal here is to ensure values are parsed appropriately at least\n    expect(builtin.second).toBe(59);\n    expect(builtin.millisecond).toBe(123);\n});\n\ndescribe(\"Parse Year-Month-DayTHour:Minute:Second(.Millisecond?)Timezone\", () => {\n    test(\"Offset\", () => {\n        let date1 = EXPRESSION.date.tryParse(\"1984-08-15T12:40:50-07:00\");\n        expect(date1.year).toBe(1984);\n        expect(date1.month).toBe(8);\n        expect(date1.day).toBe(15);\n        expect(date1.hour).toBe(12);\n        expect(date1.minute).toBe(40);\n        expect(date1.second).toBe(50);\n        expect(date1.millisecond).toBe(0);\n        expect(date1.zoneName).toBe(\"UTC-7\");\n\n        let date2 = EXPRESSION.date.tryParse(\"1984-08-15T12:40:50+9\");\n        expect(date2.zoneName).toBe(\"UTC+9\");\n\n        let date3 = EXPRESSION.date.tryParse(\"1985-12-06T19:40:10+06:30\");\n        expect(date3.zoneName).toBe(\"UTC+6:30\");\n    });\n\n    test(\"Named timezone\", () => {\n        let date1 = EXPRESSION.date.tryParse(\"2021-08-15T12:40:50[Europe/Paris]\");\n        expect(date1.year).toBe(2021);\n        expect(date1.month).toBe(8);\n        expect(date1.day).toBe(15);\n        expect(date1.hour).toBe(12);\n        expect(date1.minute).toBe(40);\n        expect(date1.second).toBe(50);\n        expect(date1.millisecond).toBe(0);\n        expect(date1.toString()).toBe(\"2021-08-15T12:40:50.000+02:00\");\n        expect(date1.zoneName).toBe(\"Europe/Paris\");\n\n        let date2 = EXPRESSION.date.tryParse(\"2021-11-15T12:40:50[Europe/Paris]\");\n        expect(date2.year).toBe(2021);\n        expect(date2.month).toBe(11);\n        expect(date2.day).toBe(15);\n        expect(date2.hour).toBe(12);\n        expect(date2.minute).toBe(40);\n        expect(date2.second).toBe(50);\n        expect(date2.millisecond).toBe(0);\n        expect(date2.toString()).toBe(\"2021-11-15T12:40:50.000+01:00\");\n        expect(date2.zoneName).toBe(\"Europe/Paris\");\n    });\n\n    test(\"Z\", () => {\n        let date1 = EXPRESSION.date.tryParse(\"1985-12-06T19:40:10Z\");\n        expect(date1.year).toBe(1985);\n        expect(date1.month).toBe(12);\n        expect(date1.day).toBe(6);\n        expect(date1.hour).toBe(19);\n        expect(date1.minute).toBe(40);\n        expect(date1.second).toBe(10);\n        expect(date1.millisecond).toBe(0);\n        expect(date1.zoneName).toBe(\"UTC\");\n\n        let date2 = EXPRESSION.date.tryParse(\"1985-12-06T19:40:10.123Z\");\n        expect(date2.zoneName).toBe(\"UTC\");\n\n        // built-in always returns UTC\n        let date3 = EXPRESSION.date.tryParse(new Date().toISOString());\n        expect(date3.zoneName).toBe(\"UTC\");\n    });\n});\n\ntest(\"Parse invalid date\", () => expect(EXPRESSION.date.parse(\"4237-14-73\").status).toBeFalsy());\n\ntest(\"Parse Today\", () => {\n    let date = EXPRESSION.dateField.tryParse(\"date(today)\") as LiteralField;\n    expect(Values.isDate(date.value)).toEqual(true);\n    expect(date.value).toEqual(DateTime.local().startOf(\"day\"));\n});\n\n// <-- Durations -->\n\ndescribe(\"Durations\", () => {\n    test(\"6 days\", () => {\n        let day = EXPRESSION.duration.tryParse(\"6 days\");\n        let day2 = EXPRESSION.duration.tryParse(\"6day\");\n\n        expect(day).toEqual(day2);\n        expect(day).toEqual(Duration.fromObject({ days: 6 }));\n    });\n\n    test(\"4 minutes\", () => {\n        let min = EXPRESSION.duration.tryParse(\"4min\");\n        let min2 = EXPRESSION.duration.tryParse(\"4 minutes\");\n        let min3 = EXPRESSION.duration.tryParse(\"4 minute\");\n\n        expect(min).toEqual(min2);\n        expect(min).toEqual(min3);\n        expect(min).toEqual(Duration.fromObject({ minutes: 4 }));\n    });\n\n    test(\"4 hours 15 minutes\", () => {\n        let dur = EXPRESSION.duration.tryParse(\"4 hr 15 min\");\n        let dur2 = EXPRESSION.duration.tryParse(\"4h15m\");\n        let dur3 = EXPRESSION.duration.tryParse(\"4 hours, 15 minutes\");\n\n        expect(dur).toEqual(dur2);\n        expect(dur).toEqual(dur3);\n        expect(dur).toEqual(Duration.fromObject({ hours: 4, minutes: 15 }));\n    });\n\n    test(\"4 years 6 weeks 9 minutes 3 seconds\", () => {\n        let dur = EXPRESSION.duration.tryParse(\"4 years 6 weeks 9 minutes 3 seconds\");\n        let dur2 = EXPRESSION.duration.tryParse(\"4yr6w9m3s\");\n        let dur3 = EXPRESSION.duration.tryParse(\"4 yrs, 6 wks, 9 mins, 3 s\");\n\n        expect(dur).toEqual(dur2);\n        expect(dur).toEqual(dur3);\n        expect(dur).toEqual(Duration.fromObject({ years: 4, weeks: 6, minutes: 9, seconds: 3 }));\n    });\n});\n\n// <-- Links -->\n\ndescribe(\"Parse Link\", () => {\n    test(\"simple\", () =>\n        expect(EXPRESSION.field.tryParse(\"[[test/Main]]\")).toEqual(Fields.literal(Link.file(\"test/Main\", false))));\n    test(\"extension\", () =>\n        expect(EXPRESSION.field.tryParse(\"[[test/Main.md]]\")).toEqual(\n            Fields.literal(Link.file(\"test/Main.md\", false))\n        ));\n    test(\"number\", () =>\n        expect(EXPRESSION.field.tryParse(\"[[simple0]]\")).toEqual(Fields.literal(Link.file(\"simple0\", false))));\n    test(\"date\", () =>\n        expect(EXPRESSION.field.tryParse(\"[[2020-08-15]]\")).toEqual(Fields.literal(Link.file(\"2020-08-15\", false))));\n    test(\"glyphs\", () =>\n        expect(EXPRESSION.field.tryParse(\"[[%Man & Machine + Mind%]]\")).toEqual(\n            Fields.literal(Link.file(\"%Man & Machine + Mind%\", false))\n        ));\n\n    test(\"escaped pipe\", () =>\n        expect(EXPRESSION.link.tryParse(\"[[Hello \\\\| There]]\")).toEqual(Link.file(\"Hello | There\")));\n    test(\"escaped pipe with display\", () =>\n        expect(EXPRESSION.link.tryParse(\"[[\\\\||Yes]]\")).toEqual(Link.file(\"|\", false, \"Yes\")));\n});\n\ntest(\"Parse link with display\", () => {\n    expect(EXPRESSION.field.tryParse(\"[[test/Main|Yes]]\")).toEqual(\n        Fields.literal(Link.file(\"test/Main\", false, \"Yes\"))\n    );\n    expect(EXPRESSION.field.tryParse(\"[[%Man + Machine%|0h no]]\")).toEqual(\n        Fields.literal(Link.file(\"%Man + Machine%\", false, \"0h no\"))\n    );\n});\n\ntest(\"Parse link with header/block\", () => {\n    expect(EXPRESSION.field.tryParse(\"[[test/Main#Yes]]\")).toEqual(\n        Fields.literal(Link.header(\"test/Main\", \"Yes\", false))\n    );\n    expect(EXPRESSION.field.tryParse(\"[[2020#^14df]]\")).toEqual(Fields.literal(Link.block(\"2020\", \"14df\", false)));\n});\n\ntest(\"Parse link with header and display\", () => {\n    expect(EXPRESSION.field.tryParse(\"[[test/Main#what|Yes]]\")).toEqual(\n        Fields.literal(Link.header(\"test/Main\", \"what\", false, \"Yes\"))\n    );\n    expect(EXPRESSION.field.tryParse(\"[[%Man + Machine%#^no|0h no]]\")).toEqual(\n        Fields.literal(Link.block(\"%Man + Machine%\", \"no\", false, \"0h no\"))\n    );\n});\n\ntest(\"Parse embedded link\", () => {\n    expect(EXPRESSION.field.tryParse(\"![[hello]]\")).toEqual(Fields.literal(Link.file(\"hello\", true)));\n});\n\n// <-- Null ->\n\ntest(\"Parse Null\", () => {\n    expect(EXPRESSION.field.tryParse(\"null\")).toEqual(Fields.NULL);\n    expect(EXPRESSION.field.tryParse('\"null\"')).toEqual(Fields.literal(\"null\"));\n});\n\n// <-- Indexes -->\n\ntest(\"Parse Dot Notation\", () => {\n    expect(EXPRESSION.field.tryParse(\"Dates.Birthday\")).toEqual(\n        Fields.index(Fields.variable(\"Dates\"), Fields.literal(\"Birthday\"))\n    );\n    expect(EXPRESSION.field.tryParse(\"a.b.c3\")).toEqual(\n        Fields.index(Fields.index(Fields.variable(\"a\"), Fields.literal(\"b\")), Fields.literal(\"c3\"))\n    );\n});\n\ntest(\"Parse Index Notation\", () => {\n    expect(EXPRESSION.field.tryParse(\"a[0]\")).toEqual(Fields.index(Fields.variable(\"a\"), Fields.literal(0)));\n    expect(EXPRESSION.field.tryParse('\"hello\"[0]')).toEqual(Fields.index(Fields.literal(\"hello\"), Fields.literal(0)));\n    expect(EXPRESSION.field.tryParse(\"hello[brain]\")).toEqual(\n        Fields.index(Fields.variable(\"hello\"), Fields.variable(\"brain\"))\n    );\n});\n\ntest(\"Parse Mixed Index/Dot Notation\", () => {\n    expect(EXPRESSION.field.tryParse(\"a.b[0]\")).toEqual(\n        Fields.index(Fields.index(Fields.variable(\"a\"), Fields.literal(\"b\")), Fields.literal(0))\n    );\n    expect(EXPRESSION.field.tryParse('\"hello\".what[yes]')).toEqual(\n        Fields.index(Fields.index(Fields.literal(\"hello\"), Fields.literal(\"what\")), Fields.variable(\"yes\"))\n    );\n});\n\ntest(\"Parse negated index\", () => {\n    expect(EXPRESSION.field.tryParse(\"!a[b]\")).toEqual(\n        Fields.negate(Fields.index(Fields.variable(\"a\"), Fields.variable(\"b\")))\n    );\n    expect(EXPRESSION.field.tryParse(\"!a.b\")).toEqual(\n        Fields.negate(Fields.index(Fields.variable(\"a\"), Fields.literal(\"b\")))\n    );\n});\n\n// <-- Functions -->\n\ntest(\"Parse function with no arguments\", () => {\n    expect(EXPRESSION.field.tryParse(\"hello()\")).toEqual(Fields.func(Fields.variable(\"hello\"), []));\n    expect(EXPRESSION.field.tryParse(\"lma0()\")).toEqual(Fields.func(Fields.variable(\"lma0\"), []));\n});\n\ntest(\"Parse function with arguments\", () => {\n    expect(EXPRESSION.field.tryParse(\"list(1, 2, 3)\")).toEqual(\n        Fields.func(Fields.variable(\"list\"), [Fields.literal(1), Fields.literal(2), Fields.literal(3)])\n    );\n    expect(EXPRESSION.field.tryParse('object(\"a\", 1, \"b\", 2)')).toEqual(\n        Fields.func(Fields.variable(\"object\"), [\n            Fields.literal(\"a\"),\n            Fields.literal(1),\n            Fields.literal(\"b\"),\n            Fields.literal(2),\n        ])\n    );\n});\n\ntest(\"Parse function with duration\", () => {\n    expect(EXPRESSION.field.tryParse(\"today() + dur(4hr)\")).toEqual(\n        Fields.binaryOp(\n            Fields.func(Fields.variable(\"today\"), []),\n            \"+\",\n            Fields.literal(Duration.fromObject({ hours: 4 }))\n        )\n    );\n});\n\ntest(\"Parse null duration\", () => {\n    expect(EXPRESSION.field.tryParse(\"dur(null)\")).toEqual(Fields.func(Fields.variable(\"dur\"), [Fields.literal(null)]));\n    expect(EXPRESSION.field.tryParse('dur(\"null\")')).toEqual(\n        Fields.func(Fields.variable(\"dur\"), [Fields.literal(\"null\")])\n    );\n});\n\ntest(\"Parse function with null duration\", () => {\n    expect(EXPRESSION.field.tryParse(\"today() + dur(null)\")).toEqual(\n        Fields.binaryOp(\n            Fields.func(Fields.variable(\"today\"), []),\n            \"+\",\n            Fields.func(Fields.variable(\"dur\"), [Fields.literal(null)])\n        )\n    );\n});\n\ntest(\"Parse date +/- null\", () => {\n    expect(EXPRESSION.field.tryParse(\"today() + null\")).toEqual(\n        Fields.binaryOp(Fields.func(Fields.variable(\"today\"), []), \"+\", Fields.literal(null))\n    );\n    expect(EXPRESSION.field.tryParse(\"today() - null\")).toEqual(\n        Fields.binaryOp(Fields.func(Fields.variable(\"today\"), []), \"-\", Fields.literal(null))\n    );\n});\n\ntest(\"Parse function with mixed dot, index, and function call\", () => {\n    expect(EXPRESSION.field.tryParse(\"list().parts[0]\")).toEqual(\n        Fields.index(Fields.index(Fields.func(Fields.variable(\"list\"), []), Fields.literal(\"parts\")), Fields.literal(0))\n    );\n});\n\n// <-- Lambdas -->\ndescribe(\"Lambda Expressions\", () => {\n    test(\"Parse 0-argument constant lambda\", () => {\n        expect(EXPRESSION.field.tryParse(\"() => 16\")).toEqual(Fields.lambda([], Fields.literal(16)));\n    });\n\n    test(\"Parse 0-argument binary op lambda\", () => {\n        expect(EXPRESSION.field.tryParse(\"() => a + 2\")).toEqual(\n            Fields.lambda([], Fields.binaryOp(Fields.variable(\"a\"), \"+\", Fields.literal(2)))\n        );\n    });\n\n    test(\"Parse 1-argument lambda\", () => {\n        expect(EXPRESSION.field.tryParse(\"(v) => v\")).toEqual(Fields.lambda([\"v\"], Fields.variable(\"v\")));\n    });\n\n    test(\"Parse 2-argument lambda\", () => {\n        expect(EXPRESSION.field.tryParse(\"(yes, no) => yes - no\")).toEqual(\n            Fields.lambda([\"yes\", \"no\"], Fields.binaryOp(Fields.variable(\"yes\"), \"-\", Fields.variable(\"no\")))\n        );\n    });\n});\n\n// <-- Lists -->\n\ndescribe(\"Lists\", () => {\n    test(\"[]\", () => expect(EXPRESSION.field.tryParse(\"[]\")).toEqual(Fields.list([])));\n    test(\"[1]\", () => expect(EXPRESSION.field.tryParse(\"[1]\")).toEqual(Fields.list([Fields.literal(1)])));\n    test(\"[1, 2]\", () =>\n        expect(EXPRESSION.field.tryParse(\"[1,2]\")).toEqual(Fields.list([Fields.literal(1), Fields.literal(2)])));\n    test(\"[1, 2, 3]\", () =>\n        expect(EXPRESSION.field.tryParse(\"[ 1,  2, 3   ]\")).toEqual(\n            Fields.list([Fields.literal(1), Fields.literal(2), Fields.literal(3)])\n        ));\n\n    test('[\"a\"]', () => expect(EXPRESSION.field.tryParse('[\"a\" ]')).toEqual(Fields.list([Fields.literal(\"a\")])));\n\n    test(\"[[]]\", () => expect(EXPRESSION.field.tryParse(\"[ [] ]\")).toEqual(Fields.list([Fields.list([])])));\n});\n\n// <-- Objects -->\n\ndescribe(\"Objects\", () => {\n    test(\"{}\", () => expect(EXPRESSION.field.tryParse(\"{}\")).toEqual(Fields.object({})));\n    test(\"{ a: 1 }\", () =>\n        expect(EXPRESSION.field.tryParse(\"{ a: 1 }\")).toEqual(Fields.object({ a: Fields.literal(1) })));\n    test('{ \"a\": 1 }', () =>\n        expect(EXPRESSION.field.tryParse('{ \"a\": 1 }')).toEqual(Fields.object({ a: Fields.literal(1) })));\n    test('{ \"yes no\": 1 }', () =>\n        expect(EXPRESSION.field.tryParse('{ \"yes no\": 1 }')).toEqual(Fields.object({ \"yes no\": Fields.literal(1) })));\n\n    test(\"{a:1,b:[2]}\", () =>\n        expect(EXPRESSION.field.tryParse(\"{ a: 1, b: [2] }\")).toEqual(\n            Fields.object({ a: Fields.literal(1), b: Fields.list([Fields.literal(2)]) })\n        ));\n});\n\n// <-- Binary Ops -->\n\ndescribe(\"Binary Operators\", () => {\n    test(\"Simple Addition\", () => {\n        let result = EXPRESSION.binaryOpField.parse('16 + \"what\"') as Success<BinaryOpField>;\n        expect(result.status).toBe(true);\n        expect(result.value).toEqual(Fields.binaryOp(Fields.literal(16), \"+\", Fields.literal(\"what\")));\n    });\n\n    test(\"Simple Division\", () => {\n        expect(EXPRESSION.field.tryParse(\"14 / 2\")).toEqual(\n            Fields.binaryOp(Fields.literal(14), \"/\", Fields.literal(2))\n        );\n        expect(EXPRESSION.field.tryParse(\"31 / 9.0\")).toEqual(\n            Fields.binaryOp(Fields.literal(31), \"/\", Fields.literal(9.0))\n        );\n    });\n\n    test(\"Simple Modulo\", () => {\n        expect(EXPRESSION.field.tryParse(\"14 % 2\")).toEqual(\n            Fields.binaryOp(Fields.literal(14), \"%\", Fields.literal(2))\n        );\n        expect(EXPRESSION.field.tryParse(\"31 % 9.0\")).toEqual(\n            Fields.binaryOp(Fields.literal(31), \"%\", Fields.literal(9.0))\n        );\n    });\n\n    test(\"Multiplication (No Spaces)\", () => {\n        expect(EXPRESSION.field.tryParse(\"3*a\")).toEqual(Fields.binaryOp(Fields.literal(3), \"*\", Fields.variable(\"a\")));\n    });\n\n    test(\"Parenthesis\", () => {\n        let result = EXPRESSION.field.parse(\"(16 - 4) - 8\") as Success<BinaryOpField>;\n        expect(result.status).toBe(true);\n        expect(result.value).toEqual(\n            Fields.binaryOp(Fields.binaryOp(Fields.literal(16), \"-\", Fields.literal(4)), \"-\", Fields.literal(8))\n        );\n    });\n\n    test(\"Order of Operations\", () => {\n        expect(EXPRESSION.field.tryParse(\"14 + 6 >= 19 - 2\")).toEqual(\n            Fields.binaryOp(\n                Fields.binaryOp(Fields.literal(14), \"+\", Fields.literal(6)),\n                \">=\",\n                Fields.binaryOp(Fields.literal(19), \"-\", Fields.literal(2))\n            )\n        );\n    });\n});\n\n// <-- Negation -->\n\ntest(\"Parse Negated field\", () => {\n    expect(EXPRESSION.field.tryParse(\"!true\")).toEqual(Fields.negate(Fields.literal(true)));\n    expect(EXPRESSION.field.tryParse(\"!14\")).toEqual(Fields.negate(Fields.literal(14)));\n    expect(EXPRESSION.field.tryParse(\"!neat(0)\")).toEqual(\n        Fields.negate(Fields.func(Fields.variable(\"neat\"), [Fields.literal(0)]))\n    );\n    expect(EXPRESSION.field.tryParse(\"!!what\")).toEqual(Fields.negate(Fields.negate(Fields.variable(\"what\"))));\n});\n\ntest(\"Parse binaryop negated field\", () => {\n    expect(EXPRESSION.field.tryParse(\"!(true & false)\")).toEqual(\n        Fields.negate(Fields.binaryOp(Fields.literal(true), \"&\", Fields.literal(false)))\n    );\n    expect(EXPRESSION.field.tryParse(\"true & !false\")).toEqual(\n        Fields.binaryOp(Fields.literal(true), \"&\", Fields.negate(Fields.literal(false)))\n    );\n});\n\n// <-- Sources -->\n\ntest(\"Parse simple sources\", () => {\n    expect(EXPRESSION.source.tryParse('\"hello\"')).toEqual(Sources.folder(\"hello\"));\n    expect(EXPRESSION.source.tryParse(\"#neat\")).toEqual(Sources.tag(\"#neat\"));\n    expect(EXPRESSION.source.tryParse('csv(\"data/a.csv\")')).toEqual(Sources.csv(\"data/a.csv\"));\n});\n\ntest(\"Parse negated source\", () => {\n    expect(EXPRESSION.source.tryParse('-\"hello\"')).toEqual(Sources.negate(Sources.folder(\"hello\")));\n    expect(EXPRESSION.source.tryParse('!\"hello\"')).toEqual(Sources.negate(Sources.folder(\"hello\")));\n    expect(EXPRESSION.source.tryParse(\"-#neat\")).toEqual(Sources.negate(Sources.tag(\"#neat\")));\n    expect(EXPRESSION.source.tryParse(\"!#neat\")).toEqual(Sources.negate(Sources.tag(\"#neat\")));\n});\n\ntest(\"Parse parens source\", () => {\n    expect(EXPRESSION.source.tryParse('(\"lma0\")')).toEqual(Sources.folder(\"lma0\"));\n    expect(EXPRESSION.source.tryParse(\"(#neat0)\")).toEqual(Sources.tag(\"#neat0\"));\n});\n\ntest(\"Parse binary source\", () => {\n    expect(EXPRESSION.source.tryParse('\"lma0\" or #neat')).toEqual(\n        Sources.binaryOp(Sources.folder(\"lma0\"), \"|\", Sources.tag(\"#neat\"))\n    );\n\n    expect(EXPRESSION.source.tryParse('\"meme\" & #dirty')).toEqual(\n        Sources.binaryOp(Sources.folder(\"meme\"), \"&\", Sources.tag(\"#dirty\"))\n    );\n});\n\ntest(\"Parse negated parens source\", () => {\n    expect(EXPRESSION.source.tryParse(\"-(#neat)\")).toEqual(Sources.negate(Sources.tag(\"#neat\")));\n    expect(EXPRESSION.source.tryParse(\"!(#neat)\")).toEqual(Sources.negate(Sources.tag(\"#neat\")));\n    expect(EXPRESSION.source.tryParse('-(\"meme\" & #dirty)')).toEqual(\n        Sources.negate(Sources.binaryOp(Sources.folder(\"meme\"), \"&\", Sources.tag(\"#dirty\")))\n    );\n    expect(EXPRESSION.source.tryParse('!(\"meme\" & #dirty)')).toEqual(\n        Sources.negate(Sources.binaryOp(Sources.folder(\"meme\"), \"&\", Sources.tag(\"#dirty\")))\n    );\n    expect(EXPRESSION.source.tryParse('-\"meme\" & #dirty')).toEqual(\n        Sources.binaryOp(Sources.negate(Sources.folder(\"meme\")), \"&\", Sources.tag(\"#dirty\"))\n    );\n    expect(EXPRESSION.source.tryParse('!\"meme\" & #dirty')).toEqual(\n        Sources.binaryOp(Sources.negate(Sources.folder(\"meme\")), \"&\", Sources.tag(\"#dirty\"))\n    );\n});\n\n// TODO: refactor this; currently cannot test file.ts because it imports obsidian\n// import { parseTaskForAnnotations } from \"data/file\";\n// test(\"Parse Task for inline annotations\", () => {\n//     expect(parseTaskForAnnotations(\"key::value\")).toEqual({ key: \"value\" });\n//     expect(parseTaskForAnnotations(\"key key::value value\")).toEqual({ key: \"value\" });\n//     expect(parseTaskForAnnotations(\"key::value key2::value2\")).toEqual({ key: \"value\", key2: \"value2\" });\n//     expect(parseTaskForAnnotations(\"some dueDate::2021-09-21\")).toEqual({ dueDate: DateTime.fromISO(\"2021-09-21\") });\n// });\n\n// <-- Stress Tests -->\n\ntest(\"Parse Various Fields\", () => {\n    expect(EXPRESSION.field.tryParse('list(a, \"b\", 3, [[4]])')).toEqual(\n        Fields.func(Fields.variable(\"list\"), [\n            Fields.variable(\"a\"),\n            Fields.literal(\"b\"),\n            Fields.literal(3),\n            Fields.literal(Link.file(\"4\", false)),\n        ])\n    );\n});\n"
  },
  {
    "path": "src/test/parse/parse.inline.test.ts",
    "content": "import { EXPRESSION } from \"expression/parse\";\nimport { Link } from \"data-model/value\";\nimport { extractInlineFields, setEmojiShorthandCompletionField, setInlineField } from \"data-import/inline-field\";\n\n// <-- Inline field weird edge cases -->\n\ntest(\"Parse commas inside inline field\", () => {\n    expect(EXPRESSION.inlineField.tryParse(\"[[yes, no, and maybe]]\")).toEqual(Link.file(\"yes, no, and maybe\"));\n});\n\n// <-- Inline Field Lists -->\n\ndescribe(\"Inline Field Values\", () => {\n    test(\"Parse basic inline fields\", () => {\n        expect(EXPRESSION.inlineField.tryParse(\"14\")).toEqual(14);\n        expect(EXPRESSION.inlineField.tryParse('\"yes,\"')).toEqual(\"yes,\");\n        expect(EXPRESSION.inlineField.tryParse(\"[[test]]\")).toEqual(Link.file(\"test\"));\n    });\n\n    test(\"Parse inline field lists\", () => {\n        expect(EXPRESSION.inlineField.tryParse(\"[[test]],\")).toEqual([Link.file(\"test\")]);\n        expect(EXPRESSION.inlineField.tryParse(\"[[test]], [[test2]]\")).toEqual([Link.file(\"test\"), Link.file(\"test2\")]);\n        expect(EXPRESSION.inlineField.tryParse('1, 2, 3, \"hello\"')).toEqual([1, 2, 3, \"hello\"]);\n    });\n\n    test(\"Parse inline booleans\", () => {\n        expect(EXPRESSION.inlineField.tryParse(\"true\")).toEqual(true);\n        expect(EXPRESSION.inlineField.tryParse(\"False\")).toEqual(false);\n    });\n});\n\n// \"Inline Inline\" Fields\n\ndescribe(\"Simple Inline Inline\", () => {\n    test(\"Empty\", () => expect(extractInlineFields(\"\")).toEqual([]));\n    test(\"Empty Brackets\", () => expect(extractInlineFields(\"[]\")).toEqual([]));\n    test(\"Empty Parenthesis\", () => expect(extractInlineFields(\"()\")).toEqual([]));\n\n    test(\"Incorrect Brackets\", () => expect(extractInlineFields(\"[key:value]\")).toEqual([]));\n    test(\"Incorrect Parenthesis\", () => expect(extractInlineFields(\"(key:value)\")).toEqual([]));\n\n    test(\"Trivial Brackets\", () =>\n        expect(extractInlineFields(\"[key::value]\")).toEqual([\n            {\n                key: \"key\",\n                value: \"value\",\n                start: 0,\n                startValue: 6,\n                end: 12,\n                wrapping: \"[\",\n            },\n        ]));\n\n    test(\"Trivial Parenthesis\", () =>\n        expect(extractInlineFields(\"(key::value)\")).toEqual([\n            {\n                key: \"key\",\n                value: \"value\",\n                start: 0,\n                startValue: 6,\n                end: 12,\n                wrapping: \"(\",\n            },\n        ]));\n\n    test(\"Two Inline Fields\", () => {\n        let fields = extractInlineFields(\"[key:: value] and so thus, [key2:: value2]\");\n        expect(fields[0]).toEqual({\n            key: \"key\",\n            value: \"value\",\n            start: 0,\n            startValue: 6,\n            end: 13,\n            wrapping: \"[\",\n        });\n\n        expect(fields[1]).toEqual({\n            key: \"key2\",\n            value: \"value2\",\n            start: 27,\n            startValue: 34,\n            end: 42,\n            wrapping: \"[\",\n        });\n    });\n});\n\ndescribe(\"Inline Inline Edge Cases\", () => {\n    test(\"Nested Brackets\", () => {\n        let result = extractInlineFields(\"[[[[hello:: 16]]\");\n        expect(result[0].key).toEqual(\"hello\");\n        expect(result[0].value).toEqual(\"16\");\n    });\n\n    test(\"Brackets In Value\", () => {\n        let result = extractInlineFields(\"This is some text. [key:: [value]]\");\n        expect(result[0].key).toEqual(\"key\");\n        expect(result[0].value).toEqual(\"[value]\");\n    });\n\n    test(\"Escaped Brackets\", () => {\n        let result = extractInlineFields(\"Hello? [key! :: \\\\[value]\");\n        expect(result[0].key).toEqual(\"key!\");\n        expect(result[0].value).toEqual(\"\\\\[value\");\n    });\n\n    test(\"Large Leading Whitespace\", () => {\n        let result = extractInlineFields(\"      - [ ] Huh! [p:: 1]\");\n        expect(result[0].key).toEqual(\"p\");\n        expect(result[0].value).toEqual(\"1\");\n    });\n});\n\ndescribe(\"Inline Inline With HTML\", () => {\n    test(\"Link\", () => {\n        let result = extractInlineFields(`[link:: <a href=\"Page\">Value</a>]`);\n        expect(result[0].key).toEqual(\"link\");\n        expect(result[0].value).toEqual(`<a href=\"Page\">Value</a>`);\n    });\n});\n\ndescribe(\"Inline task emoji shorthands\", () => {\n    test(\"Due emoji shorthand\", () => {\n        let result = extractInlineFields(\"- [ ] testTask 🗓️2022-07-14\", true);\n        expect(result[0].key).toEqual(\"due\");\n        expect(result[0].value).toEqual(\"2022-07-14\");\n\n        // This is a different calendar emoji!\n        result = extractInlineFields(\"- [ ] testTask 🗓2022-07-14\", true);\n        expect(result[0].key).toEqual(\"due\");\n        expect(result[0].value).toEqual(\"2022-07-14\");\n    });\n\n    test(\"Created emoji shorthand\", () => {\n        let result = extractInlineFields(\" - [ ] testTask \\u{2795}2022-07-25\", true);\n        expect(result[0].key).toEqual(\"created\");\n        expect(result[0].value).toEqual(\"2022-07-25\");\n    });\n\n    test(\"Start date emoji shorthand\", () => {\n        let result = extractInlineFields(\" - [ ] testTask 🛫2022-07-21\", true);\n        expect(result[0].key).toEqual(\"start\");\n        expect(result[0].value).toEqual(\"2022-07-21\");\n    });\n\n    test(\"Scheduled date emoji shorthand\", () => {\n        let result = extractInlineFields(\" - [ ] testTask ⏳2022-07-24\", true);\n        expect(result[0].key).toEqual(\"scheduled\");\n        expect(result[0].value).toEqual(\"2022-07-24\");\n    });\n});\n\ndescribe(\"Set Inline\", () => {\n    test(\"Add Annotation\", () => {\n        let input = \"- [ ] an uncompleted task\";\n        let result = setInlineField(input, \"completion\");\n        expect(result).toEqual(input);\n\n        result = setInlineField(input, \"completion\", \"2021-02-21\");\n        expect(result).toEqual(input + \" [completion:: 2021-02-21]\");\n    });\n\n    test(\"Replace Annotation\", () => {\n        let input = \"- [x] a completed task [completion:: 2021-02-21] foo bar\";\n        let result = setInlineField(input, \"completion\");\n        expect(result).toEqual(\"- [x] a completed task foo bar\");\n\n        result = setInlineField(input, \"completion\", \"2021-02-22\");\n        expect(result).toEqual(\"- [x] a completed task [completion:: 2021-02-22] foo bar\");\n    });\n\n    test(\"Add emoji shorthand annotation\", () => {\n        let input = \"- [ ] an uncompleted task\";\n        /* Nothing added when should not be */\n        let result = setEmojiShorthandCompletionField(input);\n        expect(result).toEqual(input);\n\n        /* Completion date added */\n        result = setEmojiShorthandCompletionField(input, \"2022-07-26\");\n        expect(result).toEqual(input + \" ✅ 2022-07-26\");\n        const extracted = extractInlineFields(result, true);\n        expect(extracted[0].key).toEqual(\"completion\");\n        expect(extracted[0].value).toEqual(\"2022-07-26\");\n\n        /* Remove the completion field */\n        result = setEmojiShorthandCompletionField(result);\n        expect(result).toEqual(input);\n        const input2 = \"- [x] a completed task ✅ 2022-07-26 foo bar\";\n        result = setEmojiShorthandCompletionField(input2);\n        expect(result).toEqual(\"- [x] a completed task foo bar\");\n    });\n});\n"
  },
  {
    "path": "src/test/parse/parse.query.test.ts",
    "content": "import { TableQuery, ListQuery, CalendarQuery, SortByStep, QueryFields, Query } from \"query/query\";\nimport { QUERY_LANGUAGE, parseQuery } from \"query/parse\";\nimport { Sources } from \"data-index/source\";\nimport { DEFAULT_QUERY_SETTINGS } from \"settings\";\nimport { Fields } from \"expression/field\";\n\nfunction testQueryTypeAlone(name: string) {\n    const upper = name.toUpperCase();\n    const lower = name.toLowerCase();\n\n    function expectQ(q: Query) {\n        expect(q.header.type).toBe(lower);\n    }\n\n    const tests = [upper, lower].flatMap(type => [\"\", \" \", \"\\n\", \" \\n\", \"\\n \"].map(suffix => type + suffix));\n\n    test(`Just ${upper}`, () => {\n        for (const test of tests) {\n            expectQ(parseQuery(test).orElseThrow());\n        }\n    });\n}\n\ntest(\"Parse Query Type\", () => {\n    let unknown = QUERY_LANGUAGE.queryType.parse(\"vehicle\");\n    expect(unknown.status).toBe(false);\n\n    let list = QUERY_LANGUAGE.queryType.tryParse(\"list\");\n    expect(list).toEqual(\"list\");\n\n    let listUpper = QUERY_LANGUAGE.queryType.tryParse(\"lIsT\");\n    expect(listUpper).toEqual(\"list\");\n});\n\n// <-- Source Types -->\ntest(\"Link Source\", () => {\n    expect(QUERY_LANGUAGE.query.tryParse(\"list from [[Stuff]]\")).toEqual({\n        header: { type: \"list\", showId: true },\n        source: Sources.link(\"Stuff\", true),\n        settings: DEFAULT_QUERY_SETTINGS,\n        operations: [],\n    });\n});\n\n// <-- Comments -->\ndescribe(\"Comments\", () => {\n    test(\"Valid Comments\", () => {\n        let commentWithSpace = QUERY_LANGUAGE.comment.tryParse(\"// This is a comment\");\n        expect(commentWithSpace).toEqual(\"This is a comment\");\n\n        let commentNoSpace = QUERY_LANGUAGE.comment.tryParse(\"//This is a comment\");\n        expect(commentNoSpace).toEqual(\"This is a comment\");\n\n        let empty = QUERY_LANGUAGE.comment.tryParse(\"//\");\n        expect(empty).toEqual(\"\");\n\n        let emptyWithSpace = QUERY_LANGUAGE.comment.tryParse(\"// \");\n        expect(emptyWithSpace).toEqual(\"\");\n    });\n\n    test(\"Invalid comment in quotes\", () => {\n        let commentSolo = QUERY_LANGUAGE.comment.parse('\"// This is not a comment\"');\n        if (!commentSolo.status) {\n            expect(commentSolo.expected).toEqual([\"Not a comment\"]);\n        } else {\n            fail(\"Solo comment should not have parsed\");\n        }\n\n        let commentMiddle = QUERY_LANGUAGE.comment.parse('\"The is before the comment // This is not a comment\"');\n        if (!commentMiddle.status) {\n            expect(commentMiddle.expected).toEqual([\"Not a comment\"]);\n        } else {\n            fail(\"Comment in the middle of a string should not have parsed\");\n        }\n    });\n});\n\n// <-- Fields -->\n\ndescribe(\"Named Fields\", () => {\n    test(\"Explicit\", () => {\n        let simple = QUERY_LANGUAGE.namedField.tryParse(\"time-played\");\n        expect(simple).toEqual(QueryFields.named(\"time-played\", Fields.variable(\"time-played\")));\n\n        let complex = QUERY_LANGUAGE.namedField.tryParse(\"(time-played + 4) as something\");\n        expect(complex).toEqual(\n            QueryFields.named(\"something\", Fields.binaryOp(Fields.variable(\"time-played\"), \"+\", Fields.literal(4)))\n        );\n    });\n\n    test(\"Implicit\", () => {\n        let simple = QUERY_LANGUAGE.namedField.tryParse(\"8 + 4\");\n        expect(simple).toEqual(QueryFields.named(\"8 + 4\", Fields.binaryOp(Fields.literal(8), \"+\", Fields.literal(4))));\n    });\n});\n\ntest(\"Sort Fields\", () => {\n    let simple = QUERY_LANGUAGE.sortField.tryParse(\"time-played DESC\");\n    expect(simple).toEqual(QueryFields.sortBy(Fields.variable(\"time-played\"), \"descending\"));\n\n    let complex = QUERY_LANGUAGE.sortField.tryParse('(time-played - \"where\")');\n    expect(complex).toEqual(\n        QueryFields.sortBy(Fields.binaryOp(Fields.variable(\"time-played\"), \"-\", Fields.literal(\"where\")), \"ascending\")\n    );\n});\n\n// <-- Full Queries -->\n\ndescribe(\"Task Queries\", () => {\n    testQueryTypeAlone(\"task\");\n\n    test(\"Task query with no fields\", () => {\n        let q = parseQuery(\"task from #games\").orElseThrow();\n        expect(typeof q).toBe(\"object\");\n        expect(q.header.type).toBe(\"task\");\n        expect(q.source).toEqual(Sources.tag(\"#games\"));\n    });\n});\n\ndescribe(\"List Queries\", () => {\n    testQueryTypeAlone(\"list\");\n\n    test(\"With Format\", () => {\n        let query = parseQuery(\"LIST file.name FROM #games\").orElseThrow();\n        expect(query.header.type).toBe(\"list\");\n        expect((query.header as ListQuery).format).toEqual(Fields.indexVariable(\"file.name\"));\n    });\n\n    test(\"WITHOUT ID\", () => {\n        let query = parseQuery(\"LIST WITHOUT ID file.name FROM #games\").orElseThrow();\n        expect(query.header.type).toBe(\"list\");\n        expect((query.header as ListQuery).showId).toBe(false);\n        expect((query.header as ListQuery).format).toEqual(Fields.indexVariable(\"file.name\"));\n    });\n});\n\ndescribe(\"Table Queries\", () => {\n    testQueryTypeAlone(\"table\");\n\n    test(\"Minimal Query\", () => {\n        let simple = parseQuery(\"TABLE time-played, rating, length FROM #games\").orElseThrow();\n        expect(simple.header.type).toBe(\"table\");\n        expect((simple.header as TableQuery).fields).toEqual([\n            QueryFields.named(\"time-played\", Fields.variable(\"time-played\")),\n            QueryFields.named(\"rating\", Fields.variable(\"rating\")),\n            QueryFields.named(\"length\", Fields.variable(\"length\")),\n        ]);\n        expect(simple.source).toEqual(Sources.tag(\"#games\"));\n        expect((simple.header as TableQuery).showId).toBe(true);\n    });\n\n    test(\"Fat Query\", () => {\n        let fat = parseQuery(\n            \"TABLE (time-played + 100) as long, rating as rate, length\\n\" +\n                \"FROM #games or #gaming\\n\" +\n                \"WHERE long > 150 and rate - 10 < 40\\n\" +\n                \"SORT length + 8 + 4 DESCENDING, long ASC\"\n        ).orElseThrow();\n        expect(fat.header.type).toBe(\"table\");\n        expect((fat.header as TableQuery).fields.length).toBe(3);\n        expect(fat.source).toEqual(Sources.binaryOp(Sources.tag(\"#games\"), \"|\", Sources.tag(\"#gaming\")));\n        expect((fat.operations[1] as SortByStep).fields.length).toBe(2);\n    });\n\n    test(\"Commented Query\", () => {\n        let commented = parseQuery(\n            \"// This is a comment at the beginning\\n\" +\n                \"TABLE (time-played + 100) as long, rating as rate, length\\n\" +\n                \"// This is a comment\\n\" +\n                \"// This is a second comment\\n\" +\n                \"FROM #games or #gaming  // This is an inline comment\\n\" +\n                \"// This is a third comment\\n\" +\n                \"WHERE long > 150 and rate - 10 < 40\\n\" +\n                \"// This is a fourth comment\\n\" +\n                \"\\n\" +\n                \"   // This is a comment with whitespace prior\\n\" +\n                \"SORT length + 8 + 4 DESCENDING, long ASC\\n\" +\n                \"// This is a comment at the end\"\n        ).orElseThrow();\n        expect(commented.header.type).toBe(\"table\");\n        expect((commented.header as TableQuery).fields.length).toBe(3);\n        expect(commented.source).toEqual(Sources.binaryOp(Sources.tag(\"#games\"), \"|\", Sources.tag(\"#gaming\")));\n        expect((commented.operations[1] as SortByStep).fields.length).toBe(2);\n    });\n\n    test(\"WITHOUT ID\", () => {\n        let q = parseQuery(\"TABLE WITHOUT ID name, value\").orElseThrow();\n        expect(typeof q).toBe(\"object\");\n        expect(q.header.type).toBe(\"table\");\n\n        let tq = q.header as TableQuery;\n        expect(tq.showId).toBe(false);\n    });\n\n    test(\"WITHOUT ID (weird spacing)\", () => {\n        let q = parseQuery(\"TABLE    WITHOUT     ID   name, value\").orElseThrow();\n        expect(typeof q).toBe(\"object\");\n        expect(q.header.type).toBe(\"table\");\n\n        let tq = q.header as TableQuery;\n        expect(tq.showId).toBe(false);\n    });\n});\n\ndescribe(\"Calendar Queries\", () => {\n    test(\"Minimal Query\", () => {\n        let simple = parseQuery(\"CALENDAR my-date FROM #games\\n\" + \"WHERE foo > 100\").orElseThrow();\n        expect(simple.header.type).toBe(\"calendar\");\n        expect((simple.header as CalendarQuery).field).toEqual(\n            QueryFields.named(\"my-date\", Fields.variable(\"my-date\"))\n        );\n        expect(simple.source).toEqual(Sources.tag(\"#games\"));\n    });\n});\n"
  },
  {
    "path": "src/test/util/normalize.test.ts",
    "content": "import { canonicalizeVarName, normalizeHeaderForLink } from \"util/normalize\";\n\ndescribe(\"Header Normalization\", () => {\n    test(\"Link\", () => expect(normalizeHeaderForLink(\"Header  [[Outer Wilds]]  \")).toEqual(\"Header Outer Wilds\"));\n    test(\"Dash\", () => expect(normalizeHeaderForLink(\"Header - More\")).toEqual(\"Header - More\"));\n    test(\"Underscore\", () => expect(normalizeHeaderForLink(\"Header _ More _\")).toEqual(\"Header _ More _\"));\n    test(\"Link with Display\", () =>\n        expect(normalizeHeaderForLink(\"Header  [[Outer Wilds|Thing]]  \")).toEqual(\"Header Outer Wilds Thing\"));\n    test(\"Markup\", () => expect(normalizeHeaderForLink(\"**Header** *Value\")).toEqual(\"Header Value\"));\n    test(\"Emoji\", () =>\n        expect(normalizeHeaderForLink(\"Header   📷 [[Outer Wilds]]  \")).toEqual(\"Header 📷 Outer Wilds\"));\n});\n\ndescribe(\"Variable Canonicalization\", () => {\n    test(\"Idempotent\", () => expect(canonicalizeVarName(\"test\")).toEqual(\"test\"));\n    test(\"Idempotent 2\", () => expect(canonicalizeVarName(\"property\")).toEqual(\"property\"));\n    test(\"Space\", () => expect(canonicalizeVarName(\"test thing\")).toEqual(\"test-thing\"));\n    test(\"Multiple Space\", () => expect(canonicalizeVarName(\"This     is test\")).toEqual(\"this-is-test\"));\n    test(\"Number\", () => expect(canonicalizeVarName(\"test thing 3\")).toEqual(\"test-thing-3\"));\n    test(\"Punctuation\", () => expect(canonicalizeVarName(\"This is a Test.\")).toEqual(\"this-is-a-test\"));\n    test(\"Dash\", () => expect(canonicalizeVarName(\"Yes-sir\")).toEqual(\"yes-sir\"));\n    test(\"Emoji\", () => expect(canonicalizeVarName(\"📷\")).toEqual(\"📷\"));\n    test(\"Статус\", () => expect(canonicalizeVarName(\"Статус\")).toEqual(\"статус\"));\n});\n"
  },
  {
    "path": "src/test/util/paths.test.ts",
    "content": "import { getFileTitle, getParentFolder } from \"util/normalize\";\n\ndescribe(\"getFileTitle()\", () => {\n    test(\"empty\", () => expect(getFileTitle(\"\")).toEqual(\"\"));\n\n    test(\"root getFileTitle()\", () => {\n        expect(getFileTitle(\"yes.md\")).toEqual(\"yes\");\n        expect(getFileTitle(\"yes\")).toEqual(\"yes\");\n    });\n\n    test(\"folder getFileTitle()\", () => {\n        expect(getFileTitle(\"ok/yes.md\")).toEqual(\"yes\");\n        expect(getFileTitle(\"/yes\")).toEqual(\"yes\");\n    });\n});\n\ntest(\"empty getParentFolder()\", () => {\n    expect(getParentFolder(\"\")).toEqual(\"\");\n});\n\ntest(\"root getParentFolder()\", () => {\n    expect(getParentFolder(\"yes\")).toEqual(\"\");\n    expect(getParentFolder(\"maybe\")).toEqual(\"\");\n});\n\ntest(\"folder getParentFolder()\", () => {\n    expect(getParentFolder(\"ok/yes\")).toEqual(\"ok\");\n    expect(getParentFolder(\"no/maybe\")).toEqual(\"no\");\n    expect(getParentFolder(\"/maybe\")).toEqual(\"\");\n});\n\ntest(\"nested folder getParentFolder()\", () => {\n    expect(getParentFolder(\"a/b/c.md\")).toEqual(\"a/b\");\n    expect(getParentFolder(\"hello/yes/no/maybe.md\")).toEqual(\"hello/yes/no\");\n    expect(getParentFolder(\"hello/yes/no/\")).toEqual(\"hello/yes/no\");\n});\n"
  },
  {
    "path": "src/typings/obsidian-ex.d.ts",
    "content": "import type { DataviewApi } from \"api/plugin-api\";\nimport \"obsidian\";\nimport { EditorView } from \"@codemirror/view\";\n\ndeclare module \"obsidian\" {\n    interface MetadataCache {\n        trigger(...args: Parameters<MetadataCache[\"on\"]>): void;\n        trigger(name: string, ...data: any[]): void;\n    }\n\n    interface App {\n        appId?: string;\n        plugins: {\n            enabledPlugins: Set<string>;\n            plugins: {\n                dataview?: {\n                    api: DataviewApi;\n                };\n            };\n        };\n    }\n\n    interface Workspace {\n        /** Sent to rendered dataview components to tell them to possibly refresh */\n        on(name: \"dataview:refresh-views\", callback: () => void, ctx?: any): EventRef;\n    }\n\n    interface Editor {\n        /**\n         * CodeMirror editor instance\n         */\n        cm?: EditorView;\n    }\n}\n\ndeclare global {\n    interface Window {\n        DataviewAPI?: DataviewApi;\n    }\n}\n"
  },
  {
    "path": "src/typings/workers.d.ts",
    "content": "declare module \"web-worker:*\" {\n    const WorkerFactory: new (options: any) => Worker;\n    export default WorkerFactory;\n}\n"
  },
  {
    "path": "src/ui/export/markdown.ts",
    "content": "import { SListItem } from \"data-model/serialized/markdown\";\nimport { Grouping, Groupings, Literal, Values, Widgets } from \"data-model/value\";\nimport { DEFAULT_SETTINGS, ExportSettings, QuerySettings } from \"settings\";\nimport { nestItems } from \"ui/views/task-view\";\n\n////////////\n// Tables //\n////////////\n\n/** Render a table of literals to Markdown. */\nexport function markdownTable(\n    headers: string[],\n    values: Literal[][],\n    settings?: QuerySettings & ExportSettings\n): string {\n    if (values.length > 0 && headers.length != values[0].length)\n        throw new Error(\n            `The number of headers (${headers.length}) must match the number of columns (${values[0].length})`\n        );\n\n    settings = settings ?? DEFAULT_SETTINGS;\n\n    const mvalues: string[][] = [];\n    const maxLengths: number[] = Array.from(headers, v => escapeTable(v).length);\n\n    // Pre-construct the table in memory so we can size columns.\n    for (let row = 0; row < values.length; row++) {\n        const current: string[] = [];\n        for (let col = 0; col < values[row].length; col++) {\n            const text = tableLiteral(values[row][col], settings.allowHtml, settings);\n\n            current.push(text);\n            maxLengths[col] = Math.max(maxLengths[col], text.length);\n        }\n        mvalues.push(current);\n    }\n\n    // Then construct the actual table...\n    // Append the header fields first.\n    let table = `| ${headers.map((v, i) => padright(escapeTable(v), \" \", maxLengths[i])).join(\" | \")} |\\n`;\n    // Then the separating column.\n    table += `| ${maxLengths.map(i => padright(\"\", \"-\", i)).join(\" | \")} |\\n`;\n    // Then the data columns.\n    for (let row = 0; row < values.length; row++) {\n        table += `| ${mvalues[row].map((v, i) => padright(v, \" \", maxLengths[i])).join(\" | \")} |\\n`;\n    }\n\n    return table;\n}\n\n/** Convert a value to a Markdown-friendly string. */\nfunction tableLiteral(value: Literal, allowHtml: boolean = true, settings?: QuerySettings): string {\n    return escapeTable(rawTableLiteral(value, allowHtml, settings));\n}\n\n/** Convert a value to a Markdown-friendly string; does not do escaping. */\nfunction rawTableLiteral(value: Literal, allowHtml: boolean = true, settings?: QuerySettings): string {\n    if (!allowHtml) return Values.toString(value, settings);\n\n    if (Values.isArray(value)) {\n        return `<ul>${value.map(v => \"<li>\" + tableLiteral(v, allowHtml, settings) + \"</li>\").join(\"\")}</ul>`;\n    } else if (Values.isObject(value)) {\n        const inner = Object.entries(value)\n            .map(([k, v]) => {\n                return `<li><b>${tableLiteral(k, allowHtml, settings)}</b>: ${tableLiteral(\n                    v,\n                    allowHtml,\n                    settings\n                )}</li>`;\n            })\n            .join(\"\");\n\n        return `<ul>${inner}</ul>`;\n    } else {\n        return Values.toString(value, settings);\n    }\n}\n\n/** Don't need to import a library for this one... */\nfunction padright(text: string, padding: string, length: number): string {\n    if (text.length >= length) return text;\n    return text + padding.repeat(length - text.length);\n}\n\n/** Escape bars inside table content to prevent it from messing up table rows. */\nfunction escapeTable(text: string): string {\n    return text.split(/(?!\\\\)\\|/i).join(\"\\\\|\");\n}\n\n///////////\n// Lists //\n///////////\n\n/** Render a list of literal elements to a markdown list. */\nexport function markdownList(values: Literal[], settings?: QuerySettings & ExportSettings): string {\n    return markdownListRec(values, settings, 0);\n}\n\n/** Internal recursive function which renders markdown lists. */\nfunction markdownListRec(input: Literal, settings?: QuerySettings & ExportSettings, depth: number = 0): string {\n    if (Values.isArray(input)) {\n        let result = depth == 0 ? \"\" : \"\\n\";\n        for (let value of input) {\n            result += \"    \".repeat(depth) + \"- \";\n            result += markdownListRec(value, settings, depth);\n            result += \"\\n\";\n        }\n\n        return result;\n    } else if (Values.isObject(input)) {\n        let result = depth == 0 ? \"\" : \"\\n\";\n        for (let [key, value] of Object.entries(input)) {\n            result += \"    \".repeat(depth) + \"- \";\n            result += Values.toString(key) + \": \";\n            result += markdownListRec(value, settings, depth);\n            result += \"\\n\";\n        }\n\n        return result;\n    } else if (Values.isWidget(input) && Widgets.isListPair(input)) {\n        return `${Values.toString(input.key)}: ${markdownListRec(input.value, settings, depth + 1)}`;\n    }\n\n    return Values.toString(input);\n}\n\n///////////\n// Tasks //\n///////////\n\n/** Render the result of a task query to markdown. */\nexport function markdownTaskList(\n    tasks: Grouping<SListItem>,\n    settings?: QuerySettings & ExportSettings,\n    depth: number = 0\n): string {\n    if (Groupings.isGrouping(tasks)) {\n        let result = \"\";\n        for (let element of tasks) {\n            result += \"#\".repeat(depth + 1) + \" \" + Values.toString(element.key) + \"\\n\\n\";\n            result += markdownTaskList(element.rows, settings, depth + 1);\n        }\n        return result;\n    } else {\n        // Remove task line duplicates if present to match `taskList()` behavior.\n        const [dedupTasks, _] = nestItems(tasks);\n\n        let result = \"\";\n        for (let element of dedupTasks) {\n            result += \"    \".repeat(depth) + \"- \";\n\n            if (element.task) {\n                result += `[${element.status}] ${(element.visual ?? element.text).split(\"\\n\").join(\" \")}\\n`;\n            } else {\n                result += `${(element.visual ?? element.text).split(\"\\n\").join(\" \")}\\n`;\n            }\n\n            result += markdownTaskList(element.children, settings, depth + 1);\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/ui/lp-render.ts",
    "content": "/*\n * inspired and adapted from https://github.com/artisticat1/obsidian-latex-suite/blob/main/src/conceal.ts\n *\n * The original work is MIT-licensed.\n *\n * MIT License\n *\n * Copyright (c) 2022 artisticat1\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n *\n * */\n\nimport { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetType } from \"@codemirror/view\";\nimport { EditorSelection, Range } from \"@codemirror/state\";\nimport { syntaxTree, tokenClassNodeProp } from \"@codemirror/language\";\nimport { DataviewSettings } from \"../settings\";\nimport { FullIndex } from \"../data-index\";\nimport { App, Component, editorInfoField, editorLivePreviewField, TFile } from \"obsidian\";\nimport { DataviewApi } from \"../api/plugin-api\";\nimport { tryOrPropagate } from \"../util/normalize\";\nimport { parseField } from \"../expression/parse\";\nimport { executeInline } from \"../query/engine\";\nimport { Literal } from \"../data-model/value\";\nimport { DataviewInlineApi } from \"../api/inline-api\";\nimport { renderValue } from \"./render\";\nimport { SyntaxNode } from \"@lezer/common\";\n\nexport function selectionAndRangeOverlap(selection: EditorSelection, rangeFrom: number, rangeTo: number) {\n    for (const range of selection.ranges) {\n        if (range.from <= rangeTo && range.to >= rangeFrom) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\nclass InlineWidget extends WidgetType {\n    constructor(\n        readonly cssClasses: string[],\n        readonly rawQuery: string,\n        private el: HTMLElement,\n        private view: EditorView\n    ) {\n        super();\n    }\n\n    // Widgets only get updated when the raw query changes/the element gets focus and loses it\n    // to prevent redraws when the editor updates.\n    eq(other: InlineWidget): boolean {\n        if (other.rawQuery === this.rawQuery) {\n            // change CSS classes without redrawing the element\n            for (let value of other.cssClasses) {\n                if (!this.cssClasses.includes(value)) {\n                    this.el.removeClass(value);\n                } else {\n                    this.el.addClass(value);\n                }\n            }\n            return true;\n        }\n        return false;\n    }\n\n    // Add CSS classes and return HTML element.\n    // In \"complex\" cases it will get filled with the correct text/child elements later.\n    toDOM(view: EditorView): HTMLElement {\n        this.el.addClasses(this.cssClasses);\n        return this.el;\n    }\n\n    /* Make queries only editable when shift is pressed (or navigated inside with the keyboard\n     * or the mouse is placed at the end, but that is always possible regardless of this method).\n     * Mostly useful for links, and makes results selectable.\n     * If the widgets should always be expandable, make this always return false.\n     */\n    ignoreEvent(event: MouseEvent | Event): boolean {\n        // instanceof check does not work in pop-out windows, so check it like this\n        if (event.type === \"mousedown\") {\n            const currentPos = this.view.posAtCoords({ x: (event as MouseEvent).x, y: (event as MouseEvent).y });\n            if ((event as MouseEvent).shiftKey) {\n                // Set the cursor after the element so that it doesn't select starting from the last cursor position.\n                if (currentPos) {\n                    const { editor } = this.view.state.field(editorInfoField);\n                    if (editor) {\n                        editor.setCursor(editor.offsetToPos(currentPos));\n                    }\n                }\n                return false;\n            }\n        }\n        return true;\n    }\n}\n\nfunction getCssClasses(props: Set<string>): string[] {\n    const classes: string[] = [];\n    if (props.has(\"strong\")) {\n        classes.push(\"cm-strong\");\n    }\n    if (props.has(\"em\")) {\n        classes.push(\"cm-em\");\n    }\n    if (props.has(\"highlight\")) {\n        classes.push(\"cm-highlight\");\n    }\n    if (props.has(\"strikethrough\")) {\n        classes.push(\"cm-strikethrough\");\n    }\n    if (props.has(\"comment\")) {\n        classes.push(\"cm-comment\");\n    }\n    return classes;\n}\n\nexport function inlinePlugin(app: App, index: FullIndex, settings: DataviewSettings, api: DataviewApi) {\n    return ViewPlugin.fromClass(\n        class {\n            decorations: DecorationSet;\n            component: Component;\n\n            constructor(view: EditorView) {\n                this.component = new Component();\n                this.component.load();\n                this.decorations = this.inlineRender(view) ?? Decoration.none;\n            }\n\n            update(update: ViewUpdate) {\n                // only activate in LP and not source mode\n                if (!update.state.field(editorLivePreviewField)) {\n                    this.decorations = Decoration.none;\n                    return;\n                }\n                if (update.docChanged) {\n                    this.decorations = this.decorations.map(update.changes);\n                    this.updateTree(update.view);\n                } else if (update.selectionSet) {\n                    this.updateTree(update.view);\n                } else if (update.viewportChanged /*|| update.selectionSet*/) {\n                    this.decorations = this.inlineRender(update.view) ?? Decoration.none;\n                }\n            }\n\n            updateTree(view: EditorView) {\n                for (const { from, to } of view.visibleRanges) {\n                    syntaxTree(view.state).iterate({\n                        from,\n                        to,\n                        enter: ({ node }) => {\n                            const { render, isQuery } = this.renderNode(view, node);\n                            if (!render && isQuery) {\n                                this.removeDeco(node);\n                                return;\n                            } else if (!render) {\n                                return;\n                            } else if (render) {\n                                this.addDeco(node, view);\n                            }\n                        },\n                    });\n                }\n            }\n\n            removeDeco(node: SyntaxNode) {\n                this.decorations.between(node.from - 1, node.to + 1, (from, to, value) => {\n                    this.decorations = this.decorations.update({\n                        filterFrom: from,\n                        filterTo: to,\n                        filter: (from, to, value) => false,\n                    });\n                });\n            }\n\n            addDeco(node: SyntaxNode, view: EditorView) {\n                const from = node.from - 1;\n                const to = node.to + 1;\n                let exists = false;\n                this.decorations.between(from, to, (from, to, value) => {\n                    exists = true;\n                });\n                if (!exists) {\n                    /**\n                     * In a note embedded in a Canvas, app.workspace.getActiveFile() returns\n                     * the canvas file, not the note file. On the other hand,\n                     * view.state.field(editorInfoField).file returns the note file itself,\n                     * which is more suitable here.\n                     */\n                    const currentFile = view.state.field(editorInfoField).file;\n                    if (!currentFile) return;\n                    const newDeco = this.renderWidget(node, view, currentFile)?.value;\n                    if (newDeco) {\n                        this.decorations = this.decorations.update({\n                            add: [{ from: from, to: to, value: newDeco }],\n                        });\n                    }\n                }\n            }\n\n            // checks whether a node should get rendered/unrendered\n            renderNode(view: EditorView, node: SyntaxNode) {\n                const type = node.type;\n                // current node is inline code\n                const tokenProps = type.prop<String>(tokenClassNodeProp);\n                const props = new Set(tokenProps?.split(\" \"));\n                if (props.has(\"inline-code\") && !props.has(\"formatting\")) {\n                    // contains the position of inline code\n                    const start = node.from;\n                    const end = node.to;\n                    // don't continue if current cursor position and inline code node (including formatting\n                    // symbols) overlap\n                    const selection = view.state.selection;\n                    if (selectionAndRangeOverlap(selection, start - 1, end + 1)) {\n                        if (this.isInlineQuery(view, start, end)) {\n                            return { render: false, isQuery: true };\n                        } else {\n                            return { render: false, isQuery: false };\n                        }\n                    } else if (this.isInlineQuery(view, start, end)) {\n                        return { render: true, isQuery: true };\n                    }\n                }\n                return { render: false, isQuery: false };\n            }\n\n            isInlineQuery(view: EditorView, start: number, end: number) {\n                const text = view.state.doc.sliceString(start, end);\n                const isInlineQuery =\n                    text.startsWith(settings.inlineQueryPrefix) || text.startsWith(settings.inlineJsQueryPrefix);\n                return isInlineQuery;\n            }\n\n            inlineRender(view: EditorView) {\n                // still doesn't work as expected for tables and callouts\n                if (!index.initialized) return;\n                const currentFile = view.state.field(editorInfoField).file;\n                if (!currentFile) return;\n\n                const widgets: Range<Decoration>[] = [];\n                /* before:\n                 *     em for italics\n                 *     highlight for highlight\n                 * after:\n                 *     strong for bold\n                 *     strikethrough for strikethrough\n                 */\n\n                for (const { from, to } of view.visibleRanges) {\n                    syntaxTree(view.state).iterate({\n                        from,\n                        to,\n                        enter: ({ node }) => {\n                            if (!this.renderNode(view, node).render) return;\n                            const widget = this.renderWidget(node, view, currentFile);\n                            if (widget) {\n                                widgets.push(widget);\n                            }\n                        },\n                    });\n                }\n\n                return Decoration.set(widgets, true);\n            }\n\n            renderWidget(node: SyntaxNode, view: EditorView, currentFile: TFile) {\n                const type = node.type;\n                // contains the position of inline code\n                const start = node.from;\n                const end = node.to;\n                // safety net against unclosed inline code\n                if (view.state.doc.sliceString(end, end + 1) === \"\\n\") {\n                    return;\n                }\n                const text = view.state.doc.sliceString(start, end);\n                let code: string = \"\";\n                let result: Literal = \"\";\n                const PREAMBLE: string = \"const dataview=this;const dv=this;\";\n                const el = createSpan({\n                    cls: [\"dataview\", \"dataview-inline\"],\n                });\n                /* If the query result is predefined text (e.g. in the case of errors), set innerText to it.\n                 * Otherwise, pass on an empty element and fill it in later.\n                 * This is necessary because {@link InlineWidget.toDOM} is synchronous but some rendering\n                 * asynchronous.\n                 */\n                if (text.startsWith(settings.inlineQueryPrefix)) {\n                    if (settings.enableInlineDataview) {\n                        code = text.substring(settings.inlineQueryPrefix.length).trim();\n                        const field = tryOrPropagate(() => parseField(code));\n                        if (!field.successful) {\n                            result = `Dataview (inline field '${code}'): ${field.error}`;\n                            el.innerText = result;\n                        } else {\n                            const fieldValue = field.value;\n                            const intermediateResult = tryOrPropagate(() =>\n                                executeInline(fieldValue, currentFile.path, index, settings)\n                            );\n                            if (!intermediateResult.successful) {\n                                result = `Dataview (for inline query '${fieldValue}'): ${intermediateResult.error}`;\n                                el.innerText = result;\n                            } else {\n                                const { value } = intermediateResult;\n                                result = value;\n                                renderValue(app, result, el, currentFile.path, this.component, settings);\n                            }\n                        }\n                    } else {\n                        result = \"(disabled; enable in settings)\";\n                        el.innerText = result;\n                    }\n                } else if (text.startsWith(settings.inlineJsQueryPrefix)) {\n                    if (settings.enableInlineDataviewJs) {\n                        code = text.substring(settings.inlineJsQueryPrefix.length).trim();\n                        try {\n                            // for setting the correct context for dv/dataview\n                            const myEl = createDiv();\n                            const dvInlineApi = new DataviewInlineApi(api, this.component, myEl, currentFile.path);\n                            if (code.includes(\"await\")) {\n                                (evalInContext(\"(async () => { \" + PREAMBLE + code + \" })()\") as Promise<any>).then(\n                                    (result: any) => {\n                                        renderValue(app, result, el, currentFile.path, this.component, settings);\n                                    }\n                                );\n                            } else {\n                                result = evalInContext(PREAMBLE + code);\n                                renderValue(app, result, el, currentFile.path, this.component, settings);\n                            }\n\n                            function evalInContext(script: string): any {\n                                return function () {\n                                    return eval(script);\n                                }.call(dvInlineApi);\n                            }\n                        } catch (e) {\n                            result = `Dataview (for inline JS query '${code}'): ${e}`;\n                            el.innerText = result;\n                        }\n                    } else {\n                        result = \"(disabled; enable in settings)\";\n                        el.innerText = result;\n                    }\n                } else {\n                    return;\n                }\n\n                const tokenProps = type.prop<String>(tokenClassNodeProp);\n                const props = new Set(tokenProps?.split(\" \"));\n                const classes = getCssClasses(props);\n\n                return Decoration.replace({\n                    widget: new InlineWidget(classes, code, el, view),\n                    inclusive: false,\n                    block: false,\n                }).range(start - 1, end + 1);\n            }\n\n            destroy() {\n                this.component.unload();\n            }\n        },\n        { decorations: v => v.decorations }\n    );\n}\n"
  },
  {
    "path": "src/ui/markdown.tsx",
    "content": "/** Provides core preact / rendering utilities for all view types. */\nimport { App, MarkdownRenderChild, MarkdownRenderer } from \"obsidian\";\nimport { h, createContext, ComponentChildren, render, Fragment } from \"preact\";\nimport { useContext, useEffect, useRef, useState } from \"preact/hooks\";\nimport { Component } from \"obsidian\";\nimport { DataviewSettings } from \"settings\";\nimport { FullIndex } from \"data-index\";\nimport { Literal, Values, Widgets } from \"data-model/value\";\nimport React, { unmountComponentAtNode } from \"preact/compat\";\nimport { renderMinimalDate, renderMinimalDuration } from \"util/normalize\";\nimport { currentLocale } from \"util/locale\";\nimport { DataArray } from \"api/data-array\";\nimport { extractImageDimensions, isImageEmbed } from \"util/media\";\n\nexport type MarkdownProps = { contents: string; sourcePath: string };\nexport type MarkdownContext = { component: Component };\n\n/** Context need to create dataviews. */\nexport type DataviewInit = {\n    app: App;\n    index: FullIndex;\n    settings: DataviewSettings;\n    container: HTMLElement;\n};\n\n/** Shared context for dataview views and objects. */\nexport type DataviewContexts = DataviewInit & {\n    component: Component;\n};\n\nexport const DataviewContext = createContext<DataviewContexts>(undefined!);\n\n/** Hacky preact component which wraps Obsidian's markdown renderer into a neat component. */\nexport function RawMarkdown({\n    content,\n    sourcePath,\n    inline = true,\n    style,\n    cls,\n    onClick,\n}: {\n    content: string;\n    sourcePath: string;\n    inline?: boolean;\n    style?: string;\n    cls?: string;\n    onClick?: (e: preact.JSX.TargetedMouseEvent<HTMLElement>) => void;\n}) {\n    const container = useRef<HTMLElement | null>(null);\n    const component = useContext(DataviewContext).component;\n\n    useEffect(() => {\n        if (!container.current) return;\n\n        container.current.innerHTML = \"\";\n        MarkdownRenderer.renderMarkdown(content, container.current, sourcePath, component).then(() => {\n            if (!container.current || !inline) return;\n\n            // Unwrap any created paragraph elements if we are inline.\n            let paragraph = container.current.querySelector(\"p\");\n            while (paragraph) {\n                let children = paragraph.childNodes;\n                paragraph.replaceWith(...Array.from(children));\n                paragraph = container.current.querySelector(\"p\");\n            }\n        });\n    }, [content, sourcePath, container.current]);\n\n    return <span ref={container} style={style} class={cls} onClick={onClick}></span>;\n}\n\n/** Hacky preact component which wraps Obsidian's markdown renderer into a neat component. */\nexport const Markdown = React.memo(RawMarkdown);\n\n/** Embeds an HTML element in the react DOM. */\nexport function RawEmbedHtml({ element }: { element: HTMLElement }) {\n    const container = useRef<HTMLElement | null>(null);\n\n    useEffect(() => {\n        if (!container.current) return;\n        container.current.innerHTML = \"\";\n        container.current.appendChild(element);\n    }, [container.current, element]);\n\n    return <span ref={container}></span>;\n}\n\n/** Embeds an HTML element in the react DOM. */\nexport const EmbedHtml = React.memo(RawEmbedHtml);\n\n/** Intelligently render an arbitrary literal value. */\nexport function RawLit({\n    value,\n    sourcePath,\n    inline = false,\n    depth = 0,\n}: {\n    value: Literal | undefined;\n    sourcePath: string;\n    inline?: boolean;\n    depth?: number;\n}) {\n    const context = useContext(DataviewContext);\n\n    // Short-circuit if beyond the maximum render depth.\n    if (depth >= context.settings.maxRecursiveRenderDepth) return <Fragment>...</Fragment>;\n\n    if (Values.isNull(value) || value === undefined) {\n        return <Markdown content={context.settings.renderNullAs} sourcePath={sourcePath} />;\n    } else if (Values.isString(value)) {\n        return <Markdown content={value} sourcePath={sourcePath} />;\n    } else if (Values.isNumber(value)) {\n        return <Fragment>{\"\" + value}</Fragment>;\n    } else if (Values.isBoolean(value)) {\n        return <Fragment>{\"\" + value}</Fragment>;\n    } else if (Values.isDate(value)) {\n        return <Fragment>{renderMinimalDate(value, context.settings, currentLocale())}</Fragment>;\n    } else if (Values.isDuration(value)) {\n        return <Fragment>{renderMinimalDuration(value)}</Fragment>;\n    } else if (Values.isLink(value)) {\n        // Special case handling of image/video/etc embeddings to bypass the Obsidian API not working.\n        if (isImageEmbed(value)) {\n            let realFile = context.app.metadataCache.getFirstLinkpathDest(value.path, sourcePath);\n            if (!realFile) return <Markdown content={value.markdown()} sourcePath={sourcePath} />;\n\n            let dimensions = extractImageDimensions(value);\n            let resourcePath = context.app.vault.getResourcePath(realFile);\n\n            if (dimensions && dimensions.length == 2)\n                return <img alt={value.path} src={resourcePath} width={dimensions[0]} height={dimensions[1]} />;\n            else if (dimensions && dimensions.length == 1)\n                return <img alt={value.path} src={resourcePath} width={dimensions[0]} />;\n            else return <img alt={value.path} src={resourcePath} />;\n        }\n\n        return <Markdown content={value.markdown()} sourcePath={sourcePath} />;\n    } else if (Values.isHtml(value)) {\n        return <EmbedHtml element={value} />;\n    } else if (Values.isWidget(value)) {\n        if (Widgets.isListPair(value)) {\n            return (\n                <Fragment>\n                    <Lit value={value.key} sourcePath={sourcePath} />:{\" \"}\n                    <Lit value={value.value} sourcePath={sourcePath} />\n                </Fragment>\n            );\n        } else if (Widgets.isExternalLink(value)) {\n            return (\n                <a href={value.url} rel=\"noopener\" target=\"_blank\" class=\"external-link\">\n                    {value.display ?? value.url}\n                </a>\n            );\n        } else {\n            return <b>&lt;unknown widget '{value.$widget}'&gt;</b>;\n        }\n    } else if (Values.isFunction(value)) {\n        return <Fragment>&lt;function&gt;</Fragment>;\n    } else if (Values.isArray(value) || DataArray.isDataArray(value)) {\n        if (!inline) {\n            return (\n                <ul class={\"dataview dataview-ul dataview-result-list-ul\"}>\n                    {value.map(subvalue => (\n                        <li class=\"dataview-result-list-li\">\n                            <Lit value={subvalue} sourcePath={sourcePath} inline={inline} depth={depth + 1} />\n                        </li>\n                    ))}\n                </ul>\n            );\n        } else {\n            if (value.length == 0) return <Fragment>&lt;Empty List&gt;</Fragment>;\n\n            return (\n                <span class=\"dataview dataview-result-list-span\">\n                    {value.map((subvalue, index) => (\n                        <Fragment>\n                            {index == 0 ? \"\" : \", \"}\n                            <Lit value={subvalue} sourcePath={sourcePath} inline={inline} depth={depth + 1} />\n                        </Fragment>\n                    ))}\n                </span>\n            );\n        }\n    } else if (Values.isObject(value)) {\n        // Don't render classes in case they have recursive references; spoopy.\n        if (value?.constructor?.name && value?.constructor?.name != \"Object\") {\n            return <Fragment>&lt;{value.constructor.name}&gt;</Fragment>;\n        }\n\n        if (!inline) {\n            return (\n                <ul class=\"dataview dataview-ul dataview-result-object-ul\">\n                    {Object.entries(value).map(([key, value]) => (\n                        <li class=\"dataview dataview-li dataview-result-object-li\">\n                            {key}: <Lit value={value} sourcePath={sourcePath} inline={inline} depth={depth + 1} />\n                        </li>\n                    ))}\n                </ul>\n            );\n        } else {\n            if (Object.keys(value).length == 0) return <Fragment>&lt;Empty Object&gt;</Fragment>;\n\n            return (\n                <span class=\"dataview dataview-result-object-span\">\n                    {Object.entries(value).map(([key, value], index) => (\n                        <Fragment>\n                            {index == 0 ? \"\" : \", \"}\n                            {key}: <Lit value={value} sourcePath={sourcePath} inline={inline} depth={depth + 1} />\n                        </Fragment>\n                    ))}\n                </span>\n            );\n        }\n    }\n\n    return <Fragment>&lt;Unrecognized: {JSON.stringify(value)}&gt;</Fragment>;\n}\n\n/** Intelligently render an arbitrary literal value. */\nexport const Lit = React.memo(RawLit);\n\n/** Render a simple nice looking error box in a code style. */\nexport function ErrorPre(props: { children: ComponentChildren }, {}) {\n    return <pre class=\"dataview dataview-error\">{props.children}</pre>;\n}\n\n/** Render a pretty centered error message in a box. */\nexport function ErrorMessage({ message }: { message: string }) {\n    return (\n        <div class=\"dataview dataview-error-box\">\n            <p class=\"dataview dataview-error-message\">{message}</p>\n        </div>\n    );\n}\n\n/**\n * Complex convenience hook which calls `compute` every time the index updates, updating the current state.\n */\nexport function useIndexBackedState<T>(\n    container: HTMLElement,\n    app: App,\n    settings: DataviewSettings,\n    index: FullIndex,\n    initial: T,\n    compute: () => Promise<T>\n): T {\n    let [initialized, setInitialized] = useState(false);\n    let [state, updateState] = useState(initial);\n    let [lastReload, setLastReload] = useState(index.revision);\n\n    // Initial setup to queue fetching the correct state.\n    if (!initialized) {\n        setLastReload(index.revision);\n        setInitialized(true);\n\n        compute().then(updateState);\n    }\n\n    // Updated on every container re-create; automatically updates state.\n    useEffect(() => {\n        const refreshOperation = () => {\n            if (lastReload != index.revision && container.isShown() && settings.refreshEnabled) {\n                compute().then(updateState);\n                setLastReload(index.revision);\n            }\n        };\n\n        // Refresh after index changes stop.\n        let workEvent = app.workspace.on(\"dataview:refresh-views\", refreshOperation);\n        // ...or when the DOM is shown (sidebar expands, tab selected, nodes scrolled into view).\n        let nodeEvent = container.onNodeInserted(refreshOperation);\n\n        return () => {\n            app.workspace.offref(workEvent);\n            nodeEvent();\n        };\n    }, [container, lastReload]);\n\n    return state;\n}\n\n/** A trivial wrapper which allows a react component to live for the duration of a `MarkdownRenderChild`. */\nexport class ReactRenderer extends MarkdownRenderChild {\n    public constructor(public init: DataviewInit, public element: h.JSX.Element) {\n        super(init.container);\n    }\n\n    public onload(): void {\n        const context = Object.assign({}, { component: this }, this.init);\n        render(<DataviewContext.Provider value={context}>{this.element}</DataviewContext.Provider>, this.containerEl);\n    }\n\n    public onunload(): void {\n        unmountComponentAtNode(this.containerEl);\n    }\n}\n"
  },
  {
    "path": "src/ui/refreshable-view.ts",
    "content": "import { FullIndex } from \"data-index\";\nimport { App, MarkdownRenderChild } from \"obsidian\";\nimport { DataviewSettings } from \"settings\";\n\n/** Generic code for embedded Dataviews. */\nexport abstract class DataviewRefreshableRenderer extends MarkdownRenderChild {\n    private lastReload: number;\n\n    public constructor(\n        public container: HTMLElement,\n        public index: FullIndex,\n        public app: App,\n        public settings: DataviewSettings\n    ) {\n        super(container);\n        this.lastReload = 0;\n    }\n\n    abstract render(): Promise<void>;\n\n    onload() {\n        this.render();\n        this.lastReload = this.index.revision;\n        // Refresh after index changes stop.\n        this.registerEvent(this.app.workspace.on(\"dataview:refresh-views\", this.maybeRefresh));\n        // ...or when the DOM is shown (sidebar expands, tab selected, nodes scrolled into view).\n        this.register(this.container.onNodeInserted(this.maybeRefresh));\n    }\n\n    maybeRefresh = () => {\n        // If the index revision has changed recently, then queue a reload.\n        // But only if we're mounted in the DOM and auto-refreshing is active.\n        if (this.lastReload != this.index.revision && this.container.isShown() && this.settings.refreshEnabled) {\n            this.lastReload = this.index.revision;\n            this.render();\n        }\n    };\n}\n"
  },
  {
    "path": "src/ui/render.ts",
    "content": "import { App, Component, MarkdownRenderer } from \"obsidian\";\nimport { DataArray } from \"api/data-array\";\nimport { QuerySettings } from \"settings\";\nimport { currentLocale } from \"util/locale\";\nimport { renderMinimalDate, renderMinimalDuration } from \"util/normalize\";\nimport { Literal, Values, Widgets } from \"data-model/value\";\n\n/** Render simple fields compactly, removing wrapping content like paragraph and span. */\nexport async function renderCompactMarkdown(\n    app: App,\n    markdown: string,\n    container: HTMLElement,\n    sourcePath: string,\n    component: Component,\n    isInlineFieldLivePreview: boolean = false\n) {\n    // check if the call is from the CM6 view plugin defined in src/ui/views/inline-field-live-preview.ts\n    if (isInlineFieldLivePreview) {\n        await renderCompactMarkdownForInlineFieldLivePreview(app, markdown, container, sourcePath, component);\n    } else {\n        let subcontainer = container.createSpan();\n        await MarkdownRenderer.render(app, markdown, subcontainer, sourcePath, component);\n\n        let paragraph = subcontainer.querySelector(\":scope > p\");\n        if (subcontainer.children.length == 1 && paragraph) {\n            while (paragraph.firstChild) {\n                subcontainer.appendChild(paragraph.firstChild);\n            }\n            subcontainer.removeChild(paragraph);\n        }\n    }\n}\n\nasync function renderCompactMarkdownForInlineFieldLivePreview(\n    app: App,\n    markdown: string,\n    container: HTMLElement,\n    sourcePath: string,\n    component: Component\n) {\n    const tmpContainer = createSpan();\n    await MarkdownRenderer.render(app, markdown, tmpContainer, sourcePath, component);\n    let paragraph = tmpContainer.querySelector(\":scope > p\");\n    if (tmpContainer.childNodes.length == 1 && paragraph) {\n        while (paragraph.firstChild) {\n            container.appendChild(paragraph.firstChild);\n        }\n    } else {\n        container.replaceChildren(...tmpContainer.childNodes);\n    }\n\n    tmpContainer.remove();\n}\n\n/** Render a pre block with an error in it; returns the element to allow for dynamic updating. */\nexport function renderErrorPre(container: HTMLElement, error: string): HTMLElement {\n    let pre = container.createEl(\"pre\", { cls: [\"dataview\", \"dataview-error\"] });\n    pre.appendText(error);\n    return pre;\n}\n\n/** Render a static codeblock. */\nexport function renderCodeBlock(container: HTMLElement, source: string, language?: string): HTMLElement {\n    let code = container.createEl(\"code\", { cls: [\"dataview\"] });\n    if (language) code.classList.add(\"language-\" + language);\n    code.appendText(source);\n    return code;\n}\n\nexport type ValueRenderContext = \"root\" | \"list\";\n\n/** Prettily render a value into a container with the given settings. */\nexport async function renderValue(\n    app: App,\n    field: Literal,\n    container: HTMLElement,\n    originFile: string,\n    component: Component,\n    settings: QuerySettings,\n    expandList: boolean = false,\n    context: ValueRenderContext = \"root\",\n    depth: number = 0,\n    isInlineFieldLivePreview: boolean = false\n) {\n    // Prevent infinite recursion.\n    if (depth > settings.maxRecursiveRenderDepth) {\n        container.appendText(\"...\");\n        return;\n    }\n\n    if (Values.isNull(field)) {\n        await renderCompactMarkdown(\n            app,\n            settings.renderNullAs,\n            container,\n            originFile,\n            component,\n            isInlineFieldLivePreview\n        );\n    } else if (Values.isDate(field)) {\n        container.appendText(renderMinimalDate(field, settings, currentLocale()));\n    } else if (Values.isDuration(field)) {\n        container.appendText(renderMinimalDuration(field));\n    } else if (Values.isString(field) || Values.isBoolean(field) || Values.isNumber(field)) {\n        await renderCompactMarkdown(app, \"\" + field, container, originFile, component, isInlineFieldLivePreview);\n    } else if (Values.isLink(field)) {\n        await renderCompactMarkdown(app, field.markdown(), container, originFile, component, isInlineFieldLivePreview);\n    } else if (Values.isHtml(field)) {\n        container.appendChild(field);\n    } else if (Values.isWidget(field)) {\n        if (Widgets.isListPair(field)) {\n            await renderValue(\n                app,\n                field.key,\n                container,\n                originFile,\n                component,\n                settings,\n                expandList,\n                context,\n                depth,\n                isInlineFieldLivePreview\n            );\n            container.appendText(\": \");\n            await renderValue(\n                app,\n                field.value,\n                container,\n                originFile,\n                component,\n                settings,\n                expandList,\n                context,\n                depth,\n                isInlineFieldLivePreview\n            );\n        } else if (Widgets.isExternalLink(field)) {\n            let elem = document.createElement(\"a\");\n            elem.textContent = field.display ?? field.url;\n            elem.rel = \"noopener\";\n            elem.target = \"_blank\";\n            elem.classList.add(\"external-link\");\n            elem.href = field.url;\n            container.appendChild(elem);\n        } else {\n            container.appendText(`<unknown widget '${field.$widget}>`);\n        }\n    } else if (Values.isFunction(field)) {\n        container.appendText(\"<function>\");\n    } else if (Values.isArray(field) || DataArray.isDataArray(field)) {\n        if (expandList) {\n            let list = container.createEl(\"ul\", {\n                cls: [\n                    \"dataview\",\n                    \"dataview-ul\",\n                    context == \"list\" ? \"dataview-result-list-ul\" : \"dataview-result-list-root-ul\",\n                ],\n            });\n            for (let child of field) {\n                let li = list.createEl(\"li\", { cls: \"dataview-result-list-li\" });\n                await renderValue(\n                    app,\n                    child,\n                    li,\n                    originFile,\n                    component,\n                    settings,\n                    expandList,\n                    \"list\",\n                    depth + 1,\n                    isInlineFieldLivePreview\n                );\n            }\n        } else {\n            if (field.length == 0) {\n                container.appendText(\"<empty list>\");\n                return;\n            }\n\n            let span = container.createEl(\"span\", { cls: [\"dataview\", \"dataview-result-list-span\"] });\n            let first = true;\n            for (let val of field) {\n                if (first) first = false;\n                else span.appendText(\", \");\n\n                await renderValue(\n                    app,\n                    val,\n                    span,\n                    originFile,\n                    component,\n                    settings,\n                    expandList,\n                    \"list\",\n                    depth + 1,\n                    isInlineFieldLivePreview\n                );\n            }\n        }\n    } else if (Values.isObject(field)) {\n        // Don't render classes in case they have recursive references; spoopy.\n        if (field?.constructor?.name && field?.constructor?.name != \"Object\") {\n            container.appendText(`<${field.constructor.name}>`);\n            return;\n        }\n\n        if (expandList) {\n            let list = container.createEl(\"ul\", { cls: [\"dataview\", \"dataview-ul\", \"dataview-result-object-ul\"] });\n            for (let [key, value] of Object.entries(field)) {\n                let li = list.createEl(\"li\", { cls: [\"dataview\", \"dataview-li\", \"dataview-result-object-li\"] });\n                li.appendText(key + \": \");\n                await renderValue(\n                    app,\n                    value,\n                    li,\n                    originFile,\n                    component,\n                    settings,\n                    expandList,\n                    \"list\",\n                    depth + 1,\n                    isInlineFieldLivePreview\n                );\n            }\n        } else {\n            if (Object.keys(field).length == 0) {\n                container.appendText(\"<empty object>\");\n                return;\n            }\n\n            let span = container.createEl(\"span\", { cls: [\"dataview\", \"dataview-result-object-span\"] });\n            let first = true;\n            for (let [key, value] of Object.entries(field)) {\n                if (first) first = false;\n                else span.appendText(\", \");\n\n                span.appendText(key + \": \");\n                await renderValue(\n                    app,\n                    value,\n                    span,\n                    originFile,\n                    component,\n                    settings,\n                    expandList,\n                    \"list\",\n                    depth + 1,\n                    isInlineFieldLivePreview\n                );\n            }\n        }\n    } else {\n        container.appendText(\"Unrecognized: \" + JSON.stringify(field));\n    }\n}\n"
  },
  {
    "path": "src/ui/views/calendar-view.ts",
    "content": "import { FullIndex } from \"data-index\";\nimport { Link } from \"index\";\nimport { App } from \"obsidian\";\nimport { Calendar, ICalendarSource, IDayMetadata, IDot } from \"obsidian-calendar-ui\";\nimport { executeCalendar } from \"query/engine\";\nimport { Query } from \"query/query\";\nimport { DataviewSettings } from \"settings\";\nimport { renderErrorPre } from \"ui/render\";\nimport { DataviewRefreshableRenderer } from \"ui/refreshable-view\";\nimport { asyncTryOrPropagate } from \"util/normalize\";\nimport type { Moment } from \"moment\";\n\n// CalendarFile is a representation of a particular file, displayed in the calendar view.\n// It'll be represented in the calendar as a dot.\ninterface CalendarFile extends IDot {\n    link: Link;\n}\n\nexport class DataviewCalendarRenderer extends DataviewRefreshableRenderer {\n    private calendar: Calendar;\n    constructor(\n        public query: Query,\n        public container: HTMLElement,\n        public index: FullIndex,\n        public origin: string,\n        public settings: DataviewSettings,\n        public app: App\n    ) {\n        super(container, index, app, settings);\n    }\n\n    async render() {\n        this.container.innerHTML = \"\";\n        let maybeResult = await asyncTryOrPropagate(() =>\n            executeCalendar(this.query, this.index, this.origin, this.settings)\n        );\n        if (!maybeResult.successful) {\n            renderErrorPre(this.container, \"Dataview: \" + maybeResult.error);\n            return;\n        } else if (maybeResult.value.data.length == 0 && this.settings.warnOnEmptyResult) {\n            renderErrorPre(this.container, \"Dataview: Query returned 0 results.\");\n            return;\n        }\n        let dateMap = new Map<string, CalendarFile[]>();\n        for (let data of maybeResult.value.data) {\n            const dot = {\n                color: \"default\",\n                className: \"note\",\n                isFilled: true,\n                link: data.link,\n            };\n            const d = data.date.toFormat(\"yyyyLLdd\");\n            if (!dateMap.has(d)) {\n                dateMap.set(d, [dot]);\n            } else {\n                dateMap.get(d)?.push(dot);\n            }\n        }\n\n        const querySource: ICalendarSource = {\n            getDailyMetadata: async (date: Moment): Promise<IDayMetadata> => {\n                return {\n                    dots: dateMap.get(date.format(\"YYYYMMDD\")) || [],\n                };\n            },\n        };\n\n        const sources: ICalendarSource[] = [querySource];\n        const renderer = this;\n        this.calendar = new Calendar({\n            // eslint-disable-next-line @typescript-eslint/no-explicit-any\n            target: (this as any).container,\n            props: {\n                onHoverDay(date: Moment, targetEl: EventTarget): void {\n                    const vals = dateMap.get(date.format(\"YYYYMMDD\"));\n                    if (!vals || vals.length == 0) {\n                        return;\n                    }\n                    if (vals?.length == 0) {\n                        return;\n                    }\n\n                    renderer.app.workspace.trigger(\"link-hover\", {}, targetEl, vals[0].link.path, vals[0].link.path);\n                },\n                onClickDay: async date => {\n                    const vals = dateMap.get(date.format(\"YYYYMMDD\"));\n                    if (!vals || vals.length == 0) {\n                        return;\n                    }\n                    if (vals?.length == 0) {\n                        return;\n                    }\n                    const file = renderer.app.metadataCache.getFirstLinkpathDest(vals[0].link.path, \"\");\n                    if (file == null) {\n                        return;\n                    }\n                    const leaf = renderer.app.workspace.getUnpinnedLeaf();\n                    await leaf.openFile(file, { active: true });\n                },\n                showWeekNums: false,\n                sources,\n            },\n        });\n    }\n\n    onClose(): Promise<void> {\n        if (this.calendar) {\n            this.calendar.$destroy();\n        }\n        return Promise.resolve();\n    }\n}\n"
  },
  {
    "path": "src/ui/views/inline-field-live-preview.ts",
    "content": "import { App, Component, TFile, editorInfoField, editorLivePreviewField } from \"obsidian\";\nimport { EditorState, RangeSet, RangeSetBuilder, RangeValue, StateEffect, StateField } from \"@codemirror/state\";\nimport {\n    Decoration,\n    DecorationSet,\n    EditorView,\n    PluginValue,\n    ViewPlugin,\n    ViewUpdate,\n    WidgetType,\n} from \"@codemirror/view\";\nimport { extractInlineFields, InlineField, parseInlineValue } from \"data-import/inline-field\";\nimport { canonicalizeVarName } from \"util/normalize\";\nimport { renderCompactMarkdown, renderValue } from \"ui/render\";\nimport { DataviewSettings } from \"settings\";\nimport { selectionAndRangeOverlap } from \"ui/lp-render\";\nimport { syntaxTree } from \"@codemirror/language\";\n\nclass InlineFieldValue extends RangeValue {\n    constructor(public field: InlineField) {\n        super();\n    }\n\n    eq(other: InlineFieldValue): boolean {\n        return this.field.key == other.field.key && this.field.value == other.field.value;\n    }\n}\n\nfunction buildInlineFields(state: EditorState): RangeSet<InlineFieldValue> {\n    const builder = new RangeSetBuilder<InlineFieldValue>();\n    const tree = syntaxTree(state);\n\n    for (let lineNumber = 1; lineNumber <= state.doc.lines; lineNumber++) {\n        const line = state.doc.line(lineNumber);\n        let isInsideCodeBlock = false;\n        tree.iterate({\n            from: line.from,\n            to: line.to,\n            enter: node => {\n                // ignore code blocks\n                if (node.name.startsWith(\"HyperMD-codeblock\")) {\n                    isInsideCodeBlock = true;\n                }\n                return node.name == \"Document\";\n            },\n        });\n        if (!isInsideCodeBlock) {\n            const inlineFields = extractInlineFields(line.text);\n            for (const field of inlineFields) {\n                builder.add(line.from + field.start, line.from + field.end, new InlineFieldValue(field));\n            }\n        }\n    }\n    return builder.finish();\n}\n\n/** A state field that stores the inline fields and their positions as a range set. */\nexport const inlineFieldsField = StateField.define<RangeSet<InlineFieldValue>>({\n    create: buildInlineFields,\n    update(oldFields, tr) {\n        return buildInlineFields(tr.state);\n    },\n});\n\n/** Create a view plugin that renders inline fields in live preview just as in the reading view. */\nexport const replaceInlineFieldsInLivePreview = (app: App, settings: DataviewSettings) =>\n    ViewPlugin.fromClass(\n        class implements PluginValue {\n            decorations: DecorationSet;\n            component: Component;\n\n            constructor(view: EditorView) {\n                this.component = new Component();\n                this.component.load();\n                this.decorations = this.buildDecorations(view);\n            }\n\n            destroy() {\n                this.component.unload();\n            }\n\n            buildDecorations(view: EditorView): DecorationSet {\n                // Disable in the source mode\n                if (!view.state.field(editorLivePreviewField)) return Decoration.none;\n\n                const file = view.state.field(editorInfoField).file;\n                if (!file) return Decoration.none;\n\n                const info = view.state.field(inlineFieldsField);\n                const builder = new RangeSetBuilder<Decoration>();\n                const selection = view.state.selection;\n\n                for (const { from, to } of view.visibleRanges) {\n                    info.between(from, to, (start, end, { field }) => {\n                        // If the inline field is not overlapping with the cursor, we replace it with a widget.\n                        if (!selectionAndRangeOverlap(selection, start, end)) {\n                            builder.add(\n                                start,\n                                end,\n                                Decoration.replace({\n                                    widget: new InlineFieldWidget(\n                                        app,\n                                        field,\n                                        file.path,\n                                        this.component,\n                                        settings,\n                                        view\n                                    ),\n                                })\n                            );\n                        }\n                    });\n                }\n                return builder.finish();\n            }\n\n            update(update: ViewUpdate) {\n                // only activate in LP and not source mode\n                if (!update.state.field(editorLivePreviewField)) {\n                    this.decorations = Decoration.none;\n                    return;\n                }\n\n                const layoutChanged = update.transactions.some(transaction =>\n                    transaction.effects.some(effect => effect.is(workspaceLayoutChangeEffect))\n                );\n\n                if (update.docChanged) {\n                    this.decorations = this.decorations.map(update.changes);\n                    this.updateDecorations(update.view);\n                } else if (update.selectionSet || update.viewportChanged || layoutChanged) {\n                    this.decorations = this.buildDecorations(update.view);\n                }\n            }\n\n            updateDecorations(view: EditorView) {\n                const file = view.state.field(editorInfoField).file;\n                if (!file) {\n                    this.decorations = Decoration.none;\n                    return;\n                }\n\n                const inlineFields = view.state.field(inlineFieldsField);\n                const selection = view.state.selection;\n\n                for (const { from, to } of view.visibleRanges) {\n                    inlineFields.between(from, to, (start, end, { field }) => {\n                        const overlap = selectionAndRangeOverlap(selection, start, end);\n                        if (overlap) {\n                            this.removeDeco(start, end);\n                            return;\n                        } else {\n                            this.addDeco(start, end, field, file, view);\n                        }\n                    });\n                }\n            }\n\n            removeDeco(start: number, end: number) {\n                this.decorations.between(start, end, (from, to) => {\n                    this.decorations = this.decorations.update({\n                        filterFrom: from,\n                        filterTo: to,\n                        filter: () => false,\n                    });\n                });\n            }\n\n            addDeco(start: number, end: number, field: InlineField, file: TFile, view: EditorView) {\n                let exists = false;\n                this.decorations.between(start, end, () => {\n                    exists = true;\n                });\n                if (!exists) {\n                    this.decorations = this.decorations.update({\n                        add: [\n                            {\n                                from: start,\n                                to: end,\n                                value: Decoration.replace({\n                                    widget: new InlineFieldWidget(\n                                        app,\n                                        field,\n                                        file.path,\n                                        this.component,\n                                        settings,\n                                        view\n                                    ),\n                                }),\n                            },\n                        ],\n                    });\n                }\n            }\n        },\n        {\n            decorations: instance => instance.decorations,\n        }\n    );\n\n/** A widget which inline fields are replaced with. */\nclass InlineFieldWidget extends WidgetType {\n    constructor(\n        public app: App,\n        public field: InlineField,\n        public sourcePath: string,\n        public component: Component,\n        public settings: DataviewSettings,\n        public view: EditorView\n    ) {\n        super();\n    }\n\n    eq(other: InlineFieldWidget): boolean {\n        return this.field.key == other.field.key && this.field.value == other.field.value;\n    }\n\n    toDOM() {\n        // A large part of this method was taken from replaceInlineFields() in src/ui/views/inline-field.tsx.\n        // It will be better to extract the common part as a function...\n\n        const renderContainer = createSpan({\n            cls: [\"dataview\", \"inline-field\"],\n        });\n\n        // Block inline fields render the key, parenthesis ones do not.\n        if (this.field.wrapping == \"[\") {\n            const key = renderContainer.createSpan({\n                cls: [\"dataview\", \"inline-field-key\"],\n                attr: {\n                    \"data-dv-key\": this.field.key,\n                    \"data-dv-norm-key\": canonicalizeVarName(this.field.key),\n                },\n            });\n\n            renderCompactMarkdown(this.app, this.field.key, key, this.sourcePath, this.component, true);\n\n            const value = renderContainer.createSpan({\n                cls: [\"dataview\", \"inline-field-value\"],\n                attr: {\n                    \"data-dv-key\": this.field.key,\n                    \"data-dv-norm-key\": canonicalizeVarName(this.field.key),\n                },\n            });\n            renderValue(\n                this.app,\n                parseInlineValue(this.field.value),\n                value,\n                this.sourcePath,\n                this.component,\n                this.settings,\n                false,\n                undefined,\n                undefined,\n                true\n            );\n\n            this.addKeyClickHandler(key, renderContainer);\n            this.addValueClickHandler(value, renderContainer);\n        } else {\n            const value = renderContainer.createSpan({\n                cls: [\"dataview\", \"inline-field-standalone-value\"],\n                attr: {\n                    \"data-dv-key\": this.field.key,\n                    \"data-dv-norm-key\": canonicalizeVarName(this.field.key),\n                },\n            });\n            renderValue(\n                this.app,\n                parseInlineValue(this.field.value),\n                value,\n                this.sourcePath,\n                this.component,\n                this.settings,\n                false,\n                undefined,\n                undefined,\n                true\n            );\n            this.addValueClickHandler(value, renderContainer);\n        }\n\n        return renderContainer;\n    }\n\n    // https://github.com/blacksmithgu/obsidian-dataview/issues/2101\n    // When the user clicks on a rendered inline field, move the cursor to the clicked position.\n    addKeyClickHandler(key: HTMLElement, renderContainer: HTMLElement) {\n        key.addEventListener(\"click\", event => {\n            if (event instanceof MouseEvent) {\n                const rect = key.getBoundingClientRect();\n                const relativePos = (event.x - rect.x) / rect.width;\n                const startPos = this.view.posAtCoords(renderContainer.getBoundingClientRect(), false);\n                const clickedPos = Math.round(startPos + (this.field.startValue - 2 - this.field.start) * relativePos); // 2 is the length of \"::\"\n                this.view.dispatch({ selection: { anchor: clickedPos } });\n            }\n        });\n    }\n\n    addValueClickHandler(value: HTMLElement, renderContainer: HTMLElement) {\n        value.addEventListener(\"click\", event => {\n            if (event instanceof MouseEvent) {\n                const rect = value.getBoundingClientRect();\n                const relativePos = (event.x - rect.x) / rect.width;\n                const startPos = this.view.posAtCoords(renderContainer.getBoundingClientRect(), false);\n                const clickedPos = Math.round(\n                    startPos +\n                        (this.field.startValue - this.field.start) +\n                        (this.field.end - this.field.startValue) * relativePos\n                );\n                this.view.dispatch({ selection: { anchor: clickedPos } });\n            }\n        });\n    }\n}\n\n/**\n * A state effect that represents the workspace's layout change.\n * Mainly intended to detect when the user switches between live preview and source mode.\n */\nexport const workspaceLayoutChangeEffect = StateEffect.define<null>();\n"
  },
  {
    "path": "src/ui/views/inline-field.tsx",
    "content": "import { InlineField, extractInlineFields, parseInlineValue } from \"data-import/inline-field\";\nimport { MarkdownPostProcessorContext, MarkdownRenderChild } from \"obsidian\";\nimport { h, render } from \"preact\";\nimport { DataviewContext, DataviewInit, Lit } from \"ui/markdown\";\nimport { canonicalizeVarName } from \"util/normalize\";\n\n/** Replaces raw textual inline fields in text containers with pretty HTML equivalents. */\nexport async function replaceInlineFields(ctx: MarkdownPostProcessorContext, init: DataviewInit): Promise<void> {\n    const inlineFields = extractInlineFields(init.container.innerHTML);\n    if (inlineFields.length == 0) return;\n\n    const component = new MarkdownRenderChild(init.container);\n    ctx.addChild(component);\n\n    // Iterate through the raw HTML and replace inline field matches with corresponding rendered values.\n    let result = init.container.innerHTML;\n    for (let x = inlineFields.length - 1; x >= 0; x--) {\n        let field = inlineFields[x];\n        let renderContainer = document.createElement(\"span\");\n        renderContainer.addClasses([\"dataview\", \"inline-field\"]);\n\n        // Block inline fields render the key, parenthesis ones do not.\n        if (field.wrapping == \"[\") {\n            const key = renderContainer.createSpan({\n                cls: [\"dataview\", \"inline-field-key\"],\n                attr: {\n                    \"data-dv-key\": field.key,\n                    \"data-dv-norm-key\": canonicalizeVarName(field.key),\n                },\n            });\n\n            // Explicitly set the inner HTML to respect any key formatting that we should carry over.\n            key.innerHTML = field.key;\n\n            renderContainer.createSpan({\n                cls: [\"dataview\", \"inline-field-value\"],\n                attr: { id: \"dataview-inline-field-\" + x },\n            });\n        } else {\n            renderContainer.createSpan({\n                cls: [\"dataview\", \"inline-field-standalone-value\"],\n                attr: {\n                    id: \"dataview-inline-field-\" + x,\n                    \"data-dv-key\": field.key,\n                    \"data-dv-norm-key\": canonicalizeVarName(field.key),\n                },\n            });\n        }\n\n        result = result.slice(0, field.start) + renderContainer.outerHTML + result.slice(field.end);\n    }\n\n    // Use a <template> block to render this HTML properly to nodes.\n    const template = document.createElement(\"template\");\n    template.innerHTML = result;\n\n    // Replace the container children with the new rendered children.\n    // TODO: Replace this with a dom-to-dom diff to reduce the actual amount of updates.\n    init.container.replaceChildren(...template.content.childNodes);\n    let inlineFieldsFromText: InlineField[] | undefined;\n    let hasRetrievedText: boolean = false;\n    for (let index = 0; index < inlineFields.length; index++) {\n        const box = init.container.querySelector(\"#dataview-inline-field-\" + index);\n        if (!box) continue;\n\n        const context = Object.assign({}, init, { container: box, component: component });\n        const parseInlineValueWrapper = (fieldVal: string) => {\n            if (fieldVal.startsWith('<span class=\"math\"')) {\n                // allows math symbols to be rendered in reading view\n                if (!hasRetrievedText) {\n                    hasRetrievedText = true;\n                    let text = ctx.getSectionInfo(init.container)?.text;\n                    if (text) {\n                        inlineFieldsFromText = extractInlineFields(text);\n                    }\n                }\n                if (!inlineFieldsFromText) return parseInlineValue(fieldVal);\n                return parseInlineValue(inlineFieldsFromText[index].value);\n            } else {\n                return parseInlineValue(fieldVal);\n            }\n        };\n\n        render(\n            <DataviewContext.Provider value={context}>\n                <Lit\n                    value={parseInlineValueWrapper(inlineFields[index].value)}\n                    inline={true}\n                    sourcePath={ctx.sourcePath}\n                />\n            </DataviewContext.Provider>,\n            box\n        );\n    }\n}\n"
  },
  {
    "path": "src/ui/views/inline-view.ts",
    "content": "import { FullIndex } from \"data-index\";\nimport { Field } from \"expression/field\";\nimport { App } from \"obsidian\";\nimport { executeInline } from \"query/engine\";\nimport { DataviewSettings } from \"settings\";\nimport { renderErrorPre, renderValue } from \"ui/render\";\nimport { DataviewRefreshableRenderer } from \"ui/refreshable-view\";\nimport { tryOrPropagate } from \"util/normalize\";\n\n/** Refreshable renderer which renders inline instead of in a div. */\nexport class DataviewInlineRenderer extends DataviewRefreshableRenderer {\n    // The box that the error is rendered in, if relevant.\n    errorbox?: HTMLElement;\n\n    constructor(\n        public field: Field,\n        public fieldText: string,\n        public container: HTMLElement,\n        public target: HTMLElement,\n        public index: FullIndex,\n        public origin: string,\n        public settings: DataviewSettings,\n        public app: App\n    ) {\n        super(container, index, app, settings);\n    }\n\n    async render() {\n        this.errorbox?.remove();\n        let result = tryOrPropagate(() => executeInline(this.field, this.origin, this.index, this.settings));\n        if (!result.successful) {\n            this.errorbox = this.container.createEl(\"div\");\n            renderErrorPre(this.errorbox, \"Dataview (for inline query '\" + this.fieldText + \"'): \" + result.error);\n        } else {\n            let temp = document.createElement(\"span\");\n            temp.addClasses([\"dataview\", \"dataview-inline-query\"]);\n            await renderValue(this.app, result.value, temp, this.origin, this, this.settings, false);\n\n            this.target.replaceWith(temp);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ui/views/js-view.ts",
    "content": "import { asyncEvalInContext, DataviewInlineApi } from \"api/inline-api\";\nimport { renderErrorPre, renderValue } from \"ui/render\";\nimport { DataviewRefreshableRenderer } from \"ui/refreshable-view\";\nimport { DataviewApi } from \"api/plugin-api\";\n\nexport class DataviewJSRenderer extends DataviewRefreshableRenderer {\n    static PREAMBLE: string = \"const dataview = this;const dv = this;\";\n\n    constructor(public api: DataviewApi, public script: string, public container: HTMLElement, public origin: string) {\n        super(container, api.index, api.app, api.settings);\n    }\n\n    async render() {\n        this.container.innerHTML = \"\";\n        if (!this.settings.enableDataviewJs) {\n            this.containerEl.innerHTML = \"\";\n            renderErrorPre(\n                this.container,\n                \"Dataview JS queries are disabled. You can enable them in the Dataview settings.\"\n            );\n            return;\n        }\n\n        // Assume that the code is javascript, and try to eval it.\n        try {\n            await asyncEvalInContext(\n                DataviewJSRenderer.PREAMBLE + this.script,\n                new DataviewInlineApi(this.api, this, this.container, this.origin)\n            );\n        } catch (e) {\n            this.containerEl.innerHTML = \"\";\n            renderErrorPre(this.container, \"Evaluation Error: \" + e.stack);\n        }\n    }\n}\n\n/** Inline JS renderer accessible using '=$' by default. */\nexport class DataviewInlineJSRenderer extends DataviewRefreshableRenderer {\n    static PREAMBLE: string = \"const dataview = this;const dv=this;\";\n\n    // The box that the error is rendered in, if relevant.\n    errorbox?: HTMLElement;\n\n    constructor(\n        public api: DataviewApi,\n        public script: string,\n        public container: HTMLElement,\n        public target: HTMLElement,\n        public origin: string\n    ) {\n        super(container, api.index, api.app, api.settings);\n    }\n\n    async render() {\n        this.errorbox?.remove();\n        if (!this.settings.enableDataviewJs || !this.settings.enableInlineDataviewJs) {\n            let temp = document.createElement(\"span\");\n            temp.innerText = \"(disabled; enable in settings)\";\n            this.target.replaceWith(temp);\n            this.target = temp;\n            return;\n        }\n\n        // Assume that the code is javascript, and try to eval it.\n        try {\n            let temp = document.createElement(\"span\");\n            let result = await asyncEvalInContext(\n                DataviewInlineJSRenderer.PREAMBLE + this.script,\n                new DataviewInlineApi(this.api, this, temp, this.origin)\n            );\n            this.target.replaceWith(temp);\n            this.target = temp;\n            if (result === undefined) return;\n\n            renderValue(this.api.app, result, temp, this.origin, this, this.settings, false);\n        } catch (e) {\n            this.errorbox = this.container.createEl(\"div\");\n            renderErrorPre(this.errorbox, \"Dataview (for inline JS query '\" + this.script + \"'): \" + e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ui/views/list-view.tsx",
    "content": "import { MarkdownRenderChild } from \"obsidian\";\nimport { executeList } from \"query/engine\";\nimport { Query } from \"query/query\";\nimport { asyncTryOrPropagate } from \"util/normalize\";\nimport { useContext } from \"preact/hooks\";\nimport {\n    DataviewContext,\n    DataviewInit,\n    ErrorMessage,\n    ErrorPre,\n    Lit,\n    ReactRenderer,\n    useIndexBackedState,\n} from \"ui/markdown\";\nimport { h, Fragment } from \"preact\";\nimport { Literal } from \"data-model/value\";\n\nexport function ListGrouping({ items, sourcePath }: { items: Literal[]; sourcePath: string }) {\n    return (\n        <ul class=\"dataview list-view-ul\">\n            {items.map(item => (\n                <li>\n                    <Lit value={item} sourcePath={sourcePath} />\n                </li>\n            ))}\n        </ul>\n    );\n}\n\nexport type ListViewState =\n    | { state: \"loading\" }\n    | { state: \"error\"; error: string }\n    | { state: \"ready\"; items: Literal[] };\n\n/** Pure view over list elements.  */\nexport function ListView({ query, sourcePath }: { query: Query; sourcePath: string }) {\n    let context = useContext(DataviewContext);\n\n    let items = useIndexBackedState<ListViewState>(\n        context.container,\n        context.app,\n        context.settings,\n        context.index,\n        { state: \"loading\" },\n        async () => {\n            let result = await asyncTryOrPropagate(() =>\n                executeList(query, context.index, sourcePath, context.settings)\n            );\n\n            if (!result.successful) return { state: \"error\", error: result.error, sourcePath };\n            return { state: \"ready\", items: result.value.data };\n        }\n    );\n\n    if (items.state == \"loading\")\n        return (\n            <Fragment>\n                <ErrorPre>Loading...</ErrorPre>\n            </Fragment>\n        );\n    else if (items.state == \"error\")\n        return (\n            <Fragment>\n                {\" \"}\n                <ErrorPre>Dataview: {items.error}</ErrorPre>{\" \"}\n            </Fragment>\n        );\n\n    if (items.items.length == 0 && context.settings.warnOnEmptyResult)\n        return <ErrorMessage message=\"Dataview: No results to show for list query.\" />;\n\n    return <ListGrouping items={items.items} sourcePath={sourcePath} />;\n}\n\nexport function createListView(init: DataviewInit, query: Query, sourcePath: string): MarkdownRenderChild {\n    return new ReactRenderer(init, <ListView query={query} sourcePath={sourcePath} />);\n}\n\nexport function createFixedListView(init: DataviewInit, elements: Literal[], sourcePath: string): MarkdownRenderChild {\n    return new ReactRenderer(init, <ListGrouping items={elements} sourcePath={sourcePath} />);\n}\n"
  },
  {
    "path": "src/ui/views/table-view.tsx",
    "content": "import { Literal } from \"data-model/value\";\nimport { executeTable } from \"query/engine\";\nimport { Query } from \"query/query\";\nimport { asyncTryOrPropagate } from \"util/normalize\";\nimport {\n    DataviewContext,\n    DataviewInit,\n    ErrorMessage,\n    ErrorPre,\n    Lit,\n    Markdown,\n    ReactRenderer,\n    useIndexBackedState,\n} from \"ui/markdown\";\nimport { h, Fragment } from \"preact\";\nimport { useContext } from \"preact/hooks\";\nimport { MarkdownRenderChild } from \"obsidian\";\n\n/** JSX component which returns the result count. */\nfunction ResultCount(props: { length: number }) {\n    const { settings } = useContext(DataviewContext);\n    return settings.showResultCount ? <span class=\"dataview small-text\">{props.length}</span> : <Fragment></Fragment>;\n}\n\n/** Simple table over headings and corresponding values. */\nexport function TableGrouping({\n    headings,\n    values,\n    sourcePath,\n}: {\n    headings: string[];\n    values: Literal[][];\n    sourcePath: string;\n}) {\n    let settings = useContext(DataviewContext).settings;\n\n    return (\n        <Fragment>\n            <table class=\"dataview table-view-table\">\n                <thead class=\"table-view-thead\">\n                    <tr class=\"table-view-tr-header\">\n                        {headings.map((heading, index) => (\n                            <th class=\"table-view-th\">\n                                <Markdown sourcePath={sourcePath} content={heading} />\n                                {index == 0 && <ResultCount length={values.length} />}\n                            </th>\n                        ))}\n                    </tr>\n                </thead>\n                <tbody class=\"table-view-tbody\">\n                    {values.map(row => (\n                        <tr>\n                            {row.map(element => (\n                                <td>\n                                    <Lit value={element} sourcePath={sourcePath} />\n                                </td>\n                            ))}\n                        </tr>\n                    ))}\n                </tbody>\n            </table>\n            {settings.warnOnEmptyResult && values.length == 0 && (\n                <ErrorMessage message=\"Dataview: No results to show for table query.\" />\n            )}\n        </Fragment>\n    );\n}\n\nexport type TableViewState =\n    | { state: \"loading\" }\n    | { state: \"error\"; error: string }\n    | { state: \"ready\"; headings: string[]; values: Literal[][] };\n\n/** Pure view over list elements.  */\nexport function TableView({ query, sourcePath }: { query: Query; sourcePath: string }) {\n    let context = useContext(DataviewContext);\n\n    let items = useIndexBackedState<TableViewState>(\n        context.container,\n        context.app,\n        context.settings,\n        context.index,\n        { state: \"loading\" },\n        async () => {\n            let result = await asyncTryOrPropagate(() =>\n                executeTable(query, context.index, sourcePath, context.settings)\n            );\n            if (!result.successful) return { state: \"error\", error: result.error };\n            return { state: \"ready\", headings: result.value.names, values: result.value.data };\n        }\n    );\n\n    if (items.state == \"loading\")\n        return (\n            <Fragment>\n                <ErrorPre>Loading...</ErrorPre>\n            </Fragment>\n        );\n    else if (items.state == \"error\")\n        return (\n            <Fragment>\n                {\" \"}\n                <ErrorPre>Dataview: {items.error}</ErrorPre>{\" \"}\n            </Fragment>\n        );\n\n    return <TableGrouping headings={items.headings} values={items.values} sourcePath={sourcePath} />;\n}\n\nexport function createTableView(init: DataviewInit, query: Query, sourcePath: string): MarkdownRenderChild {\n    return new ReactRenderer(init, <TableView query={query} sourcePath={sourcePath} />);\n}\n\nexport function createFixedTableView(\n    init: DataviewInit,\n    headings: string[],\n    values: Literal[][],\n    sourcePath: string\n): MarkdownRenderChild {\n    return new ReactRenderer(init, <TableGrouping values={values} headings={headings} sourcePath={sourcePath} />);\n}\n"
  },
  {
    "path": "src/ui/views/task-view.tsx",
    "content": "import { setEmojiShorthandCompletionField, setInlineField } from \"data-import/inline-field\";\nimport { LIST_ITEM_REGEX } from \"data-import/markdown-file\";\nimport { SListEntry, SListItem, STask } from \"data-model/serialized/markdown\";\nimport { GroupElement, Grouping, Groupings } from \"data-model/value\";\nimport { DateTime } from \"luxon\";\nimport { MarkdownRenderChild, Platform, Vault } from \"obsidian\";\nimport { Fragment, h } from \"preact\";\nimport { useContext } from \"preact/hooks\";\nimport { executeTask } from \"query/engine\";\nimport { Query } from \"query/query\";\nimport {\n    DataviewContext,\n    ErrorPre,\n    ErrorMessage,\n    Lit,\n    Markdown,\n    ReactRenderer,\n    useIndexBackedState,\n    DataviewInit,\n} from \"ui/markdown\";\nimport { asyncTryOrPropagate } from \"util/normalize\";\n\n/** Function used to test if a given event correspond to a pressed link */\nfunction wasLinkPressed(evt: preact.JSX.TargetedMouseEvent<HTMLElement>): boolean {\n    return evt.target != null && evt.target != undefined && (evt.target as HTMLElement).tagName == \"A\";\n}\n\n/** JSX component which renders a task element recursively. */\nfunction TaskItem({ item }: { item: STask }) {\n    let context = useContext(DataviewContext);\n\n    // Navigate to the given task on click.\n    const onClicked = (evt: preact.JSX.TargetedMouseEvent<HTMLElement>) => {\n        if (wasLinkPressed(evt)) {\n            return;\n        }\n\n        evt.stopPropagation();\n        const selectionState = {\n            eState: {\n                cursor: {\n                    from: { line: item.line, ch: item.position.start.col },\n                    to: { line: item.line + item.lineCount - 1, ch: item.position.end.col },\n                },\n                line: item.line,\n            },\n        };\n\n        // MacOS interprets the Command key as Meta.\n        context.app.workspace.openLinkText(\n            item.link.toFile().obsidianLink(),\n            item.path,\n            evt.ctrlKey || (evt.metaKey && Platform.isMacOS),\n            selectionState as any\n        );\n    };\n\n    // Check/uncheck the task in the original file.\n    const onChecked = (evt: preact.JSX.TargetedEvent<HTMLInputElement>) => {\n        evt.stopPropagation();\n        const completed = evt.currentTarget.checked;\n        const status = completed ? \"x\" : \" \";\n        // Update data-task on the parent element (css style)\n        const parent = evt.currentTarget.parentElement;\n        parent?.setAttribute(\"data-task\", status);\n\n        let flatted: STask[] = [item];\n\n        if (context.settings.recursiveSubTaskCompletion) {\n            function flatter(iitem: STask | SListItem) {\n                flatted.push(iitem as STask);\n                iitem.children.forEach(flatter);\n            }\n            item.children.forEach(flatter);\n            flatted = flatted.flat(Infinity);\n        }\n\n        async function effectFn() {\n            for (let i = 0; i < flatted.length; i++) {\n                const _item = flatted[i];\n                let updatedText: string = _item.text;\n                if (context.settings.taskCompletionTracking) {\n                    updatedText = setTaskCompletion(\n                        _item.text,\n                        context.settings.taskCompletionUseEmojiShorthand,\n                        context.settings.taskCompletionText,\n                        context.settings.taskCompletionDateFormat,\n                        completed\n                    );\n                }\n                await rewriteTask(context.app.vault, _item, status, updatedText);\n            }\n            context.app.workspace.trigger(\"dataview:refresh-views\");\n        }\n        effectFn();\n    };\n\n    const checked = item.status !== \" \";\n    return (\n        <li\n            class={\"dataview task-list-item\" + (checked ? \" is-checked\" : \"\")}\n            onClick={onClicked}\n            data-task={item.status}\n        >\n            <input class=\"dataview task-list-item-checkbox\" type=\"checkbox\" checked={checked} onClick={onChecked} />\n            <Markdown inline={true} content={item.visual ?? item.text} sourcePath={item.path} />\n            {item.children.length > 0 && <TaskList items={item.children} />}\n        </li>\n    );\n}\n\n/** JSX component which renders a plain list item recursively. */\nfunction ListItem({ item }: { item: SListEntry }) {\n    let context = useContext(DataviewContext);\n\n    // Navigate to the given task on click.\n    const onClicked = (evt: preact.JSX.TargetedMouseEvent<HTMLElement>) => {\n        if (wasLinkPressed(evt)) {\n            return;\n        }\n\n        evt.stopPropagation();\n        const selectionState = {\n            eState: {\n                cursor: {\n                    from: { line: item.line, ch: item.position.start.col },\n                    to: { line: item.line + item.lineCount - 1, ch: item.position.end.col },\n                },\n                line: item.line,\n            },\n        };\n\n        // MacOS interprets the Command key as Meta.\n        context.app.workspace.openLinkText(\n            item.link.toFile().obsidianLink(),\n            item.path,\n            evt.ctrlKey || (evt.metaKey && Platform.isMacOS),\n            selectionState as any\n        );\n    };\n\n    return (\n        <li class=\"dataview task-list-basic-item\" onClick={onClicked}>\n            <Markdown inline={true} content={item.visual ?? item.text} sourcePath={item.path} />\n            {item.children.length > 0 && <TaskList items={item.children} />}\n        </li>\n    );\n}\n\n/** JSX component which renders a list of task items recursively. */\nfunction TaskList({ items }: { items: SListItem[] }) {\n    const settings = useContext(DataviewContext).settings;\n    if (items.length == 0 && settings.warnOnEmptyResult)\n        return <ErrorMessage message=\"Dataview: No results to show for task query.\" />;\n\n    let [nest, _mask] = nestItems(items);\n    return (\n        <ul class=\"contains-task-list\">\n            {nest.map(item =>\n                item.task ? <TaskItem key={listId(item)} item={item} /> : <ListItem key={listId(item)} item={item} />\n            )}\n        </ul>\n    );\n}\n\n/** JSX component which returns the result count. */\nfunction ResultCount(props: { item: SListEntry | STask | GroupElement<SListEntry | STask> }) {\n    const { settings } = useContext(DataviewContext);\n    return settings.showResultCount ? (\n        <span class=\"dataview small-text\">{Groupings.count(props.item.rows)}</span>\n    ) : (\n        <Fragment></Fragment>\n    );\n}\n\n/** JSX component which recursively renders grouped tasks. */\nfunction TaskGrouping({ items, sourcePath }: { items: Grouping<SListItem>; sourcePath: string }) {\n    const isGrouping = items.length > 0 && Groupings.isGrouping(items);\n\n    return (\n        <Fragment>\n            {isGrouping &&\n                items.map(item => (\n                    <Fragment key={item.key}>\n                        <h4>\n                            <Lit value={item.key} sourcePath={sourcePath} />\n                            <ResultCount item={item} />\n                        </h4>\n                        <div class=\"dataview result-group\">\n                            <TaskGrouping items={item.rows} sourcePath={sourcePath} />\n                        </div>\n                    </Fragment>\n                ))}\n            {!isGrouping && <TaskList items={items as SListItem[]} />}\n        </Fragment>\n    );\n}\n\nexport type TaskViewState =\n    | { state: \"loading\" }\n    | { state: \"error\"; error: string }\n    | { state: \"ready\"; items: Grouping<SListItem> };\n\n/**\n * Pure view over (potentially grouped) tasks and list items which allows for checking/unchecking tasks and manipulating\n * the task view.\n */\nexport function TaskView({ query, sourcePath }: { query: Query; sourcePath: string }) {\n    let context = useContext(DataviewContext);\n\n    let items = useIndexBackedState<TaskViewState>(\n        context.container,\n        context.app,\n        context.settings,\n        context.index,\n        { state: \"loading\" },\n        async () => {\n            let result = await asyncTryOrPropagate(() =>\n                executeTask(query, sourcePath, context.index, context.settings)\n            );\n            if (!result.successful) return { state: \"error\", error: result.error, sourcePath };\n            else return { state: \"ready\", items: result.value.tasks };\n        }\n    );\n\n    if (items.state == \"loading\")\n        return (\n            <Fragment>\n                <ErrorPre>Loading</ErrorPre>\n            </Fragment>\n        );\n    else if (items.state == \"error\")\n        return (\n            <Fragment>\n                <ErrorPre>Dataview: {items.error}</ErrorPre>\n            </Fragment>\n        );\n\n    return (\n        <div class=\"dataview dataview-container\">\n            <TaskGrouping items={items.items} sourcePath={sourcePath} />\n        </div>\n    );\n}\n\nexport function createTaskView(init: DataviewInit, query: Query, sourcePath: string): MarkdownRenderChild {\n    return new ReactRenderer(init, <TaskView query={query} sourcePath={sourcePath} />);\n}\n\nexport function createFixedTaskView(\n    init: DataviewInit,\n    items: Grouping<SListItem>,\n    sourcePath: string\n): MarkdownRenderChild {\n    return new ReactRenderer(init, <TaskGrouping items={items} sourcePath={sourcePath} />);\n}\n\n/////////////////////////\n// Task De-Duplication //\n/////////////////////////\n\nfunction listId(item: SListItem): string {\n    return item.path + \":\" + item.line;\n}\n\nfunction parentListId(item: SListItem): string {\n    return item.path + \":\" + item.parent;\n}\n\n/** Compute a map of all task IDs -> tasks. */\nfunction enumerateChildren(item: SListItem, output: Map<string, SListItem>): Map<string, SListItem> {\n    if (!output.has(listId(item))) output.set(listId(item), item);\n    for (let child of item.children) enumerateChildren(child, output);\n\n    return output;\n}\n\n/** Replace basic tasks with tasks from a lookup map. Retains the original order of the list. */\nfunction replaceChildren(elements: SListItem[], lookup: Map<string, SListItem>): SListItem[] {\n    return elements.map(element => {\n        element.children = replaceChildren(element.children, lookup);\n\n        const id = listId(element);\n        const map = lookup.get(id);\n\n        if (map) return map;\n        else return element;\n    });\n}\n\n/**\n * Removes tasks from a list if they are already present by being a child of another task. Fixes child pointers.\n * Retains original order of input list.\n */\nexport function nestItems(raw: SListItem[]): [SListItem[], Set<string>] {\n    let elements: Map<string, SListItem> = new Map();\n    let mask: Set<string> = new Set();\n\n    for (let elem of raw) {\n        let id = listId(elem);\n        elements.set(id, elem);\n        mask.add(id);\n    }\n\n    // List all elements & their children in the lookup map.\n    for (let elem of raw) enumerateChildren(elem, elements);\n\n    let roots = raw.filter(\n        elem => elem.parent == undefined || elem.parent == null || !elements.has(parentListId(elem))\n    );\n    return [replaceChildren(roots, elements), mask];\n}\n\n/**\n * Recursively removes tasks from each subgroup if they are already present by being a child of another task.\n * Fixes child pointers. Retains original order of input list.\n */\nexport function nestGroups(raw: Grouping<SListItem>): Grouping<SListItem> {\n    if (Groupings.isGrouping(raw)) {\n        return raw.map(g => {\n            return { key: g.key, rows: nestGroups(g.rows) };\n        });\n    } else {\n        return nestItems(raw)[0];\n    }\n}\n\n///////////////////////\n// Task Manipulation //\n///////////////////////\n\n/** Trim empty ending lines. */\nfunction trimEndingLines(text: string): string {\n    let parts = text.split(/\\r?\\n/u);\n    let trim = parts.length - 1;\n    while (trim > 0 && parts[trim].trim() == \"\") trim--;\n\n    return parts.join(\"\\n\");\n}\n\n/** Set the task completion key on check. */\nexport function setTaskCompletion(\n    originalText: string,\n    useEmojiShorthand: boolean,\n    completionKey: string,\n    completionDateFormat: string,\n    complete: boolean\n): string {\n    const blockIdRegex = /\\^[a-z0-9\\-]+/i;\n\n    if (!complete && !useEmojiShorthand)\n        return trimEndingLines(setInlineField(originalText.trimEnd(), completionKey)).trimEnd();\n\n    let parts = originalText.split(/\\r?\\n/u);\n    const matches = blockIdRegex.exec(parts[parts.length - 1]);\n    console.debug(\"matchreg\", matches);\n\n    let processedPart = parts[parts.length - 1].split(blockIdRegex).join(\"\"); // last part without block id\n    if (useEmojiShorthand) {\n        processedPart = setEmojiShorthandCompletionField(\n            processedPart,\n            complete ? DateTime.now().toFormat(\"yyyy-MM-dd\") : \"\"\n        );\n    } else {\n        processedPart = setInlineField(processedPart, completionKey, DateTime.now().toFormat(completionDateFormat));\n    }\n    processedPart = `${processedPart.trimEnd()}${matches?.length ? \" \" + matches[0].trim() : \"\"}`.trimEnd(); // add back block id\n    parts[parts.length - 1] = processedPart;\n\n    return parts.join(\"\\n\");\n}\n\n/** Rewrite a task with the given completion status and new text. */\nexport async function rewriteTask(vault: Vault, task: STask, desiredStatus: string, desiredText?: string) {\n    if (desiredStatus == task.status && (desiredText == undefined || desiredText == task.text)) return;\n    desiredStatus = desiredStatus == \"\" ? \" \" : desiredStatus;\n\n    let rawFiletext = await vault.adapter.read(task.path);\n    let hasRN = rawFiletext.contains(\"\\r\");\n    let filetext = rawFiletext.split(/\\r?\\n/u);\n\n    if (filetext.length < task.line) return;\n    let match = LIST_ITEM_REGEX.exec(filetext[task.line]);\n    if (!match || match[2].length == 0) return;\n\n    let taskTextParts = task.text.split(\"\\n\");\n    if (taskTextParts[0].trim() != match[3].trim()) return;\n\n    // We have a positive match here at this point, so go ahead and do the rewrite of the status.\n    let initialSpacing = /^[\\s>]*/u.exec(filetext[task.line])!![0];\n    if (desiredText) {\n        let desiredParts = desiredText.split(\"\\n\");\n\n        let newTextLines: string[] = [`${initialSpacing}${task.symbol} [${desiredStatus}] ${desiredParts[0]}`].concat(\n            desiredParts.slice(1).map(l => initialSpacing + \"\\t\" + l)\n        );\n\n        filetext.splice(task.line, task.lineCount, ...newTextLines);\n    } else {\n        filetext[task.line] = `${initialSpacing}${task.symbol} [${desiredStatus}] ${taskTextParts[0].trim()}`;\n    }\n\n    let newText = filetext.join(hasRN ? \"\\r\\n\" : \"\\n\");\n    await vault.adapter.write(task.path, newText);\n}\n"
  },
  {
    "path": "src/util/hash.ts",
    "content": "// cyrb53 (c) 2018 bryc (github.com/bryc). License: Public domain. Attribution appreciated.\n// A fast and simple 64-bit (or 53-bit) string hash function with decent collision resistance.\n// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.\n// See https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480\n// https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js\n\nexport function cyrb53(str: string, seed: number = 0): number {\n    let h1 = 0xdeadbeef ^ seed,\n        h2 = 0x41c6ce57 ^ seed;\n    for (let i = 0, ch; i < str.length; i++) {\n        ch = str.charCodeAt(i);\n        h1 = Math.imul(h1 ^ ch, 2654435761);\n        h2 = Math.imul(h2 ^ ch, 1597334677);\n    }\n    h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n    h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n    h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n    h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n    // For a full 64-bit value we could return\n    //  [h2>>>0, h1>>>0]\n    return 4294967296 * (2097151 & h2) + (h1 >>> 0); // ;\n}\n"
  },
  {
    "path": "src/util/locale.ts",
    "content": "/** Test-environment-friendly function which fetches the current system locale. */\nexport function currentLocale(): string {\n    if (typeof window === \"undefined\") return \"en-US\";\n    return window.navigator.language;\n}\n"
  },
  {
    "path": "src/util/media.ts",
    "content": "import { Link } from \"data-model/value\";\n\nexport const IMAGE_EXTENSIONS = Object.freeze(\n    new Set([\n        \".tif\",\n        \".tiff\",\n        \".gif\",\n        \".png\",\n        \".apng\",\n        \".avif\",\n        \".jpg\",\n        \".jpeg\",\n        \".jfif\",\n        \".pjepg\",\n        \".pjp\",\n        \".svg\",\n        \".webp\",\n        \".bmp\",\n        \".ico\",\n        \".cur\",\n    ])\n);\n\n/** Determines if the given link points to an embedded image. */\nexport function isImageEmbed(link: Link): boolean {\n    if (!link.path.contains(\".\")) return false;\n\n    let extension = link.path.substring(link.path.lastIndexOf(\".\"));\n    return link.type == \"file\" && link.embed && IMAGE_EXTENSIONS.has(extension);\n}\n\n/** Extract text of the form 'WxH' or 'W' from the display of a link. */\nexport function extractImageDimensions(link: Link): [number, number] | [number] | undefined {\n    if (!link.display) return undefined;\n\n    let match = /^(\\d+)x(\\d+)$/iu.exec(link.display);\n    if (match) return [parseInt(match[1]), parseInt(match[2])];\n\n    let match2 = /^(\\d+)/.exec(link.display);\n    if (match2) return [parseInt(match2[1])];\n\n    // No match.\n    return undefined;\n}\n"
  },
  {
    "path": "src/util/normalize.ts",
    "content": "import { DateTime, Duration } from \"luxon\";\nimport { Result } from \"api/result\";\nimport * as P from \"parsimmon\";\nimport emojiRegex from \"emoji-regex\";\nimport { QuerySettings } from \"settings\";\nimport removeMd from \"remove-markdown\";\n\n/** Normalize a duration to all of the proper units. */\nexport function normalizeDuration(dur: Duration) {\n    if (dur === undefined || dur === null) return dur;\n\n    return dur.shiftToAll().normalize();\n}\n\n/** Strip the time components of a date time object. */\nexport function stripTime(dt: DateTime): DateTime {\n    if (dt === null || dt === undefined) return dt;\n\n    return DateTime.fromObject({\n        year: dt.year,\n        month: dt.month,\n        day: dt.day,\n    });\n}\n\n/** Try to extract a YYYYMMDD date from a string. */\nexport function extractDate(str: string): DateTime | undefined {\n    let dateMatch = /(\\d{4})-(\\d{2})-(\\d{2})/.exec(str);\n    if (!dateMatch) dateMatch = /(\\d{4})(\\d{2})(\\d{2})/.exec(str);\n    if (dateMatch) {\n        let year = Number.parseInt(dateMatch[1]);\n        let month = Number.parseInt(dateMatch[2]);\n        let day = Number.parseInt(dateMatch[3]);\n        return DateTime.fromObject({ year, month, day });\n    }\n\n    return undefined;\n}\n\n/** Get the folder containing the given path (i.e., like computing 'path/..'). */\nexport function getParentFolder(path: string): string {\n    return path.split(\"/\").slice(0, -1).join(\"/\");\n}\n\n/** Get the file name for the file referenced in the given path, by stripping the parent folders. */\nexport function getFileName(path: string): string {\n    return path.includes(\"/\") ? path.substring(path.lastIndexOf(\"/\") + 1) : path;\n}\n\n/** Get the \"title\" for a file, by stripping other parts of the path as well as the extension. */\nexport function getFileTitle(path: string): string {\n    if (path.includes(\"/\")) path = path.substring(path.lastIndexOf(\"/\") + 1);\n    if (path.endsWith(\".md\")) path = path.substring(0, path.length - 3);\n    return path;\n}\n\n/** Get the extension of a file from the file path. */\nexport function getExtension(path: string): string {\n    if (!path.includes(\".\")) return \"\";\n    return path.substring(path.lastIndexOf(\".\") + 1);\n}\n\n/** Parse all subtags out of the given tag. I.e., #hello/i/am would yield [#hello/i/am, #hello/i, #hello]. */\nexport function extractSubtags(tag: string): string[] {\n    let result = [tag];\n    while (tag.includes(\"/\")) {\n        tag = tag.substring(0, tag.lastIndexOf(\"/\"));\n        result.push(tag);\n    }\n\n    return result;\n}\n\n/** Try calling the given function; on failure, return the error message.  */\nexport function tryOrPropagate<T>(func: () => Result<T, string>): Result<T, string> {\n    try {\n        return func();\n    } catch (error) {\n        return Result.failure(\"\" + error + \"\\n\\n\" + error.stack);\n    }\n}\n\n/** Try asynchronously calling the given function; on failure, return the error message. */\nexport async function asyncTryOrPropagate<T>(func: () => Promise<Result<T, string>>): Promise<Result<T, string>> {\n    try {\n        return await func();\n    } catch (error) {\n        return Result.failure(\"\" + error + \"\\n\\n\" + error.stack);\n    }\n}\n\n/**\n * Escape regex characters in a string.\n * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions.\n */\nexport function escapeRegex(str: string) {\n    return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/** A parsimmon parser which canonicalizes variable names while properly respecting emoji. */\nconst VAR_NAME_CANONICALIZER: P.Parser<string> = P.alt(\n    P.regex(new RegExp(emojiRegex(), \"\")),\n    P.regex(/[0-9\\p{Letter}_-]+/u).map(str => str.toLocaleLowerCase()),\n    P.whitespace.map(_ => \"-\"),\n    P.any.map(_ => \"\")\n)\n    .many()\n    .map(result => result.join(\"\"));\n\n/** Convert an arbitrary variable name into something JS/query friendly. */\nexport function canonicalizeVarName(name: string): string {\n    return VAR_NAME_CANONICALIZER.tryParse(name);\n}\n\nconst HEADER_CANONICALIZER: P.Parser<string> = P.alt(\n    P.regex(new RegExp(emojiRegex(), \"\")),\n    P.regex(/[0-9\\p{Letter}_-]+/u),\n    P.whitespace.map(_ => \" \"),\n    P.any.map(_ => \" \")\n)\n    .many()\n    .map(result => {\n        return result.join(\"\").split(/\\s+/).join(\" \").trim();\n    });\n\n/**\n * Normalizes the text in a header to be something that is actually linkable to. This mimics\n * how Obsidian does it's normalization, collapsing repeated spaces and stripping out control characters.\n */\nexport function normalizeHeaderForLink(header: string): string {\n    return HEADER_CANONICALIZER.tryParse(header);\n}\n\n/** Render a DateTime in a minimal format to save space. */\nexport function renderMinimalDate(time: DateTime, settings: QuerySettings, locale: string): string {\n    // If there is no relevant time specified, fall back to just rendering the date.\n    if (time.second == 0 && time.minute == 0 && time.hour == 0) {\n        return time.toLocal().toFormat(settings.defaultDateFormat, { locale });\n    }\n\n    return time.toLocal().toFormat(settings.defaultDateTimeFormat, { locale });\n}\n\n/** Render a duration in a minimal format to save space. */\nexport function renderMinimalDuration(dur: Duration): string {\n    dur = normalizeDuration(dur);\n\n    // toHuman outputs zero quantities e.g. \"0 seconds\"\n    dur = Duration.fromObject(\n        Object.fromEntries(Object.entries(dur.toObject()).filter(([, quantity]) => quantity != 0))\n    );\n\n    return dur.toHuman();\n}\n\n/** Determine if two sets are equal in contents. */\nexport function setsEqual<T>(first: Set<T>, second: Set<T>): boolean {\n    if (first.size != second.size) return false;\n    for (let elem of first) if (!second.has(elem)) return false;\n\n    return true;\n}\n\n/** Normalize a markdown string. Removes all markdown tags and obsidian links. */\nexport function normalizeMarkdown(str: string): string {\n    // [[test]] -> test\n    let interim = str.replace(/\\[\\[([^\\|]*?)\\]\\]/g, \"$1\");\n\n    // [[test|test]] -> test\n    interim = interim.replace(/\\[\\[.*?\\|(.*?)\\]\\]/, \"$1\");\n\n    // remove markdown tags\n    interim = removeMd(interim);\n\n    return interim;\n}\n"
  },
  {
    "path": "styles.css",
    "content": ".block-language-dataview {\n    overflow-y: auto;\n}\n\n/*****************/\n/** Table Views **/\n/*****************/\n\n/* List View Default Styling; rendered internally as a table. */\n.table-view-table {\n    width: 100%;\n}\n\n.table-view-table > thead > tr, .table-view-table > tbody > tr {\n    margin-top: 1em;\n    margin-bottom: 1em;\n    text-align: left;\n}\n\n.table-view-table > tbody > tr:hover {\n    background-color: var(--table-row-background-hover);\n}\n\n.table-view-table > thead > tr > th {\n    font-weight: 700;\n    font-size: larger;\n    border-top: none;\n    border-left: none;\n    border-right: none;\n    border-bottom: solid;\n\n    max-width: 100%;\n}\n\n.table-view-table > tbody > tr > td {\n    text-align: left;\n    border: none;\n    font-weight: 400;\n    max-width: 100%;\n}\n\n.table-view-table ul, .table-view-table ol {\n    margin-block-start: 0.2em !important;\n    margin-block-end: 0.2em !important;\n}\n\n/** Rendered value styling for any view. */\n.dataview-result-list-root-ul {\n    padding: 0em !important;\n    margin: 0em !important;\n}\n\n.dataview-result-list-ul {\n    margin-block-start: 0.2em !important;\n    margin-block-end: 0.2em !important;\n}\n\n/** Generic grouping styling. */\n.dataview.result-group {\n    padding-left: 8px;\n}\n\n/*******************/\n/** Inline Fields **/\n/*******************/\n\n.dataview.inline-field-key {\n    padding-left: 8px;\n    padding-right: 8px;\n    font-family: var(--font-monospace);\n    background-color: var(--background-primary-alt);\n    color: var(--nav-item-color-selected);\n}\n\n.dataview.inline-field-value {\n    padding-left: 8px;\n    padding-right: 8px;\n    font-family: var(--font-monospace);\n    background-color: var(--background-secondary-alt);\n    color: var(--nav-item-color-selected);\n}\n\n.dataview.inline-field-standalone-value {\n    padding-left: 8px;\n    padding-right: 8px;\n    font-family: var(--font-monospace);\n    background-color: var(--background-secondary-alt);\n    color: var(--nav-item-color-selected);\n}\n\n/***************/\n/** Task View **/\n/***************/\n\n.dataview.task-list-item, .dataview.task-list-basic-item {\n    margin-top: 3px;\n    margin-bottom: 3px;\n    transition: 0.4s;\n}\n\n.dataview.task-list-item:hover, .dataview.task-list-basic-item:hover {\n    background-color: var(--text-selection);\n    box-shadow: -40px 0 0 var(--text-selection);\n    cursor: pointer;\n}\n\n/*****************/\n/** Error Views **/\n/*****************/\n\ndiv.dataview-error-box {\n    width: 100%;\n    min-height: 150px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    border: 4px dashed var(--background-secondary);\n}\n\n.dataview-error-message {\n    color: var(--text-muted);\n    text-align: center;\n}\n\n/*************************/\n/** Additional Metadata **/\n/*************************/\n\n.dataview.small-text {\n    font-size: smaller;\n    color: var(--text-muted);\n    margin-left: 3px;\n}\n\n.dataview.small-text::before {\n\tcontent: \"(\";\n}\n\n.dataview.small-text::after {\n\tcontent: \")\";\n}\n"
  },
  {
    "path": "test-vault/.obsidian/community-plugins.json",
    "content": "[\n  \"hot-reload\",\n  \"dataview\"\n]"
  },
  {
    "path": "test-vault/Books.md",
    "content": "# Non-Fiction\n```dataview\nlist from [[Non-Fiction]]\n```\n# Fiction\n```dataview\nlist from [[Fiction]]\n```\n"
  },
  {
    "path": "test-vault/Home.md",
    "content": "Home\n\n[[example tables]]\n\n[[example lists]]\n\n[[example tasks]]"
  },
  {
    "path": "test-vault/blog/2020-08-08-an-earlier-post.md",
    "content": "---\ndraft: true\nstart_date: 2021-10-01T02:00:00+02:00\nend_date: 2021-10-01T13:00:00-10:00\nanother_date: 2021-10-01T01:00:00+04:00\nfourth_date: 2021-11-01T01:00:00[Europe/Paris]\n---\n# A Blog Post in 2020\n\n`= this.start_date`\n\n`= this.end_date`\n\n`= this.another_date`\n\n`= localTime(this.fourth_date)`"
  },
  {
    "path": "test-vault/blog/2021-08-08-a-post.md",
    "content": ""
  },
  {
    "path": "test-vault/books/Catcher in the Rye.md",
    "content": "- Author: [[J.D. Salinger]]\n- Tag: [[Fiction]]"
  },
  {
    "path": "test-vault/books/Origin of Species.md",
    "content": "- Author: [[Charles Darwin]]\n- Tag: [[Non-Fiction]]"
  },
  {
    "path": "test-vault/books/The Great Gatsby.md",
    "content": "- Author: [[Scott Fitzgerald]]\n- Tag: [[Fiction]]"
  },
  {
    "path": "test-vault/example calendars.md",
    "content": "\n```dataview\nCALENDAR thoughtOfDate\nFROM\n\"recipes\"\n```\n\n\n```dataview\nCALENDAR file.mtime\nFROM\n\"recipes\"\n```\n"
  },
  {
    "path": "test-vault/example lists.md",
    "content": "# Lists\n```dataview\nlist from \"recipes\"\n```"
  },
  {
    "path": "test-vault/example tables.md",
    "content": "# Tables\n```dataview\ntable mtime as Modified, draft as Draft from \"blog\"\n```\n\n```dataview\ntable cuisine as Cuisine, needsStove as \"Needs Stove\"\nfrom \"recipes\"\n```\n\n\n```dataview\nTABLE file.name, file.folder, file.ctime, file.cday, file.mtime, file.mday, file.tags, file.frontmatter, file.name, file.folder, file.ctime, file.cday, file.mtime, file.mday, file.tags, file.frontmatter\nWHERE file = this.file\n```\n\n"
  },
  {
    "path": "test-vault/recipes/pbj.md",
    "content": "---\ncuisine: American\nneedsStove: false\nthoughtOfDate: 2021-12-11\n---\n# Peanut Butter and Jelly\n## Ingredients\n- [ ] Bread\n- [ ] Peanut Butter\n- [ ] Jelly\n\n## Instructions\n1. Go hog wild"
  },
  {
    "path": "test-vault/recipes/toast.md",
    "content": "---\ncuisine: British\nneedsStove: true\nthoughtOfDate: 2021-12-10\n---\n\n# Toast\n## Ingredients \n- [ ] Bread\n- [ ] Butter\n\n## Instructions\n1. Heat bread in toaster until toast-colored\n2. Put butter on bread"
  },
  {
    "path": "test-vault/tasks/Annotated Tasks.md",
    "content": "```dataview\ntask where annotated\n```\n\ngrouped\n```dataview\ntask\nwhere p\ngroup by p\n```\n"
  },
  {
    "path": "test-vault/tasks/Completed Tasks.md",
    "content": "## Completed Tasks\n\n```dataview\ntask WHERE completed\n```"
  },
  {
    "path": "test-vault/tasks/Grouped Sorted Tasks.md",
    "content": "```dataview\ntask\nwhere p\nsort p asc\ngroup by p\n```"
  },
  {
    "path": "test-vault/tasks/Sorted Tasks.md",
    "content": "```dataview\ntask\nwhere p\nsort p asc\n```"
  },
  {
    "path": "test-vault/tasks/Tasks Completed on specific Date.md",
    "content": "## Completed on a specific date\n```dataview\ntask from \"tasks\" where\ncompletion = date(2021-08-06)\n```\n"
  },
  {
    "path": "test-vault/tasks/Tasks in a specific section.md",
    "content": "# Tasks in a specific section\n\n```dataview\ntask\nwhere meta(section).subpath = \"Section\"\ngroup by section\n```\n"
  },
  {
    "path": "test-vault/tasks/Uncompleted Tasks.md",
    "content": "## Uncompleted\n```dataview\ntask from -\"recipes\"\nWHERE !completed\n```\n"
  },
  {
    "path": "test-vault/tasks/checklist.md",
    "content": "- [x] Normal task, tags inherited from page [completion:: 2021-10-23]\n* [ ] Task with a #tag, adds to inherited page tags\n\t* [ ] Task that inherits tag from above and page tags\n* [x] Completed task ✅ 2021-08-06 📅 2021-08-07\n* [x] Completed task [completion::2021-08-06] [due::2021-08-07]\n* [ ] task with [annotation::arbitrary] [completion:: 2021-10-23]\n* [ ] Scheduled task 📅  2021-08-07\n* [ ] Task that overrides creation date of file ➕ 2021-08-06\n* [ ] Repeating task 🔁Mondays\n* [ ] #tell @person some important thing [p::1]\n* [x] a less important thing [p::2]\n* [ ] another important thing [p::1]\n\n#page-tag\n\n## Section\n- [ ] additional task with a block id ^block-id\n- [ ] additional task, should link to header\n"
  },
  {
    "path": "test-vault/tasks/example tasks.md",
    "content": "# Tasks\n\n![[Uncompleted Tasks]]\n\n![[Completed Tasks]]\n\n![[Tasks Completed on specific Date]]\n\n![[Annotated Tasks]]\n\n![[Sorted Tasks]]\n\n![[Grouped Sorted Tasks]]\n\n![[Tasks in a specific section]]"
  },
  {
    "path": "test-vault/untracked/README.md",
    "content": "Git will not track files in this folder. (except this one)\n\nFeel free to write whatever for anything you want to try. Git won't see it and it won't clog up your `git status`. But if its an important thing to test, maybe include it somewhere where other people can test it too. Thanks :)\n"
  },
  {
    "path": "tsconfig-lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"outDir\": \"lib\",\n    \"plugins\": [{ \"transform\": \"@zerollup/ts-transform-paths\" }],\n  },\n  \"include\": [\"src/**/*.ts\"],\n  \"exclude\": [\"src/test/**/*\", \"lib/**/*\"]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \"src\",\n    \"sourceMap\": true,\n    \"inlineSourceMap\": false,\n    \"inlineSources\": true,\n    \"module\": \"ESNext\",\n    \"target\": \"ES2022\",\n    \"allowJs\": false,\n    \"jsx\": \"react\",\n    \"jsxFactory\": \"h\",\n    \"noImplicitAny\": true,\n    \"strictFunctionTypes\": true,\n    \"strictNullChecks\": true,\n    \"noImplicitReturns\": true,\n    \"noImplicitThis\": true,\n    \"noUnusedLocals\": true,\n    \"moduleResolution\": \"node\",\n    \"importHelpers\": true,\n    \"downlevelIteration\": true,\n    \"esModuleInterop\": true,\n    \"lib\": [\"dom\", \"scripthost\", \"ES2022\"]\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\"],\n  \"exclude\": [\"lib/**/*\"]\n}\n"
  },
  {
    "path": "versions.json",
    "content": "{\n  \"0.0.1\": \"0.10.7\",\n  \"0.1.0\": \"0.10.7\",\n  \"0.1.1\": \"0.10.7\",\n  \"0.1.2\": \"0.10.7\",\n  \"0.1.3\": \"0.10.7\",\n  \"0.1.4\": \"0.10.7\",\n  \"0.1.5\": \"0.10.13\",\n  \"0.1.6\": \"0.10.13\",\n  \"0.1.7\": \"0.10.13\",\n  \"0.1.8\": \"0.10.13\",\n  \"0.1.9\": \"0.10.13\",\n  \"0.1.10\": \"0.10.13\",\n  \"0.2.0\": \"0.10.13\",\n  \"0.2.1\": \"0.10.13\",\n  \"0.2.2\": \"0.10.13\",\n  \"0.2.3\": \"0.10.13\",\n  \"0.2.4\": \"0.10.13\",\n  \"0.2.5\": \"0.10.13\",\n  \"0.2.6\": \"0.10.13\",\n  \"0.2.7\": \"0.11.10\",\n  \"0.2.8\": \"0.11.10\",\n  \"0.2.9\": \"0.11.10\",\n  \"0.2.10\": \"0.11.10\",\n  \"0.2.11\": \"0.11.10\",\n  \"0.2.12\": \"0.11.10\",\n  \"0.2.13\": \"0.11.10\",\n  \"0.2.14\": \"0.11.10\",\n  \"0.2.15\": \"0.11.10\",\n  \"0.2.16\": \"0.11.10\",\n  \"0.2.17\": \"0.11.10\",\n  \"0.3.0\": \"0.11.10\",\n  \"0.3.1\": \"0.11.10\",\n  \"0.3.2\": \"0.11.10\",\n  \"0.3.3\": \"0.11.10\",\n  \"0.3.4\": \"0.11.10\",\n  \"0.3.5\": \"0.11.10\",\n  \"0.3.6\": \"0.11.10\",\n  \"0.3.7\": \"0.11.10\",\n  \"0.3.8\": \"0.11.10\",\n  \"0.3.9\": \"0.11.10\",\n  \"0.3.10\": \"0.11.10\",\n  \"0.3.11\": \"0.11.10\",\n  \"0.3.12\": \"0.11.10\",\n  \"0.3.13\": \"0.11.10\",\n  \"0.4.0\": \"0.12.0\",\n  \"0.4.1\": \"0.12.0\",\n  \"0.4.2\": \"0.12.0\",\n  \"0.4.3\": \"0.12.0\",\n  \"0.4.4\": \"0.12.0\",\n  \"0.4.5\": \"0.12.0\",\n  \"0.4.6\": \"0.12.0\",\n  \"0.4.7\": \"0.12.0\",\n  \"0.4.8\": \"0.12.0\",\n  \"0.4.9\": \"0.12.0\",\n  \"0.4.10\": \"0.12.0\",\n  \"0.4.11\": \"0.12.0\",\n  \"0.4.12\": \"0.12.0\",\n  \"0.4.12-hotfix1\": \"0.12.0\",\n  \"0.4.13\": \"0.12.0\",\n  \"0.4.14\": \"0.12.0\",\n  \"0.4.15\": \"0.12.0\",\n  \"0.4.16\": \"0.12.0\",\n  \"0.4.17\": \"0.12.0\",\n  \"0.4.18\": \"0.12.0\",\n  \"0.4.19\": \"0.12.0\",\n  \"0.4.20\": \"0.12.0\",\n  \"0.4.21\": \"0.12.0\",\n  \"0.4.22\": \"0.12.0\",\n  \"0.4.23\": \"0.12.0\",\n  \"0.4.23b\": \"0.12.0\",\n  \"0.4.23c\": \"0.12.0\",\n  \"0.4.23d\": \"0.12.0\",\n  \"0.4.23e\": \"0.12.0\",\n  \"0.4.23f\": \"0.12.0\",\n  \"0.4.24\": \"0.12.0\",\n  \"0.4.25\": \"0.12.0\",\n  \"0.4.26\": \"0.12.0\",\n  \"0.5.0\": \"0.13.11\",\n  \"0.5.1\": \"0.13.11\",\n  \"0.5.2\": \"0.13.11\",\n  \"0.5.3\": \"0.13.11\",\n  \"0.5.4\": \"0.13.11\",\n  \"0.5.5\": \"0.13.11\",\n  \"0.5.6\": \"0.13.11\",\n  \"0.5.7\": \"0.13.11\",\n  \"0.5.8\": \"0.13.11\",\n  \"0.5.9\": \"0.13.11\",\n  \"0.5.10\": \"0.13.11\",\n  \"0.5.11\": \"0.13.11\",\n  \"0.5.12\": \"0.13.11\",\n  \"0.5.13\": \"0.13.11\",\n  \"0.5.14\": \"0.13.11\",\n  \"0.5.15\": \"0.13.11\",\n  \"0.5.16\": \"0.13.11\",\n  \"0.5.17\": \"0.13.11\",\n  \"0.5.18\": \"0.13.11\",\n  \"0.5.19\": \"0.13.11\",\n  \"0.5.20\": \"0.13.11\",\n  \"0.5.21\": \"0.12.0\",\n  \"0.5.22\": \"0.12.0\",\n  \"0.5.23\": \"0.12.0\",\n  \"0.5.24\": \"0.12.0\",\n  \"0.5.25\": \"0.12.0\",\n  \"0.5.26\": \"0.12.0\",\n  \"0.5.27\": \"0.12.0\",\n  \"0.5.28\": \"0.12.0\",\n  \"0.5.29\": \"0.12.0\",\n  \"0.5.30\": \"0.12.0\",\n  \"0.5.31\": \"0.12.0\",\n  \"0.5.32\": \"0.12.0\",\n  \"0.5.33\": \"0.12.0\",\n  \"0.5.34\": \"0.12.0\",\n  \"0.5.35\": \"0.12.0\",\n  \"0.5.36\": \"0.12.0\",\n  \"0.5.37\": \"0.12.0\",\n  \"0.5.38\": \"0.12.0\",\n  \"0.5.39\": \"0.12.0\",\n  \"0.5.40\": \"0.12.0\",\n  \"0.5.41\": \"0.12.0\",\n  \"0.5.42\": \"0.12.0\",\n  \"0.5.43\": \"0.12.0\",\n  \"0.5.44\": \"0.12.0\",\n  \"0.5.45\": \"0.12.0\",\n  \"0.5.46\": \"0.12.0\",\n  \"0.5.47\": \"0.12.0\",\n  \"0.5.48\": \"0.13.11\",\n  \"0.5.49\": \"0.13.11\",\n  \"0.5.50\": \"0.13.11\",\n  \"0.5.51\": \"0.13.11\",\n  \"0.5.52\": \"0.12.0\",\n  \"0.5.53\": \"0.12.0\",\n  \"0.5.54\": \"0.12.0\",\n  \"0.5.55\": \"0.12.0\",\n  \"0.5.56\": \"0.12.0\",\n  \"0.5.57\": \"0.12.0\",\n  \"0.5.58\": \"0.12.0\",\n  \"0.5.59\": \"0.12.0\",\n  \"0.5.60\": \"0.12.0\",\n  \"0.5.61\": \"0.12.0\",\n  \"0.5.62\": \"0.12.0\",\n  \"0.5.63\": \"0.12.0\",\n  \"0.5.64\": \"0.12.0\",\n  \"0.5.65\": \"0.12.0\",\n  \"0.5.66\": \"0.12.0\",\n  \"0.5.67\": \"0.12.0\",\n  \"0.5.68\": \"0.12.0\",\n  \"0.5.69\": \"0.13.11\",\n  \"0.5.70\": \"0.13.11\"\n}\n"
  }
]