Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2991 (2024-07-29 16:09:47)

feat: releasable code for accessing properties via `task.file`
# Types of changes

Changes visible to users:

- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
  - Issue/discussion: #2480 

## Description

- done by pairing with @claremacrae 
- add `task.file.hasProperty()` and `task.file.property()`

## Motivation and Context

- RELEASE =)
- save users from having to understand when to use `null` or `undefined` with properties
- save @claremacrae from having to document when to use `null` or `undefined` with properties

## How has this been tested?

- new unit tests
- exploratory testing in live vault

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
  - todo for @claremacrae  =)
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follows this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/src/Scripting/TasksFile.ts b/src/Scripting/TasksFile.ts
index fbd98118..3a383084 100644
--- a/src/Scripting/TasksFile.ts
+++ b/src/Scripting/TasksFile.ts
@@ -134,4 +134,47 @@ export class TasksFile {
     get filenameWithoutExtension(): string {
         return this.withoutExtension(this.filename);
     }
+
+    public hasProperty(key: string): boolean {
+        const foundKey = this.findKeyInFrontmatter(key);
+        if (foundKey === undefined) {
+            return false;
+        }
+
+        const propertyValue = this.frontmatter[foundKey];
+        if (propertyValue === null) {
+            return false;
+        }
+
+        if (propertyValue === undefined) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public property(key: string): any {
+        const foundKey = this.findKeyInFrontmatter(key);
+        if (foundKey === undefined) {
+            return null;
+        }
+
+        const propertyValue = this.frontmatter[foundKey];
+        if (propertyValue === undefined) {
+            return null;
+        }
+
+        if (Array.isArray(propertyValue)) {
+            return propertyValue.filter((item: any) => item !== null);
+        }
+
+        return propertyValue;
+    }
+
+    private findKeyInFrontmatter(key: string) {
+        return Object.keys(this.frontmatter).find((searchKey: string) => {
+            const lowerCaseSearchKey = searchKey.toLowerCase();
+            return lowerCaseSearchKey === key.toLowerCase();
+        });
+    }
 }


Test patch:
diff --git a/resources/sample_vaults/Tasks-Demo/Test Data/yaml_capitalised_property_name.md b/resources/sample_vaults/Tasks-Demo/Test Data/yaml_capitalised_property_name.md
new file mode 100644
index 00000000..e1be80c3
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Test Data/yaml_capitalised_property_name.md
@@ -0,0 +1,7 @@
+---
+CAPITAL_property: some value
+---
+
+# yaml_capitalised_property_name
+
+- [ ] #task Task in 'yaml_capitalised_property_name'
diff --git a/tests/Obsidian/AllCacheSampleData.ts b/tests/Obsidian/AllCacheSampleData.ts
index 875e00d7..4a9abc53 100644
--- a/tests/Obsidian/AllCacheSampleData.ts
+++ b/tests/Obsidian/AllCacheSampleData.ts
@@ -45,6 +45,7 @@ import { yaml_1_alias } from './__test_data__/yaml_1_alias';
 import { yaml_2_aliases } from './__test_data__/yaml_2_aliases';
 import { yaml_all_property_types_empty } from './__test_data__/yaml_all_property_types_empty';
 import { yaml_all_property_types_populated } from './__test_data__/yaml_all_property_types_populated';
+import { yaml_capitalised_property_name } from './__test_data__/yaml_capitalised_property_name';
 import { yaml_complex_example } from './__test_data__/yaml_complex_example';
 import { yaml_complex_example_standardised } from './__test_data__/yaml_complex_example_standardised';
 import { yaml_custom_number_property } from './__test_data__/yaml_custom_number_property';
@@ -104,6 +105,7 @@ export function allCacheSampleData() {
         yaml_2_aliases,
         yaml_all_property_types_empty,
         yaml_all_property_types_populated,
+        yaml_capitalised_property_name,
         yaml_complex_example,
         yaml_complex_example_standardised,
         yaml_custom_number_property,
diff --git a/tests/Obsidian/__test_data__/yaml_capitalised_property_name.ts b/tests/Obsidian/__test_data__/yaml_capitalised_property_name.ts
new file mode 100644
index 00000000..1bbd7068
--- /dev/null
+++ b/tests/Obsidian/__test_data__/yaml_capitalised_property_name.ts
@@ -0,0 +1,132 @@
+export const yaml_capitalised_property_name = {
+    filePath: 'Test Data/yaml_capitalised_property_name.md',
+    fileContents:
+        '---\n' +
+        'CAPITAL_property: some value\n' +
+        '---\n' +
+        '\n' +
+        '# yaml_capitalised_property_name\n' +
+        '\n' +
+        "- [ ] #task Task in 'yaml_capitalised_property_name'\n",
+    cachedMetadata: {
+        tags: [
+            {
+                position: {
+                    start: {
+                        line: 6,
+                        col: 6,
+                        offset: 78,
+                    },
+                    end: {
+                        line: 6,
+                        col: 11,
+                        offset: 83,
+                    },
+                },
+                tag: '#task',
+            },
+        ],
+        headings: [
+            {
+                position: {
+                    start: {
+                        line: 4,
+                        col: 0,
+                        offset: 38,
+                    },
+                    end: {
+                        line: 4,
+                        col: 32,
+                        offset: 70,
+                    },
+                },
+                heading: 'yaml_capitalised_property_name',
+                level: 1,
+            },
+        ],
+        sections: [
+            {
+                type: 'yaml',
+                position: {
+                    start: {
+                        line: 0,
+                        col: 0,
+                        offset: 0,
+                    },
+                    end: {
+                        line: 2,
+                        col: 3,
+                        offset: 36,
+                    },
+                },
+            },
+            {
+                type: 'heading',
+                position: {
+                    start: {
+                        line: 4,
+                        col: 0,
+                        offset: 38,
+                    },
+                    end: {
+                        line: 4,
+                        col: 32,
+                        offset: 70,
+                    },
+                },
+            },
+            {
+                type: 'list',
+                position: {
+                    start: {
+                        line: 6,
+                        col: 0,
+                        offset: 72,
+                    },
+                    end: {
+                        line: 6,
+                        col: 52,
+                        offset: 124,
+                    },
+                },
+            },
+        ],
+        listItems: [
+            {
+                position: {
+                    start: {
+                        line: 6,
+                        col: 0,
+                        offset: 72,
+                    },
+                    end: {
+                        line: 6,
+                        col: 52,
+                        offset: 124,
+                    },
+                },
+                parent: -6,
+                task: ' ',
+            },
+        ],
+        frontmatter: {
+            CAPITAL_property: 'some value',
+        },
+        frontmatterPosition: {
+            start: {
+                line: 0,
+                col: 0,
+                offset: 0,
+            },
+            end: {
+                line: 2,
+                col: 3,
+                offset: 36,
+            },
+        },
+        frontmatterLinks: [],
+    },
+    obsidianApiVersion: '1.6.7',
+    getAllTags: ['#task'],
+    parseFrontMatterTags: null,
+};
diff --git a/tests/Scripting/TasksFile.test.ts b/tests/Scripting/TasksFile.test.ts
index ae78f21a..aa531a43 100644
--- a/tests/Scripting/TasksFile.test.ts
+++ b/tests/Scripting/TasksFile.test.ts
@@ -3,6 +3,7 @@ import { TasksFile } from '../../src/Scripting/TasksFile';
 import { callouts_nested_issue_2890_unlabelled } from '../Obsidian/__test_data__/callouts_nested_issue_2890_unlabelled';
 import { no_yaml } from '../Obsidian/__test_data__/no_yaml';
 import { empty_yaml } from '../Obsidian/__test_data__/empty_yaml';
+import { yaml_capitalised_property_name } from '../Obsidian/__test_data__/yaml_capitalised_property_name';
 import { yaml_tags_has_multiple_values } from '../Obsidian/__test_data__/yaml_tags_has_multiple_values';
 import { yaml_custom_number_property } from '../Obsidian/__test_data__/yaml_custom_number_property';
 import { yaml_tags_with_one_value_on_new_line } from '../Obsidian/__test_data__/yaml_tags_with_one_value_on_new_line';
@@ -206,3 +207,77 @@ describe('TasksFile - reading tags', () => {
         expect(tasksFile.frontmatter.tags).toEqual([]);
     });
 });
