fix: exclude YAML frontmatter from word count in Inspector panel
The DynamicPropertiesPanel had a local countWords that used a regex (/^---[\s\S]*?---/) which could match dashes inside frontmatter values, leaving part of the frontmatter in the body and inflating the count. Replace with the canonical countWords from utils/wikilinks.ts which uses splitFrontmatter (line-based indexOf) and correctly finds the closing delimiter on its own line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import type { ParsedFrontmatter } from '../utils/frontmatter'
|
||||
import { EditableValue, TagPillList } from './EditableValue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { getTypeColor, getTypeLightColor } from '../utils/typeColors'
|
||||
import { countWords } from '../utils/wikilinks'
|
||||
|
||||
const STATUS_STYLES: Record<string, { bg: string; color: string }> = {
|
||||
Active: { bg: 'var(--accent-green-light)', color: 'var(--accent-green)' },
|
||||
@@ -37,12 +38,6 @@ export function containsWikilinks(value: FrontmatterValue): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
function countWords(content: string | null): number {
|
||||
if (!content) return 0
|
||||
const stripped = content.replace(/^---[\s\S]*?---\n?/, '')
|
||||
const words = stripped.trim().split(/\s+/).filter((w) => w.length > 0)
|
||||
return words.length
|
||||
}
|
||||
|
||||
function formatDate(timestamp: number | null): string {
|
||||
if (!timestamp) return '\u2014'
|
||||
@@ -238,7 +233,7 @@ export function DynamicPropertiesPanel({
|
||||
const [editingKey, setEditingKey] = useState<string | null>(null)
|
||||
const [showAddDialog, setShowAddDialog] = useState(false)
|
||||
|
||||
const wordCount = countWords(content)
|
||||
const wordCount = countWords(content ?? '')
|
||||
|
||||
const propertyEntries = useMemo(() => {
|
||||
return Object.entries(frontmatter)
|
||||
|
||||
@@ -131,8 +131,8 @@ describe('Inspector', () => {
|
||||
|
||||
it('computes word count from content minus frontmatter', () => {
|
||||
render(<Inspector {...defaultProps} entry={mockEntry} content={mockContent} />)
|
||||
// "# Test Project" + "This is a test note with some words to count." = 13 words
|
||||
expect(screen.getByText('13')).toBeInTheDocument()
|
||||
// "Test Project" (# stripped) + "This is a test note with some words to count." = 12 words
|
||||
expect(screen.getByText('12')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows "Add property" button as disabled placeholder', () => {
|
||||
@@ -470,8 +470,11 @@ Status: Active
|
||||
allContent={{}}
|
||||
/>
|
||||
)
|
||||
// 2 entries reference via Belongs to
|
||||
expect(screen.getByText('2')).toBeInTheDocument()
|
||||
// 2 entries reference via Belongs to — badge appears in the Referenced by header
|
||||
const allTwos = screen.getAllByText('2')
|
||||
expect(allTwos.length).toBeGreaterThanOrEqual(1)
|
||||
// At least one "2" is inside a badge (span with ml-1 class)
|
||||
expect(allTwos.some(el => el.classList.contains('ml-1'))).toBe(true)
|
||||
})
|
||||
|
||||
it('shows "No references" when no entries reference the current note', () => {
|
||||
|
||||
@@ -163,6 +163,13 @@ describe('splitFrontmatter', () => {
|
||||
expect(fm).toBe('---\ntitle: Hello\n---\n')
|
||||
expect(body).toBe('Content')
|
||||
})
|
||||
|
||||
it('ignores dashes inside frontmatter values', () => {
|
||||
const content = '---\ntitle: "A --- B"\ntype: Note\n---\n\nBody text'
|
||||
const [fm, body] = splitFrontmatter(content)
|
||||
expect(fm).toBe('---\ntitle: "A --- B"\ntype: Note\n---\n')
|
||||
expect(body).toBe('\nBody text')
|
||||
})
|
||||
})
|
||||
|
||||
describe('countWords', () => {
|
||||
@@ -195,6 +202,67 @@ describe('countWords', () => {
|
||||
const content = 'Hello world this is four words plus three'
|
||||
expect(countWords(content)).toBe(8)
|
||||
})
|
||||
|
||||
it('excludes long frontmatter with many keys from count', () => {
|
||||
const content = [
|
||||
'---',
|
||||
'type: Note',
|
||||
'workspace: personal',
|
||||
'notion_id: 63aeb735-e6f4-4a32-b7b6-d34276a26dee',
|
||||
'status: Active',
|
||||
'owner: Luca Rossi',
|
||||
'tags: [Tauri, React, TypeScript]',
|
||||
'belongs_to:',
|
||||
' - "[[quarter/q1-2026]]"',
|
||||
'related_to:',
|
||||
' - "[[topic/software-development]]"',
|
||||
'---',
|
||||
'',
|
||||
'# My Note',
|
||||
'',
|
||||
'Only these five words count.',
|
||||
].join('\n')
|
||||
// Body: "# My Note\n\nOnly these five words count."
|
||||
// After stripping '#': " My Note\n\nOnly these five words count."
|
||||
// Words: My, Note, Only, these, five, words, count. = 7
|
||||
expect(countWords(content)).toBe(7)
|
||||
})
|
||||
|
||||
it('ignores frontmatter values containing dashes', () => {
|
||||
const content = [
|
||||
'---',
|
||||
'title: "Something --- More"',
|
||||
'type: Note',
|
||||
'---',
|
||||
'',
|
||||
'Body words here.',
|
||||
].join('\n')
|
||||
expect(countWords(content)).toBe(3)
|
||||
})
|
||||
|
||||
it('handles frontmatter with horizontal rule in body', () => {
|
||||
const content = [
|
||||
'---',
|
||||
'type: Note',
|
||||
'---',
|
||||
'',
|
||||
'# Title',
|
||||
'',
|
||||
'Before the rule.',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'After the rule.',
|
||||
].join('\n')
|
||||
// Body includes: Title, Before, the, rule., After, the, rule.
|
||||
// The --- horizontal rule is stripped by the markdown char filter
|
||||
expect(countWords(content)).toBe(7)
|
||||
})
|
||||
|
||||
it('returns 0 when content is only frontmatter with no trailing body', () => {
|
||||
const content = '---\ntitle: Hello\nstatus: Active\ntags: [a, b, c]\n---'
|
||||
expect(countWords(content)).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('restoreWikilinksInBlocks', () => {
|
||||
|
||||
Reference in New Issue
Block a user