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 4450697
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import java.time.Instant;
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 +28,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 +189,44 @@ 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(
() -> PatchUtils.generateUnifiedDiffForHtml(contentDiffDo.getOriginal(),
contentDiffDo.getRevised())
));
}

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;
}
}
53 changes: 52 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 @@ -15,14 +15,18 @@
import java.util.List;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import run.halo.app.infra.utils.JsonUtils;

/**
* @author guqing
* @since 2.0.0
*/
public class PatchUtils {
private static final String DELIMITER = "\n";
public static final String DELIMITER = "\n";
private static final Splitter lineSplitter = Splitter.on(DELIMITER);

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

/**
* <p>It will generate a unified diff for the given original and revised html content.</p>
* <p>It will add the following classes to the html elements:</p>
* <ul>
* <li>diff-html-add: added elements</li>
* <li>diff-html-delete: deleted elements</li>
* <li>diff-html-change: changed elements</li>
* </ul>
*/
public static String generateUnifiedDiffForHtml(String originalHtml, String revisedHtml) {
var originalLines = breakLine(originalHtml);
var revisedLines = breakLine(revisedHtml);

Patch<String> patch = DiffUtils.diff(originalLines, revisedLines);
Document doc = Jsoup.parse(originalHtml);

int cumOffset = 0;

for (AbstractDelta<String> delta : patch.getDeltas()) {
DeltaType type = delta.getType();
int startPos = delta.getSource().getPosition();
int length = delta.getSource().getLines().size();

if (type == DeltaType.INSERT) {
Element newElement = doc.createElement("div");
newElement.addClass("diff-html-add");
newElement.text(String.join("\n", delta.getTarget().getLines()));
doc.body().child(startPos + cumOffset).before(newElement.outerHtml());
cumOffset++;
} else if (type == DeltaType.DELETE) {
Elements elements = doc.body().children();
for (int i = startPos; i < startPos + length; i++) {
elements.get(i + cumOffset).addClass("diff-html-delete");
}
} else if (type == DeltaType.CHANGE) {
Elements elements = doc.body().children();
for (int i = startPos; i < startPos + length; i++) {
elements.get(i + cumOffset).addClass("diff-html-change");
elements.get(i + cumOffset)
.text(delta.getTarget().getLines().get(i - startPos));
}
}
}

return doc.outerHtml();
}

@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" ]
31 changes: 31 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,31 @@
package run.halo.app.content;

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

import org.junit.jupiter.api.Test;

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

@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 result = PatchUtils.generateUnifiedDiffForHtml(originalHtml, revisedHtml);
assertThat(result).isEqualToIgnoringNewLines("""
<html>
<head></head>
<body>
<p class="diff-html-change">&lt;html&gt;&lt;body&gt;&lt;p&gt;Line one&lt;/p&gt;&lt;p&gt;Line three&lt;/p&gt;&lt;p&gt;New line&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</p>
<p>Line two</p>
</body>
</html>
""");
}
}

0 comments on commit 4450697

Please sign in to comment.