add_space 0.1.24

Automated typesetting tool that adds spaces between Chinese and English characters to enhance readability. / 自动排版工具,在中文和英文字符之间添加空格以增强可读性。
Documentation
use std::iter::Peekable;
use std::str::Chars;
use unicode_script::{Script, UnicodeScript};

/// 字符状态枚举
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum State {
  Space,
  Char,
  Letter,
  Punctuation,
}

/// 判断字符的状态
pub fn state(c: char) -> State {
  // ASCII 快速通道,避免 unicode-script 查表开销
  if c.is_ascii() {
    if c.is_ascii_whitespace() {
      return State::Space;
    }
    if c == '`' {
      return State::Letter;
    }
    if matches!(
      c,
      '!' | '"'
        | '#'
        | '%'
        | '\\'
        | '\''
        | '*'
        | '+'
        | ','
        | '-'
        | '.'
        | '/'
        | ':'
        | '<'
        | '='
        | '>'
        | '?'
        | '@'
        | '^'
    ) {
      return State::Punctuation;
    }
    return State::Letter;
  }

  // 非 ASCII 字符处理
  if matches!(
    c.script(),
    Script::Han
      | Script::Hiragana
      | Script::Katakana
      | Script::Thai
      | Script::Lao
      | Script::Khmer
      | Script::Myanmar
      | Script::Tibetan
  ) || (''..='').contains(&c)
  {
    return State::Char;
  }

  if matches!(
    c,
    '·' | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
      | ''
  ) || (c.len_utf8() > 1 && unic_emoji_char::is_emoji(c))
  {
    return State::Punctuation;
  }

  State::Letter
}

/// 将左括号入栈
fn push_stack(c: char, stack: &mut Vec<char>) {
  if "[({".contains(c) {
    stack.push(c);
  }
}

/// 判断状态是否为字母或标点
pub fn state_is_letter_or_punctuation(s: State) -> bool {
  matches!(s, State::Letter | State::Punctuation)
}

/// 检查前方是否存在匹配的且未转义的引号字符
fn has_matching_quote(iter: Peekable<Chars<'_>>, quote: char) -> bool {
  let mut escaped = false;
  for c in iter {
    if escaped {
      escaped = false;
    } else if c == '\\' {
      escaped = true;
    } else if c == quote {
      return true;
    }
  }
  false
}

/// 自动在中文和英文/数字之间添加空格
pub fn add_space(txt: impl AsRef<str>) -> String {
  let txt = txt.as_ref();
  let mut r = String::new();
  let mut iter = txt.chars().peekable();

  if let Some(c) = iter.next() {
    r.push(c);
    let mut is_escape = c == '\\';
    let mut in_quote = if !is_escape && (c == '"' || c == '\'') && has_matching_quote(iter.clone(), c) {
      Some(c)
    } else {
      None
    };

    // 追踪反引号状态及长度,Some(n) 表示匹配 n 个反引号
    let mut backtick_style = if c == '`' {
      let mut count = 1;
      while iter.peek() == Some(&'`') {
        iter.next();
        count += 1;
      }
      r.extend(std::iter::repeat_n('`', count - 1));
      Some(count)
    } else {
      None
    };
    let mut pre_state = state(c);
    let mut pre_pre_state = State::Space;
    let mut pre_c = c;
    let mut stack = Vec::new();

    push_stack(c, &mut stack);

    // 辅助闭包:判断是否需要在反引号块边界加空格
    let should_space = |pre_state: State, next_c: Option<&char>| -> bool {
      next_c.is_some_and(|&nc| {
        let next_s = state(nc);
        (pre_state == State::Letter && next_s == State::Char)
          || (pre_state == State::Char && next_s == State::Letter)
      })
    };

    #[allow(clippy::while_let_on_iterator)]
    while let Some(c) = iter.next() {
      if is_escape {
        is_escape = false;
        r.push(c);
        continue;
      }

      if c == '`' {
        let mut count = 1;
        while iter.peek() == Some(&'`') {
          iter.next();
          count += 1;
        }

        match backtick_style {
          None => {
            backtick_style = Some(count);
            if should_space(pre_state, iter.peek()) {
              r.push(' ');
            }
            r.extend(std::iter::repeat_n('`', count));
            pre_pre_state = pre_state;
            pre_state = State::Letter;
            pre_c = '`';
          }
          Some(style_len) => {
            if count == style_len {
              backtick_style = None;
              let space_needed = should_space(pre_state, iter.peek());
              r.extend(std::iter::repeat_n('`', count));
              if space_needed {
                r.push(' ');
                pre_pre_state = pre_state;
                pre_state = State::Space;
                pre_c = ' ';
              } else {
                pre_pre_state = pre_state;
                pre_state = State::Letter;
                pre_c = '`';
              }
            } else {
              r.extend(std::iter::repeat_n('`', count));
              pre_pre_state = pre_state;
              pre_state = State::Letter;
              pre_c = '`';
            }
          }
        }
        continue;
      }

      if backtick_style.is_some() {
        r.push(c);
        if c == '\\' {
          is_escape = true;
        }
        pre_pre_state = pre_state;
        pre_state = state(c);
        pre_c = c;
        continue;
      }

      let s = state(c);

      let mut current_in_quote = in_quote;
      if in_quote.is_none() {
        if (c == '"' || c == '\'') && has_matching_quote(iter.clone(), c) {
          in_quote = Some(c);
          current_in_quote = Some(c);
        }
      } else if Some(c) == in_quote {
        in_quote = None;
      }

      if c == '\\' {
        is_escape = true;
      }

      if current_in_quote.is_some() {
        r.push(c);
      } else {
        push_stack(c, &mut stack);
        match s {
          State::Char => {
            if pre_state == State::Letter
              && !"[({".contains(pre_c)
              && state_is_letter_or_punctuation(pre_pre_state)
            {
              r.push(' ');
            }
            r.push(c);
          }
          State::Letter => {
            if let Some(stack_last) = stack.last() {
              if matches!((stack_last, c), ('[', ']') | ('(', ')') | ('{', '}')) {
                stack.pop();
              }
            } else if ((pre_state == State::Char)
              || (",…".contains(pre_c))
              || ("!?".contains(pre_c)
                && !matches!(pre_pre_state, State::Letter | State::Punctuation)))
              && let Some(c) = iter.peek()
              && state_is_letter_or_punctuation(state(*c))
            {
              r.push(' ');
            }
            r.push(c);
          }
          _ => r.push(c),
        }
      }
      pre_pre_state = pre_state;
      pre_state = s;
      pre_c = c;
    }
  }
  r
}