@oh-my-pi/pi-utils 18.11.1 → 18.11.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.11.3] - 2026-10-16
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Markdown fence info-string parsing to keep backtick-containing backtick openers as paragraph text while accepting backticks in tilde info strings and Unicode line separators in either fence's info string ([#15928](https://github.com/can1357/oh-my-pi/pull/15928) by [@jchanghong023](https://github.com/jchanghong023)).
10
+ - Fixed `parseHTML` treating `<link>` in RSS/Atom documents as an HTML void element, which dropped the URL text of RSS `<link>…</link>` ([#15954](https://github.com/can1357/oh-my-pi/issues/15954))
11
+
12
+ ## [18.11.2] - 2026-10-16
13
+
14
+ ### Added
15
+
16
+ - Exported `sqliteFileIdentity()`, so a caller holding a long-lived SQLite connection can tell when its path now names a different file (e.g. after a corrupt store was quarantined and replaced) ([#14361](https://github.com/can1357/oh-my-pi/pull/14361) by [@F0Rextasy](https://github.com/F0Rextasy))
17
+ - Added `slugify()`, the shared lowercase ASCII kebab-case helper with an optional `maxLength` ([#14283](https://github.com/can1357/oh-my-pi/pull/14283) by [@jaredlyon](https://github.com/jaredlyon))
18
+
19
+ ### Fixed
20
+
21
+ - Fixed an unhandled rejection when a subprocess is spawned with an already-aborted signal ([#15750](https://github.com/can1357/oh-my-pi/pull/15750) by [@harshaygadekar](https://github.com/harshaygadekar))
22
+
5
23
  ## [18.10.0] - 2026-10-12
6
24
 
7
25
  ### Added
@@ -31,6 +31,7 @@ export * as ptree from "./ptree.js";
31
31
  export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree.js";
32
32
  export * from "./runtime-install.js";
33
33
  export * from "./sanitize-text.js";
34
+ export * from "./slugify.js";
34
35
  export * from "./snowflake.js";
35
36
  export * from "./sqlite.js";
36
37
  export * from "./stderr-guard.js";
@@ -0,0 +1,4 @@
1
+ /** Lowercase ASCII kebab slug: [^a-z0-9]+ → "-", edge dashes trimmed; optional maxLength truncates then re-trims trailing dashes; throws RangeError unless maxLength is a non-negative integer. Returns "" when nothing survives. */
2
+ export declare function slugify(input: string, options?: {
3
+ maxLength?: number;
4
+ }): string;
@@ -1,5 +1,17 @@
1
1
  /** Shared SQLite opening, error attribution, and result-code classification for persistent stores. */
2
2
  import { Database } from "bun:sqlite";
3
+ /**
4
+ * A store's identity as `dev:ino:birthtimeMs`, `null` when the path holds no
5
+ * file, or `undefined` when the stat itself failed and no conclusion is safe.
6
+ */
7
+ export type SqliteFileIdentity = string | null | undefined;
8
+ /**
9
+ * Identity of the file currently at `dbPath`. Quarantine moves a corrupt store
10
+ * aside and recreates one at the same path, so a connection cached on the path
11
+ * alone keeps writing to the unlinked inode: callers that hold a long-lived
12
+ * handle compare this before reusing it.
13
+ */
14
+ export declare function sqliteFileIdentity(dbPath: string): SqliteFileIdentity;
3
15
  /** Controls opt-in replacement of an unrecoverably corrupt SQLite store. */
4
16
  export interface SqliteOpenOptions {
5
17
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-utils",
3
- "version": "18.11.1",
3
+ "version": "18.11.3",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "cli",
@@ -57,7 +57,7 @@
57
57
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
58
58
  },
59
59
  "dependencies": {
60
- "@oh-my-pi/pi-natives": "18.11.1"
60
+ "@oh-my-pi/pi-natives": "18.11.3"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/bun": "^1.3.14"
package/src/dom/parser.ts CHANGED
@@ -366,7 +366,12 @@ export function parseInto(html: string, document: Document, root: Document | Doc
366
366
  index = match ? closePattern.lastIndex : html.length;
367
367
  continue;
368
368
  }
369
- if (!VOID_ELEMENTS[parsed.name] && !(namespace === SVG_NAMESPACE && parsed.selfClosing)) {
369
+ // Feed documents follow XML rules: only an explicit `/>` closes immediately, so RSS `<link>url</link>`
370
+ // keeps its text instead of being treated as an HTML void element.
371
+ const isVoid = xmlLike
372
+ ? parsed.selfClosing
373
+ : VOID_ELEMENTS[parsed.name] || (namespace === SVG_NAMESPACE && parsed.selfClosing);
374
+ if (!isVoid) {
370
375
  stack.push(element);
371
376
  parent = element;
372
377
  }
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ export * as ptree from "./ptree";
31
31
  export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
32
32
  export * from "./runtime-install";
33
33
  export * from "./sanitize-text";
34
+ export * from "./slugify";
34
35
  export * from "./snowflake";
35
36
  export * from "./sqlite";
36
37
  export * from "./stderr-guard";
@@ -775,7 +775,10 @@ function splitTableRow(line: string): string[] {
775
775
  }
776
776
 
777
777
  function isFence(line: string): RegExpExecArray | null {
778
- return /^ {0,3}(`{3,}|~{3,})(.*?)(?:\n|$)$/.exec(line);
778
+ // Unicode line separators may occur in the info string.
779
+ const match = /^ {0,3}(`{3,}|~{3,})([^\n]*?)(?:\n|$)$/.exec(line);
780
+ // Backtick fences cannot contain backticks in their info string.
781
+ return match && match[1]!.startsWith("`") && match[2]!.includes("`") ? null : match;
779
782
  }
780
783
  function isHeading(line: string): boolean {
781
784
  return /^ {0,3}#{1,6}(?:\s|$)/.test(line);
package/src/ptree.ts CHANGED
@@ -443,7 +443,9 @@ export class ChildProcess<In extends InMask = InMask> {
443
443
  // ── Output helpers ───────────────────────────────────────────────────
444
444
 
445
445
  async #throwIfAborted(): Promise<void> {
446
- const exitReason = this.exitReason;
446
+ // A kill() issued before Bun reports the root's exit only records the
447
+ // reason as pending; output readers that do not wait for exit must still see it.
448
+ const exitReason = this.#exitReason ?? this.#exitReasonPending;
447
449
  if (!exitReason?.aborted) return;
448
450
  if (this.#terminating) await this.#terminating;
449
451
  throw exitReason;
@@ -589,10 +591,11 @@ export class ChildProcess<In extends InMask = InMask> {
589
591
  // ── Signal / timeout ─────────────────────────────────────────────────
590
592
 
591
593
  attachSignal(signal: AbortSignal): void {
594
+ const cleanup = this.#exited.catch(() => {});
592
595
  const onAbort = () => this.kill(new AbortError(signal.reason, "<cancelled>"));
593
596
  if (signal.aborted) return void onAbort();
594
597
  signal.addEventListener("abort", onAbort, { once: true });
595
- this.#exited.catch(() => {}).finally(() => signal.removeEventListener("abort", onAbort));
598
+ cleanup.finally(() => signal.removeEventListener("abort", onAbort));
596
599
  }
597
600
 
598
601
  #clearTimeout(): void {
package/src/slugify.ts ADDED
@@ -0,0 +1,11 @@
1
+ /** Lowercase ASCII kebab slug: [^a-z0-9]+ → "-", edge dashes trimmed; optional maxLength truncates then re-trims trailing dashes; throws RangeError unless maxLength is a non-negative integer. Returns "" when nothing survives. */
2
+ export function slugify(input: string, options?: { maxLength?: number }): string {
3
+ if (options?.maxLength !== undefined && !(Number.isInteger(options.maxLength) && options.maxLength >= 0)) {
4
+ throw new RangeError(`slugify maxLength must be a non-negative integer, got ${options.maxLength}`);
5
+ }
6
+ const slug = input
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9]+/g, "-")
9
+ .replace(/^-+|-+$/g, "");
10
+ return options?.maxLength === undefined ? slug : slug.slice(0, options.maxLength).replace(/-+$/g, "");
11
+ }
package/src/sqlite.ts CHANGED
@@ -11,7 +11,11 @@ const BUSY_MAX_ATTEMPTS = 4;
11
11
  const BUSY_BASE_DELAY_MS = 100;
12
12
  const SQLITE_STORE_SUFFIXES = ["-wal", "-shm", "-journal", ""];
13
13
 
14
- type SqliteFileIdentity = string | null | undefined;
14
+ /**
15
+ * A store's identity as `dev:ino:birthtimeMs`, `null` when the path holds no
16
+ * file, or `undefined` when the stat itself failed and no conclusion is safe.
17
+ */
18
+ export type SqliteFileIdentity = string | null | undefined;
15
19
 
16
20
  /**
17
21
  * Corrupt handles held open by in-process openers awaiting recovery, keyed by
@@ -77,7 +81,13 @@ class SqliteAttemptFailure extends Error {
77
81
  }
78
82
  }
79
83
 
80
- function sqliteFileIdentity(dbPath: string): SqliteFileIdentity {
84
+ /**
85
+ * Identity of the file currently at `dbPath`. Quarantine moves a corrupt store
86
+ * aside and recreates one at the same path, so a connection cached on the path
87
+ * alone keeps writing to the unlinked inode: callers that hold a long-lived
88
+ * handle compare this before reusing it.
89
+ */
90
+ export function sqliteFileIdentity(dbPath: string): SqliteFileIdentity {
81
91
  try {
82
92
  const stat = fs.statSync(dbPath);
83
93
  return `${stat.dev}:${stat.ino}:${stat.birthtimeMs}`;