@willbooster/shared-lib 10.15.0 → 10.16.1

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.
@@ -0,0 +1,2 @@
1
+ "use strict";async function e(e,t,n){if(!Number.isInteger(t)||t<1)throw RangeError(`concurrency must be a positive integer: ${t}`);let r=0,i;if(await Promise.all(Array.from({length:t},async()=>{for(;!i&&r<e.length;)try{await n(e[r++])}catch(e){i??={error:e}}})),i)throw i.error}exports.forEachConcurrently=e;
2
+ //# sourceMappingURL=concurrency.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.cjs","names":[],"sources":["../src/concurrency.ts"],"sourcesContent":["/**\n * Runs `action` for every item with at most `concurrency` actions in flight, starting the next item as soon as any\n * action settles, so one slow item does not hold back a whole batch. After an action rejects, no further item is\n * started, and once every started action has settled, the returned promise rejects with the first error.\n * The returned promise rejects with a `RangeError` when `concurrency` is not a positive integer.\n */\nexport async function forEachConcurrently<T>(\n items: readonly T[],\n concurrency: number,\n action: (item: T) => Promise<unknown>\n): Promise<void> {\n if (!Number.isInteger(concurrency) || concurrency < 1) {\n throw new RangeError(`concurrency must be a positive integer: ${concurrency}`);\n }\n let nextIndex = 0;\n let failure: { error: unknown } | undefined;\n await Promise.all(\n Array.from({ length: concurrency }, async () => {\n while (!failure && nextIndex < items.length) {\n try {\n await action(items[nextIndex++] as T);\n } catch (error) {\n failure ??= { error };\n }\n }\n })\n );\n if (failure) throw failure.error;\n}\n"],"mappings":"aAMA,eAAsB,EACpB,EACA,EACA,EACe,CACf,GAAI,CAAC,OAAO,UAAU,CAAW,GAAK,EAAc,EAClD,MAAU,WAAW,2CAA2C,GAAa,EAE/E,IAAI,EAAY,EACZ,EAYJ,GAXA,MAAM,QAAQ,IACZ,MAAM,KAAK,CAAE,OAAQ,CAAY,EAAG,SAAY,CAC9C,KAAO,CAAC,GAAW,EAAY,EAAM,QACnC,GAAI,CACF,MAAM,EAAO,EAAM,IAAiB,CACtC,OAAS,EAAO,CACd,IAAY,CAAE,OAAM,CACtB,CAEJ,CAAC,CACH,EACI,EAAS,MAAM,EAAQ,KAC7B"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Runs `action` for every item with at most `concurrency` actions in flight, starting the next item as soon as any
3
+ * action settles, so one slow item does not hold back a whole batch. After an action rejects, no further item is
4
+ * started, and once every started action has settled, the returned promise rejects with the first error.
5
+ * The returned promise rejects with a `RangeError` when `concurrency` is not a positive integer.
6
+ */
7
+ export declare function forEachConcurrently<T>(items: readonly T[], concurrency: number, action: (item: T) => Promise<unknown>): Promise<void>;
@@ -0,0 +1,2 @@
1
+ async function e(e,t,n){if(!Number.isInteger(t)||t<1)throw RangeError(`concurrency must be a positive integer: ${t}`);let r=0,i;if(await Promise.all(Array.from({length:t},async()=>{for(;!i&&r<e.length;)try{await n(e[r++])}catch(e){i??={error:e}}})),i)throw i.error}export{e as forEachConcurrently};
2
+ //# sourceMappingURL=concurrency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.js","names":[],"sources":["../src/concurrency.ts"],"sourcesContent":["/**\n * Runs `action` for every item with at most `concurrency` actions in flight, starting the next item as soon as any\n * action settles, so one slow item does not hold back a whole batch. After an action rejects, no further item is\n * started, and once every started action has settled, the returned promise rejects with the first error.\n * The returned promise rejects with a `RangeError` when `concurrency` is not a positive integer.\n */\nexport async function forEachConcurrently<T>(\n items: readonly T[],\n concurrency: number,\n action: (item: T) => Promise<unknown>\n): Promise<void> {\n if (!Number.isInteger(concurrency) || concurrency < 1) {\n throw new RangeError(`concurrency must be a positive integer: ${concurrency}`);\n }\n let nextIndex = 0;\n let failure: { error: unknown } | undefined;\n await Promise.all(\n Array.from({ length: concurrency }, async () => {\n while (!failure && nextIndex < items.length) {\n try {\n await action(items[nextIndex++] as T);\n } catch (error) {\n failure ??= { error };\n }\n }\n })\n );\n if (failure) throw failure.error;\n}\n"],"mappings":"AAMA,eAAsB,EACpB,EACA,EACA,EACe,CACf,GAAI,CAAC,OAAO,UAAU,CAAW,GAAK,EAAc,EAClD,MAAU,WAAW,2CAA2C,GAAa,EAE/E,IAAI,EAAY,EACZ,EAYJ,GAXA,MAAM,QAAQ,IACZ,MAAM,KAAK,CAAE,OAAQ,CAAY,EAAG,SAAY,CAC9C,KAAO,CAAC,GAAW,EAAY,EAAM,QACnC,GAAI,CACF,MAAM,EAAO,EAAM,IAAiB,CACtC,OAAS,EAAO,CACd,IAAY,CAAE,OAAM,CACtB,CAEJ,CAAC,CACH,EACI,EAAS,MAAM,EAAQ,KAC7B"}
package/dist/humanize.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";function e(e,{base:t=1e3,units:n=[`K`,`M`,`B`,`T`,`P`]}={}){if(e<t)return e.toString();let r=-1;for(;e>=t&&r<n.length-1;)e/=t,r++;return e.toFixed(2)+(n[r]??``)}exports.humanizeNumber=e;
1
+ "use strict";function e(e,{base:t=1e3,units:n=[`K`,`M`,`B`,`T`,`P`]}={}){if(e<t)return e.toString();let r=-1;for(;e>=t&&r<n.length-1;)e/=t,r++;return e.toFixed(2)+(n[r]??``)}function t(e){let t=Math.round(e/100)/10;if(t<60)return`${t.toFixed(1)}秒`;let n=Math.round(e/1e3);return`${Math.floor(n/60)}分${n%60}秒`}exports.formatElapsedTimeInJapanese=t,exports.humanizeNumber=e;
2
2
  //# sourceMappingURL=humanize.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"humanize.cjs","names":[],"sources":["../src/humanize.ts"],"sourcesContent":["export function humanizeNumber(value: number, { base = 1000, units = ['K', 'M', 'B', 'T', 'P'] } = {}): string {\n if (value < base) {\n return value.toString();\n }\n\n let unitIndex = -1;\n while (value >= base && unitIndex < units.length - 1) {\n value /= base;\n unitIndex++;\n }\n\n return value.toFixed(2) + (units[unitIndex] ?? '');\n}\n"],"mappings":"aAAA,SAAgB,EAAe,EAAe,CAAE,OAAO,IAAM,QAAQ,CAAC,IAAK,IAAK,IAAK,IAAK,GAAG,GAAM,CAAC,EAAW,CAC7G,GAAI,EAAQ,EACV,OAAO,EAAM,SAAS,EAGxB,IAAI,EAAY,GAChB,KAAO,GAAS,GAAQ,EAAY,EAAM,OAAS,GACjD,GAAS,EACT,IAGF,OAAO,EAAM,QAAQ,CAAC,GAAK,EAAM,IAAc,GACjD"}
1
+ {"version":3,"file":"humanize.cjs","names":[],"sources":["../src/humanize.ts"],"sourcesContent":["export function humanizeNumber(value: number, { base = 1000, units = ['K', 'M', 'B', 'T', 'P'] } = {}): string {\n if (value < base) {\n return value.toString();\n }\n\n let unitIndex = -1;\n while (value >= base && unitIndex < units.length - 1) {\n value /= base;\n unitIndex++;\n }\n\n return value.toFixed(2) + (units[unitIndex] ?? '');\n}\n\n/**\n * Formats a duration in Japanese: to a tenth of a second under a minute (`59.9秒`), otherwise in whole minutes and\n * seconds (`1分0秒`).\n */\nexport function formatElapsedTimeInJapanese(milliseconds: number): string {\n // Round to the displayed precision before choosing the format, or 59.98s would be shown as `60.0秒`.\n const roundedTenths = Math.round(milliseconds / 100) / 10;\n if (roundedTenths < 60) return `${roundedTenths.toFixed(1)}秒`;\n\n // Round before splitting into minutes and seconds, or 119.8s would be shown as `1分60秒`.\n const roundedSeconds = Math.round(milliseconds / 1000);\n return `${Math.floor(roundedSeconds / 60)}分${roundedSeconds % 60}秒`;\n}\n"],"mappings":"aAAA,SAAgB,EAAe,EAAe,CAAE,OAAO,IAAM,QAAQ,CAAC,IAAK,IAAK,IAAK,IAAK,GAAG,GAAM,CAAC,EAAW,CAC7G,GAAI,EAAQ,EACV,OAAO,EAAM,SAAS,EAGxB,IAAI,EAAY,GAChB,KAAO,GAAS,GAAQ,EAAY,EAAM,OAAS,GACjD,GAAS,EACT,IAGF,OAAO,EAAM,QAAQ,CAAC,GAAK,EAAM,IAAc,GACjD,CAMA,SAAgB,EAA4B,EAA8B,CAExE,IAAM,EAAgB,KAAK,MAAM,EAAe,GAAG,EAAI,GACvD,GAAI,EAAgB,GAAI,MAAO,GAAG,EAAc,QAAQ,CAAC,EAAE,GAG3D,IAAM,EAAiB,KAAK,MAAM,EAAe,GAAI,EACrD,MAAO,GAAG,KAAK,MAAM,EAAiB,EAAE,EAAE,GAAG,EAAiB,GAAG,EACnE"}
@@ -2,3 +2,8 @@ export declare function humanizeNumber(value: number, { base, units }?: {
2
2
  base?: number | undefined;
3
3
  units?: string[] | undefined;
4
4
  }): string;
5
+ /**
6
+ * Formats a duration in Japanese: to a tenth of a second under a minute (`59.9秒`), otherwise in whole minutes and
7
+ * seconds (`1分0秒`).
8
+ */
9
+ export declare function formatElapsedTimeInJapanese(milliseconds: number): string;
package/dist/humanize.js CHANGED
@@ -1,2 +1,2 @@
1
- function e(e,{base:t=1e3,units:n=[`K`,`M`,`B`,`T`,`P`]}={}){if(e<t)return e.toString();let r=-1;for(;e>=t&&r<n.length-1;)e/=t,r++;return e.toFixed(2)+(n[r]??``)}export{e as humanizeNumber};
1
+ function e(e,{base:t=1e3,units:n=[`K`,`M`,`B`,`T`,`P`]}={}){if(e<t)return e.toString();let r=-1;for(;e>=t&&r<n.length-1;)e/=t,r++;return e.toFixed(2)+(n[r]??``)}function t(e){let t=Math.round(e/100)/10;if(t<60)return`${t.toFixed(1)}秒`;let n=Math.round(e/1e3);return`${Math.floor(n/60)}分${n%60}秒`}export{t as formatElapsedTimeInJapanese,e as humanizeNumber};
2
2
  //# sourceMappingURL=humanize.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"humanize.js","names":[],"sources":["../src/humanize.ts"],"sourcesContent":["export function humanizeNumber(value: number, { base = 1000, units = ['K', 'M', 'B', 'T', 'P'] } = {}): string {\n if (value < base) {\n return value.toString();\n }\n\n let unitIndex = -1;\n while (value >= base && unitIndex < units.length - 1) {\n value /= base;\n unitIndex++;\n }\n\n return value.toFixed(2) + (units[unitIndex] ?? '');\n}\n"],"mappings":"AAAA,SAAgB,EAAe,EAAe,CAAE,OAAO,IAAM,QAAQ,CAAC,IAAK,IAAK,IAAK,IAAK,GAAG,GAAM,CAAC,EAAW,CAC7G,GAAI,EAAQ,EACV,OAAO,EAAM,SAAS,EAGxB,IAAI,EAAY,GAChB,KAAO,GAAS,GAAQ,EAAY,EAAM,OAAS,GACjD,GAAS,EACT,IAGF,OAAO,EAAM,QAAQ,CAAC,GAAK,EAAM,IAAc,GACjD"}
