Skip to content

Commit de15f48

Browse files
authored
Merge pull request #172 from SebastianMC/171-poc-metadata-value-extractors-idea
#171 metadata value extractors
2 parents e4da4d1 + fcacaa3 commit de15f48

File tree

6 files changed

+279
-13
lines changed

6 files changed

+279
-13
lines changed

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {MDataExtractor} from "./mdata-extractors";
2+
13
export enum CustomSortGroupType {
24
Outsiders, // Not belonging to any of other groups
35
MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is
@@ -50,8 +52,10 @@ export enum CustomSortOrder {
5052
export interface RecognizedOrderValue {
5153
order: CustomSortOrder
5254
applyToMetadataField?: string
55+
metadataValueExtractor?: MDataExtractor
5356
secondaryOrder?: CustomSortOrder
5457
secondaryApplyToMetadataField?: string
58+
secondaryMetadataValueExtractor?: MDataExtractor
5559
}
5660

5761
export type NormalizerFn = (s: string) => string | null
@@ -70,9 +74,11 @@ export interface CustomSortGroup {
7074
exactSuffix?: string
7175
regexSuffix?: RegExpSpec
7276
order?: CustomSortOrder
73-
byMetadataField?: string // for 'by-metadata:' sorting if the order is by metadata alphabetical or reverse
77+
byMetadataField?: string // for 'by-metadata:' sorting if the order is by metadata alphabetical or reverse
78+
metadataFieldValueExtractor?: MDataExtractor // and its sorting value extractor
7479
secondaryOrder?: CustomSortOrder
7580
byMetadataFieldSecondary?: string // for 'by-metadata:' sorting if the order is by metadata alphabetical or reverse
81+
metadataFieldSecondaryValueExtractor?: MDataExtractor
7682
filesOnly?: boolean
7783
matchFilenameWithExt?: boolean
7884
foldersOnly?: boolean
@@ -87,8 +93,10 @@ export interface CustomSortSpec {
8793
targetFoldersPaths: Array<string> // For root use '/'
8894
defaultOrder?: CustomSortOrder
8995
defaultSecondaryOrder?: CustomSortOrder
90-
byMetadataField?: string // for 'by-metadata:' if the defaultOrder is by metadata
96+
byMetadataField?: string // for 'by-metadata:' if the defaultOrder is by metadata
97+
metadataFieldValueExtractor?: MDataExtractor // and its sorting value extractor
9198
byMetadataFieldSecondary?: string
99+
metadataFieldSecondaryValueExtractor?: MDataExtractor
92100
groups: Array<CustomSortGroup>
93101
groupsShadow?: Array<CustomSortGroup> // A shallow copy of groups, used at applying sorting for items in a folder.
94102
// Stores folder-specific values (e.g. macros expanded with folder-specific values)

src/custom-sort/custom-sort.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
BookmarksPluginInterface
3030
} from "../utils/BookmarksCorePluginSignature";
3131
import {CustomSortPluginAPI} from "../custom-sort-plugin";
32+
import {MDataExtractor} from "./mdata-extractors";
3233

3334
export interface ProcessingContext {
3435
// For internal transient use
@@ -365,13 +366,14 @@ export const matchGroupRegex = (theRegex: RegExpSpec, nameForMatching: string):
365366
return [false, undefined, undefined]
366367
}
367368

368-
const mdataValueFromFMCaches = (mdataFieldName: string, fc?: FrontMatterCache, fcPrio?: FrontMatterCache): any => {
369+
const mdataValueFromFMCaches = (mdataFieldName: string, mdataExtractor?: MDataExtractor, fc?: FrontMatterCache, fcPrio?: FrontMatterCache): any => {
369370
let prioValue = undefined
370371
if (fcPrio) {
371372
prioValue = fcPrio?.[mdataFieldName]
372373
}
373374

374-
return prioValue ?? fc?.[mdataFieldName]
375+
const rawMDataValue = prioValue ?? fc?.[mdataFieldName]
376+
return mdataExtractor ? mdataExtractor(rawMDataValue) : rawMDataValue
375377
}
376378

377379
export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec, ctx?: ProcessingContext): FolderItemForSorting {
@@ -568,13 +570,29 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
568570
}
569571
}
570572
if (isPrimaryOrderByMetadata) metadataValueToSortBy =
571-
mdataValueFromFMCaches (group?.byMetadataField || group?.withMetadataFieldName || DEFAULT_METADATA_FIELD_FOR_SORTING, frontMatterCache, prioFrontMatterCache)
573+
mdataValueFromFMCaches (
574+
group?.byMetadataField || group?.withMetadataFieldName || DEFAULT_METADATA_FIELD_FOR_SORTING,
575+
group?.metadataFieldValueExtractor,
576+
frontMatterCache,
577+
prioFrontMatterCache)
572578
if (isSecondaryOrderByMetadata) metadataValueSecondaryToSortBy =
573-
mdataValueFromFMCaches (group?.byMetadataFieldSecondary || group?.withMetadataFieldName || DEFAULT_METADATA_FIELD_FOR_SORTING, frontMatterCache, prioFrontMatterCache)
579+
mdataValueFromFMCaches (
580+
group?.byMetadataFieldSecondary || group?.withMetadataFieldName || DEFAULT_METADATA_FIELD_FOR_SORTING,
581+
group?.metadataFieldSecondaryValueExtractor,
582+
frontMatterCache,
583+
prioFrontMatterCache)
574584
if (isDerivedPrimaryByMetadata) metadataValueDerivedPrimaryToSortBy =
575-
mdataValueFromFMCaches (spec.byMetadataField || DEFAULT_METADATA_FIELD_FOR_SORTING, frontMatterCache, prioFrontMatterCache)
585+
mdataValueFromFMCaches (
586+
spec.byMetadataField || DEFAULT_METADATA_FIELD_FOR_SORTING,
587+
spec.metadataFieldValueExtractor,
588+
frontMatterCache,
589+
prioFrontMatterCache)
576590
if (isDerivedSecondaryByMetadata) metadataValueDerivedSecondaryToSortBy =
577-
mdataValueFromFMCaches (spec.byMetadataFieldSecondary || DEFAULT_METADATA_FIELD_FOR_SORTING, frontMatterCache, prioFrontMatterCache)
591+
mdataValueFromFMCaches (
592+
spec.byMetadataFieldSecondary || DEFAULT_METADATA_FIELD_FOR_SORTING,
593+
spec.metadataFieldSecondaryValueExtractor,
594+
frontMatterCache,
595+
prioFrontMatterCache)
578596
}
579597
}
580598
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
getNormalizedDate_NormalizerFn_for
3+
} from "./matchers";
4+
import {NormalizerFn} from "./custom-sort-types";
5+
6+
type ExtractorFn = (mdataValue: string) => string|undefined
7+
8+
interface DateExtractorSpec {
9+
specPattern: string|RegExp,
10+
extractorFn: ExtractorFn
11+
}
12+
13+
export interface MDataExtractor {
14+
(mdataValue: string): string|undefined
15+
}
16+
17+
export interface MDataExtractorParseResult {
18+
m: MDataExtractor
19+
remainder: string
20+
}
21+
22+
function getGenericPlainRegexpExtractorFn(extractorRegexp: RegExp, extractedValueNormalizer: NormalizerFn) {
23+
return (mdataValue: string): string | undefined => {
24+
const hasMatch = mdataValue?.match(extractorRegexp)
25+
if (hasMatch && hasMatch[0]) {
26+
return extractedValueNormalizer(hasMatch[0]) ?? undefined
27+
} else {
28+
return undefined
29+
}
30+
}
31+
}
32+
33+
const Extractors: DateExtractorSpec[] = [
34+
{ specPattern: 'date(dd/mm/yyyy)',
35+
extractorFn: getGenericPlainRegexpExtractorFn(
36+
new RegExp('\\d{2}/\\d{2}/\\d{4}'),
37+
getNormalizedDate_NormalizerFn_for('/', 0, 1, 2)
38+
)
39+
}, {
40+
specPattern: 'date(mm/dd/yyyy)',
41+
extractorFn: getGenericPlainRegexpExtractorFn(
42+
new RegExp('\\d{2}/\\d{2}/\\d{4}'),
43+
getNormalizedDate_NormalizerFn_for('/', 1, 0, 2)
44+
)
45+
}
46+
]
47+
48+
export const tryParseAsMDataExtractorSpec = (s: string): MDataExtractorParseResult|undefined => {
49+
// Simplistic initial implementation of the idea with hardcoded two extractors
50+
for (const extrSpec of Extractors) {
51+
if ('string' === typeof extrSpec.specPattern && s.trim().startsWith(extrSpec.specPattern)) {
52+
return {
53+
m: extrSpec.extractorFn,
54+
remainder: s.substring(extrSpec.specPattern.length).trim()
55+
}
56+
}
57+
}
58+
return undefined
59+
}
60+
61+
export const _unitTests = {
62+
extractorFnForDate_ddmmyyyy: Extractors.find((it) => it.specPattern === 'date(dd/mm/yyyy)')?.extractorFn!,
63+
extractorFnForDate_mmddyyyy: Extractors.find((it) => it.specPattern === 'date(mm/dd/yyyy)')?.extractorFn!,
64+
}

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ import {
3535
MATCH_CHILDREN_2_SUFFIX,
3636
NO_PRIORITY
3737
} from "./folder-matching-rules"
38+
import {
39+
MDataExtractor,
40+
tryParseAsMDataExtractorSpec
41+
} from "./mdata-extractors";
3842

