Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
Egyperces - Egyedi Thymeleaf attribútum processzor
A bevezetésből talán már kitalálható, miről fog szólni mai egypercesem: szükségem volt egy elegáns megoldásra MarkDown forrás front oldalon történő renderelésére. A megoldás a Leaflet-hez készülő front-end alkalmazásban fog hamarosan debütálni itt is - részletek később, addig is stay stuned (reklám vége).
Szóval mindenekelőtt szükségünk lesz egy MarkDown processor implementációra - döntésem az Atlassian CommonMark nevű libraryjére esett. A név egyébként nem véletlen, a CommonMark specifikáció implementációjáról van szó, ami több szempontból is előnyös. Egyrészt a standard MarkDown dialektikát ismeri, szóval nem fog nagyon meglepetéseket okozni a használata. Másrészt képes együttműködni minden olyan libraryvel, ami szintén a CommonMark specifikáción alapul - ennek köszönhetően könnyű kiterjeszteni például szintaxis kiemelést végző JavaScript libraryvel is, mint amilyen a highlight.js (linkek a cikk végén).
Nyilvánvaló (kerülő) megoldásnak tűnik a renderelés integrálása a view felé konvertálást végző converter implementációkba - ennél azonban létezik jóval elegánsabb megoldás is. A Thymeleaf ugyanis kiterjeszthető egyedi processzor implementációkkal. A processzorok többek között képesek feldolgozni a tokenizáló által felismert XML node-okat és attribútumukat is, melyekre egyedi feldolgozó logika ültethető. A Thymeleaf dokumentációjában részletes leírás található az extension API-ról, bár maga az API használata nem mindig túl egyértelmű és egészen bonyolult formát is ölthet egy-egy saját implementáció. Jelen esetben elégséges volt egy attribútum feldolgozó implementálása, mely az alábbi módon néz ki:
public class MarkdownAttributeTagProcessor extends AbstractAttributeTagProcessor {
private static final String PROCESSOR_NAME = "th:markdown";
private static final int PROCESSOR_PRECEDENCE = 0;
// ...
// a konstruktor elég, ha csak a számunkra szükséges függéseket várja
// a paraméterek nagy része konstansként is meghatározható
protected MarkdownAttributeTagProcessor(String dialectPrefix, Parser parser,
HtmlRenderer htmlRenderer) {
super(TemplateMode.HTML, dialectPrefix, null, true, PROCESSOR_NAME, false,
PROCESSOR_PRECEDENCE, true);
this.parser = parser;
this.htmlRenderer = htmlRenderer;
}
@Override
protected void doProcess(ITemplateContext context, IProcessableElementTag tag,
AttributeName attributeName, String attributeValue,
IElementTagStructureHandler structureHandler) {
// NPE-t elkerülendő
if (Objects.nonNull(attributeValue)) {
// a th:markdown attribútum értékét ki kell standard kifejezésként értékelni
Object expressionResult =
evaluateExpressions(context, tag, attributeName, attributeValue);
if (Objects.nonNull(expressionResult)) {
// ha sikerül a kiértékelés, parse-oljuk a MarkDown forrást ...
Node node = parser.parse(expressionResult.toString());
// ... majd renderelés után beszúrjuk az attribútumot tartalmazó node belsejébe
structureHandler.setBody(htmlRenderer.render(node), true);
}
}
}
private Object evaluateExpressions(ITemplateContext context, IProcessableElementTag tag,
AttributeName attributeName, String attributeValue) {
// a dokumentáció a StandardExpressions osztályból indul ki
// ez a utility class gyakorlatilag azt wrapeli
return EngineEventUtils
.computeAttributeExpression(context, tag, attributeName, attributeValue)
.execute(context);
}
}
Az implementáció az AbstractAttributeTagProcessor-t terjeszti ki, mivel egyedi attribútum feldolgozásához ezen absztrakt osztály implementációja áll a legközelebb. Paraméterei a következők:
- templateMode: ez esetben lehet fixen HTML, természetesen lehet más az értéke is, ha nem csak HTML template-ekben akarjuk használni
- dialectPrefix: minden processzor implementáció egy dialektus implementáció alá tartozik, ennek “prefix”-ét kell átadnia processzor számára (később láthatjuk majd, hogyan)
- elementName és prefixElementName: ha nem akarjuk megkötni, milyen XML tagben lehessen használni a processzort, ezt hagyhatjuk null és false értékeken.
- attributeName és prefixAttributeName: a használni kívánt attribútum neve, és hogy a dialektus prefixe hozzá legyen-e prefixelve az attribútum nevéhez. Jelen esetben th:thymeleaf lesz az attribútum neve és nem szükséges a prefixálás.
- precedence: a dialektus processzorai precedencia szerint rendezve futnak - ezzel a paraméterrel szabályozható.
- removeAttribute: feldolgozás után az attribútum eltávolításra kerül a template forrásából - ez a th:* attribútumok mindegyikére igaz, a processzor azokat nyugodtan eltávolíthatja.
Mint az fentebb látható, a MarkDown forrás renderelése a CommonMark Node és Parse osztályai segítségével történik. A renderelt HTML-t végül a th:thymeleaf attribútumot tartalmazó node tartalmaként szúrjuk be. Fontos, hogy mivel dinamikus paraméterből történik a renderelés, a fentebb látható módon ki kell értékelni az attribútum értékét mint Thymeleaf Standard Expression-t. Gyakorlatilag ezzel kész a processzor, már csak el kell helyezni azt egy dialektikában és azt beállítani. Az alkalmazás a Thymeleaf Layout Dialect kiterjesztését használja, így alapul azt választottam - teljesen újraírni a dialektika descriptor osztályát értelmetlen, szerencsére az kiterjeszthető, így az extra processzor könnyen beszúrható, beanként példányosítás után pedig azonnal használatba vehető.
// a kiterjesztett LayoutDialect
public class ExtendedLayoutDialect extends LayoutDialect {
// Atlassian CommonMark Parser
private Parser parser;
// Atlassian CommonMark HtmlRenderer
private HtmlRenderer htmlRenderer;
public ExtendedLayoutDialect(Parser parser, HtmlRenderer htmlRenderer) {
this.parser = parser;
this.htmlRenderer = htmlRenderer;
}
@Override
public Set<IProcessor> getProcessors(String dialectPrefix) {
Set<IProcessor> processors = super.getProcessors(dialectPrefix);
// itt kell átadni a processzornak a korábban említett dialectPrefix értéket
processors.add(new MarkdownAttributeTagProcessor(dialectPrefix,
parser, htmlRenderer));
return processors;
}
}
// bean konfiguráció
@Bean
public Parser commonmarkParser() {
return Parser.builder().build();
}
@Bean
public HtmlRenderer commonmarkHtmlRenderer() {
return HtmlRenderer.builder().build();
}
@Bean
@Primary
public LayoutDialect layoutDialect(Parser commonmarkParser,
HtmlRenderer commonmarkHtmlRenderer) {
return new ExtendedLayoutDialect(commonmarkParser, commonmarkHtmlRenderer);
}
Az új attribútum használata a következőképpen néz ki:
<!-- th:block helyett mást is használhatunk, pl div-et, section-t, stb. -->
<th:block th:markdown="${article.content}" />
Szintaxis kiemeléshez még mellékeljük a headben a következő kódot:
<link rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.14.1/styles/default.min.css" />
<script type="text/javascript"
src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.14.1/highlight.min.js"></script>
<script type="text/javascript">
hljs.initHighlightingOnLoad();
</script>
Thymeleaf extension API dokumentáció
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.