1
+ {"version":3,"file":"humanize.js","names":[],"sources":["../src/humanize.ts"],"sourcesContent":["export function humanizeNumber(value: number, { base = 1000, units = ['K', 'M', 'B', 'T', 'P'] } = {}): string {\n if (value < base) {\n return value.toString();\n }\n\n let unitIndex = -1;\n while (value >= base && unitIndex < units.length - 1) {\n value /= base;\n unitIndex++;\n }\n\n return value.toFixed(2) + (units[unitIndex] ?? '');\n}\n\n/**\n * Formats a duration in Japanese: to a tenth of a second under a minute (`59.9秒`), otherwise in whole minutes and\n * seconds (`1分0秒`).\n */\nexport function formatElapsedTimeInJapanese(milliseconds: number): string {\n // Round to the displayed precision before choosing the format, or 59.98s would be shown as `60.0秒`.\n const roundedTenths = Math.round(milliseconds / 100) / 10;\n if (roundedTenths < 60) return `${roundedTenths.toFixed(1)}秒`;\n\n // Round before splitting into minutes and seconds, or 119.8s would be shown as `1分60秒`.\n const roundedSeconds = Math.round(milliseconds / 1000);\n return `${Math.floor(roundedSeconds / 60)}分${roundedSeconds % 60}秒`;\n}\n"],"mappings":"AAAA,SAAgB,EAAe,EAAe,CAAE,OAAO,IAAM,QAAQ,CAAC,IAAK,IAAK,IAAK,IAAK,GAAG,GAAM,CAAC,EAAW,CAC7G,GAAI,EAAQ,EACV,OAAO,EAAM,SAAS,EAGxB,IAAI,EAAY,GAChB,KAAO,GAAS,GAAQ,EAAY,EAAM,OAAS,GACjD,GAAS,EACT,IAGF,OAAO,EAAM,QAAQ,CAAC,GAAK,EAAM,IAAc,GACjD,CAMA,SAAgB,EAA4B,EAA8B,CAExE,IAAM,EAAgB,KAAK,MAAM,EAAe,GAAG,EAAI,GACvD,GAAI,EAAgB,GAAI,MAAO,GAAG,EAAc,QAAQ,CAAC,EAAE,GAG3D,IAAM,EAAiB,KAAK,MAAM,EAAe,GAAI,EACrD,MAAO,GAAG,KAAK,MAAM,EAAiB,EAAE,EAAE,GAAG,EAAiB,GAAG,EACnE"}
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./appEnv.cjs"),t=require("./assert.cjs"),n=require("./githubTemplates.cjs"),r=require("./sleep.cjs"),i=require("./error.cjs"),a=require("./foreignCjk.cjs"),o=require("./humanize.cjs"),s=require("./mail.cjs"),c=require("./markdown.cjs"),l=require("./parseIsoDate.cjs"),u=require("./parseCommandLineArgs.cjs"),d=require("./text.cjs"),f=require("./prompt.cjs"),p=require("./promptFormatters.cjs"),m=require("./shuffle.cjs"),h=require("./recoverJson.cjs"),g=require("./sqlite.cjs"),_=require("./testingRules.cjs"),v=require("./zenkaku.cjs");exports.BUG_ISSUE_SECTIONS=n.BUG_ISSUE_SECTIONS,exports.CHANGE_ISSUE_SECTIONS=n.CHANGE_ISSUE_SECTIONS,exports.ISSUE_TEMPLATE_RULES=n.ISSUE_TEMPLATE_RULES,exports.PULL_REQUEST_BODY_RULES=n.PULL_REQUEST_BODY_RULES,exports.PULL_REQUEST_REQUIREMENTS_RULES=n.PULL_REQUEST_REQUIREMENTS_RULES,exports.PULL_REQUEST_SECTIONS=n.PULL_REQUEST_SECTIONS,exports.TEST_WRITING_RULES=_.TEST_WRITING_RULES,exports.detectForeignCjkInJapanese=a.detectForeignCjkInJapanese,exports.ensureTruthy=t.ensureTruthy,exports.errorify=i.errorify,exports.escapePromptTag=f.escapePromptTag,exports.escapeRegExp=d.escapeRegExp,exports.extractCodeBlocks=c.extractCodeBlocks,exports.extractIfSingleOutermostCodeBlock=c.extractIfSingleOutermostCodeBlock,exports.extractSections=c.extractSections,exports.findForeignCjkCharactersInJapanese=a.findForeignCjkCharactersInJapanese,exports.formatFilesForPrompt=p.formatFilesForPrompt,exports.formatMessagesForPrompt=p.formatMessagesForPrompt,exports.formatPrompt=f.formatPrompt,exports.getAppEnv=e.getAppEnv,exports.getConnectionLevelSqlitePragmas=g.getConnectionLevelSqlitePragmas,exports.getPersistentSqlitePragmas=g.getPersistentSqlitePragmas,exports.humanizeNumber=o.humanizeNumber,exports.ignoreEnoent=i.ignoreEnoent,exports.ignoreEnoentAsync=i.ignoreEnoentAsync,exports.ignoreError=i.ignoreError,exports.ignoreErrorAsync=i.ignoreErrorAsync,exports.mailTemplates=s.mailTemplates,exports.parseCommandLineArgs=u.parseCommandLineArgs,exports.parseIsoDate=l.parseIsoDate,exports.parseMarkdownSections=c.parseMarkdownSections,exports.quoteForShell=d.quoteForShell,exports.recoverJson=h.recoverJson,exports.renderSectionChecklist=n.renderSectionChecklist,exports.renderSectionTemplate=n.renderSectionTemplate,exports.serializeForPrompt=f.serializeForPrompt,exports.serializeForPromptInTag=f.serializeForPromptInTag,exports.shuffle=m.shuffle,exports.shuffleWithSeed=m.shuffleWithSeed,exports.sleep=r.sleep,exports.toCodeBlock=d.toCodeBlock,exports.toTildeCodeBlock=d.toTildeCodeBlock,exports.truncateForPrompt=f.truncateForPrompt,exports.withRetry=i.withRetry,exports.zenkakuAlphanumericalsToHankaku=v.zenkakuAlphanumericalsToHankaku;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./appEnv.cjs"),t=require("./assert.cjs"),n=require("./concurrency.cjs"),r=require("./githubTemplates.cjs"),i=require("./sleep.cjs"),a=require("./error.cjs"),o=require("./foreignCjk.cjs"),s=require("./humanize.cjs"),c=require("./mail.cjs"),l=require("./markdown.cjs"),u=require("./parseIsoDate.cjs"),d=require("./parseCommandLineArgs.cjs"),f=require("./text.cjs"),p=require("./prompt.cjs"),m=require("./promptFormatters.cjs"),h=require("./shuffle.cjs"),g=require("./recoverJson.cjs"),_=require("./sqlite.cjs"),v=require("./testingRules.cjs"),y=require("./throttledFetch.cjs"),b=require("./url.cjs"),x=require("./zenkaku.cjs");exports.BUG_ISSUE_SECTIONS=r.BUG_ISSUE_SECTIONS,exports.CHANGE_ISSUE_SECTIONS=r.CHANGE_ISSUE_SECTIONS,exports.ISSUE_TEMPLATE_RULES=r.ISSUE_TEMPLATE_RULES,exports.PULL_REQUEST_BODY_RULES=r.PULL_REQUEST_BODY_RULES,exports.PULL_REQUEST_REQUIREMENTS_RULES=r.PULL_REQUEST_REQUIREMENTS_RULES,exports.PULL_REQUEST_SECTIONS=r.PULL_REQUEST_SECTIONS,exports.TEST_WRITING_RULES=v.TEST_WRITING_RULES,exports.createThrottledFetch=y.createThrottledFetch,exports.detectForeignCjkInJapanese=o.detectForeignCjkInJapanese,exports.ensureTruthy=t.ensureTruthy,exports.errorify=a.errorify,exports.escapePromptTag=p.escapePromptTag,exports.escapeRegExp=f.escapeRegExp,exports.extractCodeBlocks=l.extractCodeBlocks,exports.extractIfSingleOutermostCodeBlock=l.extractIfSingleOutermostCodeBlock,exports.extractSections=l.extractSections,exports.extractTaggedCodeBlock=l.extractTaggedCodeBlock,exports.findForeignCjkCharactersInJapanese=o.findForeignCjkCharactersInJapanese,exports.forEachConcurrently=n.forEachConcurrently,exports.formatElapsedTimeInJapanese=s.formatElapsedTimeInJapanese,exports.formatFilesForPrompt=m.formatFilesForPrompt,exports.formatMessagesForPrompt=m.formatMessagesForPrompt,exports.formatPrompt=p.formatPrompt,exports.getAppEnv=e.getAppEnv,exports.getConnectionLevelSqlitePragmas=_.getConnectionLevelSqlitePragmas,exports.getPersistentSqlitePragmas=_.getPersistentSqlitePragmas,exports.getSafeRedirectPath=b.getSafeRedirectPath,exports.humanizeNumber=s.humanizeNumber,exports.ignoreEnoent=a.ignoreEnoent,exports.ignoreEnoentAsync=a.ignoreEnoentAsync,exports.ignoreError=a.ignoreError,exports.ignoreErrorAsync=a.ignoreErrorAsync,exports.mailTemplates=c.mailTemplates,exports.parseCommandLineArgs=d.parseCommandLineArgs,exports.parseIsoDate=u.parseIsoDate,exports.parseMarkdownSections=l.parseMarkdownSections,exports.quoteForShell=f.quoteForShell,exports.recoverJson=g.recoverJson,exports.renderSectionChecklist=r.renderSectionChecklist,exports.renderSectionTemplate=r.renderSectionTemplate,exports.serializeForPrompt=p.serializeForPrompt,exports.serializeForPromptInTag=p.serializeForPromptInTag,exports.shuffle=h.shuffle,exports.shuffleWithSeed=h.shuffleWithSeed,exports.sleep=i.sleep,exports.toCodeBlock=f.toCodeBlock,exports.toTildeCodeBlock=f.toTildeCodeBlock,exports.truncateForPrompt=p.truncateForPrompt,exports.withRetry=a.withRetry,exports.zenkakuAlphanumericalsToHankaku=x.zenkakuAlphanumericalsToHankaku;
package/dist/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  export { getAppEnv } from './appEnv.js';
2
2
  export { ensureTruthy } from './assert.js';
3
+ export { forEachConcurrently } from './concurrency.js';
3
4
  export { BUG_ISSUE_SECTIONS, CHANGE_ISSUE_SECTIONS, ISSUE_TEMPLATE_RULES, PULL_REQUEST_BODY_RULES, PULL_REQUEST_REQUIREMENTS_RULES, PULL_REQUEST_SECTIONS, renderSectionChecklist, renderSectionTemplate, } from './githubTemplates.js';
4
5
  export { errorify, ignoreError, ignoreEnoent, ignoreErrorAsync, ignoreEnoentAsync, withRetry } from './error.js';
5
6
  export { detectForeignCjkInJapanese, findForeignCjkCharactersInJapanese } from './foreignCjk.js';
6
- export { humanizeNumber } from './humanize.js';
7
+ export { formatElapsedTimeInJapanese, humanizeNumber } from './humanize.js';
7
8
  export { mailTemplates } from './mail.js';
8
- export { extractCodeBlocks, extractIfSingleOutermostCodeBlock, extractSections, parseMarkdownSections, } from './markdown.js';
9
+ export { extractCodeBlocks, extractIfSingleOutermostCodeBlock, extractSections, extractTaggedCodeBlock, parseMarkdownSections, } from './markdown.js';
9
10
  export { parseIsoDate } from './parseIsoDate.js';
10
11
  export type { ParsedIsoDate } from './parseIsoDate.js';
11
12
  export { parseCommandLineArgs } from './parseCommandLineArgs.js';
@@ -18,9 +19,13 @@ export { sleep } from './sleep.js';
18
19
  export { getConnectionLevelSqlitePragmas, getPersistentSqlitePragmas } from './sqlite.js';
19
20
  export { escapeRegExp, quoteForShell, toCodeBlock, toTildeCodeBlock } from './text.js';
20
21
  export { TEST_WRITING_RULES } from './testingRules.js';
22
+ export { createThrottledFetch } from './throttledFetch.js';
23
+ export { getSafeRedirectPath } from './url.js';
21
24
  export { zenkakuAlphanumericalsToHankaku } from './zenkaku.js';
22
25
  export type { AppEnv } from './appEnv.js';
23
26
  export type { RetryOptions } from './error.js';
24
27
  export type { ForeignCjkLanguage } from './foreignCjk.js';
25
28
  export type { CodeBlock, MarkdownSection } from './markdown.js';
26
29
  export type { TemplateSection } from './githubTemplates.js';
