Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support view diffs between post content versions #5892

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package run.halo.app.content;

import static com.github.difflib.UnifiedDiffUtils.generateUnifiedDiff;
import static run.halo.app.content.PatchUtils.breakHtml;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.isNull;

import com.github.difflib.DiffUtils;
import com.github.difflib.patch.Patch;
import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.OptimisticLockingFailureException;
Expand All @@ -26,6 +33,7 @@
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.Ref;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.exception.NotFoundException;

/**
* Abstract Service for {@link Snapshot}.
Expand Down Expand Up @@ -186,9 +194,58 @@ protected Snapshot determineRawAndContentPatch(Snapshot snapshotToUse,
return snapshotToUse;
}

/**
* Returns the unified diff content of the right compared to the left.
*/
protected Mono<String> generateContentDiffBy(String leftSnapshot, String rightSnapshot,
String baseSnapshot) {
if (StringUtils.isBlank(leftSnapshot) || StringUtils.isBlank(rightSnapshot)) {
return Mono.error(new IllegalArgumentException(
"The leftSnapshot and rightSnapshot must not be blank."));
}
if (StringUtils.isBlank(baseSnapshot)) {
return Mono.error(new IllegalArgumentException("The baseSnapshot must not be blank."));
}

var contentDiffDo = new ContentDiffDo();
var originalMono = getContent(leftSnapshot, baseSnapshot)
.switchIfEmpty(Mono.error(new NotFoundException("The leftSnapshot not found.")))
.doOnNext(contentWrapper -> contentDiffDo.setOriginal(contentWrapper.getContent()));
var revisedMono = getContent(rightSnapshot, baseSnapshot)
.switchIfEmpty(Mono.error(new NotFoundException("The rightSnapshot not found.")))
.doOnNext(contentWrapper -> contentDiffDo.setRevised(contentWrapper.getContent()));

return Mono.when(originalMono, revisedMono)
.then(Mono.fromSupplier(() -> {
var diffLines = generateContentUnifiedDiff(contentDiffDo.getOriginal(),
contentDiffDo.getRevised());
return PatchUtils.highlightDiffChanges(diffLines);
}));
}

static List<String> generateContentUnifiedDiff(String original, String revised) {
Assert.notNull(original, "The original text must not be null.");
Assert.notNull(revised, "The revised text must not be null.");
var originalLines = breakHtml(original);
Patch<String> patch = DiffUtils.diff(originalLines, breakHtml(revised));
var unifiedDiff = generateUnifiedDiff("left", "right",
originalLines, patch, 10);
if (unifiedDiff.size() < 3) {
return List.of();
}
return unifiedDiff.subList(2, unifiedDiff.size());
}

protected Mono<String> getContextUsername() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Principal::getName);
}

