Skip to content

Commit

Permalink
feat: support image zooming based on page rules (#6)
Browse files Browse the repository at this point in the history
### What this PR does?
支持配置页面路径规则匹配来开启图片放大功能

Fixes #5

<img width="781" alt="image" src="https://github.com/halo-sigs/plugin-lightgallery/assets/38999863/2eda84ab-b1a9-4a77-8fe4-130637657878">

```release-note
支持配置页面路径规则匹配来开启图片放大功能
```
  • Loading branch information
guqing authored Jun 19, 2023
1 parent 2cfde91 commit 95521e4
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 30 deletions.
132 changes: 105 additions & 27 deletions src/main/java/run/halo/lightgallery/LightGalleryHeadProcessor.java
Original file line number Diff line number Diff line change
@@ -1,69 +1,147 @@
package run.halo.lightgallery;

import static org.apache.commons.lang3.StringUtils.defaultIfBlank;

import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

import io.micrometer.common.util.StringUtils;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.util.RouteMatcher;
import org.springframework.web.util.pattern.PathPatternRouteMatcher;
import org.springframework.web.util.pattern.PatternParseException;
import org.thymeleaf.context.Contexts;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.IModel;
import org.thymeleaf.model.IModelFactory;
import org.thymeleaf.processor.element.IElementModelStructureHandler;
import org.thymeleaf.web.IWebRequest;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.SettingFetcher;
import run.halo.app.plugin.ReactiveSettingFetcher;
import run.halo.app.theme.dialect.TemplateHeadProcessor;

/**
* @author ryanwang
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class LightGalleryHeadProcessor implements TemplateHeadProcessor {

private final SettingFetcher settingFetcher;

public LightGalleryHeadProcessor(SettingFetcher settingFetcher) {
this.settingFetcher = settingFetcher;
}
private static final String TEMPLATE_ID_VARIABLE = "_templateId";
final PropertyPlaceholderHelper placeholderHelper = new PropertyPlaceholderHelper("${", "}");
private final ReactiveSettingFetcher reactiveSettingFetcher;
private final PathPatternRouteMatcher routeMatcher = new PathPatternRouteMatcher();

@Override
public Mono<Void> process(ITemplateContext context, IModel model,
IElementModelStructureHandler structureHandler) {
if (!isContentTemplate(context)) {
return Mono.empty();
}
return settingFetcher.fetch("basic", BasicConfig.class)
.map(basicConfig -> {
return reactiveSettingFetcher.fetch("basic", BasicConfig.class)
.doOnNext(basicConfig -> {
final IModelFactory modelFactory = context.getModelFactory();
model.add(modelFactory.createText(lightGalleryScript(basicConfig.getDom_selector())));
return Mono.empty();
}).orElse(Mono.empty()).then();
String domSelector = basicConfig.getDom_selector();
if (StringUtils.isNotBlank(domSelector) && isContentTemplate(context)) {
model.add(modelFactory.createText(lightGalleryScript(Set.of(domSelector))));
}

MatchResult matchResult = isRequestPathMatchingRoute(context, basicConfig);
if (!matchResult.matched()) {
return;
}
model.add(modelFactory.createText(lightGalleryScript(matchResult.domSelectors())));
})
.onErrorContinue((throwable, o) -> log.warn("LightGalleryHeadProcessor process failed", throwable))
.then();
}

private String lightGalleryScript(String domSelector) {
static String lightGalleryScript(Set<String> domSelectors) {
return """
<!-- PluginLightGallery start -->
<link href="/plugins/PluginLightGallery/assets/static/css/lightgallery.min.css" rel="stylesheet" />
<script defer src="/plugins/PluginLightGallery/assets/static/js/lightgallery.min.js"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function () {
const imageNodes = document.querySelectorAll(`%s img`);
imageNodes.forEach(function (node) {
if (node) {
node.dataset.src = node.src;
}
});
lightGallery(document.querySelector("%s"), {
selector: "img",
});
%s
});
</script>
<!-- PluginLightGallery end -->
""".formatted(domSelector, domSelector);
""".formatted(instantiateGallery(domSelectors));
}

static String instantiateGallery(Set<String> domSelectors) {
return domSelectors.stream()
.map(domSelector -> """
document.querySelectorAll(`%s img`)?.forEach(function (node) {
if (node) {
node.dataset.src = node.src;
}
});
lightGallery(document.querySelector("%s"), {
selector: "img",
});
""".formatted(domSelector, domSelector)
)
.collect(Collectors.joining("\n"));
}

public boolean isContentTemplate(ITemplateContext context) {
return "post".equals(context.getVariable("_templateId")) || "page".equals(context.getVariable("_templateId"));
return "post".equals(context.getVariable(TEMPLATE_ID_VARIABLE))
|| "page".equals(context.getVariable(TEMPLATE_ID_VARIABLE));
}

public MatchResult isRequestPathMatchingRoute(ITemplateContext context, BasicConfig basicConfig) {
if (!Contexts.isWebContext(context)) {
return MatchResult.mismatch();
}
IWebRequest request = Contexts.asWebContext(context).getExchange().getRequest();
String requestPath = request.getRequestPath();
RouteMatcher.Route requestRoute = routeMatcher.parseRoute(requestPath);

Set<String> selectors = basicConfig.nullSafeRules()
.stream()
.filter(rule -> isMatchedRoute(requestRoute, rule))
.map(rule -> defaultIfBlank(rule.getDomSelector(), "body"))
.collect(Collectors.toSet());
return selectors.size() > 0
? new MatchResult(true, selectors)
: MatchResult.mismatch();
}

private boolean isMatchedRoute(RouteMatcher.Route requestRoute, PathMatchRule rule) {
try {
return routeMatcher.match(rule.getPathPattern(), requestRoute);
} catch (PatternParseException e) {
// ignore
log.warn("Parse route pattern [{}] failed", rule.getPathPattern(), e);
}
return false;
}

record MatchResult(boolean matched, Set<String> domSelectors) {
public static MatchResult mismatch() {
return new MatchResult(false, Set.of());
}
}

@Data
public static class BasicConfig {
String dom_selector;
List<PathMatchRule> rules;

public List<PathMatchRule> nullSafeRules() {
return ObjectUtils.defaultIfNull(rules, List.of());
}
}

@Data
public static class PathMatchRule {
private String pathPattern;
private String domSelector;
}
}
22 changes: 19 additions & 3 deletions src/main/resources/extensions/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,23 @@ spec:
label: 基本设置
formSchema:
- $formkit: text
help: 填写扫描图片的区域的 DOM 节点,支持 CSS 选择器,如:#content
label: DOM 节点选择
help: 填写扫描图片的区域的 DOM 节点,支持 CSS 选择器,如:#content,此设置已过时将在未来移除
label: 内容页面匹配
name: dom_selector
validation: required
value: ""
- $formkit: repeater
name: rules
label: 页面匹配规则
value: [ ]
children:
- $formkit: text
name: pathPattern
label: 路径匹配
value: ""
validation: required
help: 用于匹配页面路径的正则表达式,如:/archives/**
- $formkit: text
name: domSelector
label: 匹配区域
help: 填写扫描图片的区域的 DOM 节点,支持 CSS 选择器,如:#content,不填写则默认为整个页面
value: ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package run.halo.lightgallery;

import org.junit.jupiter.api.Test;

import java.util.LinkedHashSet;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class LightGalleryHeadProcessorTest {

@Test
void lightGalleryScript() {
Set<String> set = new LinkedHashSet<>();
set.add(".content");
set.add(".moment");
String result = LightGalleryHeadProcessor.lightGalleryScript(set);
assertThat(result).isEqualTo("""
<!-- PluginLightGallery start -->
<link href="/plugins/PluginLightGallery/assets/static/css/lightgallery.min.css" rel="stylesheet" />
<script defer src="/plugins/PluginLightGallery/assets/static/js/lightgallery.min.js"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(`.content img`)?.forEach(function (node) {
if (node) {
node.dataset.src = node.src;
}
});
lightGallery(document.querySelector(".content"), {
selector: "img",
});
document.querySelectorAll(`.moment img`)?.forEach(function (node) {
if (node) {
node.dataset.src = node.src;
}
});
lightGallery(document.querySelector(".moment"), {
selector: "img",
});
});
</script>
<!-- PluginLightGallery end -->
""");
}

@Test
void instantiateGallery() {
Set<String> set = new LinkedHashSet<>();
set.add(".content");
set.add(".moment");
String result = LightGalleryHeadProcessor.instantiateGallery(set);
assertThat(result).isEqualTo("""
document.querySelectorAll(`.content img`)?.forEach(function (node) {
if (node) {
node.dataset.src = node.src;
}
});
lightGallery(document.querySelector(".content"), {
selector: "img",
});
document.querySelectorAll(`.moment img`)?.forEach(function (node) {
if (node) {
node.dataset.src = node.src;
}
});
lightGallery(document.querySelector(".moment"), {
selector: "img",
});
""");
}
}

0 comments on commit 95521e4

Please sign in to comment.