diff --git a/src/plugin/obsidian-compat/__tests__/metadatacache-type-compat.test.ts b/src/plugin/obsidian-compat/__tests__/metadatacache-type-compat.test.ts index ad8b19b7..63695f50 100644 --- a/src/plugin/obsidian-compat/__tests__/metadatacache-type-compat.test.ts +++ b/src/plugin/obsidian-compat/__tests__/metadatacache-type-compat.test.ts @@ -123,13 +123,13 @@ describe('MetadataCache Type Compatibility', () => { }) it('CachedMetadata has footnotes and footnoteRefs fields', () => { - // Note: These are Reams extensions to Obsidian's CachedMetadata + // CachedMetadata uses upstream-compatible types (FootnoteCache, FootnoteRefCache) + // which only have id + position. Reams-extended fields (body, original) are + // on ReamsFootnoteCache/ReamsFootnoteRefCache in the 'reams' module. const cache: CachedMetadata = { footnotes: [ { id: '1', - body: 'Footnote body', - original: '[^1]: Footnote body', position: { start: { line: 20, col: 0, offset: 400 }, end: { line: 20, col: 19, offset: 419 }, @@ -139,7 +139,6 @@ describe('MetadataCache Type Compatibility', () => { footnoteRefs: [ { id: '1', - original: '[^1]', position: { start: { line: 5, col: 0, offset: 50 }, end: { line: 5, col: 4, offset: 54 }, @@ -179,13 +178,11 @@ describe('MetadataCache Type Compatibility', () => { expect(backlink.name).toBe('source-note.md') }) - it('FootnoteCache type is exported and has required upstream fields', () => { - // FootnoteCache extends CacheItem (has position) and adds id - // Reams also adds body and original as extensions + it('FootnoteCache type matches upstream Obsidian API (id + position only)', () => { + // FootnoteCache from 'obsidian' matches upstream exactly: id + position + // Reams-extended version (ReamsFootnoteCache with body/original) is in 'reams' module const footnote: FootnoteCache = { id: '1', - body: 'This is the footnote content.', - original: '[^1]: This is the footnote content.', position: { start: { line: 10, col: 0, offset: 100 }, end: { line: 10, col: 35, offset: 135 }, @@ -195,17 +192,13 @@ describe('MetadataCache Type Compatibility', () => { expect(footnote.id).toBe('1') expect(footnote.position).toBeDefined() expect(footnote.position.start.offset).toBe(100) - // Reams extension fields - expect(footnote.body).toBe('This is the footnote content.') - expect(footnote.original).toBe('[^1]: This is the footnote content.') }) - it('FootnoteRefCache type is exported and has required upstream fields', () => { - // FootnoteRefCache extends CacheItem (has position) and adds id - // Reams also adds original as an extension + it('FootnoteRefCache type matches upstream Obsidian API (id + position only)', () => { + // FootnoteRefCache from 'obsidian' matches upstream exactly: id + position + // Reams-extended version (ReamsFootnoteRefCache with original) is in 'reams' module const ref: FootnoteRefCache = { id: '1', - original: '[^1]', position: { start: { line: 5, col: 10, offset: 50 }, end: { line: 5, col: 14, offset: 54 }, @@ -215,8 +208,6 @@ describe('MetadataCache Type Compatibility', () => { expect(ref.id).toBe('1') expect(ref.position).toBeDefined() expect(ref.position.start.offset).toBe(50) - // Reams extension field - expect(ref.original).toBe('[^1]') }) }) }) diff --git a/src/plugin/obsidian-compat/index.ts b/src/plugin/obsidian-compat/index.ts index 6ad0eead..ff5c321c 100644 --- a/src/plugin/obsidian-compat/index.ts +++ b/src/plugin/obsidian-compat/index.ts @@ -63,7 +63,8 @@ export type { FrontMatterCache, CachedMetadata, // Footnote cache types (Obsidian 1.6.6+, 1.8.7+) - // Reams extends these with additional fields (body, original) for footnote display. + // NOTE: These match upstream exactly. Reams-extended versions (ReamsFootnoteCache, + // ReamsFootnoteRefCache) with body/original fields are exported from 'reams' module. FootnoteCache, FootnoteRefCache, // Reference link cache type (Obsidian 1.8.7+) diff --git a/src/plugin/reams/__tests__/reams-module.test-d.ts b/src/plugin/reams/__tests__/reams-module.test-d.ts index 78251986..8a07eb34 100644 --- a/src/plugin/reams/__tests__/reams-module.test-d.ts +++ b/src/plugin/reams/__tests__/reams-module.test-d.ts @@ -1,27 +1,41 @@ import { assertType, expectTypeOf, test } from 'vitest' -import type { CachedMetadata, MetadataCache, Workspace, TFile } from 'obsidian' -import type { FootnoteCache, FootnoteRefCache, FormattingAction } from 'reams' +import type { CachedMetadata, MetadataCache, Workspace, TFile, FootnoteCache, FootnoteRefCache } from 'obsidian' +import type { ReamsFootnoteCache, ReamsFootnoteRefCache, FormattingAction } from 'reams' // Side-effect import to enable augmentations import 'reams' type CachedFootnote = NonNullable[number] type CachedFootnoteRef = NonNullable[number] -test('FootnoteCache matches CachedMetadata footnotes entries', () => { +test('CachedMetadata.footnotes uses upstream-compatible FootnoteCache', () => { + // CachedMetadata.footnotes uses the upstream-compatible FootnoteCache (id + position only) expectTypeOf().toEqualTypeOf() }) -test('FootnoteRefCache matches CachedMetadata footnoteRefs entries', () => { +test('CachedMetadata.footnoteRefs uses upstream-compatible FootnoteRefCache', () => { + // CachedMetadata.footnoteRefs uses the upstream-compatible FootnoteRefCache (id + position only) expectTypeOf().toEqualTypeOf() }) -test('consumers can assign CachedMetadata footnotes to reams footnote types', () => { +test('ReamsFootnoteCache extends FootnoteCache with body and original', () => { + // ReamsFootnoteCache is the Reams-extended version with body and original fields + expectTypeOf().toMatchTypeOf() + expectTypeOf().toHaveProperty('body') + expectTypeOf().toHaveProperty('original') +}) + +test('ReamsFootnoteRefCache extends FootnoteRefCache with original', () => { + // ReamsFootnoteRefCache is the Reams-extended version with original field + expectTypeOf().toMatchTypeOf() + expectTypeOf().toHaveProperty('original') +}) + +test('CachedMetadata footnotes can be used with upstream-compatible types', () => { + // This test verifies that consumers can work with the upstream-compatible types const cache = { footnotes: [ { id: '1', - body: 'footnote body', - original: '[^1]: footnote body', position: { start: { line: 0, col: 0, offset: 0 }, end: { line: 0, col: 20, offset: 20 }, @@ -31,7 +45,6 @@ test('consumers can assign CachedMetadata footnotes to reams footnote types', () footnoteRefs: [ { id: '1', - original: '[^1]', position: { start: { line: 1, col: 0, offset: 21 }, end: { line: 1, col: 4, offset: 25 }, diff --git a/src/plugin/reams/__tests__/reams-module.test.ts b/src/plugin/reams/__tests__/reams-module.test.ts index 79fc993c..20f26a15 100644 --- a/src/plugin/reams/__tests__/reams-module.test.ts +++ b/src/plugin/reams/__tests__/reams-module.test.ts @@ -19,7 +19,7 @@ import { ReamsRequestUrlError, REAMS_VERSION, } from 'reams' -import type { BacklinkResult, FootnoteCache, FootnoteRefCache } from 'reams' +import type { BacklinkResult, ReamsFootnoteCache, ReamsFootnoteRefCache } from 'reams' // Import from source modules for identity comparison import { REAMS_VERSION as VERSION_SOURCE } from '../../loader/version' @@ -51,9 +51,10 @@ describe('Reams module resolution', () => { // Type-only exports can't be tested at runtime beyond verifying // that constructing objects with the correct shape compiles. - it('type exports compile correctly (FootnoteCache)', () => { - // FootnoteCache: id, body, original, position (per services/metadata/types.ts) - const cache: FootnoteCache = { + it('type exports compile correctly (ReamsFootnoteCache)', () => { + // ReamsFootnoteCache: id, body, original, position (Reams-extended version) + // Upstream Obsidian's FootnoteCache only has id + position + const cache: ReamsFootnoteCache = { id: '1', body: 'footnote body', original: '[^1]: footnote body', @@ -68,9 +69,10 @@ describe('Reams module resolution', () => { expect(cache.position.start.offset).toBe(0) }) - it('type exports compile correctly (FootnoteRefCache)', () => { - // FootnoteRefCache: id, original, position (per services/metadata/types.ts) - const ref: FootnoteRefCache = { + it('type exports compile correctly (ReamsFootnoteRefCache)', () => { + // ReamsFootnoteRefCache: id, original, position (Reams-extended version) + // Upstream Obsidian's FootnoteRefCache only has id + position + const ref: ReamsFootnoteRefCache = { id: '1', original: '[^1]', position: { diff --git a/src/plugin/reams/index.ts b/src/plugin/reams/index.ts index 80744dc0..0ae9ee5a 100644 --- a/src/plugin/reams/index.ts +++ b/src/plugin/reams/index.ts @@ -86,9 +86,17 @@ export type { ReamsEventMap } from './events' /** * A cached footnote definition (`[^id]: body` at line start, body may * span multiple indented continuation lines). - * Reams-specific extension to Obsidian's CachedMetadata. + * Reams-extended version with `body` and `original` fields. + * Upstream Obsidian's FootnoteCache only has `id`. */ -export type { FootnoteCache } from '../../services/metadata/types' +export type { ReamsFootnoteCache } from '../../services/metadata/types' + +/** + * A cached footnote reference (`[^id]` inline). + * Reams-extended version with `original` field. + * Upstream Obsidian's FootnoteRefCache only has `id`. + */ +export type { ReamsFootnoteRefCache } from '../../services/metadata/types' /** * Result type for MetadataCache.getBacklinks(). diff --git a/src/services/metadata/__tests__/extract-metadata.test.ts b/src/services/metadata/__tests__/extract-metadata.test.ts index 1c762e19..d8de65e7 100644 --- a/src/services/metadata/__tests__/extract-metadata.test.ts +++ b/src/services/metadata/__tests__/extract-metadata.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest' import { extractMetadata, parseFrontmatter } from '../extract-metadata' +import type { ReamsFootnoteCache, ReamsFootnoteRefCache } from '../types' describe('extractMetadata', () => { // Helper to verify Pos structure @@ -354,7 +355,8 @@ describe('extractMetadata', () => { const content = 'See [^1] for details' const result = extractMetadata(content) expect(result.footnoteRefs).toHaveLength(1) - const ref = result.footnoteRefs![0] + // Cast to ReamsFootnoteRefCache to access Reams-extended 'original' field + const ref = result.footnoteRefs![0] as ReamsFootnoteRefCache expect(ref.id).toBe('1') expect(ref.original).toBe('[^1]') expectValidPos(ref) @@ -366,7 +368,8 @@ describe('extractMetadata', () => { const content = '[^1]: body text' const result = extractMetadata(content) expect(result.footnotes).toHaveLength(1) - const def = result.footnotes![0] + // Cast to ReamsFootnoteCache to access Reams-extended 'body' and 'original' fields + const def = result.footnotes![0] as ReamsFootnoteCache expect(def.id).toBe('1') expect(def.body).toBe('body text') expect(def.original).toBe('[^1]: body text') @@ -378,7 +381,7 @@ describe('extractMetadata', () => { const content = '[^1]: first line\n second line\n third line\n' const result = extractMetadata(content) expect(result.footnotes).toHaveLength(1) - const def = result.footnotes![0] + const def = result.footnotes![0] as ReamsFootnoteCache expect(def.body).toBe('first line\nsecond line\nthird line') // End offset should cover all three lines (excluding trailing newline) expect(def.position.end.offset).toBe('[^1]: first line\n second line\n third line'.length) @@ -388,7 +391,7 @@ describe('extractMetadata', () => { const content = '[^1]: first line\n\tsecond line\n\tthird line\n' const result = extractMetadata(content) expect(result.footnotes).toHaveLength(1) - const def = result.footnotes![0] + const def = result.footnotes![0] as ReamsFootnoteCache expect(def.body).toBe('first line\nsecond line\nthird line') }) @@ -396,7 +399,7 @@ describe('extractMetadata', () => { const content = '[^1]: first line\n notIndentedEnough\n' const result = extractMetadata(content) expect(result.footnotes).toHaveLength(1) - const def = result.footnotes![0] + const def = result.footnotes![0] as ReamsFootnoteCache expect(def.body).toBe('first line') }) @@ -404,7 +407,7 @@ describe('extractMetadata', () => { const content = '[^1]: first line\n stillNotEnough\n' const result = extractMetadata(content) expect(result.footnotes).toHaveLength(1) - const def = result.footnotes![0] + const def = result.footnotes![0] as ReamsFootnoteCache expect(def.body).toBe('first line') }) @@ -412,7 +415,7 @@ describe('extractMetadata', () => { const content = '[^1]: first line\n second line\n\n NOT part of def\n' const result = extractMetadata(content) expect(result.footnotes).toHaveLength(1) - const def = result.footnotes![0] + const def = result.footnotes![0] as ReamsFootnoteCache expect(def.body).toBe('first line\nsecond line') }) @@ -420,7 +423,7 @@ describe('extractMetadata', () => { const content = '[^1]: first line\n second line\nnext paragraph' const result = extractMetadata(content) expect(result.footnotes).toHaveLength(1) - const def = result.footnotes![0] + const def = result.footnotes![0] as ReamsFootnoteCache expect(def.body).toBe('first line\nsecond line') }) @@ -520,7 +523,7 @@ describe('extractMetadata', () => { const content = '[^1]:' const result = extractMetadata(content) expect(result.footnotes).toHaveLength(1) - const def = result.footnotes![0] + const def = result.footnotes![0] as ReamsFootnoteCache expect(def.id).toBe('1') expect(def.body).toBe('') expect(def.original).toBe('[^1]:') @@ -535,7 +538,7 @@ describe('extractMetadata', () => { const content = '[^1]:\n' const result = extractMetadata(content) expect(result.footnotes).toHaveLength(1) - const def = result.footnotes![0] + const def = result.footnotes![0] as ReamsFootnoteCache expect(def.body).toBe('') expect(def.original).toBe('[^1]:') expect(def.position.end.offset).toBe(5) diff --git a/src/services/metadata/__tests__/types.test.ts b/src/services/metadata/__tests__/types.test.ts index 23563975..5866f467 100644 --- a/src/services/metadata/__tests__/types.test.ts +++ b/src/services/metadata/__tests__/types.test.ts @@ -11,6 +11,8 @@ import type { CachedMetadata, FootnoteCache, FootnoteRefCache, + ReamsFootnoteCache, + ReamsFootnoteRefCache, } from '../types' describe('Obsidian-compatible metadata types', () => { @@ -126,8 +128,20 @@ describe('Obsidian-compatible metadata types', () => { // Type-contract tests for F-VAULT-9 (chainlink #311) // These verify the types match what consumers expect - it('FootnoteRefCache has id, original, and position', () => { + it('FootnoteRefCache (upstream-compatible) has id and position', () => { const ref: FootnoteRefCache = { + id: '1', + position: { + start: { line: 5, col: 10, offset: 100 }, + end: { line: 5, col: 14, offset: 104 }, + }, + } + expect(ref.id).toBe('1') + expect(ref.position.start.offset).toBe(100) + }) + + it('ReamsFootnoteRefCache extends FootnoteRefCache with original', () => { + const ref: ReamsFootnoteRefCache = { id: '1', original: '[^1]', position: { @@ -140,8 +154,20 @@ describe('Obsidian-compatible metadata types', () => { expect(ref.position.start.offset).toBe(100) }) - it('FootnoteCache has id, body, original, and position', () => { + it('FootnoteCache (upstream-compatible) has id and position', () => { const footnote: FootnoteCache = { + id: '1', + position: { + start: { line: 10, col: 0, offset: 200 }, + end: { line: 10, col: 35, offset: 235 }, + }, + } + expect(footnote.id).toBe('1') + expect(footnote.position.end.offset).toBe(235) + }) + + it('ReamsFootnoteCache extends FootnoteCache with body and original', () => { + const footnote: ReamsFootnoteCache = { id: '1', body: 'This is the footnote content.', original: '[^1]: This is the footnote content.', @@ -156,12 +182,13 @@ describe('Obsidian-compatible metadata types', () => { }) it('CachedMetadata includes footnotes and footnoteRefs arrays', () => { + // CachedMetadata uses upstream-compatible types (FootnoteCache, FootnoteRefCache) + // which only have id + position. Reams-extended fields (body, original) are + // structurally allowed but not typed on CachedMetadata. const cached: CachedMetadata = { footnotes: [ { id: '1', - body: 'Footnote body', - original: '[^1]: Footnote body', position: { start: { line: 20, col: 0, offset: 400 }, end: { line: 20, col: 19, offset: 419 }, @@ -171,7 +198,6 @@ describe('Obsidian-compatible metadata types', () => { footnoteRefs: [ { id: '1', - original: '[^1]', position: { start: { line: 5, col: 0, offset: 50 }, end: { line: 5, col: 4, offset: 54 }, diff --git a/src/services/metadata/extract-metadata.ts b/src/services/metadata/extract-metadata.ts index c3f1f138..a1ab8af8 100644 --- a/src/services/metadata/extract-metadata.ts +++ b/src/services/metadata/extract-metadata.ts @@ -9,8 +9,8 @@ import type { HeadingCache, FrontMatterCache, CachedMetadata, - FootnoteCache, - FootnoteRefCache, + ReamsFootnoteCache, + ReamsFootnoteRefCache, } from './types' // --- Footnote regex grammar --- @@ -515,7 +515,7 @@ export function extractMetadata(content: string): CachedMetadata { // --- Footnote definitions: [^id]: body (multi-line) --- // Definition pass runs FIRST so the ref pass can skip def-prefix // occurrences (a def's `[^id]` part should NOT also count as a ref). - const footnotes: FootnoteCache[] = [] + const footnotes: ReamsFootnoteCache[] = [] // Set of ranges spanning each def's `[^id]` prefix; used to avoid // double-matching them as refs. const defPrefixRanges: Array<[number, number]> = [] @@ -578,7 +578,7 @@ export function extractMetadata(content: string): CachedMetadata { } // --- Footnote references: [^id] --- - const footnoteRefs: FootnoteRefCache[] = [] + const footnoteRefs: ReamsFootnoteRefCache[] = [] const refRegex = new RegExp(FOOTNOTE_REF_RE.source, 'g') while ((match = refRegex.exec(content)) !== null) { if (match.index < fmEndOffset) continue diff --git a/src/services/metadata/index.ts b/src/services/metadata/index.ts index 8d537916..04d0801c 100644 --- a/src/services/metadata/index.ts +++ b/src/services/metadata/index.ts @@ -7,4 +7,6 @@ export type { Loc, Pos, CacheItem, Reference, ReferenceCache, BlockCache, LinkCache, EmbedCache, TagCache, HeadingCache, ListItemCache, SectionCache, FrontMatterCache, CachedMetadata, FootnoteCache, FootnoteRefCache, ReferenceLinkCache, + // Reams-extended types with additional fields for footnote display + ReamsFootnoteCache, ReamsFootnoteRefCache, } from './types' diff --git a/src/services/metadata/types.ts b/src/services/metadata/types.ts index e43fc6ca..b4329c36 100644 --- a/src/services/metadata/types.ts +++ b/src/services/metadata/types.ts @@ -165,37 +165,51 @@ export interface SectionCache extends CacheItem { /** * A cached footnote reference (`[^id]` inline). - * Matches Obsidian's FootnoteRefCache type (extends CacheItem with `id`). - * Reams populates `original` as an extension for footnote display. + * Matches Obsidian's FootnoteRefCache type exactly (extends CacheItem with `id`). * @public * @since 1.8.7 */ export interface FootnoteRefCache extends CacheItem { /** Footnote id (e.g. "1" from [^1] or "custom-name" from [^custom-name]) */ id: string - /** Original raw text (e.g. "[^1]"). Reams extension (optional for upstream compat). */ +} + +/** + * Reams-extended FootnoteRefCache with additional fields for footnote display. + * Use this type internally; export FootnoteRefCache for Obsidian API compatibility. + * @public + */ +export interface ReamsFootnoteRefCache extends FootnoteRefCache { + /** Original raw text (e.g. "[^1]"). Reams extension. */ original?: string } /** * A cached footnote definition (`[^id]: body` at line start, body may * span multiple indented continuation lines). - * Matches Obsidian's FootnoteCache type (extends CacheItem with `id`). - * Reams populates `body` and `original` as extensions for footnote display. + * Matches Obsidian's FootnoteCache type exactly (extends CacheItem with `id`). * @public * @since 1.6.6 */ export interface FootnoteCache extends CacheItem { /** Footnote id (matches a FootnoteRefCache.id when resolved) */ id: string +} + +/** + * Reams-extended FootnoteCache with additional fields for footnote display. + * Use this type internally; export FootnoteCache for Obsidian API compatibility. + * @public + */ +export interface ReamsFootnoteCache extends FootnoteCache { /** * Body text (excludes the `[^id]:` prefix; continuation lines joined with \n). - * Reams extension (optional for upstream compat). + * Reams extension. */ body?: string /** * Original raw text covering the full definition span (no trailing newline). - * Reams extension (optional for upstream compat). + * Reams extension. */ original?: string }