Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Settings } from "./components/Settings.tsx";
import { useNotesStore } from "./store/notesStore";
import { Toaster } from "react-hot-toast";
import PreflightModal from "./components/PrereflightModal.tsx";
import { countWordsInNoteContent } from "./lib/note-utils";

import { AnimatePresence, motion } from "framer-motion";

Expand All @@ -29,7 +30,7 @@ function App() {
const { currentNote, saveTimeout } = useNotesStore();

const wordCount = currentNote
? currentNote.content.split(/\s+/).filter((word) => word.length > 0).length
? countWordsInNoteContent(currentNote.content)
: 0;

const isSaved = !saveTimeout;
Expand Down
50 changes: 49 additions & 1 deletion src/lib/note-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from 'node:assert/strict';
import { openCreatedNote } from './note-utils.ts';
import { openCreatedNote, countWordsInNoteContent } from './note-utils.ts';
import type { Note } from '../types/Note.ts';

function makeNote(partial: Partial<Note> & { id: string; title: string }): Note {
Expand Down Expand Up @@ -35,3 +35,51 @@ assert.deepEqual(
assert.equal(again.currentNote.id, 'b');

console.log('note-utils openCreatedNote: ok');

// --- countWordsInNoteContent ---
const blockDoc = JSON.stringify([
{
id: '1',
type: 'heading',
props: { level: 1 },
content: [{ type: 'text', text: 'Hello world', styles: {} }],
children: [],
},
{
id: '2',
type: 'paragraph',
content: [{ type: 'text', text: 'one two three', styles: {} }],
children: [],
},
]);

// Readable text is "Hello world one two three" → 5 words (not JSON keys)
assert.equal(countWordsInNoteContent(blockDoc), 5);
// Pretty-printed JSON still must not count structural tokens as words
const pretty = JSON.stringify(JSON.parse(blockDoc), null, 2);
assert.equal(countWordsInNoteContent(pretty), 5);
assert.ok(
pretty.split(/\s+/).filter((w) => w.length > 0).length > 5,
'sanity: naive split of pretty JSON overcounts vs real text',
);
assert.equal(countWordsInNoteContent('plain two words'), 3);
assert.equal(countWordsInNoteContent(''), 0);
assert.equal(countWordsInNoteContent(null), 0);

// Nested children
const nested = JSON.stringify([
{
type: 'bulletListItem',
content: [{ type: 'text', text: 'parent item', styles: {} }],
children: [
{
type: 'bulletListItem',
content: [{ type: 'text', text: 'child words here', styles: {} }],
children: [],
},
],
},
]);
assert.equal(countWordsInNoteContent(nested), 5);

console.log('note-utils countWordsInNoteContent: ok');
59 changes: 59 additions & 0 deletions src/lib/note-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,62 @@ export function openCreatedNote(
saveTimeout: null,
};
}

type BlockContent =
| string
| Array<{ type?: string; text?: string; content?: BlockContent }>
| null
| undefined;

type BlockLike = {
content?: BlockContent;
children?: BlockLike[];
};

function textFromContent(content: BlockContent): string {
if (content == null) return '';
if (typeof content === 'string') return content;
if (!Array.isArray(content)) return '';
return content
.map((item) => {
if (typeof item === 'string') return item;
if (item && typeof item === 'object') {
if (typeof item.text === 'string') return item.text;
if (item.content != null) return textFromContent(item.content);
}
return '';
})
.join('');
}

function textFromBlocks(blocks: BlockLike[]): string {
const parts: string[] = [];
for (const block of blocks) {
const own = textFromContent(block.content);
if (own) parts.push(own);
if (Array.isArray(block.children) && block.children.length > 0) {
const child = textFromBlocks(block.children);
if (child) parts.push(child);
}
}
return parts.join(' ');
}

/**
* Count words in note content. BlockNote stores JSON block trees; fall back to plain text.
*/
export function countWordsInNoteContent(content: string | null | undefined): number {
if (!content || !content.trim()) return 0;

try {
const parsed = JSON.parse(content);
if (Array.isArray(parsed)) {
const text = textFromBlocks(parsed as BlockLike[]);
return text.split(/\s+/).filter((w) => w.length > 0).length;
}
} catch {
// plain text / non-JSON
}

return content.split(/\s+/).filter((w) => w.length > 0).length;
}
Loading