Repository: zh-lx/pinyin-pro (ts)

Issue:
开启拼音模式后，"乐"字在姓名中的读音也变了
代码：
  console.log(
    "不开启姓氏模式,曾乐的读音:",
    pinyin("曾乐", {
      toneType: "none",
    })
  );

  console.log(
    "开启姓氏模式,曾乐的读音:",
    pinyin("曾乐", {
      toneType: "none",
      mode: "surname",
    })
  );

结果：
不开启姓氏模式,曾乐的读音: ceng le
开启姓氏模式,曾乐的读音: zeng yue

试了一下其他姓氏也会这样

问：姓氏模式打开应该只会订正姓的读音，而不应该订正名字的读音？请问是否有这个功能呢


Pull request description:
feat: 支持 surname head 模式
close #237 

Patch:
diff --git a/lib/common/segmentit/index.ts b/lib/common/segmentit/index.ts
index a9d2647..a8e8353 100644
--- a/lib/common/segmentit/index.ts
+++ b/lib/common/segmentit/index.ts
@@ -7,7 +7,8 @@ import { PatternSurname } from "@/data/surname";
 import { maxProbability } from "./max-probability";
 import { minTokenization } from "./min-tokenization";
 import { reverseMaxMatch } from "./reverse-max-match";
-import { Priority } from '@/common/constant';
+import { Priority } from "@/common/constant";
+import type { SurnameMode } from "../type";
 
 export const enum TokenizationAlgorithm {
   ReverseMaxMatch = 1,
@@ -41,7 +42,7 @@ class TrieNode {
   parent: TrieNode | null; // 父节点
   key: string; // 所在父节点的 key
 
-  constructor(parent: TrieNode | null, prefix: string = '', key = '') {
+  constructor(parent: TrieNode | null, prefix: string = "", key = "") {
     this.children = new Map();
     this.fail = null;
     this.patterns = [];
@@ -89,14 +90,14 @@ export class AC {
   buildFailPointer() {
     let queue: TrieNode[] = [];
     let queueIndex = 0;
-    this.queues.forEach(_queue => {
+    this.queues.forEach((_queue) => {
       queue = queue.concat(_queue);
-    })
+    });
     this.queues = [];
 
     while (queue.length > queueIndex) {
       let node = queue[queueIndex++] as TrieNode;
-      let failNode = node.parent && node.parent.fail as TrieNode | null;
+      let failNode = node.parent && (node.parent.fail as TrieNode | null);
       let key = node.key;
 
       while (failNode && !failNode.children.has(key)) {
@@ -157,7 +158,7 @@ export class AC {
   }
 
   // 搜索字符串返回匹配的模式串
-  match(text: string, isSurname = false) {
+  match(text: string, surname: SurnameMode) {
     let cur = this.root;
     let result: MatchPattern[] = [];
     for (let i = 0; i < text.length; i++) {
@@ -171,9 +172,15 @@ export class AC {
         cur = this.root;
       } else {
         cur = cur.children.get(c) as TrieNode;
-        const pattern = cur.patterns.find((item) =>
-          isSurname ? true : item.priority !== Priority.Surname
-        );
+        const pattern = cur.patterns.find((item) => {
+          if (surname === "off") {
+            return item.priority !== Priority.Surname;
+          } else if (surname === "head") {
+            return i === 0;
+          } else {
+            return true;
+          }
+        });
         if (pattern) {
           result.push({
             ...pattern,
@@ -182,9 +189,15 @@ export class AC {
         }
         let failNode = cur.fail;
         while (failNode !== null) {
-          const pattern = failNode.patterns.find((item) =>
-            isSurname ? true : item.priority !== Priority.Surname
-          );
+          const pattern = failNode.patterns.find((item) => {
+            if (surname === "off") {
+              return item.priority !== Priority.Surname;
+            } else if (surname === "head") {
+              return i === 0;
+            } else {
+              return true;
+            }
+          });
           if (pattern) {
             result.push({
               ...pattern,
@@ -200,10 +213,10 @@ export class AC {
 
   search(
     text: string,
-    isSurname = false,
+    surname: SurnameMode,
     algorithm: TokenizationAlgorithm = TokenizationAlgorithm.MaxProbability
   ) {
-    const patterns = this.match(text, isSurname);
+    const patterns = this.match(text, surname);
     if (algorithm === TokenizationAlgorithm.ReverseMaxMatch) {
       return reverseMaxMatch(patterns);
     } else if (algorithm === TokenizationAlgorithm.MinTokenization) {
diff --git a/lib/common/type.ts b/lib/common/type.ts
index 4187d57..b5f8755 100644
--- a/lib/common/type.ts
+++ b/lib/common/type.ts
@@ -8,6 +8,7 @@ export interface SingleWordResult {
 }
 
 // toneType 属性可选参数
-export type ToneType = 'symbol' | 'num' | 'none';
+export type ToneType = "symbol" | "num" | "none";
 
-export type PinyinMode = 'normal' | 'surname';
+export type PinyinMode = "normal" | "surname";
+export type SurnameMode = "all" | "head" | "off";
diff --git a/lib/core/pinyin/handle.ts b/lib/core/pinyin/handle.ts
index 106490d..9305eb0 100644
--- a/lib/core/pinyin/handle.ts
+++ b/lib/core/pinyin/handle.ts
@@ -5,17 +5,18 @@ import {
   SpecialFinalList,
   doubleFinalList,
   processSepecialPinyin,
-} from '@/data/special';
-import Surnames from '@/data/surname';
-import DICT1 from '@/data/dict1';
-import { getCustomMultpileDict } from '@/core/custom';
-import type { SingleWordResult, PinyinMode } from '../../common/type';
-import { acTree, TokenizationAlgorithm } from '../../common/segmentit';
+} from "@/data/special";
+import Surnames from "@/data/surname";
+import DICT1 from "@/data/dict1";
+import { getCustomMultpileDict } from "@/core/custom";
+import { SingleWordResult } from "../../common/type";
+import type { SurnameMode } from "../../common/type";
+import { acTree, TokenizationAlgorithm } from "../../common/segmentit";
 import {
   DoubleUnicodePrefixReg,
   DoubleUnicodeSuffixReg,
   Priority,
-} from '@/common/constant';
+} from "@/common/constant";
 
 /**
  * @description: 获取单个字符的拼音
@@ -27,23 +28,23 @@ export const getSingleWordPinyin: GetSingleWordPinyin = (word) => {
   const wordCode = word.charCodeAt(0);
   const pinyin = DICT1[wordCode];
   // 若查到, 则返回第一个拼音; 若未查到, 返回原字符
-  return pinyin ? pinyin.split(' ')[0] : word;
+  return pinyin ? pinyin.split(" ")[0] : word;
 };
 
 export const getPinyin = (
   word: string,
   list: SingleWordResult[],
-  mode: 'normal' | 'surname',
-  segmentit: TokenizationAlgorithm,
+  surname: SurnameMode,
+  segmentit: TokenizationAlgorithm
 ): SingleWordResult[] => {
-  const matches = acTree.search(word, mode === 'surname', segmentit);
+  const matches = acTree.search(word, surname, segmentit);
   let matchIndex = 0;
   for (let i = 0; i < word.length; ) {
     const match = matches[matchIndex];
     if (match && i === match.index) {
       if (match.length === 1 && match.priority <= Priority.Normal) {
         const char = word[i];
-        let pinyin: string = '';
+        let pinyin: string = "";
         pinyin = processSepecialPinyin(char, word[i - 1], word[i + 1]);
         list[i] = {
           origin: char,
@@ -55,7 +56,7 @@ export const getPinyin = (
         matchIndex++;
         continue;
       }
-      const pinyins = match.pinyin.split(' ');
+      const pinyins = match.pinyin.split(" ");
       let pinyinIndex = 0;
       for (let j = 0; j < match.length; j++) {
         if (
@@ -64,9 +65,9 @@ export const getPinyin = (
         ) {
           list[i + j] = {
             origin: match.zh[j],
-            result: '',
+            result: "",
             isZh: true,
-            originPinyin: '',
+            originPinyin: "",
           };
         } else {
           list[i + j] = {
@@ -82,7 +83,7 @@ export const getPinyin = (
       matchIndex++;
     } else {
       const char = word[i];
-      let pinyin: string = '';
+      let pinyin: string = "";
       pinyin = processSepecialPinyin(char, word[i - 1], word[i + 1]);
       list[i] = {
         origin: char,
@@ -104,14 +105,14 @@ export const getPinyin = (
 type GetPinyinWithoutTone = (pinyin: string) => string;
 const getPinyinWithoutTone: GetPinyinWithoutTone = (pinyin) => {
   return pinyin
-    .replace(/(ā|á|ǎ|à)/g, 'a')
-    .replace(/(ō|ó|ǒ|ò)/g, 'o')
-    .replace(/(ē|é|ě|è)/g, 'e')
-    .replace(/(ī|í|ǐ|ì)/g, 'i')
-    .replace(/(ū|ú|ǔ|ù)/g, 'u')
-    .replace(/(ǖ|ǘ|ǚ|ǜ)/g, 'ü')
-    .replace(/(ń|ň|ǹ)/g, 'n')
-    .replace(/ḿ|m̀/g, 'm');
+    .replace(/(ā|á|ǎ|à)/g, "a")
+    .replace(/(ō|ó|ǒ|ò)/g, "o")
+    .replace(/(ē|é|ě|è)/g, "e")
+    .replace(/(ī|í|ǐ|ì)/g, "i")
+    .replace(/(ū|ú|ǔ|ù)/g, "u")
+    .replace(/(ǖ|ǘ|ǚ|ǜ)/g, "ü")
+    .replace(/(ń|ň|ǹ)/g, "n")
+    .replace(/ḿ|m̀/g, "m");
 };
 
 /**
@@ -119,17 +120,14 @@ const getPinyinWithoutTone: GetPinyinWithoutTone = (pinyin) => {
  * @param {string} word
  * @return {WordResult[]}
  */
-type GetAllPinyin = (
-  word: string,
-  mode?: PinyinMode
-) => string[];
-export const getAllPinyin: GetAllPinyin = (word, mode = "normal") => {
+type GetAllPinyin = (word: string, surname?: SurnameMode) => string[];
+export const getAllPinyin: GetAllPinyin = (word, surname = "off") => {
   const wordCode = word.charCodeAt(0);
   const customMultpileDict = getCustomMultpileDict();
   let pinyin = DICT1[wordCode] ? DICT1[wordCode].split(" ") : [];
   if (customMultpileDict[wordCode]) {
     pinyin = customMultpileDict[wordCode].split(" ");
-  } else if (mode === "surname") {
+  } else if (surname !== "off") {
     const surnamePinyin = Surnames[word];
     if (surnamePinyin) {
       pinyin = [surnamePinyin].concat(
@@ -147,10 +145,10 @@ export const getAllPinyin: GetAllPinyin = (word, mode = "normal") => {
  */
 type GetMultiplePinyin = (
   word: string,
-  mode?: PinyinMode
+  surname?: SurnameMode
 ) => SingleWordResult[];
-const getMultiplePinyin: GetMultiplePinyin = (word, mode = 'normal') => {
-  let pinyin = getAllPinyin(word, mode);
+const getMultiplePinyin: GetMultiplePinyin = (word, surname = "off") => {
+  let pinyin = getAllPinyin(word, surname);
   if (pinyin.length > 0) {
     return pinyin.map((value) => ({
       origin: word,
@@ -180,7 +178,7 @@ type GetInitialAndFinal = (pinyin: string) => {
   initial: string;
 };
 const getInitialAndFinal: GetInitialAndFinal = (pinyin) => {
-  const pinyin_arr = pinyin.split(' ');
+  const pinyin_arr = pinyin.split(" ");
   const initial_arr: string[] = [];
   const final_arr: string[] = [];
   for (let _pinyin of pinyin_arr) {
@@ -201,8 +199,8 @@ const getInitialAndFinal: GetInitialAndFinal = (pinyin) => {
     }
   }
   return {
-    final: final_arr.join(' '), // 韵母
-    initial: initial_arr.join(' '), // 声母
+    final: final_arr.join(" "), // 韵母
+    initial: initial_arr.join(" "), // 声母
   };
 };
 
@@ -218,16 +216,16 @@ type GetFinalParts = (pinyin: string) => {
 };
 const getFinalParts: GetFinalParts = (pinyin) => {
   const { final } = getInitialAndFinal(pinyin);
-  let head = '',
-    body = '',
-    tail = '';
+  let head = "",
+    body = "",
+    tail = "";
   if (doubleFinalList.indexOf(getPinyinWithoutTone(final)) !== -1) {
     head = final[0];
     body = final[1];
     tail = final.slice(2);
   } else {
-    body = final[0] || '';
-    tail = final.slice(1) || '';
+    body = final[0] || "";
+    tail = final.slice(1) || "";
   }
   return { head, body, tail };
 };
@@ -245,23 +243,23 @@ const getNumOfTone: GetNumOfTone = (pinyin) => {
   const reg_tone4 = /(à|ò|è|ì|ù|ǜ|ǹ|m̀)/;
   const reg_tone0 = /(a|o|e|i|u|ü|n)/;
   const tone_num_arr: string[] = [];
-  const pinyin_arr = pinyin.split(' ');
+  const pinyin_arr = pinyin.split(" ");
   pinyin_arr.forEach((_pinyin) => {
     if (reg_tone1.test(_pinyin)) {
-      tone_num_arr.push('1');
+      tone_num_arr.push("1");
     } else if (reg_tone2.test(_pinyin)) {
-      tone_num_arr.push('2');
+      tone_num_arr.push("2");
     } else if (reg_tone3.test(_pinyin)) {
-      tone_num_arr.push('3');
+      tone_num_arr.push("3");
     } else if (reg_tone4.test(_pinyin)) {
-      tone_num_arr.push('4');
+      tone_num_arr.push("4");
     } else if (reg_tone0.test(_pinyin)) {
-      tone_num_arr.push('0');
+      tone_num_arr.push("0");
     } else {
-      tone_num_arr.push('');
+      tone_num_arr.push("");
     }
   });
-  return tone_num_arr.join(' ');
+  return tone_num_arr.join(" ");
 };
 
 /**
@@ -272,13 +270,13 @@ const getNumOfTone: GetNumOfTone = (pinyin) => {
  */
 type GetPinyinWithNum = (pinyin: string, originPinyin: string) => string;
 const getPinyinWithNum: GetPinyinWithNum = (pinyin, originPinyin) => {
-  const pinyin_arr = getPinyinWithoutTone(pinyin).split(' ');
-  const tone_num_arr = getNumOfTone(originPinyin).split(' ');
+  const pinyin_arr = getPinyinWithoutTone(pinyin).split(" ");
+  const tone_num_arr = getNumOfTone(originPinyin).split(" ");
   const res_arr: string[] = [];
   pinyin_arr.forEach((item, index) => {
     res_arr.push(`${item}${tone_num_arr[index]}`);
   });
-  return res_arr.join(' ');
+  return res_arr.join(" ");
 };
 
 /**
@@ -289,11 +287,11 @@ const getPinyinWithNum: GetPinyinWithNum = (pinyin, originPinyin) => {
 type GetFirstLetter = (pinyin: string) => string;
 const getFirstLetter: GetFirstLetter = (pinyin) => {
   const first_letter_arr: string[] = [];
-  const pinyin_arr = pinyin.split(' ');
+  const pinyin_arr = pinyin.split(" ");
   pinyin_arr.forEach((pinyin) => {
     first_letter_arr.push(pinyin[0]);
   });
-  return first_letter_arr.join(' ');
+  return first_letter_arr.join(" ");
 };
 
 export {
diff --git a/lib/core/pinyin/index.ts b/lib/core/pinyin/index.ts
index 927bcf1..0220e48 100644
--- a/lib/core/pinyin/index.ts
+++ b/lib/core/pinyin/index.ts
@@ -1,6 +1,10 @@
-import { TokenizationAlgorithm } from '../../common/segmentit';
-import type { SingleWordResult } from '../../common/type';
-import { getPinyin } from './handle';
+import { TokenizationAlgorithm } from "../../common/segmentit";
+import type {
+  SingleWordResult,
+  PinyinMode,
+  SurnameMode,
+} from "../../common/type";
+import { getPinyin } from "./handle";
 import {
   validateType,
   middleWareNonZh,
@@ -11,7 +15,7 @@ import {
   middlewareType,
   middlewareDoubleUnicode,
   middlewareToneSandhi,
-} from './middlewares';
+} from "./middlewares";
 
 interface BasicOptions {
   /**
@@ -20,7 +24,7 @@ interface BasicOptions {
    * @value num：以数字格式展示音调，并跟在拼音后面
    * @value none：不展示音调
    */
-  toneType?: 'symbol' | 'num' | 'none';
+  toneType?: "symbol" | "num" | "none";
   /**
    * @description 返回的拼音格式类型
    * @value pinyin：返回完整拼音 （默认值）
@@ -33,14 +37,14 @@ interface BasicOptions {
    * @value finalTail：返回韵尾
    */
   pattern?:
-    | 'pinyin'
-    | 'initial'
-    | 'final'
-    | 'num'
-    | 'first'
-    | 'finalHead'
-    | 'finalBody'
-    | 'finalTail';
+    | "pinyin"
+    | "initial"
+    | "final"
+    | "num"
+    | "first"
+    | "finalHead"
+    | "finalBody"
+    | "finalTail";
   /**
    * @description 是否返回单个汉字的所有多音，仅针对输入的 word 为单个汉字生效
    * @value false：返回最常用的一个拼音 （默认值）
@@ -52,7 +56,14 @@ interface BasicOptions {
    * @value normal：正常匹配模式 （默认值）
    * @value surname：姓氏模式，遇到姓氏表中的汉字时，优先匹配姓氏读音
    */
-  mode?: 'normal' | 'surname';
+  mode?: PinyinMode;
+  /**
+   * @description surname 模式作用的范围
+   * @value off：不启用 surname 模式（默认值为 off）
+   * @value all：作用于整个汉语字符串（当设置了 `mode: surname` 时，默认值为 all）
+   * @value head：作用于汉语字符串开头
+   */
+  surname?: SurnameMode;
   /**
    * @description 是否移除非汉字字符（推荐使用 removeNonZh: removed 代替）
    * @value false：返回结果保留非汉字字符 （默认值）
@@ -65,7 +76,7 @@ interface BasicOptions {
    * @value consecutive：连续非汉字字符无间距
    * @value removed：返回结果移除非汉字字符
    */
-  nonZh?: 'spaced' | 'consecutive' | 'removed';
+  nonZh?: "spaced" | "consecutive" | "removed";
   /**
    * @description 对于 ü 的返回是否转换成 v（仅在 toneType: none 启用时生效）
    * @value false：返回值中保留 ü （默认值）
@@ -109,7 +120,7 @@ interface OptionsReturnString extends BasicOptions {
    * @value array：以数组格式返回
    * @value array: 返回全部信息数组
    */
-  type?: 'string';
+  type?: "string";
   /**
    * @description 拼音之间的分隔符，默认为空格，仅在 type 为 'string' 时生效
    */
@@ -123,7 +134,7 @@ interface OptionsReturnArray extends BasicOptions {
    * @value array：以数组格式返回
    * @value array: 返回全部信息数组
    */
-  type: 'array';
+  type: "array";
 }
 
 interface OptionsReturnAll extends BasicOptions {
@@ -133,7 +144,7 @@ interface OptionsReturnAll extends BasicOptions {
    * @value array：以数组格式返回
    * @value array: 返回全部信息数组
    */
-  type: 'all';
+  type: "all";
 }
 
 export interface CompleteOptions extends BasicOptions {
@@ -143,7 +154,7 @@ export interface CompleteOptions extends BasicOptions {
    * @value array：以数组格式返回
    * @value array: 返回全部信息数组
    */
-  type?: 'string' | 'array' | 'all';
+  type?: "string" | "array" | "all";
   /**
    * @description 拼音之间的分隔符，默认为空格，仅在 type 为 'string' 时生效
    */
@@ -151,15 +162,15 @@ export interface CompleteOptions extends BasicOptions {
 }
 
 const DEFAULT_OPTIONS: CompleteOptions = {
-  pattern: 'pinyin',
-  toneType: 'symbol',
-  type: 'string',
+  pattern: "pinyin",
+  toneType: "symbol",
+  type: "string",
   multiple: false,
-  mode: 'normal',
+  mode: "normal",
   removeNonZh: false,
-  nonZh: 'spaced',
+  nonZh: "spaced",
   v: false,
-  separator: ' ',
+  separator: " ",
   toneSandhi: true,
   segmentit: TokenizationAlgorithm.MaxProbability,
 };
@@ -206,25 +217,38 @@ function pinyin(
   }
 
   // 传入空字符串
-  if (word === '') {
-    return options.type === 'array' || options.type === 'all' ? [] : '';
+  if (word === "") {
+    return options.type === "array" || options.type === "all" ? [] : "";
+  }
+
+  if (options.surname === undefined) {
+    if (options.mode === "surname") {
+      options.surname = "all";
+    } else {
+      options.surname = "off";
+    }
   }
 
-  if (options.type === 'all') {
-    options.pattern = 'pinyin';
+  if (options.type === "all") {
+    options.pattern = "pinyin";
   }
 
-  if (options.pattern === 'num') {
-    options.toneType = 'none';
+  if (options.pattern === "num") {
+    options.toneType = "none";
   }
 
   if (options.removeNonZh) {
-    options.nonZh = 'removed';
+    options.nonZh = "removed";
   }
 
   let list: SingleWordResult[] = Array(word.length);
 
-  list = getPinyin(word, list, options.mode as 'normal', options.segmentit as TokenizationAlgorithm);
+  list = getPinyin(
+    word,
+    list,
+    options.surname as SurnameMode,
+    options.segmentit as TokenizationAlgorithm
+  );
 
   // 一和不变调处理
   list = middlewareToneSandhi(list, options.toneSandhi as boolean);
diff --git a/lib/core/pinyin/middlewares.ts b/lib/core/pinyin/middlewares.ts
index a1ad476..6487443 100644
--- a/lib/core/pinyin/middlewares.ts
+++ b/lib/core/pinyin/middlewares.ts
@@ -1,11 +1,11 @@
-import { getStringLength, isZhChar } from '@/common/utils';
-import type { SingleWordResult } from '../../common/type';
+import { getStringLength, isZhChar } from "@/common/utils";
+import type { SingleWordResult } from "../../common/type";
 import {
   DoubleUnicodePrefixReg,
   DoubleUnicodeSuffixReg,
-} from '@/common/constant';
-import { getAllPinyin, getMultiplePinyin } from './handle';
-import { CompleteOptions } from './index';
+} from "@/common/constant";
+import { getAllPinyin, getMultiplePinyin } from "./handle";
+import { CompleteOptions } from "./index";
 import {
   getNumOfTone,
   getInitialAndFinal,
@@ -13,13 +13,13 @@ import {
   getFinalParts,
   getPinyinWithoutTone,
   getPinyinWithNum,
-} from './handle';
+} from "./handle";
 
 // 验证输入是否为字符串
 export const validateType = (word: unknown) => {
-  if (typeof word !== 'string') {
+  if (typeof word !== "string") {
     console.error(
-      'The first param of pinyin is error: ' +
+      "The first param of pinyin is error: " +
         word +
         ' is not assignable to type "string".'
     );
@@ -36,9 +36,9 @@ export const middleWareNonZh = (
 ) => {
   let nonZh = options.nonZh;
 
-  if (nonZh === 'removed') {
+  if (nonZh === "removed") {
     return list.filter((item) => item.isZh);
-  } else if (nonZh === 'consecutive') {
+  } else if (nonZh === "consecutive") {
     for (let i = list.length - 2; i >= 0; i--) {
       const cur = list[i];
       const pre = list[i + 1];
@@ -60,7 +60,7 @@ export const middlewareMultiple = (
   options: CompleteOptions
 ): SingleWordResult[] | false => {
   if (getStringLength(word) === 1 && options.multiple) {
-    return getMultiplePinyin(word, options.mode);
+    return getMultiplePinyin(word, options.surname);
   } else {
     return false;
   }
@@ -72,42 +72,42 @@ export const middlewarePattern = (
   options: CompleteOptions
 ) => {
   switch (options.pattern) {
-    case 'pinyin':
+    case "pinyin":
       break;
-    case 'num':
+    case "num":
       list.forEach((item) => {
-        item.result = item.isZh ? getNumOfTone(item.result) : '';
+        item.result = item.isZh ? getNumOfTone(item.result) : "";
       });
       break;
-    case 'initial':
+    case "initial":
       list.forEach((item) => {
-        item.result = item.isZh ? getInitialAndFinal(item.result).initial : '';
+        item.result = item.isZh ? getInitialAndFinal(item.result).initial : "";
       });
       break;
-    case 'final':
+    case "final":
       list.forEach((item) => {
-        item.result = item.isZh ? getInitialAndFinal(item.result).final : '';
+        item.result = item.isZh ? getInitialAndFinal(item.result).final : "";
       });
       break;
-    case 'first':
+    case "first":
       list.forEach((item) => {
         // todo: first 暂时不作为拼音一部分，不进行 isZh 识别
         item.result = getFirstLetter(item.result);
       });
       break;
-    case 'finalHead':
+    case "finalHead":
       list.forEach((item) => {
-        item.result = item.isZh ? getFinalParts(item.result).head : '';
+        item.result = item.isZh ? getFinalParts(item.result).head : "";
       });
       break;
-    case 'finalBody':
+    case "finalBody":
       list.forEach((item) => {
-        item.result = item.isZh ? getFinalParts(item.result).body : '';
+        item.result = item.isZh ? getFinalParts(item.result).body : "";
       });
       break;
-    case 'finalTail':
+    case "finalTail":
       list.forEach((item) => {
-        item.result = item.isZh ? getFinalParts(item.result).tail : '';
+        item.result = item.isZh ? getFinalParts(item.result).tail : "";
       });
       break;
     default:
@@ -121,16 +121,16 @@ export const middlewareToneType = (
   options: CompleteOptions
 ) => {
   switch (options.toneType) {
-    case 'symbol':
+    case "symbol":
       break;
-    case 'none':
+    case "none":
       list.forEach((item) => {
         if (item.isZh) {
           item.result = getPinyinWithoutTone(item.result);
         }
       });
       break;
-    case 'num': {
+    case "num": {
       list.forEach((item) => {
         if (item.isZh) {
           item.result = getPinyinWithNum(
@@ -154,7 +154,7 @@ export const middlewareV = (
   if (options.v) {
     list.forEach((item) => {
       if (item.isZh) {
-        item.result = item.result.replace(/ü/g, 'v');
+        item.result = item.result.replace(/ü/g, "v");
       }
     });
   }
@@ -167,25 +167,25 @@ export const middlewareType = (
   word: string
 ) => {
   if (options.multiple && getStringLength(word) === 1) {
-    let last = '';
+    let last = "";
     list = list.filter((item) => {
       const res = item.result !== last;
       last = item.result;
       return res;
     });
   }
-  if (options.type === 'array') {
+  if (options.type === "array") {
     return list.map((item) => item.result);
   }
-  if (options.type === 'all') {
+  if (options.type === "all") {
     return list.map((item) => {
-      const pinyin = item.isZh ? item.result : '';
+      const pinyin = item.isZh ? item.result : "";
       const { initial, final } = getInitialAndFinal(pinyin);
       const { head, body, tail } = getFinalParts(pinyin);
       let polyphonic: string[] = [];
-      if (pinyin !== '') {
+      if (pinyin !== "") {
         polyphonic = [pinyin].concat(
-          getAllPinyin(item.origin, options.mode).filter(
+          getAllPinyin(item.origin, options.surname).filter(
             (item) => item !== pinyin
           )
         );
@@ -195,7 +195,7 @@ export const middlewareType = (
         pinyin,
         initial,
         final,
-        first: item.isZh ? getFirstLetter(item.result) : '',
+        first: item.isZh ? getFirstLetter(item.result) : "",
         finalHead: head,
         finalBody: body,
         finalTail: tail,
@@ -236,16 +236,16 @@ export const middlewareDoubleUnicode = (
 // 是否开启变调
 export const middlewareToneSandhi = (
   list: SingleWordResult[],
-  toneSandhi: boolean,
+  toneSandhi: boolean
 ): SingleWordResult[] => {
   if (toneSandhi === false) {
-    list.forEach(item => {
-      if (item.origin === '一') {
-        item.result = item.originPinyin = 'yī';
-      } else if (item.origin === '不') {
-        item.result = item.originPinyin = 'bù';
+    list.forEach((item) => {
+      if (item.origin === "一") {
+        item.result = item.originPinyin = "yī";
+      } else if (item.origin === "不") {
+        item.result = item.originPinyin = "bù";
       }
-    })
+    });
   }
   return list;
 };
diff --git a/types/common/segmentit/index.d.ts b/types/common/segmentit/index.d.ts
index cb41761..d8e0608 100644
--- a/types/common/segmentit/index.d.ts
+++ b/types/common/segmentit/index.d.ts
@@ -1,3 +1,4 @@
+import type { SurnameMode } from "../type";
 export declare const enum TokenizationAlgorithm {
     ReverseMaxMatch = 1,
     MaxProbability = 2,
@@ -40,8 +41,8 @@ export declare class AC {
     addNodeToQueues(trieNode: TrieNode): void;
     insertPattern(patterns: Pattern[], pattern: Pattern): void;
     removeDict(dictName: string | symbol): void;
-    match(text: string, isSurname?: boolean): MatchPattern[];
-    search(text: string, isSurname?: boolean, algorithm?: TokenizationAlgorithm): MatchPattern[];
+    match(text: string, surname: SurnameMode): MatchPattern[];
+    search(text: string, surname: SurnameMode, algorithm?: TokenizationAlgorithm): MatchPattern[];
 }
 export declare const PatternsNormal: Pattern[];
 export declare const acTree: AC;
diff --git a/types/common/type.d.ts b/types/common/type.d.ts
index c60b4b7..b0462aa 100644
--- a/types/common/type.d.ts
+++ b/types/common/type.d.ts
@@ -5,5 +5,6 @@ export interface SingleWordResult {
     isZh: boolean;
     delete?: boolean;
 }
-export type ToneType = 'symbol' | 'num' | 'none';
-export type PinyinMode = 'normal' | 'surname';
+export type ToneType = "symbol" | "num" | "none";
+export type PinyinMode = "normal" | "surname";
+export type SurnameMode = "all" | "head" | "off";
diff --git a/types/core/pinyin/handle.d.ts b/types/core/pinyin/handle.d.ts
index 68d2810..f2406ce 100644
--- a/types/core/pinyin/handle.d.ts
+++ b/types/core/pinyin/handle.d.ts
@@ -1,5 +1,6 @@
-import type { SingleWordResult, PinyinMode } from '../../common/type';
-import { TokenizationAlgorithm } from '../../common/segmentit';
+import { SingleWordResult } from "../../common/type";
+import type { SurnameMode } from "../../common/type";
+import { TokenizationAlgorithm } from "../../common/segmentit";
 /**
  * @description: 获取单个字符的拼音
  * @param {string} word
@@ -7,7 +8,7 @@ import { TokenizationAlgorithm } from '../../common/segmentit';
  */
 type GetSingleWordPinyin = (word: string) => string;
 export declare const getSingleWordPinyin: GetSingleWordPinyin;
-export declare const getPinyin: (word: string, list: SingleWordResult[], mode: 'normal' | 'surname', segmentit: TokenizationAlgorithm) => SingleWordResult[];
+export declare const getPinyin: (word: string, list: SingleWordResult[], surname: SurnameMode, segmentit: TokenizationAlgorithm) => SingleWordResult[];
 /**
  * @description: 将带音调符号拼音转换为不带音调拼音
  * @param {string} pinyin
@@ -20,14 +21,14 @@ declare const getPinyinWithoutTone: GetPinyinWithoutTone;
  * @param {string} word
  * @return {WordResult[]}
  */
-type GetAllPinyin = (word: string, mode?: PinyinMode) => string[];
+type GetAllPinyin = (word: string, surname?: SurnameMode) => string[];
 export declare const getAllPinyin: GetAllPinyin;
 /**
  * @description: 获取单字符的多音拼音
  * @param {string} word
  * @return {WordResult[]}
  */
-type GetMultiplePinyin = (word: string, mode?: PinyinMode) => SingleWordResult[];
+type GetMultiplePinyin = (word: string, surname?: SurnameMode) => SingleWordResult[];
 declare const getMultiplePinyin: GetMultiplePinyin;
 /**
  * @description: 获取拼音的声母和韵母
diff --git a/types/core/pinyin/index.d.ts b/types/core/pinyin/index.d.ts
index 467e82e..1bd90f2 100644
--- a/types/core/pinyin/index.d.ts
+++ b/types/core/pinyin/index.d.ts
@@ -1,4 +1,5 @@
-import { TokenizationAlgorithm } from '../../common/segmentit';
+import { TokenizationAlgorithm } from "../../common/segmentit";
+import type { PinyinMode, SurnameMode } from "../../common/type";
 interface BasicOptions {
     /**
      * @description 返回的拼音音调类型
@@ -6,7 +7,7 @@ interface BasicOptions {
      * @value num：以数字格式展示音调，并跟在拼音后面
      * @value none：不展示音调
      */
-    toneType?: 'symbol' | 'num' | 'none';
+    toneType?: "symbol" | "num" | "none";
     /**
      * @description 返回的拼音格式类型
      * @value pinyin：返回完整拼音 （默认值）
@@ -18,7 +19,7 @@ interface BasicOptions {
      * @value finalBody：返回韵腹
      * @value finalTail：返回韵尾
      */
-    pattern?: 'pinyin' | 'initial' | 'final' | 'num' | 'first' | 'finalHead' | 'finalBody' | 'finalTail';
+    pattern?: "pinyin" | "initial" | "final" | "num" | "first" | "finalHead" | "finalBody" | "finalTail";
     /**
      * @description 是否返回单个汉字的所有多音，仅针对输入的 word 为单个汉字生效
      * @value false：返回最常用的一个拼音 （默认值）
@@ -30,7 +31,14 @@ interface BasicOptions {
      * @value normal：正常匹配模式 （默认值）
      * @value surname：姓氏模式，遇到姓氏表中的汉字时，优先匹配姓氏读音
      */
-    mode?: 'normal' | 'surname';
+    mode?: PinyinMode;
+    /**
+     * @description surname 模式作用的范围
+     * @value off：不启用 surname 模式（默认值为 off）
+     * @value all：作用于整个汉语字符串（当设置了 `mode: surname` 时，默认值为 all）
+     * @value head：作用于汉语字符串开头
+     */
+    surname?: SurnameMode;
     /**
      * @description 是否移除非汉字字符（推荐使用 removeNonZh: removed 代替）
      * @value false：返回结果保留非汉字字符 （默认值）
@@ -43,7 +51,7 @@ interface BasicOptions {
      * @value consecutive：连续非汉字字符无间距
      * @value removed：返回结果移除非汉字字符
      */
-    nonZh?: 'spaced' | 'consecutive' | 'removed';
+    nonZh?: "spaced" | "consecutive" | "removed";
     /**
      * @description 对于 ü 的返回是否转换成 v（仅在 toneType: none 启用时生效）
      * @value false：返回值中保留 ü （默认值）
@@ -85,7 +93,7 @@ interface OptionsReturnString extends BasicOptions {
      * @value array：以数组格式返回
      * @value array: 返回全部信息数组
      */
-    type?: 'string';
+    type?: "string";
     /**
      * @description 拼音之间的分隔符，默认为空格，仅在 type 为 'string' 时生效
      */
@@ -98,7 +106,7 @@ interface OptionsReturnArray extends BasicOptions {
      * @value array：以数组格式返回
      * @value array: 返回全部信息数组
      */
-    type: 'array';
+    type: "array";
 }
 interface OptionsReturnAll extends BasicOptions {
     /**
@@ -107,7 +115,7 @@ interface OptionsReturnAll extends BasicOptions {
      * @value array：以数组格式返回
      * @value array: 返回全部信息数组
      */
-    type: 'all';
+    type: "all";
 }
 export interface CompleteOptions extends BasicOptions {
     /**
@@ -116,7 +124,7 @@ export interface CompleteOptions extends BasicOptions {
      * @value array：以数组格式返回
      * @value array: 返回全部信息数组
      */
-    type?: 'string' | 'array' | 'all';
+    type?: "string" | "array" | "all";
     /**
      * @description 拼音之间的分隔符，默认为空格，仅在 type 为 'string' 时生效
      */
diff --git a/types/core/pinyin/middlewares.d.ts b/types/core/pinyin/middlewares.d.ts
index fbdfd7b..9f1ebb9 100644
--- a/types/core/pinyin/middlewares.d.ts
+++ b/types/core/pinyin/middlewares.d.ts
@@ -1,5 +1,5 @@
-import type { SingleWordResult } from '../../common/type';
-import { CompleteOptions } from './index';
+import type { SingleWordResult } from "../../common/type";
+import { CompleteOptions } from "./index";
 export declare const validateType: (word: unknown) => boolean;
 export declare const middleWareNonZh: (list: SingleWordResult[], options: CompleteOptions) => SingleWordResult[];
 export declare const middlewareMultiple: (word: string, options: CompleteOptions) => SingleWordResult[] | false;


Test patch:
diff --git a/test/surname.test.js b/test/surname.test.js
index 09b0474..41d8727 100644
--- a/test/surname.test.js
+++ b/test/surname.test.js
@@ -1,34 +1,49 @@
-import { pinyin } from '../lib/index';
-import { expect, describe, it } from 'vitest';
+import { pinyin } from "../lib/index";
+import { expect, describe, it } from "vitest";
 
-describe('surname', () => {
-  it('[surname]multiple surname1', () => {
-    const result = pinyin('万俟', { mode: 'surname' });
-    expect(result).to.be.equal('mò qí');
+describe("surname", () => {
+  it("[surname]multiple surname1", () => {
+    const result = pinyin("万俟", { mode: "surname" });
+    expect(result).to.be.equal("mò qí");
   });
 
-  it('[surname]multiple surname2', () => {
-    const result = pinyin('我叫令狐冲', { mode: 'surname' });
-    expect(result).to.be.equal('wǒ jiào líng hú chōng');
+  it("[surname]multiple surname2", () => {
+    const result = pinyin("我叫令狐冲", { mode: "surname" });
+    expect(result).to.be.equal("wǒ jiào líng hú chōng");
   });
 
-  it('[surname]multiple surname3', () => {
-    const result = pinyin('曾令狐冲', { mode: 'surname' });
-    expect(result).to.be.equal('zēng líng hú chōng');
+  it("[surname]multiple surname3", () => {
+    const result = pinyin("曾令狐冲", { mode: "surname" });
+    expect(result).to.be.equal("zēng líng hú chōng");
   });
 
-  it('[surname]multiple surname4', () => {
-    const result = pinyin('我叫区中青', { mode: 'surname' });
-    expect(result).to.be.equal('wǒ jiào ōu zhōng qīng');
+  it("[surname]multiple surname4", () => {
+    const result = pinyin("我叫区中青", { mode: "surname" });
+    expect(result).to.be.equal("wǒ jiào ōu zhōng qīng");
   });
 
-  it('[surname]multiple surname5', () => {
-    const result = pinyin('我叫覃晓旭', { mode: 'surname' });
-    expect(result).to.be.equal('wǒ jiào qín xiǎo xù');
+  it("[surname]multiple surname5", () => {
+    const result = pinyin("我叫覃晓旭", { mode: "surname" });
+    expect(result).to.be.equal("wǒ jiào qín xiǎo xù");
   });
 
-  it('[surname]multiple surname6', () => {
-    const result = pinyin('我叫朴岁植', { mode: 'surname' });
-    expect(result).to.be.equal('wǒ jiào piáo suì zhí');
+  it("[surname]multiple surname6", () => {
+    const result = pinyin("我叫朴岁植", { mode: "surname" });
+    expect(result).to.be.equal("wǒ jiào piáo suì zhí");
+  });
+
+  it("[surname]multiple surname head", () => {
+    const result = pinyin("曾乐乐", { mode: "surname", surname: "head" });
+    expect(result).to.be.equal("zēng lè lè");
+  });
+
+  it("[surname]multiple surname head2", () => {
+    const result = pinyin("曾经沧海难为水好好学习乐", {
+      mode: "surname",
+      surname: "head",
+    });
+    expect(result).to.be.equal(
+      "zēng jīng cāng hǎi nán wèi shuǐ hǎo hǎo xué xí lè"
+    );
   });
 });