From 75b564bcfd23876f12fa3faf7f86184cdfcd91f1 Mon Sep 17 00:00:00 2001 From: Jan-Stefan Janetzky Date: Fri, 15 Mar 2024 05:36:16 +0100 Subject: [PATCH 1/9] Fix: markdown render api change due to deprecation (#2266) * Fix: markdown render api change due to deprecation closes #177 and #781 * adding app forwarding to make typescript happy --- src/api/inline-api.ts | 3 ++- src/api/plugin-api.ts | 2 +- src/ui/lp-render.ts | 6 +++--- src/ui/render.ts | 23 ++++++++++++++++------- src/ui/views/inline-field-live-preview.ts | 4 +++- src/ui/views/inline-view.ts | 2 +- src/ui/views/js-view.ts | 2 +- 7 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/api/inline-api.ts b/src/api/inline-api.ts index 3ddec6a2..e0c991ff 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 3d4d1e83..f4d8e492 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/ui/lp-render.ts b/src/ui/lp-render.ts index 9372568b..77a03b19 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).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 04f44ce5..89201771 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 9fe5dffe..00f61f2f 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 9a7bf481..3a7befd5 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 1eef95de..e2877299 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); From 733caddf835d84332baacd800b1a86022308c141 Mon Sep 17 00:00:00 2001 From: holroy Date: Sun, 17 Mar 2024 08:03:41 +0100 Subject: [PATCH 2/9] Add workaround to refresh issue of #1752 (#2237) * Add workaround to refresh issue of #1752 Adding a command which can circumvent the issues of #1752, #1759, #2228 (and possibly #1075). This command allows for a full re-run of any query on the active page. * Forgot the check-format. again --- src/main.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main.ts b/src/main.ts index bafde60f..f42e3702 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()); From f2d9c62b9c06429d45583dc999d6c6deaa422558 Mon Sep 17 00:00:00 2001 From: holroy Date: Sun, 17 Mar 2024 08:04:19 +0100 Subject: [PATCH 3/9] Adds #2176 slice() (#2238) Adds a slice() function with documentation to the query language. --- docs/docs/reference/functions.md | 12 ++++++++++++ src/expression/functions.ts | 15 +++++++++++++++ src/test/function/functions.test.ts | 19 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/docs/docs/reference/functions.md b/docs/docs/reference/functions.md index 2bfd5d4f..310f8800 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/expression/functions.ts b/src/expression/functions.ts index a76390a7..7bd5c5de 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 = { reverse: DefaultFunctions.reverse, sort: DefaultFunctions.sort, flat: DefaultFunctions.flat, + slice: DefaultFunctions.slice, // Aggregation operations like reduce. reduce: DefaultFunctions.reduce, diff --git a/src/test/function/functions.test.ts b/src/test/function/functions.test.ts index 72595c67..fc35851d 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()", () => { From a47ac38637011681bcc9a7cd2386a496068c8173 Mon Sep 17 00:00:00 2001 From: Jan-Stefan Janetzky Date: Sun, 17 Mar 2024 08:05:02 +0100 Subject: [PATCH 4/9] Fix: properly escaping pipes in Link.obsidianLink() (#2265) * Fix: properly escaping pipes in Link.obsidianLink() Closes #2264 * upping typescript target even though obsidians empty example plugin still specifies es6, the electron runtime of obsidian actually contains es2022 features. Array.prototype.at etc. --- src/data-model/value.ts | 6 +++--- tsconfig.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data-model/value.ts b/src/data-model/value.ts index bff91e25..834c54ac 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/tsconfig.json b/tsconfig.json index 075e2ccc..e05233e9 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/**/*"] From 242a7d02a61a226bb6c05323916d946a612e15dd Mon Sep 17 00:00:00 2001 From: Jan-Stefan Janetzky Date: Sun, 17 Mar 2024 08:06:33 +0100 Subject: [PATCH 5/9] Moving CSS embedding to the top of the view container (#2261) fixing #2260 --- src/api/inline-api.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/api/inline-api.ts b/src/api/inline-api.ts index e0c991ff..79753d0b 100644 --- a/src/api/inline-api.ts +++ b/src/api/inline-api.ts @@ -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}`; @@ -352,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. */ From 4f70e92fa242a193ad1daf4f71bd514fc0c22151 Mon Sep 17 00:00:00 2001 From: holroy Date: Wed, 20 Mar 2024 07:37:22 +0100 Subject: [PATCH 6/9] feat: hash() - Generate a unique number based on input (#2272) Based on a unique combination of a seed, text and a variant number, it generates a fixed number which can be used for sorting in a random order. Typical seed could be the date of today, the text a file name, and the variant could be the line number of a list/task item. --- docs/docs/reference/functions.md | 19 ++++++++++++++++++ src/expression/functions.ts | 14 +++++++++++++ src/test/function/functions.test.ts | 7 +++++++ src/ui/render.ts | 9 ++++++++- src/util/hash.ts | 22 +++++++++++++++++++++ test-vault/.obsidian/community-plugins.json | 4 ++-- 6 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/util/hash.ts diff --git a/docs/docs/reference/functions.md b/docs/docs/reference/functions.md index 310f8800..ae3ff61d 100644 --- a/docs/docs/reference/functions.md +++ b/docs/docs/reference/functions.md @@ -700,6 +700,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 diff --git a/src/expression/functions.ts b/src/expression/functions.ts index 7bd5c5de..95c5efd7 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 @@ -704,6 +705,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; @@ -924,5 +937,6 @@ export const DEFAULT_FUNCTIONS: Record = { default: DefaultFunctions.fdefault, ldefault: DefaultFunctions.ldefault, choice: DefaultFunctions.choice, + hash: DefaultFunctions.hash, meta: DefaultFunctions.meta, }; diff --git a/src/test/function/functions.test.ts b/src/test/function/functions.test.ts index fc35851d..ab365a3c 100644 --- a/src/test/function/functions.test.ts +++ b/src/test/function/functions.test.ts @@ -124,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()", () => { diff --git a/src/ui/render.ts b/src/ui/render.ts index 89201771..3e1667aa 100644 --- a/src/ui/render.ts +++ b/src/ui/render.ts @@ -88,7 +88,14 @@ export async function renderValue( } if (Values.isNull(field)) { - await renderCompactMarkdown(app, 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)) { diff --git a/src/util/hash.ts b/src/util/hash.ts new file mode 100644 index 00000000..83ab5fd5 --- /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/test-vault/.obsidian/community-plugins.json b/test-vault/.obsidian/community-plugins.json index f51d89b0..a832eabb 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 From 1b567f314ba505f9f491e2a6399645c175f82aff Mon Sep 17 00:00:00 2001 From: holroy Date: Wed, 20 Mar 2024 07:38:03 +0100 Subject: [PATCH 7/9] bug 2253 - Allow for the date to be null (#2271) Fixed #2253 allowing the date to null, returning null, instead of breaking the query. (Also ran `npm run format` which picked up some correction of formatting in render.ts) --- src/expression/functions.ts | 1 + src/test/function/functions.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/expression/functions.ts b/src/expression/functions.ts index 95c5efd7..b3ebd0eb 100644 --- a/src/expression/functions.ts +++ b/src/expression/functions.ts @@ -293,6 +293,7 @@ export namespace DefaultFunctions { } } }) + .add2("null", "string", () => null) .add1("null", () => null) .vectorize(1, [0]) .build(); diff --git a/src/test/function/functions.test.ts b/src/test/function/functions.test.ts index ab365a3c..288ce35d 100644 --- a/src/test/function/functions.test.ts +++ b/src/test/function/functions.test.ts @@ -164,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); }); From d1883e6a4ac9a7df2b401bf5346f95aaa2126a03 Mon Sep 17 00:00:00 2001 From: holroy Date: Wed, 20 Mar 2024 07:40:09 +0100 Subject: [PATCH 8/9] Feature update documentation 0.5.64 (#2224) * Reordered function list according to documentation order In order to easier see which functions lacks documentation, the list is reorder to match the documentation of all functions. * Updated the documentation on functions according to 0.5.64 Added a few missing functions to be documented, and some aliases to be listed within the documentation. * Corrected round() example Completed #1692 * Check and fetch a version of obsidian-dataview Clarified how to check which version of obsidian-dataview the "npm install obsidian-dataview" installed, and how to get a specific version. Completes #1625 ( from https://github.com/blacksmithgu/obsidian-dataview/projects/15#card-87060855) * FAQ: Styling query results Related to #925, this documentation shows how to target the dataview results, and even throws in an extra tip related to uniquely targeting a table. See also https://github.com/blacksmithgu/obsidian-dataview/projects/15#card-86490429 * Possible addendum to style FAQ documentation Not sure if we like to link to external sites, but since it's @sblu, I wanted to also add this paragraph into the documentation. * Added note on dateformat() returning a string Completes the documentation on #1401, which would allow that to be closed. https://github.com/blacksmithgu/obsidian-dataview/projects/15#card-86490433 * On field shorthands added a hint to Tasks As described in #1520 added a hint to the Tasks plugin related to using emojis with dates in tasks. * Added note on the use of visual, and a split Changed some text related to the implicit field visual of the task metadata from #523, (and threw in a little addition on flattening tasks/lists) See also https://github.com/blacksmithgu/obsidian-dataview/projects/15#card-86490454 * Update format of render.ts The format of render.ts keeps bugging other PR's... --------- Co-authored-by: Michael "Tres" Brenan --- docs/docs/annotation/metadata-tasks.md | 13 ++- docs/docs/reference/functions.md | 36 ++++++-- .../resources/develop-against-dataview.md | 6 ++ docs/docs/resources/faq.md | 38 ++++++++- src/expression/functions.ts | 82 +++++++++---------- 5 files changed, 123 insertions(+), 52 deletions(-) diff --git a/docs/docs/annotation/metadata-tasks.md b/docs/docs/annotation/metadata-tasks.md index b5f50d8d..246095d6 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 @@ -84,6 +86,9 @@ 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. +!!! Info + Related to the `visual` implicit field, if this is set either in a [TASK](../queries/query-types.md#task-queries) query using `FLATTEN ... as visual`, or in javascript doing something like `task.visual=... `, it'll replace the original text of the task, but still keep the link to the original task. + ### Access of Implicit Fields for List Items and Tasks 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: @@ -104,4 +109,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'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. Another option if you want to split the tasks/lists into separate rows is to do a `FLATTEN file.lists AS item` in your query, which allows you to do `!item.fullyCompleted` or similar. diff --git a/docs/docs/reference/functions.md b/docs/docs/reference/functions.md index ae3ff61d..327a5d81 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 @@ -731,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" @@ -741,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. @@ -765,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 8be6be49..3da17dd1 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 16d587ce..3cb6b9f3 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 <ul> 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 `` to style various parts of table cells (and columns). diff --git a/src/expression/functions.ts b/src/expression/functions.ts index b3ebd0eb..d2b2d072 100644 --- a/src/expression/functions.ts +++ b/src/expression/functions.ts @@ -862,38 +862,60 @@ export namespace DefaultFunctions { } /** Default function implementations for the expression evaluator. */ +// Keep functions in same order as they're documented !! export const DEFAULT_FUNCTIONS: Record = { - // 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, @@ -905,39 +927,15 @@ export const DEFAULT_FUNCTIONS: Record = { 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, - slice: DefaultFunctions.slice, - - // 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, }; From 692198baffcb84ff846b35a126d6c88c77c1d0bc Mon Sep 17 00:00:00 2001 From: Michael Brenan Date: Tue, 19 Mar 2024 23:43:36 -0700 Subject: [PATCH 9/9] #2204: Update documentation --- docs/docs/api/code-reference.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/docs/api/code-reference.md b/docs/docs/api/code-reference.md index a83b9612..d70b4e6f 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)`