Skip to content

Commit 3b526f9

Browse files
committed
feat: support for inject using id and selector
1 parent 4b1aec4 commit 3b526f9

File tree

8 files changed

+272
-33
lines changed

8 files changed

+272
-33
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.erzbir.halo.injector.process;
2+
3+
import com.erzbir.halo.injector.setting.BasicConfig;
4+
import com.erzbir.halo.injector.setting.InjectionRule;
5+
import java.util.List;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.http.server.PathContainer;
9+
import org.springframework.util.RouteMatcher;
10+
import org.springframework.web.util.pattern.PathPatternParser;
11+
import org.springframework.web.util.pattern.PathPatternRouteMatcher;
12+
import org.springframework.web.util.pattern.PatternParseException;
13+
import org.thymeleaf.context.Contexts;
14+
import org.thymeleaf.context.ITemplateContext;
15+
import org.thymeleaf.model.IModel;
16+
import org.thymeleaf.web.IWebRequest;
17+
import reactor.core.publisher.Flux;
18+
import reactor.core.publisher.Mono;
19+
import run.halo.app.plugin.ReactiveSettingFetcher;
20+
21+
/**
22+
* @author Erzbir
23+
* @since 1.0.0
24+
*/
25+
@Slf4j
26+
@RequiredArgsConstructor
27+
public abstract class AbstrictInjector implements Injector {
28+
private final ReactiveSettingFetcher reactiveSettingFetcher;
29+
private final RouteMatcher routeMatcher = createRouteMatcher();
30+
31+
public Mono<Void> inject(ITemplateContext context, IModel model,
32+
InjectionRule.Location location) {
33+
return getMatchedRulesForLocation(context, location)
34+
.map(this::processRuleCode)
35+
.filter(code -> !code.trim().isEmpty())
36+
.doOnNext(code -> {
37+
model.add(context.getModelFactory().createText(finalProcessCode(code)));
38+
log.debug("Injected code: {}",
39+
code.length() > 50 ? code.substring(0, 50) + "..." : code);
40+
})
41+
.then();
42+
}
43+
44+
private Flux<InjectionRule> getMatchedRulesForLocation(ITemplateContext context,
45+
InjectionRule.Location targetLocation) {
46+
return reactiveSettingFetcher.fetch("basic", BasicConfig.class)
47+
.flatMapMany(basicConfig -> {
48+
String currentPath = getCurrentPath(context);
49+
if (currentPath.isEmpty()) {
50+
return Flux.empty();
51+
}
52+
53+
List<InjectionRule> locationRules =
54+
basicConfig.getRulesByLocation(targetLocation);
55+
56+
return Flux.fromIterable(locationRules)
57+
.filter(rule -> matchesPath(rule.getPathPatterns(), currentPath, routeMatcher));
58+
})
59+
.onErrorResume(e -> {
60+
log.error("Failed to get matched rules for location: {}", targetLocation, e);
61+
return Flux.empty();
62+
});
63+
}
64+
65+
private boolean matchesPath(List<InjectionRule.PathMatchRule> pathPatterns,
66+
String currentPath,
67+
RouteMatcher routeMatcher) {
68+
if (currentPath == null || pathPatterns == null || pathPatterns.isEmpty()) {
69+
return false;
70+
}
71+
72+
RouteMatcher.Route requestRoute = routeMatcher.parseRoute(currentPath);
73+
74+
return pathPatterns.stream()
75+
.filter(pattern -> pattern != null && !pattern.getPathPattern().trim().isEmpty())
76+
.anyMatch(pattern -> {
77+
try {
78+
return routeMatcher.match(pattern.getPathPattern(), requestRoute);
79+
} catch (PatternParseException e) {
80+
log.warn("Parse route pattern [{}] failed for path [{}]", pattern, currentPath,
81+
e);
82+
return false;
83+
}
84+
});
85+
}
86+
87+
private String finalProcessCode(String code) {
88+
String comment_start = "<!-- PluginInjector start -->";
89+
String comment_end = "<!-- PluginInjector end -->";
90+
return comment_start + code + comment_end;
91+
}
92+
93+
protected String processRuleCode(InjectionRule rule) {
94+
return rule.getCode();
95+
}
96+
97+
private String getCurrentPath(ITemplateContext context) {
98+
try {
99+
if (!Contexts.isWebContext(context)) {
100+
return "";
101+
}
102+
IWebRequest request = Contexts.asWebContext(context).getExchange().getRequest();
103+
return request.getRequestPath();
104+
} catch (Exception e) {
105+
log.debug("Failed to get current path from context", e);
106+
return "";
107+
}
108+
}
109+
110+
private RouteMatcher createRouteMatcher() {
111+
var parser = new PathPatternParser();
112+
parser.setPathOptions(PathContainer.Options.HTTP_PATH);
113+
return new PathPatternRouteMatcher(parser);
114+
}
115+
}
116+
117+
118+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.erzbir.halo.injector.process;
2+
3+
import com.erzbir.halo.injector.setting.InjectionRule;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.util.StringUtils;
7+
import org.thymeleaf.context.ITemplateContext;
8+
import org.thymeleaf.model.IModel;
9+
import org.thymeleaf.model.IProcessableElementTag;
10+
import org.thymeleaf.processor.element.IElementTagStructureHandler;
11+
import reactor.core.publisher.Mono;
12+
import run.halo.app.plugin.ReactiveSettingFetcher;
13+
import run.halo.app.theme.dialect.TemplateFooterProcessor;
14+
15+
/**
16+
* @author Erzbir
17+
* @since 1.0.0
18+
*/
19+
@Component
20+
public class ElementIDInjector extends AbstrictInjector implements TemplateFooterProcessor {
21+
22+
public ElementIDInjector(ReactiveSettingFetcher reactiveSettingFetcher) {
23+
super(reactiveSettingFetcher);
24+
}
25+
26+
@Override
27+
public Mono<Void> process(ITemplateContext context, IProcessableElementTag tag,
28+
IElementTagStructureHandler structureHandler, IModel model) {
29+
return inject(context, model, InjectionRule.Location.ID);
30+
}
31+
32+
protected String processRuleCode(InjectionRule rule) {
33+
String code = rule.getCode();
34+
String elementId = rule.getId();
35+
if (StringUtils.hasText(elementId)) {
36+
code = """
37+
<script defer>
38+
let code = '%s';
39+
let dom = new DOMParser().parseFromString(code, 'text/html');
40+
let element = dom.body.firstElementChild;
41+
document.getElementById('%s').appendChild(element);
42+
</script>
43+
""".formatted(code, elementId);
44+
}
45+
String comment_start = "<!-- PluginInjector start -->";
46+
String comment_end = "<!-- PluginInjector end -->";
47+
return comment_start + code + comment_end;
48+
}
49+
50+
}