30
+ export type { ThrottledFetchOptions } from './throttledFetch.js';
31
+ export type { SafeRedirectPathOptions } from './url.js';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{getAppEnv as e}from"./appEnv.js";import{ensureTruthy as t}from"./assert.js";import{BUG_ISSUE_SECTIONS as n,CHANGE_ISSUE_SECTIONS as r,ISSUE_TEMPLATE_RULES as i,PULL_REQUEST_BODY_RULES as a,PULL_REQUEST_REQUIREMENTS_RULES as o,PULL_REQUEST_SECTIONS as s,renderSectionChecklist as c,renderSectionTemplate as l}from"./githubTemplates.js";import{sleep as u}from"./sleep.js";import{errorify as d,ignoreEnoent as f,ignoreEnoentAsync as p,ignoreError as m,ignoreErrorAsync as h,withRetry as g}from"./error.js";import{detectForeignCjkInJapanese as _,findForeignCjkCharactersInJapanese as v}from"./foreignCjk.js";import{humanizeNumber as y}from"./humanize.js";import{mailTemplates as b}from"./mail.js";import{extractCodeBlocks as x,extractIfSingleOutermostCodeBlock as S,extractSections as C,parseMarkdownSections as w}from"./markdown.js";import{parseIsoDate as T}from"./parseIsoDate.js";import{parseCommandLineArgs as E}from"./parseCommandLineArgs.js";import{escapeRegExp as D,quoteForShell as O,toCodeBlock as k,toTildeCodeBlock as A}from"./text.js";import{escapePromptTag as j,formatPrompt as M,serializeForPrompt as N,serializeForPromptInTag as P,truncateForPrompt as F}from"./prompt.js";import{formatFilesForPrompt as I,formatMessagesForPrompt as L}from"./promptFormatters.js";import{shuffle as R,shuffleWithSeed as z}from"./shuffle.js";import{recoverJson as B}from"./recoverJson.js";import{getConnectionLevelSqlitePragmas as V,getPersistentSqlitePragmas as H}from"./sqlite.js";import{TEST_WRITING_RULES as U}from"./testingRules.js";import{zenkakuAlphanumericalsToHankaku as W}from"./zenkaku.js";export{n as BUG_ISSUE_SECTIONS,r as CHANGE_ISSUE_SECTIONS,i as ISSUE_TEMPLATE_RULES,a as PULL_REQUEST_BODY_RULES,o as PULL_REQUEST_REQUIREMENTS_RULES,s as PULL_REQUEST_SECTIONS,U as TEST_WRITING_RULES,_ as detectForeignCjkInJapanese,t as ensureTruthy,d as errorify,j as escapePromptTag,D as escapeRegExp,x as extractCodeBlocks,S as extractIfSingleOutermostCodeBlock,C as extractSections,v as findForeignCjkCharactersInJapanese,I as formatFilesForPrompt,L as formatMessagesForPrompt,M as formatPrompt,e as getAppEnv,V as getConnectionLevelSqlitePragmas,H as getPersistentSqlitePragmas,y as humanizeNumber,f as ignoreEnoent,p as ignoreEnoentAsync,m as ignoreError,h as ignoreErrorAsync,b as mailTemplates,E as parseCommandLineArgs,T as parseIsoDate,w as parseMarkdownSections,O as quoteForShell,B as recoverJson,c as renderSectionChecklist,l as renderSectionTemplate,N as serializeForPrompt,P as serializeForPromptInTag,R as shuffle,z as shuffleWithSeed,u as sleep,k as toCodeBlock,A as toTildeCodeBlock,F as truncateForPrompt,g as withRetry,W as zenkakuAlphanumericalsToHankaku};
1
+ import{getAppEnv as e}from"./appEnv.js";import{ensureTruthy as t}from"./assert.js";import{forEachConcurrently as n}from"./concurrency.js";import{BUG_ISSUE_SECTIONS as r,CHANGE_ISSUE_SECTIONS as i,ISSUE_TEMPLATE_RULES as a,PULL_REQUEST_BODY_RULES as o,PULL_REQUEST_REQUIREMENTS_RULES as s,PULL_REQUEST_SECTIONS as c,renderSectionChecklist as l,renderSectionTemplate as u}from"./githubTemplates.js";import{sleep as d}from"./sleep.js";import{errorify as f,ignoreEnoent as p,ignoreEnoentAsync as m,ignoreError as h,ignoreErrorAsync as g,withRetry as _}from"./error.js";import{detectForeignCjkInJapanese as v,findForeignCjkCharactersInJapanese as y}from"./foreignCjk.js";import{formatElapsedTimeInJapanese as b,humanizeNumber as x}from"./humanize.js";import{mailTemplates as S}from"./mail.js";import{extractCodeBlocks as C,extractIfSingleOutermostCodeBlock as w,extractSections as T,extractTaggedCodeBlock as E,parseMarkdownSections as D}from"./markdown.js";import{parseIsoDate as O}from"./parseIsoDate.js";import{parseCommandLineArgs as k}from"./parseCommandLineArgs.js";import{escapeRegExp as A,quoteForShell as j,toCodeBlock as M,toTildeCodeBlock as N}from"./text.js";import{escapePromptTag as P,formatPrompt as F,serializeForPrompt as I,serializeForPromptInTag as L,truncateForPrompt as R}from"./prompt.js";import{formatFilesForPrompt as z,formatMessagesForPrompt as B}from"./promptFormatters.js";import{shuffle as V,shuffleWithSeed as H}from"./shuffle.js";import{recoverJson as U}from"./recoverJson.js";import{getConnectionLevelSqlitePragmas as W,getPersistentSqlitePragmas as G}from"./sqlite.js";import{TEST_WRITING_RULES as K}from"./testingRules.js";import{createThrottledFetch as q}from"./throttledFetch.js";import{getSafeRedirectPath as J}from"./url.js";import{zenkakuAlphanumericalsToHankaku as Y}from"./zenkaku.js";export{r as BUG_ISSUE_SECTIONS,i as CHANGE_ISSUE_SECTIONS,a as ISSUE_TEMPLATE_RULES,o as PULL_REQUEST_BODY_RULES,s as PULL_REQUEST_REQUIREMENTS_RULES,c as PULL_REQUEST_SECTIONS,K as TEST_WRITING_RULES,q as createThrottledFetch,v as detectForeignCjkInJapanese,t as ensureTruthy,f as errorify,P as escapePromptTag,A as escapeRegExp,C as extractCodeBlocks,w as extractIfSingleOutermostCodeBlock,T as extractSections,E as extractTaggedCodeBlock,y as findForeignCjkCharactersInJapanese,n as forEachConcurrently,b as formatElapsedTimeInJapanese,z as formatFilesForPrompt,B as formatMessagesForPrompt,F as formatPrompt,e as getAppEnv,W as getConnectionLevelSqlitePragmas,G as getPersistentSqlitePragmas,J as getSafeRedirectPath,x as humanizeNumber,p as ignoreEnoent,m as ignoreEnoentAsync,h as ignoreError,g as ignoreErrorAsync,S as mailTemplates,k as parseCommandLineArgs,O as parseIsoDate,D as parseMarkdownSections,j as quoteForShell,U as recoverJson,l as renderSectionChecklist,u as renderSectionTemplate,I as serializeForPrompt,L as serializeForPromptInTag,V as shuffle,H as shuffleWithSeed,d as sleep,M as toCodeBlock,N as toTildeCodeBlock,R as truncateForPrompt,_ as withRetry,Y as zenkakuAlphanumericalsToHankaku};
package/dist/markdown.cjs CHANGED
@@ -1,5 +1,6 @@
1
- "use strict";function e(e){let t=m(e),n=[];for(let e=0;e<t.length;e++){let r=u(t[e]??``);if(!r)continue;let i=d(t,e+1,r);n.push({...r.language&&{language:r.language},code:t.slice(e+1,i).map(e=>p(e,r.indent)).join(`
2
- `),isClosed:i<t.length}),e=i}return n}function t(e,t){let n=c(e);return!n||t?.length&&!t.some(e=>e.toLowerCase()===n.language)?e:n.code}const n=new Set([``,`markdown`,`md`]);function r(e,t){let n=i(e).map(e=>({...e,key:a(e.heading)})),r=[...new Set(t)].map(e=>({name:e,key:a(e)})),o=Object.create(null);for(let{name:e,key:t}of r){let i;for(let r of n)r.key===t&&(!i||r.depth<i.depth||r.depth===i.depth&&r.heading===e&&i.heading!==e)&&(i=r);i&&(i.heading!==e&&r.some(n=>n.name!==e&&n.key===t)||i.content&&(o[e]=i.content))}return o}function i(e){let t=c(e),r=t&&n.has(t.language)?t:void 0;if(r?.isWhole)return s(r.code);let i=s(e);return i.length===0&&r?s(r.code):i}function a(e){return o(o(e).replace(/^\d+(?:\.\d+)*(?:\.\s+|\)\s*|\s+)/u,``)).replaceAll(/\s+/gu,` `).toLowerCase()}function o(e){return e.replaceAll(/^[\s*_`~"'「」]+|[\s*_`~"'「」::]+$/gu,``)}function s(e){let t=m(e),n=l(t);return n.map((e,r)=>{let i=r+1;for(;i<n.length&&(n[i]?.depth??0)>e.depth;)i++;return{depth:e.depth,heading:e.text,content:t.slice(e.index+1,n[i]?.index).join(`
3
- `).trim()}})}function c(e){let t=m(e.replace(/^\s*\n/u,``).trimEnd()),n=u(t[0]??``);if(!n)return;let r=t.length-1,i=r>0&&f(t[r]??``,n)?r:d(t,1,n);return{language:n.language,code:t.slice(1,i).map(e=>p(e,n.indent)).join(`
4
- `),isWhole:i>=r}}function l(e){let t=[];for(let n=0;n<e.length;n++){let r=e[n]??``,i=u(r);if(i){n=d(e,n+1,i);continue}let a=/^(#{1,6})(?:[ \t]+(.*))?$/u.exec(r);if(!a)continue;let o=(a[2]??``).replace(/(?:^|[ \t]+)#+[ \t]*$/u,``).trim().replaceAll(/(`+)(.+?)\1/gu,(e,t,n)=>n.startsWith(` `)&&n.endsWith(` `)?n.slice(1,-1):n);t.push({index:n,depth:a[1]?.length??1,text:o})}return t}function u(e){let t=/^( {0,3})(`{3,}|~{3,})(.*)$/u.exec(e),n=t?.[2],r=t?.[3]??``;if(!(!n||n.startsWith("`")&&r.includes("`")))return{char:n.charAt(0),length:n.length,indent:t[1]?.length??0,language:r.trim().split(/\s/u)[0]?.toLowerCase()??``}}function d(e,t,n){let r=t;for(;r<e.length&&!f(e[r]??``,n);)r++;return r}function f(e,t){let n=/^ {0,3}(`{3,}|~{3,})[ \t]*$/u.exec(e)?.[1];return n?.charAt(0)===t.char&&n.length>=t.length}function p(e,t){let n=0;for(;n<t&&e[n]===` `;)n++;return e.slice(n)}function m(e){return e.split(/\r?\n/u)}exports.extractCodeBlocks=e,exports.extractIfSingleOutermostCodeBlock=t,exports.extractSections=r,exports.parseMarkdownSections=i;
1
+ "use strict";function e(e){let t=g(e),n=[];for(let e=0;e<t.length;e++){let r=f(t[e]??``);if(!r)continue;let i=p(t,e+1,r);n.push({...r.language&&{language:r.language},code:t.slice(e+1,i).map(e=>h(e,r.indent)).join(`
2
+ `),isClosed:i<t.length}),e=i}return n}function t(e,t){let n=u(e);return!n||t?.length&&!t.some(e=>e.toLowerCase()===n.language)?e:n.code}const n=new Set([``,`markdown`,`md`]);function r(e,t){let r=`<${t}>`,a=i(g(e),r);if(a!==void 0)return a;let o=u(e);if(o?.isWhole&&n.has(o.language))return i(g(o.code),r)}function i(e,t){for(let n=0;n<e.length;n++){let r=e[n]??``,i=f(r);if(i){n=p(e,n+1,i);continue}if(r.trim()!==t)continue;let a=n+1;for(;a<e.length&&!e[a]?.trim();)a++;let o=f(e[a]??``);if(!o)continue;let s=p(e,a+1,o);if(s<e.length)return e.slice(a+1,s).map(e=>h(e,o.indent)).join(`
3
+ `).trim()}}function a(e,t){let n=o(e).map(e=>({...e,key:s(e.heading)})),r=[...new Set(t)].map(e=>({name:e,key:s(e)})),i=Object.create(null);for(let{name:e,key:t}of r){let a;for(let r of n)r.key===t&&(!a||r.depth<a.depth||r.depth===a.depth&&r.heading===e&&a.heading!==e)&&(a=r);a&&(a.heading!==e&&r.some(n=>n.name!==e&&n.key===t)||a.content&&(i[e]=a.content))}return i}function o(e){let t=u(e),r=t&&n.has(t.language)?t:void 0;if(r?.isWhole)return l(r.code);let i=l(e);return i.length===0&&r?l(r.code):i}function s(e){return c(c(e).replace(/^\d+(?:\.\d+)*(?:\.\s+|\)\s*|\s+)/u,``)).replaceAll(/\s+/gu,` `).toLowerCase()}function c(e){return e.replaceAll(/^[\s*_`~"'「」]+|[\s*_`~"'「」::]+$/gu,``)}function l(e){let t=g(e),n=d(t);return n.map((e,r)=>{let i=r+1;for(;i<n.length&&(n[i]?.depth??0)>e.depth;)i++;return{depth:e.depth,heading:e.text,content:t.slice(e.index+1,n[i]?.index).join(`
4
+ `).trim()}})}function u(e){let t=g(e.replace(/^\s*\n/u,``).trimEnd()),n=f(t[0]??``);if(!n)return;let r=t.length-1,i=r>0&&m(t[r]??``,n)?r:p(t,1,n);return{language:n.language,code:t.slice(1,i).map(e=>h(e,n.indent)).join(`
5
+ `),isWhole:i>=r}}function d(e){let t=[];for(let n=0;n<e.length;n++){let r=e[n]??``,i=f(r);if(i){n=p(e,n+1,i);continue}let a=/^(#{1,6})(?:[ \t]+(.*))?$/u.exec(r);if(!a)continue;let o=(a[2]??``).replace(/(?:^|[ \t]+)#+[ \t]*$/u,``).trim().replaceAll(/(`+)(.+?)\1/gu,(e,t,n)=>n.startsWith(` `)&&n.endsWith(` `)?n.slice(1,-1):n);t.push({index:n,depth:a[1]?.length??1,text:o})}return t}function f(e){let t=/^( {0,3})(`{3,}|~{3,})(.*)$/u.exec(e),n=t?.[2],r=t?.[3]??``;if(!(!n||n.startsWith("`")&&r.includes("`")))return{char:n.charAt(0),length:n.length,indent:t[1]?.length??0,language:r.trim().split(/\s/u)[0]?.toLowerCase()??``}}function p(e,t,n){let r=t;for(;r<e.length&&!m(e[r]??``,n);)r++;return r}function m(e,t){let n=/^ {0,3}(`{3,}|~{3,})[ \t]*$/u.exec(e)?.[1];return n?.charAt(0)===t.char&&n.length>=t.length}function h(e,t){let n=0;for(;n<t&&e[n]===` `;)n++;return e.slice(n)}function g(e){return e.split(/\r?\n/u)}exports.extractCodeBlocks=e,exports.extractIfSingleOutermostCodeBlock=t,exports.extractSections=a,exports.extractTaggedCodeBlock=r,exports.parseMarkdownSections=o;
5
6
  //# sourceMappingURL=markdown.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.cjs","names":[],"sources":["../src/markdown.ts"],"sourcesContent":["export interface CodeBlock {\n /** The first word of the info string, lowercased; `undefined` when the fence has none. */\n language?: string;\n code: string;\n /** `false` when the text ends before the closing fence, e.g. because the LLM output was cut off. */\n isClosed: boolean;\n}\n\ninterface Fence {\n char: string;\n length: number;\n indent: number;\n language: string;\n}\n\n/**\n * Extracts the fenced code blocks (backticks or tildes, any fence length) of Markdown text such as an LLM response.\n * A block runs to the first closing fence of the same character that is at least as long as its opening fence,\n * so a longer fence can hold shorter fences. Only fences indented by at most three spaces are recognized; fences\n * inside block quotes, deeply nested list items, and indented code blocks are not.\n */\nexport function extractCodeBlocks(markdown: string): CodeBlock[] {\n const lines = splitLines(markdown);\n const blocks: CodeBlock[] = [];\n for (let index = 0; index < lines.length; index++) {\n const fence = parseOpeningFence(lines[index] ?? '');\n if (!fence) continue;\n const end = findClosingFence(lines, index + 1, fence);\n blocks.push({\n ...(fence.language && { language: fence.language }),\n code: lines\n .slice(index + 1, end)\n .map((line) => stripIndent(line, fence.indent))\n .join('\\n'),\n isClosed: end < lines.length,\n });\n index = end;\n }\n return blocks;\n}\n\n/**\n * Returns the contents of the code block that an LLM wrapped its whole response in, or `text` itself when the\n * response does not start with a fence or the fence's language is not one of a nonempty `languages` list\n * (compared case-insensitively). An omitted or empty list allows any language.\n * When the response also ends with a matching closing fence, everything between its first and last lines is returned,\n * so fences the LLM nested without lengthening the outer fence stay in the contents. Otherwise the first block is\n * returned and text after it is dropped; an unclosed block runs to the end of the text.\n */\nexport function extractIfSingleOutermostCodeBlock(text: string, languages?: readonly string[]): string {\n const block = findOutermostCodeBlock(text);\n if (!block) return text;\n if (languages?.length && !languages.some((language) => language.toLowerCase() === block.language)) return text;\n return block.code;\n}\n\nexport interface MarkdownSection {\n depth: number;\n /** The heading text with the closing `#` sequence and inline-code backticks removed. */\n heading: string;\n /** The source text up to the next heading at the same or a shallower depth, trimmed; subsections are included. */\n content: string;\n}\n\nconst WRAPPED_MARKDOWN_LANGUAGES = new Set(['', 'markdown', 'md']);\n\n/**\n * Extracts the content of the section headed by each of `names` from Markdown text such as an LLM response, keyed by\n * the given names. A heading matches a name when both are equal after removing decorations at their edges (such as\n * `**bold**`, backticks, and quotes), a leading number like `1.` or `2.1`, and a trailing colon, ignoring case and spacing;\n * headings at any depth are candidates.\n * Among matching headings, the shallowest wins, then one equal to the name as written, then the first; each heading\n * serves at most one name. When distinct requested names normalize alike, only exact matches at the shallowest\n * matching depth are returned. Missing, empty, or ambiguous sections are absent from the prototype-free result.\n */\nexport function extractSections<const Name extends string>(\n markdown: string,\n names: readonly Name[]\n): Partial<Record<Name, string>> {\n const candidates = parseMarkdownSections(markdown).map((section) => ({\n ...section,\n key: normalizeHeading(section.heading),\n }));\n const requested = [...new Set(names)].map((name) => ({ name, key: normalizeHeading(name) }));\n const sections: Partial<Record<Name, string>> = Object.create(null);\n for (const { name, key } of requested) {\n let best: (typeof candidates)[number] | undefined;\n for (const candidate of candidates) {\n if (candidate.key !== key) continue;\n if (\n !best ||\n candidate.depth < best.depth ||\n (candidate.depth === best.depth && candidate.heading === name && best.heading !== name)\n ) {\n best = candidate;\n }\n }\n if (!best) continue;\n if (best.heading !== name && requested.some((other) => other.name !== name && other.key === key)) continue;\n if (best.content) sections[name] = best.content;\n }\n return sections;\n}\n\n/**\n * Returns every ATX heading of Markdown text in order, with the source text under it.\n * Only headings starting at the beginning of a line outside fenced code blocks count; setext headings (underlined\n * with `=` or `-`) are ignored. A whole-response `markdown`, `md`, or unlabeled wrapper is read inside as\n * `extractIfSingleOutermostCodeBlock` finds it, preserving nested fences. A leading wrapper followed by prose is\n * also read inside, but only when there are no headings outside fences.\n */\nexport function parseMarkdownSections(markdown: string): MarkdownSection[] {\n const block = findOutermostCodeBlock(markdown);\n const wrapper = block && WRAPPED_MARKDOWN_LANGUAGES.has(block.language) ? block : undefined;\n if (wrapper?.isWhole) return splitSections(wrapper.code);\n const sections = splitSections(markdown);\n return sections.length === 0 && wrapper ? splitSections(wrapper.code) : sections;\n}\n\nfunction normalizeHeading(heading: string): string {\n return stripHeadingEdges(stripHeadingEdges(heading).replace(/^\\d+(?:\\.\\d+)*(?:\\.\\s+|\\)\\s*|\\s+)/u, ''))\n .replaceAll(/\\s+/gu, ' ')\n .toLowerCase();\n}\n\nfunction stripHeadingEdges(text: string): string {\n // Only the edges: `_` and `*` inside a heading belong to names such as `my_file.py`.\n return text.replaceAll(/^[\\s*_`~\"'「」]+|[\\s*_`~\"'「」::]+$/gu, '');\n}\n\nfunction splitSections(markdown: string): MarkdownSection[] {\n const lines = splitLines(markdown);\n const headings = findHeadings(lines);\n return headings.map((heading, index) => {\n let nextIndex = index + 1;\n while (nextIndex < headings.length && (headings[nextIndex]?.depth ?? 0) > heading.depth) nextIndex++;\n return {\n depth: heading.depth,\n heading: heading.text,\n content: lines\n .slice(heading.index + 1, headings[nextIndex]?.index)\n .join('\\n')\n .trim(),\n };\n });\n}\n\nfunction findOutermostCodeBlock(text: string): { language: string; code: string; isWhole: boolean } | undefined {\n // Keep the opening fence's indentation, which is stripped from the contents.\n const lines = splitLines(text.replace(/^\\s*\\n/u, '').trimEnd());\n const fence = parseOpeningFence(lines[0] ?? '');\n if (!fence) return;\n const lastIndex = lines.length - 1;\n const end =\n lastIndex > 0 && isClosingFence(lines[lastIndex] ?? '', fence) ? lastIndex : findClosingFence(lines, 1, fence);\n return {\n language: fence.language,\n code: lines\n .slice(1, end)\n .map((line) => stripIndent(line, fence.indent))\n .join('\\n'),\n isWhole: end >= lastIndex,\n };\n}\n\nfunction findHeadings(lines: readonly string[]): { index: number; depth: number; text: string }[] {\n const headings: { index: number; depth: number; text: string }[] = [];\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index] ?? '';\n const fence = parseOpeningFence(line);\n if (fence) {\n index = findClosingFence(lines, index + 1, fence);\n continue;\n }\n const match = /^(#{1,6})(?:[ \\t]+(.*))?$/u.exec(line);\n if (!match) continue;\n const text = (match[2] ?? '')\n // The closing sequence of an ATX heading needs a space before it, so `# C#` keeps its `#`.\n .replace(/(?:^|[ \\t]+)#+[ \\t]*$/u, '')\n .trim()\n .replaceAll(/(`+)(.+?)\\1/gu, (_match, _delimiter: string, content: string) =>\n content.startsWith(' ') && content.endsWith(' ') ? content.slice(1, -1) : content\n );\n headings.push({ index, depth: match[1]?.length ?? 1, text });\n }\n return headings;\n}\n\nfunction parseOpeningFence(line: string): Fence | undefined {\n const match = /^( {0,3})(`{3,}|~{3,})(.*)$/u.exec(line);\n const fence = match?.[2];\n const info = match?.[3] ?? '';\n // A backtick in the info string makes the line inline code instead of a fence.\n if (!fence || (fence.startsWith('`') && info.includes('`'))) return;\n return {\n char: fence.charAt(0),\n length: fence.length,\n indent: match[1]?.length ?? 0,\n language: info.trim().split(/\\s/u)[0]?.toLowerCase() ?? '',\n };\n}\n\n/** Returns the index of the line closing `fence`, or the number of lines when it is never closed. */\nfunction findClosingFence(lines: readonly string[], start: number, fence: Fence): number {\n let index = start;\n while (index < lines.length && !isClosingFence(lines[index] ?? '', fence)) index++;\n return index;\n}\n\nfunction isClosingFence(line: string, fence: Fence): boolean {\n const closer = /^ {0,3}(`{3,}|~{3,})[ \\t]*$/u.exec(line)?.[1];\n return closer?.charAt(0) === fence.char && closer.length >= fence.length;\n}\n\nfunction stripIndent(line: string, indent: number): string {\n let index = 0;\n while (index < indent && line[index] === ' ') index++;\n return line.slice(index);\n}\n\nfunction splitLines(text: string): string[] {\n return text.split(/\\r?\\n/u);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"markdown.cjs","names":[],"sources":["../src/markdown.ts"],"sourcesContent":["export interface CodeBlock {\n /** The first word of the info string, lowercased; `undefined` when the fence has none. */\n language?: string;\n code: string;\n /** `false` when the text ends before the closing fence, e.g. because the LLM output was cut off. */\n isClosed: boolean;\n}\n\ninterface Fence {\n char: string;\n length: number;\n indent: number;\n language: string;\n}\n\n/**\n * Extracts the fenced code blocks (backticks or tildes, any fence length) of Markdown text such as an LLM response.\n * A block runs to the first closing fence of the same character that is at least as long as its opening fence,\n * so a longer fence can hold shorter fences. Only fences indented by at most three spaces are recognized; fences\n * inside block quotes, deeply nested list items, and indented code blocks are not.\n */\nexport function extractCodeBlocks(markdown: string): CodeBlock[] {\n const lines = splitLines(markdown);\n const blocks: CodeBlock[] = [];\n for (let index = 0; index < lines.length; index++) {\n const fence = parseOpeningFence(lines[index] ?? '');\n if (!fence) continue;\n const end = findClosingFence(lines, index + 1, fence);\n blocks.push({\n ...(fence.language && { language: fence.language }),\n code: lines\n .slice(index + 1, end)\n .map((line) => stripIndent(line, fence.indent))\n .join('\\n'),\n isClosed: end < lines.length,\n });\n index = end;\n }\n return blocks;\n}\n\n/**\n * Returns the contents of the code block that an LLM wrapped its whole response in, or `text` itself when the\n * response does not start with a fence or the fence's language is not one of a nonempty `languages` list\n * (compared case-insensitively). An omitted or empty list allows any language.\n * When the response also ends with a matching closing fence, everything between its first and last lines is returned,\n * so fences the LLM nested without lengthening the outer fence stay in the contents. Otherwise the first block is\n * returned and text after it is dropped; an unclosed block runs to the end of the text.\n */\nexport function extractIfSingleOutermostCodeBlock(text: string, languages?: readonly string[]): string {\n const block = findOutermostCodeBlock(text);\n if (!block) return text;\n if (languages?.length && !languages.some((language) => language.toLowerCase() === block.language)) return text;\n return block.code;\n}\n\nconst WRAPPED_MARKDOWN_LANGUAGES = new Set(['', 'markdown', 'md']);\n\n/**\n * Returns the trimmed contents of the first closed fenced code block (backticks or tildes) that follows a line holding\n * only `<tagName>` in text such as an LLM response, e.g. `<answer>\\n~~~\\n42\\n~~~`, or `undefined` when there is none.\n * Only blank lines may separate the tag line from the opening fence. Mentions of the tag within prose lines and tag\n * lines inside other fenced blocks (e.g. a format example) are skipped, but a whole-response `markdown`, `md`, or\n * unlabeled wrapper is read inside, as `parseMarkdownSections` does.\n */\nexport function extractTaggedCodeBlock(text: string, tagName: string): string | undefined {\n const tag = `<${tagName}>`;\n const found = findTaggedCodeBlock(splitLines(text), tag);\n if (found !== undefined) return found;\n // Only a real wrapper hides every tag line from the scan above; a leading example block paired with the tagged\n // block's closing fence merely looks like one, so its contents must not be read first.\n const wrapper = findOutermostCodeBlock(text);\n if (wrapper?.isWhole && WRAPPED_MARKDOWN_LANGUAGES.has(wrapper.language)) {\n return findTaggedCodeBlock(splitLines(wrapper.code), tag);\n }\n}\n\nfunction findTaggedCodeBlock(lines: readonly string[], tag: string): string | undefined {\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index] ?? '';\n const enclosingFence = parseOpeningFence(line);\n if (enclosingFence) {\n index = findClosingFence(lines, index + 1, enclosingFence);\n continue;\n }\n if (line.trim() !== tag) continue;\n let fenceIndex = index + 1;\n while (fenceIndex < lines.length && !lines[fenceIndex]?.trim()) fenceIndex++;\n const fence = parseOpeningFence(lines[fenceIndex] ?? '');\n if (!fence) continue;\n const end = findClosingFence(lines, fenceIndex + 1, fence);\n if (end < lines.length) {\n return lines\n .slice(fenceIndex + 1, end)\n .map((contentLine) => stripIndent(contentLine, fence.indent))\n .join('\\n')\n .trim();\n }\n }\n}\n\nexport interface MarkdownSection {\n depth: number;\n /** The heading text with the closing `#` sequence and inline-code backticks removed. */\n heading: string;\n /** The source text up to the next heading at the same or a shallower depth, trimmed; subsections are included. */\n content: string;\n}\n\n/**\n * Extracts the content of the section headed by each of `names` from Markdown text such as an LLM response, keyed by\n * the given names. A heading matches a name when both are equal after removing decorations at their edges (such as\n * `**bold**`, backticks, and quotes), a leading number like `1.` or `2.1`, and a trailing colon, ignoring case and spacing;\n * headings at any depth are candidates.\n * Among matching headings, the shallowest wins, then one equal to the name as written, then the first; each heading\n * serves at most one name. When distinct requested names normalize alike, only exact matches at the shallowest\n * matching depth are returned. Missing, empty, or ambiguous sections are absent from the prototype-free result.\n */\nexport function extractSections<const Name extends string>(\n markdown: string,\n names: readonly Name[]\n): Partial<Record<Name, string>> {\n const candidates = parseMarkdownSections(markdown).map((section) => ({\n ...section,\n key: normalizeHeading(section.heading),\n }));\n const requested = [...new Set(names)].map((name) => ({ name, key: normalizeHeading(name) }));\n const sections: Partial<Record<Name, string>> = Object.create(null);\n for (const { name, key } of requested) {\n let best: (typeof candidates)[number] | undefined;\n for (const candidate of candidates) {\n if (candidate.key !== key) continue;\n if (\n !best ||\n candidate.depth < best.depth ||\n (candidate.depth === best.depth && candidate.heading === name && best.heading !== name)\n ) {\n best = candidate;\n }\n }\n if (!best) continue;\n if (best.heading !== name && requested.some((other) => other.name !== name && other.key === key)) continue;\n if (best.content) sections[name] = best.content;\n }\n return sections;\n}\n\n/**\n * Returns every ATX heading of Markdown text in order, with the source text under it.\n * Only headings starting at the beginning of a line outside fenced code blocks count; setext headings (underlined\n * with `=` or `-`) are ignored. A whole-response `markdown`, `md`, or unlabeled wrapper is read inside as\n * `extractIfSingleOutermostCodeBlock` finds it, preserving nested fences. A leading wrapper followed by prose is\n * also read inside, but only when there are no headings outside fences.\n */\nexport function parseMarkdownSections(markdown: string): MarkdownSection[] {\n const block = findOutermostCodeBlock(markdown);\n const wrapper = block && WRAPPED_MARKDOWN_LANGUAGES.has(block.language) ? block : undefined;\n if (wrapper?.isWhole) return splitSections(wrapper.code);\n const sections = splitSections(markdown);\n return sections.length === 0 && wrapper ? splitSections(wrapper.code) : sections;\n}\n\nfunction normalizeHeading(heading: string): string {\n return stripHeadingEdges(stripHeadingEdges(heading).replace(/^\\d+(?:\\.\\d+)*(?:\\.\\s+|\\)\\s*|\\s+)/u, ''))\n .replaceAll(/\\s+/gu, ' ')\n .toLowerCase();\n}\n\nfunction stripHeadingEdges(text: string): string {\n // Only the edges: `_` and `*` inside a heading belong to names such as `my_file.py`.\n return text.replaceAll(/^[\\s*_`~\"'「」]+|[\\s*_`~\"'「」::]+$/gu, '');\n}\n\nfunction splitSections(markdown: string): MarkdownSection[] {\n const lines = splitLines(markdown);\n const headings = findHeadings(lines);\n return headings.map((heading, index) => {\n let nextIndex = index + 1;\n while (nextIndex < headings.length && (headings[nextIndex]?.depth ?? 0) > heading.depth) nextIndex++;\n return {\n depth: heading.depth,\n heading: heading.text,\n content: lines\n .slice(heading.index + 1, headings[nextIndex]?.index)\n .join('\\n')\n .trim(),\n };\n });\n}\n\nfunction findOutermostCodeBlock(text: string): { language: string; code: string; isWhole: boolean } | undefined {\n // Keep the opening fence's indentation, which is stripped from the contents.\n const lines = splitLines(text.replace(/^\\s*\\n/u, '').trimEnd());\n const fence = parseOpeningFence(lines[0] ?? '');\n if (!fence) return;\n const lastIndex = lines.length - 1;\n const end =\n lastIndex > 0 && isClosingFence(lines[lastIndex] ?? '', fence) ? lastIndex : findClosingFence(lines, 1, fence);\n return {\n language: fence.language,\n code: lines\n .slice(1, end)\n .map((line) => stripIndent(line, fence.indent))\n .join('\\n'),\n isWhole: end >= lastIndex,\n };\n}\n\nfunction findHeadings(lines: readonly string[]): { index: number; depth: number; text: string }[] {\n const headings: { index: number; depth: number; text: string }[] = [];\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index] ?? '';\n const fence = parseOpeningFence(line);\n if (fence) {\n index = findClosingFence(lines, index + 1, fence);\n continue;\n }\n const match = /^(#{1,6})(?:[ \\t]+(.*))?$/u.exec(line);\n if (!match) continue;\n const text = (match[2] ?? '')\n // The closing sequence of an ATX heading needs a space before it, so `# C#` keeps its `#`.\n .replace(/(?:^|[ \\t]+)#+[ \\t]*$/u, '')\n .trim()\n .replaceAll(/(`+)(.+?)\\1/gu, (_match, _delimiter: string, content: string) =>\n content.startsWith(' ') && content.endsWith(' ') ? content.slice(1, -1) : content\n );\n headings.push({ index, depth: match[1]?.length ?? 1, text });\n }\n return headings;\n}\n\nfunction parseOpeningFence(line: string): Fence | undefined {\n const match = /^( {0,3})(`{3,}|~{3,})(.*)$/u.exec(line);\n const fence = match?.[2];\n const info = match?.[3] ?? '';\n // A backtick in the info string makes the line inline code instead of a fence.\n if (!fence || (fence.startsWith('`') && info.includes('`'))) return;\n return {\n char: fence.charAt(0),\n length: fence.length,\n indent: match[1]?.length ?? 0,\n language: info.trim().split(/\\s/u)[0]?.toLowerCase() ?? '',\n };\n}\n\n/** Returns the index of the line closing `fence`, or the number of lines when it is never closed. */\nfunction findClosingFence(lines: readonly string[], start: number, fence: Fence): number {\n let index = start;\n while (index < lines.length && !isClosingFence(lines[index] ?? '', fence)) index++;\n return index;\n}\n\nfunction isClosingFence(line: string, fence: Fence): boolean {\n const closer = /^ {0,3}(`{3,}|~{3,})[ \\t]*$/u.exec(line)?.[1];\n return closer?.charAt(0) === fence.char && closer.length >= fence.length;\n}\n\nfunction stripIndent(line: string, indent: number): string {\n let index = 0;\n while (index < indent && line[index] === ' ') index++;\n return line.slice(index);\n}\n\nfunction splitLines(text: string): string[] {\n return text.split(/\\r?\\n/u);\n}\n"],"mappings":"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"}
