Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Egyperces - Egyedi Thymeleaf attribútum processzor

Szűrő megjelenítése

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ó

Atlassian CommonMark

CommonMark specifikáció

Highlight.js

Processzor implementáció

Kommentek
Hozzászólok

Még senki nem szólt hozzá ehhez a bejegyzéshez.