// 改寫前把必須原樣保留的片段換成佔位符,改寫後確認全數還在才還原。 const PROTECT = [ /```[\s\S]*?```/g, // 程式碼區塊 /`[^`\n]+`/g, // 行內程式碼(含指令) /https?:\/\/[^\s))」]+/g, // URL /(?:~|\.{1,2})?\/[\w.@-]+(?:\/[\w.@-]+)+\/?/g, // 路徑 /\d+(?:[.,:/]\d+)*%?/g, // 數字 ] export type Protected = { text: string; tokens: string[] } const open = (i: number): string => `⟦${i}⟧` export const protect = (src: string): Protected => { const tokens: string[] = [] let text = src for (const re of PROTECT) { // 已是佔位符的部分不再處理,避免把佔位符裡的數字再包一次。 text = text .split(/(⟦\d+⟧)/) .map((part, i) => i % 2 === 1 ? part : part.replace(re, m => { tokens.push(m) return open(tokens.length - 1) }), ) .join('') } return { text, tokens } } // 佔位符被遺失或多出來都視為失敗。 export const restore = (rewritten: string, tokens: readonly string[]): string | undefined => { for (let i = 0; i < tokens.length; i++) { if (rewritten.split(open(i)).length !== 2) return undefined } if ((rewritten.match(/⟦\d+⟧/g) ?? []).length !== tokens.length) return undefined return rewritten.replace(/⟦(\d+)⟧/g, (_, n: string) => tokens[Number(n)]!) } export const withPrefix = (name: string, body: string): string => `${name} > ${body}` export const REWRITE_SYSTEM = '你是文字改寫器。把使用者給的回覆改寫成指定角色的口吻,保留所有事實與結構(標題、清單、段落)。' + '⟦數字⟧ 形式的佔位符必須原樣出現且各一次,不可改動、不可刪除。只輸出改寫後的內文,不要加前綴或說明。' export const needsSonnet = (text: string, threshold: number): boolean => text.length > threshold