@@ -21,6 +21,14 @@ export declare function extractCodeBlocks(markdown: string): CodeBlock[];
21
21
  * returned and text after it is dropped; an unclosed block runs to the end of the text.
22
22
  */
23
23
  export declare function extractIfSingleOutermostCodeBlock(text: string, languages?: readonly string[]): string;
24
+ /**
25
+ * Returns the trimmed contents of the first closed fenced code block (backticks or tildes) that follows a line holding
26
+ * only `<tagName>` in text such as an LLM response, e.g. `<answer>\n~~~\n42\n~~~`, or `undefined` when there is none.
27
+ * Only blank lines may separate the tag line from the opening fence. Mentions of the tag within prose lines and tag
28
+ * lines inside other fenced blocks (e.g. a format example) are skipped, but a whole-response `markdown`, `md`, or
29
+ * unlabeled wrapper is read inside, as `parseMarkdownSections` does.
30
+ */
31
+ export declare function extractTaggedCodeBlock(text: string, tagName: string): string | undefined;
24
32
  export interface MarkdownSection {
25
33
  depth: number;
26
34
  /** The heading text with the closing `#` sequence and inline-code backticks removed. */
package/dist/markdown.js CHANGED
@@ -1,5 +1,6 @@
1
- function e(e){let t=m(e),n=[];for(let e=0;e<t.length;e++){let r=u(t[e]??``);if(!r)continue;let i=d(t,e+1,r);n.push({...r.language&&{language:r.language},code:t.slice(e+1,i).map(e=>p(e,r.indent)).join(`
2
- `),isClosed:i<t.length}),e=i}return n}function t(e,t){let n=c(e);return!n||t?.length&&!t.some(e=>e.toLowerCase()===n.language)?e:n.code}const n=new Set([``,`markdown`,`md`]);function r(e,t){let n=i(e).map(e=>({...e,key:a(e.heading)})),r=[...new Set(t)].map(e=>({name:e,key:a(e)})),o=Object.create(null);for(let{name:e,key:t}of r){let i;for(let r of n)r.key===t&&(!i||r.depth<i.depth||r.depth===i.depth&&r.heading===e&&i.heading!==e)&&(i=r);i&&(i.heading!==e&&r.some(n=>n.name!==e&&n.key===t)||i.content&&(o[e]=i.content))}return o}function i(e){let t=c(e),r=t&&n.has(t.language)?t:void 0;if(r?.isWhole)return s(r.code);let i=s(e);return i.length===0&&r?s(r.code):i}function a(e){return o(o(e).replace(/^\d+(?:\.\d+)*(?:\.\s+|\)\s*|\s+)/u,``)).replaceAll(/\s+/gu,` `).toLowerCase()}function o(e){return e.replaceAll(/^[\s*_`~"'「」]+|[\s*_`~"'「」::]+$/gu,``)}function s(e){let t=m(e),n=l(t);return n.map((e,r)=>{let i=r+1;for(;i<n.length&&(n[i]?.depth??0)>e.depth;)i++;return{depth:e.depth,heading:e.text,content:t.slice(e.index+1,n[i]?.index).join(`
3
- `).trim()}})}function c(e){let t=m(e.replace(/^\s*\n/u,``).trimEnd()),n=u(t[0]??``);if(!n)return;let r=t.length-1,i=r>0&&f(t[r]??``,n)?r:d(t,1,n);return{language:n.language,code:t.slice(1,i).map(e=>p(e,n.indent)).join(`
4
- `),isWhole:i>=r}}function l(e){let t=[];for(let n=0;n<e.length;n++){let r=e[n]??``,i=u(r);if(i){n=d(e,n+1,i);continue}let a=/^(#{1,6})(?:[ \t]+(.*))?$/u.exec(r);if(!a)continue;let o=(a[2]??``).replace(/(?:^|[ \t]+)#+[ \t]*$/u,``).trim().replaceAll(/(`+)(.+?)\1/gu,(e,t,n)=>n.startsWith(` `)&&n.endsWith(` `)?n.slice(1,-1):n);t.push({index:n,depth:a[1]?.length??1,text:o})}return t}function u(e){let t=/^( {0,3})(`{3,}|~{3,})(.*)$/u.exec(e),n=t?.[2],r=t?.[3]??``;if(!(!n||n.startsWith("`")&&r.includes("`")))return{char:n.charAt(0),length:n.length,indent:t[1]?.length??0,language:r.trim().split(/\s/u)[0]?.toLowerCase()??``}}function d(e,t,n){let r=t;for(;r<e.length&&!f(e[r]??``,n);)r++;return r}function f(e,t){let n=/^ {0,3}(`{3,}|~{3,})[ \t]*$/u.exec(e)?.[1];return n?.charAt(0)===t.char&&n.length>=t.length}function p(e,t){let n=0;for(;n<t&&e[n]===` `;)n++;return e.slice(n)}function m(e){return e.split(/\r?\n/u)}export{e as extractCodeBlocks,t as extractIfSingleOutermostCodeBlock,r as extractSections,i as parseMarkdownSections};
1
+ function e(e){let t=g(e),n=[];for(let e=0;e<t.length;e++){let r=f(t[e]??``);if(!r)continue;let i=p(t,e+1,r);n.push({...r.language&&{language:r.language},code:t.slice(e+1,i).map(e=>h(e,r.indent)).join(`
2
+ `),isClosed:i<t.length}),e=i}return n}function t(e,t){let n=u(e);return!n||t?.length&&!t.some(e=>e.toLowerCase()===n.language)?e:n.code}const n=new Set([``,`markdown`,`md`]);function r(e,t){let r=`<${t}>`,a=i(g(e),r);if(a!==void 0)return a;let o=u(e);if(o?.isWhole&&n.has(o.language))return i(g(o.code),r)}function i(e,t){for(let n=0;n<e.length;n++){let r=e[n]??``,i=f(r);if(i){n=p(e,n+1,i);continue}if(r.trim()!==t)continue;let a=n+1;for(;a<e.length&&!e[a]?.trim();)a++;let o=f(e[a]??``);if(!o)continue;let s=p(e,a+1,o);if(s<e.length)return e.slice(a+1,s).map(e=>h(e,o.indent)).join(`
3
+ `).trim()}}function a(e,t){let n=o(e).map(e=>({...e,key:s(e.heading)})),r=[...new Set(t)].map(e=>({name:e,key:s(e)})),i=Object.create(null);for(let{name:e,key:t}of r){let a;for(let r of n)r.key===t&&(!a||r.depth<a.depth||r.depth===a.depth&&r.heading===e&&a.heading!==e)&&(a=r);a&&(a.heading!==e&&r.some(n=>n.name!==e&&n.key===t)||a.content&&(i[e]=a.content))}return i}function o(e){let t=u(e),r=t&&n.has(t.language)?t:void 0;if(r?.isWhole)return l(r.code);let i=l(e);return i.length===0&&r?l(r.code):i}function s(e){return c(c(e).replace(/^\d+(?:\.\d+)*(?:\.\s+|\)\s*|\s+)/u,``)).replaceAll(/\s+/gu,` `).toLowerCase()}function c(e){return e.replaceAll(/^[\s*_`~"'「」]+|[\s*_`~"'「」::]+$/gu,``)}function l(e){let t=g(e),n=d(t);return n.map((e,r)=>{let i=r+1;for(;i<n.length&&(n[i]?.depth??0)>e.depth;)i++;return{depth:e.depth,heading:e.text,content:t.slice(e.index+1,n[i]?.index).join(`
4
+ `).trim()}})}function u(e){let t=g(e.replace(/^\s*\n/u,``).trimEnd()),n=f(t[0]??``);if(!n)return;let r=t.length-1,i=r>0&&m(t[r]??``,n)?r:p(t,1,n);return{language:n.language,code:t.slice(1,i).map(e=>h(e,n.indent)).join(`
5
+ `),isWhole:i>=r}}function d(e){let t=[];for(let n=0;n<e.length;n++){let r=e[n]??``,i=f(r);if(i){n=p(e,n+1,i);continue}let a=/^(#{1,6})(?:[ \t]+(.*))?$/u.exec(r);if(!a)continue;let o=(a[2]??``).replace(/(?:^|[ \t]+)#+[ \t]*$/u,``).trim().replaceAll(/(`+)(.+?)\1/gu,(e,t,n)=>n.startsWith(` `)&&n.endsWith(` `)?n.slice(1,-1):n);t.push({index:n,depth:a[1]?.length??1,text:o})}return t}function f(e){let t=/^( {0,3})(`{3,}|~{3,})(.*)$/u.exec(e),n=t?.[2],r=t?.[3]??``;if(!(!n||n.startsWith("`")&&r.includes("`")))return{char:n.charAt(0),length:n.length,indent:t[1]?.length??0,language:r.trim().split(/\s/u)[0]?.toLowerCase()??``}}function p(e,t,n){let r=t;for(;r<e.length&&!m(e[r]??``,n);)r++;return r}function m(e,t){let n=/^ {0,3}(`{3,}|~{3,})[ \t]*$/u.exec(e)?.[1];return n?.charAt(0)===t.char&&n.length>=t.length}function h(e,t){let n=0;for(;n<t&&e[n]===` `;)n++;return e.slice(n)}function g(e){return e.split(/\r?\n/u)}export{e as extractCodeBlocks,t as extractIfSingleOutermostCodeBlock,a as extractSections,r as extractTaggedCodeBlock,o as parseMarkdownSections};
5
6
  //# sourceMappingURL=markdown.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.js","names":[],"sources":["../src/markdown.ts"],"sourcesContent":["export interface CodeBlock {\n /** The first word of the info string, lowercased; `undefined` when the fence has none. */\n language?: string;\n code: string;\n /** `false` when the text ends before the closing fence, e.g. because the LLM output was cut off. */\n isClosed: boolean;\n}\n\ninterface Fence {\n char: string;\n length: number;\n indent: number;\n language: string;\n}\n\n/**\n * Extracts the fenced code blocks (backticks or tildes, any fence length) of Markdown text such as an LLM response.\n * A block runs to the first closing fence of the same character that is at least as long as its opening fence,\n * so a longer fence can hold shorter fences. Only fences indented by at most three spaces are recognized; fences\n * inside block quotes, deeply nested list items, and indented code blocks are not.\n */\nexport function extractCodeBlocks(markdown: string): CodeBlock[] {\n const lines = splitLines(markdown);\n const blocks: CodeBlock[] = [];\n for (let index = 0; index < lines.length; index++) {\n const fence = parseOpeningFence(lines[index] ?? '');\n if (!fence) continue;\n const end = findClosingFence(lines, index + 1, fence);\n blocks.push({\n ...(fence.language && { language: fence.language }),\n code: lines\n .slice(index + 1, end)\n .map((line) => stripIndent(line, fence.indent))\n .join('\\n'),\n isClosed: end < lines.length,\n });\n index = end;\n }\n return blocks;\n}\n\n/**\n * Returns the contents of the code block that an LLM wrapped its whole response in, or `text` itself when the\n * response does not start with a fence or the fence's language is not one of a nonempty `languages` list\n * (compared case-insensitively). An omitted or empty list allows any language.\n * When the response also ends with a matching closing fence, everything between its first and last lines is returned,\n * so fences the LLM nested without lengthening the outer fence stay in the contents. Otherwise the first block is\n * returned and text after it is dropped; an unclosed block runs to the end of the text.\n */\nexport function extractIfSingleOutermostCodeBlock(text: string, languages?: readonly string[]): string {\n const block = findOutermostCodeBlock(text);\n if (!block) return text;\n if (languages?.length && !languages.some((language) => language.toLowerCase() === block.language)) return text;\n return block.code;\n}\n\nexport interface MarkdownSection {\n depth: number;\n /** The heading text with the closing `#` sequence and inline-code backticks removed. */\n heading: string;\n /** The source text up to the next heading at the same or a shallower depth, trimmed; subsections are included. */\n content: string;\n}\n\nconst WRAPPED_MARKDOWN_LANGUAGES = new Set(['', 'markdown', 'md']);\n\n/**\n * Extracts the content of the section headed by each of `names` from Markdown text such as an LLM response, keyed by\n * the given names. A heading matches a name when both are equal after removing decorations at their edges (such as\n * `**bold**`, backticks, and quotes), a leading number like `1.` or `2.1`, and a trailing colon, ignoring case and spacing;\n * headings at any depth are candidates.\n * Among matching headings, the shallowest wins, then one equal to the name as written, then the first; each heading\n * serves at most one name. When distinct requested names normalize alike, only exact matches at the shallowest\n * matching depth are returned. Missing, empty, or ambiguous sections are absent from the prototype-free result.\n */\nexport function extractSections<const Name extends string>(\n markdown: string,\n names: readonly Name[]\n): Partial<Record<Name, string>> {\n const candidates = parseMarkdownSections(markdown).map((section) => ({\n ...section,\n key: normalizeHeading(section.heading),\n }));\n const requested = [...new Set(names)].map((name) => ({ name, key: normalizeHeading(name) }));\n const sections: Partial<Record<Name, string>> = Object.create(null);\n for (const { name, key } of requested) {\n let best: (typeof candidates)[number] | undefined;\n for (const candidate of candidates) {\n if (candidate.key !== key) continue;\n if (\n !best ||\n candidate.depth < best.depth ||\n (candidate.depth === best.depth && candidate.heading === name && best.heading !== name)\n ) {\n best = candidate;\n }\n }\n if (!best) continue;\n if (best.heading !== name && requested.some((other) => other.name !== name && other.key === key)) continue;\n if (best.content) sections[name] = best.content;\n }\n return sections;\n}\n\n/**\n * Returns every ATX heading of Markdown text in order, with the source text under it.\n * Only headings starting at the beginning of a line outside fenced code blocks count; setext headings (underlined\n * with `=` or `-`) are ignored. A whole-response `markdown`, `md`, or unlabeled wrapper is read inside as\n * `extractIfSingleOutermostCodeBlock` finds it, preserving nested fences. A leading wrapper followed by prose is\n * also read inside, but only when there are no headings outside fences.\n */\nexport function parseMarkdownSections(markdown: string): MarkdownSection[] {\n const block = findOutermostCodeBlock(markdown);\n const wrapper = block && WRAPPED_MARKDOWN_LANGUAGES.has(block.language) ? block : undefined;\n if (wrapper?.isWhole) return splitSections(wrapper.code);\n const sections = splitSections(markdown);\n return sections.length === 0 && wrapper ? splitSections(wrapper.code) : sections;\n}\n\nfunction normalizeHeading(heading: string): string {\n return stripHeadingEdges(stripHeadingEdges(heading).replace(/^\\d+(?:\\.\\d+)*(?:\\.\\s+|\\)\\s*|\\s+)/u, ''))\n .replaceAll(/\\s+/gu, ' ')\n .toLowerCase();\n}\n\nfunction stripHeadingEdges(text: string): string {\n // Only the edges: `_` and `*` inside a heading belong to names such as `my_file.py`.\n return text.replaceAll(/^[\\s*_`~\"'「」]+|[\\s*_`~\"'「」::]+$/gu, '');\n}\n\nfunction splitSections(markdown: string): MarkdownSection[] {\n const lines = splitLines(markdown);\n const headings = findHeadings(lines);\n return headings.map((heading, index) => {\n let nextIndex = index + 1;\n while (nextIndex < headings.length && (headings[nextIndex]?.depth ?? 0) > heading.depth) nextIndex++;\n return {\n depth: heading.depth,\n heading: heading.text,\n content: lines\n .slice(heading.index + 1, headings[nextIndex]?.index)\n .join('\\n')\n .trim(),\n };\n });\n}\n\nfunction findOutermostCodeBlock(text: string): { language: string; code: string; isWhole: boolean } | undefined {\n // Keep the opening fence's indentation, which is stripped from the contents.\n const lines = splitLines(text.replace(/^\\s*\\n/u, '').trimEnd());\n const fence = parseOpeningFence(lines[0] ?? '');\n if (!fence) return;\n const lastIndex = lines.length - 1;\n const end =\n lastIndex > 0 && isClosingFence(lines[lastIndex] ?? '', fence) ? lastIndex : findClosingFence(lines, 1, fence);\n return {\n language: fence.language,\n code: lines\n .slice(1, end)\n .map((line) => stripIndent(line, fence.indent))\n .join('\\n'),\n isWhole: end >= lastIndex,\n };\n}\n\nfunction findHeadings(lines: readonly string[]): { index: number; depth: number; text: string }[] {\n const headings: { index: number; depth: number; text: string }[] = [];\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index] ?? '';\n const fence = parseOpeningFence(line);\n if (fence) {\n index = findClosingFence(lines, index + 1, fence);\n continue;\n }\n const match = /^(#{1,6})(?:[ \\t]+(.*))?$/u.exec(line);\n if (!match) continue;\n const text = (match[2] ?? '')\n // The closing sequence of an ATX heading needs a space before it, so `# C#` keeps its `#`.\n .replace(/(?:^|[ \\t]+)#+[ \\t]*$/u, '')\n .trim()\n .replaceAll(/(`+)(.+?)\\1/gu, (_match, _delimiter: string, content: string) =>\n content.startsWith(' ') && content.endsWith(' ') ? content.slice(1, -1) : content\n );\n headings.push({ index, depth: match[1]?.length ?? 1, text });\n }\n return headings;\n}\n\nfunction parseOpeningFence(line: string): Fence | undefined {\n const match = /^( {0,3})(`{3,}|~{3,})(.*)$/u.exec(line);\n const fence = match?.[2];\n const info = match?.[3] ?? '';\n // A backtick in the info string makes the line inline code instead of a fence.\n if (!fence || (fence.startsWith('`') && info.includes('`'))) return;\n return {\n char: fence.charAt(0),\n length: fence.length,\n indent: match[1]?.length ?? 0,\n language: info.trim().split(/\\s/u)[0]?.toLowerCase() ?? '',\n };\n}\n\n/** Returns the index of the line closing `fence`, or the number of lines when it is never closed. */\nfunction findClosingFence(lines: readonly string[], start: number, fence: Fence): number {\n let index = start;\n while (index < lines.length && !isClosingFence(lines[index] ?? '', fence)) index++;\n return index;\n}\n\nfunction isClosingFence(line: string, fence: Fence): boolean {\n const closer = /^ {0,3}(`{3,}|~{3,})[ \\t]*$/u.exec(line)?.[1];\n return closer?.charAt(0) === fence.char && closer.length >= fence.length;\n}\n\nfunction stripIndent(line: string, indent: number): string {\n let index = 0;\n while (index < indent && line[index] === ' ') index++;\n return line.slice(index);\n}\n\nfunction splitLines(text: string): string[] {\n return text.split(/\\r?\\n/u);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"markdown.js","names":[],"sources":["../src/markdown.ts"],"sourcesContent":["export interface CodeBlock {\n /** The first word of the info string, lowercased; `undefined` when the fence has none. */\n language?: string;\n code: string;\n /** `false` when the text ends before the closing fence, e.g. because the LLM output was cut off. */\n isClosed: boolean;\n}\n\ninterface Fence {\n char: string;\n length: number;\n indent: number;\n language: string;\n}\n\n/**\n * Extracts the fenced code blocks (backticks or tildes, any fence length) of Markdown text such as an LLM response.\n * A block runs to the first closing fence of the same character that is at least as long as its opening fence,\n * so a longer fence can hold shorter fences. Only fences indented by at most three spaces are recognized; fences\n * inside block quotes, deeply nested list items, and indented code blocks are not.\n */\nexport function extractCodeBlocks(markdown: string): CodeBlock[] {\n const lines = splitLines(markdown);\n const blocks: CodeBlock[] = [];\n for (let index = 0; index < lines.length; index++) {\n const fence = parseOpeningFence(lines[index] ?? '');\n if (!fence) continue;\n const end = findClosingFence(lines, index + 1, fence);\n blocks.push({\n ...(fence.language && { language: fence.language }),\n code: lines\n .slice(index + 1, end)\n .map((line) => stripIndent(line, fence.indent))\n .join('\\n'),\n isClosed: end < lines.length,\n });\n index = end;\n }\n return blocks;\n}\n\n/**\n * Returns the contents of the code block that an LLM wrapped its whole response in, or `text` itself when the\n * response does not start with a fence or the fence's language is not one of a nonempty `languages` list\n * (compared case-insensitively). An omitted or empty list allows any language.\n * When the response also ends with a matching closing fence, everything between its first and last lines is returned,\n * so fences the LLM nested without lengthening the outer fence stay in the contents. Otherwise the first block is\n * returned and text after it is dropped; an unclosed block runs to the end of the text.\n */\nexport function extractIfSingleOutermostCodeBlock(text: string, languages?: readonly string[]): string {\n const block = findOutermostCodeBlock(text);\n if (!block) return text;\n if (languages?.length && !languages.some((language) => language.toLowerCase() === block.language)) return text;\n return block.code;\n}\n\nconst WRAPPED_MARKDOWN_LANGUAGES = new Set(['', 'markdown', 'md']);\n\n/**\n * Returns the trimmed contents of the first closed fenced code block (backticks or tildes) that follows a line holding\n * only `<tagName>` in text such as an LLM response, e.g. `<answer>\\n~~~\\n42\\n~~~`, or `undefined` when there is none.\n * Only blank lines may separate the tag line from the opening fence. Mentions of the tag within prose lines and tag\n * lines inside other fenced blocks (e.g. a format example) are skipped, but a whole-response `markdown`, `md`, or\n * unlabeled wrapper is read inside, as `parseMarkdownSections` does.\n */\nexport function extractTaggedCodeBlock(text: string, tagName: string): string | undefined {\n const tag = `<${tagName}>`;\n const found = findTaggedCodeBlock(splitLines(text), tag);\n if (found !== undefined) return found;\n // Only a real wrapper hides every tag line from the scan above; a leading example block paired with the tagged\n // block's closing fence merely looks like one, so its contents must not be read first.\n const wrapper = findOutermostCodeBlock(text);\n if (wrapper?.isWhole && WRAPPED_MARKDOWN_LANGUAGES.has(wrapper.language)) {\n return findTaggedCodeBlock(splitLines(wrapper.code), tag);\n }\n}\n\nfunction findTaggedCodeBlock(lines: readonly string[], tag: string): string | undefined {\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index] ?? '';\n const enclosingFence = parseOpeningFence(line);\n if (enclosingFence) {\n index = findClosingFence(lines, index + 1, enclosingFence);\n continue;\n }\n if (line.trim() !== tag) continue;\n let fenceIndex = index + 1;\n while (fenceIndex < lines.length && !lines[fenceIndex]?.trim()) fenceIndex++;\n const fence = parseOpeningFence(lines[fenceIndex] ?? '');\n if (!fence) continue;\n const end = findClosingFence(lines, fenceIndex + 1, fence);\n if (end < lines.length) {\n return lines\n .slice(fenceIndex + 1, end)\n .map((contentLine) => stripIndent(contentLine, fence.indent))\n .join('\\n')\n .trim();\n }\n }\n}\n\nexport interface MarkdownSection {\n depth: number;\n /** The heading text with the closing `#` sequence and inline-code backticks removed. */\n heading: string;\n /** The source text up to the next heading at the same or a shallower depth, trimmed; subsections are included. */\n content: string;\n}\n\n/**\n * Extracts the content of the section headed by each of `names` from Markdown text such as an LLM response, keyed by\n * the given names. A heading matches a name when both are equal after removing decorations at their edges (such as\n * `**bold**`, backticks, and quotes), a leading number like `1.` or `2.1`, and a trailing colon, ignoring case and spacing;\n * headings at any depth are candidates.\n * Among matching headings, the shallowest wins, then one equal to the name as written, then the first; each heading\n * serves at most one name. When distinct requested names normalize alike, only exact matches at the shallowest\n * matching depth are returned. Missing, empty, or ambiguous sections are absent from the prototype-free result.\n */\nexport function extractSections<const Name extends string>(\n markdown: string,\n names: readonly Name[]\n): Partial<Record<Name, string>> {\n const candidates = parseMarkdownSections(markdown).map((section) => ({\n ...section,\n key: normalizeHeading(section.heading),\n }));\n const requested = [...new Set(names)].map((name) => ({ name, key: normalizeHeading(name) }));\n const sections: Partial<Record<Name, string>> = Object.create(null);\n for (const { name, key } of requested) {\n let best: (typeof candidates)[number] | undefined;\n for (const candidate of candidates) {\n if (candidate.key !== key) continue;\n if (\n !best ||\n candidate.depth < best.depth ||\n (candidate.depth === best.depth && candidate.heading === name && best.heading !== name)\n ) {\n best = candidate;\n }\n }\n if (!best) continue;\n if (best.heading !== name && requested.some((other) => other.name !== name && other.key === key)) continue;\n if (best.content) sections[name] = best.content;\n }\n return sections;\n}\n\n/**\n * Returns every ATX heading of Markdown text in order, with the source text under it.\n * Only headings starting at the beginning of a line outside fenced code blocks count; setext headings (underlined\n * with `=` or `-`) are ignored. A whole-response `markdown`, `md`, or unlabeled wrapper is read inside as\n * `extractIfSingleOutermostCodeBlock` finds it, preserving nested fences. A leading wrapper followed by prose is\n * also read inside, but only when there are no headings outside fences.\n */\nexport function parseMarkdownSections(markdown: string): MarkdownSection[] {\n const block = findOutermostCodeBlock(markdown);\n const wrapper = block && WRAPPED_MARKDOWN_LANGUAGES.has(block.language) ? block : undefined;\n if (wrapper?.isWhole) return splitSections(wrapper.code);\n const sections = splitSections(markdown);\n return sections.length === 0 && wrapper ? splitSections(wrapper.code) : sections;\n}\n\nfunction normalizeHeading(heading: string): string {\n return stripHeadingEdges(stripHeadingEdges(heading).replace(/^\\d+(?:\\.\\d+)*(?:\\.\\s+|\\)\\s*|\\s+)/u, ''))\n .replaceAll(/\\s+/gu, ' ')\n .toLowerCase();\n}\n\nfunction stripHeadingEdges(text: string): string {\n // Only the edges: `_` and `*` inside a heading belong to names such as `my_file.py`.\n return text.replaceAll(/^[\\s*_`~\"'「」]+|[\\s*_`~\"'「」::]+$/gu, '');\n}\n\nfunction splitSections(markdown: string): MarkdownSection[] {\n const lines = splitLines(markdown);\n const headings = findHeadings(lines);\n return headings.map((heading, index) => {\n let nextIndex = index + 1;\n while (nextIndex < headings.length && (headings[nextIndex]?.depth ?? 0) > heading.depth) nextIndex++;\n return {\n depth: heading.depth,\n heading: heading.text,\n content: lines\n .slice(heading.index + 1, headings[nextIndex]?.index)\n .join('\\n')\n .trim(),\n };\n });\n}\n\nfunction findOutermostCodeBlock(text: string): { language: string; code: string; isWhole: boolean } | undefined {\n // Keep the opening fence's indentation, which is stripped from the contents.\n const lines = splitLines(text.replace(/^\\s*\\n/u, '').trimEnd());\n const fence = parseOpeningFence(lines[0] ?? '');\n if (!fence) return;\n const lastIndex = lines.length - 1;\n const end =\n lastIndex > 0 && isClosingFence(lines[lastIndex] ?? '', fence) ? lastIndex : findClosingFence(lines, 1, fence);\n return {\n language: fence.language,\n code: lines\n .slice(1, end)\n .map((line) => stripIndent(line, fence.indent))\n .join('\\n'),\n isWhole: end >= lastIndex,\n };\n}\n\nfunction findHeadings(lines: readonly string[]): { index: number; depth: number; text: string }[] {\n const headings: { index: number; depth: number; text: string }[] = [];\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index] ?? '';\n const fence = parseOpeningFence(line);\n if (fence) {\n index = findClosingFence(lines, index + 1, fence);\n continue;\n }\n const match = /^(#{1,6})(?:[ \\t]+(.*))?$/u.exec(line);\n if (!match) continue;\n const text = (match[2] ?? '')\n // The closing sequence of an ATX heading needs a space before it, so `# C#` keeps its `#`.\n .replace(/(?:^|[ \\t]+)#+[ \\t]*$/u, '')\n .trim()\n .replaceAll(/(`+)(.+?)\\1/gu, (_match, _delimiter: string, content: string) =>\n content.startsWith(' ') && content.endsWith(' ') ? content.slice(1, -1) : content\n );\n headings.push({ index, depth: match[1]?.length ?? 1, text });\n }\n return headings;\n}\n\nfunction parseOpeningFence(line: string): Fence | undefined {\n const match = /^( {0,3})(`{3,}|~{3,})(.*)$/u.exec(line);\n const fence = match?.[2];\n const info = match?.[3] ?? '';\n // A backtick in the info string makes the line inline code instead of a fence.\n if (!fence || (fence.startsWith('`') && info.includes('`'))) return;\n return {\n char: fence.charAt(0),\n length: fence.length,\n indent: match[1]?.length ?? 0,\n language: info.trim().split(/\\s/u)[0]?.toLowerCase() ?? '',\n };\n}\n\n/** Returns the index of the line closing `fence`, or the number of lines when it is never closed. */\nfunction findClosingFence(lines: readonly string[], start: number, fence: Fence): number {\n let index = start;\n while (index < lines.length && !isClosingFence(lines[index] ?? '', fence)) index++;\n return index;\n}\n\nfunction isClosingFence(line: string, fence: Fence): boolean {\n const closer = /^ {0,3}(`{3,}|~{3,})[ \\t]*$/u.exec(line)?.[1];\n return closer?.charAt(0) === fence.char && closer.length >= fence.length;\n}\n\nfunction stripIndent(line: string, indent: number): string {\n let index = 0;\n while (index < indent && line[index] === ' ') index++;\n return line.slice(index);\n}\n\nfunction splitLines(text: string): string[] {\n return text.split(/\\r?\\n/u);\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";const e=require("./sleep.cjs");function t({intervalMilliseconds:t,rateLimitFallbackMilliseconds:n}){let i=0,a=Promise.resolve();return async(o,s)=>{let c=a,l=(async()=>{for(await c;i>performance.now();)await e.sleep(Math.min(i-performance.now(),2147483647));i=performance.now()+t})();a=l,await l;let u=await fetch(o,s);if(u.status===429){let e=r(u.headers.get(`retry-after`));i=Math.max(i,performance.now()+(e??n))}return u}}const n=/^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/u;function r(e){let t=e?.trim()??``,r=/^\d+$/u.test(t)?Number(t)*1e3:n.test(t)?Date.parse(t)-Date.now():NaN;return Number.isFinite(r)?Math.max(0,r):void 0}exports.createThrottledFetch=t;
