FreshMarker Feiertage und Enums

„Beauty is more important in computing than anywhere else in technology because software is so complicated. Beauty is the ultimate defence against complexity.“

David Gelernter

Zu den ersten Komponenten der FreshMarker Template-Engine gehörte ein einfaches Plug-In System. Durch diese Komponente wurde die Ergänzung um neue Built-Ins, Formatter und Mapper vereinfacht. Zwei neue Plug-Ins zeigen dabei Stärken und Schwächen des bisherigen Ansatzes.

Die Template-Engine unterstütze bislang keine Enum Typen. Um Enum Werte darzustellen müssen diese bisher in eine textuelle oder numerische Darstellung umgewandelt werden. Das folgende Enum besitzt vier Konstanten

ALPHA
ALPHA,
BETA
BETA,
GAMMA
GAMMA und
DELTA
DELTA und für die Nutzung in einem Template drei Darstellungsformen. Den Namen der Konstanten über die
name
name Methode, die Darstellung über die
toString
toString Methode und die Ordinalzahl über die
ordinal
ordinal Methode.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
enum TestEnum {
ALPHA("Α"),
BETA("Β"),
GAMMA("Γ"),
DELTA("Δ");
private final String name;
TestEnum(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
enum TestEnum { ALPHA("Α"), BETA("Β"), GAMMA("Γ"), DELTA("Δ"); private final String name; TestEnum(String name) { this.name = name; } @Override public String toString() { return name; } }
enum TestEnum {
  ALPHA("Α"),
  BETA("Β"),
  GAMMA("Γ"),
  DELTA("Δ");

  private final String name;

  TestEnum(String name) {
    this.name = name;
  }

  @Override
  public String toString() {
    return name;
  }
}

Um den Konventionen zu entsprechen wird für die Interpolation

${enum
${enum} die
toString
toString Methode des Enum verwendet. Die Interpolation
${enum?c}
${enum?c} mit dem Built-In
c
c für die computer language Darstellung verwendet die
name
name Methode und die Interpolation
${enum?ordinal}
${enum?ordinal} mit dem Built-In
ordinal
ordinal stellt die Ordinalzahl des Enum dar.

Für die Enums wird ein eigener Datentyp benötigt, die beiden Built-Ins

c
c und
ordinal
ordinal, sowie ein Mapper. Der eigene Datentyp ist zügig erstellt, weil er als Subklasse von
TemplatePrimitive
TemplatePrimitive für Enum Klassen angelegt wird.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class TemplateEnum<E extends Enum<E>> extends TemplatePrimitive<E> {
public TemplateEnum(E value) {
super(value);
}
}
public class TemplateEnum<E extends Enum<E>> extends TemplatePrimitive<E> { public TemplateEnum(E value) { super(value); } }
public class TemplateEnum<E extends Enum<E>> extends TemplatePrimitive<E> {
  public TemplateEnum(E value) {
    super(value);
  }
}

Die beiden Built-Ins werden im Plug-In als Built-In Methoden angelegt. Dabei erzeugt die Methode

computerBuiltIn
computerBuiltIn eine
TemplateString
TemplateString Instanz und die Methode
ordinal
ordinal eine
TemplateNumber
TemplateNumber Instanz mit den passenden Enum Methoden.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@BuiltInMethod("c")
public static TemplateString computerBuiltIn(TemplateEnum<?> value) {
return new TemplateString(value.getValue().name());
}
@BuiltInMethod("ordinal")
public static TemplateNumber ordinal(TemplateEnum<?> value) {
return new TemplateNumber(value.getValue().ordinal());
}
@BuiltInMethod("c") public static TemplateString computerBuiltIn(TemplateEnum<?> value) { return new TemplateString(value.getValue().name()); } @BuiltInMethod("ordinal") public static TemplateNumber ordinal(TemplateEnum<?> value) { return new TemplateNumber(value.getValue().ordinal()); }
@BuiltInMethod("c")
public static TemplateString computerBuiltIn(TemplateEnum<?> value) {
  return new TemplateString(value.getValue().name());
}

@BuiltInMethod("ordinal")
public static TemplateNumber ordinal(TemplateEnum<?> value) {
  return new TemplateNumber(value.getValue().ordinal());
}

Obwohl der Einbau der Enum Unterstützung bislang recht einfach war, ergibt sich bei dem Mapper ein Problem. Bis auf zwei Ausnahmen wurden die Mapper bisher mit Hilfe einer

Map
Map realisiert.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Map<Class<?>, Function<Object, TemplateObject>> mapper = new HashMap<>();
mapper.put(String.class, o -> new TemplateString((String) o));
mapper.put(Long.class, o -> new TemplateNumber(new LongNumber((Long) o)));
mapper.put(Integer.class, o -> new TemplateNumber(new IntegerNumber((Integer) o)));
mapper.put(Short.class, o -> new TemplateNumber(new ShortNumber((Short) o)));
mapper.put(Byte.class, o -> new TemplateNumber(new ByteNumber((Byte) o)));
mapper.put(Double.class, o -> new TemplateNumber(new DoubleNumber((Double) o)));
mapper.put(Float.class, o -> new TemplateNumber(new FloatNumber((Float) o)));
mapper.put(Boolean.class, o -> Boolean.TRUE.equals(o) ? TemplateBoolean.TRUE : TemplateBoolean.FALSE);
Map<Class<?>, Function<Object, TemplateObject>> mapper = new HashMap<>(); mapper.put(String.class, o -> new TemplateString((String) o)); mapper.put(Long.class, o -> new TemplateNumber(new LongNumber((Long) o))); mapper.put(Integer.class, o -> new TemplateNumber(new IntegerNumber((Integer) o))); mapper.put(Short.class, o -> new TemplateNumber(new ShortNumber((Short) o))); mapper.put(Byte.class, o -> new TemplateNumber(new ByteNumber((Byte) o))); mapper.put(Double.class, o -> new TemplateNumber(new DoubleNumber((Double) o))); mapper.put(Float.class, o -> new TemplateNumber(new FloatNumber((Float) o))); mapper.put(Boolean.class, o -> Boolean.TRUE.equals(o) ? TemplateBoolean.TRUE : TemplateBoolean.FALSE);
Map<Class<?>, Function<Object, TemplateObject>> mapper = new HashMap<>();
mapper.put(String.class, o -> new TemplateString((String) o));
mapper.put(Long.class, o -> new TemplateNumber(new LongNumber((Long) o)));
mapper.put(Integer.class, o -> new TemplateNumber(new IntegerNumber((Integer) o)));
mapper.put(Short.class, o -> new TemplateNumber(new ShortNumber((Short) o)));
mapper.put(Byte.class, o -> new TemplateNumber(new ByteNumber((Byte) o)));
mapper.put(Double.class, o -> new TemplateNumber(new DoubleNumber((Double) o)));
mapper.put(Float.class, o -> new TemplateNumber(new FloatNumber((Float) o)));
mapper.put(Boolean.class, o -> Boolean.TRUE.equals(o) ? TemplateBoolean.TRUE : TemplateBoolean.FALSE);

Der Schlüsse in der

Map
Map ist die jeweilige Ursprungsklasse und der Wert ist eine Funktion, die den eine Instanz der Ursprungsklasse in ein
TemplateObject
TemplateObject wrapped. Bei den Enum Subklassen funktioniert das aber nicht, weil sie alle unbekannterweise in der Map aufgeführt sein müssten.

Die bereits erwähnten Ausnahmen haben das gleiche Problem wie die Enums. Sie sind die generischen Basisklassen für Listen und Maps. Auch hier kann nicht mit der

Map
Map gearbeitet werden. Damit Enums auch gemappt werden können muss die zentrale
wrap
wrap Methode angepasst werden.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
private TemplateObject wrap(Object o) {
if (o == null) {
return TemplateNull.NULL;
}
if (o instanceof TemplateObject) {
return (TemplateObject) o;
}
if (o instanceof List) {
List<Object> values = (List<Object>) o;
return new TemplateListSequence(values);
}
if (o instanceof Map) {
Map<String, Object> values = (Map<String, Object>) o;
return new TemplateBean(values);
}
if (o instanceof Enum<?>) {
return new TemplateEnum<>((Enum)o);
}
Function<Object, TemplateObject> mapping = mapper.get(o.getClass());
if (mapping != null) {
return mapping.apply(o);
}
if (!o.getClass().isPrimitive() && !o.getClass().getName().startsWith("java")) {
return new TemplateBean(beanProvider.provide(o, this));
}
throw new UnsupportedDataTypeException("unsupported data type: " + o.getClass());
}
private TemplateObject wrap(Object o) { if (o == null) { return TemplateNull.NULL; } if (o instanceof TemplateObject) { return (TemplateObject) o; } if (o instanceof List) { List<Object> values = (List<Object>) o; return new TemplateListSequence(values); } if (o instanceof Map) { Map<String, Object> values = (Map<String, Object>) o; return new TemplateBean(values); } if (o instanceof Enum<?>) { return new TemplateEnum<>((Enum)o); } Function<Object, TemplateObject> mapping = mapper.get(o.getClass()); if (mapping != null) { return mapping.apply(o); } if (!o.getClass().isPrimitive() && !o.getClass().getName().startsWith("java")) { return new TemplateBean(beanProvider.provide(o, this)); } throw new UnsupportedDataTypeException("unsupported data type: " + o.getClass()); }
private TemplateObject wrap(Object o) {
  if (o == null) {
    return TemplateNull.NULL;
  }
  if (o instanceof TemplateObject) {
    return (TemplateObject) o;
  }
  if (o instanceof List) {
    List<Object> values = (List<Object>) o;
    return new TemplateListSequence(values);
  }
  if (o instanceof Map) {
    Map<String, Object> values = (Map<String, Object>) o;
    return new TemplateBean(values);
  }
  if (o instanceof Enum<?>) {
    return new TemplateEnum<>((Enum)o);
  }
  Function<Object, TemplateObject> mapping = mapper.get(o.getClass());
  if (mapping != null) {
    return mapping.apply(o);
  }
  if (!o.getClass().isPrimitive() && !o.getClass().getName().startsWith("java")) {
    return new TemplateBean(beanProvider.provide(o, this));
  }
  throw new UnsupportedDataTypeException("unsupported data type: " + o.getClass());
}

Es wird jetzt in der Methode auch geprüft, ob das

Object
Object vom Typ
Enum
Enum ist, um dann ggf. eine
TemplateEnum
TemplateEnum Instanz zu erzeugen.

Keine wirklich elegante Lösung, wenn der Anspruch besteht, eine erweiterbare Template-Engine zu entwickeln. Eine einfache Verbesserung ist das Verlagern der Logik in Implementierungen des Interface

TemplateObjectProvider
TemplateObjectProvider. Mit diesen Implementierungen reduziert sich die Logik in der
wrap
wrap Methode und durch die Liste von
TemplateObjectProvider
TemplateObjectProvider ist die Möglichkeit für eine saubere Erweiterbarkeit gegeben.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
private final List<TemplateObjectProvider> providers;
private TemplateObject wrap(Object o) {
if (o == null) {
return TemplateNull.NULL;
}
if (o instanceof TemplateObject) {
return (TemplateObject) o;
}
return providers.stream().map(p -> p.provide(this, o)).filter(Objects::nonNull)
.findFirst().orElseThrow(() -> new UnsupportedDataTypeException("unsupported data type: " + o.getClass()));
}
}
private final List<TemplateObjectProvider> providers; private TemplateObject wrap(Object o) { if (o == null) { return TemplateNull.NULL; } if (o instanceof TemplateObject) { return (TemplateObject) o; } return providers.stream().map(p -> p.provide(this, o)).filter(Objects::nonNull) .findFirst().orElseThrow(() -> new UnsupportedDataTypeException("unsupported data type: " + o.getClass())); } }
private final List<TemplateObjectProvider> providers;

private TemplateObject wrap(Object o) {
  if (o == null) {
    return TemplateNull.NULL;
  }
  if (o instanceof TemplateObject) {
    return (TemplateObject) o;
  }
  return providers.stream().map(p -> p.provide(this, o)).filter(Objects::nonNull)
      .findFirst().orElseThrow(() -> new UnsupportedDataTypeException("unsupported data type: " + o.getClass()));
}
  }