@Data
@Accessors(chain = true)
private static class ContentDiffDo {
private String original;
private String revised;
}
}
49 changes: 48 additions & 1 deletion application/src/main/java/run/halo/app/content/PatchUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
import com.github.difflib.patch.Patch;
import com.github.difflib.patch.PatchFailedException;
import com.google.common.base.Splitter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import run.halo.app.infra.utils.JsonUtils;
Expand All @@ -22,7 +25,8 @@
* @since 2.0.0
*/
public class PatchUtils {
private static final String DELIMITER = "\n";
public static final String DELIMITER = "\n";
static final Pattern HTML_OPEN_TAG_PATTERN = Pattern.compile("(<[^>]+>)|([^<]+)");
private static final Splitter lineSplitter = Splitter.on(DELIMITER);

public static Patch<String> create(String deltasJson) {
Expand Down Expand Up @@ -72,6 +76,49 @@ public static List<String> breakLine(String content) {
return lineSplitter.splitToList(content);
}

/**
* <p>It will generate a unified diff html for the given diff lines.</p>
* <p>It will wrap the following classes to the html elements:</p>
* <ul>
* <li>diff-html-add: added elements</li>
* <li>diff-html-remove: deleted elements</li>
* </ul>
*/
public static String highlightDiffChanges(List<String> diffLines) {
final var sb = new StringBuilder();
for (String line : diffLines) {
if (line.startsWith("+")) {
sb.append(wrapLine(line.substring(1), "diff-html-add"));
} else if (line.startsWith("-")) {
sb.append(wrapLine(line.substring(1), "diff-html-remove"));
} else {
sb.append(line).append("\n");
}
}
return sb.toString();
}

private static String wrapLine(String line, String className) {
return "<div class=\"" + className + "\">" + line + "</div>\n";
}

/**
* Break line for html.
*/
public static List<String> breakHtml(String compressedHtml) {
Matcher matcher = HTML_OPEN_TAG_PATTERN.matcher(compressedHtml);
List<String> elements = new ArrayList<>();
while (matcher.find()) {
if (matcher.group(1) != null) {
elements.add(matcher.group(1));
} else if (matcher.group(2) != null) {
List<String> lines = breakLine(matcher.group(2));
elements.addAll(lines);
}
}
return elements;
}

@Data
public static class Delta {
private StringChunk source;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,12 @@ public interface PostService {
Mono<Post> revertToSpecifiedSnapshot(String postName, String snapshotName);

Mono<ContentWrapper> deleteContent(String postName, String snapshotName);

/**
* <p>Returns the unified diff content of the right compared to the left.</p>
* <p>If the left snapshot is blank, the releaseSnapshot will be used as the left snapshot.</p>
* <p>If the right snapshot is blank, the headSnapshot will be used as the right snapshot.</p>
*/
Mono<String> generateContentDiff(String postName, String leftSnapshotName,
String rightSnapshotName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -355,6 +356,23 @@ public Mono<ContentWrapper> deleteContent(String postName, String snapshotName)
});
}

@Override
public Mono<String> generateContentDiff(String postName, String leftSnapshotName,
String rightSnapshotName) {
return client.get(Post.class, postName)
.flatMap(post -> {
String ensuredLeftSnapshotName = Optional.ofNullable(leftSnapshotName)
.filter(StringUtils::isNotBlank)
.orElse(post.getSpec().getReleaseSnapshot());

String ensuredRightSnapshotName = Optional.ofNullable(rightSnapshotName)
.filter(StringUtils::isNotBlank)
.orElse(post.getSpec().getHeadSnapshot());
return generateContentDiffBy(ensuredLeftSnapshotName, ensuredRightSnapshotName,
post.getSpec().getBaseSnapshot());
});
}

private Mono<Post> updatePostWithRetry(Post post, UnaryOperator<Post> func) {
return client.update(func.apply(post))
.onErrorResume(OptimisticLockingFailureException.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,28 @@ public RouterFunction<ServerResponse> endpoint() {
.response(responseBuilder()
.implementationArray(ListedSnapshotDto.class))
)
.GET("posts/{name}/diff-content", this::diffContent,
builder -> builder.operationId("diffPostContent")
.description("Generate diff content between two snapshots.")
.tag(tag)
.parameter(parameterBuilder().name("name")
.in(ParameterIn.PATH)
.required(true)
.implementation(String.class)
)
.parameter(parameterBuilder()
.name("leftSnapshot")
.in(ParameterIn.QUERY)
.required(true)
.implementation(String.class))
.parameter(parameterBuilder()
.name("rightSnapshot")
.in(ParameterIn.QUERY)
.required(true)
.implementation(String.class))
.response(responseBuilder()
.implementation(String.class))
)
.POST("posts", this::draftPost,
builder -> builder.operationId("DraftPost")
.description("Draft a post.")
Expand Down Expand Up @@ -238,6 +260,18 @@ public RouterFunction<ServerResponse> endpoint() {
.build();
}

private Mono<ServerResponse> diffContent(ServerRequest request) {
final var postName = request.pathVariable("name");
var leftSnapshotName = request.queryParam("leftSnapshot")
.orElse(null);
var rightSnapshotName = request.queryParam("rightSnapshot")
.orElse(null);
return client.get(Post.class, postName)
.flatMap(post -> postService.generateContentDiff(postName, leftSnapshotName,
rightSnapshotName))
.flatMap(diff -> ServerResponse.ok().bodyValue(diff));
}

private Mono<ServerResponse> deleteContent(ServerRequest request) {
final var postName = request.pathVariable("name");
final var snapshotName = request.queryParam("snapshotName").orElseThrow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ rules:
resources: [ "posts" ]
verbs: [ "get", "list" ]
- apiGroups: [ "api.console.halo.run" ]
resources: [ "posts", "posts/head-content", "posts/release-content", "posts/snapshot", "posts/content" ]
resources: [ "posts", "posts/head-content", "posts/release-content", "posts/snapshot", "posts/content", "posts/diff-content" ]
verbs: [ "get", "list" ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package run.halo.app.content;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import org.junit.jupiter.api.Test;

/**
* Tests for {@link AbstractContentService}.
*
* @author guqing
* @since 2.16.0
*/
class AbstractContentServiceTest {

@Test
void generateContentUnifiedDiff() {
List<String> diff = AbstractContentService.generateContentUnifiedDiff("line1\nline2", "");
var result = String.join(PatchUtils.DELIMITER, diff);
assertThat(result).isEqualToIgnoringNewLines("""
@@ -1,2 +1,0 @@
-line1
-line2
""");
}
}
72 changes: 72 additions & 0 deletions application/src/test/java/run/halo/app/content/PatchUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package run.halo.app.content;

import static com.github.difflib.UnifiedDiffUtils.generateUnifiedDiff;
import static org.assertj.core.api.Assertions.assertThat;

import com.github.difflib.DiffUtils;
import com.github.difflib.patch.Patch;
import org.junit.jupiter.api.Test;

/**
* Tests for {@link PatchUtils}.
*
* @author guqing
* @since 2.16.0
*/
class PatchUtilsTest {

@Test
void breakHtml() {
var result = PatchUtils.breakHtml("abc\nadasdas\nafasdfdsa");
assertThat(result).containsExactly("abc", "adasdas", "afasdfdsa");

result = PatchUtils.breakHtml("<html>\n<body><p>Line one</p>\n\n</body></html>");
assertThat(result).containsExactly("<html>", "<body>", "<p>", "Line one", "</p>", "</body>",
"</html>");
}

@Test
void generateUnifiedDiffForHtml() {
String originalHtml = """
<html>
<body>
<p>Line one</p>
<p>Line two</p>
</body>
</html>
""";
String revisedHtml = """
<html>
<body>
<p>Line one</p>
<p>Line three</p>
<p>New line</p>
</body>
</html>
""";
var originalLines = PatchUtils.breakHtml(originalHtml);
Patch<String> patch = DiffUtils.diff(originalLines, PatchUtils.breakHtml(revisedHtml));
var unifiedDiff = generateUnifiedDiff("left", "right",
originalLines, patch, 10);
var highlighted = PatchUtils.highlightDiffChanges(unifiedDiff);
assertThat(highlighted).isEqualToIgnoringWhitespace("""
<div class="diff-html-remove">-- left</div>
<div class="diff-html-add">++ right</div>
@@ -1,10 +1,13 @@
<html>
<body>
<p>
Line one
</p>
<p>
<div class="diff-html-remove">Line two</div>
<div class="diff-html-add">Line three</div>
</p>
<div class="diff-html-add"><p></div>
<div class="diff-html-add">New line</div>
<div class="diff-html-add"></p></div>
</body>
</html>
""");
}
}