Merge branch 'github-master' into develop

This commit is contained in:
RememBerBer
2025-09-28 13:24:41 +08:00
26 changed files with 1893 additions and 266 deletions
-1
View File
@@ -1 +0,0 @@
www.luoboduner.com
+9
View File
@@ -55,6 +55,7 @@
<bcprov-jdk15on.version>1.70</bcprov-jdk15on.version>
<tika.version>3.0.0</tika.version>
<jackson-databind.version>2.13.3</jackson-databind.version>
<java-diff.version>4.16</java-diff.version>
</properties>
<repositories>
@@ -77,6 +78,14 @@
</repositories>
<dependencies>
<!-- 行级 diffMyers+ 生成 unified diff -->
<dependency>
<groupId>io.github.java-diff-utils</groupId>
<artifactId>java-diff-utils</artifactId>
<version>${java-diff.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
+3
View File
@@ -53,6 +53,9 @@ Windows • Linux • macOS
![Overview](screen_shoot/theme.png)
- 并排对比:![TextDiff-并排](/screen_shoot/textdiff_side_by_side.png)
- 统一差异:![TextDiff-统一差异](/screen_shoot/textdiff_unified.png)
## 下载
[https://github.com/rememberber/MooTool/releases](https://github.com/rememberber/MooTool/releases)
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

@@ -0,0 +1,12 @@
package com.luoboduner.moo.tool.bean.textdiff;
import com.luoboduner.moo.tool.enums.DiffTypeEnum;
/**
* 差异信息类,用于UI高亮显示
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 18:37
*/
public record DiffInfo(int lineNumber, String content, DiffTypeEnum type) {
}
@@ -0,0 +1,11 @@
package com.luoboduner.moo.tool.bean.textdiff;
import java.util.List;
/**
* 差异结果类
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 18:57
*/
public record DiffResult(List<DiffInfo> leftDiffs, List<DiffInfo> rightDiffs) {}
@@ -0,0 +1,11 @@
package com.luoboduner.moo.tool.bean.textdiff;
import com.luoboduner.moo.tool.enums.UnifiedSpanTypeEnum;
/**
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 18:54
*/
public record Span(int start, int end, UnifiedSpanTypeEnum type) {
}
@@ -0,0 +1,18 @@
package com.luoboduner.moo.tool.bean.textdiff;
import com.luoboduner.moo.tool.enums.DiffTypeEnum;
/**
* 用于 UI 的精细化段(按字符区间)
*
* @param leftStart -1 表示无
* @param leftEnd -1 表示无
* @param rightStart -1 表示无
* @param rightEnd -1 表示无
*
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 18:52
*/
public record TextDiffSegment(DiffTypeEnum type, int leftStart, int leftEnd, int rightStart, int rightEnd) {
}
@@ -0,0 +1,12 @@
package com.luoboduner.moo.tool.bean.textdiff;
import java.util.List;
/**
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 18:31
*/
public record UIDiff(List<TextDiffSegment> segments) {
}
@@ -0,0 +1,11 @@
package com.luoboduner.moo.tool.bean.textdiff;
import java.util.List;
/**
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 18:54
*/
public record UnifiedView(String text, List<Span> lineSpans, List<Span> charSpans, int charEventCount) {
}
@@ -0,0 +1,15 @@
package com.luoboduner.moo.tool.enums;
/**
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 18:42
*/
public enum DiffTypeEnum {
EQUAL, // 相同
INSERT, // 插入
DELETE, // 删除
CHANGE // 修改
}
@@ -0,0 +1,18 @@
package com.luoboduner.moo.tool.enums;
/**
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 18:55
*/
public enum UnifiedSpanTypeEnum {
ADD_LINE,
DEL_LINE,
HUNK_LINE,
HEADER_LINE,
ADD_CHAR,
DEL_CHAR,
CHANGE_CHAR
}
@@ -0,0 +1,344 @@
package com.luoboduner.moo.tool.service;
import com.github.difflib.DiffUtils;
import com.github.difflib.UnifiedDiffUtils;
import com.github.difflib.patch.AbstractDelta;
import com.github.difflib.patch.DeltaType;
import com.github.difflib.patch.Patch;
import com.luoboduner.moo.tool.bean.textdiff.Span;
import com.luoboduner.moo.tool.bean.textdiff.TextDiffSegment;
import com.luoboduner.moo.tool.bean.textdiff.UIDiff;
import com.luoboduner.moo.tool.bean.textdiff.UnifiedView;
import com.luoboduner.moo.tool.enums.DiffTypeEnum;
import com.luoboduner.moo.tool.enums.UnifiedSpanTypeEnum;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
* <pre>
* 差异服务
* 计算 diff 并生成 unified diff(行级)
* </pre>
*
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 10:51
*/
@Slf4j
public class DiffService {
/**
* 简单行切分器
*
* @param text 文本
* @return 行列表
*/
private static List<String> toLines(String text) {
// -1 保留末尾空格
return List.of(text.split("\\R", -1));
}
public static UIDiff getSegmentsForUI(String oldText, String newText, boolean ignoreWhitespace) {
return getSegmentsForUI(oldText, newText, ignoreWhitespace, false);
}
public static UIDiff getSegmentsForUI(String oldText, String newText, boolean ignoreWhitespace, boolean charOnly) {
List<String> oldLines = toLines(oldText);
List<String> newLines = toLines(newText);
Patch<String> patch = DiffUtils.diff(oldLines, newLines);
// 计算每一行的起始字符偏移(含换行)
int[] leftLineStarts = computeLineStartOffsets(oldText);
int[] rightLineStarts = computeLineStartOffsets(newText);
List<TextDiffSegment> textDiffSegments = new ArrayList<>();
for (AbstractDelta<String> delta : patch.getDeltas()) {
int leftPos = delta.getSource().getPosition();
int rightPos = delta.getTarget().getPosition();
List<String> leftLines = delta.getSource().getLines();
List<String> rightLines = delta.getTarget().getLines();
DeltaType type = delta.getType();
switch (type) {
case DELETE: {
if (charOnly) {
// 仅字符模式:整行删除不计入字符级差异
break;
}
for (int i = 0; i < leftLines.size(); i++) {
int lineIdx = leftPos + i;
int start = safeLineStart(leftLineStarts, lineIdx);
int end = start + leftLines.get(i).length();
if (!(ignoreWhitespace && isAllWhitespace(leftLines.get(i))))
textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.DELETE, start, end, -1, -1));
}
break;
}
case INSERT: {
if (charOnly) {
// 仅字符模式:整行插入不计入字符级差异
break;
}
for (int i = 0; i < rightLines.size(); i++) {
int lineIdx = rightPos + i;
int start = safeLineStart(rightLineStarts, lineIdx);
int end = start + rightLines.get(i).length();
if (!(ignoreWhitespace && isAllWhitespace(rightLines.get(i))))
textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.INSERT, -1, -1, start, end));
}
break;
}
case CHANGE: {
int pairCount = Math.min(leftLines.size(), rightLines.size());
// 对应行做字符级 diff
for (int i = 0; i < pairCount; i++) {
String a = leftLines.get(i);
String b = rightLines.get(i);
int aLineStart = safeLineStart(leftLineStarts, leftPos + i);
int bLineStart = safeLineStart(rightLineStarts, rightPos + i);
// 使用 DiffUtils 在字符层面做 diff
List<String> ac = toChars(a);
List<String> bc = toChars(b);
Patch<String> charPatch = DiffUtils.diff(ac, bc);
for (AbstractDelta<String> cDelta : charPatch.getDeltas()) {
int aStart = aLineStart + cDelta.getSource().getPosition();
int aEnd = aStart + cDelta.getSource().getLines().size();
int bStart = bLineStart + cDelta.getTarget().getPosition();
int bEnd = bStart + cDelta.getTarget().getLines().size();
if (cDelta.getType() == DeltaType.DELETE) {
if (!(ignoreWhitespace && isAllWhitespace(a.substring(
Math.min(a.length(), Math.max(0, cDelta.getSource().getPosition())),
Math.min(a.length(), Math.max(0, cDelta.getSource().getPosition() + cDelta.getSource().getLines().size()))
)))) {
textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.DELETE, aStart, aEnd, -1, -1));
}
} else if (cDelta.getType() == DeltaType.INSERT) {
if (!(ignoreWhitespace && isAllWhitespace(b.substring(
Math.min(b.length(), Math.max(0, cDelta.getTarget().getPosition())),
Math.min(b.length(), Math.max(0, cDelta.getTarget().getPosition() + cDelta.getTarget().getLines().size()))
)))) {
textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.INSERT, -1, -1, bStart, bEnd));
}
} else { // CHANGE
String aSub = a.substring(
Math.min(a.length(), Math.max(0, cDelta.getSource().getPosition())),
Math.min(a.length(), Math.max(0, cDelta.getSource().getPosition() + cDelta.getSource().getLines().size())));
String bSub = b.substring(
Math.min(b.length(), Math.max(0, cDelta.getTarget().getPosition())),
Math.min(b.length(), Math.max(0, cDelta.getTarget().getPosition() + cDelta.getTarget().getLines().size())));
if (!(ignoreWhitespace && equalsIgnoreWhitespace(aSub, bSub))) {
textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.CHANGE, aStart, aEnd, bStart, bEnd));
}
}
}
}
if (!charOnly) {
// 左侧多余:视为整行删除
for (int i = pairCount; i < leftLines.size(); i++) {
int aLineStart = safeLineStart(leftLineStarts, leftPos + i);
if (!(ignoreWhitespace && isAllWhitespace(leftLines.get(i))))
textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.DELETE, aLineStart, aLineStart + leftLines.get(i).length(), -1, -1));
}
// 右侧多余:视为整行插入
for (int i = pairCount; i < rightLines.size(); i++) {
int bLineStart = safeLineStart(rightLineStarts, rightPos + i);
if (!(ignoreWhitespace && isAllWhitespace(rightLines.get(i))))
textDiffSegments.add(new TextDiffSegment(DiffTypeEnum.INSERT, -1, -1, bLineStart, bLineStart + rightLines.get(i).length()));
}
}
break;
}
case EQUAL:
// 不产生片段
break;
}
}
return new UIDiff(textDiffSegments);
}
private static int[] computeLineStartOffsets(String text) {
int n = 1; // 至少一行
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '\n') n++;
}
int[] starts = new int[n];
int idx = 0;
starts[idx++] = 0;
for (int i = 0; i < text.length(); i++) {
if (text.charAt(i) == '\n') {
if (idx < n) starts[idx++] = i + 1;
}
}
return starts;
}
private static int safeLineStart(int[] starts, int lineIndex) {
if (lineIndex < 0) {
return 0;
}
if (lineIndex >= starts.length) {
return (starts.length == 0 ? 0 : starts[starts.length - 1]);
}
return starts[lineIndex];
}
public static List<String> toChars(String s) {
List<String> list = new ArrayList<>(s.length());
for (int i = 0; i < s.length(); i++) {
list.add(String.valueOf(s.charAt(i)));
}
return list;
}
public static boolean isAllWhitespace(String s) {
for (int i = 0; i < s.length(); i++) {
if (!Character.isWhitespace(s.charAt(i))) {
return false;
}
}
return true;
}
public static boolean equalsIgnoreWhitespace(String a, String b) {
int i = 0, j = 0;
while (i < a.length() && j < b.length()) {
char ca = a.charAt(i);
char cb = b.charAt(j);
if (Character.isWhitespace(ca)) {
i++;
continue;
}
if (Character.isWhitespace(cb)) {
j++;
continue;
}
if (ca != cb) return false;
i++;
j++;
}
while (i < a.length()) {
if (!Character.isWhitespace(a.charAt(i++))) return false;
}
while (j < b.length()) {
if (!Character.isWhitespace(b.charAt(j++))) return false;
}
return true;
}
/**
* 构建统一差异视图(包含文本与高亮区间)
*/
public static UnifiedView buildUnifiedView(String oldText, String newText, int context, boolean ignoreWhitespace) {
List<String> oldLines = toLines(oldText);
List<String> newLines = toLines(newText);
Patch<String> patch = DiffUtils.diff(oldLines, newLines);
List<String> unifiedLines = UnifiedDiffUtils.generateUnifiedDiff("old", "new", oldLines, patch, context);
String unifiedText = String.join("\n", unifiedLines);
List<Span> lineSpans = new ArrayList<>();
List<Span> charSpans = new ArrayList<>();
int charEvents = 0;
int offset = 0;
List<Integer> minusStarts = new ArrayList<>();
List<String> minusTexts = new ArrayList<>();
List<Integer> plusStarts = new ArrayList<>();
List<String> plusTexts = new ArrayList<>();
for (int i = 0; i < unifiedLines.size(); i++) {
String line = unifiedLines.get(i);
int lineStart = offset;
int lineLen = line.length();
int lineEnd = lineStart + lineLen;
if (line.startsWith("@@")) {
lineSpans.add(new Span(lineStart, lineEnd, UnifiedSpanTypeEnum.HUNK_LINE));
charEvents += addIntralineSpans(minusStarts, minusTexts, plusStarts, plusTexts, charSpans, ignoreWhitespace);
minusStarts.clear();
minusTexts.clear();
plusStarts.clear();
plusTexts.clear();
} else if (line.startsWith("+")) {
if (!(ignoreWhitespace && isAllWhitespace(line.substring(1)))) {
lineSpans.add(new Span(lineStart, lineEnd, UnifiedSpanTypeEnum.ADD_LINE));
}
plusStarts.add(lineStart);
plusTexts.add(line.substring(1));
} else if (line.startsWith("-")) {
if (!(ignoreWhitespace && isAllWhitespace(line.substring(1)))) {
lineSpans.add(new Span(lineStart, lineEnd, UnifiedSpanTypeEnum.DEL_LINE));
}
minusStarts.add(lineStart);
minusTexts.add(line.substring(1));
} else if (line.startsWith("---") || line.startsWith("+++")) {
lineSpans.add(new Span(lineStart, lineEnd, UnifiedSpanTypeEnum.HEADER_LINE));
}
offset = (i < unifiedLines.size() - 1) ? (lineEnd + 1) : lineEnd;
}
charEvents += addIntralineSpans(minusStarts, minusTexts, plusStarts, plusTexts, charSpans, ignoreWhitespace);
return new UnifiedView(unifiedText, lineSpans, charSpans, charEvents);
}
private static int addIntralineSpans(List<Integer> minusStarts, List<String> minusTexts,
List<Integer> plusStarts, List<String> plusTexts,
List<Span> out, boolean ignoreWhitespace) {
int events = 0;
int pairs = Math.min(minusTexts.size(), plusTexts.size());
for (int i = 0; i < pairs; i++) {
String a = minusTexts.get(i);
String b = plusTexts.get(i);
int aBase = minusStarts.get(i) + 1; // 跳过前缀符号
int bBase = plusStarts.get(i) + 1;
Patch<String> charPatch = DiffUtils.diff(toChars(a), toChars(b));
for (AbstractDelta<String> d : charPatch.getDeltas()) {
int aStart = aBase + d.getSource().getPosition();
int aEnd = aStart + d.getSource().getLines().size();
int bStart = bBase + d.getTarget().getPosition();
int bEnd = bStart + d.getTarget().getLines().size();
switch (d.getType()) {
case DELETE:
if (ignoreWhitespace && isAllWhitespace(safeSub(a, d.getSource().getPosition(), d.getSource().getLines().size())))
continue;
out.add(new Span(aStart, aEnd, UnifiedSpanTypeEnum.DEL_CHAR));
events++;
break;
case INSERT:
if (ignoreWhitespace && isAllWhitespace(safeSub(b, d.getTarget().getPosition(), d.getTarget().getLines().size())))
continue;
out.add(new Span(bStart, bEnd, UnifiedSpanTypeEnum.ADD_CHAR));
events++;
break;
case CHANGE:
String aSub = safeSub(a, d.getSource().getPosition(), d.getSource().getLines().size());
String bSub = safeSub(b, d.getTarget().getPosition(), d.getTarget().getLines().size());
if (ignoreWhitespace && equalsIgnoreWhitespace(aSub, bSub)) continue;
if (!aSub.isEmpty()) out.add(new Span(aStart, aEnd, UnifiedSpanTypeEnum.CHANGE_CHAR));
if (!bSub.isEmpty()) out.add(new Span(bStart, bEnd, UnifiedSpanTypeEnum.CHANGE_CHAR));
events++;
break;
case EQUAL:
break;
}
}
}
return events;
}
private static String safeSub(String s, int pos, int len) {
int start = Math.max(0, Math.min(pos, s.length()));
int end = Math.max(0, Math.min(pos + len, s.length()));
if (end < start) {
end = start;
}
return s.substring(start, end);
}
}
@@ -26,4 +26,9 @@ public class FuncConsts {
* 二维码
*/
public static final String QR_CODE = "QrCode";
/**
* 文本对比
*/
public static final String TEXT_DIFF = "TextDiff";
}
@@ -213,6 +213,7 @@ public class Init {
ThreadUtil.execute(ImageForm::init);
ThreadUtil.execute(VariablesForm::init);
ThreadUtil.execute(YmlPropertiesForm::init);
ThreadUtil.execute(TextDiffForm::init);
ThreadUtil.execute(FileReformattingForm::init);
// 检查新版版
@@ -210,6 +210,15 @@
<border type="none"/>
<children/>
</grid>
<grid id="d8a7c" binding="textDiffPanel" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<tabbedpane title="文本对比" icon="icon/find.png"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
</children>
</tabbedpane>
</children>
@@ -47,11 +47,12 @@ public class MainWindow {
private JPanel pdfPanel;
private JPanel variablesPanel;
private JPanel ymlProperties;
private JPanel textDiffPanel;
private JPanel aboutPanel;
private static MainWindow mainWindow;
private static final String[] ICON_PATH = {"icon/smile.svg", "icon/edit.svg", "icon/time.svg", "icon/json.svg", "icon/translate.svg", "icon/check.svg", "icon/global.svg", "icon/exchange.svg", "icon/QRcode.svg", "icon/method.svg", "icon/calculate.svg", "icon/network.svg", "icon/color.svg", "icon/image.svg", "icon/schedule.svg", "icon/reg.svg", "icon/java.svg", "icon/format_painter.svg", "icon/pdf.svg", "icon/fx.svg", "icon/suffix-yml.svg"};
private static final String[] ICON_PATH = {"icon/smile.svg", "icon/edit.svg", "icon/time.svg", "icon/json.svg", "icon/translate.svg", "icon/check.svg", "icon/global.svg", "icon/exchange.svg", "icon/QRcode.svg", "icon/method.svg", "icon/calculate.svg", "icon/network.svg", "icon/color.svg", "icon/image.svg", "icon/schedule.svg", "icon/reg.svg", "icon/java.svg", "icon/format_painter.svg", "icon/pdf.svg", "icon/fx.svg", "icon/suffix-yml.svg", "icon/find.svg"};
private MainWindow() {
}
@@ -96,6 +97,7 @@ public class MainWindow {
mainWindow.getPdfPanel().add(PdfForm.getInstance().getPdfPanel(), gridConstraints);
mainWindow.getVariablesPanel().add(VariablesForm.getInstance().getVariablesPanel(), gridConstraints);
mainWindow.getYmlProperties().add(YmlPropertiesForm.getInstance().getYmlPropertiesPanel(), gridConstraints);
mainWindow.getTextDiffPanel().add(TextDiffForm.getInstance().getTextDiffPanel(), gridConstraints);
mainWindow.getMainPanel().updateUI();
TabListener.addListeners();
@@ -285,6 +287,9 @@ public class MainWindow {
ymlProperties = new JPanel();
ymlProperties.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
tabbedPane.addTab("配置文件转换", ymlProperties);
textDiffPanel = new JPanel();
textDiffPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
tabbedPane.addTab("文本差异比对", textDiffPanel);
}
/**
@@ -0,0 +1,966 @@
package com.luoboduner.moo.tool.ui.form.func;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.formdev.flatlaf.extras.FlatSVGIcon;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.intellij.uiDesigner.core.Spacer;
import com.luoboduner.moo.tool.bean.textdiff.UIDiff;
import com.luoboduner.moo.tool.bean.textdiff.UnifiedView;
import com.luoboduner.moo.tool.dao.TFuncContentMapper;
import com.luoboduner.moo.tool.domain.TFuncContent;
import com.luoboduner.moo.tool.service.DiffService;
import com.luoboduner.moo.tool.ui.FuncConsts;
import com.luoboduner.moo.tool.ui.listener.func.TextDiffListener;
import com.luoboduner.moo.tool.util.MybatisUtil;
import com.luoboduner.moo.tool.util.ScrollUtil;
import com.luoboduner.moo.tool.util.SqliteUtil;
import com.luoboduner.moo.tool.util.UndoUtil;
import lombok.Getter;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rtextarea.RTextScrollPane;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.border.TitledBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.Highlighter;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* <pre>
* TextDiffForm - 文本对比功能界面
* </pre>
*
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 10:51
*/
@Getter
public class TextDiffForm {
private JPanel textDiffPanel;
private JPanel leftPanel;
private JPanel rightPanel;
private JPanel toolBarPanel;
private JButton compareButton;
private JButton clearButton;
private JButton swapButton;
private JButton copyDiffButton;
private JButton prevDiffButton;
private JButton nextDiffButton;
private JComboBox<String> highlightModeComboBox;
private JLabel statusLabel;
private JCheckBox realTimeCheckBox;
private JCheckBox ignoreWhitespaceCheckBox;
private JComboBox<String> displayModeComboBox;
private JSplitPane mainSplitPane;
private JScrollPane leftScrollPane;
private JScrollPane rightScrollPane;
private JScrollPane unifiedScrollPane;
private JPanel unifiedPanel;
private RSyntaxTextArea leftTextArea;
private RSyntaxTextArea rightTextArea;
private RSyntaxTextArea unifiedTextArea;
// Diff highlight painters
private final DefaultHighlighter.DefaultHighlightPainter delPainter =
new DefaultHighlighter.DefaultHighlightPainter(new Color(255, 204, 204)); // light red for deletions
private final DefaultHighlighter.DefaultHighlightPainter insPainter =
new DefaultHighlighter.DefaultHighlightPainter(new Color(204, 255, 204)); // light green for insertions
private final DefaultHighlighter.DefaultHighlightPainter changePainter =
new DefaultHighlighter.DefaultHighlightPainter(new Color(255, 249, 196)); // light yellow for modifications
// 滚动同步状态标记,避免循环触发
private boolean syncingScroll = false;
// 行级高亮(带透明度)
private final DefaultHighlighter.DefaultHighlightPainter delLinePainter =
new DefaultHighlighter.DefaultHighlightPainter(new Color(255, 102, 102, 60));
private final DefaultHighlighter.DefaultHighlightPainter insLinePainter =
new DefaultHighlighter.DefaultHighlightPainter(new Color(0, 153, 0, 60));
private final DefaultHighlighter.DefaultHighlightPainter changeLinePainter =
new DefaultHighlighter.DefaultHighlightPainter(new Color(255, 193, 7, 60));
// 统一视图:hunk 或标题行高亮
private final DefaultHighlighter.DefaultHighlightPainter headerLinePainter =
new DefaultHighlighter.DefaultHighlightPainter(new Color(230, 230, 230));
// 差异导航锚点与索引(按左/右侧段的中点行)
private final List<Integer> leftNavOffsets = new ArrayList<>();
private final List<Integer> rightNavOffsets = new ArrayList<>();
private int navIndex = -1;
private enum HighlightMode { CHAR_ONLY, LINE_ONLY, BOTH }
private HighlightMode highlightMode = HighlightMode.BOTH;
private static TextDiffForm textDiffForm;
private static final Log logger = LogFactory.get();
private static final TFuncContentMapper funcContentMapper = MybatisUtil.getSqlSession().getMapper(TFuncContentMapper.class);
private TextDiffForm() {
UndoUtil.register(this);
initTextAreas();
}
public static TextDiffForm getInstance() {
if (textDiffForm == null) {
textDiffForm = new TextDiffForm();
}
return textDiffForm;
}
public static void init() {
textDiffForm = getInstance();
textDiffForm.initForm();
}
/**
* 初始化文本区域
*/
private void initTextAreas() {
// 左侧文本区域
leftTextArea = new RSyntaxTextArea();
leftTextArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE);
leftTextArea.setCodeFoldingEnabled(true);
leftTextArea.setAntiAliasingEnabled(true);
leftTextArea.setAutoIndentEnabled(true);
leftTextArea.setMarkOccurrences(true);
leftTextArea.setPaintMarkOccurrencesBorder(true);
leftTextArea.setTabSize(4);
leftTextArea.setCaretPosition(0);
// 右侧文本区域
rightTextArea = new RSyntaxTextArea();
rightTextArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE);
rightTextArea.setCodeFoldingEnabled(true);
rightTextArea.setAntiAliasingEnabled(true);
rightTextArea.setAutoIndentEnabled(true);
rightTextArea.setMarkOccurrences(true);
rightTextArea.setPaintMarkOccurrencesBorder(true);
rightTextArea.setTabSize(4);
rightTextArea.setCaretPosition(0);
// 统一diff显示区域
unifiedTextArea = new RSyntaxTextArea();
unifiedTextArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
unifiedTextArea.setCodeFoldingEnabled(true);
unifiedTextArea.setAntiAliasingEnabled(true);
unifiedTextArea.setEditable(false);
unifiedTextArea.setTabSize(4);
unifiedTextArea.setCaretPosition(0);
// 创建滚动面板
leftScrollPane = new RTextScrollPane(leftTextArea);
rightScrollPane = new RTextScrollPane(rightTextArea);
unifiedScrollPane = new RTextScrollPane(unifiedTextArea);
ScrollUtil.smoothPane(leftScrollPane);
ScrollUtil.smoothPane(rightScrollPane);
ScrollUtil.smoothPane(unifiedScrollPane);
// 同步左右滚动(便于并排对比时直观比照)
setupScrollSync();
// 将滚动面板添加到相应的面板中
if (leftPanel != null) {
leftPanel.add(
leftScrollPane,
new GridConstraints(
0,
0,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_BOTH,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
null,
null,
null,
0,
false
)
);
}
if (rightPanel != null) {
rightPanel.add(
rightScrollPane,
new GridConstraints(
0,
0,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_BOTH,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
null,
null,
null,
0,
false
)
);
}
if (unifiedPanel != null) {
unifiedPanel.add(unifiedScrollPane,
new GridConstraints(0,
0,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_BOTH,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
null,
null,
null,
0,
false
)
);
}
}
/**
* 左右窗口竖向滚动同步
*/
private void setupScrollSync() {
if (!(leftScrollPane instanceof RTextScrollPane) || !(rightScrollPane instanceof RTextScrollPane)) {
return;
}
// 获取滚动条
JScrollBar leftBar = leftScrollPane.getVerticalScrollBar();
JScrollBar rightBar = rightScrollPane.getVerticalScrollBar();
if (leftBar == null || rightBar == null) {
return;
}
// 滚动条滚动时,同步滚动
leftBar.addAdjustmentListener(e -> {
if (syncingScroll) return;
syncingScroll = true;
rightBar.setValue(e.getValue());
syncingScroll = false;
});
rightBar.addAdjustmentListener(e -> {
if (syncingScroll) return;
syncingScroll = true;
leftBar.setValue(e.getValue());
syncingScroll = false;
});
}
/**
* 初始化
*/
public void initForm() {
TextDiffListener.addListeners();
// 设置默认显示模式
displayModeComboBox.addItem("并排对比");
displayModeComboBox.addItem("统一差异");
displayModeComboBox.setSelectedIndex(0);
// 高亮模式(默认仅字符,避免把相同部分也高亮)
highlightModeComboBox.addItem("双层高亮");
highlightModeComboBox.addItem("仅字符");
highlightModeComboBox.addItem("仅整行");
highlightModeComboBox.setSelectedIndex(1);
highlightModeComboBox.addActionListener(e -> {
switch ((String) Objects.requireNonNull(highlightModeComboBox.getSelectedItem())) {
case "仅字符":
highlightMode = HighlightMode.CHAR_ONLY; break;
case "仅整行":
highlightMode = HighlightMode.LINE_ONLY; break;
default:
highlightMode = HighlightMode.BOTH;
}
if (realTimeCheckBox.isSelected()) {
performDiff();
}
});
// 默认开启实时对比
realTimeCheckBox.setSelected(true);
// 初始化布局
updateDisplayMode();
// 恢复上次的内容
initContent();
}
/**
* 初始化内容
*/
private void initContent() {
try {
TFuncContent tFuncContent = funcContentMapper.selectByFunc(FuncConsts.TEXT_DIFF + "_left");
if (tFuncContent != null) {
leftTextArea.setText(tFuncContent.getContent());
}
tFuncContent = funcContentMapper.selectByFunc(FuncConsts.TEXT_DIFF + "_right");
if (tFuncContent != null) {
rightTextArea.setText(tFuncContent.getContent());
}
} catch (Exception e) {
logger.error("初始化文本对比内容失败", e);
}
}
/**
* 保存内容
*/
public void save() {
try {
saveLeftContent();
saveRightContent();
} catch (Exception e) {
logger.error("保存文本对比内容失败", e);
}
}
/**
* 保存左侧内容
*/
private void saveLeftContent() {
String text = leftTextArea.getText();
String now = SqliteUtil.nowDateForSqlite();
String funcKey = FuncConsts.TEXT_DIFF + "_left";
saveContent(text, now, funcKey);
}
/**
* 保存内容
* @param text 文本
* @param now 时间
* @param funcKey 函数
*/
private void saveContent(String text, String now, String funcKey) {
TFuncContent tFuncContent = funcContentMapper.selectByFunc(funcKey);
if (tFuncContent == null) {
tFuncContent = new TFuncContent();
tFuncContent.setFunc(funcKey);
tFuncContent.setContent(text);
tFuncContent.setCreateTime(now);
tFuncContent.setModifiedTime(now);
funcContentMapper.insert(tFuncContent);
} else {
tFuncContent.setContent(text);
tFuncContent.setModifiedTime(now);
funcContentMapper.updateByPrimaryKeySelective(tFuncContent);
}
}
/**
* 保存右侧内容
*/
private void saveRightContent() {
String text = rightTextArea.getText();
String now = SqliteUtil.nowDateForSqlite();
String funcKey = FuncConsts.TEXT_DIFF + "_right";
saveContent(text, now, funcKey);
}
/**
* 更新显示模式
*/
public void updateDisplayMode() {
String selectedMode = (String) displayModeComboBox.getSelectedItem();
if ("并排对比".equals(selectedMode)) {
// 显示并排对比模式
mainSplitPane.setLeftComponent(leftScrollPane);
mainSplitPane.setRightComponent(rightScrollPane);
mainSplitPane.setDividerLocation(0.5);
unifiedPanel.setVisible(false);
} else {
// 显示统一差异模式
mainSplitPane.setLeftComponent(leftScrollPane);
mainSplitPane.setRightComponent(rightScrollPane);
mainSplitPane.setDividerLocation(0.5);
unifiedPanel.setVisible(true);
}
textDiffPanel.revalidate();
textDiffPanel.repaint();
}
/**
* 执行文本对比
*/
public void performDiff() {
String leftText = leftTextArea.getText();
String rightText = rightTextArea.getText();
if (leftText.isEmpty() && rightText.isEmpty()) {
statusLabel.setText("请输入要对比的文本");
return;
}
try {
// 统一差异视图(文本 + 高亮区间)
boolean ignoreWs = ignoreWhitespaceCheckBox != null && ignoreWhitespaceCheckBox.isSelected();
var unifiedView = DiffService.buildUnifiedView(leftText, rightText, 3, ignoreWs);
unifiedTextArea.setText(unifiedView.text());
// 获取用于UI高亮的差异信息(基于字符区间)
boolean charOnlyNav = (highlightMode == HighlightMode.CHAR_ONLY);
var diffResult = DiffService.getSegmentsForUI(leftText, rightText, ignoreWs, charOnlyNav);
// 构建导航锚点:每个差异段恰好追加一项(左/右没有则为 -1)
leftNavOffsets.clear();
rightNavOffsets.clear();
navIndex = -1;
if (diffResult.segments() != null) {
for (var seg : diffResult.segments()) {
int lMid = (seg.leftStart() >= 0 && seg.leftEnd() >= 0)
? (seg.leftStart() + seg.leftEnd()) / 2 : -1;
int rMid = (seg.rightStart() >= 0 && seg.rightEnd() >= 0)
? (seg.rightStart() + seg.rightEnd()) / 2 : -1;
leftNavOffsets.add(lMid);
rightNavOffsets.add(rMid);
}
}
// 左右并排的字符/行高亮
applyDiffHighlight(diffResult);
// 统一差异视图的行级/字符级高亮
applyUnifiedSpans(unifiedView);
// 更新状态:与可跳转点一致
int changes = (diffResult.segments() != null) ? diffResult.segments().size() : 0;
if (highlightMode == HighlightMode.CHAR_ONLY) {
statusLabel.setText("字符差异 " + changes + "");
} else {
statusLabel.setText("对比完成,共发现 " + changes + " 处差异");
}
} catch (Exception e) {
logger.error("执行文本对比失败", e);
statusLabel.setText("对比失败:" + e.getMessage());
}
}
/**
* 清除两侧与统一视图上的所有高亮
*/
private void clearHighlights() {
if (leftTextArea != null && leftTextArea.getHighlighter() != null) {
leftTextArea.getHighlighter().removeAllHighlights();
}
if (rightTextArea != null && rightTextArea.getHighlighter() != null) {
rightTextArea.getHighlighter().removeAllHighlights();
}
if (unifiedTextArea != null && unifiedTextArea.getHighlighter() != null) {
unifiedTextArea.getHighlighter().removeAllHighlights();
}
}
/**
* 应用差异高亮显示
*/
private void applyDiffHighlight(UIDiff diffResult) {
clearHighlights();
if (diffResult == null || diffResult.segments() == null) {
return;
}
// 删除、插入、修改
for (var seg : diffResult.segments()) {
switch (seg.type()) {
case DELETE: {
if (highlightMode != HighlightMode.LINE_ONLY && seg.leftStart() >= 0 && seg.leftEnd() >= 0)
safeAddHighlight(leftTextArea, seg.leftStart(), seg.leftEnd(), delPainter);
if (highlightMode != HighlightMode.CHAR_ONLY && seg.leftStart() >= 0 && seg.leftEnd() >= 0)
safeAddLineHighlights(leftTextArea, seg.leftStart(), seg.leftEnd(), delLinePainter);
break;
}
case INSERT: {
if (highlightMode != HighlightMode.LINE_ONLY && seg.rightStart() >= 0 && seg.rightEnd() >= 0)
safeAddHighlight(rightTextArea, seg.rightStart(), seg.rightEnd(), insPainter);
if (highlightMode != HighlightMode.CHAR_ONLY && seg.rightStart() >= 0 && seg.rightEnd() >= 0)
safeAddLineHighlights(rightTextArea, seg.rightStart(), seg.rightEnd(), insLinePainter);
break;
}
case CHANGE: {
if (highlightMode != HighlightMode.LINE_ONLY) {
if (seg.leftStart() >= 0 && seg.leftEnd() >= 0)
safeAddHighlight(leftTextArea, seg.leftStart(), seg.leftEnd(), changePainter);
if (seg.rightStart() >= 0 && seg.rightEnd() >= 0)
safeAddHighlight(rightTextArea, seg.rightStart(), seg.rightEnd(), changePainter);
}
if (highlightMode != HighlightMode.CHAR_ONLY) {
if (seg.leftStart() >= 0 && seg.leftEnd() >= 0)
safeAddLineHighlights(leftTextArea, seg.leftStart(), seg.leftEnd(), changeLinePainter);
if (seg.rightStart() >= 0 && seg.rightEnd() >= 0)
safeAddLineHighlights(rightTextArea, seg.rightStart(), seg.rightEnd(), changeLinePainter);
}
break;
}
default:
break;
}
}
}
/**
* 在给定 TextArea 上安全添加高亮(自动裁剪区间并捕获异常)
*/
private void safeAddHighlight(RSyntaxTextArea ta, int start, int end, Highlighter.HighlightPainter painter) {
if (ta == null || painter == null) {
return;
}
try {
int docLen = ta.getDocument().getLength();
int s = Math.max(0, Math.min(start, docLen));
int e = Math.max(0, Math.min(end, docLen));
if (e > s) {
ta.getHighlighter().addHighlight(s, e, painter);
}
} catch (BadLocationException ex) {
logger.warn("添加高亮失败: {}", ex.getMessage());
}
}
/**
* 以行为单位添加高亮:把 [start, end) 覆盖到完整的行区间并逐行高亮
*/
private void safeAddLineHighlights(RSyntaxTextArea ta, int start, int end, Highlighter.HighlightPainter painter) {
if (ta == null || painter == null) return;
try {
int docLen = ta.getDocument().getLength();
int s = Math.max(0, Math.min(start, docLen));
int e = Math.max(0, Math.min(end, docLen));
if (e <= s) return;
int startLine = ta.getLineOfOffset(s);
int endLine = ta.getLineOfOffset(Math.max(s, e - 1));
for (int line = startLine; line <= endLine; line++) {
int ls = ta.getLineStartOffset(line);
int le = ta.getLineEndOffset(line);
// 为整行添加一层半透明底色,便于两侧比照
ta.getHighlighter().addHighlight(ls, le, painter);
}
} catch (BadLocationException ex) {
logger.warn("添加行级高亮失败: {}", ex.getMessage());
}
}
private void gotoDiff(int index) {
if (leftNavOffsets.isEmpty() && rightNavOffsets.isEmpty()) {
return;
}
int max = Math.max(leftNavOffsets.size(), rightNavOffsets.size());
navIndex = ((index % max) + max) % max; // 环绕
int lOff = (navIndex < leftNavOffsets.size()) ? leftNavOffsets.get(navIndex) : -1;
int rOff = (navIndex < rightNavOffsets.size()) ? rightNavOffsets.get(navIndex) : -1;
if (lOff >= 0) {
centerOnOffset(leftTextArea, lOff);
}
if (rOff >= 0) {
centerOnOffset(rightTextArea, rOff);
}
statusLabel.setText("跳至第 " + (navIndex + 1) + "/" + max + " 处差异");
}
private void centerOnOffset(RSyntaxTextArea ta, int offset) {
try {
int line = ta.getLineOfOffset(Math.max(0, Math.min(offset, ta.getDocument().getLength())));
int start = ta.getLineStartOffset(line);
ta.setCaretPosition(start);
ta.requestFocusInWindow();
} catch (BadLocationException ignore) { }
}
private void gotoNextDiff() { gotoDiff(navIndex + 1); }
private void gotoPrevDiff() { gotoDiff(navIndex - 1); }
/**
* 统一差异视图高亮:
* - 行级:+ 绿色, - 红色, @@ hunk 行黄色, 文件头灰色
* - 字符级:对同一 hunk 中的 +/- 行按序配对做字符级 diff,仅高亮变更范围
* 渲染统一差异视图的高亮(使用服务层返回的区间)
*/
private void applyUnifiedSpans(UnifiedView view) {
if (view == null) return;
// 行级底色
if (view.lineSpans() != null) {
for (var sp : view.lineSpans()) {
switch (sp.type()) {
case ADD_LINE -> safeAddLineHighlights(unifiedTextArea, sp.start(), sp.end(), insLinePainter);
case DEL_LINE -> safeAddLineHighlights(unifiedTextArea, sp.start(), sp.end(), delLinePainter);
case HUNK_LINE -> safeAddLineHighlights(unifiedTextArea, sp.start(), sp.end(), changeLinePainter);
case HEADER_LINE -> safeAddLineHighlights(unifiedTextArea, sp.start(), sp.end(), headerLinePainter);
default -> {}
}
}
}
// 字符级高亮
if (view.charSpans() != null && highlightMode != HighlightMode.LINE_ONLY) {
for (var sp : view.charSpans()) {
switch (sp.type()) {
case ADD_CHAR -> safeAddHighlight(unifiedTextArea, sp.start(), sp.end(), insPainter);
case DEL_CHAR -> safeAddHighlight(unifiedTextArea, sp.start(), sp.end(), delPainter);
case CHANGE_CHAR -> safeAddHighlight(unifiedTextArea, sp.start(), sp.end(), changePainter);
default -> {}
}
}
}
}
/**
* 清空所有文本
*/
public void clearAll() {
clearHighlights();
leftTextArea.setText("");
rightTextArea.setText("");
unifiedTextArea.setText("");
statusLabel.setText("已清空");
}
/**
* 交换左右文本
*/
public void swapTexts() {
String leftText = leftTextArea.getText();
String rightText = rightTextArea.getText();
leftTextArea.setText(rightText);
rightTextArea.setText(leftText);
// 交换后也做一次对比,确保高亮与内容同步
performDiff();
statusLabel.setText("已交换文本");
}
/**
* 复制差异结果
*/
public void copyDiffResult() {
String diffText = unifiedTextArea.getText();
if (!diffText.isEmpty()) {
Toolkit.getDefaultToolkit().getSystemClipboard()
.setContents(new java.awt.datatransfer.StringSelection(diffText), null);
statusLabel.setText("差异结果已复制到剪贴板");
} else {
statusLabel.setText("没有差异结果可复制");
}
}
{
// GUI initializer generated by IntelliJ IDEA GUI Designer
// >>> IMPORTANT!! <<<
// DO NOT EDIT OR ADD ANY CODE HERE!
$$$setupUI$$$();
prevDiffButton.addActionListener(e -> gotoPrevDiff());
nextDiffButton.addActionListener(e -> gotoNextDiff());
}
/**
* Method generated by IntelliJ IDEA GUI Designer
* >>> IMPORTANT!! <<<
* DO NOT edit this method OR call it in your code!
*
* @noinspection ALL
*/
private void $$$setupUI$$$() {
textDiffPanel = new JPanel();
textDiffPanel.setLayout(new GridLayoutManager(3, 1, new Insets(10, 10, 10, 10), -1, -1));
// 工具栏面板
toolBarPanel = new JPanel();
toolBarPanel.setLayout(new GridLayoutManager(1, 12, new Insets(0, 0, 10, 0), -1, -1));
textDiffPanel.add(toolBarPanel, new GridConstraints(0,
0,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_BOTH,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
compareButton = new JButton();
compareButton.setText("对比");
compareButton.setIcon(new FlatSVGIcon("icon/find.svg"));
toolBarPanel.add(compareButton, new GridConstraints(0,
0,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
clearButton = new JButton();
clearButton.setText("清空");
clearButton.setIcon(new FlatSVGIcon("icon/remove.svg"));
toolBarPanel.add(clearButton, new GridConstraints(0,
1,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
swapButton = new JButton();
swapButton.setText("交换");
swapButton.setIcon(new FlatSVGIcon("icon/exchange.svg"));
toolBarPanel.add(swapButton, new GridConstraints(0,
2,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
copyDiffButton = new JButton();
copyDiffButton.setText("复制差异");
copyDiffButton.setIcon(new FlatSVGIcon("icon/copy.svg"));
toolBarPanel.add(copyDiffButton, new GridConstraints(0,
3,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
prevDiffButton = new JButton();
prevDiffButton.setText("上一处");
toolBarPanel.add(prevDiffButton, new GridConstraints(0,
4,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
nextDiffButton = new JButton();
nextDiffButton.setText("下一处");
toolBarPanel.add(nextDiffButton,
new GridConstraints(0,
5,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
realTimeCheckBox = new JCheckBox();
realTimeCheckBox.setText("实时对比");
toolBarPanel.add(realTimeCheckBox, new GridConstraints(0,
6,
1,
1,
GridConstraints.ANCHOR_WEST,
GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
ignoreWhitespaceCheckBox = new JCheckBox();
ignoreWhitespaceCheckBox.setText("忽略空白");
toolBarPanel.add(ignoreWhitespaceCheckBox, new GridConstraints(0,
7,
1,
1,
GridConstraints.ANCHOR_WEST,
GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
highlightModeComboBox = new JComboBox<>();
toolBarPanel.add(highlightModeComboBox, new GridConstraints(0,
8,
1,
1,
GridConstraints.ANCHOR_WEST,
GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
displayModeComboBox = new JComboBox<>();
toolBarPanel.add(displayModeComboBox, new GridConstraints(0,
9,
1,
1,
GridConstraints.ANCHOR_WEST,
GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
final Spacer spacer1 = new Spacer();
toolBarPanel.add(spacer1, new GridConstraints(0,
10,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_HORIZONTAL,
GridConstraints.SIZEPOLICY_WANT_GROW,
1,
null,
null,
null,
0,
false));
statusLabel = new JLabel();
statusLabel.setText("准备就绪");
toolBarPanel.add(statusLabel, new GridConstraints(0,
11,
1,
1,
GridConstraints.ANCHOR_WEST,
GridConstraints.FILL_NONE,
GridConstraints.SIZEPOLICY_FIXED,
GridConstraints.SIZEPOLICY_FIXED,
null,
null,
null,
0,
false));
// 主分割面板
mainSplitPane = new JSplitPane();
mainSplitPane.setDividerLocation(400);
mainSplitPane.setResizeWeight(0.5);
textDiffPanel.add(mainSplitPane, new GridConstraints(1,
0,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_BOTH,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
null,
new Dimension(200, 200),
null,
0,
false));
// 左侧面板
leftPanel = new JPanel();
leftPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 5), -1, -1));
leftPanel.setBorder(BorderFactory.createTitledBorder(null, "原文本", TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null));
// 右侧面板
rightPanel = new JPanel();
rightPanel.setLayout(new GridLayoutManager(1, 1, new Insets(0, 5, 0, 0), -1, -1));
rightPanel.setBorder(BorderFactory.createTitledBorder(null, "新文本", TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null));
// 设置分割面板的左右组件
mainSplitPane.setLeftComponent(leftPanel);
mainSplitPane.setRightComponent(rightPanel);
// 统一差异面板
unifiedPanel = new JPanel();
unifiedPanel.setLayout(new GridLayoutManager(1, 1, new Insets(10, 0, 0, 0), -1, -1));
unifiedPanel.setBorder(BorderFactory.createTitledBorder(null, "统一差异格式", TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null));
unifiedPanel.setVisible(false);
textDiffPanel.add(unifiedPanel, new GridConstraints(2,
0,
1,
1,
GridConstraints.ANCHOR_CENTER,
GridConstraints.FILL_BOTH,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
null,
new Dimension(-1, 200),
null,
0,
false));
}
/**
* @noinspection ALL
*/
public JComponent $$$getRootComponent$$$() {
return textDiffPanel;
}
}
@@ -1094,76 +1094,94 @@ public class QuickNoteListener {
public static void format() {
try {
QuickNoteForm quickNoteForm = QuickNoteForm.getInstance();
String text = QuickNoteForm.quickNoteRSyntaxTextViewerManager.getTextByName(selectedName);
String format;
String selectedSyntax = (String) quickNoteForm.getSyntaxComboBox().getSelectedItem();
final String text = QuickNoteForm.quickNoteRSyntaxTextViewerManager.getTextByName(selectedName);
final String selectedSyntax = (String) quickNoteForm.getSyntaxComboBox().getSelectedItem();
if (StringUtils.isBlank(text) || StringUtils.isEmpty(selectedSyntax)) {
return;
}
switch ("text/" + selectedSyntax) {
case SyntaxConstants.SYNTAX_STYLE_SQL:
switch (App.config.getSqlDialect()) {
case "MariaDB":
format = SqlFormatter.of(Dialect.MariaDb).format(text, " ");
executorService.submit(() -> {
try {
String formatted;
switch ("text/" + selectedSyntax) {
case SyntaxConstants.SYNTAX_STYLE_SQL:
switch (App.config.getSqlDialect()) {
case "MariaDB":
formatted = SqlFormatter.of(Dialect.MariaDb).format(text, " ");
break;
case "MySQL":
formatted = SqlFormatter.of(Dialect.MySql).format(text, " ");
break;
case "PostgreSQL":
formatted = SqlFormatter.of(Dialect.PostgreSql).format(text, " ");
break;
case "IBM DB2":
formatted = SqlFormatter.of(Dialect.Db2).format(text, " ");
break;
case "Oracle PL/SQL":
formatted = SqlFormatter.of(Dialect.PlSql).format(text, " ");
break;
case "Couchbase N1QL":
formatted = SqlFormatter.of(Dialect.N1ql).format(text, " ");
break;
case "Amazon Redshift":
formatted = SqlFormatter.of(Dialect.Redshift).format(text, " ");
break;
case "Spark":
formatted = SqlFormatter.of(Dialect.SparkSql).format(text, " ");
break;
case "SQL Server Transact-SQL":
formatted = SqlFormatter.of(Dialect.TSql).format(text, " ");
break;
default:
formatted = SqlFormatter.of(Dialect.StandardSql).format(text, " ");
}
break;
case "MySQL":
format = SqlFormatter.of(Dialect.MySql).format(text, " ");
case SyntaxConstants.SYNTAX_STYLE_JSON:
case SyntaxConstants.SYNTAX_STYLE_JSON_WITH_COMMENTS:
try {
formatted = JSONUtil.toJsonPrettyStr(text);
} catch (Exception e1) {
log.error(ExceptionUtils.getStackTrace(e1));
formatted = JSONUtil.formatJsonStr(text);
}
break;
case "PostgreSQL":
format = SqlFormatter.of(Dialect.PostgreSql).format(text, " ");
case SyntaxConstants.SYNTAX_STYLE_XML:
formatted = CodeFormatterFactory.getFormatter(CodeFormatterFactory.FormatterType.XML).format(text);
break;
case "IBM DB2":
format = SqlFormatter.of(Dialect.Db2).format(text, " ");
break;
case "Oracle PL/SQL":
format = SqlFormatter.of(Dialect.PlSql).format(text, " ");
break;
case "Couchbase N1QL":
format = SqlFormatter.of(Dialect.N1ql).format(text, " ");
break;
case "Amazon Redshift":
format = SqlFormatter.of(Dialect.Redshift).format(text, " ");
break;
case "Spark":
format = SqlFormatter.of(Dialect.SparkSql).format(text, " ");
break;
case "SQL Server Transact-SQL":
format = SqlFormatter.of(Dialect.TSql).format(text, " ");
case SyntaxConstants.SYNTAX_STYLE_JAVA:
formatted = CodeFormatterFactory.getFormatter(CodeFormatterFactory.FormatterType.JAVA).format(text);
break;
default:
format = SqlFormatter.of(Dialect.StandardSql).format(text, " ");
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(App.mainFrame, "尚不支持对该语言格式化!\n", "不支持该语言",
JOptionPane.INFORMATION_MESSAGE));
return;
}
break;
case SyntaxConstants.SYNTAX_STYLE_JSON:
case SyntaxConstants.SYNTAX_STYLE_JSON_WITH_COMMENTS:
try {
format = JSONUtil.toJsonPrettyStr(text);
} catch (Exception e1) {
log.error(ExceptionUtils.getStackTrace(e1));
format = JSONUtil.formatJsonStr(text);
}
break;
case SyntaxConstants.SYNTAX_STYLE_JAVA:
format = CodeFormatterFactory.getFormatter(CodeFormatterFactory.FormatterType.JAVA).format(text);
break;
default:
JOptionPane.showMessageDialog(App.mainFrame, "尚不支持对该语言格式化!\n", "不支持该语言",
JOptionPane.INFORMATION_MESSAGE);
return;
}
QuickNoteForm.quickNoteRSyntaxTextViewerManager.getCurrentRSyntaxTextArea().setText(format);
QuickNoteForm.quickNoteRSyntaxTextViewerManager.getCurrentRSyntaxTextArea().setCaretPosition(0);
QuickNoteIndicatorTools.showTips("已格式化:" + selectedName, QuickNoteIndicatorTools.TipsLevel.SUCCESS);
final String result = formatted;
SwingUtilities.invokeLater(() -> {
try {
QuickNoteForm.quickNoteRSyntaxTextViewerManager.getCurrentRSyntaxTextArea().setText(result);
QuickNoteForm.quickNoteRSyntaxTextViewerManager.getCurrentRSyntaxTextArea().setCaretPosition(0);
QuickNoteIndicatorTools.showTips("已格式化:" + selectedName, QuickNoteIndicatorTools.TipsLevel.SUCCESS);
} catch (Exception ex) {
JOptionPane.showMessageDialog(App.mainFrame, "格式化失败!\n\n" + ex.getMessage(), "失败",
JOptionPane.ERROR_MESSAGE);
log.error(ExceptionUtils.getStackTrace(ex));
}
});
} catch (Exception e) {
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(App.mainFrame, "格式化失败!\n\n" + e.getMessage(), "失败",
JOptionPane.ERROR_MESSAGE));
log.error(ExceptionUtils.getStackTrace(e));
}
});
} catch (Exception e) {
JOptionPane.showMessageDialog(App.mainFrame, "格式化失败!\n\n" + e.getMessage(), "失败",
JOptionPane.ERROR_MESSAGE);
log.error(ExceptionUtils.getStackTrace(e));
}
}
}
@@ -0,0 +1,133 @@
package com.luoboduner.moo.tool.ui.listener.func;
import com.luoboduner.moo.tool.ui.form.func.TextDiffForm;
import lombok.extern.slf4j.Slf4j;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
/**
* <pre>
* TextDiffListener - 文本对比功能事件监听器
* </pre>
*
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 10:51
*/
@Slf4j
public class TextDiffListener {
/**
* 添加事件监听器
*/
public static void addListeners() {
TextDiffForm textDiffForm = TextDiffForm.getInstance();
// 对比按钮事件
textDiffForm.getCompareButton().addActionListener(e -> {
textDiffForm.performDiff();
});
// 清空按钮事件
textDiffForm.getClearButton().addActionListener(e -> {
textDiffForm.clearAll();
});
// 交换按钮事件
textDiffForm.getSwapButton().addActionListener(e -> {
textDiffForm.swapTexts();
});
// 复制差异按钮事件
textDiffForm.getCopyDiffButton().addActionListener(e -> {
textDiffForm.copyDiffResult();
});
// 显示模式切换事件
textDiffForm.getDisplayModeComboBox().addActionListener(e -> {
textDiffForm.updateDisplayMode();
// 如果当前有内容,重新执行对比
if (!textDiffForm.getLeftTextArea().getText().isEmpty() ||
!textDiffForm.getRightTextArea().getText().isEmpty()) {
textDiffForm.performDiff();
}
});
// 实时对比复选框事件
textDiffForm.getRealTimeCheckBox().addActionListener(e -> {
if (textDiffForm.getRealTimeCheckBox().isSelected()) {
// 开启实时对比时立即执行一次对比
textDiffForm.performDiff();
}
});
// 忽略空白差异复选框事件:切换时立即重算
textDiffForm.getIgnoreWhitespaceCheckBox().addActionListener(e -> {
textDiffForm.performDiff();
});
// 文本变化监听器 - 实时对比
DocumentListener realTimeDocumentListener = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
if (textDiffForm.getRealTimeCheckBox().isSelected()) {
// 延迟执行,避免频繁对比
javax.swing.SwingUtilities.invokeLater(textDiffForm::performDiff);
}
}
@Override
public void removeUpdate(DocumentEvent e) {
if (textDiffForm.getRealTimeCheckBox().isSelected()) {
javax.swing.SwingUtilities.invokeLater(textDiffForm::performDiff);
}
}
@Override
public void changedUpdate(DocumentEvent e) {
if (textDiffForm.getRealTimeCheckBox().isSelected()) {
javax.swing.SwingUtilities.invokeLater(textDiffForm::performDiff);
}
}
};
// 为左右文本区域添加文档监听器
textDiffForm.getLeftTextArea().getDocument().addDocumentListener(realTimeDocumentListener);
textDiffForm.getRightTextArea().getDocument().addDocumentListener(realTimeDocumentListener);
// 内容保存监听器
DocumentListener saveDocumentListener = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
saveContent();
}
@Override
public void removeUpdate(DocumentEvent e) {
saveContent();
}
@Override
public void changedUpdate(DocumentEvent e) {
saveContent();
}
private void saveContent() {
// 延迟保存,避免频繁IO操作
SwingUtilities.invokeLater(() -> {
try {
textDiffForm.save();
} catch (Exception ex) {
log.error("保存文本对比内容失败", ex);
}
});
}
};
// 为左右文本区域添加保存监听器
textDiffForm.getLeftTextArea().getDocument().addDocumentListener(saveDocumentListener);
textDiffForm.getRightTextArea().getDocument().addDocumentListener(saveDocumentListener);
}
}
@@ -1,92 +1,45 @@
package com.luoboduner.moo.tool.util;
import de.hunsicker.jalopy.Jalopy;
import de.hunsicker.jalopy.storage.Convention;
import de.hunsicker.jalopy.storage.ConventionKeys;
import org.jsoup.Jsoup;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.StringWriter;
public class FileReformatUtil {
private static DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
private static TransformerFactory transformerFactory = TransformerFactory.newInstance();
private static Jalopy jalopy = new Jalopy();
public static String reformatXML(File file, int spaceNum) throws Exception {
StringWriter res = new StringWriter();
try {
// 创建DocumentBuilder对象
DocumentBuilder builder = factory.newDocumentBuilder();
// 解析XML文件并返回Document对象
Document document = builder.parse(file);
// 获取文档的根元素
Element rootElement = document.getDocumentElement();
// 设置格式化属性,这里使用缩进和换行
document.setXmlStandalone(true);
document.setXmlVersion("1.0");
rootElement.normalize();
// 创建Transformer对象
Transformer transformer = transformerFactory.newTransformer();
// 设置输出格式化属性,这里使用缩进和换行
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(spaceNum));
// 创建DOMSource对象
DOMSource source = new DOMSource(document);
// 创建StreamResult对象,指定输出位置
StreamResult result = new StreamResult(res);
// 格式化XML并输出结果
transformer.transform(source, result);
return res.toString();
} catch (Exception e) {
throw e;
} finally {
res.close();
}
}
public static String reformatHTML(File file, int spaceNum) throws Exception {
return Jsoup.parse(file, "UTF-8")
.outputSettings(new org.jsoup.nodes.Document.OutputSettings().indentAmount(spaceNum))
.outerHtml();
}
public static String reformatJAVA(File file, int spaceNum) throws Exception {
Convention instance = Convention.getInstance();
instance.put(ConventionKeys.SPACE_BEFORE_BRACES, "true");
instance.put(ConventionKeys.SPACE_BEFORE_BRACKETS, "true");
instance.put(ConventionKeys.INDENT_SIZE, String.valueOf(spaceNum));
try (StringWriter stringWriter = new StringWriter()) {
jalopy.setInput(file);
jalopy.setOutput(stringWriter);
boolean result = jalopy.format();
if (!result) {
throw new Exception("格式化失败!");
}
return stringWriter.toString();
} catch (Exception e) {
throw e;
}
}
}
package com.luoboduner.moo.tool.util;
import com.luoboduner.moo.tool.util.codeformatter.XmlFormatting;
import de.hunsicker.jalopy.Jalopy;
import de.hunsicker.jalopy.storage.Convention;
import de.hunsicker.jalopy.storage.ConventionKeys;
import org.jsoup.Jsoup;
import java.io.File;
import java.io.StringWriter;
public class FileReformatUtil {
private static Jalopy jalopy = new Jalopy();
public static String reformatXML(File file, int spaceNum) throws Exception {
return XmlFormatting.formatFile(file, spaceNum);
}
public static String reformatHTML(File file, int spaceNum) throws Exception {
return Jsoup.parse(file, "UTF-8")
.outputSettings(new org.jsoup.nodes.Document.OutputSettings().indentAmount(spaceNum))
.outerHtml();
}
public static String reformatJAVA(File file, int spaceNum) throws Exception {
Convention instance = Convention.getInstance();
instance.put(ConventionKeys.SPACE_BEFORE_BRACES, "true");
instance.put(ConventionKeys.SPACE_BEFORE_BRACKETS, "true");
instance.put(ConventionKeys.INDENT_SIZE, String.valueOf(spaceNum));
try (StringWriter stringWriter = new StringWriter()) {
jalopy.setInput(file);
jalopy.setOutput(stringWriter);
boolean result = jalopy.format();
if (!result) {
throw new Exception("格式化失败!");
}
return stringWriter.toString();
} catch (Exception e) {
throw e;
}
}
}
@@ -1,90 +1,25 @@
package com.luoboduner.moo.tool.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.StringWriter;
public class XmlReformatUtil {
// log
private static final Logger logger = LoggerFactory.getLogger(XmlReformatUtil.class);
private static DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
private static TransformerFactory transformerFactory = TransformerFactory.newInstance();
public static String reformat(File file, int spaceNum) throws Exception {
StringWriter res = new StringWriter();
try {
// 创建DocumentBuilder对象
DocumentBuilder builder = factory.newDocumentBuilder();
// 解析XML文件并返回Document对象
Document document = builder.parse(file);
// 获取文档的根元素
Element rootElement = document.getDocumentElement();
// 设置格式化属性,这里使用缩进和换行
document.setXmlStandalone(true);
document.setXmlVersion("1.0");
rootElement.normalize();
// 创建Transformer对象
Transformer transformer = transformerFactory.newTransformer();
// 设置输出格式化属性,这里使用缩进和换行
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING,"UTF-8");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(spaceNum));
// 创建DOMSource对象
DOMSource source = new DOMSource(document);
// 创建StreamResult对象,指定输出位置
StreamResult result = new StreamResult(res);
// 格式化XML并输出结果
transformer.transform(source, result);
return res.toString();
} catch (Exception e) {
throw e;
} finally {
res.close();
}
}
public static String format(String xmlStr) {
try {
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new InputSource(new ByteArrayInputStream(xmlStr.getBytes("utf-8"))));
Element rootElement = document.getDocumentElement();
document.setXmlStandalone(true);
document.setXmlVersion("1.0");
rootElement.normalize();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING,"UTF-8");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
DOMSource source = new DOMSource(document);
StringWriter res = new StringWriter();
StreamResult result = new StreamResult(res);
transformer.transform(source, result);
return res.toString();
} catch (Exception e) {
logger.error("XML格式化失败", e);
return xmlStr;
}
}
}
package com.luoboduner.moo.tool.util;
import com.luoboduner.moo.tool.util.codeformatter.XmlFormatting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
public class XmlReformatUtil {
// log
private static final Logger logger = LoggerFactory.getLogger(XmlReformatUtil.class);
public static String reformat(File file, int spaceNum) throws Exception {
return XmlFormatting.formatFile(file, spaceNum);
}
public static String format(String xmlStr) {
try {
return XmlFormatting.formatString(xmlStr, 4);
} catch (Exception e) {
logger.error("XML格式化失败", e);
return xmlStr;
}
}
}
@@ -1,38 +1,14 @@
package com.luoboduner.moo.tool.util.codeformatter;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringReader;
import java.io.StringWriter;
public class XmlFormatter implements CodeFormatter {
public String format(String input) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new InputSource(new StringReader(input)));
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(document), new StreamResult(writer));
return writer.toString();
// Delegate to centralized safe XML formatter with default indent 4
return XmlFormatting.formatString(input, 4);
} catch (Exception e) {
throw new RuntimeException("Error formatting XML", e);
}
}
}
}
@@ -0,0 +1,137 @@
package com.luoboduner.moo.tool.util.codeformatter;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.parser.Parser;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.StringWriter;
/**
* XML格式化工具
*
* @author CassianFlorin
* @email flowercard591@gmail.com
* @date 2025/9/27 22:15
*/
public final class XmlFormatting {
private XmlFormatting() {
}
public static String formatString(String xml, int indent) throws Exception {
try {
return formatWithJsoup(xml, indent);
} catch (Exception ignore) {
// fallback to JAXP secure DOM formatting
org.w3c.dom.Document document = parseString(xml);
return transform(document, indent);
}
}
public static String formatFile(File file, int indent) throws Exception {
try {
String content = readFileUtf8(file);
return formatWithJsoup(content, indent);
} catch (Exception ignore) {
org.w3c.dom.Document document = parseFile(file);
return transform(document, indent);
}
}
private static String formatWithJsoup(String xml, int indent) throws Exception {
Document doc = Jsoup.parse(xml, "", Parser.xmlParser());
doc.outputSettings(new Document.OutputSettings()
.prettyPrint(true)
.indentAmount(indent)
.syntax(Document.OutputSettings.Syntax.xml));
return doc.outerHtml();
}
private static org.w3c.dom.Document parseString(String xml) throws Exception {
DocumentBuilderFactory factory = secureDocumentBuilderFactory();
DocumentBuilder builder = factory.newDocumentBuilder();
InputSource source = new InputSource(new ByteArrayInputStream(xml.getBytes("UTF-8")));
org.w3c.dom.Document document = builder.parse(source);
normalize(document);
return document;
}
private static org.w3c.dom.Document parseFile(File file) throws Exception {
DocumentBuilderFactory factory = secureDocumentBuilderFactory();
DocumentBuilder builder = factory.newDocumentBuilder();
org.w3c.dom.Document document = builder.parse(file);
normalize(document);
return document;
}
private static void normalize(org.w3c.dom.Document document) {
Element rootElement = document.getDocumentElement();
document.setXmlStandalone(true);
document.setXmlVersion("1.0");
if (rootElement != null) {
rootElement.normalize();
}
}
private static String transform(org.w3c.dom.Document document, int indent) throws Exception {
TransformerFactory transformerFactory = TransformerFactory.newInstance();
try {
transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (Throwable ignore) {
// Some TransformerFactory implementations may not support this feature; ignore to keep compatibility
}
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(indent));
StringWriter res = new StringWriter();
transformer.transform(new DOMSource(document), new StreamResult(res));
return res.toString();
}
private static String readFileUtf8(File file) throws Exception {
byte[] bytes = java.nio.file.Files.readAllBytes(file.toPath());
return new String(bytes, "UTF-8");
}
private static DocumentBuilderFactory secureDocumentBuilderFactory() throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setXIncludeAware(false);
try {
// Enable secure processing
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (Throwable ignore) {
// keep going for max compatibility with various parsers
}
try {
// Disallow loading external DTDs and external entities to avoid blocking/XXE
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
} catch (Throwable ignore) {
}
try {
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
} catch (Throwable ignore) {
}
try {
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
} catch (Throwable ignore) {
}
// Do not expand entity references automatically
factory.setExpandEntityReferences(false);
return factory;
}
}
+26
View File
@@ -111,4 +111,30 @@ create table if not exists t_func_content
remark text,
create_time datetime,
modified_time datetime
);
-- text diff
-- 文档主表
CREATE TABLE IF NOT EXISTS doc (
doc_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
-- 版本表
CREATE TABLE IF NOT EXISTS doc_revision (
rev_id INTEGER PRIMARY KEY AUTOINCREMENT,
doc_id INTEGER NOT NULL,
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
content TEXT NOT NULL,
FOREIGN KEY (doc_id) REFERENCES doc(doc_id)
);
-- 缓存行级 unified diff(从前一版本到本版本)
CREATE TABLE IF NOT EXISTS doc_diff_cache (
from_rev_id INTEGER NOT NULL,
to_rev_id INTEGER NOT NULL,
unified TEXT NOT NULL,
PRIMARY KEY (from_rev_id, to_rev_id),
FOREIGN KEY (from_rev_id) REFERENCES doc_revision(rev_id),
FOREIGN KEY (to_rev_id) REFERENCES doc_revision(rev_id)
);