tests/wordcount.test.ts
Rama main · réplica de lectura del repositorio original.
// The word count (shared/wordCount.ts) — one rule for the author's status bar
// and the visitor's "N min read", because two would drift and the first anyone
// would notice is a published article claiming a reading time its author never
// saw.
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { countNoteWords, countWords, noteProse, readingMinutes } from "../shared/wordCount.ts";
describe("noteProse", () => {
it("drops frontmatter in both formats", () => {
assert.equal(noteProse("---\ntitle: A\ntags: [x]\n---\nhello there\n").trim(), "hello there");
// A .tex note's frontmatter is a comment block that still compiles.
assert.equal(noteProse("%---\n%title: A\n%---%\nhello there\n").trim(), "hello there");
});
it("drops code, not the prose around it", () => {
const md = "before\n\n```js\nconst x = 1;\nlet y = 2;\n```\n\nafter\n";
const prose = noteProse(md);
assert.ok(prose.includes("before") && prose.includes("after"));
assert.ok(!prose.includes("const"), "code fence survived the strip");
});
it("drops math but keeps the sentence it sits in", () => {
const prose = noteProse("The identity $e^{i\\pi}+1=0$ closes the argument.");
assert.ok(prose.includes("identity") && prose.includes("closes"));
assert.ok(!prose.includes("pi"));
});
it("keeps a link's label and loses its target", () => {
assert.match(noteProse("see [the paper](https://example.com/a/b) now"), /see the paper now/);
assert.match(noteProse("see [[Notes/Heat|the paper]] now"), /see the paper now/);
assert.match(noteProse("see [[Heat Equation]] now"), /see Heat Equation now/);
});
it("keeps the words on a heading or list line, losing only the marks", () => {
const prose = noteProse("## A Real Heading\n\n- first item\n- second item\n");
assert.ok(prose.includes("A Real Heading"));
assert.ok(prose.includes("first item"));
assert.ok(!prose.includes("##") && !prose.includes("- first"));
});
it("drops %%comments%% — the syntax this product actually hides", () => {
assert.ok(!noteProse("visible %%hidden note to self%% visible").includes("hidden"));
});
});
describe("countWords", () => {
it("counts English", () => {
assert.equal(countWords("one two three"), 3);
assert.equal(countWords(" "), 0);
assert.equal(countWords(""), 0);
});
it("does not count punctuation as words", () => {
// The old whitespace split counted "—" and "·" as words; the segmenter
// knows they are not word-like.
assert.equal(countWords("one — two · three"), 3);
});
it("counts Arabic by word, not by whitespace run", () => {
assert.equal(countWords("الحمد لله رب العالمين"), 4);
});
it("counts a pointed Arabic word as one word — a letter with its harakat is one grapheme", () => {
// The same phrase, bare and fully vowelled: the count must not move.
const bare = "الحمد لله رب العالمين";
const pointed = "الْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ";
assert.equal(countWords(pointed), countWords(bare));
assert.equal(countWords(pointed), 4);
// Tanwin on the last letter, shadda mid-word, a dagger alif: still one
// word each, so three.
assert.equal(countWords("كتابٌ مُحَمَّدٌ ذَٰلِكَ"), 3);
// A tatweel stretch inside a word joins it rather than splitting it.
assert.equal(countWords("الـعـربـية جميلة"), 2);
// A mark left standing on its own (after a space) is not a word.
assert.equal(countWords("كلمة ً أخرى"), 2);
assert.equal(countWords("ـ ّ"), 0);
});
it("counts CJK, where whitespace splitting says 1", () => {
// The clearest case for the segmenter: this has no spaces at all.
assert.ok(countWords("这是一个测试") > 1, "CJK counted as a single word");
});
});
describe("the whole note", () => {
it("counts prose and ignores the furniture around it", () => {
const note = [
"---",
"title: Test",
"publish: true",
"---",
"# Heading",
"",
"One two three four five.",
"",
"```sh",
"echo this is not prose",
"```",
].join("\n");
// "Heading" + the five words of the sentence.
assert.equal(countNoteWords(note), 6);
});
it("floors at one minute for a note with prose, and zero for one without", () => {
// Zero is a different fact from "very short", and the blog relies on it:
// an empty note has an empty excerpt and no reading time.
assert.equal(readingMinutes(0), 0);
assert.equal(readingMinutes(1), 1);
assert.equal(readingMinutes(200), 1);
assert.equal(readingMinutes(201), 2);
});
});