// fixtures/wordcount.test.mjs — counting, under plain Node. // // countWords is the only part of the word count with any judgement in // it, and it is the part a person will quote to somebody else, so it is // the part worth pinning. The dialog around it is markup. import { test } from 'node:test' import assert from 'node:assert/strict' import { Schema } from 'prosemirror-model' import { EditorState } from 'prosemirror-state' import { schema as basic } from 'prosemirror-schema-basic' import { addListNodes } from 'prosemirror-schema-list' import { countWords, counts } from './wordcount.mjs' const schema = new Schema({ nodes: addListNodes(basic.spec.nodes, 'paragraph block*', 'block'), marks: basic.spec.marks, }) function docOf(...paragraphs) { return schema.node( 'doc', null, paragraphs.map((text) => text ? schema.node('paragraph', null, [schema.text(text)]) : schema.node('paragraph'), ), ) } test('nothing is nothing', () => { assert.equal(countWords(''), 0) assert.equal(countWords(' '), 0) assert.equal(countWords(null), 0) }) test('ordinary English', () => { assert.equal(countWords('one'), 1) assert.equal(countWords('one two three'), 3) assert.equal(countWords(' leading and trailing '), 3) assert.equal(countWords('line\nbreaks\tand\ttabs'), 4) }) // Punctuation is where whitespace-splitting and real segmentation // diverge, and where a person notices: an em-dashed clause is two // words, not one. test('punctuation is not a word', () => { assert.equal(countWords('yes — no'), 2) assert.equal(countWords('one, two, three.'), 3) assert.equal(countWords('"quoted"'), 1) }) // The reason this uses Intl.Segmenter at all. Whitespace-splitting // finds no boundary in these at all and would report 1. test('a language without spaces between words still counts', (t) => { if (typeof Intl === 'undefined' || typeof Intl.Segmenter !== 'function') { t.skip('this platform has no Intl.Segmenter, so only the whitespace fallback exists') return } assert.ok(countWords('今日は良い天気です') > 1, 'Japanese counted as one word') assert.ok(countWords('这是一个句子') > 1, 'Chinese counted as one word') }) test('the document count walks every block', () => { const state = EditorState.create({ doc: docOf('one two', 'three four five'), schema }) const c = counts(state) assert.equal(c.words, 5) assert.equal(c.paragraphs, 2) }) // The blocks are joined with a space, so the last word of one paragraph // and the first of the next are two words rather than one. Without // that, "one" and "two" below would be counted as "onetwo". test('a paragraph boundary is a word boundary', () => { const state = EditorState.create({ doc: docOf('one', 'two'), schema }) assert.equal(counts(state).words, 2) }) test('characters are counted with and without spaces', () => { const state = EditorState.create({ doc: docOf('ab cd'), schema }) const c = counts(state) assert.equal(c.characters, 5) assert.equal(c.charactersNoSpaces, 4) }) test('no selection means no selection count, not a zero', () => { // A zero would be indistinguishable from "nothing is selected", which // is why the dialog hides the row rather than showing 0. const state = EditorState.create({ doc: docOf('one two'), schema }) assert.equal(counts(state).selection, null) })