From 611b5bd27f23244fdf2a04d569671a719aedf31d Mon Sep 17 00:00:00 2001 From: jensp Date: Fri, 16 Feb 2018 20:36:10 +0000 Subject: [PATCH] =?UTF-8?q?CCM=20NG:=20Implementieren=20eines=20ThemeProce?= =?UTF-8?q?ssors=20f=C3=BCr=20Freemarker=20(#2759)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@5289 8810af33-2d31-482b-a856-94f89814c4df Former-commit-id: 3ceec91105ab4f8061b071972c631ce6cc4619a4 --- ccm-core/pom.xml | 5 + .../FreemarkerConfigurationProvider.java | 119 +++++++++ .../freemarker/FreemarkerThemeProcessor.java | 245 ++++++++++++++++++ .../org/libreccm/theming/utils/L10NUtils.java | 149 +++++++++++ .../theming/xslt/XsltThemeProcessor.java | 88 +------ .../ccm-freemarker/category-page.html.ftl | 108 ++++++++ .../themes/ccm-freemarker/footer.html.ftl | 19 ++ .../themes/ccm-freemarker/settings.properties | 2 + .../resources/themes/ccm-freemarker/style.css | 110 ++++++++ .../ccm-freemarker/texts/labels.properties | 5 + .../ccm-freemarker/theme-bundle.properties | 3 + .../themes/ccm-freemarker/theme.json | 6 + pom.xml | 6 + 13 files changed, 783 insertions(+), 82 deletions(-) create mode 100644 ccm-core/src/main/java/org/libreccm/theming/freemarker/FreemarkerConfigurationProvider.java create mode 100644 ccm-core/src/main/java/org/libreccm/theming/freemarker/FreemarkerThemeProcessor.java create mode 100644 ccm-core/src/main/java/org/libreccm/theming/utils/L10NUtils.java create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/category-page.html.ftl create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/footer.html.ftl create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/settings.properties create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/style.css create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/texts/labels.properties create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/theme-bundle.properties create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/theme.json diff --git a/ccm-core/pom.xml b/ccm-core/pom.xml index d5dcfbbf8..d85b96985 100644 --- a/ccm-core/pom.xml +++ b/ccm-core/pom.xml @@ -204,6 +204,11 @@ javax.json + + org.freemarker + freemarker + + net.sf.saxon Saxon-HE diff --git a/ccm-core/src/main/java/org/libreccm/theming/freemarker/FreemarkerConfigurationProvider.java b/ccm-core/src/main/java/org/libreccm/theming/freemarker/FreemarkerConfigurationProvider.java new file mode 100644 index 000000000..7864023bf --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/theming/freemarker/FreemarkerConfigurationProvider.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2018 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.theming.freemarker; + +import freemarker.cache.TemplateLoader; +import freemarker.cache.TemplateLookupStrategy; +import freemarker.template.Configuration; +import freemarker.template.TemplateExceptionHandler; +import org.libreccm.core.UnexpectedErrorException; +import org.libreccm.theming.ThemeInfo; +import org.libreccm.theming.Themes; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +/** + * + * @author Jens Pelzetter + */ +@ApplicationScoped +class FreemarkerConfigurationProvider { + + @Inject + private Themes themes; + + private final Map configurations = new HashMap<>(); + + protected Configuration getConfiguration(final ThemeInfo forTheme) { + + if (configurations.containsKey(forTheme)) { + + return configurations.get(forTheme); + } else { + + final Configuration configuration = new Configuration( + Configuration.VERSION_2_3_27); + configuration.setDefaultEncoding("UTF-8"); + configuration + .setTemplateExceptionHandler( + TemplateExceptionHandler.RETHROW_HANDLER); + configuration.setLogTemplateExceptions(false); + configuration.setWrapUncheckedExceptions(false); + configuration.setLocalizedLookup(false); + configuration.setTemplateLoader(new CcmTemplateLoader(forTheme)); + + configurations.put(forTheme, configuration); + + return configuration; + } + } + + private class CcmTemplateLoader implements TemplateLoader { + + private final ThemeInfo fromTheme; + + public CcmTemplateLoader(final ThemeInfo fromTheme) { + this.fromTheme = fromTheme; + } + + @Override + public Object findTemplateSource(final String name) throws IOException { + + return themes + .getFileFromTheme(fromTheme, name) + .orElseThrow(() -> new UnexpectedErrorException(String + .format("Failed to open Freemarker Template \"%s\" from " + + "theme \"%s\".", + name, + fromTheme.getName()))); + } + + @Override + public long getLastModified(final Object templateSource) { + + return -1; + } + + @Override + public Reader getReader(final Object templateSource, + final String encoding) throws IOException { + + final InputStream inputStream = (InputStream) templateSource; + return new InputStreamReader(inputStream, encoding); + } + + @Override + public void closeTemplateSource(final Object templateSource) + throws IOException { + + final InputStream inputStream = (InputStream) templateSource; + inputStream.close(); + } + + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/theming/freemarker/FreemarkerThemeProcessor.java b/ccm-core/src/main/java/org/libreccm/theming/freemarker/FreemarkerThemeProcessor.java new file mode 100644 index 000000000..28c3a1083 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/theming/freemarker/FreemarkerThemeProcessor.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2018 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.theming.freemarker; + +import static org.libreccm.theming.ThemeConstants.*; + +import freemarker.template.SimpleNumber; +import freemarker.template.SimpleScalar; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateMethodModelEx; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateScalarModel; +import org.libreccm.core.UnexpectedErrorException; +import org.libreccm.theming.ProcessesThemes; +import org.libreccm.theming.ThemeInfo; +import org.libreccm.theming.ThemeProcessor; +import org.libreccm.theming.ThemeProvider; +import org.libreccm.theming.manifest.ThemeTemplate; +import org.libreccm.theming.utils.L10NUtils; +import org.libreccm.theming.utils.SettingsUtils; +import org.libreccm.theming.utils.SystemInfoUtils; +import org.libreccm.theming.utils.TextUtils; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; + +/** + * + * @author Jens Pelzetter + */ +@ProcessesThemes("freemarker") +@RequestScoped +public class FreemarkerThemeProcessor implements ThemeProcessor { + + private static final long serialVersionUID = -5631706431004020559L; + + @Inject + private FreemarkerConfigurationProvider configurationProvider; + + @Inject + private L10NUtils l10nUtils; + + @Inject + private SystemInfoUtils systemInfoUtils; + + @Inject + private SettingsUtils settingsUtils; + + @Inject + private TextUtils textUtils; + + @Override + public String process(final Map page, + final ThemeInfo theme, + final ThemeProvider themeProvider) { + + final String pathToTemplate; + if (page.containsKey(PAGE_PARAMETER_TEMPLATE)) { + + final String templateName = (String) page + .get(PAGE_PARAMETER_TEMPLATE); + + final Optional template = theme + .getManifest() + .getTemplates() + .stream() + .filter(current -> current.getName().equals(templateName)) + .findAny(); + + if (template.isPresent()) { + pathToTemplate = template.get().getPath(); + } else { + throw new UnexpectedErrorException(String + .format("Theme \"%s\" does provide template \"%s\".", + theme.getName(), + templateName)); + } + } else { + pathToTemplate = theme.getManifest().getDefaultTemplate(); + } + + page.put("getContextPath", new GetContextPathMethod()); + page.put("getSetting", new GetSettingMethod(theme, themeProvider)); + page.put("localize", new LocalizeMethod(theme, themeProvider)); + page.put("truncateText", new TruncateTextMethod()); + + final Template template; + try { + template = configurationProvider + .getConfiguration(theme) + .getTemplate(pathToTemplate); + } catch (IOException ex) { + throw new UnexpectedErrorException(ex); + } + + final StringWriter writer = new StringWriter(); + try { + template.process(page, writer); + } catch (TemplateException | IOException ex) { + throw new UnexpectedErrorException(ex); + } + + return writer.toString(); + } + + private class GetContextPathMethod implements TemplateMethodModelEx { + + @Override + public Object exec(final List arguments) throws TemplateModelException { + + return systemInfoUtils.getContextPath(); + } + + } + + private class GetSettingMethod implements TemplateMethodModelEx { + + private final ThemeInfo fromTheme; + private final ThemeProvider themeProvider; + + public GetSettingMethod(final ThemeInfo fromTheme, + final ThemeProvider themeProvider) { + this.fromTheme = fromTheme; + this.themeProvider = themeProvider; + } + + @Override + public Object exec(final List arguments) throws TemplateModelException { + + switch (arguments.size()) { + case 2: { + final String filePath = ((TemplateScalarModel) arguments + .get(0)) + .getAsString(); + final String settingName = ((TemplateScalarModel) arguments + .get(0)) + .getAsString(); + + return settingsUtils.getSetting(fromTheme, + themeProvider, + filePath, + settingName); + } + case 3: { + final String filePath + = ((TemplateScalarModel) arguments.get(0)) + .getAsString(); + final String settingName + = ((TemplateScalarModel) arguments.get(1)) + .getAsString(); + final String defaultValue + = ((TemplateScalarModel) arguments.get(2)) + .getAsString(); + + return settingsUtils.getSetting(fromTheme, + themeProvider, + filePath, + settingName, + defaultValue); + } + default: + throw new TemplateModelException( + "Illegal number of arguments."); + } + } + + } + + private class LocalizeMethod implements TemplateMethodModelEx { + + private final ThemeInfo fromTheme; + private final ThemeProvider themeProvider; + + public LocalizeMethod(final ThemeInfo fromTheme, + final ThemeProvider themeProvider) { + this.fromTheme = fromTheme; + this.themeProvider = themeProvider; + } + + @Override + public Object exec(final List arguments) throws TemplateModelException { + + if (arguments.isEmpty()) { + throw new TemplateModelException("No string to localize."); + } + + final String bundle; + if (arguments.size() > 1) { + bundle = ((TemplateScalarModel) arguments.get(1)).getAsString(); + } else { + bundle = "theme-bundle"; + } + + final String key = ((TemplateScalarModel) arguments.get(0)) + .getAsString(); + + return l10nUtils.getText(fromTheme, themeProvider, bundle, key); + } + + } + + private class TruncateTextMethod implements TemplateMethodModelEx { + + @Override + public Object exec(final List arguments) throws TemplateModelException { + + if (arguments.size() == 2) { + final String text = ((TemplateScalarModel) arguments.get(0)) + .getAsString(); + final int length = ((SimpleNumber) arguments.get(1)) + .getAsNumber() + .intValue(); + + return textUtils.truncateText(text, length); + } else { + throw new TemplateModelException("Illegal number of arguments."); + } + } + + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/theming/utils/L10NUtils.java b/ccm-core/src/main/java/org/libreccm/theming/utils/L10NUtils.java new file mode 100644 index 000000000..f18982076 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/theming/utils/L10NUtils.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2018 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.theming.utils; + +import org.libreccm.l10n.GlobalizationHelper; +import org.libreccm.theming.ThemeInfo; +import org.libreccm.theming.ThemeProvider; +import org.libreccm.theming.xslt.XsltThemeProcessor; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; + +/** + * + * @author Jens Pelzetter + */ +@RequestScoped +public class L10NUtils implements Serializable { + + private static final long serialVersionUID = 7077097386650257429L; + + @Inject + private GlobalizationHelper globalizationHelper; + + public ResourceBundle getBundle(final ThemeInfo fromTheme, + final ThemeProvider themeProvider, + final String bundleName) { + + return ResourceBundle + .getBundle( + bundleName, + globalizationHelper.getNegotiatedLocale(), + new LocalizedResourceBundleControl(fromTheme, + themeProvider)); + } + + public String getText(final ThemeInfo fromTheme, + final ThemeProvider themeProvider, + final String bundleName, + final String key) { + + final ResourceBundle bundle = getBundle(fromTheme, + themeProvider, + bundleName); + + return bundle.getString(key); + } + + public String getText(final ThemeInfo fromTheme, + final ThemeProvider themeProvider, + final String bundleName, + final String key, + final String arguments) { + + final ResourceBundle bundle = getBundle(fromTheme, + themeProvider, + bundleName); + + return MessageFormat.format(bundle.getString(key), arguments); + } + + private class LocalizedResourceBundleControl + extends ResourceBundle.Control { + + private final ThemeInfo theme; + private final ThemeProvider themeProvider; + + public LocalizedResourceBundleControl( + final ThemeInfo theme, + final ThemeProvider themeProvider) { + + this.theme = theme; + this.themeProvider = themeProvider; + } + + @Override + public List getFormats(final String baseName) { + Objects.requireNonNull(baseName); + + return Arrays.asList("java.properties"); + } + + @Override + public ResourceBundle newBundle(final String baseName, + final Locale locale, + final String format, + final ClassLoader classLoader, + final boolean reload) + throws IllegalAccessException, + InstantiationException, + IOException { + + if ("java.properties".equals(format)) { + + final String bundleName = toBundleName(baseName, locale); + + final Optional inputStream = themeProvider + .getThemeFileAsStream(theme.getName(), + theme.getVersion(), + String.format("%s.properties", + bundleName)); + if (inputStream.isPresent()) { + return new PropertyResourceBundle(inputStream.get()); + } else { + return super.newBundle(baseName, + locale, + format, + classLoader, + reload); + } + + } else { + return super.newBundle(baseName, + locale, + format, + classLoader, + reload); + } + } + } +} diff --git a/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java b/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java index 6b4a75306..16e572dab 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java +++ b/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java @@ -57,6 +57,7 @@ import org.libreccm.l10n.GlobalizationHelper; import org.libreccm.theming.ProcessesThemes; import org.libreccm.theming.Themes; import org.libreccm.theming.manifest.ThemeTemplate; +import org.libreccm.theming.utils.L10NUtils; import org.libreccm.theming.utils.SettingsUtils; import org.libreccm.theming.utils.SystemInfoUtils; import org.libreccm.theming.utils.TextUtils; @@ -105,7 +106,7 @@ public class XsltThemeProcessor implements ThemeProcessor { .getLogger(XsltThemeProcessor.class); @Inject - private GlobalizationHelper globalizationHelper; + private L10NUtils l10nUtils; @Inject private SettingsUtils settingsUtils; @@ -182,15 +183,6 @@ public class XsltThemeProcessor implements ThemeProcessor { pathToTemplate = theme.getManifest().getDefaultTemplate(); } -// final InputStream xslFileInputStream = themeProvider -// .getThemeFileAsStream(theme.getName(), -// theme.getVersion(), -// pathToTemplate) -// .orElseThrow(() -> new UnexpectedErrorException(String -// .format("Failed to open XSL file \"%s\" from theme \"%s\" for " -// + "reading.", -// pathToTemplate, -// theme.getName()))); final InputStream xslFileInputStream = themes .getFileFromTheme(theme, pathToTemplate) @@ -231,9 +223,6 @@ public class XsltThemeProcessor implements ThemeProcessor { final Transformer transformer; try { transformer = transformerFactory.newTransformer(xslFileStreamSource); -// transformer.setURIResolver(new CcmUriResolver(theme.getName(), -// theme.getVersion(), -// themeProvider)); transformer.setErrorListener(new ErrorListener() { @Override @@ -492,15 +481,11 @@ public class XsltThemeProcessor implements ThemeProcessor { key, bundle); - final ResourceBundle resourceBundle = ResourceBundle - .getBundle( - bundle, - globalizationHelper.getNegotiatedLocale(), - new LocalizedResourceBundleControl(theme, - themeProvider)); - return StringValue - .makeStringValue(resourceBundle.getString(key)); + .makeStringValue(l10nUtils.getText(theme, + themeProvider, + bundle, + key)); } }; @@ -508,67 +493,6 @@ public class XsltThemeProcessor implements ThemeProcessor { } - private class LocalizedResourceBundleControl - extends ResourceBundle.Control { - - private final ThemeInfo theme; - private final ThemeProvider themeProvider; - - public LocalizedResourceBundleControl( - final ThemeInfo theme, - final ThemeProvider themeProvider) { - - this.theme = theme; - this.themeProvider = themeProvider; - } - - @Override - public List getFormats(final String baseName) { - Objects.requireNonNull(baseName); - - return Arrays.asList("java.properties"); - } - - @Override - public ResourceBundle newBundle(final String baseName, - final Locale locale, - final String format, - final ClassLoader classLoader, - final boolean reload) - throws IllegalAccessException, - InstantiationException, - IOException { - - if ("java.properties".equals(format)) { - - final String bundleName = toBundleName(baseName, locale); - - final Optional inputStream = themeProvider - .getThemeFileAsStream(theme.getName(), - theme.getVersion(), - String.format("%s.properties", - bundleName)); - if (inputStream.isPresent()) { - return new PropertyResourceBundle(inputStream.get()); - } else { - return super.newBundle(baseName, - locale, - format, - classLoader, - reload); - } - - } else { - return super.newBundle(baseName, - locale, - format, - classLoader, - reload); - } - } - - } - private class TruncateTextFunctionDefinition extends ExtensionFunctionDefinition { diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/category-page.html.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/category-page.html.ftl new file mode 100644 index 000000000..743103d28 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/category-page.html.ftl @@ -0,0 +1,108 @@ + + + + Category page + + + +
+
    + + <#list newsList.items as item> +
  • + + + + <#list item.attachments as attachmentList> + <#if attachmentList.name = ".images"> + + + + + + "${item.title}" + +
  • + +
+
+ <#list articles.items as item> +
+

+ ${item.title}" +

+

+ <#list item.attachments as attachmentList> + <#if attachmentList.name = ".images"> + + + + ${item.description}" +

+
+ +
+ +

Example of Theme Utils

+
+
+ getContextPath +
+
+ + ${getContextPath()} + +
+
+ getSetting +
+
+ + ${getSetting("settings.properties", "example.setting", "n/a")} + +
+
+ truncateText +
+
+ + ${truncateText("0123456789 123456789 123456789", 20)} + +
+
+ localized('label.critical') +
+
+ ${localize("label.critical", "texts/labels")} +
+
+ localized('label.error') +
+
+ ${localize("label.error", "texts/labels")} +
+
+ localized('label.ok') +
+
+ ${localize("label.ok", "texts/labels")} +
+
+ localized('label.warning') +
+
+ ${localize("label.warning", "texts/labels")} +
+
+ +
+ <#include "footer.html.ftl"> + + + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/footer.html.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/footer.html.ftl new file mode 100644 index 000000000..013aff2b7 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/footer.html.ftl @@ -0,0 +1,19 @@ + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/settings.properties b/ccm-core/src/main/resources/themes/ccm-freemarker/settings.properties new file mode 100644 index 000000000..8b100e968 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/settings.properties @@ -0,0 +1,2 @@ +example.setting=Properties from the Freemarker theme. + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/style.css b/ccm-core/src/main/resources/themes/ccm-freemarker/style.css new file mode 100644 index 000000000..fbbda168f --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/style.css @@ -0,0 +1,110 @@ +* { + box-sizing: border-box; + + margin: 0; + padding: 0; +} + +ul.news { + + background-color: #000; + + margin: 0 auto 3em auto; + + width: 100vw; + + padding: 3em; +} + +ul.news li { + + display: flex; + + margin-left: auto; + margin-right: auto; + + max-width: 50em; + +} + +ul.news li img { + max-height: 20em; +} + +ul.news li span { + + color: #fff; + + flex: 1; + + font-size: 2rem; + + padding-left: 1em; + padding-right: 1em; +} + +main div.boxes { + display: flex; + + margin: 3em auto 3em auto; + + max-width: 80em; +} + +main div.boxes div { + position: relative; + + flex: 1; + + margin: 0 3em; +} + +main div.boxes div p img { + width: 100%; +} + +main div.boxes div a { + +} + +footer { + background-color: #000; + color: #fff; + + width: 100vw; +} + +footer ul { + + list-style: none; + + margin-left: auto; + margin-right: auto; + + max-width: 80em; + + padding-top: 4em; + padding-bottom: 4em; +} + +footer ul li { + display: inline-block; +} + +footer ul li:not(:first-child) { + margin-left: 4em; +} + +footer ul li a:link { + + color: #fff; + text-decoration: none; +} + +footer ul li a:focus, footer ul li a:hover { + + color: #fff; + text-decoration: underline; +} + + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/texts/labels.properties b/ccm-core/src/main/resources/themes/ccm-freemarker/texts/labels.properties new file mode 100644 index 000000000..c69a4cb3f --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/texts/labels.properties @@ -0,0 +1,5 @@ +label.critical=Critical +label.error=Error +label.ok=OK +label.warning=Warning + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/theme-bundle.properties b/ccm-core/src/main/resources/themes/ccm-freemarker/theme-bundle.properties new file mode 100644 index 000000000..5df4b5be9 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/theme-bundle.properties @@ -0,0 +1,3 @@ +footer.impressum=Impressum +footer.privacy=Privacy + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json b/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json new file mode 100644 index 000000000..b3e021ea8 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json @@ -0,0 +1,6 @@ +{ + "name": "ccm-freemarker", + "type": "freemarker", + + "default-template": "category-page.html.ftl" +} diff --git a/pom.xml b/pom.xml index 379bf2624..229e26243 100644 --- a/pom.xml +++ b/pom.xml @@ -580,6 +580,12 @@ 1.1.2
+ + org.freemarker + freemarker + 2.3.27-incubating + + net.sf.saxon Saxon-HE