2
+ //# sourceMappingURL=throttledFetch.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throttledFetch.cjs","names":["sleep"],"sources":["../src/throttledFetch.ts"],"sourcesContent":["import { sleep } from './sleep.js';\n\n// `setTimeout` fires after 1 ms for longer delays, which would turn a long `Retry-After` hold into a busy loop.\nconst MAX_TIMER_MILLISECONDS = 2_147_483_647;\n\nexport interface ThrottledFetchOptions {\n /** The minimum interval between the starts of consecutive requests. */\n intervalMilliseconds: number;\n /** How long to hold later requests after a 429 response without `Retry-After` in delay seconds or IMF-fixdate. */\n rateLimitFallbackMilliseconds: number;\n}\n\n/**\n * Creates a `fetch` for a rate-limited API that starts requests in call order, at least `intervalMilliseconds` apart.\n * A 429 response holds every later request until its `Retry-After` (delay seconds or an IMF-fixdate) elapses, including\n * requests already waiting; the 429 response itself is returned to the caller, not retried. An abort signal takes effect\n * only once its request starts, not while it waits in the queue.\n */\nexport function createThrottledFetch({\n intervalMilliseconds,\n rateLimitFallbackMilliseconds,\n}: ThrottledFetchOptions): (input: string | URL | Request, init?: RequestInit) => Promise<Response> {\n let nextRequestAt = 0;\n let admission = Promise.resolve();\n\n return async (input, init) => {\n const previousAdmission = admission;\n const ready = (async () => {\n await previousAdmission;\n // A response to an earlier request can push `nextRequestAt` back while this request sleeps.\n while (nextRequestAt > performance.now()) {\n await sleep(Math.min(nextRequestAt - performance.now(), MAX_TIMER_MILLISECONDS));\n }\n nextRequestAt = performance.now() + intervalMilliseconds;\n })();\n admission = ready;\n await ready;\n\n const response = await fetch(input, init);\n if (response.status === 429) {\n const waitMilliseconds = parseRetryAfterMilliseconds(response.headers.get('retry-after'));\n nextRequestAt = Math.max(nextRequestAt, performance.now() + (waitMilliseconds ?? rateLimitFallbackMilliseconds));\n }\n return response;\n };\n}\n\n// RFC 9110 requires senders to generate HTTP dates in this IMF-fixdate form, e.g. `Sun, 06 Nov 1994 08:49:37 GMT`.\n// `Date.parse` alone would also accept malformed values such as `1.5`.\nconst IMF_FIXDATE = /^[A-Z][a-z]{2}, \\d{2} [A-Z][a-z]{2} \\d{4} \\d{2}:\\d{2}:\\d{2} GMT$/u;\n\n/** Parses `Retry-After` in delay seconds or IMF-fixdate; the obsolete RFC 850 and asctime dates yield `undefined`. */\nfunction parseRetryAfterMilliseconds(value: string | null): number | undefined {\n const trimmed = value?.trim() ?? '';\n const milliseconds = /^\\d+$/u.test(trimmed)\n ? Number(trimmed) * 1000\n : IMF_FIXDATE.test(trimmed)\n ? Date.parse(trimmed) - Date.now()\n : Number.NaN;\n return Number.isFinite(milliseconds) ? Math.max(0, milliseconds) : undefined;\n}\n"],"mappings":"4CAkBA,SAAgB,EAAqB,CACnC,uBACA,iCACkG,CAClG,IAAI,EAAgB,EAChB,EAAY,QAAQ,QAAQ,EAEhC,OAAO,MAAO,EAAO,IAAS,CAC5B,IAAM,EAAoB,EACpB,GAAS,SAAY,CAGzB,IAFA,MAAM,EAEC,EAAgB,YAAY,IAAI,GACrC,MAAMA,EAAAA,MAAM,KAAK,IAAI,EAAgB,YAAY,IAAI,EAAG,UAAsB,CAAC,EAEjF,EAAgB,YAAY,IAAI,EAAI,CACtC,EAAA,CAAG,EACH,EAAY,EACZ,MAAM,EAEN,IAAM,EAAW,MAAM,MAAM,EAAO,CAAI,EACxC,GAAI,EAAS,SAAW,IAAK,CAC3B,IAAM,EAAmB,EAA4B,EAAS,QAAQ,IAAI,aAAa,CAAC,EACxF,EAAgB,KAAK,IAAI,EAAe,YAAY,IAAI,GAAK,GAAoB,EAA8B,CACjH,CACA,OAAO,CACT,CACF,CAIA,MAAM,EAAc,oEAGpB,SAAS,EAA4B,EAA0C,CAC7E,IAAM,EAAU,GAAO,KAAK,GAAK,GAC3B,EAAe,SAAS,KAAK,CAAO,EACtC,OAAO,CAAO,EAAI,IAClB,EAAY,KAAK,CAAO,EACtB,KAAK,MAAM,CAAO,EAAI,KAAK,IAAI,EAC/B,IACN,OAAO,OAAO,SAAS,CAAY,EAAI,KAAK,IAAI,EAAG,CAAY,EAAI,IAAA,EACrE"}
@@ -0,0 +1,13 @@
1
+ export interface ThrottledFetchOptions {
2
+ /** The minimum interval between the starts of consecutive requests. */
3
+ intervalMilliseconds: number;
4
+ /** How long to hold later requests after a 429 response without `Retry-After` in delay seconds or IMF-fixdate. */
5
+ rateLimitFallbackMilliseconds: number;
6
+ }
7
+ /**
8
+ * Creates a `fetch` for a rate-limited API that starts requests in call order, at least `intervalMilliseconds` apart.
9
+ * A 429 response holds every later request until its `Retry-After` (delay seconds or an IMF-fixdate) elapses, including
10
+ * requests already waiting; the 429 response itself is returned to the caller, not retried. An abort signal takes effect
11
+ * only once its request starts, not while it waits in the queue.
12
+ */
13
+ export declare function createThrottledFetch({ intervalMilliseconds, rateLimitFallbackMilliseconds, }: ThrottledFetchOptions): (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
@@ -0,0 +1,2 @@
1
+ import{sleep as e}from"./sleep.js";function t({intervalMilliseconds:t,rateLimitFallbackMilliseconds:n}){let i=0,a=Promise.resolve();return async(o,s)=>{let c=a,l=(async()=>{for(await c;i>performance.now();)await e(Math.min(i-performance.now(),2147483647));i=performance.now()+t})();a=l,await l;let u=await fetch(o,s);if(u.status===429){let e=r(u.headers.get(`retry-after`));i=Math.max(i,performance.now()+(e??n))}return u}}const n=/^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/u;function r(e){let t=e?.trim()??``,r=/^\d+$/u.test(t)?Number(t)*1e3:n.test(t)?Date.parse(t)-Date.now():NaN;return Number.isFinite(r)?Math.max(0,r):void 0}export{t as createThrottledFetch};
2
+ //# sourceMappingURL=throttledFetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throttledFetch.js","names":[],"sources":["../src/throttledFetch.ts"],"sourcesContent":["import { sleep } from './sleep.js';\n\n// `setTimeout` fires after 1 ms for longer delays, which would turn a long `Retry-After` hold into a busy loop.\nconst MAX_TIMER_MILLISECONDS = 2_147_483_647;\n\nexport interface ThrottledFetchOptions {\n /** The minimum interval between the starts of consecutive requests. */\n intervalMilliseconds: number;\n /** How long to hold later requests after a 429 response without `Retry-After` in delay seconds or IMF-fixdate. */\n rateLimitFallbackMilliseconds: number;\n}\n\n/**\n * Creates a `fetch` for a rate-limited API that starts requests in call order, at least `intervalMilliseconds` apart.\n * A 429 response holds every later request until its `Retry-After` (delay seconds or an IMF-fixdate) elapses, including\n * requests already waiting; the 429 response itself is returned to the caller, not retried. An abort signal takes effect\n * only once its request starts, not while it waits in the queue.\n */\nexport function createThrottledFetch({\n intervalMilliseconds,\n rateLimitFallbackMilliseconds,\n}: ThrottledFetchOptions): (input: string | URL | Request, init?: RequestInit) => Promise<Response> {\n let nextRequestAt = 0;\n let admission = Promise.resolve();\n\n return async (input, init) => {\n const previousAdmission = admission;\n const ready = (async () => {\n await previousAdmission;\n // A response to an earlier request can push `nextRequestAt` back while this request sleeps.\n while (nextRequestAt > performance.now()) {\n await sleep(Math.min(nextRequestAt - performance.now(), MAX_TIMER_MILLISECONDS));\n }\n nextRequestAt = performance.now() + intervalMilliseconds;\n })();\n admission = ready;\n await ready;\n\n const response = await fetch(input, init);\n if (response.status === 429) {\n const waitMilliseconds = parseRetryAfterMilliseconds(response.headers.get('retry-after'));\n nextRequestAt = Math.max(nextRequestAt, performance.now() + (waitMilliseconds ?? rateLimitFallbackMilliseconds));\n }\n return response;\n };\n}\n\n// RFC 9110 requires senders to generate HTTP dates in this IMF-fixdate form, e.g. `Sun, 06 Nov 1994 08:49:37 GMT`.\n// `Date.parse` alone would also accept malformed values such as `1.5`.\nconst IMF_FIXDATE = /^[A-Z][a-z]{2}, \\d{2} [A-Z][a-z]{2} \\d{4} \\d{2}:\\d{2}:\\d{2} GMT$/u;\n\n/** Parses `Retry-After` in delay seconds or IMF-fixdate; the obsolete RFC 850 and asctime dates yield `undefined`. */\nfunction parseRetryAfterMilliseconds(value: string | null): number | undefined {\n const trimmed = value?.trim() ?? '';\n const milliseconds = /^\\d+$/u.test(trimmed)\n ? Number(trimmed) * 1000\n : IMF_FIXDATE.test(trimmed)\n ? Date.parse(trimmed) - Date.now()\n : Number.NaN;\n return Number.isFinite(milliseconds) ? Math.max(0, milliseconds) : undefined;\n}\n"],"mappings":"mCAkBA,SAAgB,EAAqB,CACnC,uBACA,iCACkG,CAClG,IAAI,EAAgB,EAChB,EAAY,QAAQ,QAAQ,EAEhC,OAAO,MAAO,EAAO,IAAS,CAC5B,IAAM,EAAoB,EACpB,GAAS,SAAY,CAGzB,IAFA,MAAM,EAEC,EAAgB,YAAY,IAAI,GACrC,MAAM,EAAM,KAAK,IAAI,EAAgB,YAAY,IAAI,EAAG,UAAsB,CAAC,EAEjF,EAAgB,YAAY,IAAI,EAAI,CACtC,EAAA,CAAG,EACH,EAAY,EACZ,MAAM,EAEN,IAAM,EAAW,MAAM,MAAM,EAAO,CAAI,EACxC,GAAI,EAAS,SAAW,IAAK,CAC3B,IAAM,EAAmB,EAA4B,EAAS,QAAQ,IAAI,aAAa,CAAC,EACxF,EAAgB,KAAK,IAAI,EAAe,YAAY,IAAI,GAAK,GAAoB,EAA8B,CACjH,CACA,OAAO,CACT,CACF,CAIA,MAAM,EAAc,oEAGpB,SAAS,EAA4B,EAA0C,CAC7E,IAAM,EAAU,GAAO,KAAK,GAAK,GAC3B,EAAe,SAAS,KAAK,CAAO,EACtC,OAAO,CAAO,EAAI,IAClB,EAAY,KAAK,CAAO,EACtB,KAAK,MAAM,CAAO,EAAI,KAAK,IAAI,EAC/B,IACN,OAAO,OAAO,SAAS,CAAY,EAAI,KAAK,IAAI,EAAG,CAAY,EAAI,IAAA,EACrE"}
package/dist/url.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";function e(e,{origin:t,fallback:n=`/`}){let r=new URL(t),i=(typeof e==`string`?e:e?.[0])?.trim();if(!i)return n;let a;try{a=new URL(i,r)}catch{return n}return a.protocol!==r.protocol||a.origin!==r.origin||a.pathname.startsWith(`//`)?n:`${a.pathname}${a.search}${a.hash}`}exports.getSafeRedirectPath=e;
2
+ //# sourceMappingURL=url.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url.cjs","names":[],"sources":["../src/url.ts"],"sourcesContent":["export interface SafeRedirectPathOptions {\n /** The app's own origin, e.g. `https://example.com`; an unparsable origin throws a `TypeError`. */\n origin: string;\n /** Returned when `value` does not point to a page on `origin`. */\n fallback?: string;\n}\n\n/**\n * Returns the path, query, and fragment of `value` when it points to a page on the app's own origin, and `fallback`\n * (`/` by default) otherwise, so that redirecting to a user-supplied callback URL cannot lead to another site.\n * `value` is resolved against `origin` the way browsers resolve links, so relative paths and absolute URLs on\n * `origin` are accepted, while `//evil.example`, `/\\evil.example`, and `/\\t/evil.example` are rejected. For an array,\n * such as a repeated query parameter, the first element is used.\n */\nexport function getSafeRedirectPath(\n value: string | readonly string[] | null | undefined,\n { origin, fallback = '/' }: SafeRedirectPathOptions\n): string {\n // Parsed outside the per-value `try` so that a misconfigured origin fails loudly instead of rejecting every value.\n const baseUrl = new URL(origin);\n const rawValue = (typeof value === 'string' ? value : value?.[0])?.trim();\n if (!rawValue) return fallback;\n\n let url: URL;\n try {\n url = new URL(rawValue, baseUrl);\n } catch {\n return fallback;\n }\n // A `blob:` URL shares its inner URL's origin but has that URL as its pathname, hence the protocol check.\n // Dot segments can leave a pathname such as `//evil.example`, which is protocol-relative once returned as a path.\n if (url.protocol !== baseUrl.protocol || url.origin !== baseUrl.origin || url.pathname.startsWith('//')) {\n return fallback;\n }\n return `${url.pathname}${url.search}${url.hash}`;\n}\n"],"mappings":"aAcA,SAAgB,EACd,EACA,CAAE,SAAQ,WAAW,KACb,CAER,IAAM,EAAU,IAAI,IAAI,CAAM,EACxB,GAAY,OAAO,GAAU,SAAW,EAAQ,IAAQ,GAAA,EAAK,KAAK,EACxE,GAAI,CAAC,EAAU,OAAO,EAEtB,IAAI,EACJ,GAAI,CACF,EAAM,IAAI,IAAI,EAAU,CAAO,CACjC,MAAQ,CACN,OAAO,CACT,CAMA,OAHI,EAAI,WAAa,EAAQ,UAAY,EAAI,SAAW,EAAQ,QAAU,EAAI,SAAS,WAAW,IAAI,EAC7F,EAEF,GAAG,EAAI,WAAW,EAAI,SAAS,EAAI,MAC5C"}
package/dist/url.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export interface SafeRedirectPathOptions {
2
+ /** The app's own origin, e.g. `https://example.com`; an unparsable origin throws a `TypeError`. */
3
+ origin: string;
4
+ /** Returned when `value` does not point to a page on `origin`. */
5
+ fallback?: string;
6
+ }
7
+ /**
8
+ * Returns the path, query, and fragment of `value` when it points to a page on the app's own origin, and `fallback`
9
+ * (`/` by default) otherwise, so that redirecting to a user-supplied callback URL cannot lead to another site.
10
+ * `value` is resolved against `origin` the way browsers resolve links, so relative paths and absolute URLs on
11
+ * `origin` are accepted, while `//evil.example`, `/\evil.example`, and `/\t/evil.example` are rejected. For an array,
12
+ * such as a repeated query parameter, the first element is used.
13
+ */
14
+ export declare function getSafeRedirectPath(value: string | readonly string[] | null | undefined, { origin, fallback }: SafeRedirectPathOptions): string;
package/dist/url.js ADDED
@@ -0,0 +1,2 @@
1
+ function e(e,{origin:t,fallback:n=`/`}){let r=new URL(t),i=(typeof e==`string`?e:e?.[0])?.trim();if(!i)return n;let a;try{a=new URL(i,r)}catch{return n}return a.protocol!==r.protocol||a.origin!==r.origin||a.pathname.startsWith(`//`)?n:`${a.pathname}${a.search}${a.hash}`}export{e as getSafeRedirectPath};
2
+ //# sourceMappingURL=url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url.js","names":[],"sources":["../src/url.ts"],"sourcesContent":["export interface SafeRedirectPathOptions {\n /** The app's own origin, e.g. `https://example.com`; an unparsable origin throws a `TypeError`. */\n origin: string;\n /** Returned when `value` does not point to a page on `origin`. */\n fallback?: string;\n}\n\n/**\n * Returns the path, query, and fragment of `value` when it points to a page on the app's own origin, and `fallback`\n * (`/` by default) otherwise, so that redirecting to a user-supplied callback URL cannot lead to another site.\n * `value` is resolved against `origin` the way browsers resolve links, so relative paths and absolute URLs on\n * `origin` are accepted, while `//evil.example`, `/\\evil.example`, and `/\\t/evil.example` are rejected. For an array,\n * such as a repeated query parameter, the first element is used.\n */\nexport function getSafeRedirectPath(\n value: string | readonly string[] | null | undefined,\n { origin, fallback = '/' }: SafeRedirectPathOptions\n): string {\n // Parsed outside the per-value `try` so that a misconfigured origin fails loudly instead of rejecting every value.\n const baseUrl = new URL(origin);\n const rawValue = (typeof value === 'string' ? value : value?.[0])?.trim();\n if (!rawValue) return fallback;\n\n let url: URL;\n try {\n url = new URL(rawValue, baseUrl);\n } catch {\n return fallback;\n }\n // A `blob:` URL shares its inner URL's origin but has that URL as its pathname, hence the protocol check.\n // Dot segments can leave a pathname such as `//evil.example`, which is protocol-relative once returned as a path.\n if (url.protocol !== baseUrl.protocol || url.origin !== baseUrl.origin || url.pathname.startsWith('//')) {\n return fallback;\n }\n return `${url.pathname}${url.search}${url.hash}`;\n}\n"],"mappings":"AAcA,SAAgB,EACd,EACA,CAAE,SAAQ,WAAW,KACb,CAER,IAAM,EAAU,IAAI,IAAI,CAAM,EACxB,GAAY,OAAO,GAAU,SAAW,EAAQ,IAAQ,GAAA,EAAK,KAAK,EACxE,GAAI,CAAC,EAAU,OAAO,EAEtB,IAAI,EACJ,GAAI,CACF,EAAM,IAAI,IAAI,EAAU,CAAO,CACjC,MAAQ,CACN,OAAO,CACT,CAMA,OAHI,EAAI,WAAa,EAAQ,UAAY,EAAI,SAAW,EAAQ,QAAU,EAAI,SAAS,WAAW,IAAI,EAC7F,EAEF,GAAG,EAAI,WAAW,EAAI,SAAS,EAAI,MAC5C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willbooster/shared-lib",
3
- "version": "10.15.0",
3
+ "version": "10.16.1",
4
4
  "description": "Shared library for WillBooster projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,17 +47,17 @@
47
47
  "@tsconfig/node-lts": "24.0.1",
48
48
  "@tsconfig/node-ts": "23.6.4",
49
49
  "@types/bun": "1.4.2",
50
- "@types/node": "26.5.1",
50
+ "@types/node": "26.6.2",
51
51
  "@willbooster/oxfmt-config": "1.2.2",
52
52
  "@willbooster/oxlint-config": "1.4.8",
53
- "@willbooster/wb": "22.7.0",
53
+ "@willbooster/wb": "22.7.1",
54
54
  "build-ts": "21.0.16",
55
55
  "oxfmt": "0.68.0",
56
56
  "oxlint": "1.83.0",
57
- "oxlint-tsgolint": "7.0.2001",
57
+ "oxlint-tsgolint": "7.0.2002",
58
58
  "sort-package-json": "4.0.0",
59
59
  "typescript": "7.0.2",
60
- "yaml": "2.9.0"
60
+ "yaml": "2.9.1"
61
61
  },
62
62
  "publishConfig": {
63
63
  "access": "public",