Skip to content

Commit

Permalink
feat: support view diffs between post content versions
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed May 11, 2024
1 parent a629961 commit a1ff7d7
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 2 deletions.
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>
""");
}
}

0 comments on commit a1ff7d7

Please sign in to comment.