@hraness/sponge 0.3.1 → 0.3.2

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.
@@ -7,6 +7,8 @@ export interface TextQuote {
7
7
  start: number;
8
8
  end: number;
9
9
  }
10
+ /** Spoken text without transcript cues, whitespace collapsed: what a reader quotes from a transcript. */
11
+ export declare function stripCues(raw: string): string;
10
12
  export declare function foldText(value: string): string;
11
13
  /** Every occurrence of `quote` in `source`, compared after folding. */
12
14
  export declare function locateAll(source: string, quote: string): {
@@ -1,9 +1,20 @@
1
1
  const context = 32;
2
2
  const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
3
3
  const escapable = /^[\\`*_{}[\]()#+\-.!|>~<"']$/;
4
+ /** A transcript cue at a line start: `- [00:02] ` (clipped captions) or `[00:00:02] ` (Sponge transcripts). */
5
+ const cue = /^[ \t]*(?:[-*+][ \t]+)?\[(?:\d{1,2}:)?\d{1,2}:\d{2}\][ \t]+/;
6
+ /** Spoken text without transcript cues, whitespace collapsed: what a reader quotes from a transcript. */
7
+ export function stripCues(raw) {
8
+ return raw
9
+ .split('\n')
10
+ .map((line) => line.replace(cue, ''))
11
+ .join('\n')
12
+ .replace(/\s+/g, ' ')
13
+ .trim();
14
+ }
4
15
  /**
5
16
  * Fold the differences a reader cannot see: Markdown escapes, curly quotes, dashes, runs of
6
- * whitespace and Unicode normalization. Folding works on grapheme clusters, each NFC-normalized;
17
+ * whitespace, line-start transcript cues and Unicode normalization. Folding works on grapheme clusters, each NFC-normalized;
7
18
  * every folded code unit of a cluster maps to the cluster's raw bounds, so `map[i]` and `ends[i]`
8
19
  * bound the raw text of folded unit i and matches map back exactly.
9
20
  */
@@ -19,6 +30,17 @@ function fold(raw) {
19
30
  };
20
31
  for (let c = 0; c < clusters.length; c++) {
21
32
  const { segment, index } = clusters[c];
33
+ // A transcript cue reads as the space between caption lines, so a quote of the speech spans them.
34
+ const marker = (index === 0 || raw[index - 1] === '\n') && cue.exec(raw.slice(index, index + 32));
35
+ if (marker) {
36
+ const stop = index + marker[0].length;
37
+ if (!space && text.length)
38
+ emit(' ', index, stop);
39
+ space = true;
40
+ while (c + 1 < clusters.length && clusters[c + 1].index < stop)
41
+ c++;
42
+ continue;
43
+ }
22
44
  // An escaped character maps to its backslash, so a match starting there keeps the escape.
23
45
  let ch = segment, end = index + segment.length;
24
46
  if (segment === '\\' && c + 1 < clusters.length && escapable.test(clusters[c + 1].segment)) {
@@ -98,6 +120,12 @@ function rendered(raw) {
98
120
  for (let i = 0; i < raw.length; i++) {
99
121
  let ch = raw[i], at = i;
100
122
  if (lineStart) {
123
+ const c = cue.exec(raw.slice(i, i + 32));
124
+ if (c) {
125
+ i += c[0].length - 1;
126
+ lineStart = false;
127
+ continue;
128
+ }
101
129
  const m = /^[ \t]*(?:>[ \t]?|#{1,6}[ \t]+|[-*+][ \t]+|\d{1,3}[.)][ \t]+)*/.exec(raw.slice(i, i + 64));
102
130
  if (m[0].length) {
103
131
  i += m[0].length - 1;
@@ -791,5 +791,5 @@ export const usageOf = (name) => command(name)
791
791
  .join('\n');
792
792
  export function commandHelp(name) {
793
793
  const c = command(name), flags = Object.keys(c.flags);
794
- return `sponge ${c.name} — ${c.summary}\n\nUsage:\n${c.usage.map(row).join('').replace(/^ /gm, ' sponge ')}${flags.length ? `\nFlags: ${flags.map((f) => '--' + f).join(' ')}\n` : ''}\nGlobal flags: --library /absolute/path --json --debug\n`;
794
+ return `sponge ${c.name} — ${c.summary}\n\nUsage:\n${c.usage.map(([synopsis, description]) => row([`sponge ${synopsis}`, description])).join('')}${flags.length ? `\nFlags: ${flags.map((f) => '--' + f).join(' ')}\n` : ''}\nGlobal flags: --library /absolute/path --json --debug\n`;
795
795
  }
@@ -1,4 +1,4 @@
1
- import { foldText, locateAll, locateLoose, words } from '../annotations/quote.js';
1
+ import { foldText, locateAll, locateLoose, stripCues, words } from '../annotations/quote.js';
2
2
  import { checkDigest, digestLimits as L, forms, parseDigest, } from './schema.js';
3
3
  import { SpongeError } from '../errors.js';
4
4
  import { sha256 } from '../util/hash.js';
@@ -97,7 +97,9 @@ function lastSentenceEnd(window) {
97
97
  best = m.index + m[0].length;
98
98
  return best;
99
99
  }
100
- const display = (p) => `[${p.id}] ${collapse(p.text)}`;
100
+ const display = (p) => `[${p.id}] ${stripCues(p.text)}`;
101
+ /** A raw source span as a quote: transcript cues inside it are dropped, so the quote reads as the speech. */
102
+ const spoken = (raw) => (raw.includes('\n') && stripCues(raw) !== collapse(raw) ? stripCues(raw) : raw);
101
103
  /** Pack passages into sections of at most `budget` display characters, preferring heading boundaries past half a budget. */
102
104
  export function sectionsOf(list, budget) {
103
105
  const out = [];
@@ -295,7 +297,7 @@ export function snapQuote(source, text, within) {
295
297
  const folded = locateAll(source, text);
296
298
  if (folded.length) {
297
299
  const m = pick(folded);
298
- return { text: m ? source.slice(m.start, m.end) : text, match: 'folded' };
300
+ return { text: m ? spoken(source.slice(m.start, m.end)) : text, match: 'folded' };
299
301
  }
300
302
  const loose = (within && pick(locateLoose(source, text, within))) || pick(locateLoose(source, text));
301
303
  if (!loose)
@@ -306,7 +308,7 @@ export function snapQuote(source, text, within) {
306
308
  start--;
307
309
  while (end < source.length && /[*_`]/.test(source[end]))
308
310
  end++;
309
- return { text: source.slice(start, end), match: 'snapped' };
311
+ return { text: spoken(source.slice(start, end)), match: 'snapped' };
310
312
  }
311
313
  /**
312
314
  * A located quote over the word limit keeps its leading words: a prefix of a source span is still verbatim. Trailing
@@ -16,6 +16,8 @@ declare const parents: {
16
16
  readonly 'not-digest': "unsupported";
17
17
  readonly malformed: "invalid-input";
18
18
  readonly 'no-capture': "invalid-input";
19
+ readonly 'parent-missing': "invalid-input";
20
+ readonly 'target-exists': "invalid-input";
19
21
  };
20
22
  export type DetailCode = keyof typeof parents;
21
23
  export type SpongeCode = ErrorCode | DetailCode;
@@ -30,6 +30,8 @@ const parents = {
30
30
  'not-digest': 'unsupported',
31
31
  malformed: 'invalid-input',
32
32
  'no-capture': 'invalid-input',
33
+ 'parent-missing': 'invalid-input',
34
+ 'target-exists': 'invalid-input',
33
35
  };
34
36
  export const indexCodes = [
35
37
  'invalid-index',
@@ -112,8 +112,18 @@ export function copyVerified(source, target, record) {
112
112
  export function newDirectory(path) {
113
113
  if (resolve(path) !== path)
114
114
  throw new SpongeError('invalid-input');
115
- safeDirectory(dirname(path));
116
- mkdirSync(path, { mode: 0o700 });
115
+ try {
116
+ safeDirectory(dirname(path));
117
+ mkdirSync(path, { mode: 0o700 });
118
+ }
119
+ catch (error) {
120
+ const code = error.code;
121
+ if (code === 'ENOENT')
122
+ throw new SpongeError('parent-missing', { cause: error });
123
+ if (code === 'EEXIST')
124
+ throw new SpongeError('target-exists', { cause: error });
125
+ throw error;
126
+ }
117
127
  const stat = lstatSync(path);
118
128
  return { root: path, device: stat.dev, inode: stat.ino };
119
129
  }
@@ -27,14 +27,14 @@
27
27
  }
28
28
  ],
29
29
  "build": {
30
- "asrSourceSha256": "94b9303afcf9304e695c37411cef065e9cc45a31e0e51cd171819cbfc9ee3955",
30
+ "asrSourceSha256": "8bd77ebffabae786902304eacb983fc950f70c981fb1538696517d85052c9407",
31
31
  "clangVersion": "Apple clang version 21.0.0 (clang-2100.1.1.101)",
32
- "embeddingSourceSha256": "eed7e110bc888af99e0d6e614275cea7a7dbaa56f88d3de9bdd04cc9ff4cfa45",
32
+ "embeddingSourceSha256": "2b0cd2b9c52c9dee578a3be50a443d528d5204d47f3f9ba750f6a1f37a191e78",
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.1",
36
+ "packageVersion": "0.3.2",
37
37
  "platform": "darwin",
38
38
  "schemaVersion": 1,
39
- "sourceCommit": "b8975aff1dc06e398c3f78811e76f4ff875729a1"
39
+ "sourceCommit": "e9eb0613365859c5809a05e8ad909504ac6e8d66"
40
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/sponge",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "A local library for captured articles and verified offline assets.",
5
5
  "repository": {
6
6
  "type": "git",