mirror of https://github.com/halo-dev/halo
feat: support code snippet injection for theme template (#2377)
<!-- Thanks for sending a pull request! Here are some tips for you: 1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>。 1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>. 2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。 2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. 3. 请确保你已经添加并运行了适当的测试。 3. Ensure you have added or ran the appropriate tests for your PR. --> #### What type of PR is this? /kind feature /milestone 2.0 /area core <!-- 添加其中一个类别: Add one of the following kinds: /kind bug /kind cleanup /kind documentation /kind feature /kind improvement 适当添加其中一个或多个类别(可选): Optionally add one or more of the following kinds if applicable: /kind api-change /kind deprecation /kind failing-test /kind flake /kind regression --> #### What this PR does / why we need it: 主题模板渲染时支持代码片段自动注入 如何测试: <img width="644" alt="image" src="https://user-images.githubusercontent.com/38999863/188456203-e43c56b0-a886-4feb-bc87-ed17b97ecc4d.png"> 1. 文章页 head 注入 对于文章当spec.htmlMetas字段存在数据时访问文章页面 post.html 会自动在该页面的 head 中插入 meta tag 例如 htmlMetas = [{"name":"keywords", "content": "Halo,Blog"}, {"property":"org:name", "content": "fake-name"}] 则在 post.html返回结果的 `<head></head>`中能看到 ``` <meta name="keywords" content="Halo,Blog" /> <meta property="org:name" content="fake-name" /> ``` 2. 内容页注入 当在后台设置->代码注入的`内容页 head`中填写了内容则访问 post.html 和 page.html 时会自动注入到 head 标签中(需要主题的是 page.html 为自定义页面,该功能在另一个 PR 中还未合并,因此内容页注入目前只适用 post.html) 3. 全局 head 当在后台设置->代码注入的`全局 head` 中填写的内容,访问任意模板页面时都会自动注入到 head 标签中 4. 页脚 页脚自定注入不适合,一般由主题开发者选择位置注入,因此提供了一个`<halo:footer></halo:footer>`标签,当在模板页面使用了此标签则 后台设置->代码注入的`页脚`处填写的内容在渲染后会替换此标签 #### Which issue(s) this PR fixes: <!-- PR 合并时自动关闭 issue。 Automatically closes linked issue when PR is merged. 用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)` Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`. --> Fixes #2376 #### Special notes for your reviewer: /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? <!-- 如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`。 否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新(Break Change), Release Note 需要以 `action required` 开头。 If no, just write "NONE" in the release-note block below. If yes, a release note is required: Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required". --> ```release-note None ```pull/2382/head^2
parent
de40a108e2
commit
53ee2ea547
|
@ -27,4 +27,15 @@ public class SystemSetting {
|
|||
private String post;
|
||||
private String tags;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CodeInjection {
|
||||
public static final String GROUP = "codeInjection";
|
||||
|
||||
private String globalHead;
|
||||
|
||||
private String contentHead;
|
||||
|
||||
private String footer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;
|
|||
import org.thymeleaf.templateresolver.FileTemplateResolver;
|
||||
import org.thymeleaf.templateresolver.ITemplateResolver;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.theme.dialect.HaloProcessorDialect;
|
||||
import run.halo.app.theme.engine.SpringWebFluxTemplateEngine;
|
||||
import run.halo.app.theme.message.ThemeMessageResolver;
|
||||
|
||||
|
@ -80,6 +81,7 @@ public class TemplateEngineManager {
|
|||
var mainResolver = haloTemplateResolver();
|
||||
mainResolver.setPrefix(theme.getPath() + "/templates/");
|
||||
engine.addTemplateResolver(mainResolver);
|
||||
engine.addDialect(new HaloProcessorDialect());
|
||||
|
||||
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
|
||||
dialects.orderedStream().forEach(engine::addDialect);
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IModel;
|
||||
import org.thymeleaf.model.IModelFactory;
|
||||
import org.thymeleaf.processor.element.AbstractElementModelProcessor;
|
||||
import org.thymeleaf.processor.element.IElementModelStructureHandler;
|
||||
import org.thymeleaf.spring6.context.SpringContextUtils;
|
||||
import org.thymeleaf.templatemode.TemplateMode;
|
||||
|
||||
/**
|
||||
* Global head injection processor.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class GlobalHeadInjectionProcessor extends AbstractElementModelProcessor {
|
||||
/**
|
||||
* Inserting tag will re-trigger this processor, in order to avoid the loop out trigger,
|
||||
* this flag is required to prevent the loop problem.
|
||||
*/
|
||||
private static final String PROCESS_FLAG =
|
||||
GlobalHeadInjectionProcessor.class.getName() + ".PROCESSED";
|
||||
|
||||
private static final String TAG_NAME = "head";
|
||||
private static final int PRECEDENCE = 1000;
|
||||
|
||||
public GlobalHeadInjectionProcessor(final String dialectPrefix) {
|
||||
super(
|
||||
TemplateMode.HTML, // This processor will apply only to HTML mode
|
||||
dialectPrefix, // Prefix to be applied to name for matching
|
||||
TAG_NAME, // Tag name: match specifically this tag
|
||||
false, // Apply dialect prefix to tag name
|
||||
null, // No attribute name: will match by tag name
|
||||
false, // No prefix to be applied to attribute name
|
||||
PRECEDENCE); // Precedence (inside dialect's own precedence)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doProcess(ITemplateContext context, IModel model,
|
||||
IElementModelStructureHandler structureHandler) {
|
||||
|
||||
// note that this is important!!
|
||||
Object processedAlready = context.getVariable(PROCESS_FLAG);
|
||||
if (processedAlready != null) {
|
||||
return;
|
||||
}
|
||||
structureHandler.setLocalVariable(PROCESS_FLAG, true);
|
||||
|
||||
// handle <head> tag
|
||||
/*
|
||||
* Obtain the Spring application context.
|
||||
*/
|
||||
final ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context);
|
||||
|
||||
/*
|
||||
* Create the DOM structure that will be substituting our custom tag.
|
||||
* The headline will be shown inside a '<div>' tag, and so this must
|
||||
* be created first and then a Text node must be added to it.
|
||||
*/
|
||||
final IModelFactory modelFactory = context.getModelFactory();
|
||||
IModel modelToInsert = modelFactory.createModel();
|
||||
|
||||
// apply processors to modelToInsert
|
||||
Collection<TemplateHeadProcessor> templateHeadProcessors =
|
||||
getTemplateHeadProcessors(appCtx);
|
||||
for (TemplateHeadProcessor processor : templateHeadProcessors) {
|
||||
processor.process(context, modelToInsert, structureHandler)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
// add to target model
|
||||
model.insertModel(model.size() - 1, modelToInsert);
|
||||
}
|
||||
|
||||
private Collection<TemplateHeadProcessor> getTemplateHeadProcessors(ApplicationContext ctx) {
|
||||
return ctx.getBeansOfType(TemplateHeadProcessor.class)
|
||||
.values();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.thymeleaf.dialect.AbstractProcessorDialect;
|
||||
import org.thymeleaf.processor.IProcessor;
|
||||
import org.thymeleaf.standard.StandardDialect;
|
||||
|
||||
/**
|
||||
* Thymeleaf processor dialect for Halo.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class HaloProcessorDialect extends AbstractProcessorDialect {
|
||||
private static final String DIALECT_NAME = "Halo Theme Dialect";
|
||||
|
||||
public HaloProcessorDialect() {
|
||||
// We will set this dialect the same "dialect processor" precedence as
|
||||
// the Standard Dialect, so that processor executions can interleave.
|
||||
super(DIALECT_NAME, "halo", StandardDialect.PROCESSOR_PRECEDENCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<IProcessor> getProcessors(String dialectPrefix) {
|
||||
final Set<IProcessor> processors = new HashSet<IProcessor>();
|
||||
// add more processors
|
||||
processors.add(new GlobalHeadInjectionProcessor(dialectPrefix));
|
||||
processors.add(new TemplateFooterElementTagProcessor(dialectPrefix));
|
||||
return processors;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IModel;
|
||||
import org.thymeleaf.model.IModelFactory;
|
||||
import org.thymeleaf.processor.element.IElementModelStructureHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
|
||||
/**
|
||||
* <p>The <code>head</code> html snippet injection processor for post template.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class PostTemplateHeadProcessor implements TemplateHeadProcessor {
|
||||
private static final String POST_NAME_VARIABLE = "name";
|
||||
private final PostFinder postFinder;
|
||||
|
||||
|
||||
public PostTemplateHeadProcessor(PostFinder postFinder) {
|
||||
this.postFinder = postFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> process(ITemplateContext context, IModel model,
|
||||
IElementModelStructureHandler structureHandler) {
|
||||
return Mono.just(context.getTemplateData().getTemplate())
|
||||
.filter(this::isPostTemplate)
|
||||
.map(template -> (String) context.getVariable(POST_NAME_VARIABLE))
|
||||
.map(postFinder::getByName)
|
||||
.doOnNext(postVo -> {
|
||||
List<Map<String, String>> htmlMetas = postVo.getHtmlMetas();
|
||||
String metaHtml = headMetaBuilder(htmlMetas);
|
||||
IModelFactory modelFactory = context.getModelFactory();
|
||||
model.add(modelFactory.createText(metaHtml));
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
private String headMetaBuilder(List<Map<String, String>> htmlMetas) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map<String, String> htmlMeta : htmlMetas) {
|
||||
sb.append("<meta");
|
||||
htmlMeta.forEach((k, v) -> {
|
||||
sb.append(" ").append(k).append("=\"").append(v).append("\"");
|
||||
});
|
||||
sb.append(" />\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private boolean isPostTemplate(String template) {
|
||||
return DefaultTemplateEnum.POST.getValue().equals(template);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IProcessableElementTag;
|
||||
import org.thymeleaf.processor.element.AbstractElementTagProcessor;
|
||||
import org.thymeleaf.processor.element.IElementTagStructureHandler;
|
||||
import org.thymeleaf.spring6.context.SpringContextUtils;
|
||||
import org.thymeleaf.templatemode.TemplateMode;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
|
||||
/**
|
||||
* <p>Footer element tag processor.</p>
|
||||
* <p>Replace the footer tag <code><halo:footer /></code> with the contents of the footer
|
||||
* field of the global configuration item.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class TemplateFooterElementTagProcessor extends AbstractElementTagProcessor {
|
||||
|
||||
private static final String TAG_NAME = "footer";
|
||||
private static final int PRECEDENCE = 1000;
|
||||
|
||||
/**
|
||||
* Constructor footer element tag processor with HTML mode, dialect prefix, footer tag name.
|
||||
*
|
||||
* @param dialectPrefix dialect prefix
|
||||
*/
|
||||
public TemplateFooterElementTagProcessor(final String dialectPrefix) {
|
||||
super(
|
||||
TemplateMode.HTML, // This processor will apply only to HTML mode
|
||||
dialectPrefix, // Prefix to be applied to name for matching
|
||||
TAG_NAME, // Tag name: match specifically this tag
|
||||
true, // Apply dialect prefix to tag name
|
||||
null, // No attribute name: will match by tag name
|
||||
false, // No prefix to be applied to attribute name
|
||||
PRECEDENCE); // Precedence (inside dialect's own precedence)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doProcess(ITemplateContext context, IProcessableElementTag tag,
|
||||
IElementTagStructureHandler structureHandler) {
|
||||
/*
|
||||
* Obtain the Spring application context.
|
||||
*/
|
||||
final ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context);
|
||||
String globalFooterText = getGlobalFooterText(appCtx);
|
||||
structureHandler.replaceWith(globalFooterText, false);
|
||||
}
|
||||
|
||||
private String getGlobalFooterText(ApplicationContext appCtx) {
|
||||
SystemConfigurableEnvironmentFetcher fetcher =
|
||||
appCtx.getBean(SystemConfigurableEnvironmentFetcher.class);
|
||||
return fetcher.fetch(SystemSetting.CodeInjection.GROUP, SystemSetting.CodeInjection.class)
|
||||
.map(SystemSetting.CodeInjection::getFooter)
|
||||
.block();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IModel;
|
||||
import org.thymeleaf.model.IModelFactory;
|
||||
import org.thymeleaf.processor.element.IElementModelStructureHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
|
||||
/**
|
||||
* <p>Global custom head snippet injection for theme global setting.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class TemplateGlobalHeadProcessor implements TemplateHeadProcessor {
|
||||
|
||||
private final SystemConfigurableEnvironmentFetcher fetcher;
|
||||
|
||||
public TemplateGlobalHeadProcessor(SystemConfigurableEnvironmentFetcher fetcher) {
|
||||
this.fetcher = fetcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> process(ITemplateContext context, IModel model,
|
||||
IElementModelStructureHandler structureHandler) {
|
||||
final IModelFactory modelFactory = context.getModelFactory();
|
||||
return fetchCodeInjection()
|
||||
.doOnNext(codeInjection -> {
|
||||
String globalHeader = codeInjection.getGlobalHead();
|
||||
if (StringUtils.isNotBlank(globalHeader)) {
|
||||
model.add(modelFactory.createText(globalHeader + "\n"));
|
||||
}
|
||||
|
||||
// add content head to model
|
||||
String contentHeader = codeInjection.getContentHead();
|
||||
String template = context.getTemplateData().getTemplate();
|
||||
if (StringUtils.isNotBlank(contentHeader) && isContentTemplate(template)) {
|
||||
model.add(modelFactory.createText(contentHeader + "\n"));
|
||||
}
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
private Mono<SystemSetting.CodeInjection> fetchCodeInjection() {
|
||||
return fetcher.fetch(SystemSetting.CodeInjection.GROUP, SystemSetting.CodeInjection.class);
|
||||
}
|
||||
|
||||
private boolean isContentTemplate(String template) {
|
||||
// TODO includes custom page template
|
||||
return DefaultTemplateEnum.POST.getValue().equals(template);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IModel;
|
||||
import org.thymeleaf.processor.element.IElementModelStructureHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Theme template <code>head</code> tag snippet injection processor.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TemplateHeadProcessor {
|
||||
|
||||
Mono<Void> process(ITemplateContext context, IModel model,
|
||||
IElementModelStructureHandler structureHandler);
|
||||
}
|
|
@ -28,9 +28,10 @@ public class PermalinkPatternProvider {
|
|||
return client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
|
||||
.map(configMap -> {
|
||||
Map<String, String> data = configMap.getData();
|
||||
String routeRulesJson = data.get(SystemSetting.ThemeRouteRules.GROUP);
|
||||
return JsonUtils.jsonToObject(routeRulesJson, SystemSetting.ThemeRouteRules.class);
|
||||
return data.get(SystemSetting.ThemeRouteRules.GROUP);
|
||||
})
|
||||
.map(routeRulesJson -> JsonUtils.jsonToObject(routeRulesJson,
|
||||
SystemSetting.ThemeRouteRules.class))
|
||||
.orElseGet(() -> {
|
||||
SystemSetting.ThemeRouteRules themeRouteRules = new SystemSetting.ThemeRouteRules();
|
||||
themeRouteRules.setArchives("archives");
|
||||
|
|
|
@ -9,8 +9,13 @@ data:
|
|||
}
|
||||
routeRules: |
|
||||
{
|
||||
"categories": "/categories",
|
||||
"archives": "/archives",
|
||||
"categories": "categories",
|
||||
"archives": "archives",
|
||||
"post": "/archives/{slug}",
|
||||
"tags": "/tags"
|
||||
"tags": "tags"
|
||||
}
|
||||
codeInjection: |
|
||||
{
|
||||
"globalHead": "",
|
||||
"footer": ""
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.spring6.dialect.SpringStandardDialect;
|
||||
import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;
|
||||
import org.thymeleaf.templateresolver.StringTemplateResolver;
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
import org.thymeleaf.templateresource.StringTemplateResource;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
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.vo.PostVo;
|
||||
|
||||
/**
|
||||
* Tests for {@link HaloProcessorDialect}.
|
||||
*
|
||||
* @author guqing
|
||||
* @see HaloProcessorDialect
|
||||
* @see GlobalHeadInjectionProcessor
|
||||
* @see PostTemplateHeadProcessor
|
||||
* @see TemplateHeadProcessor
|
||||
* @see TemplateGlobalHeadProcessor
|
||||
* @see TemplateFooterElementTagProcessor
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HaloProcessorDialectTest {
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
||||
@Mock
|
||||
private SystemConfigurableEnvironmentFetcher fetcher;
|
||||
|
||||
private TemplateEngine templateEngine;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
|
||||
templateEngine = new TemplateEngine();
|
||||
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
|
||||
templateEngine.addTemplateResolver(new TestTemplateResolver());
|
||||
|
||||
Map<String, TemplateHeadProcessor> map = new HashMap<>();
|
||||
map.put("postTemplateHeadProcessor", new PostTemplateHeadProcessor(postFinder));
|
||||
map.put("templateGlobalHeadProcessor", new TemplateGlobalHeadProcessor(fetcher));
|
||||
lenient().when(applicationContext.getBeansOfType(TemplateHeadProcessor.class))
|
||||
.thenReturn(map);
|
||||
|
||||
SystemSetting.CodeInjection codeInjection = new SystemSetting.CodeInjection();
|
||||
codeInjection.setContentHead("<meta name=\"content-head-test\" content=\"test\" />");
|
||||
codeInjection.setGlobalHead("<meta name=\"global-head-test\" content=\"test\" />");
|
||||
codeInjection.setFooter("<footer>hello this is global footer.</footer>");
|
||||
when(fetcher.fetch(eq(SystemSetting.CodeInjection.GROUP),
|
||||
eq(SystemSetting.CodeInjection.class))).thenReturn(Mono.just(codeInjection));
|
||||
|
||||
when(applicationContext.getBean(SystemConfigurableEnvironmentFetcher.class))
|
||||
.thenReturn(fetcher);
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalHeadAndFooterProcessors() {
|
||||
Context context = getContext();
|
||||
|
||||
String result = templateEngine.process("index", context);
|
||||
assertThat(result).isEqualTo("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Index</title>
|
||||
<meta name="global-head-test" content="test" />
|
||||
</head>
|
||||
<body>
|
||||
<p>index</p>
|
||||
<div class="footer">
|
||||
<footer>hello this is global footer.</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentHeadAndFooterAndPostProcessors() {
|
||||
Context context = getContext();
|
||||
context.setVariable("name", "fake-post");
|
||||
|
||||
List<Map<String, String>> htmlMetas = new ArrayList<>();
|
||||
htmlMetas.add(ImmutableSortedMap.of("name", "post-meta-V1", "content", "post-meta-V1"));
|
||||
htmlMetas.add(ImmutableSortedMap.of("name", "post-meta-V2", "content", "post-meta-V2"));
|
||||
PostVo postVo = PostVo.builder()
|
||||
.htmlMetas(htmlMetas)
|
||||
.name("fake-post").build();
|
||||
when(postFinder.getByName(eq("fake-post"))).thenReturn(postVo);
|
||||
|
||||
String result = templateEngine.process("post", context);
|
||||
assertThat(result).isEqualTo("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Post</title>
|
||||
<meta content="post-meta-V1" name="post-meta-V1" />
|
||||
<meta content="post-meta-V2" name="post-meta-V2" />
|
||||
<meta name="global-head-test" content="test" />
|
||||
<meta name="content-head-test" content="test" />
|
||||
</head>
|
||||
<body>
|
||||
<p>post</p>
|
||||
<div class="footer">
|
||||
<footer>hello this is global footer.</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
}
|
||||
|
||||
private Context getContext() {
|
||||
Context context = new Context();
|
||||
context.setVariable(
|
||||
ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,
|
||||
new ThymeleafEvaluationContext(applicationContext, null));
|
||||
return context;
|
||||
}
|
||||
|
||||
static class TestTemplateResolver extends StringTemplateResolver {
|
||||
@Override
|
||||
protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration,
|
||||
String ownerTemplate, String template,
|
||||
Map<String, Object> templateResolutionAttributes) {
|
||||
if (template.equals(DefaultTemplateEnum.INDEX.getValue())) {
|
||||
return new StringTemplateResource(indexTemplate());
|
||||
}
|
||||
|
||||
if (template.equals(DefaultTemplateEnum.POST.getValue())) {
|
||||
return new StringTemplateResource(postTemplate());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String indexTemplate() {
|
||||
return commonTemplate().formatted("Index", """
|
||||
<p>index</p>
|
||||
<div class="footer">
|
||||
<halo:footer></halo:footer>
|
||||
</div>
|
||||
""");
|
||||
}
|
||||
|
||||
private String postTemplate() {
|
||||
return commonTemplate().formatted("Post", """
|
||||
<p>post</p>
|
||||
<div class="footer">
|
||||
<halo:footer></halo:footer>
|
||||
</div>
|
||||
""");
|
||||
}
|
||||
|
||||
private String commonTemplate() {
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>%s</title>
|
||||
</head>
|
||||
<body>
|
||||
%s
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue