mirror of https://github.com/halo-dev/halo
feat: add TemplateFooterProcessor extension point for extending footer tag content in theme (#6191)
#### What type of PR is this? /kind feature /area core /milestone 2.17.x #### What this PR does / why we need it: 提供对模板中 halo footer 标签内容的扩展点以支持扩展页脚内容 #### Which issue(s) this PR fixes: Fixes #6189 #### Does this PR introduce a user-facing change? ```release-note 提供对模板中 halo footer 标签内容的扩展点以支持扩展页脚内容 ```pull/6279/head
parent
1f4bf8ea47
commit
f5ebd9fe43
|
@ -0,0 +1,20 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import org.pf4j.ExtensionPoint;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IModel;
|
||||
import org.thymeleaf.model.IProcessableElementTag;
|
||||
import org.thymeleaf.processor.element.IElementTagStructureHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Theme template <code>footer</code> tag snippet injection processor.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.17.0
|
||||
*/
|
||||
public interface TemplateFooterProcessor extends ExtensionPoint {
|
||||
|
||||
Mono<Void> process(ITemplateContext context, IProcessableElementTag tag,
|
||||
IElementTagStructureHandler structureHandler, IModel model);
|
||||
}
|
|
@ -1,14 +1,19 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import static org.thymeleaf.spring6.context.SpringContextUtils.getApplicationContext;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IModel;
|
||||
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 reactor.core.publisher.Flux;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||
|
||||
/**
|
||||
* <p>Footer element tag processor.</p>
|
||||
|
@ -42,12 +47,23 @@ public class TemplateFooterElementTagProcessor extends AbstractElementTagProcess
|
|||
@Override
|
||||
protected void doProcess(ITemplateContext context, IProcessableElementTag tag,
|
||||
IElementTagStructureHandler structureHandler) {
|
||||
|
||||
IModel modelToInsert = context.getModelFactory().createModel();
|
||||
/*
|
||||
* Obtain the Spring application context.
|
||||
*/
|
||||
final ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context);
|
||||
|
||||
String globalFooterText = getGlobalFooterText(appCtx);
|
||||
structureHandler.replaceWith(globalFooterText, false);
|
||||
modelToInsert.add(context.getModelFactory().createText(globalFooterText));
|
||||
|
||||
getTemplateFooterProcessors(context)
|
||||
.concatMap(processor -> processor.process(context, tag,
|
||||
structureHandler, modelToInsert)
|
||||
)
|
||||
.then()
|
||||
.block();
|
||||
structureHandler.replaceWith(modelToInsert, false);
|
||||
}
|
||||
|
||||
private String getGlobalFooterText(ApplicationContext appCtx) {
|
||||
|
@ -57,4 +73,13 @@ public class TemplateFooterElementTagProcessor extends AbstractElementTagProcess
|
|||
.map(SystemSetting.CodeInjection::getFooter)
|
||||
.block();
|
||||
}
|
||||
|
||||
private Flux<TemplateFooterProcessor> getTemplateFooterProcessors(ITemplateContext context) {
|
||||
var extensionGetter = getApplicationContext(context).getBeanProvider(ExtensionGetter.class)
|
||||
.getIfUnique();
|
||||
if (extensionGetter == null) {
|
||||
return Flux.empty();
|
||||
}
|
||||
return extensionGetter.getExtensions(TemplateFooterProcessor.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,3 +76,15 @@ spec:
|
|||
displayName: "搜索引擎"
|
||||
type: SINGLETON
|
||||
description: "扩展内容搜索引擎"
|
||||
|
||||
---
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: ExtensionPointDefinition
|
||||
metadata:
|
||||
name: template-footer-processor
|
||||
spec:
|
||||
className: run.halo.app.theme.dialect.TemplateFooterProcessor
|
||||
displayName: 页脚标签内容处理器
|
||||
type: MULTI_INSTANCE
|
||||
description: "提供用于扩展 <halo:footer/> 标签内容的扩展方式。"
|
||||
|
|
@ -125,6 +125,9 @@ class HaloProcessorDialectTest {
|
|||
when(fetcher.fetch(eq(SystemSetting.Basic.GROUP),
|
||||
eq(SystemSetting.Basic.class))).thenReturn(Mono.just(basic));
|
||||
|
||||
when(extensionGetter.getExtensions(TemplateFooterProcessor.class))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
Context context = getContext();
|
||||
|
||||
String result = templateEngine.process("index", context);
|
||||
|
@ -172,6 +175,9 @@ class HaloProcessorDialectTest {
|
|||
when(fetcher.fetch(eq(SystemSetting.Basic.GROUP),
|
||||
eq(SystemSetting.Basic.class))).thenReturn(Mono.just(basic));
|
||||
|
||||
when(extensionGetter.getExtensions(TemplateFooterProcessor.class))
|
||||
.thenReturn(Flux.empty());
|
||||
|
||||
String result = templateEngine.process("post", context);
|
||||
assertThat(result).isEqualTo("""
|
||||
<!DOCTYPE html>
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
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.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.HashSet;
|
||||
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.beans.factory.ObjectProvider;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IModel;
|
||||
import org.thymeleaf.model.IProcessableElementTag;
|
||||
import org.thymeleaf.processor.IProcessor;
|
||||
import org.thymeleaf.processor.element.IElementTagStructureHandler;
|
||||
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.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||
|
||||
/**
|
||||
* Tests for {@link TemplateFooterElementTagProcessor}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.17.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TemplateFooterElementTagProcessorTest {
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Mock
|
||||
ExtensionGetter extensionGetter;
|
||||
|
||||
@Mock
|
||||
private SystemConfigurableEnvironmentFetcher fetcher;
|
||||
|
||||
private TemplateEngine templateEngine;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
HaloProcessorDialect haloProcessorDialect = new MockHaloProcessorDialect();
|
||||
templateEngine = new TemplateEngine();
|
||||
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
|
||||
templateEngine.addTemplateResolver(new MockTemplateResolver());
|
||||
|
||||
SystemSetting.CodeInjection codeInjection = new SystemSetting.CodeInjection();
|
||||
codeInjection.setFooter(
|
||||
"<p>Powered by <a href=\"https://www.halo.run\" target=\"_blank\">Halo</a></p>");
|
||||
lenient().when(fetcher.fetch(eq(SystemSetting.CodeInjection.GROUP),
|
||||
eq(SystemSetting.CodeInjection.class))).thenReturn(Mono.just(codeInjection));
|
||||
|
||||
lenient().when(applicationContext.getBeanProvider(ExtensionGetter.class))
|
||||
.thenAnswer(invocation -> {
|
||||
var objectProvider = mock(ObjectProvider.class);
|
||||
when(objectProvider.getIfUnique()).thenReturn(extensionGetter);
|
||||
return objectProvider;
|
||||
});
|
||||
lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class)))
|
||||
.thenReturn(fetcher);
|
||||
}
|
||||
|
||||
@Test
|
||||
void footerProcessorTest() {
|
||||
when(extensionGetter.getExtensions(TemplateFooterProcessor.class))
|
||||
.thenReturn(Flux.just(new FakeFooterCodeInjection()));
|
||||
|
||||
String result = templateEngine.process("fake-template", getContext());
|
||||
// footer injected code is not processable
|
||||
assertThat(result).isEqualToIgnoringWhitespace("""
|
||||
<p>Powered by <a href="https://www.halo.run" target="_blank">Halo</a></p>
|
||||
<div>© 2024 guqing's blog</div>
|
||||
<div th:text="${footerText}"></div>
|
||||
""");
|
||||
}
|
||||
|
||||
private Context getContext() {
|
||||
Context context = new Context();
|
||||
context.setVariable(
|
||||
ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,
|
||||
new ThymeleafEvaluationContext(applicationContext, null));
|
||||
return context;
|
||||
}
|
||||
|
||||
static class MockTemplateResolver extends StringTemplateResolver {
|
||||
@Override
|
||||
protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration,
|
||||
String ownerTemplate, String template,
|
||||
Map<String, Object> templateResolutionAttributes) {
|
||||
return new StringTemplateResource("""
|
||||
<halo:footer />
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
static class MockHaloProcessorDialect extends HaloProcessorDialect {
|
||||
@Override
|
||||
public Set<IProcessor> getProcessors(String dialectPrefix) {
|
||||
var processors = new HashSet<IProcessor>();
|
||||
processors.add(new TemplateFooterElementTagProcessor(dialectPrefix));
|
||||
return processors;
|
||||
}
|
||||
}
|
||||
|
||||
static class FakeFooterCodeInjection implements TemplateFooterProcessor {
|
||||
|
||||
@Override
|
||||
public Mono<Void> process(ITemplateContext context, IProcessableElementTag tag,
|
||||
IElementTagStructureHandler structureHandler, IModel model) {
|
||||
var factory = context.getModelFactory();
|
||||
// regular footer text
|
||||
var copyRight = factory.createText("<div>© 2024 guqing's blog</div>");
|
||||
model.add(copyRight);
|
||||
// variable footer text
|
||||
model.add(factory.createText("<div th:text=\"${footerText}\"></div>"));
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue