mirror of https://github.com/halo-dev/halo
feat: support obtaining the previous post and next post according to post name (#2636)
#### What type of PR is this? /kind feature /area core /milestone 2.0 #### What this PR does / why we need it: 支持通过文章名称获取上一篇和下一篇文章数据 本 PR 通过实现一个固定大小的滑动窗口数据结构来通过传入的文章名称获取上一篇和下一篇文章 例如当具有一系列文章名为 [a, b, c, d, e, f, g] 查询文章名为 d 的文章的上一篇和下一篇则使用一个固定大小为 3 的滑动窗口, 示意图如下 <img width="526" alt="image" src="https://user-images.githubusercontent.com/38999863/198243133-20f77431-1107-4526-9f4f-6a11c68204e7.png"> 通过窗口右移来调整当窗口圈住所需元素 D 时且 元素 D 的位置不是窗口的最后一个元素时得到一个想要的窗口 此时: 1. 如果 D 位于 window 中的第一个元素则 D 没有上一篇 2. 如果 D 位于 window 中间则 window 的第一个元素为 D 的上一篇,最后一个元素为下一篇 3. 如果 D 位于 window 的最后一个位置,则 D 没有下一篇 #### Which issue(s) this PR fixes: Fixes #2635 #### Special notes for your reviewer: how to test it? 1. 创建几篇文章然后发布 4. 能通过 `${postFinder.cursor(your-post-name)}` 来获取到带有上一页下一页的文章数据 5. 当文章处于第一条时没有上一页,当文章处于最后一页时没有下一页(可以通过`${postCursor.hasPrevious()}` 和 `${postCursor.hasNext()}` 判断按钮是否展示) /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 支持通过文章名称获取上一篇和下一篇文章数据 ```pull/2646/head
parent
8b038c1e0a
commit
b63131c36e
|
@ -4,6 +4,7 @@ import org.springframework.lang.Nullable;
|
||||||
import run.halo.app.core.extension.Post;
|
import run.halo.app.core.extension.Post;
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
import run.halo.app.theme.finders.vo.ContentVo;
|
import run.halo.app.theme.finders.vo.ContentVo;
|
||||||
|
import run.halo.app.theme.finders.vo.NavigationPostVo;
|
||||||
import run.halo.app.theme.finders.vo.PostArchiveVo;
|
import run.halo.app.theme.finders.vo.PostArchiveVo;
|
||||||
import run.halo.app.theme.finders.vo.PostVo;
|
import run.halo.app.theme.finders.vo.PostVo;
|
||||||
|
|
||||||
|
@ -19,6 +20,8 @@ public interface PostFinder {
|
||||||
|
|
||||||
ContentVo content(String postName);
|
ContentVo content(String postName);
|
||||||
|
|
||||||
|
NavigationPostVo cursor(String current);
|
||||||
|
|
||||||
ListResult<PostVo> list(@Nullable Integer page, @Nullable Integer size);
|
ListResult<PostVo> list(@Nullable Integer page, @Nullable Integer size);
|
||||||
|
|
||||||
ListResult<PostVo> listByCategory(@Nullable Integer page, @Nullable Integer size,
|
ListResult<PostVo> listByCategory(@Nullable Integer page, @Nullable Integer size,
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package run.halo.app.theme.finders.impl;
|
package run.halo.app.theme.finders.impl;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.Deque;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -10,8 +13,10 @@ import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.util.comparator.Comparators;
|
import org.springframework.util.comparator.Comparators;
|
||||||
|
import reactor.util.function.Tuple2;
|
||||||
import run.halo.app.content.ContentService;
|
import run.halo.app.content.ContentService;
|
||||||
import run.halo.app.core.extension.Counter;
|
import run.halo.app.core.extension.Counter;
|
||||||
import run.halo.app.core.extension.Post;
|
import run.halo.app.core.extension.Post;
|
||||||
|
@ -28,6 +33,7 @@ import run.halo.app.theme.finders.TagFinder;
|
||||||
import run.halo.app.theme.finders.vo.CategoryVo;
|
import run.halo.app.theme.finders.vo.CategoryVo;
|
||||||
import run.halo.app.theme.finders.vo.ContentVo;
|
import run.halo.app.theme.finders.vo.ContentVo;
|
||||||
import run.halo.app.theme.finders.vo.Contributor;
|
import run.halo.app.theme.finders.vo.Contributor;
|
||||||
|
import run.halo.app.theme.finders.vo.NavigationPostVo;
|
||||||
import run.halo.app.theme.finders.vo.PostArchiveVo;
|
import run.halo.app.theme.finders.vo.PostArchiveVo;
|
||||||
import run.halo.app.theme.finders.vo.PostArchiveYearMonthVo;
|
import run.halo.app.theme.finders.vo.PostArchiveYearMonthVo;
|
||||||
import run.halo.app.theme.finders.vo.PostVo;
|
import run.halo.app.theme.finders.vo.PostVo;
|
||||||
|
@ -94,6 +100,107 @@ public class PostFinderImpl implements PostFinder {
|
||||||
.block();
|
.block();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NavigationPostVo cursor(String currentName) {
|
||||||
|
// TODO Optimize the post names query here
|
||||||
|
List<String> postNames = client.list(Post.class, FIXED_PREDICATE, defaultComparator())
|
||||||
|
.map(post -> post.getMetadata().getName())
|
||||||
|
.collectList()
|
||||||
|
.block();
|
||||||
|
if (postNames == null) {
|
||||||
|
return NavigationPostVo.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationPostVo.NavigationPostVoBuilder builder = NavigationPostVo.builder()
|
||||||
|
.current(getByName(currentName));
|
||||||
|
|
||||||
|
Pair<String, String> previousNextPair = postPreviousNextPair(postNames, currentName);
|
||||||
|
String previousPostName = previousNextPair.getLeft();
|
||||||
|
String nextPostName = previousNextPair.getRight();
|
||||||
|
|
||||||
|
if (previousPostName != null) {
|
||||||
|
builder.previous(getByName(previousPostName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextPostName != null) {
|
||||||
|
builder.next(getByName(nextPostName));
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Pair<String, String> postPreviousNextPair(List<String> postNames,
|
||||||
|
String currentName) {
|
||||||
|
FixedSizeSlidingWindow<String> window = new FixedSizeSlidingWindow<>(3);
|
||||||
|
for (String postName : postNames) {
|
||||||
|
window.add(postName);
|
||||||
|
if (!window.isFull()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int index = window.indexOf(currentName);
|
||||||
|
if (index == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// got expected window
|
||||||
|
if (index < 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> elements = window.elements();
|
||||||
|
Tuple2<String, String> previousNext;
|
||||||
|
// current post index
|
||||||
|
int index = elements.indexOf(currentName);
|
||||||
|
|
||||||
|
String previousPostName = null;
|
||||||
|
if (index != 0) {
|
||||||
|
previousPostName = elements.get(index - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String nextPostName = null;
|
||||||
|
if (elements.size() - 1 > index) {
|
||||||
|
nextPostName = elements.get(index + 1);
|
||||||
|
}
|
||||||
|
return Pair.of(previousPostName, nextPostName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class FixedSizeSlidingWindow<T> {
|
||||||
|
Deque<T> queue;
|
||||||
|
int size;
|
||||||
|
|
||||||
|
public FixedSizeSlidingWindow(int size) {
|
||||||
|
this.size = size;
|
||||||
|
// FIFO
|
||||||
|
queue = new ArrayDeque<>(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add element to the window.
|
||||||
|
* The element added first will be deleted when the element in the collection exceeds
|
||||||
|
* {@code size}.
|
||||||
|
*/
|
||||||
|
public void add(T t) {
|
||||||
|
if (queue.size() == size) {
|
||||||
|
// remove first
|
||||||
|
queue.poll();
|
||||||
|
}
|
||||||
|
// add to last
|
||||||
|
queue.add(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int indexOf(T o) {
|
||||||
|
List<T> elements = elements();
|
||||||
|
return elements.indexOf(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> elements() {
|
||||||
|
return new ArrayList<>(queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFull() {
|
||||||
|
return queue.size() == size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListResult<PostVo> list(Integer page, Integer size) {
|
public ListResult<PostVo> list(Integer page, Integer size) {
|
||||||
return listPost(page, size, null, defaultComparator());
|
return listPost(page, size, null, defaultComparator());
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package run.halo.app.theme.finders.vo;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post navigation vo to hold previous and next item.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Value
|
||||||
|
@Builder
|
||||||
|
public class NavigationPostVo {
|
||||||
|
|
||||||
|
PostVo previous;
|
||||||
|
|
||||||
|
PostVo current;
|
||||||
|
|
||||||
|
PostVo next;
|
||||||
|
|
||||||
|
public boolean hasNext() {
|
||||||
|
return next != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPrevious() {
|
||||||
|
return previous != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NavigationPostVo empty() {
|
||||||
|
return NavigationPostVo.builder().build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,8 +7,11 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.apache.logging.log4j.util.Strings;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
|
@ -114,6 +117,54 @@ class PostFinderImplTest {
|
||||||
assertThat(items.get(1).getMonths().get(0).getMonth()).isEqualTo("01");
|
assertThat(items.get(1).getMonths().get(0).getMonth()).isEqualTo("01");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fixedSizeSlidingWindow() {
|
||||||
|
PostFinderImpl.FixedSizeSlidingWindow<Integer>
|
||||||
|
window = new PostFinderImpl.FixedSizeSlidingWindow<>(3);
|
||||||
|
|
||||||
|
List<String> list = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
window.add(i);
|
||||||
|
list.add(Strings.join(window.elements(), ','));
|
||||||
|
}
|
||||||
|
assertThat(list).isEqualTo(
|
||||||
|
List.of("0", "0,1", "0,1,2", "1,2,3", "2,3,4", "3,4,5", "4,5,6", "5,6,7", "6,7,8",
|
||||||
|
"7,8,9")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void postPreviousNextPair() {
|
||||||
|
List<String> postNames = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
postNames.add("post-" + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// post-0, post-1, post-2
|
||||||
|
Pair<String, String> previousNextPair =
|
||||||
|
PostFinderImpl.postPreviousNextPair(postNames, "post-0");
|
||||||
|
assertThat(previousNextPair.getLeft()).isNull();
|
||||||
|
assertThat(previousNextPair.getRight()).isEqualTo("post-1");
|
||||||
|
|
||||||
|
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-1");
|
||||||
|
assertThat(previousNextPair.getLeft()).isEqualTo("post-0");
|
||||||
|
assertThat(previousNextPair.getRight()).isEqualTo("post-2");
|
||||||
|
|
||||||
|
// post-1, post-2, post-3
|
||||||
|
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-2");
|
||||||
|
assertThat(previousNextPair.getLeft()).isEqualTo("post-1");
|
||||||
|
assertThat(previousNextPair.getRight()).isEqualTo("post-3");
|
||||||
|
|
||||||
|
// post-7, post-8, post-9
|
||||||
|
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-8");
|
||||||
|
assertThat(previousNextPair.getLeft()).isEqualTo("post-7");
|
||||||
|
assertThat(previousNextPair.getRight()).isEqualTo("post-9");
|
||||||
|
|
||||||
|
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-9");
|
||||||
|
assertThat(previousNextPair.getLeft()).isEqualTo("post-8");
|
||||||
|
assertThat(previousNextPair.getRight()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
List<Post> postsForArchives() {
|
List<Post> postsForArchives() {
|
||||||
Post post1 = post(1);
|
Post post1 = post(1);
|
||||||
post1.getSpec().setPublished(true);
|
post1.getSpec().setPublished(true);
|
||||||
|
|
Loading…
Reference in New Issue