Repository: blacksmithgu/obsidian-dataview
Pull request #2206 (2024-01-11 13:27:49)

Fix an error with https://github.com/khoj-ai/khoj
Khoj plugin creates a separate window, and sourcePath is null, which breaks the plugin. This is a fix.



Patch:
diff --git a/docs/docs/annotation/metadata-tasks.md b/docs/docs/annotation/metadata-tasks.md
index b5f50d8..d262ad9 100644
--- a/docs/docs/annotation/metadata-tasks.md
+++ b/docs/docs/annotation/metadata-tasks.md
@@ -7,12 +7,14 @@ Just like pages, you can also add **fields** on list item and task level to bind
 - [X] I finished this on [completion:: 2021-08-15].
 ```
 
-Tasks and list items are the same data wise, so all your bullet points have all the information described here available, too. 
+Tasks and list items are the same data wise, so all your bullet points have all the information described here available, too.
 
 ## Field Shorthands
 
-For supporting "common use cases", Dataview understands a few shorthands for some fields you may want to annotate task
-with:
+The [Tasks](https://publish.obsidian.md/tasks/Introduction) plugin introduced a different [notation by using Emoji](https://publish.obsidian.md/tasks/Reference/Task+Formats/Tasks+Emoji+Format) to configure the different dates related to a task. In the context of Dataview, this notation is called `Field Shorthands`. The current version of Dataview only support the dates shorthands as shown below. The priorities and recurrence shorthands are not supported.
+
+=== "Example"
+
 
 === "Example"
     - [ ] Due this Saturday 🗓️2021-08-29
@@ -57,20 +59,20 @@ As with pages, Dataview adds a number of implicit fields to each task or list it
 
 | Field name | Data Type | Description |
 | ---------- | --------- | ----------- |
-| `status` |  Text | The completion status of this task, as determined by the character inside the `[ ]` brackets. Generally a space `" "` for incomplete tasks and a `"x"` for complete tasks, but allows for plugins which support alternative task statuses. |
-| `checked` |  Boolean  | Whether or not this task status is empty, meaning it has a space in its `[ ]` brackets |
-| `completed` |  Boolean  | Whether or not this *specific* task has been completed; this does not consider the completionnon-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, i.e. `[-]`, `checked` will be true, whereas `completed` will be false. |
+| `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. |
+| `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. |
+| `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. |
 | `fullyCompleted` |  Boolean  | Whether or not this task and **all** of its subtasks are completed. |
 | `text` |  Text  | The plain text of this task, including any metadata field annotations. |
-| `visual` | Text | The text of this task, which is rendered by Dataview. It can be modified to render arbitrary text. |
+| `visual` | Text | The text of this task, which is rendered by Dataview. This field can be overriden 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). |
 | `line` |  Number  | The line of the file this task shows up on. |
 | `lineCount` |  Number  | The number of Markdown lines that this task takes up. |
-| `path` |  Text  | The full path of the file this task is in. Equals to `file.path` for [pages](./metadata-pages.md) |
-| `section` | Link |  link to the section this task is contained in. |
-| `tags` | List  | Any tags inside of the text task. |
+| `path` |  Text  | The full path of the file this task is in. Equals to `file.path` for [pages](./metadata-pages.md). |
+| `section` | Link |  Link to the section this task is contained in. |
+| `tags` | List  | Any tags inside the task text. |
 | `outlinks` | List |  Any links defined in this task. |
-| `link` | Link  |  link to the closest linkable block near this task; useful for making links which go to the task. |
-| `children` | List  | ny subtasks or sublists of this task. |
+| `link` | Link  |  Link to the closest linkable block near this task; useful for making links which go to the task. |
+| `children` | List  | Any subtasks or sublists of this task. |
 | `task` | Boolean  | If true, this is a task; otherwise, it is a regular list element. |
 | `annotated` | Boolean  | True if the task text contains any metadata fields, false otherwise. |
 | `parent` | Number |  The line number of the task above this task, if present; will be null if this is a root-level task. |
@@ -84,7 +86,7 @@ With usage of the [shorthand syntax](#field-shorthands), following additional pr
 - `start`: The date a task can be started.
 - `scheduled`: The date a task is scheduled to work on.
 
-### Access of Implicit Fields for List Items and Tasks
+### Accessing Implicit Fields in Queries
 
 If you're using a [TASK](../queries/query-types.md#task-queries) Query, your tasks are the top level information and can be used without any prefix:
 
@@ -95,7 +97,7 @@ WHERE !fullyCompleted
 ```
 ~~~
 
-On 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:
+For every other Query type, you first need to access the implicit field `file.lists` or `file.tasks` to check for these list item specific implicit fields:
 
 ~~~markdown
 ```dataview
@@ -104,4 +106,4 @@ WHERE any(file.tasks, (t) => !t.fullyCompleted)
 ```
 ~~~
 
-This'll give you back all 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. 
+This will give you back all the file links that have unfinished tasks inside. We get back a list of tasks on page level and thus need to use a [list function](../reference/functions.md) to look at each element. 
diff --git a/docs/docs/api/code-reference.md b/docs/docs/api/code-reference.md
index a83b961..d70b4e6 100644
--- a/docs/docs/api/code-reference.md
+++ b/docs/docs/api/code-reference.md
@@ -190,8 +190,9 @@ dv.list(dv.pages("#book").where(p => p.rating > 7)) => list of all books with ra
 
 ### `dv.taskList(tasks, groupByFile)`
 
-Render a dataview list of `Task` objects, as obtained by `page.file.tasks`. Only the first argument is required; if the
-second argument `groupByFile` is provided (and is true), then tasks will be grouped by the file they come from automatically.
+Render a dataview list of `Task` objects, as obtained by `page.file.tasks`. By default, this view will automatically
+group the tasks by their origin file. If you provide `false` as a second argument explicitly, it will instead render them
+as a single unified list.
 
 ```js
 // List all tasks from pages marked '#project'
@@ -204,6 +205,9 @@ dv.taskList(dv.pages("#project").file.tasks
 // List all tasks tagged with '#tag' from pages marked #project
 dv.taskList(dv.pages("#project").file.tasks
     .where(t => t.text.includes("#tag")))
+
+// List all tasks from pages marked '#project', without grouping.
+dv.taskList(dv.pages("#project").file.tasks, false)
 ```
 
 ### `dv.table(headers, elements)`
diff --git a/docs/docs/reference/functions.md b/docs/docs/reference/functions.md
index 2bfd5d4..327a5d8 100644
--- a/docs/docs/reference/functions.md
+++ b/docs/docs/reference/functions.md
@@ -45,12 +45,12 @@ object("a", 4, "c", "yes") => object which maps a to 4, and c to "yes"
 
 ### `list(value1, value2, ...)`
 
-Creates a new list with the given values in it.
+Creates a new list with the given values in it. `array` can be used an alias for `list`.
 
 ```js
 list() => empty list
 list(1, 2, 3) => list with 1, 2, and 3
-list("a", "b", "c") => list with "a", "b", and "c"
+array("a", "b", "c") => list with "a", "b", and "c"
 ```
 
 ### `date(any)`
@@ -158,7 +158,7 @@ Round a number to a given number of digits. If the second argument is not specif
 otherwise, rounds to the given number of digits.
 
 ```js
-round(16.555555) = 7
+round(16.555555) = 17
 round(16.555555, 2) = 16.56
 ```
 
@@ -238,6 +238,20 @@ product([]) = null
 product(nonnull([null, 1, 2, 4])) = 8
 ```
 
+### `reduce(array, operand)`
+
+A 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()`.
+
+```js
+reduce([100, 20, 3], "-") = 77
+reduce([200, 10, 2], "/") = 10 
+reduce(values, "*") = Multiplies every element of values, same as product(values)
+reduce(values, this.operand) = Applies the local field operand to each of the values
+reduce(["⭐", 3], "*") = "⭐⭐⭐", same as "⭐" * 3
+
+reduce([1]), "+") = 1, has the side effect of reducing the list into a single element
+```
+
 ### `average(array)`
 
 Computes the numeric average of numeric values. If you have null values in your average, you can eliminate them via the
@@ -519,6 +533,18 @@ flat(list(1, list(21, 22), list(list (311, 312, 313))), 4) => list(1, 21, 22, 31
 flat(rows.file.outlinks)) => All the file outlinks at first level in output
 ```
 
+### `slice(array, [start, [end]])`
+
+Returns a shallow copy of a portion of an array into a new array object selected from `start`
+to `end` (`end` not included) where `start` and `end` represents the index of items in that array.
+
+```js
+slice([1, 2, 3, 4, 5], 3) = [4, 5] => All items from given position, 0 as first
+slice(["ant", "bison", "camel", "duck", "elephant"], 0, 2) = ["ant", "bison"] => First two items 
+slice([1, 2, 3, 4, 5], -2) = [4, 5] => counts from the end, last two items
+slice(someArray) => a copy of someArray
+```
+
 ---
 
 ## String Operations
@@ -688,6 +714,25 @@ choice(false, "yes", "no") = "no"
 choice(x > 4, y, z) = y if x > 4, else z
 ```
 
+### `hash(seed, [text], [variant])`
+
+Generate a hash based on the `seed`, and the optional extra `text` or a variant `number`. The function
+generates a fixed number based on the combination of these parameters, which can be used to randomise 
+the sort order of files or lists/tasks. If you choose a `seed` based on a date, i.e. "2024-03-17",
+or another timestamp, i.e. "2024-03-17 19:13", you can make the "randomness" be fixed
+related to that timestamp. `variant` is a number, which in some cases is needed to make the combination of
+`text` and `variant` become unique.
+
+```js
+hash(dateformat(date(today), "YYYY-MM-DD"), file.name) = ... A unique value for a given date in time
+hash(dateformat(date(today), "YYYY-MM-DD"), file.name, position.start.line) = ... A unique "random" value in a TASK query
+```
+
+This function can be used in a `SORT` statement to randomise the order. If you're using a `TASK` query, 
+since the file name could be the same for multiple tasks, you can add some number like the starting line
+number (as shown above) to make it a unique combination. If using something like `FLATTEN file.lists as item`, 
+the similar addition would be to do `item.position.start.line` as the last parameter.
+
 ### `striptime(date)`
 
 Strip the time component of a date, leaving only the year, month, and day. Good for date comparisons if you don't care
@@ -700,8 +745,7 @@ striptime(file.mtime) = file.mday
 
 ### `dateformat(date|datetime, string)`
 
-Format a Dataview date using a formatting string.
-Uses [Luxon tokens](https://moment.github.io/luxon/#/formatting?id=table-of-tokens).
+Format a Dataview date using a formatting string. Uses [Luxon tokens](https://moment.github.io/luxon/#/formatting?id=table-of-tokens).
 
 ```js
 dateformat(file.ctime,"yyyy-MM-dd") = "2022-01-05"
@@ -710,6 +754,8 @@ dateformat(date(now),"x") = "1407287224054"
 dateformat(file.mtime,"ffff") = "Wednesday, August 6, 2014, 1:07 PM Eastern Daylight Time"
 ```
 
+**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.
+
 ### `durationformat(duration, string)`
 
 Format a Dataview duration using a formatting string.
@@ -734,6 +780,17 @@ durationformat(dur("2000 years"), "M months") = "24000 months"
 durationformat(dur("14d"), "s 'seconds'") = "1209600 seconds"
 ```
 
+### `currencyformat(number, [currency])`
+
+Presents 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).
+
+```
+number = 123456.789
+currencyformat(number, "EUR") =  €123,456.79  in locale: en_US)
+currencyformat(number, "EUR") =  123.456,79 € in locale: de_DE)
+currencyformat(number, "EUR") =  € 123 456,79 in locale: nb)
+```
+
 ### `localtime(date)`
 
 Converts a date in a fixed timezone to a date in the current timezone.
