mirror of https://github.com/halo-dev/halo
feat: add content extension points for post and single page on theme-side (#4080)
#### What type of PR is this?
/kind feature
/milestone 2.7.x
/area core
#### What this PR does / why we need it:
为主题端的文章和自定义页面内容添加扩展点
插件可以通过实现扩展点来干预文章和自定义页面的内容显示,如修改内容的 html 结构,改变特定样式等
使用方式参考:[docs/extension-points/content.md](9b2b9f1837
)
#### Which issue(s) this PR fixes:
Fixes #4003
#### Does this PR introduce a user-facing change?
```release-note
为主题端的文章和自定义页面内容添加扩展点
```
pull/4128/head^2
parent
972ebed03a
commit
cabcd98ef4
|
@ -61,10 +61,11 @@ public class Plugin extends AbstractExtension {
|
|||
*
|
||||
* @see <a href="semver.org">semantic version</a>
|
||||
*/
|
||||
@Schema(requiredMode = REQUIRED, pattern = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-("
|
||||
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\."
|
||||
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\"
|
||||
+ ".[0-9a-zA-Z-]+)*))?$")
|
||||
@Schema(requiredMode = REQUIRED,
|
||||
pattern = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-("
|
||||
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\."
|
||||
+ "(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\"
|
||||
+ ".[0-9a-zA-Z-]+)*))?$")
|
||||
private String version;
|
||||
|
||||
private PluginAuthor author;
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.pf4j.ExtensionPoint;
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
|
||||
/**
|
||||
* <p>{@link ReactivePostContentHandler} provides a way to extend the content to be displayed in
|
||||
* the theme.</p>
|
||||
* Plugins can implement this interface to extend the content to be displayed in the theme,
|
||||
* including but not limited to adding specific styles, JS libraries, inserting specific content,
|
||||
* and intercepting content.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.7.0
|
||||
*/
|
||||
public interface ReactivePostContentHandler extends ExtensionPoint {
|
||||
|
||||
/**
|
||||
* <p>Methods for handling {@link run.halo.app.core.extension.content.Post} content.</p>
|
||||
* <p>For example, you can use this method to change the content for a better display in
|
||||
* theme-side.</p>
|
||||
*
|
||||
* @param postContent content to be handled
|
||||
* @return handled content
|
||||
*/
|
||||
Mono<PostContentContext> handle(@NonNull PostContentContext postContent);
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
class PostContentContext {
|
||||
private Post post;
|
||||
private String content;
|
||||
private String raw;
|
||||
private String rawType;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.pf4j.ExtensionPoint;
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
|
||||
/**
|
||||
* <p>{@link ReactiveSinglePageContentHandler} provides a way to extend the content to be
|
||||
* displayed in the theme.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @see ReactivePostContentHandler
|
||||
* @since 2.7.0
|
||||
*/
|
||||
public interface ReactiveSinglePageContentHandler extends ExtensionPoint {
|
||||
|
||||
/**
|
||||
* <p>Methods for handling {@link run.halo.app.core.extension.content.SinglePage} content.</p>
|
||||
* <p>For example, you can use this method to change the content for a better display in
|
||||
* theme-side.</p>
|
||||
*
|
||||
* @param singlePageContent content to be handled
|
||||
* @return handled content
|
||||
*/
|
||||
Mono<SinglePageContentContext> handle(@NonNull SinglePageContentContext singlePageContent);
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
class SinglePageContentContext {
|
||||
private SinglePage singlePage;
|
||||
private String content;
|
||||
private String raw;
|
||||
private String rawType;
|
||||
}
|
||||
}
|
|
@ -26,14 +26,14 @@ public abstract class AbstractContentService {
|
|||
private final ReactiveExtensionClient client;
|
||||
|
||||
public Mono<ContentWrapper> getContent(String snapshotName, String baseSnapshotName) {
|
||||
return client.fetch(Snapshot.class, baseSnapshotName)
|
||||
return client.get(Snapshot.class, baseSnapshotName)
|
||||
.doOnNext(this::checkBaseSnapshot)
|
||||
.flatMap(baseSnapshot -> {
|
||||
if (StringUtils.equals(snapshotName, baseSnapshotName)) {
|
||||
var contentWrapper = ContentWrapper.patchSnapshot(baseSnapshot, baseSnapshot);
|
||||
return Mono.just(contentWrapper);
|
||||
}
|
||||
return client.fetch(Snapshot.class, snapshotName)
|
||||
return client.get(Snapshot.class, snapshotName)
|
||||
.map(snapshot -> ContentWrapper.patchSnapshot(snapshot, baseSnapshot));
|
||||
});
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ public abstract class AbstractContentService {
|
|||
Assert.notNull(snapshot, "The snapshot must not be null.");
|
||||
String keepRawAnno =
|
||||
MetadataUtil.nullSafeAnnotations(snapshot).get(Snapshot.KEEP_RAW_ANNO);
|
||||
if (!org.thymeleaf.util.StringUtils.equals(Boolean.TRUE.toString(), keepRawAnno)) {
|
||||
if (!StringUtils.equals(Boolean.TRUE.toString(), keepRawAnno)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("The snapshot [%s] is not a base snapshot.",
|
||||
snapshot.getMetadata().getName()));
|
||||
|
|
|
@ -7,7 +7,10 @@ import org.springframework.lang.NonNull;
|
|||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.theme.ReactivePostContentHandler;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
|
||||
public interface PostPublicQueryService {
|
||||
Predicate<Post> FIXED_PREDICATE = post -> post.isPublished()
|
||||
|
@ -34,5 +37,27 @@ public interface PostPublicQueryService {
|
|||
* @param post post must not be null
|
||||
* @return listed post vo
|
||||
*/
|
||||
Mono<ListedPostVo> convertToListedPostVo(@NonNull Post post);
|
||||
Mono<ListedPostVo> convertToListedVo(@NonNull Post post);
|
||||
|
||||
/**
|
||||
* Converts {@link Post} to post vo and populate post content by the given snapshot name.
|
||||
* <p> This method will get post content by {@code snapshotName} and try to find
|
||||
* {@link ReactivePostContentHandler}s to extend the content</p>
|
||||
*
|
||||
* @param post post must not be null
|
||||
* @param snapshotName snapshot name must not be blank
|
||||
* @return converted post vo
|
||||
*/
|
||||
Mono<PostVo> convertToVo(Post post, String snapshotName);
|
||||
|
||||
/**
|
||||
* Gets post content by post name.
|
||||
* <p> This method will get post released content by post name and try to find
|
||||
* {@link ReactivePostContentHandler}s to extend the content</p>
|
||||
*
|
||||
* @param postName post name must not be blank
|
||||
* @return post content for theme-side
|
||||
* @see ReactivePostContentHandler
|
||||
*/
|
||||
Mono<ContentVo> getContent(String postName);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.theme.ReactiveSinglePageContentHandler;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.ListedSinglePageVo;
|
||||
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||
|
||||
|
@ -13,9 +16,40 @@ import run.halo.app.theme.finders.vo.SinglePageVo;
|
|||
*/
|
||||
public interface SinglePageConversionService {
|
||||
|
||||
/**
|
||||
* Converts the given {@link SinglePage} to {@link SinglePageVo} and populate content by
|
||||
* given snapshot name.
|
||||
*
|
||||
* @param singlePage the single page must not be null
|
||||
* @param snapshotName the snapshot name to get content must not be blank
|
||||
* @return the converted single page vo
|
||||
* @see #convertToVo(SinglePage)
|
||||
*/
|
||||
Mono<SinglePageVo> convertToVo(SinglePage singlePage, String snapshotName);
|
||||
|
||||
Mono<SinglePageVo> convertToVo(SinglePage singlePage);
|
||||
/**
|
||||
* Converts the given {@link SinglePage} to {@link SinglePageVo}.
|
||||
* <p>This method will query the additional information of the {@link SinglePageVo} needed to
|
||||
* populate.</p>
|
||||
* <p>This method will try to find {@link ReactiveSinglePageContentHandler}s to extend the
|
||||
* content.</p>
|
||||
*
|
||||
* @param singlePage the single page must not be null
|
||||
* @return the converted single page vo
|
||||
* @see #getContent(String)
|
||||
*/
|
||||
Mono<SinglePageVo> convertToVo(@NonNull SinglePage singlePage);
|
||||
|
||||
/**
|
||||
* Gets content by given page name.
|
||||
* <p>This method will get released content by page name and try to find
|
||||
* {@link ReactiveSinglePageContentHandler}s to extend the content.</p>
|
||||
*
|
||||
* @param pageName page name must not be blank
|
||||
* @return content of the specified page
|
||||
* @since 2.7.0
|
||||
*/
|
||||
Mono<ContentVo> getContent(String pageName);
|
||||
|
||||
Mono<ListedSinglePageVo> convertToListedVo(SinglePage singlePage);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import org.apache.commons.lang3.tuple.Pair;
|
|||
import org.springframework.util.comparator.Comparators;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
@ -47,27 +46,20 @@ public class PostFinderImpl implements PostFinder {
|
|||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final PostService postService;
|
||||
|
||||
private final PostPublicQueryService postPublicQueryService;
|
||||
|
||||
@Override
|
||||
public Mono<PostVo> getByName(String postName) {
|
||||
return client.get(Post.class, postName)
|
||||
.filter(FIXED_PREDICATE)
|
||||
.flatMap(postPublicQueryService::convertToListedPostVo)
|
||||
.map(PostVo::from)
|
||||
.flatMap(postVo -> content(postName)
|
||||
.doOnNext(postVo::setContent)
|
||||
.thenReturn(postVo)
|
||||
.flatMap(post -> postPublicQueryService.convertToVo(post,
|
||||
post.getSpec().getReleaseSnapshot())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentVo> content(String postName) {
|
||||
return postService.getReleaseContent(postName)
|
||||
.map(wrapper -> ContentVo.builder().content(wrapper.getContent())
|
||||
.raw(wrapper.getRaw()).build());
|
||||
return postPublicQueryService.getContent(postName);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -107,7 +99,7 @@ public class PostFinderImpl implements PostFinder {
|
|||
@Override
|
||||
public Flux<ListedPostVo> listAll() {
|
||||
return client.list(Post.class, FIXED_PREDICATE, defaultComparator())
|
||||
.concatMap(postPublicQueryService::convertToListedPostVo);
|
||||
.concatMap(postPublicQueryService::convertToListedVo);
|
||||
}
|
||||
|
||||
static Pair<String, String> postPreviousNextPair(List<String> postNames,
|
||||
|
|
|
@ -2,6 +2,7 @@ package run.halo.app.theme.finders.impl;
|
|||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
@ -11,16 +12,22 @@ import org.springframework.util.Assert;
|
|||
import org.springframework.util.CollectionUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||
import run.halo.app.theme.ReactivePostContentHandler;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
import run.halo.app.theme.finders.ContributorFinder;
|
||||
import run.halo.app.theme.finders.PostPublicQueryService;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
import run.halo.app.theme.finders.vo.StatsVo;
|
||||
|
||||
@Component
|
||||
|
@ -37,6 +44,10 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService {
|
|||
|
||||
private final CounterService counterService;
|
||||
|
||||
private final PostService postService;
|
||||
|
||||
private final ExtensionGetter extensionGetter;
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<ListedPostVo>> list(Integer page, Integer size,
|
||||
Predicate<Post> postPredicate, Comparator<Post> comparator) {
|
||||
|
@ -45,7 +56,7 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService {
|
|||
return client.list(Post.class, predicate,
|
||||
comparator, pageNullSafe(page), sizeNullSafe(size))
|
||||
.flatMap(list -> Flux.fromStream(list.get())
|
||||
.concatMap(post -> convertToListedPostVo(post)
|
||||
.concatMap(post -> convertToListedVo(post)
|
||||
.flatMap(postVo -> populateStats(postVo)
|
||||
.doOnNext(postVo::setStats).thenReturn(postVo)
|
||||
)
|
||||
|
@ -59,7 +70,7 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListedPostVo> convertToListedPostVo(@NonNull Post post) {
|
||||
public Mono<ListedPostVo> convertToListedVo(@NonNull Post post) {
|
||||
Assert.notNull(post, "Post must not be null");
|
||||
ListedPostVo postVo = ListedPostVo.from(post);
|
||||
postVo.setCategories(List.of());
|
||||
|
@ -105,6 +116,53 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService {
|
|||
.defaultIfEmpty(postVo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PostVo> convertToVo(Post post, String snapshotName) {
|
||||
final String postName = post.getMetadata().getName();
|
||||
final String baseSnapshotName = post.getSpec().getBaseSnapshot();
|
||||
return convertToListedVo(post)
|
||||
.map(PostVo::from)
|
||||
.flatMap(postVo -> postService.getContent(snapshotName, baseSnapshotName)
|
||||
.flatMap(wrapper -> extendPostContent(post, wrapper))
|
||||
.doOnNext(postVo::setContent)
|
||||
.thenReturn(postVo)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentVo> getContent(String postName) {
|
||||
return client.get(Post.class, postName)
|
||||
.filter(FIXED_PREDICATE)
|
||||
.flatMap(post -> {
|
||||
String releaseSnapshot = post.getSpec().getReleaseSnapshot();
|
||||
return postService.getContent(releaseSnapshot, post.getSpec().getBaseSnapshot())
|
||||
.flatMap(wrapper -> extendPostContent(post, wrapper));
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected Mono<ContentVo> extendPostContent(Post post,
|
||||
ContentWrapper wrapper) {
|
||||
Assert.notNull(post, "Post name must not be null");
|
||||
Assert.notNull(wrapper, "Post content must not be null");
|
||||
return extensionGetter.getEnabledExtensionByDefinition(ReactivePostContentHandler.class)
|
||||
.reduce(Mono.fromSupplier(() -> ReactivePostContentHandler.PostContentContext.builder()
|
||||
.post(post)
|
||||
.content(wrapper.getContent())
|
||||
.raw(wrapper.getRaw())
|
||||
.rawType(wrapper.getRawType())
|
||||
.build()
|
||||
),
|
||||
(contentMono, handler) -> contentMono.flatMap(handler::handle)
|
||||
)
|
||||
.flatMap(Function.identity())
|
||||
.map(postContent -> ContentVo.builder()
|
||||
.content(postContent.getContent())
|
||||
.raw(postContent.getRaw())
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
private <T extends ListedPostVo> Mono<StatsVo> populateStats(T postVo) {
|
||||
return counterService.getByName(MeterUtils.nameOf(Post.class, postVo.getMetadata()
|
||||
.getName())
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.SinglePageService;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||
import run.halo.app.theme.ReactiveSinglePageContentHandler;
|
||||
import run.halo.app.theme.ReactiveSinglePageContentHandler.SinglePageContentContext;
|
||||
import run.halo.app.theme.finders.ContributorFinder;
|
||||
import run.halo.app.theme.finders.SinglePageConversionService;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
|
@ -27,22 +34,62 @@ import run.halo.app.theme.finders.vo.StatsVo;
|
|||
@RequiredArgsConstructor
|
||||
public class SinglePageConversionServiceImpl implements SinglePageConversionService {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final SinglePageService singlePageService;
|
||||
|
||||
private final ContributorFinder contributorFinder;
|
||||
|
||||
private final CounterService counterService;
|
||||
|
||||
private final ExtensionGetter extensionGetter;
|
||||
|
||||
@Override
|
||||
public Mono<SinglePageVo> convertToVo(SinglePage singlePage, String snapshotName) {
|
||||
return convert(singlePage, snapshotName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SinglePageVo> convertToVo(SinglePage singlePage) {
|
||||
public Mono<SinglePageVo> convertToVo(@NonNull SinglePage singlePage) {
|
||||
return convert(singlePage, singlePage.getSpec().getReleaseSnapshot());
|
||||
}
|
||||
|
||||
protected Mono<ContentVo> extendPageContent(SinglePage singlePage,
|
||||
ContentWrapper wrapper) {
|
||||
Assert.notNull(singlePage, "SinglePage must not be null");
|
||||
Assert.notNull(wrapper, "SinglePage content must not be null");
|
||||
return extensionGetter.getEnabledExtensionByDefinition(
|
||||
ReactiveSinglePageContentHandler.class)
|
||||
.reduce(Mono.fromSupplier(() -> SinglePageContentContext.builder()
|
||||
.singlePage(singlePage)
|
||||
.content(wrapper.getContent())
|
||||
.raw(wrapper.getRaw())
|
||||
.rawType(wrapper.getRawType())
|
||||
.build()
|
||||
),
|
||||
(contentMono, handler) -> contentMono.flatMap(handler::handle)
|
||||
)
|
||||
.flatMap(Function.identity())
|
||||
.map(pageContent -> ContentVo.builder()
|
||||
.content(pageContent.getContent())
|
||||
.raw(pageContent.getRaw())
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentVo> getContent(String pageName) {
|
||||
return client.get(SinglePage.class, pageName)
|
||||
.flatMap(singlePage -> {
|
||||
String releaseSnapshot = singlePage.getSpec().getReleaseSnapshot();
|
||||
String baseSnapshot = singlePage.getSpec().getBaseSnapshot();
|
||||
return singlePageService.getContent(releaseSnapshot, baseSnapshot)
|
||||
.flatMap(wrapper -> extendPageContent(singlePage, wrapper));
|
||||
})
|
||||
.map(wrapper -> ContentVo.builder().content(wrapper.getContent())
|
||||
.raw(wrapper.getRaw()).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListedSinglePageVo> convertToListedVo(SinglePage singlePage) {
|
||||
return Mono.fromSupplier(
|
||||
|
@ -67,26 +114,19 @@ public class SinglePageConversionServiceImpl implements SinglePageConversionServ
|
|||
})
|
||||
.flatMap(this::populateStats)
|
||||
.flatMap(this::populateContributors)
|
||||
.flatMap(page -> populateContent(page, snapshotName))
|
||||
.flatMap(page -> {
|
||||
String baseSnapshot = page.getSpec().getBaseSnapshot();
|
||||
return singlePageService.getContent(snapshotName, baseSnapshot)
|
||||
.flatMap(wrapper -> extendPageContent(singlePage, wrapper))
|
||||
.doOnNext(page::setContent)
|
||||
.thenReturn(page);
|
||||
})
|
||||
.flatMap(page -> contributorFinder.getContributor(page.getSpec().getOwner())
|
||||
.doOnNext(page::setOwner)
|
||||
.thenReturn(page)
|
||||
);
|
||||
}
|
||||
|
||||
Mono<SinglePageVo> populateContent(SinglePageVo singlePageVo, String snapshotName) {
|
||||
Assert.notNull(singlePageVo, "Single page vo must not be null");
|
||||
Assert.hasText(snapshotName, "Snapshot name must not be empty");
|
||||
return singlePageService.getContent(snapshotName, singlePageVo.getSpec().getBaseSnapshot())
|
||||
.map(contentWrapper -> ContentVo.builder()
|
||||
.content(contentWrapper.getContent())
|
||||
.raw(contentWrapper.getRaw())
|
||||
.build()
|
||||
)
|
||||
.doOnNext(singlePageVo::setContent)
|
||||
.thenReturn(singlePageVo);
|
||||
}
|
||||
|
||||
<T extends ListedSinglePageVo> Mono<T> populateStats(T pageVo) {
|
||||
String name = pageVo.getMetadata().getName();
|
||||
return counterService.getByName(MeterUtils.nameOf(SinglePage.class, name))
|
||||
|
|
|
@ -12,7 +12,6 @@ import org.apache.commons.lang3.ObjectUtils;
|
|||
import org.springframework.lang.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.SinglePageService;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.extension.ListResult;
|
||||
|
@ -42,8 +41,6 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
|||
|
||||
private final SinglePageConversionService singlePagePublicQueryService;
|
||||
|
||||
private final SinglePageService singlePageService;
|
||||
|
||||
@Override
|
||||
public Mono<SinglePageVo> getByName(String pageName) {
|
||||
return client.get(SinglePage.class, pageName)
|
||||
|
@ -53,9 +50,7 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
|||
|
||||
@Override
|
||||
public Mono<ContentVo> content(String pageName) {
|
||||
return singlePageService.getReleaseContent(pageName)
|
||||
.map(wrapper -> ContentVo.builder().content(wrapper.getContent())
|
||||
.raw(wrapper.getRaw()).build());
|
||||
return singlePagePublicQueryService.getContent(pageName);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -26,7 +26,6 @@ import run.halo.app.infra.exception.NotFoundException;
|
|||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostPublicQueryService;
|
||||
import run.halo.app.theme.finders.SinglePageConversionService;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.ContributorVo;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
|
||||
|
@ -88,8 +87,7 @@ public class PreviewRouterFunction {
|
|||
}
|
||||
|
||||
private Mono<PostVo> convertToPostVo(Post post, String snapshotName) {
|
||||
return postPublicQueryService.convertToListedPostVo(post)
|
||||
.map(PostVo::from)
|
||||
return postPublicQueryService.convertToVo(post, snapshotName)
|
||||
.doOnNext(postVo -> {
|
||||
// fake some attributes only for preview when they are not published
|
||||
Post.PostSpec spec = postVo.getSpec();
|
||||
|
@ -107,17 +105,7 @@ public class PreviewRouterFunction {
|
|||
if (status.getLastModifyTime() == null) {
|
||||
status.setLastModifyTime(Instant.now());
|
||||
}
|
||||
})
|
||||
.flatMap(postVo ->
|
||||
postService.getContent(snapshotName, postVo.getSpec().getBaseSnapshot())
|
||||
.map(contentWrapper -> ContentVo.builder()
|
||||
.raw(contentWrapper.getRaw())
|
||||
.content(contentWrapper.getContent())
|
||||
.build()
|
||||
)
|
||||
.doOnNext(postVo::setContent)
|
||||
.thenReturn(postVo)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> previewSinglePage(ServerRequest request) {
|
||||
|
|
|
@ -8,3 +8,25 @@ spec:
|
|||
type: MULTI_INSTANCE
|
||||
description: "Contract for interception-style, chained processing of Web requests that may be used to
|
||||
implement cross-cutting, application-agnostic requirements such as security, timeouts, and others."
|
||||
|
||||
---
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: reactive-post-content-handler
|
||||
spec:
|
||||
className: run.halo.app.theme.ReactivePostContentHandler
|
||||
displayName: ReactivePostContentHandler
|
||||
type: MULTI_INSTANCE
|
||||
description: "Provides a way to extend the post content to be displayed in the theme-side."
|
||||
|
||||
---
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: reactive-singlepage-content-handler
|
||||
spec:
|
||||
className: run.halo.app.theme.ReactiveSinglePageContentHandler
|
||||
displayName: ReactiveSinglePageContentHandler
|
||||
type: MULTI_INSTANCE
|
||||
description: "Provides a way to extend the single page content to be displayed in the theme-side."
|
||||
|
|
|
@ -3,7 +3,6 @@ package run.halo.app.theme.finders.impl;
|
|||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static run.halo.app.theme.finders.PostPublicQueryService.FIXED_PREDICATE;
|
||||
|
||||
|
@ -20,7 +19,6 @@ import org.mockito.InjectMocks;
|
|||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ListResult;
|
||||
|
@ -31,7 +29,6 @@ import run.halo.app.theme.finders.CategoryFinder;
|
|||
import run.halo.app.theme.finders.ContributorFinder;
|
||||
import run.halo.app.theme.finders.PostPublicQueryService;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
import run.halo.app.theme.finders.vo.PostArchiveVo;
|
||||
import run.halo.app.theme.finders.vo.PostArchiveYearMonthVo;
|
||||
|
@ -69,23 +66,6 @@ class PostFinderImplTest {
|
|||
@InjectMocks
|
||||
private PostFinderImpl postFinder;
|
||||
|
||||
@Test
|
||||
void content() {
|
||||
Post post = post(1);
|
||||
post.getSpec().setReleaseSnapshot("release-snapshot");
|
||||
ContentWrapper contentWrapper = ContentWrapper.builder()
|
||||
.snapshotName("snapshot")
|
||||
.raw("raw")
|
||||
.content("content")
|
||||
.rawType("rawType")
|
||||
.build();
|
||||
when(postService.getReleaseContent(eq(post.getMetadata().getName())))
|
||||
.thenReturn(Mono.just(contentWrapper));
|
||||
ContentVo content = postFinder.content("post-1").block();
|
||||
assertThat(content.getContent()).isEqualTo(contentWrapper.getContent());
|
||||
assertThat(content.getRaw()).isEqualTo(contentWrapper.getRaw());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compare() {
|
||||
List<String> strings = posts().stream().sorted(PostFinderImpl.defaultComparator())
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.TestPost;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||
import run.halo.app.theme.ReactivePostContentHandler;
|
||||
|
||||
/**
|
||||
* Tests for {@link PostPublicQueryServiceImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.7.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PostPublicQueryServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private ExtensionGetter extensionGetter;
|
||||
|
||||
@InjectMocks
|
||||
private PostPublicQueryServiceImpl postPublicQueryService;
|
||||
|
||||
@Test
|
||||
void extendPostContent() {
|
||||
when(extensionGetter.getEnabledExtensionByDefinition(
|
||||
eq(ReactivePostContentHandler.class))).thenReturn(
|
||||
Flux.just(new PostContentHandlerB(), new PostContentHandlerA(),
|
||||
new PostContentHandlerC()));
|
||||
Post post = TestPost.postV1();
|
||||
post.getMetadata().setName("fake-post");
|
||||
ContentWrapper contentWrapper =
|
||||
ContentWrapper.builder().content("fake-content").raw("fake-raw").rawType("markdown")
|
||||
.build();
|
||||
postPublicQueryService.extendPostContent(post, contentWrapper)
|
||||
.as(StepVerifier::create).consumeNextWith(contentVo -> {
|
||||
assertThat(contentVo.getContent()).isEqualTo("fake-content-B-A-C");
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
static class PostContentHandlerA implements ReactivePostContentHandler {
|
||||
|
||||
@Override
|
||||
public Mono<PostContentContext> handle(PostContentContext postContent) {
|
||||
postContent.setContent(postContent.getContent() + "-A");
|
||||
return Mono.just(postContent);
|
||||
}
|
||||
}
|
||||
|
||||
static class PostContentHandlerB implements ReactivePostContentHandler {
|
||||
|
||||
@Override
|
||||
public Mono<PostContentContext> handle(PostContentContext postContent) {
|
||||
postContent.setContent(postContent.getContent() + "-B");
|
||||
return Mono.just(postContent);
|
||||
}
|
||||
}
|
||||
|
||||
static class PostContentHandlerC implements ReactivePostContentHandler {
|
||||
|
||||
@Override
|
||||
public Mono<PostContentContext> handle(PostContentContext postContent) {
|
||||
postContent.setContent(postContent.getContent() + "-C");
|
||||
return Mono.just(postContent);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||
import run.halo.app.theme.ReactiveSinglePageContentHandler;
|
||||
|
||||
/**
|
||||
* Tests for {@link SinglePageConversionServiceImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.7.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SinglePageConversionServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private ExtensionGetter extensionGetter;
|
||||
|
||||
@InjectMocks
|
||||
private SinglePageConversionServiceImpl pageConversionService;
|
||||
|
||||
@Test
|
||||
void extendPageContent() {
|
||||
when(extensionGetter.getEnabledExtensionByDefinition(
|
||||
eq(ReactiveSinglePageContentHandler.class)))
|
||||
.thenReturn(
|
||||
Flux.just(new PageContentHandlerB(),
|
||||
new PageContentHandlerA(),
|
||||
new PageContentHandlerC())
|
||||
);
|
||||
ContentWrapper contentWrapper = ContentWrapper.builder()
|
||||
.content("fake-content")
|
||||
.raw("fake-raw")
|
||||
.rawType("markdown")
|
||||
.build();
|
||||
SinglePage singlePage = new SinglePage();
|
||||
singlePage.setMetadata(new Metadata());
|
||||
singlePage.getMetadata().setName("fake-page");
|
||||
pageConversionService.extendPageContent(singlePage, contentWrapper)
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(contentVo -> {
|
||||
assertThat(contentVo.getContent()).isEqualTo("fake-content-B-A-C");
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
static class PageContentHandlerA implements ReactiveSinglePageContentHandler {
|
||||
|
||||
@Override
|
||||
public Mono<SinglePageContentContext> handle(
|
||||
@NonNull SinglePageContentContext pageContent) {
|
||||
pageContent.setContent(pageContent.getContent() + "-A");
|
||||
return Mono.just(pageContent);
|
||||
}
|
||||
}
|
||||
|
||||
static class PageContentHandlerB implements ReactiveSinglePageContentHandler {
|
||||
|
||||
@Override
|
||||
public Mono<SinglePageContentContext> handle(
|
||||
@NonNull SinglePageContentContext pageContent) {
|
||||
pageContent.setContent(pageContent.getContent() + "-B");
|
||||
return Mono.just(pageContent);
|
||||
}
|
||||
}
|
||||
|
||||
static class PageContentHandlerC implements ReactiveSinglePageContentHandler {
|
||||
|
||||
@Override
|
||||
public Mono<SinglePageContentContext> handle(
|
||||
@NonNull SinglePageContentContext pageContent) {
|
||||
pageContent.setContent(pageContent.getContent() + "-C");
|
||||
return Mono.just(pageContent);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ import org.springframework.web.reactive.function.server.HandlerStrategies;
|
|||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
|
@ -97,14 +96,8 @@ class PreviewRouterFunctionTest {
|
|||
|
||||
PostVo postVo = PostVo.from(post);
|
||||
postVo.setContributors(contributorVos());
|
||||
when(postPublicQueryService.convertToListedPostVo(post)).thenReturn(Mono.just(postVo));
|
||||
|
||||
ContentWrapper contentWrapper = ContentWrapper.builder()
|
||||
.raw("raw content")
|
||||
.content("formatted content")
|
||||
.build();
|
||||
when(postService.getContent(eq("snapshot1"), eq("snapshot2")))
|
||||
.thenReturn(Mono.just(contentWrapper));
|
||||
when(postPublicQueryService.convertToVo(eq(post), eq(post.getSpec().getHeadSnapshot())))
|
||||
.thenReturn(Mono.just(postVo));
|
||||
|
||||
when(viewNameResolver.resolveViewNameOrDefault(any(), eq("postTemplate"),
|
||||
eq("post"))).thenReturn(Mono.just("postView"));
|
||||
|
@ -114,7 +107,7 @@ class PreviewRouterFunctionTest {
|
|||
.expectStatus().isOk();
|
||||
|
||||
verify(viewResolver).resolveViewName(any(), any());
|
||||
verify(postService).getContent(eq("snapshot1"), eq("snapshot2"));
|
||||
verify(postPublicQueryService).convertToVo(eq(post), eq(post.getSpec().getHeadSnapshot()));
|
||||
verify(client).fetch(eq(Post.class), eq("post1"));
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
# 内容扩展点
|
||||
|
||||
## 文章内容扩展点
|
||||
|
||||
文章内容扩展点用于在主题端文章内容渲染之前对文章内容进行修改,比如添加广告、添加版权声明、插入脚本等。
|
||||
|
||||
## 使用方式
|
||||
|
||||
在插件中通过实现 `run.halo.app.theme.ReactivePostContentHandler` 接口来实现文章内容扩展。
|
||||
|
||||
以下是一个扩展文章内容支持 Katex 的示例:
|
||||
|
||||
```javascript
|
||||
String katexScript="""
|
||||
<link rel="stylesheet" href="/plugins/plugin-katex/assets/static/katex.min.css">
|
||||
<script defer src="/plugins/plugin-katex/assets/static/katex.min.js"></script>
|
||||
<script defer src="/plugins/plugin-katex/assets/static/contrib/auto-render.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
renderMathInElement(document.body, {
|
||||
// customised options
|
||||
// • auto-render specific keys, e.g.:
|
||||
delimiters: [
|
||||
{left: '$$', right: '$$', display: true},
|
||||
{left: '$', right: '$', display: false},
|
||||
{left: '\\(', right: '\\)', display: false},
|
||||
{left: '\\[', right: '\\]', display: true}
|
||||
],
|
||||
// • rendering keys, e.g.:
|
||||
throwOnError : false
|
||||
});
|
||||
});
|
||||
</script>
|
||||
""";
|
||||
```
|
||||
|
||||
然后在 `handle` 方法中将 Katex 的脚本字符串插入到内容前面:
|
||||
|
||||
```java
|
||||
|
||||
@Component
|
||||
public class KatexPostContentHandler implements ReactivePostContentHandler {
|
||||
|
||||
@Override
|
||||
public Mono<PostContentContext> handle(PostContentContext postContent) {
|
||||
postContent.setContent(katexScript + "\n" + postContent.getContent());
|
||||
return Mono.just(postContent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
定义了扩展点实现(扩展),还需要在插件的 `resources/extensions` 目录下添加对扩展的声明:
|
||||
|
||||
```yaml
|
||||
# resources/extensions/extension-definitions.yml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionDefinition
|
||||
metadata:
|
||||
name: ext-def-katex-post-content
|
||||
spec:
|
||||
className: run.halo.katex.KatexPostContentHandler
|
||||
# 文章内容扩展点的名称,固定值
|
||||
extensionPointName: reactive-post-content-handler
|
||||
displayName: "KatexPostContentHandler"
|
||||
description: "Katex support for post content."
|
||||
```
|
||||
|
||||
## 自定义页面内容扩展点
|
||||
|
||||
自定义页面(SinglePage)内容扩展点用于在主题端自定义页面内容渲染之前对内容进行修改,比如添加广告、添加版权声明、插入脚本等。
|
||||
|
||||
## 使用方式
|
||||
|
||||
在插件中通过实现 `run.halo.app.theme.ReactiveSinglePageContentHandler` 接口来实现内容扩展。
|
||||
|
||||
以下是一个扩展内容支持 Katex 的示例:
|
||||
|
||||
```java
|
||||
|
||||
@Component
|
||||
public class KatexSinglePageContentHandler implements ReactiveSinglePageContentHandler {
|
||||
|
||||
@Override
|
||||
public Mono<SinglePageContentContext> handle(SinglePageContentContext pageContent) {
|
||||
|
||||
String katexScript = ""; // 参考文章内容扩展点的示例脚本块
|
||||
pageContent.setContent(katexScript + "\n" + pageContent.getContent());
|
||||
return Mono.just(pageContent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在插件的 `resources/extensions` 目录下添加对自定义页面内容扩展的声明:
|
||||
|
||||
```yaml
|
||||
# resources/extensions/extension-definitions.yml
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionDefinition
|
||||
metadata:
|
||||
name: ext-def-katex-singlepage-content
|
||||
spec:
|
||||
className: run.halo.katex.KatexSinglePageContentHandler
|
||||
# 自定义页面内容扩展点的名称,固定值
|
||||
extensionPointName: reactive-post-content-handler
|
||||
displayName: "KatexSinglePageContentHandler"
|
||||
description: "Katex support for single page content."
|
||||
```
|
Loading…
Reference in New Issue