mirror of
https://github.com/rememberber/MooTool.git
synced 2026-09-19 02:37:00 +08:00
Merge branch 'github-master' into develop
This commit is contained in:
@@ -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>
|
||||
|
||||
<!-- 行级 diff(Myers)+ 生成 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>
|
||||
|
||||
@@ -53,6 +53,9 @@ Windows • Linux • macOS
|
||||
|
||||

|
||||
|
||||
- 并排对比:
|
||||
- 统一差异:
|
||||
|
||||
## 下载
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
Reference in New Issue
Block a user