diff --git a/docs/docs/resources/develop-against-dataview.md b/docs/docs/resources/develop-against-dataview.md
index 8be6be4..3da17dd 100644
--- a/docs/docs/resources/develop-against-dataview.md
+++ b/docs/docs/resources/develop-against-dataview.md
@@ -7,6 +7,12 @@ simply use:
 npm install -D obsidian-dataview
 ```
 
+To verify that it is the correct version installed, do `npm list obsidian-dataview`. If that fails to report the latest version, which currently is 0.5.64, you can do:
+
+```bash
+npm install obsidian-dataview@0.5.64
+```
+
 **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.
 
 ##### Accessing the Dataview API
diff --git a/docs/docs/resources/faq.md b/docs/docs/resources/faq.md
index 16d587c..3cb6b9f 100644
--- a/docs/docs/resources/faq.md
+++ b/docs/docs/resources/faq.md
@@ -67,4 +67,40 @@ This will give you back the example page, even though the result doesn't fulfill
 
 ### How can I hide the result count on TABLE Queries?
 
-With Dataview 0.5.52, you can hide the result count on TABLE and TASK Queries via a setting. Go to Dataview's settings -> Display result count. 
\ No newline at end of file
+With Dataview 0.5.52, you can hide the result count on TABLE and TASK Queries via a setting. Go to Dataview's settings -> Display result count.
+
+### How can I style my queries?
+
+You can use [CSS Snippets](https://help.obsidian.md/Extending+Obsidian/CSS+snippets) to define custom styling in general for Obsidian. So if you define `cssclasses: myTable` as a property, and enable the snippet below you could set the background color of a table from dataview. Similar to target the outer &lt;ul&gt; of a `TASK` or `LIST` query, you could use the `ul.contains-task-list` or `ul.list-view-ul` respectively.
+
+```css
+.myTable dataview.table {
+    background-color: green
+}
+```
+
+In general there are no unique ID's given to a specific table on a page, so the mentioned targetting 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:
+
+```js
+dv.container.className += ' clsname'
+```
+
+However, there is a trick to target any table within Obsidian using tags like in the example below, and that would apply to any table having that tag tag within it. This would apply to both manually and dataview generated tables. To make the snippet below work add the tag `#myId` _anywhere_ within your table output.
+
+```css
+[href="#myId"] {
+    display: none; /* Hides the tag from the table view */
+}
+
+table:has([href="#myId"]) {
+   /* Style your table as you like */
+  background-color: #262626;
+  & tr:nth-child(even) td:first-child{
+    background-color: #3f3f3f;  
+  }
+}
+```
+
+Which would end up having a grey background on the entire table, and the first cell in every even row a different variant of grey. **Disclaimer:** We're not style gurus, so this is just an example to show some of the syntax needed for styling different parts of a table. 
+
+Furthermore, in [Style dataview table columns](https://s-blu.github.io/obsidian_dataview_example_vault/20%20Dataview%20Queries/Style%20dataview%20table%20columns/) @s-blu describes an alternate trick using `<span>` to style various parts of table cells (and columns).
diff --git a/src/api/inline-api.ts b/src/api/inline-api.ts
index 3ddec6a..79753d0 100644
--- a/src/api/inline-api.ts
+++ b/src/api/inline-api.ts
@@ -288,7 +288,7 @@ export class DataviewInlineApi {
         }
 
         let _el = container.createEl(el, options);
-        renderValue(wrapped.value, _el, this.currentFilePath, this.component, this.settings, true);
+        renderValue(this.app, wrapped.value, _el, this.currentFilePath, this.component, this.settings, true);
         return _el;
     }
 
@@ -319,6 +319,7 @@ export class DataviewInlineApi {
         const simpleViewPath = `${viewName}.js`;
         const complexViewPath = `${viewName}/view.js`;
         let checkForCss = false;
+        let cssElement = undefined;
         let viewFile = this.app.metadataCache.getFirstLinkpathDest(simpleViewPath, this.currentFilePath);
         if (!viewFile) {
             viewFile = this.app.metadataCache.getFirstLinkpathDest(complexViewPath, this.currentFilePath);
@@ -333,6 +334,16 @@ export class DataviewInlineApi {
             return;
         }
 
+        if (checkForCss) {
+            // Check for optional CSS.
+            let cssFile = this.app.metadataCache.getFirstLinkpathDest(`${viewName}/view.css`, this.currentFilePath);
+            if (cssFile) {
+                let cssContents = await this.app.vault.read(cssFile);
+                cssContents += `\n/*# sourceURL=${location.origin}/${cssFile.path} */`;
+                cssElement = this.container.createEl("style", { text: cssContents, attr: { scope: " " } });
+            }
+        }
+
         let contents = await this.app.vault.read(viewFile);
         if (contents.contains("await")) contents = "(async () => { " + contents + " })()";
         contents += `\n//# sourceURL=${viewFile.path}`;
@@ -343,6 +354,7 @@ export class DataviewInlineApi {
             let result = await Promise.resolve(func(this, input));
             if (result)
                 await renderValue(
+                    this.app,
                     result as any,
                     this.container,
                     this.currentFilePath,
@@ -351,20 +363,9 @@ export class DataviewInlineApi {
                     true
                 );
         } catch (ex) {
+            if (cssElement) this.container.removeChild(cssElement);
             renderErrorPre(this.container, `Dataview: Failed to execute view '${viewFile.path}'.\n\n${ex}`);
         }
-
-        if (!checkForCss) {
-            return;
-        }
-
-        // Check for optional CSS.
-        let cssFile = this.app.metadataCache.getFirstLinkpathDest(`${viewName}/view.css`, this.currentFilePath);
-        if (!cssFile) return;
-
-        let cssContents = await this.app.vault.read(cssFile);
-        cssContents += `\n/*# sourceURL=${location.origin}/${cssFile.path} */`;
-        this.container.createEl("style", { text: cssContents, attr: { scope: " " } });
     }
 
     /** Render a dataview list of the given values. */
diff --git a/src/api/plugin-api.ts b/src/api/plugin-api.ts
index 3d4d1e8..6abb3a0 100644
--- a/src/api/plugin-api.ts
+++ b/src/api/plugin-api.ts
@@ -543,7 +543,7 @@ export class DataviewApi {
         filePath: string,
         inline: boolean = false
     ) {
-        return renderValue(value as Literal, container, filePath, component, this.settings, inline);
+        return renderValue(this.app, value as Literal, container, filePath, component, this.settings, inline);
     }
 
     /////////////////
@@ -608,6 +608,8 @@ export type QueryApiSettings = {
 
 /** Determines if source-path has a `?no-dataview` annotation that disables dataview. */
 export function isDataviewDisabled(sourcePath: string): boolean {
+    if (!sourcePath) return false;
+
     let questionLocation = sourcePath.lastIndexOf("?");
     if (questionLocation == -1) return false;
 
diff --git a/src/data-model/value.ts b/src/data-model/value.ts
index bff91e2..834c54a 100644
--- a/src/data-model/value.ts
+++ b/src/data-model/value.ts
@@ -554,9 +554,9 @@ export class Link {
 
     /** Convert the inner part of the link to something that Obsidian can open / understand. */
     public obsidianLink(): string {
-        const escaped = this.path.replace("|", "\\|");
-        if (this.type == "header") return escaped + "#" + this.subpath?.replace("|", "\\|");
-        if (this.type == "block") return escaped + "#^" + this.subpath?.replace("|", "\\|");
+        const escaped = this.path.replaceAll("|", "\\|");
+        if (this.type == "header") return escaped + "#" + this.subpath?.replaceAll("|", "\\|");
+        if (this.type == "block") return escaped + "#^" + this.subpath?.replaceAll("|", "\\|");
         else return escaped;
     }
 
diff --git a/src/expression/functions.ts b/src/expression/functions.ts
index a76390a..d2b2d07 100644
--- a/src/expression/functions.ts
+++ b/src/expression/functions.ts
@@ -9,6 +9,7 @@ import { Fields } from "./field";
 import { EXPRESSION } from "./parse";
 import { escapeRegex } from "util/normalize";
 import { DataArray } from "api/data-array";
+import { cyrb53 } from "util/hash";
 
 /**
  * A function implementation which takes in a function context and a variable number of arguments. Throws an error if an
@@ -292,6 +293,7 @@ export namespace DefaultFunctions {
                 }
             }
         })
+        .add2("null", "string", () => null)
         .add1("null", () => null)
         .vectorize(1, [0])
         .build();
@@ -704,6 +706,18 @@ export namespace DefaultFunctions {
         .vectorize(3, [0])
         .build();
 
+    export const hash = new FunctionBuilder("hash")
+        .add2("string", "number", (seed, variant) => {
+            return cyrb53(seed, variant);
+        })
+        .add2("string", "string", (seed, text) => {
+            return cyrb53(seed + text);
+        })
+        .add3("string", "string", "number", (seed, text, variant) => {
+            return cyrb53(seed + text, variant);
+        })
+        .build();
+
     export const reduce = new FunctionBuilder("reduce")
         .add2("array", "string", (lis, op, context) => {
             if (lis.length == 0) return null;
@@ -831,41 +845,77 @@ export namespace DefaultFunctions {
         })
         .add1("null", () => null)
         .build();
+
+    // Slices the array into a new array
+    export const slice = new FunctionBuilder("slice")
+        .add1("array", a => {
+            return a.slice();
+        })
+        .add2("array", "number", (a, start) => {
+            return a.slice(start);
+        })
+        .add3("array", "number", "number", (a, start, end) => {
+            return a.slice(start, end);
+        })
+        .add1("null", () => null)
+        .build();
 }
 
 /** Default function implementations for the expression evaluator. */
+// Keep functions in same order as they're documented !!
 export const DEFAULT_FUNCTIONS: Record<string, FunctionImpl> = {
-    // Constructors.
+    // Constructors
+    object: DefaultFunctions.object,
     list: DefaultFunctions.list,
     array: DefaultFunctions.list,
-    link: DefaultFunctions.link,
-    embed: DefaultFunctions.embed,
-    elink: DefaultFunctions.elink,
     date: DefaultFunctions.date,
     dur: DefaultFunctions.dur,
-    dateformat: DefaultFunctions.dateformat,
-    durationformat: DefaultFunctions.durationformat,
-    localtime: DefaultFunctions.localtime,
     number: DefaultFunctions.number,
-    currencyformat: DefaultFunctions.currencyformat,
     string: DefaultFunctions.string,
-    object: DefaultFunctions.object,
+    link: DefaultFunctions.link,
+    embed: DefaultFunctions.embed,
+    elink: DefaultFunctions.elink,
     typeof: DefaultFunctions.typeOf,
 
-    // Math Operations.
+    // Numeric Operations
     round: DefaultFunctions.round,
     trunc: DefaultFunctions.trunc,
     floor: DefaultFunctions.floor,
     ceil: DefaultFunctions.ceil,
     min: DefaultFunctions.min,
     max: DefaultFunctions.max,
+    sum: DefaultFunctions.sum,
+    product: DefaultFunctions.product,
+    average: DefaultFunctions.average,
     minby: DefaultFunctions.minby,
     maxby: DefaultFunctions.maxby,
 
-    // String operations.
-    regexreplace: DefaultFunctions.regexreplace,
+    // Object, Arrays, and String operations
+    contains: DefaultFunctions.contains,
+    icontains: DefaultFunctions.icontains,
+    econtains: DefaultFunctions.econtains,
+    containsword: DefaultFunctions.containsword,
+    extract: DefaultFunctions.extract,
+    sort: DefaultFunctions.sort,
+    reverse: DefaultFunctions.reverse,
+    length: DefaultFunctions.length,
+    nonnull: DefaultFunctions.nonnull,
+    all: DefaultFunctions.all,
+    any: DefaultFunctions.any,
+    none: DefaultFunctions.none,
+    join: DefaultFunctions.join,
+    filter: DefaultFunctions.filter,
+    map: DefaultFunctions.map,
+    flat: DefaultFunctions.flat,
+    slice: DefaultFunctions.slice,
+    unique: DefaultFunctions.unique,
+
+    reduce: DefaultFunctions.reduce,
+
+    // String Operations
     regextest: DefaultFunctions.regextest,
     regexmatch: DefaultFunctions.regexmatch,
+    regexreplace: DefaultFunctions.regexreplace,
     replace: DefaultFunctions.replace,
     lower: DefaultFunctions.lower,
     upper: DefaultFunctions.upper,
@@ -877,37 +927,15 @@ export const DEFAULT_FUNCTIONS: Record<string, FunctionImpl> = {
     substring: DefaultFunctions.substring,
     truncate: DefaultFunctions.truncate,
 
-    // Date Operations.
-    striptime: DefaultFunctions.striptime,
-
-    // List operations.
-    length: DefaultFunctions.length,
-    contains: DefaultFunctions.contains,
-    icontains: DefaultFunctions.icontains,
-    econtains: DefaultFunctions.econtains,
-    containsword: DefaultFunctions.containsword,
-    reverse: DefaultFunctions.reverse,
-    sort: DefaultFunctions.sort,
-    flat: DefaultFunctions.flat,
-
-    // Aggregation operations like reduce.
-    reduce: DefaultFunctions.reduce,
-    join: DefaultFunctions.join,
-    sum: DefaultFunctions.sum,
-    product: DefaultFunctions.product,
-    average: DefaultFunctions.average,
-    all: DefaultFunctions.all,
-    any: DefaultFunctions.any,
-    none: DefaultFunctions.none,
-    filter: DefaultFunctions.filter,
-    unique: DefaultFunctions.unique,
-    map: DefaultFunctions.map,
-    nonnull: DefaultFunctions.nonnull,
-
-    // Object/Utility operations.
-    extract: DefaultFunctions.extract,
+    // Utility Operations
     default: DefaultFunctions.fdefault,
     ldefault: DefaultFunctions.ldefault,
     choice: DefaultFunctions.choice,
+    striptime: DefaultFunctions.striptime,
+    dateformat: DefaultFunctions.dateformat,
+    durationformat: DefaultFunctions.durationformat,
+    currencyformat: DefaultFunctions.currencyformat,
+    localtime: DefaultFunctions.localtime,
+    hash: DefaultFunctions.hash,
     meta: DefaultFunctions.meta,
 };
diff --git a/src/main.ts b/src/main.ts
index bafde60..f42e370 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -7,6 +7,7 @@ import {
     Plugin,
     PluginSettingTab,
     Setting,
+    WorkspaceLeaf,
 } from "obsidian";
 import { renderErrorPre } from "ui/render";
 import { FullIndex } from "data-index/index";
@@ -122,6 +123,21 @@ export default class DataviewPlugin extends Plugin {
             },
         });
 
+        interface WorkspaceLeafRebuild extends WorkspaceLeaf {
+            rebuildView(): void;
+        }
+
+        this.addCommand({
+            id: "dataview-rebuild-current-view",
+            name: "Rebuild current view",
+            callback: () => {
+                const activeView: MarkdownView | null = this.app.workspace.getActiveViewOfType(MarkdownView);
+                if (activeView) {
+                    (activeView.leaf as WorkspaceLeafRebuild).rebuildView();
+                }
+            },
+        });
+
         // Run index initialization, which actually traverses the vault to index files.
         if (!this.app.workspace.layoutReady) {
             this.app.workspace.onLayoutReady(async () => this.index.initialize());
diff --git a/src/ui/lp-render.ts b/src/ui/lp-render.ts
index 9372568..77a03b1 100644
--- a/src/ui/lp-render.ts
+++ b/src/ui/lp-render.ts
@@ -317,7 +317,7 @@ export function inlinePlugin(app: App, index: FullIndex, settings: DataviewSetti
                             } else {
                                 const { value } = intermediateResult;
                                 result = value;
-                                renderValue(result, el, currentFile.path, this.component, settings);
+                                renderValue(app, result, el, currentFile.path, this.component, settings);
                             }
                         }
                     } else {
@@ -334,12 +334,12 @@ export function inlinePlugin(app: App, index: FullIndex, settings: DataviewSetti
                             if (code.includes("await")) {
                                 (evalInContext("(async () => { " + PREAMBLE + code + " })()") as Promise<any>).then(
                                     (result: any) => {
-                                        renderValue(result, el, currentFile.path, this.component, settings);
+                                        renderValue(app, result, el, currentFile.path, this.component, settings);
                                     }
                                 );
                             } else {
                                 result = evalInContext(PREAMBLE + code);
-                                renderValue(result, el, currentFile.path, this.component, settings);
+                                renderValue(app, result, el, currentFile.path, this.component, settings);
                             }
 
                             function evalInContext(script: string): any {
diff --git a/src/ui/render.ts b/src/ui/render.ts
index 04f44ce..3e1667a 100644
--- a/src/ui/render.ts
+++ b/src/ui/render.ts
@@ -1,4 +1,4 @@
-import { Component, MarkdownRenderer } from "obsidian";
+import { App, Component, MarkdownRenderer } from "obsidian";
 import { DataArray } from "api/data-array";
 import { QuerySettings } from "settings";
 import { currentLocale } from "util/locale";
@@ -7,6 +7,7 @@ import { Literal, Values, Widgets } from "data-model/value";
 
 /** Render simple fields compactly, removing wrapping content like paragraph and span. */
 export async function renderCompactMarkdown(
+    app: App,
     markdown: string,
     container: HTMLElement,
     sourcePath: string,
@@ -15,10 +16,10 @@ export async function renderCompactMarkdown(
 ) {
     // check if the call is from the CM6 view plugin defined in src/ui/views/inline-field-live-preview.ts
     if (isInlineFieldLivePreview) {
-        await renderCompactMarkdownForInlineFieldLivePreview(markdown, container, sourcePath, component);
+        await renderCompactMarkdownForInlineFieldLivePreview(app, markdown, container, sourcePath, component);
     } else {
         let subcontainer = container.createSpan();
-        await MarkdownRenderer.renderMarkdown(markdown, subcontainer, sourcePath, component);
+        await MarkdownRenderer.render(app, markdown, subcontainer, sourcePath, component);
 
         let paragraph = subcontainer.querySelector(":scope > p");
         if (subcontainer.children.length == 1 && paragraph) {
@@ -31,13 +32,14 @@ export async function renderCompactMarkdown(
 }
 
 async function renderCompactMarkdownForInlineFieldLivePreview(
+    app: App,
     markdown: string,
     container: HTMLElement,
     sourcePath: string,
     component: Component
 ) {
     const tmpContainer = createSpan();
-    await MarkdownRenderer.renderMarkdown(markdown, tmpContainer, sourcePath, component);
+    await MarkdownRenderer.render(app, markdown, tmpContainer, sourcePath, component);
 
     let paragraph = tmpContainer.querySelector(":scope > p");
     if (tmpContainer.childNodes.length == 1 && paragraph) {
@@ -68,6 +70,7 @@ export type ValueRenderContext = "root" | "list";
 
 /** Prettily render a value into a container with the given settings. */
 export async function renderValue(
+    app: App,
     field: Literal,
     container: HTMLElement,
     originFile: string,
@@ -85,20 +88,28 @@ export async function renderValue(
     }
 
     if (Values.isNull(field)) {
-        await renderCompactMarkdown(settings.renderNullAs, container, originFile, component, isInlineFieldLivePreview);
+        await renderCompactMarkdown(
+            app,
+            settings.renderNullAs,
+            container,
+            originFile,
+            component,
+            isInlineFieldLivePreview
+        );
     } else if (Values.isDate(field)) {
         container.appendText(renderMinimalDate(field, settings, currentLocale()));
     } else if (Values.isDuration(field)) {
         container.appendText(renderMinimalDuration(field));
     } else if (Values.isString(field) || Values.isBoolean(field) || Values.isNumber(field)) {
-        await renderCompactMarkdown("" + field, container, originFile, component, isInlineFieldLivePreview);
+        await renderCompactMarkdown(app, "" + field, container, originFile, component, isInlineFieldLivePreview);
     } else if (Values.isLink(field)) {
-        await renderCompactMarkdown(field.markdown(), container, originFile, component, isInlineFieldLivePreview);
+        await renderCompactMarkdown(app, field.markdown(), container, originFile, component, isInlineFieldLivePreview);
     } else if (Values.isHtml(field)) {
         container.appendChild(field);
     } else if (Values.isWidget(field)) {
         if (Widgets.isListPair(field)) {
             await renderValue(
+                app,
                 field.key,
                 container,
                 originFile,
@@ -111,6 +122,7 @@ export async function renderValue(
             );
             container.appendText(": ");
             await renderValue(
+                app,
                 field.value,
                 container,
                 originFile,
@@ -146,6 +158,7 @@ export async function renderValue(
             for (let child of field) {
                 let li = list.createEl("li", { cls: "dataview-result-list-li" });
                 await renderValue(
+                    app,
                     child,
                     li,
                     originFile,
@@ -170,6 +183,7 @@ export async function renderValue(
                 else span.appendText(", ");
 
                 await renderValue(
+                    app,
                     val,
                     span,
                     originFile,
@@ -195,6 +209,7 @@ export async function renderValue(
                 let li = list.createEl("li", { cls: ["dataview", "dataview-li", "dataview-result-object-li"] });
                 li.appendText(key + ": ");
                 await renderValue(
+                    app,
                     value,
                     li,
                     originFile,
@@ -220,6 +235,7 @@ export async function renderValue(
 
                 span.appendText(key + ": ");
                 await renderValue(
+                    app,
                     value,
                     span,
                     originFile,
diff --git a/src/ui/views/inline-field-live-preview.ts b/src/ui/views/inline-field-live-preview.ts
index 9fe5dff..00f61f2 100644
--- a/src/ui/views/inline-field-live-preview.ts
+++ b/src/ui/views/inline-field-live-preview.ts
@@ -235,12 +235,13 @@ class InlineFieldWidget extends WidgetType {
                 },
             });
 
-            renderCompactMarkdown(this.field.key, key, this.sourcePath, this.component, true);
+            renderCompactMarkdown(this.app, this.field.key, key, this.sourcePath, this.component, true);
 
             const value = renderContainer.createSpan({
                 cls: ["dataview", "inline-field-value"],
             });
             renderValue(
+                this.app,
                 parseInlineValue(this.field.value),
                 value,
                 this.sourcePath,
@@ -259,6 +260,7 @@ class InlineFieldWidget extends WidgetType {
                 cls: ["dataview", "inline-field-standalone-value"],
             });
             renderValue(
+                this.app,
                 parseInlineValue(this.field.value),
                 value,
                 this.sourcePath,
diff --git a/src/ui/views/inline-view.ts b/src/ui/views/inline-view.ts
index 9a7bf48..3a7befd 100644
--- a/src/ui/views/inline-view.ts
+++ b/src/ui/views/inline-view.ts
@@ -34,7 +34,7 @@ export class DataviewInlineRenderer extends DataviewRefreshableRenderer {
         } else {
             let temp = document.createElement("span");
             temp.addClasses(["dataview", "dataview-inline-query"]);
-            await renderValue(result.value, temp, this.origin, this, this.settings, false);
+            await renderValue(this.app, result.value, temp, this.origin, this, this.settings, false);
 
             this.target.replaceWith(temp);
         }
diff --git a/src/ui/views/js-view.ts b/src/ui/views/js-view.ts
index 1eef95d..e287729 100644
--- a/src/ui/views/js-view.ts
+++ b/src/ui/views/js-view.ts
@@ -72,7 +72,7 @@ export class DataviewInlineJSRenderer extends DataviewRefreshableRenderer {
             this.target = temp;
             if (result === undefined) return;
 
-            renderValue(result, temp, this.origin, this, this.settings, false);
+            renderValue(this.api.app, result, temp, this.origin, this, this.settings, false);
         } catch (e) {
             this.errorbox = this.container.createEl("div");
             renderErrorPre(this.errorbox, "Dataview (for inline JS query '" + this.script + "'): " + e);
diff --git a/src/util/hash.ts b/src/util/hash.ts
new file mode 100644
index 0000000..83ab5fd
--- /dev/null
+++ b/src/util/hash.ts
@@ -0,0 +1,22 @@
+// cyrb53 (c) 2018 bryc (github.com/bryc). License: Public domain. Attribution appreciated.
+// A fast and simple 64-bit (or 53-bit) string hash function with decent collision resistance.
+// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
+// See https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480
+// https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
+
+export function cyrb53(str: string, seed: number = 0): number {
+    let h1 = 0xdeadbeef ^ seed,
+        h2 = 0x41c6ce57 ^ seed;
+    for (let i = 0, ch; i < str.length; i++) {
+        ch = str.charCodeAt(i);
+        h1 = Math.imul(h1 ^ ch, 2654435761);
+        h2 = Math.imul(h2 ^ ch, 1597334677);
+    }
+    h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
+    h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
+    h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
+    h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
+    // For a full 64-bit value we could return
+    //  [h2>>>0, h1>>>0]
+    return 4294967296 * (2097151 & h2) + (h1 >>> 0); // ;
+}
diff --git a/tsconfig.json b/tsconfig.json
index 075e2cc..e05233e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,7 @@
     "inlineSourceMap": true,
     "inlineSources": true,
     "module": "ESNext",
-    "target": "es2018",
+    "target": "ES2022",
     "allowJs": false,
     "jsx": "react",
     "jsxFactory": "h",
@@ -19,7 +19,7 @@
     "importHelpers": true,
     "downlevelIteration": true,
     "esModuleInterop": true,
-    "lib": ["dom", "es5", "scripthost", "es2018", "DOM.Iterable"]
+    "lib": ["dom", "scripthost", "ES2022"]
   },
   "include": ["src/**/*.ts", "src/**/*.tsx"],
   "exclude": ["lib/**/*"]


Test patch:
diff --git a/src/test/function/functions.test.ts b/src/test/function/functions.test.ts
index 72595c6..288ce35 100644
--- a/src/test/function/functions.test.ts
+++ b/src/test/function/functions.test.ts
@@ -72,6 +72,25 @@ test("Evaluate flat()", () => {
     expect(parseEval("flat(list(1, list(2, list(3, list(4, list(5))))), 10)")).toEqual(parseEval("list(1,2,3,4,5)")); // flat(..., 10)
 });
 
+// <-- slice() -->
+
+test("Evaluate slice()", () => {
+    expect(parseEval("slice(list(1, 2, 3, 4, 5), 3)")).toEqual(parseEval("list(4, 5)")); // slice(..., 3)
+    expect(parseEval("slice(list(1, 2, 3, 4, 5), 0, 2)")).toEqual(parseEval("list(1, 2)")); // slice(..., 0, 2)
+    expect(parseEval("slice(list(1, 2, 3, 4, 5), -2)")).toEqual(parseEval("list(4, 5)")); // slice(..., -2)
+    expect(parseEval("slice(list(1, 2, 3, 4, 5), -1, 1)")).toEqual(parseEval("list()")); // slice(..., -1, 1)
+    expect(parseEval("slice(list(1, 2, 3, 4, 5))")).toEqual(parseEval("list(1, 2, 3, 4, 5)")); // slice(...)
+    expect(parseEval('slice(list(date("2021-01-01"), date("2022-02-02"), date("2023-03-03")), -2)')).toEqual([
+        DateTime.fromObject({ year: 2022, month: 2, day: 2 }),
+        DateTime.fromObject({ year: 2023, month: 3, day: 3 }),
+    ]); // slice(date list, -2)
+    expect(parseEval('slice(["ant", "bison", "camel", "duck", "elephant"], -3)')).toEqual([
+        "camel",
+        "duck",
+        "elephant",
+    ]); // slice(string list, -3)
+});
+
 // <-- sort() -->
 
 describe("sort()", () => {
@@ -105,6 +124,13 @@ test("Evaluate choose()", () => {
     expect(parseEval("choice(false, 1, 2)")).toEqual(2);
 });
 
+test("Evaulate hash()", () => {
+    expect(DefaultFunctions.hash(simpleContext(), "2024-03-17", "")).toEqual(3259376374957153);
+    expect(DefaultFunctions.hash(simpleContext(), "2024-03-17", 2)).toEqual(271608741894590);
+    expect(DefaultFunctions.hash(simpleContext(), "2024-03-17", "Home")).toEqual(3041844187830523);
+    expect(DefaultFunctions.hash(simpleContext(), "2024-03-17", "note a1", 21)).toEqual(1143088188331616);
+});
+
 // <-- extract() -->
 
 test("Evaluate 1 field extract()", () => {
@@ -138,4 +164,5 @@ test("Evaluate date()", () => {
     expect(parseEval('date("210313", "yyMMdd")')).toEqual(DateTime.fromObject({ year: 2021, month: 3, day: 13 }));
     expect(parseEval('date("946778645012","x")')).toEqual(DateTime.fromMillis(946778645012));
     expect(parseEval('date("946778645","X")')).toEqual(DateTime.fromMillis(946778645000));
+    expect(DefaultFunctions.date(simpleContext(), null, "MM/dd/yyyy")).toEqual(null);
 });
diff --git a/test-vault/.obsidian/community-plugins.json b/test-vault/.obsidian/community-plugins.json
index f51d89b..a832eab 100644
--- a/test-vault/.obsidian/community-plugins.json
+++ b/test-vault/.obsidian/community-plugins.json
@@ -1,4 +1,4 @@
 [
-  "dataview",
-  "hot-reload"
+  "hot-reload",
+  "dataview"
 ]
\ No newline at end of file