Die Liste der

TemplateObjectProvider
TemplateObjectProvider wird in der Konfiguration bereitgestellt und durch Plug-Ins ergänzt. Der ursprüngliche Code wirkt primitiv und fehleranfällig.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public void registerPlugin(PluginProvider provider) {
logger.info("register plugin: {}", provider.getClass().getSimpleName());
provider.registerBuildIn(builtIns);
provider.registerFormatter(formatter);
provider.registerMapper(mappingTemplateObjectProvider.getMapper());
provider.registerTemplateObjectProvider(providers);
}
public void registerPlugin(PluginProvider provider) { logger.info("register plugin: {}", provider.getClass().getSimpleName()); provider.registerBuildIn(builtIns); provider.registerFormatter(formatter); provider.registerMapper(mappingTemplateObjectProvider.getMapper()); provider.registerTemplateObjectProvider(providers); }
   public void registerPlugin(PluginProvider provider) {
    logger.info("register plugin: {}", provider.getClass().getSimpleName());
    provider.registerBuildIn(builtIns);
    provider.registerFormatter(formatter);
    provider.registerMapper(mappingTemplateObjectProvider.getMapper());
    provider.registerTemplateObjectProvider(providers);
}

Die internen Strukturen der Konfiguration werden direkt an die Methoden des