src/main/java/com/erzbir/halo/injector/process/FooterInjector.java

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,23 @@
1010
import org.thymeleaf.model.IProcessableElementTag;
1111
import org.thymeleaf.processor.element.IElementTagStructureHandler;
1212
import reactor.core.publisher.Mono;
13+
import run.halo.app.plugin.ReactiveSettingFetcher;
1314
import run.halo.app.theme.dialect.TemplateFooterProcessor;
1415

1516
/**
1617
* @author Erzbir
1718
* @since 1.0.0
1819
*/
19-
@Slf4j
2020
@Component
21-
@RequiredArgsConstructor
22-
public class FooterInjector implements TemplateFooterProcessor {
23-
private final InjectionService injectionService;
21+
public class FooterInjector extends AbstrictInjector implements TemplateFooterProcessor {
22+
23+
public FooterInjector(ReactiveSettingFetcher reactiveSettingFetcher) {
24+
super(reactiveSettingFetcher);
25+
}
2426

2527
@Override
2628
public Mono<Void> process(ITemplateContext context, IProcessableElementTag tag,
2729
IElementTagStructureHandler structureHandler, IModel model) {
28-
return injectionService.getMatchedCodeForLocation(context, InjectionRule.Location.FOOTER)
29-
.doOnNext(code -> {
30-
if (!code.trim().isEmpty()) {
31-
final IModelFactory modelFactory = context.getModelFactory();
32-
model.add(modelFactory.createText(code));
33-
log.debug("Injected FOOTER code: {}", code.length() > 100 ?
34-
code.substring(0, 100) + "..." : code);
35-
}
36-
})
37-
.then();
30+
return inject(context, model, InjectionRule.Location.FOOTER);
3831
}
3932
}

src/main/java/com/erzbir/halo/injector/process/HeadInjector.java

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,23 @@
99
import org.thymeleaf.model.IModelFactory;
1010
import org.thymeleaf.processor.element.IElementModelStructureHandler;
1111
import reactor.core.publisher.Mono;
12+
import run.halo.app.plugin.ReactiveSettingFetcher;
1213
import run.halo.app.theme.dialect.TemplateHeadProcessor;
1314

1415
/**
1516
* @author Erzbir
1617
* @since 1.0.0
1718
*/
18-
@Slf4j
1919
@Component
20-
@RequiredArgsConstructor
21-
public class HeadInjector implements TemplateHeadProcessor {
20+
public class HeadInjector extends AbstrictInjector implements TemplateHeadProcessor {
2221

23-
24-
private final InjectionService injectionService;
22+
public HeadInjector(ReactiveSettingFetcher reactiveSettingFetcher) {
23+
super(reactiveSettingFetcher);
24+
}
2525

2626
@Override
2727
public Mono<Void> process(ITemplateContext context, IModel model,
2828
IElementModelStructureHandler structureHandler) {
29-
return injectionService.getMatchedCodeForLocation(context, InjectionRule.Location.HEAD)
30-
.doOnNext(code -> {
31-
if (!code.trim().isEmpty()) {
32-
final IModelFactory modelFactory = context.getModelFactory();
33-
model.add(modelFactory.createText(code));
34-
log.debug("Injected HEAD code: {}", code.length() > 100 ?
35-
code.substring(0, 100) + "..." : code);
36-
}
37-
})
38-
.then();
29+
return inject(context, model, InjectionRule.Location.HEAD);
3930
}
40-
4131
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.erzbir.halo.injector.process;
2+
3+
import com.erzbir.halo.injector.setting.InjectionRule;
4+
import org.thymeleaf.context.ITemplateContext;
5+
import org.thymeleaf.model.IModel;
6+
import reactor.core.publisher.Mono;
7+
8+
/**
9+
* @author Erzbir
10+
* @since 1.0.0
11+
*/
12+
public interface Injector {
13+
Mono<Void> inject(ITemplateContext context, IModel model,
14+
InjectionRule.Location location);
15+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.erzbir.halo.injector.process;
2+
3+
import com.erzbir.halo.injector.setting.InjectionRule;
4+
import org.springframework.stereotype.Component;
5+
import org.springframework.util.StringUtils;
6+
import org.thymeleaf.context.ITemplateContext;
7+
import org.thymeleaf.model.IModel;
8+
import org.thymeleaf.processor.element.IElementModelStructureHandler;
9+
import reactor.core.publisher.Mono;
10+
import run.halo.app.plugin.ReactiveSettingFetcher;
11+
import run.halo.app.theme.dialect.TemplateHeadProcessor;
12+
13+
/**
14+
* @author Erzbir
15+
* @since 1.0.0
16+
*/
17+
@Component
18+
public class SelectorInjector extends AbstrictInjector implements TemplateHeadProcessor {
19+
public SelectorInjector(ReactiveSettingFetcher reactiveSettingFetcher) {
20+
super(reactiveSettingFetcher);
21+
}
22+
23+
@Override
24+
public Mono<Void> process(ITemplateContext context, IModel model,
25+
IElementModelStructureHandler structureHandler) {
26+
return inject(context, model, InjectionRule.Location.SELECTOR);
27+
}
28+
29+
protected String processRuleCode(InjectionRule rule) {
30+
String code = rule.getCode();
31+
String selector = rule.getSelector();
32+
if (StringUtils.hasText(selector)) {
33+
code = """
34+
<script defer>
35+
let html = '%s';
36+
let dom = new DOMParser().parseFromString(html, 'text/html');
37+
let element = dom.body.firstElementChild;
38+
document.querySelector('%s').appendChild(element);
39+
</script>
40+
""".formatted(code, selector);
41+
}
42+
String comment_start = "<!-- PluginInjector start -->";
43+
String comment_end = "<!-- PluginInjector end -->";
44+
return comment_start + code + comment_end;
45+
}
46+
}

src/main/java/com/erzbir/halo/injector/setting/InjectionRule.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ public class InjectionRule {
1515

1616
private String location = "footer";
1717

18+
private String id = "";
19+
private String selector = "";
20+
1821
private boolean enableTemplateProcess = false;
1922

2023
@Data
@@ -34,11 +37,19 @@ public boolean isValid() {
3437
return code != null && !code.trim().isEmpty()
3538
&& pathPatterns != null && !pathPatterns.isEmpty()
3639
&& pathPatterns.stream()
37-
.anyMatch(pattern -> pattern.pathPattern != null && !pattern.pathPattern.trim().isEmpty());
40+
.anyMatch(
41+
pattern -> pattern.pathPattern != null && !pattern.pathPattern.trim().isEmpty());
3842
}
3943

4044
public enum Location {
4145
HEAD,
42-
FOOTER
46+
FOOTER,
47+
ID,
48+
SELECTOR;
49+
50+
@Override
51+
public String toString() {
52+
return name().toLowerCase();
53+
}
4354
}
4455
}

src/main/resources/extensions/settings.yaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,20 @@ spec:
4141
- label: "footer"
4242
value: "footer"
4343
- label: "head"
44-
value: "head"
44+
value: "head"
45+
- label: "id"
46+
value: "id"
47+
- label: "selector"
48+
value: "selector"
49+
- $formkit: text
50+
if: "$value.location === 'id'"
51+
name: id
52+
value: ''
53+
label: 元素 id
54+
help: 使用 getElementById 来注入
55+
- $formkit: text
56+
if: "$value.location === 'selector'"
57+
name: selector
58+
value: ''
59+
label: 选择器
60+
help: 使用 querySelector 来注入

0 commit comments

Comments
 (0)