+
+describe('TasksFile - properties', () => {
+    it('should not have any properties in a file with empty frontmatter', () => {
+        const tasksFile = getTasksFileFromMockData(yaml_all_property_types_empty);
+
+        Object.keys(tasksFile.frontmatter).forEach((key) => {
+            if (key === 'tags') {
+                expect(tasksFile.hasProperty(key)).toEqual(true);
+            } else {
+                expect(tasksFile.hasProperty(key)).toEqual(false);
+            }
+        });
+    });
+
+    it('should not have any properties in a file with populated frontmatter', () => {
+        const tasksFile = getTasksFileFromMockData(yaml_all_property_types_populated);
+
+        Object.keys(tasksFile.frontmatter).forEach((key) => {
+            expect(tasksFile.hasProperty(key)).toEqual(true);
+        });
+    });
+
+    it('should treat non-exising properties correctly', () => {
+        const tasksFile = getTasksFileFromMockData(no_yaml);
+
+        expect(tasksFile.hasProperty('appleSauce')).toEqual(false);
+    });
+
+    it('should obtain a string property value', () => {
+        const tasksFile = getTasksFileFromMockData(example_kanban);
+        expect(tasksFile.property('kanban-plugin')).toEqual('basic');
+    });
+
+    it('should return null for a missing property value', () => {
+        const tasksFile = getTasksFileFromMockData(no_yaml);
+        expect(tasksFile.property('kanban-plugin')).toEqual(null);
+    });
+
+    it('should remove nulls from a list property', () => {
+        const tasksFile = getTasksFileFromMockData(yaml_complex_example);
+        expect(tasksFile.property('custom_list')).toEqual(['value 1', 'value 2']);
+        expect(tasksFile.property('unknown_list')).toEqual([]);
+    });
+
+    it('should return properties of each type', () => {
+        const tasksFile = getTasksFileFromMockData(yaml_all_property_types_populated);
+        expect(tasksFile.property('sample_checkbox_property')).toEqual(true);
+        expect(tasksFile.property('sample_date_property')).toEqual('2024-07-21');
+        expect(tasksFile.property('sample_date_and_time_property')).toEqual('2024-07-21T12:37:00');
+        expect(tasksFile.property('sample_list_property')).toEqual(['Sample', 'List', 'Value']);
+        expect(tasksFile.property('sample_number_property')).toEqual(246);
+        expect(tasksFile.property('sample_text_property')).toEqual('Sample Text Value');
+        expect(tasksFile.property('sample_link_property')).toEqual('[[yaml_all_property_types_populated]]');
+        expect(tasksFile.property('sample_link_list_property')).toEqual([
+            '[[yaml_all_property_types_populated]]',
+            '[[yaml_all_property_types_empty]]',
+        ]);
+        expect(tasksFile.property('aliases')).toEqual(['YAML All Property Types Populated']);
+        expect(tasksFile.property('tags')).toEqual(['#sample/tag/value']);
+        expect(tasksFile.property('creation date')).toEqual('2024-05-25T15:17:00');
+    });
+
+    it('should ignore the case of a lower case property name', () => {
+        const tasksFile = getTasksFileFromMockData(yaml_all_property_types_populated);
+        expect(tasksFile.hasProperty('SAMPLE_CHECKBOX_PROPERTY')).toEqual(true);
+        expect(tasksFile.property('SAMPLE_CHECKBOX_PROPERTY')).toEqual(true);
+    });
+
+    it('should ignore the case of a upper case property name', () => {
+        const tasksFile = getTasksFileFromMockData(yaml_capitalised_property_name);
+        expect(tasksFile.hasProperty('capital_property')).toEqual(true);
+        expect(tasksFile.property('capital_property')).toEqual('some value');
+    });
+});