fix: exclude title heading and wikilinks from word count

countWords() was including the title line (# Heading) in the count,
inflating it by ~2-3 words. Now strips the first H1 heading and
[[wikilinks]] before counting, so only body prose is tallied.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-02-24 23:40:08 +01:00
parent b5eb7e4098
commit f7c4e16be9
3 changed files with 44 additions and 11 deletions

View File

@@ -129,10 +129,10 @@ describe('Inspector', () => {
expect(pill).toHaveStyle({ borderRadius: '16px' })
})
it('computes word count from content minus frontmatter', () => {
it('computes word count from content minus frontmatter and title', () => {
render(<Inspector {...defaultProps} entry={mockEntry} content={mockContent} />)
// "Test Project" (# stripped) + "This is a test note with some words to count." = 12 words
expect(screen.getByText('12')).toBeInTheDocument()
// Title "# Test Project" excluded; body: "This is a test note with some words to count." = 10 words
expect(screen.getByText('10')).toBeInTheDocument()
})
it('shows "Add property" button as disabled placeholder', () => {

View File

@@ -222,10 +222,9 @@ describe('countWords', () => {
'',
'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)
// Title "# My Note" is excluded from word count
// Body: "Only these five words count." = 5 words
expect(countWords(content)).toBe(5)
})
it('ignores frontmatter values containing dashes', () => {
@@ -254,15 +253,47 @@ describe('countWords', () => {
'',
'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)
// Title "# Title" is excluded; body: Before, the, rule., After, the, rule. = 6
expect(countWords(content)).toBe(6)
})
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)
})
it('excludes title heading from word count', () => {
const content = '---\ntitle: Hello World\n---\n\n# Hello World\n\nThree body words.'
expect(countWords(content)).toBe(3)
})
it('counts correctly when no title heading is present', () => {
const content = '---\ntitle: Test\n---\n\nFour words in body.'
expect(countWords(content)).toBe(4)
})
it('excludes wikilinks from word count', () => {
const content = '---\ntitle: Test\n---\n\n# Test\n\nSee [[My Note]] for details.'
// Title excluded, wikilink excluded: See, for, details. = 3
expect(countWords(content)).toBe(3)
})
it('excludes multiple wikilinks from word count', () => {
const content = 'Check [[note-a]] and [[note-b]] here.'
// wikilinks excluded: Check, and, here. = 3
expect(countWords(content)).toBe(3)
})
it('returns 0 for note with only title and no body', () => {
const content = '---\ntitle: Empty\n---\n\n# Empty\n'
expect(countWords(content)).toBe(0)
})
it('does not strip ## or ### subheadings as title', () => {
const content = '---\ntitle: Test\n---\n\n## Subheading\n\nBody text here.'
// ## is not a title heading; "Subheading" counts: Subheading, Body, text, here. = 4
expect(countWords(content)).toBe(4)
})
})
describe('restoreWikilinksInBlocks', () => {

View File

@@ -106,7 +106,9 @@ export function splitFrontmatter(content: string): [string, string] {
export function countWords(content: string): number {
const [, body] = splitFrontmatter(content)
const text = body.replace(/[#*_[\]`>~\-|]/g, '').trim()
const withoutTitle = body.replace(/^\s*# [^\n]+\n?/, '')
const withoutWikilinks = withoutTitle.replace(/\[\[[^\]]*\]\]/g, '')
const text = withoutWikilinks.replace(/[#*_[\]`>~\-|]/g, '').trim()
if (!text) return 0
return text.split(/\s+/).filter(Boolean).length
}