mirror of https://github.com/halo-dev/halo
feat: support custom rendering templates configure through themes (#2638)
#### What type of PR is this? /kind feature /milestone 2.0 /area core #### What this PR does / why we need it: 文章/自定义页面/分类支持通过主题配置多套渲染模板 首先需要在 theme.yaml 中声明模板,以文章为例: ```yaml apiVersion: theme.halo.run/v1alpha1 kind: Theme metadata: name: fake-theme-name spec: # ... customTemplates: # 支持通过以下形式配置 post, category, page post: - name: 新闻 description: 新闻类型的文章模板 screenshot: foo.png # file 路径相对于主题目录的 templates 目录,.html 后缀可以带也可以省略 # 默认 thymeleaf 查找 html 文件,除非修改了 thymeleaf suffix 配置 file: post_news.html ``` 当文章页选择模板时便可得到下拉列表以配置使用哪个模板 see https://github.com/halo-dev/halo/issues/2322#issuecomment-1215135195 选择了模板之后将模板名存储到 post 的 spec.template 中,渲染时便会首先使用 spec.template,如果该模板不存在则渲染默认模板 #### Which issue(s) this PR fixes: Fixes #2569 #### Special notes for your reviewer: how to test it? 1. 创建一篇文章并发布 2. 安装一个主题并查看文章详情,默认渲染主题提供的 post.html 页面 3. 修改此文章的 spec.template 字段为一个新的 html 例如 spec.template=post_docs 4. 查看该文章的详情页会渲染为 post_docs.html 5. 分类、自定义页面亦如是 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 文章/自定义页面/分类支持通过主题配置多套渲染模板 ```pull/2663/head
parent
c0986a4b4c
commit
1078145b18
|
@ -1,6 +1,7 @@
|
||||||
package run.halo.app.core.extension;
|
package run.halo.app.core.extension;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import java.util.List;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
@ -10,6 +11,8 @@ import run.halo.app.extension.AbstractExtension;
|
||||||
import run.halo.app.extension.GVK;
|
import run.halo.app.extension.GVK;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <p>Theme extension.</p>
|
||||||
|
*
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
@ -54,6 +57,9 @@ public class Theme extends AbstractExtension {
|
||||||
|
|
||||||
private String configMapName;
|
private String configMapName;
|
||||||
|
|
||||||
|
@Schema
|
||||||
|
private CustomTemplates customTemplates;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public String getVersion() {
|
public String getVersion() {
|
||||||
if (StringUtils.isBlank(this.version)) {
|
if (StringUtils.isBlank(this.version)) {
|
||||||
|
@ -85,4 +91,33 @@ public class Theme extends AbstractExtension {
|
||||||
|
|
||||||
private String website;
|
private String website;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class CustomTemplates {
|
||||||
|
private List<TemplateDescriptor> post;
|
||||||
|
private List<TemplateDescriptor> category;
|
||||||
|
private List<TemplateDescriptor> page;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type used to describe custom template page.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class TemplateDescriptor {
|
||||||
|
|
||||||
|
@Schema(required = true, minLength = 1)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private String screenshot;
|
||||||
|
|
||||||
|
@Schema(required = true, minLength = 1)
|
||||||
|
private String file;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package run.halo.app.theme.router;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
|
||||||
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.theme.HaloViewResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link ViewNameResolver} is used to resolve view name.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ViewNameResolver {
|
||||||
|
private final HaloViewResolver haloViewResolver;
|
||||||
|
private final ThymeleafProperties thymeleafProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves view name.
|
||||||
|
* If the {@param #name} cannot be resolved to the view, the {@param #defaultName} is returned.
|
||||||
|
*/
|
||||||
|
public Mono<String> resolveViewNameOrDefault(ServerRequest request, String name,
|
||||||
|
String defaultName) {
|
||||||
|
if (StringUtils.isBlank(name)) {
|
||||||
|
return Mono.just(defaultName);
|
||||||
|
}
|
||||||
|
final String nameToUse = processName(name);
|
||||||
|
Locale locale = LocaleContextHolder.getLocale(request.exchange().getLocaleContext());
|
||||||
|
return haloViewResolver.resolveViewName(nameToUse, locale)
|
||||||
|
.map(view -> nameToUse)
|
||||||
|
.switchIfEmpty(Mono.just(defaultName));
|
||||||
|
}
|
||||||
|
|
||||||
|
String processName(String name) {
|
||||||
|
String nameToLookup = name;
|
||||||
|
if (StringUtils.endsWith(name, thymeleafProperties.getSuffix())) {
|
||||||
|
nameToLookup = StringUtils.substringBeforeLast(name, thymeleafProperties.getSuffix());
|
||||||
|
}
|
||||||
|
return nameToLookup;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package run.halo.app.theme.router.strategy;
|
||||||
import static run.halo.app.theme.router.PageUrlUtils.pageNum;
|
import static run.halo.app.theme.router.PageUrlUtils.pageNum;
|
||||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||||
|
@ -20,6 +21,7 @@ import run.halo.app.theme.finders.vo.CategoryVo;
|
||||||
import run.halo.app.theme.finders.vo.PostVo;
|
import run.halo.app.theme.finders.vo.PostVo;
|
||||||
import run.halo.app.theme.router.PageUrlUtils;
|
import run.halo.app.theme.router.PageUrlUtils;
|
||||||
import run.halo.app.theme.router.UrlContextListResult;
|
import run.halo.app.theme.router.UrlContextListResult;
|
||||||
|
import run.halo.app.theme.router.ViewNameResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link CategoryRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
* The {@link CategoryRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||||
|
@ -35,10 +37,13 @@ public class CategoryRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||||
|
|
||||||
private final CategoryFinder categoryFinder;
|
private final CategoryFinder categoryFinder;
|
||||||
|
|
||||||
|
private final ViewNameResolver viewNameResolver;
|
||||||
|
|
||||||
public CategoryRouteStrategy(PostFinder postFinder,
|
public CategoryRouteStrategy(PostFinder postFinder,
|
||||||
CategoryFinder categoryFinder) {
|
CategoryFinder categoryFinder, ViewNameResolver viewNameResolver) {
|
||||||
this.postFinder = postFinder;
|
this.postFinder = postFinder;
|
||||||
this.categoryFinder = categoryFinder;
|
this.categoryFinder = categoryFinder;
|
||||||
|
this.viewNameResolver = viewNameResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<UrlContextListResult<PostVo>> postListByCategoryName(String name,
|
private Mono<UrlContextListResult<PostVo>> postListByCategoryName(String name,
|
||||||
|
@ -54,19 +59,27 @@ public class CategoryRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<CategoryVo> categoryByName(String name) {
|
private Mono<CategoryVo> categoryByName(String name) {
|
||||||
return Mono.defer(() -> Mono.just(categoryFinder.getByName(name)))
|
return Mono.fromCallable(() -> categoryFinder.getByName(name))
|
||||||
.publishOn(Schedulers.boundedElastic());
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
||||||
String name) {
|
String name) {
|
||||||
return request -> ServerResponse.ok()
|
return request -> {
|
||||||
.render(DefaultTemplateEnum.CATEGORY.getValue(),
|
Map<String, Object> model = new HashMap<>();
|
||||||
Map.of("name", name,
|
model.put("name", name);
|
||||||
"posts", postListByCategoryName(name, request),
|
model.put("posts", postListByCategoryName(name, request));
|
||||||
"category", categoryByName(name),
|
|
||||||
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORY.getValue()));
|
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORY.getValue());
|
||||||
|
return categoryByName(name).flatMap(categoryVo -> {
|
||||||
|
model.put("category", categoryVo);
|
||||||
|
String template = categoryVo.getSpec().getTemplate();
|
||||||
|
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||||
|
DefaultTemplateEnum.CATEGORY.getValue())
|
||||||
|
.flatMap(viewName -> ServerResponse.ok().render(viewName, model));
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,6 +17,7 @@ import run.halo.app.infra.SystemSetting;
|
||||||
import run.halo.app.theme.DefaultTemplateEnum;
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.PostFinder;
|
import run.halo.app.theme.finders.PostFinder;
|
||||||
import run.halo.app.theme.finders.vo.PostVo;
|
import run.halo.app.theme.finders.vo.PostVo;
|
||||||
|
import run.halo.app.theme.router.ViewNameResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link PostRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
* The {@link PostRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||||
|
@ -30,9 +31,11 @@ public class PostRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||||
static final String NAME_PARAM = "name";
|
static final String NAME_PARAM = "name";
|
||||||
private final GroupVersionKind groupVersionKind = GroupVersionKind.fromExtension(Post.class);
|
private final GroupVersionKind groupVersionKind = GroupVersionKind.fromExtension(Post.class);
|
||||||
private final PostFinder postFinder;
|
private final PostFinder postFinder;
|
||||||
|
private final ViewNameResolver viewNameResolver;
|
||||||
|
|
||||||
public PostRouteStrategy(PostFinder postFinder) {
|
public PostRouteStrategy(PostFinder postFinder, ViewNameResolver viewNameResolver) {
|
||||||
this.postFinder = postFinder;
|
this.postFinder = postFinder;
|
||||||
|
this.viewNameResolver = viewNameResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -49,15 +52,19 @@ public class PostRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||||
if (pathMatchInfo != null) {
|
if (pathMatchInfo != null) {
|
||||||
model.putAll(pathMatchInfo.getUriVariables());
|
model.putAll(pathMatchInfo.getUriVariables());
|
||||||
}
|
}
|
||||||
model.put("post", postByName(name));
|
|
||||||
// used by HaloTrackerProcessor
|
// used by HaloTrackerProcessor
|
||||||
model.put("groupVersionKind", groupVersionKind);
|
model.put("groupVersionKind", groupVersionKind);
|
||||||
model.put("plural", gvk.plural());
|
model.put("plural", gvk.plural());
|
||||||
// used by TemplateGlobalHeadProcessor and PostTemplateHeadProcessor
|
// used by TemplateGlobalHeadProcessor and PostTemplateHeadProcessor
|
||||||
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue());
|
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue());
|
||||||
|
return postByName(name)
|
||||||
return ServerResponse.ok()
|
.flatMap(postVo -> {
|
||||||
.render(DefaultTemplateEnum.POST.getValue(), model);
|
model.put("post", postVo);
|
||||||
|
String template = postVo.getSpec().getTemplate();
|
||||||
|
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||||
|
DefaultTemplateEnum.POST.getValue())
|
||||||
|
.flatMap(templateName -> ServerResponse.ok().render(templateName, model));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +74,7 @@ public class PostRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<PostVo> postByName(String name) {
|
private Mono<PostVo> postByName(String name) {
|
||||||
return Mono.defer(() -> Mono.just(postFinder.getByName(name)))
|
return Mono.fromCallable(() -> postFinder.getByName(name))
|
||||||
.publishOn(Schedulers.boundedElastic());
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.theme.router.strategy;
|
package run.halo.app.theme.router.strategy;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||||
|
@ -13,6 +14,7 @@ import run.halo.app.infra.SystemSetting;
|
||||||
import run.halo.app.theme.DefaultTemplateEnum;
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.SinglePageFinder;
|
import run.halo.app.theme.finders.SinglePageFinder;
|
||||||
import run.halo.app.theme.finders.vo.SinglePageVo;
|
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||||
|
import run.halo.app.theme.router.ViewNameResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link SinglePageRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
* The {@link SinglePageRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||||
|
@ -25,9 +27,12 @@ import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||||
public class SinglePageRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
public class SinglePageRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class);
|
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class);
|
||||||
private final SinglePageFinder singlePageFinder;
|
private final SinglePageFinder singlePageFinder;
|
||||||
|
private final ViewNameResolver viewNameResolver;
|
||||||
|
|
||||||
public SinglePageRouteStrategy(SinglePageFinder singlePageFinder) {
|
public SinglePageRouteStrategy(SinglePageFinder singlePageFinder,
|
||||||
|
ViewNameResolver viewNameResolver) {
|
||||||
this.singlePageFinder = singlePageFinder;
|
this.singlePageFinder = singlePageFinder;
|
||||||
|
this.viewNameResolver = viewNameResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPlural() {
|
private String getPlural() {
|
||||||
|
@ -36,22 +41,27 @@ public class SinglePageRouteStrategy implements DetailsPageRouteHandlerStrategy
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<SinglePageVo> singlePageByName(String name) {
|
private Mono<SinglePageVo> singlePageByName(String name) {
|
||||||
return Mono.defer(() -> Mono.just(singlePageFinder.getByName(name)))
|
return Mono.fromCallable(() -> singlePageFinder.getByName(name))
|
||||||
.publishOn(Schedulers.boundedElastic());
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
||||||
String name) {
|
String name) {
|
||||||
return request -> ServerResponse.ok()
|
return request -> {
|
||||||
.render(DefaultTemplateEnum.SINGLE_PAGE.getValue(),
|
Map<String, Object> model = new HashMap<>();
|
||||||
Map.of("name", name,
|
model.put("groupVersionKind", gvk);
|
||||||
"groupVersionKind", gvk,
|
model.put("plural", getPlural());
|
||||||
"plural", getPlural(),
|
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue());
|
||||||
"singlePage", singlePageByName(name),
|
|
||||||
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue()
|
return singlePageByName(name).flatMap(singlePageVo -> {
|
||||||
)
|
model.put("singlePage", singlePageVo);
|
||||||
);
|
String template = singlePageVo.getSpec().getTemplate();
|
||||||
|
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||||
|
DefaultTemplateEnum.SINGLE_PAGE.getValue())
|
||||||
|
.flatMap(viewName -> ServerResponse.ok().render(viewName, model));
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2,11 +2,15 @@ package run.halo.app.core.extension;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.skyscreamer.jsonassert.JSONAssert;
|
import org.skyscreamer.jsonassert.JSONAssert;
|
||||||
|
import org.springframework.security.util.InMemoryResource;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link Theme}.
|
* Tests for {@link Theme}.
|
||||||
|
@ -74,4 +78,75 @@ class ThemeTest {
|
||||||
assertThat(themeSpec.getVersion()).isEqualTo("1.0.0");
|
assertThat(themeSpec.getVersion()).isEqualTo("1.0.0");
|
||||||
assertThat(themeSpec.getRequire()).isEqualTo("2.0.0");
|
assertThat(themeSpec.getRequire()).isEqualTo("2.0.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void themeCustomTemplate() throws JSONException {
|
||||||
|
String themeYaml = """
|
||||||
|
apiVersion: theme.halo.run/v1alpha1
|
||||||
|
kind: Theme
|
||||||
|
metadata:
|
||||||
|
name: guqing-higan
|
||||||
|
spec:
|
||||||
|
displayName: higan
|
||||||
|
customTemplates:
|
||||||
|
post:
|
||||||
|
- name: post-template-1
|
||||||
|
description: description for post-template-1
|
||||||
|
screenshot: foo.png
|
||||||
|
file: post_template_1.html
|
||||||
|
- name: post-template-2
|
||||||
|
description: description for post-template-2
|
||||||
|
screenshot: bar.png
|
||||||
|
file: post_template_2.html
|
||||||
|
category:
|
||||||
|
- name: category-template-1
|
||||||
|
description: description for category-template-1
|
||||||
|
screenshot: foo.png
|
||||||
|
file: category_template_1.html
|
||||||
|
page:
|
||||||
|
- name: page-template-1
|
||||||
|
description: description for page-template-1
|
||||||
|
screenshot: foo.png
|
||||||
|
file: page_template_1.html
|
||||||
|
""";
|
||||||
|
List<Unstructured> unstructuredList =
|
||||||
|
new YamlUnstructuredLoader(new InMemoryResource(themeYaml)).load();
|
||||||
|
assertThat(unstructuredList).hasSize(1);
|
||||||
|
Theme theme = Unstructured.OBJECT_MAPPER.convertValue(unstructuredList.get(0), Theme.class);
|
||||||
|
assertThat(theme).isNotNull();
|
||||||
|
JSONAssert.assertEquals("""
|
||||||
|
{
|
||||||
|
"post": [
|
||||||
|
{
|
||||||
|
"name": "post-template-1",
|
||||||
|
"description": "description for post-template-1",
|
||||||
|
"screenshot": "foo.png",
|
||||||
|
"file": "post_template_1.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post-template-2",
|
||||||
|
"description": "description for post-template-2",
|
||||||
|
"screenshot": "bar.png",
|
||||||
|
"file": "post_template_2.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"category": [
|
||||||
|
{
|
||||||
|
"name": "category-template-1",
|
||||||
|
"description": "description for category-template-1",
|
||||||
|
"screenshot": "foo.png",
|
||||||
|
"file": "category_template_1.html"
|
||||||
|
}],
|
||||||
|
"page": [
|
||||||
|
{
|
||||||
|
"name": "page-template-1",
|
||||||
|
"description": "description for page-template-1",
|
||||||
|
"screenshot": "foo.png",
|
||||||
|
"file": "page_template_1.html"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
JsonUtils.objectToJson(theme.getSpec().getCustomTemplates()),
|
||||||
|
true);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package run.halo.app.theme.router;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
import org.springframework.web.reactive.result.view.View;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
import run.halo.app.theme.HaloViewResolver;
|
||||||
|
import run.halo.app.theme.router.strategy.EmptyView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ViewNameResolver}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@ExtendWith(SpringExtension.class)
|
||||||
|
class ViewNameResolverTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HaloViewResolver haloViewResolver;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ThymeleafProperties thymeleafProperties;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ViewNameResolver viewNameResolver;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
when(thymeleafProperties.getSuffix()).thenReturn(ThymeleafProperties.DEFAULT_SUFFIX);
|
||||||
|
|
||||||
|
when(haloViewResolver.resolveViewName(eq("post_news"), any()))
|
||||||
|
.thenReturn(Mono.just(Mockito.mock(View.class)));
|
||||||
|
when(haloViewResolver.resolveViewName(eq("post_docs"), any()))
|
||||||
|
.thenReturn(Mono.just(new EmptyView()));
|
||||||
|
|
||||||
|
when(haloViewResolver.resolveViewName(eq("post_nothing"), any()))
|
||||||
|
.thenReturn(Mono.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveViewNameOrDefault() throws URISyntaxException {
|
||||||
|
ServerWebExchange exchange = Mockito.mock(ServerWebExchange.class);
|
||||||
|
MockServerRequest request = MockServerRequest.builder()
|
||||||
|
.uri(new URI("/")).method(HttpMethod.GET)
|
||||||
|
.exchange(exchange)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
viewNameResolver.resolveViewNameOrDefault(request, "post_news", "post")
|
||||||
|
.as(StepVerifier::create)
|
||||||
|
.expectNext("post_news")
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
// post_docs.html
|
||||||
|
String viewName = "post_docs" + thymeleafProperties.getSuffix();
|
||||||
|
viewNameResolver.resolveViewNameOrDefault(request, viewName, "post")
|
||||||
|
.as(StepVerifier::create)
|
||||||
|
.expectNext("post_docs")
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
viewNameResolver.resolveViewNameOrDefault(request, "post_nothing", "post")
|
||||||
|
.as(StepVerifier::create)
|
||||||
|
.expectNext("post")
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processName() {
|
||||||
|
assertThat(viewNameResolver.processName("post_news")).isEqualTo("post_news");
|
||||||
|
assertThat(viewNameResolver.processName("post_news" + thymeleafProperties.getSuffix()))
|
||||||
|
.isEqualTo("post_news");
|
||||||
|
assertThat(viewNameResolver.processName("post_news.test"))
|
||||||
|
.isEqualTo("post_news.test");
|
||||||
|
assertThat(viewNameResolver.processName(null)).isNull();
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
|
import run.halo.app.theme.finders.CategoryFinder;
|
||||||
import run.halo.app.theme.finders.PostFinder;
|
import run.halo.app.theme.finders.PostFinder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,6 +30,9 @@ class CategoryRouteStrategyTest extends RouterStrategyTestSuite {
|
||||||
@Mock
|
@Mock
|
||||||
private PostFinder postFinder;
|
private PostFinder postFinder;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CategoryFinder categoryFinder;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private CategoryRouteStrategy categoryRouteStrategy;
|
private CategoryRouteStrategy categoryRouteStrategy;
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,13 @@ import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.content.TestPost;
|
import run.halo.app.content.TestPost;
|
||||||
import run.halo.app.infra.SystemSetting;
|
import run.halo.app.infra.SystemSetting;
|
||||||
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.PostFinder;
|
import run.halo.app.theme.finders.PostFinder;
|
||||||
import run.halo.app.theme.finders.vo.PostVo;
|
import run.halo.app.theme.finders.vo.PostVo;
|
||||||
|
import run.halo.app.theme.router.ViewNameResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link PostRouteStrategy}.
|
* Tests for {@link PostRouteStrategy}.
|
||||||
|
@ -29,11 +32,16 @@ class PostRouteStrategyTest extends RouterStrategyTestSuite {
|
||||||
@Mock
|
@Mock
|
||||||
private PostFinder postFinder;
|
private PostFinder postFinder;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ViewNameResolver viewNameResolver;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private PostRouteStrategy postRouteStrategy;
|
private PostRouteStrategy postRouteStrategy;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
|
lenient().when(viewNameResolver.resolveViewNameOrDefault(any(), any(), any()))
|
||||||
|
.thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue()));
|
||||||
lenient().when(postFinder.getByName(any())).thenReturn(PostVo.from(TestPost.postV1()));
|
lenient().when(postFinder.getByName(any())).thenReturn(PostVo.from(TestPost.postV1()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package run.halo.app.theme.router.strategy;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
@ -51,7 +50,7 @@ abstract class RouterStrategyTestSuite {
|
||||||
lenient().when(environmentFetcher.fetch(eq(SystemSetting.ThemeRouteRules.GROUP),
|
lenient().when(environmentFetcher.fetch(eq(SystemSetting.ThemeRouteRules.GROUP),
|
||||||
eq(SystemSetting.ThemeRouteRules.class))).thenReturn(Mono.just(getThemeRouteRules()));
|
eq(SystemSetting.ThemeRouteRules.class))).thenReturn(Mono.just(getThemeRouteRules()));
|
||||||
lenient().when(haloProperties.getExternalUrl()).thenReturn(new URI("http://example.com"));
|
lenient().when(haloProperties.getExternalUrl()).thenReturn(new URI("http://example.com"));
|
||||||
when(viewResolver.resolveViewName(any(), any()))
|
lenient().when(viewResolver.resolveViewName(any(), any()))
|
||||||
.thenReturn(Mono.just(new EmptyView()));
|
.thenReturn(Mono.just(new EmptyView()));
|
||||||
setUp();
|
setUp();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue