Repository: blacksmithgu/obsidian-dataview
Pull request #2238 (2024-02-16 17:35:01)

Adds #2176 slice()
Adds a slice() function with documentation to the query language. Complements #2176.


Patch:
diff --git a/docs/docs/reference/functions.md b/docs/docs/reference/functions.md
index 2bfd5d4..310f880 100644
--- a/docs/docs/reference/functions.md
+++ b/docs/docs/reference/functions.md
@@ -519,6 +519,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
diff --git a/src/api/inline-api.ts b/src/api/inline-api.ts
index 3ddec6a..e0c991f 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;
     }
 
@@ -343,6 +343,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,
diff --git a/src/api/plugin-api.ts b/src/api/plugin-api.ts
index 3d4d1e8..f4d8e49 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);
     }
 
     /////////////////
diff --git a/src/expression/functions.ts b/src/expression/functions.ts
index a76390a..7bd5c5d 100644
--- a/src/expression/functions.ts
+++ b/src/expression/functions.ts
@@ -831,6 +831,20 @@ 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. */
@@ -889,6 +903,7 @@ export const DEFAULT_FUNCTIONS: Record<string, FunctionImpl> = {
     reverse: DefaultFunctions.reverse,
     sort: DefaultFunctions.sort,
     flat: DefaultFunctions.flat,
+    slice: DefaultFunctions.slice,
 
     // Aggregation operations like reduce.
     reduce: DefaultFunctions.reduce,
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..8920177 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,21 @@ 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 +115,7 @@ export async function renderValue(
             );
             container.appendText(": ");
             await renderValue(
+                app,
                 field.value,
                 container,
                 originFile,
@@ -146,6 +151,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 +176,7 @@ export async function renderValue(
                 else span.appendText(", ");
 
                 await renderValue(
+                    app,
                     val,
                     span,
                     originFile,
@@ -195,6 +202,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 +228,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);


Test patch:
diff --git a/src/test/function/functions.test.ts b/src/test/function/functions.test.ts
index 72595c6..fc35851 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()", () => {