Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: content page meta tags now override global injected #4069

Merged
merged 5 commits into from Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -8,6 +8,9 @@

/**
* Theme template <code>head</code> tag snippet injection processor.
* <p>Head processor is processed order by {@link org.springframework.core.annotation.Order}
* annotation, Higher order will be processed first and so that low-priority processor can be
* overwritten head tag written by high-priority processor.</p>
*
* @author guqing
* @since 2.0.0
Expand Down
Expand Up @@ -9,6 +9,7 @@
import java.util.Map;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.util.HtmlUtils;
import org.thymeleaf.context.ITemplateContext;
Expand All @@ -29,6 +30,7 @@
* @since 2.0.0
*/
@Component
@Order(1)
@AllArgsConstructor
public class ContentTemplateHeadProcessor implements TemplateHeadProcessor {
private static final String POST_NAME_VARIABLE = "name";
Expand Down
@@ -0,0 +1,78 @@
package run.halo.app.theme.dialect;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.IModel;
import org.thymeleaf.model.ITemplateEvent;
import org.thymeleaf.model.IText;
import org.thymeleaf.processor.element.IElementModelStructureHandler;
import reactor.core.publisher.Mono;

/**
* <p>This processor will remove the duplicate meta tag with the same name in head tag and only
* keep the last one.</p>
* <p>This processor will be executed last.</p>
*
* @author guqing
* @since 2.0.0
*/
@Order
@Component
@AllArgsConstructor
public class DuplicateMetaTagProcessor implements TemplateHeadProcessor {
static final Pattern META_PATTERN = Pattern.compile("<meta\\s+name=\"(\\w+)\"(.*?)>");

@Override
public Mono<Void> process(ITemplateContext context, IModel model,
IElementModelStructureHandler structureHandler) {
IModel newModel = context.getModelFactory().createModel();

Map<String, IndexedModel> uniqueMetaTags = new LinkedHashMap<>();
List<IndexedModel> otherModel = new ArrayList<>();
for (int i = 0; i < model.size(); i++) {
ITemplateEvent templateEvent = model.get(i);
// If the current node is a text node, it is processed separately.
// Because the text node may contain multiple meta tags.
if (templateEvent instanceof IText textNode) {
String text = textNode.getText();
Matcher matcher = META_PATTERN.matcher(text);
while (matcher.find()) {
String tagLine = matcher.group(0);
String nameAttribute = matcher.group(1);
IText metaTagNode = context.getModelFactory().createText(tagLine);
uniqueMetaTags.put(nameAttribute, new IndexedModel(i, metaTagNode));
text = text.replace(tagLine, "");
}
if (StringUtils.isNotBlank(text)) {
IText otherText = context.getModelFactory()
.createText(text);
otherModel.add(new IndexedModel(i, otherText));
}
} else {
otherModel.add(new IndexedModel(i, templateEvent));
}
}

otherModel.addAll(uniqueMetaTags.values());
otherModel.stream().sorted(Comparator.comparing(IndexedModel::index))
.map(IndexedModel::templateEvent)
.forEach(newModel::add);

model.reset();
model.addModel(newModel);
return Mono.empty();
}

record IndexedModel(int index, ITemplateEvent templateEvent) {
}
}
Expand Up @@ -2,9 +2,10 @@

import java.util.Collection;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.IModel;
import org.thymeleaf.model.IModelFactory;
import org.thymeleaf.model.ITemplateEvent;
import org.thymeleaf.processor.element.AbstractElementModelProcessor;
import org.thymeleaf.processor.element.IElementModelStructureHandler;
import org.thymeleaf.spring6.context.SpringContextUtils;
Expand Down Expand Up @@ -51,14 +52,23 @@ protected void doProcess(ITemplateContext context, IModel model,
structureHandler.setLocalVariable(PROCESS_FLAG, true);

// handle <head> tag
if (model.size() < 2) {
return;
}

/*
* Create the DOM structure that will be substituting our custom tag.
* The headline will be shown inside a '<div>' tag, and so this must
* be created first and then a Text node must be added to it.
*/
final IModelFactory modelFactory = context.getModelFactory();
IModel modelToInsert = modelFactory.createModel();
IModel modelToInsert = model.cloneModel();
// close </head> tag
final ITemplateEvent closeHeadTag = modelToInsert.get(modelToInsert.size() - 1);
modelToInsert.remove(modelToInsert.size() - 1);

// open <head> tag
final ITemplateEvent openHeadTag = modelToInsert.get(0);
modelToInsert.remove(0);

// apply processors to modelToInsert
Collection<TemplateHeadProcessor> templateHeadProcessors =
Expand All @@ -69,14 +79,20 @@ protected void doProcess(ITemplateContext context, IModel model,
.block();
}

// add to target model
model.insertModel(model.size() - 1, modelToInsert);
// reset model to insert
model.reset();
model.add(openHeadTag);
model.addModel(modelToInsert);
model.add(closeHeadTag);
guqing marked this conversation as resolved.
Show resolved Hide resolved
}

private Collection<TemplateHeadProcessor> getTemplateHeadProcessors(ITemplateContext context) {
ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context);
ExtensionComponentsFinder componentsFinder =
appCtx.getBean(ExtensionComponentsFinder.class);
return componentsFinder.getExtensions(TemplateHeadProcessor.class);
return componentsFinder.getExtensions(TemplateHeadProcessor.class)
.stream()
.sorted(AnnotationAwareOrderComparator.INSTANCE)
.toList();
}
}
Expand Up @@ -3,6 +3,7 @@
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.ITemplateContext;
Expand All @@ -20,7 +21,7 @@
* @see SystemSetting.Seo
* @since 2.0.0
*/
@Order
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
@Component
@AllArgsConstructor
public class GlobalSeoProcessor implements TemplateHeadProcessor {
Expand Down
@@ -1,6 +1,8 @@
package run.halo.app.theme.dialect;

import org.apache.commons.lang3.StringUtils;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.IModel;
Expand All @@ -14,10 +16,12 @@

/**
* <p>Global custom head snippet injection for theme global setting.</p>
* <p>Globally injected head snippet can be overridden by content template.</p>
*
* @author guqing
* @since 2.0.0
*/
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
@Component
public class TemplateGlobalHeadProcessor implements TemplateHeadProcessor {

Expand Down