oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1// fixtures/wordcount.test.mjs — counting, under plain Node.
2//
3// countWords is the only part of the word count with any judgement in
4// it, and it is the part a person will quote to somebody else, so it is
5// the part worth pinning. The dialog around it is markup.
6
7import { test } from 'node:test'
8import assert from 'node:assert/strict'
9
10import { Schema } from 'prosemirror-model'
11import { EditorState } from 'prosemirror-state'
12import { schema as basic } from 'prosemirror-schema-basic'
13import { addListNodes } from 'prosemirror-schema-list'
14
15import { countWords, counts } from './wordcount.mjs'
16
17const schema = new Schema({
18 nodes: addListNodes(basic.spec.nodes, 'paragraph block*', 'block'),
19 marks: basic.spec.marks,
20})
21
22function docOf(...paragraphs) {
23 return schema.node(
24 'doc',
25 null,
26 paragraphs.map((text) =>
27 text ? schema.node('paragraph', null, [schema.text(text)]) : schema.node('paragraph'),
28 ),
29 )
30}
31
32test('nothing is nothing', () => {
33 assert.equal(countWords(''), 0)
34 assert.equal(countWords(' '), 0)
35 assert.equal(countWords(null), 0)
36})
37
38test('ordinary English', () => {
39 assert.equal(countWords('one'), 1)
40 assert.equal(countWords('one two three'), 3)
41 assert.equal(countWords(' leading and trailing '), 3)
42 assert.equal(countWords('line\nbreaks\tand\ttabs'), 4)
43})
44
45// Punctuation is where whitespace-splitting and real segmentation
46// diverge, and where a person notices: an em-dashed clause is two
47// words, not one.
48test('punctuation is not a word', () => {
49 assert.equal(countWords('yes — no'), 2)
50 assert.equal(countWords('one, two, three.'), 3)
51 assert.equal(countWords('"quoted"'), 1)
52})
53
54// The reason this uses Intl.Segmenter at all. Whitespace-splitting
55// finds no boundary in these at all and would report 1.
56test('a language without spaces between words still counts', (t) => {
57 if (typeof Intl === 'undefined' || typeof Intl.Segmenter !== 'function') {
58 t.skip('this platform has no Intl.Segmenter, so only the whitespace fallback exists')
59 return
60 }
61 assert.ok(countWords('今日は良い天気です') > 1, 'Japanese counted as one word')
62 assert.ok(countWords('这是一个句子') > 1, 'Chinese counted as one word')
63})
64
65test('the document count walks every block', () => {
66 const state = EditorState.create({ doc: docOf('one two', 'three four five'), schema })
67 const c = counts(state)
68 assert.equal(c.words, 5)
69 assert.equal(c.paragraphs, 2)
70})
71
72// The blocks are joined with a space, so the last word of one paragraph
73// and the first of the next are two words rather than one. Without
74// that, "one" and "two" below would be counted as "onetwo".
75test('a paragraph boundary is a word boundary', () => {
76 const state = EditorState.create({ doc: docOf('one', 'two'), schema })
77 assert.equal(counts(state).words, 2)
78})
79
80test('characters are counted with and without spaces', () => {
81 const state = EditorState.create({ doc: docOf('ab cd'), schema })
82 const c = counts(state)
83 assert.equal(c.characters, 5)
84 assert.equal(c.charactersNoSpaces, 4)
85})
86
87test('no selection means no selection count, not a zero', () => {
88 // A zero would be indistinguishable from "nothing is selected", which
89 // is why the dialog hides the row rather than showing 0.
90 const state = EditorState.create({ doc: docOf('one two'), schema })
91 assert.equal(counts(state).selection, null)
92})
93