@hraness/sponge 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,7 @@ export declare function textQuote(source: string, start: number, end: number): T
20
20
  export declare function locateQuote(source: string, quote: string): TextQuote | undefined;
21
21
  /** A stored selector still names its exact text in `source`. */
22
22
  export declare function anchorHolds(source: string, anchor: TextQuote): boolean;
23
+ /** Word count: space-separated tokens, with each run of Han or kana counted as half its characters, rounded up. */
23
24
  export declare function words(value: string): number;
24
25
  /**
25
26
  * Every occurrence of `quote` in `source` ignoring inline Markdown and case, within optional raw bounds.
@@ -107,9 +107,19 @@ export function anchorHolds(source, anchor) {
107
107
  anchor.end > anchor.start &&
108
108
  source.slice(anchor.start, anchor.end) === anchor.exact);
109
109
  }
110
+ /** Han and kana are written without spaces; about two characters make a word. */
111
+ const unspaced = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/gu;
112
+ /** Word count: space-separated tokens, with each run of Han or kana counted as half its characters, rounded up. */
110
113
  export function words(value) {
111
114
  const t = foldText(value);
112
- return t ? t.split(' ').length : 0;
115
+ if (!t)
116
+ return 0;
117
+ let n = 0;
118
+ for (const token of t.split(' ')) {
119
+ const cjk = token.match(unspaced)?.length ?? 0;
120
+ n += cjk ? Math.ceil(cjk / 2) + (token.replace(unspaced, '').match(/[\p{L}\p{N}]/u) ? 1 : 0) : 1;
121
+ }
122
+ return n;
113
123
  }
114
124
  /**
115
125
  * Rendered-text projection for snapping a near-verbatim quote: drops inline Markdown syntax (link targets, brackets,
@@ -317,10 +317,13 @@ export function snapQuote(source, text, within) {
317
317
  export function trimQuote(text, max = L.quoteWords) {
318
318
  if (words(text) <= max)
319
319
  return text;
320
- const ends = [...text.matchAll(/\S+/g)].map((m) => m.index + m[0].length);
321
- let n = Math.min(max, ends.length), out = text;
320
+ // Cut after a space-separated token or, inside unspaced Han and kana, after any character.
321
+ const ends = [
322
+ ...new Set([...text.matchAll(/\S+/g), ...text.matchAll(/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/gu)].map((m) => m.index + m[0].length)),
323
+ ].sort((a, b) => a - b);
324
+ let n = ends.length, out = text;
322
325
  do {
323
- out = text.slice(0, ends[n - 1]).replace(/[\s,;:(\[\u2014\u2013-]+$/, '');
326
+ out = text.slice(0, ends[n - 1]).replace(/[\s,;:(\[\u2014\u2013\u3001\uff0c-]+$/, '');
324
327
  n--;
325
328
  } while (n > 0 && words(out) > max);
326
329
  return out;
@@ -49,8 +49,12 @@ const keys = (v, allowed) => Object.keys(v).every((k) => allowed.includes(k));
49
49
  const str = (v, max = 2000) => typeof v === 'string' && v.trim().length > 0 && v.length <= max && !/[\u0000-\u0008\u000b-\u001f\u007f]/.test(v);
50
50
  /** v1: sentence terminators followed by more text; abbreviations are rare enough in a 155-character lead to reject. */
51
51
  const sentencesV1 = (v) => foldText(v).split(/(?<=[.!?])\s+(?=\S)/).length;
52
- /** v2: a terminator (and closing quotes) followed by a capital or digit, the same split Jungle's first-sentence lead uses, so `e.g. this` stays one sentence. */
53
- const sentences = (v) => foldText(v).split(/(?<=[.!?]["')]*)\s+(?=["(]?[A-Z0-9])/).length;
52
+ /**
53
+ * v2: a terminator (and closing quotes) followed by a capital or digit, the split Jungle's first-sentence lead uses, so
54
+ * `e.g. this` stays one sentence. A period after an initial or a common title does not end a sentence: `Lucas C.
55
+ * Cordeiro`, `F. Zakaria`, `U.S. Senate`, `Dr. Lind`.
56
+ */
57
+ const sentences = (v) => foldText(v).split(/(?<=(?:(?<!\b(?:[A-Z]|Mr|Mrs|Ms|Dr|Prof|St|Jr|Sr|vs|No|Inc|Ltd|Co))\.|[!?])["')]*)\s+(?=["(]?[A-Z0-9])/).length;
54
58
  /** Parse an untrusted digest (v1 or v2) into its shape, or report why not. */
55
59
  export function parseDigest(value) {
56
60
  const issues = [], bad = (path, code = 'shape') => {
@@ -27,14 +27,14 @@
27
27
  }
28
28
  ],
29
29
  "build": {
30
- "asrSourceSha256": "ca98fb111b0d2b26ca697a6c0d55d76f7d2b5b752bff8fc053ae19389d16a52c",
30
+ "asrSourceSha256": "3b32718d4626aff97f2c3e043c9fd020f237adc7f7e5508e7d7b99a555762982",
31
31
  "clangVersion": "Apple clang version 21.0.0 (clang-2100.1.1.101)",
32
- "embeddingSourceSha256": "af6d6c01934d64427f2a9cb36f83edaea372f6bf2e522166b9e86e3b15f72784",
32
+ "embeddingSourceSha256": "66f008ea29050d114b0df74337124bac8f6cdba9732e257cc70ad85eb9ebee6f",
33
33
  "swiftVersion": "Apple Swift version 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)"
34
34
  },
35
35
  "minimumMacOS": "26.0",
36
- "packageVersion": "0.3.4",
36
+ "packageVersion": "0.3.5",
37
37
  "platform": "darwin",
38
38
  "schemaVersion": 1,
39
- "sourceCommit": "4e42216e86da07224ec81512c2efa0a710e38500"
39
+ "sourceCommit": "49b3f7f100cdbcbf5f52418d28c53dc13f198424"
40
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/sponge",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "A local library for captured articles and verified offline assets.",
5
5
  "repository": {
6
6
  "type": "git",