diff --git a/datacore.api.md b/datacore.api.md index ba6dc5aca..6efeb4858 100644 --- a/datacore.api.md +++ b/datacore.api.md @@ -117,8 +117,8 @@ export class Canvas implements Linkable, File_2, Linkbearing, Taggable, Indexabl get $file(): string; // (undocumented) get $id(): string; - // (undocumented) $infields: Record; + $infieldsMulti: Record; get $link(): Link; // (undocumented) $links: Link[]; @@ -193,8 +193,8 @@ export class CanvasTextCard extends BaseCanvasCard implements Linkbearing, Tagga $frontmatter?: Record; // (undocumented) $id: string; - // (undocumented) $infields: Record; + $infieldsMulti: Record; // (undocumented) $links: Link[]; // (undocumented) @@ -610,6 +610,7 @@ export namespace Expressions { export namespace Extractors { export function frontmatter(front: (object: T) => Record | undefined): FieldExtractor; export function inlineFields(inlineMap: (object: T) => Record | undefined): FieldExtractor; + export function inlineFieldsMulti(inlineMap: (object: T) => Record | undefined): FieldExtractor; export function intrinsics(except?: Set): FieldExtractor; export function merge(...extractors: FieldExtractor[]): FieldExtractor; } @@ -941,6 +942,9 @@ export interface InlineField { wrapping?: string; } +// @public +export type InlineFieldList = InlineField[]; + // @public export type Intent = "error" | "warn" | "info" | "success"; @@ -955,6 +959,24 @@ export const INTENT_CLASSES: Record; // @internal export function jsonFrontmatterEntry(raw: FrontmatterEntry): JsonFrontmatterEntry; +// @public +export interface JsonInlineField { + key: string; + position: { + line: number; + start: number; + startValue: number; + end: number; + }; + raw: string; + // Warning: (ae-forgotten-export) The symbol "JsonLiteral" needs to be exported by the entry point index.d.ts + value: JsonLiteral; + wrapping?: string; +} + +// @public +export type JsonInlineFieldList = JsonInlineField[]; + // @public export interface LambdaExpression { arguments: string[]; @@ -1124,6 +1146,7 @@ export class MarkdownBlock implements Indexable, Linkbearing, Taggable, Fieldbea // (undocumented) $id: string; $infields: Record; + $infieldsMulti: Record; get $link(): Link | undefined; $links: Link[]; $ordinal: number; @@ -1275,6 +1298,7 @@ export class MarkdownListItem implements Indexable, Linkbearing, Taggable, Field // (undocumented) $id: string; $infields: Record; + $infieldsMulti: Record; get $line(): number; get $lineCount(): number; $links: Link[]; @@ -1323,6 +1347,7 @@ export class MarkdownPage implements File_2, Linkbearing, Taggable, Indexable, F // (undocumented) get $id(): string; $infields: Record; + $infieldsMulti: Record; get $lineCount(): number; get $link(): Link; $links: Link[]; @@ -1366,6 +1391,7 @@ export class MarkdownSection implements Indexable, Taggable, Linkable, Linkbeari // (undocumented) $id: string; $infields: Record; + $infieldsMulti: Record; $level: number; get $lineCount(): number; get $link(): Link; diff --git a/manifest.json b/manifest.json index ffe285030..740f84f0c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "id": "datacore", "name": "Datacore", - "version": "0.1.28", + "version": "0.1.29", "minAppVersion": "1.4.11", "description": "Reactive query engine backed by Javascript or a custom query language.", "author": "Michael Brenan", "authorUrl": "https://github.com/blacksmithgu", "isDesktopOnly": false -} +} \ No newline at end of file diff --git a/src/expression/field.ts b/src/expression/field.ts index 1c8ee5124..7696508f4 100644 --- a/src/expression/field.ts +++ b/src/expression/field.ts @@ -3,7 +3,7 @@ */ import { DataObject, Literal, Literals } from "expression/literal"; import { Indexable } from "../index/types/indexable"; -import { InlineField } from "index/import/inline-field"; +import { InlineField, InlineFieldList } from "index/import/inline-field"; import { FrontmatterEntry } from "index/types/markdown"; /** The source of a field, used when determining what files to overwrite and how. */ @@ -221,6 +221,70 @@ export namespace Extractors { }; } + /** Field extractor which shows all inline fields from a multi-value inline-field map. + * Values are always lists in appearance order. + */ + export function inlineFieldsMulti( + inlineMap: (object: T) => Record | undefined + ): FieldExtractor { + return (object: T, key?: string) => { + const map = inlineMap(object); + if (!map) return []; + + function aggregate(fields: InlineFieldList): { key: string; value: Literal; raw?: string; line: number } { + if (fields.length == 0) return { key: "", value: [], raw: "", line: 0 }; + const first = fields[0]; + return { + key: first.key, + value: fields.map((f) => f.value), + raw: fields.map((f) => f.raw).join(", "), + line: first.position.line, + }; + } + + if (key == null) { + const out: Field[] = []; + for (const fields of Object.values(map)) { + const agg = aggregate(fields); + if (!agg.key) continue; + + out.push({ + key: agg.key.toLowerCase(), + value: agg.value, + raw: agg.raw, + provenance: { + type: "inline-field", + file: object.$file!, + line: agg.line, + key: agg.key, + revision: object.$revision ?? 0, + }, + }); + } + return out; + } else { + key = key.toLowerCase(); + if (!(key in map)) return []; + + const agg = aggregate(map[key]); + return [ + { + key, + value: agg.value, + raw: agg.raw, + provenance: { + type: "inline-field", + file: object.$file!, + line: agg.line, + key: agg.key, + revision: object.$revision ?? 0, + }, + }, + ]; + } + }; + } + /** Merge multiple field extractors into one; if multiple extractors produce identical keys, keys from the earlier extractor will be preferred. */ export function merge(...extractors: FieldExtractor[]): FieldExtractor { return (object: T, key?: string) => { diff --git a/src/index.ts b/src/index.ts index 46b41e944..e1a96a617 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,7 @@ export type * from "index/types/index-query"; export type * from "index/types/files"; export type * from "index/types/canvas"; -export { InlineField } from "index/import/inline-field"; +export type { InlineField, InlineFieldList, JsonInlineField, JsonInlineFieldList } from "index/import/inline-field"; export { SearchResult } from "index/datastore"; export { CardPos, CardDimensions } from "index/types/json/canvas"; diff --git a/src/index/import/canvas.ts b/src/index/import/canvas.ts index 764b699a0..f1df0dd6c 100644 --- a/src/index/import/canvas.ts +++ b/src/index/import/canvas.ts @@ -32,7 +32,10 @@ export function canvasImport( canvas.card(card); for (const tag in metadata.tags) canvas.metadata.tag(tag); for (const link of metadata.links ?? []) canvas.metadata.link(link); - for (const field of iterateInlineFields(lines)) canvas.metadata.inlineField(field); + for (const field of iterateInlineFields(lines)) { + canvas.metadata.inlineField(field); + canvas.metadata.inlineFieldMulti(field); + } } else { const card = new CanvasCardData(path, c.id, c); canvas.card(card); @@ -50,7 +53,7 @@ abstract class AbstractCanvasCardData { public path: string, public id: string, protected nodeJson: CanvasTextData | CanvasLinkData | CanvasFileData - ) {} + ) { } public build(): JsonBaseCanvasCard { return { @@ -93,6 +96,7 @@ export class CanvasCardData extends AbstractCanvasCardData { return { ...(super.build() as JsonBaseCanvasCard), $infields: this.metadata.finishInlineFields(), + $infieldsMulti: this.metadata.finishInlineFieldsMulti(), $frontmatter: this.frontmatter, $sections: this.sections.map((x) => x.build()), $tags: this.metadata.finishTags(), @@ -120,7 +124,7 @@ export class CanvasData { public cards: CanvasCardData[] = []; public metadata: Metadata = new Metadata(); - public constructor(public path: string, public stats: FileStats) {} + public constructor(public path: string, public stats: FileStats) { } public card(d: CanvasCardData): CanvasCardData { this.cards.push(d); @@ -133,6 +137,7 @@ export class CanvasData { $ctime: this.stats.ctime, $mtime: this.stats.mtime, $infields: this.metadata.finishInlineFields(), + $infieldsMulti: this.metadata.finishInlineFieldsMulti(), $links: this.metadata.finishLinks(), $tags: this.metadata.finishTags(), $path: this.path, diff --git a/src/index/import/inline-field.ts b/src/index/import/inline-field.ts index 51658dc20..38699e250 100644 --- a/src/index/import/inline-field.ts +++ b/src/index/import/inline-field.ts @@ -68,16 +68,35 @@ export interface JsonInlineField { wrapping?: string; } +/** + * Inline field values may appear multiple times with the same key. When that happens, callers may represent + * them as a list of fields in appearance order. + */ +export type InlineFieldList = InlineField[]; + +/** JSON, serializable representation of an inline field list (size >= 1 in normal usage). */ +export type JsonInlineFieldList = JsonInlineField[]; + /** Convert an inline field to a JSON format. */ export function jsonInlineField(field: InlineField): JsonInlineField { return Object.assign({}, field, { value: JsonConversion.json(field.value) }); } +/** Convert an inline field list to a JSON format. */ +export function jsonInlineFieldList(fields: InlineFieldList): JsonInlineFieldList { + return fields.map(jsonInlineField); +} + /** Convert a JSON inline field back to a regular field. */ export function valueInlineField(field: JsonInlineField): InlineField { return Object.assign({}, field, { value: JsonConversion.value(field.value) }); } +/** Convert a JSON inline field list back to a regular field representation. */ +export function valueInlineFieldList(fields: JsonInlineFieldList): InlineFieldList { + return fields.map(valueInlineField); +} + export function asInlineField(local: LocalInlineField, lineno: number): InlineField; export function asInlineField(local: LocalInlineField[], lineno: number): InlineField[]; /** Convert a local inline field into a full inline field by performing parsing and adding the correct line number. */ diff --git a/src/index/import/markdown.ts b/src/index/import/markdown.ts index 46aa56127..83334ad34 100644 --- a/src/index/import/markdown.ts +++ b/src/index/import/markdown.ts @@ -6,11 +6,13 @@ import BTree from "sorted-btree"; import { InlineField, JsonInlineField, + JsonInlineFieldList, asInlineField, extractFullLineField, extractInlineFields, extractSpecialTaskFields, jsonInlineField, + jsonInlineFieldList, } from "./inline-field"; import { JsonMarkdownBlock, @@ -251,17 +253,23 @@ export function markdownSourceImport( for (const field of iterateInlineFields(lines)) { const line = field.position.line; markdownMetadata.inlineField(field); + markdownMetadata.inlineFieldMulti(field); lookup(line, sections)?.metadata.inlineField(field); + lookup(line, sections)?.metadata.inlineFieldMulti(field); lookup(line, blocks)?.metadata.inlineField(field); + lookup(line, blocks)?.metadata.inlineFieldMulti(field); lookup(line, listItems)?.metadata.inlineField(field); + lookup(line, listItems)?.metadata.inlineFieldMulti(field); } for (const item of listItems.values()) { for (let lineno = item.start; lineno < item.end; lineno++) { const taskInlineFields = extractSpecialTaskFields(lines[lineno]); for (const field of taskInlineFields) { - item.metadata.inlineField(asInlineField(field, lineno)); + const full = asInlineField(field, lineno); + item.metadata.inlineField(full); + item.metadata.inlineFieldMulti(full); } } } @@ -369,7 +377,10 @@ export function splitFrontmatterTagOrAlias(data: unknown, on: RegExp): string[] export class Metadata { public tags: Set = new Set(); public links: Link[] = []; + /** Map of all distinct inline fields (original behavior: first occurrence wins). */ public inlineFields: Record = {}; + /** Map of all inline fields; always stores a list in appearance order. */ + public inlineFieldsMulti: Record = {}; /** Add a tag to the metadata. */ public tag(tag: string) { @@ -385,11 +396,19 @@ export class Metadata { /** Add an inline field to the metadata. */ public inlineField(field: InlineField) { const lower = field.key.toLowerCase(); - if (Object.keys(this.inlineFields).some((key) => key.toLowerCase() == lower)) return; + if (Object.keys(this.inlineFields).some((key) => key.toLowerCase() == lower)) return; this.inlineFields[lower] = field; } + /** Add an inline field to the multi-value map (always appends). */ + public inlineFieldMulti(field: InlineField) { + const lower = field.key.toLowerCase(); + const existing = this.inlineFieldsMulti[lower]; + if (existing) existing.push(field); + else this.inlineFieldsMulti[lower] = [field]; + } + /** Return a list of unique added tags. */ public finishTags(): string[] { return Array.from(this.tags); @@ -404,6 +423,11 @@ export class Metadata { public finishInlineFields(): Record { return mapObjectValues(this.inlineFields, jsonInlineField); } + + /** Return a list of JSON-serialized multi inline fields (always lists). */ + public finishInlineFieldsMulti(): Record { + return mapObjectValues(this.inlineFieldsMulti, jsonInlineFieldList); + } } /** Convienent utility for constructing page objects. */ @@ -415,7 +439,7 @@ export class PageData { public metadata: Metadata, public sections: SectionData[], public frontmatter?: Record - ) {} + ) { } public build(): JsonMarkdownPage { return { @@ -428,6 +452,7 @@ export class PageData { $tags: this.metadata.finishTags(), $links: this.metadata.finishLinks(), $infields: this.metadata.finishInlineFields(), + $infieldsMulti: this.metadata.finishInlineFieldsMulti(), $sections: this.sections.map((x) => x.build()), $frontmatter: this.frontmatter, }; @@ -445,7 +470,7 @@ export class SectionData { public title: string, public level: number, public ordinal: number - ) {} + ) { } public block(block: BlockData) { this.blocks.push(block); @@ -458,6 +483,7 @@ export class SectionData { $level: this.level, $tags: this.metadata.finishTags(), $infields: this.metadata.finishInlineFields(), + $infieldsMulti: this.metadata.finishInlineFieldsMulti(), $links: this.metadata.finishLinks(), $position: { start: this.start, end: this.end }, $blocks: this.blocks.map((block) => block.build()), @@ -471,13 +497,14 @@ export class ListBlockData { public metadata: Metadata = new Metadata(); public items: ListItemData[] = []; - public constructor(public start: number, public end: number, public ordinal: number, public blockId?: string) {} + public constructor(public start: number, public end: number, public ordinal: number, public blockId?: string) { } public build(): JsonMarkdownListBlock { return { $ordinal: this.ordinal, $position: { start: this.start, end: this.end }, $infields: this.metadata.finishInlineFields(), + $infieldsMulti: this.metadata.finishInlineFieldsMulti(), $tags: this.metadata.finishTags(), $links: this.metadata.finishLinks(), $type: "list", @@ -501,7 +528,7 @@ export class CodeblockData { public contentStart: number, public contentEnd: number, public blockId?: string - ) {} + ) { } public build(): JsonMarkdownCodeblock { return { @@ -509,6 +536,7 @@ export class CodeblockData { $ordinal: this.ordinal, $position: { start: this.start, end: this.end }, $infields: this.metadata.finishInlineFields(), + $infieldsMulti: this.metadata.finishInlineFieldsMulti(), $tags: this.metadata.finishTags(), $links: this.metadata.finishLinks(), $blockId: this.blockId, @@ -530,7 +558,7 @@ export class DatablockData { public ordinal: number, public data: Record, public blockId?: string - ) {} + ) { } public build(): JsonMarkdownDatablock { return { @@ -538,6 +566,7 @@ export class DatablockData { $ordinal: this.ordinal, $position: { start: this.start, end: this.end }, $infields: this.metadata.finishInlineFields(), + $infieldsMulti: this.metadata.finishInlineFieldsMulti(), $tags: this.metadata.finishTags(), $links: this.metadata.finishLinks(), $blockId: this.blockId, @@ -556,7 +585,7 @@ export class BaseBlockData { public ordinal: number, public type: string, public blockId?: string - ) {} + ) { } public build(): JsonMarkdownBlock { return { @@ -564,6 +593,7 @@ export class BaseBlockData { $ordinal: this.ordinal, $position: { start: this.start, end: this.end }, $infields: this.metadata.finishInlineFields(), + $infieldsMulti: this.metadata.finishInlineFieldsMulti(), $tags: this.metadata.finishTags(), $links: this.metadata.finishLinks(), $blockId: this.blockId, @@ -586,7 +616,7 @@ export class ListItemData { public blockId?: string, public status?: string, public text?: string - ) {} + ) { } public build(): JsonMarkdownListItem { return { @@ -596,6 +626,7 @@ export class ListItemData { $elements: this.elements.map((element) => element.build()), $type: this.status ? "task" : "list", $infields: this.metadata.finishInlineFields(), + $infieldsMulti: this.metadata.finishInlineFieldsMulti(), $tags: this.metadata.finishTags(), $links: this.metadata.finishLinks(), $status: this.status, diff --git a/src/index/types/canvas.ts b/src/index/types/canvas.ts index 1cf66ddad..204fecb58 100644 --- a/src/index/types/canvas.ts +++ b/src/index/types/canvas.ts @@ -23,7 +23,14 @@ import { normalizeLinks, valueFrontmatterEntry, } from "./markdown"; -import { InlineField, jsonInlineField, valueInlineField } from "index/import/inline-field"; +import { + InlineField, + InlineFieldList, + jsonInlineField, + jsonInlineFieldList, + valueInlineField, + valueInlineFieldList, +} from "index/import/inline-field"; import { File } from "index/types/indexable"; import { mapObjectValues } from "utils/data"; import { Literal } from "expression/literal"; @@ -63,7 +70,10 @@ export class Canvas implements Linkable, File, Linkbearing, Taggable, Indexable, $size: number = 0; $tags: string[]; $links: Link[]; + /** Map of distinct inline fields (original behavior: first occurrence wins). */ $infields: Record; + /** Map of all inline fields; values are always lists in appearance order. */ + $infieldsMulti: Record; private constructor(init: Partial) { Object.assign(this, init); @@ -86,6 +96,7 @@ export class Canvas implements Linkable, File, Linkbearing, Taggable, Indexable, $links: this.$links, $path: this.$path, $infields: mapObjectValues(this.$infields, jsonInlineField), + $infieldsMulti: mapObjectValues(this.$infieldsMulti, jsonInlineFieldList), $tags: this.$tags, }; } @@ -116,6 +127,11 @@ export class Canvas implements Linkable, File, Linkbearing, Taggable, Indexable, $infields: raw.$infields ? mapObjectValues(raw.$infields, (field) => normalizeLinks(valueInlineField(field), normalizer)) : {}, + $infieldsMulti: raw.$infieldsMulti + ? mapObjectValues(raw.$infieldsMulti, (fields) => + normalizeLinks(valueInlineFieldList(fields), normalizer) + ) + : {}, $tags: raw.$tags, }); } @@ -129,7 +145,7 @@ export class Canvas implements Linkable, File, Linkbearing, Taggable, Indexable, export namespace Canvas { export interface Typed extends Omit>, - TypedFieldbearing {} + TypedFieldbearing { } } /** All supported canvas card types. */ @@ -186,7 +202,10 @@ export class CanvasTextCard extends BaseCanvasCard implements Linkbearing, Tagga $title: string; $parent?: Indexable; $revision?: number; + /** Map of distinct inline fields (original behavior: first occurrence wins). */ $infields: Record; + /** Map of all inline fields; values are always lists in appearance order. */ + $infieldsMulti: Record; $frontmatter?: Record; $dimensions: CardDimensions; @@ -207,7 +226,8 @@ export class CanvasTextCard extends BaseCanvasCard implements Linkbearing, Tagga /** @internal */ public json(): JsonCanvasTextCard { return Object.assign(super.json(), { - $infields: this.$infields, + $infields: mapObjectValues(this.$infields, jsonInlineField), + $infieldsMulti: mapObjectValues(this.$infieldsMulti, jsonInlineFieldList), $links: this.$links, $tags: this.$tags, $type: "text-card", @@ -231,6 +251,11 @@ export class CanvasTextCard extends BaseCanvasCard implements Linkbearing, Tagga $infields: raw.$infields ? mapObjectValues(raw.$infields, (field) => normalizeLinks(valueInlineField(field), normalizer)) : {}, + $infieldsMulti: raw.$infieldsMulti + ? mapObjectValues(raw.$infieldsMulti, (fields) => + normalizeLinks(valueInlineFieldList(fields), normalizer) + ) + : {}, $tags: raw.$tags, }); } @@ -246,7 +271,7 @@ export class CanvasTextCard extends BaseCanvasCard implements Linkbearing, Tagga export namespace CanvasTextCard { export interface Typed extends Omit>, - TypedFieldbearing {} + TypedFieldbearing { } } /** Canvas card that is just a file embedding. */ diff --git a/src/index/types/json/canvas.ts b/src/index/types/json/canvas.ts index 24ab6f23d..0a562d936 100644 --- a/src/index/types/json/canvas.ts +++ b/src/index/types/json/canvas.ts @@ -1,5 +1,5 @@ import { JsonLink } from "expression/link"; -import { JsonInlineField } from "index/import/inline-field"; +import { JsonInlineField, JsonInlineFieldList } from "index/import/inline-field"; import { JsonFrontmatterEntry, JsonMarkdownSection } from "./markdown"; import { CachedMetadata, EmbedCache } from "obsidian"; @@ -42,6 +42,7 @@ export interface JsonCanvas { $links: JsonLink[]; /** All inline fields in the canvas. */ $infields: Record; + $infieldsMulti?: Record; } /** Common metadata for all canvas cards. */ @@ -67,6 +68,7 @@ export interface JsonCanvasTextCard extends JsonBaseCanvasCard { $links: JsonLink[]; $infields: Record; + $infieldsMulti?: Record; $sections: JsonMarkdownSection[]; $frontmatter?: Record; } diff --git a/src/index/types/json/markdown.ts b/src/index/types/json/markdown.ts index 41347ce95..e48be8522 100644 --- a/src/index/types/json/markdown.ts +++ b/src/index/types/json/markdown.ts @@ -3,7 +3,7 @@ // They only reference natively serializable JSON types - lists, maps/records, numbers, // and strings. -import { JsonInlineField } from "index/import/inline-field"; +import { JsonInlineField, JsonInlineFieldList } from "index/import/inline-field"; import { JsonLiteral } from "./common"; import { JsonLink } from "expression/link"; @@ -45,8 +45,10 @@ export interface JsonMarkdownPage { $links: JsonLink[]; /** Frontmatter values in the file, if present. Maps lower case frontmatter key to entry. */ $frontmatter?: Record; - /** Map of all distinct inline fields in the document. Maps lower case key name to full metadata. */ + /** Map of all distinct inline fields in the document (original behavior: first occurrence wins). */ $infields: Record; + /** Map of all inline fields in the document; values are always lists in appearance order. */ + $infieldsMulti?: Record; /** * All child markdown sections of this markdown file. The initial section before any content is special and is @@ -73,6 +75,8 @@ export interface JsonMarkdownSection { $blocks: JsonMarkdownBlock[]; /** Map of all distinct inline fields in the document, from key name to metadata. */ $infields: Record; + /** Map of all inline fields in the section; values are always lists in appearance order. */ + $infieldsMulti?: Record; } export interface JsonMarkdownBlock { @@ -86,6 +90,8 @@ export interface JsonMarkdownBlock { $links: JsonLink[]; /** Map of all distinct inline fields in the document, from key name to metadata. */ $infields: Record; + /** Map of all inline fields in the block; values are always lists in appearance order. */ + $infieldsMulti?: Record; /** If present, the distinct block ID for this block. */ $blockId?: string; /** The type of block - paragraph, list, and so on. */ @@ -128,6 +134,8 @@ export interface JsonMarkdownListItem { $tags: string[]; /** Map of all distinct inline fields in the document, from key name to metadata. */ $infields: Record; + /** Map of all inline fields in the list item; values are always lists in appearance order. */ + $infieldsMulti?: Record; /** All links in the file. */ $links: JsonLink[]; /** The block ID of this list item if present. */ diff --git a/src/index/types/markdown.ts b/src/index/types/markdown.ts index 5b509dfa5..bcd72008e 100644 --- a/src/index/types/markdown.ts +++ b/src/index/types/markdown.ts @@ -16,7 +16,14 @@ import { } from "index/types/indexable"; import { DateTime } from "luxon"; import { Extractors, FIELDBEARING_TYPE, Field, FieldExtractor, Fieldbearing } from "../../expression/field"; -import { InlineField, jsonInlineField, valueInlineField } from "index/import/inline-field"; +import { + InlineField, + InlineFieldList, + jsonInlineField, + jsonInlineFieldList, + valueInlineField, + valueInlineFieldList, +} from "index/import/inline-field"; import { LineSpan, JsonMarkdownPage, @@ -55,6 +62,8 @@ export class MarkdownPage implements File, Linkbearing, Taggable, Indexable, Fie $frontmatter?: Record; /** Map of all distinct inline fields in the document. Maps lower case key name to full metadata. */ $infields: Record; + /** Map of all inline fields in the document; values are always lists in appearance order. */ + $infieldsMulti: Record; /** The path this file exists at. */ $path: string; @@ -88,6 +97,9 @@ export class MarkdownPage implements File, Linkbearing, Taggable, Indexable, Fie ? mapObjectValues(raw.$frontmatter, (fm) => normalizeLinks(valueFrontmatterEntry(fm), normalizer)) : undefined, $infields: mapObjectValues(raw.$infields, (field) => normalizeLinks(valueInlineField(field), normalizer)), + $infieldsMulti: raw.$infieldsMulti + ? mapObjectValues(raw.$infieldsMulti, (fields) => normalizeLinks(valueInlineFieldList(fields), normalizer)) + : {}, $ctime: DateTime.fromMillis(raw.$ctime), $mtime: DateTime.fromMillis(raw.$mtime), $extension: raw.$extension, @@ -139,6 +151,7 @@ export class MarkdownPage implements File, Linkbearing, Taggable, Indexable, Fie $path: this.$path, $frontmatter: this.$frontmatter ? mapObjectValues(this.$frontmatter, jsonFrontmatterEntry) : undefined, $infields: mapObjectValues(this.$infields, jsonInlineField), + $infieldsMulti: mapObjectValues(this.$infieldsMulti, jsonInlineFieldList), $ctime: this.$ctime.toMillis(), $mtime: this.$mtime.toMillis(), $extension: this.$extension, @@ -161,7 +174,7 @@ export class MarkdownPage implements File, Linkbearing, Taggable, Indexable, Fie export namespace MarkdownPage { export interface Typed extends Omit>, - TypedValuebearing {} + TypedValuebearing { } } /** @public A single markdown section inside of a page. */ @@ -191,6 +204,8 @@ export class MarkdownSection implements Indexable, Taggable, Linkable, Linkbeari $blocks: MarkdownBlock[]; /** Map of all distinct inline fields in the document, from key name to metadata. */ $infields: Record; + /** Map of all inline fields in the section; values are always lists in appearance order. */ + $infieldsMulti: Record; /** @internal Convert raw markdown section data to the appropriate class. */ static from(raw: JsonMarkdownSection, file: string, normalizer: LinkNormalizer = NOOP_NORMALIZER): MarkdownSection { @@ -206,6 +221,9 @@ export class MarkdownSection implements Indexable, Taggable, Linkable, Linkbeari $links: raw.$links.map((l) => normalizer(Link.fromObject(l))), $blocks: blocks, $infields: mapObjectValues(raw.$infields, (i) => normalizeLinks(valueInlineField(i), normalizer)), + $infieldsMulti: raw.$infieldsMulti + ? mapObjectValues(raw.$infieldsMulti, (fields) => normalizeLinks(valueInlineFieldList(fields), normalizer)) + : {}, }); } @@ -253,6 +271,7 @@ export class MarkdownSection implements Indexable, Taggable, Linkable, Linkbeari $links: this.$links.map((link) => link.toObject()), $blocks: this.$blocks.map((block) => block.json()), $infields: mapObjectValues(this.$infields, jsonInlineField), + $infieldsMulti: mapObjectValues(this.$infieldsMulti, jsonInlineFieldList), }; } @@ -273,7 +292,7 @@ export class MarkdownSection implements Indexable, Taggable, Linkable, Linkbeari export namespace MarkdownSection { export interface Typed extends Omit>, - TypedValuebearing {} + TypedValuebearing { } } /** @public Base class for all markdown blocks. */ @@ -295,6 +314,8 @@ export class MarkdownBlock implements Indexable, Linkbearing, Taggable, Fieldbea $links: Link[]; /** Map of all distinct inline fields in the document, from key name to metadata. */ $infields: Record; + /** Map of all inline fields in the block; values are always lists in appearance order. */ + $infieldsMulti: Record; /** If present, the distinct block ID for this block. */ $blockId?: string; /** The type of block - paragraph, list, and so on. */ @@ -318,6 +339,9 @@ export class MarkdownBlock implements Indexable, Linkbearing, Taggable, Fieldbea $tags: object.$tags, $links: object.$links.map((l) => normalizer(Link.fromObject(l))), $infields: mapObjectValues(object.$infields, (i) => normalizeLinks(valueInlineField(i), normalizer)), + $infieldsMulti: object.$infieldsMulti + ? mapObjectValues(object.$infieldsMulti, (fields) => normalizeLinks(valueInlineFieldList(fields), normalizer)) + : {}, $blockId: object.$blockId, $type: object.$type, }); @@ -355,6 +379,7 @@ export class MarkdownBlock implements Indexable, Linkbearing, Taggable, Fieldbea $tags: this.$tags, $links: this.$links.map((l) => l.toObject()), $infields: mapObjectValues(this.$infields, jsonInlineField), + $infieldsMulti: mapObjectValues(this.$infieldsMulti, jsonInlineFieldList), $blockId: this.$blockId, $type: this.$type, }; @@ -375,7 +400,7 @@ export class MarkdownBlock implements Indexable, Linkbearing, Taggable, Fieldbea export namespace MarkdownBlock { export interface Typed extends Omit>, - TypedValuebearing {} + TypedValuebearing { } } /** @public Special block for markdown lists (of either plain list entries or tasks). */ @@ -404,6 +429,9 @@ export class MarkdownListBlock extends MarkdownBlock implements Taggable, Linkbe $tags: object.$tags, $links: object.$links.map((l) => normalizer(Link.fromObject(l))), $infields: mapObjectValues(object.$infields, (i) => normalizeLinks(valueInlineField(i), normalizer)), + $infieldsMulti: object.$infieldsMulti + ? mapObjectValues(object.$infieldsMulti, (fields) => normalizeLinks(valueInlineFieldList(fields), normalizer)) + : {}, $blockId: object.$blockId, $elements: elements, $type: "list", @@ -426,7 +454,7 @@ export class MarkdownListBlock extends MarkdownBlock implements Taggable, Linkbe export namespace MarkdownListBlock { export interface Typed extends Omit>, - TypedValuebearing {} + TypedValuebearing { } } /** @public A block containing markdown code. */ @@ -459,7 +487,10 @@ export class MarkdownCodeblock extends MarkdownBlock implements Indexable, Field $languages: object.$languages, $links: object.$links.map((link) => normalizer(Link.fromObject(link))), $tags: object.$tags, - $infields: mapObjectValues(object.$infields, valueInlineField), + $infields: mapObjectValues(object.$infields, (i) => normalizeLinks(valueInlineField(i), normalizer)), + $infieldsMulti: object.$infieldsMulti + ? mapObjectValues(object.$infieldsMulti, (fields) => normalizeLinks(valueInlineFieldList(fields), normalizer)) + : {}, $contentPosition: object.$contentPosition, $style: object.$style, }); @@ -502,7 +533,7 @@ export class MarkdownCodeblock extends MarkdownBlock implements Indexable, Field export namespace MarkdownCodeblock { export interface Typed extends Omit>, - TypedValuebearing {} + TypedValuebearing { } } /** @public A data-annotated YAML codeblock. */ @@ -578,7 +609,7 @@ export class MarkdownDatablock extends MarkdownBlock implements Indexable, Field export namespace MarkdownDatablock { export interface Typed extends Omit>, - TypedValuebearing {} + TypedValuebearing { } } /** @public A specific list item in a list. */ @@ -600,6 +631,8 @@ export class MarkdownListItem implements Indexable, Linkbearing, Taggable, Field $tags: string[]; /** Map of all distinct inline fields in the document, from key name to metadata. */ $infields: Record; + /** Map of all inline fields in the list item; values are always lists in appearance order. */ + $infieldsMulti: Record; /** All links in the file. */ $links: Link[]; /** The block ID of this list item if present. */ @@ -636,6 +669,9 @@ export class MarkdownListItem implements Indexable, Linkbearing, Taggable, Field $type: object.$type, $tags: object.$tags, $infields: mapObjectValues(object.$infields, (i) => normalizeLinks(valueInlineField(i), normalizer)), + $infieldsMulti: object.$infieldsMulti + ? mapObjectValues(object.$infieldsMulti, (fields) => normalizeLinks(valueInlineFieldList(fields), normalizer)) + : {}, $links: object.$links.map((l) => normalizer(Link.fromObject(l))), $blockId: object.$blockId, $parentLine: object.$parentLine, @@ -697,6 +733,7 @@ export class MarkdownListItem implements Indexable, Linkbearing, Taggable, Field $type: this.$type, $tags: this.$tags, $infields: mapObjectValues(this.$infields, jsonInlineField), + $infieldsMulti: mapObjectValues(this.$infieldsMulti, jsonInlineFieldList), $links: this.$links, $blockId: this.$blockId, $parentLine: this.$parentLine, @@ -720,7 +757,7 @@ export class MarkdownListItem implements Indexable, Linkbearing, Taggable, Field export namespace MarkdownListItem { export interface Typed extends Omit>, - TypedValuebearing {} + TypedValuebearing { } } /** @public A specific task inside of a markdown list. */ @@ -744,6 +781,9 @@ export class MarkdownTaskItem extends MarkdownListItem implements Indexable, Lin $type: object.$type, $tags: object.$tags, $infields: mapObjectValues(object.$infields, (i) => normalizeLinks(valueInlineField(i), normalizer)), + $infieldsMulti: object.$infieldsMulti + ? mapObjectValues(object.$infieldsMulti, (fields) => normalizeLinks(valueInlineFieldList(fields), normalizer)) + : {}, $links: object.$links.map((l) => normalizer(Link.fromObject(l))), $blockId: object.$blockId, $parentLine: object.$parentLine, @@ -774,7 +814,7 @@ export class MarkdownTaskItem extends MarkdownListItem implements Indexable, Lin export namespace MarkdownTaskItem { export interface Typed extends Omit>, - TypedValuebearing {} + TypedValuebearing { } } /** @public An entry in the frontmatter; includes the raw value, parsed value, and raw key (before lower-casing). */ diff --git a/src/test/index/types/field.test.ts b/src/test/index/types/field.test.ts index 15779abeb..bdbd3214e 100644 --- a/src/test/index/types/field.test.ts +++ b/src/test/index/types/field.test.ts @@ -1,4 +1,4 @@ -import { InlineField } from "index/import/inline-field"; +import { InlineField, InlineFieldList } from "index/import/inline-field"; import { Extractors } from "expression/field"; import { Indexable } from "index/types/indexable"; import { FrontmatterEntry } from "index/types/markdown"; @@ -9,7 +9,7 @@ class DummyFields implements Indexable { public $id: string = "dummy"; public $typename: string = "Dummy"; - public constructor(public $text: string, public $value: number, public $size: number) {} + public constructor(public $text: string, public $value: number, public $size: number) { } public get $valueSize(): number { return this.$value + this.$size; @@ -40,7 +40,7 @@ class DummyMarkdown implements Indexable { public $id: string = "dummy"; public $typename: string = "Dummy"; - public constructor(public frontmatter: Record) {} + public constructor(public frontmatter: Record) { } } describe("Frontmatter Behavior", () => { @@ -90,7 +90,7 @@ class DummyInlineFields implements Indexable { public $id: string = "dummy"; public $typename: string = "Dummy"; - public constructor(public fields: Record) {} + public constructor(public fields: Record) { } } describe("Inline Field Behavior", () => { @@ -145,3 +145,44 @@ describe("Inline Field Behavior", () => { expect(elements).toEqual(new Set(["a", "b"])); }); }); + +class DummyInlineFieldsMulti implements Indexable { + public $types: string[] = ["a", "b", "c"]; + public $file: string = "file"; + public $id: string = "dummy"; + public $typename: string = "Dummy"; + + public constructor(public fields: Record) { } +} + +describe("Inline Field Multi Behavior", () => { + const extractor = Extractors.inlineFieldsMulti((x) => x.fields); + + test("Duplicate Keys Become Lists", () => { + const dup = new DummyInlineFieldsMulti({ + a: [ + { + key: "a", + value: 10, + raw: "10", + position: { line: 1, start: 1, startValue: 1, end: 2 }, + }, + { + key: "a", + value: 20, + raw: "20", + position: { line: 5, start: 1, startValue: 1, end: 2 }, + }, + ], + }); + + expect(extractor(dup, "a")).toEqual([ + { + key: "a", + value: [10, 20], + raw: "10, 20", + provenance: { type: "inline-field", key: "a", file: "file", line: 1, revision: 0 }, + }, + ]); + }); +});