PluginProviders
PluginProviders übergeben und dieser befüllt sie nach eigenem Gusto. Das Exponieren interner Strukturen ist keine Best Practice und sollte unterlassen werden. An dieser Stelle ergibt sich zusätzlich das Problem, dass am Ende der Liste immer der
BeanTemplateObjectProvider
BeanTemplateObjectProvider stehen muss. Dieser
TemplateObjectProvider
TemplateObjectProvider ist die letzte Möglichkeit eine unbekannte Klasse als
TemplateBean
TemplateBean zur Verfügung zu stellen. Die
TemplateObjectProvider
TemplateObjectProvider die nach diesem eingefügt werden, erhalten kaum die Chance aufgerufen zu werden.

Aber auch diese einfache Schnittstelle lässt sich durch eine Indirektion sicherer und sauberer gestalten. Die folgende Version mit zwei zusätzlichen Zeilen sorgt auch die die Beachtung des

BeanTemplateObjectProvider
BeanTemplateObjectProvider.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public void registerPlugin(PluginProvider provider) {
logger.info("register plugin: {}", provider.getClass().getSimpleName());
provider.registerBuildIn(builtIns);
provider.registerFormatter(formatter);
provider.registerMapper(mappingTemplateObjectProvider.getMapper());
List<TemplateObjectProvider> list = new ArrayList<>();
provider.registerTemplateObjectProvider(list);
providers.addAll(providers.size() - 2, list);
}
public void registerPlugin(PluginProvider provider) { logger.info("register plugin: {}", provider.getClass().getSimpleName()); provider.registerBuildIn(builtIns); provider.registerFormatter(formatter); provider.registerMapper(mappingTemplateObjectProvider.getMapper()); List<TemplateObjectProvider> list = new ArrayList<>(); provider.registerTemplateObjectProvider(list); providers.addAll(providers.size() - 2, list); }
   public void registerPlugin(PluginProvider provider) {
    logger.info("register plugin: {}", provider.getClass().getSimpleName());
    provider.registerBuildIn(builtIns);
    provider.registerFormatter(formatter);
    provider.registerMapper(mappingTemplateObjectProvider.getMapper());
    List<TemplateObjectProvider> list = new ArrayList<>();
    provider.registerTemplateObjectProvider(list);
    providers.addAll(providers.size() - 2, list);
}

