Skip to content

Commit 4de405e

Browse files
committed
#195 - support for metadata value matchers
- implementation completed, tested locally on a dedicated vault - need to create unit tests for the new and modified areas before release
1 parent 10e0ae0 commit 4de405e

File tree

5 files changed

+152
-8
lines changed

5 files changed

+152
-8
lines changed

src/custom-sort/custom-sort-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {MDataExtractor} from "./mdata-extractors";
2+
import {MDataMatcher} from "./mdata-matchers";
23

34
export enum CustomSortGroupType {
45
Outsiders, // Not belonging to any of other groups
@@ -81,6 +82,7 @@ export interface CustomSortGroup {
8182
matchFilenameWithExt?: boolean
8283
foldersOnly?: boolean
8384
withMetadataFieldName?: string // for 'with-metadata:' grouping
85+
withMetadataMatcher?: MDataMatcher // optionally can come for 'with-metadata:' grouping
8486
iconName?: string // for integration with obsidian-folder-icon community plugin
8587
priority?: number
8688
combineWithIdx?: number

src/custom-sort/custom-sort.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,17 +476,24 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
476476
const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md`
477477
let frontMatterCache: FrontMatterCache | undefined = ctx._mCache.getCache(notePathToScan)?.frontmatter
478478
let hasMetadata: boolean | undefined = frontMatterCache?.hasOwnProperty(group.withMetadataFieldName)
479+
let metadataValue: string | undefined = hasMetadata ? frontMatterCache?.[group.withMetadataFieldName] : undefined
479480
// For folders, if index-based folder note mode, scan the index file, giving it the priority
480481
if (aFolder) {
481482
const indexNoteBasename = ctx?.plugin?.indexNoteBasename()
482483
if (indexNoteBasename) {
483484
frontMatterCache = ctx._mCache.getCache(`${entry.path}/${indexNoteBasename}.md`)?.frontmatter
484485
hasMetadata = hasMetadata || frontMatterCache?.hasOwnProperty(group.withMetadataFieldName)
486+
metadataValue = hasMetadata ? frontMatterCache?.[group.withMetadataFieldName] : undefined
485487
}
486488
}
487489

488490
if (hasMetadata) {
489-
determined = true
491+
if (group.withMetadataMatcher) {
492+
// note: empty metadata value doesn't match anything, by design
493+
determined = !!(metadataValue && group.withMetadataMatcher(metadataValue))
494+
} else {
495+
determined = true
496+
}
490497
}
491498
}
492499
}

src/custom-sort/mdata-extractors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ type ExtractorFn = (mdataValue: string) => string|undefined
77

88
interface DateExtractorSpec {
99
specPattern: string|RegExp,
10-
extractorFn: ExtractorFn
10+
extractorFn: MDataExtractor
1111
}
1212

1313
export interface MDataExtractor {

src/custom-sort/mdata-matchers.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {
2+
getNormalizedDate_NormalizerFn_for
3+
} from "./matchers";
4+
import {NormalizerFn} from "./custom-sort-types";
5+
6+
export interface MDataMatcher {
7+
(mdataValue: string): boolean
8+
}
9+
10+
export interface MDataMatcherFactory {
11+
(specsMatch: string|RegExpMatchArray): MDataMatcher
12+
}
13+
14+
interface ValueMatcherSpec {
15+
specPattern: string|RegExp,
16+
valueMatcherFnFactory: MDataMatcherFactory
17+
unitTestsId: string
18+
}
19+
20+
export interface MDataMatcherParseResult {
21+
m: MDataMatcher
22+
remainder: string
23+
}
24+
25+
const VALUE_MATCHER_REGEX = /value\(([^)]+)\)/
26+
function getPlainValueMatcherFn(specsMatch: RegExpMatchArray) {
27+
const EXACT_VALUE_IDX = 1 // Related to the spec regexp
28+
const expectedValue = specsMatch[EXACT_VALUE_IDX].trim()
29+
return (mdataValue: string): boolean => {
30+
return mdataValue === expectedValue
31+
}
32+
}
33+
34+
const RANGE_MATCHER_REGEX = /range([[(])([^,]*),([^)\]]*)([)\]])/
35+
/*
36+
range(aaa,bbb)
37+
range[aaa,bbb)
38+
range(, x)
39+
range( y, ]
40+
*/
41+
enum RangeEdgeType { INCLUSIVE, EXCLUSIVE}
42+
function getRangeMatcherFn(specsMatch: RegExpMatchArray) {
43+
const RANGE_START_TYPE_IDX = 1
44+
const RANGE_START_IDX = 2
45+
const RANGE_END_IDX = 3
46+
const RANGE_END_TYPE_IDX = 4
47+
const rangeStartType: RangeEdgeType = specsMatch[RANGE_END_TYPE_IDX] === '(' ? RangeEdgeType.EXCLUSIVE : RangeEdgeType.INCLUSIVE
48+
const rangeStartValue: string = specsMatch[RANGE_START_IDX].trim()
49+
const rangeEndValue: string = specsMatch[RANGE_END_IDX].trim()
50+
const rangeEndType: RangeEdgeType = specsMatch[RANGE_END_TYPE_IDX] === ')' ? RangeEdgeType.EXCLUSIVE : RangeEdgeType.INCLUSIVE
51+
return (mdataValue: string): boolean => {
52+
let rangeStartMatched = true
53+
if (rangeStartValue) {
54+
if (rangeStartType === RangeEdgeType.INCLUSIVE) {
55+
rangeStartMatched = mdataValue >= rangeStartValue
56+
} else {
57+
rangeStartMatched = mdataValue > rangeStartValue
58+
}
59+
}
60+
let rangeEndMatched = true
61+
if (rangeEndValue) {
62+
if (rangeEndType === RangeEdgeType.INCLUSIVE) {
63+
rangeEndMatched = mdataValue <= rangeEndValue
64+
} else {
65+
rangeEndMatched = mdataValue < rangeEndValue
66+
}
67+
}
68+
69+
return rangeStartMatched && rangeEndMatched
70+
}
71+
}
72+
73+
const ValueMatchers: ValueMatcherSpec[] = [
74+
{ specPattern: VALUE_MATCHER_REGEX,
75+
valueMatcherFnFactory: getPlainValueMatcherFn,
76+
unitTestsId: 'value'
77+
}, {
78+
specPattern: RANGE_MATCHER_REGEX,
79+
valueMatcherFnFactory: getRangeMatcherFn,
80+
unitTestsId: 'range'
81+
}, {
82+
specPattern: 'any-value', // Artificially added for testing purposes
83+
valueMatcherFnFactory: () => (s: string) => true,
84+
unitTestsId: 'any-value-explicit'
85+
}
86+
]
87+
88+
export const tryParseAsMDataMatcherSpec = (s: string): MDataMatcherParseResult|undefined => {
89+
// Simplistic initial implementation of the idea, not closing the way to more complex implementations
90+
for (const matcherSpec of ValueMatchers) {
91+
if ('string' === typeof matcherSpec.specPattern && s.trim().startsWith(matcherSpec.specPattern)) {
92+
return {
93+
m: matcherSpec.valueMatcherFnFactory(matcherSpec.specPattern),
94+
remainder: s.substring(matcherSpec.specPattern.length).trim()
95+
}
96+
} else { // regexp
97+
const match = s.match(matcherSpec.specPattern)
98+
if (match) {
99+
return {
100+
m: matcherSpec.valueMatcherFnFactory(match),
101+
remainder: s.substring(match[0].length).trim()
102+
}
103+
}
104+
}
105+
}
106+
return undefined
107+
}
108+
109+
export const _unitTests = {
110+
matcherFn_value: ValueMatchers.find((it) => it.unitTestsId === 'value'),
111+
matcherFn_range: ValueMatchers.find((it) => it.unitTestsId === 'range'),
112+
matcherFn_anyValue: ValueMatchers.find((it) => it.unitTestsId === 'any-value-explicit'),
113+
}

src/custom-sort/sorting-spec-processor.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
NO_PRIORITY
5252
} from "./folder-matching-rules"
5353
import {MDataExtractor, tryParseAsMDataExtractorSpec} from "./mdata-extractors";
54+
import {MDataMatcher, tryParseAsMDataMatcherSpec} from "./mdata-matchers";
5455

5556
interface ProcessingContext {
5657
folderPath: string
@@ -103,7 +104,8 @@ export enum ProblemCode {
103104
InlineRegexInPrefixAndSuffix,
104105
DuplicateByNameSortSpecForFolder,
105106
EmptyFolderNameToMatch,
106-
InvalidOrEmptyFolderMatchingRegexp
107+
InvalidOrEmptyFolderMatchingRegexp,
108+
UnrecognizedMetadataValueMatcher
107109
}
108110

109111
const ContextFreeProblems = new Set<ProblemCode>([
@@ -280,6 +282,8 @@ const HideItemVerboseLexeme: string = '/--hide:'
280282

281283
const MetadataFieldIndicatorLexeme: string = 'with-metadata:'
282284

285+
const ValueMatcherLexeme: string = 'matching:'
286+
283287
const BookmarkedItemIndicatorLexeme: string = 'bookmarked:'
284288

285289
const IconIndicatorLexeme: string = 'with-icon:'
@@ -900,6 +904,8 @@ class AttrError {
900904
}
901905

902906
// Simplistic
907+
// TODO: accept spaces in the name, as already done for the parsing related to extractors for by-metadata
908+
// TODO: update unit tests with metadata names containing spaces
903909
const extractIdentifier = (text: string, defaultResult?: string): string | undefined => {
904910
const identifier: string = text.trim().split(' ')?.[0]?.trim()
905911
return identifier ? identifier : defaultResult
@@ -1765,13 +1771,29 @@ export class SortingSpecProcessor {
17651771
} // theoretically could match the sorting of matched files
17661772
} else {
17671773
if (theOnly.startsWith(MetadataFieldIndicatorLexeme)) {
1768-
const metadataFieldName: string | undefined = extractIdentifier(
1769-
theOnly.substring(MetadataFieldIndicatorLexeme.length),
1770-
DEFAULT_METADATA_FIELD_FOR_SORTING
1771-
)
1774+
let metadataFieldName: string|undefined = ''
1775+
let metadataMatcher: MDataMatcher|undefined = undefined
1776+
const metadataNameAndOptionalMatcherSpec = theOnly.substring(MetadataFieldIndicatorLexeme.length).trim() || undefined
1777+
if (metadataNameAndOptionalMatcherSpec) {
1778+
if (metadataNameAndOptionalMatcherSpec.indexOf(ValueMatcherLexeme) > -1) {
1779+
const metadataSpec = metadataNameAndOptionalMatcherSpec.split(ValueMatcherLexeme)
1780+
metadataFieldName = metadataSpec.shift()?.trim()
1781+
const metadataMatcherSpec = metadataSpec?.shift()?.trim()
1782+
const hasMetadataMatcher = metadataMatcherSpec ? tryParseAsMDataMatcherSpec(metadataMatcherSpec) : undefined
1783+
if (hasMetadataMatcher) {
1784+
metadataMatcher = hasMetadataMatcher.m
1785+
} else {
1786+
this.problem(ProblemCode.UnrecognizedMetadataValueMatcher, "unrecognized metadata value matcher specification")
1787+
return null;
1788+
}
1789+
} else {
1790+
metadataFieldName = metadataNameAndOptionalMatcherSpec
1791+
}
1792+
}
17721793
return {
17731794
type: CustomSortGroupType.HasMetadataField,
1774-
withMetadataFieldName: metadataFieldName,
1795+
withMetadataFieldName: metadataFieldName || DEFAULT_METADATA_FIELD_FOR_SORTING,
1796+
withMetadataMatcher: metadataMatcher,
17751797
filesOnly: spec.filesOnly,
17761798
foldersOnly: spec.foldersOnly,
17771799
matchFilenameWithExt: spec.matchFilenameWithExt

0 commit comments

Comments
 (0)