diff --git a/.eslintrc.json b/.eslintrc.json index 0bcb85c6..9a35c294 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,8 +4,14 @@ "env": { "node": true }, - "plugins": ["@typescript-eslint"], - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "plugins": [ + "@typescript-eslint", + "eslint-plugin-tsdoc" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], "parserOptions": { "sourceType": "module" }, @@ -15,6 +21,7 @@ "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-unused-vars": "off", - "no-useless-escape": "off" + "no-useless-escape": "off", + "tsdoc/syntax": "warn" } } diff --git a/package-lock.json b/package-lock.json index 211b360e..851ad876 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "esbuild-plugin-replace": "^1.4.0", "esbuild-svelte": "^0.7.4", "eslint": "^8.57.0", + "eslint-plugin-tsdoc": "^0.3.0", "fp-ts": "^2.16.6", "i18next": "^21.10.0", "immer": "^9.0.21", @@ -1895,6 +1896,46 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", + "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", + "dev": true + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.0.tgz", + "integrity": "sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.15.0", + "ajv": "~8.12.0", + "jju": "~1.4.0", + "resolve": "~1.22.2" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3772,6 +3813,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-plugin-tsdoc": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.3.0.tgz", + "integrity": "sha512-0MuFdBrrJVBjT/gyhkP2BqpD0np1NxNLfQ38xXDlSs/KVVpKI2A6vN7jx2Rve/CyUsvOsMGwp9KKrinv7q9g3A==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.15.0", + "@microsoft/tsdoc-config": "0.17.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -6677,6 +6728,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7718,6 +7775,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/package.json b/package.json index 4d21d846..5af69540 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "esbuild-plugin-replace": "^1.4.0", "esbuild-svelte": "^0.7.4", "eslint": "^8.57.0", + "eslint-plugin-tsdoc": "^0.3.0", "fp-ts": "^2.16.6", "i18next": "^21.10.0", "immer": "^9.0.21", diff --git a/src/lib/datasources/folder/datasource.ts b/src/lib/datasources/folder/datasource.ts index 4d7e57e1..04ee40a0 100644 --- a/src/lib/datasources/folder/datasource.ts +++ b/src/lib/datasources/folder/datasource.ts @@ -54,9 +54,9 @@ export class FolderDataSource extends FrontMatterDataSource { * * Assumes both folder path and file path have been normalized. * - * @param folderPath path to the root folder, e.g. Work - * @param filePath path to the file to test, e.g. Work/Untitled.md - * @returns + * @param folderPath - The path to the root folder, e.g. Work + * @param filePath - The path to the file to test, e.g. Work/Untitled.md + * @returns True if the folder contains the given file, else false */ function folderContainsPath(folderPath: string, filePath: string): boolean { const fileElements = filePath.split("/").slice(0, -1); @@ -70,9 +70,9 @@ function folderContainsPath(folderPath: string, filePath: string): boolean { * * Assumes both folder path and file path have been normalized. * - * @param folderPath path to the root folder, e.g. Work - * @param filePath path to the file to test, e.g. Work/Meetings/Untitled.md - * @returns + * @param folderPath - The path to the root folder, e.g. Work + * @param filePath - The path to the file to test, e.g. Work/Meetings/Untitled.md + * @returns True if the file exists in any subfolder. */ function folderContainsDeepPath(folderPath: string, filePath: string): boolean { const fileElements = filePath.split("/").filter((el) => el); diff --git a/src/lib/datasources/helpers.test.ts b/src/lib/datasources/helpers.test.ts index 5a7991b2..8322e3f7 100644 --- a/src/lib/datasources/helpers.test.ts +++ b/src/lib/datasources/helpers.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "@jest/globals"; +import { test, describe, expect, it } from "@jest/globals"; import { DataFieldType, type DataField, @@ -148,28 +148,33 @@ describe("detectFields", () => { }); describe("detectCellType", () => { - it("detects simple data types", () => { - expect(detectCellType("My value")).toStrictEqual(DataFieldType.String); - expect(detectCellType(12.0)).toStrictEqual(DataFieldType.Number); - expect(detectCellType(true)).toStrictEqual(DataFieldType.Boolean); - }); - - it("detects repeated field types", () => { - expect(detectCellType(["foo", "bar"])).toStrictEqual(DataFieldType.String); - expect(detectCellType([1, 2])).toStrictEqual(DataFieldType.Number); + const cases: [unknown, DataFieldType][] = [ + // Primitive values. + [null, DataFieldType.Unknown], + [undefined, DataFieldType.Unknown], + ["My value", DataFieldType.String], + ["", DataFieldType.String], + [12.0, DataFieldType.Number], + [0, DataFieldType.Number], + [true, DataFieldType.Boolean], + [false, DataFieldType.Boolean], - // Fallback to String field - expect(detectCellType([true, 1])).toStrictEqual(DataFieldType.String); - }); + // Repeated values. + [["foo", "bar"], DataFieldType.String], + [[null, "bar"], DataFieldType.String], + [[1, 2], DataFieldType.Number], + [[1, null], DataFieldType.Number], + [[true, false], DataFieldType.Boolean], + [[null, false], DataFieldType.Boolean], + [[true, 1], DataFieldType.String], // Fall back to String field. + [[], DataFieldType.String], // Current behavior, but is this what we want? - it("detects null fields", () => { - expect(detectCellType(null)).toStrictEqual(DataFieldType.Unknown); - }); + // Complex values. + ["2022-01-01", DataFieldType.Date], + [{ my: "object" }, DataFieldType.Unknown], + ]; - it("detects complex field types", () => { - expect(detectCellType("2022-01-01")).toStrictEqual(DataFieldType.Date); - expect(detectCellType({ my: "object" })).toStrictEqual( - DataFieldType.Unknown - ); + test.each(cases)("%s should be detected as %s", (value, expected) => { + expect(detectCellType(value)).toStrictEqual(expected); }); }); diff --git a/src/lib/datasources/helpers.ts b/src/lib/datasources/helpers.ts index aa97b3b8..f55fb8dc 100644 --- a/src/lib/datasources/helpers.ts +++ b/src/lib/datasources/helpers.ts @@ -9,8 +9,7 @@ import { } from "../dataframe/dataframe"; /** - * parseRecords parses the values for each record based on the detected field - * types. + * Parses the values for each record based on the detected field types. * * If field types matches with the corresponding data types in the records, * this function does nothing. @@ -18,8 +17,8 @@ import { * In the case where a field contains more than one data type, this function * tries to parse the value in each record to match the field type. * - * For example, if the record contains { "weight": 12 }, and the field type is - * DataFieldType.String, the resulting record has { "weight": "12"}. + * For example, if the record contains \{ "weight": 12 \}, and the field type is + * DataFieldType.String, the resulting record has \{ "weight": "12" \}. */ export function parseRecords( records: DataRecord[], @@ -59,9 +58,9 @@ export function parseRecords( /** * Merges a new version of `values` into a copy of data record. * - * @param {Readonly} record - the original data record - * @param {Readonly} values - the values to merge into the original record - * @return {DataRecord} a new data record with the merged values + * @param record - The original data record + * @param values - The values to merge into the original record + * @returns A new data record with the merged values */ export function updateRecordValues( record: Readonly, @@ -144,8 +143,14 @@ export function detectCellType(value: unknown): DataFieldType { return DataFieldType.Unknown; } -function stringToBoolean(stringValue: string): boolean { - switch (stringValue?.toLowerCase()?.trim()) { +/** + * Converts a string to a boolean. + * + * @param str - The string to convert. + * @returns The boolean representation of the string. + */ +function stringToBoolean(str: string): boolean { + switch (str?.toLowerCase()?.trim()) { case "true": case "yes": case "1": @@ -159,10 +164,13 @@ function stringToBoolean(stringValue: string): boolean { return false; default: - return !!stringValue; + return !!str; } } +/** + * Thrown to avoid processing more files than the plugin can handle. + */ export class TooManyNotesError extends Error { constructor(n: number, limit: number) { const message = `This project contains ${Intl.NumberFormat().format( diff --git a/src/lib/datasources/index.ts b/src/lib/datasources/index.ts index 8994022d..c3b4b9c0 100644 --- a/src/lib/datasources/index.ts +++ b/src/lib/datasources/index.ts @@ -15,24 +15,25 @@ export abstract class DataSource { ) {} /** - * queryAll returns a DataFrame with all records in the project. + * Returns a DataFrame with all records in the project. */ abstract queryAll(): Promise; /** - * queryOne returns a DataFrame with a single record for the given file. + * Returns a DataFrame with a single record for the given file. * - * @param fields contains existing fields, to be able to parse file into the existing schema. + * @param fields - The existing fields to allow parsing file into the existing schema + * @returns A dataframe containing a single record */ abstract queryOne(file: IFile, fields: DataField[]): Promise; /** - * includes returns whether a path belongs to the current project. + * Returns whether a path belongs to the current project. */ abstract includes(path: string): boolean; /** - * readonly returns whether the data source is read-only. + * Returns whether the data source is read-only. * * Read-only data sources are typically derived records where the data * source can't determine the original names of the fields. diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index e13233bb..598008f1 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -7,34 +7,52 @@ import { getContext, setContext } from "svelte"; import type { DataField } from "./dataframe/dataframe"; /** - * notEmpty is a convenience function for filtering arrays with optional values. + * Convenience function for filtering null or undefined values in an array. + * + * @param value - The value to check + * @returns Whether value is null or undefined */ export function notEmpty(value: T | null | undefined): value is T { return value !== null && value !== undefined; } +/** + * Convenience function for filtering undefined values in an array. + * + * @param value - The value to check + * @returns Whether value is undefined + */ export function notUndefined(value: T | undefined): value is T { return value !== undefined; } /** - * nextUniqueFileName returns the given file name with the lowest available - * sequence number appended to it. + * Returns the given file name with the lowest available sequence number + * appended to it. + * + * @param folderPath - The folder to check against + * @param name - The name of the note + * @returns A unique file name prefixed with `name` */ -export function nextUniqueFileName(path: string, name: string) { +export function nextUniqueFileName(folderPath: string, name: string): string { return uniquify(name, (name) => { return ( get(app).vault.getAbstractFileByPath( - normalizePath(path + "/" + name + ".md") + normalizePath(folderPath + "/" + name + ".md") ) instanceof TFile ); }); } /** - * nextUniqueProjectName returns the given project name with the lowest - * available sequence number appended to it. + * Returns the given project name with the lowest available sequence number + * appended to it. + * + * @param projects - The existing projects to check against + * @param name - The ideal name for a new project + * @returns A unique project name prefixed with `name` */ + export function nextUniqueProjectName( projects: ProjectDefinition[], name: string @@ -45,8 +63,12 @@ export function nextUniqueProjectName( } /** - * nextUniqueViewName returns the given view name with the lowest - * available sequence number appended to it. + * Returns the given view name with the lowest available sequence number + * appended to it. + * + * @param views - The existing views to check against + * @param name - The ideal name for a new view + * @returns A unique view name prefixed with `name` */ export function nextUniqueViewName(views: ViewDefinition[], name: string) { return uniquify(name, (candidate) => { @@ -55,8 +77,12 @@ export function nextUniqueViewName(views: ViewDefinition[], name: string) { } /** - * nextUniqueFieldName returns the given field name with the lowest - * available sequence number appended to it. + * Returns the given field name with the lowest available sequence number + * appended to it. + * + * @param fields - The existing fields to check against + * @param name - The ideal name for a new field + * @returns A unique field name prefixed with `name` */ export function nextUniqueFieldName(fields: DataField[], name: string) { return uniquify(name, (candidate) => { @@ -65,11 +91,12 @@ export function nextUniqueFieldName(fields: DataField[], name: string) { } /** - * uniquify appends a sequence number to a string, where the number is the - * lowest available according to a callback function. + * Appends a sequence number to a string, where the number is the lowest + * available according to a callback function. * - * @param name is the preferred name. - * @param exists is a predicate for whether a candidate string is already taken. + * @param name - The preferred name + * @param exists - A predicate for whether a candidate string is already taken + * @returns A unique string */ function uniquify(name: string, exists: (name: string) => boolean): string { if (!exists(name)) { @@ -84,9 +111,18 @@ function uniquify(name: string, exists: (name: string) => boolean): string { return name + " " + num; } -export function getNameFromPath(path: string) { - const start: number = path.lastIndexOf("/") + 1; - const end: number = path.lastIndexOf("."); +/** + * Returns the name of a note from a path. + * + * For example, Daily/2001-01-01.md returns 2001-01-01. + * + * @param path - The path to extract name from + * @returns The name of the note + */ +export function getNameFromPath(path: string): string { + const start = path.lastIndexOf("/") + 1; + const end = path.lastIndexOf("."); + return path.substring(start, end); } @@ -94,6 +130,7 @@ export type Context = Readonly<{ get: () => T; set: (value: T) => void; }>; + export function makeContext(): Context { const key = Symbol(); return { diff --git a/src/lib/metadata/encode.ts b/src/lib/metadata/encode.ts index 633c5eb8..3a496b24 100644 --- a/src/lib/metadata/encode.ts +++ b/src/lib/metadata/encode.ts @@ -3,11 +3,11 @@ import { stringify } from "yaml"; import { parseYaml } from "./decode"; /** - * encodeFrontMatter updates the front matter of a note. + * Updates the front matter of a note. * - * @param data is the current content of the note, including front matter. - * @param frontmatter is the front matter to add to the note. - * @returns data with the updated front matter. + * @param data - The current content of the note, including front matter. + * @param frontmatter - The front matter to add to the note. + * @returns Data with the updated front matter. */ export function encodeFrontMatter( data: string, diff --git a/src/lib/templates/interpolate.ts b/src/lib/templates/interpolate.ts index 5c8725f1..d912609f 100644 --- a/src/lib/templates/interpolate.ts +++ b/src/lib/templates/interpolate.ts @@ -1,11 +1,11 @@ /** - * interpolateTemplate interpolates occurrences of double curly braces. + * Interpolates occurrences of double curly braces. * * The data parameter contains a map of available template variables, for * example `date` and `time`. * * The value of each template variable is a function with an optional argument. - * The argument is any text after an optional colon, e.g. {{date:YYYY-MM-DD}}. + * The argument is any text after an optional colon, e.g. \{\{date:YYYY-MM-DD\}\}. */ export function interpolateTemplate( template: string, diff --git a/src/ui/app/viewSort.ts b/src/ui/app/viewSort.ts index f9fe4488..95591bd3 100644 --- a/src/ui/app/viewSort.ts +++ b/src/ui/app/viewSort.ts @@ -19,8 +19,8 @@ export function applySort(frame: DataFrame, sort: SortDefinition): DataFrame { * Sorts records in place. This method mutates the array * and returns a reference to the same array. * - * @param {DataRecord[]} records - the records to be sorted - * @param {SortDefinition} sort - the definition for sorting the records + * @param records - the records to be sorted + * @param sort - the definition for sorting the records */ export function sortRecords(records: DataRecord[], sort: SortDefinition) { return records.sort((a, b): number => { diff --git a/src/ui/views/Board/board.ts b/src/ui/views/Board/board.ts index cd968746..26ee4d77 100644 --- a/src/ui/views/Board/board.ts +++ b/src/ui/views/Board/board.ts @@ -135,10 +135,10 @@ function groupRecordsByField( * order of records in the column settings. This method mutates the array and * returns a reference to the same array. * - * @param {DataRecord[]} records - The records to be sorted. - * @param {ColumnSettings[string]} [columnSettings] - The column settings for sorting the records. - * @param {DataField} [orderSyncField] - The priority field for sorting the records. - * @return {DataRecord[]} The sorted records. + * @param records - The records to be sorted. + * @param columnSettings - The column settings for sorting the records. + * @param orderSyncField - The priority field for sorting the records. + * @returns The sorted records. */ function applyCustomRecordOrder( records: DataRecord[],