diff --git a/application/src/main/java/run/halo/app/theme/dialect/CommentElementTagProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/CommentElementTagProcessor.java index e27c15608..96b9935c1 100644 --- a/application/src/main/java/run/halo/app/theme/dialect/CommentElementTagProcessor.java +++ b/application/src/main/java/run/halo/app/theme/dialect/CommentElementTagProcessor.java @@ -1,33 +1,21 @@ package run.halo.app.theme.dialect; -import static org.apache.commons.lang3.BooleanUtils.isFalse; -import static org.apache.commons.lang3.BooleanUtils.isTrue; - -import java.util.Optional; -import org.springframework.context.ApplicationContext; -import org.springframework.core.convert.support.DefaultConversionService; -import org.thymeleaf.context.Contexts; import org.thymeleaf.context.ITemplateContext; -import org.thymeleaf.context.IWebContext; 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.plugin.extensionpoint.ExtensionGetter; - /** *

Comment element tag processor.

*

Replace the comment tag <halo:comment /> with the given content.

* * @author guqing + * @see CommentEnabledVariableProcessor * @since 2.0.0 */ public class CommentElementTagProcessor extends AbstractElementTagProcessor { - public static final String COMMENT_ENABLED_MODEL_ATTRIBUTE = "haloCommentEnabled"; private static final String TAG_NAME = "comment"; private static final int PRECEDENCE = 1000; @@ -51,49 +39,12 @@ public class CommentElementTagProcessor extends AbstractElementTagProcessor { @Override protected void doProcess(ITemplateContext context, IProcessableElementTag tag, IElementTagStructureHandler structureHandler) { - getCommentWidget(context).ifPresentOrElse(commentWidget -> { - populateAllowCommentAttribute(context, true); - commentWidget.render(context, tag, structureHandler); - }, () -> { - populateAllowCommentAttribute(context, false); + var commentWidget = (CommentWidget) context.getVariable( + CommentEnabledVariableProcessor.COMMENT_WIDGET_OBJECT_VARIABLE); + if (commentWidget == null) { structureHandler.replaceWith("", false); - }); - } - - static void populateAllowCommentAttribute(ITemplateContext context, boolean allowComment) { - if (Contexts.isWebContext(context)) { - IWebContext webContext = Contexts.asWebContext(context); - webContext.getExchange() - .setAttributeValue(COMMENT_ENABLED_MODEL_ATTRIBUTE, allowComment); + return; } - } - - static Optional getCommentWidget(ITemplateContext context) { - final ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context); - SystemConfigurableEnvironmentFetcher environmentFetcher = - appCtx.getBean(SystemConfigurableEnvironmentFetcher.class); - var commentSetting = environmentFetcher.fetchComment() - .blockOptional() - .orElseThrow(); - var globalEnabled = isTrue(commentSetting.getEnable()); - if (!globalEnabled) { - return Optional.empty(); - } - - if (Contexts.isWebContext(context)) { - IWebContext webContext = Contexts.asWebContext(context); - Object attributeValue = webContext.getExchange() - .getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE); - Boolean enabled = DefaultConversionService.getSharedInstance() - .convert(attributeValue, Boolean.class); - if (isFalse(enabled)) { - return Optional.empty(); - } - } - - ExtensionGetter extensionGetter = appCtx.getBean(ExtensionGetter.class); - return extensionGetter.getEnabledExtensionByDefinition(CommentWidget.class) - .next() - .blockOptional(); + commentWidget.render(context, tag, structureHandler); } } diff --git a/application/src/main/java/run/halo/app/theme/dialect/CommentEnabledVariableProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/CommentEnabledVariableProcessor.java new file mode 100644 index 000000000..b5cece50a --- /dev/null +++ b/application/src/main/java/run/halo/app/theme/dialect/CommentEnabledVariableProcessor.java @@ -0,0 +1,91 @@ +package run.halo.app.theme.dialect; + +import static org.apache.commons.lang3.BooleanUtils.isFalse; +import static org.apache.commons.lang3.BooleanUtils.isTrue; + +import java.util.Optional; +import org.springframework.context.ApplicationContext; +import org.springframework.core.convert.support.DefaultConversionService; +import org.thymeleaf.context.Contexts; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.context.IWebContext; +import org.thymeleaf.model.ITemplateEnd; +import org.thymeleaf.model.ITemplateStart; +import org.thymeleaf.processor.templateboundaries.AbstractTemplateBoundariesProcessor; +import org.thymeleaf.processor.templateboundaries.ITemplateBoundariesStructureHandler; +import org.thymeleaf.spring6.context.SpringContextUtils; +import org.thymeleaf.standard.StandardDialect; +import org.thymeleaf.templatemode.TemplateMode; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.plugin.extensionpoint.ExtensionGetter; + +/** + * Comment enabled variable processor. + *

Compute comment enabled state and set it to the model when the template is start rendering

+ *

It is not suitable for scenarios where there are multiple comment components on the same page + * and some of them need to be controlled to be closed.

+ * + * @author guqing + * @since 2.9.0 + */ +public class CommentEnabledVariableProcessor extends AbstractTemplateBoundariesProcessor { + + public static final String COMMENT_WIDGET_OBJECT_VARIABLE = CommentWidget.class.getName(); + public static final String COMMENT_ENABLED_MODEL_ATTRIBUTE = "haloCommentEnabled"; + + public CommentEnabledVariableProcessor() { + super(TemplateMode.HTML, StandardDialect.PROCESSOR_PRECEDENCE); + } + + @Override + public void doProcessTemplateStart(ITemplateContext context, ITemplateStart templateStart, + ITemplateBoundariesStructureHandler structureHandler) { + getCommentWidget(context).ifPresentOrElse(commentWidget -> { + populateAllowCommentAttribute(context, true); + structureHandler.setLocalVariable(COMMENT_WIDGET_OBJECT_VARIABLE, commentWidget); + }, () -> populateAllowCommentAttribute(context, false)); + } + + @Override + public void doProcessTemplateEnd(ITemplateContext context, ITemplateEnd templateEnd, + ITemplateBoundariesStructureHandler structureHandler) { + structureHandler.removeLocalVariable(COMMENT_WIDGET_OBJECT_VARIABLE); + } + + static void populateAllowCommentAttribute(ITemplateContext context, boolean allowComment) { + if (Contexts.isWebContext(context)) { + IWebContext webContext = Contexts.asWebContext(context); + webContext.getExchange() + .setAttributeValue(COMMENT_ENABLED_MODEL_ATTRIBUTE, allowComment); + } + } + + static Optional getCommentWidget(ITemplateContext context) { + final ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context); + SystemConfigurableEnvironmentFetcher environmentFetcher = + appCtx.getBean(SystemConfigurableEnvironmentFetcher.class); + var commentSetting = environmentFetcher.fetchComment() + .blockOptional() + .orElseThrow(); + var globalEnabled = isTrue(commentSetting.getEnable()); + if (!globalEnabled) { + return Optional.empty(); + } + + if (Contexts.isWebContext(context)) { + IWebContext webContext = Contexts.asWebContext(context); + Object attributeValue = webContext.getExchange() + .getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE); + Boolean enabled = DefaultConversionService.getSharedInstance() + .convert(attributeValue, Boolean.class); + if (isFalse(enabled)) { + return Optional.empty(); + } + } + + ExtensionGetter extensionGetter = appCtx.getBean(ExtensionGetter.class); + return extensionGetter.getEnabledExtensionByDefinition(CommentWidget.class) + .next() + .blockOptional(); + } +} 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 cf0aff5c6..6d61b3c48 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 @@ -35,6 +35,7 @@ public class HaloProcessorDialect extends AbstractProcessorDialect implements processors.add(new TemplateFooterElementTagProcessor(dialectPrefix)); processors.add(new JsonNodePropertyAccessorBoundariesProcessor()); processors.add(new CommentElementTagProcessor(dialectPrefix)); + processors.add(new CommentEnabledVariableProcessor()); 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 a44ff8c0c..cd6f5bd02 100644 --- a/application/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java +++ b/application/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java @@ -1,6 +1,8 @@ package run.halo.app.theme; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -24,6 +26,8 @@ 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.infra.utils.JsonUtils; import run.halo.app.theme.dialect.HaloProcessorDialect; @@ -40,6 +44,9 @@ public class ReactiveFinderExpressionParserTests { @Mock private ApplicationContext applicationContext; + @Mock + private SystemConfigurableEnvironmentFetcher environmentFetcher; + private TemplateEngine templateEngine; @BeforeEach @@ -53,6 +60,10 @@ public class ReactiveFinderExpressionParserTests { } })); templateEngine.addTemplateResolver(new TestTemplateResolver()); + lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class))) + .thenReturn(environmentFetcher); + lenient().when(environmentFetcher.fetchComment()) + .thenReturn(Mono.just(new SystemSetting.Comment())); } @Test 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 d8245cc46..43ba5354f 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 @@ -4,7 +4,6 @@ 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.verify; import static org.mockito.Mockito.when; import java.util.Map; @@ -19,7 +18,6 @@ import org.thymeleaf.IEngineConfiguration; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; import org.thymeleaf.context.ITemplateContext; -import org.thymeleaf.context.WebEngineContext; import org.thymeleaf.model.IProcessableElementTag; import org.thymeleaf.processor.element.IElementTagStructureHandler; import org.thymeleaf.spring6.dialect.SpringStandardDialect; @@ -27,7 +25,6 @@ import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext; import org.thymeleaf.templateresolver.StringTemplateResolver; import org.thymeleaf.templateresource.ITemplateResource; import org.thymeleaf.templateresource.StringTemplateResource; -import org.thymeleaf.web.IWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; @@ -105,74 +102,6 @@ class CommentElementTagProcessorTest { """); } - @Test - void getCommentWidget() { - when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class))) - .thenReturn(environmentFetcher); - SystemSetting.Comment commentSetting = mock(SystemSetting.Comment.class); - when(environmentFetcher.fetchComment()) - .thenReturn(Mono.just(commentSetting)); - - CommentWidget commentWidget = mock(CommentWidget.class); - when(extensionGetter.getEnabledExtensionByDefinition(CommentWidget.class)) - .thenReturn(Flux.just(commentWidget)); - WebEngineContext webContext = mock(WebEngineContext.class); - var evaluationContext = mock(ThymeleafEvaluationContext.class); - when(webContext.getVariable( - eq(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME))) - .thenReturn(evaluationContext); - when(evaluationContext.getApplicationContext()).thenReturn(applicationContext); - IWebExchange webExchange = mock(IWebExchange.class); - when(webContext.getExchange()).thenReturn(webExchange); - - // comment disabled - when(commentSetting.getEnable()).thenReturn(true); - assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isTrue(); - - // comment enabled - when(commentSetting.getEnable()).thenReturn(false); - assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isFalse(); - - // comment enabled and ENABLE_COMMENT_ATTRIBUTE is true - when(commentSetting.getEnable()).thenReturn(true); - when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE)) - .thenReturn(true); - assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isTrue(); - - // comment enabled and ENABLE_COMMENT_ATTRIBUTE is false - when(commentSetting.getEnable()).thenReturn(true); - when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE)) - .thenReturn(false); - assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isFalse(); - - // comment enabled and ENABLE_COMMENT_ATTRIBUTE is null - when(commentSetting.getEnable()).thenReturn(true); - when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE)) - .thenReturn(null); - assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isTrue(); - - // comment enabled and ENABLE_COMMENT_ATTRIBUTE is 'false' - when(commentSetting.getEnable()).thenReturn(true); - when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE)) - .thenReturn("false"); - assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isFalse(); - } - - @Test - void populateAllowCommentAttribute() { - WebEngineContext webContext = mock(WebEngineContext.class); - IWebExchange webExchange = mock(IWebExchange.class); - when(webContext.getExchange()).thenReturn(webExchange); - - CommentElementTagProcessor.populateAllowCommentAttribute(webContext, true); - verify(webExchange).setAttributeValue( - eq(CommentElementTagProcessor.COMMENT_ENABLED_MODEL_ATTRIBUTE), eq(true)); - - CommentElementTagProcessor.populateAllowCommentAttribute(webContext, false); - verify(webExchange).setAttributeValue( - eq(CommentElementTagProcessor.COMMENT_ENABLED_MODEL_ATTRIBUTE), eq(false)); - } - static class DefaultCommentWidget implements CommentWidget { @Override diff --git a/application/src/test/java/run/halo/app/theme/dialect/CommentEnabledVariableProcessorTest.java b/application/src/test/java/run/halo/app/theme/dialect/CommentEnabledVariableProcessorTest.java new file mode 100644 index 000000000..9ba5ac696 --- /dev/null +++ b/application/src/test/java/run/halo/app/theme/dialect/CommentEnabledVariableProcessorTest.java @@ -0,0 +1,121 @@ +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.verify; +import static org.mockito.Mockito.when; + +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.context.WebEngineContext; +import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext; +import org.thymeleaf.web.IWebExchange; +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 CommentEnabledVariableProcessor}. + * + * @author guqing + * @since 2.9.0 + */ +@ExtendWith(MockitoExtension.class) +class CommentEnabledVariableProcessorTest { + @Mock + private ApplicationContext applicationContext; + + @Mock + private ExtensionGetter extensionGetter; + + @Mock + private SystemConfigurableEnvironmentFetcher environmentFetcher; + + @BeforeEach + void setUp() { + lenient().when(applicationContext.getBean(eq(ExtensionGetter.class))) + .thenReturn(extensionGetter); + } + + @Test + void getCommentWidget() { + when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class))) + .thenReturn(environmentFetcher); + SystemSetting.Comment commentSetting = mock(SystemSetting.Comment.class); + when(environmentFetcher.fetchComment()) + .thenReturn(Mono.just(commentSetting)); + + CommentWidget commentWidget = mock(CommentWidget.class); + when(extensionGetter.getEnabledExtensionByDefinition(CommentWidget.class)) + .thenReturn(Flux.just(commentWidget)); + WebEngineContext webContext = mock(WebEngineContext.class); + var evaluationContext = mock(ThymeleafEvaluationContext.class); + when(webContext.getVariable( + eq(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME))) + .thenReturn(evaluationContext); + when(evaluationContext.getApplicationContext()).thenReturn(applicationContext); + IWebExchange webExchange = mock(IWebExchange.class); + when(webContext.getExchange()).thenReturn(webExchange); + + // comment disabled + when(commentSetting.getEnable()).thenReturn(true); + assertThat( + CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isTrue(); + + // comment enabled + when(commentSetting.getEnable()).thenReturn(false); + assertThat( + CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isFalse(); + + // comment enabled and ENABLE_COMMENT_ATTRIBUTE is true + when(commentSetting.getEnable()).thenReturn(true); + when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE)) + .thenReturn(true); + assertThat( + CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isTrue(); + + // comment enabled and ENABLE_COMMENT_ATTRIBUTE is false + when(commentSetting.getEnable()).thenReturn(true); + when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE)) + .thenReturn(false); + assertThat( + CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isFalse(); + + // comment enabled and ENABLE_COMMENT_ATTRIBUTE is null + when(commentSetting.getEnable()).thenReturn(true); + when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE)) + .thenReturn(null); + assertThat( + CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isTrue(); + + // comment enabled and ENABLE_COMMENT_ATTRIBUTE is 'false' + when(commentSetting.getEnable()).thenReturn(true); + when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE)) + .thenReturn("false"); + assertThat( + CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isFalse(); + } + + @Test + void populateAllowCommentAttribute() { + WebEngineContext webContext = mock(WebEngineContext.class); + IWebExchange webExchange = mock(IWebExchange.class); + when(webContext.getExchange()).thenReturn(webExchange); + + CommentEnabledVariableProcessor.populateAllowCommentAttribute(webContext, true); + verify(webExchange).setAttributeValue( + eq(CommentEnabledVariableProcessor.COMMENT_ENABLED_MODEL_ATTRIBUTE), eq(true)); + + CommentEnabledVariableProcessor.populateAllowCommentAttribute(webContext, false); + verify(webExchange).setAttributeValue( + eq(CommentEnabledVariableProcessor.COMMENT_ENABLED_MODEL_ATTRIBUTE), eq(false)); + } +} \ No newline at end of file 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 6af24a135..0ef34208f 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 @@ -108,6 +108,10 @@ class ContentTemplateHeadProcessorIntegrationTest { lenient().when(extensionComponentsFinder.getExtensions(eq(TemplateHeadProcessor.class))) .thenReturn(new ArrayList<>(map.values())); + + lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class))) + .thenReturn(fetcher); + lenient().when(fetcher.fetchComment()).thenReturn(Mono.just(new SystemSetting.Comment())); } @@ -137,19 +141,19 @@ class ContentTemplateHeadProcessorIntegrationTest { 3. but global head meta is not overridden by global seo meta */ assertThat(Jsoup.parse(result).html()).isEqualTo(""" - - - - - Post detail - - - - - - this is body - - """); + + + + + Post detail + + + + + + this is body + + """); } Map mutableMetaMap(String nameValue, String contentValue) { 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 b23e59587..f572e1d60 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 @@ -104,6 +104,11 @@ class HaloProcessorDialectTest { lenient().when(extensionComponentsFinder.getExtensions(eq(TemplateHeadProcessor.class))) .thenReturn(new ArrayList<>(map.values())); + + lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class))) + .thenReturn(fetcher); + lenient().when(fetcher.fetchComment()) + .thenReturn(Mono.just(new SystemSetting.Comment())); } @Test