Statt die echte

providers
providers Liste zu übergeben, wird dem
PluginProvider
PluginProvider eine leere Liste übergeben. Nachdem die Liste befüllt wurde, fügt die Konfiguration die enthaltenen
TemplateObjectProvider
TemplateObjectProvider vor die letzte Position in der
providers
providers Liste ein. Auf diese Weise können auch die internen Konstrukte
builtIns
builtIns,
formatter
formatter und
mappingTemplateObjectProvider.getMapper()
mappingTemplateObjectProvider.getMapper() vor den
PluginProvider
PluginProvider verborgen werden.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<#list sequence as s>
${s?counter}. ${s} name=${s?c} ordinal=${s?ordinal}
</#list>
<#list sequence as s> ${s?counter}. ${s} name=${s?c} ordinal=${s?ordinal} </#list>
<#list sequence as s>
${s?counter}. ${s} name=${s?c} ordinal=${s?ordinal}
</#list>

Mit dem

EnumPluginProvider
EnumPluginProvider und dem obigen Template und dem Datenmodel
Map.of("sequence", EnumSet.allOf(TestEnum.class)))
Map.of("sequence", EnumSet.allOf(TestEnum.class))) produziert die Template-Engine die folgende Ausgabe.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
1. Α name=ALPHA ordinal=0
2. Β name=BETA ordinal=1
3. Γ name=GAMMA ordinal=2
4. Δ name=DELTA ordinal=3
1. Α name=ALPHA ordinal=0 2. Β name=BETA ordinal=1 3. Γ name=GAMMA ordinal=2 4. Δ name=DELTA ordinal=3
1. Α name=ALPHA ordinal=0
2. Β name=BETA ordinal=1
3. Γ name=GAMMA ordinal=2
4. Δ name=DELTA ordinal=3

Eine weitere Ergänzung in Form eines Plug-Ins ist die Unterstützung der Holidays Bibliothek. Das Plug-In stellt zwei Built-Ins für

LocalDate
LocalDate Interpolations bereit.

Das Built-In

get_holiday
get_holiday liefert den Namen eines Feiertages. Die erste Variante
${date?get_holiday}
${date?get_holiday} liefert für den 25. Dezember den Text “1. Weihnachtstag” und die zweite Variante
${date?get_holiday('Kein Feiertag')}
${date?get_holiday('Kein Feiertag')} liefert für den 24. August den Text “Kein Feiertag“.

Das Built-In

is_holiday
is_holiday liefert den boolean Wert
TRUE
TRUE wenn das entsprechende Datum ein Feiertag ist und anderenfalls den Wert
FALSE
FALSE.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@Override
public void registerBuildIn(Map<BuiltInKey, BuiltIn> builtIns) {
builtIns.put(DATE_BUILDER.of("get_holiday"),
new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext c) -> {
Locale locale = checkCountry(c.getEnvironment().getLocale());
TemplateLocalDate value = x.evaluate(c, TemplateLocalDate.class);
return new TemplateString(getHolidaysFromStore(c, locale).getHoliday(value.getValue()).orElseGet(() -> getOptionalFallback(y)));
}));
builtIns.put(DATE_BUILDER.of("is_holiday"),
new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext c) -> {
Locale locale = checkCountry(c.getEnvironment().getLocale());
TemplateLocalDate value = x.evaluate(c, TemplateLocalDate.class);
return TemplateBoolean.from(getHolidaysFromStore(c, locale).isHoliday(value.getValue()));
}));
}
@Override public void registerBuildIn(Map<BuiltInKey, BuiltIn> builtIns) { builtIns.put(DATE_BUILDER.of("get_holiday"), new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext c) -> { Locale locale = checkCountry(c.getEnvironment().getLocale()); TemplateLocalDate value = x.evaluate(c, TemplateLocalDate.class); return new TemplateString(getHolidaysFromStore(c, locale).getHoliday(value.getValue()).orElseGet(() -> getOptionalFallback(y))); })); builtIns.put(DATE_BUILDER.of("is_holiday"), new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext c) -> { Locale locale = checkCountry(c.getEnvironment().getLocale()); TemplateLocalDate value = x.evaluate(c, TemplateLocalDate.class); return TemplateBoolean.from(getHolidaysFromStore(c, locale).isHoliday(value.getValue())); })); }
@Override
public void registerBuildIn(Map<BuiltInKey, BuiltIn> builtIns) {
  builtIns.put(DATE_BUILDER.of("get_holiday"),
      new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext c) -> {
        Locale locale = checkCountry(c.getEnvironment().getLocale());
        TemplateLocalDate value = x.evaluate(c, TemplateLocalDate.class);
        return new TemplateString(getHolidaysFromStore(c, locale).getHoliday(value.getValue()).orElseGet(() -> getOptionalFallback(y)));
      }));
  builtIns.put(DATE_BUILDER.of("is_holiday"),
      new FunctionalBuiltIn((TemplateObject x, List<TemplateObject> y, ProcessContext c) -> {
        Locale locale = checkCountry(c.getEnvironment().getLocale());
        TemplateLocalDate value = x.evaluate(c, TemplateLocalDate.class);
        return TemplateBoolean.from(getHolidaysFromStore(c, locale).isHoliday(value.getValue()));
      }));
}

