@willbooster/shared-lib 10.14.0 → 10.16.0

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"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=/\p{Script=Hangul}/u,t=/\p{Script=Han}/u,n=new Set(`个乘亂亞从佛來們假傳僞價儉兒兩册刄剩劍劑勞勳勵勸區卷參听呀咯哇哦唔單嘛嚴囑囘圈國圍圓圖團墮壓壘壞壤壯壹壻壽奧奬姙孃學寢實寫寶將專對屆屬峽巖帶广廢廣廳彈很徑從怎恆惡惱愼慘懷戀戰戲拂拔拜挾插搖搜擇擔據擧擴攜攝收效敍敕數斷晝曉會條棧榮樂樓樞樣檢權歐歡歸殘殼毆氣沒淨淺滯滿潛澁濕濟灣燈燒營爐爭爲犧狹獨獵獸獻瓣畫當疊癡發盜盡碎碼祕禪禮稱稻穗穩竊竝粹絲經縣縱總繩繪繼續纖缺罐聲聽肅腦膽臟臺與舉舊舍舖艷莊莖萬藏藥處號螢蟲蠶蠻衞裝襃覺覽觀觸謠證譯譽讀變讓讚豐豫貳賣贊跟踐輕轉辨辭辯這遙遞遲鄰醉醫釀釋錢鎭鏈鐵鑄鑛關陷隨險隱隸雖雙雜霸靈靜顏顯飜飮餘餠騷驅驗驛髓體髮鬪鷄鹽麥麼黏默點黨齊齒齡龜`);let r;function i(t,n){let r=a(t,n),i=[];return r.some(t=>!e.test(t))&&i.push(`chinese`),r.some(t=>e.test(t))&&i.push(`korean`),i}function a(i,a=``){let s=new Set(a.normalize(`NFKC`)),c=r??=o(),l=new Set;for(let r of i.normalize(`NFKC`))s.has(r)||(e.test(r)||t.test(r)&&(!c.has(r)||n.has(r)))&&l.add(r);return[...l]}function o(){let e=new TextDecoder(`shift_jis`),t=new Set(`𠮟塡剝頰`);for(let n=129;n<=234;n++)if(!(n>=160&&n<=223))for(let r=64;r<=252;r++)t.add(e.decode(new Uint8Array([n,r])));return t}exports.detectForeignCjkInJapanese=i,exports.findForeignCjkCharactersInJapanese=a;
1
+ "use strict";const e=/\p{Script=Hangul}/u,t=/\p{Script=Han}/u,n=new Set(`个乘亂亞从佛來們假傳僞價儉兒兩册刄剩劍劑勞勳勵勸區卷參听呀咯哇哦唔單嘛嚴囑囘圈國圍圓圖團墮壓壘壞壤壯壹壻壽奧奬姙孃學寢實寫寶將專對屆屬峽巖帶广廢廣廳彈很徑從怎恆惡惱愼慘懷戀戰戲拂拔拜挾插搖搜擇擔據擧擴攜攝收效敍敕數斷晝曉會條棧榮樂樓樞樣檢權歐歡歸殘殼毆氣沒淨淺滯滿潛澁濕濟灣燈燒營爐爭爲犧狹獨獵獸獻瓣畫當疊癡發盜盡碎碼祕禪禮稱稻穗穩竊竝粹絲經縣縱總繩繪繼續纖缺罐聲聽肅腦膽臟臺與舉舊舍舖艷莊莖萬藏藥處號螢蟲蠶蠻衞裝襃覺覽觀觸謠證譯譽讀變讓讚豐豫貳賣贊跟踐輕轉辨辭辯這遙遞遲鄰醉醫釀釋錢鎭鏈鐵鑄鑛關陷隨險隱隸雖雙雜霸靈靜顏顯飜飮餘餠騷驅驗驛髓體髮鬪鷄鹽麥麼黏默點黨齊齒齡龜`);let r;function i(t,n){let r=a(t,n),i=[];return r.some(t=>!e.test(t))&&i.push(`chinese`),r.some(t=>e.test(t))&&i.push(`korean`),i}function a(i,a=``){let s=new Set(a.normalize(`NFKC`)),c=r??=o(),l=new Set;for(let r of i.normalize(`NFKC`))s.has(r)||(e.test(r)||t.test(r)&&(!c.has(r)||n.has(r)))&&l.add(r);return[...l]}function o(){let e=new TextDecoder(`shift_jis`),t=new Set(`𠮟塡剝頰仼伃伹侊俿偂僘僴兊兤冝凬刕劜劯匇匤厓叝咊咜喆坙坥垬埈墲夋奛奝奣尞岺峵嵓嵭嶹巐弡弴彅惞愑愰憘戓抦敎昞昻晳暿曺曻朎杦栁桒棏樰橳櫢櫤涖淸澵濵焏犱犾猤玽甁硺礰竧箞絈緖罇荢葈蓜蘒蠇裵褜訷贒郞鄕釞釥鈼鉙鉷鍈鏆隝隯霳霻靍靏靑靕髙魲鮏鮱鮻鰀﨎﨏﨑﨓﨔﨟﨡﨣﨤﨧﨨﨩𠮷`);for(let n=129;n<=234;n++)if(!(n>=160&&n<=223))for(let r=64;r<=252;r++)t.add(e.decode(new Uint8Array([n,r])));return t}exports.detectForeignCjkInJapanese=i,exports.findForeignCjkCharactersInJapanese=a;
2
2
  //# sourceMappingURL=foreignCjk.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"foreignCjk.cjs","names":[],"sources":["../src/foreignCjk.ts"],"sourcesContent":["export type ForeignCjkLanguage = 'chinese' | 'korean';\n\nconst HANGUL_REGEX = /\\p{Script=Hangul}/u;\nconst HAN_REGEX = /\\p{Script=Han}/u;\n// JIS X 0208 cannot encode these Jōyō kanji, which were added in 2010.\nconst JOYO_KANJI_OUTSIDE_JIS_X_0208 = '𠮟塡剝頰';\n// Characters JIS X 0208 can encode but modern Japanese prose does not use:\n// - old forms (kyūjitai) of Jōyō kanji, from the `kyujitai` npm package and Unihan's kJapaneseOldVariant,\n// except those still common in Japanese words or names (e.g., 應 in 慶應, 澤, 邊, 龍, 藝 in 東京藝術大学, 糺す)\n// - Chinese function words that Japanese writes in kana (e.g., 們, 這, 麼, 很, 怎, 跟, 嘛) and stray simplified forms (e.g., 个)\n// - traditional Chinese characters that neither table covers but Japanese never uses: 碼, 舉, 鏈, 雖\n// Other Chinese-only content characters (e.g., 媽, 豬) are not listed because separating them from rare kanji\n// that Japanese prose does use (e.g., 乖離, 瑕疵, 齟齬, 俯瞰) needs Japanese usage frequencies.\nconst CHINESE_CHARACTERS_IN_JIS_X_0208 = new Set(\n '个乘亂亞从佛來們假傳僞價儉兒兩册刄剩劍劑勞勳勵勸區卷參听呀咯哇哦唔單嘛嚴囑囘圈國圍圓圖團墮壓壘壞壤壯壹壻壽奧奬姙孃學寢實寫寶將專對屆屬峽巖帶广廢廣廳彈很徑從怎恆惡惱愼慘懷戀戰戲拂拔拜挾插搖搜擇擔據擧擴攜攝收效敍敕數斷晝曉會條棧榮樂樓樞樣檢權歐歡歸殘殼毆氣沒淨淺滯滿潛澁濕濟灣燈燒營爐爭爲犧狹獨獵獸獻瓣畫當疊癡發盜盡碎碼祕禪禮稱稻穗穩竊竝粹絲經縣縱總繩繪繼續纖缺罐聲聽肅腦膽臟臺與舉舊舍舖艷莊莖萬藏藥處號螢蟲蠶蠻衞裝襃覺覽觀觸謠證譯譽讀變讓讚豐豫貳賣贊跟踐輕轉辨辭辯這遙遞遲鄰醉醫釀釋錢鎭鏈鐵鑄鑛關陷隨險隱隸雖雙雜霸靈靜顏顯飜飮餘餠騷驅驗驛髓體髮鬪鷄鹽麥麼黏默點黨齊齒齡龜'\n);\n\nlet japaneseKanji: Set<string> | undefined;\n\n/**\n * Detects Chinese or Korean characters mixed into Japanese text, such as LLM-generated Japanese prose.\n * See {@link findForeignCjkCharactersInJapanese} for which characters count and how `allowedText` works.\n * Returns the detected languages, or an empty array if none are found.\n */\nexport function detectForeignCjkInJapanese(text: string, allowedText?: string): ForeignCjkLanguage[] {\n const characters = findForeignCjkCharactersInJapanese(text, allowedText);\n const languages: ForeignCjkLanguage[] = [];\n if (characters.some((char) => !HANGUL_REGEX.test(char))) languages.push('chinese');\n if (characters.some((char) => HANGUL_REGEX.test(char))) languages.push('korean');\n return languages;\n}\n\n/**\n * Returns the distinct Chinese or Korean characters mixed into Japanese text, NFKC-normalized, in order of appearance.\n * A kanji is treated as Chinese when it is outside JIS X 0208 (except the four Jōyō kanji 𠮟塡剝頰)\n * or is one of the listed old forms, Chinese function words, or other Chinese-only characters that modern Japanese\n * prose does not use (e.g., 對, 國, 這, 們, 很, 碼).\n * Rare kanji in personal names (e.g., 髙, 與) are therefore also reported, while Chinese containing none of\n * these characters (e.g., 最后返回答案, 豬肉) is not.\n * Characters that also occur in `allowedText` are not reported, so passing the input that an LLM may quote\n * (e.g., the prompt, a question, or an error message) keeps quoted names and code from being reported.\n */\nexport function findForeignCjkCharactersInJapanese(text: string, allowedText = ''): string[] {\n const allowedCharacters = new Set(allowedText.normalize('NFKC'));\n const kanjiSet = (japaneseKanji ??= buildJapaneseKanjiSet());\n const characters = new Set<string>();\n for (const char of text.normalize('NFKC')) {\n if (allowedCharacters.has(char)) continue;\n if (\n HANGUL_REGEX.test(char) ||\n (HAN_REGEX.test(char) && (!kanjiSet.has(char) || CHINESE_CHARACTERS_IN_JIS_X_0208.has(char)))\n ) {\n characters.add(char);\n }\n }\n return [...characters];\n}\n\nfunction buildJapaneseKanjiSet(): Set<string> {\n // The WHATWG `shift_jis` decoder is Windows-31J, so skip its vendor-extension lead bytes (0xED-0xFC) to keep JIS X 0208 only.\n const decoder = new TextDecoder('shift_jis');\n const kanjiSet = new Set(JOYO_KANJI_OUTSIDE_JIS_X_0208);\n for (let lead = 0x81; lead <= 0xEA; lead++) {\n if (lead >= 0xA0 && lead <= 0xDF) continue;\n for (let trail = 0x40; trail <= 0xFC; trail++) {\n kanjiSet.add(decoder.decode(new Uint8Array([lead, trail])));\n }\n }\n return kanjiSet;\n}\n"],"mappings":"aAEA,MAAM,EAAe,qBACf,EAAY,kBAUZ,EAAmC,IAAI,IAC3C,+RACF,EAEA,IAAI,EAOJ,SAAgB,EAA2B,EAAc,EAA4C,CACnG,IAAM,EAAa,EAAmC,EAAM,CAAW,EACjE,EAAkC,CAAC,EAGzC,OAFI,EAAW,KAAM,GAAS,CAAC,EAAa,KAAK,CAAI,CAAC,GAAG,EAAU,KAAK,SAAS,EAC7E,EAAW,KAAM,GAAS,EAAa,KAAK,CAAI,CAAC,GAAG,EAAU,KAAK,QAAQ,EACxE,CACT,CAYA,SAAgB,EAAmC,EAAc,EAAc,GAAc,CAC3F,IAAM,EAAoB,IAAI,IAAI,EAAY,UAAU,MAAM,CAAC,EACzD,EAAY,IAAkB,EAAsB,EACpD,EAAa,IAAI,IACvB,IAAK,IAAM,KAAQ,EAAK,UAAU,MAAM,EAClC,EAAkB,IAAI,CAAI,IAE5B,EAAa,KAAK,CAAI,GACrB,EAAU,KAAK,CAAI,IAAM,CAAC,EAAS,IAAI,CAAI,GAAK,EAAiC,IAAI,CAAI,KAE1F,EAAW,IAAI,CAAI,EAGvB,MAAO,CAAC,GAAG,CAAU,CACvB,CAEA,SAAS,GAAqC,CAE5C,IAAM,EAAU,IAAI,YAAY,WAAW,EACrC,EAAW,IAAI,IAAI,OAA6B,EACtD,IAAK,IAAI,EAAO,IAAM,GAAQ,IAAM,IAC9B,QAAQ,KAAQ,GAAQ,KAC5B,IAAK,IAAI,EAAQ,GAAM,GAAS,IAAM,IACpC,EAAS,IAAI,EAAQ,OAAO,IAAI,WAAW,CAAC,EAAM,CAAK,CAAC,CAAC,CAAC,EAG9D,OAAO,CACT"}
