Skip to main content

pagesift_diff/
inline.rs

1//! 行内差异:英文按词、中日韩按字。
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::algo::{self, Edit};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub enum InlineOp {
12    Equal,
13    Delete,
14    Insert,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct InlineSegment {
20    pub op: InlineOp,
21    pub text: String,
22}
23
24fn is_cjk(c: char) -> bool {
25    matches!(c as u32,
26        0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF | 0x20000..=0x2FFFF
27        | 0x3040..=0x30FF | 0xAC00..=0xD7AF)
28}
29
30pub(crate) fn tokenize(s: &str) -> Vec<&str> {
31    #[derive(PartialEq, Clone, Copy)]
32    enum Class {
33        Word,
34        Space,
35        Single,
36    }
37    let class = |c: char| {
38        if is_cjk(c) {
39            Class::Single
40        } else if c.is_alphanumeric() {
41            Class::Word
42        } else if c.is_whitespace() {
43            Class::Space
44        } else {
45            Class::Single
46        }
47    };
48    let mut out = Vec::new();
49    let mut start: Option<(usize, Class)> = None;
50    for (i, c) in s.char_indices() {
51        let cl = class(c);
52        match start {
53            Some((_, k)) if k == cl && k != Class::Single => {}
54            Some((st, _)) => {
55                out.push(&s[st..i]);
56                start = Some((i, cl));
57            }
58            None => start = Some((i, cl)),
59        }
60    }
61    if let Some((st, _)) = start {
62        out.push(&s[st..]);
63    }
64    out
65}
66
67pub fn inline_diff(old: &str, new: &str) -> Vec<InlineSegment> {
68    let ot = tokenize(old);
69    let nt = tokenize(new);
70    let mut ids: HashMap<&str, u32> = HashMap::new();
71    let oi: Vec<u32> = ot
72        .iter()
73        .map(|s| {
74            let n = ids.len() as u32;
75            *ids.entry(s).or_insert(n)
76        })
77        .collect();
78    let ni: Vec<u32> = nt
79        .iter()
80        .map(|s| {
81            let n = ids.len() as u32;
82            *ids.entry(s).or_insert(n)
83        })
84        .collect();
85    let mut segs: Vec<InlineSegment> = Vec::new();
86    let mut push = |op: InlineOp, t: &str| match segs.last_mut() {
87        Some(l) if l.op == op => l.text.push_str(t),
88        _ => segs.push(InlineSegment {
89            op,
90            text: t.to_string(),
91        }),
92    };
93    // 同一处修改里先输出删除,再输出插入,便于阅读
94    let (mut dels, mut inss): (Vec<&str>, Vec<&str>) = (Vec::new(), Vec::new());
95    for e in algo::diff(&oi, &ni) {
96        match e {
97            Edit::Equal(i, _) => {
98                for d in dels.drain(..) {
99                    push(InlineOp::Delete, d);
100                }
101                for s in inss.drain(..) {
102                    push(InlineOp::Insert, s);
103                }
104                push(InlineOp::Equal, ot[i]);
105            }
106            Edit::Delete(i) => dels.push(ot[i]),
107            Edit::Insert(j) => inss.push(nt[j]),
108        }
109    }
110    for d in dels {
111        push(InlineOp::Delete, d);
112    }
113    for s in inss {
114        push(InlineOp::Insert, s);
115    }
116    segs
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn cjk_by_char() {
125        let segs = inline_diff("今天天气很好", "今天天气很差");
126        let del: Vec<_> = segs.iter().filter(|s| s.op == InlineOp::Delete).collect();
127        assert_eq!(del.len(), 1);
128        assert_eq!(del[0].text, "好");
129    }
130
131    #[test]
132    fn words() {
133        let segs = inline_diff("the quick fox", "the slow fox");
134        assert!(
135            segs.iter()
136                .any(|s| s.op == InlineOp::Delete && s.text == "quick")
137        );
138        assert!(
139            segs.iter()
140                .any(|s| s.op == InlineOp::Insert && s.text == "slow")
141        );
142    }
143}