Skip to content

Commit

Permalink
feat: add content extension points for post and single page on theme-…
Browse files Browse the repository at this point in the history
…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](9b2b9f1)

#### Which issue(s) this PR fixes:

Fixes #4003

#### Does this PR introduce a user-facing change?

```release-note
为主题端的文章和自定义页面内容添加扩展点
```
  • Loading branch information
guqing committed Jun 28, 2023
1 parent 972ebed commit cabcd98
Show file tree
Hide file tree
Showing 17 changed files with 571 additions and 88 deletions.
9 changes: 5 additions & 4 deletions api/src/main/java/run/halo/app/core/extension/Plugin.java
Expand Up @@ -61,10 +61,11 @@ public static class PluginSpec {
*
* @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;
Expand Down
@@ -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;
}
}
Expand Up @@ -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));
});
}
Expand All @@ -42,7 +42,7 @@ protected void checkBaseSnapshot(Snapshot snapshot) {
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()));
Expand Down
Expand Up @@ -7,7 +7,10 @@
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()
Expand All @@ -34,5 +37,27 @@ Mono<ListResult<ListedPostVo>> list(Integer page, Integer size,
* @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;

Expand All @@ -13,9 +16,40 @@
*/
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);
}
Expand Up @@ -18,7 +18,6 @@
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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -107,7 +99,7 @@ private Mono<PostVo> fetchByName(String name) {
@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,
Expand Down
Expand Up @@ -2,6 +2,7 @@

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;
Expand All @@ -11,16 +12,22 @@
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
Expand All @@ -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) {
Expand All @@ -45,7 +56,7 @@ public Mono<ListResult<ListedPostVo>> list(Integer page, Integer size,
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)
)
Expand All @@ -59,7 +70,7 @@ comparator, pageNullSafe(page), sizeNullSafe(size))
}

@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());
Expand Down Expand Up @@ -105,6 +116,53 @@ public Mono<ListedPostVo> convertToListedPostVo(@NonNull Post post) {
.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())
Expand Down

0 comments on commit cabcd98

Please sign in to comment.