diff --git a/api/src/main/java/run/halo/app/theme/dialect/ElementTagPostProcessor.java b/api/src/main/java/run/halo/app/theme/dialect/ElementTagPostProcessor.java
new file mode 100644
index 00000000000..88d4828d3f9
--- /dev/null
+++ b/api/src/main/java/run/halo/app/theme/dialect/ElementTagPostProcessor.java
@@ -0,0 +1,40 @@
+package run.halo.app.theme.dialect;
+
+import org.pf4j.ExtensionPoint;
+import org.thymeleaf.context.ITemplateContext;
+import org.thymeleaf.model.IProcessableElementTag;
+import org.thymeleaf.processor.element.IElementTagStructureHandler;
+import reactor.core.publisher.Mono;
+
+/**
+ * An extension point for post-processing element tag.
+ *
+ * @author johnniang
+ * @since 2.20.0
+ */
+public interface ElementTagPostProcessor extends ExtensionPoint {
+
+ /**
+ *
+ * Execute the processor.
+ *
+ *
+ * The {@link IProcessableElementTag} object argument is immutable, so all modifications to
+ * this object or any
+ * instructions to be given to the engine should be done through the specified
+ * {@link IElementTagStructureHandler} handler.
+ *
+ *
+ * @param context the execution context.
+ * @param tag the event this processor is executing on.
+ * @param structureHandler the handler that will centralise modifications and commands to the
+ * engine.
+ * @return a {@link Mono} that will complete when processing finishes or empty mono if
+ * not support.
+ */
+ Mono process(
+ final ITemplateContext context,
+ final IProcessableElementTag tag,
+ final IElementTagStructureHandler structureHandler);
+
+}
diff --git a/application/src/main/java/run/halo/app/theme/TemplateEngineManager.java b/application/src/main/java/run/halo/app/theme/TemplateEngineManager.java
index 36ac57da3ba..bf99d52a237 100644
--- a/application/src/main/java/run/halo/app/theme/TemplateEngineManager.java
+++ b/application/src/main/java/run/halo/app/theme/TemplateEngineManager.java
@@ -19,6 +19,7 @@
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.plugin.HaloPluginManager;
+import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.app.theme.dialect.HaloProcessorDialect;
import run.halo.app.theme.engine.HaloTemplateEngine;
import run.halo.app.theme.engine.PluginClassloaderTemplateResolver;
@@ -56,16 +57,22 @@ public class TemplateEngineManager {
private final ThemeResolver themeResolver;
+ private final ExtensionGetter extensionGetter;
+
public TemplateEngineManager(ThymeleafProperties thymeleafProperties,
ExternalUrlSupplier externalUrlSupplier,
- HaloPluginManager haloPluginManager, ObjectProvider templateResolvers,
- ObjectProvider dialects, ThemeResolver themeResolver) {
+ HaloPluginManager haloPluginManager,
+ ObjectProvider templateResolvers,
+ ObjectProvider dialects,
+ ThemeResolver themeResolver,
+ ExtensionGetter extensionGetter) {
this.thymeleafProperties = thymeleafProperties;
this.externalUrlSupplier = externalUrlSupplier;
this.haloPluginManager = haloPluginManager;
this.templateResolvers = templateResolvers;
this.dialects = dialects;
this.themeResolver = themeResolver;
+ this.extensionGetter = extensionGetter;
engineCache = new ConcurrentLruCache<>(CACHE_SIZE_LIMIT, this::templateEngineGenerator);
}
@@ -134,7 +141,7 @@ public IStandardVariableExpressionEvaluator getVariableExpressionEvaluator() {
return ReactiveSpelVariableExpressionEvaluator.INSTANCE;
}
});
- engine.addDialect(new HaloProcessorDialect());
+ engine.addDialect(new HaloProcessorDialect(extensionGetter));
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
diff --git a/application/src/main/java/run/halo/app/theme/dialect/ElementTagPostProcessors.java b/application/src/main/java/run/halo/app/theme/dialect/ElementTagPostProcessors.java
new file mode 100644
index 00000000000..7f97d0c50ce
--- /dev/null
+++ b/application/src/main/java/run/halo/app/theme/dialect/ElementTagPostProcessors.java
@@ -0,0 +1,56 @@
+package run.halo.app.theme.dialect;
+
+import java.time.Duration;
+import lombok.extern.slf4j.Slf4j;
+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.templatemode.TemplateMode;
+import run.halo.app.plugin.extensionpoint.ExtensionGetter;
+
+/**
+ * Element tag processors.
+ *
+ * @author johnniang
+ * @since 2.20.0
+ */
+@Slf4j
+public class ElementTagPostProcessors extends AbstractElementTagProcessor {
+
+ private static final int PRECEDENCE = Integer.MAX_VALUE;
+
+ private final ExtensionGetter extensionGetter;
+
+ public ElementTagPostProcessors(ExtensionGetter extensionGetter) {
+ super(TemplateMode.HTML,
+ null,
+ null,
+ false,
+ null,
+ false,
+ PRECEDENCE);
+ this.extensionGetter = extensionGetter;
+ }
+
+ @Override
+ protected void doProcess(
+ ITemplateContext context,
+ IProcessableElementTag tag,
+ IElementTagStructureHandler structureHandler
+ ) {
+ extensionGetter.getExtensions(ElementTagPostProcessor.class)
+ .concatMap(processor -> processor.process(context, tag, structureHandler)
+ .doOnSuccess(v -> {
+ if (log.isDebugEnabled()) {
+ log.debug("Processed tag [{}] with processor [{}]",
+ tag.getElementCompleteName(), processor.getClass().getName()
+ );
+ }
+ })
+ )
+ .then()
+ .block(Duration.ofSeconds(20));
+ }
+
+}
diff --git a/application/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java b/application/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java
index 6d61b3c48ee..6d9fdff659f 100644
--- a/application/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java
+++ b/application/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java
@@ -7,6 +7,7 @@
import org.thymeleaf.expression.IExpressionObjectFactory;
import org.thymeleaf.processor.IProcessor;
import org.thymeleaf.standard.StandardDialect;
+import run.halo.app.plugin.extensionpoint.ExtensionGetter;
/**
* Thymeleaf processor dialect for Halo.
@@ -21,10 +22,13 @@ public class HaloProcessorDialect extends AbstractProcessorDialect implements
private static final IExpressionObjectFactory HALO_EXPRESSION_OBJECTS_FACTORY =
new HaloExpressionObjectFactory();
- public HaloProcessorDialect() {
+ private final ExtensionGetter extensionGetter;
+
+ public HaloProcessorDialect(ExtensionGetter extensionGetter) {
// 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);
+ this.extensionGetter = extensionGetter;
}
@Override
@@ -36,6 +40,7 @@ public Set getProcessors(String dialectPrefix) {
processors.add(new JsonNodePropertyAccessorBoundariesProcessor());
processors.add(new CommentElementTagProcessor(dialectPrefix));
processors.add(new CommentEnabledVariableProcessor());
+ processors.add(new ElementTagPostProcessors(extensionGetter));
return processors;
}
diff --git a/application/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java b/application/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java
index cd6f5bd02dd..2550bbf8394 100644
--- a/application/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java
+++ b/application/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java
@@ -3,6 +3,7 @@
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.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -29,6 +30,8 @@
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.utils.JsonUtils;
+import run.halo.app.plugin.extensionpoint.ExtensionGetter;
+import run.halo.app.theme.dialect.ElementTagPostProcessor;
import run.halo.app.theme.dialect.HaloProcessorDialect;
/**
@@ -47,11 +50,15 @@ public class ReactiveFinderExpressionParserTests {
@Mock
private SystemConfigurableEnvironmentFetcher environmentFetcher;
+ @Mock
+ ExtensionGetter extensionGetter;
+
private TemplateEngine templateEngine;
@BeforeEach
void setUp() {
- HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
+ when(extensionGetter.getExtensions(ElementTagPostProcessor.class)).thenReturn(Flux.empty());
+ HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect(extensionGetter);
templateEngine = new TemplateEngine();
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect() {
@Override
diff --git a/application/src/test/java/run/halo/app/theme/dialect/CommentElementTagProcessorTest.java b/application/src/test/java/run/halo/app/theme/dialect/CommentElementTagProcessorTest.java
index 2a0726b2e7d..1c8d16aac74 100644
--- a/application/src/test/java/run/halo/app/theme/dialect/CommentElementTagProcessorTest.java
+++ b/application/src/test/java/run/halo/app/theme/dialect/CommentElementTagProcessorTest.java
@@ -55,12 +55,13 @@ class CommentElementTagProcessorTest {
@BeforeEach
void setUp() {
- HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
+ HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect(extensionGetter);
templateEngine = new TemplateEngine();
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
templateEngine.addTemplateResolver(new TestTemplateResolver());
lenient().when(applicationContext.getBean(eq(ExtensionGetter.class)))
.thenReturn(extensionGetter);
+ when(extensionGetter.getExtensions(ElementTagPostProcessor.class)).thenReturn(Flux.empty());
}
@Test
diff --git a/application/src/test/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessorIntegrationTest.java b/application/src/test/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessorIntegrationTest.java
index 2ab16d454e1..92b51e3e815 100644
--- a/application/src/test/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessorIntegrationTest.java
+++ b/application/src/test/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessorIntegrationTest.java
@@ -74,7 +74,7 @@ class ContentTemplateHeadProcessorIntegrationTest {
@BeforeEach
void setUp() {
- HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
+ HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect(extensionGetter);
templateEngine = new TemplateEngine();
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
templateEngine.addTemplateResolver(new TestTemplateResolver());
@@ -114,6 +114,7 @@ void setUp() {
lenient().when(extensionGetter.getExtensions(TemplateHeadProcessor.class)).thenReturn(
Flux.fromIterable(map.values()).sort(AnnotationAwareOrderComparator.INSTANCE)
);
+ when(extensionGetter.getExtensions(ElementTagPostProcessor.class)).thenReturn(Flux.empty());
lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class)))
.thenReturn(fetcher);
lenient().when(fetcher.fetchComment()).thenReturn(Mono.just(new SystemSetting.Comment()));
diff --git a/application/src/test/java/run/halo/app/theme/dialect/ElementTagPostProcessorsTest.java b/application/src/test/java/run/halo/app/theme/dialect/ElementTagPostProcessorsTest.java
new file mode 100644
index 00000000000..d58d070ec26
--- /dev/null
+++ b/application/src/test/java/run/halo/app/theme/dialect/ElementTagPostProcessorsTest.java
@@ -0,0 +1,81 @@
+package run.halo.app.theme.dialect;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+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.thymeleaf.exceptions.TemplateProcessingException;
+import org.thymeleaf.model.IProcessableElementTag;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import run.halo.app.plugin.extensionpoint.ExtensionGetter;
+
+@ExtendWith(MockitoExtension.class)
+class ElementTagPostProcessorsTest {
+
+ @Mock
+ ExtensionGetter extensionGetter;
+
+ @InjectMocks
+ ElementTagPostProcessors elementTagPostProcessors;
+
+ @Test
+ void shouldDoNothingIfNoProcessorsFound() {
+ when(extensionGetter.getExtensions(ElementTagPostProcessor.class)).thenReturn(Flux.empty());
+ elementTagPostProcessors.process(null, null, null);
+ }
+
+ @Test
+ void shouldProcessWithOneProcessor() {
+ var processor = mock(ElementTagPostProcessor.class);
+ var tag = mock(IProcessableElementTag.class);
+ doReturn(Mono.empty()).when(processor).process(null, tag, null);
+ when(extensionGetter.getExtensions(ElementTagPostProcessor.class))
+ .thenReturn(Flux.just(processor));
+ elementTagPostProcessors.process(null, tag, null);
+ }
+
+ @Test
+ void shouldProcessWithTwoProcessors() {
+ var processor1 = mock(ElementTagPostProcessor.class);
+ var processor2 = mock(ElementTagPostProcessor.class);
+ var tag = mock(IProcessableElementTag.class);
+ doReturn(Mono.empty()).when(processor1).process(null, tag, null);
+ doReturn(Mono.empty()).when(processor2).process(null, tag, null);
+ when(extensionGetter.getExtensions(ElementTagPostProcessor.class))
+ .thenReturn(Flux.just(processor1, processor2));
+ elementTagPostProcessors.process(null, tag, null);
+ var inOrder = inOrder(processor1, processor2);
+ inOrder.verify(processor1).process(null, tag, null);
+ inOrder.verify(processor2).process(null, tag, null);
+ }
+
+ @Test
+ void shouldProcessWithFailure() {
+ var tag = mock(IProcessableElementTag.class);
+ var processor1 = mock(ElementTagPostProcessor.class);
+ var processor2 = mock(ElementTagPostProcessor.class);
+ doReturn(Mono.error(new IllegalStateException("failed to process")))
+ .when(processor1).process(null, tag, null);
+ when(extensionGetter.getExtensions(ElementTagPostProcessor.class))
+ .thenReturn(Flux.just(processor1, processor2));
+
+ var e = assertThrows(TemplateProcessingException.class,
+ () -> elementTagPostProcessors.process(null, tag, null)
+ );
+ assertInstanceOf(IllegalStateException.class, e.getCause());
+ assertEquals("failed to process", e.getCause().getMessage());
+ verify(processor2, never()).process(null, tag, null);
+ }
+}
\ No newline at end of file
diff --git a/application/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java b/application/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java
index c4c18afe500..15418c51f3b 100644
--- a/application/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java
+++ b/application/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java
@@ -79,7 +79,7 @@ class HaloProcessorDialectTest {
@BeforeEach
void setUp() {
- HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
+ HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect(extensionGetter);
templateEngine = new TemplateEngine();
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
templateEngine.addTemplateResolver(new TestTemplateResolver());
@@ -113,6 +113,7 @@ void setUp() {
lenient().when(extensionGetter.getExtensions(TemplateHeadProcessor.class)).thenReturn(
Flux.fromIterable(map.values()).sort(AnnotationAwareOrderComparator.INSTANCE)
);
+ when(extensionGetter.getExtensions(ElementTagPostProcessor.class)).thenReturn(Flux.empty());
lenient().when(fetcher.fetchComment())
.thenReturn(Mono.just(new SystemSetting.Comment()));
diff --git a/application/src/test/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessorTest.java b/application/src/test/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessorTest.java
index 9fec46b91ef..68a7257980d 100644
--- a/application/src/test/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessorTest.java
+++ b/application/src/test/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessorTest.java
@@ -56,7 +56,7 @@ class TemplateFooterElementTagProcessorTest {
@BeforeEach
void setUp() {
- HaloProcessorDialect haloProcessorDialect = new MockHaloProcessorDialect();
+ HaloProcessorDialect haloProcessorDialect = new MockHaloProcessorDialect(extensionGetter);
templateEngine = new TemplateEngine();
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
templateEngine.addTemplateResolver(new MockTemplateResolver());
@@ -111,6 +111,10 @@ protected ITemplateResource computeTemplateResource(IEngineConfiguration configu
}
static class MockHaloProcessorDialect extends HaloProcessorDialect {
+ MockHaloProcessorDialect(ExtensionGetter extensionGetter) {
+ super(extensionGetter);
+ }
+
@Override
public Set getProcessors(String dialectPrefix) {
var processors = new HashSet();