1
+ {"version":3,"file":"foreignCjk.cjs","names":[],"sources":["../src/foreignCjk.ts"],"sourcesContent":["export type ForeignCjkLanguage = 'chinese' | 'korean';\n\nconst HANGUL_REGEX = /\\p{Script=Hangul}/u;\nconst HAN_REGEX = /\\p{Script=Han}/u;\n// JIS X 0208 cannot encode these Jōyō kanji, which were added in 2010.\nconst JOYO_KANJI_OUTSIDE_JIS_X_0208 = '𠮟塡剝頰';\n// Kanji for Japanese names (e.g., 髙, 﨑, 濵, 栁) that Chinese prose rarely contains: the NFKC forms of the\n// Windows-31J NEC-selected IBM and IBM extension kanji that neither JIS X 0208, Big5, nor GB2312 encodes, plus 𠮷 from\n// JIS X 0213. NFKC forms are listed because the lookup runs after NFKC normalization (e.g., U+FA20 becomes U+8612).\n// Some also appear in Chinese names (e.g., 喆), but prose that mixes Chinese into Japanese hardly contains them.\n// Extension kanji that Big5 or GB2312 encodes (e.g., 德, 瀨) stay Chinese because Chinese prose uses them.\nconst JAPANESE_NAME_KANJI_OUTSIDE_JIS_X_0208 =\n '仼伃伹侊俿偂僘僴兊兤冝凬刕劜劯匇匤厓叝咊咜喆坙坥垬埈墲夋奛奝奣尞岺峵嵓嵭嶹巐弡弴彅惞愑愰憘戓抦敎昞昻晳暿曺曻朎杦栁桒棏樰橳櫢櫤涖淸澵濵焏犱犾猤玽甁硺礰竧箞絈緖罇荢葈蓜蘒蠇裵褜訷贒郞鄕釞釥鈼鉙鉷鍈鏆隝隯霳霻靍靏靑靕髙魲鮏鮱鮻鰀﨎﨏﨑﨓﨔﨟﨡﨣﨤﨧﨨﨩𠮷';\n// Characters JIS X 0208 can encode but modern Japanese prose does not use:\n// - old forms (kyūjitai) of Jōyō kanji, from the `kyujitai` npm package and Unihan's kJapaneseOldVariant,\n// except those still common in Japanese words or names (e.g., 應 in 慶應, 澤, 邊, 龍, 藝 in 東京藝術大学, 糺す)\n// - Chinese function words that Japanese writes in kana (e.g., 們, 這, 麼, 很, 怎, 跟, 嘛) and stray simplified forms (e.g., 个)\n// - traditional Chinese characters that neither table covers but Japanese never uses: 碼, 舉, 鏈, 雖\n// Other Chinese-only content characters (e.g., 媽, 豬) are not listed because separating them from rare kanji\n// that Japanese prose does use (e.g., 乖離, 瑕疵, 齟齬, 俯瞰) needs Japanese usage frequencies.\nconst CHINESE_CHARACTERS_IN_JIS_X_0208 = new Set(\n '个乘亂亞从佛來們假傳僞價儉兒兩册刄剩劍劑勞勳勵勸區卷參听呀咯哇哦唔單嘛嚴囑囘圈國圍圓圖團墮壓壘壞壤壯壹壻壽奧奬姙孃學寢實寫寶將專對屆屬峽巖帶广廢廣廳彈很徑從怎恆惡惱愼慘懷戀戰戲拂拔拜挾插搖搜擇擔據擧擴攜攝收效敍敕數斷晝曉會條棧榮樂樓樞樣檢權歐歡歸殘殼毆氣沒淨淺滯滿潛澁濕濟灣燈燒營爐爭爲犧狹獨獵獸獻瓣畫當疊癡發盜盡碎碼祕禪禮稱稻穗穩竊竝粹絲經縣縱總繩繪繼續纖缺罐聲聽肅腦膽臟臺與舉舊舍舖艷莊莖萬藏藥處號螢蟲蠶蠻衞裝襃覺覽觀觸謠證譯譽讀變讓讚豐豫貳賣贊跟踐輕轉辨辭辯這遙遞遲鄰醉醫釀釋錢鎭鏈鐵鑄鑛關陷隨險隱隸雖雙雜霸靈靜顏顯飜飮餘餠騷驅驗驛髓體髮鬪鷄鹽麥麼黏默點黨齊齒齡龜'\n);\n\nlet japaneseKanji: Set<string> | undefined;\n\n/**\n * Detects Chinese or Korean characters mixed into Japanese text, such as LLM-generated Japanese prose.\n * See {@link findForeignCjkCharactersInJapanese} for which characters count and how `allowedText` works.\n * Returns the detected languages, or an empty array if none are found.\n */\nexport function detectForeignCjkInJapanese(text: string, allowedText?: string): ForeignCjkLanguage[] {\n const characters = findForeignCjkCharactersInJapanese(text, allowedText);\n const languages: ForeignCjkLanguage[] = [];\n if (characters.some((char) => !HANGUL_REGEX.test(char))) languages.push('chinese');\n if (characters.some((char) => HANGUL_REGEX.test(char))) languages.push('korean');\n return languages;\n}\n\n/**\n * Returns the distinct Chinese or Korean characters mixed into Japanese text, NFKC-normalized, in order of appearance.\n * A kanji is treated as Chinese when it is outside JIS X 0208 (except the four Jōyō kanji 𠮟塡剝頰 and Japanese name\n * kanji that neither Big5 nor GB2312 encodes, e.g., 髙, 﨑, 𠮷) or is one of the listed old forms, Chinese function\n * words, or other Chinese-only characters that modern Japanese prose does not use (e.g., 對, 國, 這, 們, 很, 碼).\n * Old forms in names that Chinese prose also uses (e.g., 國, 與, 德) are therefore also reported, while Chinese\n * containing none of these characters (e.g., 最后返回答案, 豬肉) is not.\n * Characters that also occur in `allowedText` are not reported, so passing the input that an LLM may quote\n * (e.g., the prompt, a question, or an error message) keeps quoted names and code from being reported.\n */\nexport function findForeignCjkCharactersInJapanese(text: string, allowedText = ''): string[] {\n const allowedCharacters = new Set(allowedText.normalize('NFKC'));\n const kanjiSet = (japaneseKanji ??= buildJapaneseKanjiSet());\n const characters = new Set<string>();\n for (const char of text.normalize('NFKC')) {\n if (allowedCharacters.has(char)) continue;\n if (\n HANGUL_REGEX.test(char) ||\n (HAN_REGEX.test(char) && (!kanjiSet.has(char) || CHINESE_CHARACTERS_IN_JIS_X_0208.has(char)))\n ) {\n characters.add(char);\n }\n }\n return [...characters];\n}\n\nfunction buildJapaneseKanjiSet(): Set<string> {\n // The WHATWG `shift_jis` decoder is Windows-31J, so skip its vendor-extension lead bytes (0xED-0xFC) to keep JIS X 0208 only.\n const decoder = new TextDecoder('shift_jis');\n const kanjiSet = new Set(JOYO_KANJI_OUTSIDE_JIS_X_0208 + JAPANESE_NAME_KANJI_OUTSIDE_JIS_X_0208);\n for (let lead = 0x81; lead <= 0xEA; lead++) {\n if (lead >= 0xA0 && lead <= 0xDF) continue;\n for (let trail = 0x40; trail <= 0xFC; trail++) {\n kanjiSet.add(decoder.decode(new Uint8Array([lead, trail])));\n }\n }\n return kanjiSet;\n}\n"],"mappings":"aAEA,MAAM,EAAe,qBACf,EAAY,kBAiBZ,EAAmC,IAAI,IAC3C,+RACF,EAEA,IAAI,EAOJ,SAAgB,EAA2B,EAAc,EAA4C,CACnG,IAAM,EAAa,EAAmC,EAAM,CAAW,EACjE,EAAkC,CAAC,EAGzC,OAFI,EAAW,KAAM,GAAS,CAAC,EAAa,KAAK,CAAI,CAAC,GAAG,EAAU,KAAK,SAAS,EAC7E,EAAW,KAAM,GAAS,EAAa,KAAK,CAAI,CAAC,GAAG,EAAU,KAAK,QAAQ,EACxE,CACT,CAYA,SAAgB,EAAmC,EAAc,EAAc,GAAc,CAC3F,IAAM,EAAoB,IAAI,IAAI,EAAY,UAAU,MAAM,CAAC,EACzD,EAAY,IAAkB,EAAsB,EACpD,EAAa,IAAI,IACvB,IAAK,IAAM,KAAQ,EAAK,UAAU,MAAM,EAClC,EAAkB,IAAI,CAAI,IAE5B,EAAa,KAAK,CAAI,GACrB,EAAU,KAAK,CAAI,IAAM,CAAC,EAAS,IAAI,CAAI,GAAK,EAAiC,IAAI,CAAI,KAE1F,EAAW,IAAI,CAAI,EAGvB,MAAO,CAAC,GAAG,CAAU,CACvB,CAEA,SAAS,GAAqC,CAE5C,IAAM,EAAU,IAAI,YAAY,WAAW,EACrC,EAAW,IAAI,IAAI,qIAAsE,EAC/F,IAAK,IAAI,EAAO,IAAM,GAAQ,IAAM,IAC9B,QAAQ,KAAQ,GAAQ,KAC5B,IAAK,IAAI,EAAQ,GAAM,GAAS,IAAM,IACpC,EAAS,IAAI,EAAQ,OAAO,IAAI,WAAW,CAAC,EAAM,CAAK,CAAC,CAAC,CAAC,EAG9D,OAAO,CACT"}
@@ -7,11 +7,11 @@ export type ForeignCjkLanguage = 'chinese' | 'korean';
7
7
  export declare function detectForeignCjkInJapanese(text: string, allowedText?: string): ForeignCjkLanguage[];
8
8
  /**
9
9
  * Returns the distinct Chinese or Korean characters mixed into Japanese text, NFKC-normalized, in order of appearance.
10
- * A kanji is treated as Chinese when it is outside JIS X 0208 (except the four Jōyō kanji 𠮟塡剝頰)
11
- * or is one of the listed old forms, Chinese function words, or other Chinese-only characters that modern Japanese
12
- * prose does not use (e.g., 對, 國, 這, 們, 很, 碼).
13
- * Rare kanji in personal names (e.g., 髙, 與) are therefore also reported, while Chinese containing none of
14
- * these characters (e.g., 最后返回答案, 豬肉) is not.
10
+ * A kanji is treated as Chinese when it is outside JIS X 0208 (except the four Jōyō kanji 𠮟塡剝頰 and Japanese name
11
+ * kanji that neither Big5 nor GB2312 encodes, e.g., 髙, 﨑, 𠮷) or is one of the listed old forms, Chinese function
12
+ * words, or other Chinese-only characters that modern Japanese prose does not use (e.g., 對, 國, 這, 們, 很, 碼).
13
+ * Old forms in names that Chinese prose also uses (e.g., 國, 與, 德) are therefore also reported, while Chinese
14
+ * containing none of these characters (e.g., 最后返回答案, 豬肉) is not.
15
15
  * Characters that also occur in `allowedText` are not reported, so passing the input that an LLM may quote
16
16
  * (e.g., the prompt, a question, or an error message) keeps quoted names and code from being reported.
17
17
  */
@@ -1,2 +1,2 @@
1
- const e=/\p{Script=Hangul}/u,t=/\p{Script=Han}/u,n=new Set(`个乘亂亞从佛來們假傳僞價儉兒兩册刄剩劍劑勞勳勵勸區卷參听呀咯哇哦唔單嘛嚴囑囘圈國圍圓圖團墮壓壘壞壤壯壹壻壽奧奬姙孃學寢實寫寶將專對屆屬峽巖帶广廢廣廳彈很徑從怎恆惡惱愼慘懷戀戰戲拂拔拜挾插搖搜擇擔據擧擴攜攝收效敍敕數斷晝曉會條棧榮樂樓樞樣檢權歐歡歸殘殼毆氣沒淨淺滯滿潛澁濕濟灣燈燒營爐爭爲犧狹獨獵獸獻瓣畫當疊癡發盜盡碎碼祕禪禮稱稻穗穩竊竝粹絲經縣縱總繩繪繼續纖缺罐聲聽肅腦膽臟臺與舉舊舍舖艷莊莖萬藏藥處號螢蟲蠶蠻衞裝襃覺覽觀觸謠證譯譽讀變讓讚豐豫貳賣贊跟踐輕轉辨辭辯這遙遞遲鄰醉醫釀釋錢鎭鏈鐵鑄鑛關陷隨險隱隸雖雙雜霸靈靜顏顯飜飮餘餠騷驅驗驛髓體髮鬪鷄鹽麥麼黏默點黨齊齒齡龜`);let r;function i(t,n){let r=a(t,n),i=[];return r.some(t=>!e.test(t))&&i.push(`chinese`),r.some(t=>e.test(t))&&i.push(`korean`),i}function a(i,a=``){let s=new Set(a.normalize(`NFKC`)),c=r??=o(),l=new Set;for(let r of i.normalize(`NFKC`))s.has(r)||(e.test(r)||t.test(r)&&(!c.has(r)||n.has(r)))&&l.add(r);return[...l]}function o(){let e=new TextDecoder(`shift_jis`),t=new Set(`𠮟塡剝頰`);for(let n=129;n<=234;n++)if(!(n>=160&&n<=223))for(let r=64;r<=252;r++)t.add(e.decode(new Uint8Array([n,r])));return t}export{i as detectForeignCjkInJapanese,a as findForeignCjkCharactersInJapanese};
1
+ const e=/\p{Script=Hangul}/u,t=/\p{Script=Han}/u,n=new Set(`个乘亂亞从佛來們假傳僞價儉兒兩册刄剩劍劑勞勳勵勸區卷參听呀咯哇哦唔單嘛嚴囑囘圈國圍圓圖團墮壓壘壞壤壯壹壻壽奧奬姙孃學寢實寫寶將專對屆屬峽巖帶广廢廣廳彈很徑從怎恆惡惱愼慘懷戀戰戲拂拔拜挾插搖搜擇擔據擧擴攜攝收效敍敕數斷晝曉會條棧榮樂樓樞樣檢權歐歡歸殘殼毆氣沒淨淺滯滿潛澁濕濟灣燈燒營爐爭爲犧狹獨獵獸獻瓣畫當疊癡發盜盡碎碼祕禪禮稱稻穗穩竊竝粹絲經縣縱總繩繪繼續纖缺罐聲聽肅腦膽臟臺與舉舊舍舖艷莊莖萬藏藥處號螢蟲蠶蠻衞裝襃覺覽觀觸謠證譯譽讀變讓讚豐豫貳賣贊跟踐輕轉辨辭辯這遙遞遲鄰醉醫釀釋錢鎭鏈鐵鑄鑛關陷隨險隱隸雖雙雜霸靈靜顏顯飜飮餘餠騷驅驗驛髓體髮鬪鷄鹽麥麼黏默點黨齊齒齡龜`);let r;function i(t,n){let r=a(t,n),i=[];return r.some(t=>!e.test(t))&&i.push(`chinese`),r.some(t=>e.test(t))&&i.push(`korean`),i}function a(i,a=``){let s=new Set(a.normalize(`NFKC`)),c=r??=o(),l=new Set;for(let r of i.normalize(`NFKC`))s.has(r)||(e.test(r)||t.test(r)&&(!c.has(r)||n.has(r)))&&l.add(r);return[...l]}function o(){let e=new TextDecoder(`shift_jis`),t=new Set(`𠮟塡剝頰仼伃伹侊俿偂僘僴兊兤冝凬刕劜劯匇匤厓叝咊咜喆坙坥垬埈墲夋奛奝奣尞岺峵嵓嵭嶹巐弡弴彅惞愑愰憘戓抦敎昞昻晳暿曺曻朎杦栁桒棏樰橳櫢櫤涖淸澵濵焏犱犾猤玽甁硺礰竧箞絈緖罇荢葈蓜蘒蠇裵褜訷贒郞鄕釞釥鈼鉙鉷鍈鏆隝隯霳霻靍靏靑靕髙魲鮏鮱鮻鰀﨎﨏﨑﨓﨔﨟﨡﨣﨤﨧﨨﨩𠮷`);for(let n=129;n<=234;n++)if(!(n>=160&&n<=223))for(let r=64;r<=252;r++)t.add(e.decode(new Uint8Array([n,r])));return t}export{i as detectForeignCjkInJapanese,a as findForeignCjkCharactersInJapanese};
2
2
  //# sourceMappingURL=foreignCjk.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"foreignCjk.js","names":[],"sources":["../src/foreignCjk.ts"],"sourcesContent":["export type ForeignCjkLanguage = 'chinese' | 'korean';\n\nconst HANGUL_REGEX = /\\p{Script=Hangul}/u;\nconst HAN_REGEX = /\\p{Script=Han}/u;\n// JIS X 0208 cannot encode these Jōyō kanji, which were added in 2010.\nconst JOYO_KANJI_OUTSIDE_JIS_X_0208 = '𠮟塡剝頰';\n// Characters JIS X 0208 can encode but modern Japanese prose does not use:\n// - old forms (kyūjitai) of Jōyō kanji, from the `kyujitai` npm package and Unihan's kJapaneseOldVariant,\n// except those still common in Japanese words or names (e.g., 應 in 慶應, 澤, 邊, 龍, 藝 in 東京藝術大学, 糺す)\n// - Chinese function words that Japanese writes in kana (e.g., 們, 這, 麼, 很, 怎, 跟, 嘛) and stray simplified forms (e.g., 个)\n// - traditional Chinese characters that neither table covers but Japanese never uses: 碼, 舉, 鏈, 雖\n// Other Chinese-only content characters (e.g., 媽, 豬) are not listed because separating them from rare kanji\n// that Japanese prose does use (e.g., 乖離, 瑕疵, 齟齬, 俯瞰) needs Japanese usage frequencies.\nconst CHINESE_CHARACTERS_IN_JIS_X_0208 = new Set(\n '个乘亂亞从佛來們假傳僞價儉兒兩册刄剩劍劑勞勳勵勸區卷參听呀咯哇哦唔單嘛嚴囑囘圈國圍圓圖團墮壓壘壞壤壯壹壻壽奧奬姙孃學寢實寫寶將專對屆屬峽巖帶广廢廣廳彈很徑從怎恆惡惱愼慘懷戀戰戲拂拔拜挾插搖搜擇擔據擧擴攜攝收效敍敕數斷晝曉會條棧榮樂樓樞樣檢權歐歡歸殘殼毆氣沒淨淺滯滿潛澁濕濟灣燈燒營爐爭爲犧狹獨獵獸獻瓣畫當疊癡發盜盡碎碼祕禪禮稱稻穗穩竊竝粹絲經縣縱總繩繪繼續纖缺罐聲聽肅腦膽臟臺與舉舊舍舖艷莊莖萬藏藥處號螢蟲蠶蠻衞裝襃覺覽觀觸謠證譯譽讀變讓讚豐豫貳賣贊跟踐輕轉辨辭辯這遙遞遲鄰醉醫釀釋錢鎭鏈鐵鑄鑛關陷隨險隱隸雖雙雜霸靈靜顏顯飜飮餘餠騷驅驗驛髓體髮鬪鷄鹽麥麼黏默點黨齊齒齡龜'\n);\n\nlet japaneseKanji: Set<string> | undefined;\n\n/**\n * Detects Chinese or Korean characters mixed into Japanese text, such as LLM-generated Japanese prose.\n * See {@link findForeignCjkCharactersInJapanese} for which characters count and how `allowedText` works.\n * Returns the detected languages, or an empty array if none are found.\n */\nexport function detectForeignCjkInJapanese(text: string, allowedText?: string): ForeignCjkLanguage[] {\n const characters = findForeignCjkCharactersInJapanese(text, allowedText);\n const languages: ForeignCjkLanguage[] = [];\n if (characters.some((char) => !HANGUL_REGEX.test(char))) languages.push('chinese');\n if (characters.some((char) => HANGUL_REGEX.test(char))) languages.push('korean');\n return languages;\n}\n\n/**\n * Returns the distinct Chinese or Korean characters mixed into Japanese text, NFKC-normalized, in order of appearance.\n * A kanji is treated as Chinese when it is outside JIS X 0208 (except the four Jōyō kanji 𠮟塡剝頰)\n * or is one of the listed old forms, Chinese function words, or other Chinese-only characters that modern Japanese\n * prose does not use (e.g., 對, 國, 這, 們, 很, 碼).\n * Rare kanji in personal names (e.g., 髙, 與) are therefore also reported, while Chinese containing none of\n * these characters (e.g., 最后返回答案, 豬肉) is not.\n * Characters that also occur in `allowedText` are not reported, so passing the input that an LLM may quote\n * (e.g., the prompt, a question, or an error message) keeps quoted names and code from being reported.\n */\nexport function findForeignCjkCharactersInJapanese(text: string, allowedText = ''): string[] {\n const allowedCharacters = new Set(allowedText.normalize('NFKC'));\n const kanjiSet = (japaneseKanji ??= buildJapaneseKanjiSet());\n const characters = new Set<string>();\n for (const char of text.normalize('NFKC')) {\n if (allowedCharacters.has(char)) continue;\n if (\n HANGUL_REGEX.test(char) ||\n (HAN_REGEX.test(char) && (!kanjiSet.has(char) || CHINESE_CHARACTERS_IN_JIS_X_0208.has(char)))\n ) {\n characters.add(char);\n }\n }\n return [...characters];\n}\n\nfunction buildJapaneseKanjiSet(): Set<string> {\n // The WHATWG `shift_jis` decoder is Windows-31J, so skip its vendor-extension lead bytes (0xED-0xFC) to keep JIS X 0208 only.\n const decoder = new TextDecoder('shift_jis');\n const kanjiSet = new Set(JOYO_KANJI_OUTSIDE_JIS_X_0208);\n for (let lead = 0x81; lead <= 0xEA; lead++) {\n if (lead >= 0xA0 && lead <= 0xDF) continue;\n for (let trail = 0x40; trail <= 0xFC; trail++) {\n kanjiSet.add(decoder.decode(new Uint8Array([lead, trail])));\n }\n }\n return kanjiSet;\n}\n"],"mappings":"AAEA,MAAM,EAAe,qBACf,EAAY,kBAUZ,EAAmC,IAAI,IAC3C,+RACF,EAEA,IAAI,EAOJ,SAAgB,EAA2B,EAAc,EAA4C,CACnG,IAAM,EAAa,EAAmC,EAAM,CAAW,EACjE,EAAkC,CAAC,EAGzC,OAFI,EAAW,KAAM,GAAS,CAAC,EAAa,KAAK,CAAI,CAAC,GAAG,EAAU,KAAK,SAAS,EAC7E,EAAW,KAAM,GAAS,EAAa,KAAK,CAAI,CAAC,GAAG,EAAU,KAAK,QAAQ,EACxE,CACT,CAYA,SAAgB,EAAmC,EAAc,EAAc,GAAc,CAC3F,IAAM,EAAoB,IAAI,IAAI,EAAY,UAAU,MAAM,CAAC,EACzD,EAAY,IAAkB,EAAsB,EACpD,EAAa,IAAI,IACvB,IAAK,IAAM,KAAQ,EAAK,UAAU,MAAM,EAClC,EAAkB,IAAI,CAAI,IAE5B,EAAa,KAAK,CAAI,GACrB,EAAU,KAAK,CAAI,IAAM,CAAC,EAAS,IAAI,CAAI,GAAK,EAAiC,IAAI,CAAI,KAE1F,EAAW,IAAI,CAAI,EAGvB,MAAO,CAAC,GAAG,CAAU,CACvB,CAEA,SAAS,GAAqC,CAE5C,IAAM,EAAU,IAAI,YAAY,WAAW,EACrC,EAAW,IAAI,IAAI,OAA6B,EACtD,IAAK,IAAI,EAAO,IAAM,GAAQ,IAAM,IAC9B,QAAQ,KAAQ,GAAQ,KAC5B,IAAK,IAAI,EAAQ,GAAM,GAAS,IAAM,IACpC,EAAS,IAAI,EAAQ,OAAO,IAAI,WAAW,CAAC,EAAM,CAAK,CAAC,CAAC,CAAC,EAG9D,OAAO,CACT"}
1
+ {"version":3,"file":"foreignCjk.js","names":[],"sources":["../src/foreignCjk.ts"],"sourcesContent":["export type ForeignCjkLanguage = 'chinese' | 'korean';\n\nconst HANGUL_REGEX = /\\p{Script=Hangul}/u;\nconst HAN_REGEX = /\\p{Script=Han}/u;\n// JIS X 0208 cannot encode these Jōyō kanji, which were added in 2010.\nconst JOYO_KANJI_OUTSIDE_JIS_X_0208 = '𠮟塡剝頰';\n// Kanji for Japanese names (e.g., 髙, 﨑, 濵, 栁) that Chinese prose rarely contains: the NFKC forms of the\n// Windows-31J NEC-selected IBM and IBM extension kanji that neither JIS X 0208, Big5, nor GB2312 encodes, plus 𠮷 from\n// JIS X 0213. NFKC forms are listed because the lookup runs after NFKC normalization (e.g., U+FA20 becomes U+8612).\n// Some also appear in Chinese names (e.g., 喆), but prose that mixes Chinese into Japanese hardly contains them.\n// Extension kanji that Big5 or GB2312 encodes (e.g., 德, 瀨) stay Chinese because Chinese prose uses them.\nconst JAPANESE_NAME_KANJI_OUTSIDE_JIS_X_0208 =\n '仼伃伹侊俿偂僘僴兊兤冝凬刕劜劯匇匤厓叝咊咜喆坙坥垬埈墲夋奛奝奣尞岺峵嵓嵭嶹巐弡弴彅惞愑愰憘戓抦敎昞昻晳暿曺曻朎杦栁桒棏樰橳櫢櫤涖淸澵濵焏犱犾猤玽甁硺礰竧箞絈緖罇荢葈蓜蘒蠇裵褜訷贒郞鄕釞釥鈼鉙鉷鍈鏆隝隯霳霻靍靏靑靕髙魲鮏鮱鮻鰀﨎﨏﨑﨓﨔﨟﨡﨣﨤﨧﨨﨩𠮷';\n// Characters JIS X 0208 can encode but modern Japanese prose does not use:\n// - old forms (kyūjitai) of Jōyō kanji, from the `kyujitai` npm package and Unihan's kJapaneseOldVariant,\n// except those still common in Japanese words or names (e.g., 應 in 慶應, 澤, 邊, 龍, 藝 in 東京藝術大学, 糺す)\n// - Chinese function words that Japanese writes in kana (e.g., 們, 這, 麼, 很, 怎, 跟, 嘛) and stray simplified forms (e.g., 个)\n// - traditional Chinese characters that neither table covers but Japanese never uses: 碼, 舉, 鏈, 雖\n// Other Chinese-only content characters (e.g., 媽, 豬) are not listed because separating them from rare kanji\n// that Japanese prose does use (e.g., 乖離, 瑕疵, 齟齬, 俯瞰) needs Japanese usage frequencies.\nconst CHINESE_CHARACTERS_IN_JIS_X_0208 = new Set(\n '个乘亂亞从佛來們假傳僞價儉兒兩册刄剩劍劑勞勳勵勸區卷參听呀咯哇哦唔單嘛嚴囑囘圈國圍圓圖團墮壓壘壞壤壯壹壻壽奧奬姙孃學寢實寫寶將專對屆屬峽巖帶广廢廣廳彈很徑從怎恆惡惱愼慘懷戀戰戲拂拔拜挾插搖搜擇擔據擧擴攜攝收效敍敕數斷晝曉會條棧榮樂樓樞樣檢權歐歡歸殘殼毆氣沒淨淺滯滿潛澁濕濟灣燈燒營爐爭爲犧狹獨獵獸獻瓣畫當疊癡發盜盡碎碼祕禪禮稱稻穗穩竊竝粹絲經縣縱總繩繪繼續纖缺罐聲聽肅腦膽臟臺與舉舊舍舖艷莊莖萬藏藥處號螢蟲蠶蠻衞裝襃覺覽觀觸謠證譯譽讀變讓讚豐豫貳賣贊跟踐輕轉辨辭辯這遙遞遲鄰醉醫釀釋錢鎭鏈鐵鑄鑛關陷隨險隱隸雖雙雜霸靈靜顏顯飜飮餘餠騷驅驗驛髓體髮鬪鷄鹽麥麼黏默點黨齊齒齡龜'\n);\n\nlet japaneseKanji: Set<string> | undefined;\n\n/**\n * Detects Chinese or Korean characters mixed into Japanese text, such as LLM-generated Japanese prose.\n * See {@link findForeignCjkCharactersInJapanese} for which characters count and how `allowedText` works.\n * Returns the detected languages, or an empty array if none are found.\n */\nexport function detectForeignCjkInJapanese(text: string, allowedText?: string): ForeignCjkLanguage[] {\n const characters = findForeignCjkCharactersInJapanese(text, allowedText);\n const languages: ForeignCjkLanguage[] = [];\n if (characters.some((char) => !HANGUL_REGEX.test(char))) languages.push('chinese');\n if (characters.some((char) => HANGUL_REGEX.test(char))) languages.push('korean');\n return languages;\n}\n\n/**\n * Returns the distinct Chinese or Korean characters mixed into Japanese text, NFKC-normalized, in order of appearance.\n * A kanji is treated as Chinese when it is outside JIS X 0208 (except the four Jōyō kanji 𠮟塡剝頰 and Japanese name\n * kanji that neither Big5 nor GB2312 encodes, e.g., 髙, 﨑, 𠮷) or is one of the listed old forms, Chinese function\n * words, or other Chinese-only characters that modern Japanese prose does not use (e.g., 對, 國, 這, 們, 很, 碼).\n * Old forms in names that Chinese prose also uses (e.g., 國, 與, 德) are therefore also reported, while Chinese\n * containing none of these characters (e.g., 最后返回答案, 豬肉) is not.\n * Characters that also occur in `allowedText` are not reported, so passing the input that an LLM may quote\n * (e.g., the prompt, a question, or an error message) keeps quoted names and code from being reported.\n */\nexport function findForeignCjkCharactersInJapanese(text: string, allowedText = ''): string[] {\n const allowedCharacters = new Set(allowedText.normalize('NFKC'));\n const kanjiSet = (japaneseKanji ??= buildJapaneseKanjiSet());\n const characters = new Set<string>();\n for (const char of text.normalize('NFKC')) {\n if (allowedCharacters.has(char)) continue;\n if (\n HANGUL_REGEX.test(char) ||\n (HAN_REGEX.test(char) && (!kanjiSet.has(char) || CHINESE_CHARACTERS_IN_JIS_X_0208.has(char)))\n ) {\n characters.add(char);\n }\n }\n return [...characters];\n}\n\nfunction buildJapaneseKanjiSet(): Set<string> {\n // The WHATWG `shift_jis` decoder is Windows-31J, so skip its vendor-extension lead bytes (0xED-0xFC) to keep JIS X 0208 only.\n const decoder = new TextDecoder('shift_jis');\n const kanjiSet = new Set(JOYO_KANJI_OUTSIDE_JIS_X_0208 + JAPANESE_NAME_KANJI_OUTSIDE_JIS_X_0208);\n for (let lead = 0x81; lead <= 0xEA; lead++) {\n if (lead >= 0xA0 && lead <= 0xDF) continue;\n for (let trail = 0x40; trail <= 0xFC; trail++) {\n kanjiSet.add(decoder.decode(new Uint8Array([lead, trail])));\n }\n }\n return kanjiSet;\n}\n"],"mappings":"AAEA,MAAM,EAAe,qBACf,EAAY,kBAiBZ,EAAmC,IAAI,IAC3C,+RACF,EAEA,IAAI,EAOJ,SAAgB,EAA2B,EAAc,EAA4C,CACnG,IAAM,EAAa,EAAmC,EAAM,CAAW,EACjE,EAAkC,CAAC,EAGzC,OAFI,EAAW,KAAM,GAAS,CAAC,EAAa,KAAK,CAAI,CAAC,GAAG,EAAU,KAAK,SAAS,EAC7E,EAAW,KAAM,GAAS,EAAa,KAAK,CAAI,CAAC,GAAG,EAAU,KAAK,QAAQ,EACxE,CACT,CAYA,SAAgB,EAAmC,EAAc,EAAc,GAAc,CAC3F,IAAM,EAAoB,IAAI,IAAI,EAAY,UAAU,MAAM,CAAC,EACzD,EAAY,IAAkB,EAAsB,EACpD,EAAa,IAAI,IACvB,IAAK,IAAM,KAAQ,EAAK,UAAU,MAAM,EAClC,EAAkB,IAAI,CAAI,IAE5B,EAAa,KAAK,CAAI,GACrB,EAAU,KAAK,CAAI,IAAM,CAAC,EAAS,IAAI,CAAI,GAAK,EAAiC,IAAI,CAAI,KAE1F,EAAW,IAAI,CAAI,EAGvB,MAAO,CAAC,GAAG,CAAU,CACvB,CAEA,SAAS,GAAqC,CAE5C,IAAM,EAAU,IAAI,YAAY,WAAW,EACrC,EAAW,IAAI,IAAI,qIAAsE,EAC/F,IAAK,IAAI,EAAO,IAAM,GAAQ,IAAM,IAC9B,QAAQ,KAAQ,GAAQ,KAC5B,IAAK,IAAI,EAAQ,GAAM,GAAS,IAAM,IACpC,EAAS,IAAI,EAAQ,OAAO,IAAI,WAAW,CAAC,EAAM,CAAK,CAAC,CAAC,CAAC,EAG9D,OAAO,CACT"}
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":"aAqBA,SAAgB,EAAkB,EAA+B,CAC/D,IAAM,EAAQ,EAAW,CAAQ,EAC3B,EAAsB,CAAC,EAC7B,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAM,OAAQ,IAAS,CACjD,IAAM,EAAQ,EAAkB,EAAM,IAAU,EAAE,EAClD,GAAI,CAAC,EAAO,SACZ,IAAM,EAAM,EAAiB,EAAO,EAAQ,EAAG,CAAK,EACpD,EAAO,KAAK,CACV,GAAI,EAAM,UAAY,CAAE,SAAU,EAAM,QAAS,EACjD,KAAM,EACH,MAAM,EAAQ,EAAG,CAAG,CAAC,CACrB,IAAK,GAAS,EAAY,EAAM,EAAM,MAAM,CAAC,CAAC,CAC9C,KAAK;CAAI,EACZ,SAAU,EAAM,EAAM,MACxB,CAAC,EACD,EAAQ,CACV,CACA,OAAO,CACT,CAUA,SAAgB,EAAkC,EAAc,EAAuC,CACrG,IAAM,EAAQ,EAAuB,CAAI,EAGzC,MAFI,CAAC,GACD,GAAW,QAAU,CAAC,EAAU,KAAM,GAAa,EAAS,YAAY,IAAM,EAAM,QAAQ,EAAU,EACnG,EAAM,IACf,CAUA,MAAM,EAA6B,IAAI,IAAI,CAAC,GAAI,WAAY,IAAI,CAAC,EAWjE,SAAgB,EACd,EACA,EAC+B,CAC/B,IAAM,EAAa,EAAsB,CAAQ,CAAC,CAAC,IAAK,IAAa,CACnE,GAAG,EACH,IAAK,EAAiB,EAAQ,OAAO,CACvC,EAAE,EACI,EAAY,CAAC,GAAG,IAAI,IAAI,CAAK,CAAC,CAAC,CAAC,IAAK,IAAU,CAAE,OAAM,IAAK,EAAiB,CAAI,CAAE,EAAE,EACrF,EAA0C,OAAO,OAAO,IAAI,EAClE,IAAK,GAAM,CAAE,OAAM,SAAS,EAAW,CACrC,IAAI,EACJ,IAAK,IAAM,KAAa,EAClB,EAAU,MAAQ,IAEpB,CAAC,GACD,EAAU,MAAQ,EAAK,OACtB,EAAU,QAAU,EAAK,OAAS,EAAU,UAAY,GAAQ,EAAK,UAAY,KAElF,EAAO,GAGN,IACD,EAAK,UAAY,GAAQ,EAAU,KAAM,GAAU,EAAM,OAAS,GAAQ,EAAM,MAAQ,CAAG,GAC3F,EAAK,UAAS,EAAS,GAAQ,EAAK,SAC1C,CACA,OAAO,CACT,CASA,SAAgB,EAAsB,EAAqC,CACzE,IAAM,EAAQ,EAAuB,CAAQ,EACvC,EAAU,GAAS,EAA2B,IAAI,EAAM,QAAQ,EAAI,EAAQ,IAAA,GAClF,GAAI,GAAS,QAAS,OAAO,EAAc,EAAQ,IAAI,EACvD,IAAM,EAAW,EAAc,CAAQ,EACvC,OAAO,EAAS,SAAW,GAAK,EAAU,EAAc,EAAQ,IAAI,EAAI,CAC1E,CAEA,SAAS,EAAiB,EAAyB,CACjD,OAAO,EAAkB,EAAkB,CAAO,CAAC,CAAC,QAAQ,qCAAsC,EAAE,CAAC,CAAC,CACnG,WAAW,QAAS,GAAG,CAAC,CACxB,YAAY,CACjB,CAEA,SAAS,EAAkB,EAAsB,CAE/C,OAAO,EAAK,WAAW,oCAAqC,EAAE,CAChE,CAEA,SAAS,EAAc,EAAqC,CAC1D,IAAM,EAAQ,EAAW,CAAQ,EAC3B,EAAW,EAAa,CAAK,EACnC,OAAO,EAAS,KAAK,EAAS,IAAU,CACtC,IAAI,EAAY,EAAQ,EACxB,KAAO,EAAY,EAAS,SAAW,EAAS,EAAU,EAAE,OAAS,GAAK,EAAQ,OAAO,IACzF,MAAO,CACL,MAAO,EAAQ,MACf,QAAS,EAAQ,KACjB,QAAS,EACN,MAAM,EAAQ,MAAQ,EAAG,EAAS,EAAU,EAAE,KAAK,CAAC,CACpD,KAAK;CAAI,CAAC,CACV,KAAK,CACV,CACF,CAAC,CACH,CAEA,SAAS,EAAuB,EAAgF,CAE9G,IAAM,EAAQ,EAAW,EAAK,QAAQ,UAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,EACxD,EAAQ,EAAkB,EAAM,IAAM,EAAE,EAC9C,GAAI,CAAC,EAAO,OACZ,IAAM,EAAY,EAAM,OAAS,EAC3B,EACJ,EAAY,GAAK,EAAe,EAAM,IAAc,GAAI,CAAK,EAAI,EAAY,EAAiB,EAAO,EAAG,CAAK,EAC/G,MAAO,CACL,SAAU,EAAM,SAChB,KAAM,EACH,MAAM,EAAG,CAAG,CAAC,CACb,IAAK,GAAS,EAAY,EAAM,EAAM,MAAM,CAAC,CAAC,CAC9C,KAAK;CAAI,EACZ,QAAS,GAAO,CAClB,CACF,CAEA,SAAS,EAAa,EAA4E,CAChG,IAAM,EAA6D,CAAC,EACpE,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAM,OAAQ,IAAS,CACjD,IAAM,EAAO,EAAM,IAAU,GACvB,EAAQ,EAAkB,CAAI,EACpC,GAAI,EAAO,CACT,EAAQ,EAAiB,EAAO,EAAQ,EAAG,CAAK,EAChD,QACF,CACA,IAAM,EAAQ,6BAA6B,KAAK,CAAI,EACpD,GAAI,CAAC,EAAO,SACZ,IAAM,GAAQ,EAAM,IAAM,GAAA,CAEvB,QAAQ,yBAA0B,EAAE,CAAC,CACrC,KAAK,CAAC,CACN,WAAW,iBAAkB,EAAQ,EAAoB,IACxD,EAAQ,WAAW,GAAG,GAAK,EAAQ,SAAS,GAAG,EAAI,EAAQ,MAAM,EAAG,EAAE,EAAI,CAC5E,EACF,EAAS,KAAK,CAAE,QAAO,MAAO,EAAM,EAAE,EAAE,QAAU,EAAG,MAAK,CAAC,CAC7D,CACA,OAAO,CACT,CAEA,SAAS,EAAkB,EAAiC,CAC1D,IAAM,EAAQ,+BAA+B,KAAK,CAAI,EAChD,EAAQ,IAAQ,GAChB,EAAO,IAAQ,IAAM,GAEvB,MAAC,GAAU,EAAM,WAAW,GAAG,GAAK,EAAK,SAAS,GAAG,GACzD,MAAO,CACL,KAAM,EAAM,OAAO,CAAC,EACpB,OAAQ,EAAM,OACd,OAAQ,EAAM,EAAE,EAAE,QAAU,EAC5B,SAAU,EAAK,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,EAAE,YAAY,GAAK,EAC1D,CACF,CAGA,SAAS,EAAiB,EAA0B,EAAe,EAAsB,CACvF,IAAI,EAAQ,EACZ,KAAO,EAAQ,EAAM,QAAU,CAAC,EAAe,EAAM,IAAU,GAAI,CAAK,GAAG,IAC3E,OAAO,CACT,CAEA,SAAS,EAAe,EAAc,EAAuB,CAC3D,IAAM,EAAS,+BAA+B,KAAK,CAAI,CAAC,GAAG,GAC3D,OAAO,GAAQ,OAAO,CAAC,IAAM,EAAM,MAAQ,EAAO,QAAU,EAAM,MACpE,CAEA,SAAS,EAAY,EAAc,EAAwB,CACzD,IAAI,EAAQ,EACZ,KAAO,EAAQ,GAAU,EAAK,KAAW,KAAK,IAC9C,OAAO,EAAK,MAAM,CAAK,CACzB,CAEA,SAAS,EAAW,EAAwB,CAC1C,OAAO,EAAK,MAAM,QAAQ,CAC5B"}
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.14.0",
3
+ "version": "10.16.0",
4
4
  "description": "Shared library for WillBooster projects",
5
5
  "repository": {
6
6
  "type": "git",