Beide Built-Ins sind als

FunctionalBuiltIn
FunctionalBuiltIn implementiert. Gemeinsam ist beiden Built-Ins, dass zuerst das aktuelle
Locale
Locale auf eine Länderangabe geprüft wird. Die Holidays Bibliothek benötigt diese Angabe, damit eine landesspezifische Feiertag geprüft werden kann. Zusätzlich wird aus dem übergebenen
TemplateObject
TemplateObject ein
LocalDate
LocalDate bestimmt. Danach wird eine
Holidays
Holidays Instanz für dieses
Locale
Locale erzeugt und die entsprechende Methode darauf aufgerufen.

Damit nicht jeder Aufruf im Dokument ein neues

Holidays
Holidays Objekt erzeugt, werden diese im
ProcessContext
ProcessContext zwischengespeichert.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
private Holidays getHolidaysFromStore(ProcessContext c, Locale locale) {
return (Holidays) c.getStore("holidays").computeIfAbsent(locale, l -> Holidays.in(locale, locale));
}
private Holidays getHolidaysFromStore(ProcessContext c, Locale locale) { return (Holidays) c.getStore("holidays").computeIfAbsent(locale, l -> Holidays.in(locale, locale)); }
private Holidays getHolidaysFromStore(ProcessContext c, Locale locale) {
  return (Holidays) c.getStore("holidays").computeIfAbsent(locale, l -> Holidays.in(locale, locale));
}

Dafür erhält der

ProcessContext
ProcessContext eine zusätzliche
Map
Map auf die jederzeit über
getStore
getStore zugegriffen werden kann. In dieser werden unter dem Schlüssel
holidays
holidays die
Holidays
Holidays Instanzen pro
Locale
Locale gespeichert.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<#list sequence as s>
${s?counter}. ${s} ${s?get_holiday}
</#list>
<#list sequence as s> ${s?counter}. ${s} ${s?get_holiday} </#list>
<#list sequence as s>
${s?counter}. ${s} ${s?get_holiday}
</#list>

Mit dem

HolidayPluginProvider
HolidayPluginProvider und dem obigen Template und dem Datenmodel
Map.of("sequence", Holidays.in(Locale.GERMANY).getHolidays(2022).keySet().stream().sorted().collected(toList()))
Map.of("sequence", Holidays.in(Locale.GERMANY).getHolidays(2022).keySet().stream().sorted().collected(toList())) produziert die Template-Engine die folgende Ausgabe.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
1. 2022-01-01 Neujahr
2. 2022-04-15 Karfreitag
3. 2022-04-18 Ostermontag
4. 2022-05-01 Tag der Arbeit
5. 2022-05-26 Christi Himmelfahrt
6. 2022-06-06 Pfingstmontag
7. 2022-10-03 Tag der Deutschen Einheit
8. 2022-12-25 1. Weihnachtstag
9. 2022-12-26 2. Weihnachtstag
1. 2022-01-01 Neujahr 2. 2022-04-15 Karfreitag 3. 2022-04-18 Ostermontag 4. 2022-05-01 Tag der Arbeit 5. 2022-05-26 Christi Himmelfahrt 6. 2022-06-06 Pfingstmontag 7. 2022-10-03 Tag der Deutschen Einheit 8. 2022-12-25 1. Weihnachtstag 9. 2022-12-26 2. Weihnachtstag
1. 2022-01-01 Neujahr
2. 2022-04-15 Karfreitag
3. 2022-04-18 Ostermontag
4. 2022-05-01 Tag der Arbeit
5. 2022-05-26 Christi Himmelfahrt
6. 2022-06-06 Pfingstmontag
7. 2022-10-03 Tag der Deutschen Einheit
8. 2022-12-25 1. Weihnachtstag
9. 2022-12-26 2. Weihnachtstag

Damit sind zwei neue Plugins für Freshmarker erstellt und nebenbei die Template-Engine durch neue Anforderungen verbessert worden. Schwächen am Code, wie die Änderungen an der Konfiguration durch die Plugins, können häufig durch kleine umsichtige Änderungen verbessert werden.

Leave a Comment