3943
interface ProcessingContext {
4044
folderPath: string
@@ -114,6 +118,7 @@ interface CustomSortOrderAscDescPair {
114118
interface CustomSortOrderSpec {
115119
order: CustomSortOrder
116120
byMetadataField?: string
121+
metadataFieldExtractor?: MDataExtractor
117122
}
118123

119124
const MAX_SORT_LEVEL: number = 1
@@ -141,6 +146,8 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
141146

142147
const OrderByMetadataLexeme: string = 'by-metadata:'
143148

149+
const ValueExtractorLexeme: string = 'using-extractor:'
150+
144151
const OrderLevelsSeparator: string = ','
145152

146153
enum Attribute {
@@ -1090,8 +1097,10 @@ export class SortingSpecProcessor {
10901097
}
10911098
this.ctx.currentSpec.defaultOrder = (attr.value as RecognizedOrderValue).order
10921099
this.ctx.currentSpec.byMetadataField = (attr.value as RecognizedOrderValue).applyToMetadataField
1100+
this.ctx.currentSpec.metadataFieldValueExtractor = (attr.value as RecognizedOrderValue).metadataValueExtractor
10931101
this.ctx.currentSpec.defaultSecondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder
10941102
this.ctx.currentSpec.byMetadataFieldSecondary = (attr.value as RecognizedOrderValue).secondaryApplyToMetadataField
1103+
this.ctx.currentSpec.metadataFieldSecondaryValueExtractor = (attr.value as RecognizedOrderValue).secondaryMetadataValueExtractor
10951104
return true;
10961105
} else if (attr.nesting > 0) { // For now only distinguishing nested (indented) and not-nested (not-indented), the depth doesn't matter
10971106
if (!this.ctx.currentSpec || !this.ctx.currentSpecGroup) {
@@ -1105,8 +1114,10 @@ export class SortingSpecProcessor {
11051114
}
11061115
this.ctx.currentSpecGroup.order = (attr.value as RecognizedOrderValue).order
11071116
this.ctx.currentSpecGroup.byMetadataField = (attr.value as RecognizedOrderValue).applyToMetadataField
1117+
this.ctx.currentSpecGroup.metadataFieldValueExtractor = (attr.value as RecognizedOrderValue).metadataValueExtractor
11081118
this.ctx.currentSpecGroup.secondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder
11091119
this.ctx.currentSpecGroup.byMetadataFieldSecondary = (attr.value as RecognizedOrderValue).secondaryApplyToMetadataField
1120+
this.ctx.currentSpecGroup.metadataFieldSecondaryValueExtractor = (attr.value as RecognizedOrderValue).secondaryMetadataValueExtractor
11101121
return true;
11111122
}
11121123
}
@@ -1506,10 +1517,29 @@ export class SortingSpecProcessor {
15061517
orderSpec = hasDirectionPostfix ? orderSpec.substring(hasDirectionPostfix.lexeme.length).trim() : orderSpec
15071518

15081519
let metadataName: string|undefined
1520+
let metadataExtractor: MDataExtractor|undefined
15091521
if (orderSpec.startsWith(OrderByMetadataLexeme)) {
15101522
applyToMetadata = true
1511-
metadataName = orderSpec.substring(OrderByMetadataLexeme.length).trim() || undefined
1512-
orderSpec = '' // metadataName is unparsed, consumes the remainder string, even if malformed, e.g. with infix spaces
1523+
const metadataNameAndOptionalExtractorSpec = orderSpec.substring(OrderByMetadataLexeme.length).trim() || undefined
1524+
if (metadataNameAndOptionalExtractorSpec) {
1525+
if (metadataNameAndOptionalExtractorSpec.indexOf(ValueExtractorLexeme) > -1) {
1526+
const metadataSpec = metadataNameAndOptionalExtractorSpec.split(ValueExtractorLexeme)
1527+
metadataName = metadataSpec.shift()?.trim()
1528+
const metadataExtractorSpec = metadataSpec?.shift()?.trim()
1529+
const hasMetadataExtractor = metadataExtractorSpec ? tryParseAsMDataExtractorSpec(metadataExtractorSpec) : undefined
1530+
if (hasMetadataExtractor) {
1531+
metadataExtractor = hasMetadataExtractor.m
1532+
} else {
1533+
return new AttrError(`${orderNameForErrorMsg} sorting order contains unrecognized value extractor: >>> ${metadataExtractorSpec} <<<`)
1534+
}
1535+
orderSpec = '' // all consumed as metadata and extractor
1536+
} else {
1537+
metadataName = metadataNameAndOptionalExtractorSpec
1538+
orderSpec = '' // all consumed as metadata name
1539+
}
1540+
} else {
1541+
orderSpec = '' // no metadata name found
1542+
}
15131543
}
15141544

15151545
// check for any superfluous text
@@ -1562,7 +1592,8 @@ export class SortingSpecProcessor {
15621592
}
15631593
sortOrderSpec[level] = {
15641594
order: order!,
1565-
byMetadataField: metadataName
1595+
byMetadataField: metadataName,
1596+
metadataFieldExtractor: metadataExtractor
15661597
}
15671598
}
15681599
return sortOrderSpec
@@ -1573,8 +1604,10 @@ export class SortingSpecProcessor {
15731604
return recognized ? (recognized instanceof AttrError ? recognized : {
15741605
order: recognized[0].order,
15751606
applyToMetadataField: recognized[0].byMetadataField,
1607+
metadataValueExtractor: recognized[0].metadataFieldExtractor,
15761608
secondaryOrder: recognized[1]?.order,
1577-
secondaryApplyToMetadataField: recognized[1]?.byMetadataField
1609+
secondaryApplyToMetadataField: recognized[1]?.byMetadataField,
1610+
secondaryMetadataValueExtractor: recognized[1]?.metadataFieldExtractor
15781611
}) : null;
15791612
}
15801613

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {
2+
_unitTests
3+
} from '../../custom-sort/mdata-extractors'
4+
5+
describe('extractor for date(dd/mm/yyyy)', () => {
6+
const params = [
7+
// Positive
8+
['03/05/2019', '2019-05-03//'],
9+
['103/05/2019', '2019-05-03//'],
10+
['103/05/20193232', '2019-05-03//'],
11+
['99/99/9999', '9999-99-99//'],
12+
['00/00/0000', '0000-00-00//'],
13+
['Created at: 03/05/2019', '2019-05-03//'],
14+
['03/05/2019 | 22:00', '2019-05-03//'],
15+
['Created at: 03/05/2019 | 22:00', '2019-05-03//'],
16+
17+
// Negative
18+
['88-Dec-2012', undefined],
19+
['13-JANUARY-2012', undefined],
20+
['1 .1', undefined],
21+
['', undefined],
22+
['abc', undefined],
23+
['def-abc', undefined],
24+
['3/5/2019', undefined],
25+
];
26+
it.each(params)('>%s< should become %s', (s: string, out: string) => {
27+
expect(_unitTests.extractorFnForDate_ddmmyyyy(s)).toBe(out)
28+
})
29+
})
30+
31+
describe('extractor for date(mm/dd/yyyy)', () => {
32+
const params = [
33+
// Positive
34+
['03/05/2019', '2019-03-05//'],
35+
['103/05/2019', '2019-03-05//'],
36+
['103/05/20193232', '2019-03-05//'],
37+
['99/99/9999', '9999-99-99//'],
38+
['00/00/0000', '0000-00-00//'],
39+
['Created at: 03/05/2019', '2019-03-05//'],
40+
['03/05/2019 | 22:00', '2019-03-05//'],
41+
['Created at: 03/05/2019 | 22:00', '2019-03-05//'],
42+
43+
// Negative
44+
['88-Dec-2012', undefined],
45+
['13-JANUARY-2012', undefined],
46+
['1 .1', undefined],
47+
['', undefined],
48+
['abc', undefined],
49+
['def-abc', undefined],
50+
['3/5/2019', undefined],
51+
];
52+
it.each(params)('>%s< should become %s', (s: string, out: string) => {
53+
expect(_unitTests.extractorFnForDate_mmddyyyy(s)).toBe(out)
54+
})
55+
})

0 commit comments

Comments
 (0)