Skip to content

Commit 7e7eb2f

Browse files
docs: readability changes (#57)
* docs: readability changes * 📝 Add docstrings to `docs` (#58) Docstrings generation was requested by @brianrodri. * #57 (comment) The following files were modified: * `src/model/collection/periodic-notes.ts` * `src/model/collection/util.ts` * `src/model/task/util.ts` * `src/util/luxon-utils.ts` Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * docs: fix lint errors * 📝 Add docstrings to `docs` (#59) * 📝 Add docstrings to `docs` Docstrings generation was requested by @brianrodri. * #57 (comment) The following files were modified: * `src/model/collection/periodic-notes.ts` * Update src/model/collection/periodic-notes.ts * Update periodic-notes.ts --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Brian Rodriguez <[email protected]> * Update periodic-notes.ts --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent e3b3818 commit 7e7eb2f

File tree

11 files changed

+101
-105
lines changed

11 files changed

+101
-105
lines changed

src/model/collection/__tests__/__snapshots__/periodic-notes.test.ts.snap

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ exports[`PeriodicNotes > pre-conditions > should throw when everything is invali
99
[AggregateError: invalid config
1010
AssertionError: folder must be non-empty
1111
AssertionError: date format must parse the formatted strings it produces
12-
AssertionError: interval offset is invalid: invalid time: unspecified
13-
AssertionError: interval duration is invalid: invalid time: infinity]
12+
AssertionError: interval duration is invalid: invalid time: infinity
13+
AssertionError: interval offset is invalid: invalid time: unspecified]
1414
`;
1515

1616
exports[`PeriodicNotes > pre-conditions > should throw when folder is empty 1`] = `
+5-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import { describe, expect, it } from "vitest";
22

3-
import { sanitizeFolder } from "../util";
3+
import { stripTrailingSlash } from "../util";
44

5-
describe(`${sanitizeFolder.name}`, () => {
5+
describe(`${stripTrailingSlash.name}`, () => {
66
it("should strip trailing slashes", () => {
7-
expect(sanitizeFolder("foo/")).toEqual("foo");
7+
expect(stripTrailingSlash("foo/")).toEqual("foo");
88
});
99

1010
it("should do nothing if there are no trailing slashes", () => {
11-
expect(sanitizeFolder("foo")).toEqual("foo");
11+
expect(stripTrailingSlash("foo")).toEqual("foo");
1212
});
1313

1414
it("should do nothing when folder is the vault root", () => {
15-
expect(sanitizeFolder("/")).toEqual("/");
15+
expect(stripTrailingSlash("/")).toEqual("/");
1616
});
1717
});

src/model/collection/periodic-notes.ts

+54-43
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,13 @@
1-
import assert from "assert";
2-
import { attempt, isError } from "lodash";
1+
import { notStrictEqual as assertNotStrictEqual } from "assert";
2+
import { attempt, clone, isError } from "lodash";
33
import { DateTime, DateTimeOptions, Duration, DurationLike, Interval, IntervalMaybeValid } from "luxon";
44
import { parse } from "path";
5+
import { DeepReadonly } from "utility-types";
56

67
import { assertLuxonFormat, assertValid } from "@/util/luxon-utils";
78

89
import { DateBasedCollection } from "./schema";
9-
import { sanitizeFolder } from "./util";
10-
11-
/**
12-
* Configuration options for {@link PeriodicNotes}.
13-
* @param IsValidConfiguration - whether the configuration is valid.
14-
*/
15-
export type PeriodicNotesConfig<IsValidConfiguration extends boolean> =
16-
IsValidConfiguration extends true ?
17-
{
18-
/** {@inheritDoc PeriodicNotes.folder} */
19-
folder: string;
20-
/** {@inheritDoc PeriodicNotes.dateFormat} */
21-
dateFormat: string;
22-
/** {@inheritDoc PeriodicNotes.dateOptions} */
23-
dateOptions: DateTimeOptions;
24-
/** {@inheritDoc PeriodicNotes.intervalDuration} */
25-
intervalDuration: Duration<true>;
26-
/** {@inheritDoc PeriodicNotes.intervalOffset} */
27-
intervalOffset: Duration<true>;
28-
}
29-
: {
30-
/** {@inheritDoc PeriodicNotes.folder} */
31-
folder: string;
32-
/** {@inheritDoc PeriodicNotes.dateFormat} */
33-
dateFormat: string;
34-
/** {@inheritDoc PeriodicNotes.dateOptions} */
35-
dateOptions?: DateTimeOptions;
36-
/** {@inheritDoc PeriodicNotes.intervalDuration} */
37-
intervalDuration: DurationLike;
38-
/** {@inheritDoc PeriodicNotes.intervalOffset} */
39-
intervalOffset?: DurationLike;
40-
};
10+
import { stripTrailingSlash } from "./util";
4111

4212
/**
4313
* Intended to handle the popular "Periodic Notes" community plugin.
@@ -55,7 +25,7 @@ export class PeriodicNotes extends DateBasedCollection implements PeriodicNotesC
5525
public readonly dateFormat: string;
5626

5727
/** Luxon options used when parsing {@link DateTime}s from file names. */
58-
public readonly dateOptions: DateTimeOptions;
28+
public readonly dateOptions: DeepReadonly<DateTimeOptions>;
5929

6030
/**
6131
* The {@link Duration} of each file's corresponding {@link Interval}.
@@ -101,23 +71,64 @@ export class PeriodicNotes extends DateBasedCollection implements PeriodicNotesC
10171
}
10272

10373
/**
104-
* @param config - the config to validate.
105-
* @throws if any of the config's properties are invalid.
106-
* @returns a valid config.
74+
* Configuration options for {@link PeriodicNotes}.
75+
* @typeParam IsValidConfiguration - whether the configuration is valid.
76+
*/
77+
export type PeriodicNotesConfig<IsValidConfiguration extends boolean> =
78+
IsValidConfiguration extends true ?
79+
{
80+
/** {@inheritDoc PeriodicNotes.folder} */
81+
folder: string;
82+
/** {@inheritDoc PeriodicNotes.dateFormat} */
83+
dateFormat: string;
84+
/** {@inheritDoc PeriodicNotes.dateOptions} */
85+
dateOptions: DateTimeOptions;
86+
/** {@inheritDoc PeriodicNotes.intervalDuration} */
87+
intervalDuration: Duration<true>;
88+
/** {@inheritDoc PeriodicNotes.intervalOffset} */
89+
intervalOffset: Duration<true>;
90+
}
91+
: {
92+
/** {@inheritDoc PeriodicNotes.folder} */
93+
folder: string;
94+
/** {@inheritDoc PeriodicNotes.dateFormat} */
95+
dateFormat: string;
96+
/** {@inheritDoc PeriodicNotes.dateOptions} */
97+
dateOptions?: DateTimeOptions;
98+
/** {@inheritDoc PeriodicNotes.intervalDuration} */
99+
intervalDuration: DurationLike;
100+
/** {@inheritDoc PeriodicNotes.intervalOffset} */
101+
intervalOffset?: DurationLike;
102+
};
103+
104+
/**
105+
* Validates and normalizes a PeriodicNotes configuration object.
106+
*
107+
* This function checks that:
108+
* - the folder path (after stripping any trailing slash) is non-empty,
109+
* - the date format is valid with the given date options,
110+
* - the interval duration is a valid, non-zero duration, and
111+
* - the interval offset is a valid duration.
112+
*
113+
* If any of these validations fail, an {@link AggregateError} is thrown containing details
114+
* about each issue. On success, a new configuration object with strictly defined properties is returned.
115+
* @param config - The configuration object to validate.
116+
* @returns A validated configuration object with normalized properties.
117+
* @throws If one or more configuration properties are invalid.
107118
*/
108119
function validated(config: PeriodicNotesConfig<false>): PeriodicNotesConfig<true> {
109-
const folder = sanitizeFolder(config.folder);
120+
const folder = stripTrailingSlash(config.folder);
110121
const dateFormat = config.dateFormat;
111-
const dateOptions = config.dateOptions ?? {};
122+
const dateOptions = clone(config.dateOptions ?? {});
112123
const intervalDuration = Duration.fromDurationLike(config.intervalDuration);
113124
const intervalOffset = Duration.fromDurationLike(config.intervalOffset ?? 0);
114125

115126
const errors = [
116-
attempt(() => assert(folder.length > 0, "folder must be non-empty")),
127+
attempt(() => assertNotStrictEqual(folder.length, 0, "folder must be non-empty")),
117128
attempt(() => assertLuxonFormat(dateFormat, dateOptions)),
118-
attempt(() => assertValid(intervalOffset, "interval offset is invalid")),
119129
attempt(() => assertValid(intervalDuration, "interval duration is invalid")),
120-
attempt(() => assert(intervalDuration.valueOf() !== 0, "interval duration must not be zero")),
130+
attempt(() => assertNotStrictEqual(intervalDuration.valueOf(), 0, "interval duration must not be zero")),
131+
attempt(() => assertValid(intervalOffset, "interval offset is invalid")),
121132
].filter(isError);
122133

123134
if (errors.length > 0) {

src/model/collection/schema.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export abstract class Collection {
99
public abstract includes(filePath: string): boolean;
1010
}
1111

12-
/** A {@link Collection} where each note corresponds to a unique {@link luxon.Interval} of time. */
12+
/** Interface for {@link Collection} where each note corresponds to a unique {@link luxon.Interval} of time. */
1313
export abstract class DateBasedCollection extends Collection {
1414
/**
1515
* @param filePath - the path to check.

src/model/collection/util.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/**
2-
* @param folder - the folder to sanitize.
3-
* @returns sanitized value with trailing slashes removed, if applicable.
2+
* Returns a sanitized folder path by removing a trailing slash unless the path is the root ("/").
3+
*
4+
* The function trims whitespace from the input folder string. If the trimmed string equals "/", it is returned unchanged. Otherwise, any trailing slash is removed.
5+
* @param folder - The folder path to sanitize.
6+
* @returns The folder path without a trailing slash, except for the root path.
47
*/
5-
export function sanitizeFolder(folder: string): string {
8+
export function stripTrailingSlash(folder: string): string {
69
folder = folder.trim();
710
return folder === "/" ? folder : folder.replace(/\/$/, "");
811
}

src/model/index/schema.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DeepReadonly } from "utility-types";
33

44
import { Collection } from "../collection/schema";
55

6-
/** Provides an API for efficiently accessing {@link Collection}s in a vault. */
6+
/** API for efficiently accessing the notes of a {@link Collection}. */
77
export interface VaultIndex {
88
/**
99
* Register a new {@link Collection} into the index.

src/model/task/schema.const.ts

+8-11
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,26 @@ import { DateTime } from "luxon";
22

33
import { Task, TaskSource, TaskStatus } from "./schema";
44

5-
/** The default date used by {@link Task}s. Valid dates will always take precedence over invalid dates. */
6-
export const DEFAULT_DATETIME_VALUE: DateTime = DateTime.invalid("unspecified");
7-
85
/** The default priority used by {@link Task}s. Different values will always take precedence over this value. */
9-
export const DEFAULT_PRIORITY_VALUE: Task["priority"] = 3;
6+
export const DEFAULT_PRIORITY_VALUE = 3 as const satisfies Task["priority"];
107

118
/**
129
* The default type used by {@link TaskSource} and {@link TaskStatus}.
1310
* Different values will always take precedence over this value.
1411
*/
15-
export const DEFAULT_TYPE_VALUE: TaskSource["type"] & TaskStatus["type"] = "UNKNOWN";
12+
export const DEFAULT_TYPE_VALUE = "UNKNOWN" as const satisfies TaskSource["type"] & TaskStatus["type"];
1613

1714
/** A strongly-typed {@link Task} with all-default values. */
1815
export const TASK_WITH_DEFAULT_VALUES: Task = {
1916
status: { type: DEFAULT_TYPE_VALUE },
2017
source: { type: DEFAULT_TYPE_VALUE },
2118
dates: {
22-
cancelled: DEFAULT_DATETIME_VALUE,
23-
created: DEFAULT_DATETIME_VALUE,
24-
done: DEFAULT_DATETIME_VALUE,
25-
due: DEFAULT_DATETIME_VALUE,
26-
scheduled: DEFAULT_DATETIME_VALUE,
27-
start: DEFAULT_DATETIME_VALUE,
19+
cancelled: DateTime.invalid("unspecified"),
20+
created: DateTime.invalid("unspecified"),
21+
done: DateTime.invalid("unspecified"),
22+
due: DateTime.invalid("unspecified"),
23+
scheduled: DateTime.invalid("unspecified"),
24+
start: DateTime.invalid("unspecified"),
2825
},
2926
description: "",
3027
priority: DEFAULT_PRIORITY_VALUE,

src/model/task/schema.ts

+6-26
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import { DateTime } from "luxon";
22

3-
export {
4-
DEFAULT_DATETIME_VALUE,
5-
DEFAULT_PRIORITY_VALUE,
6-
DEFAULT_TYPE_VALUE,
7-
TASK_WITH_DEFAULT_VALUES,
8-
} from "./schema.const";
3+
export { DEFAULT_PRIORITY_VALUE, DEFAULT_TYPE_VALUE, TASK_WITH_DEFAULT_VALUES } from "./schema.const";
94

105
/** Objo-related metadata for the tasks in a user's vault. */
116
export interface Task {
@@ -43,39 +38,24 @@ export interface Task {
4338
/** Metadata about the actionable status of a task. */
4439
export type TaskStatus =
4540
| {
46-
/**
47-
* The status of the task. Mirrors the status provided by the "obsidian-tasks" plugin.
48-
* `"UNKNOWN"` is reserved for tasks that are invalid or unparsable.
49-
*/
41+
/** `"UNKNOWN"` is reserved for tasks that are invalid or unparsable. */
5042
type: "UNKNOWN";
5143
}
5244
| {
53-
/**
54-
* The status of the task. Mirrors the status provided by the "obsidian-tasks" plugin.
55-
* `"UNKNOWN"` is reserved for tasks that are invalid or unparsable.
56-
*/
45+
/** The status of the task. Mirrors the status provided by the "obsidian-tasks" plugin. */
5746
type: "OPEN" | "DONE" | "CANCELLED" | "NON_TASK";
58-
/**
59-
* The character inside the `[ ]` brackets on the line with the task.
60-
* Generally a space (" ") for incomplete tasks and an ("x") for completed tasks.
61-
*/
47+
/** The character inside the `[ ]` brackets on the line with the task. */
6248
symbol: string;
6349
};
6450

6551
/** Metadata about where a task was extracted/generated from. */
6652
export type TaskSource =
6753
| {
68-
/**
69-
* Identifies where this task was extracted/generated from.
70-
* `"UNKNOWN"` is reserved for tasks that are invalid or unparsable.
71-
*/
54+
/** `"UNKNOWN"` is reserved for tasks that are invalid or unparsable. */
7255
type: "UNKNOWN";
7356
}
7457
| {
75-
/**
76-
* Identifies where this task was extracted/generated from.
77-
* `"UNKNOWN"` is reserved for tasks that are invalid or unparsable.
78-
*/
58+
/** Identifies where this task was extracted/generated from. */
7959
type: "PAGE";
8060
/** The full path of the file this task was taken from. */
8161
path: string;

src/model/task/util.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { DeepPartial } from "utility-types";
55
import { DEFAULT_PRIORITY_VALUE, DEFAULT_TYPE_VALUE, Task, TASK_WITH_DEFAULT_VALUES } from "@/model/task/schema";
66

77
/**
8-
* @param parts - the task parts to merge.
9-
* @returns a new {@link Task} with the front-most non-default values taken from the parts.
8+
* Merges multiple partial Task objects into a complete Task using a custom merge strategy.
9+
* The function begins with default Task values and sequentially merges each provided partial Task. For each property, the front-most non-default value is retained. Special handling is applied for properties such as task type, priority, strings, DateTime objects, and Set collections.
10+
* @param parts - The partial Task objects to merge.
11+
* @returns new {@link Task} with the front-most non-default values taken from the parts.
1012
*/
1113
export function mergeTaskParts(...parts: DeepPartial<Task>[]): Task {
1214
const defaults = { ...TASK_WITH_DEFAULT_VALUES };

src/render/preact/lib/obsidian/obsidian-markdown.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import { MARKDOWN_RENDER_DEBOUNCE_TIME } from "./obsidian-markdown.const";
1313
*/
1414
export const ObsidianMarkdown: FunctionalComponent<ObsidianMarkdownProps> = (props) => {
1515
const { app, component, markdown, sourcePath, tagName = "span", delay = MARKDOWN_RENDER_DEBOUNCE_TIME } = props;
16+
const elRef = useRef<HTMLElement>();
1617
const delayMs = useMemo(() => Duration.fromDurationLike(delay).toMillis(), [delay]);
1718
const renderObsidianMarkdown = useDebounceCallback(MarkdownRenderer["render"], delayMs);
18-
const elRef = useRef<HTMLElement>();
1919

2020
useEffect(() => {
2121
const el = elRef.current;

src/util/luxon-utils.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AssertionError, ok as assert } from "assert";
1+
import assert from "assert";
22
import { DateTime, DateTimeOptions, Duration, Interval } from "luxon";
33
import { Brand } from "utility-types";
44

@@ -9,20 +9,23 @@ export type LuxonValue<IsValid extends boolean> = DateTime<IsValid> | Duration<I
99
export type LuxonFormat = Brand<string, "LuxonFormat">;
1010

1111
/**
12-
* @param value - the {@link LuxonValue} to check.
13-
* @param message - the message to use in the error.
14-
* @throws error if value is invalid.
12+
* Asserts that the provided Luxon value is valid.
13+
*
14+
* This function checks whether a Luxon date, duration, or interval is valid. If the value is invalid,
15+
* it throws an error with a message combining a custom header (or default constructor name) with the
16+
* value's invalid reason and, if available, its invalid explanation.
17+
* @param value - The Luxon object to validate.
18+
* @param message - Optional custom header for the error message.
19+
* @throws If the provided value is invalid.
1520
*/
1621
export function assertValid(
1722
value: LuxonValue<true> | LuxonValue<false>,
1823
message?: string,
1924
): asserts value is LuxonValue<true> {
20-
if (!value.isValid) {
21-
const { invalidReason, invalidExplanation } = value;
22-
const header = message ?? `Invalid ${value.constructor.name}`;
23-
const reason = invalidExplanation ? `${invalidReason}: ${invalidExplanation}` : invalidReason;
24-
throw new AssertionError({ message: `${header}: ${reason}`, actual: false, expected: true, operator: "==" });
25-
}
25+
const { invalidReason, invalidExplanation } = value;
26+
const header = message ?? `Invalid ${value.constructor.name}`;
27+
const reason = invalidExplanation ? `${invalidReason}: ${invalidExplanation}` : invalidReason;
28+
assert(value.isValid, `${header}: ${reason}`);
2629
}
2730

2831
/**

0 commit comments

Comments
 (0)