feat: add theme link expression dialect (#2438)

#### What type of PR is this?
/kind feature
/milestone 2.0
/area core

#### What this PR does / why we need it:
允许主题模板在 HTML 或 JavaScript 片段中使用表达式对象获得链接:
- `${#theme.assets('/js/main.js'))}`
- `${#theme.route('/categories')}`

#### Which issue(s) this PR fixes:

Fixes #2435

#### Special notes for your reviewer:
/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/2460/head
guqing 2022-09-23 11:12:13 +08:00 committed by GitHub
parent d40626b07b
commit f0892b2f4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 126 additions and 2 deletions

View File

@ -14,6 +14,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.infra.utils.FilePathUtils;
import run.halo.app.theme.dialect.LinkExpressionObjectDialect;
import run.halo.app.theme.dialect.ThemeJava8TimeDialect;
/**
@ -52,4 +53,9 @@ public class ThemeConfiguration {
Java8TimeDialect java8TimeDialect() {
return new ThemeJava8TimeDialect();
}
@Bean
LinkExpressionObjectDialect linkExpressionObjectDialect() {
return new LinkExpressionObjectDialect();
}
}

View File

@ -11,8 +11,8 @@ import run.halo.app.infra.utils.PathUtils;
* @since 2.0.0
*/
public class ThemeLinkBuilder extends StandardLinkBuilder {
private static final String THEME_ASSETS_PREFIX = "/assets";
private static final String THEME_PREVIEW_PREFIX = "/themes";
public static final String THEME_ASSETS_PREFIX = "/assets";
public static final String THEME_PREVIEW_PREFIX = "/themes";
private final ThemeContext theme;

View File

@ -0,0 +1,66 @@
package run.halo.app.theme.dialect;
import java.util.Set;
import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.expression.IExpressionObjectFactory;
import org.thymeleaf.linkbuilder.ILinkBuilder;
import org.thymeleaf.util.Validate;
import run.halo.app.theme.ThemeLinkBuilder;
/**
* A default implementation of {@link IExpressionObjectFactory}.
*
* @author guqing
* @since 2.0.0
*/
public class DefaultLinkExpressionFactory implements IExpressionObjectFactory {
private static final String THEME_EVALUATION_VARIABLE_NAME = "theme";
@Override
public Set<String> getAllExpressionObjectNames() {
return Set.of(THEME_EVALUATION_VARIABLE_NAME);
}
@Override
public Object buildObject(IExpressionContext context, String expressionObjectName) {
if (THEME_EVALUATION_VARIABLE_NAME.equals(expressionObjectName)) {
return new ThemeLinkExpressObject(context);
}
return null;
}
@Override
public boolean isCacheable(String expressionObjectName) {
return THEME_EVALUATION_VARIABLE_NAME.equals(expressionObjectName);
}
public static class ThemeLinkExpressObject {
private final ILinkBuilder linkBuilder;
private final IExpressionContext context;
/**
* Construct an expression object that provides a set of methods to handle link in
* Javascript or HTML through {@link IExpressionContext}.
*
* @param context expression context
*/
public ThemeLinkExpressObject(IExpressionContext context) {
Validate.notNull(context, "Context cannot be null");
this.context = context;
Set<ILinkBuilder> linkBuilders = context.getConfiguration().getLinkBuilders();
linkBuilder = linkBuilders.stream()
.findFirst()
.orElseThrow(() -> new TemplateProcessingException("Link builder not found"));
}
public String assets(String path) {
String assetsPath = ThemeLinkBuilder.THEME_ASSETS_PREFIX + path;
return linkBuilder.buildLink(context, assetsPath, null);
}
public String route(String path) {
return linkBuilder.buildLink(context, path, null);
}
}
}

View File

@ -0,0 +1,27 @@
package run.halo.app.theme.dialect;
import org.thymeleaf.dialect.AbstractDialect;
import org.thymeleaf.dialect.IExpressionObjectDialect;
import org.thymeleaf.expression.IExpressionObjectFactory;
/**
* An expression object dialect for theme link.
*
* @author guqing
* @since 2.0.0
*/
public class LinkExpressionObjectDialect extends AbstractDialect implements
IExpressionObjectDialect {
private static final IExpressionObjectFactory LINK_EXPRESSION_OBJECTS_FACTORY =
new DefaultLinkExpressionFactory();
public LinkExpressionObjectDialect() {
super("themeLink");
}
@Override
public IExpressionObjectFactory getExpressionObjectFactory() {
return LINK_EXPRESSION_OBJECTS_FACTORY;
}
}

View File

@ -0,0 +1,25 @@
package run.halo.app.theme.dialect;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link LinkExpressionObjectDialect}.
*
* @author guqing
* @since 2.0.0
*/
class LinkExpressionObjectDialectTest {
private final LinkExpressionObjectDialect linkExpressionObjectDialect =
new LinkExpressionObjectDialect();
@Test
void getExpressionObjectFactory() {
assertThat(linkExpressionObjectDialect.getName())
.isEqualTo("themeLink");
assertThat(linkExpressionObjectDialect.getExpressionObjectFactory())
.isInstanceOf(DefaultLinkExpressionFactory.class);
}
}