diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 8cefb3587..c23b63b01 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -20,11 +20,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up JDK 21 - uses: actions/setup-java@v4 + - uses: actions/checkout@v5 + - name: Set up JDK 25 + uses: actions/setup-java@v5 with: - java-version: '21' + java-version: '25-ea' distribution: 'temurin' cache: maven - name: Build with Maven diff --git a/.gitignore b/.gitignore index 0b3f1f96a..6393afa36 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ test-server/hosts/marx-software test-server/hosts/demo/modules_data/search-module/index test-server/hosts/demo/modules_data/ui-module test-server/hosts/demo/temp/ +test-server/hosts/demo/data/ +test-server/hosts/demo_spa/data/ +test-server/hosts/demo_de/data/ test-server/hosts/features/modules_data/ test-server/hosts/features/temp test-server/hosts/features_de/modules_data/ @@ -65,3 +68,7 @@ test-server/modules/markedj-module test-server/cms.pid test-server/hosts/features/data/ .DS_Store +test-server/mail-server/fake-smtp-server-2.4.2.jar +modules/api-module/src/test/resources/site/data +cms-content/src/test/resources/site/data +integration-tests/hosts/test/data \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 7dbfe07fb..12fbe1e90 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -16,4 +16,4 @@ # under the License. wrapperVersion=3.3.2 distributionType=only-script -distributionUrl=http://book-build:8081/nexus/content/groups/public/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/CHANGELOG.md b/CHANGELOG.md index a3aff0a4f..92c04fe26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,11 @@ see wiki for more information: [wiki](https://github.com/thmarx/cms/wiki) # changelog -## 7.9.0 +## 8.0.0 + +* **BREAKING CHANGE** Sorted sections now use the _layout.order_ meta attribute for sorting +* **BREAKING CHANGE** ShortCodes are renamed to tags +* **BREAKING CHANGE** The default value for published switched from _true_ to _false_ * **BUGFIX** TemplateEngine should use cache only if activated [456](https://github.com/CondationCMS/cms-server/issues/456) * **MAINTENANCE** multiple dependencies updated @@ -17,9 +21,12 @@ see wiki for more information: [wiki](https://github.com/thmarx/cms/wiki) * **FEATURE** Aliases for content [442](https://github.com/CondationCMS/cms-server/issues/442) * **FEATURE** Add redirect support for aliases [454](https://github.com/CondationCMS/cms-server/issues/454) * **FEATURE** Signature for modules and themes [471](https://github.com/CondationCMS/cms-server/issues/471) -* **FEATURE** Switch password has to secure algorithm [472](https://github.com/CondationCMS/cms-server/issues/472) +* **FEATURE** Switch password hash to secure algorithm [472](https://github.com/CondationCMS/cms-server/issues/472) * **FEATURE** Simple http api for basic use cases [479](https://github.com/CondationCMS/cms-server/issues/479) * **FEATURE** Add spa mode for sites [476](https://github.com/CondationCMS/cms-server/issues/476) +* **FEATURE** Custom repository urls for modules, extensions and themes [466](https://github.com/CondationCMS/cms-server/issues/466) +* **FEATURE** UI to manage content [PR-446](https://github.com/CondationCMS/cms-server/pull/446) +* **FEATURE** Introduce server modules [503](https://github.com/CondationCMS/cms-server/issues/503) ### Developer experience diff --git a/cms-api/pom.xml b/cms-api/pom.xml index 4b344617e..d0c3846cc 100644 --- a/cms-api/pom.xml +++ b/cms-api/pom.xml @@ -4,11 +4,24 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 cms-api jar - + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + + --enable-preview + + + + + org.projectlombok @@ -27,6 +40,10 @@ org.yaml snakeyaml + + com.google.code.gson + gson + org.eclipse.jetty jetty-http @@ -40,9 +57,19 @@ org.slf4j slf4j-api + com.google.inject guice + classes + + + org.ow2.asm + asm + 9.8 + + 25 + diff --git a/cms-api/src/main/java/com/condation/cms/api/Constants.java b/cms-api/src/main/java/com/condation/cms/api/Constants.java index 0ffb647d3..c8d2c4ef9 100644 --- a/cms-api/src/main/java/com/condation/cms/api/Constants.java +++ b/cms-api/src/main/java/com/condation/cms/api/Constants.java @@ -56,6 +56,8 @@ public static class MetaFields { public static final String MENU_POSITION = "position"; public static final String MENU_TITLE = "title"; + public static final String LAYOUT_ORDER = "layout.order"; + public static final String REDIRECT_STATUS = "redirect.status"; public static final String REDIRECT_LOCATION = "redirect.location"; @@ -65,6 +67,8 @@ public static class MetaFields { public static final String ALIASES = "aliases"; public static final String ALIASES_REDIRECT = "aliases_redirect"; + + public static final String TRANSLATIONS = "translations"; } public static class Folders { @@ -97,13 +101,13 @@ public static class ContentTypes { return Pattern.compile("%s\\.(?
[a-zA-Z0-9-]+)\\.md".formatted(Pattern.quote(fileName))); }; - public static final Pattern SECTION_ORDERED_PATTERN = Pattern.compile("[\\w-]+\\.(?
[a-zA-Z0-9-]+)\\.(?\\d+)\\.md"); + public static final Pattern SECTION_NAMED_PATTERN = Pattern.compile("[\\w-]+\\.(?
[a-zA-Z0-9-]+)\\.(?[\\w-]+)\\.md"); - public static final Function SECTION_ORDERED_OF_PATTERN = (fileName) -> { - return Pattern.compile("%s\\.[a-zA-Z0-9-]+\\.[0-9]+\\.md".formatted(Pattern.quote(fileName))); + public static final Function SECTION_NAMED_OF_PATTERN = (fileName) -> { + return Pattern.compile("%s\\.[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+\\.md".formatted(Pattern.quote(fileName))); }; - public static final int DEFAULT_SECTION_ORDERED_INDEX = 0; + public static final int DEFAULT_SECTION_LAYOUT_ORDER = 0; public static final double DEFAULT_MENU_POSITION = 1000f; public static final boolean DEFAULT_MENU_VISIBILITY = true; public static final int DEFAULT_EXCERPT_LENGTH = 200; @@ -111,18 +115,24 @@ public static class ContentTypes { public static final int DEFAULT_PAGE_SIZE = 5; public static final String DEFAULT_CONTENT_TYPE = ContentTypes.HTML; - public static final List DEFAULT_CONTENT_PIPELINE = List.of("markdown", "shortcode"); + public static final List DEFAULT_CONTENT_PIPELINE = List.of("markdown", "tags"); public static final int DEFAULT_REDIRECT_STATUS = 301; public static final String DEFAULT_CACHE_ENGINE = "local"; public static final boolean DEFAULT_CONTENT_CACHE_ENABLED = false; - public static final String DEFAULT_MODULE_NAMESPACE = "mod"; + public static class TemplateNamespaces { + public static final String DEFAULT_MODULE_NAMESPACE = "mod"; + public static final String CMS = "cms"; + public static final String NODE = "node"; + public static final String SITE = "site"; + } public static class Taxonomy { public static final String DEFAULT_TEMPLATE = "taxonomy.html"; public static final String DEFAULT_SINGLE_TEMPLATE = "taxonomy.single.html"; } + public static final String REQUEST_CONTEXT_ATTRIBUTE_NAME = "_requestContext"; } diff --git a/cms-api/src/main/java/com/condation/cms/api/ServerProperties.java b/cms-api/src/main/java/com/condation/cms/api/ServerProperties.java index 7bbe13498..697dfd580 100644 --- a/cms-api/src/main/java/com/condation/cms/api/ServerProperties.java +++ b/cms-api/src/main/java/com/condation/cms/api/ServerProperties.java @@ -51,4 +51,8 @@ public interface ServerProperties { public List themeRepositories (); public List extensionRepositories (); + + String secret(); + + public List activeModules(); } diff --git a/cms-api/src/main/java/com/condation/cms/api/SiteProperties.java b/cms-api/src/main/java/com/condation/cms/api/SiteProperties.java index a66c8d807..706823f96 100644 --- a/cms-api/src/main/java/com/condation/cms/api/SiteProperties.java +++ b/cms-api/src/main/java/com/condation/cms/api/SiteProperties.java @@ -38,6 +38,8 @@ public interface SiteProperties { public String contextPath (); + public String baseUrl (); + public String id (); public Object get (String field); @@ -67,4 +69,8 @@ public interface SiteProperties { public default boolean spaEnabled () { return false; } + + public UIProperties ui(); + + public TranslationProperties translation (); } diff --git a/cms-api/src/main/java/com/condation/cms/api/TranslationProperties.java b/cms-api/src/main/java/com/condation/cms/api/TranslationProperties.java new file mode 100644 index 000000000..e3d55bbbb --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/TranslationProperties.java @@ -0,0 +1,39 @@ +package com.condation.cms.api; + +import java.util.List; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public interface TranslationProperties { + boolean isEnabled(); + + List getLanguages(); + + List getMapping (); + + public static record Mapping (String site, String language){}; +} diff --git a/cms-api/src/main/java/com/condation/cms/api/UIProperties.java b/cms-api/src/main/java/com/condation/cms/api/UIProperties.java new file mode 100644 index 000000000..fc1de4287 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/UIProperties.java @@ -0,0 +1,33 @@ +package com.condation.cms.api; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public interface UIProperties { + boolean force2fa(); + + boolean managerEnabled (); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/annotations/FeatureScope.java b/cms-api/src/main/java/com/condation/cms/api/annotations/FeatureScope.java index 650ab8f2f..798a703bb 100644 --- a/cms-api/src/main/java/com/condation/cms/api/annotations/FeatureScope.java +++ b/cms-api/src/main/java/com/condation/cms/api/annotations/FeatureScope.java @@ -43,7 +43,8 @@ public enum Scope { REQUEST, GLOBAL, - MODULE + MODULE, + SERVER } } diff --git a/cms-api/src/main/java/com/condation/cms/api/annotations/ShortCode.java b/cms-api/src/main/java/com/condation/cms/api/annotations/Tag.java similarity index 97% rename from cms-api/src/main/java/com/condation/cms/api/annotations/ShortCode.java rename to cms-api/src/main/java/com/condation/cms/api/annotations/Tag.java index 9cef1bf04..7b39f8a24 100644 --- a/cms-api/src/main/java/com/condation/cms/api/annotations/ShortCode.java +++ b/cms-api/src/main/java/com/condation/cms/api/annotations/Tag.java @@ -33,6 +33,6 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) -public @interface ShortCode { +public @interface Tag { String value (); } diff --git a/cms-api/src/main/java/com/condation/cms/api/annotations/TemplateFunction.java b/cms-api/src/main/java/com/condation/cms/api/annotations/TemplateFunction.java new file mode 100644 index 000000000..64180d58b --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/annotations/TemplateFunction.java @@ -0,0 +1,38 @@ +package com.condation.cms.api.annotations; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface TemplateFunction { + String value (); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/auth/Permissions.java b/cms-api/src/main/java/com/condation/cms/api/auth/Permissions.java new file mode 100644 index 000000000..7a54fd011 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/auth/Permissions.java @@ -0,0 +1,32 @@ +package com.condation.cms.api.auth; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thmar + */ +public class Permissions { + public static final String CONTENT_EDIT = "content.edit"; + public static final String CACHE_INVALIDATE = "cache.invalidate"; +} diff --git a/cms-api/src/main/java/com/condation/cms/api/cache/CacheProvider.java b/cms-api/src/main/java/com/condation/cms/api/cache/CacheProvider.java index c1de8888c..34afedd14 100644 --- a/cms-api/src/main/java/com/condation/cms/api/cache/CacheProvider.java +++ b/cms-api/src/main/java/com/condation/cms/api/cache/CacheProvider.java @@ -23,7 +23,6 @@ */ -import java.io.Serializable; import java.util.function.Function; /** diff --git a/cms-core/src/main/java/com/condation/cms/core/content/MapAccess.java b/cms-api/src/main/java/com/condation/cms/api/content/MapAccess.java similarity index 95% rename from cms-core/src/main/java/com/condation/cms/core/content/MapAccess.java rename to cms-api/src/main/java/com/condation/cms/api/content/MapAccess.java index dcd0423d8..04e2e28a5 100644 --- a/cms-core/src/main/java/com/condation/cms/core/content/MapAccess.java +++ b/cms-api/src/main/java/com/condation/cms/api/content/MapAccess.java @@ -1,4 +1,4 @@ -package com.condation.cms.core.content; +package com.condation.cms.api.content; /*- * #%L @@ -35,6 +35,10 @@ public class MapAccess implements Map { private final Map wrapped; + public static MapAccess of (Map map) { + return new MapAccess(map); + } + @Override public int size() { return wrapped.size(); diff --git a/cms-api/src/main/java/com/condation/cms/api/db/ContentNode.java b/cms-api/src/main/java/com/condation/cms/api/db/ContentNode.java index ad9f6b79a..6af063ef3 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/ContentNode.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/ContentNode.java @@ -26,7 +26,8 @@ import com.condation.cms.api.feature.features.IsPreviewFeature; import com.condation.cms.api.feature.features.SitePropertiesFeature; import com.condation.cms.api.request.RequestContext; -import com.condation.cms.api.request.ThreadLocalRequestContext; +import com.condation.cms.api.request.RequestContextScope; +import com.condation.cms.api.utils.DateRange; import com.condation.cms.api.utils.MapUtil; import com.condation.cms.api.utils.SectionUtil; import java.io.Serializable; @@ -69,8 +70,8 @@ public boolean isView () { public String contentType() { String defaultContentType = Constants.DEFAULT_CONTENT_TYPE; - if (ThreadLocalRequestContext.REQUEST_CONTEXT.get() != null) { - RequestContext requestContext = ThreadLocalRequestContext.REQUEST_CONTEXT.get(); + if (RequestContextScope.REQUEST_CONTEXT.isBound()) { + RequestContext requestContext = RequestContextScope.REQUEST_CONTEXT.get(); defaultContentType = requestContext.get(SitePropertiesFeature.class).siteProperties().defaultContentType(); } return (String) ((Map) data @@ -100,29 +101,28 @@ public boolean isHidden() { } public boolean isDraft() { - return !((boolean) data().getOrDefault(Constants.MetaFields.PUBLISHED, true)); + return !((boolean) data().getOrDefault(Constants.MetaFields.PUBLISHED, false)); + } + + public boolean isParentPathHidden () { + return uri().startsWith(".") || uri().contains("/."); } - public boolean isPublished() { - if (ThreadLocalRequestContext.REQUEST_CONTEXT.get() != null - && ThreadLocalRequestContext.REQUEST_CONTEXT.get().has(IsPreviewFeature.class)) { + public boolean isVisible() { + if (RequestContextScope.REQUEST_CONTEXT.isBound() + && RequestContextScope.REQUEST_CONTEXT.get().has(IsPreviewFeature.class)) { return true; } - + if (isDraft()) { return false; } + var publish_date = (Date) data.getOrDefault(Constants.MetaFields.PUBLISH_DATE, Date.from(Instant.now())); - var now = Date.from(Instant.now()); - if (!(publish_date.before(now) || publish_date.equals(now))) { - return false; - } + var unpublish_date = (Date) data.getOrDefault(Constants.MetaFields.UNPUBLISH_DATE, null); - if (unpublish_date != null - && (unpublish_date.before(now) || unpublish_date.equals(now))) { - return false; - } - return true; + + return DateRange.isNowWithin(publish_date, unpublish_date); } public boolean isSection() { diff --git a/cms-api/src/main/java/com/condation/cms/api/db/ContentQuery.java b/cms-api/src/main/java/com/condation/cms/api/db/ContentQuery.java index b541a85cd..b57376f42 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/ContentQuery.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/ContentQuery.java @@ -66,6 +66,8 @@ public interface ContentQuery { ContentQuery whereNotIn(final String field, final List value); ContentQuery whereExists(final String field); + + ContentQuery expression(final String expressions); public static interface Sort { public ContentQuery asc(); diff --git a/cms-api/src/main/java/com/condation/cms/api/db/cms/NIOReadOnlyFile.java b/cms-api/src/main/java/com/condation/cms/api/db/cms/NIOReadOnlyFile.java index 7ac2e682a..5c3042aaa 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/cms/NIOReadOnlyFile.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/cms/NIOReadOnlyFile.java @@ -46,6 +46,16 @@ public class NIOReadOnlyFile implements ReadOnlyFile { protected final Path file; private final Path basePath; + @Override + public String uri () { + return PathUtil.toURL(file, basePath); + } + + @Override + public String relativePath() { + return PathUtil.toRelativeFile(file, basePath); + } + @Override public boolean exists() { return Files.exists(file); @@ -116,7 +126,9 @@ public ReadOnlyFile getParent() { @Override public List children() throws IOException { - return Files.list(file).map(child -> new NIOReadOnlyFile(child, basePath)).map(ReadOnlyFile.class::cast).toList(); + try (var childrenStream = Files.list(file)) { + return childrenStream.map(child -> new NIOReadOnlyFile(child, basePath)).map(ReadOnlyFile.class::cast).toList(); + } } @Override diff --git a/cms-api/src/main/java/com/condation/cms/api/db/cms/ReadOnlyFile.java b/cms-api/src/main/java/com/condation/cms/api/db/cms/ReadOnlyFile.java index b5e17c4d8..371eb0366 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/cms/ReadOnlyFile.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/cms/ReadOnlyFile.java @@ -34,6 +34,10 @@ public interface ReadOnlyFile { boolean exists (); + String uri (); + + String relativePath(); + ReadOnlyFile resolve (String uri); String getContent () throws IOException; diff --git a/cms-api/src/main/java/com/condation/cms/api/eventbus/EventListener.java b/cms-api/src/main/java/com/condation/cms/api/eventbus/EventListener.java index fe5ee4843..cf797d475 100644 --- a/cms-api/src/main/java/com/condation/cms/api/eventbus/EventListener.java +++ b/cms-api/src/main/java/com/condation/cms/api/eventbus/EventListener.java @@ -26,6 +26,7 @@ /** * * @author t.marx + * @param */ public interface EventListener { diff --git a/cms-api/src/main/java/com/condation/cms/api/request/ThreadLocalRequestContext.java b/cms-api/src/main/java/com/condation/cms/api/eventbus/events/InvalidateMediaCache.java similarity index 80% rename from cms-api/src/main/java/com/condation/cms/api/request/ThreadLocalRequestContext.java rename to cms-api/src/main/java/com/condation/cms/api/eventbus/events/InvalidateMediaCache.java index cd75d22e4..7230ad0a9 100644 --- a/cms-api/src/main/java/com/condation/cms/api/request/ThreadLocalRequestContext.java +++ b/cms-api/src/main/java/com/condation/cms/api/eventbus/events/InvalidateMediaCache.java @@ -1,4 +1,4 @@ -package com.condation.cms.api.request; +package com.condation.cms.api.eventbus.events; /*- * #%L @@ -23,10 +23,11 @@ */ +import com.condation.cms.api.eventbus.Event; +import java.nio.file.Path; + /** * * @author t.marx */ -public class ThreadLocalRequestContext { - public static ThreadLocal REQUEST_CONTEXT = new ThreadLocal<>(); -} +public record InvalidateMediaCache (Path mediaPath) implements Event {} diff --git a/cms-api/src/main/java/com/condation/cms/api/eventbus/events/ReIndexContentMetaDataEvent.java b/cms-api/src/main/java/com/condation/cms/api/eventbus/events/ReIndexContentMetaDataEvent.java index 7520131ac..2ebea142e 100644 --- a/cms-api/src/main/java/com/condation/cms/api/eventbus/events/ReIndexContentMetaDataEvent.java +++ b/cms-api/src/main/java/com/condation/cms/api/eventbus/events/ReIndexContentMetaDataEvent.java @@ -29,4 +29,8 @@ * * @author t.marx */ -public record ReIndexContentMetaDataEvent () implements Event {} +public record ReIndexContentMetaDataEvent (String uri) implements Event { + public ReIndexContentMetaDataEvent () { + this(null); + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/exceptions/AnnotationExecutionException.java b/cms-api/src/main/java/com/condation/cms/api/exceptions/AnnotationExecutionException.java new file mode 100644 index 000000000..9e3662eb5 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/exceptions/AnnotationExecutionException.java @@ -0,0 +1,46 @@ +package com.condation.cms.api.exceptions; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + + +/** + * + * @author t.marx + */ +public class AnnotationExecutionException extends RuntimeException { + + /** + * Creates a new instance of AccessNotAllowedException without detail message. + */ + public AnnotationExecutionException() { + } + + /** + * Constructs an instance of AccessNotAllowedException with the specified detail message. + * + * @param msg the detail message. + */ + public AnnotationExecutionException(String msg) { + super(msg); + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/AbstractExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/AbstractExtensionPoint.java index 3ef887553..1ae62c05c 100644 --- a/cms-api/src/main/java/com/condation/cms/api/extensions/AbstractExtensionPoint.java +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/AbstractExtensionPoint.java @@ -23,8 +23,8 @@ */ -import com.condation.cms.api.module.CMSModuleContext; -import com.condation.cms.api.module.CMSRequestContext; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.module.SiteRequestContext; import com.condation.modules.api.ExtensionPoint; import com.condation.modules.api.ModuleConfiguration; import lombok.Getter; @@ -33,13 +33,13 @@ * * @author t.marx */ -public abstract class AbstractExtensionPoint implements ExtensionPoint { +public abstract class AbstractExtensionPoint implements ExtensionPoint { @Getter protected ModuleConfiguration moduleConfiguration; @Getter - protected CMSModuleContext context; + protected SiteModuleContext context; @Getter - protected CMSRequestContext requestContext; + protected SiteRequestContext requestContext; @Override public void setConfiguration(ModuleConfiguration configuration) { @@ -47,11 +47,11 @@ public void setConfiguration(ModuleConfiguration configuration) { } @Override - public void setContext(CMSModuleContext context) { + public void setContext(SiteModuleContext context) { this.context = context; } @Override - public void setRequestContext(CMSRequestContext requestContext) { + public void setRequestContext(SiteRequestContext requestContext) { this.requestContext = requestContext; } diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/BackupFilePostProcessingExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/BackupFilePostProcessingExtensionPoint.java new file mode 100644 index 000000000..0138f4cf0 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/BackupFilePostProcessingExtensionPoint.java @@ -0,0 +1,38 @@ +package com.condation.cms.api.extensions; + +import java.nio.file.Path; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + + + + +/** + * + * @author t.marx + */ +public abstract class BackupFilePostProcessingExtensionPoint extends AbstractExtensionPoint { + + public abstract void postProcess (Path backupFile); + +} diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/HookSystemRegisterExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/HookSystemRegisterExtensionPoint.java index 7d875ad52..be18db96e 100644 --- a/cms-api/src/main/java/com/condation/cms/api/extensions/HookSystemRegisterExtensionPoint.java +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/HookSystemRegisterExtensionPoint.java @@ -30,6 +30,5 @@ */ public abstract class HookSystemRegisterExtensionPoint extends AbstractExtensionPoint{ - public abstract void register (final HookSystem hookSystem); - + public void register (final HookSystem hookSystem) {} } diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/Mapping.java b/cms-api/src/main/java/com/condation/cms/api/extensions/Mapping.java index 7363eb7cc..df4045d98 100644 --- a/cms-api/src/main/java/com/condation/cms/api/extensions/Mapping.java +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/Mapping.java @@ -36,21 +36,23 @@ */ public class Mapping { - private Map handlerMapping; + private List handlers; + + private static record PathHandler (PathSpec pathSpec, HttpHandler httpHandler){} public Mapping () { - handlerMapping = new HashMap<>(); + handlers = new ArrayList<>(); } public void add (PathSpec pathSpec, HttpHandler handler) { - handlerMapping.put(pathSpec, handler); + handlers.add(new PathHandler(pathSpec, handler)); } public Optional getMatchingHandler (String uri) { - return handlerMapping.entrySet().stream().filter(entry -> entry.getKey().matches(uri)).map(entry -> entry.getValue()).findFirst(); + return handlers.stream().filter(handler -> handler.pathSpec.matches(uri)).map(PathHandler::httpHandler).findFirst(); } public List getHandlers () { - return new ArrayList<>(handlerMapping.values()); + return handlers.stream().map(PathHandler::httpHandler).toList(); } } diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterShortCodesExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterTagsExtensionPoint.java similarity index 84% rename from cms-api/src/main/java/com/condation/cms/api/extensions/RegisterShortCodesExtensionPoint.java rename to cms-api/src/main/java/com/condation/cms/api/extensions/RegisterTagsExtensionPoint.java index a5adadf36..b719c51fa 100644 --- a/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterShortCodesExtensionPoint.java +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterTagsExtensionPoint.java @@ -33,13 +33,13 @@ * * @author t.marx */ -public abstract class RegisterShortCodesExtensionPoint extends AbstractExtensionPoint { +public abstract class RegisterTagsExtensionPoint extends AbstractExtensionPoint { - public Map> shortCodes () { + public Map> tags () { return Collections.emptyMap(); } - public List shortCodeDefinitions () { + public List tagDefinitions () { return Collections.emptyList(); } } diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/HttpRouteExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterTemplateFunctionExtensionPoint.java similarity index 65% rename from cms-api/src/main/java/com/condation/cms/api/extensions/HttpRouteExtensionPoint.java rename to cms-api/src/main/java/com/condation/cms/api/extensions/RegisterTemplateFunctionExtensionPoint.java index 48c8095c2..93ae9d614 100644 --- a/cms-api/src/main/java/com/condation/cms/api/extensions/HttpRouteExtensionPoint.java +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterTemplateFunctionExtensionPoint.java @@ -22,18 +22,24 @@ * #L% */ -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Callback; +import com.condation.cms.api.model.Parameter; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; /** * * @author t.marx */ -@Deprecated(since = "7.3.0", forRemoval = true) -public abstract class HttpRouteExtensionPoint extends AbstractExtensionPoint { - abstract public String getRoute (); +public abstract class RegisterTemplateFunctionExtensionPoint extends AbstractExtensionPoint { - abstract public void handle (Request request, Response response, Callback callback); + public Map> functions () { + return Collections.emptyMap(); + } + + public List functionDefinitions () { + return Collections.emptyList(); + } } diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/TemplateModelExtendingExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/TemplateModelExtendingExtensionPoint.java index 8ea1dd4fe..917e7f122 100644 --- a/cms-api/src/main/java/com/condation/cms/api/extensions/TemplateModelExtendingExtensionPoint.java +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/TemplateModelExtendingExtensionPoint.java @@ -26,25 +26,13 @@ * #L% */ -import com.condation.cms.api.template.TemplateEngine; public abstract class TemplateModelExtendingExtensionPoint extends AbstractExtensionPoint{ - /** - * deprecated: use @TemplateModelExtendingExtensionPoint.getModel instead - * @param model - */ - @Deprecated(since = "7.3.0", forRemoval = true) - public abstract void extendModel (TemplateEngine.Model model); - - public Map getModel () { - TemplateEngine.Model model = new TemplateEngine.Model(null, null, null); - extendModel(model); - return model.values; - } + public abstract Map getModel (); public String getNamespace () { - return Constants.DEFAULT_MODULE_NAMESPACE; + return Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE; } } diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesExtensionPoint.java index 496cd7da4..e28b393a8 100644 --- a/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesExtensionPoint.java +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesExtensionPoint.java @@ -23,12 +23,10 @@ */ import com.condation.cms.api.extensions.AbstractExtensionPoint; -import java.util.List; /** * * @author thorstenmarx */ public abstract class RoutesExtensionPoint extends AbstractExtensionPoint { - abstract public List getRouteDefinitions (); } diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesManager.java b/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesManager.java index d3ef35d61..3429b9b43 100644 --- a/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesManager.java +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesManager.java @@ -21,14 +21,13 @@ * . * #L% */ - import com.condation.cms.api.annotations.Route; import com.condation.cms.api.extensions.http.HttpHandler; import com.condation.cms.api.extensions.http.PathMapping; +import com.condation.cms.api.utils.AnnotationsUtil; import org.eclipse.jetty.http.pathmap.PathSpec; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; +import java.util.List; import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.server.Request; @@ -38,45 +37,31 @@ @Slf4j public class RoutesManager { - private final PathMapping pathMapping = new PathMapping(); + private final PathMapping pathMapping = new PathMapping(); - public Optional findFirst (String path, String method) { + public Optional findFirst(String path, String method) { return pathMapping.getMatchingHandler(path, method); } - - public void register(Object controller) { - Class clazz = controller.getClass(); - - for (Method method : clazz.getDeclaredMethods()) { - Route route = method.getAnnotation(Route.class); - if (route != null && isValidHandlerMethod(method)) { - method.setAccessible(true); - PathSpec pathSpec = PathSpec.from(route.value()); + public void register(Object controller) { - HttpHandler handler = (request, response, callback) -> { - try { - return (Boolean) method.invoke(controller, request, response, callback); - } catch (Exception e) { - log.error("", e); - response.setStatus(500); - return true; - } - }; - pathMapping.add(pathSpec, route.method(), handler); - } - } - } + AnnotationsUtil.process( + controller, + Route.class, + List.of(Request.class, Response.class, Callback.class), boolean.class) + .forEach(cmsAnnotation -> { + PathSpec pathSpec = PathSpec.from(cmsAnnotation.annotation().value()); - private boolean isValidHandlerMethod(Method method) { - // Muss "boolean handle(Request, Response, Callback)" sein - if (!Modifier.isPublic(method.getModifiers())) return false; - if (!method.getReturnType().equals(boolean.class)) return false; - - Class[] params = method.getParameterTypes(); - return params.length == 3 && - Request.class.isAssignableFrom(params[0]) && - Response.class.isAssignableFrom(params[1]) && - Callback.class.isAssignableFrom(params[2]); - } + HttpHandler handler = (request, response, callback) -> { + try { + return cmsAnnotation.invoke(request, response, callback); + } catch (Exception e) { + log.error("", e); + response.setStatus(500); + return true; + } + }; + pathMapping.add(pathSpec, cmsAnnotation.annotation().method(), handler); + }); + } } diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/server/ServerExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/server/ServerExtensionPoint.java new file mode 100644 index 000000000..530c3505a --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/server/ServerExtensionPoint.java @@ -0,0 +1,61 @@ +package com.condation.cms.api.extensions.server; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.module.ServerModuleContext; +import com.condation.cms.api.module.ServerRequestContext; +import com.condation.modules.api.ExtensionPoint; +import com.condation.modules.api.ModuleConfiguration; +import lombok.Getter; + +/** + * + * @author thmar + */ +public abstract class ServerExtensionPoint implements ExtensionPoint { + @Getter + protected ModuleConfiguration moduleConfiguration; + @Getter + protected ServerModuleContext context; + @Getter + protected ServerRequestContext requestContext; + + @Override + public void setConfiguration(ModuleConfiguration configuration) { + this.moduleConfiguration = configuration; + } + + @Override + public void setContext(ServerModuleContext context) { + this.context = context; + } + + @Override + public void setRequestContext(ServerRequestContext requestContext) { + this.requestContext = requestContext; + } + + @Override + public void init() { + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/server/ServerHookSystemRegisterExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/server/ServerHookSystemRegisterExtensionPoint.java new file mode 100644 index 000000000..e27148fd5 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/server/ServerHookSystemRegisterExtensionPoint.java @@ -0,0 +1,34 @@ +package com.condation.cms.api.extensions.server; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.hooks.HookSystem; + +/** + * ExtensionPoint for modules to register hooks. + * + */ +public abstract class ServerHookSystemRegisterExtensionPoint extends ServerExtensionPoint { + + public void register (final HookSystem hookSystem) {} +} diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/server/ServerLifecycleExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/server/ServerLifecycleExtensionPoint.java new file mode 100644 index 000000000..a5d3fb0f9 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/server/ServerLifecycleExtensionPoint.java @@ -0,0 +1,35 @@ +package com.condation.cms.api.extensions.server; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thmar + */ +public abstract class ServerLifecycleExtensionPoint extends ServerExtensionPoint { + + public abstract void started (); + + public abstract void stopped (); + +} diff --git a/cms-api/src/main/java/com/condation/cms/api/feature/features/HookSystemFeature.java b/cms-api/src/main/java/com/condation/cms/api/feature/features/HookSystemFeature.java index c15bfcc5c..f6d99fc80 100644 --- a/cms-api/src/main/java/com/condation/cms/api/feature/features/HookSystemFeature.java +++ b/cms-api/src/main/java/com/condation/cms/api/feature/features/HookSystemFeature.java @@ -33,5 +33,4 @@ */ @FeatureScope({FeatureScope.Scope.REQUEST}) public record HookSystemFeature(HookSystem hookSystem) implements Feature { - } diff --git a/cms-api/src/main/java/com/condation/cms/api/feature/features/InjectorFeature.java b/cms-api/src/main/java/com/condation/cms/api/feature/features/InjectorFeature.java index 346f4b225..4ea7a181b 100644 --- a/cms-api/src/main/java/com/condation/cms/api/feature/features/InjectorFeature.java +++ b/cms-api/src/main/java/com/condation/cms/api/feature/features/InjectorFeature.java @@ -31,7 +31,7 @@ * * @author t.marx */ -@FeatureScope({FeatureScope.Scope.REQUEST}) +@FeatureScope({FeatureScope.Scope.REQUEST, FeatureScope.Scope.MODULE, FeatureScope.Scope.SERVER}) public record InjectorFeature(Injector injector) implements Feature { } diff --git a/cms-api/src/main/java/com/condation/cms/api/feature/features/IsPreviewFeature.java b/cms-api/src/main/java/com/condation/cms/api/feature/features/IsPreviewFeature.java index 87bce7454..8d457ae00 100644 --- a/cms-api/src/main/java/com/condation/cms/api/feature/features/IsPreviewFeature.java +++ b/cms-api/src/main/java/com/condation/cms/api/feature/features/IsPreviewFeature.java @@ -31,6 +31,33 @@ * @author t.marx */ @FeatureScope({FeatureScope.Scope.REQUEST}) -public record IsPreviewFeature() implements Feature { +public record IsPreviewFeature(Mode mode) implements Feature { + public IsPreviewFeature() { + this(Mode.PREVIEW); + } + + public static enum Mode { + MANAGER("manager"), + PREVIEW("preview"); + + private final String value; + + private Mode (String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static Mode forValue (String value) { + for (var mode : values()) { + if (mode.value.equals(value)) { + return mode; + } + } + return PREVIEW; + } + } } diff --git a/cms-api/src/main/java/com/condation/cms/api/feature/features/ServerHookSystemFeature.java b/cms-api/src/main/java/com/condation/cms/api/feature/features/ServerHookSystemFeature.java new file mode 100644 index 000000000..68428a459 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/feature/features/ServerHookSystemFeature.java @@ -0,0 +1,37 @@ +package com.condation.cms.api.feature.features; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + + +import com.condation.cms.api.annotations.FeatureScope; +import com.condation.cms.api.feature.Feature; +import com.condation.cms.api.hooks.HookSystem; +import java.util.function.Supplier; + +/** + * + * @author t.marx + */ +@FeatureScope({FeatureScope.Scope.SERVER}) +public record ServerHookSystemFeature(HookSystem hookSystem) implements Feature { +} diff --git a/cms-api/src/main/java/com/condation/cms/api/feature/features/TemplateEngineFeature.java b/cms-api/src/main/java/com/condation/cms/api/feature/features/TemplateEngineFeature.java index 4c7da2693..10a2d90f4 100644 --- a/cms-api/src/main/java/com/condation/cms/api/feature/features/TemplateEngineFeature.java +++ b/cms-api/src/main/java/com/condation/cms/api/feature/features/TemplateEngineFeature.java @@ -37,7 +37,7 @@ @FeatureScope({FeatureScope.Scope.REQUEST}) public record TemplateEngineFeature(TemplateEngine templateEngine) implements Feature { - public String render(String template, Map model, RequestContext requestContext) { + public String render(String template, Map model, RequestContext requestContext) { try { var templateModel = new TemplateEngine.Model(null, null, requestContext); templateModel.values.putAll(model); diff --git a/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java b/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java index b173e4d6e..2106a7837 100644 --- a/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java +++ b/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java @@ -23,11 +23,13 @@ */ import com.condation.cms.api.annotations.Filter; import com.condation.cms.api.annotations.Action; +import com.condation.cms.api.utils.AnnotationsUtil; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -39,70 +41,37 @@ * @author t.marx */ @Slf4j -@RequiredArgsConstructor public class HookSystem { Multimap actions = ArrayListMultimap.create(); Multimap filters = ArrayListMultimap.create(); - private HookSystem(HookSystem source) { + public HookSystem () { + + } + public HookSystem(HookSystem source) { this.actions.putAll(source.actions); this.filters.putAll(source.filters); } - public HookSystem clone() { - return new HookSystem(this); - } - public void register(Object sourceObject) { - Class objectClass = sourceObject.getClass(); - for (Method method : objectClass.getDeclaredMethods()) { - // regsiter actions - var actionAnnotation = method.getAnnotation(Action.class); - if (actionAnnotation != null) { - - var parameters = method.getParameterTypes(); - if (parameters.length != 1 || !ActionContext.class.isAssignableFrom(parameters[0])) { - log.warn("Method {}.{} ignored: must have exactly one parameter of type ActionContext", - objectClass.getSimpleName(), method.getName()); - } else { - if (!method.canAccess(sourceObject)) { - method.setAccessible(true); - } + // Action-Methoden registrieren + List> actionMethods + = AnnotationsUtil.process(sourceObject, Action.class, List.of(ActionContext.class), Void.class); - registerAction(actionAnnotation.value(), context -> { - try { - return method.invoke(sourceObject, context); - } catch (Exception e) { - log.error("Error invoking action method {}.{}", objectClass.getSimpleName(), method.getName(), e); - throw new RuntimeException(e); - } - }, actionAnnotation.priority()); - } - } - - // register filters - var filterAnnotation = method.getAnnotation(Filter.class); - if (filterAnnotation != null) { - var parameters = method.getParameterTypes(); - if (parameters.length != 1 || !FilterContext.class.isAssignableFrom(parameters[0])) { - log.warn("Method {}.{} ignored for Filter: must have exactly one parameter of type FilterContext", - objectClass.getSimpleName(), method.getName()); - } else { - if (!method.canAccess(sourceObject)) { - method.setAccessible(true); - } - registerFilter(filterAnnotation.value(), context -> { - try { - return method.invoke(sourceObject, context); - } catch (Exception e) { - log.error("Error invoking filter method {}.{}", objectClass.getSimpleName(), method.getName(), e); - throw new RuntimeException(e); - } - }, filterAnnotation.priority()); - } - } + for (AnnotationsUtil.CMSAnnotation ann : actionMethods) { + Action annotation = ann.annotation(); + registerAction(annotation.value(), context -> ann.invoke(context), annotation.priority()); + } + + // Filter-Methoden registrieren + List> filterMethods + = AnnotationsUtil.process(sourceObject, Filter.class, List.of(FilterContext.class), Object.class); + + for (AnnotationsUtil.CMSAnnotation ann : filterMethods) { + Filter annotation = ann.annotation(); + registerFilter(annotation.value(), context -> ann.invoke(context), annotation.priority()); } } diff --git a/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java b/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java index 17781464a..6d79e5f20 100644 --- a/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java +++ b/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java @@ -31,7 +31,7 @@ public enum Hooks { NAVIGATION_PATH("system/navigation/%s/path"), NAVIGATION_LIST("system/navigation/%s/list"), - CONTENT_SHORTCODE("system/content/shortcodes"), + CONTENT_TAGS("system/content/tags"), CONTENT_FILTER("system/content/filter"), TEMPLATE_COMPONENT("system/template/component"), DB_QUERY_OPERATIONS("system/db/query/operations"), diff --git a/cms-api/src/main/java/com/condation/cms/api/mapper/ContentNodeMapper.java b/cms-api/src/main/java/com/condation/cms/api/mapper/ContentNodeMapper.java index e0eac51f0..a4d0d363a 100644 --- a/cms-api/src/main/java/com/condation/cms/api/mapper/ContentNodeMapper.java +++ b/cms-api/src/main/java/com/condation/cms/api/mapper/ContentNodeMapper.java @@ -23,6 +23,7 @@ */ import com.condation.cms.api.Constants; import com.condation.cms.api.content.ContentParser; +import com.condation.cms.api.content.MapAccess; import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.DB; import com.condation.cms.api.db.cms.ReadOnlyFile; @@ -32,7 +33,6 @@ import com.condation.cms.api.utils.HTTPUtil; import com.condation.cms.api.utils.NodeUtil; import com.condation.cms.api.utils.PathUtil; -import com.google.inject.Inject; import java.io.IOException; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -75,7 +75,7 @@ public ListNode toListNode(final ContentNode node, final RequestContext context, var md = parse(temp_path); var excerpt = NodeUtil.excerpt(node, md.get().content(), excerptLength, context.get(MarkdownRendererFeature.class).markdownRenderer()); - return new ListNode(name, url, excerpt, node.data()); + return new ListNode(name, url, excerpt, MapAccess.of(node.data())); } diff --git a/cms-api/src/main/java/com/condation/cms/api/media/Media.java b/cms-api/src/main/java/com/condation/cms/api/media/Media.java index 70f6be19e..a07dbea29 100644 --- a/cms-api/src/main/java/com/condation/cms/api/media/Media.java +++ b/cms-api/src/main/java/com/condation/cms/api/media/Media.java @@ -29,4 +29,13 @@ * * @author t.marx */ -public record Media (String uri, Meta meta, boolean exists) {} +public record Media (String uri, Meta meta, boolean exists, Size size) { + + public static Size NO_SIZE = new Size(-1, -1); + + public Media (String uri, Meta meta, boolean exists) { + this(uri, meta, exists, NO_SIZE); + } + + public static record Size(int width, int height) {} +} diff --git a/cms-api/src/main/java/com/condation/cms/api/media/meta/Meta.java b/cms-api/src/main/java/com/condation/cms/api/media/meta/Meta.java index a0de890a8..bc686f104 100644 --- a/cms-api/src/main/java/com/condation/cms/api/media/meta/Meta.java +++ b/cms-api/src/main/java/com/condation/cms/api/media/meta/Meta.java @@ -21,14 +21,35 @@ * . * #L% */ - - +import java.util.Collections; import java.util.HashMap; +import java.util.Map; /** * * @author t.marx */ -public class Meta extends HashMap{ +public class Meta extends HashMap { + + public double getFocalPoint_x() { + Object value = ((Map) getOrDefault("focal", Collections.emptyMap())).getOrDefault("x", 0.5); + return toDouble(value, 0.5); + } + + public double getFocalPoint_y() { + Object value = ((Map) getOrDefault("focal", Collections.emptyMap())).getOrDefault("y", 0.5); + return toDouble(value, 0.5); + } + + private double toDouble(Object value, double defaultValue) { + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + try { + return Double.parseDouble(value.toString()); + } catch (Exception e) { + return defaultValue; + } + } } diff --git a/cms-git/src/main/java/com/condation/cms/git/Repo.java b/cms-api/src/main/java/com/condation/cms/api/module/ServerModuleContext.java similarity index 69% rename from cms-git/src/main/java/com/condation/cms/git/Repo.java rename to cms-api/src/main/java/com/condation/cms/api/module/ServerModuleContext.java index b8845f852..d7c73e816 100644 --- a/cms-git/src/main/java/com/condation/cms/git/Repo.java +++ b/cms-api/src/main/java/com/condation/cms/api/module/ServerModuleContext.java @@ -1,8 +1,8 @@ -package com.condation.cms.git; +package com.condation.cms.api.module; /*- * #%L - * cms-git + * cms-api * %% * Copyright (C) 2023 - 2024 CondationCMS * %% @@ -22,26 +22,16 @@ * #L% */ -import lombok.Data; +import com.condation.cms.api.feature.FeatureContainer; +import com.condation.modules.api.Context; +import lombok.RequiredArgsConstructor; /** * * @author t.marx */ -@Data -public class Repo { - - private String name; - private String uri; - private String folder; - private String branch; - private String cron; - private Credentials credentials; +@RequiredArgsConstructor +public class ServerModuleContext extends FeatureContainer implements Context { - @Data - public static class Credentials { - private String username; - private String password; - } } diff --git a/cms-api/src/main/java/com/condation/cms/api/module/ServerRequestContext.java b/cms-api/src/main/java/com/condation/cms/api/module/ServerRequestContext.java new file mode 100644 index 000000000..e0371cfc3 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/module/ServerRequestContext.java @@ -0,0 +1,35 @@ +package com.condation.cms.api.module; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + + +import com.condation.modules.api.ModuleRequestContext; +import lombok.RequiredArgsConstructor; + +/** + * + * @author t.marx + */ +@RequiredArgsConstructor +public class ServerRequestContext implements ModuleRequestContext { +} diff --git a/cms-api/src/main/java/com/condation/cms/api/module/CMSModuleContext.java b/cms-api/src/main/java/com/condation/cms/api/module/SiteModuleContext.java similarity index 92% rename from cms-api/src/main/java/com/condation/cms/api/module/CMSModuleContext.java rename to cms-api/src/main/java/com/condation/cms/api/module/SiteModuleContext.java index e347ac0fe..0fa6db58e 100644 --- a/cms-api/src/main/java/com/condation/cms/api/module/CMSModuleContext.java +++ b/cms-api/src/main/java/com/condation/cms/api/module/SiteModuleContext.java @@ -32,6 +32,6 @@ * @author t.marx */ @RequiredArgsConstructor -public class CMSModuleContext extends FeatureContainer implements Context { +public class SiteModuleContext extends FeatureContainer implements Context { } diff --git a/cms-api/src/main/java/com/condation/cms/api/module/CMSRequestContext.java b/cms-api/src/main/java/com/condation/cms/api/module/SiteRequestContext.java similarity index 95% rename from cms-api/src/main/java/com/condation/cms/api/module/CMSRequestContext.java rename to cms-api/src/main/java/com/condation/cms/api/module/SiteRequestContext.java index 1b8167dd6..1af951201 100644 --- a/cms-api/src/main/java/com/condation/cms/api/module/CMSRequestContext.java +++ b/cms-api/src/main/java/com/condation/cms/api/module/SiteRequestContext.java @@ -34,7 +34,7 @@ * @author t.marx */ @RequiredArgsConstructor -public class CMSRequestContext extends RequestContext implements ModuleRequestContext { +public class SiteRequestContext extends RequestContext implements ModuleRequestContext { private final RequestContext delegate; diff --git a/cms-api/src/main/java/com/condation/cms/api/request/RequestContextScope.java b/cms-api/src/main/java/com/condation/cms/api/request/RequestContextScope.java new file mode 100644 index 000000000..fa377e43c --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/request/RequestContextScope.java @@ -0,0 +1,31 @@ +package com.condation.cms.api.request; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public class RequestContextScope { + public static final ScopedValue REQUEST_CONTEXT = ScopedValue.newInstance(); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/site/Site.java b/cms-api/src/main/java/com/condation/cms/api/site/Site.java new file mode 100644 index 000000000..1cd0f2cad --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/site/Site.java @@ -0,0 +1,68 @@ +package com.condation.cms.api.site; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.SiteProperties; +import com.google.inject.Injector; +import java.util.List; + +/** + * + * @author thmar + */ +public record Site(Injector injector) { + + public String id() { + return injector.getInstance(SiteProperties.class).id(); + } + + public List modules() { + return injector.getInstance(SiteProperties.class).activeModules(); + } + + public String baseurl() { + return (String) injector.getInstance(SiteProperties.class).get("baseurl"); + } + + public boolean manager() { + return injector.getInstance(SiteProperties.class).ui().managerEnabled(); + } + + public String realUrl() { + var baseUrl = baseurl(); + var contextPath = injector.getInstance(SiteProperties.class).contextPath(); + + // Normalize baseUrl: remove trailing slashes + String normalizedBase = baseUrl.replaceAll("/+$", ""); + + // Normalize contextPath: ensure it starts with a slash (except if it's just "/") + String normalizedContext = contextPath.equals("/") ? "" : contextPath.replaceAll("^/+", ""); + + // Combine + if (normalizedContext.isEmpty()) { + return normalizedBase + "/"; + } else { + return normalizedBase + "/" + normalizedContext + "/"; + } + } + +} diff --git a/cms-api/src/main/java/com/condation/cms/api/site/SiteService.java b/cms-api/src/main/java/com/condation/cms/api/site/SiteService.java new file mode 100644 index 000000000..053e08126 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/site/SiteService.java @@ -0,0 +1,35 @@ +package com.condation.cms.api.site; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.stream.Stream; + +/** + * + * @author thmar + */ +public interface SiteService { + void add (Site site); + + Stream sites (); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/action/UIAction.java b/cms-api/src/main/java/com/condation/cms/api/ui/action/UIAction.java new file mode 100644 index 000000000..5c0207688 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/action/UIAction.java @@ -0,0 +1,40 @@ +package com.condation.cms.api.ui.action; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2024 - 2025 Condation + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public abstract class UIAction { + + private final String type; + + public UIAction (String type) { + this.type = type; + } + + public String getType () { + return type; + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/action/UIHookAction.java b/cms-api/src/main/java/com/condation/cms/api/ui/action/UIHookAction.java new file mode 100644 index 000000000..fcfb3ac0b --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/action/UIHookAction.java @@ -0,0 +1,52 @@ +package com.condation.cms.api.ui.action; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2024 - 2025 Condation + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * + * @author thorstenmarx + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UIHookAction extends UIAction { + + public static final String TYPE = "hook"; + + private String hook; + + private Map parameters; + + public UIHookAction () { + super(TYPE); + } + + public UIHookAction(String hook, Map parameters) { + this(); + this.hook = hook; + this.parameters = parameters; + } + +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/action/UIScriptAction.java b/cms-api/src/main/java/com/condation/cms/api/ui/action/UIScriptAction.java new file mode 100644 index 000000000..ad390c962 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/action/UIScriptAction.java @@ -0,0 +1,58 @@ +package com.condation.cms.api.ui.action; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2024 - 2025 Condation + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * + * @author thorstenmarx + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class UIScriptAction extends UIAction { + + public static final String TYPE = "script"; + + private String function; + private String module; + + private Map parameters; + + public UIScriptAction() { + super(TYPE); + } + + public UIScriptAction (String module, String function, Map parameters) { + this(); + this.function = function; + this.module = module; + this.parameters = parameters; + } + + public UIScriptAction (String module, Map parameters) { + this(module, "runAction", parameters); + } + +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/annotations/HookAction.java b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/HookAction.java new file mode 100644 index 000000000..af83e8ced --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/HookAction.java @@ -0,0 +1,38 @@ +package com.condation.cms.api.ui.annotations; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface HookAction { + String value(); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/annotations/MenuEntry.java b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/MenuEntry.java new file mode 100644 index 000000000..783b3912b --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/MenuEntry.java @@ -0,0 +1,46 @@ +package com.condation.cms.api.ui.annotations; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface MenuEntry { + String name(); + String id(); + String parent() default ""; + boolean divider() default false; + int position() default 0; + String[] permissions(); + + ScriptAction scriptAction () default @ScriptAction(function = "", module = ""); + HookAction hookAction () default @HookAction(""); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/annotations/RemoteMethod.java b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/RemoteMethod.java new file mode 100644 index 000000000..7f0f09f11 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/RemoteMethod.java @@ -0,0 +1,39 @@ +package com.condation.cms.api.ui.annotations; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface RemoteMethod { + String name(); + String[] permissions(); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/annotations/ScriptAction.java b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/ScriptAction.java new file mode 100644 index 000000000..378286c46 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/ScriptAction.java @@ -0,0 +1,39 @@ +package com.condation.cms.api.ui.annotations; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ScriptAction { + String module (); + String function () default "runAction"; +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/annotations/ShortCut.java b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/ShortCut.java new file mode 100644 index 000000000..73d1d7975 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/ShortCut.java @@ -0,0 +1,48 @@ +package com.condation.cms.api.ui.annotations; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ShortCut { + String title(); + String id(); + String parent() default ""; + String icon() default ""; + String hotkey () default ""; + String section () default ""; + + String[] permissions(); + + ScriptAction scriptAction () default @ScriptAction(function = "", module = ""); + HookAction hookAction () default @HookAction(""); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypes.java b/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypes.java new file mode 100644 index 000000000..c76afd142 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypes.java @@ -0,0 +1,114 @@ +package com.condation.cms.api.ui.elements; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +/** + * + * @author thmar + */ +public class ContentTypes { + + public Set pageTemplates = new HashSet(); + + public Set sectionTemplates = new HashSet<>(); + + public Set listItemTypes = new HashSet<>(); + + public void registerListItemType(Map listItemType) { + listItemTypes.add(new ListItemType(listItemType)); + } + + public Set getListItemTypes () { + return new HashSet<>(listItemTypes); + } + + public Optional getPageTemplate (String name) { + return pageTemplates.stream().filter(pt -> pt.name.equals(name)).findFirst(); + } + + public void registerPageTemplate(Map pageTemplate) { + pageTemplates.add(new PageTemplate(pageTemplate)); + } + + public void registerSectionTemplate(Map sectionTempate) { + sectionTemplates.add(new SectionTemplate(sectionTempate)); + } + + public Set getPageTemplates () { + return new HashSet<>(pageTemplates); + } + + public Set getSectionTemplates (String section) { + return sectionTemplates.stream() + .filter(template -> template.section().equals(section)) + .collect(Collectors.toSet()); + } + + public static record PageTemplate(String name, String template, Map data) { + + public PageTemplate (Map data) { + this( + (String) data.getOrDefault("name", ""), + (String) data.getOrDefault("template", ""), + data); + } + + public Map getForm (String name) { + var forms = (Map)data.getOrDefault("forms", Collections.emptyMap()); + return (Map)forms.getOrDefault(name, Collections.emptyMap()); + } + } + + public static record SectionTemplate(String name, String template, Map data) { + + public SectionTemplate (Map data) { + this( + (String) data.getOrDefault("name", ""), + (String) data.getOrDefault("template", ""), + data); + } + + public String section() { + return (String) data.getOrDefault("section", ""); + } + } + + public static record ListItemType(String name, Map data) { + + public ListItemType (Map data) { + this( + (String) data.getOrDefault("name", ""), + data); + } + + public Map getForm (String name) { + return (Map)data.getOrDefault("form", Collections.emptyMap()); + } + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/elements/MediaForms.java b/cms-api/src/main/java/com/condation/cms/api/ui/elements/MediaForms.java new file mode 100644 index 000000000..0c9ec6f4c --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/elements/MediaForms.java @@ -0,0 +1,52 @@ +package com.condation.cms.api.ui.elements; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +/** + * + * @author thmar + */ +public class MediaForms { + + public Map metaForms = new HashMap<>(); + + + public void registerForm(String name, Map metaForm) { + metaForms.put(name, new MetaForm(name, metaForm)); + } + + + + public Map getMetaForms () { + return new HashMap<>(metaForms); + } + + public static record MetaForm(String name, Map form) { + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/elements/Menu.java b/cms-api/src/main/java/com/condation/cms/api/ui/elements/Menu.java new file mode 100644 index 000000000..7ebff0c96 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/elements/Menu.java @@ -0,0 +1,65 @@ +package com.condation.cms.api.ui.elements; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * + * @author thorstenmarx + */ +public class Menu { + + private Map menu = new HashMap<>(); + + public Optional getMenuEntry(String id) { + return Optional.ofNullable(menu.get(id)); + } + + public void addMenuEntry(MenuEntry entry) { + menu.put(entry.getId(), entry); + } + + public List entries() { + List entries = new ArrayList<>(menu.values()); + sortMenuEntries(entries); + return entries; + } + + private void sortMenuEntries(List entries) { + if (entries == null || entries.isEmpty()) { + return; + } + // Sortiere die aktuelle Ebene + entries.sort(Comparator.comparingInt(MenuEntry::getPosition)); + + // Sortiere rekursiv die Kinder + for (MenuEntry entry : entries) { + sortMenuEntries(entry.getChildren()); + } + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/elements/MenuEntry.java b/cms-api/src/main/java/com/condation/cms/api/ui/elements/MenuEntry.java new file mode 100644 index 000000000..ee4b64e21 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/elements/MenuEntry.java @@ -0,0 +1,74 @@ +package com.condation.cms.api.ui.elements; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2024 - 2025 Condation + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.ui.action.UIAction; +import com.condation.cms.api.utils.JSONUtil; +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +/** + * + * @author thorstenmarx + */ +@Builder +@Getter +public class MenuEntry { + + private String name; + + private String id; + + @Builder.Default + private boolean divider = false; + + @Builder.Default + private List permissions = new ArrayList<>(); + + @Builder.Default + private int position = 0; + + @Builder.Default + private List children = new ArrayList<>(); + + private UIAction action; + + public void addChildren (MenuEntry entry) { + if (children == null) { + children = new ArrayList<>(); + } + + children = new ArrayList<>(children); + children.add(entry); + } + + public String getActionDefinition () { + return action != null ? JSONUtil.toJson(action) : ""; + } + + public boolean hasChildren () { + return children != null && ! children.isEmpty(); + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIActionsExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIActionsExtensionPoint.java new file mode 100644 index 000000000..1b88e4e58 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIActionsExtensionPoint.java @@ -0,0 +1,37 @@ +package com.condation.cms.api.ui.extensions; + +/*- + * #%L + * ui-api + * %% + * Copyright (C) 2024 Marx-Software + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.module.SiteRequestContext; +import com.condation.cms.api.ui.elements.Menu; +import com.condation.modules.api.ExtensionPoint; + +/** + * + * @author t.marx + */ +public interface UIActionsExtensionPoint extends ExtensionPoint { + + public default void addMenuItems (Menu menu) {}; +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UILocalizationExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UILocalizationExtensionPoint.java new file mode 100644 index 000000000..61873a3e0 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UILocalizationExtensionPoint.java @@ -0,0 +1,37 @@ +package com.condation.cms.api.ui.extensions; + +/*- + * #%L + * ui-api + * %% + * Copyright (C) 2024 Marx-Software + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.module.SiteRequestContext; +import com.condation.modules.api.ExtensionPoint; +import java.util.Map; + +/** + * + * @author t.marx + */ +public interface UILocalizationExtensionPoint extends ExtensionPoint { + + public Map> getLocalizations (); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIRemoteMethodExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIRemoteMethodExtensionPoint.java new file mode 100644 index 000000000..896a8a8b2 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIRemoteMethodExtensionPoint.java @@ -0,0 +1,33 @@ +package com.condation.cms.api.ui.extensions; + +/*- + * #%L + * ui-api + * %% + * Copyright (C) 2024 Marx-Software + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.extensions.AbstractExtensionPoint; + +/** + * + * @author t.marx + */ +public abstract class UIRemoteMethodExtensionPoint extends AbstractExtensionPoint { + +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/rpc/RPCError.java b/cms-api/src/main/java/com/condation/cms/api/ui/rpc/RPCError.java new file mode 100644 index 000000000..edf2e28bc --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/rpc/RPCError.java @@ -0,0 +1,33 @@ +package com.condation.cms.api.ui.rpc; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public record RPCError (int code, String message) { + public RPCError (String message) { + this(-1, message); + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/rpc/RPCException.java b/cms-api/src/main/java/com/condation/cms/api/ui/rpc/RPCException.java new file mode 100644 index 000000000..16fc84b9e --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/rpc/RPCException.java @@ -0,0 +1,45 @@ +package com.condation.cms.api.ui.rpc; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thmar + */ +public class RPCException extends Exception { + + private int code = 0; + + public RPCException (String message) { + this(0, message); + } + + public RPCException(int code, String message) { + super(message); + this.code = code; + } + + public int getCode () { + return code; + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/rpc/RPCResult.java b/cms-api/src/main/java/com/condation/cms/api/ui/rpc/RPCResult.java new file mode 100644 index 000000000..3db295adb --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/rpc/RPCResult.java @@ -0,0 +1,41 @@ +package com.condation.cms.api.ui.rpc; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public record RPCResult (Object result, RPCError error) { + public RPCResult () { + this(null, null); + } + + public RPCResult (Object result) { + this(result, null); + } + + public RPCResult (RPCError error) { + this(null, error); + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/AnnotationsUtil.java b/cms-api/src/main/java/com/condation/cms/api/utils/AnnotationsUtil.java new file mode 100644 index 000000000..17eaa95fa --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/utils/AnnotationsUtil.java @@ -0,0 +1,115 @@ +package com.condation.cms.api.utils; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.exceptions.AnnotationExecutionException; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import lombok.extern.slf4j.Slf4j; + +/** + * + * @author thmar + */ +@Slf4j +public class AnnotationsUtil { + + public static List> process(Object target, Class annotation, Class returnType) { + return process(target, annotation, Collections.emptyList(), returnType); + } + + public static List> process(Object target, Class annotationClass, List> parameters, Class returnType) { + Objects.requireNonNull(target); + Objects.requireNonNull(annotationClass); + Objects.requireNonNull(parameters); + + List> result = new ArrayList(); + Class clazz = target.getClass(); + for (Method method : clazz.getDeclaredMethods()) { + if (!method.isAnnotationPresent(annotationClass)) { + continue; + } + if (!hasValidSignatur(method, parameters, returnType)) { + continue; + } + + A annotation = method.getAnnotation(annotationClass); + + result.add(new CMSAnnotation<>(annotation, (params) -> { + try { + return (R) method.invoke(target, params); + } catch (IllegalAccessException | InvocationTargetException ex) { + log.error("", ex); + throw new AnnotationExecutionException(ex.getMessage()); + } + })); + } + + return result; + } + + private static boolean hasValidSignatur(Method method, List> parameters, Class returnType) { + + if (!Modifier.isPublic(method.getModifiers())) { + return false; + } + + if (returnType != Void.class) { + Class actualReturnType = method.getReturnType(); + if (returnType == Object.class) { + if (actualReturnType == Void.TYPE) { + return false; + } + } else if (!actualReturnType.equals(returnType)) { + return false; + } + } + + Class[] params = method.getParameterTypes(); + + if (params.length != parameters.size()) { + return false; + } + + for (int i = 0; i < params.length; i++) { + if (!params[i].isAssignableFrom(parameters.get(i))) { + return false; + } + } + + return true; + } + + public record CMSAnnotation(A annotation, Function function) { + + public R invoke(Object... parameters) { + return function.apply(parameters); + } + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/DateRange.java b/cms-api/src/main/java/com/condation/cms/api/utils/DateRange.java new file mode 100644 index 000000000..891b5a056 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/utils/DateRange.java @@ -0,0 +1,53 @@ +package com.condation.cms.api.utils; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.time.Instant; +import java.util.Date; + +public class DateRange { + + + + /** + * Checks if the current UTC time is within the range. + * @param from + * @param to + * @return + */ + public static boolean isNowWithin(Date from, Date to) { + + var now = Date.from(Instant.now()); + + if ( from != null && !(from.before(now) || from.equals(now)) ) { + return false; + } + if (to != null + && (to.before(now) || to.equals(now))) { + return false; + } + return true; + } + + +} diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/FileUtils.java b/cms-api/src/main/java/com/condation/cms/api/utils/FileUtils.java index a16e62fb7..c0aeaee11 100644 --- a/cms-api/src/main/java/com/condation/cms/api/utils/FileUtils.java +++ b/cms-api/src/main/java/com/condation/cms/api/utils/FileUtils.java @@ -23,11 +23,12 @@ */ import java.io.File; import java.io.IOException; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; -import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.Comparator; /** @@ -40,16 +41,48 @@ private FileUtils() { } public static void deleteFolder(Path pathToBeDeleted) throws IOException { - Files.walk(pathToBeDeleted) - .sorted(Comparator.reverseOrder()) - .map(Path::toFile) - .forEach(File::delete); + if (!Files.exists(pathToBeDeleted)) { + return; + } + try (var walkStream = Files.walk(pathToBeDeleted)) { + walkStream.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } } - public static void touch(Path path) throws IOException{ + public static void deleteDirectoryContents(Path directory) throws IOException { + if (!Files.exists(directory) || !Files.isDirectory(directory)) { + throw new IllegalArgumentException("Pfad ist kein existierendes Verzeichnis: " + directory); + } + + Files.walkFileTree(directory, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + if (!dir.equals(directory)) { + Files.delete(dir); + } + return FileVisitResult.CONTINUE; + } + }); + } + + public static void touch(Path path) throws IOException { long timeMillis = System.currentTimeMillis(); FileTime accessFileTime = FileTime.fromMillis(timeMillis); Files.setAttribute(path, "lastAccessTime", accessFileTime); Files.setLastModifiedTime(path, accessFileTime); } + + public static long countChildren(final Path path) throws IOException { + try (var children = Files.list(path)) { + return children.count(); + } + } } diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/HTTPUtil.java b/cms-api/src/main/java/com/condation/cms/api/utils/HTTPUtil.java index b726ec84c..46644f819 100644 --- a/cms-api/src/main/java/com/condation/cms/api/utils/HTTPUtil.java +++ b/cms-api/src/main/java/com/condation/cms/api/utils/HTTPUtil.java @@ -26,7 +26,6 @@ import com.condation.cms.api.feature.FeatureContainer; import com.condation.cms.api.feature.features.IsPreviewFeature; import com.condation.cms.api.feature.features.SitePropertiesFeature; -import com.condation.cms.api.request.RequestContext; import com.google.common.base.Strings; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; @@ -61,10 +60,11 @@ public static String modifyUrl(String url, final FeatureContainer featureContain url = modifyUrl(url, featureContainer.get(SitePropertiesFeature.class).siteProperties()); if (featureContainer.has(IsPreviewFeature.class)) { + var feature = featureContainer.get(IsPreviewFeature.class); if (url.contains("?")) { - url += "&preview=true"; + url += "&preview=" + feature.mode().getValue(); } else { - url += "?preview=true"; + url += "?preview=" + feature.mode().getValue(); } } diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/ImageUtil.java b/cms-api/src/main/java/com/condation/cms/api/utils/ImageUtil.java new file mode 100644 index 000000000..77ea44ba7 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/utils/ImageUtil.java @@ -0,0 +1,72 @@ +package com.condation.cms.api.utils; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.feature.features.SitePropertiesFeature; +import com.condation.cms.api.request.RequestContext; +import java.nio.file.Paths; +import java.util.List; + +/** + * + * @author thmar + */ +public class ImageUtil { + + /** + * This method takes image pathes like + * + * /de/assets/test.jpg /de/media/test.jpg /assets/test.jpg /media/test.jpg + * + * and extract the real path of the image and returns: test.jpg + * + * @param image + * @param requestContext + * @return raw image path + */ + public static String getRawPath(String image, RequestContext requestContext) { + if (image == null || image.isEmpty()) { + return ""; + } + + String normalized = image.trim(); + + if (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + + String contextPath = requestContext.get(SitePropertiesFeature.class).siteProperties().contextPath(); + if (contextPath != null && !contextPath.isEmpty()) { + contextPath = contextPath.replaceAll("^/+", "").replaceAll("/+$", ""); + if (normalized.startsWith(contextPath + "/")) { + normalized = normalized.substring(contextPath.length() + 1); + } + } + for (var path : List.of("assets", "media")) { + if (normalized.startsWith(path + "/")) { + normalized = normalized.substring(path.length() + 1); + } + } + + return normalized; + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/JSONUtil.java b/cms-api/src/main/java/com/condation/cms/api/utils/JSONUtil.java new file mode 100644 index 000000000..a7ba0c730 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/utils/JSONUtil.java @@ -0,0 +1,37 @@ +package com.condation.cms.api.utils; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.google.gson.Gson; + +/** + * + * @author thorstenmarx + */ +public class JSONUtil { + private static final Gson GSON = new Gson(); + + public static String toJson (Object value) { + return GSON.toJson(value); + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/PathUtil.java b/cms-api/src/main/java/com/condation/cms/api/utils/PathUtil.java index ee6a850a9..28bee1f49 100644 --- a/cms-api/src/main/java/com/condation/cms/api/utils/PathUtil.java +++ b/cms-api/src/main/java/com/condation/cms/api/utils/PathUtil.java @@ -90,29 +90,6 @@ public static String toRelativeFile(ReadOnlyFile contentFile, final ReadOnlyFile return uri; } - /** - * - * @param contentFile - * @param contentBase - * @return - * @deprecated use PathUtil.toURL instead - */ - @Deprecated(since = "8.0.0") - public static String toURI (final Path contentFile, final Path contentBase) { - return toURL(contentFile, contentBase); - } - /** - * - * @param contentFile - * @param contentBase - * @return - * @deprecated use PathUtil.toURL instead - */ - @Deprecated(since = "8.0.0") - public static String toURI(final ReadOnlyFile contentFile, final ReadOnlyFile contentBase) { - return toURL(contentFile, contentBase); - } - public static String toURL(final Path contentFile, final Path contentBase) { var relFile = toRelativeFile(contentFile, contentBase); return toURL(relFile); @@ -123,7 +100,7 @@ public static String toURL(final ReadOnlyFile contentFile, final ReadOnlyFile co return toURL(relFile); } - private static String toURL (String relFile) { + public static String toURL (String relFile) { if (relFile.endsWith("index.md")) { relFile = relFile.replace("index.md", ""); } diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/RequestUtil.java b/cms-api/src/main/java/com/condation/cms/api/utils/RequestUtil.java index 1fc1559ae..1ab9b80d8 100644 --- a/cms-api/src/main/java/com/condation/cms/api/utils/RequestUtil.java +++ b/cms-api/src/main/java/com/condation/cms/api/utils/RequestUtil.java @@ -23,6 +23,11 @@ */ +import com.condation.cms.api.Constants; +import com.condation.cms.api.feature.features.SitePropertiesFeature; +import com.condation.cms.api.request.RequestContext; +import java.net.InetSocketAddress; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.server.Request; /** @@ -31,6 +36,11 @@ */ public class RequestUtil { + public static String getContextPath(Request request) { + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); + return requestContext.get(SitePropertiesFeature.class).siteProperties().contextPath(); + } + /** * removes the context from the path * @param request @@ -38,7 +48,7 @@ public class RequestUtil { */ public static String getContentPath(Request request) { var path = request.getHttpURI().getPath(); - var contextPath = request.getContext().getContextPath(); + var contextPath = getContextPath(request); if (!"/".equals(contextPath) && path.startsWith(contextPath)) { path = path.replaceFirst(contextPath, ""); } @@ -49,4 +59,13 @@ public static String getContentPath(Request request) { return path; } + + public static String clientAddress(Request request) { + String forwarded = request.getHeaders().get(HttpHeader.X_FORWARDED_FOR); + if (forwarded != null && !forwarded.isEmpty()) { + return forwarded.split(",")[0].trim(); + } + return ((InetSocketAddress) request.getConnectionMetaData().getRemoteSocketAddress()) + .getAddress().getHostAddress(); + } } diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/SectionUtil.java b/cms-api/src/main/java/com/condation/cms/api/utils/SectionUtil.java index 6364d15c0..bee06b90a 100644 --- a/cms-api/src/main/java/com/condation/cms/api/utils/SectionUtil.java +++ b/cms-api/src/main/java/com/condation/cms/api/utils/SectionUtil.java @@ -30,13 +30,13 @@ */ public class SectionUtil { - public static boolean isOrderedSection(final String name) { - return Constants.SECTION_ORDERED_PATTERN.matcher(name).matches(); + public static boolean isNamedSection(final String name) { + return Constants.SECTION_NAMED_PATTERN.matcher(name).matches(); } public static String getSectionName(final String name) { - if (isOrderedSection(name)) { - var matcher = Constants.SECTION_ORDERED_PATTERN.matcher(name); + if (isNamedSection(name)) { + var matcher = Constants.SECTION_NAMED_PATTERN.matcher(name); matcher.matches(); return matcher.group("section"); } else { @@ -46,18 +46,8 @@ public static String getSectionName(final String name) { } } - public static int getSectionIndex(final String name) { - if (isOrderedSection(name)) { - var matcher = Constants.SECTION_ORDERED_PATTERN.matcher(name); - matcher.matches(); - return Integer.parseInt(matcher.group("index")); - } else { - return Constants.DEFAULT_SECTION_ORDERED_INDEX; - } - } - public static boolean isSection(final String name) { return Constants.SECTION_PATTERN.matcher(name).matches() - || Constants.SECTION_ORDERED_PATTERN.matcher(name).matches(); + || Constants.SECTION_NAMED_PATTERN.matcher(name).matches(); } } diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/SiteUtil.java b/cms-api/src/main/java/com/condation/cms/api/utils/SiteUtil.java index 715a08774..a0ede2241 100644 --- a/cms-api/src/main/java/com/condation/cms/api/utils/SiteUtil.java +++ b/cms-api/src/main/java/com/condation/cms/api/utils/SiteUtil.java @@ -21,6 +21,7 @@ * . * #L% */ +import com.condation.cms.api.Constants; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -29,6 +30,8 @@ import com.condation.cms.api.SiteProperties; import com.condation.cms.api.theme.Theme; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @@ -44,20 +47,36 @@ public static boolean isSite(Path check) { || Files.exists(check.resolve("site.toml")); } + public static Stream sitesStream() throws IOException { + List sites; + try (var siteStream = Files.list(ServerUtil.getPath(Constants.Folders.HOSTS))) { + sites = siteStream + .filter(SiteUtil::isSite) + .map(Site::new) + .collect(Collectors.toList()); + } + return sites.stream(); + } + public static List getActiveModules(SiteProperties siteProperties, Theme theme) { List activeModules = new ArrayList<>(); activeModules.addAll(siteProperties.activeModules()); if (!theme.empty()) { activeModules.addAll(theme.properties().activeModules()); - + if (theme.getParentTheme() != null) { activeModules.addAll(theme.getParentTheme().properties().activeModules()); } } return activeModules; } - + public static String getRequiredTheme(SiteProperties siteProperties, Theme theme) { return siteProperties.theme(); } + + public record Site(Path basePath) { + + } +; } diff --git a/cms-api/src/test/java/com/condation/cms/api/ConstantsNGTest.java b/cms-api/src/test/java/com/condation/cms/api/ConstantsNGTest.java index 45fd8a296..f5fa19c03 100644 --- a/cms-api/src/test/java/com/condation/cms/api/ConstantsNGTest.java +++ b/cms-api/src/test/java/com/condation/cms/api/ConstantsNGTest.java @@ -54,36 +54,36 @@ public void test_section_pattern() { @Test - public void test_ordered_sections_pattern() { - Assertions.assertThat(Constants.SECTION_ORDERED_PATTERN.matcher("index.md").matches()).isFalse(); - Assertions.assertThat(Constants.SECTION_ORDERED_PATTERN.matcher(".section.md").matches()).isFalse(); + public void test_named_sections_pattern() { + Assertions.assertThat(Constants.SECTION_NAMED_PATTERN.matcher("index.md").matches()).isFalse(); + Assertions.assertThat(Constants.SECTION_NAMED_PATTERN.matcher(".section.md").matches()).isFalse(); - Matcher matcher = Constants.SECTION_ORDERED_PATTERN.matcher("page.section.md"); + Matcher matcher = Constants.SECTION_NAMED_PATTERN.matcher("page.section.md"); Assertions.assertThat(matcher.matches()).isFalse(); - matcher = Constants.SECTION_ORDERED_PATTERN.matcher("page.section..md"); + matcher = Constants.SECTION_NAMED_PATTERN.matcher("page.section..md"); Assertions.assertThat(matcher.matches()).isFalse(); - matcher = Constants.SECTION_ORDERED_PATTERN.matcher("index.card.1.md"); + matcher = Constants.SECTION_NAMED_PATTERN.matcher("index.card.1.md"); Assertions.assertThat(matcher.matches()).isTrue(); Assertions.assertThat(matcher.group("section")).isEqualTo("card"); - Assertions.assertThat(matcher.group("index")).isEqualTo("1"); + Assertions.assertThat(matcher.group("id")).isEqualTo("1"); - matcher = Constants.SECTION_ORDERED_PATTERN.matcher("index.card.10.md"); + matcher = Constants.SECTION_NAMED_PATTERN.matcher("index.card.10.md"); Assertions.assertThat(matcher.matches()).isTrue(); Assertions.assertThat(matcher.group("section")).isEqualTo("card"); - Assertions.assertThat(matcher.group("index")).isEqualTo("10"); + Assertions.assertThat(matcher.group("id")).isEqualTo("10"); } @Test - public void test_ordered_section_of() { + public void test_named_section_of() { - var pattern = Constants.SECTION_ORDERED_OF_PATTERN.apply("page"); + var pattern = Constants.SECTION_NAMED_OF_PATTERN.apply("page"); var matcher = pattern.matcher("page.left.10.md"); Assertions.assertThat(matcher.matches()).isTrue(); - pattern = Constants.SECTION_ORDERED_OF_PATTERN.apply("other"); + pattern = Constants.SECTION_NAMED_OF_PATTERN.apply("other"); matcher = pattern.matcher("page.left.10.md"); Assertions.assertThat(matcher.matches()).isFalse(); diff --git a/cms-api/src/test/java/com/condation/cms/api/db/ContentNodeTest.java b/cms-api/src/test/java/com/condation/cms/api/db/ContentNodeTest.java index 3c76ad9ae..e7f7c32d1 100644 --- a/cms-api/src/test/java/com/condation/cms/api/db/ContentNodeTest.java +++ b/cms-api/src/test/java/com/condation/cms/api/db/ContentNodeTest.java @@ -40,7 +40,7 @@ public class ContentNodeTest { @Test public void test_publish() { var contentNode = new ContentNode("", "", Map.of()); - Assertions.assertThat(contentNode.isPublished()).isTrue(); + Assertions.assertThat(contentNode.isVisible()).isFalse(); } @Test @@ -48,9 +48,10 @@ public void test_publish_date_1_11_2023() { var cal = Calendar.getInstance(); cal.set(2023, 11, 1); var contentNode = new ContentNode("", "", Map.of( - Constants.MetaFields.PUBLISH_DATE, cal.getTime() + Constants.MetaFields.PUBLISH_DATE, cal.getTime(), + Constants.MetaFields.PUBLISHED, true )); - Assertions.assertThat(contentNode.isPublished()).isTrue(); + Assertions.assertThat(contentNode.isVisible()).isTrue(); } @Test @@ -58,9 +59,10 @@ public void test_publish_date_1_11_2123() { var cal = Calendar.getInstance(); cal.set(2123, 11, 1); var contentNode = new ContentNode("", "", Map.of( - Constants.MetaFields.PUBLISH_DATE, cal.getTime() + Constants.MetaFields.PUBLISH_DATE, cal.getTime(), + Constants.MetaFields.PUBLISHED, true )); - Assertions.assertThat(contentNode.isPublished()).isFalse(); + Assertions.assertThat(contentNode.isVisible()).isFalse(); } @Test @@ -68,9 +70,10 @@ public void test_unpublish_date_1_11_2023() { var cal = Calendar.getInstance(); cal.set(2023, 11, 1); var contentNode = new ContentNode("", "", Map.of( - Constants.MetaFields.UNPUBLISH_DATE, cal.getTime() + Constants.MetaFields.UNPUBLISH_DATE, cal.getTime(), + Constants.MetaFields.PUBLISHED, true )); - Assertions.assertThat(contentNode.isPublished()).isFalse(); + Assertions.assertThat(contentNode.isVisible()).isFalse(); } @Test @@ -78,8 +81,9 @@ public void test_unpublish_date_1_11_2123() { var cal = Calendar.getInstance(); cal.set(2123, 11, 1); var contentNode = new ContentNode("", "", Map.of( - Constants.MetaFields.UNPUBLISH_DATE, cal.getTime() + Constants.MetaFields.UNPUBLISH_DATE, cal.getTime(), + Constants.MetaFields.PUBLISHED, true )); - Assertions.assertThat(contentNode.isPublished()).isTrue(); + Assertions.assertThat(contentNode.isVisible()).isTrue(); } } diff --git a/cms-api/src/test/java/com/condation/cms/api/extensions/http/routes/RoutesManagerTest.java b/cms-api/src/test/java/com/condation/cms/api/extensions/http/routes/RoutesManagerTest.java index 9139f8b1f..08f53a9dc 100644 --- a/cms-api/src/test/java/com/condation/cms/api/extensions/http/routes/RoutesManagerTest.java +++ b/cms-api/src/test/java/com/condation/cms/api/extensions/http/routes/RoutesManagerTest.java @@ -1,7 +1,3 @@ -/* - * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license - * Click nbfs://nbhost/SystemFileSystem/Templates/UnitTests/JUnit5TestClass.java to edit this template - */ package com.condation.cms.api.extensions.http.routes; /*- diff --git a/cms-api/src/test/java/com/condation/cms/api/hooks/HookSystemTest.java b/cms-api/src/test/java/com/condation/cms/api/hooks/HookSystemTest.java index 1f2155166..cbe974ca2 100644 --- a/cms-api/src/test/java/com/condation/cms/api/hooks/HookSystemTest.java +++ b/cms-api/src/test/java/com/condation/cms/api/hooks/HookSystemTest.java @@ -25,7 +25,6 @@ import com.condation.cms.api.annotations.Filter; import com.condation.cms.api.annotations.Action; -import com.condation.cms.api.hooks.HookSystem; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; diff --git a/cms-api/src/test/java/com/condation/cms/api/utils/AnnotationsUtilTest.java b/cms-api/src/test/java/com/condation/cms/api/utils/AnnotationsUtilTest.java new file mode 100644 index 000000000..e9741e649 --- /dev/null +++ b/cms-api/src/test/java/com/condation/cms/api/utils/AnnotationsUtilTest.java @@ -0,0 +1,122 @@ +package com.condation.cms.api.utils; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.exceptions.AnnotationExecutionException; +import com.condation.cms.api.utils.AnnotationsUtil.CMSAnnotation; +import org.junit.jupiter.api.Test; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +public class AnnotationsUtilTest { + + @Retention(RetentionPolicy.RUNTIME) + public @interface TestMarker {} + + static class TestTarget { + + @TestMarker + public String greet(String name) { + return "Hello " + name; + } + + @TestMarker + public String wrongParameterType(Integer i) { + return "Number " + i; + } + + @TestMarker + public void wrongReturnType(String name) {} + + @TestMarker + private String privateMethod(String name) { + return "Private " + name; + } + + public String notAnnotated(String name) { + return "Ignored"; + } + } + + @Test + void shouldReturnOnlyPublicAnnotatedMethodsWithCorrectSignature() { + TestTarget target = new TestTarget(); + + List> results = + AnnotationsUtil.process(target, TestMarker.class, List.of(String.class), String.class); + + assertThat(results).hasSize(1); + String result = results.get(0).invoke("World"); + assertThat(result).isEqualTo("Hello World"); + } + + @Test + void shouldIgnorePrivateMethods() { + TestTarget target = new TestTarget(); + + List> results = + AnnotationsUtil.process(target, TestMarker.class, List.of(String.class), String.class); + + boolean includesPrivate = results.stream() + .anyMatch(ann -> ann.annotation().annotationType().equals(TestMarker.class) + && ann.invoke("test").startsWith("Private")); + + assertThat(includesPrivate).isFalse(); + } + + @Test + void shouldIgnoreWrongParameterTypes() { + TestTarget target = new TestTarget(); + + List> results = + AnnotationsUtil.process(target, TestMarker.class, List.of(Double.class), String.class); + + assertThat(results).isEmpty(); + } + + @Test + void shouldIgnoreMethodsWithWrongReturnType() { + TestTarget target = new TestTarget(); + + List> results = + AnnotationsUtil.process(target, TestMarker.class, List.of(String.class), String.class); + + assertThat(results).noneMatch(ann -> ann.annotation().annotationType().equals(TestMarker.class) + && ann.invoke("Test") == null); + } + + + @Test + void shouldIgnoreNonAnnotatedMethods() { + TestTarget target = new TestTarget(); + + List> results = + AnnotationsUtil.process(target, TestMarker.class, List.of(String.class), String.class); + + assertThat(results).allSatisfy(ann -> assertThat(ann.annotation()).isInstanceOf(TestMarker.class)); + } +} diff --git a/cms-api/src/test/java/com/condation/cms/api/utils/DateRangeTest.java b/cms-api/src/test/java/com/condation/cms/api/utils/DateRangeTest.java new file mode 100644 index 000000000..a88f16c69 --- /dev/null +++ b/cms-api/src/test/java/com/condation/cms/api/utils/DateRangeTest.java @@ -0,0 +1,96 @@ +package com.condation.cms.api.utils; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; + +class DateRangeTest { + + @Test + void should_return_true_when_both_dates_are_null() { + assertThat(DateRange.isNowWithin(null, null)).isTrue(); + } + + @Test + void should_return_true_when_now_is_after_from_and_to_is_null() { + Date from = Date.from(Instant.now().minus(1, ChronoUnit.HOURS)); + assertThat(DateRange.isNowWithin(from, null)).isTrue(); + } + + @Test + void should_return_false_when_now_is_before_from_and_to_is_null() { + Date from = Date.from(Instant.now().plus(1, ChronoUnit.HOURS)); + assertThat(DateRange.isNowWithin(from, null)).isFalse(); + } + + @Test + void should_return_true_when_now_is_before_to_and_from_is_null() { + Date to = Date.from(Instant.now().plus(1, ChronoUnit.HOURS)); + assertThat(DateRange.isNowWithin(null, to)).isTrue(); + } + + @Test + void should_return_false_when_now_is_after_to_and_from_is_null() { + Date to = Date.from(Instant.now().minus(1, ChronoUnit.HOURS)); + assertThat(DateRange.isNowWithin(null, to)).isFalse(); + } + + @Test + void should_return_true_when_now_is_between_from_and_to() { + Date from = Date.from(Instant.now().minus(1, ChronoUnit.HOURS)); + Date to = Date.from(Instant.now().plus(1, ChronoUnit.HOURS)); + assertThat(DateRange.isNowWithin(from, to)).isTrue(); + } + + @Test + void should_return_false_when_now_is_before_from_even_if_to_is_in_future() { + Date from = Date.from(Instant.now().plus(30, ChronoUnit.MINUTES)); + Date to = Date.from(Instant.now().plus(2, ChronoUnit.HOURS)); + assertThat(DateRange.isNowWithin(from, to)).isFalse(); + } + + @Test + void should_return_false_when_now_is_after_to_even_if_from_is_in_past() { + Date from = Date.from(Instant.now().minus(2, ChronoUnit.HOURS)); + Date to = Date.from(Instant.now().minus(30, ChronoUnit.MINUTES)); + assertThat(DateRange.isNowWithin(from, to)).isFalse(); + } + + @Test + void should_return_true_when_now_equals_from_and_to_is_null() { + Date now = Date.from(Instant.now()); + assertThat(DateRange.isNowWithin(now, null)).isTrue(); + } + + @Test + void should_return_false_when_now_equals_to() { + Date now = Date.from(Instant.now()); + assertThat(DateRange.isNowWithin(null, now)).isFalse(); + } +} diff --git a/cms-api/src/test/java/com/condation/cms/api/utils/ImageUtilTest.java b/cms-api/src/test/java/com/condation/cms/api/utils/ImageUtilTest.java new file mode 100644 index 000000000..2490821c0 --- /dev/null +++ b/cms-api/src/test/java/com/condation/cms/api/utils/ImageUtilTest.java @@ -0,0 +1,78 @@ +package com.condation.cms.api.utils; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.SiteProperties; +import com.condation.cms.api.feature.features.SitePropertiesFeature; +import com.condation.cms.api.request.RequestContext; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * + * @author thmar + */ +@ExtendWith(MockitoExtension.class) +public class ImageUtilTest { + + @Mock + private RequestContext requestContext; + + @Test + public void test_raw_path_default_context() { + + var siteProperties = Mockito.mock(SiteProperties.class); + Mockito.when(siteProperties.contextPath()).thenReturn("/"); + Mockito.when(requestContext.get(SitePropertiesFeature.class)).thenReturn(new SitePropertiesFeature(siteProperties)); + + Assertions.assertThat(ImageUtil.getRawPath("/test.jpg", requestContext)).isEqualTo("test.jpg"); + Assertions.assertThat(ImageUtil.getRawPath("/images/test.jpg", requestContext)).isEqualTo("images/test.jpg"); + + Assertions.assertThat(ImageUtil.getRawPath("/assets/test.jpg", requestContext)).isEqualTo("test.jpg"); + Assertions.assertThat(ImageUtil.getRawPath("/assets/images/test.jpg", requestContext)).isEqualTo("images/test.jpg"); + + Assertions.assertThat(ImageUtil.getRawPath("/media/test.jpg", requestContext)).isEqualTo("test.jpg"); + Assertions.assertThat(ImageUtil.getRawPath("/media/images/test.jpg", requestContext)).isEqualTo("images/test.jpg"); + } + + @Test + public void test_raw_path_context() { + + var siteProperties = Mockito.mock(SiteProperties.class); + Mockito.when(siteProperties.contextPath()).thenReturn("/de"); + Mockito.when(requestContext.get(SitePropertiesFeature.class)).thenReturn(new SitePropertiesFeature(siteProperties)); + + Assertions.assertThat(ImageUtil.getRawPath("/de/test.jpg", requestContext)).isEqualTo("test.jpg"); + Assertions.assertThat(ImageUtil.getRawPath("/de/images/test.jpg", requestContext)).isEqualTo("images/test.jpg"); + + Assertions.assertThat(ImageUtil.getRawPath("/de/assets/test.jpg", requestContext)).isEqualTo("test.jpg"); + Assertions.assertThat(ImageUtil.getRawPath("/de/assets/images/test.jpg", requestContext)).isEqualTo("images/test.jpg"); + + Assertions.assertThat(ImageUtil.getRawPath("/de/media/test.jpg", requestContext)).isEqualTo("test.jpg"); + Assertions.assertThat(ImageUtil.getRawPath("/de/media/images/test.jpg", requestContext)).isEqualTo("images/test.jpg"); + } +} diff --git a/cms-git/src/test/java/com/condation/cms/git/ConfigTest.java b/cms-api/src/test/java/com/condation/cms/api/utils/JSONUtilTest.java similarity index 63% rename from cms-git/src/test/java/com/condation/cms/git/ConfigTest.java rename to cms-api/src/test/java/com/condation/cms/api/utils/JSONUtilTest.java index 62c776c95..88cce7135 100644 --- a/cms-git/src/test/java/com/condation/cms/git/ConfigTest.java +++ b/cms-api/src/test/java/com/condation/cms/api/utils/JSONUtilTest.java @@ -1,10 +1,10 @@ -package com.condation.cms.git; +package com.condation.cms.api.utils; /*- * #%L - * cms-git + * cms-api * %% - * Copyright (C) 2023 - 2024 CondationCMS + * Copyright (C) 2023 - 2025 CondationCMS * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -21,29 +21,20 @@ * . * #L% */ - - -import com.condation.cms.git.Config; -import java.io.IOException; -import java.nio.file.Path; +import java.util.List; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; - /** * - * @author t.marx + * @author thorstenmarx */ -public class ConfigTest { +public class JSONUtilTest { - @Test - public void testSomeMethod() throws IOException { - var config = Config.load(Path.of("git.yaml")); - - Assertions.assertThat(config).isNotNull(); - - Assertions.assertThat(config.getRepos()).isNotNull().isNotEmpty().hasSize(1); + void testList() { + var listJson = JSONUtil.toJson(List.of("Condation", "CMS")); + Assertions.assertThat(listJson).isEqualToIgnoringWhitespace("[\"Condation\", \"CMS\"]"); } } diff --git a/cms-api/src/test/java/com/condation/cms/api/utils/PathUtilTest.java b/cms-api/src/test/java/com/condation/cms/api/utils/PathUtilTest.java index c26c4b1e0..5e8d997bc 100644 --- a/cms-api/src/test/java/com/condation/cms/api/utils/PathUtilTest.java +++ b/cms-api/src/test/java/com/condation/cms/api/utils/PathUtilTest.java @@ -43,7 +43,7 @@ public void test_canonical () throws IOException { } @Test - public void test_to_uri() { + public void test_to_url() { Path contentBase = Path.of("src/"); diff --git a/cms-api/src/test/java/com/condation/cms/api/utils/SectionUtilTest.java b/cms-api/src/test/java/com/condation/cms/api/utils/SectionUtilTest.java new file mode 100644 index 000000000..d555434c7 --- /dev/null +++ b/cms-api/src/test/java/com/condation/cms/api/utils/SectionUtilTest.java @@ -0,0 +1,44 @@ +package com.condation.cms.api.utils; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** + * + * @author thorstenmarx + */ +public class SectionUtilTest { + + @ParameterizedTest + @CsvSource({ + "index.asection.md, asection", + "index.asection.1.md, asection", + "index.asection.blabla.md, asection" + }) + public void test_getSectionName(String filename, String sectionname) { + Assertions.assertThat(SectionUtil.getSectionName(filename)).isEqualTo(sectionname); + } +} diff --git a/cms-auth/pom.xml b/cms-auth/pom.xml index 79a5caf7f..b7468bab2 100644 --- a/cms-auth/pom.xml +++ b/cms-auth/pom.xml @@ -4,7 +4,7 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 cms-auth jar diff --git a/cms-auth/src/main/java/com/condation/cms/auth/permissions/Permission.java b/cms-auth/src/main/java/com/condation/cms/auth/permissions/Permission.java new file mode 100644 index 000000000..a7a122dd2 --- /dev/null +++ b/cms-auth/src/main/java/com/condation/cms/auth/permissions/Permission.java @@ -0,0 +1,31 @@ +package com.condation.cms.auth.permissions; + +/*- + * #%L + * cms-auth + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thmar + */ +public record Permission(String key, String description) { + public static final Permission CONTENT_EDIT = new Permission("content.edit", "Edit content"); +} diff --git a/cms-auth/src/main/java/com/condation/cms/auth/permissions/PermissionRegistry.java b/cms-auth/src/main/java/com/condation/cms/auth/permissions/PermissionRegistry.java new file mode 100644 index 000000000..378aebcb8 --- /dev/null +++ b/cms-auth/src/main/java/com/condation/cms/auth/permissions/PermissionRegistry.java @@ -0,0 +1,48 @@ +package com.condation.cms.auth.permissions; + +/*- + * #%L + * cms-auth + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author thmar + */ +public class PermissionRegistry { + + private static final Map registry = new HashMap<>(); + + public static void register(Permission permission) { + registry.put(permission.key(), permission); + } + + public static Permission get(String key) { + return registry.get(key); + } + + public static Collection all() { + return registry.values(); + } +} diff --git a/cms-auth/src/main/java/com/condation/cms/auth/services/AuthService.java b/cms-auth/src/main/java/com/condation/cms/auth/services/AuthService.java index 86ffeb303..522ae7ab0 100644 --- a/cms-auth/src/main/java/com/condation/cms/auth/services/AuthService.java +++ b/cms-auth/src/main/java/com/condation/cms/auth/services/AuthService.java @@ -79,18 +79,18 @@ public Optional find (final String path) { public static class AuthPath { private String path; private String realm; - private List groups; + private List roles; - public boolean allowed (UserService.User user) { - if (user.groups() == null || user.groups().length == 0) { + public boolean allowed (User user) { + if (user.roles() == null || user.roles().length == 0) { return false; } - if (groups == null || groups.isEmpty()) { + if (roles == null || roles.isEmpty()) { return false; } - for (String group : user.groups()) { - if (groups.contains(group)) { + for (String role : user.roles()) { + if (roles.contains(role)) { return true; } } diff --git a/cms-auth/src/main/java/com/condation/cms/auth/services/AuthorizationService.java b/cms-auth/src/main/java/com/condation/cms/auth/services/AuthorizationService.java new file mode 100644 index 000000000..ce29e8fcf --- /dev/null +++ b/cms-auth/src/main/java/com/condation/cms/auth/services/AuthorizationService.java @@ -0,0 +1,119 @@ +package com.condation.cms.auth.services; + +/*- + * #%L + * cms-auth + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.auth.permissions.Permission; +import com.condation.cms.auth.permissions.PermissionRegistry; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Authorization service for role and permission checks. + * + * Permissions are managed via a central registry. + * Roles only contain permission keys so that modules can add custom permissions. + */ +public class AuthorizationService { + + /** + * Predefined roles with associated permission keys. + * These can later be extended via configuration or by modules. + */ + public enum Role { + EDITOR(Set.of(Permissions.CONTENT_EDIT)), + MANAGER(Set.of(Permissions.CONTENT_EDIT, Permissions.CACHE_INVALIDATE)), + ADMIN(Set.of(Permissions.CONTENT_EDIT, Permissions.CACHE_INVALIDATE)); + + private final Set permissionKeys; + + Role(Set permissionKeys) { + this.permissionKeys = permissionKeys; + } + + public Set getPermissionKeys() { + return permissionKeys; + } + + public static Role fromString(String role) { + try { + return Role.valueOf(role.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return null; // ignore unknown roles + } + } + } + + /** + * Collects all permission keys of a given user (based on their roles). + */ + public Set getPermissionKeys(User user) { + if (user == null || user.roles() == null) { + return Set.of(); + } + return Arrays.stream(user.roles()) + .map(Role::fromString) + .filter(Objects::nonNull) + .flatMap(r -> r.getPermissionKeys().stream()) + .collect(Collectors.toSet()); + } + + /** + * Checks if a user has a specific permission. + * @param user + * @param permissionKey + * @return + */ + public boolean hasPermission(User user, String permissionKey) { + return getPermissionKeys(user).contains(permissionKey); + } + + /** + * Checks if a user has at least one of the given permissions. + * @param user + * @param required + * @return + */ + public boolean hasAnyPermission(User user, String... required) { + Set userPerms = getPermissionKeys(user); + for (String key : required) { + if (userPerms.contains(key)) { + return true; + } + } + return false; + } + + /** + * Checks if a user has all of the given permissions. + */ + public boolean hasAllPermissions(User user, String... required) { + Set userPerms = getPermissionKeys(user); + return userPerms.containsAll(Set.of(required)); + } + + // --- Register default/core permissions --- + static { + PermissionRegistry.register(Permission.CONTENT_EDIT); + } +} diff --git a/cms-auth/src/main/java/com/condation/cms/auth/services/Realm.java b/cms-auth/src/main/java/com/condation/cms/auth/services/Realm.java new file mode 100644 index 000000000..57090960f --- /dev/null +++ b/cms-auth/src/main/java/com/condation/cms/auth/services/Realm.java @@ -0,0 +1,36 @@ +package com.condation.cms.auth.services; + +/*- + * #%L + * cms-auth + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + + + +/** + * + * @author thorstenmarx + */ +public record Realm (String name) { + + public static Realm of(String name) { + return new Realm(name); + } +} diff --git a/cms-auth/src/main/java/com/condation/cms/auth/services/User.java b/cms-auth/src/main/java/com/condation/cms/auth/services/User.java new file mode 100644 index 000000000..088616589 --- /dev/null +++ b/cms-auth/src/main/java/com/condation/cms/auth/services/User.java @@ -0,0 +1,76 @@ +package com.condation.cms.auth.services; + +/*- + * #%L + * cms-auth + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.core.configuration.GSONProvider; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + + +/** + * + * @author thorstenmarx + */ +public record User (String username, String passwordHash, String [] roles, Map data) { + + public User(String username, String passwordHash, String[] roles) { + this(username, passwordHash, roles, Collections.emptyMap()); + } + + public String line() { + try { + String json = GSONProvider.GSON.toJson(data != null ? data : Map.of()); + String encodedData = Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); + return "%s:%s:%s:%s\r\n".formatted(username, passwordHash, roles != null ? String.join(",", roles) : "", encodedData); + } catch (Exception e) { + throw new RuntimeException("Error writing user data", e); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof User other)) { + return false; + } + return Objects.equals(username, other.username) && Objects.equals(passwordHash, other.passwordHash) && Arrays.equals(roles, other.roles) && Objects.equals(data, other.data); + } + + @Override + public int hashCode() { + int result = Objects.hash(username, passwordHash, data); + result = 31 * result + Arrays.hashCode(roles); + return result; + } + + @Override + public String toString() { + return "User{" + "username='" + username + '\'' + ", passwordHash='***'" + ", roles=" + Arrays.toString(roles) + ", data=" + data + '}'; + } +} diff --git a/cms-auth/src/main/java/com/condation/cms/auth/services/UserService.java b/cms-auth/src/main/java/com/condation/cms/auth/services/UserService.java index a1ddf92a5..799d82fba 100644 --- a/cms-auth/src/main/java/com/condation/cms/auth/services/UserService.java +++ b/cms-auth/src/main/java/com/condation/cms/auth/services/UserService.java @@ -32,12 +32,10 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; -import java.util.Arrays; import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -55,9 +53,13 @@ public class UserService { private final static Splitter userSplitter = Splitter.on(":").trimResults(); private final static Splitter groupSplitter = Splitter.on(",").trimResults(); - private final Path hostBase; + private final Path serverBase; - public void addUser(Realm realm, String username, String password, String[] groups) throws IOException { + public void addUser (Realm realm, String username, String password, String[] roles) throws Exception { + addUser(realm, username, password, roles, Map.of()); + } + + public void addUser(Realm realm, String username, String password, String[] roles, Map data) throws IOException { List users = loadUsers(realm); users = new ArrayList<>(users.stream() .filter(user -> !user.username().equals(username)) @@ -67,19 +69,28 @@ public void addUser(Realm realm, String username, String password, String[] grou String saltBase64 = Base64.getEncoder().encodeToString(salt); String passwordHash = SecurityUtil.hashPBKDF2(password, salt); - Map data = new HashMap<>(); - data.put("salt", saltBase64); + Map userData = new HashMap<>(data); + userData.put("salt", saltBase64); - users.add(new User(username, passwordHash, groups, data)); + users.add(new User(username, passwordHash, roles, userData)); saveUsers(realm, users); } public void removeUser(Realm realm, String username) throws IOException { - var users = loadUsers(realm); - users = new ArrayList<>(users.stream().filter(user -> !user.username.equals(username)).toList()); + java.util.List users = loadUsers(realm); + users = new ArrayList<>(users.stream().filter(user -> !user.username().equals(username)).toList()); saveUsers(realm, users); } + public Optional byUsername(final Realm realm, final String username) { + try { + return loadUsers(realm).stream().filter(user -> user.username().equals(username)).findFirst(); + } catch (Exception ex) { + log.error("", ex); + } + return Optional.empty(); + } + private static User fromString(final String userString) { List userParts = userSplitter.splitToList(userString); @@ -102,7 +113,7 @@ private static User fromString(final String userString) { } private List loadUsers(final Realm realm) throws IOException { - Path usersFile = hostBase.resolve("config/" + FILENAME_PATTERN.formatted(realm.name)); + Path usersFile = serverBase.resolve("config/" + FILENAME_PATTERN.formatted(realm.name())); List users = new ArrayList<>(); if (Files.exists(usersFile)) { List lines = Files.readAllLines(usersFile, StandardCharsets.UTF_8); @@ -123,7 +134,7 @@ private List loadUsers(final Realm realm) throws IOException { public Optional login(final Realm realm, final String username, final String password) { try { - var userOpt = loadUsers(realm).stream() + java.util.Optional userOpt = loadUsers(realm).stream() .filter(user -> user.username().equals(username)) .findFirst(); @@ -153,9 +164,11 @@ public Optional login(final Realm realm, final String username, final Stri } private void saveUsers(Realm realm, List users) throws IOException { - Path usersFile = hostBase.resolve("config/" + FILENAME_PATTERN.formatted(realm.name)); + Path usersFile = serverBase.resolve("config/" + FILENAME_PATTERN.formatted(realm.name())); Files.deleteIfExists(usersFile); + Files.createDirectories(usersFile.getParent()); + StringBuilder userContent = new StringBuilder(); users.forEach(user -> userContent.append(user.line())); @@ -163,49 +176,5 @@ private void saveUsers(Realm realm, List users) throws IOException { Files.writeString(usersFile, userContent, StandardCharsets.UTF_8, StandardOpenOption.CREATE); } - public static record User(String username, String passwordHash, String[] groups, Map data) { - - public String line() { - try { - String json = GSONProvider.GSON.toJson(data != null ? data : Map.of()); - String encodedData = Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8)); - return "%s:%s:%s:%s\r\n".formatted( - username, - passwordHash, - groups != null ? String.join(",", groups) : "", - encodedData - ); - } catch (Exception e) { - throw new RuntimeException("Error writing user data", e); - } - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof User other)) { - return false; - } - return Objects.equals(username, other.username) - && Objects.equals(passwordHash, other.passwordHash) - && Arrays.equals(groups, other.groups) - && Objects.equals(data, other.data); - } - - @Override - public int hashCode() { - int result = Objects.hash(username, passwordHash, data); - result = 31 * result + Arrays.hashCode(groups); - return result; - } - } - - public static record Realm(String name) { - public static Realm of(String name) { - return new Realm(name); - } - } } diff --git a/cms-auth/src/test/java/com/condation/cms/auth/services/AuthorizationServiceTest.java b/cms-auth/src/test/java/com/condation/cms/auth/services/AuthorizationServiceTest.java new file mode 100644 index 000000000..1f656820e --- /dev/null +++ b/cms-auth/src/test/java/com/condation/cms/auth/services/AuthorizationServiceTest.java @@ -0,0 +1,96 @@ +package com.condation.cms.auth.services; + +/*- + * #%L + * cms-auth + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.auth.permissions.Permission; +import com.condation.cms.auth.permissions.PermissionRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class AuthorizationServiceTest { + + private AuthorizationService authorizationService; + + @BeforeEach + void setUp() { + authorizationService = new AuthorizationService(); + } + + @Test + void editorShouldHaveEditPermissionButNotPublishPermission() { + User editor = new User("editor", "hash", new String[]{"editor"}); + + assertThat(authorizationService.hasPermission(editor, Permissions.CONTENT_EDIT)).isTrue(); + assertThat(authorizationService.hasPermission(editor, Permissions.CACHE_INVALIDATE)).isFalse(); + } + + @Test + void managerShouldHaveEditAndPublishPermissions() { + User manager = new User("manager", "hash", new String[]{"manager"}); + + assertThat(authorizationService.hasPermission(manager, Permissions.CONTENT_EDIT)).isTrue(); + assertThat(authorizationService.hasPermission(manager, Permissions.CACHE_INVALIDATE)).isTrue(); + } + + @Test + void adminShouldHaveAllCorePermissions() { + User admin = new User("admin", "hash", new String[]{"admin"}); + + assertThat(authorizationService.hasPermission(admin, Permissions.CONTENT_EDIT)).isTrue(); + assertThat(authorizationService.hasPermission(admin, Permissions.CACHE_INVALIDATE)).isTrue(); + } + + @Test + void hasAnyPermissionShouldWorkCorrectly() { + User manager = new User("manager", "hash", new String[]{"manager"}); + + assertThat(authorizationService.hasAnyPermission(manager, Permissions.CACHE_INVALIDATE, Permissions.CONTENT_EDIT)).isTrue(); + assertThat(authorizationService.hasAllPermissions(manager, Permissions.CACHE_INVALIDATE, "user.manage")).isFalse(); + } + + @Test + void hasAllPermissionsShouldWorkCorrectly() { + User admin = new User("admin", "hash", new String[]{"admin"}); + + assertThat(authorizationService.hasAllPermissions(admin, Permissions.CACHE_INVALIDATE, Permissions.CONTENT_EDIT)).isTrue(); + assertThat(authorizationService.hasAllPermissions(admin, Permissions.CONTENT_EDIT, "unknown.permission")).isFalse(); + } + + @Test + void customModulePermissionShouldBeRegisterable() { + // Register a custom permission from a module + PermissionRegistry.register( + new Permission("blog.write", "Write blog posts") + ); + + // Extend role manually (ADMIN should conceptually have all, but for test we check via key set) + User admin = new User("admin", "hash", new String[]{"admin"}); + + assertThat(PermissionRegistry.get("blog.write")).isNotNull(); + // Admin does not automatically get blog.write unless Role.ADMIN is extended. + assertThat(authorizationService.hasPermission(admin, "blog.write")).isFalse(); + } +} diff --git a/cms-auth/src/test/java/com/condation/cms/auth/services/UserServiceTest.java b/cms-auth/src/test/java/com/condation/cms/auth/services/UserServiceTest.java index e4fc05fb3..c71591d6b 100644 --- a/cms-auth/src/test/java/com/condation/cms/auth/services/UserServiceTest.java +++ b/cms-auth/src/test/java/com/condation/cms/auth/services/UserServiceTest.java @@ -49,9 +49,9 @@ public static void setup () throws IOException { @Test - public void test_login_and_remove() throws IOException { + public void test_login_and_remove() throws Exception { - var realm = UserService.Realm.of("users"); + com.condation.cms.auth.services.Realm realm = Realm.of("users"); Assertions.assertThat(userService.login(realm, "test", "demo")).isEmpty(); @@ -65,9 +65,9 @@ public void test_login_and_remove() throws IOException { } @Test - public void test_multiple_users() throws IOException { + public void test_multiple_users() throws Exception { - var realm = UserService.Realm.of("musers"); + com.condation.cms.auth.services.Realm realm = Realm.of("musers"); userService.addUser(realm, "test1", "demo", new String[]{"eins","zwei"}); userService.addUser(realm, "test2", "demo", new String[]{"eins","zwei"}); diff --git a/cms-auth/src/test/resources/hosts/demo/config/auth.yaml b/cms-auth/src/test/resources/hosts/demo/config/auth.yaml index 9ca86f398..38080c7ae 100644 --- a/cms-auth/src/test/resources/hosts/demo/config/auth.yaml +++ b/cms-auth/src/test/resources/hosts/demo/config/auth.yaml @@ -1,4 +1,4 @@ paths: - path: "/secured" realm: "users" - groups: ["eins"] \ No newline at end of file + roles: ["eins"] \ No newline at end of file diff --git a/cms-content/pom.xml b/cms-content/pom.xml index 132b80aa2..55db26983 100644 --- a/cms-content/pom.xml +++ b/cms-content/pom.xml @@ -6,16 +6,33 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 cms-content jar - - + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + --enable-preview + + + + + + com.condation.cms cms-api + + com.condation.cms + cms-core + com.condation.cms cms-filesystem diff --git a/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java b/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java index c4ac5eac4..42e9efdcc 100644 --- a/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java +++ b/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java @@ -34,6 +34,7 @@ import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.utils.HTTPUtil; import com.condation.cms.api.utils.PathUtil; +import com.condation.cms.core.content.ContentResolvingStrategy; import com.google.common.base.Strings; import java.io.IOException; import java.util.List; @@ -86,6 +87,7 @@ public Optional getErrorContent (final RequestContext context) } private Optional getContent(final RequestContext context, boolean checkVisibility) { + /* String path; if (Strings.isNullOrEmpty(context.get(RequestFeature.class).uri())) { path = ""; @@ -111,7 +113,12 @@ private Optional getContent(final RequestContext context, boole contentFile = temp; } } + */ + var contentBase = db.getReadOnlyFileSystem().contentBase(); + var path = ContentResolvingStrategy.uriToPath(context.get(RequestFeature.class).uri()); + Optional contentFileOpt = ContentResolvingStrategy.resolve(context.get(RequestFeature.class).uri(), db); + ReadOnlyFile contentFile = contentFileOpt.orElse(null); // handle alias ContentNode contentNode = null; boolean aliasRedirect = false; @@ -125,7 +132,10 @@ private Optional getContent(final RequestContext context, boole } } else { var uri = PathUtil.toRelativeFile(contentFile, contentBase); - contentNode = db.getContent().byUri(uri).get(); + final Optional nodeByUri = db.getContent().byUri(uri); + if (nodeByUri.isPresent()) { + contentNode = nodeByUri.get(); + } } if (contentNode == null) { diff --git a/cms-content/src/main/java/com/condation/cms/content/DefaultContentParser.java b/cms-content/src/main/java/com/condation/cms/content/DefaultContentParser.java index 068146c63..b2d14486b 100644 --- a/cms-content/src/main/java/com/condation/cms/content/DefaultContentParser.java +++ b/cms-content/src/main/java/com/condation/cms/content/DefaultContentParser.java @@ -27,8 +27,6 @@ import java.io.IOException; import java.util.Collections; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import lombok.extern.slf4j.Slf4j; import org.yaml.snakeyaml.Yaml; diff --git a/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java b/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java index 0dcdbf242..95503f72b 100644 --- a/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java +++ b/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java @@ -36,6 +36,7 @@ import com.condation.cms.api.feature.features.InjectorFeature; import com.condation.cms.api.feature.features.IsDevModeFeature; import com.condation.cms.api.feature.features.IsPreviewFeature; +import com.condation.cms.api.feature.features.MarkdownRendererFeature; import com.condation.cms.api.feature.features.RequestFeature; import com.condation.cms.api.feature.features.ServerPropertiesFeature; import com.condation.cms.api.feature.features.SiteMediaServiceFeature; @@ -47,15 +48,19 @@ import com.condation.cms.api.utils.SectionUtil; import com.condation.cms.content.pipeline.ContentPipelineFactory; import com.condation.cms.content.views.model.View; -import com.condation.cms.core.content.MapAccess; +import com.condation.cms.api.content.MapAccess; +import com.condation.cms.api.utils.HTTPUtil; import com.condation.cms.extensions.hooks.DBHooks; import com.condation.cms.extensions.hooks.TemplateHooks; import com.condation.cms.content.template.functions.LinkFunction; +import com.condation.cms.content.template.functions.MarkdownFunction; import com.condation.cms.content.template.functions.list.NodeListFunctionBuilder; import com.condation.cms.content.template.functions.navigation.NavigationFunction; import com.condation.cms.content.template.functions.query.QueryFunction; -import com.condation.cms.content.template.functions.shortcode.ShortCodeTemplateFunction; +import com.condation.cms.content.template.functions.tag.TagTemplateFunction; import com.condation.cms.content.template.functions.taxonomy.TaxonomyFunction; +import com.condation.cms.content.template.functions.translation.NodeTranslations; +import com.condation.cms.content.template.functions.translation.SiteTranslations; import com.condation.modules.api.ModuleManager; import java.io.IOException; import java.util.ArrayList; @@ -137,44 +142,48 @@ public String render(final ReadOnlyFile contentFile, final RequestContext contex TemplateEngine.Model model = new TemplateEngine.Model( contentFile, - contentNode.isPresent() ? contentNode.get() : null, + contentNode.orElse(null), context); modelExtending.accept(model); Namespace namespace = new Namespace(); - model.values.put("meta", new MapAccess(meta)); - model.values.put("sections", sections); - - namespace.add("node", "meta", new MapAccess(meta)); - namespace.add("node", "sections", sections); + namespace.add(Constants.TemplateNamespaces.NODE, "meta", new MapAccess(meta)); + namespace.add(Constants.TemplateNamespaces.NODE, "sections", sections); + namespace.add(Constants.TemplateNamespaces.NODE, "uri", uri); + namespace.add(Constants.TemplateNamespaces.NODE, "translation", new NodeTranslations(contentNode.orElse(null), siteProperties)); + + var canonicalUrl = ""; + if (contentNode.isPresent()) { + canonicalUrl = PathUtil.toURL(contentNode.get().uri()); + canonicalUrl = HTTPUtil.modifyUrl(canonicalUrl, siteProperties); + } + namespace.add(Constants.TemplateNamespaces.NODE, "canonicalUrl", canonicalUrl); - ShortCodeTemplateFunction shortCodeFunction = createShortCodeFunction(context); - model.values.put(ShortCodeTemplateFunction.KEY, shortCodeFunction); - namespace.add("cms", ShortCodeTemplateFunction.KEY, shortCodeFunction); + TagTemplateFunction tagFunction = createTagFunction(context); + namespace.add(Constants.TemplateNamespaces.CMS, TagTemplateFunction.KEY, tagFunction); NavigationFunction navigationFunction = createNavigationFunction(contentFile, context); - model.values.put("navigation", navigationFunction); - namespace.add("cms", "navigation", shortCodeFunction); + namespace.add(Constants.TemplateNamespaces.CMS, "navigation", navigationFunction); NodeListFunctionBuilder nodeListFunction = createNodeListFunction(contentFile, context); - model.values.put("nodeList", nodeListFunction); - namespace.add("cms", "nodeList", nodeListFunction); + namespace.add(Constants.TemplateNamespaces.CMS, "nodeList", nodeListFunction); QueryFunction queryFunction = createQueryFunction(contentFile, context); - model.values.put("query", queryFunction); - namespace.add("cms", "query", queryFunction); + namespace.add(Constants.TemplateNamespaces.CMS, "query", queryFunction); + MarkdownFunction markdownFunction = createMarkdownFunction(context); + namespace.add(Constants.TemplateNamespaces.CMS, "markdown", markdownFunction); + model.values.put("requestContext", context.get(RequestFeature.class)); model.values.put("theme", context.get(RenderContext.class).theme()); - model.values.put("site", siteProperties); - model.values.put("mediaService", context.get(SiteMediaServiceFeature.class).mediaService()); - namespace.add("cms", "mediaService", context.get(SiteMediaServiceFeature.class).mediaService()); - - model.values.put("taxonomies", context.get(InjectorFeature.class).injector().getInstance(TaxonomyFunction.class)); - namespace.add("cms", "taxonomies", context.get(InjectorFeature.class).injector().getInstance(TaxonomyFunction.class)); + namespace.add(Constants.TemplateNamespaces.CMS, "mediaService", context.get(SiteMediaServiceFeature.class).mediaService()); + namespace.add(Constants.TemplateNamespaces.CMS, "taxonomies", context.get(InjectorFeature.class).injector().getInstance(TaxonomyFunction.class)); + namespace.add(Constants.TemplateNamespaces.SITE, "properties", siteProperties); + namespace.add(Constants.TemplateNamespaces.SITE, "translation", new SiteTranslations(siteProperties)); + var theme = context.get(RenderContext.class).theme(); if (theme.empty()) { model.values.put("messages", context.get(InjectorFeature.class).injector().getInstance(MessageSource.class)); @@ -182,13 +191,12 @@ public String render(final ReadOnlyFile contentFile, final RequestContext contex model.values.put("messages", theme.getMessages()); } - model.values.put("hooks", context.get(HookSystemFeature.class).hookSystem()); - namespace.add("cms", "hooks", context.get(HookSystemFeature.class).hookSystem()); + namespace.add(Constants.TemplateNamespaces.CMS, "hooks", context.get(HookSystemFeature.class).hookSystem()); - model.values.put("links", new LinkFunction(context)); - namespace.add("cms", "links", new LinkFunction(context)); + namespace.add(Constants.TemplateNamespaces.CMS, "links", new LinkFunction(context)); model.values.put("PREVIEW_MODE", isPreview(context)); + model.values.put("MANAGER", isManager(context)); model.values.put("DEV_MODE", isDevMode(context)); model.values.put("ENV", context.get(ServerPropertiesFeature.class).serverProperties().env()); @@ -198,11 +206,7 @@ public String render(final ReadOnlyFile contentFile, final RequestContext contex context.get(TemplateHooks.class).getTemplateSupplier().getRegisterTemplateSupplier().forEach(service -> { model.values.put(service.name(), service.supplier()); - namespace.add(Constants.DEFAULT_MODULE_NAMESPACE, service.name(), service.supplier()); - }); - context.get(TemplateHooks.class).getTemplateFunctions().getRegisterTemplateFunctions().forEach(service -> { - model.values.put(service.name(), service.function()); - namespace.add(Constants.DEFAULT_MODULE_NAMESPACE, service.name(), service.function()); + namespace.add(Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE, service.name(), service.supplier()); }); extendModel(model, namespace); @@ -212,13 +216,17 @@ public String render(final ReadOnlyFile contentFile, final RequestContext contex String content = renderContent(rawContent, context, modelCopy); model.values.put("content", content); - namespace.add("node", "content", content); + namespace.add(Constants.TemplateNamespaces.NODE, "content", content); model.values.putAll(namespace.getNamespaces()); return templates.get().render((String) meta.get("template"), model); } + protected MarkdownFunction createMarkdownFunction(final RequestContext context) { + return new MarkdownFunction(context.get(MarkdownRendererFeature.class).markdownRenderer()); + } + protected QueryFunction createQueryFunction(final ReadOnlyFile contentFile, final RequestContext context) { Map> customOperators = new HashMap<>(); @@ -250,6 +258,13 @@ private boolean isPreview(final RequestContext context) { return context.has(IsPreviewFeature.class); } + private boolean isManager(final RequestContext context) { + if (context.has(IsPreviewFeature.class)) { + return context.get(IsPreviewFeature.class).mode().equals(IsPreviewFeature.Mode.MANAGER); + } + return false; + } + private boolean isDevMode(final RequestContext context) { return context.has(IsDevModeFeature.class); } @@ -282,13 +297,13 @@ public Map> renderSections(final List section var sectionPath = contentBase.resolve(node.uri()); var content = render(sectionPath, context); var name = SectionUtil.getSectionName(node.name()); - var index = SectionUtil.getSectionIndex(node.name()); + var index = node.getMetaValue(Constants.MetaFields.LAYOUT_ORDER, Constants.DEFAULT_SECTION_LAYOUT_ORDER); if (!sections.containsKey(name)) { sections.put(name, new ArrayList<>()); } - sections.get(name).add(new Section(name, index, content)); + sections.get(name).add(new Section(name, index, content, node.data())); } catch (Exception ex) { log.error("error render section", ex); } @@ -300,8 +315,8 @@ public Map> renderSections(final List section return sections; } - private ShortCodeTemplateFunction createShortCodeFunction(RequestContext context) { - return new ShortCodeTemplateFunction(context, context.get(RenderContext.class).shortCodes()); + private TagTemplateFunction createTagFunction(RequestContext context) { + return new TagTemplateFunction(context, context.get(RenderContext.class).tags()); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/RenderContext.java b/cms-content/src/main/java/com/condation/cms/content/RenderContext.java index 5931e72af..7a1e98d3a 100644 --- a/cms-content/src/main/java/com/condation/cms/content/RenderContext.java +++ b/cms-content/src/main/java/com/condation/cms/content/RenderContext.java @@ -27,7 +27,7 @@ import com.condation.cms.api.feature.Feature; import com.condation.cms.api.markdown.MarkdownRenderer; import com.condation.cms.api.theme.Theme; -import com.condation.cms.content.shortcodes.ShortCodes; +import com.condation.cms.content.tags.Tags; import lombok.extern.slf4j.Slf4j; /** @@ -36,7 +36,7 @@ */ @Slf4j @FeatureScope(FeatureScope.Scope.REQUEST) -public record RenderContext(MarkdownRenderer markdownRenderer, ShortCodes shortCodes, Theme theme) +public record RenderContext(MarkdownRenderer markdownRenderer, Tags tags, Theme theme) implements AutoCloseable, Feature { @Override diff --git a/cms-content/src/main/java/com/condation/cms/content/Section.java b/cms-content/src/main/java/com/condation/cms/content/Section.java index d9a5e3f18..74f53d932 100644 --- a/cms-content/src/main/java/com/condation/cms/content/Section.java +++ b/cms-content/src/main/java/com/condation/cms/content/Section.java @@ -24,15 +24,20 @@ import com.condation.cms.api.Constants; +import java.util.Map; /** * * @author t.marx */ -public record Section(String name, int index, String content) { +public record Section(String name, int index, String content, Map data, String uri) { - public Section(String name, String content) { - this(name, Constants.DEFAULT_SECTION_ORDERED_INDEX, content); + public Section(String name, int index, String content, Map data) { + this(name, index, content, data, null); + } + + public Section(String name, String content, Map data) { + this(name, Constants.DEFAULT_SECTION_LAYOUT_ORDER, content, data, null); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java b/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java index 5f9cd492b..b407968e4 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java @@ -1,5 +1,10 @@ package com.condation.cms.content.markdown; +import com.condation.cms.api.feature.features.IsPreviewFeature; +import com.condation.cms.api.request.RequestContext; +import com.condation.cms.api.request.RequestContextScope; +import java.util.Optional; + /*- * #%L * cms-content @@ -21,15 +26,30 @@ * . * #L% */ - - /** * * @author t.marx */ public interface InlineBlock { + int start(); + int end(); + + String render(); + + default boolean isPreview() { + if (!RequestContextScope.REQUEST_CONTEXT.isBound()) { + return false; + } + var requestContext = RequestContextScope.REQUEST_CONTEXT.get(); + return requestContext != null && requestContext.has(IsPreviewFeature.class); + } - String render (); + default Optional getRequestContext () { + if (!RequestContextScope.REQUEST_CONTEXT.isBound()) { + return Optional.empty(); + } + return Optional.ofNullable(RequestContextScope.REQUEST_CONTEXT.get()); + } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/Options.java b/cms-content/src/main/java/com/condation/cms/content/markdown/Options.java index e670fa3eb..8d543bcd0 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/Options.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/Options.java @@ -29,7 +29,7 @@ import com.condation.cms.content.markdown.rules.block.HeadingBlockRule; import com.condation.cms.content.markdown.rules.block.HorizontalRuleBlockRule; import com.condation.cms.content.markdown.rules.block.ListBlockRule; -import com.condation.cms.content.markdown.rules.block.ShortCodeBlockRule; +import com.condation.cms.content.markdown.rules.block.TagBlockRule; import com.condation.cms.content.markdown.rules.block.TableBlockRule; import com.condation.cms.content.markdown.rules.block.TaskListBlockRule; import com.condation.cms.content.markdown.rules.inline.HighlightInlineRule; @@ -38,7 +38,7 @@ import com.condation.cms.content.markdown.rules.inline.ItalicInlineRule; import com.condation.cms.content.markdown.rules.inline.LinkInlineRule; import com.condation.cms.content.markdown.rules.inline.NewlineInlineRule; -import com.condation.cms.content.markdown.rules.inline.ShortCodeInlineBlockRule; +import com.condation.cms.content.markdown.rules.inline.TagInlineBlockRule; import com.condation.cms.content.markdown.rules.inline.StrikethroughInlineRule; import com.condation.cms.content.markdown.rules.inline.StrongInlineRule; import com.condation.cms.content.markdown.rules.inline.SubscriptInlineRule; @@ -54,7 +54,7 @@ public class Options { public static Options all () { Options options = new Options(); - options.addInlineRule(new ShortCodeInlineBlockRule()); + options.addInlineRule(new TagInlineBlockRule()); options.addInlineRule(new StrongInlineRule()); options.addInlineRule(new ItalicInlineRule()); options.addInlineRule(new NewlineInlineRule()); @@ -66,7 +66,7 @@ public static Options all () { options.addInlineRule(new SubscriptInlineRule()); options.addInlineRule(new SuperscriptInlineRule()); - options.addBlockRule(new ShortCodeBlockRule()); + options.addBlockRule(new TagBlockRule()); options.addBlockRule(new CodeBlockRule()); options.addBlockRule(new HeadingBlockRule()); options.addBlockRule(new TaskListBlockRule()); diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/ShortCodeBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TagBlockRule.java similarity index 89% rename from cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/ShortCodeBlockRule.java rename to cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TagBlockRule.java index eaab38343..8db72fb5c 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/ShortCodeBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TagBlockRule.java @@ -24,15 +24,15 @@ import com.condation.cms.content.markdown.Block; import com.condation.cms.content.markdown.BlockElementRule; import com.condation.cms.content.markdown.InlineRenderer; -import com.condation.cms.content.shortcodes.TagMap; -import com.condation.cms.content.shortcodes.TagParser; +import com.condation.cms.content.tags.TagMap; +import com.condation.cms.content.tags.TagParser; import java.util.List; /** * * @author t.marx */ -public class ShortCodeBlockRule implements BlockElementRule { +public class TagBlockRule implements BlockElementRule { private static final TagParser tagParser = new TagParser(null); @@ -51,14 +51,14 @@ public boolean has(String codeName) { return null; } var tag = tags.getFirst(); - return new ShortCodeBlock( + return new TagBlock( tag.startIndex(), tag.endIndex(), tag); } - public static record ShortCodeBlock(int start, int end, TagParser.TagInfo tagInfo) implements Block { + public static record TagBlock(int start, int end, TagParser.TagInfo tagInfo) implements Block { @Override public String render(InlineRenderer inlineRenderer) { @@ -73,7 +73,7 @@ public String render(InlineRenderer inlineRenderer) { .formatted( tagInfo.name(), String.join(" ", params), - tagInfo.rawAttributes().getOrDefault("_content", ""), + inlineRenderer.render((String)tagInfo.rawAttributes().getOrDefault("_content", "")), tagInfo.name() ); } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java index 4aa4f7854..b0540506e 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java @@ -23,8 +23,11 @@ */ +import com.condation.cms.api.feature.features.SiteMediaServiceFeature; +import com.condation.cms.api.utils.ImageUtil; import com.condation.cms.content.markdown.InlineBlock; import com.condation.cms.content.markdown.InlineElementRule; +import com.google.common.base.Strings; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -50,12 +53,26 @@ public static record ImageInlineBlock(int start, int end, String src, String alt @Override public String render() { + var altText = alt; + var requestContext = getRequestContext(); + if (Strings.isNullOrEmpty(altText) && requestContext.isPresent()) { + var imageUrl = ImageUtil.getRawPath(src, requestContext.get()); + var media = requestContext.get().get(SiteMediaServiceFeature.class).mediaService().get(imageUrl); + + if (media != null && media.meta().containsKey("alt")) { + altText = (String) media.meta().get("alt"); + } + } + + var uiSelector = ""; + if (isPreview()) { + uiSelector = " data-cms-ui-selector=\"content-image\" "; + } + if (title != null && !"".equals(title.trim())) { - return "\"%s\"".formatted(src, alt, title); + return "\"%s\"".formatted(src, altText, title, uiSelector); } - return "\"%s\"".formatted(src, alt); + return "\"%s\"".formatted(src, altText, uiSelector); } - } - } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageLinkInlineRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageLinkInlineRule.java index 24cb57489..cf164fcee 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageLinkInlineRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageLinkInlineRule.java @@ -24,7 +24,7 @@ import com.github.slugify.Slugify; import com.condation.cms.api.feature.features.IsPreviewFeature; import com.condation.cms.api.feature.features.SitePropertiesFeature; -import com.condation.cms.api.request.ThreadLocalRequestContext; +import com.condation.cms.api.request.RequestContextScope; import com.condation.cms.content.markdown.InlineBlock; import com.condation.cms.content.markdown.InlineElementRule; import java.util.regex.Pattern; @@ -52,11 +52,13 @@ public InlineBlock next(String md) { var id = SLUG.slugify(alt); - var requestContext = ThreadLocalRequestContext.REQUEST_CONTEXT.get(); + - if (requestContext != null + if (RequestContextScope.REQUEST_CONTEXT.isBound() && isInternalUrl(href)) { - + + var requestContext = RequestContextScope.REQUEST_CONTEXT.get(); + if (requestContext.has(SitePropertiesFeature.class)) { var contextPath = requestContext.get(SitePropertiesFeature.class).siteProperties().contextPath(); if (!"/".equals(contextPath) && !href.startsWith(contextPath) && href.startsWith("/")) { @@ -64,10 +66,11 @@ && isInternalUrl(href)) { } } if (requestContext.has(IsPreviewFeature.class)) { + var previewContext = requestContext.get(IsPreviewFeature.class); if (href.contains("?")) { - href += "&preview"; + href += "&preview=" + previewContext.mode().getValue(); } else { - href += "?preview"; + href += "?preview=" + previewContext.mode().getValue(); } } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/LinkInlineRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/LinkInlineRule.java index ca77987a1..ca8b1c9ec 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/LinkInlineRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/LinkInlineRule.java @@ -25,7 +25,7 @@ import com.github.slugify.Slugify; import com.condation.cms.api.feature.features.IsPreviewFeature; import com.condation.cms.api.feature.features.SitePropertiesFeature; -import com.condation.cms.api.request.ThreadLocalRequestContext; +import com.condation.cms.api.request.RequestContextScope; import com.condation.cms.content.markdown.InlineBlock; import com.condation.cms.content.markdown.InlineElementRule; import java.util.regex.Pattern; @@ -51,11 +51,12 @@ public InlineBlock next(String md) { var id = SLUG.slugify(text); - var requestContext = ThreadLocalRequestContext.REQUEST_CONTEXT.get(); - - if (requestContext != null + + if (RequestContextScope.REQUEST_CONTEXT.isBound() && isInternalUrl(href)) { + var requestContext = RequestContextScope.REQUEST_CONTEXT.get(); + if (requestContext.has(SitePropertiesFeature.class)) { var contextPath = requestContext.get(SitePropertiesFeature.class).siteProperties().contextPath(); if (!"/".equals(contextPath) && !href.startsWith(contextPath) && href.startsWith("/")) { @@ -63,10 +64,11 @@ && isInternalUrl(href)) { } } if (requestContext.has(IsPreviewFeature.class)) { + var previewContext = requestContext.get(IsPreviewFeature.class); if (href.contains("?")) { - href += "&preview"; + href += "&preview=" + previewContext.mode().getValue(); } else { - href += "?preview"; + href += "?preview=" + previewContext.mode().getValue(); } } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ShortCodeInlineBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/TagInlineBlockRule.java similarity index 87% rename from cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ShortCodeInlineBlockRule.java rename to cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/TagInlineBlockRule.java index 62080ce1f..953fbb10c 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ShortCodeInlineBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/TagInlineBlockRule.java @@ -23,15 +23,15 @@ */ import com.condation.cms.content.markdown.InlineBlock; import com.condation.cms.content.markdown.InlineElementRule; -import com.condation.cms.content.shortcodes.TagMap; -import com.condation.cms.content.shortcodes.TagParser; +import com.condation.cms.content.tags.TagMap; +import com.condation.cms.content.tags.TagParser; import java.util.List; /** * * @author t.marx */ -public class ShortCodeInlineBlockRule implements InlineElementRule { +public class TagInlineBlockRule implements InlineElementRule { private static final TagParser tagParser = new TagParser(null); @@ -48,13 +48,13 @@ public boolean has(String codeName) { return null; } var tag = tags.getFirst(); - return new ShortCodeInlineBlock( + return new TagInlineBlock( tag.startIndex(), tag.endIndex(), tag); } - public static record ShortCodeInlineBlock(int start, int end, TagParser.TagInfo tagInfo) implements InlineBlock { + public static record TagInlineBlock(int start, int end, TagParser.TagInfo tagInfo) implements InlineBlock { @Override public String render() { diff --git a/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipeline.java b/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipeline.java index 508b125cb..d50d14720 100644 --- a/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipeline.java +++ b/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipeline.java @@ -58,7 +58,7 @@ protected void init() { pipeline.forEach(processor -> { switch (processor) { case "markdown" -> hookSystem.registerFilter(Hooks.CONTENT_FILTER.hook(), this::processMarkdown, prio.getAndAdd(10)); - case "shortcode" -> hookSystem.registerFilter(Hooks.CONTENT_FILTER.hook(), this::processShortCodes, prio.getAndAdd(10)); + case "tags" -> hookSystem.registerFilter(Hooks.CONTENT_FILTER.hook(), this::processTags, prio.getAndAdd(10)); case "template" -> hookSystem.registerFilter(Hooks.CONTENT_FILTER.hook(), this::processTemplate, prio.getAndAdd(10)); } }); @@ -73,8 +73,8 @@ private String processMarkdown(FilterContext context) { return requestContext.get(RenderContext.class).markdownRenderer().render(context.value()); } - private String processShortCodes(FilterContext context) { - return requestContext.get(RenderContext.class).shortCodes().replace(context.value(), model.values, requestContext); + private String processTags(FilterContext context) { + return requestContext.get(RenderContext.class).tags().replace(context.value(), model.values, requestContext); } private String processTemplate(FilterContext context) { diff --git a/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipelineFactory.java b/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipelineFactory.java index 9cd1a31dd..73c1c5a37 100644 --- a/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipelineFactory.java +++ b/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipelineFactory.java @@ -23,6 +23,7 @@ */ import com.condation.cms.api.feature.features.HookSystemFeature; +import com.condation.cms.api.hooks.HookSystem; import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.template.TemplateEngine; import lombok.AccessLevel; @@ -38,7 +39,7 @@ public final class ContentPipelineFactory { public static ContentPipeline create (final RequestContext requestContext, final TemplateEngine.Model model) { var hookSystem = requestContext.get(HookSystemFeature.class).hookSystem(); - var pipeline = new ContentPipeline(hookSystem.clone(), requestContext, model); + var pipeline = new ContentPipeline(new HookSystem(hookSystem), requestContext, model); pipeline.init(); return pipeline; diff --git a/cms-content/src/main/java/com/condation/cms/content/shortcodes/ShortCodeParser.java b/cms-content/src/main/java/com/condation/cms/content/tags/ShortCodeParser.java similarity index 98% rename from cms-content/src/main/java/com/condation/cms/content/shortcodes/ShortCodeParser.java rename to cms-content/src/main/java/com/condation/cms/content/tags/ShortCodeParser.java index be7c57566..c61edd180 100644 --- a/cms-content/src/main/java/com/condation/cms/content/shortcodes/ShortCodeParser.java +++ b/cms-content/src/main/java/com/condation/cms/content/tags/ShortCodeParser.java @@ -1,4 +1,4 @@ -package com.condation.cms.content.shortcodes; +package com.condation.cms.content.tags; /*- * #%L diff --git a/cms-content/src/main/java/com/condation/cms/content/shortcodes/TagMap.java b/cms-content/src/main/java/com/condation/cms/content/tags/TagMap.java similarity index 97% rename from cms-content/src/main/java/com/condation/cms/content/shortcodes/TagMap.java rename to cms-content/src/main/java/com/condation/cms/content/tags/TagMap.java index b8daff203..dedaee969 100644 --- a/cms-content/src/main/java/com/condation/cms/content/shortcodes/TagMap.java +++ b/cms-content/src/main/java/com/condation/cms/content/tags/TagMap.java @@ -1,4 +1,4 @@ -package com.condation.cms.content.shortcodes; +package com.condation.cms.content.tags; /*- * #%L diff --git a/cms-content/src/main/java/com/condation/cms/content/shortcodes/TagParser.java b/cms-content/src/main/java/com/condation/cms/content/tags/TagParser.java similarity index 73% rename from cms-content/src/main/java/com/condation/cms/content/shortcodes/TagParser.java rename to cms-content/src/main/java/com/condation/cms/content/tags/TagParser.java index e7f16aac3..3f91cd504 100644 --- a/cms-content/src/main/java/com/condation/cms/content/shortcodes/TagParser.java +++ b/cms-content/src/main/java/com/condation/cms/content/tags/TagParser.java @@ -1,4 +1,4 @@ -package com.condation.cms.content.shortcodes; +package com.condation.cms.content.tags; /*- * #%L @@ -59,11 +59,19 @@ public List findTags(String text, TagMap tagHandlers) { tagContent = tagContent.substring(0, tagContent.length() - 1).trim(); } - int spaceIndex = tagContent.indexOf(' '); - String tagName = spaceIndex == -1 ? tagContent : tagContent.substring(0, spaceIndex); - Parameter rawAttributes = spaceIndex == -1 + // Suche erstes Whitespace-Zeichen (auch Zeilenumbrüche etc.) + int firstWhitespaceIndex = -1; + for (int j = 0; j < tagContent.length(); j++) { + if (Character.isWhitespace(tagContent.charAt(j))) { + firstWhitespaceIndex = j; + break; + } + + } + String tagName = firstWhitespaceIndex == -1 ? tagContent : tagContent.substring(0, firstWhitespaceIndex); + Parameter rawAttributes = firstWhitespaceIndex == -1 ? new Parameter() - : parseRawAttributes(tagContent.substring(spaceIndex + 1)); + : parseRawAttributes(tagContent.substring(firstWhitespaceIndex + 1)); int closingTagIndex = -1; if (!isSelfClosing) { @@ -124,9 +132,9 @@ public String parse(String text, TagMap tagHandlers, Map context return result.toString(); } - // Methode zum Finden des Endes eines Tags + // Methode zum Finden des Endes eines Tags, auch über mehrere Zeilen private int findTagEnd(String text, int startIndex) { - for (int i = startIndex; i < text.length() - 1; i++) { + for (int i = startIndex + 2; i < text.length() - 1; i++) { if (text.charAt(i) == ']' && text.charAt(i + 1) == ']') { return i; } @@ -134,39 +142,73 @@ private int findTagEnd(String text, int startIndex) { return -1; // Kein schließendes ']]' gefunden } - // Methode zur Attribut-Analyse im ersten Schritt (Rohwerte als Strings speichern) private Parameter parseRawAttributes(String attributesString) { Parameter attributes = new Parameter(); - StringBuilder key = new StringBuilder(); + String key = null; StringBuilder value = new StringBuilder(); boolean inQuotes = false; - boolean readingKey = true; + char quoteChar = 0; + boolean readingValue = false; + StringBuilder buffer = new StringBuilder(); for (int i = 0; i < attributesString.length(); i++) { char c = attributesString.charAt(i); - if (c == '"' || c == '\'') { - inQuotes = !inQuotes; - } else if (!inQuotes && (c == '=' || c == ' ')) { - if (readingKey) { - readingKey = false; - } else { - attributes.put(key.toString().trim(), value.toString().trim()); // Rohwert speichern - key.setLength(0); + + if (c == '\n' || c == '\r') { + // Zeilenumbrüche im Attributwert oder Key sind nicht erlaubt → aktuelles Attribut abbrechen + key = null; + value.setLength(0); + readingValue = false; + inQuotes = false; + buffer.setLength(0); + continue; + } + + if (!inQuotes && (c == '"' || c == '\'')) { + inQuotes = true; + quoteChar = c; + continue; + } + + if (inQuotes && c == quoteChar) { + inQuotes = false; + if (key != null) { + attributes.put(key.trim(), value.toString().trim()); + key = null; value.setLength(0); - readingKey = true; + readingValue = false; } - } else { - if (readingKey) { - key.append(c); - } else { - value.append(c); + continue; + } + + if (!inQuotes && c == '=') { + key = buffer.toString().trim(); + buffer.setLength(0); + readingValue = true; + continue; + } + + if (!inQuotes && Character.isWhitespace(c)) { + if (readingValue && key != null && value.length() > 0) { + // Nur dann speichern, wenn der Wert abgeschlossen wurde (z. B. name="abc") + attributes.put(key.trim(), value.toString().trim()); + key = null; + value.setLength(0); + readingValue = false; } + continue; + } + + if (readingValue) { + value.append(c); + } else { + buffer.append(c); } } - // Letztes Attribut verarbeiten - if (key.length() > 0 && value.length() > 0) { - attributes.put(key.toString().trim(), value.toString().trim()); // Rohwert speichern + // Falls etwas am Ende übrig bleibt (nur gültig, wenn kein Zeilenumbruch): + if (key != null && value.length() > 0 && !inQuotes) { + attributes.put(key.trim(), value.toString().trim()); } return attributes; @@ -195,9 +237,9 @@ private Object parseValue(String value, Map contextModel, Reques var contextMap = new HashMap(); contextMap.putAll(contextModel); if (requestContext != null) { - contextMap.putAll(requestContext.getVariables()); + contextMap.putAll(requestContext.getVariables()); } - + var expression = engine.createExpression(expressionString); return expression.evaluate(new MapContext(contextMap)); } diff --git a/cms-content/src/main/java/com/condation/cms/content/shortcodes/ShortCodes.java b/cms-content/src/main/java/com/condation/cms/content/tags/Tags.java similarity index 65% rename from cms-content/src/main/java/com/condation/cms/content/shortcodes/ShortCodes.java rename to cms-content/src/main/java/com/condation/cms/content/tags/Tags.java index ba0d1c6a4..c3b72cc1e 100644 --- a/cms-content/src/main/java/com/condation/cms/content/shortcodes/ShortCodes.java +++ b/cms-content/src/main/java/com/condation/cms/content/tags/Tags.java @@ -1,4 +1,4 @@ -package com.condation.cms.content.shortcodes; +package com.condation.cms.content.tags; /*- * #%L @@ -21,10 +21,9 @@ * . * #L% */ -import com.condation.cms.api.annotations.ShortCode; import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; -import java.lang.reflect.Method; +import com.condation.cms.api.utils.AnnotationsUtil; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -33,6 +32,7 @@ import java.util.function.Function; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import com.condation.cms.api.annotations.Tag; /** * @@ -40,18 +40,18 @@ */ @Slf4j @RequiredArgsConstructor -public class ShortCodes { +public class Tags { private final TagMap tagMap; private final TagParser parser; - public ShortCodes(Map> codes, TagParser tagParser) { + public Tags(Map> codes, TagParser tagParser) { this.parser = tagParser; this.tagMap = new TagMap(); this.tagMap.putAll(codes); } - public Set getShortCodeNames() { + public Set getTagNames() { return tagMap.names(); } @@ -85,7 +85,7 @@ public String execute(String name, Map parameters, RequestContex return ""; } - public static ShortCodes.Builder builder(TagParser tagParser) { + public static Tags.Builder builder(TagParser tagParser) { return new Builder(tagParser); } @@ -93,63 +93,56 @@ public static class Builder { private final TagParser tagParser; - private final Map> codes = new HashMap<>(); + private final Map> tags = new HashMap<>(); private Builder(TagParser tagParser) { this.tagParser = tagParser; } - public Builder register(String name, Function shortCodeFN) { - codes.put(name, shortCodeFN); + public Builder register(String name, Function tagFN) { + tags.put(name, tagFN); return this; } - - public Builder register (Map> codes) { - this.codes.putAll(codes); + + public Builder register(Map> codes) { + this.tags.putAll(codes); return this; } - public Builder register (List handlers) { + public Builder register(List handlers) { if (handlers == null || handlers.isEmpty()) { return this; } - + handlers.forEach(this::register); - + return this; } - + public Builder register(Object handler) { if (handler == null) { return this; } - Class clazz = handler.getClass(); - for (Method method : clazz.getDeclaredMethods()) { - if (method.isAnnotationPresent(ShortCode.class)) { - if (method.getParameterCount() == 1 && method.getParameterTypes()[0] == Parameter.class) { - method.setAccessible(true); // falls private - ShortCode annotation = method.getAnnotation(ShortCode.class); - String key = annotation.value(); - - codes.put(key, param -> { - try { - return (String) method.invoke(handler, param); - } catch (Exception e) { - throw new RuntimeException("Error calling shortcode: " + key, e); - } - }); - } else { - log.error("ignore methode" + method.getName() + " – wrong signature."); + // Wir erwarten Methoden mit @Tag(Parameter) -> String + var annotations = AnnotationsUtil.process(handler, Tag.class, List.of(Parameter.class), String.class); + + for (var entry : annotations) { + String key = entry.annotation().value(); + tags.put(key, param -> { + try { + return entry.invoke(param); + } catch (Exception e) { + throw new RuntimeException("Error calling tag: " + key, e); } - } + }); } return this; } - public ShortCodes build() { - return new ShortCodes(codes, tagParser); + public Tags build() { + return new Tags(tags, tagParser); } } } diff --git a/cms-content/src/main/java/com/condation/cms/content/template/functions/AbstractCurrentNodeFunction.java b/cms-content/src/main/java/com/condation/cms/content/template/functions/AbstractCurrentNodeFunction.java index 6fd1d5899..bad4c26ec 100644 --- a/cms-content/src/main/java/com/condation/cms/content/template/functions/AbstractCurrentNodeFunction.java +++ b/cms-content/src/main/java/com/condation/cms/content/template/functions/AbstractCurrentNodeFunction.java @@ -28,7 +28,7 @@ import com.condation.cms.api.mapper.ContentNodeMapper; import com.condation.cms.api.markdown.MarkdownRenderer; import com.condation.cms.api.request.RequestContext; -import com.condation.cms.api.request.ThreadLocalRequestContext; +import com.condation.cms.api.request.RequestContextScope; import com.condation.cms.api.utils.HTTPUtil; import java.io.IOException; import java.util.Optional; @@ -78,13 +78,21 @@ protected String getUrl(ReadOnlyFile node) { } protected boolean isPreview() { - if (ThreadLocalRequestContext.REQUEST_CONTEXT.get() != null - && ThreadLocalRequestContext.REQUEST_CONTEXT.get().has(IsPreviewFeature.class)) { + if (RequestContextScope.REQUEST_CONTEXT.isBound() + && RequestContextScope.REQUEST_CONTEXT.get().has(IsPreviewFeature.class)) { return true; } return false; } + protected String getPreviewMode() { + if (RequestContextScope.REQUEST_CONTEXT.isBound() + && RequestContextScope.REQUEST_CONTEXT.get().has(IsPreviewFeature.class)) { + return RequestContextScope.REQUEST_CONTEXT.get().get(IsPreviewFeature.class).mode().getValue(); + } + + return IsPreviewFeature.Mode.PREVIEW.getValue(); + } protected Optional parse(ReadOnlyFile node) { try { diff --git a/cms-content/src/main/java/com/condation/cms/content/template/functions/MarkdownFunction.java b/cms-content/src/main/java/com/condation/cms/content/template/functions/MarkdownFunction.java new file mode 100644 index 000000000..285cb8a6d --- /dev/null +++ b/cms-content/src/main/java/com/condation/cms/content/template/functions/MarkdownFunction.java @@ -0,0 +1,38 @@ +package com.condation.cms.content.template.functions; + +/*- + * #%L + * cms-content + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.markdown.MarkdownRenderer; + +public class MarkdownFunction { + + private final MarkdownRenderer renderer; + + public MarkdownFunction(MarkdownRenderer renderer) { + this.renderer = renderer; + } + + public String render (String markdown) { + return renderer.render(markdown); + } +} diff --git a/cms-content/src/main/java/com/condation/cms/content/template/functions/list/NodeListFunctionBuilder.java b/cms-content/src/main/java/com/condation/cms/content/template/functions/list/NodeListFunctionBuilder.java index 4dc4cacc3..14d4c7eb8 100644 --- a/cms-content/src/main/java/com/condation/cms/content/template/functions/list/NodeListFunctionBuilder.java +++ b/cms-content/src/main/java/com/condation/cms/content/template/functions/list/NodeListFunctionBuilder.java @@ -21,7 +21,6 @@ * . * #L% */ - import com.condation.cms.api.Constants; import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.DB; @@ -56,7 +55,7 @@ public class NodeListFunctionBuilder extends AbstractCurrentNodeFunction { String sort = "title"; boolean reverse = false; - + String contentType = Constants.DEFAULT_CONTENT_TYPE; final NodeListFunction nodeListFunction; @@ -95,37 +94,37 @@ public NodeListFunctionBuilder contentType(String contentType) { this.contentType = contentType; return this; } - + public NodeListFunctionBuilder json() { this.contentType = "application/json"; return this; } - + public NodeListFunctionBuilder excerpt(long length) { - this.excerptLength = (int)length; + this.excerptLength = (int) length; return this; } - + public NodeListFunctionBuilder page(long page) { - this.page = (int)page; + this.page = (int) page; return this; } - + public NodeListFunctionBuilder page(String page) { this.page = Integer.parseInt(page.trim()); return this; } public NodeListFunctionBuilder size(long size) { - this.size = (int)size; + this.size = (int) size; return this; } - + public NodeListFunctionBuilder size(String size) { this.size = Integer.parseInt(size.trim()); return this; } - + public NodeListFunctionBuilder index(boolean index) { this.index = index; return this; @@ -156,26 +155,33 @@ public Page list() { } private Comparator getComparator() { - if (sort == null || "name".equals("sort")) { + if (sort == null || "name".equals(sort)) { return nameComparator; } else { - - return Comparator.comparing(new Function() { - @Override - public Object apply(ContentNode node) { - return node.data().get(sort); - } - }, Comparator.nullsLast((key1, key2) -> { - if (Objects.equals(key1, key2)) { + // Comparator für die extrahierten Werte + Comparator valueComparator = Comparator.nullsLast((Object o1, Object o2) -> { + if (Objects.equals(o1, o2)) { return 0; } - if (key1 == null && key2 != null) { + // null-Handling + if (o1 == null) { return -1; - } else if (key1 != null && key2 == null) { + } + if (o2 == null) { return 1; } - return ((Comparable)key1).compareTo((Comparable)key2); - }) + // falls beides Strings, case‐insensitive vergleichen + if (o1 instanceof String && o2 instanceof String) { + return ((String) o1).compareToIgnoreCase((String) o2); + } + // sonst normaler Comparable‐Vergleich + return ((Comparable) o1).compareTo(o2); + }); + + // Comparator auf den ContentNode anwenden + return Comparator.comparing( + (ContentNode node) -> node.data().get(sort), + valueComparator ); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/template/functions/query/QueryFunction.java b/cms-content/src/main/java/com/condation/cms/content/template/functions/query/QueryFunction.java index 2935f7a53..e1aa35677 100644 --- a/cms-content/src/main/java/com/condation/cms/content/template/functions/query/QueryFunction.java +++ b/cms-content/src/main/java/com/condation/cms/content/template/functions/query/QueryFunction.java @@ -116,6 +116,6 @@ public String toUrl(String uri) { uri = uri.substring(0, uri.length() - 1); } - return uri + (isPreview() ? "?preview" : ""); + return uri + (isPreview() ? "?preview=" + getPreviewMode() : ""); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/template/functions/shortcode/ShortCodeTemplateFunction.java b/cms-content/src/main/java/com/condation/cms/content/template/functions/tag/TagTemplateFunction.java similarity index 75% rename from cms-content/src/main/java/com/condation/cms/content/template/functions/shortcode/ShortCodeTemplateFunction.java rename to cms-content/src/main/java/com/condation/cms/content/template/functions/tag/TagTemplateFunction.java index 76a356893..127cd4189 100644 --- a/cms-content/src/main/java/com/condation/cms/content/template/functions/shortcode/ShortCodeTemplateFunction.java +++ b/cms-content/src/main/java/com/condation/cms/content/template/functions/tag/TagTemplateFunction.java @@ -1,4 +1,4 @@ -package com.condation.cms.content.template.functions.shortcode; +package com.condation.cms.content.template.functions.tag; /*- * #%L @@ -24,7 +24,7 @@ import com.condation.cms.api.request.RequestContext; -import com.condation.cms.content.shortcodes.ShortCodes; +import com.condation.cms.content.tags.Tags; import java.util.Map; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -34,20 +34,20 @@ * @author t.marx */ @RequiredArgsConstructor -public class ShortCodeTemplateFunction { +public class TagTemplateFunction { - public static final String KEY = "shortCodes"; + public static final String KEY = "tags"; private final RequestContext requestContext; @Getter - private final ShortCodes shortCodes; + private final Tags tags; public String call (String name, Map parameters) { - return shortCodes.execute(name, parameters, requestContext); + return tags.execute(name, parameters, requestContext); } public String call (String name) { - return shortCodes.execute(name, Map.of(), requestContext); + return tags.execute(name, Map.of(), requestContext); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/template/functions/translation/NodeTranslations.java b/cms-content/src/main/java/com/condation/cms/content/template/functions/translation/NodeTranslations.java new file mode 100644 index 000000000..15e8ea847 --- /dev/null +++ b/cms-content/src/main/java/com/condation/cms/content/template/functions/translation/NodeTranslations.java @@ -0,0 +1,75 @@ +package com.condation.cms.content.template.functions.translation; + +/*- + * #%L + * cms-content + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.Constants; +import com.condation.cms.api.SiteProperties; +import com.condation.cms.api.db.ContentNode; +import com.condation.cms.api.utils.HTTPUtil; +import com.condation.cms.api.utils.PathUtil; +import com.condation.cms.core.serivce.ServiceRegistry; +import com.condation.cms.core.serivce.impl.SiteLinkService; +import com.condation.cms.core.serivce.impl.SitePropertiesService; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * + * @author thorstenmarx + */ +public class NodeTranslations { + + private final ContentNode node; + private final SiteProperties siteProperties; + + public NodeTranslations(ContentNode node, SiteProperties siteProperties) { + this.node = node; + this.siteProperties = siteProperties; + } + + public List translations () { + return siteProperties.translation().getMapping().stream().map(mapping -> { + var translations = (Map)node.data().getOrDefault(Constants.MetaFields.TRANSLATIONS, Collections.emptyMap()); + + var url = ""; + String locale = null; + if (translations.containsKey(mapping.language())) { + var linkService = ServiceRegistry.getInstance().get(mapping.site(), SiteLinkService.class).get(); + url = linkService.link((String)translations.get(mapping.language())); + + locale = ServiceRegistry.getInstance().get(mapping.site(), SitePropertiesService.class).get().siteProperties().locale().getCountry().toLowerCase(); + } else if (mapping.language().equals(siteProperties.language())) { + url = HTTPUtil.modifyUrl( + PathUtil.toURL(node.uri()), + siteProperties); + locale = siteProperties.locale().getCountry().toLowerCase(); + } + + return new TranslationDto(mapping.language(), locale, mapping.language().equals(siteProperties.language()), url); + }).toList(); + } + + public static record TranslationDto (String lang, String locale, boolean current, String url) { + } +} diff --git a/cms-content/src/main/java/com/condation/cms/content/template/functions/translation/SiteTranslations.java b/cms-content/src/main/java/com/condation/cms/content/template/functions/translation/SiteTranslations.java new file mode 100644 index 000000000..9511e60e3 --- /dev/null +++ b/cms-content/src/main/java/com/condation/cms/content/template/functions/translation/SiteTranslations.java @@ -0,0 +1,53 @@ +package com.condation.cms.content.template.functions.translation; + +/*- + * #%L + * cms-content + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.SiteProperties; +import java.util.List; + +/** + * + * @author thorstenmarx + */ +public class SiteTranslations { + + private final SiteProperties siteProperties; + + public SiteTranslations(SiteProperties siteProperties) { + this.siteProperties = siteProperties; + } + + public boolean enabled () { + return siteProperties.translation().isEnabled(); + } + + public List languages () { + return siteProperties.translation().getMapping().stream().map(mapping -> { + return new LanguageDto(mapping.language(), mapping.language().equals(siteProperties.language())); + }).toList(); + } + + public static record LanguageDto (String lang, boolean current) { + + } +} diff --git a/cms-content/src/test/java/com/condation/cms/content/ContentBaseTest.java b/cms-content/src/test/java/com/condation/cms/content/ContentBaseTest.java index a86307a41..c5db5203a 100644 --- a/cms-content/src/test/java/com/condation/cms/content/ContentBaseTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/ContentBaseTest.java @@ -22,8 +22,8 @@ * #L% */ -import com.condation.cms.content.shortcodes.ShortCodeParser; -import com.condation.cms.content.shortcodes.TagParser; +import com.condation.cms.content.tags.ShortCodeParser; +import com.condation.cms.content.tags.TagParser; import org.apache.commons.jexl3.JexlBuilder; /** diff --git a/cms-content/src/test/java/com/condation/cms/content/DefaultContentParserTest.java b/cms-content/src/test/java/com/condation/cms/content/DefaultContentParserTest.java index 90154ad55..e3e528933 100644 --- a/cms-content/src/test/java/com/condation/cms/content/DefaultContentParserTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/DefaultContentParserTest.java @@ -41,7 +41,7 @@ public class DefaultContentParserTest { ContentParser sut; @BeforeEach - private void setup () { + void setup () { sut = new DefaultContentParser(); } diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/FeaturesTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/FeaturesTest.java index 9770629c9..adf3d6c53 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/FeaturesTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/FeaturesTest.java @@ -26,6 +26,7 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; /** * @@ -38,6 +39,7 @@ public class FeaturesTest extends MarkdownTest { @BeforeAll public static void setup() { SUT = new CMSMarkdown(Options.all()); + } @RepeatedTest(1) @@ -89,14 +91,32 @@ public void test_tasklist() throws IOException { } @RepeatedTest(1) - public void test_shortcodes() throws IOException { + public void test_tags() throws IOException { - var md = load("features.shortcodes.md").trim(); - var expected = load("features.shortcodes.html"); + var md = load("features.tags.md").trim(); + var expected = load("features.tags.html"); expected = removeComments(expected); var result = SUT.render(md); result = "
" + result + "
"; Assertions.assertThat(result).isEqualToIgnoringWhitespace(expected); } + + + @Test + void tag_with_markdown () throws IOException { + var md = """ + [[hello]] + **bold text** + [[/hello]] + """; + var expected = """ + [[hello]] + bold text + [[/hello]] +

+ """.trim(); + var result = SUT.render(md).trim(); + Assertions.assertThat(result).isEqualToIgnoringWhitespace(expected); + } } diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/ShortCodeBlockRuleTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TagBlockRuleTest.java similarity index 84% rename from cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/ShortCodeBlockRuleTest.java rename to cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TagBlockRuleTest.java index facd66f74..5194e63a7 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/ShortCodeBlockRuleTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TagBlockRuleTest.java @@ -26,16 +26,15 @@ import com.condation.cms.content.markdown.Block; import java.util.Map; import org.assertj.core.api.Assertions; -import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; /** * * @author t.marx */ -public class ShortCodeBlockRuleTest { +public class TagBlockRuleTest { - private ShortCodeBlockRule sut = new ShortCodeBlockRule(); + private TagBlockRule sut = new TagBlockRule(); @Test void long_form() { @@ -46,9 +45,9 @@ void long_form() { Assertions.assertThat(next) .isNotNull() - .isInstanceOf(ShortCodeBlockRule.ShortCodeBlock.class); + .isInstanceOf(TagBlockRule.TagBlock.class); - var tag = (ShortCodeBlockRule.ShortCodeBlock)next; + var tag = (TagBlockRule.TagBlock)next; Assertions.assertThat(tag.tagInfo()) .hasFieldOrPropertyWithValue("name", "link") .hasFieldOrPropertyWithValue("rawAttributes", Map.of( @@ -68,9 +67,9 @@ void short_form() { Assertions.assertThat(next) .isNotNull() - .isInstanceOf(ShortCodeBlockRule.ShortCodeBlock.class); + .isInstanceOf(TagBlockRule.TagBlock.class); - var tag = (ShortCodeBlockRule.ShortCodeBlock)next; + var tag = (TagBlockRule.TagBlock)next; Assertions.assertThat(tag.tagInfo()) .hasFieldOrPropertyWithValue("name", "link") .hasFieldOrPropertyWithValue("rawAttributes", Map.of( @@ -88,10 +87,10 @@ void test_issue () { Assertions.assertThat(next) .isNotNull() - .isInstanceOf(ShortCodeBlockRule.ShortCodeBlock.class) + .isInstanceOf(TagBlockRule.TagBlock.class) ; - var tag = (ShortCodeBlockRule.ShortCodeBlock)next; + var tag = (TagBlockRule.TagBlock)next; Assertions.assertThat(tag.tagInfo()) .hasFieldOrPropertyWithValue("name", "video") .hasFieldOrPropertyWithValue("rawAttributes", Map.of( diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRuleTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRuleTest.java index 7e8fdc0dd..09b7a3a27 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRuleTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRuleTest.java @@ -39,14 +39,14 @@ public class ImageInlineRuleTest { public void test_image_rule() { var result = SUT.next("![TestBild!](/assets/images/test.jpg)"); Assertions.assertThat(result.render()) - .isEqualTo("\"TestBild!\""); + .isEqualToIgnoringWhitespace("\"TestBild!\""); } @Test public void test_image_rule_title() { var result = SUT.next("![TestBild!](/assets/images/test.jpg \"Test Bild\")"); Assertions.assertThat(result.render()) - .isEqualTo("\"TestBild!\""); + .isEqualToIgnoringWhitespace("\"TestBild!\""); } } diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/LinkInlineRuleTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/LinkInlineRuleTest.java index d95ec8037..257d53a24 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/LinkInlineRuleTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/LinkInlineRuleTest.java @@ -21,11 +21,11 @@ * . * #L% */ - import com.condation.cms.api.SiteProperties; import com.condation.cms.api.feature.features.SitePropertiesFeature; import com.condation.cms.api.request.RequestContext; -import com.condation.cms.api.request.ThreadLocalRequestContext; +import com.condation.cms.api.request.RequestContextScope; +import com.condation.cms.content.markdown.InlineBlock; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -44,7 +44,7 @@ public class LinkInlineRuleTest { @Mock SiteProperties siteProperties; - + @Test public void test_link() { @@ -75,20 +75,23 @@ public void test_relativ_linking() { @Test public void test_relativ_linking_with_context() { + Mockito.when(siteProperties.contextPath()).thenReturn("/de"); + + RequestContext requestContext = new RequestContext(); + requestContext.add(SitePropertiesFeature.class, new SitePropertiesFeature(siteProperties)); + + InlineBlock result = null; try { - - Mockito.when(siteProperties.contextPath()).thenReturn("/de"); - - RequestContext requestContext = new RequestContext(); - requestContext.add(SitePropertiesFeature.class, new SitePropertiesFeature(siteProperties)); - ThreadLocalRequestContext.REQUEST_CONTEXT.set(requestContext); - - var result = SUT.next("[relative link](../sibling/test)"); - - Assertions.assertThat(result.render()) - .isEqualTo("relative link"); - } finally { - ThreadLocalRequestContext.REQUEST_CONTEXT.remove(); + result = ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, requestContext).call(() -> { + return SUT.next("[relative link](../sibling/test)"); + }); + } catch (Exception ex) { + System.getLogger(LinkInlineRuleTest.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex); } + + Assertions.assertThat(result).isNotNull(); + Assertions.assertThat(result.render()) + .isEqualTo("relative link"); + } } diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/ShortCodeInlineBlockRuleTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/TagInlineBlockRuleTest.java similarity index 83% rename from cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/ShortCodeInlineBlockRuleTest.java rename to cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/TagInlineBlockRuleTest.java index 24bc99b31..58da60729 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/ShortCodeInlineBlockRuleTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/inline/TagInlineBlockRuleTest.java @@ -34,9 +34,9 @@ * * @author t.marx */ -public class ShortCodeInlineBlockRuleTest { +public class TagInlineBlockRuleTest { - private ShortCodeInlineBlockRule sut = new ShortCodeInlineBlockRule(); + private TagInlineBlockRule sut = new TagInlineBlockRule(); @Test void long_form() { @@ -47,9 +47,9 @@ void long_form() { Assertions.assertThat(next) .isNotNull() - .isInstanceOf(ShortCodeInlineBlockRule.ShortCodeInlineBlock.class); + .isInstanceOf(TagInlineBlockRule.TagInlineBlock.class); - var tag = (ShortCodeInlineBlockRule.ShortCodeInlineBlock)next; + var tag = (TagInlineBlockRule.TagInlineBlock)next; Assertions.assertThat(tag.tagInfo()) .hasFieldOrPropertyWithValue("name", "link") .hasFieldOrPropertyWithValue("rawAttributes", Map.of( @@ -69,9 +69,9 @@ void short_form() { Assertions.assertThat(next) .isNotNull() - .isInstanceOf(ShortCodeInlineBlockRule.ShortCodeInlineBlock.class); + .isInstanceOf(TagInlineBlockRule.TagInlineBlock.class); - var tag = (ShortCodeInlineBlockRule.ShortCodeInlineBlock)next; + var tag = (TagInlineBlockRule.TagInlineBlock)next; Assertions.assertThat(tag.tagInfo()) .hasFieldOrPropertyWithValue("name", "link") .hasFieldOrPropertyWithValue("rawAttributes", Map.of( diff --git a/cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodeParserReplaceTest.java b/cms-content/src/test/java/com/condation/cms/content/tags/ShortCodeParserReplaceTest.java similarity index 98% rename from cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodeParserReplaceTest.java rename to cms-content/src/test/java/com/condation/cms/content/tags/ShortCodeParserReplaceTest.java index 743b6955b..48f032013 100644 --- a/cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodeParserReplaceTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/tags/ShortCodeParserReplaceTest.java @@ -1,4 +1,4 @@ -package com.condation.cms.content.shortcodes; +package com.condation.cms.content.tags; /*- * #%L @@ -23,6 +23,7 @@ */ +import com.condation.cms.content.tags.ShortCodeParser; import com.condation.cms.content.ContentBaseTest; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; diff --git a/cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodeParserTest.java b/cms-content/src/test/java/com/condation/cms/content/tags/ShortCodeParserTest.java similarity index 97% rename from cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodeParserTest.java rename to cms-content/src/test/java/com/condation/cms/content/tags/ShortCodeParserTest.java index 30ea03983..9e664e069 100644 --- a/cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodeParserTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/tags/ShortCodeParserTest.java @@ -1,4 +1,4 @@ -package com.condation.cms.content.shortcodes; +package com.condation.cms.content.tags; /*- * #%L @@ -23,6 +23,7 @@ */ +import com.condation.cms.content.tags.ShortCodeParser; import com.condation.cms.content.ContentBaseTest; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; diff --git a/cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java b/cms-content/src/test/java/com/condation/cms/content/tags/TagParserTest.java similarity index 89% rename from cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java rename to cms-content/src/test/java/com/condation/cms/content/tags/TagParserTest.java index 642304e4a..16221d711 100644 --- a/cms-content/src/test/java/com/condation/cms/content/shortcodes/TagParserTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/tags/TagParserTest.java @@ -1,4 +1,4 @@ -package com.condation.cms.content.shortcodes; +package com.condation.cms.content.tags; /*- * #%L @@ -22,6 +22,8 @@ * #L% */ +import com.condation.cms.content.tags.TagMap; +import com.condation.cms.content.tags.TagParser; import com.condation.cms.api.request.RequestContext; import org.apache.commons.jexl3.JexlBuilder; import org.assertj.core.api.Assertions; @@ -47,7 +49,7 @@ void setup() { tagMap = new TagMap(); tagMap.put("code", params -> { // Verarbeitung der Parameter hier - return "Ausgabe des Shortcodes"; + return "Ausgabe des Tags"; }); tagMap.put("content", params -> { return (String)params.get("_content"); @@ -76,27 +78,27 @@ void setup() { } @Test - public void no_shortcode() { - String result = tagParser.parse("Dein Shortcode-Text hier", tagMap, requestContext); - Assertions.assertThat(result).isEqualTo("Dein Shortcode-Text hier"); + public void no_tag() { + String result = tagParser.parse("Dein Tag-Text hier", tagMap, requestContext); + Assertions.assertThat(result).isEqualTo("Dein Tag-Text hier"); } @Test public void self_closing_tag() { String result = tagParser.parse("[[code/]]", tagMap, requestContext); - Assertions.assertThat(result).isEqualTo("Ausgabe des Shortcodes"); + Assertions.assertThat(result).isEqualTo("Ausgabe des Tags"); } @Test public void self_closing_tag_with_space() { String result = tagParser.parse("[[code /]]", tagMap, requestContext); - Assertions.assertThat(result).isEqualTo("Ausgabe des Shortcodes"); + Assertions.assertThat(result).isEqualTo("Ausgabe des Tags"); } @Test public void end_closing_tag() { String result = tagParser.parse("[[code]][[/code]]", tagMap, requestContext); - Assertions.assertThat(result).isEqualTo("Ausgabe des Shortcodes"); + Assertions.assertThat(result).isEqualTo("Ausgabe des Tags"); } @Test @@ -142,7 +144,7 @@ public void parameters_with_content() { } @Test - public void shortCode_in_text() { + public void tag_in_text() { String result = tagParser.parse("Hello [[content]]CondationCMS[[/content]]!", tagMap, requestContext); Assertions.assertThat(result).isEqualTo("Hello CondationCMS!"); } @@ -160,13 +162,13 @@ public void namespace() { public void multiline () { String content = """ [[content]] - This is a multiline shortcode! + This is a multiline tag! [[/content]] """; String result = tagParser.parse(content, tagMap, requestContext); - Assertions.assertThat(result).isEqualToIgnoringWhitespace("This is a multiline shortcode!"); + Assertions.assertThat(result).isEqualToIgnoringWhitespace("This is a multiline tag!"); } @Test diff --git a/cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodesTest.java b/cms-content/src/test/java/com/condation/cms/content/tags/TagsTest.java similarity index 73% rename from cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodesTest.java rename to cms-content/src/test/java/com/condation/cms/content/tags/TagsTest.java index 618577a5a..dd11eaa2f 100644 --- a/cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodesTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/tags/TagsTest.java @@ -1,4 +1,4 @@ -package com.condation.cms.content.shortcodes; +package com.condation.cms.content.tags; /*- * #%L @@ -23,7 +23,7 @@ */ -import com.condation.cms.api.annotations.ShortCode; +import com.condation.cms.content.tags.Tags; import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; import com.condation.cms.content.ContentBaseTest; @@ -33,18 +33,19 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.condation.cms.api.annotations.Tag; /** * * @author t.marx */ -public class ShortCodesTest extends ContentBaseTest { +public class TagsTest extends ContentBaseTest { - static ShortCodes shortCodes; + static Tags tags; @BeforeEach public void init () { - var builder = ShortCodes.builder(getTagParser()); + var builder = Tags.builder(getTagParser()); builder.register( "youtube", @@ -82,24 +83,24 @@ public void init () { } ); - builder.register(new ShortCodesHandler()); + builder.register(new TagHandler()); - shortCodes = builder.build(); + tags = builder.build(); } @Test void simpleTest () { - var result = shortCodes.replace("[[youtube /]]"); + var result = tags.replace("[[youtube /]]"); Assertions.assertThat(result).isEqualTo(""); - result = shortCodes.replace("[[youtube/]]"); + result = tags.replace("[[youtube/]]"); Assertions.assertThat(result).isEqualTo(""); } @Test void simple_with_text_before_and_After () { - var result = shortCodes.replace("before [[youtube /]] after"); + var result = tags.replace("before [[youtube /]] after"); Assertions.assertThat(result).isEqualTo("before after"); } @@ -114,7 +115,7 @@ void complexTest () { some text after """; - var result = shortCodes.replace(content); + var result = tags.replace(content); var expected = """ some text before @@ -129,32 +130,32 @@ void complexTest () { @Test void unknown_tag () { - var result = shortCodes.replace("before [[vimeo id='TEST' /]] after"); + var result = tags.replace("before [[vimeo id='TEST' /]] after"); Assertions.assertThat(result).isEqualToIgnoringWhitespace("before [[vimeo id='TEST' /]] after"); } @Test void hello_from () { - var result = shortCodes.replace("[[hello_from name=\"Thorsten\" from=\"Bochum\" /]]"); + var result = tags.replace("[[hello_from name=\"Thorsten\" from=\"Bochum\" /]]"); Assertions.assertThat(result).isEqualTo("

Thorsten

from Bochum

"); - result = shortCodes.replace("[[hello_from name='Thorsten' from='Bochum' /]]"); + result = tags.replace("[[hello_from name='Thorsten' from='Bochum' /]]"); Assertions.assertThat(result).isEqualTo("

Thorsten

from Bochum

"); - result = shortCodes.replace("[[hello_from name='Thorsten' from='Bochum' /]]"); + result = tags.replace("[[hello_from name='Thorsten' from='Bochum' /]]"); Assertions.assertThat(result).isEqualTo("

Thorsten

from Bochum

"); } @Test void test_long () { - var result = shortCodes.replace("[[mark]]Important[[/mark]]"); + var result = tags.replace("[[mark]]Important[[/mark]]"); Assertions.assertThat(result).isEqualTo("Important"); } @Test void test_long_with_params () { - var result = shortCodes.replace("[[mark2 class='test-class']]Important[[/mark2]]"); + var result = tags.replace("[[mark2 class='test-class']]Important[[/mark2]]"); Assertions.assertThat(result).isEqualTo("Important"); } @@ -170,7 +171,7 @@ void long_complex () { some text after """; - var result = shortCodes.replace(content); + var result = tags.replace(content); var expected = """ some text before @@ -191,7 +192,7 @@ void multiple_hello () { var expected = """

Thorsten

from Bochum

Thorsten

from Bochum

"""; - var result = shortCodes.replace(input); + var result = tags.replace(input); Assertions.assertThat(result).isEqualTo(expected); input = """ @@ -200,20 +201,20 @@ void multiple_hello () { expected = """

Thorsten

from Bochum

Thorsten

from Bochum

"""; - result = shortCodes.replace(input); + result = tags.replace(input); Assertions.assertThat(result).isEqualTo(expected); } @Test void test_mismach() { - var result = shortCodes.replace("[[mark1 class='test-class']]Important[[/mark2]]"); + var result = tags.replace("[[mark1 class='test-class']]Important[[/mark2]]"); Assertions.assertThat(result).isEqualTo("[[mark1 class='test-class']]Important[[/mark2]]"); } @Test void test_expression() { - var result = shortCodes.replace("[[exp expression='${meta.title}' /]]", + var result = tags.replace("[[exp expression='${meta.title}' /]]", Map.of( "meta", Map.of("title", "CondationCMS") ) @@ -227,9 +228,9 @@ void test_variables() { RequestContext requestContext = new RequestContext(); - shortCodes.replace("[[set_var /]]", Map.of(), requestContext); + tags.replace("[[set_var /]]", Map.of(), requestContext); - var result = shortCodes.replace("[[get_var /]]", Map.of(), requestContext); + var result = tags.replace("[[get_var /]]", Map.of(), requestContext); Assertions.assertThat(result).isEqualTo("Hello world!"); } @@ -238,13 +239,33 @@ void test_variables() { void test_handler () { RequestContext requestContext = new RequestContext(); - var result = shortCodes.replace("[[printHello name='CondationCMS' /]]", Map.of(), requestContext); + var result = tags.replace("[[printHello name='CondationCMS' /]]", Map.of(), requestContext); Assertions.assertThat(result).isEqualTo("hello CondationCMS"); } - public static class ShortCodesHandler { - @ShortCode("printHello") + @Test + void test_multiline () { + var template = """ + [[hello_from + name=\"Thorsten\" + from=\"Bochum\" /]] + """; + var result = tags.replace(template); + Assertions.assertThat(result).isEqualToIgnoringWhitespace("

Thorsten

from Bochum

"); + + template = """ + [[hello_from + name=\"Thorsten\" + from=\"Bochum\"]] + [[/hello_from]] + """; + result = tags.replace(template); + Assertions.assertThat(result).isEqualToIgnoringWhitespace("

Thorsten

from Bochum

"); + } + + public static class TagHandler { + @Tag("printHello") public String printHello (Parameter parameter) { return "hello " + parameter.getOrDefault("name", ""); } diff --git a/cms-content/src/test/java/com/condation/cms/content/template/navigation/NavigationFunctionITest.java b/cms-content/src/test/java/com/condation/cms/content/template/navigation/NavigationFunctionITest.java index ba9a25689..a48092df0 100644 --- a/cms-content/src/test/java/com/condation/cms/content/template/navigation/NavigationFunctionITest.java +++ b/cms-content/src/test/java/com/condation/cms/content/template/navigation/NavigationFunctionITest.java @@ -21,7 +21,6 @@ * . * #L% */ - import com.condation.cms.api.Constants; import com.condation.cms.api.SiteProperties; import com.condation.cms.api.configuration.Configuration; @@ -38,12 +37,13 @@ import com.condation.cms.api.hooks.HookSystem; import com.condation.cms.api.mapper.ContentNodeMapper; import com.condation.cms.api.request.RequestContext; -import com.condation.cms.api.request.ThreadLocalRequestContext; +import com.condation.cms.api.request.RequestContextScope; import com.condation.cms.content.DefaultContentParser; import com.condation.cms.content.markdown.module.CMSMarkdownRenderer; import com.condation.cms.content.template.functions.navigation.NavigationFunction; import com.condation.cms.core.eventbus.DefaultEventBus; import com.condation.cms.filesystem.FileDB; +import com.google.inject.Injector; import java.nio.file.Path; import org.assertj.core.api.Assertions; import org.eclipse.jetty.server.Request; @@ -63,19 +63,19 @@ */ @ExtendWith(MockitoExtension.class) public class NavigationFunctionITest { + private static FileDB db; @Mock private Request request; - + private RequestContext requestContext; - + private static ContentParser contentParser = new DefaultContentParser(); - + @BeforeAll public static void setup() throws Exception { - var hostBase = Path.of("src/test/resources/site"); var config = new Configuration(); db = new FileDB(hostBase, new DefaultEventBus(), (file) -> { @@ -88,51 +88,45 @@ public static void setup() throws Exception { }, config); db.init(); } - + @BeforeEach - public void setupRequestContext () { + public void setupRequestContext() { requestContext = new RequestContext(); var siteProperties = Mockito.mock(SiteProperties.class); Mockito.when(siteProperties.contextPath()).thenReturn("/"); Mockito.when(siteProperties.defaultContentType()).thenReturn(Constants.ContentTypes.JSON); var siteConfiguration = new SiteConfiguration(siteProperties); - + var configuration = new Configuration(); configuration.add(SiteConfiguration.class, siteConfiguration); ConfigurationFeature configFeature = new ConfigurationFeature(configuration); - + requestContext.add(ConfigurationFeature.class, configFeature); requestContext.add(SitePropertiesFeature.class, new SitePropertiesFeature(siteProperties)); requestContext.add(ContentParserFeature.class, new ContentParserFeature(contentParser)); - requestContext.add(ContentNodeMapperFeature.class, new ContentNodeMapperFeature(new ContentNodeMapper(db, contentParser))); - requestContext.add(MarkdownRendererFeature.class, new MarkdownRendererFeature(new CMSMarkdownRenderer())); + requestContext.add(ContentNodeMapperFeature.class, new ContentNodeMapperFeature(new ContentNodeMapper(db, contentParser))); + requestContext.add(MarkdownRendererFeature.class, new MarkdownRendererFeature(new CMSMarkdownRenderer())); requestContext.add(HookSystemFeature.class, new HookSystemFeature(new HookSystem())); - - + Mockito.lenient().when(request.getAttribute("_requestContext")).thenReturn(requestContext); - - ThreadLocalRequestContext.REQUEST_CONTEXT.set(requestContext); } - - @AfterEach - public void clearRequestContext () { - ThreadLocalRequestContext.REQUEST_CONTEXT.remove(); - } - + @AfterAll - public static void shutdown () throws Exception { + public static void shutdown() throws Exception { db.close(); } - + @Test - void test_root () { - var currentNode = db.getReadOnlyFileSystem().contentBase(); - NavigationFunction fn = new NavigationFunction(db, currentNode, requestContext); - - var nodes = fn.json().list("."); - - Assertions.assertThat(nodes) - .isNotEmpty() - .hasSize(3); + void test_root() { + ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, requestContext).run(() -> { + var currentNode = db.getReadOnlyFileSystem().contentBase(); + NavigationFunction fn = new NavigationFunction(db, currentNode, requestContext); + + var nodes = fn.json().list("."); + + Assertions.assertThat(nodes) + .isNotEmpty() + .hasSize(3); + }); } } diff --git a/cms-content/src/test/java/com/condation/cms/content/template/shortcode/FreeMarkerShortCodeTemplateFunctionTest.java b/cms-content/src/test/java/com/condation/cms/content/template/tag/FreeMarkerTagTemplateFunctionTest.java similarity index 77% rename from cms-content/src/test/java/com/condation/cms/content/template/shortcode/FreeMarkerShortCodeTemplateFunctionTest.java rename to cms-content/src/test/java/com/condation/cms/content/template/tag/FreeMarkerTagTemplateFunctionTest.java index 14d3bf03f..2af071f68 100644 --- a/cms-content/src/test/java/com/condation/cms/content/template/shortcode/FreeMarkerShortCodeTemplateFunctionTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/template/tag/FreeMarkerTagTemplateFunctionTest.java @@ -1,4 +1,4 @@ -package com.condation.cms.content.template.shortcode; +package com.condation.cms.content.template.tag; /*- * #%L @@ -24,8 +24,8 @@ import com.condation.cms.content.ContentBaseTest; -import com.condation.cms.content.template.functions.shortcode.ShortCodeTemplateFunction; -import com.condation.cms.content.shortcodes.ShortCodes; +import com.condation.cms.content.template.functions.tag.TagTemplateFunction; +import com.condation.cms.content.tags.Tags; import freemarker.template.Configuration; import freemarker.template.Template; import java.io.StringReader; @@ -42,11 +42,11 @@ * * @author t.marx */ -public class FreeMarkerShortCodeTemplateFunctionTest extends ContentBaseTest { +public class FreeMarkerTagTemplateFunctionTest extends ContentBaseTest { static Configuration cfg; - ShortCodes shortCodes; + Tags tags; @BeforeAll public static void setup() { @@ -55,8 +55,8 @@ public static void setup() { } @BeforeEach - public void setupShortCodes() { - shortCodes = new ShortCodes(Map.of( + public void setupTags() { + tags = new Tags(Map.of( "echo", (params) -> "Hello world", "greet", (params) -> "Hello " + params.get("name") ), getTagParser()); @@ -64,11 +64,11 @@ public void setupShortCodes() { @Test public void testSomeMethod() throws Exception { - String templateString = "${shortCode.call('echo')}"; + String templateString = "${tag.call('echo')}"; Template template = new Template("templateName", new StringReader(templateString), cfg); Map model = new HashMap<>(); - model.put("shortCode", new ShortCodeTemplateFunction(null, shortCodes)); + model.put("tag", new TagTemplateFunction(null, tags)); Writer out = new StringWriter(); template.process(model, out); @@ -78,11 +78,11 @@ public void testSomeMethod() throws Exception { @Test public void test_greet() throws Exception { - String templateString = "${shortCode.call('greet', {'name':'CondationCMS'})}"; + String templateString = "${tag.call('greet', {'name':'CondationCMS'})}"; Template template = new Template("templateName", new StringReader(templateString), cfg); Map model = new HashMap<>(); - model.put("shortCode", new ShortCodeTemplateFunction(null, shortCodes)); + model.put("tag", new TagTemplateFunction(null, tags)); Writer out = new StringWriter(); template.process(model, out); diff --git a/cms-content/src/test/java/com/condation/cms/content/template/shortcode/ThymeleafShortCodeTemplateFunctionTest.java b/cms-content/src/test/java/com/condation/cms/content/template/tag/ThymeleafTagTemplateFunctionTest.java similarity index 75% rename from cms-content/src/test/java/com/condation/cms/content/template/shortcode/ThymeleafShortCodeTemplateFunctionTest.java rename to cms-content/src/test/java/com/condation/cms/content/template/tag/ThymeleafTagTemplateFunctionTest.java index bda879c8a..c20e30669 100644 --- a/cms-content/src/test/java/com/condation/cms/content/template/shortcode/ThymeleafShortCodeTemplateFunctionTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/template/tag/ThymeleafTagTemplateFunctionTest.java @@ -1,4 +1,4 @@ -package com.condation.cms.content.template.shortcode; +package com.condation.cms.content.template.tag; /*- * #%L @@ -24,8 +24,8 @@ import com.condation.cms.content.ContentBaseTest; -import com.condation.cms.content.template.functions.shortcode.ShortCodeTemplateFunction; -import com.condation.cms.content.shortcodes.ShortCodes; +import com.condation.cms.content.template.functions.tag.TagTemplateFunction; +import com.condation.cms.content.tags.Tags; import java.util.Map; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -39,9 +39,9 @@ * * @author t.marx */ -public class ThymeleafShortCodeTemplateFunctionTest extends ContentBaseTest { +public class ThymeleafTagTemplateFunctionTest extends ContentBaseTest { - ShortCodes shortCodes; + Tags tags; static TemplateEngine templateEngine; @@ -56,8 +56,8 @@ public static void setup() { } @BeforeEach - public void setupShortCodes() { - shortCodes = new ShortCodes(Map.of( + public void setupTags() { + tags = new Tags(Map.of( "echo", (params) -> "Hello world", "greet", (params) -> "Hello " + params.get("name") ), getTagParser()); @@ -65,20 +65,20 @@ public void setupShortCodes() { @Test public void testSomeMethod() throws Exception { - String templateString = "[(${shortCode.call('echo')})]"; + String templateString = "[(${tag.call('echo')})]"; Context context = new Context(); - context.setVariable("shortCode", new ShortCodeTemplateFunction(null, shortCodes)); + context.setVariable("tag", new TagTemplateFunction(null, tags)); String renderedString = templateEngine.process(templateString, context); Assertions.assertThat(renderedString).isEqualTo("Hello world"); } @Test public void test_greet() throws Exception { - String templateString = "[(${shortCode.call('greet', #{'name': 'CondationCMS'})})]"; + String templateString = "[(${tag.call('greet', #{'name': 'CondationCMS'})})]"; Context context = new Context(); - context.setVariable("shortCode", new ShortCodeTemplateFunction(null, shortCodes)); + context.setVariable("tag", new TagTemplateFunction(null, tags)); String renderedString = templateEngine.process(templateString, context); Assertions.assertThat(renderedString).isEqualTo("Hello CondationCMS"); } diff --git a/cms-content/src/test/resources/com/condation/cms/content/markdown/features.html b/cms-content/src/test/resources/com/condation/cms/content/markdown/features.html index 052b976ff..de0017f30 100644 --- a/cms-content/src/test/resources/com/condation/cms/content/markdown/features.html +++ b/cms-content/src/test/resources/com/condation/cms/content/markdown/features.html @@ -53,7 +53,7 @@

an ordered list

a block quote

-

TestBild!

+

TestBild!

some heading

and paragraph without empty line before

diff --git a/cms-content/src/test/resources/com/condation/cms/content/markdown/features.shortcodes.html b/cms-content/src/test/resources/com/condation/cms/content/markdown/features.tags.html similarity index 100% rename from cms-content/src/test/resources/com/condation/cms/content/markdown/features.shortcodes.html rename to cms-content/src/test/resources/com/condation/cms/content/markdown/features.tags.html diff --git a/cms-content/src/test/resources/com/condation/cms/content/markdown/features.shortcodes.md b/cms-content/src/test/resources/com/condation/cms/content/markdown/features.tags.md similarity index 100% rename from cms-content/src/test/resources/com/condation/cms/content/markdown/features.shortcodes.md rename to cms-content/src/test/resources/com/condation/cms/content/markdown/features.tags.md diff --git a/cms-content/src/test/resources/site/content/child.md b/cms-content/src/test/resources/site/content/child.md index af59a4180..62f07d8e6 100644 --- a/cms-content/src/test/resources/site/content/child.md +++ b/cms-content/src/test/resources/site/content/child.md @@ -1,5 +1,6 @@ --- title: the child +published: true --- some content \ No newline at end of file diff --git a/cms-content/src/test/resources/site/content/index.md b/cms-content/src/test/resources/site/content/index.md index 330e16737..e0c03b88c 100644 --- a/cms-content/src/test/resources/site/content/index.md +++ b/cms-content/src/test/resources/site/content/index.md @@ -1,5 +1,6 @@ --- title: the title +published: true --- some content \ No newline at end of file diff --git a/cms-content/src/test/resources/site/content/sub/child/another.md b/cms-content/src/test/resources/site/content/sub/child/another.md index c26609379..408e65b97 100644 --- a/cms-content/src/test/resources/site/content/sub/child/another.md +++ b/cms-content/src/test/resources/site/content/sub/child/another.md @@ -1,5 +1,6 @@ --- title: another +published: true --- some content \ No newline at end of file diff --git a/cms-content/src/test/resources/site/content/sub/child/index.md b/cms-content/src/test/resources/site/content/sub/child/index.md index af59a4180..62f07d8e6 100644 --- a/cms-content/src/test/resources/site/content/sub/child/index.md +++ b/cms-content/src/test/resources/site/content/sub/child/index.md @@ -1,5 +1,6 @@ --- title: the child +published: true --- some content \ No newline at end of file diff --git a/cms-core/pom.xml b/cms-core/pom.xml index bf2f0046c..d27c42eb7 100644 --- a/cms-core/pom.xml +++ b/cms-core/pom.xml @@ -4,7 +4,7 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 cms-core jar @@ -19,8 +19,14 @@ gson - org.tomlj - tomlj + io.github.wasabithumb + jtoml + 1.3.0 + + + io.github.wasabithumb + jtoml-serializer-gson + 1.3.0 org.projectlombok diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/GSONProvider.java b/cms-core/src/main/java/com/condation/cms/core/configuration/GSONProvider.java index f11b9b93f..20a0f8dfe 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/GSONProvider.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/GSONProvider.java @@ -33,7 +33,7 @@ * * @author t.marx */ -public class GSONProvider { + public class GSONProvider { public static final Gson GSON = new GsonBuilder() .enableComplexMapKeySerialization() diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/configs/MediaConfiguration.java b/cms-core/src/main/java/com/condation/cms/core/configuration/configs/MediaConfiguration.java index 1118a6b41..e6ece87f0 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/configs/MediaConfiguration.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/configs/MediaConfiguration.java @@ -21,7 +21,6 @@ * . * #L% */ - import com.condation.cms.api.eventbus.EventBus; import com.condation.cms.core.configuration.ConfigSource; import com.condation.cms.core.configuration.IConfiguration; @@ -30,8 +29,14 @@ import com.condation.cms.api.eventbus.events.ConfigurationReloadEvent; import com.condation.cms.api.media.MediaFormat; import com.condation.cms.api.media.MediaUtils; +import com.google.common.hash.HashCode; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.UUID; import lombok.Data; import lombok.NoArgsConstructor; @@ -55,7 +60,7 @@ public MediaConfiguration(Builder builder) { this.eventBus = builder.eventBus; this.id = builder.id; reloadStrategy.register(this); - + reload(); } @@ -63,87 +68,117 @@ public MediaConfiguration(Builder builder) { public List getSources() { return sources; } - + @Override - public String id () { + public String id() { return id; } @Override - public void reload () { + public void reload() { sources.forEach(source -> { if (source.reload()) { - eventBus.publish(new ConfigurationReloadEvent(id)); + eventBus.publish(new ConfigurationReloadEvent(id)); } }); } - - - public List getFormats () { - return getList("formats", Format.class); + + public List getFormats() { + var sorted = new TreeSet((o1, o2) -> o1.name.compareTo(o2.name)); + sorted.addAll(getList("formats", Format.class)); + return new ArrayList<>(sorted); } - - public List getMediaFormats () { + + public List getMediaFormats() { return getFormats().stream().map(format -> { - return new MediaFormat(format.name, format.width, format.height, toFormat(format.format), format.compression); + return new MediaFormat(format.name, format.width, format.height, toFormat(format.format), format.compression, format.cropped); }).toList(); } - - private MediaUtils.Format toFormat (String format) { + + private MediaUtils.Format toFormat(String format) { return switch (format) { - case "png" -> MediaUtils.Format.PNG; - case "webp" -> MediaUtils.Format.WEBP; - case "jpeg" -> MediaUtils.Format.JPEG; - default -> throw new AssertionError(); + case "png" -> + MediaUtils.Format.PNG; + case "webp" -> + MediaUtils.Format.WEBP; + case "jpeg" -> + MediaUtils.Format.JPEG; + default -> + throw new AssertionError(); }; } - - public static MediaConfiguration.Builder builder (EventBus eventBus) { + + public static MediaConfiguration.Builder builder(EventBus eventBus) { return new MediaConfiguration.Builder(eventBus); } - + public static class Builder { + private final List sources = new ArrayList<>(); private ReloadStrategy reloadStrategy = new NoReload(); private String id = UUID.randomUUID().toString(); private final EventBus eventBus; - - public Builder (EventBus eventbus) { + + public Builder(EventBus eventbus) { this.eventBus = eventbus; } - - public Builder id (String uniqueId) { + + public Builder id(String uniqueId) { this.id = uniqueId; return this; } - + public Builder addSource(ConfigSource source) { sources.add(source); return this; } - - public Builder addAllSources (List sources) { + + public Builder addAllSources(List sources) { this.sources.addAll(sources); return this; } - - public Builder reloadStrategy (ReloadStrategy reload) { + + public Builder reloadStrategy(ReloadStrategy reload) { this.reloadStrategy = reload; return this; } - - public MediaConfiguration build () { + + public MediaConfiguration build() { return new MediaConfiguration(this); } } - + @Data @NoArgsConstructor public static class Format { + private String name; private String format; private boolean compression; private int width; private int height; + private boolean cropped; + + @Override + public final int hashCode() { + int hash = 7; + hash = 31 * hash + (name == null ? 0 : name.hashCode()); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Format other = (Format) obj; + return Objects.equals(this.name, other.name); + } } } diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/configs/SimpleConfiguration.java b/cms-core/src/main/java/com/condation/cms/core/configuration/configs/SimpleConfiguration.java index 7f449c9c6..08058158a 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/configs/SimpleConfiguration.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/configs/SimpleConfiguration.java @@ -31,7 +31,6 @@ import com.condation.cms.core.configuration.reload.NoReload; import com.condation.cms.api.eventbus.events.ConfigurationReloadEvent; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedServerProperties.java b/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedServerProperties.java index 45abb68c1..0719eca9b 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedServerProperties.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedServerProperties.java @@ -32,6 +32,7 @@ import java.nio.file.Path; import java.util.Collections; import java.util.List; +import java.util.Optional; /** * @@ -59,7 +60,7 @@ public IPCProperties ipc() { public APMProperties apm() { return configuration.get("apm", ExtendedAPMProperties.class); } - + @Override public Path getThemesFolder() { return ServerUtil.getPath(Constants.Folders.THEMES); @@ -110,6 +111,18 @@ public List extensionRepositories() { return (List) configuration.getOrDefault("urls.extensions", Collections.emptyList()); } + @Override + public String secret() { + return Optional.ofNullable(System.getenv("CMS_UI_SECRET")) + .filter(s -> !s.isEmpty()) + .orElse(configuration.getString("ui.secret")); + } + + @Override + public List activeModules() { + return configuration.getList("modules.active", String.class); + } + public static record Server (int port, String ip) { } diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedSiteProperties.java b/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedSiteProperties.java index 742c96a3b..146d91d56 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedSiteProperties.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedSiteProperties.java @@ -21,21 +21,23 @@ * . * #L% */ - import com.condation.cms.api.Constants; import com.condation.cms.api.SiteProperties; +import com.condation.cms.api.TranslationProperties; +import com.condation.cms.api.UIProperties; import com.condation.cms.core.configuration.configs.SimpleConfiguration; import java.util.List; import java.util.Locale; +import org.eclipse.jetty.util.AtomicBiInteger; /** * * @author t.marx */ public class ExtendedSiteProperties implements SiteProperties { - + private final SimpleConfiguration configuration; - + public ExtendedSiteProperties(SimpleConfiguration configuration) { this.configuration = configuration; } @@ -44,16 +46,16 @@ public ExtendedSiteProperties(SimpleConfiguration configuration) { public List hostnames() { // "localhost" var hostname = configuration.getValue("hostname", String.class); - - if (hostname !=null ) { + + if (hostname != null) { return List.of(hostname); } - + var hostnames = configuration.getList("hostname", String.class); if (hostnames != null && !hostnames.isEmpty()) { return hostnames; } - + return List.of("localhost"); } @@ -67,6 +69,11 @@ public String contextPath() { return configuration.getString("context_path", "/"); } + @Override + public String baseUrl() { + return configuration.getString("baseurl", ""); + } + @Override public String id() { return configuration.getString("id", "default-site"); @@ -79,20 +86,31 @@ public String theme() { @Override public String queryIndexMode() { - return configuration.getString("index.query.mode", "MEMORY"); + return configuration.getString("index.query.mode", "PERSISTENT"); } @Override public Locale locale() { - if (configuration.get("language") != null) { - Locale.forLanguageTag((String)configuration.getString("language")); + if (configuration.get("locale") != null) { + var value = configuration.get("locale"); + if (value instanceof Locale locale) { + return locale; + } else if (value instanceof String s) { + // Erlaubt z.B. "de_DE" oder "en-US" + String[] parts = s.split("[-_]"); + if (parts.length == 1) { + return Locale.of(parts[0]); + } else if (parts.length == 2) { + return Locale.of(parts[0], parts[1]); + } + } } return Locale.getDefault(); } - + @Override public String language() { - return configuration.getString("language"); + return locale().getLanguage(); } @Override @@ -118,12 +136,12 @@ public String cacheEngine() { public boolean cacheContent() { return configuration.getBoolean("cache.content", Constants.DEFAULT_CONTENT_CACHE_ENABLED); } - + @Override public boolean spaEnabled() { return configuration.getBoolean("spa.enabled", false); } - + @Override public String templateEngine() { return configuration.getString("template.engine"); @@ -133,9 +151,9 @@ public String templateEngine() { public List activeModules() { return configuration.getList("modules.active", String.class); } - + @Override - public Object get (String field) { + public Object get(String field) { return configuration.get(field); } @@ -143,4 +161,15 @@ public Object get (String field) { public T getOrDefault(String field, T defaultValue) { return (T) configuration.getOrDefault(field, defaultValue); } + + @Override + public UIProperties ui() { + return configuration.get("ui", ExtendedUIProperties.class); + } + + @Override + public TranslationProperties translation() { + return configuration.get("translation", ExtendedTranslationProperties.class); + } + } diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedTranslationProperties.java b/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedTranslationProperties.java new file mode 100644 index 000000000..4a14a5b6d --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedTranslationProperties.java @@ -0,0 +1,55 @@ +package com.condation.cms.core.configuration.properties; + +/*- + * #%L + * cms-core + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.TranslationProperties; +import java.util.List; + +/** + * + * @author t.marx + */ +public class ExtendedTranslationProperties implements TranslationProperties { + + + private boolean enabled = false; + + private List mapping; + + private List languages; + + @Override + public List getLanguages() { + return languages; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public List getMapping() { + return mapping; + } +} diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedUIProperties.java b/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedUIProperties.java new file mode 100644 index 000000000..a36372242 --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/properties/ExtendedUIProperties.java @@ -0,0 +1,48 @@ +package com.condation.cms.core.configuration.properties; + +/*- + * #%L + * cms-core + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.UIProperties; +import java.util.Optional; + +/** + * + * @author t.marx + */ +public class ExtendedUIProperties implements UIProperties { + + + private boolean force2fa = false; + + private boolean managerEnabled = false; + + @Override + public boolean force2fa() { + return force2fa; + } + + @Override + public boolean managerEnabled() { + return managerEnabled; + } +} diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/source/TomlConfigSource.java b/cms-core/src/main/java/com/condation/cms/core/configuration/source/TomlConfigSource.java index bee21ad7d..45de19d9a 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/source/TomlConfigSource.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/source/TomlConfigSource.java @@ -25,6 +25,9 @@ import com.condation.cms.api.utils.MapUtil; import com.condation.cms.core.configuration.ConfigSource; import com.condation.cms.core.configuration.GSONProvider; +import com.google.gson.JsonObject; +import io.github.wasabithumb.jtoml.JToml; +import io.github.wasabithumb.jtoml.document.TomlDocument; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -34,9 +37,6 @@ import java.util.Map; import java.util.Set; import lombok.extern.slf4j.Slf4j; -import org.tomlj.Toml; -import org.tomlj.TomlPosition; -import org.tomlj.TomlTable; /** * @@ -45,15 +45,18 @@ @Slf4j public class TomlConfigSource implements ConfigSource { + private static JToml JTOML = JToml.jToml(); + public static ConfigSource build(Path tomlfile) throws IOException { - TomlTable result = null; + + TomlDocument document = null; if (Files.exists(tomlfile)) { - result = Toml.parse(tomlfile); + document = JTOML.read(tomlfile); } else { - result = EmptyTomlTable.EMPTY_TABLE; + document = JTOML.readFromString(""); } - - return new TomlConfigSource(tomlfile, result); + + return new TomlConfigSource(tomlfile, document); } private Map result; @@ -62,11 +65,14 @@ public static ConfigSource build(Path tomlfile) throws IOException { private long lastModified = 0; - private TomlConfigSource(Path tomlFile, TomlTable result) { + private TomlConfigSource(Path tomlFile, TomlDocument document) { this.tomlFile = tomlFile; try { - this.result = GSONProvider.GSON.fromJson(result.toJson(), HashMap.class); + var json = JTOML.fromToml(JsonObject.class, document); + this.result = GSONProvider.GSON.fromJson( + GSONProvider.GSON.toJson(json), + HashMap.class); if (Files.exists(tomlFile)) { this.lastModified = Files.getLastModifiedTime(tomlFile).toMillis(); @@ -88,9 +94,12 @@ public boolean reload() { return false; } lastModified = modified; - var toml = Toml.parse(tomlFile); + var document = JTOML.read(tomlFile); - this.result = GSONProvider.GSON.fromJson(toml.toJson(), HashMap.class); + var json = JTOML.fromToml(JsonObject.class, document); + this.result = GSONProvider.GSON.fromJson( + GSONProvider.GSON.toJson(json), + HashMap.class); } catch (IOException ex) { throw new RuntimeException(ex); } @@ -119,58 +128,4 @@ public List getList(String field) { public boolean exists() { return Files.exists(tomlFile); } - - private static class EmptyTomlTable implements TomlTable { - - static final TomlTable EMPTY_TABLE = new EmptyTomlTable(); - - private EmptyTomlTable() { - } - - @Override - public int size() { - return 0; - } - - @Override - public boolean isEmpty() { - return true; - } - - @Override - public Set keySet() { - return Collections.emptySet(); - } - - @Override - public Set> keyPathSet(boolean includeTables) { - return Collections.emptySet(); - } - - @Override - public Set> entrySet() { - return Collections.emptySet(); - } - - @Override - public Set, Object>> entryPathSet(boolean includeTables) { - return Collections.emptySet(); - } - - @Override - public Object get(List path) { - return null; - } - - @Override - public TomlPosition inputPositionOf(List path) { - return null; - } - - @Override - public Map toMap() { - return Collections.emptyMap(); - } - } - } \ No newline at end of file diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/source/YamlConfigSource.java b/cms-core/src/main/java/com/condation/cms/core/configuration/source/YamlConfigSource.java index 374860015..6ca784a65 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/source/YamlConfigSource.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/source/YamlConfigSource.java @@ -44,7 +44,9 @@ public class YamlConfigSource implements ConfigSource { public static ConfigSource build (Path yamlFile) throws IOException { Map result = null; if (Files.exists(yamlFile)) { - result = (Map) new Yaml().load(Files.newBufferedReader(yamlFile, StandardCharsets.UTF_8)); + try (var configReader = Files.newBufferedReader(yamlFile, StandardCharsets.UTF_8)) { + result = (Map) new Yaml().load(configReader); + } } else { result = Collections.emptyMap(); } @@ -83,7 +85,9 @@ public boolean reload() { } lastModified = modified; - result = (Map) new Yaml().load(Files.newBufferedReader(configFile, StandardCharsets.UTF_8)); + try (var configByteBuffer = Files.newBufferedReader(configFile, StandardCharsets.UTF_8)) { + result = (Map) new Yaml().load(configByteBuffer); + } return true; } catch (IOException ex) { log.error("", ex); diff --git a/cms-core/src/main/java/com/condation/cms/core/content/ContentResolvingStrategy.java b/cms-core/src/main/java/com/condation/cms/core/content/ContentResolvingStrategy.java new file mode 100644 index 000000000..dd3322bb5 --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/content/ContentResolvingStrategy.java @@ -0,0 +1,67 @@ +package com.condation.cms.core.content; + +/*- + * #%L + * cms-core + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.db.DB; +import com.condation.cms.api.db.cms.ReadOnlyFile; +import java.util.Optional; + +/** + * + * @author thmar + */ +public class ContentResolvingStrategy { + + public static String uriToPath (String uri) { + if (uri == null) { + return ""; + } else if (uri.equals("/")) { + return ""; + } else if (uri.startsWith("/")) { + return uri.substring(1); + } + return uri; + } + + public static Optional resolve (String uri, DB db) { + var path = uriToPath(uri); + + var contentBase = db.getReadOnlyFileSystem().contentBase(); + var contentPath = contentBase.resolve(path); + ReadOnlyFile contentFile = null; + if (contentPath.exists() && contentPath.isDirectory()) { + // use index.md + var tempFile = contentPath.resolve("index.md"); + if (tempFile.exists()) { + contentFile = tempFile; + } + } else { + var temp = contentBase.resolve(path + ".md"); + if (temp.exists()) { + contentFile = temp; + } + } + + return Optional.ofNullable(contentFile); + } +} diff --git a/cms-core/src/main/java/com/condation/cms/core/content/io/ContentFileParser.java b/cms-core/src/main/java/com/condation/cms/core/content/io/ContentFileParser.java new file mode 100644 index 000000000..efde05497 --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/content/io/ContentFileParser.java @@ -0,0 +1,77 @@ +package com.condation.cms.core.content.io; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.db.cms.ReadOnlyFile; +import org.yaml.snakeyaml.Yaml; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +public class ContentFileParser { + + private String content; + private Map header; + + public ContentFileParser(String filePath) throws IOException { + String fileContent = new String(Files.readAllBytes(Paths.get(filePath))); + parseFile(fileContent); + } + + public ContentFileParser (ReadOnlyFile contentFile) throws IOException { + parseFile(contentFile.getContent()); + } + + private void parseFile(String fileContent) { + if (fileContent.startsWith("---")) { + int endIndex = fileContent.indexOf("---", 3); + if (endIndex != -1) { + String headerContent = fileContent.substring(3, endIndex).trim(); + this.content = fileContent.substring(endIndex + 3).trim(); + parseHeader(headerContent); + } else { + this.content = fileContent; + } + } else { + this.content = fileContent; + } + } + + private void parseHeader(String headerContent) { + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + this.header = yaml.load(headerContent); + } + + public String getContent() { + return content; + } + + public Map getHeader() { + return header; + } + +} diff --git a/cms-core/src/main/java/com/condation/cms/core/content/io/YamlHeaderUpdater.java b/cms-core/src/main/java/com/condation/cms/core/content/io/YamlHeaderUpdater.java new file mode 100644 index 000000000..86d44a741 --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/content/io/YamlHeaderUpdater.java @@ -0,0 +1,119 @@ +package com.condation.cms.core.content.io; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.*; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +public class YamlHeaderUpdater { + + /** + * Inserts flat keys like "seo.title" into a nested map structure. + * + * @param targetMap The existing map (e.g., the YAML header) + * @param flatMap A flat map using dot notation ("seo.title" → "...") + */ + @SuppressWarnings("unchecked") + public static void mergeFlatMapIntoNestedMap(Map targetMap, Map flatMap) { + for (Map.Entry entry : flatMap.entrySet()) { + String[] keys = entry.getKey().split("\\."); + Map current = targetMap; + + for (int i = 0; i < keys.length - 1; i++) { + String key = keys[i]; + + Object next = current.get(key); + if (next instanceof Map) { + current = (Map) next; + } else if (next == null) { + Map newMap = new LinkedHashMap<>(); + current.put(key, newMap); + current = newMap; + } else { + // Konflikt: vorhandener Wert ist kein Map → überschreiben + Map newMap = new LinkedHashMap<>(); + current.put(key, newMap); + current = newMap; + } + } + + // Letzter Key → Wert setzen + current.put(keys[keys.length - 1], entry.getValue()); + } + } + + /** + * Saves the metadata as a YAML front matter and appends the Markdown content. + * + * @param filePath Path to the Markdown file to write to + * @param metadata Map containing the nested metadata (YAML structure) + * @param content The Markdown content (body) + * @throws IOException if writing fails + */ + public static void saveMarkdownFileWithHeader(Path filePath, Map metadata, String content) throws IOException { + // Configure pretty YAML output + DumperOptions options = new DumperOptions(); + options.setIndent(2); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + + Yaml yaml = new Yaml(options); + String yamlContent = yaml.dump(metadata); + + // Build full file content + StringBuilder builder = new StringBuilder(); + builder.append("---\n"); + builder.append(yamlContent); + builder.append("---\n\n"); + builder.append(content.trim()).append("\n"); + + // Write to file + Files.write(filePath, builder.toString().getBytes(StandardCharsets.UTF_8)); + } + + public static void saveMetaData(Path filePath, Map metadata) throws IOException { + // Configure pretty YAML output + DumperOptions options = new DumperOptions(); + options.setIndent(2); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + + Yaml yaml = new Yaml(options); + String yamlContent = yaml.dump(metadata); + + // Build full file content + StringBuilder builder = new StringBuilder(); + builder.append(yamlContent); + + // Write to file + Files.write(filePath, builder.toString().getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/cms-core/src/main/java/com/condation/cms/core/messages/DefaultMessageSource.java b/cms-core/src/main/java/com/condation/cms/core/messages/DefaultMessageSource.java index eccc5a493..a9f194f54 100644 --- a/cms-core/src/main/java/com/condation/cms/core/messages/DefaultMessageSource.java +++ b/cms-core/src/main/java/com/condation/cms/core/messages/DefaultMessageSource.java @@ -29,10 +29,8 @@ import java.net.URLClassLoader; import java.nio.file.Path; import java.text.MessageFormat; -import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.ResourceBundle; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -71,8 +69,12 @@ public String getLabel(final String bundle, final String label, final List messages.put(key, bundle.getString(key))); + URLClassLoader loader = new URLClassLoader(urls); + try { + var bundle = ResourceBundle.getBundle(bundleName, locale, loader); + bundle.keySet().forEach(key -> messages.put(key, bundle.getString(key))); + } finally { + loader.close(); + } } } diff --git a/cms-core/src/main/java/com/condation/cms/core/messages/ThemeMessageSource.java b/cms-core/src/main/java/com/condation/cms/core/messages/ThemeMessageSource.java index 55648e0df..1b118a3da 100644 --- a/cms-core/src/main/java/com/condation/cms/core/messages/ThemeMessageSource.java +++ b/cms-core/src/main/java/com/condation/cms/core/messages/ThemeMessageSource.java @@ -29,7 +29,6 @@ import java.nio.file.Path; import java.text.MessageFormat; import java.util.List; -import java.util.Map; import lombok.extern.slf4j.Slf4j; /** diff --git a/cms-core/src/main/java/com/condation/cms/core/scheduler/DefaultCronJobRunner.java b/cms-core/src/main/java/com/condation/cms/core/scheduler/DefaultCronJobRunner.java index e057d2afe..7d0dae6dc 100644 --- a/cms-core/src/main/java/com/condation/cms/core/scheduler/DefaultCronJobRunner.java +++ b/cms-core/src/main/java/com/condation/cms/core/scheduler/DefaultCronJobRunner.java @@ -25,8 +25,6 @@ import com.condation.cms.api.scheduler.CronJob; -import com.condation.cms.api.scheduler.CronJobContext; -import static com.condation.cms.core.scheduler.SingleCronJobRunner.DATA_CONTEXT; import static com.condation.cms.core.scheduler.SingleCronJobRunner.DATA_CRONJOB; import org.quartz.Job; import org.quartz.JobExecutionContext; @@ -41,8 +39,7 @@ public class DefaultCronJobRunner implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { if (context.getJobDetail().getJobDataMap().get(DATA_CRONJOB) != null) { - CronJobContext jobContext = (CronJobContext) context.getJobDetail().getJobDataMap().get(DATA_CONTEXT); - ((CronJob)context.getJobDetail().getJobDataMap().get(DATA_CRONJOB)).accept(jobContext); + ((CronJob)context.getJobDetail().getJobDataMap().get(DATA_CRONJOB)).accept(null); } } diff --git a/cms-git/src/main/java/com/condation/cms/git/GitScheduler.java b/cms-core/src/main/java/com/condation/cms/core/scheduler/ServerCronJobScheduler.java similarity index 58% rename from cms-git/src/main/java/com/condation/cms/git/GitScheduler.java rename to cms-core/src/main/java/com/condation/cms/core/scheduler/ServerCronJobScheduler.java index 06751286d..a1ae174d7 100644 --- a/cms-git/src/main/java/com/condation/cms/git/GitScheduler.java +++ b/cms-core/src/main/java/com/condation/cms/core/scheduler/ServerCronJobScheduler.java @@ -1,8 +1,8 @@ -package com.condation.cms.git; +package com.condation.cms.core.scheduler; /*- * #%L - * cms-git + * cms-core * %% * Copyright (C) 2023 - 2024 CondationCMS * %% @@ -21,15 +21,19 @@ * . * #L% */ - - +import com.condation.cms.api.SiteProperties; +import com.condation.cms.api.scheduler.CronJob; +import com.condation.cms.api.scheduler.CronJobContext; +import com.condation.cms.api.scheduler.CronJobScheduler; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.quartz.CronScheduleBuilder; import org.quartz.CronTrigger; +import org.quartz.Job; import org.quartz.JobBuilder; import org.quartz.JobDataMap; import org.quartz.JobDetail; +import org.quartz.JobKey; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.TriggerBuilder; @@ -38,29 +42,31 @@ * * @author t.marx */ -@Slf4j @RequiredArgsConstructor -public class GitScheduler { +@Slf4j +public class ServerCronJobScheduler implements CronJobScheduler { private final Scheduler scheduler; + @Override + public void schedule(String cronExpression, String name, CronJob job) { + + var identity = "server-%s".formatted(name); - public void schedule(final Repo repo) { JobDataMap data = new JobDataMap(); - data.put("repo", repo); + data.put(SingleCronJobRunner.DATA_CRONJOB, job); JobDetail jobDetail = JobBuilder - .newJob(UpdateRepoJob.class) - .withIdentity(repo.getName(), "update-repo") + .newJob(DefaultCronJobRunner.class) + .withIdentity(identity) .usingJobData(data) .build(); - + CronTrigger trigger = TriggerBuilder.newTrigger() - .withIdentity(repo.getName(), "update-repo") - .withSchedule(CronScheduleBuilder.cronSchedule(repo.getCron())) + .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) .startNow() .forJob(jobDetail) .build(); - + try { scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException ex) { @@ -68,4 +74,25 @@ public void schedule(final Repo repo) { throw new RuntimeException(ex); } } + + @Override + public boolean exists(String name) { + try { + return scheduler.checkExists(JobKey.jobKey(name)); + } catch (SchedulerException ex) { + log.error("", ex); + throw new RuntimeException(ex); + } + } + + @Override + public void remove(String name) { + try { + scheduler.deleteJob(JobKey.jobKey(name)); + } catch (SchedulerException ex) { + log.error("", ex); + throw new RuntimeException(ex); + } + } + } diff --git a/cms-core/src/main/java/com/condation/cms/core/scheduler/SingleCronJobRunner.java b/cms-core/src/main/java/com/condation/cms/core/scheduler/SingleCronJobRunner.java index 932d22ccd..5500e70a8 100644 --- a/cms-core/src/main/java/com/condation/cms/core/scheduler/SingleCronJobRunner.java +++ b/cms-core/src/main/java/com/condation/cms/core/scheduler/SingleCronJobRunner.java @@ -24,8 +24,6 @@ import com.condation.cms.api.scheduler.CronJob; import com.condation.cms.api.scheduler.CronJobContext; import java.util.concurrent.Semaphore; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import lombok.extern.slf4j.Slf4j; import org.quartz.DisallowConcurrentExecution; diff --git a/cms-git/src/main/java/com/condation/cms/git/Task.java b/cms-core/src/main/java/com/condation/cms/core/serivce/Service.java similarity index 80% rename from cms-git/src/main/java/com/condation/cms/git/Task.java rename to cms-core/src/main/java/com/condation/cms/core/serivce/Service.java index 13c28398f..919ea5411 100644 --- a/cms-git/src/main/java/com/condation/cms/git/Task.java +++ b/cms-core/src/main/java/com/condation/cms/core/serivce/Service.java @@ -1,10 +1,10 @@ -package com.condation.cms.git; +package com.condation.cms.core.serivce; /*- * #%L - * cms-git + * cms-core * %% - * Copyright (C) 2023 - 2024 CondationCMS + * Copyright (C) 2023 - 2025 CondationCMS * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -22,13 +22,9 @@ * #L% */ - -import java.util.concurrent.Callable; - /** * * @author t.marx */ -public interface Task extends Callable { - +public interface Service { } diff --git a/cms-core/src/main/java/com/condation/cms/core/serivce/ServiceRegistry.java b/cms-core/src/main/java/com/condation/cms/core/serivce/ServiceRegistry.java new file mode 100644 index 000000000..db0c46bef --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/serivce/ServiceRegistry.java @@ -0,0 +1,72 @@ +package com.condation.cms.core.serivce; + +/*- + * #%L + * cms-core + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * + * @author t.marx + */ +public class ServiceRegistry { + + private static ServiceRegistry INSTANCE = null; + + public synchronized static ServiceRegistry getInstance() { + + if (INSTANCE == null) { + INSTANCE = new ServiceRegistry(); + } + + return INSTANCE; + } + + private final List> services = new CopyOnWriteArrayList<>(); + + private record ServiceHolder (Class serviceClass, T implementation, String site) {} + + private ServiceRegistry() { + } + + public void clear () { + services.clear(); + } + + public boolean has(final String site, final Class serviceClass) { + return services.stream().anyMatch(service -> service.site.equals(site) && service.serviceClass == serviceClass); + } + + public Optional get(final String site, final Class serviceClass) { + return services.stream() + .filter(service -> service.site.equals(site) && service.serviceClass == serviceClass) + .map(service -> serviceClass.cast(service.implementation())) + .findFirst(); + } + + public void register(final String site, final Class serviceClass, final T service) { + this.services.add(new ServiceHolder(serviceClass, service, site)); + } + +} diff --git a/cms-core/src/main/java/com/condation/cms/core/serivce/impl/NodeTranslationService.java b/cms-core/src/main/java/com/condation/cms/core/serivce/impl/NodeTranslationService.java new file mode 100644 index 000000000..7e1e90af2 --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/serivce/impl/NodeTranslationService.java @@ -0,0 +1,119 @@ +package com.condation.cms.core.serivce.impl; + +/*- + * #%L + * cms-core + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.Constants; +import com.condation.cms.api.configuration.Configuration; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.api.db.DB; +import com.condation.cms.api.eventbus.EventBus; +import com.condation.cms.api.eventbus.events.ReIndexContentMetaDataEvent; +import com.condation.cms.api.feature.features.EventBusFeature; +import com.condation.cms.api.utils.HTTPUtil; +import com.condation.cms.api.utils.PathUtil; +import com.condation.cms.core.content.ContentResolvingStrategy; +import com.condation.cms.core.content.io.ContentFileParser; +import com.condation.cms.core.content.io.YamlHeaderUpdater; +import com.condation.cms.core.serivce.Service; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; + +/** + * + * @author thmar + */ +@Slf4j +public class NodeTranslationService implements Service { + + private final DB db; + private final EventBus eventBus; + + public NodeTranslationService (final DB db, final EventBus eventBus) { + this.db = db; + this.eventBus = eventBus; + } + + public boolean removeTranslation (String uri, String language) { + var contentFile = ContentResolvingStrategy.resolve(uri, db).orElse(null); + + if (contentFile != null) { + try { + ContentFileParser parser = new ContentFileParser(contentFile); + + Map meta = parser.getHeader(); + var translations = (Map)meta.getOrDefault("translations", new HashMap<>()); + translations.remove(language); + meta.put("translations", translations); + + var path = contentFile.relativePath(); + var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(path); + + YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, meta, parser.getContent()); + log.debug("file {} saved", path); + + eventBus.publish(new ReIndexContentMetaDataEvent(path)); + + return true; + } catch (IOException ex) { + log.error("", ex); + return false; + } + } + + return false; + } + + public boolean addTranslation (String uri, String site, String translationUri, String language) { + + var contentFile = ContentResolvingStrategy.resolve(uri, db).orElse(null); + + if (contentFile != null) { + try { + ContentFileParser parser = new ContentFileParser(contentFile); + + Map meta = parser.getHeader(); + var translations = (Map)meta.getOrDefault("translations", new HashMap<>()); + translations.put(language, PathUtil.toURL(translationUri)); + meta.put("translations", translations); + + var path = contentFile.relativePath(); + + var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(path); + + YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, meta, parser.getContent()); + log.debug("file {} saved", path); + + eventBus.publish(new ReIndexContentMetaDataEvent(path)); + + return true; + } catch (IOException ex) { + log.error("", ex); + return false; + } + } + + return false; + } +} diff --git a/cms-core/src/main/java/com/condation/cms/core/serivce/impl/SiteDBService.java b/cms-core/src/main/java/com/condation/cms/core/serivce/impl/SiteDBService.java new file mode 100644 index 000000000..311c30b0c --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/serivce/impl/SiteDBService.java @@ -0,0 +1,34 @@ +package com.condation.cms.core.serivce.impl; + +/*- + * #%L + * cms-core + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.db.DB; +import com.condation.cms.core.serivce.Service; + +/** + * + * @author thmar + */ +public record SiteDBService (DB db) implements Service { + +} diff --git a/cms-core/src/main/java/com/condation/cms/core/serivce/impl/SiteLinkService.java b/cms-core/src/main/java/com/condation/cms/core/serivce/impl/SiteLinkService.java new file mode 100644 index 000000000..02d6f8fc6 --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/serivce/impl/SiteLinkService.java @@ -0,0 +1,62 @@ +package com.condation.cms.core.serivce.impl; + +/*- + * #%L + * cms-core + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.configuration.Configuration; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.api.utils.HTTPUtil; +import com.condation.cms.core.serivce.Service; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * + * @author thmar + */ +public class SiteLinkService implements Service { + + private final Configuration configuration; + + public SiteLinkService (final Configuration configuration) { + this.configuration = configuration; + } + + public String managerDeepLink (String url) { + var siteProperties = configuration.get(SiteConfiguration.class).siteProperties(); + url = HTTPUtil.modifyUrl(url, siteProperties); + String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8); + var deepLink = HTTPUtil.modifyUrl("/manager/index.html?page=%s".formatted(encodedUrl), siteProperties); + + var baseUrl = siteProperties.baseUrl(); + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.substring(0, baseUrl.length() - 1); + } + + return baseUrl + deepLink; + } + + public String link (String url) { + var siteProperties = configuration.get(SiteConfiguration.class).siteProperties(); + return HTTPUtil.modifyUrl(url, siteProperties); + } +} diff --git a/cms-core/src/main/java/com/condation/cms/core/serivce/impl/SitePropertiesService.java b/cms-core/src/main/java/com/condation/cms/core/serivce/impl/SitePropertiesService.java new file mode 100644 index 000000000..2903ff0b2 --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/serivce/impl/SitePropertiesService.java @@ -0,0 +1,46 @@ +package com.condation.cms.core.serivce.impl; + +/*- + * #%L + * cms-core + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.SiteProperties; +import com.condation.cms.api.configuration.Configuration; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.core.serivce.Service; + +/** + * + * @author thmar + */ +public class SitePropertiesService implements Service { + + private final Configuration configuration; + + public SitePropertiesService (final Configuration configuration) { + this.configuration = configuration; + } + + public SiteProperties siteProperties () { + return configuration.get(SiteConfiguration.class).siteProperties(); + } + +} diff --git a/cms-core/src/main/java/com/condation/cms/core/site/DefaultSiteService.java b/cms-core/src/main/java/com/condation/cms/core/site/DefaultSiteService.java new file mode 100644 index 000000000..9d8482649 --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/site/DefaultSiteService.java @@ -0,0 +1,50 @@ +package com.condation.cms.core.site; + +/*- + * #%L + * cms-core + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.site.Site; +import com.condation.cms.api.site.SiteService; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** + * + * @author thmar + */ +public class DefaultSiteService implements SiteService { + + private final List sites = new ArrayList<>(); + + @Override + public void add(Site site) { + sites.add(site); + } + + @Override + public Stream sites() { + return sites.stream(); + } + + +} diff --git a/cms-core/src/main/java/com/condation/cms/core/theme/DefaultTheme.java b/cms-core/src/main/java/com/condation/cms/core/theme/DefaultTheme.java index d27f93788..9b239dd23 100644 --- a/cms-core/src/main/java/com/condation/cms/core/theme/DefaultTheme.java +++ b/cms-core/src/main/java/com/condation/cms/core/theme/DefaultTheme.java @@ -50,18 +50,18 @@ @RequiredArgsConstructor public class DefaultTheme implements Theme { - public static final Theme EMPTY = new DefaultTheme(null, new EmptyThemeProperties(Map.of()), true, new EmptyMessageSource()); + public static final Theme NO_THEME = new DefaultTheme(null, new EmptyThemeProperties(Map.of()), true, new EmptyMessageSource()); private final Path themePath; private final ThemeProperties properties; private final MessageSource messages; - private boolean empty = false; + private boolean noTheme = false; private Theme parent; - private DefaultTheme(final Path themePath, final ThemeProperties themeProperties, final boolean empty, final MessageSource messages) { + private DefaultTheme(final Path themePath, final ThemeProperties themeProperties, final boolean noTheme, final MessageSource messages) { this(themePath, themeProperties, messages); - this.empty = empty; + this.noTheme = noTheme; } public static Theme load( @@ -110,7 +110,7 @@ private static Theme load( @Override public boolean empty() { - return empty; + return noTheme; } @Override diff --git a/cms-core/src/test/java/com/condation/cms/core/configuration/MediaConfigurationTest.java b/cms-core/src/test/java/com/condation/cms/core/configuration/MediaConfigurationTest.java index 81616e99a..706bfceb4 100644 --- a/cms-core/src/test/java/com/condation/cms/core/configuration/MediaConfigurationTest.java +++ b/cms-core/src/test/java/com/condation/cms/core/configuration/MediaConfigurationTest.java @@ -65,10 +65,10 @@ public void test_object() { .hasSize(4); MediaConfiguration.Format f1 = medias.getFirst(); - Assertions.assertThat(f1.getName()).isEqualTo("yaml"); + Assertions.assertThat(f1.getName()).isEqualTo("big"); Assertions.assertThat(f1.getFormat()).isEqualTo("webp"); Assertions.assertThat(f1.isCompression()).isTrue(); - Assertions.assertThat(f1.getHeight()).isEqualTo(256); - Assertions.assertThat(f1.getWidth()).isEqualTo(256); + Assertions.assertThat(f1.getHeight()).isEqualTo(512); + Assertions.assertThat(f1.getWidth()).isEqualTo(512); } } diff --git a/cms-core/src/test/java/com/condation/cms/core/content/MapAccessTest.java b/cms-core/src/test/java/com/condation/cms/core/content/MapAccessTest.java index 7b2daeb7a..e6c347029 100644 --- a/cms-core/src/test/java/com/condation/cms/core/content/MapAccessTest.java +++ b/cms-core/src/test/java/com/condation/cms/core/content/MapAccessTest.java @@ -22,6 +22,7 @@ * #L% */ +import com.condation.cms.api.content.MapAccess; import java.util.Map; import org.assertj.core.api.Assertions; diff --git a/cms-core/src/test/java/com/condation/cms/core/messaging/DefaultTopicClassLoaderTest.java b/cms-core/src/test/java/com/condation/cms/core/messaging/DefaultTopicClassLoaderTest.java index 1a9ddc67b..1b21489c4 100644 --- a/cms-core/src/test/java/com/condation/cms/core/messaging/DefaultTopicClassLoaderTest.java +++ b/cms-core/src/test/java/com/condation/cms/core/messaging/DefaultTopicClassLoaderTest.java @@ -29,7 +29,9 @@ import java.net.URLClassLoader; import com.condation.cms.api.messaging.Listener; import com.condation.cms.api.messaging.Topic; +import java.io.IOException; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterEach; @Slf4j class DefaultTopicClassLoaderTest { @@ -52,6 +54,12 @@ void setUp() throws Exception { constructor.setAccessible(true); topic = (DefaultTopic) constructor.newInstance("TestTopic"); } + + @AfterEach + void teardown () throws IOException { + loader1.close(); + loader2.close(); + } @Test void testPublishWithDifferentClassLoaders() throws Exception { diff --git a/cms-core/src/test/java/com/condation/cms/core/serivce/ServiceRegistryTest.java b/cms-core/src/test/java/com/condation/cms/core/serivce/ServiceRegistryTest.java new file mode 100644 index 000000000..e8285ec90 --- /dev/null +++ b/cms-core/src/test/java/com/condation/cms/core/serivce/ServiceRegistryTest.java @@ -0,0 +1,79 @@ +package com.condation.cms.core.serivce; + +/*- + * #%L + * cms-core + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; + +/** + * + * @author thorstenmarx + */ +public class ServiceRegistryTest { + + @BeforeAll + static void registerServices() { + ServiceRegistry.getInstance().register("site-1", MyService.class, new MyService("site-1")); + ServiceRegistry.getInstance().register("site-2", MyService.class, new MyService("site-2")); + } + + @AfterAll + static void clear () { + ServiceRegistry.getInstance().clear(); + } + + @Test + public void testHasService() { + Assertions.assertThat(ServiceRegistry.getInstance().has("site-1", MyService.class)).isTrue(); + + Assertions.assertThat(ServiceRegistry.getInstance().has("site-3", MyService.class)).isFalse(); + } + + @Test + public void testGetService() { + var service1 = ServiceRegistry.getInstance().get("site-1", MyService.class); + + Assertions.assertThat(service1).isPresent(); + Assertions.assertThat(service1.get().getName()).isEqualTo("site-1"); + + var service2 = ServiceRegistry.getInstance().get("site-2", MyService.class); + + Assertions.assertThat(service2).isPresent(); + Assertions.assertThat(service2.get().getName()).isEqualTo("site-2"); + } + + private static class MyService implements Service { + + private String name; + + public MyService(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } +} diff --git a/cms-extensions/pom.xml b/cms-extensions/pom.xml index 5b522fbe4..b7f9b2989 100644 --- a/cms-extensions/pom.xml +++ b/cms-extensions/pom.xml @@ -4,7 +4,7 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 cms-extensions jar diff --git a/cms-extensions/src/main/java/com/condation/cms/extensions/ExtensionFileSystem.java b/cms-extensions/src/main/java/com/condation/cms/extensions/ExtensionFileSystem.java index e6cd0e7da..db1f18999 100644 --- a/cms-extensions/src/main/java/com/condation/cms/extensions/ExtensionFileSystem.java +++ b/cms-extensions/src/main/java/com/condation/cms/extensions/ExtensionFileSystem.java @@ -21,8 +21,6 @@ * . * #L% */ - - import com.condation.cms.api.theme.Theme; import java.io.IOException; import java.io.InputStream; @@ -64,7 +62,7 @@ public Path parsePath(String path) { return Path.of(path); } var resolved = siteExtensionPath.resolve(path); - if (!Files.exists(resolved) && !theme.empty() ) { + if (!Files.exists(resolved) && !theme.empty()) { resolved = theme.resolveExtension(path); } return resolved; @@ -76,25 +74,25 @@ public void checkAccess(Path path, Set modes, LinkOption.. @Override public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { - throw new UnsupportedOperationException("Not supported yet."); + throw new UnsupportedOperationException("Not supported yet."); } @Override public void delete(Path path) throws IOException { - throw new UnsupportedOperationException("Not supported yet."); + throw new UnsupportedOperationException("Not supported yet."); } @Override public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { if (path.startsWith("system/") || path.startsWith("system\\")) { var localPath = path.toString().replaceAll("\\\\", "/"); - InputStream resourceAsStream = ExtensionFileSystem.class.getResourceAsStream(localPath); - - byte[] content = new byte[0]; - if (resourceAsStream != null) { - content = resourceAsStream.readAllBytes(); + try (InputStream resourceAsStream = ExtensionFileSystem.class.getResourceAsStream(localPath);) { + byte[] content = new byte[0]; + if (resourceAsStream != null) { + content = resourceAsStream.readAllBytes(); + } + return new SeekableInMemoryByteChannel(content); } - return new SeekableInMemoryByteChannel(content); } return Files.newByteChannel(path, options, attrs); } @@ -116,7 +114,7 @@ public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException @Override public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { - throw new UnsupportedOperationException("Not supported yet."); + throw new UnsupportedOperationException("Not supported yet."); } } diff --git a/cms-extensions/src/main/java/com/condation/cms/extensions/ExtensionManager.java b/cms-extensions/src/main/java/com/condation/cms/extensions/ExtensionManager.java index b4a3aefcd..4b266b41f 100644 --- a/cms-extensions/src/main/java/com/condation/cms/extensions/ExtensionManager.java +++ b/cms-extensions/src/main/java/com/condation/cms/extensions/ExtensionManager.java @@ -64,7 +64,8 @@ private ClassLoader getClassLoader() throws IOException { Path libs = db.getFileSystem().resolve("libs/"); List urls = new ArrayList<>(); if (Files.exists(libs)) { - Files.list(libs) + try (var libsStream = Files.list(libs)) { + libsStream .filter(path -> path.getFileName().toString().endsWith(".jar")) .forEach(path -> { try { @@ -73,6 +74,7 @@ private ClassLoader getClassLoader() throws IOException { log.error("", ex); } }); + } } return new URLClassLoader(urls.toArray(URL[]::new), ClassLoader.getSystemClassLoader()); } @@ -81,7 +83,8 @@ protected void loadExtensions(final Path extPath, final Consumer loader) if (!Files.exists(extPath)) { return; } - Files.list(extPath) + try (var extStream = Files.list(extPath)) { + extStream .filter(path -> !Files.isDirectory(path) && path.getFileName().toString().endsWith(".js")) .map(extFile -> { try { @@ -99,21 +102,23 @@ protected void loadExtensions(final Path extPath, final Consumer loader) return null; }).filter(source -> source != null) .forEach(loader); + } } public RequestExtensions newContext(Theme theme, RequestContext requestContext) throws IOException { + var libsClassLoader = getClassLoader(); var context = Context.newBuilder() .allowAllAccess(true) .allowHostClassLookup(className -> true) .allowHostAccess(HostAccess.ALL) .allowValueSharing(true) - .hostClassLoader(getClassLoader()) + .hostClassLoader(libsClassLoader) .allowIO(IOAccess.newBuilder() .fileSystem(new ExtensionFileSystem(db.getFileSystem().resolve("extensions/"), theme)) .build()) .engine(engine).build(); - RequestExtensions requestExtensions = new RequestExtensions(context); + RequestExtensions requestExtensions = new RequestExtensions(context, libsClassLoader); final Value bindings = context.getBindings("js"); setUpBinding(bindings, requestExtensions, theme, requestContext); diff --git a/cms-extensions/src/main/java/com/condation/cms/extensions/TemplateFunctionExtension.java b/cms-extensions/src/main/java/com/condation/cms/extensions/TemplateFunctionExtension.java index a6633a881..8f59b38fc 100644 --- a/cms-extensions/src/main/java/com/condation/cms/extensions/TemplateFunctionExtension.java +++ b/cms-extensions/src/main/java/com/condation/cms/extensions/TemplateFunctionExtension.java @@ -23,10 +23,11 @@ */ +import com.condation.cms.api.model.Parameter; import java.util.function.Function; /** * * @author t.marx */ -public record TemplateFunctionExtension (String name, Function function) {} +public record TemplateFunctionExtension (String name, Function function) {} diff --git a/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/ContentHooks.java b/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/ContentHooks.java index a4c531416..bf9b3e7ff 100644 --- a/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/ContentHooks.java +++ b/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/ContentHooks.java @@ -45,11 +45,10 @@ public class ContentHooks implements Feature { private final RequestContext requestContext; - public ShortCodesWrapper getShortCodes (Map> codes) { - var codeWrapper = new ShortCodesWrapper(codes); + public TagsWrapper getTags (Map> codes) { + var codeWrapper = new TagsWrapper(codes); requestContext.get(HookSystemFeature.class).hookSystem() - .execute(Hooks.CONTENT_SHORTCODE.hook(), Map.of("shortCodes", codeWrapper)); - + .execute(Hooks.CONTENT_TAGS.hook(), Map.of("tags", codeWrapper)); return codeWrapper; } } diff --git a/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/ShortCodesWrapper.java b/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/TagsWrapper.java similarity index 72% rename from cms-extensions/src/main/java/com/condation/cms/extensions/hooks/ShortCodesWrapper.java rename to cms-extensions/src/main/java/com/condation/cms/extensions/hooks/TagsWrapper.java index 1b097fd79..c28108e22 100644 --- a/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/ShortCodesWrapper.java +++ b/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/TagsWrapper.java @@ -34,20 +34,20 @@ * @author t.marx */ @RequiredArgsConstructor -public class ShortCodesWrapper { +public class TagsWrapper { @Getter - private final Map> shortCodes; + private final Map> tags; - public void put(final String shortCode, final Function function) { - shortCodes.put(shortCode, function); + public void put(final String tag, final Function function) { + tags.put(tag, function); } - public boolean contains(final String shortCode) { - return shortCodes.containsKey(shortCode); + public boolean contains(final String tag) { + return tags.containsKey(tag); } - public void remove(final String shortCode) { - shortCodes.remove(shortCode); + public void remove(final String tag) { + tags.remove(tag); } } diff --git a/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/TemplateFunctionWrapper.java b/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/TemplateFunctionWrapper.java index 517b8d0c1..62332ac23 100644 --- a/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/TemplateFunctionWrapper.java +++ b/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/TemplateFunctionWrapper.java @@ -23,6 +23,7 @@ */ +import com.condation.cms.api.model.Parameter; import com.condation.cms.extensions.TemplateFunctionExtension; import java.util.ArrayList; import java.util.List; @@ -38,7 +39,7 @@ public class TemplateFunctionWrapper { @Getter private final List registerTemplateFunctions = new ArrayList<>(); - public void add(final String path, final Function function) { + public void put(final String path, final Function function) { registerTemplateFunctions.add(new TemplateFunctionExtension(path, function)); } } diff --git a/cms-extensions/src/main/java/com/condation/cms/extensions/request/RequestExtensions.java b/cms-extensions/src/main/java/com/condation/cms/extensions/request/RequestExtensions.java index 767d5dd91..f3b096088 100644 --- a/cms-extensions/src/main/java/com/condation/cms/extensions/request/RequestExtensions.java +++ b/cms-extensions/src/main/java/com/condation/cms/extensions/request/RequestExtensions.java @@ -25,19 +25,7 @@ import com.condation.cms.api.annotations.FeatureScope; import com.condation.cms.api.feature.Feature; -import com.condation.cms.api.model.Parameter; -import com.condation.cms.extensions.HttpHandlerExtension; -import com.condation.cms.extensions.TemplateFunctionExtension; -import com.condation.cms.extensions.TemplateSupplierExtension; -import com.condation.cms.extensions.http.ExtensionHttpHandler; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.BiPredicate; -import java.util.function.Function; -import java.util.function.Supplier; +import java.net.URLClassLoader; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.graalvm.polyglot.Context; @@ -53,8 +41,16 @@ public class RequestExtensions implements AutoCloseable, Feature { @Getter private final Context context; + @Getter + private final ClassLoader libsClassLoader; + @Override public void close() throws Exception { - context.close(); + if (context != null) { + context.close(); + } + if (libsClassLoader != null) { + ((URLClassLoader)libsClassLoader).close(); + } } } diff --git a/cms-extensions/src/main/resources/com/condation/cms/extensions/system/links.mjs b/cms-extensions/src/main/resources/com/condation/cms/extensions/system/links.mjs new file mode 100644 index 000000000..c92baf72f --- /dev/null +++ b/cms-extensions/src/main/resources/com/condation/cms/extensions/system/links.mjs @@ -0,0 +1,33 @@ +import { IsPreviewFeature, SitePropertiesFeature, $features } from 'system/features.mjs'; + + +const siteProperties = $features.get(SitePropertiesFeature).siteProperties() + +export const $links = { + createUrl : (url) => { + // Falls absolute URL (http/https), direkt zurückgeben + if (url.startsWith("http://") || url.startsWith("https://")) { + return url; + } + + // Context Path holen (z. B. aus globaler Variable oder Config) + const contextPath = siteProperties.contextPath(); + + // Wenn ContextPath nicht "/", dann davor setzen + if (contextPath !== "/") { + url = contextPath + url; + } + + // Preview-Parameter anhängen, falls aktiv + if ($features.has(IsPreviewFeature)) { + const feature = $features.get(IsPreviewFeature); + const previewValue = feature.mode().getValue(); + + // Prüfen, ob bereits Parameter vorhanden sind + const separator = url.includes("?") ? "&" : "?"; + url = `${url}${separator}preview=${previewValue}`; + } + + return url; + } +} \ No newline at end of file diff --git a/cms-extensions/src/main/resources/com/condation/cms/extensions/system/templates.mjs b/cms-extensions/src/main/resources/com/condation/cms/extensions/system/templates.mjs new file mode 100644 index 000000000..e30b5103f --- /dev/null +++ b/cms-extensions/src/main/resources/com/condation/cms/extensions/system/templates.mjs @@ -0,0 +1,10 @@ +import { TemplateEngineFeature, $features } from 'system/features.mjs'; + + +const templateEngine = $features.get(TemplateEngineFeature) + +export const $templates = { + render : (template, model) => { + return templateEngine.render(template, model, requestContext); + } +} \ No newline at end of file diff --git a/cms-extensions/src/test/java/com/condation/cms/extensions/repository/SignaturedRemoteModuleRepositoryTest.java b/cms-extensions/src/test/java/com/condation/cms/extensions/repository/SignaturedRemoteModuleRepositoryTest.java index fd2184421..f8fb6e4b6 100644 --- a/cms-extensions/src/test/java/com/condation/cms/extensions/repository/SignaturedRemoteModuleRepositoryTest.java +++ b/cms-extensions/src/test/java/com/condation/cms/extensions/repository/SignaturedRemoteModuleRepositoryTest.java @@ -115,14 +115,17 @@ void shouldDownloadAndVerifyFileSignature() throws Exception { repo.download(fileBaseUrl + "/test.zip", hash, installTarget); // Prüfung, ob Datei im entpackten Verzeichnis vorhanden ist - boolean found = Files.walk(installTarget) - .anyMatch(p -> p.getFileName().toString().equals("content.txt")); + boolean found = false; + try (var walkStream = Files.walk(installTarget)) { + found = walkStream + .anyMatch(p -> p.getFileName().toString().equals("content.txt")); + } Assertions.assertThat(found) .as("content.txt sollte entpackt vorhanden sein") .isTrue(); } - + @Test void shouldThrowAnExceptionOnInvalidSignature() throws Exception { @@ -134,6 +137,6 @@ void shouldThrowAnExceptionOnInvalidSignature() throws Exception { Assertions.assertThatCode(() -> { repo.download(fileBaseUrl + "/test.zip", "wrong_signature", installTarget); }).isInstanceOf(RuntimeException.class).hasMessage("error downloading module"); - + } } diff --git a/cms-filesystem/pom.xml b/cms-filesystem/pom.xml index fb57366fa..9ea9367f3 100644 --- a/cms-filesystem/pom.xml +++ b/cms-filesystem/pom.xml @@ -4,13 +4,12 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 cms-filesystem jar - org.apache.lucene lucene-core @@ -23,7 +22,10 @@ com.h2database h2-mvstore - + + org.jsoup + jsoup + com.condation.cms cms-api diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java index 2148f2fef..742c18623 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java @@ -57,7 +57,7 @@ public class FileDB implements DB { private FileTaxonomies taxonomies; public void init () throws IOException { - init(MetaData.Type.MEMORY); + init(MetaData.Type.PERSISTENT); } public void init (MetaData.Type metaDataType) throws IOException { @@ -67,8 +67,7 @@ public void init (MetaData.Type metaDataType) throws IOException { content = new FileContent(fileSystem, readOnlyFileSystem); - taxonomies = new FileTaxonomies(configuration, fileSystem); - + taxonomies = new FileTaxonomies(configuration, fileSystem); } @Override diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java index 8861ff126..083988bd8 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java @@ -53,8 +53,6 @@ import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.regex.Pattern; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -227,16 +225,16 @@ public List listSections(final String filename, String folder) { List nodes = new ArrayList<>(); final Pattern isSectionOf = Constants.SECTION_OF_PATTERN.apply(filename); - final Pattern isOrderedSectionOf = Constants.SECTION_ORDERED_OF_PATTERN.apply(filename); - + final Pattern isNamedSectionOf = Constants.SECTION_NAMED_OF_PATTERN.apply(filename); + if ("".equals(folder)) { metaData.getTree().values() .stream() .filter(node -> !node.isHidden()) - .filter(node -> node.isPublished()) + .filter(node -> node.isVisible()) .filter(node -> node.isSection()) .filter(node -> { - return isSectionOf.matcher(node.name()).matches() || isOrderedSectionOf.matcher(node.name()).matches(); + return isSectionOf.matcher(node.name()).matches() || isNamedSectionOf.matcher(node.name()).matches(); }) .forEach((node) -> { nodes.add(node); @@ -247,11 +245,10 @@ public List listSections(final String filename, String folder) { findFolder.get().children().values() .stream() .filter(node -> !node.isHidden()) - .filter(node -> node.isPublished()) + .filter(node -> node.isVisible()) .filter(node -> node.isSection()) .filter(node - -> isSectionOf.matcher(node.name()).matches() - || isOrderedSectionOf.matcher(node.name()).matches() + -> isSectionOf.matcher(node.name()).matches() || isNamedSectionOf.matcher(node.name()).matches() ) .forEach((node) -> { nodes.add(node); @@ -290,10 +287,10 @@ public void init() throws IOException { public void init(MetaData.Type metaDataType) throws IOException { log.debug("init filesystem"); - if (MetaData.Type.PERSISTENT.equals(metaDataType)) { - this.metaData = new PersistentMetaData(this.hostBaseDirectory); - } else { + if (MetaData.Type.MEMORY.equals(metaDataType)) { this.metaData = new MemoryMetaData(); + } else { + this.metaData = new PersistentMetaData(this.hostBaseDirectory); } this.metaData.open(); @@ -337,7 +334,12 @@ public void onNext(FileEvent item) { eventBus.register(ReIndexContentMetaDataEvent.class, (event) -> { try { - swapMetaData(); + if (event.uri() == null) { + swapMetaData(); + } else { + var contentFile = contentBase.resolve(event.uri()); + addOrUpdateMetaData(contentFile); + } } catch (IOException ex) { log.error("error while reindex meta data", ex); } diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/MultiRootRecursiveWatcher.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/MultiRootRecursiveWatcher.java index 007755dbf..026a6796f 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/MultiRootRecursiveWatcher.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/MultiRootRecursiveWatcher.java @@ -1,3 +1,4 @@ + package com.condation.cms.filesystem; /*- @@ -21,7 +22,6 @@ * . * #L% */ - import com.condation.cms.api.utils.PathUtil; import java.io.File; import java.io.IOException; @@ -45,16 +45,19 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; +import java.util.concurrent.Executors; import java.util.concurrent.Flow; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.SubmissionPublisher; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import lombok.extern.slf4j.Slf4j; /** * The recursive file watcher monitors a folder (and its sub-folders). */ + @Slf4j public class MultiRootRecursiveWatcher { @@ -64,7 +67,8 @@ public class MultiRootRecursiveWatcher { private Thread watchThread; private final Map watchPathKeyMap; - private Timer timer; + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private ScheduledFuture scheduledFuture; private final Map roots; @@ -79,8 +83,6 @@ public MultiRootRecursiveWatcher(List roots) { this.watchThread = null; this.watchPathKeyMap = new HashMap<>(); - this.timer = null; - this.roots = new HashMap<>(); roots.forEach(path -> { this.roots.put(path, new Root(path, new SubmissionPublisher<>())); @@ -97,12 +99,15 @@ public SubmissionPublisher getPublisher(Path root) { } /** - * Starts the watcher service and registers watches in all of the sub-folders of the given root folder. + * Starts the watcher service and registers watches in all of the + * sub-folders of the given root folder. * *

- * Important: This method returns immediately, even though the watches might not be in place yet. For large - * file trees, it might take several seconds until all directories are being monitored. For normal cases (1-100 - * folders), this should not take longer than a few milliseconds. + * Important: This method returns immediately, even though the + * watches might not be in place yet. For large file trees, it might take + * several seconds until all directories are being monitored. For normal + * cases (1-100 folders), this should not take longer than a few + * milliseconds. */ public void start() throws IOException { watchService = FileSystems.getDefault().newWatchService(); @@ -110,16 +115,16 @@ public void start() throws IOException { watchThread = Thread.ofVirtual().name("Watcher").start(() -> { running.set(true); walkTreeAndSetWatches(); - + while (running.get()) { try { WatchKey watchKey = watchService.take(); List> events = watchKey.pollEvents(); - + events.forEach((event) -> { Path path = (Path) watchKey.watchable(); File file = path.resolve((Path) event.context()).toFile(); - + final FileEvent fileEvent; if (event.kind().equals(ENTRY_CREATE)) { fileEvent = new FileEvent(file, FileEvent.Type.CREATED); @@ -127,10 +132,14 @@ public void start() throws IOException { fileEvent = new FileEvent(file, FileEvent.Type.DELETED); } else if (event.kind().equals(ENTRY_MODIFY)) { fileEvent = new FileEvent(file, FileEvent.Type.MODIFIED); + } else if (event.kind() == OVERFLOW) { + log.warn("Overflow occurred, resyncing watches"); + walkTreeAndSetWatches(); + fileEvent = null; } else { fileEvent = null; } - + if (fileEvent != null) { roots.values().forEach((root) -> { if (PathUtil.isChild(root.path, file.toPath())) { @@ -139,7 +148,7 @@ public void start() throws IOException { }); } }); - + // fire events watchKey.reset(); resetWaitSettlementTimer(); @@ -159,6 +168,8 @@ public synchronized void stop() { watchService.close(); running.set(false); watchThread.interrupt(); + + scheduler.shutdown(); } catch (IOException e) { // Don't care } @@ -166,20 +177,16 @@ public synchronized void stop() { } private synchronized void resetWaitSettlementTimer() { - if (timer != null) { - timer.cancel(); - timer = null; + if (scheduledFuture != null) { + scheduledFuture.cancel(false); } + scheduledFuture = scheduler.schedule(this::updateWatches, 10, TimeUnit.SECONDS); + } - timer = new Timer("WatchTimer"); - timer.schedule(new TimerTask() { - @Override - public void run() { - log.debug("File system actions (on watched folders) settled. Updating watches ..."); - walkTreeAndSetWatches(); - unregisterStaleWatches(); - } - }, 10000); + private void updateWatches() { + log.debug("File system actions settled. Updating watches..."); + walkTreeAndSetWatches(); + unregisterStaleWatches(); } private synchronized void walkTreeAndSetWatches() { @@ -210,7 +217,7 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx } }); } catch (IOException e) { - // Don't care + log.warn("Failed to walkTreeAndSetWatches {}", root, e); } }); @@ -243,7 +250,7 @@ private synchronized void registerWatch(Path dir) { WatchKey watchKey = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW); watchPathKeyMap.put(dir, watchKey); } catch (IOException e) { - // Don't care! + log.warn("Failed to register watch for {}", dir, e); } } } diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/AbstractMetaData.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/AbstractMetaData.java index 891edf73c..4f9b8ee04 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/AbstractMetaData.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/AbstractMetaData.java @@ -72,12 +72,16 @@ public ConcurrentMap getTree() { } public static boolean isVisible (ContentNode node) { - return node != null - // check if some parent is hidden - && !node.uri().startsWith(".") && !node.uri().contains("/.") - && node.isPublished() - && !node.isHidden() - && !node.isSection(); + + if (node == null || node.isSection()) { + return false; + } + + if (node.isParentPathHidden()) { + return false; + } + + return node.isVisible() && !node.isHidden(); } @Override diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/memory/MemoryQuery.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/memory/MemoryQuery.java index 2ae0e0cea..672cf3f65 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/memory/MemoryQuery.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/memory/MemoryQuery.java @@ -44,6 +44,7 @@ * @author t.marx * @param */ +@Deprecated(since = "2025.10") public class MemoryQuery extends ExtendableQuery { private QueryContext context; @@ -197,6 +198,11 @@ public Map> groupby(final String field) { return QueryUtil.groupby(context.getNodes(), field); } + @Override + public ContentQuery expression(String expressions) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + public static record Sort(String field, QueryContext context) implements ContentQuery.Sort { public MemoryQuery asc() { diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/DocumentHelper.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/DocumentHelper.java index 72f4d9ced..9b942aeb7 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/DocumentHelper.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/DocumentHelper.java @@ -57,7 +57,9 @@ public static void addAvailableFields (Document document) { public static void addData(final Document document, Map data) { var flatten = FlattenMap.flattenMap(data); - flatten.entrySet().forEach(entry -> { + flatten.entrySet().stream() + .filter(entry -> entry.getValue() != null) + .forEach(entry -> { switch (entry.getValue()) { case List listValue -> diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java index fd937a95a..ccdef967f 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java @@ -32,6 +32,8 @@ import com.condation.cms.filesystem.metadata.query.ExcerptMapperFunction; import com.condation.cms.filesystem.metadata.query.ExtendableQuery; import com.condation.cms.filesystem.metadata.query.Queries; +import com.condation.cms.filesystem.metadata.query.parser.Parser; +import com.condation.cms.filesystem.metadata.query.parser.expressions.Expression; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -76,6 +78,8 @@ enum Order { private List> extensionOperations = new ArrayList<>(); + private final Parser expressionsParser = new Parser(); + public LuceneQuery( final String startUri, final LuceneIndex index, @@ -275,6 +279,15 @@ public ContentQuery whereExists(String field) { QueryHelper.exists(queryBuilder, field); return this; } + + @Override + public ContentQuery expression (final String expression) { + Expression expAst = expressionsParser.parse(expression); + + QueryHelper.buildFromExpression(queryBuilder, expAst); + + return this; + } private ContentQuery where(final String field, final Queries.Operator operator, final Object value) { diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/QueryHelper.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/QueryHelper.java index 6d0cb23c1..1c94653b9 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/QueryHelper.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/QueryHelper.java @@ -21,10 +21,16 @@ * . * #L% */ - import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.utils.MapUtil; import com.condation.cms.filesystem.metadata.query.Queries; +import com.condation.cms.filesystem.metadata.query.parser.expressions.Condition; +import com.condation.cms.filesystem.metadata.query.parser.expressions.ContainsCondition; +import com.condation.cms.filesystem.metadata.query.parser.expressions.Expression; +import com.condation.cms.filesystem.metadata.query.parser.expressions.InCondition; +import com.condation.cms.filesystem.metadata.query.parser.expressions.Logical; +import com.condation.cms.filesystem.metadata.query.parser.expressions.LogicalOperator; +import com.condation.cms.filesystem.metadata.query.parser.values.Value; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -48,13 +54,9 @@ public class QueryHelper { public static void exists(BooleanQuery.Builder queryBuilder, String field) { - - if (true) { - queryBuilder.add( - new TermQuery(new Term("_fields", field)), - BooleanClause.Occur.FILTER); - return; - } + queryBuilder.add( + new TermQuery(new Term("_fields", field)), + BooleanClause.Occur.FILTER); } public static void lt(BooleanQuery.Builder queryBuilder, String field, Object value) { @@ -247,4 +249,123 @@ protected static List sorted(final List nodes, fin return tempNodes; } + + public static void buildFromExpression(BooleanQuery.Builder queryBuilder, Expression expression) { + if (expression instanceof Condition condition) { + buildCondition(queryBuilder, condition); + } else if (expression instanceof Logical logical) { + // Subqueries bauen + BooleanQuery.Builder leftBuilder = new BooleanQuery.Builder(); + buildFromExpression(leftBuilder, logical.left()); + + BooleanQuery.Builder rightBuilder = new BooleanQuery.Builder(); + buildFromExpression(rightBuilder, logical.right()); + + BooleanQuery subQuery = new BooleanQuery.Builder() + .add(leftBuilder.build(), toOccur(logical.operator())) + .add(rightBuilder.build(), toOccur(logical.operator())) + .build(); + + queryBuilder.add(subQuery, BooleanClause.Occur.MUST); + } else if (expression instanceof InCondition inCondition) { + buildInCondition(queryBuilder, inCondition); + } else if (expression instanceof ContainsCondition containsCondition) { + buildContainsCondition(queryBuilder, containsCondition); + } + } + + private static void buildInCondition(BooleanQuery.Builder queryBuilder, InCondition condition) { + var field = condition.field(); + var not = condition.negated(); + + exists(queryBuilder, field); + + var values = condition.values().stream().map(Value::get).toList(); + + if (not) { + in(queryBuilder, field, values, BooleanClause.Occur.MUST_NOT); + } else { + in(queryBuilder, field, values, BooleanClause.Occur.MUST); + } + } + + private static void buildContainsCondition(BooleanQuery.Builder queryBuilder, ContainsCondition condition) { + var field = condition.field(); + var not = condition.negated(); + + exists(queryBuilder, field); + + var values = condition.values().stream().map(Value::get).toList(); + if (values.isEmpty()) { + return; // nichts zu tun + } + + BooleanQuery.Builder containsBuilder = new BooleanQuery.Builder(); + for (Object item : values) { + Query termQuery = toQuery(field, item); + containsBuilder.add(termQuery, BooleanClause.Occur.SHOULD); + } + + Query containsQuery = containsBuilder.build(); + + if (not) { + // Variante A: KEINER der Werte darf vorkommen + // also: Dokumente ausschließen, die einen Treffer hätten + queryBuilder.add(containsQuery, BooleanClause.Occur.MUST_NOT); + } else { + // Variante B: Mindestens EIN Wert muss vorkommen + queryBuilder.add(containsQuery, BooleanClause.Occur.MUST); + } + } + + private static void buildCondition(BooleanQuery.Builder queryBuilder, Condition condition) { + String field = condition.field(); + Value value = condition.value(); + + exists(queryBuilder, field); + + switch (condition.operator()) { + case EQ -> + eq(queryBuilder, field, value.get(), BooleanClause.Occur.MUST); + case NEQ -> { + BooleanQuery.Builder notBuilder = new BooleanQuery.Builder(); + eq(notBuilder, field, value.get(), BooleanClause.Occur.MUST); + queryBuilder.add(notBuilder.build(), BooleanClause.Occur.MUST_NOT); + } + case GT -> + gt(queryBuilder, field, value.get()); + case GTE -> + gte(queryBuilder, field, value.get()); + case LT -> + lt(queryBuilder, field, value.get()); + case LTE -> + lte(queryBuilder, field, value.get()); + case IN -> + in(queryBuilder, field, value.get(), BooleanClause.Occur.MUST); + case NOT_IN -> { + BooleanQuery.Builder notInBuilder = new BooleanQuery.Builder(); + in(notInBuilder, field, value.get(), BooleanClause.Occur.MUST); + queryBuilder.add(notInBuilder.build(), BooleanClause.Occur.MUST_NOT); + } + case CONTAINS -> + contains(queryBuilder, field, value.get(), BooleanClause.Occur.MUST); + case CONTAINS_NOT -> { + BooleanQuery.Builder notContains = new BooleanQuery.Builder(); + contains(notContains, field, value.get(), BooleanClause.Occur.MUST); + queryBuilder.add(notContains.build(), BooleanClause.Occur.MUST_NOT); + } + default -> + throw new UnsupportedOperationException("Unsupported operator: " + condition.operator()); + } + } + + private static BooleanClause.Occur toOccur(LogicalOperator operator) { + return switch (operator) { + case AND -> + BooleanClause.Occur.MUST; + case OR -> + BooleanClause.Occur.SHOULD; + }; + } + } diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/Queries.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/Queries.java index 0bda577a0..8d268ae81 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/Queries.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/Queries.java @@ -104,29 +104,17 @@ public static Operator operator4String(final String operator) { return Operator.EQ; } return switch (operator) { - case "=" -> + case "=", "eq" -> Operator.EQ; - case "eq" -> - Operator.EQ; - case "!=" -> - Operator.NOT_EQ; - case "not eq" -> + case "!=", "not eq" -> Operator.NOT_EQ; - case ">" -> - Operator.GT; - case "gt" -> + case ">", "gt" -> Operator.GT; - case ">=" -> + case ">=", "gte" -> Operator.GTE; - case "gte" -> - Operator.GTE; - case "<" -> - Operator.LT; - case "lt" -> + case "<", "lt" -> Operator.LT; - case "<=" -> - Operator.LTE; - case "lte" -> + case "<=", "lte" -> Operator.LTE; case "in" -> Operator.IN; diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/ComparisonOperator.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/ComparisonOperator.java new file mode 100644 index 000000000..b1ec33ec0 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/ComparisonOperator.java @@ -0,0 +1,63 @@ +package com.condation.cms.filesystem.metadata.query.parser; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +public enum ComparisonOperator { + EQ("="), + NE("!="), + LT("<"), + LE("<="), + GT(">"), + GE(">="), + IN("IN"), + NOT_IN("NOT IN"), + CONTAINS("CONTAINS"), + CONTAINS_NOT("CONTAINS NOT"); + + private final String symbol; + + ComparisonOperator(String symbol) { + this.symbol = symbol; + } + + @Override + public String toString() { + return symbol; + } + + public static ComparisonOperator fromToken(String token) { + return switch (token.toUpperCase()) { + case "=" -> EQ; + case "!=" -> NE; + case "<" -> LT; + case "<=" -> LE; + case ">" -> GT; + case ">=" -> GE; + case "IN" -> IN; + case "NOT IN" -> NOT_IN; + case "CONTAINS" -> CONTAINS; + case "CONTAINS NOT" -> CONTAINS_NOT; + default -> throw new IllegalArgumentException("Unknown operator: " + token); + }; + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/Parser.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/Parser.java new file mode 100644 index 000000000..43659c30d --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/Parser.java @@ -0,0 +1,173 @@ +package com.condation.cms.filesystem.metadata.query.parser; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.filesystem.metadata.query.parser.expressions.Expression; +import com.condation.cms.filesystem.metadata.query.parser.expressions.Condition; +import com.condation.cms.filesystem.metadata.query.parser.expressions.ContainsCondition; +import com.condation.cms.filesystem.metadata.query.parser.expressions.Logical; +import com.condation.cms.filesystem.metadata.query.parser.expressions.InCondition; +import com.condation.cms.filesystem.metadata.query.parser.expressions.LogicalOperator; +import com.condation.cms.filesystem.metadata.query.parser.expressions.Operator; +import com.condation.cms.filesystem.metadata.query.parser.values.NumberValue; +import com.condation.cms.filesystem.metadata.query.parser.values.BooleanValue; +import com.condation.cms.filesystem.metadata.query.parser.values.Value; +import com.condation.cms.filesystem.metadata.query.parser.values.StringValue; + +import java.util.ArrayList; +import java.util.List; + +public class Parser { + + private static class Context { + private final List tokens; + private int pos = 0; + + Context(List tokens) { + this.tokens = tokens; + } + + Token current() { + return tokens.get(pos); + } + + boolean match(String token) { + if (pos < tokens.size() && tokens.get(pos).text().equalsIgnoreCase(token)) { + pos++; + return true; + } + return false; + } + + Token consume() { + if (pos >= tokens.size()) { + throw new RuntimeException("Unexpected end"); + } + return tokens.get(pos++); + } + + boolean hasNext() { + return pos < tokens.size(); + } + } + + public Expression parse(String input) { + var context = new Context(Tokenizer.tokenize(input)); + Expression expr = parseOr(context); + if (context.current().type() != TokenType.EOF) { + throw new RuntimeException("Unexpected token: " + context.current()); + } + return expr; + } + + private Expression parseOr(Context context) { + Expression expr = parseAnd(context); + while (context.match("OR")) { + Expression right = parseAnd(context); + expr = new Logical(expr, LogicalOperator.OR, right); + } + return expr; + } + + private Expression parseAnd(Context context) { + Expression expr = parsePrimary(context); + while (context.match("AND")) { + Expression right = parsePrimary(context); + expr = new Logical(expr, LogicalOperator.AND, right); + } + return expr; + } + + private Expression parsePrimary(Context context) { + if (context.match("(")) { + Expression expr = parseOr(context); + if (!context.match(")")) { + throw new RuntimeException("Missing closing parenthesis"); + } + return expr; + } + return parseComparison(context); + } + + private Expression parseComparison(Context context) { + String field = context.consume().text(); + String op = context.consume().text().toUpperCase(); + + switch (op) { + case "IN": + return parseInCondition(context, field, false); + case "NOT IN": + return parseInCondition(context, field, true); + case "CONTAINS": + return parseContainsCondition(context, field, false); + case "CONTAINS NOT": + return parseContainsCondition(context, field, true); + default: + // alle Standardoperatoren (=, !=, <, <=, >, >=) + return new Condition(field, Operator.forName(op), parseValue(context.consume().text())); + } + } + + private ContainsCondition parseContainsCondition(Context context, String field, boolean negated) { + if (!context.match("(")) { + throw new RuntimeException("Expected '(' after CONTAINS/CONTAINS NOT"); + } + List values = new ArrayList<>(); + do { + values.add(parseValue(context.consume().text())); + } while (context.match(",")); + if (!context.match(")")) { + throw new RuntimeException("Missing closing ')' in CONTAINS/CONTAINS NOT"); + } + return new ContainsCondition(field, values, negated); + } + + private InCondition parseInCondition(Context context, String field, boolean negated) { + if (!context.match("(")) { + throw new RuntimeException("Expected '(' after IN/NOT IN"); + } + List values = new ArrayList<>(); + do { + values.add(parseValue(context.consume().text())); + } while (context.match(",")); + if (!context.match(")")) { + throw new RuntimeException("Missing closing ')' in IN/NOT IN"); + } + return new InCondition(field, values, negated); + } + + private Value parseValue(String token) { + String lower = token.toLowerCase(); + if (lower.equals("true") || lower.equals("false")) { + return new BooleanValue(Boolean.valueOf(lower)); + } + try { + if (token.contains(".")) { + return new NumberValue(Double.valueOf(token)); + } else { + return new NumberValue(Integer.valueOf(token)); + } + } catch (NumberFormatException e) { + return new StringValue(token); + } + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/QueryParserDemo.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/QueryParserDemo.java new file mode 100644 index 000000000..9156a20b0 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/QueryParserDemo.java @@ -0,0 +1,47 @@ +package com.condation.cms.filesystem.metadata.query.parser; + +import com.condation.cms.filesystem.metadata.query.parser.expressions.Expression; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public class QueryParserDemo { + + public static void main(String[] args) throws Exception { + Parser parser = new Parser(); + String query = "(name LIKE 'Th%' OR age >= 30) AND active != false"; + Expression expr = parser.parse(query); + System.out.println(expr); + + query = "status IN ('NEW','OPEN','CLOSED') AND age >= 18"; + expr = parser.parse(query); + System.out.println(expr); + + query = "(status IN ('NEW','OPEN') OR age >= 18) AND active = true AND score > 99.5"; + expr = parser.parse(query); + System.out.println(expr); + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/Token.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/Token.java new file mode 100644 index 000000000..d9f49d372 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/Token.java @@ -0,0 +1,30 @@ +package com.condation.cms.filesystem.metadata.query.parser; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +public record Token(TokenType type, String text) { + @Override + public String toString() { + return type + "(" + text + ")"; + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/TokenType.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/TokenType.java new file mode 100644 index 000000000..0f9fc82e8 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/TokenType.java @@ -0,0 +1,39 @@ +package com.condation.cms.filesystem.metadata.query.parser; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +public enum TokenType { + IDENTIFIER, // z.B. name, age, country + STRING, // 'Thorsten' + NUMBER, // 123, 45.67 + BOOLEAN, // true, false + OPERATOR, // =, !=, <, <=, >, >=, IN, NOT IN, CONTAINS, CONTAINS NOT + LPAREN, // ( + RPAREN, // ) + COMMA, // , + AND, // AND + OR, // OR + EOF // Ende +} + + diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/Tokenizer.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/Tokenizer.java new file mode 100644 index 000000000..93f8963e2 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/Tokenizer.java @@ -0,0 +1,181 @@ +package com.condation.cms.filesystem.metadata.query.parser; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.ArrayList; +import java.util.List; + +public class Tokenizer { + private final String input; + private int pos = 0; + + public Tokenizer(String input) { + this.input = input; + } + + public static List tokenize (String input) { + return new Tokenizer(input).tokenize(); + } + + public List tokenize() { + List tokens = new ArrayList<>(); + while (pos < input.length()) { + char c = input.charAt(pos); + + if (Character.isWhitespace(c)) { + pos++; + continue; + } + + if (c == '(') { + tokens.add(new Token(TokenType.LPAREN, "(")); + pos++; + continue; + } + if (c == ')') { + tokens.add(new Token(TokenType.RPAREN, ")")); + pos++; + continue; + } + if (c == ',') { + tokens.add(new Token(TokenType.COMMA, ",")); + pos++; + continue; + } + + if (c == '\'' || c == '"') { // String literal + tokens.add(new Token(TokenType.STRING, readString(c))); + continue; + } + + if (Character.isDigit(c)) { // Number literal + tokens.add(new Token(TokenType.NUMBER, readNumber())); + continue; + } + + // Allow identifiers to start with letter OR underscore + if (Character.isLetter(c) || c == '_') { + String word = readWord(); + + switch (word.toUpperCase()) { + case "AND" -> tokens.add(new Token(TokenType.AND, word)); + case "OR" -> tokens.add(new Token(TokenType.OR, word)); + case "TRUE", "FALSE" -> tokens.add(new Token(TokenType.BOOLEAN, word.toLowerCase())); + case "IN", "NOT", "CONTAINS" -> { + // später schauen, ob zusammengesetzt + tokens.add(new Token(TokenType.OPERATOR, word.toUpperCase())); + } + default -> tokens.add(new Token(TokenType.IDENTIFIER, word)); + } + continue; + } + + if (isOperatorStart(c)) { + tokens.add(new Token(TokenType.OPERATOR, readOperator())); + continue; + } + + throw new RuntimeException("Unexpected character: " + c); + } + + // zusammengesetzte Operatoren zusammenfassen + List merged = mergeMultiWordOperators(tokens); + + merged.add(new Token(TokenType.EOF, "")); + return merged; + } + + private String readString(char quote) { + pos++; // skip opening quote + StringBuilder sb = new StringBuilder(); + while (pos < input.length() && input.charAt(pos) != quote) { + sb.append(input.charAt(pos++)); + } + pos++; // skip closing quote + return sb.toString(); + } + + private String readNumber() { + StringBuilder sb = new StringBuilder(); + while (pos < input.length() && + (Character.isDigit(input.charAt(pos)) || input.charAt(pos) == '.')) { + sb.append(input.charAt(pos++)); + } + return sb.toString(); + } + + // accept letters, digits, underscore and hyphen in identifiers (e.g. number2, my_var) + private String readWord() { + StringBuilder sb = new StringBuilder(); + while (pos < input.length()) { + char c = input.charAt(pos); + if (Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.') { + sb.append(c); + pos++; + } else { + break; + } + } + return sb.toString(); + } + + private boolean isOperatorStart(char c) { + return "=!<>".indexOf(c) >= 0; + } + + private String readOperator() { + if (pos + 1 < input.length()) { + String two = input.substring(pos, pos + 2); + if (two.equals("!=") || two.equals("<=") || two.equals(">=")) { + pos += 2; + return two; + } + } + return String.valueOf(input.charAt(pos++)); + } + + private List mergeMultiWordOperators(List tokens) { + List merged = new ArrayList<>(); + for (int i = 0; i < tokens.size(); i++) { + Token t = tokens.get(i); + + if (t.type() == TokenType.OPERATOR && t.text().equalsIgnoreCase("NOT")) { + if (i + 1 < tokens.size() && tokens.get(i + 1).text().equalsIgnoreCase("IN")) { + merged.add(new Token(TokenType.OPERATOR, "NOT IN")); + i++; + continue; + } + } + if (t.type() == TokenType.OPERATOR && t.text().equalsIgnoreCase("CONTAINS")) { + if (i + 1 < tokens.size() && tokens.get(i + 1).text().equalsIgnoreCase("NOT")) { + merged.add(new Token(TokenType.OPERATOR, "CONTAINS NOT")); + i++; + continue; + } + } + + merged.add(t); + } + return merged; + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Condition.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Condition.java new file mode 100644 index 000000000..3291b6b56 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Condition.java @@ -0,0 +1,36 @@ +package com.condation.cms.filesystem.metadata.query.parser.expressions; + +import com.condation.cms.filesystem.metadata.query.parser.values.Value; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public record Condition (String field, Operator operator, Value value) implements Expression { + @Override + public String toString() { + return field + " " + operator + " " + value; + } +} diff --git a/cms-git/src/main/java/com/condation/cms/git/Config.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/ContainsCondition.java similarity index 52% rename from cms-git/src/main/java/com/condation/cms/git/Config.java rename to cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/ContainsCondition.java index 3fbb626cf..25bb0755f 100644 --- a/cms-git/src/main/java/com/condation/cms/git/Config.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/ContainsCondition.java @@ -1,10 +1,10 @@ -package com.condation.cms.git; +package com.condation.cms.filesystem.metadata.query.parser.expressions; /*- * #%L - * cms-git + * cms-filesystem * %% - * Copyright (C) 2023 - 2024 CondationCMS + * Copyright (C) 2023 - 2025 CondationCMS * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -22,32 +22,16 @@ * #L% */ - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; +import com.condation.cms.filesystem.metadata.query.parser.values.Value; import java.util.List; -import java.util.Optional; -import lombok.Getter; -import lombok.Setter; -import org.yaml.snakeyaml.Yaml; - -/** - * - * @author t.marx - */ -public class Config { +import java.util.stream.Collectors; - public static Config load (final Path file) throws IOException { - Yaml yaml = new Yaml(); - return yaml.loadAs(Files.readString(file), Config.class); - } - - @Getter - @Setter - List repos; - - public Optional find (String name) { - return repos.stream().filter(repo -> repo.getName().equals(name)).findFirst(); - } +public record ContainsCondition (String field, List values, boolean negated) implements Expression { + @Override + public String toString() { + String op = negated ? "CONTAINS NOT" : "CONTAINS"; + return field + " " + op + " (" + + values.stream().map(Object::toString).collect(Collectors.joining(", ")) + + ")"; + } } diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Expression.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Expression.java new file mode 100644 index 000000000..4d6973a82 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Expression.java @@ -0,0 +1,31 @@ +package com.condation.cms.filesystem.metadata.query.parser.expressions; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public interface Expression { + +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/InCondition.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/InCondition.java new file mode 100644 index 000000000..248227a49 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/InCondition.java @@ -0,0 +1,37 @@ +package com.condation.cms.filesystem.metadata.query.parser.expressions; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.filesystem.metadata.query.parser.values.Value; +import java.util.List; +import java.util.stream.Collectors; + +public record InCondition (String field, List values, boolean negated) implements Expression { + @Override + public String toString() { + String op = negated ? "NOT IN" : "IN"; + return field + " " + op + " (" + + values.stream().map(Object::toString).collect(Collectors.joining(", ")) + + ")"; + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Logical.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Logical.java new file mode 100644 index 000000000..aacf3ac42 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Logical.java @@ -0,0 +1,36 @@ +package com.condation.cms.filesystem.metadata.query.parser.expressions; + +import lombok.Getter; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +/** + * + * @author thorstenmarx + */ +public record Logical (Expression left, LogicalOperator operator, Expression right) implements Expression { + + @Override + public String toString() { + return "(" + left + " " + operator + " " + right + ")"; + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/LogicalOperator.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/LogicalOperator.java new file mode 100644 index 000000000..6fd009862 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/LogicalOperator.java @@ -0,0 +1,36 @@ +package com.condation.cms.filesystem.metadata.query.parser.expressions; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +public enum LogicalOperator { + AND, OR; + + public static LogicalOperator forName (String name) { + for (var op : values()) { + if (op.name().equalsIgnoreCase(name)) { + return op; + } + } + return null; + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Operator.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Operator.java new file mode 100644 index 000000000..b42cb7147 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/expressions/Operator.java @@ -0,0 +1,45 @@ +package com.condation.cms.filesystem.metadata.query.parser.expressions; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + + +public enum Operator { + EQ, NEQ, GT, GTE, LT, LTE, IN, NOT_IN, CONTAINS, CONTAINS_NOT; + + public static Operator forName (String name) { + switch (name.toLowerCase()) { + case "=": return EQ; + case "!=": return NEQ; + case ">": return GT; + case ">=": return GTE; + case "<": return LT; + case "<=": return LTE; + } + for (var op : values()) { + if (op.name().equalsIgnoreCase(name)) { + return op; + } + } + return null; + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/BooleanValue.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/BooleanValue.java new file mode 100644 index 000000000..6deb4eade --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/BooleanValue.java @@ -0,0 +1,40 @@ +package com.condation.cms.filesystem.metadata.query.parser.values; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +public class BooleanValue implements Value { + + private final Boolean value; + + public BooleanValue(Boolean value) { + this.value = value; + } + + @Override + public Boolean get() { + return value; + } + + public String toString() { + return value.toString(); + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/NumberValue.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/NumberValue.java new file mode 100644 index 000000000..a8315763e --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/NumberValue.java @@ -0,0 +1,40 @@ +package com.condation.cms.filesystem.metadata.query.parser.values; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +public class NumberValue implements Value { + + private final Number value; + + public NumberValue(Number value) { + this.value = value; + } + + @Override + public Number get() { + return value; + } + + public String toString() { + return value.toString(); + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/StringValue.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/StringValue.java new file mode 100644 index 000000000..f62978f8d --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/StringValue.java @@ -0,0 +1,40 @@ +package com.condation.cms.filesystem.metadata.query.parser.values; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +public class StringValue implements Value { + + private final String value; + + public StringValue(String value) { + this.value = value; + } + + @Override + public String get() { + return value; + } + + public String toString() { + return "'" + value + "'"; + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/Value.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/Value.java new file mode 100644 index 000000000..c35b7646e --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/query/parser/values/Value.java @@ -0,0 +1,32 @@ +package com.condation.cms.filesystem.metadata.query.parser.values; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public interface Value { + + T get(); +} diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/PresistentFileSystemTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/PresistentFileSystemTest.java index 65ad2d711..2edfaf652 100644 --- a/cms-filesystem/src/test/java/com/condation/cms/filesystem/PresistentFileSystemTest.java +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/PresistentFileSystemTest.java @@ -74,12 +74,18 @@ static void shutdown () throws IOException { public void test_query() throws IOException { var nodes = fileSystem.query((node, i) -> node).where("featured", true).get(); Assertions.assertThat(nodes).hasSize(2); + + nodes = fileSystem.query((node, i) -> node).expression("featured = true").get(); + Assertions.assertThat(nodes).hasSize(2); } @Test public void test_query_in() throws IOException { var nodes = fileSystem.query((node, i) -> node).whereIn("name", "test1", "test2").get(); Assertions.assertThat(nodes).hasSize(2); + + nodes = fileSystem.query((node, i) -> node).expression("name IN ('test1', 'test2')").get(); + Assertions.assertThat(nodes).hasSize(2); } @Test @@ -87,17 +93,27 @@ public void test_query_not_in() throws IOException { var nodes = fileSystem.query((node, i) -> node).whereNotIn("name", "test1", "test2").get(); Assertions.assertThat(nodes).hasSize(1); Assertions.assertThat(nodes.get(0).data().get("name")).isEqualTo("start"); + + nodes = fileSystem.query((node, i) -> node).expression("name NOT IN ('test1', 'test2')").get(); + Assertions.assertThat(nodes).hasSize(1); + Assertions.assertThat(nodes.get(0).data().get("name")).isEqualTo("start"); } @Test public void test_query_contains() throws IOException { var nodes = fileSystem.query((node, i) -> node).whereContains("taxonomy.tags", "eins").get(); Assertions.assertThat(nodes).hasSize(1); + + nodes = fileSystem.query((node, i) -> node).expression("taxonomy.tags CONTAINS ('eins')").get(); + Assertions.assertThat(nodes).hasSize(1); } @Test public void test_query_contains_not() throws IOException { var nodes = fileSystem.query((node, i) -> node).whereNotContains("taxonomy.tags", "eins").get(); Assertions.assertThat(nodes).hasSize(1); + + nodes = fileSystem.query((node, i) -> node).expression("taxonomy.tags CONTAINS NOT ('eins')").get(); + Assertions.assertThat(nodes).hasSize(1); } @Test @@ -107,6 +123,12 @@ public void test_lt_lte() throws IOException { nodes = fileSystem.query((node, i) -> node).where("number2", "lte", 5).get(); Assertions.assertThat(nodes).hasSize(1); + + nodes = fileSystem.query((node, i) -> node).expression("number2 < 5").get(); + Assertions.assertThat(nodes).hasSize(0); + + nodes = fileSystem.query((node, i) -> node).expression("number2 <= 5").get(); + Assertions.assertThat(nodes).hasSize(1); } @Test diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/metadata/query/parser/ParserTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/metadata/query/parser/ParserTest.java new file mode 100644 index 000000000..b81ae736c --- /dev/null +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/metadata/query/parser/ParserTest.java @@ -0,0 +1,122 @@ +package com.condation.cms.filesystem.metadata.query.parser; + +/*- + * #%L + * cms-filesystem + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.filesystem.metadata.query.parser.expressions.Expression; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ParserTest { + + private final Parser parser = new Parser(); + + @Test + void parsesSimpleEqualsString() { + Expression expr = parser.parse("name = 'Thorsten'"); + assertThat(expr.toString()).isEqualTo("name EQ 'Thorsten'"); + } + + @Test + void parses_complex_name() { + Expression expr = parser.parse("name.test.demo = 'Thorsten'"); + assertThat(expr.toString()).isEqualTo("name.test.demo EQ 'Thorsten'"); + } + + @Test + void parsesNumberComparison() { + Expression expr = parser.parse("age >= 30"); + assertThat(expr.toString()).isEqualTo("age GTE 30"); + } + + @Test + void parsesBooleanComparison() { + Expression expr = parser.parse("active = true"); + assertThat(expr.toString()).isEqualTo("active EQ true"); + } + + @Test + void parsesAndOrOperators() { + Expression expr = parser.parse("age > 18 AND active = true OR name = 'Anna'"); + assertThat(expr.toString()).isEqualTo("((age GT 18 AND active EQ true) OR name EQ 'Anna')"); + } + + @Test + void parsesWithParentheses() { + Expression expr = parser.parse("(age > 18 AND active = true) OR (name = 'Anna' AND country = 'DE')"); + assertThat(expr.toString()).isEqualTo("((age GT 18 AND active EQ true) OR (name EQ 'Anna' AND country EQ 'DE'))"); + } + + @Test + void parsesInWithStrings() { + Expression expr = parser.parse("country IN ('DE', 'FR', 'US')"); + assertThat(expr.toString()).isEqualTo("country IN ('DE', 'FR', 'US')"); + } + + @Test + void parsesInWithNumbers() { + Expression expr = parser.parse("id IN (1, 2, 3, 4)"); + assertThat(expr.toString()).isEqualTo("id IN (1, 2, 3, 4)"); + } + + @Test + void parsesNestedParentheses() { + Expression expr = parser.parse("((age > 18 AND active = true) OR (country = 'DE')) AND premium = false"); + assertThat(expr.toString()).isEqualTo("(((age GT 18 AND active EQ true) OR country EQ 'DE') AND premium EQ false)"); + } + + @Test + void parsesNotEquals() { + Expression expr = parser.parse("name != 'Peter'"); + assertThat(expr.toString()).isEqualTo("name NEQ 'Peter'"); + } + + @Test + void parsesLessOrEqual() { + Expression expr = parser.parse("age <= 65"); + assertThat(expr.toString()).isEqualTo("age LTE 65"); + } + + @Test + void parsesGreaterOrEqual() { + Expression expr = parser.parse("age >= 21"); + assertThat(expr.toString()).isEqualTo("age GTE 21"); + } + + @Test + void parsesContains() { + Expression expr = parser.parse("name CONTAINS ('Thor')"); + assertThat(expr.toString()).isEqualTo("name CONTAINS ('Thor')"); + } + + @Test + void parsesContainsNot() { + Expression expr = parser.parse("name CONTAINS NOT ('Max')"); + assertThat(expr.toString()).isEqualTo("name CONTAINS NOT ('Max')"); + } + + @Test + void parsesNotIn() { + Expression expr = parser.parse("country NOT IN ('DE', 'FR')"); + assertThat(expr.toString()).isEqualTo("country NOT IN ('DE', 'FR')"); + } +} diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/query/QueryPerfTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/query/QueryPerfTest.java index f07e2043f..35bb9a00c 100644 --- a/cms-filesystem/src/test/java/com/condation/cms/filesystem/query/QueryPerfTest.java +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/query/QueryPerfTest.java @@ -56,6 +56,7 @@ public static void setup() { var node = new ContentNode("/test" + i, "test2.md", Map.of( "article", Map.of("featured", (i % 2 == 0 ? true : false)), "index", i, + Constants.MetaFields.PUBLISHED, true, Constants.MetaFields.PUBLISH_DATE, Date.from(Instant.now().minus(1, ChronoUnit.DAYS)), "tags", List.of("one", "two")) ); diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/query/QueryTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/query/QueryTest.java index 67d3cc4c9..93cf34923 100644 --- a/cms-filesystem/src/test/java/com/condation/cms/filesystem/query/QueryTest.java +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/query/QueryTest.java @@ -49,15 +49,18 @@ public static void setup() { nodes = new ArrayList<>(); ContentNode node = new ContentNode("/", "index.md", Map.of( "featured", true, + Constants.MetaFields.PUBLISHED, true, Constants.MetaFields.PUBLISH_DATE, Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))); nodes.add(node); node = new ContentNode("/2", "index2.md", Map.of( "featured", true, + Constants.MetaFields.PUBLISHED, true, Constants.MetaFields.PUBLISH_DATE, Date.from(Instant.now().minus(1, ChronoUnit.DAYS)))); nodes.add(node); node = new ContentNode("/test1", "test1.md", Map.of( "featured", false, "index", 1, + Constants.MetaFields.PUBLISHED, true, Constants.MetaFields.PUBLISH_DATE, Date.from(Instant.now().minus(1, ChronoUnit.DAYS)), "tags", List.of("three", "four") )); @@ -65,6 +68,7 @@ public static void setup() { node = new ContentNode("/test2", "test2.md", Map.of( "featured", false, "index", 2, + Constants.MetaFields.PUBLISHED, true, Constants.MetaFields.PUBLISH_DATE, Date.from(Instant.now().minus(1, ChronoUnit.DAYS)), "tags", List.of("one", "two")) ); @@ -73,6 +77,7 @@ public static void setup() { node = new ContentNode("/json", "test-json.md", Map.of( "featured", false, "index", 2, + Constants.MetaFields.PUBLISHED, true, Constants.MetaFields.PUBLISH_DATE, Date.from(Instant.now().minus(1, ChronoUnit.DAYS)), "tags", List.of("one", "two"), "content", Map.of("type", Constants.ContentTypes.JSON)) diff --git a/cms-filesystem/src/test/resources/content/index.md b/cms-filesystem/src/test/resources/content/index.md index e72167977..51b3cd6f3 100644 --- a/cms-filesystem/src/test/resources/content/index.md +++ b/cms-filesystem/src/test/resources/content/index.md @@ -1,4 +1,5 @@ featured: true publish_date: 2024-09-23 name: start -keywords: ["eins", "zwei"] \ No newline at end of file +keywords: ["eins", "zwei"] +published: true \ No newline at end of file diff --git a/cms-filesystem/src/test/resources/content/test/test1.md b/cms-filesystem/src/test/resources/content/test/test1.md index 05acf4b04..4ad29432c 100644 --- a/cms-filesystem/src/test/resources/content/test/test1.md +++ b/cms-filesystem/src/test/resources/content/test/test1.md @@ -5,4 +5,5 @@ taxonomy: number1: 1 number2: 5 publish_date: 2024-10-01 -keywords: ["eins", "zwei"] \ No newline at end of file +keywords: ["eins", "zwei"] +published: true \ No newline at end of file diff --git a/cms-filesystem/src/test/resources/content/test/test2.md b/cms-filesystem/src/test/resources/content/test/test2.md index 426de733d..07c8fc636 100644 --- a/cms-filesystem/src/test/resources/content/test/test2.md +++ b/cms-filesystem/src/test/resources/content/test/test2.md @@ -2,4 +2,5 @@ name: test2 featured: false taxonomy: tags: [zwei, drei] -publish_date: 2024-11-04 \ No newline at end of file +publish_date: 2024-11-04 +published: true \ No newline at end of file diff --git a/cms-git/git.yaml b/cms-git/git.yaml deleted file mode 100644 index 8c8ea8751..000000000 --- a/cms-git/git.yaml +++ /dev/null @@ -1,11 +0,0 @@ -## YAML Template. ---- -repos: - - name: "test-repo" - credentials: - username: thmarx - password: "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" - uri: "https://github.com/thmarx/test-repo.git" - folder: "target/sites/test-repo" - branch: "main" - cron: "0/10 * * * * ?" \ No newline at end of file diff --git a/cms-git/pom.xml b/cms-git/pom.xml deleted file mode 100644 index 576fae724..000000000 --- a/cms-git/pom.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - 4.0.0 - - com.condation.cms - cms-parent - 7.8.0 - - cms-git - jar - - - com.condation.cms - cms-api - - - org.eclipse.jgit - org.eclipse.jgit - - - org.projectlombok - lombok - provided - - - org.yaml - snakeyaml - - - org.quartz-scheduler - quartz - - - com.zaxxer - HikariCP-java7 - - - com.mchange - c3p0 - - - - - \ No newline at end of file diff --git a/cms-git/src/main/java/com/condation/cms/git/RepositoryManager.java b/cms-git/src/main/java/com/condation/cms/git/RepositoryManager.java deleted file mode 100644 index 64de68b61..000000000 --- a/cms-git/src/main/java/com/condation/cms/git/RepositoryManager.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.condation.cms.git; - -/*- - * #%L - * cms-git - * %% - * Copyright (C) 2023 - 2024 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - -import com.condation.cms.git.tasks.CloneTask; -import java.nio.file.Files; -import java.nio.file.Path; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.quartz.Scheduler; - -/** - * - * @author t.marx - */ -@RequiredArgsConstructor -@Slf4j -public class RepositoryManager { - - private final Scheduler scheduler; - - Config config; - - GitScheduler gitScheduler; - - public void init(final Path configFile) throws Exception { - if (!Files.exists(configFile)) { - log.info("no repository configuration found"); - return; - } - config = Config.load(configFile); - gitScheduler = new GitScheduler(scheduler); - - if (config.getRepos() != null) { - log.debug("initial clone repositories"); - for (var repo : config.getRepos()) { - log.debug("clone {}", repo.getName()); - var result = new CloneTask(repo).call(); - try { - log.debug("result : {} ", result); - log.debug("schedule repo"); - gitScheduler.schedule(repo); - } catch (Exception ex) { - log.error("error cloneing repository", ex); - } - } - - } - } - - public void updateRepo (String name) { - log.debug("try updating git repo: {}", name); - if (config == null) { - log.warn("config not loaded"); - return; - } - var repo = config.find(name); - if (repo.isEmpty()) { - log.warn("repository {} not found", name); - return; - } - log.debug("updating git repo: {}", name); - new UpdateRepoJob().execute(repo.get()); - log.debug("get repo {} updated", name); - } -} diff --git a/cms-git/src/main/java/com/condation/cms/git/UpdateRepoJob.java b/cms-git/src/main/java/com/condation/cms/git/UpdateRepoJob.java deleted file mode 100644 index 3fc0c36e1..000000000 --- a/cms-git/src/main/java/com/condation/cms/git/UpdateRepoJob.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.condation.cms.git; - -/*- - * #%L - * cms-git - * %% - * Copyright (C) 2023 - 2024 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ -import com.condation.cms.git.tasks.FetchTask; -import com.condation.cms.git.tasks.MergeTask; -import com.condation.cms.git.tasks.ResetTask; -import lombok.extern.slf4j.Slf4j; -import org.quartz.Job; -import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; - -/** - * - * @author t.marx - */ -@Slf4j -public class UpdateRepoJob implements Job { - - @Override - public void execute(JobExecutionContext context) throws JobExecutionException { - - Repo repo = (Repo) context.getJobDetail().getJobDataMap().get("repo"); - - execute(repo); - } - - protected void execute(Repo repo) { - try { - var fetch = new FetchTask(repo).call(); - - if (fetch) { - log.debug("fetch {} done", repo.getName()); - var merge = new MergeTask(repo).call(); - if (merge) { - log.debug("{} merged", repo.getName()); - } else { - log.error("merge {} error", repo.getName()); - var reset = new ResetTask(repo).call(); - log.debug("reset {}", repo.getName()); - } - } else { - log.error("fetch {} error", repo.getName()); - } - } catch (Exception e) { - log.error(null, e); - } - } -} diff --git a/cms-git/src/main/java/com/condation/cms/git/tasks/CloneTask.java b/cms-git/src/main/java/com/condation/cms/git/tasks/CloneTask.java deleted file mode 100644 index 7e09c2cc4..000000000 --- a/cms-git/src/main/java/com/condation/cms/git/tasks/CloneTask.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.condation.cms.git.tasks; - -import com.condation.cms.api.utils.ServerUtil; - -/*- - * #%L - * cms-git - * %% - * Copyright (C) 2023 - 2024 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - - -import com.condation.cms.git.Repo; -import com.condation.cms.git.Task; -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; - -/** - * - * @author t.marx - */ -@Slf4j -@RequiredArgsConstructor -public class CloneTask implements Task { - - private final Repo repo; - - @Override - public Boolean call() throws Exception { - - Path targetFolder = ServerUtil.getPath(repo.getFolder()); - if (Files.exists(targetFolder)) { - log.trace("repository already cloned"); - return Boolean.TRUE; - } - - UsernamePasswordCredentialsProvider credentialProvider = new UsernamePasswordCredentialsProvider( - repo.getCredentials().getUsername(), - repo.getCredentials().getPassword() - ); - - Git result = null; - try { - result = Git.cloneRepository() - .setURI(repo.getUri()) - .setDirectory(targetFolder.toFile()) - .setBranchesToClone(Arrays.asList("refs/heads/" + repo.getBranch())) - .setBranch("refs/heads/" + repo.getBranch()) - .setCredentialsProvider(credentialProvider) - .call(); - - return true; - } finally { - if (result != null) { - result.close(); - } - } - } -} diff --git a/cms-git/src/main/java/com/condation/cms/git/tasks/FetchTask.java b/cms-git/src/main/java/com/condation/cms/git/tasks/FetchTask.java deleted file mode 100644 index f22748730..000000000 --- a/cms-git/src/main/java/com/condation/cms/git/tasks/FetchTask.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.condation.cms.git.tasks; - -import com.condation.cms.api.utils.ServerUtil; - -/*- - * #%L - * cms-git - * %% - * Copyright (C) 2023 - 2024 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - -import com.condation.cms.git.Repo; -import com.condation.cms.git.Task; -import java.nio.file.Files; -import java.nio.file.Path; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; - -/** - * - * @author t.marx - */ -@RequiredArgsConstructor -@Slf4j -public class FetchTask implements Task { - - private final Repo repo; - - @Override - public Boolean call() throws Exception { - - Path targetFolder = ServerUtil.getPath(repo.getFolder()); - if (!Files.exists(targetFolder)) { - log.trace("target folder does not exists"); - return Boolean.FALSE; - } - - UsernamePasswordCredentialsProvider credentialProvider = new UsernamePasswordCredentialsProvider( - repo.getCredentials().getUsername(), - repo.getCredentials().getPassword() - ); - - Git git_repo = Git.open(targetFolder.toFile()); - try { - - var result = git_repo.fetch() - .setRemote("origin") - .setCredentialsProvider(credentialProvider) - .call(); - return true; - } finally { - if (git_repo != null) { - git_repo.close(); - } - } - } - -} diff --git a/cms-git/src/main/java/com/condation/cms/git/tasks/MergeTask.java b/cms-git/src/main/java/com/condation/cms/git/tasks/MergeTask.java deleted file mode 100644 index 9b3de59b2..000000000 --- a/cms-git/src/main/java/com/condation/cms/git/tasks/MergeTask.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.condation.cms.git.tasks; - -import com.condation.cms.api.utils.ServerUtil; - -/*- - * #%L - * cms-git - * %% - * Copyright (C) 2023 - 2024 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - -import com.condation.cms.git.Repo; -import com.condation.cms.git.Task; -import java.nio.file.Files; -import java.nio.file.Path; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; - -/** - * - * @author t.marx - */ -@RequiredArgsConstructor -@Slf4j -public class MergeTask implements Task { - - private final Repo repo; - - @Override - public Boolean call() throws Exception { - - Path targetFolder = ServerUtil.getPath(repo.getFolder()); - if (!Files.exists(targetFolder)) { - log.trace("target folder does not exists"); - return Boolean.FALSE; - } - - UsernamePasswordCredentialsProvider credentialProvider = new UsernamePasswordCredentialsProvider( - repo.getCredentials().getUsername(), - repo.getCredentials().getPassword() - ); - - Git git_repo = Git.open(targetFolder.toFile()); - try { - - var result = git_repo.merge() - .include(git_repo.getRepository().findRef("origin/" + repo.getBranch())) - .call(); - return result.getConflicts() == null || result.getConflicts().isEmpty(); - } finally { - if (git_repo != null) { - git_repo.close(); - } - } - } - -} diff --git a/cms-git/src/main/java/com/condation/cms/git/tasks/ResetTask.java b/cms-git/src/main/java/com/condation/cms/git/tasks/ResetTask.java deleted file mode 100644 index 2acaf960a..000000000 --- a/cms-git/src/main/java/com/condation/cms/git/tasks/ResetTask.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.condation.cms.git.tasks; - -import com.condation.cms.api.utils.ServerUtil; - -/*- - * #%L - * cms-git - * %% - * Copyright (C) 2023 - 2024 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - -import com.condation.cms.git.Repo; -import com.condation.cms.git.Task; -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.ResetCommand; -import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; - -/** - * - * @author t.marx - */ -@RequiredArgsConstructor -@Slf4j -public class ResetTask implements Task { - - private final Repo repo; - - @Override - public Boolean call() throws Exception { - - Path targetFolder = ServerUtil.getPath(repo.getFolder()); - if (!Files.exists(targetFolder)) { - log.trace("target folder does not exists"); - return Boolean.FALSE; - } - - UsernamePasswordCredentialsProvider credentialProvider = new UsernamePasswordCredentialsProvider( - repo.getCredentials().getUsername(), - repo.getCredentials().getPassword() - ); - - Git git_repo = Git.open(targetFolder.toFile()); - try { - - git_repo.reset().setMode(ResetCommand.ResetType.HARD).setRef("refs/heads/" + repo.getBranch()); - - return true; - } finally { - if (git_repo != null) { - git_repo.close(); - } - } - } - -} diff --git a/cms-git/src/test/java/com/condation/cms/git/FlowTest.java b/cms-git/src/test/java/com/condation/cms/git/FlowTest.java deleted file mode 100644 index 1c09bd28c..000000000 --- a/cms-git/src/test/java/com/condation/cms/git/FlowTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.condation.cms.git; - -/*- - * #%L - * cms-git - * %% - * Copyright (C) 2023 - 2024 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - - -import com.condation.cms.git.tasks.CloneTask; -import com.condation.cms.git.tasks.FetchTask; -import com.condation.cms.git.tasks.MergeTask; -import com.condation.cms.git.tasks.ResetTask; -import java.io.IOException; -import java.nio.file.Path; -import java.util.concurrent.ExecutionException; -import org.junit.jupiter.api.Test; - -/** - * - * @author t.marx - */ -public class FlowTest { - - @Test - void flow_test () throws Exception { - var config = Config.load(Path.of("git.yaml")); - - Boolean clone = new CloneTask(config.getRepos().get(0)).call(); - - if (clone) { - System.out.println("clone done"); - - var fetch = new FetchTask(config.getRepos().get(0)).call(); - - if (fetch) { - System.out.println("fetch done"); - var merge = new MergeTask(config.getRepos().get(0)).call(); - if (merge) { - System.out.println("merged"); - } else { - System.out.println("merge error"); - var reset = new ResetTask(config.getRepos().get(0)).call(); - System.out.println("reset " + reset); - } - } else { - System.out.println("fetch error"); - } - - } else { - System.out.println("clone error"); - } - } -} diff --git a/cms-git/src/test/java/com/condation/cms/git/GitSchedulerTest.java b/cms-git/src/test/java/com/condation/cms/git/GitSchedulerTest.java deleted file mode 100644 index 418a7bec2..000000000 --- a/cms-git/src/test/java/com/condation/cms/git/GitSchedulerTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.condation.cms.git; - -/*- - * #%L - * cms-git - * %% - * Copyright (C) 2023 - 2024 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - - -import java.io.IOException; -import java.nio.file.Path; -import java.time.Duration; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.quartz.Scheduler; -import org.quartz.SchedulerException; -import org.quartz.SchedulerFactory; -import org.quartz.impl.StdSchedulerFactory; - -/** - * - * @author t.marx - */ -public class GitSchedulerTest { - - static GitScheduler gitScheduler; - - static Scheduler scheduler; - - @BeforeAll - static void setup () throws Exception { - - SchedulerFactory schedulerFactory = new StdSchedulerFactory(); - scheduler = schedulerFactory.getScheduler(); - scheduler.start(); - - gitScheduler = new GitScheduler(scheduler); - } - @AfterAll - static void shutdown () throws Exception { - scheduler.shutdown(); - } - - @Test - public void testSomeMethod() throws IOException, SchedulerException, InterruptedException { - var config = Config.load(Path.of("git.yaml")); - gitScheduler.schedule(config.getRepos().get(0)); - Thread.sleep(Duration.ofSeconds(15)); - } - -} diff --git a/cms-media/pom.xml b/cms-media/pom.xml index fb8d7c517..c8b9375f3 100644 --- a/cms-media/pom.xml +++ b/cms-media/pom.xml @@ -4,7 +4,7 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 cms-media jar diff --git a/cms-media/src/main/java/com/condation/cms/media/CropCalculator.java b/cms-media/src/main/java/com/condation/cms/media/CropCalculator.java new file mode 100644 index 000000000..93b0fa9d9 --- /dev/null +++ b/cms-media/src/main/java/com/condation/cms/media/CropCalculator.java @@ -0,0 +1,91 @@ +package com.condation.cms.media; + +import java.awt.Rectangle; + +/*- + * #%L + * cms-media + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +public class CropCalculator { + + public static record CropArea (int x, int y, int width, int height) { + @Override + public String toString() { + return "CropArea{x=" + x + ", y=" + y + ", width=" + width + ", height=" + height + "}"; + } + + public Rectangle toRectangle () { + return new Rectangle(x, y, width, height); + } + } + + /** + * Calculates the crop area for an image based on the focal point and the + * target aspect ratio. + * + * @param imageWidth Width of the image in pixels + * @param imageHeight Height of the image in pixels + * @param focalX Focal point X coordinate (between 0.0 and 1.0) + * @param focalY Focal point Y coordinate (between 0.0 and 1.0) + * @param targetWidth Target aspect ratio width component + * @param targetHeight Target aspect ratio height component + * @return Calculated crop area + */ + public static CropArea calculateCrop(int imageWidth, int imageHeight, + double focalX, double focalY, + int targetWidth, int targetHeight) { + + double targetAspect = (double) targetWidth / targetHeight; + double imageAspect = (double) imageWidth / imageHeight; + + int cropW, cropH; + + if (imageAspect > targetAspect) { + // Bild ist breiter als Zielverhältnis – Breite beschneiden + cropH = imageHeight; + cropW = (int) Math.round(targetAspect * cropH); + } else { + // Bild ist höher als Zielverhältnis – Höhe beschneiden + cropW = imageWidth; + cropH = (int) Math.round(cropW / targetAspect); + } + + // Focal Point in Pixelkoordinaten + int focalPixelX = (int) Math.round(focalX * imageWidth); + int focalPixelY = (int) Math.round(focalY * imageHeight); + + // Crop-Startposition um den Focal Point herum + int cropX = focalPixelX - cropW / 2; + int cropY = focalPixelY - cropH / 2; + + // Grenzen prüfen und anpassen + cropX = Math.max(0, Math.min(cropX, imageWidth - cropW)); + cropY = Math.max(0, Math.min(cropY, imageHeight - cropH)); + + return new CropArea(cropX, cropY, cropW, cropH); + } + + // Test + public static void main(String[] args) { + CropArea crop = calculateCrop(1200, 800, 0.5, 0.5, 200, 300); + System.out.println(crop); // Beispielausgabe + } +} diff --git a/cms-media/src/main/java/com/condation/cms/media/FileMediaService.java b/cms-media/src/main/java/com/condation/cms/media/FileMediaService.java index 20fd7671a..544eeaa66 100644 --- a/cms-media/src/main/java/com/condation/cms/media/FileMediaService.java +++ b/cms-media/src/main/java/com/condation/cms/media/FileMediaService.java @@ -21,11 +21,10 @@ * . * #L% */ - - import com.condation.cms.api.media.Media; import com.condation.cms.api.media.MediaService; import com.condation.cms.api.media.meta.Meta; +import com.google.common.base.Strings; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -48,8 +47,8 @@ public boolean hasMetaData(final String media) { var metaFile = getMetaFile(media); return Files.exists(metaFile); } - - private Path getMetaFile (final String media) { + + private Path getMetaFile(final String media) { String mediaPath = media; if (mediaPath.startsWith("/")) { mediaPath = mediaPath.substring(1); @@ -57,12 +56,12 @@ private Path getMetaFile (final String media) { var mediaFile = assetBase.resolve(mediaPath); var metaFileName = mediaFile.getFileName().toString() + ".meta.yaml"; var metaFile = mediaFile.getParent().resolve(metaFileName); - + return metaFile; } - - private Meta loadMeta (final String media) { - + + private Meta loadMeta(final String media) { + var metaFile = getMetaFile(media); if (Files.exists(metaFile)) { try { @@ -72,19 +71,25 @@ private Meta loadMeta (final String media) { log.error(null, ex); } } - + return new Meta(); } @Override public Media get(final String media) { + if (Strings.isNullOrEmpty(media)) { + var meta = new Meta(); + return new Media("", meta, false); + } String mediaPath = media; if (mediaPath.startsWith("/")) { mediaPath = mediaPath.substring(1); } var mediaFile = assetBase.resolve(mediaPath); var meta = loadMeta(media); - - return new Media(mediaPath, meta, Files.exists(mediaFile)); + + var size = ImageSize.getSize(mediaFile); + + return new Media(mediaPath, meta, Files.exists(mediaFile), size); } } diff --git a/cms-media/src/main/java/com/condation/cms/media/ImageSize.java b/cms-media/src/main/java/com/condation/cms/media/ImageSize.java new file mode 100644 index 000000000..08cf97b03 --- /dev/null +++ b/cms-media/src/main/java/com/condation/cms/media/ImageSize.java @@ -0,0 +1,75 @@ +package com.condation.cms.media; + +/*- + * #%L + * cms-media + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.media.Media; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import lombok.extern.slf4j.Slf4j; + +/** + * + * @author thorstenmarx + */ +@Slf4j +public class ImageSize { + + public static com.condation.cms.api.media.Media.Size getSize(Path image) { + if (!Files.exists(image)) { + return Media.NO_SIZE; + } + + try (ImageInputStream in = ImageIO.createImageInputStream(image.toFile())) { + if (in == null) { + log.error("can not load image: {}", image.toString()); + return Media.NO_SIZE; + } + + // passenden Reader für das Format finden + Iterator readers = ImageIO.getImageReaders(in); + if (!readers.hasNext()) { + log.error("no reader found for image: {}", image.toString()); + return Media.NO_SIZE; + } + + ImageReader reader = readers.next(); + try { + reader.setInput(in); + int width = reader.getWidth(0); + int height = reader.getHeight(0); + + return new Media.Size(width, height); + } finally { + reader.dispose(); + } + } catch (IOException ex) { + log.error("error resolving image size", ex); + } + return Media.NO_SIZE; + } +} diff --git a/cms-media/src/main/java/com/condation/cms/media/MediaManager.java b/cms-media/src/main/java/com/condation/cms/media/MediaManager.java index 2506b26de..41a642ccd 100644 --- a/cms-media/src/main/java/com/condation/cms/media/MediaManager.java +++ b/cms-media/src/main/java/com/condation/cms/media/MediaManager.java @@ -21,17 +21,20 @@ * . * #L% */ - - import com.condation.cms.api.configuration.Configuration; import com.condation.cms.api.configuration.configs.MediaConfiguration; import com.condation.cms.api.eventbus.EventListener; import com.condation.cms.api.eventbus.events.ConfigurationReloadEvent; +import com.condation.cms.api.eventbus.events.InvalidateMediaCache; import com.condation.cms.api.media.MediaFormat; import com.condation.cms.api.media.MediaUtils; +import com.condation.cms.api.media.meta.Meta; import com.condation.cms.api.theme.Theme; +import com.condation.cms.api.utils.FileUtils; +import com.condation.cms.api.utils.PathUtil; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; @@ -40,8 +43,7 @@ import java.util.Optional; import lombok.extern.slf4j.Slf4j; import net.coobird.thumbnailator.Thumbnails; -import net.coobird.thumbnailator.geometry.Position; -import net.coobird.thumbnailator.geometry.Positions; +import org.yaml.snakeyaml.Yaml; /** * @@ -57,34 +59,34 @@ public abstract class MediaManager implements EventListener mediaFormats; protected Path tempDirectory; - - protected MediaManager (List assetPath, Path tempFolder, Theme theme, Configuration configuration) { + + protected MediaManager(List assetPath, Path tempFolder, Theme theme, Configuration configuration) { this.assetBase = assetPath; this.tempFolder = tempFolder; this.theme = theme; this.configuration = configuration; } - - public abstract void reloadTheme (Theme updateTheme); - - - public Path resolve (String uri) { + + public abstract void reloadTheme(Theme updateTheme); + + public Optional resolve(String uri) { for (Path assets : assetBase) { var resolved = assets.resolve(uri); if (Files.exists(resolved)) { - return resolved; + return Optional.of(resolved); } } - return null; + return Optional.empty(); } - - public boolean hasMediaFormat (String format) { + + public boolean hasMediaFormat(String format) { return getMediaFormats().containsKey(format); } - public MediaFormat getMediaFormat (String format) { + + public MediaFormat getMediaFormat(String format) { return getMediaFormats().get(format); } - + private Path getTempDirectory() throws IOException { if (tempDirectory == null) { tempDirectory = tempFolder.resolve("media"); @@ -95,25 +97,52 @@ private Path getTempDirectory() throws IOException { return tempDirectory; } + public void clearTempDirectory () { + try { + FileUtils.deleteDirectoryContents(getTempDirectory()); + } catch (IOException e) { + log.error("error clearing media tempfolder", e); + } + } + + public void deleteTempFile(final Path mediaPath) { + var baseDir = assetBase.stream().filter((base) -> PathUtil.isChild(base, mediaPath)).findFirst(); + + if (baseDir.isEmpty()) { + log.warn("could not find asset base director for {}", mediaPath); + return; + } + + mediaFormats.values().forEach(mediaFormat -> { + try { + var mediaUri = PathUtil.toRelativeFile(mediaPath, baseDir.get()); + var tempFilename = getTempFilename(mediaUri, mediaFormat); + var tempFile = getTempDirectory().resolve(tempFilename); + Files.deleteIfExists(tempFile); + } catch (IOException ex) { + log.error("error deleting file {} for format {}", mediaPath, mediaFormat, ex); + } + }); + } + public Optional getScaledContent(final String mediaPath, final MediaFormat mediaFormat) throws IOException { - Path resolve = resolve(mediaPath); + Optional resolve = resolve(mediaPath); - if (resolve != null) { + if (resolve.isPresent()) { Optional tempContent = getTempContent(mediaPath, mediaFormat); if (tempContent.isPresent()) { return tempContent; } Thumbnails.Builder scaleBuilder = Thumbnails - .of(resolve.toFile()) - .size(mediaFormat.width(), mediaFormat.height()) - ; - + .of(resolve.get().toFile()) + .size(mediaFormat.width(), mediaFormat.height()); + if (mediaFormat.cropped()) { - scaleBuilder.crop(getCropCenter(resolve)); + setupImageBuilder(scaleBuilder, resolve.get(), mediaFormat); } - + byte[] data = Scale.toFormat(scaleBuilder.asBufferedImage(), mediaFormat); writeTempContent(mediaPath, mediaFormat, data); @@ -122,25 +151,27 @@ public Optional getScaledContent(final String mediaPath, final MediaForm } return Optional.empty(); } - - public Position getCropCenter (Path media) { -// var metaFileName = media.getFileName().toString() + ".meta.yaml"; -// var metaFile = media.getParent().resolve(metaFileName); -// if (Files.exists(metaFile)){ -// try { -// final Meta meta = new Yaml().loadAs(Files.readString(metaFile, StandardCharsets.UTF_8), Meta.class); -// return new Position() { -// @Override -// public Point calculate(int enclosingWidth, int enclosingHeight, int width, int height, int insetLeft, int insetRight, int insetTop, int insetBottom) { -// return new Point(meta.getCrop().getCenter_x(), meta.getCrop().getCenter_y()); -// } -// }; -// } catch (IOException ex) { -// log.error(null, ex); -// } -// } - - return Positions.CENTER; + + private void setupImageBuilder(Thumbnails.Builder builder, Path media, MediaFormat format) { + var metaFileName = media.getFileName().toString() + ".meta.yaml"; + var metaFile = media.getParent().resolve(metaFileName); + var size = ImageSize.getSize(media); + double focal_x = 0.5; + double focal_y = 0.5; + if (Files.exists(metaFile)) { + try { + final Meta meta = new Yaml().loadAs(Files.readString(metaFile, StandardCharsets.UTF_8), Meta.class); + focal_x = meta.getFocalPoint_x(); + focal_y = meta.getFocalPoint_y(); + } catch (IOException ex) { + log.warn("Could not read meta file: {}", metaFile, ex); + } + } + CropCalculator.CropArea crop = CropCalculator.calculateCrop( + size.width(), size.height(), + focal_x, focal_y, + format.width(), format.height()); + builder.sourceRegion(crop.toRectangle()); } public String getTempFilename(final String mediaPath, final MediaFormat mediaFormat) { @@ -156,7 +187,7 @@ private Path writeTempContent(final String mediaPath, final MediaFormat mediaFor var tempFile = getTempDirectory().resolve(tempFilename); Files.deleteIfExists(tempFile); Files.write(tempFile, content); - + return tempFile; } @@ -170,7 +201,7 @@ private Optional getTempContent(final String mediaPath, final MediaForma return Optional.empty(); } - + private Map getMediaFormats() { if (mediaFormats == null) { diff --git a/cms-media/src/main/java/com/condation/cms/media/Scale.java b/cms-media/src/main/java/com/condation/cms/media/Scale.java index d455c7e0d..d33802ad0 100644 --- a/cms-media/src/main/java/com/condation/cms/media/Scale.java +++ b/cms-media/src/main/java/com/condation/cms/media/Scale.java @@ -21,7 +21,6 @@ * . * #L% */ - import com.condation.cms.api.media.MediaFormat; import com.condation.cms.api.media.MediaUtils; import com.luciad.imageio.webp.WebPWriteParam; @@ -73,15 +72,22 @@ public static ScaleResult scaleWithAspectIfTooLarge(byte[] fileData, int maxWidt BufferedImage imageBuff = new BufferedImage(result.width, result.height, BufferedImage.TYPE_INT_RGB); - imageBuff.getGraphics().drawImage(scaledImage, 0, 0, new Color(0, 0, 0), null); - // output - if (MediaUtils.Format.JPEG.equals(format)) { - result.data = toJPG(imageBuff, uncompressed); - } else if (MediaUtils.Format.WEBP.equals(format)) { - result.data = toWEBP(imageBuff, uncompressed); - } else if (MediaUtils.Format.PNG.equals(format)) { - result.data = toPNG(imageBuff, uncompressed); + + var g = imageBuff.getGraphics(); + try { + g.drawImage(scaledImage, 0, 0, new Color(0, 0, 0), null); + // output + if (MediaUtils.Format.JPEG.equals(format)) { + result.data = toJPG(imageBuff, uncompressed); + } else if (MediaUtils.Format.WEBP.equals(format)) { + result.data = toWEBP(imageBuff, uncompressed); + } else if (MediaUtils.Format.PNG.equals(format)) { + result.data = toPNG(imageBuff, uncompressed); + } + } finally { + g.dispose(); } + } } catch (IOException e) { log.error("scaleWithAspectIfTooLarge(): IOexception ", e); @@ -92,9 +98,12 @@ public static ScaleResult scaleWithAspectIfTooLarge(byte[] fileData, int maxWidt public static byte[] toFormat(final BufferedImage imageBuff, final MediaFormat mediaFormat) throws IOException { if (null != mediaFormat.format()) { return switch (mediaFormat.format()) { - case JPEG -> toJPG(imageBuff, !mediaFormat.compression()); - case WEBP -> toWEBP(imageBuff, !mediaFormat.compression()); - case PNG -> toPNG(imageBuff, !mediaFormat.compression()); + case JPEG -> + toJPG(imageBuff, !mediaFormat.compression()); + case WEBP -> + toWEBP(imageBuff, !mediaFormat.compression()); + case PNG -> + toPNG(imageBuff, !mediaFormat.compression()); }; } throw new IllegalArgumentException("unknown media format"); @@ -104,16 +113,22 @@ private static byte[] toPNG(final BufferedImage imageBuff, final boolean uncompr try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { if (uncompressed) { final ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next(); - writer.setOutput(new MemoryCacheImageOutputStream(buffer)); - - var writeParam = writer.getDefaultWriteParam(); - if (writeParam.canWriteCompressed()) { - writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - writeParam.setCompressionQuality(1f); + try { + var writeParam = writer.getDefaultWriteParam(); + if (writeParam.canWriteCompressed()) { + writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + writeParam.setCompressionQuality(1f); + } + + try (MemoryCacheImageOutputStream memoryCacheImageOutputStream = new MemoryCacheImageOutputStream(buffer)) { + writer.setOutput(memoryCacheImageOutputStream); + writer.write(null, new IIOImage(imageBuff, null, null), writeParam); + } + + return buffer.toByteArray(); + } finally { + writer.dispose(); } - - writer.write(null, new IIOImage(imageBuff, null, null), writeParam); - return buffer.toByteArray(); } else { ImageIO.write(imageBuff, "png", buffer); return buffer.toByteArray(); @@ -128,9 +143,14 @@ private static byte[] toJPG(final BufferedImage imageBuff, final boolean uncompr jpegParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); jpegParams.setCompressionQuality(1f); final ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next(); - writer.setOutput(new MemoryCacheImageOutputStream(buffer)); - writer.write(null, new IIOImage(imageBuff, null, null), jpegParams); - return buffer.toByteArray(); + try (MemoryCacheImageOutputStream memoryCacheImageOutputStream = new MemoryCacheImageOutputStream(buffer)) { + writer.setOutput(memoryCacheImageOutputStream); + writer.write(null, new IIOImage(imageBuff, null, null), jpegParams); + + return buffer.toByteArray(); + } finally { + writer.dispose(); + } } else { ImageIO.write(imageBuff, "jpg", buffer); return buffer.toByteArray(); @@ -145,11 +165,15 @@ private static byte[] toWEBP(final BufferedImage imageBuff, final boolean uncomp writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); writeParam.setCompressionType(writeParam.getCompressionTypes()[WebPWriteParam.LOSSLESS_COMPRESSION]); final ImageWriter writer = ImageIO.getImageWritersByMIMEType("image/webp").next(); - final MemoryCacheImageOutputStream memoryCacheImageOutputStream = new MemoryCacheImageOutputStream(buffer); - writer.setOutput(memoryCacheImageOutputStream); - writer.write(null, new IIOImage(imageBuff, null, null), writeParam); - memoryCacheImageOutputStream.close(); - return buffer.toByteArray(); + try (MemoryCacheImageOutputStream memoryCacheImageOutputStream = new MemoryCacheImageOutputStream(buffer)) { + writer.setOutput(memoryCacheImageOutputStream); + writer.write(null, new IIOImage(imageBuff, null, null), writeParam); + + return buffer.toByteArray(); + } finally { + writer.dispose(); + } + } else { ImageIO.write(imageBuff, "webp", buffer); return buffer.toByteArray(); diff --git a/cms-media/src/test/java/com/condation/cms/media/ImageSizeTest.java b/cms-media/src/test/java/com/condation/cms/media/ImageSizeTest.java new file mode 100644 index 000000000..d4ed05934 --- /dev/null +++ b/cms-media/src/test/java/com/condation/cms/media/ImageSizeTest.java @@ -0,0 +1,52 @@ +package com.condation.cms.media; + +/*- + * #%L + * cms-media + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.nio.file.Path; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * + * @author thorstenmarx + */ +public class ImageSizeTest { + + public ImageSizeTest() { + } + + @Test + public void image_does_not_exists() { + var size = ImageSize.getSize(Path.of("no.jpg")); + + Assertions.assertThat(size.width()).isEqualTo(-1); + Assertions.assertThat(size.height()).isEqualTo(-1); + } + + @Test + public void image_with_correct_size () { + var size = ImageSize.getSize(Path.of("src/test/resources/assets/test.jpg")); + Assertions.assertThat(size.width()).isEqualTo(689); + Assertions.assertThat(size.height()).isEqualTo(689); + } +} diff --git a/cms-media/src/test/java/com/condation/cms/media/MediaManagerTest.java b/cms-media/src/test/java/com/condation/cms/media/MediaManagerTest.java index 21d1605e3..ce8dd513d 100644 --- a/cms-media/src/test/java/com/condation/cms/media/MediaManagerTest.java +++ b/cms-media/src/test/java/com/condation/cms/media/MediaManagerTest.java @@ -23,12 +23,12 @@ */ -import com.condation.cms.api.ThemeProperties; import com.condation.cms.api.configuration.Configuration; import com.condation.cms.api.configuration.configs.MediaConfiguration; import com.condation.cms.api.configuration.configs.ServerConfiguration; import com.condation.cms.api.media.MediaFormat; import com.condation.cms.api.media.MediaUtils; +import com.condation.cms.api.utils.FileUtils; import com.condation.cms.core.configuration.ConfigurationFactory; import com.condation.cms.core.configuration.properties.ExtendedServerProperties; import com.condation.cms.test.PropertiesLoader; @@ -50,9 +50,12 @@ public class MediaManagerTest { @BeforeAll public static void setup () throws IOException { + FileUtils.deleteFolder(Path.of("target/media")); + Configuration config = new Configuration(); config.add(MediaConfiguration.class, new MediaConfiguration(List.of( - new MediaFormat("cropped", 40, 40, MediaUtils.Format.PNG, true, true) + new MediaFormat("cropped", 40, 40, MediaUtils.Format.PNG, true, true), + new MediaFormat("focal", 400, 100, MediaUtils.Format.PNG, true, true) ))); var serverConfig = new ServerConfiguration(new ExtendedServerProperties(ConfigurationFactory.serverConfiguration())); config.add(ServerConfiguration.class, serverConfig); @@ -66,10 +69,17 @@ public static void setup () throws IOException { } @Test - public void testSomeMethod() throws IOException { + public void test_cropped() throws IOException { var content = mediaManager.getScaledContent("test.jpg", mediaManager.getMediaFormat("cropped")); Files.write(Path.of("target/test_cropped.jpg"), content.get()); } + @Test + public void test_focal() throws IOException { + var content = mediaManager.getScaledContent("demo.jpg", mediaManager.getMediaFormat("focal")); + + Files.write(Path.of("target/demo_focal.jpg"), content.get()); + } + } diff --git a/cms-media/src/test/java/com/condation/cms/media/MediaServiceTest.java b/cms-media/src/test/java/com/condation/cms/media/MediaServiceTest.java index 6d15f462e..9c9a0fa77 100644 --- a/cms-media/src/test/java/com/condation/cms/media/MediaServiceTest.java +++ b/cms-media/src/test/java/com/condation/cms/media/MediaServiceTest.java @@ -52,8 +52,8 @@ public void media_not_exists() { @Test public void media_exists_without_meta() { - Assertions.assertThat(mediaService.hasMetaData("demo.jpg")).isFalse(); - var media = mediaService.get("demo.jpg"); + Assertions.assertThat(mediaService.hasMetaData("demo_1.jpg")).isFalse(); + var media = mediaService.get("demo_1.jpg"); Assertions.assertThat(media.exists()).isTrue(); Assertions.assertThat(media.meta()).isEmpty(); } @@ -68,7 +68,7 @@ public void media_exists_with_meta() { @Test public void subfolder_media_exists_without_meta() { - Assertions.assertThat(mediaService.hasMetaData("images/demo.jpg")).isFalse(); + Assertions.assertThat(mediaService.hasMetaData("images/demo_1.jpg")).isFalse(); } @Test diff --git a/cms-media/src/test/resources/assets/demo.jpg.meta.yaml b/cms-media/src/test/resources/assets/demo.jpg.meta.yaml new file mode 100644 index 000000000..5cfb366e8 --- /dev/null +++ b/cms-media/src/test/resources/assets/demo.jpg.meta.yaml @@ -0,0 +1,3 @@ +focal: + x: 0.5 + y: 0.5 \ No newline at end of file diff --git a/cms-media/src/test/resources/assets/demo_1.jpg b/cms-media/src/test/resources/assets/demo_1.jpg new file mode 100644 index 000000000..fa61cd3a0 Binary files /dev/null and b/cms-media/src/test/resources/assets/demo_1.jpg differ diff --git a/cms-media/src/test/resources/assets/test.jpg.meta.yaml b/cms-media/src/test/resources/assets/test.jpg.meta.yaml index 5403336a1..5cfb366e8 100644 --- a/cms-media/src/test/resources/assets/test.jpg.meta.yaml +++ b/cms-media/src/test/resources/assets/test.jpg.meta.yaml @@ -1,3 +1,3 @@ -crop: - center_x: 50 - center_y: 50 \ No newline at end of file +focal: + x: 0.5 + y: 0.5 \ No newline at end of file diff --git a/cms-sandbox/benchmark/pom.xml b/cms-sandbox/benchmark/pom.xml index 0db81e351..dc475ea61 100644 --- a/cms-sandbox/benchmark/pom.xml +++ b/cms-sandbox/benchmark/pom.xml @@ -4,7 +4,7 @@ com.condation.cms cms-sandbox - 7.3.0 + 8.0.0 benchmark jar diff --git a/cms-sandbox/pom.xml b/cms-sandbox/pom.xml index abac2f972..ea6d629bd 100644 --- a/cms-sandbox/pom.xml +++ b/cms-sandbox/pom.xml @@ -4,7 +4,7 @@ com.condation.cms cms-parent - 7.3.0 + 8.0.0 cms-sandbox pom diff --git a/cms-sandbox/tests/pom.xml b/cms-sandbox/tests/pom.xml index 725275cab..adffde4a6 100644 --- a/cms-sandbox/tests/pom.xml +++ b/cms-sandbox/tests/pom.xml @@ -6,7 +6,7 @@ com.condation.cms cms-sandbox - 7.3.0 + 8.0.0 tests jar @@ -53,25 +53,4 @@ 4.4.0 - - - - - org.antlr - antlr4-maven-plugin - 4.13.2 - - src/main/resources - target/generated-sources/antlr4 - - - - - antlr4 - - - - - - \ No newline at end of file diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/Tests.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/Tests.java deleted file mode 100644 index 7743c392c..000000000 --- a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/Tests.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.condation.cms.tests; - -/*- - * #%L - * tests - * %% - * Copyright (C) 2023 - 2024 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - -import java.util.Arrays; -import org.apache.commons.jexl3.JexlBuilder; -import org.apache.commons.jexl3.JexlContext; -import org.apache.commons.jexl3.JexlEngine; -import org.apache.commons.jexl3.MapContext; - -/** - * - * @author t.marx - */ -public class Tests { - - private static final JexlEngine jexl = new JexlBuilder().cache(512).strict(true).silent(false).create(); - - public static void main(String[] args) throws Exception { - JexlContext context = new MapContext(); - var expr = jexl.createExpression("[1, 2, 3]"); - - int[]value = (int[])expr.evaluate(context); - - var list = Arrays.stream(value) - .boxed().toList(); - - list.forEach(System.out::println); - - - expr = jexl.createExpression("{'key' : 'value'}"); - - var map = expr.evaluate(context); - - System.out.println(map); - } -} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/expressions/EvaluationException.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/expressions/EvaluationException.java new file mode 100644 index 000000000..01be6b891 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/expressions/EvaluationException.java @@ -0,0 +1,33 @@ +package com.condation.cms.tests.expressions; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +public class EvaluationException extends RuntimeException { + public EvaluationException(String message) { + super(message); + } + + public EvaluationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/expressions/ExpressionEngine.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/expressions/ExpressionEngine.java new file mode 100644 index 000000000..4ac644d13 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/expressions/ExpressionEngine.java @@ -0,0 +1,320 @@ +package com.condation.cms.tests.expressions; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.reflect.*; +import java.util.*; +import java.util.function.*; + +/** + * Erweiterbare Enterprise Expression Engine mit Parser für komplexe Ausdrücke. + * Unterstützt: + * - Objektzugriff über : + * - Map-Keys, Listen, Methodenaufrufe + * - Vergleichs- und logische Operatoren (eq, lt, lte, gt, gte, and, or, not) + * - Erweiterbare globale Funktionen + * - Parser für komplexe, verschachtelte Ausdrücke mit Klammern + * - Boolean-, Null- und Collection-Literale + */ +public class ExpressionEngine { + + private final Map> operators = new LinkedHashMap<>(); + private final Map, Object>> globalMethods = new HashMap<>(); + private final Map cache = new HashMap<>(); + + // Interface for a compiled/parsed expression + private interface ParsedExpression { + Object evaluate(Map context); + } + + public ExpressionEngine() { + registerDefaultOperators(); + } + + private void registerDefaultOperators() { + registerOperator("eq", (a, b) -> Objects.equals(a, b)); + registerOperator("lt", (a, b) -> compare(a, b) < 0); + registerOperator("lte", (a, b) -> compare(a, b) <= 0); + registerOperator("gt", (a, b) -> compare(a, b) > 0); + registerOperator("gte", (a, b) -> compare(a, b) >= 0); + registerOperator("and", (a, b) -> toBool(a) && toBool(b)); + registerOperator("or", (a, b) -> toBool(a) || toBool(b)); + } + + private static boolean toBool(Object o) { + if (o instanceof Boolean b) return b; + if (o instanceof Number n) return n.doubleValue() != 0.0; + if (o instanceof String s) return Boolean.parseBoolean(s); + return o != null; + } + + private int compare(Object a, Object b) { + if (a instanceof Comparable && b != null && a.getClass().isAssignableFrom(b.getClass())) { + return ((Comparable) a).compareTo(b); + } + throw new EvaluationException("Cannot compare " + a + " and " + b); + } + + public void registerOperator(String name, BiFunction op) { + operators.put(name, op); + } + + public void registerMethod(String name, Function, Object> func) { + globalMethods.put(name, func); + } + + public Object evaluate(String expression, Map context) { + ParsedExpression parsed = cache.computeIfAbsent(expression, this::parse); + return parsed.evaluate(context); + } + + private ParsedExpression parse(String expression) { + ExpressionParser parser = new ExpressionParser(this); + return parser.parse(expression); + } + + Object resolve(String expr, Map context) { + if (expr == null || expr.isEmpty()) return null; + + expr = expr.trim(); + + // === Boolean und Null Literale === + if (expr.equals("true")) return true; + if (expr.equals("false")) return false; + if (expr.equals("null")) return null; + + // === String Literals === + if (expr.startsWith("\"") && expr.endsWith("\"")) { + return expr.substring(1, expr.length() - 1); + } + + // === Zahlen === + if (expr.matches("-?\\d+")) return Integer.parseInt(expr); + if (expr.matches("-?\\d+\\.\\d+")) return Double.parseDouble(expr); + + // === Listen-Literal === [1,2,3] + if (expr.startsWith("[") && expr.endsWith("]")) { + String inside = expr.substring(1, expr.length() - 1).trim(); + if (inside.isEmpty()) return new ArrayList<>(); + List list = new ArrayList<>(); + for (String part : splitArgs(inside)) { + list.add(resolve(part.trim(), context)); + } + return list; + } + + // === Map-Literal === {x: 1, y: 2} + if (expr.startsWith("{") && expr.endsWith("}")) { + String inside = expr.substring(1, expr.length() - 1).trim(); + if (inside.isEmpty()) return new LinkedHashMap<>(); + Map map = new LinkedHashMap<>(); + for (String entry : splitArgs(inside)) { + int sep = entry.indexOf(':'); + if (sep < 0) continue; + String key = entry.substring(0, sep).trim(); + String val = entry.substring(sep + 1).trim(); + if (key.startsWith("\"") && key.endsWith("\"")) { + key = key.substring(1, key.length() - 1); + } + map.put(key, resolve(val, context)); + } + return map; + } + + // === NOT-Operator === + if (expr.startsWith("not ")) { + Object val = evaluate(expr.substring(4).trim(), context); + return !toBool(val); + } + + // === Funktionsaufruf === + if (expr.contains("(") && expr.endsWith(")")) { + String name = expr.substring(0, expr.indexOf('(')); + String inside = expr.substring(expr.indexOf('(') + 1, expr.length() - 1); + List args = new ArrayList<>(); + if (!inside.isEmpty()) { + for (String part : splitArgs(inside)) { + args.add(resolve(part.trim(), context)); + } + } + if (globalMethods.containsKey(name)) { + return globalMethods.get(name).apply(args); + } + } + + // === Objektauflösung mit "." === + String[] parts = expr.split("\\."); + Object current = resolvePart(context, parts[0], context); + for (int i = 1; i < parts.length; i++) { + current = resolvePart(current, parts[i], context); + } + return current; + } + + private List splitArgs(String inside) { + List args = new ArrayList<>(); + int depth = 0; + StringBuilder current = new StringBuilder(); + for (char c : inside.toCharArray()) { + if (c == ',' && depth == 0) { + args.add(current.toString()); + current.setLength(0); + } else { + if (c == '(' || c == '[' || c == '{') depth++; + if (c == ')' || c == ']' || c == '}') depth--; + current.append(c); + } + } + if (current.length() > 0) args.add(current.toString()); + return args; + } + + private Object resolvePart(Object base, String part, Map context) { + if (base == null) { + throw new EvaluationException("Cannot resolve part '" + part + "' on null object"); + } + + // Liste: z. B. users[0] + if (part.matches(".+\\[\\d+\\]")) { + String name = part.substring(0, part.indexOf('[')); + int idx = Integer.parseInt(part.replaceAll(".*\\[(\\d+)\\].*", "$1")); + Object listObj = resolvePart(base, name, context); + + if (listObj instanceof List list) { + if (idx >= 0 && idx < list.size()) { + return list.get(idx); + } else { + throw new EvaluationException("Index " + idx + " out of bounds for list " + name); + } + } else { + throw new EvaluationException("Cannot access by index on non-list object: " + name); + } + } + + // Map + if (base instanceof Map map) { + if (map.containsKey(part)) { + return map.get(part); + } + if (base == context) { + return null; + } + throw new EvaluationException("Could not resolve part '" + part + "' on object " + base); + } + + // Try getter/method/field + try { + try { + Method m = base.getClass().getMethod(part); + return m.invoke(base); + } catch (NoSuchMethodException ignored) {} + + String getter = "get" + Character.toUpperCase(part.charAt(0)) + part.substring(1); + try { + Method m = base.getClass().getMethod(getter); + return m.invoke(base); + } catch (NoSuchMethodException ignored) {} + + try { + Field f = base.getClass().getDeclaredField(part); + f.setAccessible(true); + return f.get(base); + } catch (NoSuchFieldException ignored) {} + } catch (Exception e) { + throw new EvaluationException("Error resolving part: " + part, e); + } + throw new EvaluationException("Could not resolve part '" + part + "' on object " + base); + } + + private static class ExpressionParser { + private final ExpressionEngine engine; + + ExpressionParser(ExpressionEngine engine) { + this.engine = engine; + } + + public ParsedExpression parse(String expr) { + expr = expr.trim(); + if (expr.startsWith("(") && expr.endsWith(")") && isBalanced(expr.substring(1, expr.length() - 1))) { + expr = expr.substring(1, expr.length() - 1).trim(); + } + + for (String op : engine.operators.keySet()) { + int idx = findTopLevelOperator(expr, op); + if (idx >= 0) { + String left = expr.substring(0, idx).trim(); + String right = expr.substring(idx + op.length()).trim(); + if (right.isEmpty()) { + throw new ExpressionParseException("Missing right operand for operator: " + op); + } + ParsedExpression lVal = parse(left); + ParsedExpression rVal = parse(right); + return context -> engine.operators.get(op).apply(lVal.evaluate(context), rVal.evaluate(context)); + } + } + final String finalExpr = expr; + return context -> engine.resolve(finalExpr, context); + } + + private int findTopLevelOperator(String expr, String op) { + int depth = 0; + for (int i = 0; i < expr.length() - op.length() + 1; i++) { + char c = expr.charAt(i); + if (c == '(' || c == '[' || c == '{') depth++; + if (c == ')' || c == ']' || c == '}') depth--; + if (depth == 0 && expr.startsWith(op, i)) { + boolean leftSpace = i == 0 || Character.isWhitespace(expr.charAt(i - 1)); + boolean rightSpace = (i + op.length() >= expr.length()) + || Character.isWhitespace(expr.charAt(i + op.length())); + if (leftSpace && rightSpace) return i; + } + } + return -1; + } + + private boolean isBalanced(String s) { + int depth = 0; + for (char c : s.toCharArray()) { + if (c == '(') depth++; + if (c == ')') depth--; + if (depth < 0) return false; + } + return depth == 0; + } + } + + // Beispielmain + public static void main(String[] args) { + ExpressionEngine engine = new ExpressionEngine(); + engine.registerMethod("contains", argsList -> argsList.get(0).toString().contains(argsList.get(1).toString())); + + Map ctx = new HashMap<>(); + ctx.put("user", Map.of("name", "Thorsten", "age", 42)); + + System.out.println(engine.evaluate("true", ctx)); // true + System.out.println(engine.evaluate("false", ctx)); // false + System.out.println(engine.evaluate("null", ctx)); // null + System.out.println(engine.evaluate("[1, 2, 3]", ctx)); // [1, 2, 3] + System.out.println(engine.evaluate("{x: 1, y: 2}", ctx)); // {x=1, y=2} + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/expressions/ExpressionParseException.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/expressions/ExpressionParseException.java new file mode 100644 index 000000000..883649827 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/expressions/ExpressionParseException.java @@ -0,0 +1,29 @@ +package com.condation.cms.tests.expressions; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +public class ExpressionParseException extends RuntimeException { + public ExpressionParseException(String message) { + super(message); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/AllowMultiple.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/AllowMultiple.java new file mode 100644 index 000000000..fd3dc1bae --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/AllowMultiple.java @@ -0,0 +1,40 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/AnnotationType.java to edit this template + */ +package com.condation.cms.tests.injection.test1; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = ElementType.TYPE) +public @interface AllowMultiple {} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/BeanDefinition.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/BeanDefinition.java new file mode 100644 index 000000000..6e35745d3 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/BeanDefinition.java @@ -0,0 +1,69 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package com.condation.cms.tests.injection.test1; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.function.Supplier; + +// --- core types --- + +final class BeanDefinition { + + final Class exposedType; // interface or base type used for registration + final Class implType; // concrete implementation + final String name; + final Supplier supplier; + final Scope.Type scope; + final boolean primary; + final boolean allowMultiple; + volatile T singletonInstance; + + BeanDefinition(Class exposedType, Class implType, String name, Supplier supplier, Scope.Type scope, boolean primary, boolean allowMultiple) { + this.exposedType = exposedType; + this.implType = implType; + this.name = name; + this.supplier = supplier; + this.scope = scope; + this.primary = primary; + this.allowMultiple = allowMultiple; + } + + T get(SimpleDIContainer container) { + if (scope == Scope.Type.SINGLETON) { + if (singletonInstance == null) { + synchronized (this) { + if (singletonInstance == null) { + singletonInstance = container.createAndInject(this); + } + } + } + return singletonInstance; + } else { + return container.createAndInject(this); + } + } + +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Inject.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Inject.java new file mode 100644 index 000000000..43f030591 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Inject.java @@ -0,0 +1,38 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/AnnotationType.java to edit this template + */ +package com.condation.cms.tests.injection.test1; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// --- annotations --- + +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = {ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD}) +public @interface Inject {} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Named.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Named.java new file mode 100644 index 000000000..fde779dca --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Named.java @@ -0,0 +1,44 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/AnnotationType.java to edit this template + */ +package com.condation.cms.tests.injection.test1; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = {ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) +public @interface Named { + + String value(); + +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/PostConstruct.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/PostConstruct.java new file mode 100644 index 000000000..85761f443 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/PostConstruct.java @@ -0,0 +1,40 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/AnnotationType.java to edit this template + */ +package com.condation.cms.tests.injection.test1; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = ElementType.METHOD) +public @interface PostConstruct {} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/PreDestroy.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/PreDestroy.java new file mode 100644 index 000000000..1b51adef6 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/PreDestroy.java @@ -0,0 +1,40 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/AnnotationType.java to edit this template + */ +package com.condation.cms.tests.injection.test1; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = ElementType.METHOD) +public @interface PreDestroy {} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Primary.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Primary.java new file mode 100644 index 000000000..29bee96f4 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Primary.java @@ -0,0 +1,40 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/AnnotationType.java to edit this template + */ +package com.condation.cms.tests.injection.test1; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = {ElementType.TYPE, ElementType.METHOD}) +public @interface Primary {} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Scope.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Scope.java new file mode 100644 index 000000000..efd56dfdb --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/Scope.java @@ -0,0 +1,48 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/AnnotationType.java to edit this template + */ +package com.condation.cms.tests.injection.test1; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = {ElementType.TYPE, ElementType.METHOD}) +public @interface Scope { + + Type value(); + + public static enum Type { + SINGLETON, PROTOTYPE + } + +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/SimpleDIContainer.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/SimpleDIContainer.java new file mode 100644 index 000000000..837fbf419 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/SimpleDIContainer.java @@ -0,0 +1,241 @@ +package com.condation.cms.tests.injection.test1; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.*; +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/* + Extended SimpleDIContainer.java + - Added support for registering multiple implementations for the same interface. + - Now you can do: + container.register(Interface.class, Impl1.class); + container.register(Interface.class, Impl2.class); + - You can still retrieve single or all implementations. +*/ + +public class SimpleDIContainer implements AutoCloseable { + + + // type -> list of bean defs + private final Map, List>> beans = new ConcurrentHashMap<>(); + private final List> allDefs = Collections.synchronizedList(new ArrayList<>()); + + // --- Registration API --- + public void register(Class exposedType, Class implType) { + Objects.requireNonNull(exposedType); + Objects.requireNonNull(implType); + boolean primary = implType.isAnnotationPresent(Primary.class); + boolean allowMultiple = implType.isAnnotationPresent(AllowMultiple.class); + Scope.Type scope = implType.isAnnotationPresent(Scope.class) ? implType.getAnnotation(Scope.class).value() : Scope.Type.SINGLETON; + String name = getNameFromAnnotation(implType); + Supplier supplier = () -> instantiateByConstructor(implType); + registerSupplierInternal(exposedType, implType, name, supplier, scope, primary, allowMultiple); + } + + public void registerInstance(Class exposedType, T instance) { + Objects.requireNonNull(exposedType); + Objects.requireNonNull(instance); + boolean allowMultiple = instance.getClass().isAnnotationPresent(AllowMultiple.class) || exposedType.isAnnotationPresent(AllowMultiple.class); + boolean primary = instance.getClass().isAnnotationPresent(Primary.class); + String name = getNameFromAnnotation(instance.getClass()); + Supplier supplier = () -> instance; + registerSupplierInternal(exposedType, (Class) instance.getClass(), name, supplier, Scope.Type.SINGLETON, primary, allowMultiple); + } + + private void registerSupplierInternal(Class exposedType, Class implType, String name, Supplier supplier, Scope.Type scope, boolean primary, boolean allowMultiple) { + BeanDefinition def = new BeanDefinition<>(exposedType, implType, name, supplier, scope, primary, allowMultiple); + beans.compute(exposedType, (k, list) -> { + if (list == null) list = new ArrayList<>(); + else { + boolean anyAllowed = list.stream().anyMatch(d -> d.allowMultiple) || def.allowMultiple; + if (!list.isEmpty() && !anyAllowed) { + throw new IllegalStateException("Duplicate bean registration for type " + exposedType + " (no AllowMultiple)"); + } + } + list.add(def); + return list; + }); + allDefs.add(def); + } + + private static String getNameFromAnnotation(AnnotatedElement el) { + Named n = el.getAnnotation(Named.class); + return n == null ? null : n.value(); + } + + // --- Retrieval API --- + @SuppressWarnings("unchecked") + public T getBean(Class type) { + List> list = beans.get(type); + if (list == null || list.isEmpty()) throw new NoSuchElementException("No bean for type: " + type); + if (list.size() == 1) return (T) list.get(0).get(this); + Optional> primary = list.stream().filter(d -> d.primary).findFirst(); + if (primary.isPresent()) return (T) primary.get().get(this); + throw new IllegalStateException("Multiple beans found for type " + type + ". Use getBean(type, name) or getBeans(type)"); + } + + @SuppressWarnings("unchecked") + public T getBean(Class type, String name) { + List> list = beans.get(type); + if (list == null) throw new NoSuchElementException("No bean for type: " + type); + for (BeanDefinition d : list) { + if (Objects.equals(d.name, name)) return (T) d.get(this); + } + throw new NoSuchElementException("No bean of type " + type + " with name '" + name + "'"); + } + + @SuppressWarnings("unchecked") + public List getBeans(Class type) { + List> list = beans.get(type); + if (list == null) return Collections.emptyList(); + List out = new ArrayList<>(); + for (BeanDefinition d : list) out.add((T) d.get(this)); + return Collections.unmodifiableList(out); + } + + // --- instantiation & injection --- + T createAndInject(BeanDefinition def) { + T instance = def.supplier.get(); + injectFields(instance); + callAnnotatedMethods(instance, PostConstruct.class); + return instance; + } + + private void injectFields(T instance) { + Class cls = instance.getClass(); + for (Field f : getAllFields(cls)) { + if (f.isAnnotationPresent(Inject.class)) { + boolean accessible = f.canAccess(instance); + try { + f.setAccessible(true); + Class t = f.getType(); + Named n = f.getAnnotation(Named.class); + Object value = (n != null) ? getBean(t, n.value()) : getBean(t); + f.set(instance, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } finally { + try { f.setAccessible(accessible); } catch (Exception ignored) {} + } + } + } + } + + private static List getAllFields(Class cls) { + List fields = new ArrayList<>(); + Class cur = cls; + while (cur != null && cur != Object.class) { + fields.addAll(Arrays.asList(cur.getDeclaredFields())); + cur = cur.getSuperclass(); + } + return fields; + } + + private void callAnnotatedMethods(Object instance, Class annotation) { + Class cls = instance.getClass(); + for (Method m : getAllMethods(cls)) { + if (m.isAnnotationPresent(annotation)) { + boolean accessible = m.canAccess(instance); + try { + m.setAccessible(true); + if (m.getParameterCount() > 0) throw new IllegalStateException("Lifecycle methods must be no-arg: " + m); + m.invoke(instance); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } finally { + try { m.setAccessible(accessible); } catch (Exception ignored) {} + } + } + } + } + + private static List getAllMethods(Class cls) { + List methods = new ArrayList<>(); + Class cur = cls; + while (cur != null && cur != Object.class) { + methods.addAll(Arrays.asList(cur.getDeclaredMethods())); + cur = cur.getSuperclass(); + } + return methods; + } + + @SuppressWarnings("unchecked") + private T instantiateByConstructor(Class implType) { + try { + Constructor[] ctors = implType.getDeclaredConstructors(); + Constructor injectCtor = null; + for (Constructor c : ctors) if (c.isAnnotationPresent(Inject.class)) injectCtor = c; + if (injectCtor == null) { + if (ctors.length == 1) injectCtor = ctors[0]; + else { + try { + injectCtor = implType.getDeclaredConstructor(); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("No @Inject constructor and no default constructor for " + implType); + } + } + } + boolean accessible = injectCtor.canAccess(null); + try { + injectCtor.setAccessible(true); + Object[] args = Arrays.stream(injectCtor.getParameters()).map(p -> resolveParameter(p)).toArray(); + return (T) injectCtor.newInstance(args); + } finally { + try { injectCtor.setAccessible(accessible); } catch (Exception ignored) {} + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private Object resolveParameter(Parameter p) { + Class t = p.getType(); + Named n = p.getAnnotation(Named.class); + if (n != null) return getBean(t, n.value()); + return getBean(t); + } + + @Override + public void close() { + List> copy = new ArrayList<>(allDefs); + Collections.reverse(copy); + for (BeanDefinition d : copy) { + if (d.scope == Scope.Type.SINGLETON && d.singletonInstance != null) { + callAnnotatedMethods(d.singletonInstance, PreDestroy.class); + } + } + } + + public void debugDump() { + System.out.println("Registered beans:"); + for (BeanDefinition d : allDefs) { + System.out.printf(" - exposed=%s impl=%s (name=%s, scope=%s, primary=%s, allowMultiple=%s)\n", d.exposedType.getName(), d.implType.getName(), d.name, d.scope, d.primary, d.allowMultiple); + } + } + + +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/sample/Example.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/sample/Example.java new file mode 100644 index 000000000..c24aeb72f --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test1/sample/Example.java @@ -0,0 +1,65 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package com.condation.cms.tests.injection.test1.sample; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.tests.injection.test1.AllowMultiple; +import com.condation.cms.tests.injection.test1.SimpleDIContainer; + +/** + * + * @author thorstenmarx + */ +public class Example { + // Example usage + public static void main(String[] args) { + SimpleDIContainer c = new SimpleDIContainer(); + + // register interface implementations + c.register(Service.class, ServiceImpl1.class); + c.register(Service.class, ServiceImpl2.class); + + for (Service s : c.getBeans(Service.class)) { + s.execute(); + } + + c.debugDump(); + } + + public interface Service { + void execute(); + } + + @AllowMultiple + public static class ServiceImpl1 implements Service { + public void execute() { System.out.println("ServiceImpl1 executed"); } + } + + @AllowMultiple + public static class ServiceImpl2 implements Service { + public void execute() { System.out.println("ServiceImpl2 executed"); } + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/BeanDefinition.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/BeanDefinition.java new file mode 100644 index 000000000..a8b6a9235 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/BeanDefinition.java @@ -0,0 +1,47 @@ +package com.condation.cms.tests.injection.test2; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public class BeanDefinition { + private final Class type; + private final String name; + private final boolean isPrimary; + private final Object instance; + + public BeanDefinition(Class type, String name, boolean isPrimary, Object instance) { + this.type = type; + this.name = name; + this.isPrimary = isPrimary; + this.instance = instance; + } + + // Getters + public Class getType() { return type; } + public String getName() { return name; } + public boolean isPrimary() { return isPrimary; } + public Object getInstance() { return instance; } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/BeanNotFoundException.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/BeanNotFoundException.java new file mode 100644 index 000000000..e18bb98f0 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/BeanNotFoundException.java @@ -0,0 +1,29 @@ +package com.condation.cms.tests.injection.test2; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +public class BeanNotFoundException extends DIException { + public BeanNotFoundException(String message) { + super(message); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/CircularDependencyException.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/CircularDependencyException.java new file mode 100644 index 000000000..3688dfd29 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/CircularDependencyException.java @@ -0,0 +1,29 @@ +package com.condation.cms.tests.injection.test2; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +public class CircularDependencyException extends DIException { + public CircularDependencyException(String message) { + super(message); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Component.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Component.java new file mode 100644 index 000000000..99bfb011e --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Component.java @@ -0,0 +1,34 @@ +package com.condation.cms.tests.injection.test2; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Component { + String value() default ""; +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/DIException.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/DIException.java new file mode 100644 index 000000000..5722a2697 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/DIException.java @@ -0,0 +1,34 @@ +package com.condation.cms.tests.injection.test2; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +// Exception Classes +public class DIException extends RuntimeException { + public DIException(String message) { + super(message); + } + + public DIException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Inject.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Inject.java new file mode 100644 index 000000000..30c60cfa7 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Inject.java @@ -0,0 +1,33 @@ +package com.condation.cms.tests.injection.test2; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Inject { +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Named.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Named.java new file mode 100644 index 000000000..48ad62ac5 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Named.java @@ -0,0 +1,34 @@ +package com.condation.cms.tests.injection.test2; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Named { + String value(); +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Primary.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Primary.java new file mode 100644 index 000000000..e5be2621a --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/Primary.java @@ -0,0 +1,37 @@ +package com.condation.cms.tests.injection.test2; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Primary { +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/SimpleDIContainer.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/SimpleDIContainer.java new file mode 100644 index 000000000..4e91fbbbf --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/SimpleDIContainer.java @@ -0,0 +1,318 @@ +package com.condation.cms.tests.injection.test2; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Parameter; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class SimpleDIContainer { + private final Map beans = new ConcurrentHashMap<>(); + private final Map, List> beansByType = new ConcurrentHashMap<>(); + private final Set currentlyCreating = new HashSet<>(); + + /** + * Register a bean instance with the container + */ + public void registerBean(Class type, T instance) { + registerBean(type, type.getSimpleName(), instance, false); + } + + /** + * Register a bean instance with name and primary flag + */ + public void registerBean(Class type, String name, T instance, boolean isPrimary) { + Objects.requireNonNull(type, "Type cannot be null"); + Objects.requireNonNull(name, "Name cannot be null"); + Objects.requireNonNull(instance, "Instance cannot be null"); + + BeanDefinition definition = new BeanDefinition(type, name, isPrimary, instance); + beans.put(name, definition); + + // Register by type for lookup + beansByType.computeIfAbsent(type, k -> new ArrayList<>()).add(definition); + + // Also register by all implemented interfaces + for (Class iface : type.getInterfaces()) { + beansByType.computeIfAbsent(iface, k -> new ArrayList<>()).add(definition); + } + + // Register by superclasses + Class superclass = type.getSuperclass(); + while (superclass != null && superclass != Object.class) { + beansByType.computeIfAbsent(superclass, k -> new ArrayList<>()).add(definition); + superclass = superclass.getSuperclass(); + } + } + + /** + * Register a bean class that will be instantiated by the container + */ + public void registerBean(Class type) { + Component component = type.getAnnotation(Component.class); + String name = (component != null && !component.value().isEmpty()) + ? component.value() + : type.getSimpleName(); + + boolean isPrimary = type.isAnnotationPresent(Primary.class); + + try { + T instance = createInstance(type); + registerBean(type, name, instance, isPrimary); + } catch (Exception e) { + throw new DIException("Failed to register bean of type " + type.getName(), e); + } + } + + /** + * Get bean by name + */ + @SuppressWarnings("unchecked") + public T getBean(String name) { + BeanDefinition definition = beans.get(name); + if (definition == null) { + throw new BeanNotFoundException("No bean found with name: " + name); + } + return (T) definition.getInstance(); + } + + /** + * Get bean by type + */ + @SuppressWarnings("unchecked") + public T getBean(Class type) { + List candidates = beansByType.get(type); + if (candidates == null || candidates.isEmpty()) { + throw new BeanNotFoundException("No bean found of type: " + type.getName()); + } + + if (candidates.size() == 1) { + return (T) candidates.get(0).getInstance(); + } + + // Multiple candidates - look for primary + List primaryBeans = candidates.stream() + .filter(BeanDefinition::isPrimary) + .collect(Collectors.toList()); + + if (primaryBeans.size() == 1) { + return (T) primaryBeans.get(0).getInstance(); + } else if (primaryBeans.size() > 1) { + throw new DIException("Multiple primary beans found for type: " + type.getName()); + } + + throw new DIException("Multiple beans found for type " + type.getName() + + " and no primary bean specified. Available beans: " + + candidates.stream().map(BeanDefinition::getName).collect(Collectors.joining(", "))); + } + + /** + * Get bean by type and name + */ + @SuppressWarnings("unchecked") + public T getBean(Class type, String name) { + BeanDefinition definition = beans.get(name); + if (definition == null) { + throw new BeanNotFoundException("No bean found with name: " + name); + } + + if (!type.isAssignableFrom(definition.getType())) { + throw new DIException("Bean with name '" + name + "' is not of type " + type.getName()); + } + + return (T) definition.getInstance(); + } + + /** + * Get all beans of a specific type + */ + @SuppressWarnings("unchecked") + public List getBeansOfType(Class type) { + List candidates = beansByType.get(type); + if (candidates == null) { + return Collections.emptyList(); + } + + return candidates.stream() + .map(def -> (T) def.getInstance()) + .collect(Collectors.toList()); + } + + /** + * Check if container has a bean with given name + */ + public boolean hasBean(String name) { + return beans.containsKey(name); + } + + /** + * Check if container has a bean of given type + */ + public boolean hasBean(Class type) { + List candidates = beansByType.get(type); + return candidates != null && !candidates.isEmpty(); + } + + /** + * Create instance of a class with dependency injection + */ + private T createInstance(Class clazz) { + String className = clazz.getName(); + + // Check for circular dependency + if (currentlyCreating.contains(className)) { + throw new CircularDependencyException("Circular dependency detected for: " + className); + } + + try { + currentlyCreating.add(className); + + // Find constructor to use + Constructor constructor = findConstructor(clazz); + + // Create instance + T instance; + if (constructor.getParameterCount() == 0) { + instance = constructor.newInstance(); + } else { + Object[] args = resolveConstructorArguments(constructor); + instance = constructor.newInstance(args); + } + + // Inject fields + injectFields(instance); + + return instance; + + } catch (Exception e) { + throw new DIException("Failed to create instance of " + className, e); + } finally { + currentlyCreating.remove(className); + } + } + + /** + * Find the constructor to use for instantiation + */ + @SuppressWarnings("unchecked") + private Constructor findConstructor(Class clazz) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + + // Look for @Inject annotated constructor + for (Constructor constructor : constructors) { + if (constructor.isAnnotationPresent(Inject.class)) { + constructor.setAccessible(true); + return (Constructor) constructor; + } + } + + // Fall back to default constructor + try { + Constructor defaultConstructor = clazz.getDeclaredConstructor(); + defaultConstructor.setAccessible(true); + return defaultConstructor; + } catch (NoSuchMethodException e) { + throw new DIException("No suitable constructor found for " + clazz.getName() + + ". Either provide a default constructor or annotate a constructor with @Inject"); + } + } + + /** + * Resolve constructor arguments + */ + private Object[] resolveConstructorArguments(Constructor constructor) { + Parameter[] parameters = constructor.getParameters(); + Object[] args = new Object[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + Class paramType = parameter.getType(); + + Named named = parameter.getAnnotation(Named.class); + if (named != null) { + args[i] = getBean(paramType, named.value()); + } else { + args[i] = getBean(paramType); + } + } + + return args; + } + + /** + * Inject fields marked with @Inject + */ + private void injectFields(Object instance) { + Class clazz = instance.getClass(); + + while (clazz != null && clazz != Object.class) { + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(Inject.class)) { + field.setAccessible(true); + + try { + Object value; + Named named = field.getAnnotation(Named.class); + if (named != null) { + value = getBean(field.getType(), named.value()); + } else { + value = getBean(field.getType()); + } + + field.set(instance, value); + } catch (IllegalAccessException e) { + throw new DIException("Failed to inject field: " + field.getName(), e); + } + } + } + clazz = clazz.getSuperclass(); + } + } + + /** + * Get bean names + */ + public Set getBeanNames() { + return Collections.unmodifiableSet(beans.keySet()); + } + + /** + * Clear all beans + */ + public void clear() { + beans.clear(); + beansByType.clear(); + currentlyCreating.clear(); + } + + /** + * Get container statistics + */ + public String getStats() { + return String.format("DI Container Stats: %d beans registered, %d types covered", + beans.size(), beansByType.size()); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/DIFrameworkDemo.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/DIFrameworkDemo.java new file mode 100644 index 000000000..b1117f921 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/DIFrameworkDemo.java @@ -0,0 +1,72 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package com.condation.cms.tests.injection.test2.example; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.tests.injection.test2.SimpleDIContainer; + +/** + * + * @author thorstenmarx + */ +public class DIFrameworkDemo { + public static void main(String[] args) { + SimpleDIContainer container = new SimpleDIContainer(); + + try { + // Register components + container.registerBean(EmailNotificationService.class); + container.registerBean(SmsNotificationService.class); + container.registerBean(DatabaseUserService.class); + container.registerBean(OrderService.class); + + System.out.println(container.getStats()); + System.out.println("Registered beans: " + container.getBeanNames()); + + // Use the services + UserService userService = container.getBean(UserService.class); + userService.createUser("John Doe"); + + System.out.println("---"); + + OrderService orderService = container.getBean(OrderService.class); + orderService.processOrder("John Doe"); + + System.out.println("---"); + + // Get specific implementation + NotificationService emailService = container.getBean(NotificationService.class, "emailService"); + emailService.sendNotification("Direct email"); + + // Get primary bean (SMS service in this case) + NotificationService primaryService = container.getBean(NotificationService.class); + primaryService.sendNotification("Primary service message"); + + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/DatabaseUserService.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/DatabaseUserService.java new file mode 100644 index 000000000..eba4e4c0c --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/DatabaseUserService.java @@ -0,0 +1,45 @@ +package com.condation.cms.tests.injection.test2.example; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.tests.injection.test2.Component; +import com.condation.cms.tests.injection.test2.Inject; +import com.condation.cms.tests.injection.test2.Named; + +@Component +public class DatabaseUserService implements UserService { + + @Inject + private NotificationService notificationService; + + @Inject + @Named("emailService") + private NotificationService emailService; + + @Override + public void createUser(String name) { + System.out.println("Creating user: " + name); + notificationService.sendNotification("User " + name + " created"); + emailService.sendNotification("Welcome email sent to " + name); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/EmailNotificationService.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/EmailNotificationService.java new file mode 100644 index 000000000..5ea6486e0 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/EmailNotificationService.java @@ -0,0 +1,33 @@ +package com.condation.cms.tests.injection.test2.example; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.tests.injection.test2.Component; + +@Component("emailService") +public class EmailNotificationService implements NotificationService { + @Override + public void sendNotification(String message) { + System.out.println("Email: " + message); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/NotificationService.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/NotificationService.java new file mode 100644 index 000000000..e761ee171 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/NotificationService.java @@ -0,0 +1,31 @@ +package com.condation.cms.tests.injection.test2.example; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public interface NotificationService { + void sendNotification(String message); +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/OrderService.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/OrderService.java new file mode 100644 index 000000000..47d83c852 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/OrderService.java @@ -0,0 +1,45 @@ +package com.condation.cms.tests.injection.test2.example; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.tests.injection.test2.Component; +import com.condation.cms.tests.injection.test2.Inject; +import com.condation.cms.tests.injection.test2.Named; + +@Component +public class OrderService { + private final UserService userService; + private final NotificationService notificationService; + + @Inject + public OrderService(UserService userService, + @Named("smsService") NotificationService notificationService) { + this.userService = userService; + this.notificationService = notificationService; + } + + public void processOrder(String userId) { + System.out.println("Processing order for user: " + userId); + notificationService.sendNotification("Order processed for " + userId); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/SmsNotificationService.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/SmsNotificationService.java new file mode 100644 index 000000000..6a988aec6 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/SmsNotificationService.java @@ -0,0 +1,35 @@ +package com.condation.cms.tests.injection.test2.example; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.tests.injection.test2.Component; +import com.condation.cms.tests.injection.test2.Primary; + +@Component("smsService") +@Primary +public class SmsNotificationService implements NotificationService { + @Override + public void sendNotification(String message) { + System.out.println("SMS: " + message); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/UserService.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/UserService.java new file mode 100644 index 000000000..108d8795e --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/injection/test2/example/UserService.java @@ -0,0 +1,28 @@ +package com.condation.cms.tests.injection.test2.example; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +public // Example Usage and Test Classes +interface UserService { + void createUser(String name); +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/AstNodeFactory.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/AstNodeFactory.java new file mode 100644 index 000000000..86743b2bc --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/AstNodeFactory.java @@ -0,0 +1,39 @@ +package com.condation.cms.tests.template.xml; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.tests.template.xml.ast.AstNode; +import com.condation.cms.tests.template.xml.ast.HtmlElementNode; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +/** + * + * @author thorstenmarx + */ +public interface AstNodeFactory { + + boolean supports(String namespace, String localName); + + AstNode create(XMLStreamReader reader, StAXTemplateRenderer parser, HtmlElementNode current) throws XMLStreamException; +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/DefaultAstNodeFactory.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/DefaultAstNodeFactory.java new file mode 100644 index 000000000..970d84c1c --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/DefaultAstNodeFactory.java @@ -0,0 +1,52 @@ +package com.condation.cms.tests.template.xml; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.tests.template.xml.ast.AstNode; +import com.condation.cms.tests.template.xml.ast.HtmlElementNode; +import java.util.HashMap; +import java.util.Map; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +/** + * + * @author thorstenmarx + */ +public class DefaultAstNodeFactory implements AstNodeFactory { + + @Override + public boolean supports(String namespace, String localName) { + return !"cms".equals(namespace) + && !"view".equalsIgnoreCase(namespace); // fallback for all standard HTML + } + + @Override + public AstNode create(XMLStreamReader reader, StAXTemplateRenderer parser, HtmlElementNode current) throws XMLStreamException { + Map attrs = new HashMap<>(); + for (int i = 0; i < reader.getAttributeCount(); i++) { + attrs.put(reader.getAttributeLocalName(i), reader.getAttributeValue(i)); + } + return new HtmlElementNode(reader.getLocalName(), attrs); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/StAXTemplateRenderer.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/StAXTemplateRenderer.java new file mode 100644 index 000000000..8ff29eed8 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/StAXTemplateRenderer.java @@ -0,0 +1,243 @@ +package com.condation.cms.tests.template.xml; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.tests.template.xml.ast.AstNode; +import com.condation.cms.tests.template.xml.ast.HtmlElementNode; +import com.condation.cms.tests.template.xml.ast.IfNode; +import com.condation.cms.tests.template.xml.ast.IncludeNode; +import com.condation.cms.tests.template.xml.ast.LoopNode; +import com.condation.cms.tests.template.xml.ast.TextNode; +import com.condation.cms.tests.template.xml.ast.ViewNode; +import java.io.InputStream; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +/** + * + * @author thorstenmarx + */ +public class StAXTemplateRenderer implements TemplateLoader { + + private final List factories = new ArrayList<>(); + + public StAXTemplateRenderer() { + registerBuiltInFactories(); + } + + public void registerFactory(AstNodeFactory factory) { + factories.add(0, factory); // first match wins + } + + private void registerBuiltInFactories() { + registerFactory(new AstNodeFactory() { + @Override + public boolean supports(String ns, String local) { + return "view".equals(ns); + } + @Override + public AstNode create(XMLStreamReader reader, StAXTemplateRenderer parser, HtmlElementNode current) throws XMLStreamException { + String key = reader.getLocalName(); + parser.skipToEndElement(reader, reader.getPrefix(), key); + return new ViewNode(key); + } + }); + + registerFactory(new AstNodeFactory() { + @Override + public boolean supports(String ns, String local) { + return "cms".equals(ns) && "if".equals(local); + } + @Override + public AstNode create(XMLStreamReader reader, StAXTemplateRenderer parser, HtmlElementNode current) throws XMLStreamException { + String var = reader.getAttributeValue(null, "var"); + IfNode node = new IfNode(var); + parser.parseChildrenInto(reader, node); + return node; + } + }); + + registerFactory(new AstNodeFactory() { + @Override + public boolean supports(String ns, String local) { + return "cms".equals(ns) && "loop".equals(local); + } + @Override + public AstNode create(XMLStreamReader reader, StAXTemplateRenderer parser, HtmlElementNode current) throws XMLStreamException { + String var = reader.getAttributeValue(null, "var"); + LoopNode node = new LoopNode(var); + parser.parseChildrenInto(reader, node); + return node; + } + }); + + registerFactory(new AstNodeFactory() { + @Override + public boolean supports(String ns, String local) { + return "cms".equals(ns) && "include".equals(local); + } + @Override + public AstNode create(XMLStreamReader reader, StAXTemplateRenderer parser, HtmlElementNode current) throws XMLStreamException { + String name = reader.getAttributeValue(null, "template"); + parser.skipToEndElement(reader, reader.getPrefix(), reader.getLocalName()); + return new IncludeNode(name, parser); + } + }); + + registerFactory(new DefaultAstNodeFactory()); + } + + public AstNode parse(InputStream templateStream) throws XMLStreamException { + XMLInputFactory factory = XMLInputFactory.newFactory(); + XMLStreamReader reader = factory.createXMLStreamReader(templateStream); + return parseNodes(reader); + } + + private AstNode parseNodes(XMLStreamReader reader) throws XMLStreamException { + HtmlElementNode root = new HtmlElementNode("root", Map.of()); + Deque stack = new ArrayDeque<>(); + stack.push(root); + + while (reader.hasNext()) { + int event = reader.next(); + switch (event) { + case XMLStreamConstants.START_ELEMENT -> { + String ns = reader.getPrefix(); + String local = reader.getLocalName(); + + AstNode node = createAstNode(reader, ns, local, stack.peek()); + if (node instanceof HtmlElementNode htmlNode) { + stack.peek().addChild(htmlNode); + stack.push(htmlNode); + } else { + stack.peek().addChild(node); + } + } + case XMLStreamConstants.CHARACTERS -> { + String text = reader.getText(); + if (!text.trim().isEmpty()) { + stack.peek().addChild(new TextNode(text)); + } + } + case XMLStreamConstants.END_ELEMENT -> { + String ns = reader.getPrefix(); + if (!"view".equals(ns) && !"cms".equals(ns)) { + stack.pop(); + } + } + } + } + + return root; + } + + private AstNode createAstNode(XMLStreamReader reader, String ns, String local, HtmlElementNode current) throws XMLStreamException { + for (AstNodeFactory factory : factories) { + if (factory.supports(ns, local)) { + return factory.create(reader, this, current); + } + } + throw new XMLStreamException("No suitable factory for " + ns + ":" + local); + } + + public void parseChildrenInto(XMLStreamReader reader, AstNode parent) throws XMLStreamException { + Deque stack = new ArrayDeque<>(); + HtmlElementNode dummy = new HtmlElementNode("dummy", Map.of()); + stack.push(dummy); + + while (reader.hasNext()) { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT) { + String ns = reader.getPrefix(); + String local = reader.getLocalName(); + AstNode node = createAstNode(reader, ns, local, stack.peek()); + + if (parent instanceof IfNode ifNode) ifNode.addChild(node); + if (parent instanceof LoopNode loopNode) loopNode.addChild(node); + if (node instanceof HtmlElementNode htmlNode) stack.push(htmlNode); + } else if (event == XMLStreamConstants.CHARACTERS) { + String text = reader.getText(); + if (!text.trim().isEmpty()) { + if (parent instanceof IfNode ifNode) ifNode.addChild(new TextNode(text)); + if (parent instanceof LoopNode loopNode) loopNode.addChild(new TextNode(text)); + } + } else if (event == XMLStreamConstants.END_ELEMENT) { + String prefix = reader.getPrefix(); + String local = reader.getLocalName(); + if ("cms".equals(prefix) && ("if".equals(local) || "loop".equals(local))) return; + if (!stack.isEmpty()) stack.pop(); + } + } + } + + public void skipToEndElement(XMLStreamReader reader, String prefix, String localName) throws XMLStreamException { + int depth = 1; + while (reader.hasNext() && depth > 0) { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT && prefix.equals(reader.getPrefix()) && localName.equals(reader.getLocalName())) depth++; + if (event == XMLStreamConstants.END_ELEMENT && prefix.equals(reader.getPrefix()) && localName.equals(reader.getLocalName())) depth--; + } + } + + @Override + public AstNode load(String name) throws Exception { + String included = "

Included Template: " + name + "

"; + InputStream stream = new java.io.ByteArrayInputStream(included.getBytes()); + return parse(stream); + } + + public static void main(String[] args) throws Exception { + String template = """ + +

+

This is static HTML content.

+ +

Hello,

+
+ +
  • +
    + +
    + """; + + Map context = new HashMap<>(); + context.put("title", "Welcome!"); + context.put("isLoggedIn", true); + context.put("username", "Thorsten"); + context.put("items", List.of("One", "Two", "Three")); + + InputStream stream = new java.io.ByteArrayInputStream(template.getBytes()); + StAXTemplateRenderer renderer = new StAXTemplateRenderer(); + AstNode ast = renderer.parse(stream); + + System.out.println(ast.render(context)); + } +} \ No newline at end of file diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/TemplateLoader.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/TemplateLoader.java new file mode 100644 index 000000000..21071abcf --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/TemplateLoader.java @@ -0,0 +1,34 @@ +package com.condation.cms.tests.template.xml; + +import com.condation.cms.tests.template.xml.ast.AstNode; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public interface TemplateLoader { + + AstNode load(String name) throws Exception; +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/AstNode.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/AstNode.java new file mode 100644 index 000000000..68366f06d --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/AstNode.java @@ -0,0 +1,31 @@ +package com.condation.cms.tests.template.xml.ast; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.*; + +// === AST Nodes === +public abstract class AstNode { + + public abstract String render(Map context); +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/HtmlElementNode.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/HtmlElementNode.java new file mode 100644 index 000000000..e8731ea76 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/HtmlElementNode.java @@ -0,0 +1,60 @@ +package com.condation.cms.tests.template.xml.ast; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * + * @author thorstenmarx + */ +public class HtmlElementNode extends AstNode { + + private final String tagName; + private final Map attributes; + private final List children = new ArrayList<>(); + + public HtmlElementNode(String tagName, Map attributes) { + this.tagName = tagName; + this.attributes = attributes; + } + + public void addChild(AstNode node) { + children.add(node); + } + + @Override + public String render(Map context) { + StringBuilder sb = new StringBuilder(); + sb.append("<").append(tagName); + attributes.forEach((k, v) -> sb.append(" ").append(k).append("=\"").append(v).append("\"")); + sb.append(">"); + for (AstNode child : children) { + sb.append(child.render(context)); + } + sb.append(""); + return sb.toString(); + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/IfNode.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/IfNode.java new file mode 100644 index 000000000..45061607b --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/IfNode.java @@ -0,0 +1,57 @@ +package com.condation.cms.tests.template.xml.ast; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * + * @author thorstenmarx + */ +public class IfNode extends AstNode { + + private final String var; + private final List children = new ArrayList<>(); + + public IfNode(String var) { + this.var = var; + } + + public void addChild(AstNode node) { + children.add(node); + } + + @Override + public String render(Map context) { + if (Boolean.TRUE.equals(context.get(var))) { + StringBuilder sb = new StringBuilder(); + for (AstNode child : children) { + sb.append(child.render(context)); + } + return sb.toString(); + } + return ""; + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/IncludeNode.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/IncludeNode.java new file mode 100644 index 000000000..0bf824807 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/IncludeNode.java @@ -0,0 +1,51 @@ +package com.condation.cms.tests.template.xml.ast; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.tests.template.xml.TemplateLoader; +import java.util.Map; + +/** + * + * @author thorstenmarx + */ +public class IncludeNode extends AstNode { + + private final String templateName; + private final TemplateLoader loader; + + public IncludeNode(String templateName, TemplateLoader loader) { + this.templateName = templateName; + this.loader = loader; + } + + @Override + public String render(Map context) { + try { + AstNode node = loader.load(templateName); + return node.render(context); + } catch (Exception e) { + return ""; + } + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/LoopNode.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/LoopNode.java new file mode 100644 index 000000000..bfdf66043 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/LoopNode.java @@ -0,0 +1,63 @@ +package com.condation.cms.tests.template.xml.ast; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + * @author thorstenmarx + */ +public class LoopNode extends AstNode { + + private final String var; + private final List children = new ArrayList<>(); + + public LoopNode(String var) { + this.var = var; + } + + public void addChild(AstNode node) { + children.add(node); + } + + @Override + public String render(Map context) { + Object value = context.get(var); + if (value instanceof Iterable iterable) { + StringBuilder sb = new StringBuilder(); + for (Object item : iterable) { + Map localContext = new HashMap<>(context); + localContext.put("item", item); + for (AstNode child : children) { + sb.append(child.render(localContext)); + } + } + return sb.toString(); + } + return ""; + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/TextNode.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/TextNode.java new file mode 100644 index 000000000..92d252869 --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/TextNode.java @@ -0,0 +1,43 @@ +package com.condation.cms.tests.template.xml.ast; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.Map; + +/** + * + * @author thorstenmarx + */ +public class TextNode extends AstNode { + + private final String text; + + public TextNode(String text) { + this.text = text; + } + + @Override + public String render(Map context) { + return text; + } +} diff --git a/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/ViewNode.java b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/ViewNode.java new file mode 100644 index 000000000..166b9ac8d --- /dev/null +++ b/cms-sandbox/tests/src/main/java/com/condation/cms/tests/template/xml/ast/ViewNode.java @@ -0,0 +1,39 @@ +package com.condation.cms.tests.template.xml.ast; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.Map; + +public class ViewNode extends AstNode { + + private final String key; + + public ViewNode(String key) { + this.key = key; + } + + @Override + public String render(Map context) { + return String.valueOf(context.getOrDefault(key, "")); + } +} diff --git a/cms-sandbox/tests/src/test/java/com/condation/cms/tests/expressions/ExpressionEngineTest.java b/cms-sandbox/tests/src/test/java/com/condation/cms/tests/expressions/ExpressionEngineTest.java new file mode 100644 index 000000000..218574f24 --- /dev/null +++ b/cms-sandbox/tests/src/test/java/com/condation/cms/tests/expressions/ExpressionEngineTest.java @@ -0,0 +1,149 @@ +package com.condation.cms.tests.expressions; + +/*- + * #%L + * tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ExpressionEngineTest { + + private ExpressionEngine engine; + private Map ctx; + + @BeforeEach + void setup() { + engine = new ExpressionEngine(); + + // Beispiel: einfache globale Methode + engine.registerMethod("contains", argsList -> { + if (argsList.size() < 2) return false; + Object val = argsList.get(0); + Object part = argsList.get(1); + return val != null && val.toString().contains(part.toString()); + }); + + ctx = new HashMap<>(); + ctx.put("user", Map.of("name", "Thorsten", "age", 42)); + ctx.put("numbers", Arrays.asList(1, 2, 3, 4)); + ctx.put("nested", Map.of("inner", Map.of("key", "value"))); + } + + @Test + void testBooleanAndNullLiterals() { + assertThat(engine.evaluate("true", ctx)).isEqualTo(true); + assertThat(engine.evaluate("false", ctx)).isEqualTo(false); + assertThat(engine.evaluate("null", ctx)).isNull(); + } + + @Test + void testNumberLiterals() { + assertThat(engine.evaluate("123", ctx)).isEqualTo(123); + assertThat(engine.evaluate("-5", ctx)).isEqualTo(-5); + assertThat(engine.evaluate("3.14", ctx)).isEqualTo(3.14); + } + + @Test + void testStringLiterals() { + assertThat(engine.evaluate("\"Hello\"", ctx)).isEqualTo("Hello"); + assertThat(engine.evaluate("\"123\"", ctx)).isEqualTo("123"); + } + + @Test + void testListAndMapLiterals() { + assertThat((List)engine.evaluate("[1, 2, 3]", ctx)).containsExactly(1, 2, 3); + Map expectedMap = new LinkedHashMap<>(); + expectedMap.put("x", 1); + expectedMap.put("y", 2); + assertThat(engine.evaluate("{x: 1, y: 2}", ctx)).isEqualTo(expectedMap); + } + + @Test + void testOperators() { + ctx.put("val1", 10); + ctx.put("val2", 20); + + assertThat(engine.evaluate("val1 eq 10", ctx)).isEqualTo(true); + assertThat(engine.evaluate("val1 lt val2", ctx)).isEqualTo(true); + assertThat(engine.evaluate("val1 lte val2", ctx)).isEqualTo(true); + assertThat(engine.evaluate("val2 gt val1", ctx)).isEqualTo(true); + assertThat(engine.evaluate("val2 gte val1", ctx)).isEqualTo(true); + assertThat(engine.evaluate("true and false", ctx)).isEqualTo(false); + assertThat(engine.evaluate("true or false", ctx)).isEqualTo(true); + assertThat(engine.evaluate("not true", ctx)).isEqualTo(false); + } + + @Test + void testObjectAndMapAccess() { + assertThat(engine.evaluate("user.name", ctx)).isEqualTo("Thorsten"); + assertThat(engine.evaluate("user.age", ctx)).isEqualTo(42); + assertThat(engine.evaluate("nested.inner.key", ctx)).isEqualTo("value"); + ctx.put("data", Map.of("users", Arrays.asList(Map.of("name", "John"), Map.of("name", "Jane")))); + assertThat(engine.evaluate("data.users[1].name", ctx)).isEqualTo("Jane"); + } + + @Test + void testListAccess() { + assertThat(engine.evaluate("numbers[0]", ctx)).isEqualTo(1); + assertThat(engine.evaluate("numbers[3]", ctx)).isEqualTo(4); + } + + @Test + void testGlobalMethods() { + assertThat(engine.evaluate("contains(user.name, \"ors\")", ctx)).isEqualTo(true); + assertThat(engine.evaluate("contains(user.name, \"xyz\")", ctx)).isEqualTo(false); + } + + @Test + void testComplexExpression() { + assertThat(engine.evaluate("(user.age gt 30) and contains(user.name, \"Thor\")", ctx)).isEqualTo(true); + assertThat(engine.evaluate("not (user.age lt 20) or contains(user.name, \"xyz\")", ctx)).isEqualTo(true); + } + + @Test + void testExceptionHandling() { + assertThatThrownBy(() -> engine.evaluate("user.nonexistent", ctx)) + .isInstanceOf(EvaluationException.class) + .hasMessageContaining("Could not resolve part 'nonexistent'"); + + assertThatThrownBy(() -> engine.evaluate("numbers[99]", ctx)) + .isInstanceOf(EvaluationException.class) + .hasMessageContaining("Index 99 out of bounds"); + + assertThatThrownBy(() -> engine.evaluate("user.name[0]", ctx)) + .isInstanceOf(EvaluationException.class) + .hasMessageContaining("Cannot access by index on non-list object"); + + assertThatThrownBy(() -> engine.evaluate("10 eq ", ctx)) + .isInstanceOf(ExpressionParseException.class) + .hasMessageContaining("Missing right operand for operator: eq"); + + assertThatThrownBy(() -> engine.evaluate("nonexistent.value", ctx)) + .isInstanceOf(EvaluationException.class) + .hasMessageContaining("Cannot resolve part 'value' on null object"); + } +} diff --git a/cms-server/nbactions-dist.xml b/cms-server/nbactions-dist.xml index 3e7e89108..aa0fb0a2b 100644 --- a/cms-server/nbactions-dist.xml +++ b/cms-server/nbactions-dist.xml @@ -10,12 +10,12 @@ org.codehaus.mojo:exec-maven-plugin:3.1.0:exec - + -DCMS_HOME=../test-server ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} server start com.condation.cms.cli.CMSCli java - C:\entwicklung\workspaces\tma\cms\cms-server\test-server + ../test-server @@ -28,13 +28,13 @@ org.codehaus.mojo:exec-maven-plugin:3.1.0:exec - -agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address} + -DCMS_HOME=../test-server -agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address} ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} server start com.condation.cms.cli.CMSCli java true - C:\entwicklung\workspaces\tma\cms\cms-server\test-server + ../test-server @@ -47,12 +47,12 @@ org.codehaus.mojo:exec-maven-plugin:3.1.0:exec - + -DCMS_HOME=../test-server ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} com.condation.cms.cli.CMSCli java server start - C:\entwicklung\workspaces\tma\cms\cms-server\test-server + ../test-server diff --git a/cms-server/pom.xml b/cms-server/pom.xml index 8e0443136..89a306fb4 100644 --- a/cms-server/pom.xml +++ b/cms-server/pom.xml @@ -4,14 +4,14 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 cms-server jar UTF-8 - 21 - 21 + 25 + 25 com.condation.cms.cli.CMSCli yyyy-MM-dd HH:mm @@ -33,10 +33,6 @@ com.condation.cms cms-media - - com.condation.cms - cms-git - com.condation.cms cms-content @@ -58,6 +54,10 @@ com.condation.cms.modules api-module + + com.condation.cms.modules + ui-module + com.condation.cms @@ -73,6 +73,26 @@ org.eclipse.jetty jetty-server + + org.eclipse.jetty.compression + jetty-compression-server + ${jetty.version} + + + org.eclipse.jetty.compression + jetty-compression-brotli + ${jetty.version} + + + org.eclipse.jetty.compression + jetty-compression-gzip + ${jetty.version} + + + org.eclipse.jetty.compression + jetty-compression-zstandard + ${jetty.version} + org.projectlombok lombok @@ -135,12 +155,17 @@ true - - - org.apache.maven.plugins - maven-compiler-plugin - - + + + org.apache.maven.plugins + maven-compiler-plugin + + + --enable-preview + + + + diff --git a/cms-server/src/main/java/com/condation/cms/CMSServer.java b/cms-server/src/main/java/com/condation/cms/CMSServer.java index 2fb9deb9e..3f52bbfa4 100644 --- a/cms-server/src/main/java/com/condation/cms/CMSServer.java +++ b/cms-server/src/main/java/com/condation/cms/CMSServer.java @@ -29,7 +29,6 @@ import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.util.Properties; import lombok.extern.slf4j.Slf4j; import org.semver4j.Semver; diff --git a/cms-server/src/main/java/com/condation/cms/cli/CLICommand.java b/cms-server/src/main/java/com/condation/cms/cli/CLICommand.java index 2c7726b83..0f74fc203 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/CLICommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/CLICommand.java @@ -31,7 +31,11 @@ * * @author t.marx */ -@CommandLine.Command(name = "", subcommands = { +@CommandLine.Command( + name = "", + description = "CondationServer CLI", + mixinStandardHelpOptions = true, + subcommands = { ServerCommand.class, HostCommands.class, ExtensionCommands.class, ModuleCommands.class, ThemeCommands.class, RepoCommands.class}) @Slf4j public class CLICommand implements Runnable { diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/ExtensionCommands.java b/cms-server/src/main/java/com/condation/cms/cli/commands/ExtensionCommands.java index 62be9351c..723cf5b94 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/ExtensionCommands.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/ExtensionCommands.java @@ -34,6 +34,10 @@ */ @CommandLine.Command( name = "extension", + mixinStandardHelpOptions = true, + description = { + "commands to manage extensions" + }, subcommands = { InfoCommand.class, InstallCommand.class diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/HostCommands.java b/cms-server/src/main/java/com/condation/cms/cli/commands/HostCommands.java index 1b6d36896..ea62acca4 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/HostCommands.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/HostCommands.java @@ -33,6 +33,10 @@ */ @CommandLine.Command( name = "host", + description = { + "Host/site related commands" + }, + mixinStandardHelpOptions = true, subcommands = { ReloadHost.class }) diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/ModuleCommands.java b/cms-server/src/main/java/com/condation/cms/cli/commands/ModuleCommands.java index 8a232d208..8b1d9d5ca 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/ModuleCommands.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/ModuleCommands.java @@ -36,6 +36,10 @@ */ @CommandLine.Command( name = "module", + mixinStandardHelpOptions = true, + description = { + "commands to manage modules" + }, subcommands = { InfoCommand.class, GetCommand.class, diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/RepoCommands.java b/cms-server/src/main/java/com/condation/cms/cli/commands/RepoCommands.java index b2c051e6b..f1724bcce 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/RepoCommands.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/RepoCommands.java @@ -33,6 +33,10 @@ */ @CommandLine.Command( name = "repo", + mixinStandardHelpOptions = true, + description = { + "commands to manage git repos" + }, subcommands = { Checkout.class }) diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/ServerCommand.java b/cms-server/src/main/java/com/condation/cms/cli/commands/ServerCommand.java index bb146644d..19fc8f80b 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/ServerCommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/ServerCommand.java @@ -24,6 +24,7 @@ import com.condation.cms.cli.commands.server.AddUser; +import com.condation.cms.cli.commands.server.Info; import com.condation.cms.cli.commands.server.RemoveUser; import com.condation.cms.cli.commands.server.Startup; import com.condation.cms.cli.commands.server.Stop; @@ -34,8 +35,14 @@ * * @author t.marx */ -@CommandLine.Command(name = "server", subcommands = { - Startup.class, AddUser.class, RemoveUser.class, Stop.class, HostCommands.class, ExtensionCommands.class, ModuleCommands.class, ThemeCommands.class}) +@CommandLine.Command(name = "server", + descriptionHeading = "Server commands", + description = { + "Commands to manage server related issues." + }, + mixinStandardHelpOptions = true, + subcommands = { + Startup.class, AddUser.class, RemoveUser.class, Stop.class, Info.class}) @Slf4j public class ServerCommand implements Runnable { diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/ThemeCommands.java b/cms-server/src/main/java/com/condation/cms/cli/commands/ThemeCommands.java index c0f66cbed..98605c77f 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/ThemeCommands.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/ThemeCommands.java @@ -36,6 +36,10 @@ */ @CommandLine.Command( name = "theme", + mixinStandardHelpOptions = true, + description = { + "commands to manage themes" + }, subcommands = { InfoCommand.class, GetCommand.class, diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/extensions/InfoCommand.java b/cms-server/src/main/java/com/condation/cms/cli/commands/extensions/InfoCommand.java index c4283a27c..2b94b41a9 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/extensions/InfoCommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/extensions/InfoCommand.java @@ -25,7 +25,6 @@ import com.condation.cms.CMSServer; import com.condation.cms.extensions.repository.ExtensionInfo; -import com.condation.cms.extensions.repository.RemoteRepository; import com.google.common.base.Strings; import java.util.Optional; import lombok.Setter; @@ -35,7 +34,12 @@ * * @author t.marx */ -@CommandLine.Command(name = "info") +@CommandLine.Command( + name = "info", + description = { + "displays some information about a extension" + } +) public class InfoCommand extends AbstractExtensionCommand implements Runnable { @CommandLine.Parameters( diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/extensions/InstallCommand.java b/cms-server/src/main/java/com/condation/cms/cli/commands/extensions/InstallCommand.java index dcd31ee41..871227ffb 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/extensions/InstallCommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/extensions/InstallCommand.java @@ -27,11 +27,9 @@ -import com.condation.cms.extensions.repository.RemoteRepository; import com.google.common.base.Strings; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.util.Optional; import lombok.extern.slf4j.Slf4j; import picocli.CommandLine; @@ -41,7 +39,12 @@ * @author t.marx */ @Slf4j -@CommandLine.Command(name = "install") +@CommandLine.Command( + name = "install", + description = { + "intalls an extension into a host" + } +) public class InstallCommand extends AbstractExtensionCommand implements Runnable { diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/host/ReloadHost.java b/cms-server/src/main/java/com/condation/cms/cli/commands/host/ReloadHost.java index 9cf23af69..4899fc2de 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/host/ReloadHost.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/host/ReloadHost.java @@ -33,7 +33,6 @@ import com.condation.cms.ipc.IPCClient; import com.google.common.base.Strings; import java.nio.file.Files; -import java.nio.file.Path; import java.util.Optional; import lombok.extern.slf4j.Slf4j; import picocli.CommandLine; @@ -42,7 +41,12 @@ * * @author t.marx */ -@CommandLine.Command(name = "reload") +@CommandLine.Command( + name = "reload", + description = { + "reloads a host" + } +) @Slf4j public class ReloadHost implements Runnable { diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/modules/GetAllCommand.java b/cms-server/src/main/java/com/condation/cms/cli/commands/modules/GetAllCommand.java index 7f2e4700f..4cd579432 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/modules/GetAllCommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/modules/GetAllCommand.java @@ -26,7 +26,6 @@ import com.condation.cms.cli.tools.ModulesUtil; import com.condation.cms.extensions.repository.InstallationHelper; import java.io.IOException; -import java.nio.file.Path; import lombok.extern.slf4j.Slf4j; import picocli.CommandLine; @@ -35,7 +34,13 @@ * @author t.marx */ @Slf4j -@CommandLine.Command(name = "get-all") +@CommandLine.Command( + name = "get-all", + description = { + "gets all modules that are use by theme or host", + "to update already installed modules use -f" + } +) public class GetAllCommand extends AbstractModuleCommand implements Runnable { @CommandLine.Option(names = "-f", description = "force the update if module is already installed") diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/modules/GetCommand.java b/cms-server/src/main/java/com/condation/cms/cli/commands/modules/GetCommand.java index 0f8a2a234..7f6aec107 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/modules/GetCommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/modules/GetCommand.java @@ -30,7 +30,6 @@ import static com.condation.cms.cli.commands.modules.AbstractModuleCommand.createModulesFolder; import com.condation.cms.extensions.repository.InstallationHelper; import java.io.IOException; -import java.nio.file.Path; import lombok.extern.slf4j.Slf4j; import picocli.CommandLine; @@ -39,7 +38,13 @@ * @author t.marx */ @Slf4j -@CommandLine.Command(name = "get") +@CommandLine.Command( + name = "get", + description = { + "gets a single module by id if not installed already", + "for update use -f to force reinstall" + } +) public class GetCommand extends AbstractModuleCommand implements Runnable { @CommandLine.Parameters( diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/modules/InfoCommand.java b/cms-server/src/main/java/com/condation/cms/cli/commands/modules/InfoCommand.java index 163aa9de2..6855f8f55 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/modules/InfoCommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/modules/InfoCommand.java @@ -31,7 +31,12 @@ * * @author t.marx */ -@CommandLine.Command(name = "info") +@CommandLine.Command( + name = "info", + description = { + "displays some information about a module" + } +) public class InfoCommand extends AbstractModuleCommand implements Runnable { @CommandLine.Parameters( diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/modules/RemoveCommand.java b/cms-server/src/main/java/com/condation/cms/cli/commands/modules/RemoveCommand.java index ac1b755bf..29c8b37bd 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/modules/RemoveCommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/modules/RemoveCommand.java @@ -34,7 +34,13 @@ * @author t.marx */ @Slf4j -@CommandLine.Command(name = "remove") +@CommandLine.Command( + name = "remove", + description = { + "removes an installed module", + "prints an error if the module is in use by a host or theme" + } +) public class RemoveCommand extends AbstractModuleCommand implements Runnable { @CommandLine.Parameters( diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/repo/Checkout.java b/cms-server/src/main/java/com/condation/cms/cli/commands/repo/Checkout.java index f49aa1893..9e5180041 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/repo/Checkout.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/repo/Checkout.java @@ -41,7 +41,12 @@ * * @author t.marx */ -@CommandLine.Command(name = "checkout") +@CommandLine.Command( + name = "checkout", + description = { + "forces the immediate mull of the repo" + } +) @Slf4j public class Checkout implements Runnable { diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/server/AddUser.java b/cms-server/src/main/java/com/condation/cms/cli/commands/server/AddUser.java index ca5730f03..6da6854dd 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/server/AddUser.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/server/AddUser.java @@ -2,6 +2,7 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.utils.ServerUtil; +import com.condation.cms.auth.services.Realm; /*- * #%L @@ -27,7 +28,9 @@ import com.condation.cms.auth.services.UserService; -import java.nio.file.Path; +import com.google.common.base.Strings; +import java.util.HashMap; +import java.util.Map; import lombok.extern.slf4j.Slf4j; import picocli.CommandLine; import picocli.CommandLine.Parameters; @@ -36,18 +39,20 @@ * * @author t.marx */ -@CommandLine.Command(name = "add_user") +@CommandLine.Command( + name = "add_user", + description = { + "adds a user to a realm" + } +) @Slf4j public class AddUser implements Runnable { @CommandLine.Option(names = {"-r", "--realm"}, description = "The realm") String realm = "users"; - - @CommandLine.Option(names = {"-h", "--host"}, description = "The host", required = true) - String host = null; - @CommandLine.Option(names = {"-g", "--groups"}, description = "The groups", split = ",") - String[] groups = null; + @CommandLine.Option(names = {"-ro", "--roles"}, description = "The roles", split = ",") + String[] roles = null; @Parameters( paramLabel = "", @@ -63,12 +68,23 @@ public class AddUser implements Runnable { ) private String password = ""; + @Parameters( + paramLabel = "", + index = "2", + description = "The users mail address." + ) + private String mail = ""; + @Override public void run() { try { - UserService userService = new UserService(ServerUtil.getPath(Constants.Folders.HOSTS).resolve(host)); + UserService userService = new UserService(ServerUtil.getHome()); - userService.addUser(UserService.Realm.of(realm), username, password, groups); + Map data = new HashMap<>(); + if (!Strings.isNullOrEmpty(mail)) { + data.put("mail", mail); + } + userService.addUser(Realm.of(realm), username, password, roles, data); log.info("user added successfuly"); } catch (Exception e) { throw new RuntimeException(e); diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/server/Info.java b/cms-server/src/main/java/com/condation/cms/cli/commands/server/Info.java new file mode 100644 index 000000000..90406b3dd --- /dev/null +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/server/Info.java @@ -0,0 +1,62 @@ +package com.condation.cms.cli.commands.server; + +/*- + * #%L + * cms-server + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + + + +import com.condation.cms.cli.tools.CLIServerUtils; +import lombok.extern.slf4j.Slf4j; +import picocli.CommandLine; + +/** + * + * @author t.marx + */ +@CommandLine.Command( + name = "info", + description = { + "returns some information about the server" + } +) +@Slf4j +public class Info implements Runnable { + + @Override + public void run() { + try { + var info = new StringBuilder(); + + info.append("Info about CMS-Server").append(System.lineSeparator()); + info.append("=====================").append(System.lineSeparator()); + info.append("Version: ").append(CLIServerUtils.getVersion().toString()).append(System.lineSeparator()); + info.append("Running: ").append(CLIServerUtils.getCMSProcess().isPresent()).append(System.lineSeparator()); + info.append("Started at: ").append(CLIServerUtils.getStartedAt()).append(System.lineSeparator()); + info.append("Uptime: ").append(CLIServerUtils.getUptime()).append(System.lineSeparator()); + + System.out.println(info.toString()); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/server/RemoveUser.java b/cms-server/src/main/java/com/condation/cms/cli/commands/server/RemoveUser.java index 630b9fbfa..b6e2e34f6 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/server/RemoveUser.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/server/RemoveUser.java @@ -2,6 +2,7 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.utils.ServerUtil; +import com.condation.cms.auth.services.Realm; /*- * #%L @@ -27,7 +28,6 @@ import com.condation.cms.auth.services.UserService; -import java.nio.file.Path; import lombok.extern.slf4j.Slf4j; import picocli.CommandLine; import picocli.CommandLine.Parameters; @@ -36,15 +36,17 @@ * * @author t.marx */ -@CommandLine.Command(name = "remove_user") +@CommandLine.Command( + name = "remove_user", + description = { + "removes a user from the given realm" + } +) @Slf4j public class RemoveUser implements Runnable { @CommandLine.Option(names = {"-r", "--realm"}, description = "The realm") String realm = "users"; - - @CommandLine.Option(names = {"-h", "--host"}, description = "The host", required = true) - String host = null; @Parameters( paramLabel = "", @@ -56,9 +58,9 @@ public class RemoveUser implements Runnable { @Override public void run() { try { - UserService userService = new UserService(ServerUtil.getPath(Constants.Folders.HOSTS).resolve(host)); + UserService userService = new UserService(ServerUtil.getHome()); - userService.removeUser(UserService.Realm.of(realm), username); + userService.removeUser(Realm.of(realm), username); log.info("user added successfuly"); } catch (Exception e) { throw new RuntimeException(e); diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/server/Startup.java b/cms-server/src/main/java/com/condation/cms/cli/commands/server/Startup.java index e0b38dc48..94db5bd08 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/server/Startup.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/server/Startup.java @@ -24,19 +24,17 @@ -import com.condation.cms.api.Constants; import com.condation.cms.api.ServerContext; import com.condation.cms.api.ServerProperties; -import com.condation.cms.api.utils.ServerUtil; +import com.condation.cms.cli.tools.CLIServerUtils; import com.condation.cms.cli.tools.ModulesUtil; import com.condation.cms.cli.tools.ThemesUtil; -import com.condation.cms.git.RepositoryManager; import com.condation.cms.ipc.IPCServer; import com.condation.cms.server.configs.ServerGlobalModule; import com.condation.cms.server.JettyServer; import com.google.inject.Guice; +import com.google.inject.Stage; import java.io.IOException; -import java.nio.file.Files; import java.util.Properties; import lombok.extern.slf4j.Slf4j; import picocli.CommandLine; @@ -45,7 +43,9 @@ * * @author t.marx */ -@CommandLine.Command(name = "start") +@CommandLine.Command( + name = "start", + description = "starts the server") @Slf4j public class Startup implements Runnable { @@ -53,6 +53,12 @@ public class Startup implements Runnable { public void run() { try { + var cmsProcess = CLIServerUtils.getCMSProcess(); + if (cmsProcess.isPresent()) { + System.err.println("cms server is running, please stop it firste"); + System.exit(10); + } + System.setProperty("polyglot.engine.WarnInterpreterOnly", "false"); System.setProperty("polyglotimpl.DisableClassPathIsolation", "true"); //System.setProperty("polyglot.engine.WarnVirtualThreadSupport", "false"); @@ -66,8 +72,6 @@ public void run() { printStartup(properties); ServerContext.IS_DEV = properties.dev(); - - globalInjector.getInstance(RepositoryManager.class); var server = new JettyServer(globalInjector); @@ -75,7 +79,7 @@ public void run() { ipcServer.start(); server.startup(); - writePidFile(); + CLIServerUtils.writePidFile(); } catch (Exception e) { throw new RuntimeException(e); } @@ -108,12 +112,6 @@ private void checkInstalledModules() { log.trace("all required modules are intalled"); } } - - - private static void writePidFile () throws IOException { - Files.deleteIfExists(ServerUtil.getPath(Constants.PID_FILE)); - Files.writeString(ServerUtil.getPath(Constants.PID_FILE), String.valueOf(ProcessHandle.current().pid())); - } private static void printStartup(ServerProperties properties) throws IOException { diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/server/Stop.java b/cms-server/src/main/java/com/condation/cms/cli/commands/server/Stop.java index a15be1fd9..9d61fd262 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/server/Stop.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/server/Stop.java @@ -21,12 +21,10 @@ * . * #L% */ - - - import com.condation.cms.api.Constants; import com.condation.cms.api.ServerProperties; import com.condation.cms.api.utils.ServerUtil; +import com.condation.cms.cli.tools.CLIServerUtils; import com.condation.cms.core.configuration.ConfigurationFactory; import com.condation.cms.core.configuration.properties.ExtendedServerProperties; import com.condation.cms.ipc.Command; @@ -41,41 +39,35 @@ * * @author t.marx */ -@CommandLine.Command(name = "stop") +@CommandLine.Command( + name = "stop", + description = { + "Stops the server" + } +) @Slf4j public class Stop implements Runnable { @Override public void run() { try { - - Optional handle = getCMSProcess(); - + + Optional handle = CLIServerUtils.getCMSProcess(); + if (handle.isEmpty()) { System.out.println("can not find cms process"); } else { ServerProperties properties = new ExtendedServerProperties(ConfigurationFactory.serverConfiguration()); IPCClient ipcClient = new IPCClient(properties.ipc()); - + ipcClient.send(new Command("shutdown")); - + Files.deleteIfExists(ServerUtil.getPath(Constants.PID_FILE)); } - - + } catch (Exception e) { throw new RuntimeException(e); } } - - private static Optional getCMSProcess () throws Exception { - var pidFile = ServerUtil.getPath(Constants.PID_FILE); - if (!Files.exists(pidFile)) { - return Optional.empty(); - } - var pid = Files.readString(pidFile); - return ProcessHandle.of(Long.parseLong(pid.trim())); - } - } diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/themes/GetAllCommand.java b/cms-server/src/main/java/com/condation/cms/cli/commands/themes/GetAllCommand.java index 66d440ed9..606d91ced 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/themes/GetAllCommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/themes/GetAllCommand.java @@ -37,7 +37,13 @@ * @author t.marx */ @Slf4j -@CommandLine.Command(name = "get-all") +@CommandLine.Command( + name = "get-all", + description = { + "gets all themes used by host or child theme", + "to force update of already installed themes use -f" + } +) public class GetAllCommand extends AbstractThemeCommand implements Runnable { @CommandLine.Option(names = "-f", description = "force the update if theme is already installed") diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/themes/GetCommand.java b/cms-server/src/main/java/com/condation/cms/cli/commands/themes/GetCommand.java index a314c788b..f5f01bd17 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/themes/GetCommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/themes/GetCommand.java @@ -28,7 +28,6 @@ import com.condation.cms.extensions.repository.InstallationHelper; import com.google.common.base.Strings; -import java.nio.file.Path; import lombok.extern.slf4j.Slf4j; import picocli.CommandLine; @@ -37,7 +36,13 @@ * @author t.marx */ @Slf4j -@CommandLine.Command(name = "get") +@CommandLine.Command( + name = "get", + description = { + "gets a theme from registry by id", + "to force update of already installed theme use -f" + } +) public class GetCommand extends AbstractThemeCommand implements Runnable { @CommandLine.Parameters( diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/themes/InfoCommand.java b/cms-server/src/main/java/com/condation/cms/cli/commands/themes/InfoCommand.java index 24e2942f7..9c6315dd2 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/themes/InfoCommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/themes/InfoCommand.java @@ -34,7 +34,12 @@ * @author t.marx */ @Slf4j -@CommandLine.Command(name = "info") +@CommandLine.Command( + name = "info", + description = { + "prints some information about a theme" + } +) public class InfoCommand extends AbstractThemeCommand implements Runnable { @CommandLine.Parameters( diff --git a/cms-server/src/main/java/com/condation/cms/cli/commands/themes/RemoveCommand.java b/cms-server/src/main/java/com/condation/cms/cli/commands/themes/RemoveCommand.java index 9e645baff..d3dcfaaa4 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/commands/themes/RemoveCommand.java +++ b/cms-server/src/main/java/com/condation/cms/cli/commands/themes/RemoveCommand.java @@ -34,7 +34,12 @@ * @author t.marx */ @Slf4j -@CommandLine.Command(name = "remove") +@CommandLine.Command( + name = "remove", + description = { + "removes a theme if it is not in use anymore" + } +) public class RemoveCommand extends AbstractThemeCommand implements Runnable { @CommandLine.Parameters( diff --git a/cms-server/src/main/java/com/condation/cms/cli/tools/CLIServerUtils.java b/cms-server/src/main/java/com/condation/cms/cli/tools/CLIServerUtils.java new file mode 100644 index 000000000..e04b553e2 --- /dev/null +++ b/cms-server/src/main/java/com/condation/cms/cli/tools/CLIServerUtils.java @@ -0,0 +1,105 @@ +package com.condation.cms.cli.tools; + +/*- + * #%L + * cms-server + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.Startup; +import com.condation.cms.api.Constants; +import com.condation.cms.api.utils.ServerUtil; +import java.io.IOException; +import java.nio.file.Files; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import java.util.Properties; +import org.semver4j.Semver; + +/** + * + * @author thorstenmarx + */ +public class CLIServerUtils { + + private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneOffset.UTC); + + public static Optional getCMSProcess() throws Exception { + var pidFile = ServerUtil.getPath(Constants.PID_FILE); + if (!Files.exists(pidFile)) { + return Optional.empty(); + } + var pid = Files.readString(pidFile); + return ProcessHandle.of(Long.parseLong(pid.trim())); + } + + private static Optional getStartTime() { + try { + var process = getCMSProcess(); + if (process.isPresent()) { + return process.get().info().startInstant(); + } + } catch (Exception ex) { + System.getLogger(CLIServerUtils.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex); + } + return Optional.empty(); + } + + public static String getStartedAt() { + var startTime = getStartTime(); + if (startTime.isEmpty()) { + return ""; + } + var started = startTime.get(); + return formatter.format(started); + } + + public static String getUptime() { + var startTime = getStartTime(); + if (startTime.isEmpty()) { + return ""; + } + + Instant now = Instant.now(); + Duration uptime = Duration.between(startTime.get(), now); + + long hours = uptime.toHours(); + long minutes = uptime.toMinutes() % 60; // Rest-Minuten nach Stunden + + return String.format("%dh %dm", hours, minutes); + } + + public static void writePidFile() throws IOException { + Files.deleteIfExists(ServerUtil.getPath(Constants.PID_FILE)); + Files.writeString(ServerUtil.getPath(Constants.PID_FILE), String.valueOf(ProcessHandle.current().pid())); + } + + public static Semver getVersion() { + try (var in = Startup.class.getResourceAsStream("application.properties")) { + Properties props = new Properties(); + props.load(in); + + return Semver.coerce(props.getProperty("version")); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } +} diff --git a/cms-server/src/main/java/com/condation/cms/cli/tools/ModulesUtil.java b/cms-server/src/main/java/com/condation/cms/cli/tools/ModulesUtil.java index cddccab29..0f15567b5 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/tools/ModulesUtil.java +++ b/cms-server/src/main/java/com/condation/cms/cli/tools/ModulesUtil.java @@ -24,11 +24,10 @@ * . * #L% */ - - import com.condation.cms.api.utils.SiteUtil; import com.condation.cms.cli.commands.modules.AbstractModuleCommand; import com.condation.cms.core.configuration.ConfigurationFactory; +import com.condation.cms.core.configuration.properties.ExtendedServerProperties; import com.condation.cms.core.configuration.properties.ExtendedSiteProperties; import com.condation.cms.core.configuration.properties.ExtendedThemeProperties; import java.io.IOException; @@ -58,22 +57,28 @@ public static Set getRequiredModules() { Set requiredModules = new HashSet<>(); try { + var serverConfig = ConfigurationFactory.serverConfiguration(); + ExtendedServerProperties serverProperties = new ExtendedServerProperties(serverConfig); + requiredModules.addAll(serverProperties.activeModules()); + var hosts = ServerUtil.getPath(Constants.Folders.HOSTS); var themes = ServerUtil.getPath(Constants.Folders.THEMES); if (Files.exists(hosts)) { - Files.list(hosts) - .filter(ModulesUtil::isHost) - .forEach(site -> { - try { - var hostProperties = new ExtendedSiteProperties(ConfigurationFactory.siteConfiguration("bla", site)); - requiredModules.addAll(hostProperties.activeModules()); - } catch (IOException ex) { - log.error("", ex); - } - }); + try (var hostStream = Files.list(hosts)) { + hostStream.filter(ModulesUtil::isHost) + .forEach(site -> { + try { + var hostProperties = new ExtendedSiteProperties(ConfigurationFactory.siteConfiguration("bla", site)); + requiredModules.addAll(hostProperties.activeModules()); + } catch (IOException ex) { + log.error("", ex); + } + }); + } } if (Files.exists(themes)) { - Files.list(themes) + try (var themesStream = Files.list(themes)) { + themesStream .filter(ModulesUtil::isTheme) .forEach(themeConfig -> { try { @@ -83,6 +88,7 @@ public static Set getRequiredModules() { log.error("", ex); } }); + } } } catch (IOException ex) { log.error("", ex); diff --git a/cms-server/src/main/java/com/condation/cms/cli/tools/ThemesUtil.java b/cms-server/src/main/java/com/condation/cms/cli/tools/ThemesUtil.java index 25c261b1f..55c6d453e 100644 --- a/cms-server/src/main/java/com/condation/cms/cli/tools/ThemesUtil.java +++ b/cms-server/src/main/java/com/condation/cms/cli/tools/ThemesUtil.java @@ -24,9 +24,6 @@ * . * #L% */ - - - import com.condation.cms.api.utils.SiteUtil; import com.condation.cms.cli.commands.themes.AbstractThemeCommand; import com.condation.cms.core.configuration.ConfigurationFactory; @@ -49,29 +46,29 @@ @Slf4j public class ThemesUtil { - public static boolean allInstalled (Set themes) { + public static boolean allInstalled(Set themes) { return themes.stream().allMatch(AbstractThemeCommand::isInstalled); } - - public static Set filterUnInstalled (Set themes) { + + public static Set filterUnInstalled(Set themes) { return themes.stream().filter(theme -> !AbstractThemeCommand.isInstalled(theme)).collect(Collectors.toSet()); } - - public static Set getRequiredThemes () { + + public static Set getRequiredThemes() { if (!Files.exists(ServerUtil.getPath(Constants.Folders.THEMES))) { return Collections.emptySet(); } var themes = getRequiredSiteThemes(); - + themes.addAll(getRequiredParentThemes()); - + return themes; } - - public static Set getRequiredSiteThemes () { + + public static Set getRequiredSiteThemes() { Set requiredThemes = new HashSet<>(); - try { - Files.list(ServerUtil.getPath(Constants.Folders.THEMES)) + try (var themeStream = Files.list(ServerUtil.getPath(Constants.Folders.THEMES))) { + themeStream .filter(ThemesUtil::isHost) .forEach(site -> { try { @@ -88,14 +85,14 @@ public static Set getRequiredSiteThemes () { } catch (IOException ex) { log.error("", ex); } - + return requiredThemes; } - - private static Set getRequiredParentThemes () { + + private static Set getRequiredParentThemes() { Set requiredThemes = new HashSet<>(); - try { - Files.list(ServerUtil.getPath(Constants.Folders.THEMES)) + try (var themesStream = Files.list(ServerUtil.getPath(Constants.Folders.THEMES))) { + themesStream .filter(ThemesUtil::isTheme) .forEach(themeConfig -> { try { @@ -110,14 +107,14 @@ private static Set getRequiredParentThemes () { } catch (IOException ex) { log.error("", ex); } - + return requiredThemes; } - + public static boolean isTheme(Path host) { return Files.exists(host.resolve("theme.yaml")); } - + public static boolean isHost(Path host) { return SiteUtil.isSite(host); } diff --git a/cms-server/src/main/java/com/condation/cms/request/RequestContextFactory.java b/cms-server/src/main/java/com/condation/cms/request/RequestContextFactory.java index 151485564..6fbc72312 100644 --- a/cms-server/src/main/java/com/condation/cms/request/RequestContextFactory.java +++ b/cms-server/src/main/java/com/condation/cms/request/RequestContextFactory.java @@ -28,15 +28,15 @@ import com.condation.cms.api.configuration.configs.SiteConfiguration; import com.condation.cms.api.content.ContentParser; import com.condation.cms.api.extensions.HookSystemRegisterExtensionPoint; -import com.condation.cms.api.extensions.RegisterShortCodesExtensionPoint; +import com.condation.cms.api.extensions.RegisterTagsExtensionPoint; import com.condation.cms.api.feature.features.ConfigurationFeature; import com.condation.cms.api.feature.features.ContentNodeMapperFeature; import com.condation.cms.api.feature.features.ContentParserFeature; import com.condation.cms.api.feature.features.HookSystemFeature; import com.condation.cms.api.feature.features.InjectorFeature; import com.condation.cms.api.feature.features.IsDevModeFeature; -import com.condation.cms.api.feature.features.IsPreviewFeature; import com.condation.cms.api.feature.features.MarkdownRendererFeature; +import com.condation.cms.api.feature.features.ModuleManagerFeature; import com.condation.cms.api.feature.features.RequestFeature; import com.condation.cms.api.feature.features.ServerPropertiesFeature; import com.condation.cms.api.feature.features.SiteMediaServiceFeature; @@ -54,8 +54,8 @@ import com.condation.cms.api.utils.HTTPUtil; import com.condation.cms.api.utils.RequestUtil; import com.condation.cms.content.RenderContext; -import com.condation.cms.content.shortcodes.ShortCodes; -import com.condation.cms.content.shortcodes.TagParser; +import com.condation.cms.content.tags.Tags; +import com.condation.cms.content.tags.TagParser; import com.condation.cms.extensions.ExtensionManager; import com.condation.cms.extensions.hooks.ContentHooks; import com.condation.cms.extensions.hooks.DBHooks; @@ -127,11 +127,6 @@ public void initContext (RequestContext requestContext, Request request) throws var theme = requestContext.get(ThemeFeature.class).theme(); requestContext.add(RequestFeature.class, new RequestFeature(request.getContext().getContextPath(), uri, queryParameters, request)); - if (ServerContext.IS_DEV) { - if (queryParameters.containsKey("preview")) { - requestContext.add(IsPreviewFeature.class, new IsPreviewFeature()); - } - } var markdownRenderer = injector.getInstance(MarkdownRenderer.class); var extensionManager = injector.getInstance(ExtensionManager.class); @@ -142,19 +137,13 @@ public void initContext (RequestContext requestContext, Request request) throws RenderContext renderContext = new RenderContext( markdownRenderer, - initShortCodes(requestContext), + initContentTags(requestContext), theme); requestContext.add(RenderContext.class, renderContext); requestContext.add(RequestExtensions.class, requestExtensions); } - private void initHookSystem(RequestContext requestContext) { - var hookSystem = requestContext.get(HookSystemFeature.class).hookSystem(); - var moduleManager = injector.getInstance(ModuleManager.class); - moduleManager.extensions(HookSystemRegisterExtensionPoint.class).forEach(extensionPoint -> extensionPoint.register(hookSystem)); - } - /** * Has to run as one of the last steps, because we need the requestContext * to be filled @@ -162,30 +151,22 @@ private void initHookSystem(RequestContext requestContext) { * @param requestContext * @return */ - private HookSystem setupAndGetHookSystem() { - var hookSystem = injector.getInstance(HookSystem.class); - var moduleManager = injector.getInstance(ModuleManager.class); - moduleManager.extensions(HookSystemRegisterExtensionPoint.class).forEach(extensionPoint -> extensionPoint.register(hookSystem)); - - return hookSystem; - } - - private ShortCodes initShortCodes(RequestContext requestContext) { + private Tags initContentTags(RequestContext requestContext) { var parser = injector.getInstance(TagParser.class); - var builder = ShortCodes.builder(parser); + var builder = Tags.builder(parser); - injector.getInstance(ModuleManager.class).extensions(RegisterShortCodesExtensionPoint.class) + injector.getInstance(ModuleManager.class).extensions(RegisterTagsExtensionPoint.class) .forEach(extension -> { - builder.register(extension.shortCodes()); + builder.register(extension.tags()); - builder.register(extension.shortCodeDefinitions()); + builder.register(extension.tagDefinitions()); }); var codes = new HashMap>(); - var wrapper = requestContext.get(ContentHooks.class).getShortCodes(codes); + var wrapper = requestContext.get(ContentHooks.class).getTags(codes); - builder.register(wrapper.getShortCodes()); + builder.register(wrapper.getTags()); return builder.build(); } @@ -220,13 +201,13 @@ public RequestContext create() throws IOException { requestContext.add(DBHooks.class, new DBHooks(requestContext)); requestContext.add(ContentHooks.class, new ContentHooks(requestContext)); - requestContext.add(HookSystemFeature.class, new HookSystemFeature(setupAndGetHookSystem())); + requestContext.add(HookSystemFeature.class, new HookSystemFeature(injector.getInstance(HookSystem.class))); RequestExtensions requestExtensions = extensionManager.newContext(theme, requestContext); RenderContext renderContext = new RenderContext( markdownRenderer, - initShortCodes(requestContext), + initContentTags(requestContext), theme); requestContext.add(RenderContext.class, renderContext); requestContext.add(MarkdownRendererFeature.class, new MarkdownRendererFeature(markdownRenderer)); @@ -252,12 +233,22 @@ public RequestContext create( var requestContext = create(); requestContext.add(RequestFeature.class, new RequestFeature(contextPath, uri, queryParameters, request.orElse(null))); - if (ServerContext.IS_DEV) { - if (queryParameters.containsKey("preview")) { - requestContext.add(IsPreviewFeature.class, new IsPreviewFeature()); - } - } return requestContext; } + + /** + * HookSystem must be registered here instead of the guice module because otherwise the RequestContext may not be present + * @param requestContext + */ + private void initHookSystem(RequestContext requestContext) { + var hookSystem = requestContext.get(HookSystemFeature.class).hookSystem(); + var moduleManager = injector.getInstance(ModuleManager.class); + + moduleManager.extensions(HookSystemRegisterExtensionPoint.class).forEach(extensionPoint -> { + extensionPoint.register(hookSystem); + hookSystem.register(extensionPoint); + }); + + } } diff --git a/cms-server/src/main/java/com/condation/cms/server/FileFolderPathResource.java b/cms-server/src/main/java/com/condation/cms/server/FileFolderPathResource.java index e2bd7ff34..8dfc5f48c 100644 --- a/cms-server/src/main/java/com/condation/cms/server/FileFolderPathResource.java +++ b/cms-server/src/main/java/com/condation/cms/server/FileFolderPathResource.java @@ -108,9 +108,9 @@ public Resource resolve(String subUriPath) { URI uri = getURI(); URI resolvedUri = URIUtil.addPath(uri, subUriPath); - Path path = Paths.get(resolvedUri); - if (Files.exists(path) && !(path.getFileName().toString().endsWith(".meta.yaml"))) { - return newResource(path); + Path resolvedPath = Paths.get(resolvedUri); + if (Files.exists(resolvedPath) && !(resolvedPath.getFileName().toString().endsWith(".meta.yaml"))) { + return newResource(resolvedPath); } return null; diff --git a/cms-server/src/main/java/com/condation/cms/server/JettyServer.java b/cms-server/src/main/java/com/condation/cms/server/JettyServer.java index f27632da7..329468b00 100644 --- a/cms-server/src/main/java/com/condation/cms/server/JettyServer.java +++ b/cms-server/src/main/java/com/condation/cms/server/JettyServer.java @@ -21,31 +21,35 @@ * . * #L% */ - - +import com.condation.cms.server.host.VHost; import com.condation.cms.api.Constants; import com.condation.cms.api.ServerProperties; -import com.condation.cms.api.configuration.Configuration; -import com.condation.cms.api.configuration.configs.ServerConfiguration; import com.condation.cms.api.eventbus.Event; import com.condation.cms.api.eventbus.EventBus; -import com.condation.cms.api.eventbus.events.RepoCheckoutEvent; import com.condation.cms.api.eventbus.events.lifecycle.HostReadyEvent; import com.condation.cms.api.eventbus.events.lifecycle.ReloadHostEvent; import com.condation.cms.api.eventbus.events.lifecycle.ServerReadyEvent; import com.condation.cms.api.eventbus.events.lifecycle.ServerShutdownInitiated; +import com.condation.cms.api.extensions.server.ServerHookSystemRegisterExtensionPoint; +import com.condation.cms.api.extensions.server.ServerLifecycleExtensionPoint; +import com.condation.cms.api.hooks.HookSystem; +import com.condation.cms.api.module.ServerModuleContext; +import com.condation.cms.api.site.Site; +import com.condation.cms.api.site.SiteService; import com.condation.cms.api.utils.ServerUtil; import com.condation.cms.api.utils.SiteUtil; import com.condation.cms.core.eventbus.DefaultEventBus; -import com.condation.cms.git.RepositoryManager; +import com.condation.modules.api.ModuleManager; import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.name.Names; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.compression.server.CompressionConfig; +import org.eclipse.jetty.compression.server.CompressionHandler; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.server.CustomRequestLog; import org.eclipse.jetty.server.HttpConfiguration; @@ -80,7 +84,7 @@ public class JettyServer implements AutoCloseable { public void fireServerEvent(Event event) { serverEventBus.publish(event); } - + public void reloadVHost(String vhost) { log.debug("try reloading " + vhost); vhosts.stream() @@ -93,20 +97,20 @@ public void reloadVHost(String vhost) { } }); } - + public void startup() throws IOException { var properties = globalInjector.getInstance(ServerProperties.class); - Files.list(ServerUtil.getPath(Constants.Folders.HOSTS)).forEach((hostPath) -> { - if (SiteUtil.isSite(hostPath)) { - try { - var host = new VHost(hostPath); - host.init(ServerUtil.getPath(Constants.Folders.MODULES), globalInjector); - vhosts.add(host); - } catch (IOException ex) { - log.error(null, ex); - } + SiteUtil.sitesStream().forEach((site) -> { + try { + var host = new VHost(site.basePath()); + host.init(ServerUtil.getPath(Constants.Folders.MODULES), globalInjector); + vhosts.add(host); + + globalInjector.getInstance(SiteService.class).add(new Site(host.getInjector())); + } catch (IOException ex) { + log.error(null, ex); } }); @@ -123,19 +127,19 @@ public void startup() throws IOException { serverEventBus.register(ReloadHostEvent.class, (event) -> { reloadVHost(event.host()); }); - serverEventBus.register(RepoCheckoutEvent.class, (event) -> { - globalInjector.getInstance(RepositoryManager.class).updateRepo(event.repo()); - }); Runtime.getRuntime().addShutdownHook(new Thread(() -> { log.debug("shutting down"); + var moduleManager = globalInjector.getInstance(Key.get(ModuleManager.class, Names.named("server"))); + moduleManager.extensions(ServerLifecycleExtensionPoint.class).forEach(ServerLifecycleExtensionPoint::stopped); + vhosts.forEach(host -> { log.debug("shutting down vhost : " + host.hostnames()); host.shutdown(); }); // scheduledExecutorService.shutdownNow(); - + try { globalInjector.getInstance(Scheduler.class).shutdown(); } catch (SchedulerException ex) { @@ -164,12 +168,21 @@ public void startup() throws IOException { server.addConnector(connector); + CompressionHandler compressionHandler = new CompressionHandler(handlerCollection); + CompressionConfig compressionConfig = CompressionConfig.builder() + .compressIncludeMimeType("text/plain") + .compressIncludeMimeType("text/html") + .compressIncludeMimeType("text/css") + .compressIncludeMimeType("application/javascript") + .build(); + compressionHandler.putConfiguration("/*", compressionConfig); + var apm = properties.apm(); if (apm.enabled()) { log.info("enable application performance management"); ThreadLimitHandler threadLimitHandler = new ThreadLimitHandler(HttpHeader.X_FORWARDED_FOR.asString()); threadLimitHandler.setThreadLimit(apm.thread_limit()); - threadLimitHandler.setHandler(handlerCollection); + threadLimitHandler.setHandler(compressionHandler); QoSHandler qosHandler = new QoSHandler(threadLimitHandler); qosHandler.setMaxRequestCount(apm.max_requests()); @@ -177,7 +190,7 @@ public void startup() throws IOException { server.setHandler(qosHandler); } else { - server.setHandler(handlerCollection); + server.setHandler(compressionHandler); } try { @@ -187,6 +200,8 @@ public void startup() throws IOException { host.getInjector().getInstance(EventBus.class).publish(new HostReadyEvent(host.id())); host.getInjector().getInstance(EventBus.class).publish(new ServerReadyEvent()); }); + + initServerModules(); } catch (Exception ex) { log.error(null, ex); @@ -194,6 +209,31 @@ public void startup() throws IOException { System.out.println("cms startup successfully"); } + private void initServerModules() { + var moduleManager = globalInjector.getInstance(Key.get(ModuleManager.class, Names.named("server"))); + moduleManager.initModules(); + List activeModules = globalInjector.getInstance(ServerProperties.class).activeModules(); + activeModules.stream() + .filter(module_id -> moduleManager.getModuleIds().contains(module_id)) + .forEach(module_id -> { + try { + log.debug("activate module {}", module_id); + moduleManager.activateModule(module_id); + } catch (IOException ex) { + log.error(null, ex); + } + }); + var context = globalInjector.getInstance(ServerModuleContext.class); + + var hookSystem = globalInjector.getInstance(Key.get(HookSystem.class, Names.named("server"))); + moduleManager.extensions(ServerHookSystemRegisterExtensionPoint.class).forEach(extensionPoint -> { + extensionPoint.register(hookSystem); + hookSystem.register(extensionPoint); + }); + + moduleManager.extensions(ServerLifecycleExtensionPoint.class).forEach(ServerLifecycleExtensionPoint::started); + } + @Override public void close() throws Exception { server.stop(); diff --git a/cms-server/src/main/java/com/condation/cms/server/configs/ServerGlobalModule.java b/cms-server/src/main/java/com/condation/cms/server/configs/ServerGlobalModule.java index 269282501..640629437 100644 --- a/cms-server/src/main/java/com/condation/cms/server/configs/ServerGlobalModule.java +++ b/cms-server/src/main/java/com/condation/cms/server/configs/ServerGlobalModule.java @@ -21,18 +21,36 @@ * . * #L% */ - - import com.condation.cms.api.ServerProperties; +import com.condation.cms.api.eventbus.EventBus; +import com.condation.cms.api.feature.features.InjectorFeature; +import com.condation.cms.api.feature.features.ModuleManagerFeature; +import com.condation.cms.api.feature.features.ServerHookSystemFeature; +import com.condation.cms.api.hooks.HookSystem; +import com.condation.cms.api.messaging.Messaging; +import com.condation.cms.api.module.ServerModuleContext; +import com.condation.cms.api.scheduler.CronJobScheduler; +import com.condation.cms.api.site.SiteService; import com.condation.cms.api.utils.ServerUtil; +import com.condation.cms.auth.services.UserService; import com.condation.cms.core.configuration.ConfigurationFactory; import com.condation.cms.core.configuration.properties.ExtendedServerProperties; -import com.condation.cms.git.RepositoryManager; +import com.condation.cms.core.eventbus.DefaultEventBus; +import com.condation.cms.core.eventbus.MessagingEventBus; +import com.condation.cms.core.messaging.DefaultMessaging; +import com.condation.cms.core.scheduler.ServerCronJobScheduler; +import com.condation.cms.core.site.DefaultSiteService; +import com.condation.modules.api.ModuleManager; +import com.condation.modules.manager.ModuleAPIClassLoader; +import com.condation.modules.manager.ModuleManagerImpl; import com.google.inject.Binder; +import com.google.inject.Injector; import com.google.inject.Provides; import com.google.inject.Singleton; +import com.google.inject.name.Named; import java.io.IOException; import java.nio.file.Path; +import java.util.List; import lombok.extern.slf4j.Slf4j; import org.graalvm.polyglot.Engine; import org.quartz.Scheduler; @@ -60,9 +78,9 @@ public Scheduler scheduler() { DirectSchedulerFactory schedulerFactory = DirectSchedulerFactory.getInstance(); schedulerFactory.createScheduler( - "cms-scheduler", - "cms-scheduler", - new SimpleThreadPool(5, Thread.NORM_PRIORITY), + "cms-scheduler", + "cms-scheduler", + new SimpleThreadPool(5, Thread.NORM_PRIORITY), new RAMJobStore()); var scheduler = schedulerFactory.getScheduler("cms-scheduler"); scheduler.start(); @@ -74,27 +92,98 @@ public Scheduler scheduler() { } } + @Provides + @Singleton + @Named("server") + public Messaging serverMessaging () { + return new DefaultMessaging(); + } + + @Provides + @Singleton + @Named("server") + public EventBus serverEventBus (@Named("server") Messaging messaging) { + return new MessagingEventBus(messaging); + } + + @Provides + @Singleton + @Named("server") + public CronJobScheduler serverCronJobScheudler (Scheduler scheduler) { + return new ServerCronJobScheduler(scheduler); + } + @Provides public ServerProperties serverProperties() throws IOException { return new ExtendedServerProperties(ConfigurationFactory.serverConfiguration()); } - + @Provides public Engine engine() throws IOException { return Engine.newBuilder("js") - .option("engine.WarnInterpreterOnly", "false") - .build(); + .option("engine.WarnInterpreterOnly", "false") + .build(); + } + + @Provides + @Singleton + public UserService userService() { + return new UserService(ServerUtil.getHome()); + } + + @Provides + @Singleton + public SiteService siteService() { + return new DefaultSiteService(); + } + + @Provides + @Singleton + @Named("server") + public HookSystem hookSystem () { + return new HookSystem(); + } + + @Provides + @Singleton + public ServerModuleContext serverModuleContext (Injector injector, @Named("server") HookSystem hookSystem) { + var context = new ServerModuleContext(); + + context.add(InjectorFeature.class, new InjectorFeature(injector)); + context.add(ServerHookSystemFeature.class, new ServerHookSystemFeature(hookSystem)); + + return context; } @Provides @Singleton - public RepositoryManager repositoryManager (Scheduler scheduler) throws Exception { - Path gitConfig = ServerUtil.getPath("git.yaml"); + @Named("server") + public ModuleManager serverModuleManager(Injector injector, ServerModuleContext context) { + var classLoader = new ModuleAPIClassLoader(ClassLoader.getSystemClassLoader(), + List.of( + "org.slf4j", + "com.condation.cms", + "com.condation.modules", + "org.apache.logging", + "org.graalvm.polyglot", + "org.graalvm.js", + "org.eclipse.jetty", + "jakarta.servlet", + "com.google", + "org.w3c" + )); - log.info("repository configuration found"); - final RepositoryManager repositoryManager = new RepositoryManager(scheduler); - repositoryManager.init(gitConfig); + var homePath = ServerUtil.getHome(); + var moduleManager = ModuleManagerImpl.builder() + .setClassLoader(classLoader) + .setInjector((instance) -> injector.injectMembers(instance)) + .setModulesDataPath(homePath.resolve("modules_data").toFile()) + .setModulesPath(homePath.resolve("modules").toFile()) + .setContext(context) + .build(); + + context.add(ModuleManagerFeature.class, new ModuleManagerFeature(moduleManager)); - return repositoryManager; + return moduleManager; } } diff --git a/cms-server/src/main/java/com/condation/cms/server/configs/SiteConfigInitializer.java b/cms-server/src/main/java/com/condation/cms/server/configs/SiteConfigInitializer.java index f78349904..e379db23e 100644 --- a/cms-server/src/main/java/com/condation/cms/server/configs/SiteConfigInitializer.java +++ b/cms-server/src/main/java/com/condation/cms/server/configs/SiteConfigInitializer.java @@ -33,20 +33,19 @@ import com.condation.cms.api.feature.features.CronJobSchedulerFeature; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.feature.features.EventBusFeature; +import com.condation.cms.api.feature.features.InjectorFeature; import com.condation.cms.api.feature.features.MessagingFeature; import com.condation.cms.api.feature.features.ServerPropertiesFeature; import com.condation.cms.api.feature.features.SitePropertiesFeature; import com.condation.cms.api.feature.features.ThemeFeature; import com.condation.cms.api.messaging.Messaging; -import com.condation.cms.api.module.CMSModuleContext; +import com.condation.cms.api.module.SiteModuleContext; import com.condation.cms.api.theme.Theme; import com.condation.cms.content.ContentResolver; -import com.condation.cms.core.configuration.ConfigManagement; import com.condation.cms.core.scheduler.SiteCronJobScheduler; import com.condation.cms.filesystem.FileDB; import com.condation.cms.module.DefaultRenderContentFunction; import com.condation.cms.request.RequestContextFactory; -import com.condation.modules.api.ModuleManager; import com.google.inject.Injector; import lombok.RequiredArgsConstructor; @@ -66,7 +65,7 @@ public void init () { } private void initCronJobContext() { - var context = injector.getInstance(CMSModuleContext.class); + var context = injector.getInstance(SiteModuleContext.class); context.add(SitePropertiesFeature.class, new SitePropertiesFeature(injector.getInstance(SiteProperties.class))); context.add(ServerPropertiesFeature.class, new ServerPropertiesFeature(injector.getInstance(ServerProperties.class))); @@ -78,7 +77,7 @@ private void initCronJobContext() { } private void initModuleContext () { - var cmsModuleContext = injector.getInstance(CMSModuleContext.class); + var cmsModuleContext = injector.getInstance(SiteModuleContext.class); cmsModuleContext.add(SitePropertiesFeature.class, new SitePropertiesFeature(injector.getInstance(SiteProperties.class))); cmsModuleContext.add(ServerPropertiesFeature.class, new ServerPropertiesFeature(injector.getInstance(ServerProperties.class))); @@ -89,6 +88,7 @@ private void initModuleContext () { cmsModuleContext.add(ConfigurationFeature.class, new ConfigurationFeature(injector.getInstance(Configuration.class))); cmsModuleContext.add(CronJobSchedulerFeature.class, new CronJobSchedulerFeature(injector.getInstance(SiteCronJobScheduler.class))); cmsModuleContext.add(CacheManagerFeature.class, new CacheManagerFeature(injector.getInstance(CacheManager.class))); + cmsModuleContext.add(InjectorFeature.class, new InjectorFeature(injector)); var contentResolver = injector.getInstance(ContentResolver.class); var requestContextFactory = injector.getInstance(RequestContextFactory.class); diff --git a/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java b/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java index e89a99006..a1ae72674 100644 --- a/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java +++ b/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java @@ -42,6 +42,7 @@ import com.condation.cms.media.SiteMediaManager; import com.condation.cms.server.FileFolderPathResource; import com.condation.cms.server.filter.InitRequestContextFilter; +import com.condation.cms.server.filter.PreviewFilter; import com.condation.cms.server.handler.auth.JettyAuthenticationHandler; import com.condation.cms.server.handler.content.JettyContentHandler; import com.condation.cms.server.handler.content.JettyTaxonomyHandler; @@ -51,7 +52,6 @@ import com.condation.cms.server.handler.http.RoutesHandler; import com.condation.cms.server.handler.media.JettyMediaHandler; import com.condation.cms.server.handler.module.JettyModuleHandler; -import com.condation.cms.server.handler.module.JettyRouteHandler; import com.condation.modules.api.ModuleManager; import com.google.inject.AbstractModule; import com.google.inject.Provides; @@ -73,13 +73,14 @@ protected void configure() { bind(JettyViewHandler.class).in(Singleton.class); bind(JettyContentHandler.class).in(Singleton.class); bind(JettyTaxonomyHandler.class).in(Singleton.class); - bind(JettyRouteHandler.class).in(Singleton.class); bind(RoutesHandler.class).in(Singleton.class); bind(JettyHttpHandlerExtensionHandler.class).in(Singleton.class); bind(InitRequestContextFilter.class).in(Singleton.class); bind(APIHandler.class).in(Singleton.class); + bind(PreviewFilter.class).in(Singleton.class); + //bind(JettyAuthenticationHandler.class).in(Singleton.class); } diff --git a/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java b/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java index 0482b0a81..142203f4f 100644 --- a/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java +++ b/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java @@ -3,8 +3,6 @@ import java.io.IOException; import java.nio.file.Path; import java.time.Duration; -import java.util.Map; -import java.util.ResourceBundle; import org.apache.commons.jexl3.JexlBuilder; import org.graalvm.polyglot.Engine; @@ -40,18 +38,12 @@ import com.condation.cms.api.configuration.Configuration; import com.condation.cms.api.configuration.configs.ServerConfiguration; import com.condation.cms.api.content.ContentParser; +import com.condation.cms.api.content.RenderContentFunction; import com.condation.cms.api.db.DB; import com.condation.cms.api.db.cms.NIOReadOnlyFile; import com.condation.cms.api.db.cms.ReadOnlyFile; import com.condation.cms.api.eventbus.EventBus; import com.condation.cms.api.eventbus.events.ConfigurationReloadEvent; -import com.condation.cms.api.feature.features.ConfigurationFeature; -import com.condation.cms.api.feature.features.DBFeature; -import com.condation.cms.api.feature.features.EventBusFeature; -import com.condation.cms.api.feature.features.MessagingFeature; -import com.condation.cms.api.feature.features.ServerPropertiesFeature; -import com.condation.cms.api.feature.features.SitePropertiesFeature; -import com.condation.cms.api.feature.features.ThemeFeature; import com.condation.cms.api.mapper.ContentNodeMapper; import com.condation.cms.api.media.MediaService; import com.condation.cms.api.messages.MessageSource; @@ -67,7 +59,7 @@ import com.condation.cms.content.DefaultContentRenderer; import com.condation.cms.content.TaxonomyResolver; import com.condation.cms.content.ViewResolver; -import com.condation.cms.content.shortcodes.TagParser; +import com.condation.cms.content.tags.TagParser; import com.condation.cms.content.template.functions.taxonomy.TaxonomyFunction; import com.condation.cms.core.configuration.ConfigManagement; import com.condation.cms.core.configuration.ConfigurationFactory; @@ -82,6 +74,7 @@ import com.condation.cms.filesystem.MetaData; import com.condation.cms.media.FileMediaService; import com.condation.cms.media.SiteMediaManager; +import com.condation.cms.module.DefaultRenderContentFunction; import com.condation.cms.request.RequestContextFactory; import com.condation.modules.api.ModuleManager; import com.google.inject.AbstractModule; @@ -181,13 +174,7 @@ public Theme loadTheme( return DefaultTheme.load(themeFolder, siteProperties, messageSource, serverProperties, cacheManager); } - return DefaultTheme.EMPTY; - } - - @Provides - @Singleton - public UserService userService(DB db) { - return new UserService(db.getFileSystem().hostBase()); + return DefaultTheme.NO_THEME; } @Provides @@ -243,10 +230,10 @@ public DB fileDb(SiteProperties site, DefaultContentParser contentParser, Config throw new RuntimeException(ioe); } }, configuration); - if ("PERSISTENT".equals(site.queryIndexMode())) { - db.init(MetaData.Type.PERSISTENT); + if ("MEMORY".equals(site.queryIndexMode())) { + db.init(MetaData.Type.MEMORY); } else { - db.init(); + db.init(MetaData.Type.PERSISTENT); } return db; } @@ -284,6 +271,12 @@ public RequestContextFactory requestContextFactory(Injector injector) { injector ); } + + @Provides + @Singleton + public RenderContentFunction renderContentFunction (ContentResolver contentResolver, RequestContextFactory requestContextFatory) { + return new DefaultRenderContentFunction(contentResolver, requestContextFatory); + } @Provides @Singleton diff --git a/cms-server/src/main/java/com/condation/cms/server/configs/ModulesModule.java b/cms-server/src/main/java/com/condation/cms/server/configs/SiteModulesModule.java similarity index 82% rename from cms-server/src/main/java/com/condation/cms/server/configs/ModulesModule.java rename to cms-server/src/main/java/com/condation/cms/server/configs/SiteModulesModule.java index 17951530b..34d42ff83 100644 --- a/cms-server/src/main/java/com/condation/cms/server/configs/ModulesModule.java +++ b/cms-server/src/main/java/com/condation/cms/server/configs/SiteModulesModule.java @@ -22,17 +22,17 @@ * #L% */ import com.condation.cms.api.SiteProperties; +import com.condation.cms.api.extensions.HookSystemRegisterExtensionPoint; import com.condation.cms.api.extensions.MarkdownRendererProviderExtensionPoint; import com.condation.cms.api.extensions.TemplateEngineProviderExtensionPoint; import com.condation.cms.api.feature.features.ModuleManagerFeature; import com.condation.cms.api.hooks.HookSystem; import com.condation.cms.api.markdown.MarkdownRenderer; -import com.condation.cms.api.module.CMSModuleContext; -import com.condation.cms.api.module.CMSRequestContext; -import com.condation.cms.api.request.ThreadLocalRequestContext; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.module.SiteRequestContext; +import com.condation.cms.api.request.RequestContextScope; import com.condation.cms.api.template.TemplateEngine; import com.condation.cms.api.theme.Theme; -import com.condation.cms.content.markdown.module.CMSMarkdownRenderer; import com.condation.cms.filesystem.FileDB; import com.condation.modules.api.ModuleManager; import com.condation.modules.api.ModuleRequestContextFactory; @@ -55,7 +55,7 @@ */ @RequiredArgsConstructor @Slf4j -public class ModulesModule extends AbstractModule { +public class SiteModulesModule extends AbstractModule { private final Path modulesPath; @@ -65,7 +65,7 @@ protected void configure() { @Provides @Singleton - public ModuleManager moduleManager(Injector injector, CMSModuleContext context, ModuleRequestContextFactory requestContextFactory) { + public ModuleManager moduleManager(Injector injector, SiteModuleContext context, ModuleRequestContextFactory requestContextFactory) { var classLoader = new ModuleAPIClassLoader(ClassLoader.getSystemClassLoader(), List.of( "org.slf4j", @@ -75,7 +75,9 @@ public ModuleManager moduleManager(Injector injector, CMSModuleContext context, "org.graalvm.polyglot", "org.graalvm.js", "org.eclipse.jetty", - "jakarta.servlet" + "jakarta.servlet", + "com.google", + "org.w3c" )); var moduleManager = ModuleManagerImpl.builder() .setClassLoader(classLoader) @@ -95,14 +97,19 @@ public ModuleManager moduleManager(Injector injector, CMSModuleContext context, @Singleton public ModuleRequestContextFactory requestContextFactory() { return () -> { - return new CMSRequestContext(ThreadLocalRequestContext.REQUEST_CONTEXT.get()); + if (RequestContextScope.REQUEST_CONTEXT.isBound()) { + return new SiteRequestContext(RequestContextScope.REQUEST_CONTEXT.get()); + } else { + return new SiteRequestContext(null); + } + }; } @Provides @Singleton - public CMSModuleContext moduleContext() { - final CMSModuleContext cmsModuleContext = new CMSModuleContext(); + public SiteModuleContext moduleContext() { + final SiteModuleContext cmsModuleContext = new SiteModuleContext(); return cmsModuleContext; } @@ -112,7 +119,6 @@ public CMSModuleContext moduleContext() { * * @param siteProperties * @param moduleManager - * @param defaultMarkdownRenderer * @return */ @Provides @@ -164,11 +170,20 @@ public TemplateEngine resolveTemplateEngine(SiteProperties siteProperties, Theme /** * new HookSystem for each request * + * @param moduleManager * @return */ @Provides - public HookSystem hookSystem() { + public HookSystem hookSystem(final ModuleManager moduleManager) { var hookSystem = new HookSystem(); + + /* + moduleManager.extensions(HookSystemRegisterExtensionPoint.class).forEach(extensionPoint -> { + extensionPoint.register(hookSystem); + hookSystem.register(extensionPoint); + }); + */ + return hookSystem; } } diff --git a/cms-server/src/main/java/com/condation/cms/server/filter/CreateRequestContextFilter.java b/cms-server/src/main/java/com/condation/cms/server/filter/CreateRequestContextFilter.java index ce6f0bf53..3bd2421b3 100644 --- a/cms-server/src/main/java/com/condation/cms/server/filter/CreateRequestContextFilter.java +++ b/cms-server/src/main/java/com/condation/cms/server/filter/CreateRequestContextFilter.java @@ -23,7 +23,8 @@ */ -import com.condation.cms.api.request.ThreadLocalRequestContext; +import com.condation.cms.api.Constants; +import com.condation.cms.api.request.RequestContextScope; import com.condation.cms.request.RequestContextFactory; import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.server.Handler; @@ -40,8 +41,6 @@ public class CreateRequestContextFilter extends Handler.Wrapper { private final RequestContextFactory requestContextFactory; - public static final String REQUEST_CONTEXT = "_requestContext"; - public CreateRequestContextFilter(final Handler handler, final RequestContextFactory requestContextFactory) { super(handler); this.requestContextFactory = requestContextFactory; @@ -50,12 +49,16 @@ public CreateRequestContextFilter(final Handler handler, final RequestContextFac @Override public boolean handle(final Request httpRequest, final Response rspns, final Callback clbck) throws Exception { try (var requestContext = requestContextFactory.createContext()) { - ThreadLocalRequestContext.REQUEST_CONTEXT.set(requestContext); - httpRequest.setAttribute(REQUEST_CONTEXT, requestContext); + httpRequest.setAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME, requestContext); - return super.handle(httpRequest, rspns, clbck); - } finally { - ThreadLocalRequestContext.REQUEST_CONTEXT.remove(); + return ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, requestContext).call(() -> { + return super.handle(httpRequest, rspns, clbck); + }); + + } catch (Exception e) { + log.error("", e); + Response.writeError(httpRequest, rspns, clbck, e); + return true; } } diff --git a/cms-server/src/main/java/com/condation/cms/server/filter/InitRequestContextFilter.java b/cms-server/src/main/java/com/condation/cms/server/filter/InitRequestContextFilter.java index bc2750111..502319bc5 100644 --- a/cms-server/src/main/java/com/condation/cms/server/filter/InitRequestContextFilter.java +++ b/cms-server/src/main/java/com/condation/cms/server/filter/InitRequestContextFilter.java @@ -21,6 +21,7 @@ * . * #L% */ +import com.condation.cms.api.Constants; import com.condation.cms.api.feature.Feature; import com.condation.cms.api.request.RequestContext; import com.condation.cms.request.RequestContextFactory; @@ -40,8 +41,6 @@ public class InitRequestContextFilter extends Handler.Abstract { private final RequestContextFactory requestContextFactory; - public static final String REQUEST_CONTEXT = "_requestContext"; - @Inject public InitRequestContextFilter(final RequestContextFactory requestContextFactory) { super(); @@ -50,7 +49,7 @@ public InitRequestContextFilter(final RequestContextFactory requestContextFactor @Override public boolean handle(final Request httpRequest, final Response rspns, final Callback clbck) throws Exception { - var requestContext = (RequestContext) httpRequest.getAttribute(REQUEST_CONTEXT); + var requestContext = (RequestContext) httpRequest.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); if (requestContext.has(AlreadyInitialized.class)) { return false; diff --git a/cms-server/src/main/java/com/condation/cms/server/filter/PooledRequestContextFilter.java b/cms-server/src/main/java/com/condation/cms/server/filter/PooledRequestContextFilter.java index d178fa032..8d6ddd939 100644 --- a/cms-server/src/main/java/com/condation/cms/server/filter/PooledRequestContextFilter.java +++ b/cms-server/src/main/java/com/condation/cms/server/filter/PooledRequestContextFilter.java @@ -22,12 +22,11 @@ * #L% */ import com.condation.cms.api.PerformanceProperties; -import com.condation.cms.api.ServerContext; import com.condation.cms.api.annotations.Experimental; import com.condation.cms.api.feature.features.IsPreviewFeature; import com.condation.cms.api.feature.features.RequestFeature; import com.condation.cms.api.request.RequestContext; -import com.condation.cms.api.request.ThreadLocalRequestContext; +import com.condation.cms.api.request.RequestContextScope; import com.condation.cms.api.utils.HTTPUtil; import com.condation.cms.api.utils.RequestUtil; import com.condation.cms.request.RequestContextFactory; @@ -84,19 +83,15 @@ public boolean handle(Request httpRequest, Response rspns, Callback clbck) throw var contextPath = httpRequest.getContext().getContextPath(); requestContext.add(RequestFeature.class, new RequestFeature(contextPath, uri, queryParameters, httpRequest)); - if (ServerContext.IS_DEV && queryParameters.containsKey("preview")) { - requestContext.add(IsPreviewFeature.class, new IsPreviewFeature()); - } - - ThreadLocalRequestContext.REQUEST_CONTEXT.set(requestContext); httpRequest.setAttribute(REQUEST_CONTEXT, requestContext); - return super.handle(httpRequest, rspns, clbck); + return ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, requestContext).call(() -> { + return super.handle(httpRequest, rspns, clbck); + }); } finally { requestContext.features.remove(RequestFeature.class); requestContext.features.remove(IsPreviewFeature.class); - ThreadLocalRequestContext.REQUEST_CONTEXT.remove(); requestContextPoolable.release(); } } diff --git a/cms-server/src/main/java/com/condation/cms/server/filter/PreviewFilter.java b/cms-server/src/main/java/com/condation/cms/server/filter/PreviewFilter.java new file mode 100644 index 000000000..4d4c32906 --- /dev/null +++ b/cms-server/src/main/java/com/condation/cms/server/filter/PreviewFilter.java @@ -0,0 +1,144 @@ +package com.condation.cms.server.filter; + +/*- + * #%L + * cms-server + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.Constants; +import com.condation.cms.api.ServerContext; +import com.condation.cms.api.configuration.Configuration; +import com.condation.cms.api.configuration.configs.ServerConfiguration; +import com.condation.cms.api.feature.features.AuthFeature; +import com.condation.cms.api.feature.features.IsDevModeFeature; +import com.condation.cms.api.feature.features.IsPreviewFeature; +import com.condation.cms.api.request.RequestContext; +import com.condation.cms.api.utils.HTTPUtil; +import com.condation.cms.modules.ui.utils.TokenUtils; +import com.google.inject.Inject; +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author t.marx + */ +@Slf4j +public class PreviewFilter extends Handler.Abstract { + + private final Configuration configuration; + + @Inject + public PreviewFilter(Configuration configuration) { + this.configuration = configuration; + } + + @Override + public boolean handle(final Request request, final Response rspns, final Callback clbck) throws Exception { + + var queryParameters = HTTPUtil.queryParameters(request.getHttpURI().getQuery()); + if (!queryParameters.containsKey("preview")) { + return false; + } + var mode = IsPreviewFeature.Mode.forValue(queryParameters.get("preview").getFirst()); + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); + + if (IsPreviewFeature.Mode.PREVIEW.equals(mode)) { + if (ServerContext.IS_DEV) { + requestContext.add(IsPreviewFeature.class, new IsPreviewFeature(mode)); + return false; + } + } + + var token = handlePreviewParameter(request, rspns); + + if (token.isEmpty()) { + token = getTokenFromCookie(request, "cms-preview-token"); + } + + if (token.isPresent() && handleToken(request, token.get())) { + return false; + } + + return false; + } + + private Optional getTokenFromCookie(Request request, String cookieName) { + var tokenCookie = Request.getCookies(request).stream().filter(cookie -> cookieName.equals(cookie.getName())).findFirst(); + if (tokenCookie.isEmpty()) { + return Optional.empty(); + } + return Optional.of(tokenCookie.get().getValue()); + } + + private boolean handleToken(Request request, String token) { + var secret = configuration.get(ServerConfiguration.class).serverProperties().secret(); + + var payload = TokenUtils.getPayload(token, secret); + + if (payload.isPresent()) { + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); + requestContext.add(IsPreviewFeature.class, new IsPreviewFeature(IsPreviewFeature.Mode.MANAGER)); + + requestContext.add(AuthFeature.class, new AuthFeature(payload.get().username())); + return true; + } + return false; + } + + private Optional handlePreviewParameter(Request request, Response response) { + var secret = configuration.get(ServerConfiguration.class).serverProperties().secret(); + var queryParameters = HTTPUtil.queryParameters(request.getHttpURI().getQuery()); + if (queryParameters.containsKey("preview-token")) { + var token = queryParameters.get("preview-token").getFirst(); + if (TokenUtils.getPayload(token, secret).isPresent()) { + setCookie(request, "cms-preview-token", token, response); + return Optional.of(token); + } + } + return Optional.empty(); + } + + private void setCookie(Request request, String name, String token, Response response) { + + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); + + boolean isDev = requestContext.has(IsDevModeFeature.class); + + HttpCookie cookie = HttpCookie.from(name, token, + Map.of( + HttpCookie.SAME_SITE_ATTRIBUTE, "Strict", + HttpCookie.HTTP_ONLY_ATTRIBUTE, "true", + HttpCookie.MAX_AGE_ATTRIBUTE, String.valueOf(Duration.ofHours(1).toSeconds()), + HttpCookie.PATH_ATTRIBUTE, "/" + )); + if (!isDev) { + cookie = HttpCookie.from(cookie, HttpCookie.SECURE_ATTRIBUTE, "true"); + } + Response.addCookie(response, cookie); + } + +} diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/AbstractHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/AbstractHandler.java new file mode 100644 index 000000000..a756ead92 --- /dev/null +++ b/cms-server/src/main/java/com/condation/cms/server/handler/AbstractHandler.java @@ -0,0 +1,41 @@ +package com.condation.cms.server.handler; + +/*- + * #%L + * cms-server + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.Constants; +import com.condation.cms.api.feature.features.IsPreviewFeature; +import com.condation.cms.api.request.RequestContext; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; + +/** + * + * @author thorstenmarx + */ +public abstract class AbstractHandler extends Handler.Abstract { + + protected boolean isPreview(Request request) { + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); + return requestContext.has(IsPreviewFeature.class); + } +} diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/auth/JettyAuthenticationHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/auth/JettyAuthenticationHandler.java index f3ab1e280..1517579a2 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/auth/JettyAuthenticationHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/auth/JettyAuthenticationHandler.java @@ -21,19 +21,17 @@ * . * #L% */ - - +import com.condation.cms.api.Constants; import com.condation.cms.api.cache.ICache; import com.condation.cms.api.feature.features.AuthFeature; import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.utils.RequestUtil; import com.condation.cms.auth.services.AuthService; +import com.condation.cms.auth.services.Realm; import com.condation.cms.auth.services.UserService; -import com.condation.cms.server.filter.CreateRequestContextFilter; import com.google.inject.Inject; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.InetSocketAddress; import java.util.Base64; import java.util.Optional; import java.util.StringTokenizer; @@ -60,10 +58,6 @@ public class JettyAuthenticationHandler extends Handler.Abstract { private final ICache loginFails; // LoadingCache loginFails = Caffeine.newBuilder() -// .maximumSize(10_000) -// .expireAfterWrite(Duration.ofMinutes(1)) -// .expireAfterAccess(Duration.ofMinutes(1)) -// .build(key -> new AtomicInteger(0)); static final int ATTEMPTS_TO_BLOCK = 3; @@ -104,7 +98,7 @@ public boolean handle(Request request, Response response, Callback callback) thr String username = credentials.substring(0, p).trim(); String password = credentials.substring(p + 1).trim(); - var userOpt = userService.login(UserService.Realm.of(authPath.getRealm()), username, password); + java.util.Optional userOpt = userService.login(Realm.of(authPath.getRealm()), username, password); if (userOpt.isEmpty()) { unauthorized(request, response, callback, authPath.getRealm()); @@ -112,11 +106,11 @@ public boolean handle(Request request, Response response, Callback callback) thr } if (authPath.allowed(userOpt.get())) { - - var requestContext = (RequestContext) request.getAttribute(CreateRequestContextFilter.REQUEST_CONTEXT); + + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); requestContext.add(AuthFeature.class, new AuthFeature(username)); - - loginFails.invalidate(clientAddress(request)); + + loginFails.invalidate(RequestUtil.clientAddress(request)); return false; } @@ -143,13 +137,8 @@ private void unauthorized(Request request, Response response, Callback callback, callback.succeeded(); } - private String clientAddress(Request request) { - return ((InetSocketAddress) request.getConnectionMetaData().getRemoteSocketAddress()) - .getAddress().getHostAddress(); - } - private AtomicInteger getClientLoginCounter(Request request) { - return loginFails.get(clientAddress(request)); + return loginFails.get(RequestUtil.clientAddress(request)); } } diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyContentHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyContentHandler.java index 7000f6372..4250f299a 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyContentHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyContentHandler.java @@ -21,8 +21,8 @@ * . * #L% */ - - +import com.condation.cms.api.Constants; +import com.condation.cms.api.ServerContext; import com.condation.cms.api.configuration.configs.SiteConfiguration; import com.condation.cms.api.content.ContentResponse; import com.condation.cms.api.content.DefaultContentResponse; @@ -33,11 +33,14 @@ import com.condation.cms.api.utils.RequestUtil; import com.condation.cms.content.ContentResolver; import com.condation.cms.request.RequestContextFactory; -import com.condation.cms.server.filter.CreateRequestContextFilter; import com.google.inject.Inject; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.Handler; @@ -62,7 +65,7 @@ public boolean handle(Request request, Response response, Callback callback) thr // var uri = request.getHttpURI().getPath(); var uri = RequestUtil.getContentPath(request); var queryParameters = HTTPUtil.queryParameters(request.getHttpURI().getQuery()); - var requestContext = (RequestContext) request.getAttribute(CreateRequestContextFilter.REQUEST_CONTEXT); + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); // handle enabled spa mode var spaEnabled = requestContext.get(ConfigurationFeature.class).configuration().get(SiteConfiguration.class).siteProperties().spaEnabled(); @@ -71,7 +74,7 @@ public boolean handle(Request request, Response response, Callback callback) thr uri = ""; notFoundContent = "/"; } - + try { Optional content = contentResolver.getContent(requestContext); response.setStatus(200); @@ -109,7 +112,11 @@ public boolean handle(Request request, Response response, Callback callback) thr log.error("error handling content", e); response.setStatus(500); response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/html; charset=utf-8"); - callback.succeeded(); + + if (ServerContext.IS_DEV) { + var stacktrace = ExceptionUtils.getStackTrace(e); + Content.Sink.write(response, true, "
    %s
    ".formatted(stacktrace), callback); + } } return true; } diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyTaxonomyHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyTaxonomyHandler.java index 8d2d20796..5ad9bb080 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyTaxonomyHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyTaxonomyHandler.java @@ -23,6 +23,7 @@ */ +import com.condation.cms.api.Constants; import com.condation.cms.api.content.TaxonomyResponse; import com.condation.cms.api.request.RequestContext; import com.condation.cms.content.TaxonomyResolver; @@ -48,7 +49,7 @@ public class JettyTaxonomyHandler extends Handler.Abstract { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - var requestContext = (RequestContext) request.getAttribute(CreateRequestContextFilter.REQUEST_CONTEXT); + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); try { if (!taxonomyResolver.isTaxonomy(requestContext)) { diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyViewHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyViewHandler.java index a2f966cc3..7cc644039 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyViewHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyViewHandler.java @@ -23,6 +23,7 @@ */ +import com.condation.cms.api.Constants; import com.condation.cms.api.content.ContentResponse; import com.condation.cms.api.content.DefaultContentResponse; import com.condation.cms.api.request.RequestContext; @@ -51,7 +52,7 @@ public class JettyViewHandler extends Handler.Abstract { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - var requestContext = (RequestContext) request.getAttribute(CreateRequestContextFilter.REQUEST_CONTEXT); + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); try { Optional viewResponse = viewResolver.getViewContent(requestContext); diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyHttpHandlerExtensionHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyHttpHandlerExtensionHandler.java index a8ed7ec83..c2b607263 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyHttpHandlerExtensionHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyHttpHandlerExtensionHandler.java @@ -23,7 +23,9 @@ */ +import com.condation.cms.api.Constants; import com.condation.cms.api.request.RequestContext; +import com.condation.cms.api.utils.RequestUtil; import com.condation.cms.extensions.HttpHandlerExtension; import com.condation.cms.extensions.hooks.ServerHooks; import com.condation.cms.extensions.http.JettyHttpHandlerWrapper; @@ -48,7 +50,7 @@ public class JettyHttpHandlerExtensionHandler extends Handler.Abstract { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - var requestContext = (RequestContext) request.getAttribute(CreateRequestContextFilter.REQUEST_CONTEXT); + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); String extension = getExtensionName(request); var method = request.getMethod(); @@ -69,7 +71,7 @@ public boolean handle(Request request, Response response, Callback callback) thr private String getExtensionName(Request request) { var path = request.getHttpURI().getPath(); - var contextPath = request.getContext().getContextPath(); + var contextPath = RequestUtil.getContentPath(request); if (!contextPath.endsWith("/")) { contextPath += "/"; diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/http/APIHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/http/APIHandler.java index e23b780ce..da0408efa 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/http/APIHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/http/APIHandler.java @@ -1,5 +1,6 @@ package com.condation.cms.server.handler.http; +import com.condation.cms.api.Constants; import com.condation.cms.api.configuration.configs.SiteConfiguration; import com.condation.cms.api.extensions.http.APIHandlerExtensionPoint; import com.condation.cms.api.extensions.http.PathMapping; @@ -28,10 +29,12 @@ */ import com.condation.cms.api.request.RequestContext; +import com.condation.cms.api.utils.RequestUtil; import com.condation.cms.extensions.HttpHandlerExtension; import com.condation.cms.extensions.hooks.ServerHooks; import com.condation.cms.extensions.http.JettyHttpHandlerWrapper; import com.condation.cms.server.filter.CreateRequestContextFilter; +import com.condation.cms.server.handler.AbstractHandler; import com.condation.modules.api.ModuleManager; import com.google.inject.Inject; @@ -50,14 +53,14 @@ @RequiredArgsConstructor(onConstructor = @__({ @Inject })) @Slf4j -public class APIHandler extends Handler.Abstract { +public class APIHandler extends AbstractHandler { public static final String PATH = "api"; private final ModuleManager moduleManager; private boolean isApiActivated (Request request) { - var requestContext = (RequestContext) request.getAttribute(CreateRequestContextFilter.REQUEST_CONTEXT); + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); var siteProperties = requestContext.get(ConfigurationFeature.class).configuration().get(SiteConfiguration.class).siteProperties(); return siteProperties.getOrDefault("api.enabled", Boolean.FALSE); @@ -109,7 +112,7 @@ private boolean handleModuleRoute(Request request, Response response, Callback c } private boolean handleExtensionRoute(Request request, Response response, Callback callback) throws Exception { - var requestContext = (RequestContext) request.getAttribute(CreateRequestContextFilter.REQUEST_CONTEXT); + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); String extension = getApiRoute(request); var method = request.getMethod(); @@ -126,7 +129,7 @@ private boolean handleExtensionRoute(Request request, Response response, Callbac private String getApiRoute(Request request) { var path = request.getHttpURI().getPath(); - var contextPath = request.getContext().getContextPath(); + var contextPath = RequestUtil.getContextPath(request); if (!contextPath.endsWith("/")) { contextPath += "/"; diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/http/RoutesHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/http/RoutesHandler.java index 04c52d49f..27d8b6f3d 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/http/RoutesHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/http/RoutesHandler.java @@ -22,6 +22,7 @@ * #L% */ +import com.condation.cms.api.Constants; import com.condation.cms.api.extensions.HttpRoutesExtensionPoint; import com.condation.cms.api.extensions.Mapping; import com.condation.cms.api.extensions.http.routes.RoutesExtensionPoint; @@ -81,11 +82,7 @@ private boolean tryRoutesManager (Request request, Response response, Callback c moduleManager.extensions(RoutesExtensionPoint.class) - .stream() - .map(RoutesExtensionPoint::getRouteDefinitions) - .filter(routeDefinitions -> routeDefinitions != null && !routeDefinitions.isEmpty()) - .flatMap(List::stream) - .forEach(controller -> routesManager.register(controller)); + .forEach(routesManager::register); var handler = routesManager.findFirst(route, request.getMethod()); if (handler.isPresent()) { @@ -114,7 +111,7 @@ private boolean tryModuleRoutes(Request request, Response response, Callback cal } private boolean tryExtensionRoutes(Request request, Response response, Callback callback) throws Exception { - var requestContext = (RequestContext) request.getAttribute(CreateRequestContextFilter.REQUEST_CONTEXT); + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); String route = "/" + RequestUtil.getContentPath(request); var method = request.getMethod(); diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/media/JettyMediaHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/media/JettyMediaHandler.java index 2b16e420e..b5253275a 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/media/JettyMediaHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/media/JettyMediaHandler.java @@ -26,18 +26,20 @@ import com.condation.cms.api.ServerContext; import com.condation.cms.api.media.MediaUtils; import com.condation.cms.api.utils.HTTPUtil; +import com.condation.cms.api.utils.RequestUtil; import com.condation.cms.media.MediaManager; +import com.condation.cms.server.handler.AbstractHandler; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.List; +import java.util.Optional; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.Callback; @@ -48,7 +50,7 @@ */ @RequiredArgsConstructor @Slf4j -public class JettyMediaHandler extends Handler.Abstract { +public class JettyMediaHandler extends AbstractHandler { @Getter private final MediaManager mediaManager; @@ -63,12 +65,12 @@ public boolean handle(Request request, Response response, Callback callback) thr if ("##original##".equalsIgnoreCase(formatValue)) { var mediaPath = getRelativeMediaPath(request); - Path assetPath = mediaManager.resolve(mediaPath); - if (assetPath != null) { - var bytes = Files.readAllBytes(assetPath); - var mimetype = Files.probeContentType(assetPath); + Optional assetPath = mediaManager.resolve(mediaPath); + if (assetPath.isPresent()) { + var bytes = Files.readAllBytes(assetPath.get()); + var mimetype = Files.probeContentType(assetPath.get()); - deliver(bytes, mimetype, response); + deliver(bytes, mimetype, request, response); callback.succeeded(); return true; @@ -87,7 +89,7 @@ public boolean handle(Request request, Response response, Callback callback) thr var result = mediaManager.getScaledContent(mediaPath, format); if (result.isPresent()) { - deliver(result.get(), MediaUtils.mime4Format(format.format()), response); + deliver(result.get(), MediaUtils.mime4Format(format.format()), request, response); callback.succeeded(); return true; @@ -103,10 +105,12 @@ public boolean handle(Request request, Response response, Callback callback) thr return true; } - private void deliver(final byte[] bytes, final String mimetype, Response response) throws IOException { + + + private void deliver(final byte[] bytes, final String mimetype, Request request, Response response) throws IOException { response.getHeaders().add("Content-Type", mimetype); response.getHeaders().add("Content-Length", bytes.length); - if (!ServerContext.IS_DEV) { + if (!ServerContext.IS_DEV && !isPreview(request)) { response.getHeaders().add("Access-Control-Max-Age", Duration.ofDays(10).toSeconds()); response.getHeaders().add("Cache-Control", "max-age=" + Duration.ofDays(10).toSeconds()); } @@ -117,7 +121,7 @@ private void deliver(final byte[] bytes, final String mimetype, Response respons private String getRelativeMediaPath(Request request) { var path = request.getHttpURI().getPath(); - var contextPath = request.getContext().getContextPath(); + var contextPath = RequestUtil.getContextPath(request); if (!contextPath.endsWith("/")) { contextPath += "/"; } diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/module/JettyRouteHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/module/JettyRouteHandler.java deleted file mode 100644 index 97a6cb289..000000000 --- a/cms-server/src/main/java/com/condation/cms/server/handler/module/JettyRouteHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.condation.cms.server.handler.module; - -/*- - * #%L - * cms-server - * %% - * Copyright (C) 2023 - 2024 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - - -import com.condation.cms.api.extensions.HttpRouteExtensionPoint; -import com.condation.cms.api.utils.RequestUtil; -import com.condation.modules.api.ModuleManager; -import com.google.inject.Inject; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Callback; - -/** - * - * @author t.marx - */ -@RequiredArgsConstructor(onConstructor = @__({ - @Inject})) -@Slf4j -@Deprecated(since = "7.3.0", forRemoval = true) -public class JettyRouteHandler extends Handler.Abstract { - - private final ModuleManager moduleManager; - - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception { - - String route = RequestUtil.getContentPath(request); - - Optional findRoute = moduleManager.extensions(HttpRouteExtensionPoint.class).stream() - .filter(extension -> extension.getRoute().equals(route)).findFirst(); - - if (findRoute.isPresent()) { - findRoute.get().handle(request, response, callback); - return true; - } - - return false; - } -} diff --git a/cms-server/src/main/java/com/condation/cms/server/host/Initializer.java b/cms-server/src/main/java/com/condation/cms/server/host/Initializer.java new file mode 100644 index 000000000..a723ea95a --- /dev/null +++ b/cms-server/src/main/java/com/condation/cms/server/host/Initializer.java @@ -0,0 +1,57 @@ +package com.condation.cms.server.host; + +/*- + * #%L + * cms-server + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.configuration.Configuration; +import com.condation.cms.api.db.DB; +import com.condation.cms.api.eventbus.EventBus; +import com.condation.cms.core.serivce.ServiceRegistry; +import com.condation.cms.core.serivce.impl.NodeTranslationService; +import com.condation.cms.core.serivce.impl.SiteDBService; +import com.condation.cms.core.serivce.impl.SiteLinkService; +import com.condation.cms.core.serivce.impl.SitePropertiesService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * + * @author thorstenmarx + */ +@Slf4j +@RequiredArgsConstructor +public class Initializer { + + private final VHost host; + + void initServices () { + var db = host.injector.getInstance(DB.class); + ServiceRegistry.getInstance().register(host.id(), SiteDBService.class, new SiteDBService(db)); + + var config = host.injector.getInstance(Configuration.class); + ServiceRegistry.getInstance().register(host.id(), SiteLinkService.class, new SiteLinkService(config)); + + ServiceRegistry.getInstance().register(host.id(), SitePropertiesService.class, new SitePropertiesService(config)); + + ServiceRegistry.getInstance().register(host.id(), NodeTranslationService.class, new NodeTranslationService(db, host.injector.getInstance(EventBus.class))); + } +} diff --git a/cms-server/src/main/java/com/condation/cms/server/VHost.java b/cms-server/src/main/java/com/condation/cms/server/host/VHost.java similarity index 89% rename from cms-server/src/main/java/com/condation/cms/server/VHost.java rename to cms-server/src/main/java/com/condation/cms/server/host/VHost.java index ac220d3a8..ded34d412 100644 --- a/cms-server/src/main/java/com/condation/cms/server/VHost.java +++ b/cms-server/src/main/java/com/condation/cms/server/host/VHost.java @@ -1,4 +1,4 @@ -package com.condation.cms.server; +package com.condation.cms.server.host; import java.io.IOException; import java.nio.file.Path; @@ -10,7 +10,6 @@ import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.PathMappingsHandler; import org.eclipse.jetty.server.handler.ResourceHandler; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; /*- * #%L @@ -33,8 +32,6 @@ * . * #L% */ - - import com.condation.cms.api.SiteProperties; import com.condation.cms.api.cache.CacheManager; import com.condation.cms.api.configuration.Configuration; @@ -46,11 +43,12 @@ import com.condation.cms.api.eventbus.EventListener; import com.condation.cms.api.eventbus.events.ConfigurationReloadEvent; import com.condation.cms.api.eventbus.events.InvalidateContentCacheEvent; +import com.condation.cms.api.eventbus.events.InvalidateMediaCache; import com.condation.cms.api.eventbus.events.InvalidateTemplateCacheEvent; import com.condation.cms.api.eventbus.events.lifecycle.HostReloadedEvent; import com.condation.cms.api.eventbus.events.lifecycle.HostStoppedEvent; import com.condation.cms.api.feature.features.ThemeFeature; -import com.condation.cms.api.module.CMSModuleContext; +import com.condation.cms.api.module.SiteModuleContext; import com.condation.cms.api.template.TemplateEngine; import com.condation.cms.api.theme.Theme; import com.condation.cms.api.utils.SiteUtil; @@ -62,7 +60,8 @@ import com.condation.cms.media.SiteMediaManager; import com.condation.cms.media.ThemeMediaManager; import com.condation.cms.request.RequestContextFactory; -import com.condation.cms.server.configs.ModulesModule; +import com.condation.cms.server.FileFolderPathResource; +import com.condation.cms.server.configs.SiteModulesModule; import com.condation.cms.server.configs.SiteConfigInitializer; import com.condation.cms.server.configs.SiteGlobalModule; import com.condation.cms.server.configs.SiteHandlerModule; @@ -72,6 +71,7 @@ import com.condation.cms.server.filter.InitRequestContextFilter; import com.condation.cms.server.filter.PooledRequestContextFilter; import com.condation.cms.server.filter.RequestLoggingFilter; +import com.condation.cms.server.filter.PreviewFilter; import com.condation.cms.server.handler.auth.JettyAuthenticationHandler; import com.condation.cms.server.handler.cache.CacheHandler; import com.condation.cms.server.handler.content.JettyContentHandler; @@ -82,7 +82,6 @@ import com.condation.cms.server.handler.http.RoutesHandler; import com.condation.cms.server.handler.media.JettyMediaHandler; import com.condation.cms.server.handler.module.JettyModuleHandler; -import com.condation.cms.server.handler.module.JettyRouteHandler; import com.condation.modules.api.ModuleManager; import com.google.inject.Injector; import com.google.inject.Key; @@ -105,7 +104,7 @@ public class VHost { @Getter protected Injector injector; - + private final Configuration configuration = new Configuration(); public VHost(final Path hostBase) { @@ -143,7 +142,7 @@ public void reload() { themeAssetsHandler.start(); this.injector.getInstance(TemplateEngine.class).updateTheme(theme); - this.injector.getInstance(CMSModuleContext.class).get(ThemeFeature.class).updateTheme(theme); + this.injector.getInstance(SiteModuleContext.class).get(ThemeFeature.class).updateTheme(theme); injector.getInstance(EventBus.class).syncPublish(new HostReloadedEvent(id())); } catch (Exception e) { @@ -156,10 +155,9 @@ public List hostnames() { } public void init(Path modulesPath, Injector globalInjector) throws IOException { - this.injector = globalInjector.createChildInjector( - new SiteGlobalModule(), + this.injector = globalInjector.createChildInjector(new SiteGlobalModule(), new SiteModule(hostBase, this.configuration), - new ModulesModule(modulesPath), + new SiteModulesModule(modulesPath), new SiteHandlerModule(), new ThemeModule()); @@ -167,7 +165,7 @@ public void init(Path modulesPath, Injector globalInjector) throws IOException { injector.getInstance(ConfigManagement.class).initConfiguration(configuration); // run site initializer injector.getInstance(SiteConfigInitializer.class).init(); - + var moduleManager = injector.getInstance(ModuleManager.class); moduleManager.initModules(); List activeModules = getActiveModules(); @@ -191,6 +189,8 @@ public void init(Path modulesPath, Injector globalInjector) throws IOException { injector.getInstance(TemplateEngine.class).invalidateCache(); }); + Initializer initializer = new Initializer(this); + initializer.initServices(); initSiteGlobals(); } @@ -222,15 +222,16 @@ public Handler buildHttpHandler() { var taxonomyHandler = injector.getInstance(JettyTaxonomyHandler.class); var viewHandler = injector.getInstance(JettyViewHandler.class); - var routeHandler = injector.getInstance(JettyRouteHandler.class); var routesHandler = injector.getInstance(RoutesHandler.class); var authHandler = injector.getInstance(JettyAuthenticationHandler.class); var initContextHandler = injector.getInstance(InitRequestContextFilter.class); + var uiPreviewFilter = injector.getInstance(PreviewFilter.class); + var defaultHandlerSequence = new Handler.Sequence( authHandler, initContextHandler, - routeHandler, + uiPreviewFilter, routesHandler, viewHandler, taxonomyHandler, @@ -248,15 +249,27 @@ public Handler buildHttpHandler() { PathMappingsHandler pathMappingsHandler = new PathMappingsHandler(); pathMappingsHandler.addMapping( PathSpec.from("/"), - requestContextFilter(defaultHandlerSequence, injector) + defaultHandlerSequence ); pathMappingsHandler.addMapping(PathSpec.from("/assets/*"), assetsHandler); pathMappingsHandler.addMapping(PathSpec.from("/favicon.ico"), faviconHandler); var assetsMediaManager = this.injector.getInstance(SiteMediaManager.class); injector.getInstance(EventBus.class).register(ConfigurationReloadEvent.class, assetsMediaManager); + injector.getInstance(EventBus.class).register(InvalidateMediaCache.class, (event) -> { + if (event.mediaPath() != null) { + assetsMediaManager.deleteTempFile(event.mediaPath()); + } else { + assetsMediaManager.clearTempDirectory(); + } + }); final JettyMediaHandler mediaHandler = this.injector.getInstance(Key.get(JettyMediaHandler.class, Names.named("site"))); - pathMappingsHandler.addMapping(PathSpec.from("/media/*"), mediaHandler); + + var siteMediaHandlerSequence = new Handler.Sequence( + uiPreviewFilter, + mediaHandler + ); + pathMappingsHandler.addMapping(PathSpec.from("/media/*"), siteMediaHandlerSequence); pathMappingsHandler.addMapping(PathSpec.from("/" + JettyModuleHandler.PATH + "/*"), createModuleHandler() @@ -288,19 +301,13 @@ public Handler buildHttpHandler() { RequestLoggingFilter logContextHandler = new RequestLoggingFilter(contextCollection, injector.getInstance(SiteProperties.class)); - GzipHandler gzipHandler = new GzipHandler(logContextHandler); - gzipHandler.setMinGzipSize(1024); - gzipHandler.addIncludedMimeTypes("text/plain"); - gzipHandler.addIncludedMimeTypes("text/html"); - gzipHandler.addIncludedMimeTypes("text/css"); - gzipHandler.addIncludedMimeTypes("application/javascript"); - - hostHandler = gzipHandler; - - return hostHandler; + hostHandler = logContextHandler; + + + return requestContextFilter(hostHandler, injector); } - - private Handler.Wrapper createAPIHandler() { + + private Handler createAPIHandler() { var authHandler = injector.getInstance(JettyAuthenticationHandler.class); var initContextHandler = injector.getInstance(InitRequestContextFilter.class); var apiHandler = injector.getInstance(APIHandler.class); @@ -309,10 +316,10 @@ private Handler.Wrapper createAPIHandler() { initContextHandler, apiHandler ); - return requestContextFilter(handlerSequence, injector); + return handlerSequence; } - private Handler.Wrapper createExtensionHandler() { + private Handler createExtensionHandler() { var authHandler = injector.getInstance(JettyAuthenticationHandler.class); var initContextHandler = injector.getInstance(InitRequestContextFilter.class); var extensionHandler = injector.getInstance(JettyHttpHandlerExtensionHandler.class); @@ -321,10 +328,10 @@ private Handler.Wrapper createExtensionHandler() { initContextHandler, extensionHandler ); - return requestContextFilter(handlerSequence, injector); + return handlerSequence; } - - private Handler.Wrapper createModuleHandler() { + + private Handler createModuleHandler() { var authHandler = injector.getInstance(JettyAuthenticationHandler.class); var initContextHandler = injector.getInstance(InitRequestContextFilter.class); var modulehandler = injector.getInstance(JettyModuleHandler.class); @@ -333,7 +340,7 @@ private Handler.Wrapper createModuleHandler() { initContextHandler, modulehandler ); - return requestContextFilter(handlerSequence, injector); + return handlerSequence; } private Handler.Wrapper requestContextFilter(Handler handler, Injector injector) { diff --git a/cms-templates/pom.xml b/cms-templates/pom.xml index 9c6b6ac2a..4c0f72f69 100644 --- a/cms-templates/pom.xml +++ b/cms-templates/pom.xml @@ -7,15 +7,15 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 cms-templates jar - 21 - 21 + 25 + 25 diff --git a/cms-templates/src/main/java/com/condation/cms/templates/DefaultTemplate.java b/cms-templates/src/main/java/com/condation/cms/templates/DefaultTemplate.java index 52f38dd4e..3568d056b 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/DefaultTemplate.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/DefaultTemplate.java @@ -22,6 +22,8 @@ * #L% */ +import com.condation.cms.templates.functions.JexlTemplateFunction; +import com.condation.cms.templates.functions.impl.DateFunction; import com.condation.cms.templates.parser.ASTNode; import com.condation.cms.templates.renderer.Renderer; import com.condation.cms.templates.renderer.ScopeStack; @@ -46,8 +48,7 @@ public class DefaultTemplate implements Template { @Override public void evaluate(Map context, Writer writer, DynamicConfiguration dynamicConfiguration) throws IOException { - - ScopeStack scopes = new ScopeStack(context); + ScopeStack scopes = createScope(context, dynamicConfiguration); evaluate(scopes, writer, dynamicConfiguration); @@ -56,7 +57,7 @@ public void evaluate(Map context, Writer writer, DynamicConfigur @Override public String evaluate(Map context, DynamicConfiguration dynamicConfiguration) throws IOException { - ScopeStack scopes = new ScopeStack(context); + ScopeStack scopes = createScope(context, dynamicConfiguration); try (var writer = new StringWriter()) { renderer.render(rootNode, scopes, writer, dynamicConfiguration); @@ -69,4 +70,15 @@ public void evaluate (ScopeStack scopes, Writer writer, DynamicConfiguration dyn renderer.render(rootNode, scopes, writer, dynamicConfiguration); } + private ScopeStack createScope (Map context, DynamicConfiguration dynamicConfiguration) { + var scope = new ScopeStack(context); + scope.setVariable("date", new JexlTemplateFunction(new DateFunction())); + + dynamicConfiguration.templateFunctions().forEach(tf -> { + scope.setVariable(tf.name(), new JexlTemplateFunction(tf)); + }); + + return scope; + } + } diff --git a/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java b/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java index 945c62cda..fe944046f 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java @@ -21,14 +21,26 @@ * . * #L% */ +import com.condation.cms.api.extensions.RegisterTemplateFunctionExtensionPoint; +import com.condation.cms.api.feature.features.InjectorFeature; +import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; +import com.condation.cms.extensions.TemplateFunctionExtension; +import com.condation.cms.extensions.hooks.TemplateHooks; import com.condation.cms.templates.components.TemplateComponents; +import com.condation.cms.templates.components.TemplateFunctions; +import com.condation.cms.templates.functions.TemplateFunction; import com.condation.cms.templates.tags.component.EndComponentTag; import com.condation.cms.templates.tags.component.ComponentTag; +import com.condation.modules.api.ModuleManager; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; +import lombok.RequiredArgsConstructor; /** * @@ -42,7 +54,7 @@ public record DynamicConfiguration(TemplateComponents templateComponents, Map getComponent(String name) { return Optional.ofNullable(components.get(name)); } + + public List templateFunctions() { + if (requestContext == null) { + return Collections.emptyList(); + } + var templateFunctions = requestContext.get(TemplateHooks.class).getTemplateFunctions(); + var tfs = new ArrayList(); + templateFunctions.getRegisterTemplateFunctions().stream() + .map(function -> new FunctionWrapper(function.name(), function.function(), requestContext)) + .forEach(tfs::add); + + var injector = requestContext.get(InjectorFeature.class).injector(); + + var templateFunctionsModel = new TemplateFunctions(); + injector.getInstance(ModuleManager.class) + .extensions(RegisterTemplateFunctionExtensionPoint.class) + .forEach(extension -> { + templateFunctionsModel.register(extension.functions()); + templateFunctionsModel.register(extension.functionDefinitions()); + }); + + templateFunctionsModel.getFunctionMap().names().forEach(name -> { + tfs.add( + new FunctionWrapper(name, templateFunctionsModel.getFunctionMap().get(name), requestContext) + ); + }); + + return tfs; + } + + @RequiredArgsConstructor + public static class FunctionWrapper implements TemplateFunction { + + private final String name; + private final Function function; + private final RequestContext requestContext; + + @Override + public Object invoke(Object... params) { + Parameter parameter = null; + if (params.length == 1 && params[0] instanceof Parameter) { + parameter = (Parameter)params[0]; + } else if (params.length == 1 && params[0] instanceof Map) { + parameter = new Parameter((Map)params[0], requestContext); + } else { + parameter = new Parameter(); + } + return function.apply(parameter); + } + + @Override + public String name() { + return name; + } + + } } diff --git a/cms-templates/src/main/java/com/condation/cms/templates/TemplateEngineFactory.java b/cms-templates/src/main/java/com/condation/cms/templates/TemplateEngineFactory.java index 88abbe907..c684a01a5 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/TemplateEngineFactory.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/TemplateEngineFactory.java @@ -2,6 +2,7 @@ import com.condation.cms.api.cache.ICache; import com.condation.cms.templates.filter.impl.DateFilter; +import com.condation.cms.templates.filter.impl.DefaultFilter; import com.condation.cms.templates.filter.impl.RawFilter; import com.condation.cms.templates.filter.impl.UpperFilter; @@ -95,7 +96,8 @@ public TemplateEngineFactory defaultFilters() { configuration .registerFilter(DateFilter.NAME, new DateFilter()) .registerFilter(UpperFilter.NAME, new UpperFilter()) - .registerFilter(RawFilter.NAME, new RawFilter()); + .registerFilter(RawFilter.NAME, new RawFilter()) + .registerFilter(DefaultFilter.NAME, new DefaultFilter()); return this; } diff --git a/cms-templates/src/main/java/com/condation/cms/templates/components/FunctionMap.java b/cms-templates/src/main/java/com/condation/cms/templates/components/FunctionMap.java new file mode 100644 index 000000000..13a6401bf --- /dev/null +++ b/cms-templates/src/main/java/com/condation/cms/templates/components/FunctionMap.java @@ -0,0 +1,60 @@ +package com.condation.cms.templates.components; + +/*- + * #%L + * cms-content + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.model.Parameter; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import lombok.extern.slf4j.Slf4j; + +/** + * + * @author t.marx + */ +@Slf4j +public class FunctionMap { + + private final Map> tags = new HashMap<>(); + + public Set names() { + return Collections.unmodifiableSet(tags.keySet()); + } + + public void put(String codeName, Function function) { + tags.put(codeName, function); + } + + public void putAll(Map> tags) { + this.tags.putAll(tags); + } + + public boolean has(String codeName) { + return tags.containsKey(codeName); + } + + public Function get(String name) { + return tags.getOrDefault(name, (params) -> ""); + } +} diff --git a/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java b/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java index 93befd56e..48b005de3 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java @@ -24,6 +24,7 @@ import com.condation.cms.api.annotations.TemplateComponent; import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; +import com.condation.cms.api.utils.AnnotationsUtil; import java.lang.reflect.Method; import java.util.List; import java.util.Map; @@ -64,25 +65,18 @@ public void register(Object handler) { return; } - Class clazz = handler.getClass(); - for (Method method : clazz.getDeclaredMethods()) { - if (method.isAnnotationPresent(TemplateComponent.class)) { - if (method.getParameterCount() == 1 && method.getParameterTypes()[0] == Parameter.class) { - method.setAccessible(true); - var annotation = method.getAnnotation(TemplateComponent.class); - String key = annotation.value(); + var annotations = AnnotationsUtil.process(handler, TemplateComponent.class, List.of(Parameter.class), String.class); - componentMap.put(key, param -> { - try { - return (String) method.invoke(handler, param); - } catch (Exception e) { - throw new RuntimeException("Error calling component: " + key, e); - } - }); - } else { - log.error("ignore methode" + method.getName() + " – wrong signature."); + for (var entry : annotations) { + String key = entry.annotation().value(); + + componentMap.put(key, param -> { + try { + return entry.invoke(param); + } catch (Exception e) { + throw new RuntimeException("Error calling component: " + key, e); } - } + }); } } diff --git a/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateFunctions.java b/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateFunctions.java new file mode 100644 index 000000000..4ee32d0f5 --- /dev/null +++ b/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateFunctions.java @@ -0,0 +1,120 @@ +package com.condation.cms.templates.components; + +/*- + * #%L + * cms-content + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.annotations.TemplateComponent; +import com.condation.cms.api.annotations.TemplateFunction; +import com.condation.cms.api.model.Parameter; +import com.condation.cms.api.request.RequestContext; +import com.condation.cms.api.utils.AnnotationsUtil; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * + * @author t.marx + */ +@Slf4j +public class TemplateFunctions { + + @Getter + private final FunctionMap functionMap; + + public TemplateFunctions() { + this.functionMap = new FunctionMap(); + } + + public void register(String name, Function templateFunction) { + functionMap.put(name, templateFunction); + } + + public void register(Map> tempaleFunctions) { + this.functionMap.putAll(tempaleFunctions); + } + + public void register(List handlers) { + if (handlers == null || handlers.isEmpty()) { + return; + } + handlers.forEach(this::register); + } + + public void register(Object handler) { + if (handler == null) { + return; + } + + var annotations = AnnotationsUtil.process(handler, TemplateFunction.class, List.of(Parameter.class), Object.class); + + for (var entry : annotations) { + String key = entry.annotation().value(); + + functionMap.put(key, param -> { + try { + return entry.invoke(param); + } catch (Exception e) { + throw new RuntimeException("Error calling component: " + key, e); + } + }); + } + + annotations = AnnotationsUtil.process(handler, TemplateFunction.class, List.of(), Object.class); + + for (var entry : annotations) { + String key = entry.annotation().value(); + + functionMap.put(key, param -> { + try { + return entry.invoke(null); + } catch (Exception e) { + throw new RuntimeException("Error calling component: " + key, e); + } + }); + } + } + + public Set getFunctionNames() { + return functionMap.names(); + } + + public Object execute(String name, Map parameters, RequestContext requestContext) { + if (!functionMap.has(name)) { + return ""; + } + try { + Parameter params; + if (parameters != null) { + params = new Parameter(parameters); + } else { + params = new Parameter(); + } + return functionMap.get(name).apply(params); + } catch (Exception e) { + log.error("", e); + } + return ""; + } +} diff --git a/cms-templates/src/main/java/com/condation/cms/templates/filter/impl/DefaultFilter.java b/cms-templates/src/main/java/com/condation/cms/templates/filter/impl/DefaultFilter.java new file mode 100644 index 000000000..ee5a7e51b --- /dev/null +++ b/cms-templates/src/main/java/com/condation/cms/templates/filter/impl/DefaultFilter.java @@ -0,0 +1,50 @@ +package com.condation.cms.templates.filter.impl; + +/*- + * #%L + * templates + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.templates.filter.Filter; + +public class DefaultFilter implements Filter { + + public static final String NAME = "default"; + +@Override +public Object apply(Object input, Object... params) { + if (input == null) { + return params.length == 1 ? params[0] : null; + } + + String str = input.toString().trim(); + + // Prüfe auf escaped leeres

    + if (str.isEmpty() + || str.equals("<p></p>") + || str.equals("&lt;p&gt;&lt;/p&gt;") + || str.equals("

    ")) { + return params.length == 1 ? params[0] : input; + } + + return input; +} + +} diff --git a/cms-templates/src/main/java/com/condation/cms/templates/functions/JexlTemplateFunction.java b/cms-templates/src/main/java/com/condation/cms/templates/functions/JexlTemplateFunction.java new file mode 100644 index 000000000..b4c7f2f26 --- /dev/null +++ b/cms-templates/src/main/java/com/condation/cms/templates/functions/JexlTemplateFunction.java @@ -0,0 +1,71 @@ +package com.condation.cms.templates.functions; + +/*- + * #%L + * cms-templates + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.jexl3.JexlEngine; +import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.introspection.JexlMethod; + +/** + * + * @author thorstenmarx + */ +@RequiredArgsConstructor +@Slf4j +public class JexlTemplateFunction implements JexlMethod { + + private final TemplateFunction function; + + @Override + public Class getReturnType() { + return Object.class; + } + + @Override + public Object invoke(Object obj, Object... params) throws Exception { + return function.invoke(params); + } + + @Override + public boolean isCacheable() { + return false; + } + + @Override + public boolean tryFailed(Object rval) { + return JexlEngine.TRY_FAILED.equals(rval); + } + + @Override + public Object tryInvoke(String name, Object obj, Object... params) throws JexlException.TryFailed { + try { + return invoke(obj, params); + } catch (Exception ex) { + log.error("error calling macro", ex); + } + return JexlEngine.TRY_FAILED; + } + +} diff --git a/cms-templates/src/main/java/com/condation/cms/templates/functions/TemplateFunction.java b/cms-templates/src/main/java/com/condation/cms/templates/functions/TemplateFunction.java new file mode 100644 index 000000000..4baa90f66 --- /dev/null +++ b/cms-templates/src/main/java/com/condation/cms/templates/functions/TemplateFunction.java @@ -0,0 +1,33 @@ +package com.condation.cms.templates.functions; + +/*- + * #%L + * cms-templates + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public interface TemplateFunction { + Object invoke (Object... params); + + String name(); +} diff --git a/cms-templates/src/main/java/com/condation/cms/templates/functions/impl/DateFunction.java b/cms-templates/src/main/java/com/condation/cms/templates/functions/impl/DateFunction.java new file mode 100644 index 000000000..cc983aa50 --- /dev/null +++ b/cms-templates/src/main/java/com/condation/cms/templates/functions/impl/DateFunction.java @@ -0,0 +1,44 @@ +package com.condation.cms.templates.functions.impl; + +/*- + * #%L + * cms-templates + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.templates.functions.TemplateFunction; +import java.util.Date; + +/** + * + * @author thorstenmarx + */ +public class DateFunction implements TemplateFunction { + + @Override + public Object invoke(Object... params) { + return new Date(); + } + + @Override + public String name() { + return "date"; + } + +} diff --git a/cms-templates/src/main/java/com/condation/cms/templates/loaders/ClasspathTemplateLoader.java b/cms-templates/src/main/java/com/condation/cms/templates/loaders/ClasspathTemplateLoader.java new file mode 100644 index 000000000..b139c13e2 --- /dev/null +++ b/cms-templates/src/main/java/com/condation/cms/templates/loaders/ClasspathTemplateLoader.java @@ -0,0 +1,63 @@ +package com.condation.cms.templates.loaders; + +/*- + * #%L + * templates + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General + * Public License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.templates.TemplateLoader; +import com.condation.cms.templates.exceptions.TemplateNotFoundException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + +/** + * Lädt Templates aus dem Klassenpfad. z. B. für Ressourcen im Ordner /templates + * innerhalb des JARs. + * + * @author t.marx + */ +public class ClasspathTemplateLoader implements TemplateLoader { + + private final String basePath; + + public ClasspathTemplateLoader(String basePath) { + this.basePath = basePath.endsWith("/") ? basePath : basePath + "/"; + } + + @Override + public String load(String template) { + var resourcePath = basePath + template; + try (var resourceStream = getClass().getClassLoader().getResourceAsStream(resourcePath);) { + if (resourceStream == null) { + throw new TemplateNotFoundException("Template not found in classpath: " + resourcePath); + } + + try (var reader = new BufferedReader(new InputStreamReader(resourceStream, StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.joining("\n")); + } catch (Exception e) { + throw new TemplateNotFoundException("Error loading template from classpath: " + e.getMessage()); + } + } catch (IOException ex) { + throw new TemplateNotFoundException("Error loading template from classpath: " + ex.getMessage()); + } + } +} diff --git a/cms-templates/src/main/java/com/condation/cms/templates/module/CMSModuleTemplateEngine.java b/cms-templates/src/main/java/com/condation/cms/templates/module/CMSModuleTemplateEngine.java index 9f9ac3ee5..8b72ab68d 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/module/CMSModuleTemplateEngine.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/module/CMSModuleTemplateEngine.java @@ -146,7 +146,7 @@ private TemplateComponents createTemplateComponents(RequestContext requestContex return templateComponents; } - + @Override public String renderFromString(String templateString, Model model) throws IOException { var template = templateEngine.getTemplateFromString(templateString); diff --git a/cms-templates/src/main/java/com/condation/cms/templates/parser/Parser.java b/cms-templates/src/main/java/com/condation/cms/templates/parser/Parser.java index 2e71250aa..bdd4dc322 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/parser/Parser.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/parser/Parser.java @@ -22,8 +22,6 @@ * #L% */ -import com.condation.cms.templates.Component; -import com.condation.cms.templates.DynamicConfiguration; import com.condation.cms.templates.lexer.TokenStream; import com.condation.cms.templates.Tag; import com.condation.cms.templates.TemplateConfiguration; @@ -166,7 +164,7 @@ private ASTNode _parse(final TokenStream tokenStream, final ParserConfiguration case IDENTIFIER: { ASTNode currentNode = nodeStack.peek(); if (currentNode instanceof TagNode tagNode1) { - tagNode1.setName(token.value); + tagNode1.setName(token.value != null ? token.value.trim() : token.value); } else if (currentNode instanceof VariableNode variableNode1) { var identifier = token.value; if (TemplateUtils.hasFilters(identifier)) { diff --git a/cms-templates/src/main/java/com/condation/cms/templates/renderer/Renderer.java b/cms-templates/src/main/java/com/condation/cms/templates/renderer/Renderer.java index c44d8290b..96a39129e 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/renderer/Renderer.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/renderer/Renderer.java @@ -21,7 +21,6 @@ * . * #L% */ -import com.condation.cms.api.request.RequestContext; import com.condation.cms.templates.DefaultTemplate; import com.condation.cms.templates.RenderFunction; import com.condation.cms.templates.TemplateConfiguration; diff --git a/cms-templates/src/main/java/com/condation/cms/templates/tags/component/ComponentTag.java b/cms-templates/src/main/java/com/condation/cms/templates/tags/component/ComponentTag.java index 816e66e19..4dc944715 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/tags/component/ComponentTag.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/tags/component/ComponentTag.java @@ -22,7 +22,6 @@ * #L% */ -import com.condation.cms.content.shortcodes.ShortCodes; import com.condation.cms.templates.Component; import com.condation.cms.templates.components.TemplateComponents; import com.condation.cms.templates.exceptions.RenderException; @@ -67,9 +66,9 @@ public void render(ComponentNode node, Renderer.Context context, Writer writer) params.put("_content", content); - var shortCodeResult = components.execute(componentName, params, context.dynamicConfiguration().requestContext()); - if (!Strings.isNullOrEmpty(shortCodeResult)) { - writer.write(shortCodeResult); + var componentResult = components.execute(componentName, params, context.dynamicConfiguration().requestContext()); + if (!Strings.isNullOrEmpty(componentResult)) { + writer.write(componentResult); } } catch (Exception e) { throw new RenderException(e.getMessage(), node.getLine(), node.getColumn()); diff --git a/cms-templates/src/main/java/com/condation/cms/templates/utils/TemplateUtils.java b/cms-templates/src/main/java/com/condation/cms/templates/utils/TemplateUtils.java index 1cbc2f220..e25f8053c 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/utils/TemplateUtils.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/utils/TemplateUtils.java @@ -42,54 +42,85 @@ public class TemplateUtils { public static boolean hasFilters(String expression) { if (expression == null || expression.isBlank()) { - return false; - } + return false; + } - String[] parts = expression.split("\\s+\\|\\s+"); // Nur " | " als Trenner verwenden - return parts.length > 1; + String[] parts = expression.split("\\s+\\|\\s+"); // Nur " | " als Trenner verwenden + return parts.length > 1; } public static List extractFilters(String expression) { List filters = new ArrayList<>(); - if (expression == null || expression.isBlank()) { - return filters; - } + if (expression == null || expression.isBlank()) { + return filters; + } - String[] parts = expression.split("\\s+\\|\\s+"); - if (parts.length < 2) { - return filters; - } + String[] parts = expression.split("\\s+\\|\\s+"); + if (parts.length < 2) { + return filters; + } - for (int i = 1; i < parts.length; i++) { - filters.add(parts[i]); - } + for (int i = 1; i < parts.length; i++) { + filters.add(parts[i]); + } - return filters; + return filters; } public static Filter parseFilter(String filterDefinition) { - // Regular expression to match the filter name and parameters - String regex = "^(\\w+)(?:\\((.*?)\\))?$"; - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(filterDefinition); - - if (matcher.matches()) { - String filterName = matcher.group(1); // Filter name - String paramsString = matcher.group(2); // Optional parameters - - // Split the parameters if present, otherwise return an empty list - List parameters = new ArrayList<>(); - if (paramsString != null && !paramsString.isBlank()) { - for (String param : paramsString.split(",")) { - parameters.add(param.trim()); - } - } - - return new Filter(filterName, parameters); - } else { - throw new IllegalArgumentException("Invalid filter definition: " + filterDefinition); - } - } + // Regex nur für Filtername und Gesamte Parameter + String regex = "^(\\w+)\\s*(?:\\((.*)\\))?$"; + Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); + Matcher matcher = pattern.matcher(filterDefinition.trim()); + + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid filter definition: " + filterDefinition); + } + + String filterName = matcher.group(1); + String paramsString = matcher.group(2); + + List parameters = parseParameters(paramsString); + return new Filter(filterName, parameters); + } + + /** + * Parst eine Parameterliste, unterstützt Kommas innerhalb von Hochkommas. + */ + private static List parseParameters(String paramsString) { + List parameters = new ArrayList<>(); + if (paramsString == null || paramsString.isBlank()) { + return parameters; + } + + StringBuilder current = new StringBuilder(); + boolean inSingleQuotes = false; + boolean inDoubleQuotes = false; + + for (int i = 0; i < paramsString.length(); i++) { + char c = paramsString.charAt(i); + + if (c == '\'' && !inDoubleQuotes) { + inSingleQuotes = !inSingleQuotes; + current.append(c); + } else if (c == '"' && !inSingleQuotes) { + inDoubleQuotes = !inDoubleQuotes; + current.append(c); + } else if (c == ',' && !inSingleQuotes && !inDoubleQuotes) { + // Parameterende + parameters.add(current.toString().trim()); + current.setLength(0); + } else { + current.append(c); + } + } + + if (current.length() > 0) { + parameters.add(current.toString().trim()); + } + + return parameters; + } public static String extractVariableName(String input) { // Split basierend auf "|" diff --git a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java index 7b205476f..ca059c6f1 100644 --- a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java +++ b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java @@ -43,7 +43,7 @@ public class TemplateEngineComponentTest extends AbstractTemplateEngineTest { static DynamicConfiguration dynamicConfiguration; @BeforeAll - public void setupShortCodes() { + public void setupComponents() { components = new TemplateComponents(); components.register(Map.of( "tag1", (params) -> { diff --git a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFilterTest.java b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFilterTest.java index 834da5f42..c298dbdec 100644 --- a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFilterTest.java +++ b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFilterTest.java @@ -38,8 +38,10 @@ public class TemplateEngineFilterTest extends AbstractTemplateEngineTest { @Override public TemplateLoader getLoader() { return new StringTemplateLoader() + .add("var_default", "{{ content | default('the default text') }}") .add("simple", "{{ meta['date'] | date }}") .add("month_year", "{{ meta['date'] | date('MM/yyyy')}}") + .add("format_issue", "{{ meta['date'] | date('MMM d, yyyy')}}") ; } @@ -79,4 +81,47 @@ public void test_month_year() throws IOException { Assertions.assertThat(result).isEqualTo(format.format(date)); } + + @Test + public void test_default_filter() throws IOException { + + Template simpleTemplate = SUT.getTemplate("var_default"); + Assertions.assertThat(simpleTemplate).isNotNull(); + + var result = simpleTemplate.evaluate( + Map.of("content", "<p></p>") + ); + + Assertions.assertThat(result).isEqualTo("the default text"); + + result = simpleTemplate.evaluate( + Map.of("content", "

    ") + ); + + Assertions.assertThat(result).isEqualTo("the default text"); + + result = simpleTemplate.evaluate( + Map.of("content", "") + ); + + Assertions.assertThat(result).isEqualTo("the default text"); + } + + @Test + public void issue_format() throws IOException { + + Template simpleTemplate = SUT.getTemplate("format_issue"); + Assertions.assertThat(simpleTemplate).isNotNull(); + + SimpleDateFormat format = new SimpleDateFormat("MMM d, yyyy"); + var date = new Date(); + + Map context = Map.of("meta", Map.of( + "date", date + )); + + var result = simpleTemplate.evaluate(context); + + Assertions.assertThat(result).isEqualTo(format.format(date)); + } } diff --git a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFunctionsTest.java b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFunctionsTest.java new file mode 100644 index 000000000..34697bfbd --- /dev/null +++ b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFunctionsTest.java @@ -0,0 +1,120 @@ +package com.condation.cms.templates; + +/*- + * #%L + * templates + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.annotations.TemplateFunction; +import com.condation.cms.api.extensions.RegisterTemplateFunctionExtensionPoint; +import com.condation.cms.api.feature.features.HookSystemFeature; +import com.condation.cms.api.feature.features.InjectorFeature; +import com.condation.cms.api.feature.features.ModuleManagerFeature; +import com.condation.cms.api.hooks.HookSystem; +import com.condation.cms.api.model.Parameter; +import com.condation.cms.api.request.RequestContext; +import com.condation.cms.extensions.hooks.TemplateHooks; +import com.condation.cms.templates.components.TemplateComponents; +import com.condation.cms.templates.loaders.StringTemplateLoader; +import com.condation.modules.api.ModuleManager; +import com.google.inject.Injector; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +/** + * + * @author thmar + */ +public class TemplateEngineFunctionsTest extends AbstractTemplateEngineTest { + + @Override + public TemplateLoader getLoader() { + return new StringTemplateLoader() + .add("date", "{{ date() | date('YYYY') }}"); + + } + + @Test + public void test_date() throws IOException { + + var year = new SimpleDateFormat("YYYY").format(new Date()); + + Template simpleTemplate = SUT.getTemplate("date"); + Assertions.assertThat(simpleTemplate).isNotNull(); + Assertions.assertThat(simpleTemplate.evaluate()).isEqualToIgnoringWhitespace(year); + } + + + @Test + void getFunctionsFromDynamicConig () { + var requestContext = new RequestContext(); + requestContext.add(HookSystemFeature.class, new HookSystemFeature(new HookSystem())); + requestContext.add(TemplateHooks.class, new TemplateHooks(requestContext)); + + var injectorMock = Mockito.mock(Injector.class); + requestContext.add(InjectorFeature.class, new InjectorFeature(injectorMock)); + + var moduleManagerMock = Mockito.mock(ModuleManager.class); + requestContext.add(ModuleManagerFeature.class, new ModuleManagerFeature(moduleManagerMock)); + + Mockito.when(injectorMock.getInstance(ModuleManager.class)).thenReturn(moduleManagerMock); + Mockito.when(moduleManagerMock.extensions(RegisterTemplateFunctionExtensionPoint.class)).thenReturn( + List.of(new TestFunctions()) + ); + + DynamicConfiguration dc = new DynamicConfiguration(new TemplateComponents(), requestContext); + + var tfs = dc.templateFunctions(); + + Assertions.assertThat(tfs).isNotNull().hasSize(3); + } + + public static class TestFunctions extends RegisterTemplateFunctionExtensionPoint { + + @Override + public Map> functions() { + return Map.of("testfn1", (params) -> { + return "hi I'm testfn1"; + }); + } + + @Override + public List functionDefinitions() { + return List.of(this); + } + + @TemplateFunction("testfn2") + public Object testfn2 (Parameter params) { + return "hi I'm testfn2"; + } + + @TemplateFunction("testfn3") + public Object testfn3 () { + return "hi I'm testfn3"; + } + } +} diff --git a/cms-templates/src/test/java/com/condation/cms/templates/content/ContentBaseTest.java b/cms-templates/src/test/java/com/condation/cms/templates/content/ContentBaseTest.java index 5a4fe25b6..5b34db8b7 100644 --- a/cms-templates/src/test/java/com/condation/cms/templates/content/ContentBaseTest.java +++ b/cms-templates/src/test/java/com/condation/cms/templates/content/ContentBaseTest.java @@ -22,8 +22,8 @@ * #L% */ -import com.condation.cms.content.shortcodes.ShortCodeParser; -import com.condation.cms.content.shortcodes.TagParser; +import com.condation.cms.content.tags.ShortCodeParser; +import com.condation.cms.content.tags.TagParser; import org.apache.commons.jexl3.JexlBuilder; /** diff --git a/cms-templates/src/test/java/com/condation/cms/templates/content/ShortCodeTemplateFunctionTest.java b/cms-templates/src/test/java/com/condation/cms/templates/content/TagTemplateFunctionTest.java similarity index 73% rename from cms-templates/src/test/java/com/condation/cms/templates/content/ShortCodeTemplateFunctionTest.java rename to cms-templates/src/test/java/com/condation/cms/templates/content/TagTemplateFunctionTest.java index b0822d7a3..63faa3959 100644 --- a/cms-templates/src/test/java/com/condation/cms/templates/content/ShortCodeTemplateFunctionTest.java +++ b/cms-templates/src/test/java/com/condation/cms/templates/content/TagTemplateFunctionTest.java @@ -23,8 +23,8 @@ */ -import com.condation.cms.content.template.functions.shortcode.ShortCodeTemplateFunction; -import com.condation.cms.content.shortcodes.ShortCodes; +import com.condation.cms.content.template.functions.tag.TagTemplateFunction; +import com.condation.cms.content.tags.Tags; import com.condation.cms.templates.CMSTemplateEngine; import com.condation.cms.templates.TemplateEngineFactory; import com.condation.cms.templates.loaders.StringTemplateLoader; @@ -39,9 +39,9 @@ * * @author t.marx */ -public class ShortCodeTemplateFunctionTest extends ContentBaseTest { +public class TagTemplateFunctionTest extends ContentBaseTest { - ShortCodes shortCodes; + Tags tags; static CMSTemplateEngine engine; static StringTemplateLoader templateLoader; @@ -57,22 +57,22 @@ public static void setup() { .create(); } @BeforeEach - public void setupShortCodes() { - shortCodes = new ShortCodes(Map.of( + public void setupTags() { + tags = new Tags(Map.of( "echo", (params) -> "Hello world", "greet", (params) -> "Hello " + params.get("name") ), getTagParser()); } @Test - public void test_call_shortcode() throws Exception { - String templateString = "{{shortCode.call('echo')}}"; + public void test_call_tag() throws Exception { + String templateString = "{{tag.call('echo')}}"; - templateLoader.add("test_call_shortcode", templateString); - var template = engine.getTemplate("test_call_shortcode"); + templateLoader.add("test_call_tag", templateString); + var template = engine.getTemplate("test_call_tag"); Map context = new HashMap<>(); - context.put("shortCode", new ShortCodeTemplateFunction(null, shortCodes)); + context.put("tag", new TagTemplateFunction(null, tags)); var content = template.evaluate(context); @@ -81,14 +81,14 @@ public void test_call_shortcode() throws Exception { @Test public void test_greet() throws Exception { - String templateString = "{{shortCode.call('greet', {'name': 'CondationCMS'})}}"; + String templateString = "{{tag.call('greet', {'name': 'CondationCMS'})}}"; templateLoader.add("test_greet", templateString); var template = engine.getTemplate("test_greet"); Map context = new HashMap<>(); - context.put("shortCode", new ShortCodeTemplateFunction(null, shortCodes)); + context.put("tag", new TagTemplateFunction(null, tags)); var content = template.evaluate(context); diff --git a/cms-templates/src/test/java/com/condation/cms/templates/filter/FilterTest.java b/cms-templates/src/test/java/com/condation/cms/templates/filter/FilterTest.java index e843d1aa1..e2f6474a0 100644 --- a/cms-templates/src/test/java/com/condation/cms/templates/filter/FilterTest.java +++ b/cms-templates/src/test/java/com/condation/cms/templates/filter/FilterTest.java @@ -23,8 +23,10 @@ */ import com.condation.cms.templates.filter.impl.DateFilter; +import com.condation.cms.templates.filter.impl.RawFilter; import java.text.SimpleDateFormat; import java.util.Date; +import org.apache.commons.text.StringEscapeUtils; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -91,4 +93,22 @@ void date_custom_format() { Object result = pipeline.execute(date); Assertions.assertThat(result).isEqualTo(formatted); } + + @Test + void raw() { + + FilterRegistry registry = new FilterRegistry(); + FilterPipeline pipeline = new FilterPipeline(registry); + registry.register(RawFilter.NAME, new RawFilter()); + + pipeline.addStep("raw"); + + String input = """ +

    "We believe that great content is the foundation for successful communication and lasting connections. With Condation, we aim to create the base where ideas can grow and thrive. Our goal is to provide a solid, reliable platform that enables everyone to easily create, manage, and share their content—helping to change the world, one idea at a time."

    + """; + + String escaped = StringEscapeUtils.ESCAPE_HTML4.translate(input); + Object result = pipeline.execute(escaped); + Assertions.assertThat(result).isEqualTo(input); + } } diff --git a/cms-templates/src/test/java/com/condation/cms/templates/loaders/ClasspathTemplateLoaderTest.java b/cms-templates/src/test/java/com/condation/cms/templates/loaders/ClasspathTemplateLoaderTest.java new file mode 100644 index 000000000..5bf908939 --- /dev/null +++ b/cms-templates/src/test/java/com/condation/cms/templates/loaders/ClasspathTemplateLoaderTest.java @@ -0,0 +1,51 @@ +package com.condation.cms.templates.loaders; + +/*- + * #%L + * cms-templates + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * + * @author thmar + */ +public class ClasspathTemplateLoaderTest { + + + @Test + public void testSomeMethod() { + + var templateLoader = new ClasspathTemplateLoader("templates"); + + var templateContent = templateLoader.load("test.html"); + + Assertions.assertThat(removeXmlComments(templateContent)) + .isNotNull() + .isEqualTo("Hello Template!"); + } + + public static String removeXmlComments(String xml) { + return xml.replaceAll("(?s)", "").trim(); + } + +} diff --git a/cms-templates/src/test/java/com/condation/cms/templates/utils/TemplateUtilsTest.java b/cms-templates/src/test/java/com/condation/cms/templates/utils/TemplateUtilsTest.java new file mode 100644 index 000000000..12c83ee8b --- /dev/null +++ b/cms-templates/src/test/java/com/condation/cms/templates/utils/TemplateUtilsTest.java @@ -0,0 +1,50 @@ +package com.condation.cms.templates.utils; + +/*- + * #%L + * cms-templates + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * + * @author thorstenmarx + */ +public class TemplateUtilsTest { + + public TemplateUtilsTest() { + } + + @Test + public void test_filter_issue() { + var filters = TemplateUtils.extractFilters("test | date('MMM d, yyyy')"); + Assertions.assertThat(filters).hasSize(1); + Assertions.assertThat(filters.getFirst()).isEqualTo("date('MMM d, yyyy')"); + + var filter = TemplateUtils.parseFilter(filters.getFirst()); + Assertions.assertThat(filter.name()).isEqualTo("date"); + Assertions.assertThat(filter.parameters()) + .hasSize(1) + .contains("'MMM d, yyyy'"); + } + +} diff --git a/cms-templates/src/test/resources/templates/test.html b/cms-templates/src/test/resources/templates/test.html new file mode 100644 index 000000000..5102b1eaa --- /dev/null +++ b/cms-templates/src/test/resources/templates/test.html @@ -0,0 +1,22 @@ + +Hello Template! diff --git a/cms-test/pom.xml b/cms-test/pom.xml index 3bb9a00c7..2e321d543 100644 --- a/cms-test/pom.xml +++ b/cms-test/pom.xml @@ -4,7 +4,7 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 cms-test jar diff --git a/cms-test/src/main/java/com/condation/cms/test/TestSiteProperties.java b/cms-test/src/main/java/com/condation/cms/test/TestSiteProperties.java index 4d6eddcdc..02f21ca8a 100644 --- a/cms-test/src/main/java/com/condation/cms/test/TestSiteProperties.java +++ b/cms-test/src/main/java/com/condation/cms/test/TestSiteProperties.java @@ -24,6 +24,8 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.SiteProperties; +import com.condation.cms.api.TranslationProperties; +import com.condation.cms.api.UIProperties; import java.util.List; import java.util.Locale; import java.util.Map; @@ -54,6 +56,11 @@ public String markdownEngine() { public String contextPath() { return (String) values.getOrDefault("context_path", "/"); } + + @Override + public String baseUrl() { + return (String) values.getOrDefault("baseurl", ""); + } @Override public String id() { @@ -120,4 +127,16 @@ public List activeModules() { return (List)values.getOrDefault("active.modules", List.of()); } + @Override + public UIProperties ui() { + return new TestUiProperties(); + } + + @Override + public TranslationProperties translation() { + return new TestTranslationProperties(true, List.of(), List.of()); + } + + + } diff --git a/cms-test/src/main/java/com/condation/cms/test/TestTranslationProperties.java b/cms-test/src/main/java/com/condation/cms/test/TestTranslationProperties.java new file mode 100644 index 000000000..5be98015b --- /dev/null +++ b/cms-test/src/main/java/com/condation/cms/test/TestTranslationProperties.java @@ -0,0 +1,59 @@ +package com.condation.cms.test; + +/*- + * #%L + * cms-test + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.TranslationProperties; +import com.condation.cms.api.UIProperties; +import java.util.List; + +/** + * + * @author thorstenmarx + */ +public class TestTranslationProperties implements TranslationProperties { + + private boolean enabled; + private List language; + private List mapping; + + public TestTranslationProperties(boolean enabled, List language, List mapping) { + this.enabled = enabled; + this.language = language; + this.mapping = mapping; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public List getLanguages() { + return language; + } + + @Override + public List getMapping() { + return mapping; + } +} diff --git a/cms-test/src/main/java/com/condation/cms/test/TestUiProperties.java b/cms-test/src/main/java/com/condation/cms/test/TestUiProperties.java new file mode 100644 index 000000000..934c51f3a --- /dev/null +++ b/cms-test/src/main/java/com/condation/cms/test/TestUiProperties.java @@ -0,0 +1,52 @@ +package com.condation.cms.test; + +/*- + * #%L + * cms-test + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.UIProperties; + +/** + * + * @author thorstenmarx + */ +public class TestUiProperties implements UIProperties { + + private boolean force2Fa; + private boolean managerEnabled; + + public TestUiProperties() { + } + + public TestUiProperties( boolean force2Fa, boolean managerEnabled) { + this.force2Fa = force2Fa; + this.managerEnabled = managerEnabled; + } + @Override + public boolean force2fa() { + return force2Fa; + } + + @Override + public boolean managerEnabled() { + return managerEnabled; + } +} diff --git a/distribution/Dockerfile b/distribution/Dockerfile new file mode 100644 index 000000000..8567e2170 --- /dev/null +++ b/distribution/Dockerfile @@ -0,0 +1,7 @@ +FROM eclipse-temurin:21-jre-alpine +WORKDIR /opt/www +COPY ./build condation-server +WORKDIR /opt/www/condation-server +ADD ./docker/server.toml /opt/www/condation-server/ +ADD ./docker/site.toml /opt/www/condation-server/hosts/demo/ +CMD ["java", "-jar", "cms-server-7.8.0.jar", "server", "start"] \ No newline at end of file diff --git a/distribution/build.xml b/distribution/build.xml index 1988a9211..9e9ee24ee 100644 --- a/distribution/build.xml +++ b/distribution/build.xml @@ -21,6 +21,25 @@ + + + + + + + + + + + + + + + + + + + @@ -33,10 +52,99 @@ - - - - - + + build ${system.name} ${system.arch} distribution + + dist/${system.name}_${system.arch} + dist/java/${system.name}_${system.arch} + condation-server-${system.name}-${system.arch}-${cms.version} + + OpenJDK21U-jre_${system.arch}_${system.name}_hotspot_21.0.8_9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + create binary distributions + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/distribution/docker/Caddyfile b/distribution/docker/Caddyfile new file mode 100644 index 000000000..0e76ffc2f --- /dev/null +++ b/distribution/docker/Caddyfile @@ -0,0 +1,3 @@ +http://condation-server { + reverse_proxy condation-server:2020 +} \ No newline at end of file diff --git a/distribution/docker/docker-compose.yml b/distribution/docker/docker-compose.yml new file mode 100644 index 000000000..e865e41bf --- /dev/null +++ b/distribution/docker/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3.9" + +services: + condation-server: + image: condation-server:7.8.0 + container_name: condation-server + volumes: + - ./data:/app/data + expose: + - 2020 + networks: + - web + + caddy: + image: caddy:2 + container_name: caddy + restart: unless-stopped + ports: + - "8080:80" + - "8443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config + depends_on: + - condation-server + networks: + - web + +volumes: + caddy_data: + caddy_config: + +networks: + web: + external: false diff --git a/distribution/docker/server.toml b/distribution/docker/server.toml new file mode 100644 index 000000000..b6e091b6c --- /dev/null +++ b/distribution/docker/server.toml @@ -0,0 +1,26 @@ +env = "dev" + +[server] +port = 2020 +ip = "0.0.0.0" + +[ipc] +port = 6868 +password = "test_pwd" + +[apm] +enabled = false +max_requests = 100 +thread_limit = 10 + +[performance] +pool_enabled = false + +[list] +test = ["eins", "zwei"] + +[map] +test = {"key1"="value2", "key2"="value2"} + +[ui] +secret = "super-secret-token" \ No newline at end of file diff --git a/distribution/docker/site.toml b/distribution/docker/site.toml new file mode 100644 index 000000000..d12415de7 --- /dev/null +++ b/distribution/docker/site.toml @@ -0,0 +1,5 @@ +id = "demo-site" +hostname = [ "condation-server" ] +baseurl = "http://condation-server:2020" +language = "en" +theme = "demo" diff --git a/distribution/scripts/server.bat b/distribution/scripts/server.bat new file mode 100644 index 000000000..95fe61d92 --- /dev/null +++ b/distribution/scripts/server.bat @@ -0,0 +1,10 @@ +@echo off +setlocal +set JAVA=%~dp0java\bin\java.exe +set JAR=%~dp0cms-server-@CMS_VERSION@.jar + +if not defined JAVA_OPTS ( + set JAVA_OPTS=-Xms256m -Xmx512m +) + +"%JAVA%" %JAVA_OPTS% -jar "%JAR%" %* \ No newline at end of file diff --git a/distribution/scripts/server.sh b/distribution/scripts/server.sh new file mode 100644 index 000000000..1c3dbbd93 --- /dev/null +++ b/distribution/scripts/server.sh @@ -0,0 +1,8 @@ +#!/bin/bash +DIR=$(dirname "$0") + +if [ -z "$JAVA_OPTS" ]; then + JAVA_OPTS="-Xms256m -Xmx512m" +fi + +"$DIR/java/bin/java" $JAVA_OPTS -jar "$DIR/cms-server-@CMS_VERSION@.jar" "$@" \ No newline at end of file diff --git a/documentation/manager/README.md b/documentation/manager/README.md new file mode 100644 index 000000000..83db3190f --- /dev/null +++ b/documentation/manager/README.md @@ -0,0 +1,4 @@ +# URLs + +## Manager +http://localhost:1010/module/ui-module/manager/index.html \ No newline at end of file diff --git a/documentation/manager/requests/getContentNode.command.http b/documentation/manager/requests/getContentNode.command.http new file mode 100644 index 000000000..b4be1f8be --- /dev/null +++ b/documentation/manager/requests/getContentNode.command.http @@ -0,0 +1,8 @@ +POST http://localhost:1010/module/ui-module/command + +{ + type: "getContentNode", + parameters: { + "url": "http://localhost:1010/" + } +} \ No newline at end of file diff --git a/documentation/manager/requests/hooks.http b/documentation/manager/requests/hooks.http new file mode 100644 index 000000000..daad9960b --- /dev/null +++ b/documentation/manager/requests/hooks.http @@ -0,0 +1,8 @@ +POST http://localhost:1010/module/ui-module/hooks + +{ + type: "module/ui/menu", + parameters: { + + } +} \ No newline at end of file diff --git a/documentation/manager/requests/islocked.command.http b/documentation/manager/requests/islocked.command.http new file mode 100644 index 000000000..962be44de --- /dev/null +++ b/documentation/manager/requests/islocked.command.http @@ -0,0 +1,9 @@ +POST http://localhost:1010/module/ui-module/command + +{ + type: "islocked", + parameters: { + mode: "edit", + uri: "/" + } +} \ No newline at end of file diff --git a/documentation/manager/requests/test.command.http b/documentation/manager/requests/test.command.http new file mode 100644 index 000000000..18c9ca2de --- /dev/null +++ b/documentation/manager/requests/test.command.http @@ -0,0 +1,8 @@ +POST http://localhost:1010/module/ui-module/command + +{ + type: "test", + parameters: { + + } +} \ No newline at end of file diff --git a/integration-tests/hosts/test/.technical/404.md b/integration-tests/hosts/test/.technical/404.md index 776ccf043..a558079ae 100644 --- a/integration-tests/hosts/test/.technical/404.md +++ b/integration-tests/hosts/test/.technical/404.md @@ -1,5 +1,6 @@ --- title: Leider nichts gefunden template: error.html +published: true --- Da haben wir leider nichts gefunden! diff --git a/integration-tests/hosts/test/content/.technical/404.md b/integration-tests/hosts/test/content/.technical/404.md index a8754bdd8..4c440340f 100644 --- a/integration-tests/hosts/test/content/.technical/404.md +++ b/integration-tests/hosts/test/content/.technical/404.md @@ -1,5 +1,6 @@ --- title: Leider nichts gefunden template: start.ftl +published: true --- Da haben wir leider nichts gefunden! diff --git a/integration-tests/hosts/test/content/.technical/test/example.md b/integration-tests/hosts/test/content/.technical/test/example.md index b06f805a1..3a1c3c460 100644 --- a/integration-tests/hosts/test/content/.technical/test/example.md +++ b/integration-tests/hosts/test/content/.technical/test/example.md @@ -1,5 +1,6 @@ --- title: Das ist der neue Titel +published: true --- Das ist der ganz aktuelle Inhalt \ No newline at end of file diff --git a/integration-tests/hosts/test/content/blog/2023-09/entry.md b/integration-tests/hosts/test/content/blog/2023-09/entry.md index 124daa43b..eb4b9df77 100644 --- a/integration-tests/hosts/test/content/blog/2023-09/entry.md +++ b/integration-tests/hosts/test/content/blog/2023-09/entry.md @@ -1,4 +1,5 @@ --- title: September publish_date: 2023-09-01 +published: true --- diff --git a/integration-tests/hosts/test/content/blog/2023-10/entry.md b/integration-tests/hosts/test/content/blog/2023-10/entry.md index ac054bcd1..d3a50cb20 100644 --- a/integration-tests/hosts/test/content/blog/2023-10/entry.md +++ b/integration-tests/hosts/test/content/blog/2023-10/entry.md @@ -1,4 +1,5 @@ --- title: Oktober publish_date: 2023-10-01 +published: true --- diff --git a/integration-tests/hosts/test/content/index.md b/integration-tests/hosts/test/content/index.md index 1f0aaca21..9a1ee3fcf 100644 --- a/integration-tests/hosts/test/content/index.md +++ b/integration-tests/hosts/test/content/index.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/json/html-node.md b/integration-tests/hosts/test/content/json/html-node.md index 4562df59c..30c958998 100644 --- a/integration-tests/hosts/test/content/json/html-node.md +++ b/integration-tests/hosts/test/content/json/html-node.md @@ -1,6 +1,7 @@ --- title: HTML template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/json/json-node.md b/integration-tests/hosts/test/content/json/json-node.md index dca5cb8e8..728104415 100644 --- a/integration-tests/hosts/test/content/json/json-node.md +++ b/integration-tests/hosts/test/content/json/json-node.md @@ -1,6 +1,7 @@ --- title: JSON template: start.ftl +published: true content: type: "application/json" --- diff --git a/integration-tests/hosts/test/content/nav/folder1/index.md b/integration-tests/hosts/test/content/nav/folder1/index.md index 1f0aaca21..9a1ee3fcf 100644 --- a/integration-tests/hosts/test/content/nav/folder1/index.md +++ b/integration-tests/hosts/test/content/nav/folder1/index.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/nav/folder1/test.md b/integration-tests/hosts/test/content/nav/folder1/test.md index b41f485bb..4771bb0a5 100644 --- a/integration-tests/hosts/test/content/nav/folder1/test.md +++ b/integration-tests/hosts/test/content/nav/folder1/test.md @@ -4,6 +4,7 @@ template: test.ftl tags: [eins,zwei,drei] date: 2023-12-02 datetime: 2023-12-02T13:10:12 +published: true --- Und hier der Inhalt \ No newline at end of file diff --git a/integration-tests/hosts/test/content/nav/index.md b/integration-tests/hosts/test/content/nav/index.md index 1f0aaca21..9a1ee3fcf 100644 --- a/integration-tests/hosts/test/content/nav/index.md +++ b/integration-tests/hosts/test/content/nav/index.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/nav3/folder1/index.md b/integration-tests/hosts/test/content/nav3/folder1/index.md index d8669e724..cda12a13c 100644 --- a/integration-tests/hosts/test/content/nav3/folder1/index.md +++ b/integration-tests/hosts/test/content/nav3/folder1/index.md @@ -1,6 +1,7 @@ --- title: folder1 template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/nav3/index.md b/integration-tests/hosts/test/content/nav3/index.md index 14bcff062..6e6dace51 100644 --- a/integration-tests/hosts/test/content/nav3/index.md +++ b/integration-tests/hosts/test/content/nav3/index.md @@ -1,6 +1,7 @@ --- title: nav3 template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/nodelist/folder1/index.md b/integration-tests/hosts/test/content/nodelist/folder1/index.md index 1f0aaca21..9a1ee3fcf 100644 --- a/integration-tests/hosts/test/content/nodelist/folder1/index.md +++ b/integration-tests/hosts/test/content/nodelist/folder1/index.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/nodelist/folder1/test.md b/integration-tests/hosts/test/content/nodelist/folder1/test.md index b41f485bb..4771bb0a5 100644 --- a/integration-tests/hosts/test/content/nodelist/folder1/test.md +++ b/integration-tests/hosts/test/content/nodelist/folder1/test.md @@ -4,6 +4,7 @@ template: test.ftl tags: [eins,zwei,drei] date: 2023-12-02 datetime: 2023-12-02T13:10:12 +published: true --- Und hier der Inhalt \ No newline at end of file diff --git a/integration-tests/hosts/test/content/nodelist/folder2/index.md b/integration-tests/hosts/test/content/nodelist/folder2/index.md index 1f0aaca21..9a1ee3fcf 100644 --- a/integration-tests/hosts/test/content/nodelist/folder2/index.md +++ b/integration-tests/hosts/test/content/nodelist/folder2/index.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/nodelist/folder2/test.md b/integration-tests/hosts/test/content/nodelist/folder2/test.md index b41f485bb..4771bb0a5 100644 --- a/integration-tests/hosts/test/content/nodelist/folder2/test.md +++ b/integration-tests/hosts/test/content/nodelist/folder2/test.md @@ -4,6 +4,7 @@ template: test.ftl tags: [eins,zwei,drei] date: 2023-12-02 datetime: 2023-12-02T13:10:12 +published: true --- Und hier der Inhalt \ No newline at end of file diff --git a/integration-tests/hosts/test/content/nodelist/index.md b/integration-tests/hosts/test/content/nodelist/index.md index 1f0aaca21..9a1ee3fcf 100644 --- a/integration-tests/hosts/test/content/nodelist/index.md +++ b/integration-tests/hosts/test/content/nodelist/index.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/nodelist2/index.md b/integration-tests/hosts/test/content/nodelist2/index.md index 1f0aaca21..9a1ee3fcf 100644 --- a/integration-tests/hosts/test/content/nodelist2/index.md +++ b/integration-tests/hosts/test/content/nodelist2/index.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/nodelist2/index_1.md b/integration-tests/hosts/test/content/nodelist2/index_1.md index 1f0aaca21..9a1ee3fcf 100644 --- a/integration-tests/hosts/test/content/nodelist2/index_1.md +++ b/integration-tests/hosts/test/content/nodelist2/index_1.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/nodelist2/sub_folder/folder1/index.md b/integration-tests/hosts/test/content/nodelist2/sub_folder/folder1/index.md index 1f0aaca21..9a1ee3fcf 100644 --- a/integration-tests/hosts/test/content/nodelist2/sub_folder/folder1/index.md +++ b/integration-tests/hosts/test/content/nodelist2/sub_folder/folder1/index.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/nodelist2/sub_folder/folder1/test.md b/integration-tests/hosts/test/content/nodelist2/sub_folder/folder1/test.md index b41f485bb..4771bb0a5 100644 --- a/integration-tests/hosts/test/content/nodelist2/sub_folder/folder1/test.md +++ b/integration-tests/hosts/test/content/nodelist2/sub_folder/folder1/test.md @@ -4,6 +4,7 @@ template: test.ftl tags: [eins,zwei,drei] date: 2023-12-02 datetime: 2023-12-02T13:10:12 +published: true --- Und hier der Inhalt \ No newline at end of file diff --git a/integration-tests/hosts/test/content/nodelist2/sub_folder/folder2/index.md b/integration-tests/hosts/test/content/nodelist2/sub_folder/folder2/index.md index 1f0aaca21..9a1ee3fcf 100644 --- a/integration-tests/hosts/test/content/nodelist2/sub_folder/folder2/index.md +++ b/integration-tests/hosts/test/content/nodelist2/sub_folder/folder2/index.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/nodelist2/sub_folder/folder2/test.md b/integration-tests/hosts/test/content/nodelist2/sub_folder/folder2/test.md index b41f485bb..4771bb0a5 100644 --- a/integration-tests/hosts/test/content/nodelist2/sub_folder/folder2/test.md +++ b/integration-tests/hosts/test/content/nodelist2/sub_folder/folder2/test.md @@ -4,6 +4,7 @@ template: test.ftl tags: [eins,zwei,drei] date: 2023-12-02 datetime: 2023-12-02T13:10:12 +published: true --- Und hier der Inhalt \ No newline at end of file diff --git a/integration-tests/hosts/test/content/page.left.02.md b/integration-tests/hosts/test/content/page.left.02.md index 60846e065..1fe264fb3 100644 --- a/integration-tests/hosts/test/content/page.left.02.md +++ b/integration-tests/hosts/test/content/page.left.02.md @@ -1,6 +1,7 @@ --- title: Startseite template: test.ftl +published: true layout: order: 2 --- diff --git a/integration-tests/hosts/test/content/page.left.1.md b/integration-tests/hosts/test/content/page.left.1.md index bce81b8e9..5b10864c2 100644 --- a/integration-tests/hosts/test/content/page.left.1.md +++ b/integration-tests/hosts/test/content/page.left.1.md @@ -1,6 +1,7 @@ --- title: Startseite template: test.ftl +published: true layout: order: 1 --- diff --git a/integration-tests/hosts/test/content/page.left.10.md b/integration-tests/hosts/test/content/page.left.10.md index 137a4489d..7176086d3 100644 --- a/integration-tests/hosts/test/content/page.left.10.md +++ b/integration-tests/hosts/test/content/page.left.10.md @@ -1,6 +1,7 @@ --- title: Startseite template: test.ftl +published: true layout: order: 10 --- diff --git a/integration-tests/hosts/test/content/page.left.md b/integration-tests/hosts/test/content/page.left.md index 193426389..580fc9efe 100644 --- a/integration-tests/hosts/test/content/page.left.md +++ b/integration-tests/hosts/test/content/page.left.md @@ -1,6 +1,7 @@ --- title: Startseite template: test.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/page.md b/integration-tests/hosts/test/content/page.md index 193426389..580fc9efe 100644 --- a/integration-tests/hosts/test/content/page.md +++ b/integration-tests/hosts/test/content/page.md @@ -1,6 +1,7 @@ --- title: Startseite template: test.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/products/index.md b/integration-tests/hosts/test/content/products/index.md index d8ee1828b..cb3d33730 100644 --- a/integration-tests/hosts/test/content/products/index.md +++ b/integration-tests/hosts/test/content/products/index.md @@ -1,6 +1,7 @@ --- title: ProduktSeite template: pages/product.ftl +published: true --- Das ist ein Produkt! \ No newline at end of file diff --git a/integration-tests/hosts/test/content/products/test.md b/integration-tests/hosts/test/content/products/test.md index 7b957f21e..ff0b6a8a2 100644 --- a/integration-tests/hosts/test/content/products/test.md +++ b/integration-tests/hosts/test/content/products/test.md @@ -2,6 +2,7 @@ title: ProduktSeite template: test.ftl tags: [eins,zwei,drei] +published: true --- Das ist ein Produkt! \ No newline at end of file diff --git a/integration-tests/hosts/test/content/query/view.yaml b/integration-tests/hosts/test/content/query/view.yaml index 14afc9707..b0c04e784 100644 --- a/integration-tests/hosts/test/content/query/view.yaml +++ b/integration-tests/hosts/test/content/query/view.yaml @@ -1,5 +1,6 @@ ## YAML Template. --- +published: true template: views/test.html content: query: diff --git a/integration-tests/hosts/test/content/subnav/folder1/folder2/index.md b/integration-tests/hosts/test/content/subnav/folder1/folder2/index.md index ff62d47e8..b2935a78e 100644 --- a/integration-tests/hosts/test/content/subnav/folder1/folder2/index.md +++ b/integration-tests/hosts/test/content/subnav/folder1/folder2/index.md @@ -1,6 +1,7 @@ --- title: folder2 template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/subnav/folder1/index.md b/integration-tests/hosts/test/content/subnav/folder1/index.md index d8669e724..cda12a13c 100644 --- a/integration-tests/hosts/test/content/subnav/folder1/index.md +++ b/integration-tests/hosts/test/content/subnav/folder1/index.md @@ -1,6 +1,7 @@ --- title: folder1 template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/subnav/index.md b/integration-tests/hosts/test/content/subnav/index.md index 14bcff062..6e6dace51 100644 --- a/integration-tests/hosts/test/content/subnav/index.md +++ b/integration-tests/hosts/test/content/subnav/index.md @@ -1,6 +1,7 @@ --- title: nav3 template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/tags.md b/integration-tests/hosts/test/content/tags.md index d0e2105db..ff9e4d05c 100644 --- a/integration-tests/hosts/test/content/tags.md +++ b/integration-tests/hosts/test/content/tags.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true tags: - eins - zwei diff --git a/integration-tests/hosts/test/content/test.md b/integration-tests/hosts/test/content/test.md index 74a8a4043..6e0f7fdc7 100644 --- a/integration-tests/hosts/test/content/test.md +++ b/integration-tests/hosts/test/content/test.md @@ -1,6 +1,7 @@ --- title: StartseiteView template: test.ftl +published: true tags: [eins,zwei,drei] date: 2023-12-02 datetime: 2023-12-02T13:10:12 diff --git a/integration-tests/hosts/test/content/test2.md b/integration-tests/hosts/test/content/test2.md index 5868528f3..52f4ae26e 100644 --- a/integration-tests/hosts/test/content/test2.md +++ b/integration-tests/hosts/test/content/test2.md @@ -1,6 +1,7 @@ --- title: Alias without redirect template: test.ftl +published: true tags: [eins,zwei,drei] date: 2023-12-02 datetime: 2023-12-02T13:10:12 diff --git a/integration-tests/hosts/test/content/view/view.yaml b/integration-tests/hosts/test/content/view/view.yaml index de70837aa..9cfbbda60 100644 --- a/integration-tests/hosts/test/content/view/view.yaml +++ b/integration-tests/hosts/test/content/view/view.yaml @@ -1,5 +1,6 @@ ## YAML Template. --- +published: true template: views/test.html content: nodelist: diff --git a/integration-tests/hosts/test/content/visibility/folder1/index.md b/integration-tests/hosts/test/content/visibility/folder1/index.md index 1f0aaca21..9a1ee3fcf 100644 --- a/integration-tests/hosts/test/content/visibility/folder1/index.md +++ b/integration-tests/hosts/test/content/visibility/folder1/index.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true --- Und hier der Inhalt diff --git a/integration-tests/hosts/test/content/visibility/folder2/index.md b/integration-tests/hosts/test/content/visibility/folder2/index.md index eb1ae57cc..9642a0055 100644 --- a/integration-tests/hosts/test/content/visibility/folder2/index.md +++ b/integration-tests/hosts/test/content/visibility/folder2/index.md @@ -1,6 +1,7 @@ --- title: Startseite template: start.ftl +published: true menu: visible: false --- diff --git a/integration-tests/hosts/test/templates/test.ftl b/integration-tests/hosts/test/templates/test.ftl index 998a31192..0e12e409a 100644 --- a/integration-tests/hosts/test/templates/test.ftl +++ b/integration-tests/hosts/test/templates/test.ftl @@ -1,8 +1,8 @@ - ${meta.title} + ${node.meta.title} - ${content} + ${node.content} \ No newline at end of file diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 0a2b8df6c..cfffcd86e 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -4,7 +4,7 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 integration-tests jar diff --git a/integration-tests/src/test/java/com/condation/cms/SectionsTest.java b/integration-tests/src/test/java/com/condation/cms/SectionsTest.java index 52f534ade..d7beadf66 100644 --- a/integration-tests/src/test/java/com/condation/cms/SectionsTest.java +++ b/integration-tests/src/test/java/com/condation/cms/SectionsTest.java @@ -24,8 +24,6 @@ import com.condation.cms.api.Constants; -import com.condation.cms.api.SiteProperties; -import com.condation.cms.api.cache.CacheManager; import com.condation.cms.api.configuration.Configuration; import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.cms.NIOReadOnlyFile; @@ -35,7 +33,6 @@ import com.condation.cms.content.DefaultContentParser; import com.condation.cms.content.DefaultContentRenderer; import com.condation.cms.content.Section; -import com.condation.cms.core.cache.LocalCacheProvider; import com.condation.cms.core.eventbus.DefaultEventBus; import com.condation.cms.filesystem.FileDB; import com.condation.cms.template.TemplateEngineTest; @@ -62,9 +59,12 @@ public class SectionsTest extends TemplateEngineTest { @BeforeAll public static void beforeClass() throws IOException { var contentParser = new DefaultContentParser(); - var hostBase = Path.of("hosts/test/"); + + var hostBase = Path.of("target/test-" + System.currentTimeMillis()); + TestDirectoryUtils.copyDirectory(Path.of("hosts/test"), hostBase); + var config = new Configuration(); - db = new FileDB(Path.of("hosts/test/"), new DefaultEventBus(), (file) -> { + db = new FileDB(hostBase, new DefaultEventBus(), (file) -> { try { ReadOnlyFile cmsFile = new NIOReadOnlyFile(file, hostBase.resolve(Constants.Folders.CONTENT)); return contentParser.parseMeta(cmsFile); diff --git a/integration-tests/src/test/java/com/condation/cms/TestDirectoryUtils.java b/integration-tests/src/test/java/com/condation/cms/TestDirectoryUtils.java new file mode 100644 index 000000000..bce67f585 --- /dev/null +++ b/integration-tests/src/test/java/com/condation/cms/TestDirectoryUtils.java @@ -0,0 +1,90 @@ +package com.condation.cms; + +/*- + * #%L + * integration-tests + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; + +public class TestDirectoryUtils { + + /** + * Kopiert den Inhalt eines Verzeichnisses rekursiv in ein anderes Verzeichnis. + * + * @param sourceDir Quellverzeichnis (muss existieren) + * @param targetDir Zielverzeichnis (wird erstellt, falls nicht vorhanden) + * @throws IOException wenn beim Kopieren ein Fehler auftritt + */ + public static void copyDirectory(Path sourceDir, Path targetDir) throws IOException { + if (!Files.exists(sourceDir)) { + throw new IllegalArgumentException("Source directory does not exist: " + sourceDir); + } + + if (Files.exists(targetDir)) { + // Optional: löschen, um sicherzugehen, dass keine alten Files stören + deleteDirectory(targetDir); + } + + Files.createDirectories(targetDir); + + Files.walkFileTree(sourceDir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + Path targetPath = targetDir.resolve(sourceDir.relativize(dir)); + Files.createDirectories(targetPath); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Path targetPath = targetDir.resolve(sourceDir.relativize(file)); + Files.copy(file, targetPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + return FileVisitResult.CONTINUE; + } + }); + } + + /** + * Löscht ein Verzeichnis rekursiv. + * + * @param dir Verzeichnis, das gelöscht werden soll + * @throws IOException wenn ein Fehler auftritt + */ + public static void deleteDirectory(Path dir) throws IOException { + if (!Files.exists(dir)) return; + + Files.walkFileTree(dir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.deleteIfExists(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path directory, IOException exc) throws IOException { + Files.deleteIfExists(directory); + return FileVisitResult.CONTINUE; + } + }); + } +} diff --git a/integration-tests/src/test/java/com/condation/cms/TestHelper.java b/integration-tests/src/test/java/com/condation/cms/TestHelper.java index f20c5aeb2..d6b54e947 100644 --- a/integration-tests/src/test/java/com/condation/cms/TestHelper.java +++ b/integration-tests/src/test/java/com/condation/cms/TestHelper.java @@ -42,8 +42,8 @@ import com.condation.cms.api.markdown.MarkdownRenderer; import com.condation.cms.api.request.RequestContext; import com.condation.cms.content.RenderContext; -import com.condation.cms.content.shortcodes.ShortCodes; -import com.condation.cms.content.shortcodes.TagParser; +import com.condation.cms.content.tags.Tags; +import com.condation.cms.content.tags.TagParser; import com.condation.cms.core.configuration.ConfigurationFactory; import com.condation.cms.core.configuration.properties.ExtendedServerProperties; import com.condation.cms.extensions.hooks.DBHooks; @@ -76,11 +76,11 @@ public static RequestContext requestContext(String uri) throws IOException { var markdownRenderer = TestHelper.getRenderer(); RequestContext context = new RequestContext(); - var shortCodeParser = new TagParser(new JexlBuilder().create()); + var tagparser = new TagParser(new JexlBuilder().create()); context.add(RequestFeature.class, new RequestFeature(uri, Map.of())); - context.add(RequestExtensions.class, new RequestExtensions(null)); - context.add(RenderContext.class, new RenderContext(markdownRenderer, new ShortCodes(Map.of(), shortCodeParser), DefaultTheme.EMPTY)); + context.add(RequestExtensions.class, new RequestExtensions(null, null)); + context.add(RenderContext.class, new RenderContext(markdownRenderer, new Tags(Map.of(), tagparser), DefaultTheme.NO_THEME)); context.add(SiteMediaServiceFeature.class, new SiteMediaServiceFeature(new FileMediaService(null))); context.add(InjectorFeature.class, new InjectorFeature(Mockito.mock(Injector.class))); diff --git a/integration-tests/src/test/java/com/condation/cms/TestTemplateEngine.java b/integration-tests/src/test/java/com/condation/cms/TestTemplateEngine.java index 86430c441..1e3683e10 100644 --- a/integration-tests/src/test/java/com/condation/cms/TestTemplateEngine.java +++ b/integration-tests/src/test/java/com/condation/cms/TestTemplateEngine.java @@ -25,6 +25,7 @@ import com.condation.cms.api.template.TemplateEngine; import com.condation.cms.api.theme.Theme; +import com.condation.cms.api.utils.MapUtil; import com.condation.cms.filesystem.FileDB; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -54,7 +55,12 @@ public String render(final String template, Model model) throws IOException { Map values = new HashMap<>(); values.putAll(model.values); - values.put("meta.title", ((Map)model.values.getOrDefault("meta", Map.of())).getOrDefault("title", "")); + values.put("node.meta.title", + MapUtil.getValue(model.values, "node.meta.title", "") + ); + values.put("node.content", + MapUtil.getValue(model.values, "node.content", "") + ); String templateContent = db.getFileSystem().loadContent(db.getFileSystem().resolve("templates").resolve(template), StandardCharsets.UTF_8); diff --git a/integration-tests/src/test/java/com/condation/cms/content/ContentRendererNGTest.java b/integration-tests/src/test/java/com/condation/cms/content/ContentRendererNGTest.java index 1e2c2dd17..6040bfb30 100644 --- a/integration-tests/src/test/java/com/condation/cms/content/ContentRendererNGTest.java +++ b/integration-tests/src/test/java/com/condation/cms/content/ContentRendererNGTest.java @@ -25,10 +25,10 @@ import com.condation.cms.MockModuleManager; +import com.condation.cms.TestDirectoryUtils; import com.condation.cms.TestHelper; import com.condation.cms.TestTemplateEngine; import com.condation.cms.api.Constants; -import com.condation.cms.api.SiteProperties; import com.condation.cms.api.configuration.Configuration; import com.condation.cms.api.db.cms.NIOReadOnlyFile; import com.condation.cms.api.db.cms.ReadOnlyFile; @@ -61,13 +61,17 @@ public class ContentRendererNGTest extends TemplateEngineTest { static ModuleManager moduleManager = new MockModuleManager(); static FileDB db; - static Path hostBase = Path.of("hosts/test/"); + static Path hostBase; @BeforeAll public static void beforeClass () throws IOException { + + hostBase = Path.of("target/test-" + System.currentTimeMillis()); + TestDirectoryUtils.copyDirectory(Path.of("hosts/test"), hostBase); + var contentParser = new DefaultContentParser(); var config = new Configuration(); - db = new FileDB(Path.of("hosts/test/"), new DefaultEventBus(), (file) -> { + db = new FileDB(hostBase, new DefaultEventBus(), (file) -> { try { ReadOnlyFile cmsFile = new NIOReadOnlyFile(file, hostBase.resolve(Constants.Folders.CONTENT)); return contentParser.parseMeta(cmsFile); diff --git a/integration-tests/src/test/java/com/condation/cms/content/ContentResolverTest.java b/integration-tests/src/test/java/com/condation/cms/content/ContentResolverTest.java index 2f0ffd025..d5b80242e 100644 --- a/integration-tests/src/test/java/com/condation/cms/content/ContentResolverTest.java +++ b/integration-tests/src/test/java/com/condation/cms/content/ContentResolverTest.java @@ -23,6 +23,7 @@ */ +import com.condation.cms.TestDirectoryUtils; import com.condation.cms.TestHelper; import com.condation.cms.TestTemplateEngine; import com.condation.cms.api.Constants; @@ -59,9 +60,10 @@ public class ContentResolverTest { @BeforeAll public static void setup() throws IOException { var contentParser = new DefaultContentParser(); - var hostBase = Path.of("hosts/test/"); + var hostBase = Path.of("target/test-" + System.currentTimeMillis()); + TestDirectoryUtils.copyDirectory(Path.of("hosts/test"), hostBase); var config = new Configuration(); - db = new FileDB(Path.of("hosts/test/"), new DefaultEventBus(), (file) -> { + db = new FileDB(hostBase, new DefaultEventBus(), (file) -> { try { ReadOnlyFile cmsFile = new NIOReadOnlyFile(file, hostBase.resolve(Constants.Folders.CONTENT)); return contentParser.parseMeta(cmsFile); diff --git a/integration-tests/src/test/java/com/condation/cms/content/views/ViewParserTest.java b/integration-tests/src/test/java/com/condation/cms/content/views/ViewParserTest.java index 3cc242da9..9010c6c43 100644 --- a/integration-tests/src/test/java/com/condation/cms/content/views/ViewParserTest.java +++ b/integration-tests/src/test/java/com/condation/cms/content/views/ViewParserTest.java @@ -23,6 +23,7 @@ */ +import com.condation.cms.TestDirectoryUtils; import com.condation.cms.TestHelper; import com.condation.cms.api.Constants; import com.condation.cms.api.configuration.Configuration; @@ -36,6 +37,7 @@ import com.condation.cms.content.DefaultContentParser; import com.condation.cms.core.eventbus.DefaultEventBus; import com.condation.cms.filesystem.FileDB; +import com.google.inject.Injector; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Path; @@ -50,6 +52,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; /** * @@ -70,9 +73,13 @@ void setup_test () throws IOException { @BeforeAll static void setup () throws IOException { - var hostBase = Path.of("hosts/test/"); + + var hostBase = Path.of("target/test-" + System.currentTimeMillis()); + TestDirectoryUtils.copyDirectory(Path.of("hosts/test"), hostBase); + var config = new Configuration(); - db = new FileDB(Path.of("hosts/test/"), new DefaultEventBus(), (file) -> { + + db = new FileDB(hostBase, new DefaultEventBus(), (file) -> { try { ReadOnlyFile cmsFile = new NIOReadOnlyFile(file, hostBase.resolve(Constants.Folders.CONTENT)); return parser.parseMeta(cmsFile); diff --git a/integration-tests/src/test/java/com/condation/cms/template/functions/list/NodeListFunctionBuilderNGTest.java b/integration-tests/src/test/java/com/condation/cms/template/functions/list/NodeListFunctionBuilderNGTest.java index 6456a8e64..73ce77473 100644 --- a/integration-tests/src/test/java/com/condation/cms/template/functions/list/NodeListFunctionBuilderNGTest.java +++ b/integration-tests/src/test/java/com/condation/cms/template/functions/list/NodeListFunctionBuilderNGTest.java @@ -24,6 +24,7 @@ +import com.condation.cms.TestDirectoryUtils; import com.condation.cms.TestHelper; import com.condation.cms.api.Constants; import com.condation.cms.api.configuration.Configuration; @@ -57,13 +58,16 @@ public class NodeListFunctionBuilderNGTest { static DefaultContentParser parser = new DefaultContentParser(); static MarkdownRenderer markdownRenderer = TestHelper.getRenderer(); - static Path hostBase = Path.of("hosts/test/"); + static Path hostBase; @BeforeAll static void setup () throws IOException { + hostBase = Path.of("target/test-" + System.currentTimeMillis()); + TestDirectoryUtils.copyDirectory(Path.of("hosts/test"), hostBase); + var config = new Configuration(); - db = new FileDB(Path.of("hosts/test"), new DefaultEventBus(), (file) -> { + db = new FileDB(hostBase, new DefaultEventBus(), (file) -> { try { ReadOnlyFile cmsFile = new NIOReadOnlyFile(file, hostBase.resolve(Constants.Folders.CONTENT)); return parser.parseMeta(cmsFile); diff --git a/integration-tests/src/test/java/com/condation/cms/template/functions/navigation/NavigationFunctionNGTest.java b/integration-tests/src/test/java/com/condation/cms/template/functions/navigation/NavigationFunctionNGTest.java index a15e6c963..68a077960 100644 --- a/integration-tests/src/test/java/com/condation/cms/template/functions/navigation/NavigationFunctionNGTest.java +++ b/integration-tests/src/test/java/com/condation/cms/template/functions/navigation/NavigationFunctionNGTest.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.stream.Collectors; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -55,6 +56,10 @@ public class NavigationFunctionNGTest { static MarkdownRenderer markdownRenderer = TestHelper.getRenderer(); static Path hostBase = Path.of("hosts/test/"); + @AfterAll + static void close () throws Exception { + db.close(); + } @BeforeAll static void init() throws IOException { var contentParser = new DefaultContentParser(); diff --git a/integration-tests/src/test/java/com/condation/cms/template/functions/query/QueryFunctionTest.java b/integration-tests/src/test/java/com/condation/cms/template/functions/query/QueryFunctionTest.java index fd2f7623c..de1876ba6 100644 --- a/integration-tests/src/test/java/com/condation/cms/template/functions/query/QueryFunctionTest.java +++ b/integration-tests/src/test/java/com/condation/cms/template/functions/query/QueryFunctionTest.java @@ -23,6 +23,7 @@ */ +import com.condation.cms.TestDirectoryUtils; import com.condation.cms.TestHelper; import com.condation.cms.api.Constants; import com.condation.cms.api.configuration.Configuration; @@ -37,6 +38,7 @@ import java.io.IOException; import java.nio.file.Path; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -50,12 +52,18 @@ public class QueryFunctionTest { private static FileDB db; static MarkdownRenderer markdownRenderer = TestHelper.getRenderer(); + @AfterAll + static void close () throws Exception { + db.close(); + } @BeforeAll static void init() throws IOException { - var hostBase = Path.of("hosts/test/"); + var hostBase = Path.of("target/test-" + System.nanoTime()); + TestDirectoryUtils.copyDirectory(Path.of("hosts/test"), hostBase); + var contentParser = new DefaultContentParser(); var config = new Configuration(); - db = new FileDB(Path.of("hosts/test"), new DefaultEventBus(), (file) -> { + db = new FileDB(hostBase, new DefaultEventBus(), (file) -> { try { ReadOnlyFile cmsFile = new NIOReadOnlyFile(file, hostBase.resolve(Constants.Folders.CONTENT)); return contentParser.parseMeta(cmsFile); diff --git a/modules/api-module/pom.xml b/modules/api-module/pom.xml index cb401b2b6..c3ffedbca 100644 --- a/modules/api-module/pom.xml +++ b/modules/api-module/pom.xml @@ -4,7 +4,7 @@ com.condation.cms.modules cms-modules - 7.8.0 + 8.0.0 api-module jar diff --git a/modules/api-module/src/main/java/com/condation/cms/modules/system/api/handlers/ApiEndpoints.java b/modules/api-module/src/main/java/com/condation/cms/modules/system/api/handlers/ApiEndpoints.java index 3b80745ec..c6061fc9b 100644 --- a/modules/api-module/src/main/java/com/condation/cms/modules/system/api/handlers/ApiEndpoints.java +++ b/modules/api-module/src/main/java/com/condation/cms/modules/system/api/handlers/ApiEndpoints.java @@ -31,6 +31,7 @@ import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.modules.system.api.services.ContentService; import com.condation.cms.modules.system.api.handlers.v1.NavigationHandler; +import com.condation.cms.modules.system.api.handlers.v1.QueryHandler; import com.condation.modules.api.annotation.Extension; import java.util.HashSet; import java.util.List; @@ -64,6 +65,11 @@ public PathMapping getMapping() { new NavigationHandler(getContext().get(DBFeature.class).db(), getRequestContext()) ); + mapping.add(PathSpec.from("/v1/query"), + "POST", + new QueryHandler(getContext().get(DBFeature.class).db()) + ); + return mapping; } diff --git a/modules/api-module/src/main/java/com/condation/cms/modules/system/api/handlers/v1/QueryHandler.java b/modules/api-module/src/main/java/com/condation/cms/modules/system/api/handlers/v1/QueryHandler.java new file mode 100644 index 000000000..049e63c65 --- /dev/null +++ b/modules/api-module/src/main/java/com/condation/cms/modules/system/api/handlers/v1/QueryHandler.java @@ -0,0 +1,74 @@ +package com.condation.cms.modules.system.api.handlers.v1; + +/*- + * #%L + * api-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.db.DB; +import com.condation.cms.api.extensions.http.HttpHandler; +import static com.condation.cms.core.configuration.GSONProvider.GSON; +import com.condation.cms.modules.system.api.helpers.QueryParser; +import com.google.gson.JsonSyntaxException; +import java.io.IOException; +import java.util.Map; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +public class QueryHandler implements HttpHandler { + + private final DB db; + private final QueryParser queryParser; + + public QueryHandler(final DB db) { + this.db = db; + this.queryParser = new QueryParser(); + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + try { + String body = Content.Source.asString(request); + Object result = queryParser.parse(db, body); + + response.getHeaders().add(HttpHeader.CONTENT_TYPE, "application/json; charset=utf-8"); + Content.Sink.write(response, true, GSON.toJson(result), callback); + + } catch (JsonSyntaxException | IOException e) { + response.setStatus(400); + var respObject = Map.of( + "status", 400, + "error", "Bad Request: Invalid JSON or I/O error." + ); + Content.Sink.write(response, true, GSON.toJson(respObject), callback); + } catch (Exception e) { + response.setStatus(500); + var respObject = Map.of( + "status", 500, + "error", "Internal Server Error: " + e.getMessage() + ); + Content.Sink.write(response, true, GSON.toJson(respObject), callback); + } + return true; + } +} diff --git a/modules/api-module/src/main/java/com/condation/cms/modules/system/api/helpers/NodeHelper.java b/modules/api-module/src/main/java/com/condation/cms/modules/system/api/helpers/NodeHelper.java index ae399e194..00a64545e 100644 --- a/modules/api-module/src/main/java/com/condation/cms/modules/system/api/helpers/NodeHelper.java +++ b/modules/api-module/src/main/java/com/condation/cms/modules/system/api/helpers/NodeHelper.java @@ -70,10 +70,11 @@ public static Map getLinks(String nodeUri, Request request) { } if (requestContext.has(IsPreviewFeature.class)) { + var feature = requestContext.get(IsPreviewFeature.class); if (nodeUri.contains("?")) { - nodeUri += "&preview=true"; + nodeUri += "&preview=" + feature.mode().getValue(); } else { - nodeUri += "?preview=true"; + nodeUri += "?preview=" + feature.mode().getValue(); } } diff --git a/modules/api-module/src/main/java/com/condation/cms/modules/system/api/helpers/QueryParser.java b/modules/api-module/src/main/java/com/condation/cms/modules/system/api/helpers/QueryParser.java new file mode 100644 index 000000000..0d1958001 --- /dev/null +++ b/modules/api-module/src/main/java/com/condation/cms/modules/system/api/helpers/QueryParser.java @@ -0,0 +1,76 @@ +package com.condation.cms.modules.system.api.helpers; + +/*- + * #%L + * api-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.db.ContentNode; +import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.DB; +import static com.condation.cms.core.configuration.GSONProvider.GSON; +import java.util.List; +import java.util.Map; + +public class QueryParser { + + public Object parse(DB db, String jsonBody) { + Map queryMap = GSON.fromJson(jsonBody, Map.class); + ContentQuery query = db.getContent().query((node, a) -> node); + + if (queryMap.containsKey("contentType")) { + query.contentType((String) queryMap.get("contentType")); + } + + if (queryMap.containsKey("where")) { + List> whereClauses = (List>) queryMap.get("where"); + for (Map clause : whereClauses) { + String field = (String) clause.get("field"); + Object value = clause.get("value"); + String operator = (String) clause.getOrDefault("operator", "="); + query.where(field, operator, value); + } + } + + if (queryMap.containsKey("expression")) { + query.expression((String) queryMap.get("expression")); + } + + if (queryMap.containsKey("orderby")) { + Map orderbyMap = (Map) queryMap.get("orderby"); + String field = orderbyMap.get("field"); + String direction = orderbyMap.getOrDefault("direction", "asc"); + if ("desc".equalsIgnoreCase(direction)) { + query.orderby(field).desc(); + } else { + query.orderby(field).asc(); + } + } + + if (queryMap.containsKey("page")) { + Map pageMap = (Map) queryMap.get("page"); + long page = pageMap.get("number").longValue(); + long size = pageMap.get("size").longValue(); + return query.page(page, size); + } else { + return query.get(); + } + } +} diff --git a/modules/api-module/src/test/java/com/condation/cms/modules/system/api/helpers/QueryParserTest.java b/modules/api-module/src/test/java/com/condation/cms/modules/system/api/helpers/QueryParserTest.java new file mode 100644 index 000000000..0792279e1 --- /dev/null +++ b/modules/api-module/src/test/java/com/condation/cms/modules/system/api/helpers/QueryParserTest.java @@ -0,0 +1,104 @@ +package com.condation.cms.modules.system.api.helpers; + +/*- + * #%L + * api-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.db.Content; +import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.DB; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.mockito.ArgumentMatchers.any; + +import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +public class QueryParserTest { + + @Mock + private DB db; + + @Mock + private Content content; + + @Mock + private ContentQuery contentQuery; + + @Mock + private ContentQuery.Sort sort; + + private QueryParser queryParser; + + @BeforeEach + void setUp() { + queryParser = new QueryParser(); + when(db.getContent()).thenReturn(content); + when(content.query(any())).thenReturn(contentQuery); + } + + @Test + void testSimpleQuery() { + String json = "{\"contentType\": \"post\"}"; + queryParser.parse(db, json); + verify(contentQuery).contentType("post"); + verify(contentQuery).get(); + } + + @Test + void testWhereQuery() { + String json = "{\"where\": [{\"field\": \"author\", \"value\": \"John Doe\"}]}"; + queryParser.parse(db, json); + verify(contentQuery).where("author", "=", "John Doe"); + verify(contentQuery).get(); + } + + @Test + void testOrderByQuery() { + String json = "{\"orderby\": {\"field\": \"date\", \"direction\": \"desc\"}}"; + when(contentQuery.orderby("date")).thenReturn(sort); + queryParser.parse(db, json); + verify(sort).desc(); + verify(contentQuery).get(); + } + + @Test + void testPageQuery() { + String json = "{\"page\": {\"number\": 2, \"size\": 10}}"; + queryParser.parse(db, json); + verify(contentQuery).page(2, 10); + } + + @Test + void testComplexQuery() { + String json = "{\"contentType\": \"article\", \"where\": [{\"field\": \"category\", \"value\": \"tech\"}], \"orderby\": {\"field\": \"views\", \"direction\": \"desc\"}}"; + when(contentQuery.orderby("views")).thenReturn(sort); + queryParser.parse(db, json); + verify(contentQuery).contentType("article"); + verify(contentQuery).where("category", "=", "tech"); + verify(sort).desc(); + verify(contentQuery).get(); + } +} diff --git a/modules/api-module/src/test/java/com/condation/cms/modules/system/api/services/ContentServiceTest.java b/modules/api-module/src/test/java/com/condation/cms/modules/system/api/services/ContentServiceTest.java index e8813ac48..0e9827f15 100644 --- a/modules/api-module/src/test/java/com/condation/cms/modules/system/api/services/ContentServiceTest.java +++ b/modules/api-module/src/test/java/com/condation/cms/modules/system/api/services/ContentServiceTest.java @@ -33,6 +33,7 @@ import com.condation.cms.content.DefaultContentParser; import com.condation.cms.core.eventbus.DefaultEventBus; import com.condation.cms.filesystem.FileDB; +import com.google.inject.Injector; import java.nio.file.Path; import java.util.Set; import org.assertj.core.api.Assertions; @@ -103,13 +104,13 @@ public static void shutdown () throws Exception { public void publised_content_is_returned() { var content = contentService.resolve("", request); - Assertions.assertThat(content.isPresent()); + Assertions.assertThat(content).isPresent(); } @Test public void unpublised_content_is_not_returned() { var content = contentService.resolve("sub", request); - Assertions.assertThat(content.isEmpty()); + Assertions.assertThat(content).isEmpty(); } } diff --git a/modules/api-module/src/test/resources/site/content/child.md b/modules/api-module/src/test/resources/site/content/child.md new file mode 100644 index 000000000..62f07d8e6 --- /dev/null +++ b/modules/api-module/src/test/resources/site/content/child.md @@ -0,0 +1,6 @@ +--- +title: the child +published: true +--- + +some content \ No newline at end of file diff --git a/modules/api-module/src/test/resources/site/content/child_draft.md b/modules/api-module/src/test/resources/site/content/child_draft.md new file mode 100644 index 000000000..eab947c52 --- /dev/null +++ b/modules/api-module/src/test/resources/site/content/child_draft.md @@ -0,0 +1,6 @@ +--- +title: the draft child +published: false +--- + +some content \ No newline at end of file diff --git a/modules/api-module/src/test/resources/site/content/index.md b/modules/api-module/src/test/resources/site/content/index.md new file mode 100644 index 000000000..e0c03b88c --- /dev/null +++ b/modules/api-module/src/test/resources/site/content/index.md @@ -0,0 +1,6 @@ +--- +title: the title +published: true +--- + +some content \ No newline at end of file diff --git a/modules/api-module/src/test/resources/site/content/sub/child/another.md b/modules/api-module/src/test/resources/site/content/sub/child/another.md new file mode 100644 index 000000000..408e65b97 --- /dev/null +++ b/modules/api-module/src/test/resources/site/content/sub/child/another.md @@ -0,0 +1,6 @@ +--- +title: another +published: true +--- + +some content \ No newline at end of file diff --git a/modules/api-module/src/test/resources/site/content/sub/child/index.md b/modules/api-module/src/test/resources/site/content/sub/child/index.md new file mode 100644 index 000000000..62f07d8e6 --- /dev/null +++ b/modules/api-module/src/test/resources/site/content/sub/child/index.md @@ -0,0 +1,6 @@ +--- +title: the child +published: true +--- + +some content \ No newline at end of file diff --git a/modules/api-module/src/test/resources/site/content/sub/child_draft.md b/modules/api-module/src/test/resources/site/content/sub/child_draft.md new file mode 100644 index 000000000..eab947c52 --- /dev/null +++ b/modules/api-module/src/test/resources/site/content/sub/child_draft.md @@ -0,0 +1,6 @@ +--- +title: the draft child +published: false +--- + +some content \ No newline at end of file diff --git a/modules/api-module/src/test/resources/site/content/sub/index.md b/modules/api-module/src/test/resources/site/content/sub/index.md new file mode 100644 index 000000000..2aa234a16 --- /dev/null +++ b/modules/api-module/src/test/resources/site/content/sub/index.md @@ -0,0 +1,6 @@ +--- +title: the sub title +published: false +--- + +some content \ No newline at end of file diff --git a/modules/example-module/pom.xml b/modules/example-module/pom.xml index 79b7b9bb1..e856cd39a 100644 --- a/modules/example-module/pom.xml +++ b/modules/example-module/pom.xml @@ -4,7 +4,7 @@ com.condation.cms.modules cms-modules - 7.8.0 + 8.0.0 example-module jar diff --git a/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleJettyHttpRouteExtension.java b/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleJettyHttpRouteExtension.java deleted file mode 100644 index b2de001d1..000000000 --- a/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleJettyHttpRouteExtension.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.condation.cms.modules.example; - -/*- - * #%L - * example-module - * %% - * Copyright (C) 2023 - 2024 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - - -import com.condation.cms.api.extensions.HttpRouteExtensionPoint; -import com.condation.cms.api.feature.features.RequestFeature; -import com.condation.modules.api.annotation.Extension; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Callback; - -/** - * - * @author t.marx - */ -@Extension(HttpRouteExtensionPoint.class) -public class ExampleJettyHttpRouteExtension extends HttpRouteExtensionPoint { - - @Override - public String getRoute() { - return "example/route"; - } - - @Override - public void handle(Request request, Response response, Callback callback) { - - String message = "example route"; - message += "\n"; - if (requestContext != null && requestContext.has(RequestFeature.class)) { - var requestFeature = requestContext.get(RequestFeature.class); - message += "HELlO: " + requestFeature.getQueryParameter("name", "NO-NAME"); - } else { - message += "no request feature"; - } - - response.write(true, ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)), callback); - } -} diff --git a/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleShortcodeExtension.java b/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleTagExtension.java similarity index 71% rename from modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleShortcodeExtension.java rename to modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleTagExtension.java index 2f0f8f394..f89725945 100644 --- a/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleShortcodeExtension.java +++ b/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleTagExtension.java @@ -23,7 +23,7 @@ */ -import com.condation.cms.api.extensions.RegisterShortCodesExtensionPoint; +import com.condation.cms.api.extensions.RegisterTagsExtensionPoint; import com.condation.cms.api.model.Parameter; import com.condation.modules.api.annotation.Extension; import java.util.HashMap; @@ -34,16 +34,16 @@ * * @author t.marx */ -@Extension(RegisterShortCodesExtensionPoint.class) -public class ExampleShortcodeExtension extends RegisterShortCodesExtensionPoint { +@Extension(RegisterTagsExtensionPoint.class) +public class ExampleTagExtension extends RegisterTagsExtensionPoint { @Override - public Map> shortCodes() { - Map> codes = new HashMap<>(); + public Map> tags() { + Map> tags = new HashMap<>(); - codes.put("example", (params) -> "example from module"); + tags.put("example", (params) -> "example from module"); - return codes; + return tags; } diff --git a/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleTemplateModelExtendingExtensionEndPoint.java b/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleTemplateModelExtendingExtensionEndPoint.java index adc50d9b4..562636340 100644 --- a/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleTemplateModelExtendingExtensionEndPoint.java +++ b/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleTemplateModelExtendingExtensionEndPoint.java @@ -39,10 +39,6 @@ @Extension(TemplateModelExtendingExtensionPoint.class) public class ExampleTemplateModelExtendingExtensionEndPoint extends TemplateModelExtendingExtensionPoint { - @Override - public void extendModel(TemplateEngine.Model model) { - } - @Override public Map getModel() { return Map.of("searcher", new Searcher()); diff --git a/modules/pom.xml b/modules/pom.xml index aaf974746..ba0124754 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -4,7 +4,7 @@ com.condation.cms cms-parent - 7.8.0 + 8.0.0 com.condation.cms.modules cms-modules @@ -14,5 +14,6 @@ example-module system-modules api-module + ui-module diff --git a/modules/system-modules/pom.xml b/modules/system-modules/pom.xml index 1fc2b3dac..6dd24cadb 100644 --- a/modules/system-modules/pom.xml +++ b/modules/system-modules/pom.xml @@ -4,7 +4,7 @@ com.condation.cms.modules cms-modules - 7.8.0 + 8.0.0 cms-system-modules jar diff --git a/modules/system-modules/src/main/java/com/condation/cms/modules/system/AuthShortCodes.java b/modules/system-modules/src/main/java/com/condation/cms/modules/system/tags/AuthTags.java similarity index 77% rename from modules/system-modules/src/main/java/com/condation/cms/modules/system/AuthShortCodes.java rename to modules/system-modules/src/main/java/com/condation/cms/modules/system/tags/AuthTags.java index b15a624be..3ebe8399b 100644 --- a/modules/system-modules/src/main/java/com/condation/cms/modules/system/AuthShortCodes.java +++ b/modules/system-modules/src/main/java/com/condation/cms/modules/system/tags/AuthTags.java @@ -1,4 +1,4 @@ -package com.condation.cms.modules.system; +package com.condation.cms.modules.system.tags; /*- * #%L @@ -22,11 +22,11 @@ * #L% */ -import com.condation.cms.api.extensions.RegisterShortCodesExtensionPoint; +import com.condation.cms.api.extensions.RegisterTagsExtensionPoint; import com.condation.cms.api.feature.Feature; import com.condation.cms.api.feature.features.AuthFeature; import com.condation.cms.api.model.Parameter; -import com.condation.cms.api.module.CMSRequestContext; +import com.condation.cms.api.module.SiteRequestContext; import com.condation.modules.api.annotation.Extension; import java.util.Map; import java.util.function.Function; @@ -35,11 +35,11 @@ * * @author thmar */ -@Extension(RegisterShortCodesExtensionPoint.class) -public class AuthShortCodes extends RegisterShortCodesExtensionPoint { +@Extension(RegisterTagsExtensionPoint.class) +public class AuthTags extends RegisterTagsExtensionPoint { @Override - public Map> shortCodes() { + public Map> tags() { return Map.of( "username", this::getUserName, "cms:username", this::getUserName @@ -54,7 +54,7 @@ private String getUserName (Parameter param) { ""); } - private Object getFeatureValueOrDefault(CMSRequestContext context, + private Object getFeatureValueOrDefault(SiteRequestContext context, Class feature, Function valueFunction, Object defaultValue) { if (context.has(feature)) { return valueFunction.apply(context.get(feature)); diff --git a/modules/system-modules/src/main/java/com/condation/cms/modules/system/tags/ImageTags.java b/modules/system-modules/src/main/java/com/condation/cms/modules/system/tags/ImageTags.java new file mode 100644 index 000000000..99f5986c6 --- /dev/null +++ b/modules/system-modules/src/main/java/com/condation/cms/modules/system/tags/ImageTags.java @@ -0,0 +1,76 @@ +package com.condation.cms.modules.system.tags; + +/*- + * #%L + * cms-auth + * %% + * Copyright (C) 2023 - 2024 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.extensions.RegisterTagsExtensionPoint; +import com.condation.cms.api.feature.features.SiteMediaServiceFeature; +import com.condation.cms.api.model.Parameter; +import com.condation.modules.api.annotation.Extension; +import com.google.common.base.Strings; +import java.util.Map; +import java.util.function.Function; +import org.apache.commons.text.StringEscapeUtils; + +/** + * + * @author thmar + */ +@Extension(RegisterTagsExtensionPoint.class) +public class ImageTags extends RegisterTagsExtensionPoint { + + @Override + public Map> tags() { + return Map.of( + "cms:image", this::getImage + ); + } + + private String getImage (Parameter param) { + var imageFile = (String)param.getOrDefault("image", ""); + var format = (String)param.get("format"); + var alt = param.getOrDefault("alt", ""); + + if (imageFile.startsWith("/")) { + imageFile = imageFile.substring(1); + } + + var mediaService = requestContext.get(SiteMediaServiceFeature.class).mediaService(); + + var media = mediaService.get(imageFile); + + var mediaUrl = "/assets/" + media.uri(); + if (!Strings.isNullOrEmpty(format)) { + mediaUrl = "/media/" + media.uri() + "?format=" + format; + } + var altText = (String)media.meta().getOrDefault("alt", alt); + + return "\"%s\"" + .formatted( + mediaUrl, + StringEscapeUtils.ESCAPE_HTML4.translate(altText), + media.meta().getOrDefault("width", -1), + media.meta().getOrDefault("height", -1) + ); + } + +} diff --git a/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/AuthTemplateFunctionExtensions.java b/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/AuthTemplateFunctionExtensions.java new file mode 100644 index 000000000..c0a56bcb3 --- /dev/null +++ b/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/AuthTemplateFunctionExtensions.java @@ -0,0 +1,62 @@ +package com.condation.cms.modules.system.templates; + +/*- + * #%L + * cms-system-modules + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.annotations.TemplateFunction; +import com.condation.cms.api.extensions.RegisterTemplateFunctionExtensionPoint; +import com.condation.cms.api.feature.Feature; +import com.condation.cms.api.feature.features.AuthFeature; +import com.condation.cms.api.module.SiteRequestContext; +import com.condation.modules.api.annotation.Extension; +import java.util.List; +import java.util.function.Function; + +/** + * + * @author thorstenmarx + */ +@Extension(RegisterTemplateFunctionExtensionPoint.class) +public class AuthTemplateFunctionExtensions extends RegisterTemplateFunctionExtensionPoint { + + @Override + public List functionDefinitions() { + return List.of(this); + } + + @TemplateFunction("username") + public Object userName() { + return (String) getFeatureValueOrDefault( + getRequestContext(), + AuthFeature.class, + (feature) -> feature.username(), + ""); + } + + private Object getFeatureValueOrDefault(SiteRequestContext context, + Class feature, Function valueFunction, Object defaultValue) { + if (context.has(feature)) { + return valueFunction.apply(context.get(feature)); + } + return defaultValue; + } +} diff --git a/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/HooksTemplateFunctionExtensions.java b/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/HooksTemplateFunctionExtensions.java new file mode 100644 index 000000000..c001b4790 --- /dev/null +++ b/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/HooksTemplateFunctionExtensions.java @@ -0,0 +1,61 @@ +package com.condation.cms.modules.system.templates; + +/*- + * #%L + * cms-system-modules + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.annotations.TemplateFunction; +import com.condation.cms.api.extensions.RegisterTemplateFunctionExtensionPoint; +import com.condation.cms.api.feature.features.HookSystemFeature; +import com.condation.cms.api.model.Parameter; +import com.condation.modules.api.annotation.Extension; +import com.google.common.base.Strings; +import java.util.List; + +/** + * + * @author thorstenmarx + */ +@Extension(RegisterTemplateFunctionExtensionPoint.class) +public class HooksTemplateFunctionExtensions extends RegisterTemplateFunctionExtensionPoint { + + @Override + public List functionDefinitions() { + return List.of(this); + } + + @TemplateFunction("hooks") + public Object hook(Parameter params){ + + var hook = (String) params.get("hook"); + if (Strings.isNullOrEmpty(hook)) { + return ""; + } + + List results = getRequestContext().get(HookSystemFeature.class).hookSystem().execute(hook).results().stream() + .map(Object::toString) // oder o -> o.getName() + .toList(); + + return String.join("\r\n", results); + } + + +} diff --git a/modules/ui-module/designs/drag-drop.webp b/modules/ui-module/designs/drag-drop.webp new file mode 100644 index 000000000..bd096f329 Binary files /dev/null and b/modules/ui-module/designs/drag-drop.webp differ diff --git a/modules/ui-module/designs/field.asset.css b/modules/ui-module/designs/field.asset.css new file mode 100644 index 000000000..877a28cd7 --- /dev/null +++ b/modules/ui-module/designs/field.asset.css @@ -0,0 +1,8 @@ +.cms-drop-zone { + width: 50px; + height: 50px; +} + +.cms-form-field { + max-width: 400px; +} \ No newline at end of file diff --git a/modules/ui-module/designs/field.asset.html b/modules/ui-module/designs/field.asset.html new file mode 100644 index 000000000..e5cf74ecb --- /dev/null +++ b/modules/ui-module/designs/field.asset.html @@ -0,0 +1,43 @@ + + + + + + + Bootstrap demo + + + + + + + +
    +
    + +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    + + +
    +
    +
    + + + + + \ No newline at end of file diff --git a/modules/ui-module/designs/field.media.html b/modules/ui-module/designs/field.media.html new file mode 100644 index 000000000..6a496a68d --- /dev/null +++ b/modules/ui-module/designs/field.media.html @@ -0,0 +1,104 @@ + + + + + + + Media Field Demo + + + + + + + + +
    +
    + Image preview +
    +
    + + +
    +
    + + + + + + + diff --git a/modules/ui-module/designs/focal/focal.css b/modules/ui-module/designs/focal/focal.css new file mode 100644 index 000000000..13ce9c21c --- /dev/null +++ b/modules/ui-module/designs/focal/focal.css @@ -0,0 +1,137 @@ +/* +*, *:before { + box-sizing: border-box; +} + +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + width: 100vw; +} + +*/ +img { + max-width: 100%; +} +/* +.focal-point { + display: grid; + grid-template-areas: + "preview preview" + "controls controls"; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + grid-gap: 1px; + width: 90vw; + height: 90vh; +} + +@media (orientation: landscape) { + .focal-point { + grid-template-areas: + "preview controls" + "preview controls"; + } +} +*/ + +.focal-point .controls { + grid-area: controls; + display: flex; + justify-content: center; + align-items: flex-start; +} + +@media (orientation: landscape) { + .focal-point .controls { + align-items: center; + } +} + +.focal-point .picker { + position: relative; +} + +.focal-point .picker img { + user-select: none; +} + +.focal-point .controls .dot { + position: absolute; + top: calc(50% - 10px); + left: calc(50% - 10px); + width: 20px; + height: 20px; + background: hsla(0, 100%, 50%, 1); + border-radius: 20px; + border: 2px solid white; + box-shadow: 0 0 4px black; +} + +.focal-point .previews { + grid-area: preview; + display: flex; + justify-content: center; + align-items: flex-end; +} + +@media (orientation: landscape) { + .focal-point .previews { + align-items: center; + } +} + +.focal-point .previews .collage { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; + grid-template-areas: + "preview-2-1 preview-2-1 preview-2-1" + "preview-1-2 preview-1-1 preview-1-1" + "preview-1-2 preview-1-1 preview-1-1"; + grid-gap: 1px; +} + +.focal-point .previews .collage [class*="preview-"] { + position: relative; +} + +.focal-point .preview-2-1 { + grid-area: preview-2-1; +} + +.focal-point .preview-1-2 { + grid-area: preview-1-2; +} + +.focal-point .preview-1-1 { + grid-area: preview-1-1; +} + +.focal-point .previews img { + object-fit: cover; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.aspect-square { + width: 90vw; + height: 45vh; + max-width: 45vh; + max-height: 90vw; +} + +@media (orientation: landscape) { + .aspect-square { + width: 45vw; + height: 90vh; + max-width: 90vh; + max-height: 45vw; + } +} + diff --git a/modules/ui-module/designs/focal/focal.html b/modules/ui-module/designs/focal/focal.html new file mode 100644 index 000000000..4b6d671b5 --- /dev/null +++ b/modules/ui-module/designs/focal/focal.html @@ -0,0 +1,71 @@ + + + + + Focal Point Auswahl + + + + +

    Focal Point auswählen

    + +
    + Testbild + +
    + +
    Klicke auf das Bild, um einen Vokalpunkt zu setzen.
    + + + + + diff --git a/modules/ui-module/designs/focal/focal.js b/modules/ui-module/designs/focal/focal.js new file mode 100644 index 000000000..55a5461ee --- /dev/null +++ b/modules/ui-module/designs/focal/focal.js @@ -0,0 +1,121 @@ +function inPercent(value, total) { + return (value / total) * 100 +} + +function inNumber(percent, total) { + if (percent > 100) { + throw Error('Invalid number, percent can\'t be more than 100') + } + return (percent / 100) * total +} + +/** + * if any of the conditions are true, the position + * is outside the containment. As we want to know + * if the position is inside the container we + * reverse the result of the set of conditions. + */ +const insideContainment = (x, y, containment) => { + return !( + x < (containment.x) || + x > (containment.x + containment.width) || + y < (containment.y) || + y > (containment.y + containment.height) + ) +} + +const makeFocalPoint = (initialPos = { x: 50, y: 50 }) => { + const dot = document.querySelector('.focal-point .dot') + const previews = document.querySelectorAll("[class*='preview-'] img") + let pos = initialPos + let isDown = false; + let img + + const updateImage = () => { + img = document.querySelector('.focal-point .controls img').getBoundingClientRect() + } + + const updatePosition = (e) => { + const x = Math.round(inPercent(e.clientX - img.x, img.width)) + const y = Math.round(inPercent(e.clientY - img.y, img.height)) + + if (x <= 0) { + pos.x = 0 + } else if (x >= 100) { + pos.x = 100 + } else { + pos.x = x + } + + if (y <= 0) { + pos.y = 0 + } else if (y >= 100) { + pos.y = 100 + } else { + pos.y = y + } + } + + const updateDot = () => { + dot.style.left = (inNumber(pos.x, img.width) - (dot.offsetWidth / 2)) + 'px' + dot.style.top = (inNumber(pos.y, img.height) - (dot.offsetHeight / 2)) + 'px' + } + + const updatePreviews = () => { + previews.forEach(preview => { + preview.style.objectPosition = `${pos.x}% ${pos.y}%` + }) + } + + const insideImage = (e) => { + return insideContainment(e.clientX, e.clientY, img) + } + + const handleImageDown = (e) => { + if (!insideImage(e)) { + return + } + + isDown = true + updatePosition(e) + updateDot() + updatePreviews() + } + + const handleImageUp = () => { + isDown = false + } + + const handleDotMove = (e) => { + if (!isDown) { + return + } + + updatePosition(e) + updateDot() + updatePreviews() + } + + const handleWindowResize = () => { + updateImage() + updateDot() + updatePreviews() + } + + const init = (initialCoords) => { + window.addEventListener('resize', handleWindowResize) + document.addEventListener('mousedown', handleImageDown, true) + document.addEventListener('mouseup', handleImageUp, true) + document.addEventListener('mousemove', handleDotMove, true) + + updateImage() + updateDot() + updatePreviews() + } + + init() +} + +document.addEventListener('DOMContentLoaded', () => { + makeFocalPoint() +}) \ No newline at end of file diff --git a/modules/ui-module/designs/list/field.list.css b/modules/ui-module/designs/list/field.list.css new file mode 100644 index 000000000..48411a967 --- /dev/null +++ b/modules/ui-module/designs/list/field.list.css @@ -0,0 +1,7 @@ +/* Optional: damit der Input beim Editieren nicht das Layout sprengt */ +#object-list input.form-control-sm { + max-width: 100%; +} +.cms-form-field { + max-width: 400px; +} \ No newline at end of file diff --git a/modules/ui-module/designs/list/field.list.html b/modules/ui-module/designs/list/field.list.html new file mode 100644 index 000000000..8e50322a8 --- /dev/null +++ b/modules/ui-module/designs/list/field.list.html @@ -0,0 +1,47 @@ + + + + + + + Bootstrap demo + + + + + + +
    + + +
    +
    + Objekt 1 + +
    +
    + Objekt 2 + +
    +
    + + +
    + + + + + + + + + \ No newline at end of file diff --git a/modules/ui-module/designs/list/field.list.js b/modules/ui-module/designs/list/field.list.js new file mode 100644 index 000000000..d1c116fb0 --- /dev/null +++ b/modules/ui-module/designs/list/field.list.js @@ -0,0 +1,43 @@ +// Minimaler JS-Code +const list = document.getElementById('object-list'); +const addBtn = document.getElementById('add-object'); + +// Element hinzufügen +addBtn.addEventListener('click', () => { + const item = document.createElement('div'); + item.className = 'list-group-item d-flex justify-content-between align-items-center'; + item.innerHTML = ` + Neues Objekt + + `; + list.appendChild(item); +}); + +// Entfernen +list.addEventListener('click', e => { + if (e.target.closest('.remove-btn')) { + e.target.closest('.list-group-item').remove(); + } +}); + +// Doppelklick bearbeiten +list.addEventListener('dblclick', e => { + const span = e.target.closest('.object-name'); + if (span) { + const oldText = span.textContent.trim(); + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'form-control form-control-sm'; + input.value = oldText; + span.replaceWith(input); + input.focus(); + input.addEventListener('blur', () => { + const newSpan = document.createElement('span'); + newSpan.className = 'object-name flex-grow-1'; + newSpan.textContent = input.value || oldText; + input.replaceWith(newSpan); + }); + } +}); \ No newline at end of file diff --git a/modules/ui-module/pom.xml b/modules/ui-module/pom.xml new file mode 100644 index 000000000..cad06444d --- /dev/null +++ b/modules/ui-module/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + com.condation.cms.modules + cms-modules + 8.0.0 + + ui-module + jar + + + + com.condation.cms + cms-api + + + com.condation.cms + cms-core + + + com.condation.cms + cms-auth + + + org.apache.tika + tika-core + 3.2.3 + + + com.condation.cms + cms-filesystem + + + com.condation.cms + cms-templates + + + org.simplejavamail + simple-java-mail + 8.12.6 + + + org.projectlombok + lombok + provided + + + \ No newline at end of file diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuHookExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuHookExtension.java new file mode 100644 index 000000000..e13f8c25d --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuHookExtension.java @@ -0,0 +1,77 @@ +package com.condation.cms.modules.ui.extensionpoints; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.hooks.HookSystem; +import com.condation.cms.api.extensions.HookSystemRegisterExtensionPoint; +import com.condation.cms.api.hooks.ActionContext; +import com.condation.cms.api.hooks.FilterContext; +import com.condation.cms.api.ui.action.UIHookAction; +import com.condation.cms.api.ui.action.UIScriptAction; +import com.condation.cms.api.ui.elements.Menu; +import com.condation.cms.api.ui.elements.MenuEntry; +import com.condation.modules.api.annotation.Extension; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * + * @author t.marx + */ +//@Extension(HookSystemRegisterExtensionPoint.class) +public class MenuHookExtension extends HookSystemRegisterExtensionPoint { + + @Override + public void register(HookSystem hookSystem) { + hookSystem.registerFilter("module/ui/menu", (FilterContext context) + -> { + var menu = context.value(); + menu.addMenuEntry(MenuEntry.builder() + .children(new ArrayList<>( + List.of(MenuEntry.builder().id("child1").name("ScriptAction") + .position(0) + .action(new UIScriptAction("/manager/actions/page/edit-content", Map.of("name", "CondationCMS"))) + .build(), + MenuEntry.builder().id("div1").divider(true).position(1).build(), + MenuEntry.builder().id("child2").name("HookAction") + .position(2) + .action(new UIHookAction("module/ui/demo/menu/action", Map.of("name", "CondationCMS"))) + .build() + ))) + .name("ExampleMenu") + .id("example-menu") + .build()); + + return menu; + } + ); + + hookSystem.registerAction("module/ui/demo/menu/action", (ActionContext context) -> { + System.out.println("hook action executed"); + System.out.println("hello " + context.arguments().get("name")); + return ""; + }); + + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java new file mode 100644 index 000000000..b2fcaa109 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java @@ -0,0 +1,143 @@ +package com.condation.cms.modules.ui.extensionpoints; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.extensions.HookSystemRegisterExtensionPoint; +import com.condation.cms.api.ui.annotations.ShortCut; +import com.condation.modules.api.annotation.Extension; +import com.condation.modules.api.annotation.Extensions; +import com.condation.cms.api.ui.extensions.UIActionsExtensionPoint; +import com.condation.cms.api.ui.extensions.UILocalizationExtensionPoint; +import java.util.Map; + +/** + * + * @author t.marx + */ + +@Extensions({ + @Extension(UIActionsExtensionPoint.class), + @Extension(HookSystemRegisterExtensionPoint.class), + @Extension(UILocalizationExtensionPoint.class) +}) +public class PageMenuExtension extends HookSystemRegisterExtensionPoint implements UIActionsExtensionPoint, UILocalizationExtensionPoint { + +// @com.condation.cms.api.ui.annotations.MenuEntry( +// id = "pageMenu", +// name = "Page", +// position = 10 +// ) +// public void parentDefinition() { +// +// } + + /* + @com.condation.cms.api.ui.annotations.MenuEntry( + parent = "pageMenu", + id = "page-create", + name = "Create new page", + position = 1, + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/create-page") + )*/ + @ShortCut( + id = "page-create", + title = "Create new page", + permissions = {Permissions.CONTENT_EDIT}, + hotkey = "ctrl-3", + section = "Page", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/create-page") + ) + public void create_page() { + + } + /* + @com.condation.cms.api.ui.annotations.MenuEntry( + parent = "pageMenu", + id = "page-edit-meta", + name = "Edit MetaData", + position = 3, + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/edit-page-settings") + )*/ + @ShortCut( + id = "page-edit-meta", + title = "Edit page settings", + permissions = {Permissions.CONTENT_EDIT}, + hotkey = "ctrl-2", + section = "Page", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/edit-page-settings") + ) + public void page_settings() {} + /* + @com.condation.cms.api.ui.annotations.MenuEntry( + parent = "pageMenu", + id = "manage-assets", + name = "Manage assets", + position = 10, + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/manage-assets") + ) + */ + @ShortCut( + id = "manager-assets", + title = "Manage assets", + permissions = {Permissions.CONTENT_EDIT}, + hotkey = "ctrl-4", + section = "Assets", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/manage-assets") + ) + public void manage_media() { + + } + + @ShortCut( + id = "page-edit-translations", + title = "Edit page translations", + permissions = {Permissions.CONTENT_EDIT}, + hotkey = "ctrl-5", + section = "Page", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/translations") + ) + public void manage_translations() {} + + + @Override + public Map> getLocalizations() { + return Map.of( + "de", Map.of( + "pageMenu", "Seite", + "page-create", "Neue Seite erstellen", + "page-edit-content", "Inhalt bearbeiten", + "page-edit-meta", "Metadaten bearbeiten", + "language.de", "Deutsch", + "language.en", "Englisch" + ), + "en", Map.of( + "pageMenu", "Page", + "page-create", "Create new page", + "page-edit-content", "Edit content", + "page-edit-meta", "Edit metadata", + "language.de", "German", + "language.en", "English" + ) + ); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/SiteHookExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/SiteHookExtension.java new file mode 100644 index 000000000..aa6b69a1e --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/SiteHookExtension.java @@ -0,0 +1,54 @@ +package com.condation.cms.modules.ui.extensionpoints; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.annotations.Action; +import com.condation.cms.api.eventbus.events.InvalidateContentCacheEvent; +import com.condation.cms.api.eventbus.events.InvalidateMediaCache; +import com.condation.cms.api.eventbus.events.InvalidateTemplateCacheEvent; +import com.condation.cms.api.extensions.HookSystemRegisterExtensionPoint; +import com.condation.cms.api.feature.features.EventBusFeature; +import com.condation.cms.api.hooks.ActionContext; +import com.condation.modules.api.annotation.Extension; + +/** + * + * @author t.marx + */ +@Extension(HookSystemRegisterExtensionPoint.class) +public class SiteHookExtension extends HookSystemRegisterExtensionPoint { + + @Action(value = "ui/manager/tools/media/cache/clear") + public void clear_media_cache(ActionContext context) { + getContext().get(EventBusFeature.class).eventBus().publish(new InvalidateMediaCache(null)); + } + + @Action(value = "ui/manager/tools/template/cache/clear") + public void clear_template_cache(ActionContext context) { + getContext().get(EventBusFeature.class).eventBus().publish(new InvalidateTemplateCacheEvent()); + } + + @Action(value = "ui/manager/tools/content/cache/clear") + public void clear_content_cache(ActionContext context) { + getContext().get(EventBusFeature.class).eventBus().publish(new InvalidateContentCacheEvent()); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/SiteMenuExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/SiteMenuExtension.java new file mode 100644 index 000000000..b3c0a1c0d --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/SiteMenuExtension.java @@ -0,0 +1,144 @@ +package com.condation.cms.modules.ui.extensionpoints; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.extensions.AbstractExtensionPoint; +import com.condation.cms.api.feature.features.InjectorFeature; +import com.condation.cms.api.site.Site; +import com.condation.cms.api.site.SiteService; +import com.condation.cms.api.ui.action.UIScriptAction; +import com.condation.cms.api.ui.annotations.HookAction; +import com.condation.cms.api.ui.annotations.MenuEntry; +import com.condation.cms.api.ui.annotations.ShortCut; +import com.condation.cms.api.ui.elements.Menu; +import com.condation.cms.api.ui.extensions.UIActionsExtensionPoint; +import com.condation.cms.api.utils.HTTPUtil; +import com.condation.modules.api.annotation.Extension; +import com.condation.modules.api.annotation.Extensions; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * + * @author t.marx + */ +@Extensions({ + @Extension(UIActionsExtensionPoint.class),}) +public class SiteMenuExtension extends AbstractExtensionPoint implements UIActionsExtensionPoint { + + @MenuEntry( + id = "toolMenu", + name = "Tools", + position = 10, + permissions = {Permissions.CACHE_INVALIDATE} + ) + public void parentDefinition() { + + } + + @MenuEntry( + parent = "toolMenu", + id = "media-cache-clear", + name = "Clear media cache", + permissions = {Permissions.CACHE_INVALIDATE}, + position = 1, + hookAction = @HookAction(value = "ui/manager/tools/media/cache/clear") + ) + @ShortCut( + id = "media-cache-clear", + title = "Clear media cache", + permissions = {Permissions.CACHE_INVALIDATE}, + section = "tools", + hookAction = @HookAction(value = "ui/manager/tools/media/cache/clear") + ) + public void clear_media_cache() { + } + + @MenuEntry( + parent = "toolMenu", + id = "content-cache-clear", + name = "Clear content cache", + permissions = {Permissions.CACHE_INVALIDATE}, + position = 2, + hookAction = @HookAction(value = "ui/manager/tools/content/cache/clear") + ) + @ShortCut( + id = "content-cache-clear", + title = "Clear content cache", + permissions = {Permissions.CACHE_INVALIDATE}, + section = "tools", + hookAction = @HookAction(value = "ui/manager/tools/content/cache/clear") + ) + public void clear_content_cache() { + } + + @MenuEntry( + parent = "toolMenu", + id = "template-cache-clear", + name = "Clear template cache", + permissions = {Permissions.CACHE_INVALIDATE}, + position = 3, + hookAction = @HookAction(value = "ui/manager/tools/template/cache/clear") + ) + @ShortCut( + id = "template-cache-clear", + title = "Clear template cache", + permissions = {Permissions.CACHE_INVALIDATE}, + section = "tools", + hookAction = @HookAction(value = "ui/manager/tools/template/cache/clear") + ) + public void clear_template_cache() { + } + + + @Override + public void addMenuItems(Menu menu) { + menu.addMenuEntry(com.condation.cms.api.ui.elements.MenuEntry.builder() + .id("site-menu") + .name("Sites") + .position(1) + .permissions(List.of(Permissions.CONTENT_EDIT)) + .children(siteMenus()) + .build()); + } + + private List siteMenus() { + var siteService = getContext().get(InjectorFeature.class).injector().getInstance(SiteService.class); + + var counter = new AtomicInteger(1); + return new ArrayList<>(siteService.sites() + .filter(site -> site.manager()) + .map(site -> { + return com.condation.cms.api.ui.elements.MenuEntry.builder() + .id("site-" + site.id()) + .name(site.id()) + .action(new UIScriptAction( + HTTPUtil.modifyUrl("/manager/actions/site-change", getContext()) + , Map.of("href", site.realUrl() + "manager/index.html"))) + .position(counter.getAndIncrement()) + .build(); + }).toList()); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UIJettyHttpHandlerExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UIJettyHttpHandlerExtension.java new file mode 100644 index 000000000..69a1e7f17 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UIJettyHttpHandlerExtension.java @@ -0,0 +1,224 @@ +package com.condation.cms.modules.ui.extensionpoints; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.Constants; +import com.condation.cms.api.cache.CacheManager; +import com.condation.cms.api.cache.ICache; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.api.extensions.HttpRoutesExtensionPoint; +import com.condation.cms.api.extensions.Mapping; +import com.condation.cms.api.feature.features.CacheManagerFeature; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.feature.features.DBFeature; +import com.condation.cms.api.feature.features.HookSystemFeature; +import com.condation.cms.api.feature.features.ModuleManagerFeature; +import com.condation.modules.api.annotation.Extension; +import com.condation.cms.modules.ui.http.HookHandler; +import com.condation.cms.modules.ui.http.JSActionHandler; +import com.condation.cms.modules.ui.http.RemoteCallHandler; +import com.condation.cms.modules.ui.http.ResourceHandler; +import com.condation.cms.modules.ui.http.UploadHandler; +import com.condation.cms.modules.ui.http.CompositeHttpHandler; +import com.condation.cms.modules.ui.http.PublicResourceHandler; +import com.condation.cms.modules.ui.http.auth.AjaxLoginHandler; +import com.condation.cms.modules.ui.http.auth.CSRFHandler; +import com.condation.cms.modules.ui.http.auth.LoginResourceHandler; +import com.condation.cms.modules.ui.http.auth.LogoutHandler; +import com.condation.cms.modules.ui.http.auth.UIAuthHandler; +import com.condation.cms.modules.ui.http.auth.UIAuthRedirectHandler; +import com.condation.cms.modules.ui.services.RemoteMethodService; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.pathmap.PathSpec; + +/** + * + * @author t.marx + */ +@Extension(HttpRoutesExtensionPoint.class) +@Slf4j +public class UIJettyHttpHandlerExtension extends HttpRoutesExtensionPoint { + + private static FileSystem managerFileSystem = null; + + public static FileSystem createFileSystem(String base) { + // Schneller Pfad: bereits initialisiert + if (managerFileSystem != null && managerFileSystem.isOpen()) { + return managerFileSystem; + } + + synchronized (UIJettyHttpHandlerExtension.class) { + // Doppelte Prüfung, falls zwei Threads gleichzeitig reinlaufen + if (managerFileSystem != null && managerFileSystem.isOpen()) { + return managerFileSystem; + } + + try { + URL resource = UIJettyHttpHandlerExtension.class.getResource(base); + if (resource == null) { + throw new IllegalStateException("Resource '" + base + "' not found"); + } + + String[] array = resource.toURI().toString().split("!"); + URI uri = URI.create(array[0]); + + try { + managerFileSystem = FileSystems.getFileSystem(uri); + } catch (FileSystemNotFoundException e) { + final Map env = new HashMap<>(); + managerFileSystem = FileSystems.newFileSystem(uri, env); + } + + } catch (URISyntaxException | IOException ex) { + log.error("Fehler beim Erstellen des FileSystems", ex); + throw new RuntimeException(ex); + } + + return managerFileSystem; + } + } + + @Override + public Mapping getMapping() { + + Mapping mapping = new Mapping(); + + var siteProperties = getContext().get(ConfigurationFeature.class).configuration().get(SiteConfiguration.class).siteProperties(); + if (!siteProperties.ui().managerEnabled()) { + return mapping; + } + + var hookSystem = getRequestContext().get(HookSystemFeature.class).hookSystem(); + var moduleManager = getContext().get(ModuleManagerFeature.class).moduleManager(); + + ICache failedLoginsCounter = getContext().get(CacheManagerFeature.class).cacheManager().get("loginFails", + new CacheManager.CacheConfig(10_000l, Duration.ofMinutes(1)), + key -> new AtomicInteger(0) + ); + + ICache logins = getContext().get(CacheManagerFeature.class).cacheManager().get("logins", + new CacheManager.CacheConfig(10_000l, Duration.ofMinutes(5)) + ); + + RemoteMethodService remoteCallService = new RemoteMethodService(); + remoteCallService.init(moduleManager); + + try { + + mapping.add(PathSpec.from("/manager/login"), new LoginResourceHandler(getContext(), getRequestContext())); + //mapping.add(PathSpec.from("/manager/login.action"), new LoginHandler(getContext(), getRequestContext(), failedLoginsCounter)); + mapping.add(PathSpec.from("/manager/login.action"), new AjaxLoginHandler(getContext(), getRequestContext(), failedLoginsCounter, logins)); + mapping.add(PathSpec.from("/manager/logout"), new LogoutHandler(getRequestContext())); + + mapping.add(PathSpec.from("/manager/upload"), + new CompositeHttpHandler(List.of( + new UIAuthHandler(getContext(), getRequestContext()), + new CSRFHandler(getContext()), + new UploadHandler( + "/manager/upload", + getContext().get(DBFeature.class).db().getFileSystem().resolve(Constants.Folders.ASSETS)) + ))); + mapping.add(PathSpec.from("/manager/upload2"), + new CompositeHttpHandler(List.of( + new UIAuthHandler(getContext(), getRequestContext()), + new CSRFHandler(getContext()), + new UploadHandler( + "/manager/upload2", + getContext().get(DBFeature.class).db().getFileSystem().resolve(Constants.Folders.ASSETS), + true + ) + ))); + mapping.add(PathSpec.from("/manager/rpc"), + new CompositeHttpHandler(List.of( + new UIAuthHandler(getContext(), getRequestContext()), + new CSRFHandler(getContext()), + new RemoteCallHandler(remoteCallService, getContext()) + ))); + + mapping.add(PathSpec.from("/manager/hooks"), + new CompositeHttpHandler(List.of( + new UIAuthHandler(getContext(), getRequestContext()), + new CSRFHandler(getContext()), + new HookHandler(hookSystem) + ))); + + mapping.add(PathSpec.from("/manager/actions/*"), + new CompositeHttpHandler(List.of( + new UIAuthHandler(getContext(), getRequestContext()), + new JSActionHandler(createFileSystem("/manager/actions"), "/manager/actions", getContext()) + ))); + + mapping.add(PathSpec.from("/manager/bootstrap/*"), + new PublicResourceHandler( + getContext(), + createFileSystem("/manager"), + "/manager", + List.of( + "bootstrap/bootstrap.bundle.min.js", + "bootstrap/bootstrap.bundle.min.js.map", + "bootstrap/bootstrap.min.css", + "bootstrap/bootstrap-superhero.min.css", + "bootstrap/bootstrap-icons.min.css", + "bootstrap/fonts/bootstrap-icons.woff", + "bootstrap/fonts/bootstrap-icons.woff2" + ) + ) + ); + mapping.add(PathSpec.from("/manager/public/*"), + new PublicResourceHandler( + getContext(), + createFileSystem("/manager"), + "/manager", + List.of( + "public/manager-login.js" + ) + ) + ); + + mapping.add(PathSpec.from("/manager/*"), + new CompositeHttpHandler( + List.of( + new UIAuthRedirectHandler(getContext(), getRequestContext()), + new ResourceHandler(createFileSystem("/manager"), "/manager", getContext(), getRequestContext()) + ) + ) + ); + + } catch (Exception ex) { + log.error(null, ex); + } + return mapping; + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UILifecycleExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UILifecycleExtension.java new file mode 100644 index 000000000..ac1e91709 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UILifecycleExtension.java @@ -0,0 +1,59 @@ +package com.condation.cms.modules.ui.extensionpoints; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.feature.features.CacheManagerFeature; +import com.condation.cms.api.feature.features.DBFeature; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.modules.ui.services.LockService; +import com.condation.cms.modules.ui.utils.TemplateEngine; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import lombok.Getter; +import org.simplejavamail.config.ConfigLoader; + +/** + * + * @author t.marx + */ +public class UILifecycleExtension { + + @Getter + private LockService lockService; + @Getter + private TemplateEngine templateEngine; + + private static UILifecycleExtension INSTANCE = null; + + public static UILifecycleExtension getInstance(SiteModuleContext context) { + if (INSTANCE == null) { + INSTANCE = new UILifecycleExtension(context); + } + return INSTANCE; + } + + private UILifecycleExtension(SiteModuleContext context) { + lockService = new LockService(); + templateEngine = new TemplateEngine(context.get(CacheManagerFeature.class).cacheManager()); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtension.java new file mode 100644 index 000000000..042fad952 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtension.java @@ -0,0 +1,139 @@ +package com.condation.cms.modules.ui.extensionpoints; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.extensions.TemplateModelExtendingExtensionPoint; +import com.condation.cms.api.feature.features.IsPreviewFeature; +import com.condation.cms.api.module.SiteRequestContext; +import com.condation.cms.api.template.TemplateEngine; +import com.condation.cms.api.utils.JSONUtil; +import com.condation.modules.api.annotation.Extension; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; + +/** + * + * @author thorstenmarx + */ +@Extension(TemplateModelExtendingExtensionPoint.class) +public class UiTemplateModelExtension extends TemplateModelExtendingExtensionPoint { + + @Override + public Map getModel() { + return Map.of("ui", new UIHelper(getRequestContext())); + } + + @RequiredArgsConstructor + public static class UIHelper { + + private final SiteRequestContext requestContext; + + public String editMeta (String editor, String element) { + return editMeta(editor, element, Collections.emptyMap()); + } + + public String editMeta (String editor, String element, Map options) { + if (!requestContext.has(IsPreviewFeature.class)) { + return ""; + } + return " data-cms-editor='%s' data-cms-editor-options='%s' data-cms-element='meta' data-cms-meta-element='%s' ".formatted( + editor, + JSONUtil.toJson(options), + element); + } + + public String editMeta (String editor, String element, String uri, String toolbar) { + return editMeta(editor, element, uri, toolbar, Collections.emptyMap()); + } + + public String editMeta (String editor, String element, String uri, String toolbar, Map options) { + if (!requestContext.has(IsPreviewFeature.class)) { + return ""; + } + return " data-cms-editor='%s' data-cms-editor-options='%s' data-cms-element='meta' data-cms-meta-element='%s' %s ".formatted( + editor, + JSONUtil.toJson(options), + element, + toolbar(toolbar, uri) + ); + } + + public String toolbar (String id, String type, String[] actions) { + if (!requestContext.has(IsPreviewFeature.class)) { + return ""; + } + Map toolbar = Map.of( + "id", id, + "type", type, + "actions", actions + ); + return " data-cms-toolbar='%s' ".formatted( + JSONUtil.toJson(toolbar) + ); + } + public String toolbar (String id, String type, String[] actions, Map additional) { + if (!requestContext.has(IsPreviewFeature.class)) { + return ""; + } + Map base = Map.of( + "id", id, + "type", type, + "actions", actions + ); + HashMap toolbar = new HashMap<>(additional); + toolbar.putAll(base); + return " data-cms-toolbar='%s' ".formatted( + JSONUtil.toJson(toolbar) + ); + } + public String toolbar (String id, String uri) { + if (!requestContext.has(IsPreviewFeature.class)) { + return ""; + } + Map toolbar = Map.of( + "id", id, + "uri", uri + ); + return " data-cms-toolbar='%s' ".formatted( + JSONUtil.toJson(toolbar) + ); + } + + public String mediaToolbar (String [] actions, Map options) { + if (!requestContext.has(IsPreviewFeature.class)) { + return ""; + } + + Map toolbar = Map.of( + "actions", actions, + "options", options + ); + return " data-cms-media-toolbar='%s' ".formatted( + JSONUtil.toJson(toolbar) + ); + } + + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/AbstractRemoteMethodeExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/AbstractRemoteMethodeExtension.java new file mode 100644 index 000000000..066be7d45 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/AbstractRemoteMethodeExtension.java @@ -0,0 +1,60 @@ +package com.condation.cms.modules.ui.extensionpoints.remotemethods; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.db.DB; +import com.condation.cms.api.feature.features.AuthFeature; +import com.condation.cms.api.feature.features.DBFeature; +import com.condation.cms.api.feature.features.HookSystemFeature; +import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; +import com.condation.cms.core.serivce.ServiceRegistry; +import com.condation.cms.core.serivce.impl.SiteDBService; +import com.condation.cms.modules.ui.utils.UIHooks; +import java.util.Map; + +/** + * + * @author thorstenmarx + */ +public abstract class AbstractRemoteMethodeExtension extends UIRemoteMethodExtensionPoint { + protected String getUserName() { + if (getRequestContext().has(AuthFeature.class)) { + return getRequestContext().get(AuthFeature.class).username(); + } + return ""; + } + + protected UIHooks uiHooks() { + return new UIHooks(getRequestContext().get(HookSystemFeature.class).hookSystem()); + } + + protected DB getDB (Map parameters) { + if (parameters.containsKey("siteId")) { + return ServiceRegistry.getInstance().get( + (String)parameters.get("siteId"), + SiteDBService.class).get().db(); + } else { + return getContext().get(DBFeature.class).db(); + } + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/LocalizationEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/LocalizationEnpoints.java new file mode 100644 index 000000000..197e21b93 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/LocalizationEnpoints.java @@ -0,0 +1,68 @@ +package com.condation.cms.modules.ui.extensionpoints.remotemethods; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.feature.features.HookSystemFeature; +import com.condation.cms.api.feature.features.ModuleManagerFeature; +import com.condation.cms.api.ui.extensions.UILocalizationExtensionPoint; +import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; +import com.condation.cms.modules.ui.utils.TranslationMerger; +import com.condation.modules.api.annotation.Extension; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import com.condation.cms.api.ui.annotations.RemoteMethod; +import com.condation.cms.modules.ui.utils.UIHooks; + +/** + * + * @author thorstenmarx + */ +@Slf4j +@Extension(UIRemoteMethodExtensionPoint.class) +public class LocalizationEnpoints extends UIRemoteMethodExtensionPoint { + + + + @RemoteMethod(name = "i18n.load", permissions = {Permissions.CONTENT_EDIT}) + public Object list(Map parameters) { + + var moduleManager = getContext().get(ModuleManagerFeature.class).moduleManager(); + + Map> localizations = new HashMap<>(); + try { + moduleManager.extensions(UILocalizationExtensionPoint.class).forEach(ext -> { + TranslationMerger.mergeTranslationMaps(ext.getLocalizations(), localizations); + }); + + UIHooks uiHooks = new UIHooks(getRequestContext().get(HookSystemFeature.class).hookSystem()); + + TranslationMerger.mergeTranslationMaps(uiHooks.translations(), localizations); + } catch (Exception e) { + log.error("error loading translation", e); + } + + return localizations; + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java new file mode 100644 index 000000000..275dcc988 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java @@ -0,0 +1,321 @@ +package com.condation.cms.modules.ui.extensionpoints.remotemethods; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.Constants; +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.db.DB; +import com.condation.cms.api.db.cms.ReadOnlyFile; +import com.condation.cms.api.eventbus.events.InvalidateContentCacheEvent; +import com.condation.cms.api.eventbus.events.ReIndexContentMetaDataEvent; +import com.condation.cms.api.feature.features.DBFeature; +import com.condation.cms.api.feature.features.EventBusFeature; +import com.condation.cms.api.feature.features.RequestFeature; +import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; +import com.condation.cms.api.utils.PathUtil; +import com.condation.cms.api.utils.SectionUtil; +import com.condation.cms.content.Section; +import com.condation.cms.core.content.io.ContentFileParser; +import com.condation.cms.core.content.io.YamlHeaderUpdater; +import com.condation.modules.api.annotation.Extension; +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import com.condation.cms.api.ui.annotations.RemoteMethod; +import com.condation.cms.modules.ui.utils.FormHelper; +import com.condation.cms.modules.ui.utils.MetaConverter; +import com.condation.cms.modules.ui.utils.UIFileNameUtil; +import com.condation.cms.modules.ui.utils.UIPathUtil; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * + * @author t.marx + */ +@Slf4j +@Extension(UIRemoteMethodExtensionPoint.class) +public class RemoteContentEndpointsExtension extends UIRemoteMethodExtensionPoint { + + @RemoteMethod(name = "content.get", permissions = {Permissions.CONTENT_EDIT}) + public Object getContent(Map parameters) { + final DB db = getContext().get(DBFeature.class).db(); + var contentBase = db.getReadOnlyFileSystem().resolve(Constants.Folders.CONTENT); + + var uri = (String) parameters.get("uri"); + + var contentFile = contentBase.resolve(uri); + + Map result = new HashMap<>(); + result.put("uri", uri); + if (contentFile != null) { + try { + ContentFileParser parser = new ContentFileParser(contentFile); + result.put("content", parser.getContent()); + result.put("meta", parser.getHeader()); + } catch (IOException ex) { + log.error("", ex); + } + } + + return result; + } + + @RemoteMethod(name = "content.set", permissions = {Permissions.CONTENT_EDIT}) + public Object setContent(Map parameters) { + final DB db = getContext().get(DBFeature.class).db(); + var contentBase = db.getReadOnlyFileSystem().resolve(Constants.Folders.CONTENT); + + var updatedContent = FormHelper.getContent(parameters.get("content")); + var uri = (String) parameters.get("uri"); + + var contentFile = contentBase.resolve(uri); + + Map result = new HashMap<>(); + result.put("uri", uri); + if (contentFile != null) { + try { + ContentFileParser parser = new ContentFileParser(contentFile); + + Map meta = parser.getHeader(); + + var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri); + + YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, meta, updatedContent); + log.debug("file {} saved", uri); + } catch (IOException ex) { + log.error("", ex); + } + } + + return result; + } + + @RemoteMethod(name = "meta.set", permissions = {Permissions.CONTENT_EDIT}) + public Object setMeta(Map parameters) { + final DB db = getContext().get(DBFeature.class).db(); + var contentBase = db.getReadOnlyFileSystem().resolve(Constants.Folders.CONTENT); + + var updateParam = (Map>) parameters.get("meta"); + var update = MetaConverter.convertMeta(updateParam); + var uri = (String) parameters.get("uri"); + + var contentFile = contentBase.resolve(uri); + + Map result = new HashMap<>(); + result.put("uri", uri); + if (contentFile != null) { + try { + ContentFileParser parser = new ContentFileParser(contentFile); + + Map meta = parser.getHeader(); + YamlHeaderUpdater.mergeFlatMapIntoNestedMap(meta, update); + + var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri); + + YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, meta, parser.getContent()); + log.debug("file {} saved", uri); + + getContext().get(EventBusFeature.class).eventBus().publish(new ReIndexContentMetaDataEvent(uri)); + } catch (IOException ex) { + log.error("", ex); + } + } + + return result; + } + + private record Update (String uri, Map> meta) {} + + @RemoteMethod(name = "meta.set.batch", permissions = {Permissions.CONTENT_EDIT}) + public Object setMetaBatch(Map parameters) { + final DB db = getContext().get(DBFeature.class).db(); + var contentBase = db.getReadOnlyFileSystem().resolve(Constants.Folders.CONTENT); + + Map result = new HashMap<>(); + result.put("endpoint", "meta.set.batch"); + + List> updatesParam = (List>) parameters.get("updates"); + + var updates = updatesParam.stream().map(update -> { + return new Update( + (String)update.get("uri"), + (Map>)update.get("meta")); + }).toList(); + + updates.forEach(update -> { + var contentFile = contentBase.resolve(update.uri); + + if (contentFile != null) { + try { + ContentFileParser parser = new ContentFileParser(contentFile); + + Map fileMeta = parser.getHeader(); + var metaUpdated = MetaConverter.convertMeta(update.meta); + YamlHeaderUpdater.mergeFlatMapIntoNestedMap(fileMeta, metaUpdated); + + var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(update.uri); + + YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, fileMeta, parser.getContent()); + log.debug("file {} saved", update.uri); + + getContext().get(EventBusFeature.class).eventBus().publish(new ReIndexContentMetaDataEvent(update.uri)); + } catch (IOException ex) { + log.error("", ex); + } + } + }); + + return result; + } + + @RemoteMethod(name = "content.section.delete", permissions = {Permissions.CONTENT_EDIT}) + public Object deleteSection(Map parameters) { + final DB db = getContext().get(DBFeature.class).db(); + var uri = (String) parameters.get("uri"); + final Path contentBase = db.getFileSystem().resolve(Constants.Folders.CONTENT); + + var contentFile = contentBase.resolve(uri); + + Map result = new HashMap<>(); + result.put("uri", uri); + if (contentFile != null + && PathUtil.isChild(contentBase, contentFile) + && Files.exists(contentFile) + && !Files.isDirectory(contentFile) + ) { + try { + Files.delete(contentFile); + getContext().get(EventBusFeature.class).eventBus().publish(new InvalidateContentCacheEvent()); + } catch (Exception ex) { + result.put("error", true); + log.error("", ex); + } + } + + return result; + } + + @RemoteMethod(name = "content.section.add", permissions = {Permissions.CONTENT_EDIT}) + public Object addSection(Map parameters) { + final DB db = getContext().get(DBFeature.class).db(); + var contentBase = db.getReadOnlyFileSystem().resolve(Constants.Folders.CONTENT); + + var content = (String) parameters.getOrDefault("content", ""); + var parentUri = (String) parameters.get("parentUri"); + var parentSectionName = (String) parameters.get("parentSectionName"); + var sectionItemName = (String) parameters.get("sectionItemName"); + var template = (String) parameters.get("template"); + + var title = sectionItemName; + sectionItemName = UIPathUtil.slugify(sectionItemName); + + var uri = UIFileNameUtil.createSectionFileName(parentUri, parentSectionName, sectionItemName); + + var contentFile = contentBase.resolve(uri); + + Map result = new HashMap<>(); + result.put("uri", uri); + if (contentFile != null) { + try { + Map meta = Map.of( + "template", template, + "title", title, + "layout", Map.of( + "order", 1000) + ); + + var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri); + + YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, meta, content); + log.debug("file {} saved", uri); + + getContext().get(EventBusFeature.class).eventBus().publish(new ReIndexContentMetaDataEvent(uri)); + } catch (IOException ex) { + result.put("error", true); + log.error("", ex); + } + } else { + result.put("error", true); + } + + return result; + } + + @RemoteMethod(name = "content.node", permissions = {Permissions.CONTENT_EDIT}) + public Object getContentNode (Map parameters) { + final DB db = getContext().get(DBFeature.class).db(); + var contentBase = db.getReadOnlyFileSystem().resolve(Constants.Folders.CONTENT); + + var url = (String) parameters.get("url"); + + var path = URI.create(url).getPath(); + + var contextPath = requestContext.get(RequestFeature.class).context(); + if (!"/".equals(contextPath) && path.startsWith(contextPath)) { + path = path.replaceFirst(contextPath, ""); + } + + if (path.startsWith("/")) { + path = path.substring(1); + } + + var contentPath = contentBase.resolve(path); + ReadOnlyFile contentFile = null; + if (contentPath.exists() && contentPath.isDirectory()) { + // use index.md + var tempFile = contentPath.resolve("index.md"); + if (tempFile.exists()) { + contentFile = tempFile; + } + } else { + var temp = contentBase.resolve(path + ".md"); + if (temp.exists()) { + contentFile = temp; + } + } + + Map result = new HashMap<>(); + result.put("url", url); + if (contentFile != null) { + result.put("uri", PathUtil.toRelativeFile(contentFile, contentBase)); + + var sections = db.getContent().listSections(contentFile); + Map> sectionMap = new HashMap<>(); + sections.forEach(section -> { + String uri = section.uri(); + String name = SectionUtil.getSectionName(section.name()); + var index = section.getMetaValue(Constants.MetaFields.LAYOUT_ORDER, Constants.DEFAULT_SECTION_LAYOUT_ORDER); + + sectionMap.computeIfAbsent(name, k -> new ArrayList<>()) + .add(new Section(section.name(), index, "", section.data(), uri)); + }); + result.put("sections", sectionMap); + } + + return result; + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteFileEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteFileEnpoints.java new file mode 100644 index 000000000..87e68e5b4 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteFileEnpoints.java @@ -0,0 +1,351 @@ +package com.condation.cms.modules.ui.extensionpoints.remotemethods; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.Constants; +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.db.DB; +import com.condation.cms.api.db.DBFileSystem; +import com.condation.cms.api.db.cms.ReadOnlyFile; +import com.condation.cms.api.db.cms.ReadyOnlyFileSystem; +import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; +import com.condation.cms.api.utils.FileUtils; +import com.condation.cms.api.utils.SectionUtil; +import com.condation.modules.api.annotation.Extension; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import com.condation.cms.api.ui.annotations.RemoteMethod; +import com.condation.cms.api.ui.rpc.RPCException; +import com.condation.cms.api.utils.PathUtil; +import com.condation.cms.modules.ui.utils.UIPathUtil; +import java.nio.file.Path; + +/** + * + * @author thorstenmarx + */ +@Slf4j +@Extension(UIRemoteMethodExtensionPoint.class) +public class RemoteFileEnpoints extends AbstractRemoteMethodeExtension { + + private static ReadOnlyFile getBase(ReadyOnlyFileSystem fileSystem, String type) { + return switch (type) { + case "content" -> + fileSystem.contentBase(); + case "assets" -> + fileSystem.assetBase(); + default -> + null; + }; + } + + private static Path getWritableBase(DBFileSystem fileSystem, String type) { + return switch (type) { + case "content" -> + fileSystem.resolve(Constants.Folders.CONTENT); + case "assets" -> + fileSystem.resolve(Constants.Folders.ASSETS); + default -> + null; + }; + } + + @RemoteMethod(name = "files.list", permissions = {Permissions.CONTENT_EDIT}) + public Object list(Map parameters) { + final DB db = getDB(parameters); + + var uri = (String) parameters.getOrDefault("uri", ""); + if (uri == null) { + uri = ""; + } + if (uri.startsWith("/")) { + uri = uri.substring(1); + } + var type = (String) parameters.get("type"); + var contentBase = getBase(db.getReadOnlyFileSystem(), type); + + var contentFile = contentBase.resolve(uri); + + Map result = new HashMap<>(); + result.put("uri", uri); + + List files = new ArrayList<>(); + if (contentFile.isDirectory()) { + try { + if (contentFile.hasParent()) { + var parent = contentFile.getParent(); + files.add(new Directory("..", parent.uri())); + } + contentFile.children().stream() + .filter(child -> !SectionUtil.isSection(child.getFileName())) + .map(this::map) + .forEach(files::add); + } catch (IOException ex) { + log.error("", ex); + } + } + files.sort((f1, f2) -> { + if (f1.directory() && !f2.directory()) { + return -1; + } else if (!f1.directory() && f2.directory()) { + return 1; + } else { + return f1.name().compareToIgnoreCase(f2.name()); + } + }); + + result.put("files", files); + + return result; + } + + @RemoteMethod(name = "files.delete", permissions = {Permissions.CONTENT_EDIT}) + public Object delete(Map parameters) throws RPCException { + final DB db = getDB(parameters); + + Map result = new HashMap<>(); + + try { + var uri = (String) parameters.getOrDefault("uri", ""); + var name = (String) parameters.getOrDefault("name", ""); + var type = (String) parameters.get("type"); + var contentBase = getBase(db.getReadOnlyFileSystem(), type); + + var contentFile = contentBase.resolve(uri).resolve(name); + + var writableBase = getWritableBase(db.getFileSystem(), type); + + log.debug("deleting file {}", contentFile.uri()); + if (contentFile.isDirectory()) { + FileUtils.deleteFolder(writableBase.resolve(uri).resolve(name)); + } else if ("assets".equals(type)) { + Files.deleteIfExists(writableBase.resolve(uri).resolve(name)); + } else { + var sections = db.getContent().listSections(contentFile); + Files.deleteIfExists(writableBase.resolve(uri).resolve(name)); + sections.forEach(node -> { + try { + log.debug("deleting section {}", node.uri()); + FileUtils.deleteFolder(writableBase.resolve(node.uri())); + } catch (IOException ioe) { + log.error("error deleting file {}", node.uri(), ioe); + } + }); + } + } catch (Exception e) { + log.error("", e); + throw new RPCException(0, e.getMessage()); + } + + return result; + } + + @RemoteMethod(name = "files.rename", permissions = {Permissions.CONTENT_EDIT}) + public Object renameFile(Map parameters) throws RPCException { + final DB db = getDB(parameters); + Map result = new HashMap<>(); + + try { + var uri = (String) parameters.getOrDefault("uri", ""); + var name = (String) parameters.getOrDefault("name", ""); + var newName = (String) parameters.get("newName"); + var type = (String) parameters.get("type"); + + if (newName == null || newName.isBlank()) { + throw new IllegalArgumentException("newName must not be null or blank"); + } + + var contentBase = getBase(db.getReadOnlyFileSystem(), type); + + // check if both paths are in host directory + contentBase.resolve(uri).resolve(name); + contentBase.resolve(uri).resolve(newName); + + var writableBase = getWritableBase(db.getFileSystem(), type); + + var sourcePath = writableBase.resolve(uri).resolve(name); + var targetPath = writableBase.resolve(uri).resolve(newName); + + log.debug("renaming from {} to {}", sourcePath, targetPath); + + if (!Files.exists(sourcePath)) { + throw new RPCException("Source file not found: " + sourcePath); + } + if (Files.exists(targetPath)) { + throw new RPCException("Target file already exists: " + targetPath); + } + + Files.move(sourcePath, targetPath); + + if (!"assets".equals(type) && !Files.isDirectory(targetPath)) { + var contentFile = contentBase.resolve(uri).resolve(name); + var sections = db.getContent().listSections(contentFile); + + for (var node : sections) { + var sourceSectionPath = writableBase.resolve(node.uri()); + var targetSectionPath = writableBase.resolve(node.uri().replace(name, newName)); + if (Files.exists(sourceSectionPath)) { + log.debug("renaming section {} to {}", sourceSectionPath, targetSectionPath); + Files.move(sourceSectionPath, targetSectionPath); + } + } + } + + result.put("success", true); + result.put("newName", newName); + + } catch (Exception e) { + log.error("Error during rename", e); + throw new RPCException(0, e.getMessage()); + } + + return result; + } + + @RemoteMethod(name = "folders.create", permissions = {Permissions.CONTENT_EDIT}) + public Object createFolder(Map parameters) throws RPCException { + final DB db = getDB(parameters); + + Map result = new HashMap<>(); + + try { + var name = (String) parameters.getOrDefault("name", ""); + var uri = (String) parameters.getOrDefault("uri", ""); + var type = (String) parameters.get("type"); + var contentBase = getWritableBase(db.getFileSystem(), type); + + name = UIPathUtil.slugify(name); + + Path newFile = contentBase.resolve(uri).resolve(name); + if (newFile.isAbsolute()) { + throw new RPCException(1, "absolut path is not supported"); + } else if (Files.exists(newFile)) { + throw new RPCException(1, "directory already exists"); + } else if (!PathUtil.isChild(contentBase, newFile)) { + throw new RPCException(1, "invalid path"); + } + Files.createDirectories(newFile); + } catch (Exception e) { + log.error("", e); + throw new RPCException(0, e.getMessage()); + } + + return result; + } + + @RemoteMethod(name = "files.create", permissions = {Permissions.CONTENT_EDIT}) + public Object createFile(Map parameters) throws RPCException { + final DB db = getDB(parameters); + + Map result = new HashMap<>(); + + try { + var uri = (String) parameters.getOrDefault("uri", ""); + var name = (String) parameters.getOrDefault("name", ""); + var type = (String) parameters.get("type"); + var contentBase = getWritableBase(db.getFileSystem(), type); + + name = UIPathUtil.slugify(name); + + Path newFile = contentBase.resolve(uri).resolve(name); + if (newFile.isAbsolute()) { + throw new RPCException(1, "absolut path is not supported"); + } else if (Files.exists(newFile)) { + throw new RPCException(1, "file already exists"); + } else if (!PathUtil.isChild(contentBase, newFile)) { + throw new RPCException(1, "invalid path"); + } + Files.createDirectories(newFile.getParent()); + Files.createFile(newFile); + } catch (Exception e) { + log.error("", e); + throw new RPCException(0, e.getMessage()); + } + + return result; + } + + private boolean isMedia (String filename) { + var name = filename.toLowerCase(); + return name.endsWith(".jpg") + || name.endsWith(".jpeg") + || name.endsWith(".webp") + || name.endsWith(".png") + || name.endsWith(".svg") + || name.endsWith(".gif"); + } + + private File map (ReadOnlyFile readOnlyFile) { + if (readOnlyFile.isDirectory()) { + return new Directory( + readOnlyFile.getFileName(), + readOnlyFile.uri() + ); + } else if (isMedia(readOnlyFile.getFileName())) { + return new Media( + readOnlyFile.getFileName(), + readOnlyFile.uri() + ); + } else { + return new Content( + readOnlyFile.getFileName(), + readOnlyFile.uri() + ); + } + } + + public record Content(String name, String uri) implements File { + } + + public record Media(String name, String uri) implements File { + @Override + public boolean media() { + return true; + } + } + + public record Directory (String name, String uri) implements File { + @Override + public boolean directory() { + return true; + } + } + + public static interface File { + String name (); + String uri (); + default boolean directory () { + return false; + } + default boolean content () { + return name().endsWith(".md"); + } + default boolean media () { + return false; + } + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteManagerEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteManagerEnpoints.java new file mode 100644 index 000000000..0df95eb74 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteManagerEnpoints.java @@ -0,0 +1,95 @@ +package com.condation.cms.modules.ui.extensionpoints.remotemethods; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.configuration.configs.MediaConfiguration; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; +import com.condation.modules.api.annotation.Extension; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import com.condation.cms.api.ui.annotations.RemoteMethod; +import com.condation.cms.api.ui.rpc.RPCException; +import com.condation.cms.content.RenderContext; + +/** + * + * @author thorstenmarx + */ +@Slf4j +@Extension(UIRemoteMethodExtensionPoint.class) +public class RemoteManagerEnpoints extends AbstractRemoteMethodeExtension { + + @RemoteMethod(name = "manager.content.tags", permissions = {Permissions.CONTENT_EDIT}) + public Object getShortCodeNames (Map parameters) throws RPCException { + return getRequestContext().get(RenderContext.class).tags().getTagNames(); + } + + @RemoteMethod(name = "manager.media.form", permissions = {Permissions.CONTENT_EDIT}) + public Object getMediaForm(Map parameters) throws RPCException { + try { + var form = (String) parameters.getOrDefault("form", ""); + return uiHooks().mediaForms().getMetaForms().get(form); + } catch (Exception e) { + log.error("", e); + throw new RPCException(0, e.getMessage()); + } + } + + @RemoteMethod(name = "manager.media.formats", permissions = {Permissions.CONTENT_EDIT}) + public Object getMediaFormats (Map parameters) throws RPCException { + var configuration = getContext().get(ConfigurationFeature.class).configuration(); + return configuration.get(MediaConfiguration.class).getFormats(); + } + + @RemoteMethod(name = "manager.contentTypes.sections", permissions = {Permissions.CONTENT_EDIT}) + public Object getSectionTemplates(Map parameters) throws RPCException { + try { + var section = (String) parameters.getOrDefault("section", ""); + return uiHooks().contentTypes().getSectionTemplates(section); + } catch (Exception e) { + log.error("", e); + throw new RPCException(0, e.getMessage()); + } + } + + @RemoteMethod(name = "manager.contentTypes.pages", permissions = {Permissions.CONTENT_EDIT}) + public Object getPageTemplates(Map parameters) throws RPCException { + try { + return uiHooks().contentTypes().getPageTemplates(); + } catch (Exception e) { + log.error("", e); + throw new RPCException(0, e.getMessage()); + } + } + + @RemoteMethod(name = "manager.contentTypes.listItemTypes", permissions = {Permissions.CONTENT_EDIT}) + public Object getListItemTypes(Map parameters) throws RPCException { + try { + return uiHooks().contentTypes().getListItemTypes(); + } catch (Exception e) { + log.error("", e); + throw new RPCException(0, e.getMessage()); + } + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteMediaEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteMediaEnpoints.java new file mode 100644 index 000000000..ca306d4b5 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteMediaEnpoints.java @@ -0,0 +1,101 @@ +package com.condation.cms.modules.ui.extensionpoints.remotemethods; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.Constants; +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.eventbus.events.InvalidateMediaCache; +import com.condation.cms.api.feature.features.DBFeature; +import com.condation.cms.api.feature.features.EventBusFeature; +import com.condation.cms.api.feature.features.SiteMediaServiceFeature; +import com.condation.cms.api.feature.features.SitePropertiesFeature; +import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; +import com.condation.modules.api.annotation.Extension; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import com.condation.cms.api.ui.annotations.RemoteMethod; +import com.condation.cms.api.ui.rpc.RPCException; +import com.condation.cms.api.utils.ImageUtil; +import com.condation.cms.modules.ui.utils.MetaConverter; +import com.condation.cms.core.content.io.YamlHeaderUpdater; +import java.net.URI; +import java.util.HashMap; + +/** + * + * @author thorstenmarx + */ +@Slf4j +@Extension(UIRemoteMethodExtensionPoint.class) +public class RemoteMediaEnpoints extends UIRemoteMethodExtensionPoint { + + @RemoteMethod(name = "media.meta.get", permissions = {Permissions.CONTENT_EDIT}) + public Object getMediaMeta(Map parameters) throws RPCException { + try { + var image = (String) parameters.getOrDefault("image", ""); + + var imagePath = getMediaPath(image); + + return getRequestContext().get(SiteMediaServiceFeature.class).mediaService().get(imagePath); + } catch (Exception e) { + log.error("", e); + throw new RPCException(0, e.getMessage()); + } + } + + @RemoteMethod(name = "media.meta.set", permissions = {Permissions.CONTENT_EDIT}) + public Object setMediaMeta(Map parameters) throws RPCException { + try { + var data = (Map>) parameters.getOrDefault("meta", Map.of()); + var image = (String) parameters.getOrDefault("image", ""); + + var imagePath = getMediaPath(image); + var media = getRequestContext().get(SiteMediaServiceFeature.class).mediaService().get(imagePath); + + var metaData = new HashMap(media.meta()); + YamlHeaderUpdater.mergeFlatMapIntoNestedMap(metaData, MetaConverter.convertMeta(data)); + + var fs = getContext().get(DBFeature.class).db().getFileSystem(); + var assets = fs.resolve(Constants.Folders.ASSETS); + var metaFile = assets.resolve(imagePath + ".meta.yaml"); + + YamlHeaderUpdater.saveMetaData(metaFile, metaData); + + getContext().get(EventBusFeature.class).eventBus().publish(new InvalidateMediaCache(assets.resolve(imagePath))); + + return Map.of( + "status", "success" + ); + } catch (Exception e) { + log.error("", e); + throw new RPCException(0, e.getMessage()); + } + } + + private String getMediaPath (String image) { + var contextPath = getContext().get(SitePropertiesFeature.class).siteProperties().contextPath(); + var baseUrl = getContext().get(SitePropertiesFeature.class).siteProperties().getOrDefault("baseurl", "-----"); + URI uri = URI.create(image); + var path = uri.getPath(); + return ImageUtil.getRawPath(path, requestContext); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemotePageEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemotePageEnpoints.java new file mode 100644 index 000000000..be3316a42 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemotePageEnpoints.java @@ -0,0 +1,147 @@ +package com.condation.cms.modules.ui.extensionpoints.remotemethods; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.Constants; +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.db.DB; +import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; +import com.condation.cms.api.utils.FileUtils; +import com.condation.modules.api.annotation.Extension; +import java.io.IOException; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import com.condation.cms.api.ui.annotations.RemoteMethod; +import com.condation.cms.api.ui.rpc.RPCException; +import com.condation.cms.modules.ui.utils.UIPathUtil; +import com.condation.cms.core.content.io.YamlHeaderUpdater; +import com.google.common.base.Strings; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Date; + +/** + * + * @author thorstenmarx + */ +@Slf4j +@Extension(UIRemoteMethodExtensionPoint.class) +public class RemotePageEnpoints extends AbstractRemoteMethodeExtension { + + @RemoteMethod(name = "page.delete", permissions = {Permissions.CONTENT_EDIT}) + public Object deletePage(Map parameters) throws RPCException { + final DB db = getDB(parameters); + + Map result = new HashMap<>(); + + try { + var uri = (String) parameters.getOrDefault("uri", ""); + var name = (String) parameters.getOrDefault("name", ""); + var contentBase = db.getReadOnlyFileSystem().contentBase(); + + if (Strings.isNullOrEmpty(name)) { + throw new RPCException(0, "filename can not be null"); + } + + var contentFile = contentBase.resolve(uri).resolve(name); + + log.debug("deleting file {}", contentFile.uri()); + var sections = db.getContent().listSections(contentFile); + Files.deleteIfExists(db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri).resolve(name)); + sections.forEach(node -> { + try { + log.debug("deleting section {}", node.uri()); + FileUtils.deleteFolder(db.getFileSystem().resolve(node.uri())); + } catch (IOException ioe) { + log.error("error deleting file {}", node.uri(), ioe); + } + }); + } catch (Exception e) { + log.error("", e); + throw new RPCException(0, e.getMessage()); + } + + return result; + } + + @RemoteMethod(name = "page.create", permissions = {Permissions.CONTENT_EDIT}) + public Object createPage(Map parameters) throws RPCException { + final DB db = getDB(parameters); + + Map result = new HashMap<>(); + + try { + var uri = (String) parameters.getOrDefault("uri", ""); + var name = (String) parameters.getOrDefault("name", ""); + + if (Strings.isNullOrEmpty(name)) { + throw new RPCException(1, "name must not be empty"); + } + + var contentBase = db.getFileSystem().resolve(Constants.Folders.CONTENT); + + var contentType = (String) parameters.getOrDefault("contentType", ""); + + var pageTemplate = uiHooks().contentTypes().getPageTemplate(contentType); + + if (pageTemplate.isEmpty()) { + throw new RPCException(1, "no contentType selected"); + } + + Map meta = new HashMap<>(); + meta.put("createdAt", Date.from(Instant.now())); + meta.put("createdBy", getUserName()); + meta.put(Constants.MetaFields.TITLE, name); + meta.put(Constants.MetaFields.TEMPLATE, pageTemplate.get().template()); + meta.put(Constants.MetaFields.PUBLISHED, false); + + name = UIPathUtil.toValidFilename(name); + + Path newFile = null; + if (name.endsWith(".md")) { + newFile = contentBase.resolve(uri).resolve(name); + } else { + newFile = contentBase.resolve(uri).resolve(name).resolve("index.md"); + } + + + if (newFile.isAbsolute()) { + throw new RPCException(1, "absolut path is not supported"); + } else if (Files.exists(newFile)) { + throw new RPCException(1, "directory already exists"); + } else if (!UIPathUtil.isChild(contentBase, newFile)) { + throw new RPCException(1, "invalid path"); + } + Files.createDirectories(newFile.getParent()); + Files.createFile(newFile); + + YamlHeaderUpdater.saveMarkdownFileWithHeader(newFile, meta, ""); + } catch (Exception e) { + log.error("", e); + throw new RPCException(0, e.getMessage()); + } + + return result; + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteTranslationEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteTranslationEnpoints.java new file mode 100644 index 000000000..373ef2f31 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteTranslationEnpoints.java @@ -0,0 +1,203 @@ +package com.condation.cms.modules.ui.extensionpoints.remotemethods; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.Constants; +import com.condation.cms.api.SiteProperties; +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.api.db.DB; +import com.condation.cms.api.eventbus.events.ReIndexContentMetaDataEvent; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.feature.features.DBFeature; +import com.condation.cms.api.feature.features.EventBusFeature; +import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; +import com.condation.modules.api.annotation.Extension; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import com.condation.cms.api.ui.annotations.RemoteMethod; +import com.condation.cms.api.ui.rpc.RPCException; +import com.condation.cms.modules.ui.extensionpoints.remotemethods.dto.TranslationDto; +import com.condation.cms.core.content.io.ContentFileParser; +import com.condation.cms.modules.ui.utils.MetaConverter; +import com.condation.cms.modules.ui.utils.TranslationHelper; +import com.condation.cms.core.content.io.YamlHeaderUpdater; +import com.condation.cms.core.serivce.ServiceRegistry; +import com.condation.cms.core.serivce.impl.NodeTranslationService; +import com.condation.cms.core.serivce.impl.SiteLinkService; +import com.condation.cms.core.serivce.impl.SitePropertiesService; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author thorstenmarx + */ +@Slf4j +@Extension(UIRemoteMethodExtensionPoint.class) +public class RemoteTranslationEnpoints extends AbstractRemoteMethodeExtension { + + @RemoteMethod(name = "translations.get", permissions = {Permissions.CONTENT_EDIT}) + public Object get(Map parameters) throws RPCException { + final DB db = getDB(parameters); + + Map result = new HashMap<>(); + List translations = new ArrayList<>(); + result.put("translations", translations); + + var uri = (String) parameters.getOrDefault("uri", ""); + + var contentNodeOpt = db.getContent().byUri(uri); + var contentNode = contentNodeOpt.orElseThrow(() -> new RPCException("content node for uri %s not found".formatted(uri))); + + var siteProperties = getContext().get(ConfigurationFeature.class).configuration().get(SiteConfiguration.class).siteProperties(); + + var translationHelper = new TranslationHelper(siteProperties); + translationHelper.getFilteredMapping().forEach(mapping -> { + if (mapping.language().equals(siteProperties.language())) { + return; + } + var translationUri = contentNode.getMetaValue("translations.%s".formatted(mapping.language()), ""); + var deepLink = ""; + if (!translationUri.equals("")) { + deepLink = ServiceRegistry.getInstance().get(mapping.site(), SiteLinkService.class).get().managerDeepLink(translationUri); + } + var service = ServiceRegistry.getInstance().get(mapping.site(), SitePropertiesService.class).get(); + translations.add(new TranslationDto( + mapping.site(), + mapping.language(), + service.siteProperties().locale().getCountry().toLowerCase(), + translationUri, + deepLink)); + }); + + return result; + } + + @RemoteMethod(name = "translations.remove", permissions = {Permissions.CONTENT_EDIT}) + public Object remove(Map parameters) { + final DB db = getContext().get(DBFeature.class).db(); + var contentBase = db.getReadOnlyFileSystem().resolve(Constants.Folders.CONTENT); + + var uri = (String) parameters.get("uri"); + var language = (String) parameters.get("language"); + + var contentFile = contentBase.resolve(uri); + + Map result = new HashMap<>(); + result.put("uri", uri); + if (contentFile != null) { + try { + ContentFileParser parser = new ContentFileParser(contentFile); + + Map meta = parser.getHeader(); + if (meta.containsKey("translations")) { + var translations = (Map) meta.get("translations"); + if (!translations.containsKey(language)) { + return result; + } + var oldTranslationUri = (String)translations.remove(language); + var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri); + + YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, meta, parser.getContent()); + log.debug("file {} saved", uri); + + getContext().get(EventBusFeature.class).eventBus().publish(new ReIndexContentMetaDataEvent(uri)); + + final SiteProperties siteProperties = getContext().get(ConfigurationFeature.class) + .configuration().get(SiteConfiguration.class) + .siteProperties(); + // update target site + var translationSite = siteProperties + .translation() + .getMapping().stream() + .filter(mapping -> mapping.language().equals(language)).findFirst(); + if (translationSite.isPresent()) { + var nodeTranslationService = ServiceRegistry.getInstance().get(translationSite.get().site(), NodeTranslationService.class); + + if (nodeTranslationService.isPresent()) { + nodeTranslationService.get().removeTranslation(oldTranslationUri, siteProperties.language()); + } + } + } + } catch (IOException ex) { + log.error("", ex); + } + } + + return result; + } + + @RemoteMethod(name = "translations.add", permissions = {Permissions.CONTENT_EDIT}) + public Object add(Map parameters) { + final DB db = getContext().get(DBFeature.class).db(); + var contentBase = db.getReadOnlyFileSystem().resolve(Constants.Folders.CONTENT); + + var uri = (String) parameters.get("uri"); + var language = (String) parameters.get("language"); + var translation_url = (String) parameters.get("translationUri"); + + var contentFile = contentBase.resolve(uri); + + Map result = new HashMap<>(); + result.put("uri", uri); + if (contentFile != null) { + try { + ContentFileParser parser = new ContentFileParser(contentFile); + + Map meta = parser.getHeader(); + var translations = (Map) meta.getOrDefault("translations", new HashMap<>()); + translations.put(language, translation_url); + meta.put("translations", translations); + + var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri); + + YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, meta, parser.getContent()); + log.debug("file {} saved", uri); + + getContext().get(EventBusFeature.class).eventBus().publish(new ReIndexContentMetaDataEvent(uri)); + final SiteProperties siteProperties = getContext().get(ConfigurationFeature.class) + .configuration().get(SiteConfiguration.class) + .siteProperties(); + // update target site + var translationSite = siteProperties + .translation() + .getMapping().stream() + .filter(mapping -> mapping.language().equals(language)).findFirst(); + if (translationSite.isPresent()) { + var nodeTranslationService = ServiceRegistry.getInstance().get(translationSite.get().site(), NodeTranslationService.class); + + if (nodeTranslationService.isPresent()) { + nodeTranslationService.get().addTranslation(translation_url, siteProperties.id(), uri, siteProperties.language()); + } + } + } catch (IOException ex) { + log.error("", ex); + result.put("error", true); + } + } + + return result; + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/dto/TranslationDto.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/dto/TranslationDto.java new file mode 100644 index 000000000..2fc2f2b59 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/dto/TranslationDto.java @@ -0,0 +1,31 @@ +package com.condation.cms.modules.ui.extensionpoints.remotemethods.dto; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thmar + */ +public record TranslationDto (String site, String lang, String country, String url, String managerDeepLink) { + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/CompositeHttpHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/CompositeHttpHandler.java new file mode 100644 index 000000000..df1c0ed51 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/CompositeHttpHandler.java @@ -0,0 +1,49 @@ +package com.condation.cms.modules.ui.http; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.extensions.HttpHandler; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; +import java.util.List; + +public class CompositeHttpHandler implements HttpHandler { + + private final List handlers; + + public CompositeHttpHandler(List handlers) { + this.handlers = handlers; + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + for (HttpHandler handler : handlers) { + if (handler.handle(request, response, callback)) { + return true; // Stop, this handler has handled the request + } + } + return false; // No handler handled the request + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/HookHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/HookHandler.java new file mode 100644 index 000000000..c3438e775 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/HookHandler.java @@ -0,0 +1,76 @@ +package com.condation.cms.modules.ui.http; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.hooks.ActionContext; +import com.condation.cms.api.hooks.HookSystem; +import com.condation.cms.modules.ui.model.HookCall; +import com.condation.cms.modules.ui.utils.json.UIGsonProvider; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author t.marx + */ +@RequiredArgsConstructor +@Slf4j +public class HookHandler extends JettyHandler { + + private final HookSystem hookSystem; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + + if (!request.getMethod().equalsIgnoreCase("POST")) { + Response.writeError(request, response, callback, HttpStatus.METHOD_NOT_ALLOWED_405, ""); + return true; + } + + String body = getBody(request); + var command = UIGsonProvider.INSTANCE.fromJson(body, HookCall.class); + ActionContext actionContext = hookSystem.execute(command.hook(), command.parameters()); + + Map commandResponse = new HashMap<>(); + commandResponse.put("hook", command.hook()); + if (!actionContext.results().isEmpty()) { + commandResponse.put("result", actionContext.results()); + } + + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "application/json; charset=UTF-8"); + response.setStatus(200); + Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(commandResponse), callback); + + + return true; + } + + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JSActionHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JSActionHandler.java new file mode 100644 index 000000000..ed65ddd7d --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JSActionHandler.java @@ -0,0 +1,71 @@ +package com.condation.cms.modules.ui.http; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.utils.HTTPUtil; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author t.marx + */ +@Slf4j +@RequiredArgsConstructor +public class JSActionHandler extends JettyHandler { + + private final FileSystem fileSystem; + private final String base; + private final SiteModuleContext context; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + var resource = request.getHttpURI().getPath().replace( + managerURL("/manager/actions/", context), "") + ".js"; + + var files = fileSystem.getPath(base); + + if (resource.startsWith("/")) { + resource = resource.substring(1); + } + + var path = files.resolve(resource); + if (Files.exists(path)) { + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "%s; charset=UTF-8".formatted(Files.probeContentType(path))); + Content.Sink.write(response, true, Files.readString(path, StandardCharsets.UTF_8), callback); + } else { + callback.succeeded(); + } + + return true; + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JettyHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JettyHandler.java new file mode 100644 index 000000000..3d76adda4 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JettyHandler.java @@ -0,0 +1,106 @@ +package com.condation.cms.modules.ui.http; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.configuration.configs.ServerConfiguration; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.api.extensions.HttpHandler; +import com.condation.cms.api.feature.FeatureContainer; +import com.condation.cms.api.feature.features.AuthFeature; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.feature.features.InjectorFeature; +import com.condation.cms.api.feature.features.SitePropertiesFeature; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.module.SiteRequestContext; +import com.condation.cms.api.utils.HTTPUtil; +import com.condation.cms.auth.services.Realm; +import com.condation.cms.auth.services.User; +import com.condation.cms.auth.services.UserService; +import com.condation.cms.modules.ui.utils.TokenUtils; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.server.Request; + +/** + * + * @author t.marx + */ +@Slf4j +public abstract class JettyHandler implements HttpHandler { + + protected String getBody(Request request) { + try (var inputStream = Request.asInputStream(request)) { + + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (Exception ex) { + log.error("", ex); + } + return ""; + } + + public void setAuthFeature (String username, SiteRequestContext requestContext) { + if (requestContext.has(AuthFeature.class)) { + return; + } + requestContext.add(AuthFeature.class, new AuthFeature(username)); + } + + public Optional getUser(Request request, SiteModuleContext moduleContext) { + + try { + var tokenCookie = Request.getCookies(request).stream().filter(cookie -> "cms-token".equals(cookie.getName())).findFirst(); + + if (tokenCookie.isEmpty()) { + Optional.empty(); + } + var token = tokenCookie.get().getValue(); + var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); + var username = TokenUtils.getPayload(token, secret); + + if (username.isEmpty()) { + return Optional.empty(); + } + + return moduleContext.get(InjectorFeature.class).injector().getInstance(UserService.class).byUsername(Realm.of("manager-users"), username.get().username()); + } catch (Exception e) { + log.error("error getting user", e); + } + return Optional.empty(); + } + + protected String getUsername (Request request, SiteModuleContext moduleContext) { + var user = getUser(request, moduleContext); + if (user.isPresent()) { + return user.get().username(); + } + return ""; + } + + protected String managerBaseURL(FeatureContainer featureContainer) { + return managerURL("/manager", featureContainer); + } + + protected String managerURL(String url, FeatureContainer featureContainer) { + return HTTPUtil.modifyUrl(url, featureContainer.get(SitePropertiesFeature.class).siteProperties()); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/PublicResourceHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/PublicResourceHandler.java new file mode 100644 index 000000000..a8b597795 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/PublicResourceHandler.java @@ -0,0 +1,97 @@ +package com.condation.cms.modules.ui.http; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.module.SiteModuleContext; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author t.marx + */ +@Slf4j +@RequiredArgsConstructor +public class PublicResourceHandler extends JettyHandler { + + private final SiteModuleContext context; + private final FileSystem fileSystem; + private final String base; + private final List publicResources; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + + var resource = request.getHttpURI().getPath().replace( + managerURL("/manager/", context), ""); + + if (resource.equals("")) { + response.setStatus(404); + callback.succeeded(); + return true; + } + if (!publicResources.contains(resource)) { + response.setStatus(403); + callback.succeeded(); + return true; + } + + var files = fileSystem.getPath(base); + + if (resource.startsWith("/")) { + resource = resource.substring(1); + } + + var path = files.resolve(resource); + if (Files.exists(path)) { + String contentType = Files.probeContentType(path); + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "%s; charset=UTF-8".formatted(contentType)); + + String fileName = path.getFileName().toString(); + boolean useString = fileName.endsWith(".js") || fileName.endsWith(".css") || fileName.endsWith(".map"); + + if (useString) { + String content = Files.readString(path, StandardCharsets.UTF_8); + Content.Sink.write(response, true, content, callback); + } else { + byte[] bytes = Files.readAllBytes(path); + Content.Sink.write(response, true, ByteBuffer.wrap(bytes)); + callback.succeeded(); + } + } else { + callback.succeeded(); + } + + return true; + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/RemoteCallHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/RemoteCallHandler.java new file mode 100644 index 000000000..a080ef98e --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/RemoteCallHandler.java @@ -0,0 +1,82 @@ +package com.condation.cms.modules.ui.http; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.ui.rpc.RPCError; +import com.condation.cms.api.ui.rpc.RPCResult; +import com.condation.cms.modules.ui.model.RemoteCall; +import com.condation.cms.modules.ui.services.RemoteMethodService; +import com.condation.cms.modules.ui.utils.json.UIGsonProvider; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author t.marx + */ +@RequiredArgsConstructor +@Slf4j +public class RemoteCallHandler extends JettyHandler { + + private final RemoteMethodService remoteCallService; + private final SiteModuleContext moduleContext; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + + if (!request.getMethod().equalsIgnoreCase("POST")) { + Response.writeError(request, response, callback, HttpStatus.METHOD_NOT_ALLOWED_405, ""); + return true; + } + + String body = getBody(request); + var remoteCall = UIGsonProvider.INSTANCE.fromJson(body, RemoteCall.class); + + RPCResult rpcResult; + try { + Optional result = remoteCallService.execute(remoteCall.method(), remoteCall.parameters(), getUser(request, moduleContext).get()); + if (result.isPresent()) { + rpcResult = new RPCResult(result.get()); + } else { + rpcResult = new RPCResult(); + } + } catch (Exception e) { + log.error("error executing endpoint", remoteCall.method(), e); + rpcResult = new RPCResult(new RPCError(e.getMessage())); + } + + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "application/json; charset=UTF-8"); + response.setStatus(200); + Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(rpcResult), callback); + + return true; + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/ResourceHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/ResourceHandler.java new file mode 100644 index 000000000..41ede44cf --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/ResourceHandler.java @@ -0,0 +1,121 @@ +package com.condation.cms.modules.ui.http; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.SiteProperties; +import com.condation.cms.api.configuration.configs.ServerConfiguration; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.feature.features.HookSystemFeature; +import com.condation.cms.api.feature.features.ModuleManagerFeature; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.module.SiteRequestContext; +import com.condation.cms.modules.ui.extensionpoints.UILifecycleExtension; +import com.condation.cms.modules.ui.utils.ActionFactory; +import com.condation.cms.modules.ui.utils.TokenUtils; +import com.condation.cms.modules.ui.utils.TranslationHelper; +import com.condation.cms.modules.ui.utils.template.UILinkFunction; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author t.marx + */ +@Slf4j +@RequiredArgsConstructor +public class ResourceHandler extends JettyHandler { + + private final FileSystem fileSystem; + private final String base; + private final SiteModuleContext context; + private final SiteRequestContext requestContext; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + + var hookSystem = requestContext.get(HookSystemFeature.class).hookSystem(); + var moduleManager = context.get(ModuleManagerFeature.class).moduleManager(); + + var actionFactory = new ActionFactory(hookSystem, moduleManager, getUser(request, context).get()); + + var resource = request.getHttpURI().getPath().replaceFirst( + managerURL("/manager/", requestContext), ""); + + if (resource.equals("")) { + resource = "index.html"; + } + + if (resource.endsWith(".html")) { + try { + var secret = context.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); + final SiteProperties siteProperties = context.get(ConfigurationFeature.class).configuration().get(SiteConfiguration.class).siteProperties(); + String content = UILifecycleExtension.getInstance(context).getTemplateEngine().render(resource, + Map.of( + "actionFactory", actionFactory, + "csrfToken", TokenUtils.createToken("csrf", secret), + "links", new UILinkFunction(requestContext), + "managerBaseURL", managerBaseURL(requestContext), + "previewToken", TokenUtils.createToken(getUsername(request, context), secret), + "contextPath", siteProperties.contextPath(), + "siteId", siteProperties.id(), + "translation", new TranslationHelper(siteProperties) + )); + Content.Sink.write(response, true, content, callback); + } catch (Exception e) { + log.error("", e); + callback.failed(e); + } + } else { + var files = fileSystem.getPath(base); + + if (resource.startsWith("/")) { + resource = resource.substring(1); + } + + var path = files.resolve(resource); + if (Files.exists(path)) { + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "%s; charset=UTF-8".formatted(Files.probeContentType(path))); + Content.Sink.write(response, true, Files.readString(path, StandardCharsets.UTF_8), callback); + } else { + path = files.resolve(resource + ".js"); + if (Files.exists(path)) { + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "%s; charset=UTF-8".formatted(Files.probeContentType(path))); + Content.Sink.write(response, true, Files.readString(path, StandardCharsets.UTF_8), callback); + } else { + callback.succeeded(); + } + } + } + + return true; + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/UploadHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/UploadHandler.java new file mode 100644 index 000000000..1b43dee0e --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/UploadHandler.java @@ -0,0 +1,245 @@ +package com.condation.cms.modules.ui.http; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.utils.PathUtil; +import com.condation.cms.modules.ui.utils.UIPathUtil; +import com.condation.cms.modules.ui.utils.json.UIGsonProvider; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.Map; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.tika.Tika; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.MultiPart; +import org.eclipse.jetty.http.MultiPartFormData; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; +import static org.eclipse.jetty.util.IO.ensureDirExists; +import org.eclipse.jetty.util.StringUtil; + +/** + * + * @author thorstenmarx + */ +@Slf4j +public class UploadHandler extends JettyHandler { + + private final String contextPath; + private final Path outputDir; + + private final Path TEMP_UPLOAD_DIR; + + private final boolean useDateFolder; + + /** + * Maximum allowed size of uploaded files in bytes (10 MB). + */ + public static final long MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; + + public static final Set ALLOWED_MIME_TYPES = Set.of( + "image/png", + "image/jpeg", + "image/gif", + "image/webp", + "image/svg+xml", + "image/tiff", + "image/avif" + ); + + private static final Tika tika = new Tika(); + + public UploadHandler(String contextPath, Path outputDir) throws IOException { + this(contextPath, outputDir, false); + } + + public UploadHandler(String contextPath, Path outputDir, boolean useDateFolder) throws IOException { + super(); + this.useDateFolder = useDateFolder; + this.contextPath = contextPath; + this.outputDir = outputDir; + ensureDirExists(this.outputDir); + this.TEMP_UPLOAD_DIR = Files.createTempDirectory("condation-uploads"); + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + if (!request.getHttpURI().getPath().startsWith(contextPath)) { + // not meant for us, skip it. + return false; + } + + if (!request.getMethod().equalsIgnoreCase("POST")) { + // Not a POST method + Response.writeError(request, response, callback, HttpStatus.METHOD_NOT_ALLOWED_405); + return true; + } + + String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + if (!HttpField.getValueParameters(contentType, null).equals("multipart/form-data")) { + // Not a content-type supporting multi-part + Response.writeError(request, response, callback, HttpStatus.NOT_ACCEPTABLE_406); + return true; + } + + String boundary = MultiPart.extractBoundary(contentType); + MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary); + formData.setFilesDirectory(TEMP_UPLOAD_DIR); + + try { + formData.parse(request, new org.eclipse.jetty.util.Promise.Invocable() { + @Override + public void failed(Throwable x) { + Response.writeError(request, response, callback, x); + } + + @Override + public void succeeded(MultiPartFormData.Parts parts) { + if (parts == null || parts.size() == 0) { + log.warn("Multipart upload received, but no parts found."); + Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "No parts in upload."); + return; + } + + try { + var filename = process(parts); + response.setStatus(HttpStatus.OK_200); + Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(Map.of("filename", filename)), callback); + } catch (Exception ex) { + log.error("Fehler beim Verarbeiten des Uploads", ex); + Response.writeError(request, response, callback, ex); + } + } + }); + } catch (Exception x) { + Response.writeError(request, response, callback, x); + } + return true; + } + + private String process(MultiPartFormData.Parts parts) throws IOException { + try { + MultiPart.Part filePart = null; + String uri = null; + + for (MultiPart.Part part : parts) { + if ("uri".equals(part.getName())) { + try (InputStream is = Content.Source.asInputStream(part.getContentSource())) { + uri = new String(is.readAllBytes(), StandardCharsets.UTF_8).trim(); + } + } else if ("file".equals(part.getName())) { + filePart = part; + } + } + + if (useDateFolder) { + LocalDate now = LocalDate.now(); + String year = String.valueOf(now.getYear()); + String month = String.format("%02d", now.getMonthValue()); + + uri = "%s/%s/".formatted(year, month); + } + + if (filePart != null) { + String rawFilename = filePart.getFileName(); + if (StringUtil.isNotBlank(rawFilename)) { + // Temporäre Datei erzeugen, um MIME-Type zu ermitteln + Path tempFile = Files.createTempFile("upload-", ".tmp"); + try (InputStream inputStream = Content.Source.asInputStream(filePart.getContentSource()); OutputStream outputStream = Files.newOutputStream(tempFile)) { + long bytesCopied = ByteStreams.copy(inputStream, outputStream); + + if (bytesCopied > MAX_FILE_SIZE_BYTES) { + Files.deleteIfExists(tempFile); + throw new IOException("Uploaded file too large (" + bytesCopied + " bytes)"); + } + } + + String detectedMimeType = tika.detect(tempFile); + log.debug("Detected MIME type: {}", detectedMimeType); + + if (!ALLOWED_MIME_TYPES.contains(detectedMimeType)) { + Files.deleteIfExists(tempFile); + throw new IOException("Unsupported file type: " + detectedMimeType); + } + + // Zieldatei vorbereiten + //String safeFilename = URLEncoder.encode(rawFilename, StandardCharsets.UTF_8); + String safeFilename = slugifyFilename(rawFilename); + Path targetDir = outputDir; + + if (StringUtil.isNotBlank(uri)) { + uri = uri.replaceAll("[^a-zA-Z0-9/_\\-]", "_"); // nur sichere Zeichen + targetDir = outputDir.resolve(uri).normalize(); + } + + if (!PathUtil.isChild(outputDir, targetDir)) { + throw new RuntimeException(""); + } + + ensureDirExists(targetDir); + Path outputFile = targetDir.resolve(safeFilename); + + // Temporäre Datei an Zielort verschieben + Files.move(tempFile, outputFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + log.info("Saved uploaded file to {}", outputFile); + + return PathUtil.toRelativeFile(outputFile, outputDir); + } + } + } finally { + for (MultiPart.Part part : parts) { + part.delete(); + } + } + + return ""; + } + + private String slugifyFilename(String rawFilename) { + String extension = ""; + int dotIndex = rawFilename.lastIndexOf('.'); + String namePart = rawFilename; + + if (dotIndex > 0 && dotIndex < rawFilename.length() - 1) { + extension = rawFilename.substring(dotIndex); // inkl. Punkt + namePart = rawFilename.substring(0, dotIndex); + } + + // Slugify nur auf den Namensteil anwenden + String slug = UIPathUtil.SLUGIFY.slugify(namePart); + + // Endung wieder anhängen + return slug + extension.toLowerCase(); + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/AjaxLoginHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/AjaxLoginHandler.java new file mode 100644 index 000000000..55c134d94 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/AjaxLoginHandler.java @@ -0,0 +1,256 @@ +package com.condation.cms.modules.ui.http.auth; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.cache.ICache; +import com.condation.cms.api.configuration.configs.ServerConfiguration; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.feature.features.InjectorFeature; +import com.condation.cms.api.feature.features.IsDevModeFeature; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.request.RequestContext; +import com.condation.cms.api.utils.RequestUtil; +import com.condation.cms.auth.services.Realm; +import com.condation.cms.auth.services.User; +import com.condation.cms.auth.services.UserService; +import com.condation.cms.modules.ui.http.JettyHandler; +import com.condation.cms.modules.ui.utils.MailerProvider; +import com.condation.cms.modules.ui.utils.TokenUtils; +import com.condation.cms.modules.ui.utils.json.UIGsonProvider; +import java.security.SecureRandom; +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; +import org.simplejavamail.email.EmailBuilder; + +/** + * + * @author thorstenmarx + */ +@RequiredArgsConstructor +@Slf4j +public class AjaxLoginHandler extends JettyHandler { + + private final SiteModuleContext moduleContext; + private final RequestContext requestContext; + + private final ICache loginFails; + + private final ICache loginAttempts; + + private static final int ATTEMPTS_TO_BLOCK = 3; + + public static record Login(User user, String token) { + + } + + public static record Command(String command, Map data) { + + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + + if (!request.getMethod().equalsIgnoreCase("POST")) { + return false; + } + if (getClientLoginAttempts(request) > ATTEMPTS_TO_BLOCK) { + response.setStatus(403); + callback.succeeded(); + return true; + } + + var command = UIGsonProvider.INSTANCE.fromJson(getBody(request), Command.class); + + if (!is2FAenabled()) { + simpleLogin(request, response, callback, command); + return true; + } + + if ("login".equals(command.command())) { + handleLogin(request, response, callback, command); + } else if ("validate".equals(command.command())) { + validate(request, response, callback, command); + } + + return true; + } + + private boolean is2FAenabled () { + return moduleContext.get(ConfigurationFeature.class).configuration().get(SiteConfiguration.class).siteProperties() + .ui().force2fa(); + } + + private void simpleLogin (Request request, Response response, Callback callback, Command command) throws Exception { + var username = (String) command.data().getOrDefault("username", ""); + var password = (String) command.data().getOrDefault("password", ""); + + Optional userOpt = moduleContext.get(InjectorFeature.class).injector().getInstance(UserService.class).login(Realm.of("manager-users"), username, password); + if (userOpt.isPresent()) { + com.condation.cms.auth.services.User user = userOpt.get(); + var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); + var token = TokenUtils.createToken(user.username(), secret); + + boolean isDev = requestContext.has(IsDevModeFeature.class); + + HttpCookie cookie = HttpCookie.from("cms-token", token, + Map.of( + HttpCookie.SAME_SITE_ATTRIBUTE, "Strict", + HttpCookie.HTTP_ONLY_ATTRIBUTE, "true", + HttpCookie.PATH_ATTRIBUTE, "/" + )); + if (!isDev) { + cookie = HttpCookie.from(cookie, HttpCookie.SECURE_ATTRIBUTE, "true"); + } + Response.addCookie(response, cookie); + + Content.Sink.write( + response, + true, + UIGsonProvider.INSTANCE.toJson(Map.of("status", "ok")), + callback); + + } else { + getClientLoginCounter(request).incrementAndGet(); + Content.Sink.write( + response, + true, + UIGsonProvider.INSTANCE.toJson(Map.of("status", "error")), + callback); + } + } + + private void handleLogin(Request request, Response response, Callback callback, Command command) throws Exception { + var username = (String) command.data().getOrDefault("username", ""); + var password = (String) command.data().getOrDefault("password", ""); + + java.util.Optional userOpt = moduleContext.get(InjectorFeature.class).injector().getInstance(UserService.class).login(Realm.of("manager-users"), username, password); + if (userOpt.isPresent()) { + com.condation.cms.auth.services.User user = userOpt.get(); + + var code = generateCode(); + var login = new Login(user, code); + loginAttempts.put(code, login); + sendLoginCode(user, code); + + Map responseData = Map.of( + "status", "2fa_required" + ); + + Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(responseData), callback); + + callback.succeeded(); + } else { + getClientLoginCounter(request).incrementAndGet(); + + Map responseData = Map.of( + "status", "error" + ); + + Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(responseData), callback); + } + } + + private void validate(Request request, Response response, Callback callback, Command command) throws Exception { + var code = (String) command.data().getOrDefault("code", ""); + + Optional userOpt = Optional.empty(); + if (loginAttempts.contains(code)) { + userOpt = Optional.of(loginAttempts.get(code).user()); + } + + if (userOpt.isPresent()) { + com.condation.cms.auth.services.User user = userOpt.get(); + var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); + var token = TokenUtils.createToken(user.username(), secret); + + boolean isDev = requestContext.has(IsDevModeFeature.class); + + HttpCookie cookie = HttpCookie.from("cms-token", token, + Map.of( + HttpCookie.SAME_SITE_ATTRIBUTE, "Strict", + HttpCookie.HTTP_ONLY_ATTRIBUTE, "true", + HttpCookie.MAX_AGE_ATTRIBUTE, String.valueOf(Duration.ofHours(1).toSeconds()), + HttpCookie.PATH_ATTRIBUTE, "/" + )); + if (!isDev) { + cookie = HttpCookie.from(cookie, HttpCookie.SECURE_ATTRIBUTE, "true"); + } + Response.addCookie(response, cookie); + + Map responseData = Map.of( + "status", "ok" + ); + + Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(responseData), callback); + } else { + getClientLoginCounter(request).incrementAndGet(); + Map responseData = Map.of( + "status", "error" + ); + + Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(responseData), callback); + } + } + + private String generateCode() { + int code = new SecureRandom().nextInt(1_000_000); + return String.format("%06d", code); + } + + private void sendLoginCode(User user, String code) { + + var siteProperties = moduleContext.get(ConfigurationFeature.class).configuration().get(SiteConfiguration.class).siteProperties(); + + var mailer = MailerProvider.provide(siteProperties.id()); + var mail = EmailBuilder.startingBlank() + .to(user.username(), (String) user.data().getOrDefault("mail", "test@localhost.de")) + .from( + (String)siteProperties.getOrDefault("ui.2fa.mail_sender", "CondationCMS"), + (String)siteProperties.get("ui.2fa.mail_from") + ).withSubject(siteProperties.getOrDefault("ui.2fa.mail_title", "CondationCMS login code")) + .withPlainText( + siteProperties.getOrDefault("ui.2fa.mail_message", "your code: ") + .replace("", code) + .replace("", user.username()) + ).buildEmail(); + mailer.sendMail(mail); + } + + private int getClientLoginAttempts(Request request) { + return getClientLoginCounter(request).get(); + } + + private AtomicInteger getClientLoginCounter(Request request) { + return loginFails.get(RequestUtil.clientAddress(request)); + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/CSRFHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/CSRFHandler.java new file mode 100644 index 000000000..66fe96dd3 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/CSRFHandler.java @@ -0,0 +1,71 @@ +package com.condation.cms.modules.ui.http.auth; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.configuration.configs.ServerConfiguration; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.modules.ui.http.JettyHandler; +import com.condation.cms.modules.ui.utils.TokenUtils; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author thorstenmarx + */ +@RequiredArgsConstructor +@Slf4j +public class CSRFHandler extends JettyHandler { + + private final SiteModuleContext moduleContext; + + private static final Set METHODS_TO_CHECK = Set.of("POST", "PUT", "DELETE", "PATCH"); + + @Override + public boolean handle(Request request, Response response, Callback callback) { + + String method = request.getMethod(); + if (!METHODS_TO_CHECK.contains(method)) { + return false; + } + + var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); + + // ⛔️ CSRF-Token header + String csrfToken = request.getHeaders().get("X-CSRF-Token"); + if (csrfToken == null || TokenUtils.getPayload(csrfToken, secret).isEmpty()) { + log.warn("Invalid or missing CSRF token from {} {}", request.getMethod(), request.getHttpURI().toString()); + response.setStatus(403); + callback.succeeded(); + return true; + } + + return false; + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/LoginResourceHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/LoginResourceHandler.java new file mode 100644 index 000000000..f53a50c0d --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/LoginResourceHandler.java @@ -0,0 +1,78 @@ +package com.condation.cms.modules.ui.http.auth; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.configuration.configs.ServerConfiguration; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.request.RequestContext; +import com.condation.cms.content.template.functions.LinkFunction; +import com.condation.cms.modules.ui.extensionpoints.UILifecycleExtension; +import com.condation.cms.modules.ui.http.JettyHandler; +import com.condation.cms.modules.ui.utils.TokenUtils; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author t.marx + */ +@Slf4j +@RequiredArgsConstructor +public class LoginResourceHandler extends JettyHandler { + + private final SiteModuleContext context; + private final RequestContext requestContext; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + + if (!request.getMethod().equalsIgnoreCase("GET")) { + return false; + } + + try { + var siteProperties = context.get(ConfigurationFeature.class).configuration().get(SiteConfiguration.class).siteProperties(); + var secret = context.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); + var force2fa = siteProperties.ui().force2fa(); + String content = UILifecycleExtension.getInstance(context).getTemplateEngine().render("login.html", Map.of( + "csrfToken", TokenUtils.createToken("csrf", secret), + "links", new LinkFunction(requestContext), + "managerBaseURL", managerBaseURL(requestContext), + "force2fa", force2fa + )); + Content.Sink.write(response, true, content, callback); + } catch (Exception e) { + log.error("", e); + callback.failed(e); + } + + return true; + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/LogoutHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/LogoutHandler.java new file mode 100644 index 000000000..602b2a198 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/LogoutHandler.java @@ -0,0 +1,82 @@ +package com.condation.cms.modules.ui.http.auth; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.feature.features.IsDevModeFeature; +import com.condation.cms.api.module.SiteRequestContext; +import com.condation.cms.api.utils.HTTPUtil; +import com.condation.cms.modules.ui.http.JettyHandler; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author thorstenmarx + */ +@RequiredArgsConstructor +@Slf4j +public class LogoutHandler extends JettyHandler { + + private final SiteRequestContext requestContext; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + + boolean isDev = requestContext.has(IsDevModeFeature.class); + + HttpCookie cookie = HttpCookie.from("cms-token", "", + Map.of( + HttpCookie.SAME_SITE_ATTRIBUTE, "Strict", + HttpCookie.HTTP_ONLY_ATTRIBUTE, "true", + HttpCookie.MAX_AGE_ATTRIBUTE, "0", + HttpCookie.PATH_ATTRIBUTE, "/" + )); + if (!isDev) { + cookie = HttpCookie.from(cookie, HttpCookie.SECURE_ATTRIBUTE, "true"); + } + Response.addCookie(response, cookie); + + HttpCookie preview_cookie = HttpCookie.from("cms-preview-token", "", + Map.of( + HttpCookie.SAME_SITE_ATTRIBUTE, "Strict", + HttpCookie.HTTP_ONLY_ATTRIBUTE, "true", + HttpCookie.MAX_AGE_ATTRIBUTE, "0", + HttpCookie.PATH_ATTRIBUTE, "/" + )); + if (!isDev) { + preview_cookie = HttpCookie.from(preview_cookie, HttpCookie.SECURE_ATTRIBUTE, "true"); + } + Response.addCookie(response, preview_cookie); + + response.setStatus(302); + response.getHeaders().add("Location", managerURL("/manager/login", requestContext)); + callback.succeeded(); + + return true; + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/UIAuthHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/UIAuthHandler.java new file mode 100644 index 000000000..e74e98ee5 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/UIAuthHandler.java @@ -0,0 +1,77 @@ +package com.condation.cms.modules.ui.http.auth; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.configuration.configs.ServerConfiguration; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.feature.features.IsPreviewFeature; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.module.SiteRequestContext; +import com.condation.cms.modules.ui.http.JettyHandler; +import com.condation.cms.modules.ui.utils.TokenUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author thorstenmarx + */ +@RequiredArgsConstructor +@Slf4j +public class UIAuthHandler extends JettyHandler { + + private final SiteModuleContext moduleContext; + private final SiteRequestContext requestContext; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + + var tokenCookie = Request.getCookies(request).stream().filter(cookie -> "cms-token".equals(cookie.getName())).findFirst(); + + if (tokenCookie.isEmpty()) { + response.setStatus(403); + callback.succeeded(); + return true; + } + var token = tokenCookie.get().getValue(); + var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); + + var payload = TokenUtils.getPayload(token, secret); + + if (payload.isEmpty()) { + response.setStatus(403); + callback.succeeded(); + return true; + } + + setAuthFeature(payload.get().username(), requestContext); + + requestContext.add(IsPreviewFeature.class, new IsPreviewFeature(IsPreviewFeature.Mode.MANAGER)); + + return false; + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/UIAuthRedirectHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/UIAuthRedirectHandler.java new file mode 100644 index 000000000..597469336 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/UIAuthRedirectHandler.java @@ -0,0 +1,78 @@ +package com.condation.cms.modules.ui.http.auth; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.configuration.configs.ServerConfiguration; +import com.condation.cms.api.configuration.configs.SiteConfiguration; +import com.condation.cms.api.feature.FeatureContainer; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.module.SiteRequestContext; +import com.condation.cms.modules.ui.http.JettyHandler; +import com.condation.cms.modules.ui.utils.TokenUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author thorstenmarx + */ +@RequiredArgsConstructor +@Slf4j +public class UIAuthRedirectHandler extends JettyHandler { + + private final SiteModuleContext moduleContext; + private final SiteRequestContext requestContext; + + @Override + public boolean handle(Request request, Response response, Callback callback) { + + var tokenCookie = Request.getCookies(request).stream().filter(cookie -> "cms-token".equals(cookie.getName())).findFirst(); + + if (tokenCookie.isEmpty()) { + redirectToLogin(response, moduleContext); + callback.succeeded(); + return true; + } + var token = tokenCookie.get().getValue(); + var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); + var payload = TokenUtils.getPayload(token, secret); + + if (payload.isEmpty()) { + redirectToLogin(response, moduleContext); + callback.succeeded(); + return true; + } + setAuthFeature(payload.get().username(), requestContext); + + return false; + } + + private void redirectToLogin(Response response, FeatureContainer container) { + response.setStatus(302); + response.getHeaders().add("Location", managerURL("/manager/login", container)); + } + +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/model/HookCall.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/model/HookCall.java new file mode 100644 index 000000000..dc6f49c74 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/model/HookCall.java @@ -0,0 +1,31 @@ +package com.condation.cms.modules.ui.model; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.Map; + +/** + * + * @author t.marx + */ +public record HookCall (String hook, Map parameters) {} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/model/RemoteCall.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/model/RemoteCall.java new file mode 100644 index 000000000..0ba3639a1 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/model/RemoteCall.java @@ -0,0 +1,31 @@ +package com.condation.cms.modules.ui.model; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.Map; + +/** + * + * @author t.marx + */ +public record RemoteCall (String method, Map parameters) {} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/services/LockService.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/services/LockService.java new file mode 100644 index 000000000..31588fd64 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/services/LockService.java @@ -0,0 +1,65 @@ +package com.condation.cms.modules.ui.services; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * + * @author t.marx + */ +public class LockService { + + private final ConcurrentMap> locks = new ConcurrentHashMap<>(); + + public boolean isLocked (String mode, String node) { + if (locks.containsKey(mode)) { + return locks.get(mode).contains(node); + } + return false; + } + + private synchronized void addMode (String mode) { + if (!locks.containsKey(mode)) { + locks.put(mode, new ArrayList<>()); + } + } + + public boolean lock (String mode, String node) { + if (!locks.containsKey(mode)) { + addMode(mode); + } + + return locks.get(mode).add(node); + } + + public boolean unlock (String mode, String node) { + if (locks.containsKey(mode)) { + locks.get(mode).remove(node); + } + return true; + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/services/RemoteMethodService.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/services/RemoteMethodService.java new file mode 100644 index 000000000..2ffd2cc91 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/services/RemoteMethodService.java @@ -0,0 +1,88 @@ +package com.condation.cms.modules.ui.services; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; +import com.condation.cms.api.utils.AnnotationsUtil; +import com.condation.modules.api.ModuleManager; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import com.condation.cms.api.ui.annotations.RemoteMethod; +import com.condation.cms.auth.services.AuthorizationService; +import com.condation.cms.auth.services.User; +import java.util.function.Function; +import lombok.RequiredArgsConstructor; + +/** + * + * @author t.marx + */ +public class RemoteMethodService { + + public Map handlers = new HashMap<>(); + + protected static AuthorizationService authorizationService = new AuthorizationService(); + + public void init (final ModuleManager moduleManager) { + moduleManager.extensions(UIRemoteMethodExtensionPoint.class).forEach(this::register); + } + + public void register (UIRemoteMethodExtensionPoint extension) { + AnnotationsUtil.process(extension, RemoteMethod.class, List.of(Map.class), Object.class) + .forEach(ann -> { + handlers.put(ann.annotation().name(), + new RMethod( + ann.annotation(), + (parameters) -> ann.invoke(parameters) + )); + }); + } + + public Optional execute (final String endpoint, final Map parameters, User user) { + if (!handlers.containsKey(endpoint)) { + return Optional.empty(); + } + return Optional.ofNullable(handlers.get(endpoint).execute(parameters, user)); + } + + @RequiredArgsConstructor + public static class RMethod { + private final RemoteMethod remoteMethodAnnotation; + private final Function, Object> function; + + public Object execute (final Map parameters, User user) { + if (!RemoteMethodService.authorizationService.hasAnyPermission(user, remoteMethodAnnotation.permissions())) { + throw new RemoteMethodException("access not allowed"); + } + return function.apply(parameters); + }; + } + + public static class RemoteMethodException extends RuntimeException { + public RemoteMethodException (String message) { + super(message); + } + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java new file mode 100644 index 000000000..c6245df5e --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java @@ -0,0 +1,274 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.annotations.Action; +import com.condation.cms.api.hooks.HookSystem; +import com.condation.cms.api.ui.action.UIHookAction; +import com.condation.cms.api.ui.action.UIAction; +import com.condation.cms.api.ui.action.UIScriptAction; +import com.condation.cms.api.ui.elements.Menu; +import com.condation.cms.api.ui.elements.MenuEntry; +import com.condation.modules.api.ModuleManager; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.condation.cms.api.ui.extensions.UIActionsExtensionPoint; +import com.condation.cms.api.utils.JSONUtil; +import com.condation.cms.auth.services.AuthorizationService; +import com.condation.cms.auth.services.User; +import java.util.Arrays; + +/** + * + * @author thorstenmarx + */ +@Slf4j +@RequiredArgsConstructor +public class ActionFactory { + + private final HookSystem hookSystem; + private final ModuleManager moduleManager; + private final User user; + + AuthorizationService authService = new AuthorizationService(); + + public List createShortCuts() { + List shortCuts = new ArrayList<>(); + moduleManager.extensions(UIActionsExtensionPoint.class).forEach(extension -> { + shortCuts.addAll(scanShortCuts(extension)); + }); + + return shortCuts; + } + + public Menu createMenu() { + UIHooks uiHooks = new UIHooks(hookSystem); + var menu = uiHooks.menu(); + + moduleManager.extensions(UIActionsExtensionPoint.class).forEach(extension -> { + try { + extension.addMenuItems(menu); + } catch (Exception e) { + log.error("", e); + } + }); + + + List entries = new ArrayList<>(); + moduleManager.extensions(UIActionsExtensionPoint.class).forEach(extension -> { + try { + entries.addAll(scanMenuEntries(extension)); + } catch (Exception e) { + log.error("", e); + } + }); + + insertEntriesIntoMenu(menu, entries); + + var filteredMenu = new Menu(); + var menuEntries = menu.entries(); + menuEntries.stream() + .filter(entry -> authService.hasAllPermissions(user, entry.getPermissions().toArray(new String[0]))) + .forEach(filteredMenu::addMenuEntry); + + return filteredMenu; + } + + private List scanShortCuts(Object moduleInstance) { + List shortCuts = new ArrayList<>(); + + for (Method method : moduleInstance.getClass().getDeclaredMethods()) { + var shortcutAnnotation = method.getAnnotation(com.condation.cms.api.ui.annotations.ShortCut.class); + if (shortcutAnnotation == null) { + continue; + } + + method.setAccessible(true); + UIAction menuAction = null; + + // 1. Methode hat @Action? + Action actionAnn = method.getAnnotation(Action.class); + if (actionAnn != null) { + menuAction = new UIHookAction(actionAnn.value(), Map.of()); + } // 2. @Hook in @MenuEntry + else if (!shortcutAnnotation.hookAction().value().isEmpty()) { + menuAction = new UIHookAction(shortcutAnnotation.hookAction().value(), Map.of()); + } // 3. @ScriptAction in @MenuEntry + else if (!shortcutAnnotation.scriptAction().module().isEmpty()) { + menuAction = new UIScriptAction(shortcutAnnotation.scriptAction().module(), shortcutAnnotation.scriptAction().function(), Map.of()); + } + + if (menuAction == null) { + var menuAnn = method.getAnnotation(com.condation.cms.api.ui.annotations.MenuEntry.class); + if (menuAnn != null) { + if (!menuAnn.hookAction().value().isEmpty()) { + menuAction = new UIHookAction(menuAnn.hookAction().value(), Map.of()); + } // 3. @ScriptAction in @MenuEntry + else if (!menuAnn.scriptAction().module().isEmpty()) { + menuAction = new UIScriptAction(menuAnn.scriptAction().module(), menuAnn.scriptAction().function(), Map.of()); + } + } + } + + if (menuAction != null) { + shortCuts.add(new ShortCutHolder( + shortcutAnnotation.id(), + shortcutAnnotation.title(), + shortcutAnnotation.icon(), + shortcutAnnotation.hotkey(), + shortcutAnnotation.parent(), + shortcutAnnotation.section(), + menuAction, + shortcutAnnotation.permissions())); + } + + } + + return shortCuts; + } + + private List scanMenuEntries(Object moduleInstance) { + + List entries = new ArrayList<>(); + + for (Method method : moduleInstance.getClass().getDeclaredMethods()) { + var menuAnn = method.getAnnotation(com.condation.cms.api.ui.annotations.MenuEntry.class); + if (menuAnn == null) { + continue; + } + + method.setAccessible(true); + UIAction menuAction = null; + + // 1. Methode hat @Action? + Action actionAnn = method.getAnnotation(Action.class); + if (actionAnn != null) { + menuAction = new UIHookAction(actionAnn.value(), Map.of()); + } // 2. @Hook in @MenuEntry + else if (!menuAnn.hookAction().value().isEmpty()) { + menuAction = new UIHookAction(menuAnn.hookAction().value(), Map.of()); + } // 3. @ScriptAction in @MenuEntry + else if (!menuAnn.scriptAction().module().isEmpty()) { + menuAction = new UIScriptAction(menuAnn.scriptAction().module(), menuAnn.scriptAction().function(), Map.of()); + } + + var entry = MenuEntry.builder() + .id(menuAnn.id()) + .name(menuAnn.name()) + .divider(menuAnn.divider()) + .position(menuAnn.position()) + .action(menuAction) + .children(new ArrayList<>()) + .permissions(Arrays.asList(menuAnn.permissions())) + .build(); + + entries.add(new EntryHolder(menuAnn.parent(), entry)); + } + + return entries; + } + + private void insertEntriesIntoMenu(Menu menu, List entries) { + Map index = new HashMap<>(); + entries.forEach(holder -> index.put(holder.entry().getId(), holder.entry())); + + // füge alle mit parent == "" oder null in die Wurzel ein + for (EntryHolder holder : entries) { + String parentId = holder.parent(); + MenuEntry entry = holder.entry(); + + if (parentId == null || parentId.isBlank()) { + menu.addMenuEntry(entry); + } else { + // Versuche Parent in fertigem Menü zu finden + Optional parentInMenu = findEntryById(menu, parentId); + + if (parentInMenu.isEmpty()) { + // Versuche in den noch nicht eingefügten Entries + MenuEntry parentInBatch = index.get(parentId); + if (parentInBatch != null) { + parentInBatch.getChildren().add(entry); + } else { + log.warn("Parent entry with ID '" + parentId + "' not found for menu entry '" + entry.getId() + "'"); + } + } else { + parentInMenu.get().addChildren(entry); + } + } + } + + // Jetzt alle "Wurzel"-Einträge, die nicht direkt im Menü sind, einfügen + for (EntryHolder holder : entries) { + String parentId = holder.parent(); + if (parentId == null || parentId.isBlank()) { + menu.addMenuEntry(holder.entry()); + } + } + } + + private Optional findEntryById(Menu menu, String id) { + if (menu.entries() == null) { + return Optional.empty(); + } + for (MenuEntry entry : menu.entries()) { + Optional result = findEntryByIdRecursive(entry, id); + if (result.isPresent()) { + return result; + } + } + return Optional.empty(); + } + + private Optional findEntryByIdRecursive(MenuEntry entry, String id) { + if (entry.getId().equals(id)) { + return Optional.of(entry); + } + if (entry.getChildren() == null) { + return Optional.empty(); + } + for (MenuEntry child : entry.getChildren()) { + Optional result = findEntryByIdRecursive(child, id); + if (result.isPresent()) { + return result; + } + } + return Optional.empty(); + } + + private record EntryHolder(String parent, MenuEntry entry) { + + } + + public record ShortCutHolder(String id, String title, String icon, String hotkey, String parent, String section, UIAction action, String[] permissions) { + + public String getActionDefinition() { + return action != null ? JSONUtil.toJson(action) : ""; + } + } +; +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/FormHelper.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/FormHelper.java new file mode 100644 index 000000000..d026a8b47 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/FormHelper.java @@ -0,0 +1,46 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.Map; + +/** + * + * @author thorstenmarx + */ +public class FormHelper { + + public static String getContent (Object formData) { + if (formData instanceof Map formMap) { + if ("markdown".equals(formMap.get("type"))) { + return (String)formMap.get("value"); + } else if ("code".equals(formMap.get("type"))) { + return (String)formMap.get("value"); + } else if ("easymde".equals(formMap.get("type"))) { + return (String)formMap.get("value"); + } + } + + return ""; + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/Helper.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/Helper.java new file mode 100644 index 000000000..3f7f9a01d --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/Helper.java @@ -0,0 +1,51 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.google.common.hash.Hashing; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +/** + * + * @author t.marx + */ +public abstract class Helper { + /** + * + * @author marx + */ + private static final SecureRandom RANDOM = new SecureRandom(); + + public static String hash(String value) { + return Hashing.sha256() + .hashString(value, StandardCharsets.UTF_8) + .toString(); + + } + + public static String randomString() { + return new BigInteger(130, RANDOM).toString(32); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MailerProvider.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MailerProvider.java new file mode 100644 index 000000000..3475594eb --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MailerProvider.java @@ -0,0 +1,68 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import org.simplejavamail.api.mailer.Mailer; +import org.simplejavamail.mailer.MailerBuilder; +import org.simplejavamail.api.mailer.config.TransportStrategy; + +public class MailerProvider { + + public static Mailer provide(String siteId) { + var key = normalizeHostId(siteId); + String host = getRequiredEnv("CMS_UI_SMTP_HOST_%s".formatted(key)); + int port = Integer.parseInt(getRequiredEnv("CMS_UI_SMTP_PORT_%s".formatted(key))); + String username = getRequiredEnv("CMS_UI_SMTP_USER_%s".formatted(key)); + String password = getRequiredEnv("CMS_UI_SMTP_PASS_%s".formatted(key)); + + String transport = System.getenv().getOrDefault("CMS_UI_SMTP_TRANSPORT_%s".formatted(key), "SMTP_TLS"); + + TransportStrategy strategy = switch (transport.toUpperCase()) { + case "SMTP_TLS" -> + TransportStrategy.SMTP_TLS; + case "SMTPS" -> + TransportStrategy.SMTPS; + case "SMTP_PLAIN" -> + TransportStrategy.SMTP; + default -> + throw new IllegalArgumentException("Unsupported SMTP_TRANSPORT: " + transport); + }; + + return MailerBuilder + .withSMTPServer(host, port, username, password) + .withTransportStrategy(strategy) + .buildMailer(); + } + + private static String getRequiredEnv(String name) { + String value = System.getenv(name); + if (value == null || value.isEmpty()) { + throw new IllegalStateException("Missing required environment variable: " + name); + } + return value; + } + + private static String normalizeHostId(String hostId) { + return hostId.toUpperCase().replaceAll("[^A-Z0-9]", "_"); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MetaConverter.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MetaConverter.java new file mode 100644 index 000000000..9c0ffa447 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MetaConverter.java @@ -0,0 +1,65 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.time.*; +import java.util.*; + +public class MetaConverter { + + public static Map convertMeta(Map> rawMeta) { + Map result = new HashMap<>(); + + for (Map.Entry> entry : rawMeta.entrySet()) { + String key = entry.getKey(); + Map field = entry.getValue(); + + Object typeObj = field.get("type"); + Object valObj = field.get("value"); + + if (!(typeObj instanceof String) || valObj == null) { + result.put(key, valObj); + continue; + } + + String type = ((String) typeObj).toLowerCase(); + String valueStr = valObj.toString(); + + try { + switch (type) { + case "date", "datetime" -> { + // expected ISO String like 2025-05-31T13:30:00Z + Instant instant = Instant.parse(valueStr); + result.put(key, Date.from(instant)); + } + default -> result.put(key, valObj); + } + } catch (Exception e) { + // Falls Parsing fehlschlägt → originaler Wert + result.put(key, valObj); + } + } + + return result; + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/RoleUtil.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/RoleUtil.java new file mode 100644 index 000000000..7bb5bbeb5 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/RoleUtil.java @@ -0,0 +1,53 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +public class RoleUtil { + + public static boolean hasAccess(String[] annotationRoles, String[] userRoles) { + return hasAccess(Arrays.asList(annotationRoles), Arrays.asList(userRoles)); + } + + public static boolean hasAccess(List roles, String[] userRoles) { + return hasAccess(roles, Arrays.asList(userRoles)); + } + + /** + * Checks whether the user has at least one role that is allowed by the + * annotation. + * + * @param annotationRoles The roles defined in the annotation (e.g., + * {"admin", "editor"}). + * @param userRoles The roles assigned to the current user (e.g., + * List.of("editor", "user")). + * @return true if the user has access, false otherwise. + */ + public static boolean hasAccess(List annotationRoles, List userRoles) { + return annotationRoles.stream() + .anyMatch(allowed -> userRoles.contains(allowed)); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TemplateEngine.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TemplateEngine.java new file mode 100644 index 000000000..6aa452a60 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TemplateEngine.java @@ -0,0 +1,56 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.cache.CacheManager; +import com.condation.cms.api.cache.CacheProvider; +import com.condation.cms.templates.CMSTemplateEngine; +import com.condation.cms.templates.TemplateEngineFactory; +import com.condation.cms.templates.loaders.ClasspathTemplateLoader; +import java.io.IOException; +import java.time.Duration; +import java.util.Map; + +/** + * + * @author thorstenmarx + */ +public class TemplateEngine { + + CMSTemplateEngine templateEngine; + + public TemplateEngine(CacheManager cacheManager) { + + templateEngine = TemplateEngineFactory + .newInstance(new ClasspathTemplateLoader("manager")) + .cache(cacheManager.get("ui/templates", new CacheManager.CacheConfig(100l, Duration.ofSeconds(60)))) + .defaultFilters() + .defaultTags() + .create(); + } + + public String render(String templateName, Map model) throws IOException { + var template = templateEngine.getTemplate(templateName); + + return template.evaluate(model); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TokenUtils.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TokenUtils.java new file mode 100644 index 000000000..321d0a84e --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TokenUtils.java @@ -0,0 +1,89 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.modules.ui.utils.json.UIGsonProvider; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +public class TokenUtils { + + public static Optional getPayload(String token, String secret) { + try { + String[] parts = token.split(":"); + if (parts.length != 2) { + return Optional.empty(); + } + + String base64Payload = parts[0]; + String signature = parts[1]; + + String expectedSig = hmacSha256(base64Payload, secret); + if (!MessageDigest.isEqual(expectedSig.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8))) { + return Optional.empty(); + } + + String json = new String(Base64.getUrlDecoder().decode(base64Payload), StandardCharsets.UTF_8); + Payload payload = UIGsonProvider.INSTANCE.fromJson(json, Payload.class); + + long now = Instant.now().getEpochSecond(); + if ((now - payload.timestamp()) >= 3600) { + return Optional.empty(); + } + + return Optional.of(payload); + } catch (Exception e) { + return Optional.empty(); + } + } + + public static String createToken(String username, String SECRET, Map payloadData) throws Exception { + Payload payload = new Payload(username, Instant.now().getEpochSecond(), payloadData); + String json = UIGsonProvider.INSTANCE.toJson(payload); + String base64Payload = Base64.getUrlEncoder().withoutPadding().encodeToString(json.getBytes(StandardCharsets.UTF_8)); + String signature = hmacSha256(base64Payload, SECRET); + return base64Payload + ":" + signature; + } + + public static String createToken(String username, String SECRET) throws Exception { + return createToken(username, SECRET, Collections.emptyMap()); + } + + private static String hmacSha256(String data, String key) throws Exception { + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + mac.init(secretKeySpec); + byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(hash); + } + + public record Payload(String username, long timestamp, Map data) { + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TranslationHelper.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TranslationHelper.java new file mode 100644 index 000000000..c77a6a274 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TranslationHelper.java @@ -0,0 +1,65 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.SiteProperties; +import com.condation.cms.api.TranslationProperties; +import java.util.List; + +/** + * + * @author thmar + */ +public class TranslationHelper { + private final TranslationProperties translationProperties; + private final SiteProperties siteProperties; + + public TranslationHelper(SiteProperties siteProperties) { + this.translationProperties = siteProperties.translation(); + this.siteProperties = siteProperties; + } + + public boolean isEnabled () { + return translationProperties.isEnabled(); + } + + public List getLanguages () { + return translationProperties.getLanguages(); + } + + public List getFilteredLanguages () { + return translationProperties.getLanguages().stream() + .filter(lang -> !lang.equals(siteProperties.language())) + .toList(); + } + + public List getMapping () { + return translationProperties.getMapping(); + } + + public List getFilteredMapping () { + return translationProperties.getMapping().stream() + .filter(mapping -> !mapping.language().equals(siteProperties.language()) ) + .toList(); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TranslationMerger.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TranslationMerger.java new file mode 100644 index 000000000..37b73d40c --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/TranslationMerger.java @@ -0,0 +1,47 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.*; + +public class TranslationMerger { + + /** + * Merges the entries of the given source map into the target map. + * The target map will be modified directly. + * + * @param source The map containing new values to merge + * @param target The map into which values will be merged + */ + public static void mergeTranslationMaps(Map> source, + Map> target) { + for (Map.Entry> languageEntry : source.entrySet()) { + String language = languageEntry.getKey(); + Map newTranslations = languageEntry.getValue(); + + // Ensure the inner map exists in the target + target.computeIfAbsent(language, k -> new HashMap<>()) + .putAll(newTranslations); // Overwrites existing keys if necessary + } + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIFileNameUtil.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIFileNameUtil.java new file mode 100644 index 000000000..331aee330 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIFileNameUtil.java @@ -0,0 +1,63 @@ +package com.condation.cms.modules.ui.utils; + +import java.util.Arrays; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +/** + * + * @author thorstenmarx + */ +public class UIFileNameUtil { + + public static String createSectionFileName(String parentUri, String section, String sectionItem) { + // Pfadteile per "/" splitten + String[] parts = parentUri.split("/"); + + // Letztes Segment = Dateiname + String fileName = parts[parts.length - 1]; + + // Basisname und Endung trennen + int dotIndex = fileName.lastIndexOf('.'); + String baseName; + String extension = ""; + + if (dotIndex > 0) { + baseName = fileName.substring(0, dotIndex); + extension = fileName.substring(dotIndex); // inkl. Punkt + } else { + baseName = fileName; + } + + // Neuen Dateinamen erstellen + String newFileName = baseName + "." + section + "." + sectionItem + extension; + + // Pfad wieder zusammenbauen + if (parts.length > 1) { + String pathPrefix = String.join("/", Arrays.copyOf(parts, parts.length - 1)); + return pathPrefix + "/" + newFileName; + } else { + return newFileName; + } + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIHooks.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIHooks.java new file mode 100644 index 000000000..fb8486db1 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIHooks.java @@ -0,0 +1,75 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.hooks.HookSystem; +import com.condation.cms.api.ui.elements.ContentTypes; +import com.condation.cms.api.ui.elements.MediaForms; +import com.condation.cms.api.ui.elements.Menu; +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author thorstenmarx + */ +public class UIHooks { + + public static final String HOOK_MENU = "module/ui/menu"; + public static final String HOOK_TRANSLATIONS = "module/ui/translations"; + public static final String HOOK_REGISTER_CONTENT_TYPES = "manager/contentTypes/register"; + public static final String HOOK_REGISTER_MEDIA_FORMS = "manager/media/forms"; + + private final HookSystem hookSystem; + + public UIHooks (final HookSystem hookSystem) { + this.hookSystem = hookSystem; + } + + public ContentTypes contentTypes () { + var contentTypes = new ContentTypes(); + + return hookSystem.filter(HOOK_REGISTER_CONTENT_TYPES, contentTypes).value(); + } + + public MediaForms mediaForms () { + var mediaForms = new MediaForms(); + + return hookSystem.filter(HOOK_REGISTER_MEDIA_FORMS, mediaForms).value(); + } + + public Menu menu() { + var menu = new Menu(); + + menu = hookSystem.filter(HOOK_MENU, menu).value(); + + return menu; + } + + public Map> translations () { + Map> translations = new HashMap<>(Map.of( + "de", new HashMap<>(), + "en", new HashMap<>() + )); + return hookSystem.filter(HOOK_TRANSLATIONS, translations).value(); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIPathUtil.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIPathUtil.java new file mode 100644 index 000000000..be4aaa183 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIPathUtil.java @@ -0,0 +1,99 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.utils.FileUtils; +import com.github.slugify.Slugify; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import lombok.extern.slf4j.Slf4j; + +/** + * + * @author t.marx + */ +@Slf4j +public class UIPathUtil { + + public static Slugify SLUGIFY = Slugify.builder() + .customReplacement("ä", "ae") + .customReplacement("Ä", "ae") + .customReplacement("ü", "ue") + .customReplacement("Ü", "ue") + .customReplacement("ö", "oe") + .customReplacement("Ö", "oe") + .customReplacement("ß", "ss") + .lowerCase(true).build(); + + public static String toUri(final Path contentFile, final Path contentBase) { + Path relativize = contentBase.relativize(contentFile); +// if (Files.isDirectory(contentFile)) { +// relativize = relativize.resolve("index.md"); +// } + var uri = relativize.toString(); + uri = uri.replaceAll("\\\\", "/"); + return uri; + } + + public static boolean isChild(Path possibleParent, Path maybeChild) throws IOException { + return maybeChild.toFile().getCanonicalPath().startsWith(possibleParent.toFile().getCanonicalPath()); + } + + public static boolean hasChildren(Path path) { + try { + if (!Files.isDirectory(path)) { + return false; + } + return FileUtils.countChildren(path) > 0; + } catch (IOException ex) { + log.error(null, ex); + } + return false; + } + + public static String getType(Path path) { + if (Files.isDirectory(path)) { + return "folder"; + } else { + return "file"; + } + } + + public static String slugify (String input) { + return SLUGIFY.slugify(input); + } + + public static String toValidFilename(String input) { + + input = input.toLowerCase(); + String extension = ""; + if (input.endsWith(".md")) { + extension = input.substring(input.lastIndexOf(".")); + input = input.substring(0, input.lastIndexOf(".")); + } + + var slugified = slugify(input); + + return slugified + extension; + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/json/FileTypeAdapterFactory.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/json/FileTypeAdapterFactory.java new file mode 100644 index 000000000..8774403f3 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/json/FileTypeAdapterFactory.java @@ -0,0 +1,76 @@ +package com.condation.cms.modules.ui.utils.json; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.modules.ui.extensionpoints.remotemethods.RemoteFileEnpoints; +import com.google.gson.*; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +/** + * + * @author thorstenmarx + */ +public class FileTypeAdapterFactory implements TypeAdapterFactory { + + @Override + @SuppressWarnings("unchecked") + public TypeAdapter create(Gson gson, TypeToken typeToken) { + Class rawType = typeToken.getRawType(); + + // Nur anpassen, wenn es File implementiert + if (!RemoteFileEnpoints.File.class.isAssignableFrom(rawType)) { + return null; + } + + // Hole den Standardadapter für die konkrete Klasse (z. B. Content.class) + TypeAdapter delegate = gson.getDelegateAdapter(this, typeToken); + + return new TypeAdapter<>() { + @Override + public void write(JsonWriter out, T value) throws IOException { + // Serialisiere Objekt normal + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + // Ergänze Interface-Methoden + RemoteFileEnpoints.File file = (RemoteFileEnpoints.File) value; + jsonObject.addProperty("directory", file.directory()); + jsonObject.addProperty("media", file.media()); + jsonObject.addProperty("content", file.content()); + + // Schreibe das zusammengesetzte JSON + Streams.write(jsonObject, out); + } + + @Override + public T read(JsonReader in) throws IOException { + // Standard-Deserialisierung + return delegate.read(in); + } + }; + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/json/UIGsonProvider.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/json/UIGsonProvider.java new file mode 100644 index 000000000..15f28bd61 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/json/UIGsonProvider.java @@ -0,0 +1,64 @@ +package com.condation.cms.modules.ui.utils.json; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.ToNumberPolicy; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; +import java.lang.reflect.Type; + +/** + * + * @author thorstenmarx + */ +public class UIGsonProvider { + + public static final Gson INSTANCE = new GsonBuilder() + .setPrettyPrinting() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + .registerTypeAdapter(Date.class, new UtcDateSerializer()) + .registerTypeAdapterFactory(new FileTypeAdapterFactory()) + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .create(); + + public static class UtcDateSerializer implements JsonSerializer { + + private final SimpleDateFormat sdf; + + public UtcDateSerializer() { + sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + @Override + public JsonElement serialize(Date date, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(sdf.format(date)); + } + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/template/UILinkFunction.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/template/UILinkFunction.java new file mode 100644 index 000000000..2f30aaf85 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/template/UILinkFunction.java @@ -0,0 +1,41 @@ +package com.condation.cms.modules.ui.utils.template; + +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.feature.features.SitePropertiesFeature; +import com.condation.cms.api.request.RequestContext; +import com.condation.cms.api.utils.HTTPUtil; +import lombok.RequiredArgsConstructor; + +/** + * + * @author thmar + */ +@RequiredArgsConstructor +public class UILinkFunction { + private final RequestContext requestContext; + + public String createUrl (String url) { + return HTTPUtil.modifyUrl(url, requestContext.get(SitePropertiesFeature.class).siteProperties()); + } +} diff --git a/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.js b/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.js new file mode 100644 index 000000000..da7732ef6 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.js @@ -0,0 +1,112 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { i18n } from "../../js/modules/localization.js"; +import { openModal } from "../../js/modules/modal.js"; +import { reloadPreview } from "../../js/modules/preview.utils.js"; +import { getMediaMetaData, setMediaMetaData } from "../../js/modules/rpc/rpc-media.js"; +import { showToast } from "../../js/modules/toast.js"; +export async function runAction(params) { + var uri = params.options.uri || null; + var mediaUrl = removeFormatParamFromUrl(uri); + const template = ` +
    + + +
    + `; + var mediaMeta = (await getMediaMetaData({ image: params.options.uri })).result.meta; + const focal = mediaMeta?.focal || {}; + const focalX = typeof focal.x === 'number' ? focal.x : 0.5; + const focalY = typeof focal.y === 'number' ? focal.y : 0.5; + openModal({ + title: i18n.t("media.focal.title", "Edit focal point"), + body: template, + onCancel: (event) => { }, + onOk: async (event) => { + var setMetaResponse = await setMediaMetaData({ + image: mediaUrl, + meta: { + "focal.x": { + "type": "number", + "value": focal.x + }, + "focal.y": { + "type": "number", + "value": focal.y + } + } + }); + showToast({ + title: i18n.t('manager.actions.media.focal-point.toast.title', "Media focal point updated"), + message: i18n.t('manager.actions.media.focal-point.toast.message', "The focal point was successfuly updated."), + type: 'success', + timeout: 3000 + }); + reloadPreview(); + }, + onShow: () => { + const wrapper = document.getElementById("cmsFocalWrapper"); + const image = document.getElementById("cms-image"); + const point = document.getElementById("cmsFocalPoint"); + if (image.complete) { + setFocalPoint(image, point, focalX, focalY); + } + else { + image.onload = () => setFocalPoint(image, point, focalX, focalY); + } + wrapper.addEventListener("click", function (e) { + const rect = image.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const relX = (x / rect.width).toFixed(4); + const relY = (y / rect.height).toFixed(4); + // Punkt anzeigen + point.style.left = `${x}px`; + point.style.top = `${y}px`; + point.style.display = "block"; + focal.x = parseFloat(relX); + focal.y = parseFloat(relY); + // Ausgabe + console.log(`Focal Point: x: ${relX}, y: ${relY}`); + }); + } + }); +} +const setFocalPoint = (image, point, relX, relY) => { + const rect = image.getBoundingClientRect(); + const x = rect.width * relX; + const y = rect.height * relY; + point.style.left = `${x}px`; + point.style.top = `${y}px`; + point.style.display = "block"; +}; +const removeFormatParamFromUrl = (url) => { + try { + const parsedUrl = new URL(url, window.location.origin); // Fallback-Basis falls nur Pfad übergeben wird + parsedUrl.searchParams.delete("format"); + return parsedUrl.toString(); + } + catch (e) { + console.warn("Ungültige URL:", url); + return url; // Fallback: gib Original zurück + } +}; diff --git a/modules/ui-module/src/main/resources/manager/actions/media/edit-media-form.js b/modules/ui-module/src/main/resources/manager/actions/media/edit-media-form.js new file mode 100644 index 000000000..1ac188040 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/media/edit-media-form.js @@ -0,0 +1,88 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { openSidebar } from '../../js/modules/sidebar.js'; +import { createForm } from '../../js/modules/form/forms.js'; +import { showToast } from '../../js/modules/toast.js'; +import { setMeta } from '../../js/modules/rpc/rpc-content.js'; +import { reloadPreview } from '../../js/modules/preview.utils.js'; +import { i18n } from '../../js/modules/localization.js'; +import { getMediaForm } from '../../js/modules/rpc/rpc-manager.js'; +import { getMediaMetaData, setMediaMetaData } from '../../js/modules/rpc/rpc-media.js'; +export async function runAction(params) { + var mediaForm = (await getMediaForm({ + form: params.options.form || 'meta' + })).result; + const fields = [ + ...mediaForm?.form?.fields + ]; + const values = { + ...(await getMediaMetaData({ image: params.options.image })).result.meta + }; + const form = createForm({ + fields: fields, + values: values + }); + openSidebar({ + title: 'Media attributes', + body: 'modal body', + form: form, + onCancel: (event) => { }, + onOk: async (event) => { + var updateData = form.getData(); + var setMetaResponse = await setMediaMetaData({ + image: params.options.image, + meta: updateData + }); + showToast({ + title: i18n.t('manager.actions.media.edit-media-form.toast.title', "Media meta updated"), + message: i18n.t('manager.actions.media.edit-media-form.toast.message', "The media meta have been updated successfully."), + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview(); + } + }); +} +/** + * Retrieves a nested value from an object using a dot-notated path like "meta.title" + * @param {object} sourceObj - The object to retrieve the value from + * @param {string} path - Dot-notated string path, e.g., "meta.title" + * @returns {*} - The value found at the given path, or undefined if not found + */ +const getValueByPath = (sourceObj, path) => { + return path.split('.').reduce((acc, part) => acc?.[part], sourceObj); +}; +/** + * Builds a values object from an array of form fields + * @param {Array} fields - Array of form field objects, each with a .name property + * @param {object} sourceObj - The source object to extract the values from + * @returns {object} values - An object mapping field names to their corresponding values + */ +const buildValuesFromFields = (fields, sourceObj) => { + const values = {}; + for (const field of fields) { + if (!field.name) + continue; + values[field.name] = getValueByPath(sourceObj, field.name); + } + return values; +}; diff --git a/modules/ui-module/src/main/resources/manager/actions/media/select-media.js b/modules/ui-module/src/main/resources/manager/actions/media/select-media.js new file mode 100644 index 000000000..7fa55de49 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/media/select-media.js @@ -0,0 +1,68 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { openFileBrowser } from "../../js/modules/filebrowser.js"; +import { i18n } from "../../js/modules/localization.js"; +import { getPreviewUrl, reloadPreview } from "../../js/modules/preview.utils.js"; +import { getContentNode, setMeta } from "../../js/modules/rpc/rpc-content.js"; +import { showToast } from "../../js/modules/toast.js"; +export async function runAction(params) { + var uri = null; + if (params.options.uri) { + uri = params.options.uri; + } + else { + const contentNode = await getContentNode({ + url: getPreviewUrl() + }); + uri = contentNode.result.uri; + } + openFileBrowser({ + type: "assets", + filter: (file) => { + return file.media || file.directory; + }, + onSelect: async (file) => { + if (file && file.uri) { + var selectedFile = file.uri; // Use the file's URI + if (file.uri.startsWith("/")) { + selectedFile = file.uri.substring(1); // Remove leading slash if present + } + var updateData = {}; + updateData[params.options.metaElement] = { + type: 'media', + value: selectedFile + }; + var setMetaResponse = await setMeta({ + uri: uri, + meta: updateData + }); + showToast({ + title: i18n.t('manager.actions.media.select-media.toast.title', "Media updated"), + message: i18n.t('manager.actions.media.select-media.toast.message', "New media has been updated successfully."), + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview(); + } + } + }); +} diff --git a/modules/ui-module/src/main/resources/manager/actions/page/add-section.js b/modules/ui-module/src/main/resources/manager/actions/page/add-section.js new file mode 100644 index 000000000..c4458c927 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/page/add-section.js @@ -0,0 +1,130 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { openModal } from '../../js/modules/modal.js'; +import { showToast } from '../../js/modules/toast.js'; +import { addSection, getContentNode } from '../../js/modules/rpc/rpc-content.js'; +import { getPreviewUrl, reloadPreview } from '../../js/modules/preview.utils.js'; +import Handlebars from '../../js/libs/handlebars.min.js'; +import { i18n } from '../../js/modules/localization.js'; +import { getSectionTemplates } from '../../js/modules/rpc/rpc-manager.js'; +export async function runAction(params) { + const contentNode = await getContentNode({ + url: getPreviewUrl() + }); + var template = Handlebars.compile(` +
    + + +
    + + `); + var sectionsResponse = await getSectionTemplates({ + section: params.sectionName + }); + openModal({ + title: i18n.t("addsection.titles.modal", 'Add section'), + body: template({ + templates: sectionsResponse.result, + }), + fullscreen: false, + onCancel: (event) => { }, + validate: () => validate(contentNode, params.sectionName), + onOk: async (event) => { + var result = await createSection(contentNode.result.uri, params.sectionName); + if (result) { + showToast({ + title: i18n.t("manager.actions.addsection.titles.alert", "Create section"), + message: i18n.t("manager.actions.addsection.alerts.success.message", "Section successfuly created."), + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + await new Promise(resolve => setTimeout(resolve, 1000)); + reloadPreview(); + } + else { + showToast({ + title: i18n.t("manager.actions.addsection.titles.alert", 'Create section'), + message: i18n.t("manager.actions.addsection.alerts.error.message", "Section not created."), + type: 'warning', // optional: info | success | warning | error + timeout: 3000 + }); + } + } + }); +} +const getSectionItemName = () => { + return document.getElementById("cms-section-name").value; +}; +const validate = (contentNode, targetSectionName) => { + const template = document.getElementById("cms-section-template-selection").value; + if (template === "000") { + showToast({ + title: i18n.t("manager.actions.addsection.titles.alert", 'Create section'), + message: i18n.t("manager.actions.addsection.alerts.notemplate.message", "No template selected."), + type: 'error', // optional: info | success | warning | error + timeout: 3000 + }); + return false; + } + const sectionItemName = getSectionItemName(); + if (sectionItemName === "" || sectionItemName === null) { + showToast({ + title: i18n.t("manager.actions.addsection.titles.alert", 'Create section'), + message: i18n.t("manager.actions.addsection.alerts.noname.message", "No section name provided."), + type: 'error', + timeout: 3000 + }); + return false; + } + return true; +}; +function isUriInSection(data, sectionKey, targetUri) { + if (!data || + !data.result || + !data.result.sections || + typeof data.result.sections !== 'object') { + return false; + } + const sectionArray = data.result.sections[sectionKey]; + if (!Array.isArray(sectionArray)) { + return false; + } + return sectionArray.some(item => item.uri === targetUri); +} +const createSection = async (parentUri, parentSectionName) => { + const template = document.getElementById("cms-section-template-selection").value; + if (template === "000") { + return false; + } + await addSection({ + parentUri: parentUri, + sectionItemName: getSectionItemName(), + parentSectionName: parentSectionName, + template: template + }); + return true; +}; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/create-page.js b/modules/ui-module/src/main/resources/manager/actions/page/create-page.js new file mode 100644 index 000000000..d744915fa --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/page/create-page.js @@ -0,0 +1,28 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { openFileBrowser } from '../../js/modules/filebrowser.js'; +// hook.js +export async function runAction(params) { + openFileBrowser({ + type: "content" + }); +} diff --git a/modules/ui-module/src/main/resources/manager/actions/page/delete-section.js b/modules/ui-module/src/main/resources/manager/actions/page/delete-section.js new file mode 100644 index 000000000..bb3bbb595 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/page/delete-section.js @@ -0,0 +1,59 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { i18n } from '../../js/modules/localization.js'; +import { alertConfirm } from '../../js/modules/alerts.js'; +import { deleteSection } from '../../js/modules/rpc/rpc-content.js'; +import { showToast } from '../../js/modules/toast.js'; +import { reloadPreview } from '../../js/modules/preview.utils.js'; +export async function runAction(params) { + var confimred = await alertConfirm({ + title: i18n.t("filebrowser.delete.confirm.title", "Are you sure?"), + message: i18n.t("filebrowser.delete.confirm.message", "You won't be able to revert this!"), + confirmText: i18n.t("filebrowser.delete.confirm.yes", "Yes, delete it!"), + cancelText: i18n.t("filebrowser.delete.confirm.no", "No, cancel!") + }); + if (!confimred) { + return; + } + const sectionUri = params.sectionUri; + deleteSection({ + uri: sectionUri + }).then((response) => { + if (response.error) { + showToast({ + title: i18n.t("manager.actions.section.delete.error.title", "Error deleting section"), + message: i18n.t("manager.actions.section.delete.error.message", "Error deleting section"), + type: 'error', + timeout: 3000 + }); + } + else { + showToast({ + title: i18n.t("manager.actions.section.delete.success.title", "Delete section"), + message: i18n.t("manager.actions.section.delete.success.message", "Section deleted successfully"), + type: 'success', + timeout: 3000 + }); + reloadPreview(); + } + }); +} diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-content.js b/modules/ui-module/src/main/resources/manager/actions/page/edit-content.js new file mode 100644 index 000000000..96d97b90b --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-content.js @@ -0,0 +1,77 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { openModal } from '../../js/modules/modal.js'; +import { createForm } from '../../js/modules/form/forms.js'; +import { getPreviewUrl, reloadPreview } from '../../js/modules/preview.utils.js'; +import { getContentNode, getContent, setContent } from '../../js/modules/rpc/rpc-content.js'; +import { i18n } from '../../js/modules/localization.js'; +import { showToast } from '../../js/modules/toast.js'; +// hook.js +export async function runAction(params) { + var uri = null; + if (params.uri) { + uri = params.uri; + } + else { + const contentNode = await getContentNode({ + url: getPreviewUrl() + }); + uri = contentNode.result.uri; + } + const nodeContent = await getContent({ + uri: uri + }); + const form = createForm({ + fields: [ + { + type: params.editor, + name: 'content', + title: 'Main content', + height: '80%' + } + ], + values: { + "content": nodeContent?.result?.content + } + }); + openModal({ + title: 'Edit Content', + body: 'modal body', + form: form, + fullscreen: true, + onCancel: (event) => { }, + onOk: async (event) => { + var updateData = form.getData(); + var setContentResponse = await setContent({ + uri: uri, + content: updateData.content + }); + showToast({ + title: i18n.t('manager.actions.page.edit-content.toast.title', "Content updated"), + message: i18n.t('manager.actions.page.edit-content.toast.message', "The content has been updated successfully."), + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview(); + } + }); +} diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-list.js b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-list.js new file mode 100644 index 000000000..f214138a6 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-list.js @@ -0,0 +1,78 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { createForm } from '../../js/modules/form/forms.js'; +import { showToast } from '../../js/modules/toast.js'; +import { getPreviewUrl, reloadPreview } from '../../js/modules/preview.utils.js'; +import { getValueByPath } from '../../js/modules/node.js'; +import { getContentNode, getContent, setMeta } from '../../js/modules/rpc/rpc-content.js'; +import { i18n } from '../../js/modules/localization.js'; +import { openSidebar } from '../../js/modules/sidebar.js'; +// hook.js +export async function runAction(params) { + var uri = null; + if (params.uri) { + uri = params.uri; + } + else { + const contentNode = await getContentNode({ + url: getPreviewUrl() + }); + uri = contentNode.result.uri; + } + const getContentResponse = await getContent({ + uri: uri + }); + let formDefinition = { + fields: [], + values: {} + }; + params.attributes.forEach(attr => { + formDefinition.values[attr.name] = getValueByPath(getContentResponse?.result?.meta, attr.name); + formDefinition.fields.push({ + type: attr.editor, + name: attr.name, + title: attr.name, + options: attr.options ? attr.options : {} + }); + }); + const form = createForm(formDefinition); + openSidebar({ + title: 'Edit meta attribute', + body: 'modal body', + form: form, + onCancel: (event) => { }, + onOk: async (event) => { + var updateData = form.getData(); + var setMetaResponse = await setMeta({ + uri: uri, + meta: updateData + }); + showToast({ + title: i18n.t('manager.actions.page.edit-metaattribute-list.toast.title', "MetaData updated"), + message: i18n.t('manager.actions.page.edit-metaattribute-list.toast.message', "The metadata has been updated successfully."), + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview(); + } + }); +} diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute.js b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute.js new file mode 100644 index 000000000..001169192 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute.js @@ -0,0 +1,77 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { createForm } from '../../js/modules/form/forms.js'; +import { showToast } from '../../js/modules/toast.js'; +import { getPreviewUrl, reloadPreview } from '../../js/modules/preview.utils.js'; +import { getValueByPath } from '../../js/modules/node.js'; +import { getContentNode, setMeta, getContent } from '../../js/modules/rpc/rpc-content.js'; +import { i18n } from '../../js/modules/localization.js'; +import { openSidebar } from '../../js/modules/sidebar.js'; +// hook.js +export async function runAction(params) { + var uri = null; + if (params.uri) { + uri = params.uri; + } + else { + const contentNode = await getContentNode({ + url: getPreviewUrl() + }); + uri = contentNode.result.uri; + } + const getContentResponse = await getContent({ + uri: uri + }); + let formDefinition = { + fields: [ + { + type: params.editor, + options: params.options ? params.options : {}, + name: params.attribute, + title: "Edit attribute: " + params.attribute + } + ], + values: {} + }; + formDefinition.values[params.attribute] = getValueByPath(getContentResponse?.result?.meta, params.attribute); + const form = createForm(formDefinition); + openSidebar({ + title: 'Edit meta attribute', + body: 'modal body', + form: form, + onCancel: (event) => { }, + onOk: async (event) => { + var updateData = form.getData(); + var setMetaResponse = await setMeta({ + uri: uri, + meta: updateData + }); + showToast({ + title: i18n.t('manager.actions.page.edit-metaattribute.toast.title', "MetaData updated"), + message: i18n.t('manager.actions.page.edit-metaattribute.toast.message', "The metadata has been updated successfully."), + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview(); + } + }); +} diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-page-settings.js b/modules/ui-module/src/main/resources/manager/actions/page/edit-page-settings.js new file mode 100644 index 000000000..bb8d7961a --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-page-settings.js @@ -0,0 +1,108 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { openSidebar } from '../../js/modules/sidebar.js'; +import { createForm } from '../../js/modules/form/forms.js'; +import { showToast } from '../../js/modules/toast.js'; +import { getContentNode, setMeta, getContent } from '../../js/modules/rpc/rpc-content.js'; +import { getPreviewUrl, reloadPreview } from '../../js/modules/preview.utils.js'; +import { i18n } from '../../js/modules/localization.js'; +import { getPageTemplates } from '../../js/modules/rpc/rpc-manager.js'; +import { buildValuesFromFields } from '../../js/modules/node.js'; +const DEFAULT_FIELDS = [ + { + type: 'text', + name: 'title', + title: 'Title' + }, + { + type: 'select', + name: 'published', + title: 'Published', + options: { + choices: [ + { label: 'No', value: false }, + { label: 'Yes', value: true } + ] + } + }, + { + type: 'datetime', + name: 'publish_date', + title: 'Publish Date', + }, + { + type: 'datetime', + name: 'unpublish_date', + title: 'Unpublish Date', + } +]; +export async function runAction(params) { + const contentNode = await getContentNode({ + url: getPreviewUrl() + }); + const getContentResponse = await getContent({ + uri: contentNode.result.uri + }); + var pageTemplates = (await getPageTemplates()).result; + var selected = pageTemplates.filter(pageTemplate => pageTemplate.template === getContentResponse?.result?.meta?.template); + var pageSettingsForm = []; + if (selected.length === 1) { + pageSettingsForm = selected[0].data?.forms?.settings ? selected[0].data.forms.settings : []; + } + //const previewMetaForm = getMetaForm() + const fields = [ + ...DEFAULT_FIELDS, + ...pageSettingsForm + ]; + const values = { + 'title': getContentResponse?.result?.meta?.title, + 'published': getContentResponse?.result?.meta?.published, + 'publish_date': getContentResponse?.result?.meta?.publish_date, + 'unpublish_date': getContentResponse?.result?.meta?.unpublish_date, + ...buildValuesFromFields(pageSettingsForm, getContentResponse?.result?.meta) + }; + const form = createForm({ + fields: fields, + values: values + }); + openSidebar({ + title: 'Page settings', + body: 'modal body', + form: form, + resizable: true, + onCancel: (event) => { }, + onOk: async (event) => { + var updateData = form.getData(); + var setMetaResponse = await setMeta({ + uri: contentNode.result.uri, + meta: updateData + }); + showToast({ + title: i18n.t('manager.actions.page.edit-page-settings.toast.title', "Page settings updated"), + message: i18n.t('manager.actions.page.edit-page-settings.toast.message', "The page settings have been updated successfully."), + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview(); + } + }); +} diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.js b/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.js new file mode 100644 index 000000000..7087b5cb2 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.js @@ -0,0 +1,94 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { openModal } from '../../js/modules/modal.js'; +import { showToast } from '../../js/modules/toast.js'; +import { getPreviewUrl, reloadPreview } from '../../js/modules/preview.utils.js'; +import { Sortable } from '../../js/libs/sortablejs.min.js'; +import Handlebars from '../../js/libs/handlebars.min.js'; +import { getContentNode, setMetaBatch } from '../../js/modules/rpc/rpc-content.js'; +// hook.js +export async function runAction(params) { + const contentNode = await getContentNode({ + url: getPreviewUrl() + }); + var template = Handlebars.compile(` +
      + {{#each sections}} +
    • + {{#if data.title}} + {{data.title}} + {{else}} + {{uri}} + {{/if}} +
    • + {{/each}} +
    + `); + var sections = []; + if (contentNode.result.sections[params.sectionName]) { + var sections = contentNode.result.sections[params.sectionName]; + } + sections = sections.sort((a, b) => a.index - b.index); + openModal({ + title: 'Edit Sections', + body: template({ sections: sections }), + fullscreen: false, + onCancel: (event) => { }, + onOk: async (event) => { + await saveSections(); + showToast({ + title: 'Sections saved', + message: 'Sections successfuly saved.', + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + await new Promise(resolve => setTimeout(resolve, 1000)); + reloadPreview(); + }, + onShow: () => { + document.querySelectorAll(".cms-sortable").forEach(elem => { + var sortable = Sortable.create(elem, { + handle: '[data-cms-section-handle]' + }); + }); + } + }); +} +const saveSections = async () => { + const items = document.querySelectorAll(".cms-sortable li"); + var updates = []; + for (const [index, el] of Array.from(items).entries()) { + var uri = el.dataset.cmsSectionUri; + updates.push({ + uri: uri, + meta: { + "layout.order": { + type: 'number', + value: parseInt(index) + } + } + }); + } + await setMetaBatch({ + updates: updates + }); +}; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/manage-assets.js b/modules/ui-module/src/main/resources/manager/actions/page/manage-assets.js new file mode 100644 index 000000000..304c50623 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/page/manage-assets.js @@ -0,0 +1,28 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { openFileBrowser } from '../../js/modules/filebrowser.js'; +// hook.js +export async function runAction(params) { + openFileBrowser({ + type: "assets" + }); +} diff --git a/modules/ui-module/src/main/resources/manager/actions/page/section-set-published.js b/modules/ui-module/src/main/resources/manager/actions/page/section-set-published.js new file mode 100644 index 000000000..b2e8df77c --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/page/section-set-published.js @@ -0,0 +1,36 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { reloadPreview } from "../../js/modules/preview.utils"; +import { setMeta } from "../../js/modules/rpc/rpc-content"; +export async function runAction(params) { + var request = { + uri: params.sectionUri, + meta: { + published: { + type: "select", + value: params.published ? true : false, + } + } + }; + var setMetaResponse = await setMeta(request); + reloadPreview(); +} diff --git a/modules/ui-module/src/main/resources/manager/actions/page/translations.js b/modules/ui-module/src/main/resources/manager/actions/page/translations.js new file mode 100644 index 000000000..19b11e6fb --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/page/translations.js @@ -0,0 +1,117 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { openFileBrowser } from '../../js/modules/filebrowser.js'; +import { i18n } from '../../js/modules/localization.js'; +import { openModal } from '../../js/modules/modal.js'; +import { getPreviewUrl } from '../../js/modules/preview.utils.js'; +import { getContentNode } from '../../js/modules/rpc/rpc-content.js'; +import { addTranslation, getTranslations } from '../../js/modules/rpc/rpc-translation.js'; +import { showToast } from '../../js/modules/toast.js'; +// hook.js +export async function runAction(params) { + const contentNode = await getContentNode({ + url: getPreviewUrl() + }); + const uri = contentNode.result.uri; + var translations = await getTranslations({ uri: uri }); + console.log('Translations:', translations); + var modelContent = createTranslationsTable(translations.translations); + openModal({ + title: 'Manage Translations', + body: modelContent, + onCancel: (event) => { }, + onOk: async (event) => { + }, + onShow: async (modalElement) => { + modalElement.querySelectorAll('button[data-action]').forEach(button => { + button.addEventListener('click', async (e) => { + const action = e.currentTarget.getAttribute('data-action'); + const siteId = e.currentTarget.getAttribute('data-id'); + const lang = e.currentTarget.getAttribute('data-lang'); + if (action === 'select') { + // Open file browser to select existing translation + openFileBrowser({ + siteId: siteId || '', + type: 'content', + onSelect: async (file) => { + console.log('Selected translation file:', file); + if (file && file.uri) { + var selectedFile = file.uri; // Use the file's URI + await addTranslation({ + uri: uri, + language: lang || '', + translationUri: selectedFile + }); + showToast({ + title: i18n.t('manager.translation.added.title', "Translation Added"), + message: i18n.t('manager.translation.added.message', "Translation successfuly added."), + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + } + } + }); + } + }); + }); + } + }); +} +function createTranslationsTable(translations) { + const table = document.createElement('table'); + table.className = 'table table-striped table-bordered'; + table.innerHTML = ` + + + Language + Site + Status + Actions + + + + ${translations.map(translation => createTranslationRow(translation)).join('')} + + `; + return table.outerHTML; +} +function createTranslationRow(translation) { + const status = translation.managerDeepLink ? `Linked` : 'Not linked'; + return ` + + + ${translation.site} + ${status} + + ${getActionButtons(translation)} + + + `; +} +function getActionButtons(translation) { + let buttons = ''; + buttons += ``; + if (translation.url) { + buttons += ``; + } + return buttons; +} diff --git a/modules/ui-module/src/main/resources/manager/actions/reload-preview.js b/modules/ui-module/src/main/resources/manager/actions/reload-preview.js new file mode 100644 index 000000000..bc8cce531 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/reload-preview.js @@ -0,0 +1,25 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { reloadPreview } from "../js/modules/preview.utils"; +export async function runAction(params) { + reloadPreview(); +} diff --git a/modules/ui-module/src/main/resources/manager/actions/site-change.js b/modules/ui-module/src/main/resources/manager/actions/site-change.js new file mode 100644 index 000000000..757d5edcf --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/site-change.js @@ -0,0 +1,24 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +export async function runAction(parameters) { + window.location.href = parameters.href; +} diff --git a/modules/ui-module/src/main/resources/manager/actions/test-command.js b/modules/ui-module/src/main/resources/manager/actions/test-command.js new file mode 100644 index 000000000..92cc3a2c3 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/test-command.js @@ -0,0 +1,33 @@ +/*- + * #%L + * ui-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import { executeCommand } from '../../js/modules/system-commands.js'; +import { getPreviewUrl } from '../../js/modules/preview.utils.js'; +// hook.js +export async function runAction(params) { + var contentNode = await executeCommand({ + command: "getContentNode", + parameters: { + url: getPreviewUrl() + } + }); + console.log(contentNode); +} diff --git a/test-server/themes/test/assets/bootstrap-icons-1.11.1/bootstrap-icons.min.css b/modules/ui-module/src/main/resources/manager/bootstrap/bootstrap-icons.min.css similarity index 98% rename from test-server/themes/test/assets/bootstrap-icons-1.11.1/bootstrap-icons.min.css rename to modules/ui-module/src/main/resources/manager/bootstrap/bootstrap-icons.min.css index 0f6d8ac7c..706a5c8b8 100644 --- a/test-server/themes/test/assets/bootstrap-icons-1.11.1/bootstrap-icons.min.css +++ b/modules/ui-module/src/main/resources/manager/bootstrap/bootstrap-icons.min.css @@ -1,5 +1,5 @@ /*! - * Bootstrap Icons v1.11.1 (https://icons.getbootstrap.com/) - * Copyright 2019-2023 The Bootstrap Authors + * Bootstrap Icons v1.13.1 (https://icons.getbootstrap.com/) + * Copyright 2019-2024 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) - */@font-face{font-display:block;font-family:bootstrap-icons;src:url("fonts/bootstrap-icons.woff2?2820a3852bdb9a5832199cc61cec4e65") format("woff2"),url("fonts/bootstrap-icons.woff?2820a3852bdb9a5832199cc61cec4e65") format("woff")}.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bi-123::before{content:"\f67f"}.bi-alarm-fill::before{content:"\f101"}.bi-alarm::before{content:"\f102"}.bi-align-bottom::before{content:"\f103"}.bi-align-center::before{content:"\f104"}.bi-align-end::before{content:"\f105"}.bi-align-middle::before{content:"\f106"}.bi-align-start::before{content:"\f107"}.bi-align-top::before{content:"\f108"}.bi-alt::before{content:"\f109"}.bi-app-indicator::before{content:"\f10a"}.bi-app::before{content:"\f10b"}.bi-archive-fill::before{content:"\f10c"}.bi-archive::before{content:"\f10d"}.bi-arrow-90deg-down::before{content:"\f10e"}.bi-arrow-90deg-left::before{content:"\f10f"}.bi-arrow-90deg-right::before{content:"\f110"}.bi-arrow-90deg-up::before{content:"\f111"}.bi-arrow-bar-down::before{content:"\f112"}.bi-arrow-bar-left::before{content:"\f113"}.bi-arrow-bar-right::before{content:"\f114"}.bi-arrow-bar-up::before{content:"\f115"}.bi-arrow-clockwise::before{content:"\f116"}.bi-arrow-counterclockwise::before{content:"\f117"}.bi-arrow-down-circle-fill::before{content:"\f118"}.bi-arrow-down-circle::before{content:"\f119"}.bi-arrow-down-left-circle-fill::before{content:"\f11a"}.bi-arrow-down-left-circle::before{content:"\f11b"}.bi-arrow-down-left-square-fill::before{content:"\f11c"}.bi-arrow-down-left-square::before{content:"\f11d"}.bi-arrow-down-left::before{content:"\f11e"}.bi-arrow-down-right-circle-fill::before{content:"\f11f"}.bi-arrow-down-right-circle::before{content:"\f120"}.bi-arrow-down-right-square-fill::before{content:"\f121"}.bi-arrow-down-right-square::before{content:"\f122"}.bi-arrow-down-right::before{content:"\f123"}.bi-arrow-down-short::before{content:"\f124"}.bi-arrow-down-square-fill::before{content:"\f125"}.bi-arrow-down-square::before{content:"\f126"}.bi-arrow-down-up::before{content:"\f127"}.bi-arrow-down::before{content:"\f128"}.bi-arrow-left-circle-fill::before{content:"\f129"}.bi-arrow-left-circle::before{content:"\f12a"}.bi-arrow-left-right::before{content:"\f12b"}.bi-arrow-left-short::before{content:"\f12c"}.bi-arrow-left-square-fill::before{content:"\f12d"}.bi-arrow-left-square::before{content:"\f12e"}.bi-arrow-left::before{content:"\f12f"}.bi-arrow-repeat::before{content:"\f130"}.bi-arrow-return-left::before{content:"\f131"}.bi-arrow-return-right::before{content:"\f132"}.bi-arrow-right-circle-fill::before{content:"\f133"}.bi-arrow-right-circle::before{content:"\f134"}.bi-arrow-right-short::before{content:"\f135"}.bi-arrow-right-square-fill::before{content:"\f136"}.bi-arrow-right-square::before{content:"\f137"}.bi-arrow-right::before{content:"\f138"}.bi-arrow-up-circle-fill::before{content:"\f139"}.bi-arrow-up-circle::before{content:"\f13a"}.bi-arrow-up-left-circle-fill::before{content:"\f13b"}.bi-arrow-up-left-circle::before{content:"\f13c"}.bi-arrow-up-left-square-fill::before{content:"\f13d"}.bi-arrow-up-left-square::before{content:"\f13e"}.bi-arrow-up-left::before{content:"\f13f"}.bi-arrow-up-right-circle-fill::before{content:"\f140"}.bi-arrow-up-right-circle::before{content:"\f141"}.bi-arrow-up-right-square-fill::before{content:"\f142"}.bi-arrow-up-right-square::before{content:"\f143"}.bi-arrow-up-right::before{content:"\f144"}.bi-arrow-up-short::before{content:"\f145"}.bi-arrow-up-square-fill::before{content:"\f146"}.bi-arrow-up-square::before{content:"\f147"}.bi-arrow-up::before{content:"\f148"}.bi-arrows-angle-contract::before{content:"\f149"}.bi-arrows-angle-expand::before{content:"\f14a"}.bi-arrows-collapse::before{content:"\f14b"}.bi-arrows-expand::before{content:"\f14c"}.bi-arrows-fullscreen::before{content:"\f14d"}.bi-arrows-move::before{content:"\f14e"}.bi-aspect-ratio-fill::before{content:"\f14f"}.bi-aspect-ratio::before{content:"\f150"}.bi-asterisk::before{content:"\f151"}.bi-at::before{content:"\f152"}.bi-award-fill::before{content:"\f153"}.bi-award::before{content:"\f154"}.bi-back::before{content:"\f155"}.bi-backspace-fill::before{content:"\f156"}.bi-backspace-reverse-fill::before{content:"\f157"}.bi-backspace-reverse::before{content:"\f158"}.bi-backspace::before{content:"\f159"}.bi-badge-3d-fill::before{content:"\f15a"}.bi-badge-3d::before{content:"\f15b"}.bi-badge-4k-fill::before{content:"\f15c"}.bi-badge-4k::before{content:"\f15d"}.bi-badge-8k-fill::before{content:"\f15e"}.bi-badge-8k::before{content:"\f15f"}.bi-badge-ad-fill::before{content:"\f160"}.bi-badge-ad::before{content:"\f161"}.bi-badge-ar-fill::before{content:"\f162"}.bi-badge-ar::before{content:"\f163"}.bi-badge-cc-fill::before{content:"\f164"}.bi-badge-cc::before{content:"\f165"}.bi-badge-hd-fill::before{content:"\f166"}.bi-badge-hd::before{content:"\f167"}.bi-badge-tm-fill::before{content:"\f168"}.bi-badge-tm::before{content:"\f169"}.bi-badge-vo-fill::before{content:"\f16a"}.bi-badge-vo::before{content:"\f16b"}.bi-badge-vr-fill::before{content:"\f16c"}.bi-badge-vr::before{content:"\f16d"}.bi-badge-wc-fill::before{content:"\f16e"}.bi-badge-wc::before{content:"\f16f"}.bi-bag-check-fill::before{content:"\f170"}.bi-bag-check::before{content:"\f171"}.bi-bag-dash-fill::before{content:"\f172"}.bi-bag-dash::before{content:"\f173"}.bi-bag-fill::before{content:"\f174"}.bi-bag-plus-fill::before{content:"\f175"}.bi-bag-plus::before{content:"\f176"}.bi-bag-x-fill::before{content:"\f177"}.bi-bag-x::before{content:"\f178"}.bi-bag::before{content:"\f179"}.bi-bar-chart-fill::before{content:"\f17a"}.bi-bar-chart-line-fill::before{content:"\f17b"}.bi-bar-chart-line::before{content:"\f17c"}.bi-bar-chart-steps::before{content:"\f17d"}.bi-bar-chart::before{content:"\f17e"}.bi-basket-fill::before{content:"\f17f"}.bi-basket::before{content:"\f180"}.bi-basket2-fill::before{content:"\f181"}.bi-basket2::before{content:"\f182"}.bi-basket3-fill::before{content:"\f183"}.bi-basket3::before{content:"\f184"}.bi-battery-charging::before{content:"\f185"}.bi-battery-full::before{content:"\f186"}.bi-battery-half::before{content:"\f187"}.bi-battery::before{content:"\f188"}.bi-bell-fill::before{content:"\f189"}.bi-bell::before{content:"\f18a"}.bi-bezier::before{content:"\f18b"}.bi-bezier2::before{content:"\f18c"}.bi-bicycle::before{content:"\f18d"}.bi-binoculars-fill::before{content:"\f18e"}.bi-binoculars::before{content:"\f18f"}.bi-blockquote-left::before{content:"\f190"}.bi-blockquote-right::before{content:"\f191"}.bi-book-fill::before{content:"\f192"}.bi-book-half::before{content:"\f193"}.bi-book::before{content:"\f194"}.bi-bookmark-check-fill::before{content:"\f195"}.bi-bookmark-check::before{content:"\f196"}.bi-bookmark-dash-fill::before{content:"\f197"}.bi-bookmark-dash::before{content:"\f198"}.bi-bookmark-fill::before{content:"\f199"}.bi-bookmark-heart-fill::before{content:"\f19a"}.bi-bookmark-heart::before{content:"\f19b"}.bi-bookmark-plus-fill::before{content:"\f19c"}.bi-bookmark-plus::before{content:"\f19d"}.bi-bookmark-star-fill::before{content:"\f19e"}.bi-bookmark-star::before{content:"\f19f"}.bi-bookmark-x-fill::before{content:"\f1a0"}.bi-bookmark-x::before{content:"\f1a1"}.bi-bookmark::before{content:"\f1a2"}.bi-bookmarks-fill::before{content:"\f1a3"}.bi-bookmarks::before{content:"\f1a4"}.bi-bookshelf::before{content:"\f1a5"}.bi-bootstrap-fill::before{content:"\f1a6"}.bi-bootstrap-reboot::before{content:"\f1a7"}.bi-bootstrap::before{content:"\f1a8"}.bi-border-all::before{content:"\f1a9"}.bi-border-bottom::before{content:"\f1aa"}.bi-border-center::before{content:"\f1ab"}.bi-border-inner::before{content:"\f1ac"}.bi-border-left::before{content:"\f1ad"}.bi-border-middle::before{content:"\f1ae"}.bi-border-outer::before{content:"\f1af"}.bi-border-right::before{content:"\f1b0"}.bi-border-style::before{content:"\f1b1"}.bi-border-top::before{content:"\f1b2"}.bi-border-width::before{content:"\f1b3"}.bi-border::before{content:"\f1b4"}.bi-bounding-box-circles::before{content:"\f1b5"}.bi-bounding-box::before{content:"\f1b6"}.bi-box-arrow-down-left::before{content:"\f1b7"}.bi-box-arrow-down-right::before{content:"\f1b8"}.bi-box-arrow-down::before{content:"\f1b9"}.bi-box-arrow-in-down-left::before{content:"\f1ba"}.bi-box-arrow-in-down-right::before{content:"\f1bb"}.bi-box-arrow-in-down::before{content:"\f1bc"}.bi-box-arrow-in-left::before{content:"\f1bd"}.bi-box-arrow-in-right::before{content:"\f1be"}.bi-box-arrow-in-up-left::before{content:"\f1bf"}.bi-box-arrow-in-up-right::before{content:"\f1c0"}.bi-box-arrow-in-up::before{content:"\f1c1"}.bi-box-arrow-left::before{content:"\f1c2"}.bi-box-arrow-right::before{content:"\f1c3"}.bi-box-arrow-up-left::before{content:"\f1c4"}.bi-box-arrow-up-right::before{content:"\f1c5"}.bi-box-arrow-up::before{content:"\f1c6"}.bi-box-seam::before{content:"\f1c7"}.bi-box::before{content:"\f1c8"}.bi-braces::before{content:"\f1c9"}.bi-bricks::before{content:"\f1ca"}.bi-briefcase-fill::before{content:"\f1cb"}.bi-briefcase::before{content:"\f1cc"}.bi-brightness-alt-high-fill::before{content:"\f1cd"}.bi-brightness-alt-high::before{content:"\f1ce"}.bi-brightness-alt-low-fill::before{content:"\f1cf"}.bi-brightness-alt-low::before{content:"\f1d0"}.bi-brightness-high-fill::before{content:"\f1d1"}.bi-brightness-high::before{content:"\f1d2"}.bi-brightness-low-fill::before{content:"\f1d3"}.bi-brightness-low::before{content:"\f1d4"}.bi-broadcast-pin::before{content:"\f1d5"}.bi-broadcast::before{content:"\f1d6"}.bi-brush-fill::before{content:"\f1d7"}.bi-brush::before{content:"\f1d8"}.bi-bucket-fill::before{content:"\f1d9"}.bi-bucket::before{content:"\f1da"}.bi-bug-fill::before{content:"\f1db"}.bi-bug::before{content:"\f1dc"}.bi-building::before{content:"\f1dd"}.bi-bullseye::before{content:"\f1de"}.bi-calculator-fill::before{content:"\f1df"}.bi-calculator::before{content:"\f1e0"}.bi-calendar-check-fill::before{content:"\f1e1"}.bi-calendar-check::before{content:"\f1e2"}.bi-calendar-date-fill::before{content:"\f1e3"}.bi-calendar-date::before{content:"\f1e4"}.bi-calendar-day-fill::before{content:"\f1e5"}.bi-calendar-day::before{content:"\f1e6"}.bi-calendar-event-fill::before{content:"\f1e7"}.bi-calendar-event::before{content:"\f1e8"}.bi-calendar-fill::before{content:"\f1e9"}.bi-calendar-minus-fill::before{content:"\f1ea"}.bi-calendar-minus::before{content:"\f1eb"}.bi-calendar-month-fill::before{content:"\f1ec"}.bi-calendar-month::before{content:"\f1ed"}.bi-calendar-plus-fill::before{content:"\f1ee"}.bi-calendar-plus::before{content:"\f1ef"}.bi-calendar-range-fill::before{content:"\f1f0"}.bi-calendar-range::before{content:"\f1f1"}.bi-calendar-week-fill::before{content:"\f1f2"}.bi-calendar-week::before{content:"\f1f3"}.bi-calendar-x-fill::before{content:"\f1f4"}.bi-calendar-x::before{content:"\f1f5"}.bi-calendar::before{content:"\f1f6"}.bi-calendar2-check-fill::before{content:"\f1f7"}.bi-calendar2-check::before{content:"\f1f8"}.bi-calendar2-date-fill::before{content:"\f1f9"}.bi-calendar2-date::before{content:"\f1fa"}.bi-calendar2-day-fill::before{content:"\f1fb"}.bi-calendar2-day::before{content:"\f1fc"}.bi-calendar2-event-fill::before{content:"\f1fd"}.bi-calendar2-event::before{content:"\f1fe"}.bi-calendar2-fill::before{content:"\f1ff"}.bi-calendar2-minus-fill::before{content:"\f200"}.bi-calendar2-minus::before{content:"\f201"}.bi-calendar2-month-fill::before{content:"\f202"}.bi-calendar2-month::before{content:"\f203"}.bi-calendar2-plus-fill::before{content:"\f204"}.bi-calendar2-plus::before{content:"\f205"}.bi-calendar2-range-fill::before{content:"\f206"}.bi-calendar2-range::before{content:"\f207"}.bi-calendar2-week-fill::before{content:"\f208"}.bi-calendar2-week::before{content:"\f209"}.bi-calendar2-x-fill::before{content:"\f20a"}.bi-calendar2-x::before{content:"\f20b"}.bi-calendar2::before{content:"\f20c"}.bi-calendar3-event-fill::before{content:"\f20d"}.bi-calendar3-event::before{content:"\f20e"}.bi-calendar3-fill::before{content:"\f20f"}.bi-calendar3-range-fill::before{content:"\f210"}.bi-calendar3-range::before{content:"\f211"}.bi-calendar3-week-fill::before{content:"\f212"}.bi-calendar3-week::before{content:"\f213"}.bi-calendar3::before{content:"\f214"}.bi-calendar4-event::before{content:"\f215"}.bi-calendar4-range::before{content:"\f216"}.bi-calendar4-week::before{content:"\f217"}.bi-calendar4::before{content:"\f218"}.bi-camera-fill::before{content:"\f219"}.bi-camera-reels-fill::before{content:"\f21a"}.bi-camera-reels::before{content:"\f21b"}.bi-camera-video-fill::before{content:"\f21c"}.bi-camera-video-off-fill::before{content:"\f21d"}.bi-camera-video-off::before{content:"\f21e"}.bi-camera-video::before{content:"\f21f"}.bi-camera::before{content:"\f220"}.bi-camera2::before{content:"\f221"}.bi-capslock-fill::before{content:"\f222"}.bi-capslock::before{content:"\f223"}.bi-card-checklist::before{content:"\f224"}.bi-card-heading::before{content:"\f225"}.bi-card-image::before{content:"\f226"}.bi-card-list::before{content:"\f227"}.bi-card-text::before{content:"\f228"}.bi-caret-down-fill::before{content:"\f229"}.bi-caret-down-square-fill::before{content:"\f22a"}.bi-caret-down-square::before{content:"\f22b"}.bi-caret-down::before{content:"\f22c"}.bi-caret-left-fill::before{content:"\f22d"}.bi-caret-left-square-fill::before{content:"\f22e"}.bi-caret-left-square::before{content:"\f22f"}.bi-caret-left::before{content:"\f230"}.bi-caret-right-fill::before{content:"\f231"}.bi-caret-right-square-fill::before{content:"\f232"}.bi-caret-right-square::before{content:"\f233"}.bi-caret-right::before{content:"\f234"}.bi-caret-up-fill::before{content:"\f235"}.bi-caret-up-square-fill::before{content:"\f236"}.bi-caret-up-square::before{content:"\f237"}.bi-caret-up::before{content:"\f238"}.bi-cart-check-fill::before{content:"\f239"}.bi-cart-check::before{content:"\f23a"}.bi-cart-dash-fill::before{content:"\f23b"}.bi-cart-dash::before{content:"\f23c"}.bi-cart-fill::before{content:"\f23d"}.bi-cart-plus-fill::before{content:"\f23e"}.bi-cart-plus::before{content:"\f23f"}.bi-cart-x-fill::before{content:"\f240"}.bi-cart-x::before{content:"\f241"}.bi-cart::before{content:"\f242"}.bi-cart2::before{content:"\f243"}.bi-cart3::before{content:"\f244"}.bi-cart4::before{content:"\f245"}.bi-cash-stack::before{content:"\f246"}.bi-cash::before{content:"\f247"}.bi-cast::before{content:"\f248"}.bi-chat-dots-fill::before{content:"\f249"}.bi-chat-dots::before{content:"\f24a"}.bi-chat-fill::before{content:"\f24b"}.bi-chat-left-dots-fill::before{content:"\f24c"}.bi-chat-left-dots::before{content:"\f24d"}.bi-chat-left-fill::before{content:"\f24e"}.bi-chat-left-quote-fill::before{content:"\f24f"}.bi-chat-left-quote::before{content:"\f250"}.bi-chat-left-text-fill::before{content:"\f251"}.bi-chat-left-text::before{content:"\f252"}.bi-chat-left::before{content:"\f253"}.bi-chat-quote-fill::before{content:"\f254"}.bi-chat-quote::before{content:"\f255"}.bi-chat-right-dots-fill::before{content:"\f256"}.bi-chat-right-dots::before{content:"\f257"}.bi-chat-right-fill::before{content:"\f258"}.bi-chat-right-quote-fill::before{content:"\f259"}.bi-chat-right-quote::before{content:"\f25a"}.bi-chat-right-text-fill::before{content:"\f25b"}.bi-chat-right-text::before{content:"\f25c"}.bi-chat-right::before{content:"\f25d"}.bi-chat-square-dots-fill::before{content:"\f25e"}.bi-chat-square-dots::before{content:"\f25f"}.bi-chat-square-fill::before{content:"\f260"}.bi-chat-square-quote-fill::before{content:"\f261"}.bi-chat-square-quote::before{content:"\f262"}.bi-chat-square-text-fill::before{content:"\f263"}.bi-chat-square-text::before{content:"\f264"}.bi-chat-square::before{content:"\f265"}.bi-chat-text-fill::before{content:"\f266"}.bi-chat-text::before{content:"\f267"}.bi-chat::before{content:"\f268"}.bi-check-all::before{content:"\f269"}.bi-check-circle-fill::before{content:"\f26a"}.bi-check-circle::before{content:"\f26b"}.bi-check-square-fill::before{content:"\f26c"}.bi-check-square::before{content:"\f26d"}.bi-check::before{content:"\f26e"}.bi-check2-all::before{content:"\f26f"}.bi-check2-circle::before{content:"\f270"}.bi-check2-square::before{content:"\f271"}.bi-check2::before{content:"\f272"}.bi-chevron-bar-contract::before{content:"\f273"}.bi-chevron-bar-down::before{content:"\f274"}.bi-chevron-bar-expand::before{content:"\f275"}.bi-chevron-bar-left::before{content:"\f276"}.bi-chevron-bar-right::before{content:"\f277"}.bi-chevron-bar-up::before{content:"\f278"}.bi-chevron-compact-down::before{content:"\f279"}.bi-chevron-compact-left::before{content:"\f27a"}.bi-chevron-compact-right::before{content:"\f27b"}.bi-chevron-compact-up::before{content:"\f27c"}.bi-chevron-contract::before{content:"\f27d"}.bi-chevron-double-down::before{content:"\f27e"}.bi-chevron-double-left::before{content:"\f27f"}.bi-chevron-double-right::before{content:"\f280"}.bi-chevron-double-up::before{content:"\f281"}.bi-chevron-down::before{content:"\f282"}.bi-chevron-expand::before{content:"\f283"}.bi-chevron-left::before{content:"\f284"}.bi-chevron-right::before{content:"\f285"}.bi-chevron-up::before{content:"\f286"}.bi-circle-fill::before{content:"\f287"}.bi-circle-half::before{content:"\f288"}.bi-circle-square::before{content:"\f289"}.bi-circle::before{content:"\f28a"}.bi-clipboard-check::before{content:"\f28b"}.bi-clipboard-data::before{content:"\f28c"}.bi-clipboard-minus::before{content:"\f28d"}.bi-clipboard-plus::before{content:"\f28e"}.bi-clipboard-x::before{content:"\f28f"}.bi-clipboard::before{content:"\f290"}.bi-clock-fill::before{content:"\f291"}.bi-clock-history::before{content:"\f292"}.bi-clock::before{content:"\f293"}.bi-cloud-arrow-down-fill::before{content:"\f294"}.bi-cloud-arrow-down::before{content:"\f295"}.bi-cloud-arrow-up-fill::before{content:"\f296"}.bi-cloud-arrow-up::before{content:"\f297"}.bi-cloud-check-fill::before{content:"\f298"}.bi-cloud-check::before{content:"\f299"}.bi-cloud-download-fill::before{content:"\f29a"}.bi-cloud-download::before{content:"\f29b"}.bi-cloud-drizzle-fill::before{content:"\f29c"}.bi-cloud-drizzle::before{content:"\f29d"}.bi-cloud-fill::before{content:"\f29e"}.bi-cloud-fog-fill::before{content:"\f29f"}.bi-cloud-fog::before{content:"\f2a0"}.bi-cloud-fog2-fill::before{content:"\f2a1"}.bi-cloud-fog2::before{content:"\f2a2"}.bi-cloud-hail-fill::before{content:"\f2a3"}.bi-cloud-hail::before{content:"\f2a4"}.bi-cloud-haze-fill::before{content:"\f2a6"}.bi-cloud-haze::before{content:"\f2a7"}.bi-cloud-haze2-fill::before{content:"\f2a8"}.bi-cloud-lightning-fill::before{content:"\f2a9"}.bi-cloud-lightning-rain-fill::before{content:"\f2aa"}.bi-cloud-lightning-rain::before{content:"\f2ab"}.bi-cloud-lightning::before{content:"\f2ac"}.bi-cloud-minus-fill::before{content:"\f2ad"}.bi-cloud-minus::before{content:"\f2ae"}.bi-cloud-moon-fill::before{content:"\f2af"}.bi-cloud-moon::before{content:"\f2b0"}.bi-cloud-plus-fill::before{content:"\f2b1"}.bi-cloud-plus::before{content:"\f2b2"}.bi-cloud-rain-fill::before{content:"\f2b3"}.bi-cloud-rain-heavy-fill::before{content:"\f2b4"}.bi-cloud-rain-heavy::before{content:"\f2b5"}.bi-cloud-rain::before{content:"\f2b6"}.bi-cloud-slash-fill::before{content:"\f2b7"}.bi-cloud-slash::before{content:"\f2b8"}.bi-cloud-sleet-fill::before{content:"\f2b9"}.bi-cloud-sleet::before{content:"\f2ba"}.bi-cloud-snow-fill::before{content:"\f2bb"}.bi-cloud-snow::before{content:"\f2bc"}.bi-cloud-sun-fill::before{content:"\f2bd"}.bi-cloud-sun::before{content:"\f2be"}.bi-cloud-upload-fill::before{content:"\f2bf"}.bi-cloud-upload::before{content:"\f2c0"}.bi-cloud::before{content:"\f2c1"}.bi-clouds-fill::before{content:"\f2c2"}.bi-clouds::before{content:"\f2c3"}.bi-cloudy-fill::before{content:"\f2c4"}.bi-cloudy::before{content:"\f2c5"}.bi-code-slash::before{content:"\f2c6"}.bi-code-square::before{content:"\f2c7"}.bi-code::before{content:"\f2c8"}.bi-collection-fill::before{content:"\f2c9"}.bi-collection-play-fill::before{content:"\f2ca"}.bi-collection-play::before{content:"\f2cb"}.bi-collection::before{content:"\f2cc"}.bi-columns-gap::before{content:"\f2cd"}.bi-columns::before{content:"\f2ce"}.bi-command::before{content:"\f2cf"}.bi-compass-fill::before{content:"\f2d0"}.bi-compass::before{content:"\f2d1"}.bi-cone-striped::before{content:"\f2d2"}.bi-cone::before{content:"\f2d3"}.bi-controller::before{content:"\f2d4"}.bi-cpu-fill::before{content:"\f2d5"}.bi-cpu::before{content:"\f2d6"}.bi-credit-card-2-back-fill::before{content:"\f2d7"}.bi-credit-card-2-back::before{content:"\f2d8"}.bi-credit-card-2-front-fill::before{content:"\f2d9"}.bi-credit-card-2-front::before{content:"\f2da"}.bi-credit-card-fill::before{content:"\f2db"}.bi-credit-card::before{content:"\f2dc"}.bi-crop::before{content:"\f2dd"}.bi-cup-fill::before{content:"\f2de"}.bi-cup-straw::before{content:"\f2df"}.bi-cup::before{content:"\f2e0"}.bi-cursor-fill::before{content:"\f2e1"}.bi-cursor-text::before{content:"\f2e2"}.bi-cursor::before{content:"\f2e3"}.bi-dash-circle-dotted::before{content:"\f2e4"}.bi-dash-circle-fill::before{content:"\f2e5"}.bi-dash-circle::before{content:"\f2e6"}.bi-dash-square-dotted::before{content:"\f2e7"}.bi-dash-square-fill::before{content:"\f2e8"}.bi-dash-square::before{content:"\f2e9"}.bi-dash::before{content:"\f2ea"}.bi-diagram-2-fill::before{content:"\f2eb"}.bi-diagram-2::before{content:"\f2ec"}.bi-diagram-3-fill::before{content:"\f2ed"}.bi-diagram-3::before{content:"\f2ee"}.bi-diamond-fill::before{content:"\f2ef"}.bi-diamond-half::before{content:"\f2f0"}.bi-diamond::before{content:"\f2f1"}.bi-dice-1-fill::before{content:"\f2f2"}.bi-dice-1::before{content:"\f2f3"}.bi-dice-2-fill::before{content:"\f2f4"}.bi-dice-2::before{content:"\f2f5"}.bi-dice-3-fill::before{content:"\f2f6"}.bi-dice-3::before{content:"\f2f7"}.bi-dice-4-fill::before{content:"\f2f8"}.bi-dice-4::before{content:"\f2f9"}.bi-dice-5-fill::before{content:"\f2fa"}.bi-dice-5::before{content:"\f2fb"}.bi-dice-6-fill::before{content:"\f2fc"}.bi-dice-6::before{content:"\f2fd"}.bi-disc-fill::before{content:"\f2fe"}.bi-disc::before{content:"\f2ff"}.bi-discord::before{content:"\f300"}.bi-display-fill::before{content:"\f301"}.bi-display::before{content:"\f302"}.bi-distribute-horizontal::before{content:"\f303"}.bi-distribute-vertical::before{content:"\f304"}.bi-door-closed-fill::before{content:"\f305"}.bi-door-closed::before{content:"\f306"}.bi-door-open-fill::before{content:"\f307"}.bi-door-open::before{content:"\f308"}.bi-dot::before{content:"\f309"}.bi-download::before{content:"\f30a"}.bi-droplet-fill::before{content:"\f30b"}.bi-droplet-half::before{content:"\f30c"}.bi-droplet::before{content:"\f30d"}.bi-earbuds::before{content:"\f30e"}.bi-easel-fill::before{content:"\f30f"}.bi-easel::before{content:"\f310"}.bi-egg-fill::before{content:"\f311"}.bi-egg-fried::before{content:"\f312"}.bi-egg::before{content:"\f313"}.bi-eject-fill::before{content:"\f314"}.bi-eject::before{content:"\f315"}.bi-emoji-angry-fill::before{content:"\f316"}.bi-emoji-angry::before{content:"\f317"}.bi-emoji-dizzy-fill::before{content:"\f318"}.bi-emoji-dizzy::before{content:"\f319"}.bi-emoji-expressionless-fill::before{content:"\f31a"}.bi-emoji-expressionless::before{content:"\f31b"}.bi-emoji-frown-fill::before{content:"\f31c"}.bi-emoji-frown::before{content:"\f31d"}.bi-emoji-heart-eyes-fill::before{content:"\f31e"}.bi-emoji-heart-eyes::before{content:"\f31f"}.bi-emoji-laughing-fill::before{content:"\f320"}.bi-emoji-laughing::before{content:"\f321"}.bi-emoji-neutral-fill::before{content:"\f322"}.bi-emoji-neutral::before{content:"\f323"}.bi-emoji-smile-fill::before{content:"\f324"}.bi-emoji-smile-upside-down-fill::before{content:"\f325"}.bi-emoji-smile-upside-down::before{content:"\f326"}.bi-emoji-smile::before{content:"\f327"}.bi-emoji-sunglasses-fill::before{content:"\f328"}.bi-emoji-sunglasses::before{content:"\f329"}.bi-emoji-wink-fill::before{content:"\f32a"}.bi-emoji-wink::before{content:"\f32b"}.bi-envelope-fill::before{content:"\f32c"}.bi-envelope-open-fill::before{content:"\f32d"}.bi-envelope-open::before{content:"\f32e"}.bi-envelope::before{content:"\f32f"}.bi-eraser-fill::before{content:"\f330"}.bi-eraser::before{content:"\f331"}.bi-exclamation-circle-fill::before{content:"\f332"}.bi-exclamation-circle::before{content:"\f333"}.bi-exclamation-diamond-fill::before{content:"\f334"}.bi-exclamation-diamond::before{content:"\f335"}.bi-exclamation-octagon-fill::before{content:"\f336"}.bi-exclamation-octagon::before{content:"\f337"}.bi-exclamation-square-fill::before{content:"\f338"}.bi-exclamation-square::before{content:"\f339"}.bi-exclamation-triangle-fill::before{content:"\f33a"}.bi-exclamation-triangle::before{content:"\f33b"}.bi-exclamation::before{content:"\f33c"}.bi-exclude::before{content:"\f33d"}.bi-eye-fill::before{content:"\f33e"}.bi-eye-slash-fill::before{content:"\f33f"}.bi-eye-slash::before{content:"\f340"}.bi-eye::before{content:"\f341"}.bi-eyedropper::before{content:"\f342"}.bi-eyeglasses::before{content:"\f343"}.bi-facebook::before{content:"\f344"}.bi-file-arrow-down-fill::before{content:"\f345"}.bi-file-arrow-down::before{content:"\f346"}.bi-file-arrow-up-fill::before{content:"\f347"}.bi-file-arrow-up::before{content:"\f348"}.bi-file-bar-graph-fill::before{content:"\f349"}.bi-file-bar-graph::before{content:"\f34a"}.bi-file-binary-fill::before{content:"\f34b"}.bi-file-binary::before{content:"\f34c"}.bi-file-break-fill::before{content:"\f34d"}.bi-file-break::before{content:"\f34e"}.bi-file-check-fill::before{content:"\f34f"}.bi-file-check::before{content:"\f350"}.bi-file-code-fill::before{content:"\f351"}.bi-file-code::before{content:"\f352"}.bi-file-diff-fill::before{content:"\f353"}.bi-file-diff::before{content:"\f354"}.bi-file-earmark-arrow-down-fill::before{content:"\f355"}.bi-file-earmark-arrow-down::before{content:"\f356"}.bi-file-earmark-arrow-up-fill::before{content:"\f357"}.bi-file-earmark-arrow-up::before{content:"\f358"}.bi-file-earmark-bar-graph-fill::before{content:"\f359"}.bi-file-earmark-bar-graph::before{content:"\f35a"}.bi-file-earmark-binary-fill::before{content:"\f35b"}.bi-file-earmark-binary::before{content:"\f35c"}.bi-file-earmark-break-fill::before{content:"\f35d"}.bi-file-earmark-break::before{content:"\f35e"}.bi-file-earmark-check-fill::before{content:"\f35f"}.bi-file-earmark-check::before{content:"\f360"}.bi-file-earmark-code-fill::before{content:"\f361"}.bi-file-earmark-code::before{content:"\f362"}.bi-file-earmark-diff-fill::before{content:"\f363"}.bi-file-earmark-diff::before{content:"\f364"}.bi-file-earmark-easel-fill::before{content:"\f365"}.bi-file-earmark-easel::before{content:"\f366"}.bi-file-earmark-excel-fill::before{content:"\f367"}.bi-file-earmark-excel::before{content:"\f368"}.bi-file-earmark-fill::before{content:"\f369"}.bi-file-earmark-font-fill::before{content:"\f36a"}.bi-file-earmark-font::before{content:"\f36b"}.bi-file-earmark-image-fill::before{content:"\f36c"}.bi-file-earmark-image::before{content:"\f36d"}.bi-file-earmark-lock-fill::before{content:"\f36e"}.bi-file-earmark-lock::before{content:"\f36f"}.bi-file-earmark-lock2-fill::before{content:"\f370"}.bi-file-earmark-lock2::before{content:"\f371"}.bi-file-earmark-medical-fill::before{content:"\f372"}.bi-file-earmark-medical::before{content:"\f373"}.bi-file-earmark-minus-fill::before{content:"\f374"}.bi-file-earmark-minus::before{content:"\f375"}.bi-file-earmark-music-fill::before{content:"\f376"}.bi-file-earmark-music::before{content:"\f377"}.bi-file-earmark-person-fill::before{content:"\f378"}.bi-file-earmark-person::before{content:"\f379"}.bi-file-earmark-play-fill::before{content:"\f37a"}.bi-file-earmark-play::before{content:"\f37b"}.bi-file-earmark-plus-fill::before{content:"\f37c"}.bi-file-earmark-plus::before{content:"\f37d"}.bi-file-earmark-post-fill::before{content:"\f37e"}.bi-file-earmark-post::before{content:"\f37f"}.bi-file-earmark-ppt-fill::before{content:"\f380"}.bi-file-earmark-ppt::before{content:"\f381"}.bi-file-earmark-richtext-fill::before{content:"\f382"}.bi-file-earmark-richtext::before{content:"\f383"}.bi-file-earmark-ruled-fill::before{content:"\f384"}.bi-file-earmark-ruled::before{content:"\f385"}.bi-file-earmark-slides-fill::before{content:"\f386"}.bi-file-earmark-slides::before{content:"\f387"}.bi-file-earmark-spreadsheet-fill::before{content:"\f388"}.bi-file-earmark-spreadsheet::before{content:"\f389"}.bi-file-earmark-text-fill::before{content:"\f38a"}.bi-file-earmark-text::before{content:"\f38b"}.bi-file-earmark-word-fill::before{content:"\f38c"}.bi-file-earmark-word::before{content:"\f38d"}.bi-file-earmark-x-fill::before{content:"\f38e"}.bi-file-earmark-x::before{content:"\f38f"}.bi-file-earmark-zip-fill::before{content:"\f390"}.bi-file-earmark-zip::before{content:"\f391"}.bi-file-earmark::before{content:"\f392"}.bi-file-easel-fill::before{content:"\f393"}.bi-file-easel::before{content:"\f394"}.bi-file-excel-fill::before{content:"\f395"}.bi-file-excel::before{content:"\f396"}.bi-file-fill::before{content:"\f397"}.bi-file-font-fill::before{content:"\f398"}.bi-file-font::before{content:"\f399"}.bi-file-image-fill::before{content:"\f39a"}.bi-file-image::before{content:"\f39b"}.bi-file-lock-fill::before{content:"\f39c"}.bi-file-lock::before{content:"\f39d"}.bi-file-lock2-fill::before{content:"\f39e"}.bi-file-lock2::before{content:"\f39f"}.bi-file-medical-fill::before{content:"\f3a0"}.bi-file-medical::before{content:"\f3a1"}.bi-file-minus-fill::before{content:"\f3a2"}.bi-file-minus::before{content:"\f3a3"}.bi-file-music-fill::before{content:"\f3a4"}.bi-file-music::before{content:"\f3a5"}.bi-file-person-fill::before{content:"\f3a6"}.bi-file-person::before{content:"\f3a7"}.bi-file-play-fill::before{content:"\f3a8"}.bi-file-play::before{content:"\f3a9"}.bi-file-plus-fill::before{content:"\f3aa"}.bi-file-plus::before{content:"\f3ab"}.bi-file-post-fill::before{content:"\f3ac"}.bi-file-post::before{content:"\f3ad"}.bi-file-ppt-fill::before{content:"\f3ae"}.bi-file-ppt::before{content:"\f3af"}.bi-file-richtext-fill::before{content:"\f3b0"}.bi-file-richtext::before{content:"\f3b1"}.bi-file-ruled-fill::before{content:"\f3b2"}.bi-file-ruled::before{content:"\f3b3"}.bi-file-slides-fill::before{content:"\f3b4"}.bi-file-slides::before{content:"\f3b5"}.bi-file-spreadsheet-fill::before{content:"\f3b6"}.bi-file-spreadsheet::before{content:"\f3b7"}.bi-file-text-fill::before{content:"\f3b8"}.bi-file-text::before{content:"\f3b9"}.bi-file-word-fill::before{content:"\f3ba"}.bi-file-word::before{content:"\f3bb"}.bi-file-x-fill::before{content:"\f3bc"}.bi-file-x::before{content:"\f3bd"}.bi-file-zip-fill::before{content:"\f3be"}.bi-file-zip::before{content:"\f3bf"}.bi-file::before{content:"\f3c0"}.bi-files-alt::before{content:"\f3c1"}.bi-files::before{content:"\f3c2"}.bi-film::before{content:"\f3c3"}.bi-filter-circle-fill::before{content:"\f3c4"}.bi-filter-circle::before{content:"\f3c5"}.bi-filter-left::before{content:"\f3c6"}.bi-filter-right::before{content:"\f3c7"}.bi-filter-square-fill::before{content:"\f3c8"}.bi-filter-square::before{content:"\f3c9"}.bi-filter::before{content:"\f3ca"}.bi-flag-fill::before{content:"\f3cb"}.bi-flag::before{content:"\f3cc"}.bi-flower1::before{content:"\f3cd"}.bi-flower2::before{content:"\f3ce"}.bi-flower3::before{content:"\f3cf"}.bi-folder-check::before{content:"\f3d0"}.bi-folder-fill::before{content:"\f3d1"}.bi-folder-minus::before{content:"\f3d2"}.bi-folder-plus::before{content:"\f3d3"}.bi-folder-symlink-fill::before{content:"\f3d4"}.bi-folder-symlink::before{content:"\f3d5"}.bi-folder-x::before{content:"\f3d6"}.bi-folder::before{content:"\f3d7"}.bi-folder2-open::before{content:"\f3d8"}.bi-folder2::before{content:"\f3d9"}.bi-fonts::before{content:"\f3da"}.bi-forward-fill::before{content:"\f3db"}.bi-forward::before{content:"\f3dc"}.bi-front::before{content:"\f3dd"}.bi-fullscreen-exit::before{content:"\f3de"}.bi-fullscreen::before{content:"\f3df"}.bi-funnel-fill::before{content:"\f3e0"}.bi-funnel::before{content:"\f3e1"}.bi-gear-fill::before{content:"\f3e2"}.bi-gear-wide-connected::before{content:"\f3e3"}.bi-gear-wide::before{content:"\f3e4"}.bi-gear::before{content:"\f3e5"}.bi-gem::before{content:"\f3e6"}.bi-geo-alt-fill::before{content:"\f3e7"}.bi-geo-alt::before{content:"\f3e8"}.bi-geo-fill::before{content:"\f3e9"}.bi-geo::before{content:"\f3ea"}.bi-gift-fill::before{content:"\f3eb"}.bi-gift::before{content:"\f3ec"}.bi-github::before{content:"\f3ed"}.bi-globe::before{content:"\f3ee"}.bi-globe2::before{content:"\f3ef"}.bi-google::before{content:"\f3f0"}.bi-graph-down::before{content:"\f3f1"}.bi-graph-up::before{content:"\f3f2"}.bi-grid-1x2-fill::before{content:"\f3f3"}.bi-grid-1x2::before{content:"\f3f4"}.bi-grid-3x2-gap-fill::before{content:"\f3f5"}.bi-grid-3x2-gap::before{content:"\f3f6"}.bi-grid-3x2::before{content:"\f3f7"}.bi-grid-3x3-gap-fill::before{content:"\f3f8"}.bi-grid-3x3-gap::before{content:"\f3f9"}.bi-grid-3x3::before{content:"\f3fa"}.bi-grid-fill::before{content:"\f3fb"}.bi-grid::before{content:"\f3fc"}.bi-grip-horizontal::before{content:"\f3fd"}.bi-grip-vertical::before{content:"\f3fe"}.bi-hammer::before{content:"\f3ff"}.bi-hand-index-fill::before{content:"\f400"}.bi-hand-index-thumb-fill::before{content:"\f401"}.bi-hand-index-thumb::before{content:"\f402"}.bi-hand-index::before{content:"\f403"}.bi-hand-thumbs-down-fill::before{content:"\f404"}.bi-hand-thumbs-down::before{content:"\f405"}.bi-hand-thumbs-up-fill::before{content:"\f406"}.bi-hand-thumbs-up::before{content:"\f407"}.bi-handbag-fill::before{content:"\f408"}.bi-handbag::before{content:"\f409"}.bi-hash::before{content:"\f40a"}.bi-hdd-fill::before{content:"\f40b"}.bi-hdd-network-fill::before{content:"\f40c"}.bi-hdd-network::before{content:"\f40d"}.bi-hdd-rack-fill::before{content:"\f40e"}.bi-hdd-rack::before{content:"\f40f"}.bi-hdd-stack-fill::before{content:"\f410"}.bi-hdd-stack::before{content:"\f411"}.bi-hdd::before{content:"\f412"}.bi-headphones::before{content:"\f413"}.bi-headset::before{content:"\f414"}.bi-heart-fill::before{content:"\f415"}.bi-heart-half::before{content:"\f416"}.bi-heart::before{content:"\f417"}.bi-heptagon-fill::before{content:"\f418"}.bi-heptagon-half::before{content:"\f419"}.bi-heptagon::before{content:"\f41a"}.bi-hexagon-fill::before{content:"\f41b"}.bi-hexagon-half::before{content:"\f41c"}.bi-hexagon::before{content:"\f41d"}.bi-hourglass-bottom::before{content:"\f41e"}.bi-hourglass-split::before{content:"\f41f"}.bi-hourglass-top::before{content:"\f420"}.bi-hourglass::before{content:"\f421"}.bi-house-door-fill::before{content:"\f422"}.bi-house-door::before{content:"\f423"}.bi-house-fill::before{content:"\f424"}.bi-house::before{content:"\f425"}.bi-hr::before{content:"\f426"}.bi-hurricane::before{content:"\f427"}.bi-image-alt::before{content:"\f428"}.bi-image-fill::before{content:"\f429"}.bi-image::before{content:"\f42a"}.bi-images::before{content:"\f42b"}.bi-inbox-fill::before{content:"\f42c"}.bi-inbox::before{content:"\f42d"}.bi-inboxes-fill::before{content:"\f42e"}.bi-inboxes::before{content:"\f42f"}.bi-info-circle-fill::before{content:"\f430"}.bi-info-circle::before{content:"\f431"}.bi-info-square-fill::before{content:"\f432"}.bi-info-square::before{content:"\f433"}.bi-info::before{content:"\f434"}.bi-input-cursor-text::before{content:"\f435"}.bi-input-cursor::before{content:"\f436"}.bi-instagram::before{content:"\f437"}.bi-intersect::before{content:"\f438"}.bi-journal-album::before{content:"\f439"}.bi-journal-arrow-down::before{content:"\f43a"}.bi-journal-arrow-up::before{content:"\f43b"}.bi-journal-bookmark-fill::before{content:"\f43c"}.bi-journal-bookmark::before{content:"\f43d"}.bi-journal-check::before{content:"\f43e"}.bi-journal-code::before{content:"\f43f"}.bi-journal-medical::before{content:"\f440"}.bi-journal-minus::before{content:"\f441"}.bi-journal-plus::before{content:"\f442"}.bi-journal-richtext::before{content:"\f443"}.bi-journal-text::before{content:"\f444"}.bi-journal-x::before{content:"\f445"}.bi-journal::before{content:"\f446"}.bi-journals::before{content:"\f447"}.bi-joystick::before{content:"\f448"}.bi-justify-left::before{content:"\f449"}.bi-justify-right::before{content:"\f44a"}.bi-justify::before{content:"\f44b"}.bi-kanban-fill::before{content:"\f44c"}.bi-kanban::before{content:"\f44d"}.bi-key-fill::before{content:"\f44e"}.bi-key::before{content:"\f44f"}.bi-keyboard-fill::before{content:"\f450"}.bi-keyboard::before{content:"\f451"}.bi-ladder::before{content:"\f452"}.bi-lamp-fill::before{content:"\f453"}.bi-lamp::before{content:"\f454"}.bi-laptop-fill::before{content:"\f455"}.bi-laptop::before{content:"\f456"}.bi-layer-backward::before{content:"\f457"}.bi-layer-forward::before{content:"\f458"}.bi-layers-fill::before{content:"\f459"}.bi-layers-half::before{content:"\f45a"}.bi-layers::before{content:"\f45b"}.bi-layout-sidebar-inset-reverse::before{content:"\f45c"}.bi-layout-sidebar-inset::before{content:"\f45d"}.bi-layout-sidebar-reverse::before{content:"\f45e"}.bi-layout-sidebar::before{content:"\f45f"}.bi-layout-split::before{content:"\f460"}.bi-layout-text-sidebar-reverse::before{content:"\f461"}.bi-layout-text-sidebar::before{content:"\f462"}.bi-layout-text-window-reverse::before{content:"\f463"}.bi-layout-text-window::before{content:"\f464"}.bi-layout-three-columns::before{content:"\f465"}.bi-layout-wtf::before{content:"\f466"}.bi-life-preserver::before{content:"\f467"}.bi-lightbulb-fill::before{content:"\f468"}.bi-lightbulb-off-fill::before{content:"\f469"}.bi-lightbulb-off::before{content:"\f46a"}.bi-lightbulb::before{content:"\f46b"}.bi-lightning-charge-fill::before{content:"\f46c"}.bi-lightning-charge::before{content:"\f46d"}.bi-lightning-fill::before{content:"\f46e"}.bi-lightning::before{content:"\f46f"}.bi-link-45deg::before{content:"\f470"}.bi-link::before{content:"\f471"}.bi-linkedin::before{content:"\f472"}.bi-list-check::before{content:"\f473"}.bi-list-nested::before{content:"\f474"}.bi-list-ol::before{content:"\f475"}.bi-list-stars::before{content:"\f476"}.bi-list-task::before{content:"\f477"}.bi-list-ul::before{content:"\f478"}.bi-list::before{content:"\f479"}.bi-lock-fill::before{content:"\f47a"}.bi-lock::before{content:"\f47b"}.bi-mailbox::before{content:"\f47c"}.bi-mailbox2::before{content:"\f47d"}.bi-map-fill::before{content:"\f47e"}.bi-map::before{content:"\f47f"}.bi-markdown-fill::before{content:"\f480"}.bi-markdown::before{content:"\f481"}.bi-mask::before{content:"\f482"}.bi-megaphone-fill::before{content:"\f483"}.bi-megaphone::before{content:"\f484"}.bi-menu-app-fill::before{content:"\f485"}.bi-menu-app::before{content:"\f486"}.bi-menu-button-fill::before{content:"\f487"}.bi-menu-button-wide-fill::before{content:"\f488"}.bi-menu-button-wide::before{content:"\f489"}.bi-menu-button::before{content:"\f48a"}.bi-menu-down::before{content:"\f48b"}.bi-menu-up::before{content:"\f48c"}.bi-mic-fill::before{content:"\f48d"}.bi-mic-mute-fill::before{content:"\f48e"}.bi-mic-mute::before{content:"\f48f"}.bi-mic::before{content:"\f490"}.bi-minecart-loaded::before{content:"\f491"}.bi-minecart::before{content:"\f492"}.bi-moisture::before{content:"\f493"}.bi-moon-fill::before{content:"\f494"}.bi-moon-stars-fill::before{content:"\f495"}.bi-moon-stars::before{content:"\f496"}.bi-moon::before{content:"\f497"}.bi-mouse-fill::before{content:"\f498"}.bi-mouse::before{content:"\f499"}.bi-mouse2-fill::before{content:"\f49a"}.bi-mouse2::before{content:"\f49b"}.bi-mouse3-fill::before{content:"\f49c"}.bi-mouse3::before{content:"\f49d"}.bi-music-note-beamed::before{content:"\f49e"}.bi-music-note-list::before{content:"\f49f"}.bi-music-note::before{content:"\f4a0"}.bi-music-player-fill::before{content:"\f4a1"}.bi-music-player::before{content:"\f4a2"}.bi-newspaper::before{content:"\f4a3"}.bi-node-minus-fill::before{content:"\f4a4"}.bi-node-minus::before{content:"\f4a5"}.bi-node-plus-fill::before{content:"\f4a6"}.bi-node-plus::before{content:"\f4a7"}.bi-nut-fill::before{content:"\f4a8"}.bi-nut::before{content:"\f4a9"}.bi-octagon-fill::before{content:"\f4aa"}.bi-octagon-half::before{content:"\f4ab"}.bi-octagon::before{content:"\f4ac"}.bi-option::before{content:"\f4ad"}.bi-outlet::before{content:"\f4ae"}.bi-paint-bucket::before{content:"\f4af"}.bi-palette-fill::before{content:"\f4b0"}.bi-palette::before{content:"\f4b1"}.bi-palette2::before{content:"\f4b2"}.bi-paperclip::before{content:"\f4b3"}.bi-paragraph::before{content:"\f4b4"}.bi-patch-check-fill::before{content:"\f4b5"}.bi-patch-check::before{content:"\f4b6"}.bi-patch-exclamation-fill::before{content:"\f4b7"}.bi-patch-exclamation::before{content:"\f4b8"}.bi-patch-minus-fill::before{content:"\f4b9"}.bi-patch-minus::before{content:"\f4ba"}.bi-patch-plus-fill::before{content:"\f4bb"}.bi-patch-plus::before{content:"\f4bc"}.bi-patch-question-fill::before{content:"\f4bd"}.bi-patch-question::before{content:"\f4be"}.bi-pause-btn-fill::before{content:"\f4bf"}.bi-pause-btn::before{content:"\f4c0"}.bi-pause-circle-fill::before{content:"\f4c1"}.bi-pause-circle::before{content:"\f4c2"}.bi-pause-fill::before{content:"\f4c3"}.bi-pause::before{content:"\f4c4"}.bi-peace-fill::before{content:"\f4c5"}.bi-peace::before{content:"\f4c6"}.bi-pen-fill::before{content:"\f4c7"}.bi-pen::before{content:"\f4c8"}.bi-pencil-fill::before{content:"\f4c9"}.bi-pencil-square::before{content:"\f4ca"}.bi-pencil::before{content:"\f4cb"}.bi-pentagon-fill::before{content:"\f4cc"}.bi-pentagon-half::before{content:"\f4cd"}.bi-pentagon::before{content:"\f4ce"}.bi-people-fill::before{content:"\f4cf"}.bi-people::before{content:"\f4d0"}.bi-percent::before{content:"\f4d1"}.bi-person-badge-fill::before{content:"\f4d2"}.bi-person-badge::before{content:"\f4d3"}.bi-person-bounding-box::before{content:"\f4d4"}.bi-person-check-fill::before{content:"\f4d5"}.bi-person-check::before{content:"\f4d6"}.bi-person-circle::before{content:"\f4d7"}.bi-person-dash-fill::before{content:"\f4d8"}.bi-person-dash::before{content:"\f4d9"}.bi-person-fill::before{content:"\f4da"}.bi-person-lines-fill::before{content:"\f4db"}.bi-person-plus-fill::before{content:"\f4dc"}.bi-person-plus::before{content:"\f4dd"}.bi-person-square::before{content:"\f4de"}.bi-person-x-fill::before{content:"\f4df"}.bi-person-x::before{content:"\f4e0"}.bi-person::before{content:"\f4e1"}.bi-phone-fill::before{content:"\f4e2"}.bi-phone-landscape-fill::before{content:"\f4e3"}.bi-phone-landscape::before{content:"\f4e4"}.bi-phone-vibrate-fill::before{content:"\f4e5"}.bi-phone-vibrate::before{content:"\f4e6"}.bi-phone::before{content:"\f4e7"}.bi-pie-chart-fill::before{content:"\f4e8"}.bi-pie-chart::before{content:"\f4e9"}.bi-pin-angle-fill::before{content:"\f4ea"}.bi-pin-angle::before{content:"\f4eb"}.bi-pin-fill::before{content:"\f4ec"}.bi-pin::before{content:"\f4ed"}.bi-pip-fill::before{content:"\f4ee"}.bi-pip::before{content:"\f4ef"}.bi-play-btn-fill::before{content:"\f4f0"}.bi-play-btn::before{content:"\f4f1"}.bi-play-circle-fill::before{content:"\f4f2"}.bi-play-circle::before{content:"\f4f3"}.bi-play-fill::before{content:"\f4f4"}.bi-play::before{content:"\f4f5"}.bi-plug-fill::before{content:"\f4f6"}.bi-plug::before{content:"\f4f7"}.bi-plus-circle-dotted::before{content:"\f4f8"}.bi-plus-circle-fill::before{content:"\f4f9"}.bi-plus-circle::before{content:"\f4fa"}.bi-plus-square-dotted::before{content:"\f4fb"}.bi-plus-square-fill::before{content:"\f4fc"}.bi-plus-square::before{content:"\f4fd"}.bi-plus::before{content:"\f4fe"}.bi-power::before{content:"\f4ff"}.bi-printer-fill::before{content:"\f500"}.bi-printer::before{content:"\f501"}.bi-puzzle-fill::before{content:"\f502"}.bi-puzzle::before{content:"\f503"}.bi-question-circle-fill::before{content:"\f504"}.bi-question-circle::before{content:"\f505"}.bi-question-diamond-fill::before{content:"\f506"}.bi-question-diamond::before{content:"\f507"}.bi-question-octagon-fill::before{content:"\f508"}.bi-question-octagon::before{content:"\f509"}.bi-question-square-fill::before{content:"\f50a"}.bi-question-square::before{content:"\f50b"}.bi-question::before{content:"\f50c"}.bi-rainbow::before{content:"\f50d"}.bi-receipt-cutoff::before{content:"\f50e"}.bi-receipt::before{content:"\f50f"}.bi-reception-0::before{content:"\f510"}.bi-reception-1::before{content:"\f511"}.bi-reception-2::before{content:"\f512"}.bi-reception-3::before{content:"\f513"}.bi-reception-4::before{content:"\f514"}.bi-record-btn-fill::before{content:"\f515"}.bi-record-btn::before{content:"\f516"}.bi-record-circle-fill::before{content:"\f517"}.bi-record-circle::before{content:"\f518"}.bi-record-fill::before{content:"\f519"}.bi-record::before{content:"\f51a"}.bi-record2-fill::before{content:"\f51b"}.bi-record2::before{content:"\f51c"}.bi-reply-all-fill::before{content:"\f51d"}.bi-reply-all::before{content:"\f51e"}.bi-reply-fill::before{content:"\f51f"}.bi-reply::before{content:"\f520"}.bi-rss-fill::before{content:"\f521"}.bi-rss::before{content:"\f522"}.bi-rulers::before{content:"\f523"}.bi-save-fill::before{content:"\f524"}.bi-save::before{content:"\f525"}.bi-save2-fill::before{content:"\f526"}.bi-save2::before{content:"\f527"}.bi-scissors::before{content:"\f528"}.bi-screwdriver::before{content:"\f529"}.bi-search::before{content:"\f52a"}.bi-segmented-nav::before{content:"\f52b"}.bi-server::before{content:"\f52c"}.bi-share-fill::before{content:"\f52d"}.bi-share::before{content:"\f52e"}.bi-shield-check::before{content:"\f52f"}.bi-shield-exclamation::before{content:"\f530"}.bi-shield-fill-check::before{content:"\f531"}.bi-shield-fill-exclamation::before{content:"\f532"}.bi-shield-fill-minus::before{content:"\f533"}.bi-shield-fill-plus::before{content:"\f534"}.bi-shield-fill-x::before{content:"\f535"}.bi-shield-fill::before{content:"\f536"}.bi-shield-lock-fill::before{content:"\f537"}.bi-shield-lock::before{content:"\f538"}.bi-shield-minus::before{content:"\f539"}.bi-shield-plus::before{content:"\f53a"}.bi-shield-shaded::before{content:"\f53b"}.bi-shield-slash-fill::before{content:"\f53c"}.bi-shield-slash::before{content:"\f53d"}.bi-shield-x::before{content:"\f53e"}.bi-shield::before{content:"\f53f"}.bi-shift-fill::before{content:"\f540"}.bi-shift::before{content:"\f541"}.bi-shop-window::before{content:"\f542"}.bi-shop::before{content:"\f543"}.bi-shuffle::before{content:"\f544"}.bi-signpost-2-fill::before{content:"\f545"}.bi-signpost-2::before{content:"\f546"}.bi-signpost-fill::before{content:"\f547"}.bi-signpost-split-fill::before{content:"\f548"}.bi-signpost-split::before{content:"\f549"}.bi-signpost::before{content:"\f54a"}.bi-sim-fill::before{content:"\f54b"}.bi-sim::before{content:"\f54c"}.bi-skip-backward-btn-fill::before{content:"\f54d"}.bi-skip-backward-btn::before{content:"\f54e"}.bi-skip-backward-circle-fill::before{content:"\f54f"}.bi-skip-backward-circle::before{content:"\f550"}.bi-skip-backward-fill::before{content:"\f551"}.bi-skip-backward::before{content:"\f552"}.bi-skip-end-btn-fill::before{content:"\f553"}.bi-skip-end-btn::before{content:"\f554"}.bi-skip-end-circle-fill::before{content:"\f555"}.bi-skip-end-circle::before{content:"\f556"}.bi-skip-end-fill::before{content:"\f557"}.bi-skip-end::before{content:"\f558"}.bi-skip-forward-btn-fill::before{content:"\f559"}.bi-skip-forward-btn::before{content:"\f55a"}.bi-skip-forward-circle-fill::before{content:"\f55b"}.bi-skip-forward-circle::before{content:"\f55c"}.bi-skip-forward-fill::before{content:"\f55d"}.bi-skip-forward::before{content:"\f55e"}.bi-skip-start-btn-fill::before{content:"\f55f"}.bi-skip-start-btn::before{content:"\f560"}.bi-skip-start-circle-fill::before{content:"\f561"}.bi-skip-start-circle::before{content:"\f562"}.bi-skip-start-fill::before{content:"\f563"}.bi-skip-start::before{content:"\f564"}.bi-slack::before{content:"\f565"}.bi-slash-circle-fill::before{content:"\f566"}.bi-slash-circle::before{content:"\f567"}.bi-slash-square-fill::before{content:"\f568"}.bi-slash-square::before{content:"\f569"}.bi-slash::before{content:"\f56a"}.bi-sliders::before{content:"\f56b"}.bi-smartwatch::before{content:"\f56c"}.bi-snow::before{content:"\f56d"}.bi-snow2::before{content:"\f56e"}.bi-snow3::before{content:"\f56f"}.bi-sort-alpha-down-alt::before{content:"\f570"}.bi-sort-alpha-down::before{content:"\f571"}.bi-sort-alpha-up-alt::before{content:"\f572"}.bi-sort-alpha-up::before{content:"\f573"}.bi-sort-down-alt::before{content:"\f574"}.bi-sort-down::before{content:"\f575"}.bi-sort-numeric-down-alt::before{content:"\f576"}.bi-sort-numeric-down::before{content:"\f577"}.bi-sort-numeric-up-alt::before{content:"\f578"}.bi-sort-numeric-up::before{content:"\f579"}.bi-sort-up-alt::before{content:"\f57a"}.bi-sort-up::before{content:"\f57b"}.bi-soundwave::before{content:"\f57c"}.bi-speaker-fill::before{content:"\f57d"}.bi-speaker::before{content:"\f57e"}.bi-speedometer::before{content:"\f57f"}.bi-speedometer2::before{content:"\f580"}.bi-spellcheck::before{content:"\f581"}.bi-square-fill::before{content:"\f582"}.bi-square-half::before{content:"\f583"}.bi-square::before{content:"\f584"}.bi-stack::before{content:"\f585"}.bi-star-fill::before{content:"\f586"}.bi-star-half::before{content:"\f587"}.bi-star::before{content:"\f588"}.bi-stars::before{content:"\f589"}.bi-stickies-fill::before{content:"\f58a"}.bi-stickies::before{content:"\f58b"}.bi-sticky-fill::before{content:"\f58c"}.bi-sticky::before{content:"\f58d"}.bi-stop-btn-fill::before{content:"\f58e"}.bi-stop-btn::before{content:"\f58f"}.bi-stop-circle-fill::before{content:"\f590"}.bi-stop-circle::before{content:"\f591"}.bi-stop-fill::before{content:"\f592"}.bi-stop::before{content:"\f593"}.bi-stoplights-fill::before{content:"\f594"}.bi-stoplights::before{content:"\f595"}.bi-stopwatch-fill::before{content:"\f596"}.bi-stopwatch::before{content:"\f597"}.bi-subtract::before{content:"\f598"}.bi-suit-club-fill::before{content:"\f599"}.bi-suit-club::before{content:"\f59a"}.bi-suit-diamond-fill::before{content:"\f59b"}.bi-suit-diamond::before{content:"\f59c"}.bi-suit-heart-fill::before{content:"\f59d"}.bi-suit-heart::before{content:"\f59e"}.bi-suit-spade-fill::before{content:"\f59f"}.bi-suit-spade::before{content:"\f5a0"}.bi-sun-fill::before{content:"\f5a1"}.bi-sun::before{content:"\f5a2"}.bi-sunglasses::before{content:"\f5a3"}.bi-sunrise-fill::before{content:"\f5a4"}.bi-sunrise::before{content:"\f5a5"}.bi-sunset-fill::before{content:"\f5a6"}.bi-sunset::before{content:"\f5a7"}.bi-symmetry-horizontal::before{content:"\f5a8"}.bi-symmetry-vertical::before{content:"\f5a9"}.bi-table::before{content:"\f5aa"}.bi-tablet-fill::before{content:"\f5ab"}.bi-tablet-landscape-fill::before{content:"\f5ac"}.bi-tablet-landscape::before{content:"\f5ad"}.bi-tablet::before{content:"\f5ae"}.bi-tag-fill::before{content:"\f5af"}.bi-tag::before{content:"\f5b0"}.bi-tags-fill::before{content:"\f5b1"}.bi-tags::before{content:"\f5b2"}.bi-telegram::before{content:"\f5b3"}.bi-telephone-fill::before{content:"\f5b4"}.bi-telephone-forward-fill::before{content:"\f5b5"}.bi-telephone-forward::before{content:"\f5b6"}.bi-telephone-inbound-fill::before{content:"\f5b7"}.bi-telephone-inbound::before{content:"\f5b8"}.bi-telephone-minus-fill::before{content:"\f5b9"}.bi-telephone-minus::before{content:"\f5ba"}.bi-telephone-outbound-fill::before{content:"\f5bb"}.bi-telephone-outbound::before{content:"\f5bc"}.bi-telephone-plus-fill::before{content:"\f5bd"}.bi-telephone-plus::before{content:"\f5be"}.bi-telephone-x-fill::before{content:"\f5bf"}.bi-telephone-x::before{content:"\f5c0"}.bi-telephone::before{content:"\f5c1"}.bi-terminal-fill::before{content:"\f5c2"}.bi-terminal::before{content:"\f5c3"}.bi-text-center::before{content:"\f5c4"}.bi-text-indent-left::before{content:"\f5c5"}.bi-text-indent-right::before{content:"\f5c6"}.bi-text-left::before{content:"\f5c7"}.bi-text-paragraph::before{content:"\f5c8"}.bi-text-right::before{content:"\f5c9"}.bi-textarea-resize::before{content:"\f5ca"}.bi-textarea-t::before{content:"\f5cb"}.bi-textarea::before{content:"\f5cc"}.bi-thermometer-half::before{content:"\f5cd"}.bi-thermometer-high::before{content:"\f5ce"}.bi-thermometer-low::before{content:"\f5cf"}.bi-thermometer-snow::before{content:"\f5d0"}.bi-thermometer-sun::before{content:"\f5d1"}.bi-thermometer::before{content:"\f5d2"}.bi-three-dots-vertical::before{content:"\f5d3"}.bi-three-dots::before{content:"\f5d4"}.bi-toggle-off::before{content:"\f5d5"}.bi-toggle-on::before{content:"\f5d6"}.bi-toggle2-off::before{content:"\f5d7"}.bi-toggle2-on::before{content:"\f5d8"}.bi-toggles::before{content:"\f5d9"}.bi-toggles2::before{content:"\f5da"}.bi-tools::before{content:"\f5db"}.bi-tornado::before{content:"\f5dc"}.bi-trash-fill::before{content:"\f5dd"}.bi-trash::before{content:"\f5de"}.bi-trash2-fill::before{content:"\f5df"}.bi-trash2::before{content:"\f5e0"}.bi-tree-fill::before{content:"\f5e1"}.bi-tree::before{content:"\f5e2"}.bi-triangle-fill::before{content:"\f5e3"}.bi-triangle-half::before{content:"\f5e4"}.bi-triangle::before{content:"\f5e5"}.bi-trophy-fill::before{content:"\f5e6"}.bi-trophy::before{content:"\f5e7"}.bi-tropical-storm::before{content:"\f5e8"}.bi-truck-flatbed::before{content:"\f5e9"}.bi-truck::before{content:"\f5ea"}.bi-tsunami::before{content:"\f5eb"}.bi-tv-fill::before{content:"\f5ec"}.bi-tv::before{content:"\f5ed"}.bi-twitch::before{content:"\f5ee"}.bi-twitter::before{content:"\f5ef"}.bi-type-bold::before{content:"\f5f0"}.bi-type-h1::before{content:"\f5f1"}.bi-type-h2::before{content:"\f5f2"}.bi-type-h3::before{content:"\f5f3"}.bi-type-italic::before{content:"\f5f4"}.bi-type-strikethrough::before{content:"\f5f5"}.bi-type-underline::before{content:"\f5f6"}.bi-type::before{content:"\f5f7"}.bi-ui-checks-grid::before{content:"\f5f8"}.bi-ui-checks::before{content:"\f5f9"}.bi-ui-radios-grid::before{content:"\f5fa"}.bi-ui-radios::before{content:"\f5fb"}.bi-umbrella-fill::before{content:"\f5fc"}.bi-umbrella::before{content:"\f5fd"}.bi-union::before{content:"\f5fe"}.bi-unlock-fill::before{content:"\f5ff"}.bi-unlock::before{content:"\f600"}.bi-upc-scan::before{content:"\f601"}.bi-upc::before{content:"\f602"}.bi-upload::before{content:"\f603"}.bi-vector-pen::before{content:"\f604"}.bi-view-list::before{content:"\f605"}.bi-view-stacked::before{content:"\f606"}.bi-vinyl-fill::before{content:"\f607"}.bi-vinyl::before{content:"\f608"}.bi-voicemail::before{content:"\f609"}.bi-volume-down-fill::before{content:"\f60a"}.bi-volume-down::before{content:"\f60b"}.bi-volume-mute-fill::before{content:"\f60c"}.bi-volume-mute::before{content:"\f60d"}.bi-volume-off-fill::before{content:"\f60e"}.bi-volume-off::before{content:"\f60f"}.bi-volume-up-fill::before{content:"\f610"}.bi-volume-up::before{content:"\f611"}.bi-vr::before{content:"\f612"}.bi-wallet-fill::before{content:"\f613"}.bi-wallet::before{content:"\f614"}.bi-wallet2::before{content:"\f615"}.bi-watch::before{content:"\f616"}.bi-water::before{content:"\f617"}.bi-whatsapp::before{content:"\f618"}.bi-wifi-1::before{content:"\f619"}.bi-wifi-2::before{content:"\f61a"}.bi-wifi-off::before{content:"\f61b"}.bi-wifi::before{content:"\f61c"}.bi-wind::before{content:"\f61d"}.bi-window-dock::before{content:"\f61e"}.bi-window-sidebar::before{content:"\f61f"}.bi-window::before{content:"\f620"}.bi-wrench::before{content:"\f621"}.bi-x-circle-fill::before{content:"\f622"}.bi-x-circle::before{content:"\f623"}.bi-x-diamond-fill::before{content:"\f624"}.bi-x-diamond::before{content:"\f625"}.bi-x-octagon-fill::before{content:"\f626"}.bi-x-octagon::before{content:"\f627"}.bi-x-square-fill::before{content:"\f628"}.bi-x-square::before{content:"\f629"}.bi-x::before{content:"\f62a"}.bi-youtube::before{content:"\f62b"}.bi-zoom-in::before{content:"\f62c"}.bi-zoom-out::before{content:"\f62d"}.bi-bank::before{content:"\f62e"}.bi-bank2::before{content:"\f62f"}.bi-bell-slash-fill::before{content:"\f630"}.bi-bell-slash::before{content:"\f631"}.bi-cash-coin::before{content:"\f632"}.bi-check-lg::before{content:"\f633"}.bi-coin::before{content:"\f634"}.bi-currency-bitcoin::before{content:"\f635"}.bi-currency-dollar::before{content:"\f636"}.bi-currency-euro::before{content:"\f637"}.bi-currency-exchange::before{content:"\f638"}.bi-currency-pound::before{content:"\f639"}.bi-currency-yen::before{content:"\f63a"}.bi-dash-lg::before{content:"\f63b"}.bi-exclamation-lg::before{content:"\f63c"}.bi-file-earmark-pdf-fill::before{content:"\f63d"}.bi-file-earmark-pdf::before{content:"\f63e"}.bi-file-pdf-fill::before{content:"\f63f"}.bi-file-pdf::before{content:"\f640"}.bi-gender-ambiguous::before{content:"\f641"}.bi-gender-female::before{content:"\f642"}.bi-gender-male::before{content:"\f643"}.bi-gender-trans::before{content:"\f644"}.bi-headset-vr::before{content:"\f645"}.bi-info-lg::before{content:"\f646"}.bi-mastodon::before{content:"\f647"}.bi-messenger::before{content:"\f648"}.bi-piggy-bank-fill::before{content:"\f649"}.bi-piggy-bank::before{content:"\f64a"}.bi-pin-map-fill::before{content:"\f64b"}.bi-pin-map::before{content:"\f64c"}.bi-plus-lg::before{content:"\f64d"}.bi-question-lg::before{content:"\f64e"}.bi-recycle::before{content:"\f64f"}.bi-reddit::before{content:"\f650"}.bi-safe-fill::before{content:"\f651"}.bi-safe2-fill::before{content:"\f652"}.bi-safe2::before{content:"\f653"}.bi-sd-card-fill::before{content:"\f654"}.bi-sd-card::before{content:"\f655"}.bi-skype::before{content:"\f656"}.bi-slash-lg::before{content:"\f657"}.bi-translate::before{content:"\f658"}.bi-x-lg::before{content:"\f659"}.bi-safe::before{content:"\f65a"}.bi-apple::before{content:"\f65b"}.bi-microsoft::before{content:"\f65d"}.bi-windows::before{content:"\f65e"}.bi-behance::before{content:"\f65c"}.bi-dribbble::before{content:"\f65f"}.bi-line::before{content:"\f660"}.bi-medium::before{content:"\f661"}.bi-paypal::before{content:"\f662"}.bi-pinterest::before{content:"\f663"}.bi-signal::before{content:"\f664"}.bi-snapchat::before{content:"\f665"}.bi-spotify::before{content:"\f666"}.bi-stack-overflow::before{content:"\f667"}.bi-strava::before{content:"\f668"}.bi-wordpress::before{content:"\f669"}.bi-vimeo::before{content:"\f66a"}.bi-activity::before{content:"\f66b"}.bi-easel2-fill::before{content:"\f66c"}.bi-easel2::before{content:"\f66d"}.bi-easel3-fill::before{content:"\f66e"}.bi-easel3::before{content:"\f66f"}.bi-fan::before{content:"\f670"}.bi-fingerprint::before{content:"\f671"}.bi-graph-down-arrow::before{content:"\f672"}.bi-graph-up-arrow::before{content:"\f673"}.bi-hypnotize::before{content:"\f674"}.bi-magic::before{content:"\f675"}.bi-person-rolodex::before{content:"\f676"}.bi-person-video::before{content:"\f677"}.bi-person-video2::before{content:"\f678"}.bi-person-video3::before{content:"\f679"}.bi-person-workspace::before{content:"\f67a"}.bi-radioactive::before{content:"\f67b"}.bi-webcam-fill::before{content:"\f67c"}.bi-webcam::before{content:"\f67d"}.bi-yin-yang::before{content:"\f67e"}.bi-bandaid-fill::before{content:"\f680"}.bi-bandaid::before{content:"\f681"}.bi-bluetooth::before{content:"\f682"}.bi-body-text::before{content:"\f683"}.bi-boombox::before{content:"\f684"}.bi-boxes::before{content:"\f685"}.bi-dpad-fill::before{content:"\f686"}.bi-dpad::before{content:"\f687"}.bi-ear-fill::before{content:"\f688"}.bi-ear::before{content:"\f689"}.bi-envelope-check-fill::before{content:"\f68b"}.bi-envelope-check::before{content:"\f68c"}.bi-envelope-dash-fill::before{content:"\f68e"}.bi-envelope-dash::before{content:"\f68f"}.bi-envelope-exclamation-fill::before{content:"\f691"}.bi-envelope-exclamation::before{content:"\f692"}.bi-envelope-plus-fill::before{content:"\f693"}.bi-envelope-plus::before{content:"\f694"}.bi-envelope-slash-fill::before{content:"\f696"}.bi-envelope-slash::before{content:"\f697"}.bi-envelope-x-fill::before{content:"\f699"}.bi-envelope-x::before{content:"\f69a"}.bi-explicit-fill::before{content:"\f69b"}.bi-explicit::before{content:"\f69c"}.bi-git::before{content:"\f69d"}.bi-infinity::before{content:"\f69e"}.bi-list-columns-reverse::before{content:"\f69f"}.bi-list-columns::before{content:"\f6a0"}.bi-meta::before{content:"\f6a1"}.bi-nintendo-switch::before{content:"\f6a4"}.bi-pc-display-horizontal::before{content:"\f6a5"}.bi-pc-display::before{content:"\f6a6"}.bi-pc-horizontal::before{content:"\f6a7"}.bi-pc::before{content:"\f6a8"}.bi-playstation::before{content:"\f6a9"}.bi-plus-slash-minus::before{content:"\f6aa"}.bi-projector-fill::before{content:"\f6ab"}.bi-projector::before{content:"\f6ac"}.bi-qr-code-scan::before{content:"\f6ad"}.bi-qr-code::before{content:"\f6ae"}.bi-quora::before{content:"\f6af"}.bi-quote::before{content:"\f6b0"}.bi-robot::before{content:"\f6b1"}.bi-send-check-fill::before{content:"\f6b2"}.bi-send-check::before{content:"\f6b3"}.bi-send-dash-fill::before{content:"\f6b4"}.bi-send-dash::before{content:"\f6b5"}.bi-send-exclamation-fill::before{content:"\f6b7"}.bi-send-exclamation::before{content:"\f6b8"}.bi-send-fill::before{content:"\f6b9"}.bi-send-plus-fill::before{content:"\f6ba"}.bi-send-plus::before{content:"\f6bb"}.bi-send-slash-fill::before{content:"\f6bc"}.bi-send-slash::before{content:"\f6bd"}.bi-send-x-fill::before{content:"\f6be"}.bi-send-x::before{content:"\f6bf"}.bi-send::before{content:"\f6c0"}.bi-steam::before{content:"\f6c1"}.bi-terminal-dash::before{content:"\f6c3"}.bi-terminal-plus::before{content:"\f6c4"}.bi-terminal-split::before{content:"\f6c5"}.bi-ticket-detailed-fill::before{content:"\f6c6"}.bi-ticket-detailed::before{content:"\f6c7"}.bi-ticket-fill::before{content:"\f6c8"}.bi-ticket-perforated-fill::before{content:"\f6c9"}.bi-ticket-perforated::before{content:"\f6ca"}.bi-ticket::before{content:"\f6cb"}.bi-tiktok::before{content:"\f6cc"}.bi-window-dash::before{content:"\f6cd"}.bi-window-desktop::before{content:"\f6ce"}.bi-window-fullscreen::before{content:"\f6cf"}.bi-window-plus::before{content:"\f6d0"}.bi-window-split::before{content:"\f6d1"}.bi-window-stack::before{content:"\f6d2"}.bi-window-x::before{content:"\f6d3"}.bi-xbox::before{content:"\f6d4"}.bi-ethernet::before{content:"\f6d5"}.bi-hdmi-fill::before{content:"\f6d6"}.bi-hdmi::before{content:"\f6d7"}.bi-usb-c-fill::before{content:"\f6d8"}.bi-usb-c::before{content:"\f6d9"}.bi-usb-fill::before{content:"\f6da"}.bi-usb-plug-fill::before{content:"\f6db"}.bi-usb-plug::before{content:"\f6dc"}.bi-usb-symbol::before{content:"\f6dd"}.bi-usb::before{content:"\f6de"}.bi-boombox-fill::before{content:"\f6df"}.bi-displayport::before{content:"\f6e1"}.bi-gpu-card::before{content:"\f6e2"}.bi-memory::before{content:"\f6e3"}.bi-modem-fill::before{content:"\f6e4"}.bi-modem::before{content:"\f6e5"}.bi-motherboard-fill::before{content:"\f6e6"}.bi-motherboard::before{content:"\f6e7"}.bi-optical-audio-fill::before{content:"\f6e8"}.bi-optical-audio::before{content:"\f6e9"}.bi-pci-card::before{content:"\f6ea"}.bi-router-fill::before{content:"\f6eb"}.bi-router::before{content:"\f6ec"}.bi-thunderbolt-fill::before{content:"\f6ef"}.bi-thunderbolt::before{content:"\f6f0"}.bi-usb-drive-fill::before{content:"\f6f1"}.bi-usb-drive::before{content:"\f6f2"}.bi-usb-micro-fill::before{content:"\f6f3"}.bi-usb-micro::before{content:"\f6f4"}.bi-usb-mini-fill::before{content:"\f6f5"}.bi-usb-mini::before{content:"\f6f6"}.bi-cloud-haze2::before{content:"\f6f7"}.bi-device-hdd-fill::before{content:"\f6f8"}.bi-device-hdd::before{content:"\f6f9"}.bi-device-ssd-fill::before{content:"\f6fa"}.bi-device-ssd::before{content:"\f6fb"}.bi-displayport-fill::before{content:"\f6fc"}.bi-mortarboard-fill::before{content:"\f6fd"}.bi-mortarboard::before{content:"\f6fe"}.bi-terminal-x::before{content:"\f6ff"}.bi-arrow-through-heart-fill::before{content:"\f700"}.bi-arrow-through-heart::before{content:"\f701"}.bi-badge-sd-fill::before{content:"\f702"}.bi-badge-sd::before{content:"\f703"}.bi-bag-heart-fill::before{content:"\f704"}.bi-bag-heart::before{content:"\f705"}.bi-balloon-fill::before{content:"\f706"}.bi-balloon-heart-fill::before{content:"\f707"}.bi-balloon-heart::before{content:"\f708"}.bi-balloon::before{content:"\f709"}.bi-box2-fill::before{content:"\f70a"}.bi-box2-heart-fill::before{content:"\f70b"}.bi-box2-heart::before{content:"\f70c"}.bi-box2::before{content:"\f70d"}.bi-braces-asterisk::before{content:"\f70e"}.bi-calendar-heart-fill::before{content:"\f70f"}.bi-calendar-heart::before{content:"\f710"}.bi-calendar2-heart-fill::before{content:"\f711"}.bi-calendar2-heart::before{content:"\f712"}.bi-chat-heart-fill::before{content:"\f713"}.bi-chat-heart::before{content:"\f714"}.bi-chat-left-heart-fill::before{content:"\f715"}.bi-chat-left-heart::before{content:"\f716"}.bi-chat-right-heart-fill::before{content:"\f717"}.bi-chat-right-heart::before{content:"\f718"}.bi-chat-square-heart-fill::before{content:"\f719"}.bi-chat-square-heart::before{content:"\f71a"}.bi-clipboard-check-fill::before{content:"\f71b"}.bi-clipboard-data-fill::before{content:"\f71c"}.bi-clipboard-fill::before{content:"\f71d"}.bi-clipboard-heart-fill::before{content:"\f71e"}.bi-clipboard-heart::before{content:"\f71f"}.bi-clipboard-minus-fill::before{content:"\f720"}.bi-clipboard-plus-fill::before{content:"\f721"}.bi-clipboard-pulse::before{content:"\f722"}.bi-clipboard-x-fill::before{content:"\f723"}.bi-clipboard2-check-fill::before{content:"\f724"}.bi-clipboard2-check::before{content:"\f725"}.bi-clipboard2-data-fill::before{content:"\f726"}.bi-clipboard2-data::before{content:"\f727"}.bi-clipboard2-fill::before{content:"\f728"}.bi-clipboard2-heart-fill::before{content:"\f729"}.bi-clipboard2-heart::before{content:"\f72a"}.bi-clipboard2-minus-fill::before{content:"\f72b"}.bi-clipboard2-minus::before{content:"\f72c"}.bi-clipboard2-plus-fill::before{content:"\f72d"}.bi-clipboard2-plus::before{content:"\f72e"}.bi-clipboard2-pulse-fill::before{content:"\f72f"}.bi-clipboard2-pulse::before{content:"\f730"}.bi-clipboard2-x-fill::before{content:"\f731"}.bi-clipboard2-x::before{content:"\f732"}.bi-clipboard2::before{content:"\f733"}.bi-emoji-kiss-fill::before{content:"\f734"}.bi-emoji-kiss::before{content:"\f735"}.bi-envelope-heart-fill::before{content:"\f736"}.bi-envelope-heart::before{content:"\f737"}.bi-envelope-open-heart-fill::before{content:"\f738"}.bi-envelope-open-heart::before{content:"\f739"}.bi-envelope-paper-fill::before{content:"\f73a"}.bi-envelope-paper-heart-fill::before{content:"\f73b"}.bi-envelope-paper-heart::before{content:"\f73c"}.bi-envelope-paper::before{content:"\f73d"}.bi-filetype-aac::before{content:"\f73e"}.bi-filetype-ai::before{content:"\f73f"}.bi-filetype-bmp::before{content:"\f740"}.bi-filetype-cs::before{content:"\f741"}.bi-filetype-css::before{content:"\f742"}.bi-filetype-csv::before{content:"\f743"}.bi-filetype-doc::before{content:"\f744"}.bi-filetype-docx::before{content:"\f745"}.bi-filetype-exe::before{content:"\f746"}.bi-filetype-gif::before{content:"\f747"}.bi-filetype-heic::before{content:"\f748"}.bi-filetype-html::before{content:"\f749"}.bi-filetype-java::before{content:"\f74a"}.bi-filetype-jpg::before{content:"\f74b"}.bi-filetype-js::before{content:"\f74c"}.bi-filetype-jsx::before{content:"\f74d"}.bi-filetype-key::before{content:"\f74e"}.bi-filetype-m4p::before{content:"\f74f"}.bi-filetype-md::before{content:"\f750"}.bi-filetype-mdx::before{content:"\f751"}.bi-filetype-mov::before{content:"\f752"}.bi-filetype-mp3::before{content:"\f753"}.bi-filetype-mp4::before{content:"\f754"}.bi-filetype-otf::before{content:"\f755"}.bi-filetype-pdf::before{content:"\f756"}.bi-filetype-php::before{content:"\f757"}.bi-filetype-png::before{content:"\f758"}.bi-filetype-ppt::before{content:"\f75a"}.bi-filetype-psd::before{content:"\f75b"}.bi-filetype-py::before{content:"\f75c"}.bi-filetype-raw::before{content:"\f75d"}.bi-filetype-rb::before{content:"\f75e"}.bi-filetype-sass::before{content:"\f75f"}.bi-filetype-scss::before{content:"\f760"}.bi-filetype-sh::before{content:"\f761"}.bi-filetype-svg::before{content:"\f762"}.bi-filetype-tiff::before{content:"\f763"}.bi-filetype-tsx::before{content:"\f764"}.bi-filetype-ttf::before{content:"\f765"}.bi-filetype-txt::before{content:"\f766"}.bi-filetype-wav::before{content:"\f767"}.bi-filetype-woff::before{content:"\f768"}.bi-filetype-xls::before{content:"\f76a"}.bi-filetype-xml::before{content:"\f76b"}.bi-filetype-yml::before{content:"\f76c"}.bi-heart-arrow::before{content:"\f76d"}.bi-heart-pulse-fill::before{content:"\f76e"}.bi-heart-pulse::before{content:"\f76f"}.bi-heartbreak-fill::before{content:"\f770"}.bi-heartbreak::before{content:"\f771"}.bi-hearts::before{content:"\f772"}.bi-hospital-fill::before{content:"\f773"}.bi-hospital::before{content:"\f774"}.bi-house-heart-fill::before{content:"\f775"}.bi-house-heart::before{content:"\f776"}.bi-incognito::before{content:"\f777"}.bi-magnet-fill::before{content:"\f778"}.bi-magnet::before{content:"\f779"}.bi-person-heart::before{content:"\f77a"}.bi-person-hearts::before{content:"\f77b"}.bi-phone-flip::before{content:"\f77c"}.bi-plugin::before{content:"\f77d"}.bi-postage-fill::before{content:"\f77e"}.bi-postage-heart-fill::before{content:"\f77f"}.bi-postage-heart::before{content:"\f780"}.bi-postage::before{content:"\f781"}.bi-postcard-fill::before{content:"\f782"}.bi-postcard-heart-fill::before{content:"\f783"}.bi-postcard-heart::before{content:"\f784"}.bi-postcard::before{content:"\f785"}.bi-search-heart-fill::before{content:"\f786"}.bi-search-heart::before{content:"\f787"}.bi-sliders2-vertical::before{content:"\f788"}.bi-sliders2::before{content:"\f789"}.bi-trash3-fill::before{content:"\f78a"}.bi-trash3::before{content:"\f78b"}.bi-valentine::before{content:"\f78c"}.bi-valentine2::before{content:"\f78d"}.bi-wrench-adjustable-circle-fill::before{content:"\f78e"}.bi-wrench-adjustable-circle::before{content:"\f78f"}.bi-wrench-adjustable::before{content:"\f790"}.bi-filetype-json::before{content:"\f791"}.bi-filetype-pptx::before{content:"\f792"}.bi-filetype-xlsx::before{content:"\f793"}.bi-1-circle-fill::before{content:"\f796"}.bi-1-circle::before{content:"\f797"}.bi-1-square-fill::before{content:"\f798"}.bi-1-square::before{content:"\f799"}.bi-2-circle-fill::before{content:"\f79c"}.bi-2-circle::before{content:"\f79d"}.bi-2-square-fill::before{content:"\f79e"}.bi-2-square::before{content:"\f79f"}.bi-3-circle-fill::before{content:"\f7a2"}.bi-3-circle::before{content:"\f7a3"}.bi-3-square-fill::before{content:"\f7a4"}.bi-3-square::before{content:"\f7a5"}.bi-4-circle-fill::before{content:"\f7a8"}.bi-4-circle::before{content:"\f7a9"}.bi-4-square-fill::before{content:"\f7aa"}.bi-4-square::before{content:"\f7ab"}.bi-5-circle-fill::before{content:"\f7ae"}.bi-5-circle::before{content:"\f7af"}.bi-5-square-fill::before{content:"\f7b0"}.bi-5-square::before{content:"\f7b1"}.bi-6-circle-fill::before{content:"\f7b4"}.bi-6-circle::before{content:"\f7b5"}.bi-6-square-fill::before{content:"\f7b6"}.bi-6-square::before{content:"\f7b7"}.bi-7-circle-fill::before{content:"\f7ba"}.bi-7-circle::before{content:"\f7bb"}.bi-7-square-fill::before{content:"\f7bc"}.bi-7-square::before{content:"\f7bd"}.bi-8-circle-fill::before{content:"\f7c0"}.bi-8-circle::before{content:"\f7c1"}.bi-8-square-fill::before{content:"\f7c2"}.bi-8-square::before{content:"\f7c3"}.bi-9-circle-fill::before{content:"\f7c6"}.bi-9-circle::before{content:"\f7c7"}.bi-9-square-fill::before{content:"\f7c8"}.bi-9-square::before{content:"\f7c9"}.bi-airplane-engines-fill::before{content:"\f7ca"}.bi-airplane-engines::before{content:"\f7cb"}.bi-airplane-fill::before{content:"\f7cc"}.bi-airplane::before{content:"\f7cd"}.bi-alexa::before{content:"\f7ce"}.bi-alipay::before{content:"\f7cf"}.bi-android::before{content:"\f7d0"}.bi-android2::before{content:"\f7d1"}.bi-box-fill::before{content:"\f7d2"}.bi-box-seam-fill::before{content:"\f7d3"}.bi-browser-chrome::before{content:"\f7d4"}.bi-browser-edge::before{content:"\f7d5"}.bi-browser-firefox::before{content:"\f7d6"}.bi-browser-safari::before{content:"\f7d7"}.bi-c-circle-fill::before{content:"\f7da"}.bi-c-circle::before{content:"\f7db"}.bi-c-square-fill::before{content:"\f7dc"}.bi-c-square::before{content:"\f7dd"}.bi-capsule-pill::before{content:"\f7de"}.bi-capsule::before{content:"\f7df"}.bi-car-front-fill::before{content:"\f7e0"}.bi-car-front::before{content:"\f7e1"}.bi-cassette-fill::before{content:"\f7e2"}.bi-cassette::before{content:"\f7e3"}.bi-cc-circle-fill::before{content:"\f7e6"}.bi-cc-circle::before{content:"\f7e7"}.bi-cc-square-fill::before{content:"\f7e8"}.bi-cc-square::before{content:"\f7e9"}.bi-cup-hot-fill::before{content:"\f7ea"}.bi-cup-hot::before{content:"\f7eb"}.bi-currency-rupee::before{content:"\f7ec"}.bi-dropbox::before{content:"\f7ed"}.bi-escape::before{content:"\f7ee"}.bi-fast-forward-btn-fill::before{content:"\f7ef"}.bi-fast-forward-btn::before{content:"\f7f0"}.bi-fast-forward-circle-fill::before{content:"\f7f1"}.bi-fast-forward-circle::before{content:"\f7f2"}.bi-fast-forward-fill::before{content:"\f7f3"}.bi-fast-forward::before{content:"\f7f4"}.bi-filetype-sql::before{content:"\f7f5"}.bi-fire::before{content:"\f7f6"}.bi-google-play::before{content:"\f7f7"}.bi-h-circle-fill::before{content:"\f7fa"}.bi-h-circle::before{content:"\f7fb"}.bi-h-square-fill::before{content:"\f7fc"}.bi-h-square::before{content:"\f7fd"}.bi-indent::before{content:"\f7fe"}.bi-lungs-fill::before{content:"\f7ff"}.bi-lungs::before{content:"\f800"}.bi-microsoft-teams::before{content:"\f801"}.bi-p-circle-fill::before{content:"\f804"}.bi-p-circle::before{content:"\f805"}.bi-p-square-fill::before{content:"\f806"}.bi-p-square::before{content:"\f807"}.bi-pass-fill::before{content:"\f808"}.bi-pass::before{content:"\f809"}.bi-prescription::before{content:"\f80a"}.bi-prescription2::before{content:"\f80b"}.bi-r-circle-fill::before{content:"\f80e"}.bi-r-circle::before{content:"\f80f"}.bi-r-square-fill::before{content:"\f810"}.bi-r-square::before{content:"\f811"}.bi-repeat-1::before{content:"\f812"}.bi-repeat::before{content:"\f813"}.bi-rewind-btn-fill::before{content:"\f814"}.bi-rewind-btn::before{content:"\f815"}.bi-rewind-circle-fill::before{content:"\f816"}.bi-rewind-circle::before{content:"\f817"}.bi-rewind-fill::before{content:"\f818"}.bi-rewind::before{content:"\f819"}.bi-train-freight-front-fill::before{content:"\f81a"}.bi-train-freight-front::before{content:"\f81b"}.bi-train-front-fill::before{content:"\f81c"}.bi-train-front::before{content:"\f81d"}.bi-train-lightrail-front-fill::before{content:"\f81e"}.bi-train-lightrail-front::before{content:"\f81f"}.bi-truck-front-fill::before{content:"\f820"}.bi-truck-front::before{content:"\f821"}.bi-ubuntu::before{content:"\f822"}.bi-unindent::before{content:"\f823"}.bi-unity::before{content:"\f824"}.bi-universal-access-circle::before{content:"\f825"}.bi-universal-access::before{content:"\f826"}.bi-virus::before{content:"\f827"}.bi-virus2::before{content:"\f828"}.bi-wechat::before{content:"\f829"}.bi-yelp::before{content:"\f82a"}.bi-sign-stop-fill::before{content:"\f82b"}.bi-sign-stop-lights-fill::before{content:"\f82c"}.bi-sign-stop-lights::before{content:"\f82d"}.bi-sign-stop::before{content:"\f82e"}.bi-sign-turn-left-fill::before{content:"\f82f"}.bi-sign-turn-left::before{content:"\f830"}.bi-sign-turn-right-fill::before{content:"\f831"}.bi-sign-turn-right::before{content:"\f832"}.bi-sign-turn-slight-left-fill::before{content:"\f833"}.bi-sign-turn-slight-left::before{content:"\f834"}.bi-sign-turn-slight-right-fill::before{content:"\f835"}.bi-sign-turn-slight-right::before{content:"\f836"}.bi-sign-yield-fill::before{content:"\f837"}.bi-sign-yield::before{content:"\f838"}.bi-ev-station-fill::before{content:"\f839"}.bi-ev-station::before{content:"\f83a"}.bi-fuel-pump-diesel-fill::before{content:"\f83b"}.bi-fuel-pump-diesel::before{content:"\f83c"}.bi-fuel-pump-fill::before{content:"\f83d"}.bi-fuel-pump::before{content:"\f83e"}.bi-0-circle-fill::before{content:"\f83f"}.bi-0-circle::before{content:"\f840"}.bi-0-square-fill::before{content:"\f841"}.bi-0-square::before{content:"\f842"}.bi-rocket-fill::before{content:"\f843"}.bi-rocket-takeoff-fill::before{content:"\f844"}.bi-rocket-takeoff::before{content:"\f845"}.bi-rocket::before{content:"\f846"}.bi-stripe::before{content:"\f847"}.bi-subscript::before{content:"\f848"}.bi-superscript::before{content:"\f849"}.bi-trello::before{content:"\f84a"}.bi-envelope-at-fill::before{content:"\f84b"}.bi-envelope-at::before{content:"\f84c"}.bi-regex::before{content:"\f84d"}.bi-text-wrap::before{content:"\f84e"}.bi-sign-dead-end-fill::before{content:"\f84f"}.bi-sign-dead-end::before{content:"\f850"}.bi-sign-do-not-enter-fill::before{content:"\f851"}.bi-sign-do-not-enter::before{content:"\f852"}.bi-sign-intersection-fill::before{content:"\f853"}.bi-sign-intersection-side-fill::before{content:"\f854"}.bi-sign-intersection-side::before{content:"\f855"}.bi-sign-intersection-t-fill::before{content:"\f856"}.bi-sign-intersection-t::before{content:"\f857"}.bi-sign-intersection-y-fill::before{content:"\f858"}.bi-sign-intersection-y::before{content:"\f859"}.bi-sign-intersection::before{content:"\f85a"}.bi-sign-merge-left-fill::before{content:"\f85b"}.bi-sign-merge-left::before{content:"\f85c"}.bi-sign-merge-right-fill::before{content:"\f85d"}.bi-sign-merge-right::before{content:"\f85e"}.bi-sign-no-left-turn-fill::before{content:"\f85f"}.bi-sign-no-left-turn::before{content:"\f860"}.bi-sign-no-parking-fill::before{content:"\f861"}.bi-sign-no-parking::before{content:"\f862"}.bi-sign-no-right-turn-fill::before{content:"\f863"}.bi-sign-no-right-turn::before{content:"\f864"}.bi-sign-railroad-fill::before{content:"\f865"}.bi-sign-railroad::before{content:"\f866"}.bi-building-add::before{content:"\f867"}.bi-building-check::before{content:"\f868"}.bi-building-dash::before{content:"\f869"}.bi-building-down::before{content:"\f86a"}.bi-building-exclamation::before{content:"\f86b"}.bi-building-fill-add::before{content:"\f86c"}.bi-building-fill-check::before{content:"\f86d"}.bi-building-fill-dash::before{content:"\f86e"}.bi-building-fill-down::before{content:"\f86f"}.bi-building-fill-exclamation::before{content:"\f870"}.bi-building-fill-gear::before{content:"\f871"}.bi-building-fill-lock::before{content:"\f872"}.bi-building-fill-slash::before{content:"\f873"}.bi-building-fill-up::before{content:"\f874"}.bi-building-fill-x::before{content:"\f875"}.bi-building-fill::before{content:"\f876"}.bi-building-gear::before{content:"\f877"}.bi-building-lock::before{content:"\f878"}.bi-building-slash::before{content:"\f879"}.bi-building-up::before{content:"\f87a"}.bi-building-x::before{content:"\f87b"}.bi-buildings-fill::before{content:"\f87c"}.bi-buildings::before{content:"\f87d"}.bi-bus-front-fill::before{content:"\f87e"}.bi-bus-front::before{content:"\f87f"}.bi-ev-front-fill::before{content:"\f880"}.bi-ev-front::before{content:"\f881"}.bi-globe-americas::before{content:"\f882"}.bi-globe-asia-australia::before{content:"\f883"}.bi-globe-central-south-asia::before{content:"\f884"}.bi-globe-europe-africa::before{content:"\f885"}.bi-house-add-fill::before{content:"\f886"}.bi-house-add::before{content:"\f887"}.bi-house-check-fill::before{content:"\f888"}.bi-house-check::before{content:"\f889"}.bi-house-dash-fill::before{content:"\f88a"}.bi-house-dash::before{content:"\f88b"}.bi-house-down-fill::before{content:"\f88c"}.bi-house-down::before{content:"\f88d"}.bi-house-exclamation-fill::before{content:"\f88e"}.bi-house-exclamation::before{content:"\f88f"}.bi-house-gear-fill::before{content:"\f890"}.bi-house-gear::before{content:"\f891"}.bi-house-lock-fill::before{content:"\f892"}.bi-house-lock::before{content:"\f893"}.bi-house-slash-fill::before{content:"\f894"}.bi-house-slash::before{content:"\f895"}.bi-house-up-fill::before{content:"\f896"}.bi-house-up::before{content:"\f897"}.bi-house-x-fill::before{content:"\f898"}.bi-house-x::before{content:"\f899"}.bi-person-add::before{content:"\f89a"}.bi-person-down::before{content:"\f89b"}.bi-person-exclamation::before{content:"\f89c"}.bi-person-fill-add::before{content:"\f89d"}.bi-person-fill-check::before{content:"\f89e"}.bi-person-fill-dash::before{content:"\f89f"}.bi-person-fill-down::before{content:"\f8a0"}.bi-person-fill-exclamation::before{content:"\f8a1"}.bi-person-fill-gear::before{content:"\f8a2"}.bi-person-fill-lock::before{content:"\f8a3"}.bi-person-fill-slash::before{content:"\f8a4"}.bi-person-fill-up::before{content:"\f8a5"}.bi-person-fill-x::before{content:"\f8a6"}.bi-person-gear::before{content:"\f8a7"}.bi-person-lock::before{content:"\f8a8"}.bi-person-slash::before{content:"\f8a9"}.bi-person-up::before{content:"\f8aa"}.bi-scooter::before{content:"\f8ab"}.bi-taxi-front-fill::before{content:"\f8ac"}.bi-taxi-front::before{content:"\f8ad"}.bi-amd::before{content:"\f8ae"}.bi-database-add::before{content:"\f8af"}.bi-database-check::before{content:"\f8b0"}.bi-database-dash::before{content:"\f8b1"}.bi-database-down::before{content:"\f8b2"}.bi-database-exclamation::before{content:"\f8b3"}.bi-database-fill-add::before{content:"\f8b4"}.bi-database-fill-check::before{content:"\f8b5"}.bi-database-fill-dash::before{content:"\f8b6"}.bi-database-fill-down::before{content:"\f8b7"}.bi-database-fill-exclamation::before{content:"\f8b8"}.bi-database-fill-gear::before{content:"\f8b9"}.bi-database-fill-lock::before{content:"\f8ba"}.bi-database-fill-slash::before{content:"\f8bb"}.bi-database-fill-up::before{content:"\f8bc"}.bi-database-fill-x::before{content:"\f8bd"}.bi-database-fill::before{content:"\f8be"}.bi-database-gear::before{content:"\f8bf"}.bi-database-lock::before{content:"\f8c0"}.bi-database-slash::before{content:"\f8c1"}.bi-database-up::before{content:"\f8c2"}.bi-database-x::before{content:"\f8c3"}.bi-database::before{content:"\f8c4"}.bi-houses-fill::before{content:"\f8c5"}.bi-houses::before{content:"\f8c6"}.bi-nvidia::before{content:"\f8c7"}.bi-person-vcard-fill::before{content:"\f8c8"}.bi-person-vcard::before{content:"\f8c9"}.bi-sina-weibo::before{content:"\f8ca"}.bi-tencent-qq::before{content:"\f8cb"}.bi-wikipedia::before{content:"\f8cc"}.bi-alphabet-uppercase::before{content:"\f2a5"}.bi-alphabet::before{content:"\f68a"}.bi-amazon::before{content:"\f68d"}.bi-arrows-collapse-vertical::before{content:"\f690"}.bi-arrows-expand-vertical::before{content:"\f695"}.bi-arrows-vertical::before{content:"\f698"}.bi-arrows::before{content:"\f6a2"}.bi-ban-fill::before{content:"\f6a3"}.bi-ban::before{content:"\f6b6"}.bi-bing::before{content:"\f6c2"}.bi-cake::before{content:"\f6e0"}.bi-cake2::before{content:"\f6ed"}.bi-cookie::before{content:"\f6ee"}.bi-copy::before{content:"\f759"}.bi-crosshair::before{content:"\f769"}.bi-crosshair2::before{content:"\f794"}.bi-emoji-astonished-fill::before{content:"\f795"}.bi-emoji-astonished::before{content:"\f79a"}.bi-emoji-grimace-fill::before{content:"\f79b"}.bi-emoji-grimace::before{content:"\f7a0"}.bi-emoji-grin-fill::before{content:"\f7a1"}.bi-emoji-grin::before{content:"\f7a6"}.bi-emoji-surprise-fill::before{content:"\f7a7"}.bi-emoji-surprise::before{content:"\f7ac"}.bi-emoji-tear-fill::before{content:"\f7ad"}.bi-emoji-tear::before{content:"\f7b2"}.bi-envelope-arrow-down-fill::before{content:"\f7b3"}.bi-envelope-arrow-down::before{content:"\f7b8"}.bi-envelope-arrow-up-fill::before{content:"\f7b9"}.bi-envelope-arrow-up::before{content:"\f7be"}.bi-feather::before{content:"\f7bf"}.bi-feather2::before{content:"\f7c4"}.bi-floppy-fill::before{content:"\f7c5"}.bi-floppy::before{content:"\f7d8"}.bi-floppy2-fill::before{content:"\f7d9"}.bi-floppy2::before{content:"\f7e4"}.bi-gitlab::before{content:"\f7e5"}.bi-highlighter::before{content:"\f7f8"}.bi-marker-tip::before{content:"\f802"}.bi-nvme-fill::before{content:"\f803"}.bi-nvme::before{content:"\f80c"}.bi-opencollective::before{content:"\f80d"}.bi-pci-card-network::before{content:"\f8cd"}.bi-pci-card-sound::before{content:"\f8ce"}.bi-radar::before{content:"\f8cf"}.bi-send-arrow-down-fill::before{content:"\f8d0"}.bi-send-arrow-down::before{content:"\f8d1"}.bi-send-arrow-up-fill::before{content:"\f8d2"}.bi-send-arrow-up::before{content:"\f8d3"}.bi-sim-slash-fill::before{content:"\f8d4"}.bi-sim-slash::before{content:"\f8d5"}.bi-sourceforge::before{content:"\f8d6"}.bi-substack::before{content:"\f8d7"}.bi-threads-fill::before{content:"\f8d8"}.bi-threads::before{content:"\f8d9"}.bi-transparency::before{content:"\f8da"}.bi-twitter-x::before{content:"\f8db"}.bi-type-h4::before{content:"\f8dc"}.bi-type-h5::before{content:"\f8dd"}.bi-type-h6::before{content:"\f8de"}.bi-backpack-fill::before{content:"\f8df"}.bi-backpack::before{content:"\f8e0"}.bi-backpack2-fill::before{content:"\f8e1"}.bi-backpack2::before{content:"\f8e2"}.bi-backpack3-fill::before{content:"\f8e3"}.bi-backpack3::before{content:"\f8e4"}.bi-backpack4-fill::before{content:"\f8e5"}.bi-backpack4::before{content:"\f8e6"}.bi-brilliance::before{content:"\f8e7"}.bi-cake-fill::before{content:"\f8e8"}.bi-cake2-fill::before{content:"\f8e9"}.bi-duffle-fill::before{content:"\f8ea"}.bi-duffle::before{content:"\f8eb"}.bi-exposure::before{content:"\f8ec"}.bi-gender-neuter::before{content:"\f8ed"}.bi-highlights::before{content:"\f8ee"}.bi-luggage-fill::before{content:"\f8ef"}.bi-luggage::before{content:"\f8f0"}.bi-mailbox-flag::before{content:"\f8f1"}.bi-mailbox2-flag::before{content:"\f8f2"}.bi-noise-reduction::before{content:"\f8f3"}.bi-passport-fill::before{content:"\f8f4"}.bi-passport::before{content:"\f8f5"}.bi-person-arms-up::before{content:"\f8f6"}.bi-person-raised-hand::before{content:"\f8f7"}.bi-person-standing-dress::before{content:"\f8f8"}.bi-person-standing::before{content:"\f8f9"}.bi-person-walking::before{content:"\f8fa"}.bi-person-wheelchair::before{content:"\f8fb"}.bi-shadows::before{content:"\f8fc"}.bi-suitcase-fill::before{content:"\f8fd"}.bi-suitcase-lg-fill::before{content:"\f8fe"}.bi-suitcase-lg::before{content:"\f8ff"}.bi-suitcase::before{content:"\f900"}.bi-suitcase2-fill::before{content:"\f901"}.bi-suitcase2::before{content:"\f902"}.bi-vignette::before{content:"\f903"} \ No newline at end of file + */@font-face{font-display:block;font-family:bootstrap-icons;src:url("fonts/bootstrap-icons.woff2?e34853135f9e39acf64315236852cd5a") format("woff2"),url("fonts/bootstrap-icons.woff?e34853135f9e39acf64315236852cd5a") format("woff")}.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bi-123::before{content:"\f67f"}.bi-alarm-fill::before{content:"\f101"}.bi-alarm::before{content:"\f102"}.bi-align-bottom::before{content:"\f103"}.bi-align-center::before{content:"\f104"}.bi-align-end::before{content:"\f105"}.bi-align-middle::before{content:"\f106"}.bi-align-start::before{content:"\f107"}.bi-align-top::before{content:"\f108"}.bi-alt::before{content:"\f109"}.bi-app-indicator::before{content:"\f10a"}.bi-app::before{content:"\f10b"}.bi-archive-fill::before{content:"\f10c"}.bi-archive::before{content:"\f10d"}.bi-arrow-90deg-down::before{content:"\f10e"}.bi-arrow-90deg-left::before{content:"\f10f"}.bi-arrow-90deg-right::before{content:"\f110"}.bi-arrow-90deg-up::before{content:"\f111"}.bi-arrow-bar-down::before{content:"\f112"}.bi-arrow-bar-left::before{content:"\f113"}.bi-arrow-bar-right::before{content:"\f114"}.bi-arrow-bar-up::before{content:"\f115"}.bi-arrow-clockwise::before{content:"\f116"}.bi-arrow-counterclockwise::before{content:"\f117"}.bi-arrow-down-circle-fill::before{content:"\f118"}.bi-arrow-down-circle::before{content:"\f119"}.bi-arrow-down-left-circle-fill::before{content:"\f11a"}.bi-arrow-down-left-circle::before{content:"\f11b"}.bi-arrow-down-left-square-fill::before{content:"\f11c"}.bi-arrow-down-left-square::before{content:"\f11d"}.bi-arrow-down-left::before{content:"\f11e"}.bi-arrow-down-right-circle-fill::before{content:"\f11f"}.bi-arrow-down-right-circle::before{content:"\f120"}.bi-arrow-down-right-square-fill::before{content:"\f121"}.bi-arrow-down-right-square::before{content:"\f122"}.bi-arrow-down-right::before{content:"\f123"}.bi-arrow-down-short::before{content:"\f124"}.bi-arrow-down-square-fill::before{content:"\f125"}.bi-arrow-down-square::before{content:"\f126"}.bi-arrow-down-up::before{content:"\f127"}.bi-arrow-down::before{content:"\f128"}.bi-arrow-left-circle-fill::before{content:"\f129"}.bi-arrow-left-circle::before{content:"\f12a"}.bi-arrow-left-right::before{content:"\f12b"}.bi-arrow-left-short::before{content:"\f12c"}.bi-arrow-left-square-fill::before{content:"\f12d"}.bi-arrow-left-square::before{content:"\f12e"}.bi-arrow-left::before{content:"\f12f"}.bi-arrow-repeat::before{content:"\f130"}.bi-arrow-return-left::before{content:"\f131"}.bi-arrow-return-right::before{content:"\f132"}.bi-arrow-right-circle-fill::before{content:"\f133"}.bi-arrow-right-circle::before{content:"\f134"}.bi-arrow-right-short::before{content:"\f135"}.bi-arrow-right-square-fill::before{content:"\f136"}.bi-arrow-right-square::before{content:"\f137"}.bi-arrow-right::before{content:"\f138"}.bi-arrow-up-circle-fill::before{content:"\f139"}.bi-arrow-up-circle::before{content:"\f13a"}.bi-arrow-up-left-circle-fill::before{content:"\f13b"}.bi-arrow-up-left-circle::before{content:"\f13c"}.bi-arrow-up-left-square-fill::before{content:"\f13d"}.bi-arrow-up-left-square::before{content:"\f13e"}.bi-arrow-up-left::before{content:"\f13f"}.bi-arrow-up-right-circle-fill::before{content:"\f140"}.bi-arrow-up-right-circle::before{content:"\f141"}.bi-arrow-up-right-square-fill::before{content:"\f142"}.bi-arrow-up-right-square::before{content:"\f143"}.bi-arrow-up-right::before{content:"\f144"}.bi-arrow-up-short::before{content:"\f145"}.bi-arrow-up-square-fill::before{content:"\f146"}.bi-arrow-up-square::before{content:"\f147"}.bi-arrow-up::before{content:"\f148"}.bi-arrows-angle-contract::before{content:"\f149"}.bi-arrows-angle-expand::before{content:"\f14a"}.bi-arrows-collapse::before{content:"\f14b"}.bi-arrows-expand::before{content:"\f14c"}.bi-arrows-fullscreen::before{content:"\f14d"}.bi-arrows-move::before{content:"\f14e"}.bi-aspect-ratio-fill::before{content:"\f14f"}.bi-aspect-ratio::before{content:"\f150"}.bi-asterisk::before{content:"\f151"}.bi-at::before{content:"\f152"}.bi-award-fill::before{content:"\f153"}.bi-award::before{content:"\f154"}.bi-back::before{content:"\f155"}.bi-backspace-fill::before{content:"\f156"}.bi-backspace-reverse-fill::before{content:"\f157"}.bi-backspace-reverse::before{content:"\f158"}.bi-backspace::before{content:"\f159"}.bi-badge-3d-fill::before{content:"\f15a"}.bi-badge-3d::before{content:"\f15b"}.bi-badge-4k-fill::before{content:"\f15c"}.bi-badge-4k::before{content:"\f15d"}.bi-badge-8k-fill::before{content:"\f15e"}.bi-badge-8k::before{content:"\f15f"}.bi-badge-ad-fill::before{content:"\f160"}.bi-badge-ad::before{content:"\f161"}.bi-badge-ar-fill::before{content:"\f162"}.bi-badge-ar::before{content:"\f163"}.bi-badge-cc-fill::before{content:"\f164"}.bi-badge-cc::before{content:"\f165"}.bi-badge-hd-fill::before{content:"\f166"}.bi-badge-hd::before{content:"\f167"}.bi-badge-tm-fill::before{content:"\f168"}.bi-badge-tm::before{content:"\f169"}.bi-badge-vo-fill::before{content:"\f16a"}.bi-badge-vo::before{content:"\f16b"}.bi-badge-vr-fill::before{content:"\f16c"}.bi-badge-vr::before{content:"\f16d"}.bi-badge-wc-fill::before{content:"\f16e"}.bi-badge-wc::before{content:"\f16f"}.bi-bag-check-fill::before{content:"\f170"}.bi-bag-check::before{content:"\f171"}.bi-bag-dash-fill::before{content:"\f172"}.bi-bag-dash::before{content:"\f173"}.bi-bag-fill::before{content:"\f174"}.bi-bag-plus-fill::before{content:"\f175"}.bi-bag-plus::before{content:"\f176"}.bi-bag-x-fill::before{content:"\f177"}.bi-bag-x::before{content:"\f178"}.bi-bag::before{content:"\f179"}.bi-bar-chart-fill::before{content:"\f17a"}.bi-bar-chart-line-fill::before{content:"\f17b"}.bi-bar-chart-line::before{content:"\f17c"}.bi-bar-chart-steps::before{content:"\f17d"}.bi-bar-chart::before{content:"\f17e"}.bi-basket-fill::before{content:"\f17f"}.bi-basket::before{content:"\f180"}.bi-basket2-fill::before{content:"\f181"}.bi-basket2::before{content:"\f182"}.bi-basket3-fill::before{content:"\f183"}.bi-basket3::before{content:"\f184"}.bi-battery-charging::before{content:"\f185"}.bi-battery-full::before{content:"\f186"}.bi-battery-half::before{content:"\f187"}.bi-battery::before{content:"\f188"}.bi-bell-fill::before{content:"\f189"}.bi-bell::before{content:"\f18a"}.bi-bezier::before{content:"\f18b"}.bi-bezier2::before{content:"\f18c"}.bi-bicycle::before{content:"\f18d"}.bi-binoculars-fill::before{content:"\f18e"}.bi-binoculars::before{content:"\f18f"}.bi-blockquote-left::before{content:"\f190"}.bi-blockquote-right::before{content:"\f191"}.bi-book-fill::before{content:"\f192"}.bi-book-half::before{content:"\f193"}.bi-book::before{content:"\f194"}.bi-bookmark-check-fill::before{content:"\f195"}.bi-bookmark-check::before{content:"\f196"}.bi-bookmark-dash-fill::before{content:"\f197"}.bi-bookmark-dash::before{content:"\f198"}.bi-bookmark-fill::before{content:"\f199"}.bi-bookmark-heart-fill::before{content:"\f19a"}.bi-bookmark-heart::before{content:"\f19b"}.bi-bookmark-plus-fill::before{content:"\f19c"}.bi-bookmark-plus::before{content:"\f19d"}.bi-bookmark-star-fill::before{content:"\f19e"}.bi-bookmark-star::before{content:"\f19f"}.bi-bookmark-x-fill::before{content:"\f1a0"}.bi-bookmark-x::before{content:"\f1a1"}.bi-bookmark::before{content:"\f1a2"}.bi-bookmarks-fill::before{content:"\f1a3"}.bi-bookmarks::before{content:"\f1a4"}.bi-bookshelf::before{content:"\f1a5"}.bi-bootstrap-fill::before{content:"\f1a6"}.bi-bootstrap-reboot::before{content:"\f1a7"}.bi-bootstrap::before{content:"\f1a8"}.bi-border-all::before{content:"\f1a9"}.bi-border-bottom::before{content:"\f1aa"}.bi-border-center::before{content:"\f1ab"}.bi-border-inner::before{content:"\f1ac"}.bi-border-left::before{content:"\f1ad"}.bi-border-middle::before{content:"\f1ae"}.bi-border-outer::before{content:"\f1af"}.bi-border-right::before{content:"\f1b0"}.bi-border-style::before{content:"\f1b1"}.bi-border-top::before{content:"\f1b2"}.bi-border-width::before{content:"\f1b3"}.bi-border::before{content:"\f1b4"}.bi-bounding-box-circles::before{content:"\f1b5"}.bi-bounding-box::before{content:"\f1b6"}.bi-box-arrow-down-left::before{content:"\f1b7"}.bi-box-arrow-down-right::before{content:"\f1b8"}.bi-box-arrow-down::before{content:"\f1b9"}.bi-box-arrow-in-down-left::before{content:"\f1ba"}.bi-box-arrow-in-down-right::before{content:"\f1bb"}.bi-box-arrow-in-down::before{content:"\f1bc"}.bi-box-arrow-in-left::before{content:"\f1bd"}.bi-box-arrow-in-right::before{content:"\f1be"}.bi-box-arrow-in-up-left::before{content:"\f1bf"}.bi-box-arrow-in-up-right::before{content:"\f1c0"}.bi-box-arrow-in-up::before{content:"\f1c1"}.bi-box-arrow-left::before{content:"\f1c2"}.bi-box-arrow-right::before{content:"\f1c3"}.bi-box-arrow-up-left::before{content:"\f1c4"}.bi-box-arrow-up-right::before{content:"\f1c5"}.bi-box-arrow-up::before{content:"\f1c6"}.bi-box-seam::before{content:"\f1c7"}.bi-box::before{content:"\f1c8"}.bi-braces::before{content:"\f1c9"}.bi-bricks::before{content:"\f1ca"}.bi-briefcase-fill::before{content:"\f1cb"}.bi-briefcase::before{content:"\f1cc"}.bi-brightness-alt-high-fill::before{content:"\f1cd"}.bi-brightness-alt-high::before{content:"\f1ce"}.bi-brightness-alt-low-fill::before{content:"\f1cf"}.bi-brightness-alt-low::before{content:"\f1d0"}.bi-brightness-high-fill::before{content:"\f1d1"}.bi-brightness-high::before{content:"\f1d2"}.bi-brightness-low-fill::before{content:"\f1d3"}.bi-brightness-low::before{content:"\f1d4"}.bi-broadcast-pin::before{content:"\f1d5"}.bi-broadcast::before{content:"\f1d6"}.bi-brush-fill::before{content:"\f1d7"}.bi-brush::before{content:"\f1d8"}.bi-bucket-fill::before{content:"\f1d9"}.bi-bucket::before{content:"\f1da"}.bi-bug-fill::before{content:"\f1db"}.bi-bug::before{content:"\f1dc"}.bi-building::before{content:"\f1dd"}.bi-bullseye::before{content:"\f1de"}.bi-calculator-fill::before{content:"\f1df"}.bi-calculator::before{content:"\f1e0"}.bi-calendar-check-fill::before{content:"\f1e1"}.bi-calendar-check::before{content:"\f1e2"}.bi-calendar-date-fill::before{content:"\f1e3"}.bi-calendar-date::before{content:"\f1e4"}.bi-calendar-day-fill::before{content:"\f1e5"}.bi-calendar-day::before{content:"\f1e6"}.bi-calendar-event-fill::before{content:"\f1e7"}.bi-calendar-event::before{content:"\f1e8"}.bi-calendar-fill::before{content:"\f1e9"}.bi-calendar-minus-fill::before{content:"\f1ea"}.bi-calendar-minus::before{content:"\f1eb"}.bi-calendar-month-fill::before{content:"\f1ec"}.bi-calendar-month::before{content:"\f1ed"}.bi-calendar-plus-fill::before{content:"\f1ee"}.bi-calendar-plus::before{content:"\f1ef"}.bi-calendar-range-fill::before{content:"\f1f0"}.bi-calendar-range::before{content:"\f1f1"}.bi-calendar-week-fill::before{content:"\f1f2"}.bi-calendar-week::before{content:"\f1f3"}.bi-calendar-x-fill::before{content:"\f1f4"}.bi-calendar-x::before{content:"\f1f5"}.bi-calendar::before{content:"\f1f6"}.bi-calendar2-check-fill::before{content:"\f1f7"}.bi-calendar2-check::before{content:"\f1f8"}.bi-calendar2-date-fill::before{content:"\f1f9"}.bi-calendar2-date::before{content:"\f1fa"}.bi-calendar2-day-fill::before{content:"\f1fb"}.bi-calendar2-day::before{content:"\f1fc"}.bi-calendar2-event-fill::before{content:"\f1fd"}.bi-calendar2-event::before{content:"\f1fe"}.bi-calendar2-fill::before{content:"\f1ff"}.bi-calendar2-minus-fill::before{content:"\f200"}.bi-calendar2-minus::before{content:"\f201"}.bi-calendar2-month-fill::before{content:"\f202"}.bi-calendar2-month::before{content:"\f203"}.bi-calendar2-plus-fill::before{content:"\f204"}.bi-calendar2-plus::before{content:"\f205"}.bi-calendar2-range-fill::before{content:"\f206"}.bi-calendar2-range::before{content:"\f207"}.bi-calendar2-week-fill::before{content:"\f208"}.bi-calendar2-week::before{content:"\f209"}.bi-calendar2-x-fill::before{content:"\f20a"}.bi-calendar2-x::before{content:"\f20b"}.bi-calendar2::before{content:"\f20c"}.bi-calendar3-event-fill::before{content:"\f20d"}.bi-calendar3-event::before{content:"\f20e"}.bi-calendar3-fill::before{content:"\f20f"}.bi-calendar3-range-fill::before{content:"\f210"}.bi-calendar3-range::before{content:"\f211"}.bi-calendar3-week-fill::before{content:"\f212"}.bi-calendar3-week::before{content:"\f213"}.bi-calendar3::before{content:"\f214"}.bi-calendar4-event::before{content:"\f215"}.bi-calendar4-range::before{content:"\f216"}.bi-calendar4-week::before{content:"\f217"}.bi-calendar4::before{content:"\f218"}.bi-camera-fill::before{content:"\f219"}.bi-camera-reels-fill::before{content:"\f21a"}.bi-camera-reels::before{content:"\f21b"}.bi-camera-video-fill::before{content:"\f21c"}.bi-camera-video-off-fill::before{content:"\f21d"}.bi-camera-video-off::before{content:"\f21e"}.bi-camera-video::before{content:"\f21f"}.bi-camera::before{content:"\f220"}.bi-camera2::before{content:"\f221"}.bi-capslock-fill::before{content:"\f222"}.bi-capslock::before{content:"\f223"}.bi-card-checklist::before{content:"\f224"}.bi-card-heading::before{content:"\f225"}.bi-card-image::before{content:"\f226"}.bi-card-list::before{content:"\f227"}.bi-card-text::before{content:"\f228"}.bi-caret-down-fill::before{content:"\f229"}.bi-caret-down-square-fill::before{content:"\f22a"}.bi-caret-down-square::before{content:"\f22b"}.bi-caret-down::before{content:"\f22c"}.bi-caret-left-fill::before{content:"\f22d"}.bi-caret-left-square-fill::before{content:"\f22e"}.bi-caret-left-square::before{content:"\f22f"}.bi-caret-left::before{content:"\f230"}.bi-caret-right-fill::before{content:"\f231"}.bi-caret-right-square-fill::before{content:"\f232"}.bi-caret-right-square::before{content:"\f233"}.bi-caret-right::before{content:"\f234"}.bi-caret-up-fill::before{content:"\f235"}.bi-caret-up-square-fill::before{content:"\f236"}.bi-caret-up-square::before{content:"\f237"}.bi-caret-up::before{content:"\f238"}.bi-cart-check-fill::before{content:"\f239"}.bi-cart-check::before{content:"\f23a"}.bi-cart-dash-fill::before{content:"\f23b"}.bi-cart-dash::before{content:"\f23c"}.bi-cart-fill::before{content:"\f23d"}.bi-cart-plus-fill::before{content:"\f23e"}.bi-cart-plus::before{content:"\f23f"}.bi-cart-x-fill::before{content:"\f240"}.bi-cart-x::before{content:"\f241"}.bi-cart::before{content:"\f242"}.bi-cart2::before{content:"\f243"}.bi-cart3::before{content:"\f244"}.bi-cart4::before{content:"\f245"}.bi-cash-stack::before{content:"\f246"}.bi-cash::before{content:"\f247"}.bi-cast::before{content:"\f248"}.bi-chat-dots-fill::before{content:"\f249"}.bi-chat-dots::before{content:"\f24a"}.bi-chat-fill::before{content:"\f24b"}.bi-chat-left-dots-fill::before{content:"\f24c"}.bi-chat-left-dots::before{content:"\f24d"}.bi-chat-left-fill::before{content:"\f24e"}.bi-chat-left-quote-fill::before{content:"\f24f"}.bi-chat-left-quote::before{content:"\f250"}.bi-chat-left-text-fill::before{content:"\f251"}.bi-chat-left-text::before{content:"\f252"}.bi-chat-left::before{content:"\f253"}.bi-chat-quote-fill::before{content:"\f254"}.bi-chat-quote::before{content:"\f255"}.bi-chat-right-dots-fill::before{content:"\f256"}.bi-chat-right-dots::before{content:"\f257"}.bi-chat-right-fill::before{content:"\f258"}.bi-chat-right-quote-fill::before{content:"\f259"}.bi-chat-right-quote::before{content:"\f25a"}.bi-chat-right-text-fill::before{content:"\f25b"}.bi-chat-right-text::before{content:"\f25c"}.bi-chat-right::before{content:"\f25d"}.bi-chat-square-dots-fill::before{content:"\f25e"}.bi-chat-square-dots::before{content:"\f25f"}.bi-chat-square-fill::before{content:"\f260"}.bi-chat-square-quote-fill::before{content:"\f261"}.bi-chat-square-quote::before{content:"\f262"}.bi-chat-square-text-fill::before{content:"\f263"}.bi-chat-square-text::before{content:"\f264"}.bi-chat-square::before{content:"\f265"}.bi-chat-text-fill::before{content:"\f266"}.bi-chat-text::before{content:"\f267"}.bi-chat::before{content:"\f268"}.bi-check-all::before{content:"\f269"}.bi-check-circle-fill::before{content:"\f26a"}.bi-check-circle::before{content:"\f26b"}.bi-check-square-fill::before{content:"\f26c"}.bi-check-square::before{content:"\f26d"}.bi-check::before{content:"\f26e"}.bi-check2-all::before{content:"\f26f"}.bi-check2-circle::before{content:"\f270"}.bi-check2-square::before{content:"\f271"}.bi-check2::before{content:"\f272"}.bi-chevron-bar-contract::before{content:"\f273"}.bi-chevron-bar-down::before{content:"\f274"}.bi-chevron-bar-expand::before{content:"\f275"}.bi-chevron-bar-left::before{content:"\f276"}.bi-chevron-bar-right::before{content:"\f277"}.bi-chevron-bar-up::before{content:"\f278"}.bi-chevron-compact-down::before{content:"\f279"}.bi-chevron-compact-left::before{content:"\f27a"}.bi-chevron-compact-right::before{content:"\f27b"}.bi-chevron-compact-up::before{content:"\f27c"}.bi-chevron-contract::before{content:"\f27d"}.bi-chevron-double-down::before{content:"\f27e"}.bi-chevron-double-left::before{content:"\f27f"}.bi-chevron-double-right::before{content:"\f280"}.bi-chevron-double-up::before{content:"\f281"}.bi-chevron-down::before{content:"\f282"}.bi-chevron-expand::before{content:"\f283"}.bi-chevron-left::before{content:"\f284"}.bi-chevron-right::before{content:"\f285"}.bi-chevron-up::before{content:"\f286"}.bi-circle-fill::before{content:"\f287"}.bi-circle-half::before{content:"\f288"}.bi-circle-square::before{content:"\f289"}.bi-circle::before{content:"\f28a"}.bi-clipboard-check::before{content:"\f28b"}.bi-clipboard-data::before{content:"\f28c"}.bi-clipboard-minus::before{content:"\f28d"}.bi-clipboard-plus::before{content:"\f28e"}.bi-clipboard-x::before{content:"\f28f"}.bi-clipboard::before{content:"\f290"}.bi-clock-fill::before{content:"\f291"}.bi-clock-history::before{content:"\f292"}.bi-clock::before{content:"\f293"}.bi-cloud-arrow-down-fill::before{content:"\f294"}.bi-cloud-arrow-down::before{content:"\f295"}.bi-cloud-arrow-up-fill::before{content:"\f296"}.bi-cloud-arrow-up::before{content:"\f297"}.bi-cloud-check-fill::before{content:"\f298"}.bi-cloud-check::before{content:"\f299"}.bi-cloud-download-fill::before{content:"\f29a"}.bi-cloud-download::before{content:"\f29b"}.bi-cloud-drizzle-fill::before{content:"\f29c"}.bi-cloud-drizzle::before{content:"\f29d"}.bi-cloud-fill::before{content:"\f29e"}.bi-cloud-fog-fill::before{content:"\f29f"}.bi-cloud-fog::before{content:"\f2a0"}.bi-cloud-fog2-fill::before{content:"\f2a1"}.bi-cloud-fog2::before{content:"\f2a2"}.bi-cloud-hail-fill::before{content:"\f2a3"}.bi-cloud-hail::before{content:"\f2a4"}.bi-cloud-haze-fill::before{content:"\f2a6"}.bi-cloud-haze::before{content:"\f2a7"}.bi-cloud-haze2-fill::before{content:"\f2a8"}.bi-cloud-lightning-fill::before{content:"\f2a9"}.bi-cloud-lightning-rain-fill::before{content:"\f2aa"}.bi-cloud-lightning-rain::before{content:"\f2ab"}.bi-cloud-lightning::before{content:"\f2ac"}.bi-cloud-minus-fill::before{content:"\f2ad"}.bi-cloud-minus::before{content:"\f2ae"}.bi-cloud-moon-fill::before{content:"\f2af"}.bi-cloud-moon::before{content:"\f2b0"}.bi-cloud-plus-fill::before{content:"\f2b1"}.bi-cloud-plus::before{content:"\f2b2"}.bi-cloud-rain-fill::before{content:"\f2b3"}.bi-cloud-rain-heavy-fill::before{content:"\f2b4"}.bi-cloud-rain-heavy::before{content:"\f2b5"}.bi-cloud-rain::before{content:"\f2b6"}.bi-cloud-slash-fill::before{content:"\f2b7"}.bi-cloud-slash::before{content:"\f2b8"}.bi-cloud-sleet-fill::before{content:"\f2b9"}.bi-cloud-sleet::before{content:"\f2ba"}.bi-cloud-snow-fill::before{content:"\f2bb"}.bi-cloud-snow::before{content:"\f2bc"}.bi-cloud-sun-fill::before{content:"\f2bd"}.bi-cloud-sun::before{content:"\f2be"}.bi-cloud-upload-fill::before{content:"\f2bf"}.bi-cloud-upload::before{content:"\f2c0"}.bi-cloud::before{content:"\f2c1"}.bi-clouds-fill::before{content:"\f2c2"}.bi-clouds::before{content:"\f2c3"}.bi-cloudy-fill::before{content:"\f2c4"}.bi-cloudy::before{content:"\f2c5"}.bi-code-slash::before{content:"\f2c6"}.bi-code-square::before{content:"\f2c7"}.bi-code::before{content:"\f2c8"}.bi-collection-fill::before{content:"\f2c9"}.bi-collection-play-fill::before{content:"\f2ca"}.bi-collection-play::before{content:"\f2cb"}.bi-collection::before{content:"\f2cc"}.bi-columns-gap::before{content:"\f2cd"}.bi-columns::before{content:"\f2ce"}.bi-command::before{content:"\f2cf"}.bi-compass-fill::before{content:"\f2d0"}.bi-compass::before{content:"\f2d1"}.bi-cone-striped::before{content:"\f2d2"}.bi-cone::before{content:"\f2d3"}.bi-controller::before{content:"\f2d4"}.bi-cpu-fill::before{content:"\f2d5"}.bi-cpu::before{content:"\f2d6"}.bi-credit-card-2-back-fill::before{content:"\f2d7"}.bi-credit-card-2-back::before{content:"\f2d8"}.bi-credit-card-2-front-fill::before{content:"\f2d9"}.bi-credit-card-2-front::before{content:"\f2da"}.bi-credit-card-fill::before{content:"\f2db"}.bi-credit-card::before{content:"\f2dc"}.bi-crop::before{content:"\f2dd"}.bi-cup-fill::before{content:"\f2de"}.bi-cup-straw::before{content:"\f2df"}.bi-cup::before{content:"\f2e0"}.bi-cursor-fill::before{content:"\f2e1"}.bi-cursor-text::before{content:"\f2e2"}.bi-cursor::before{content:"\f2e3"}.bi-dash-circle-dotted::before{content:"\f2e4"}.bi-dash-circle-fill::before{content:"\f2e5"}.bi-dash-circle::before{content:"\f2e6"}.bi-dash-square-dotted::before{content:"\f2e7"}.bi-dash-square-fill::before{content:"\f2e8"}.bi-dash-square::before{content:"\f2e9"}.bi-dash::before{content:"\f2ea"}.bi-diagram-2-fill::before{content:"\f2eb"}.bi-diagram-2::before{content:"\f2ec"}.bi-diagram-3-fill::before{content:"\f2ed"}.bi-diagram-3::before{content:"\f2ee"}.bi-diamond-fill::before{content:"\f2ef"}.bi-diamond-half::before{content:"\f2f0"}.bi-diamond::before{content:"\f2f1"}.bi-dice-1-fill::before{content:"\f2f2"}.bi-dice-1::before{content:"\f2f3"}.bi-dice-2-fill::before{content:"\f2f4"}.bi-dice-2::before{content:"\f2f5"}.bi-dice-3-fill::before{content:"\f2f6"}.bi-dice-3::before{content:"\f2f7"}.bi-dice-4-fill::before{content:"\f2f8"}.bi-dice-4::before{content:"\f2f9"}.bi-dice-5-fill::before{content:"\f2fa"}.bi-dice-5::before{content:"\f2fb"}.bi-dice-6-fill::before{content:"\f2fc"}.bi-dice-6::before{content:"\f2fd"}.bi-disc-fill::before{content:"\f2fe"}.bi-disc::before{content:"\f2ff"}.bi-discord::before{content:"\f300"}.bi-display-fill::before{content:"\f301"}.bi-display::before{content:"\f302"}.bi-distribute-horizontal::before{content:"\f303"}.bi-distribute-vertical::before{content:"\f304"}.bi-door-closed-fill::before{content:"\f305"}.bi-door-closed::before{content:"\f306"}.bi-door-open-fill::before{content:"\f307"}.bi-door-open::before{content:"\f308"}.bi-dot::before{content:"\f309"}.bi-download::before{content:"\f30a"}.bi-droplet-fill::before{content:"\f30b"}.bi-droplet-half::before{content:"\f30c"}.bi-droplet::before{content:"\f30d"}.bi-earbuds::before{content:"\f30e"}.bi-easel-fill::before{content:"\f30f"}.bi-easel::before{content:"\f310"}.bi-egg-fill::before{content:"\f311"}.bi-egg-fried::before{content:"\f312"}.bi-egg::before{content:"\f313"}.bi-eject-fill::before{content:"\f314"}.bi-eject::before{content:"\f315"}.bi-emoji-angry-fill::before{content:"\f316"}.bi-emoji-angry::before{content:"\f317"}.bi-emoji-dizzy-fill::before{content:"\f318"}.bi-emoji-dizzy::before{content:"\f319"}.bi-emoji-expressionless-fill::before{content:"\f31a"}.bi-emoji-expressionless::before{content:"\f31b"}.bi-emoji-frown-fill::before{content:"\f31c"}.bi-emoji-frown::before{content:"\f31d"}.bi-emoji-heart-eyes-fill::before{content:"\f31e"}.bi-emoji-heart-eyes::before{content:"\f31f"}.bi-emoji-laughing-fill::before{content:"\f320"}.bi-emoji-laughing::before{content:"\f321"}.bi-emoji-neutral-fill::before{content:"\f322"}.bi-emoji-neutral::before{content:"\f323"}.bi-emoji-smile-fill::before{content:"\f324"}.bi-emoji-smile-upside-down-fill::before{content:"\f325"}.bi-emoji-smile-upside-down::before{content:"\f326"}.bi-emoji-smile::before{content:"\f327"}.bi-emoji-sunglasses-fill::before{content:"\f328"}.bi-emoji-sunglasses::before{content:"\f329"}.bi-emoji-wink-fill::before{content:"\f32a"}.bi-emoji-wink::before{content:"\f32b"}.bi-envelope-fill::before{content:"\f32c"}.bi-envelope-open-fill::before{content:"\f32d"}.bi-envelope-open::before{content:"\f32e"}.bi-envelope::before{content:"\f32f"}.bi-eraser-fill::before{content:"\f330"}.bi-eraser::before{content:"\f331"}.bi-exclamation-circle-fill::before{content:"\f332"}.bi-exclamation-circle::before{content:"\f333"}.bi-exclamation-diamond-fill::before{content:"\f334"}.bi-exclamation-diamond::before{content:"\f335"}.bi-exclamation-octagon-fill::before{content:"\f336"}.bi-exclamation-octagon::before{content:"\f337"}.bi-exclamation-square-fill::before{content:"\f338"}.bi-exclamation-square::before{content:"\f339"}.bi-exclamation-triangle-fill::before{content:"\f33a"}.bi-exclamation-triangle::before{content:"\f33b"}.bi-exclamation::before{content:"\f33c"}.bi-exclude::before{content:"\f33d"}.bi-eye-fill::before{content:"\f33e"}.bi-eye-slash-fill::before{content:"\f33f"}.bi-eye-slash::before{content:"\f340"}.bi-eye::before{content:"\f341"}.bi-eyedropper::before{content:"\f342"}.bi-eyeglasses::before{content:"\f343"}.bi-facebook::before{content:"\f344"}.bi-file-arrow-down-fill::before{content:"\f345"}.bi-file-arrow-down::before{content:"\f346"}.bi-file-arrow-up-fill::before{content:"\f347"}.bi-file-arrow-up::before{content:"\f348"}.bi-file-bar-graph-fill::before{content:"\f349"}.bi-file-bar-graph::before{content:"\f34a"}.bi-file-binary-fill::before{content:"\f34b"}.bi-file-binary::before{content:"\f34c"}.bi-file-break-fill::before{content:"\f34d"}.bi-file-break::before{content:"\f34e"}.bi-file-check-fill::before{content:"\f34f"}.bi-file-check::before{content:"\f350"}.bi-file-code-fill::before{content:"\f351"}.bi-file-code::before{content:"\f352"}.bi-file-diff-fill::before{content:"\f353"}.bi-file-diff::before{content:"\f354"}.bi-file-earmark-arrow-down-fill::before{content:"\f355"}.bi-file-earmark-arrow-down::before{content:"\f356"}.bi-file-earmark-arrow-up-fill::before{content:"\f357"}.bi-file-earmark-arrow-up::before{content:"\f358"}.bi-file-earmark-bar-graph-fill::before{content:"\f359"}.bi-file-earmark-bar-graph::before{content:"\f35a"}.bi-file-earmark-binary-fill::before{content:"\f35b"}.bi-file-earmark-binary::before{content:"\f35c"}.bi-file-earmark-break-fill::before{content:"\f35d"}.bi-file-earmark-break::before{content:"\f35e"}.bi-file-earmark-check-fill::before{content:"\f35f"}.bi-file-earmark-check::before{content:"\f360"}.bi-file-earmark-code-fill::before{content:"\f361"}.bi-file-earmark-code::before{content:"\f362"}.bi-file-earmark-diff-fill::before{content:"\f363"}.bi-file-earmark-diff::before{content:"\f364"}.bi-file-earmark-easel-fill::before{content:"\f365"}.bi-file-earmark-easel::before{content:"\f366"}.bi-file-earmark-excel-fill::before{content:"\f367"}.bi-file-earmark-excel::before{content:"\f368"}.bi-file-earmark-fill::before{content:"\f369"}.bi-file-earmark-font-fill::before{content:"\f36a"}.bi-file-earmark-font::before{content:"\f36b"}.bi-file-earmark-image-fill::before{content:"\f36c"}.bi-file-earmark-image::before{content:"\f36d"}.bi-file-earmark-lock-fill::before{content:"\f36e"}.bi-file-earmark-lock::before{content:"\f36f"}.bi-file-earmark-lock2-fill::before{content:"\f370"}.bi-file-earmark-lock2::before{content:"\f371"}.bi-file-earmark-medical-fill::before{content:"\f372"}.bi-file-earmark-medical::before{content:"\f373"}.bi-file-earmark-minus-fill::before{content:"\f374"}.bi-file-earmark-minus::before{content:"\f375"}.bi-file-earmark-music-fill::before{content:"\f376"}.bi-file-earmark-music::before{content:"\f377"}.bi-file-earmark-person-fill::before{content:"\f378"}.bi-file-earmark-person::before{content:"\f379"}.bi-file-earmark-play-fill::before{content:"\f37a"}.bi-file-earmark-play::before{content:"\f37b"}.bi-file-earmark-plus-fill::before{content:"\f37c"}.bi-file-earmark-plus::before{content:"\f37d"}.bi-file-earmark-post-fill::before{content:"\f37e"}.bi-file-earmark-post::before{content:"\f37f"}.bi-file-earmark-ppt-fill::before{content:"\f380"}.bi-file-earmark-ppt::before{content:"\f381"}.bi-file-earmark-richtext-fill::before{content:"\f382"}.bi-file-earmark-richtext::before{content:"\f383"}.bi-file-earmark-ruled-fill::before{content:"\f384"}.bi-file-earmark-ruled::before{content:"\f385"}.bi-file-earmark-slides-fill::before{content:"\f386"}.bi-file-earmark-slides::before{content:"\f387"}.bi-file-earmark-spreadsheet-fill::before{content:"\f388"}.bi-file-earmark-spreadsheet::before{content:"\f389"}.bi-file-earmark-text-fill::before{content:"\f38a"}.bi-file-earmark-text::before{content:"\f38b"}.bi-file-earmark-word-fill::before{content:"\f38c"}.bi-file-earmark-word::before{content:"\f38d"}.bi-file-earmark-x-fill::before{content:"\f38e"}.bi-file-earmark-x::before{content:"\f38f"}.bi-file-earmark-zip-fill::before{content:"\f390"}.bi-file-earmark-zip::before{content:"\f391"}.bi-file-earmark::before{content:"\f392"}.bi-file-easel-fill::before{content:"\f393"}.bi-file-easel::before{content:"\f394"}.bi-file-excel-fill::before{content:"\f395"}.bi-file-excel::before{content:"\f396"}.bi-file-fill::before{content:"\f397"}.bi-file-font-fill::before{content:"\f398"}.bi-file-font::before{content:"\f399"}.bi-file-image-fill::before{content:"\f39a"}.bi-file-image::before{content:"\f39b"}.bi-file-lock-fill::before{content:"\f39c"}.bi-file-lock::before{content:"\f39d"}.bi-file-lock2-fill::before{content:"\f39e"}.bi-file-lock2::before{content:"\f39f"}.bi-file-medical-fill::before{content:"\f3a0"}.bi-file-medical::before{content:"\f3a1"}.bi-file-minus-fill::before{content:"\f3a2"}.bi-file-minus::before{content:"\f3a3"}.bi-file-music-fill::before{content:"\f3a4"}.bi-file-music::before{content:"\f3a5"}.bi-file-person-fill::before{content:"\f3a6"}.bi-file-person::before{content:"\f3a7"}.bi-file-play-fill::before{content:"\f3a8"}.bi-file-play::before{content:"\f3a9"}.bi-file-plus-fill::before{content:"\f3aa"}.bi-file-plus::before{content:"\f3ab"}.bi-file-post-fill::before{content:"\f3ac"}.bi-file-post::before{content:"\f3ad"}.bi-file-ppt-fill::before{content:"\f3ae"}.bi-file-ppt::before{content:"\f3af"}.bi-file-richtext-fill::before{content:"\f3b0"}.bi-file-richtext::before{content:"\f3b1"}.bi-file-ruled-fill::before{content:"\f3b2"}.bi-file-ruled::before{content:"\f3b3"}.bi-file-slides-fill::before{content:"\f3b4"}.bi-file-slides::before{content:"\f3b5"}.bi-file-spreadsheet-fill::before{content:"\f3b6"}.bi-file-spreadsheet::before{content:"\f3b7"}.bi-file-text-fill::before{content:"\f3b8"}.bi-file-text::before{content:"\f3b9"}.bi-file-word-fill::before{content:"\f3ba"}.bi-file-word::before{content:"\f3bb"}.bi-file-x-fill::before{content:"\f3bc"}.bi-file-x::before{content:"\f3bd"}.bi-file-zip-fill::before{content:"\f3be"}.bi-file-zip::before{content:"\f3bf"}.bi-file::before{content:"\f3c0"}.bi-files-alt::before{content:"\f3c1"}.bi-files::before{content:"\f3c2"}.bi-film::before{content:"\f3c3"}.bi-filter-circle-fill::before{content:"\f3c4"}.bi-filter-circle::before{content:"\f3c5"}.bi-filter-left::before{content:"\f3c6"}.bi-filter-right::before{content:"\f3c7"}.bi-filter-square-fill::before{content:"\f3c8"}.bi-filter-square::before{content:"\f3c9"}.bi-filter::before{content:"\f3ca"}.bi-flag-fill::before{content:"\f3cb"}.bi-flag::before{content:"\f3cc"}.bi-flower1::before{content:"\f3cd"}.bi-flower2::before{content:"\f3ce"}.bi-flower3::before{content:"\f3cf"}.bi-folder-check::before{content:"\f3d0"}.bi-folder-fill::before{content:"\f3d1"}.bi-folder-minus::before{content:"\f3d2"}.bi-folder-plus::before{content:"\f3d3"}.bi-folder-symlink-fill::before{content:"\f3d4"}.bi-folder-symlink::before{content:"\f3d5"}.bi-folder-x::before{content:"\f3d6"}.bi-folder::before{content:"\f3d7"}.bi-folder2-open::before{content:"\f3d8"}.bi-folder2::before{content:"\f3d9"}.bi-fonts::before{content:"\f3da"}.bi-forward-fill::before{content:"\f3db"}.bi-forward::before{content:"\f3dc"}.bi-front::before{content:"\f3dd"}.bi-fullscreen-exit::before{content:"\f3de"}.bi-fullscreen::before{content:"\f3df"}.bi-funnel-fill::before{content:"\f3e0"}.bi-funnel::before{content:"\f3e1"}.bi-gear-fill::before{content:"\f3e2"}.bi-gear-wide-connected::before{content:"\f3e3"}.bi-gear-wide::before{content:"\f3e4"}.bi-gear::before{content:"\f3e5"}.bi-gem::before{content:"\f3e6"}.bi-geo-alt-fill::before{content:"\f3e7"}.bi-geo-alt::before{content:"\f3e8"}.bi-geo-fill::before{content:"\f3e9"}.bi-geo::before{content:"\f3ea"}.bi-gift-fill::before{content:"\f3eb"}.bi-gift::before{content:"\f3ec"}.bi-github::before{content:"\f3ed"}.bi-globe::before{content:"\f3ee"}.bi-globe2::before{content:"\f3ef"}.bi-google::before{content:"\f3f0"}.bi-graph-down::before{content:"\f3f1"}.bi-graph-up::before{content:"\f3f2"}.bi-grid-1x2-fill::before{content:"\f3f3"}.bi-grid-1x2::before{content:"\f3f4"}.bi-grid-3x2-gap-fill::before{content:"\f3f5"}.bi-grid-3x2-gap::before{content:"\f3f6"}.bi-grid-3x2::before{content:"\f3f7"}.bi-grid-3x3-gap-fill::before{content:"\f3f8"}.bi-grid-3x3-gap::before{content:"\f3f9"}.bi-grid-3x3::before{content:"\f3fa"}.bi-grid-fill::before{content:"\f3fb"}.bi-grid::before{content:"\f3fc"}.bi-grip-horizontal::before{content:"\f3fd"}.bi-grip-vertical::before{content:"\f3fe"}.bi-hammer::before{content:"\f3ff"}.bi-hand-index-fill::before{content:"\f400"}.bi-hand-index-thumb-fill::before{content:"\f401"}.bi-hand-index-thumb::before{content:"\f402"}.bi-hand-index::before{content:"\f403"}.bi-hand-thumbs-down-fill::before{content:"\f404"}.bi-hand-thumbs-down::before{content:"\f405"}.bi-hand-thumbs-up-fill::before{content:"\f406"}.bi-hand-thumbs-up::before{content:"\f407"}.bi-handbag-fill::before{content:"\f408"}.bi-handbag::before{content:"\f409"}.bi-hash::before{content:"\f40a"}.bi-hdd-fill::before{content:"\f40b"}.bi-hdd-network-fill::before{content:"\f40c"}.bi-hdd-network::before{content:"\f40d"}.bi-hdd-rack-fill::before{content:"\f40e"}.bi-hdd-rack::before{content:"\f40f"}.bi-hdd-stack-fill::before{content:"\f410"}.bi-hdd-stack::before{content:"\f411"}.bi-hdd::before{content:"\f412"}.bi-headphones::before{content:"\f413"}.bi-headset::before{content:"\f414"}.bi-heart-fill::before{content:"\f415"}.bi-heart-half::before{content:"\f416"}.bi-heart::before{content:"\f417"}.bi-heptagon-fill::before{content:"\f418"}.bi-heptagon-half::before{content:"\f419"}.bi-heptagon::before{content:"\f41a"}.bi-hexagon-fill::before{content:"\f41b"}.bi-hexagon-half::before{content:"\f41c"}.bi-hexagon::before{content:"\f41d"}.bi-hourglass-bottom::before{content:"\f41e"}.bi-hourglass-split::before{content:"\f41f"}.bi-hourglass-top::before{content:"\f420"}.bi-hourglass::before{content:"\f421"}.bi-house-door-fill::before{content:"\f422"}.bi-house-door::before{content:"\f423"}.bi-house-fill::before{content:"\f424"}.bi-house::before{content:"\f425"}.bi-hr::before{content:"\f426"}.bi-hurricane::before{content:"\f427"}.bi-image-alt::before{content:"\f428"}.bi-image-fill::before{content:"\f429"}.bi-image::before{content:"\f42a"}.bi-images::before{content:"\f42b"}.bi-inbox-fill::before{content:"\f42c"}.bi-inbox::before{content:"\f42d"}.bi-inboxes-fill::before{content:"\f42e"}.bi-inboxes::before{content:"\f42f"}.bi-info-circle-fill::before{content:"\f430"}.bi-info-circle::before{content:"\f431"}.bi-info-square-fill::before{content:"\f432"}.bi-info-square::before{content:"\f433"}.bi-info::before{content:"\f434"}.bi-input-cursor-text::before{content:"\f435"}.bi-input-cursor::before{content:"\f436"}.bi-instagram::before{content:"\f437"}.bi-intersect::before{content:"\f438"}.bi-journal-album::before{content:"\f439"}.bi-journal-arrow-down::before{content:"\f43a"}.bi-journal-arrow-up::before{content:"\f43b"}.bi-journal-bookmark-fill::before{content:"\f43c"}.bi-journal-bookmark::before{content:"\f43d"}.bi-journal-check::before{content:"\f43e"}.bi-journal-code::before{content:"\f43f"}.bi-journal-medical::before{content:"\f440"}.bi-journal-minus::before{content:"\f441"}.bi-journal-plus::before{content:"\f442"}.bi-journal-richtext::before{content:"\f443"}.bi-journal-text::before{content:"\f444"}.bi-journal-x::before{content:"\f445"}.bi-journal::before{content:"\f446"}.bi-journals::before{content:"\f447"}.bi-joystick::before{content:"\f448"}.bi-justify-left::before{content:"\f449"}.bi-justify-right::before{content:"\f44a"}.bi-justify::before{content:"\f44b"}.bi-kanban-fill::before{content:"\f44c"}.bi-kanban::before{content:"\f44d"}.bi-key-fill::before{content:"\f44e"}.bi-key::before{content:"\f44f"}.bi-keyboard-fill::before{content:"\f450"}.bi-keyboard::before{content:"\f451"}.bi-ladder::before{content:"\f452"}.bi-lamp-fill::before{content:"\f453"}.bi-lamp::before{content:"\f454"}.bi-laptop-fill::before{content:"\f455"}.bi-laptop::before{content:"\f456"}.bi-layer-backward::before{content:"\f457"}.bi-layer-forward::before{content:"\f458"}.bi-layers-fill::before{content:"\f459"}.bi-layers-half::before{content:"\f45a"}.bi-layers::before{content:"\f45b"}.bi-layout-sidebar-inset-reverse::before{content:"\f45c"}.bi-layout-sidebar-inset::before{content:"\f45d"}.bi-layout-sidebar-reverse::before{content:"\f45e"}.bi-layout-sidebar::before{content:"\f45f"}.bi-layout-split::before{content:"\f460"}.bi-layout-text-sidebar-reverse::before{content:"\f461"}.bi-layout-text-sidebar::before{content:"\f462"}.bi-layout-text-window-reverse::before{content:"\f463"}.bi-layout-text-window::before{content:"\f464"}.bi-layout-three-columns::before{content:"\f465"}.bi-layout-wtf::before{content:"\f466"}.bi-life-preserver::before{content:"\f467"}.bi-lightbulb-fill::before{content:"\f468"}.bi-lightbulb-off-fill::before{content:"\f469"}.bi-lightbulb-off::before{content:"\f46a"}.bi-lightbulb::before{content:"\f46b"}.bi-lightning-charge-fill::before{content:"\f46c"}.bi-lightning-charge::before{content:"\f46d"}.bi-lightning-fill::before{content:"\f46e"}.bi-lightning::before{content:"\f46f"}.bi-link-45deg::before{content:"\f470"}.bi-link::before{content:"\f471"}.bi-linkedin::before{content:"\f472"}.bi-list-check::before{content:"\f473"}.bi-list-nested::before{content:"\f474"}.bi-list-ol::before{content:"\f475"}.bi-list-stars::before{content:"\f476"}.bi-list-task::before{content:"\f477"}.bi-list-ul::before{content:"\f478"}.bi-list::before{content:"\f479"}.bi-lock-fill::before{content:"\f47a"}.bi-lock::before{content:"\f47b"}.bi-mailbox::before{content:"\f47c"}.bi-mailbox2::before{content:"\f47d"}.bi-map-fill::before{content:"\f47e"}.bi-map::before{content:"\f47f"}.bi-markdown-fill::before{content:"\f480"}.bi-markdown::before{content:"\f481"}.bi-mask::before{content:"\f482"}.bi-megaphone-fill::before{content:"\f483"}.bi-megaphone::before{content:"\f484"}.bi-menu-app-fill::before{content:"\f485"}.bi-menu-app::before{content:"\f486"}.bi-menu-button-fill::before{content:"\f487"}.bi-menu-button-wide-fill::before{content:"\f488"}.bi-menu-button-wide::before{content:"\f489"}.bi-menu-button::before{content:"\f48a"}.bi-menu-down::before{content:"\f48b"}.bi-menu-up::before{content:"\f48c"}.bi-mic-fill::before{content:"\f48d"}.bi-mic-mute-fill::before{content:"\f48e"}.bi-mic-mute::before{content:"\f48f"}.bi-mic::before{content:"\f490"}.bi-minecart-loaded::before{content:"\f491"}.bi-minecart::before{content:"\f492"}.bi-moisture::before{content:"\f493"}.bi-moon-fill::before{content:"\f494"}.bi-moon-stars-fill::before{content:"\f495"}.bi-moon-stars::before{content:"\f496"}.bi-moon::before{content:"\f497"}.bi-mouse-fill::before{content:"\f498"}.bi-mouse::before{content:"\f499"}.bi-mouse2-fill::before{content:"\f49a"}.bi-mouse2::before{content:"\f49b"}.bi-mouse3-fill::before{content:"\f49c"}.bi-mouse3::before{content:"\f49d"}.bi-music-note-beamed::before{content:"\f49e"}.bi-music-note-list::before{content:"\f49f"}.bi-music-note::before{content:"\f4a0"}.bi-music-player-fill::before{content:"\f4a1"}.bi-music-player::before{content:"\f4a2"}.bi-newspaper::before{content:"\f4a3"}.bi-node-minus-fill::before{content:"\f4a4"}.bi-node-minus::before{content:"\f4a5"}.bi-node-plus-fill::before{content:"\f4a6"}.bi-node-plus::before{content:"\f4a7"}.bi-nut-fill::before{content:"\f4a8"}.bi-nut::before{content:"\f4a9"}.bi-octagon-fill::before{content:"\f4aa"}.bi-octagon-half::before{content:"\f4ab"}.bi-octagon::before{content:"\f4ac"}.bi-option::before{content:"\f4ad"}.bi-outlet::before{content:"\f4ae"}.bi-paint-bucket::before{content:"\f4af"}.bi-palette-fill::before{content:"\f4b0"}.bi-palette::before{content:"\f4b1"}.bi-palette2::before{content:"\f4b2"}.bi-paperclip::before{content:"\f4b3"}.bi-paragraph::before{content:"\f4b4"}.bi-patch-check-fill::before{content:"\f4b5"}.bi-patch-check::before{content:"\f4b6"}.bi-patch-exclamation-fill::before{content:"\f4b7"}.bi-patch-exclamation::before{content:"\f4b8"}.bi-patch-minus-fill::before{content:"\f4b9"}.bi-patch-minus::before{content:"\f4ba"}.bi-patch-plus-fill::before{content:"\f4bb"}.bi-patch-plus::before{content:"\f4bc"}.bi-patch-question-fill::before{content:"\f4bd"}.bi-patch-question::before{content:"\f4be"}.bi-pause-btn-fill::before{content:"\f4bf"}.bi-pause-btn::before{content:"\f4c0"}.bi-pause-circle-fill::before{content:"\f4c1"}.bi-pause-circle::before{content:"\f4c2"}.bi-pause-fill::before{content:"\f4c3"}.bi-pause::before{content:"\f4c4"}.bi-peace-fill::before{content:"\f4c5"}.bi-peace::before{content:"\f4c6"}.bi-pen-fill::before{content:"\f4c7"}.bi-pen::before{content:"\f4c8"}.bi-pencil-fill::before{content:"\f4c9"}.bi-pencil-square::before{content:"\f4ca"}.bi-pencil::before{content:"\f4cb"}.bi-pentagon-fill::before{content:"\f4cc"}.bi-pentagon-half::before{content:"\f4cd"}.bi-pentagon::before{content:"\f4ce"}.bi-people-fill::before{content:"\f4cf"}.bi-people::before{content:"\f4d0"}.bi-percent::before{content:"\f4d1"}.bi-person-badge-fill::before{content:"\f4d2"}.bi-person-badge::before{content:"\f4d3"}.bi-person-bounding-box::before{content:"\f4d4"}.bi-person-check-fill::before{content:"\f4d5"}.bi-person-check::before{content:"\f4d6"}.bi-person-circle::before{content:"\f4d7"}.bi-person-dash-fill::before{content:"\f4d8"}.bi-person-dash::before{content:"\f4d9"}.bi-person-fill::before{content:"\f4da"}.bi-person-lines-fill::before{content:"\f4db"}.bi-person-plus-fill::before{content:"\f4dc"}.bi-person-plus::before{content:"\f4dd"}.bi-person-square::before{content:"\f4de"}.bi-person-x-fill::before{content:"\f4df"}.bi-person-x::before{content:"\f4e0"}.bi-person::before{content:"\f4e1"}.bi-phone-fill::before{content:"\f4e2"}.bi-phone-landscape-fill::before{content:"\f4e3"}.bi-phone-landscape::before{content:"\f4e4"}.bi-phone-vibrate-fill::before{content:"\f4e5"}.bi-phone-vibrate::before{content:"\f4e6"}.bi-phone::before{content:"\f4e7"}.bi-pie-chart-fill::before{content:"\f4e8"}.bi-pie-chart::before{content:"\f4e9"}.bi-pin-angle-fill::before{content:"\f4ea"}.bi-pin-angle::before{content:"\f4eb"}.bi-pin-fill::before{content:"\f4ec"}.bi-pin::before{content:"\f4ed"}.bi-pip-fill::before{content:"\f4ee"}.bi-pip::before{content:"\f4ef"}.bi-play-btn-fill::before{content:"\f4f0"}.bi-play-btn::before{content:"\f4f1"}.bi-play-circle-fill::before{content:"\f4f2"}.bi-play-circle::before{content:"\f4f3"}.bi-play-fill::before{content:"\f4f4"}.bi-play::before{content:"\f4f5"}.bi-plug-fill::before{content:"\f4f6"}.bi-plug::before{content:"\f4f7"}.bi-plus-circle-dotted::before{content:"\f4f8"}.bi-plus-circle-fill::before{content:"\f4f9"}.bi-plus-circle::before{content:"\f4fa"}.bi-plus-square-dotted::before{content:"\f4fb"}.bi-plus-square-fill::before{content:"\f4fc"}.bi-plus-square::before{content:"\f4fd"}.bi-plus::before{content:"\f4fe"}.bi-power::before{content:"\f4ff"}.bi-printer-fill::before{content:"\f500"}.bi-printer::before{content:"\f501"}.bi-puzzle-fill::before{content:"\f502"}.bi-puzzle::before{content:"\f503"}.bi-question-circle-fill::before{content:"\f504"}.bi-question-circle::before{content:"\f505"}.bi-question-diamond-fill::before{content:"\f506"}.bi-question-diamond::before{content:"\f507"}.bi-question-octagon-fill::before{content:"\f508"}.bi-question-octagon::before{content:"\f509"}.bi-question-square-fill::before{content:"\f50a"}.bi-question-square::before{content:"\f50b"}.bi-question::before{content:"\f50c"}.bi-rainbow::before{content:"\f50d"}.bi-receipt-cutoff::before{content:"\f50e"}.bi-receipt::before{content:"\f50f"}.bi-reception-0::before{content:"\f510"}.bi-reception-1::before{content:"\f511"}.bi-reception-2::before{content:"\f512"}.bi-reception-3::before{content:"\f513"}.bi-reception-4::before{content:"\f514"}.bi-record-btn-fill::before{content:"\f515"}.bi-record-btn::before{content:"\f516"}.bi-record-circle-fill::before{content:"\f517"}.bi-record-circle::before{content:"\f518"}.bi-record-fill::before{content:"\f519"}.bi-record::before{content:"\f51a"}.bi-record2-fill::before{content:"\f51b"}.bi-record2::before{content:"\f51c"}.bi-reply-all-fill::before{content:"\f51d"}.bi-reply-all::before{content:"\f51e"}.bi-reply-fill::before{content:"\f51f"}.bi-reply::before{content:"\f520"}.bi-rss-fill::before{content:"\f521"}.bi-rss::before{content:"\f522"}.bi-rulers::before{content:"\f523"}.bi-save-fill::before{content:"\f524"}.bi-save::before{content:"\f525"}.bi-save2-fill::before{content:"\f526"}.bi-save2::before{content:"\f527"}.bi-scissors::before{content:"\f528"}.bi-screwdriver::before{content:"\f529"}.bi-search::before{content:"\f52a"}.bi-segmented-nav::before{content:"\f52b"}.bi-server::before{content:"\f52c"}.bi-share-fill::before{content:"\f52d"}.bi-share::before{content:"\f52e"}.bi-shield-check::before{content:"\f52f"}.bi-shield-exclamation::before{content:"\f530"}.bi-shield-fill-check::before{content:"\f531"}.bi-shield-fill-exclamation::before{content:"\f532"}.bi-shield-fill-minus::before{content:"\f533"}.bi-shield-fill-plus::before{content:"\f534"}.bi-shield-fill-x::before{content:"\f535"}.bi-shield-fill::before{content:"\f536"}.bi-shield-lock-fill::before{content:"\f537"}.bi-shield-lock::before{content:"\f538"}.bi-shield-minus::before{content:"\f539"}.bi-shield-plus::before{content:"\f53a"}.bi-shield-shaded::before{content:"\f53b"}.bi-shield-slash-fill::before{content:"\f53c"}.bi-shield-slash::before{content:"\f53d"}.bi-shield-x::before{content:"\f53e"}.bi-shield::before{content:"\f53f"}.bi-shift-fill::before{content:"\f540"}.bi-shift::before{content:"\f541"}.bi-shop-window::before{content:"\f542"}.bi-shop::before{content:"\f543"}.bi-shuffle::before{content:"\f544"}.bi-signpost-2-fill::before{content:"\f545"}.bi-signpost-2::before{content:"\f546"}.bi-signpost-fill::before{content:"\f547"}.bi-signpost-split-fill::before{content:"\f548"}.bi-signpost-split::before{content:"\f549"}.bi-signpost::before{content:"\f54a"}.bi-sim-fill::before{content:"\f54b"}.bi-sim::before{content:"\f54c"}.bi-skip-backward-btn-fill::before{content:"\f54d"}.bi-skip-backward-btn::before{content:"\f54e"}.bi-skip-backward-circle-fill::before{content:"\f54f"}.bi-skip-backward-circle::before{content:"\f550"}.bi-skip-backward-fill::before{content:"\f551"}.bi-skip-backward::before{content:"\f552"}.bi-skip-end-btn-fill::before{content:"\f553"}.bi-skip-end-btn::before{content:"\f554"}.bi-skip-end-circle-fill::before{content:"\f555"}.bi-skip-end-circle::before{content:"\f556"}.bi-skip-end-fill::before{content:"\f557"}.bi-skip-end::before{content:"\f558"}.bi-skip-forward-btn-fill::before{content:"\f559"}.bi-skip-forward-btn::before{content:"\f55a"}.bi-skip-forward-circle-fill::before{content:"\f55b"}.bi-skip-forward-circle::before{content:"\f55c"}.bi-skip-forward-fill::before{content:"\f55d"}.bi-skip-forward::before{content:"\f55e"}.bi-skip-start-btn-fill::before{content:"\f55f"}.bi-skip-start-btn::before{content:"\f560"}.bi-skip-start-circle-fill::before{content:"\f561"}.bi-skip-start-circle::before{content:"\f562"}.bi-skip-start-fill::before{content:"\f563"}.bi-skip-start::before{content:"\f564"}.bi-slack::before{content:"\f565"}.bi-slash-circle-fill::before{content:"\f566"}.bi-slash-circle::before{content:"\f567"}.bi-slash-square-fill::before{content:"\f568"}.bi-slash-square::before{content:"\f569"}.bi-slash::before{content:"\f56a"}.bi-sliders::before{content:"\f56b"}.bi-smartwatch::before{content:"\f56c"}.bi-snow::before{content:"\f56d"}.bi-snow2::before{content:"\f56e"}.bi-snow3::before{content:"\f56f"}.bi-sort-alpha-down-alt::before{content:"\f570"}.bi-sort-alpha-down::before{content:"\f571"}.bi-sort-alpha-up-alt::before{content:"\f572"}.bi-sort-alpha-up::before{content:"\f573"}.bi-sort-down-alt::before{content:"\f574"}.bi-sort-down::before{content:"\f575"}.bi-sort-numeric-down-alt::before{content:"\f576"}.bi-sort-numeric-down::before{content:"\f577"}.bi-sort-numeric-up-alt::before{content:"\f578"}.bi-sort-numeric-up::before{content:"\f579"}.bi-sort-up-alt::before{content:"\f57a"}.bi-sort-up::before{content:"\f57b"}.bi-soundwave::before{content:"\f57c"}.bi-speaker-fill::before{content:"\f57d"}.bi-speaker::before{content:"\f57e"}.bi-speedometer::before{content:"\f57f"}.bi-speedometer2::before{content:"\f580"}.bi-spellcheck::before{content:"\f581"}.bi-square-fill::before{content:"\f582"}.bi-square-half::before{content:"\f583"}.bi-square::before{content:"\f584"}.bi-stack::before{content:"\f585"}.bi-star-fill::before{content:"\f586"}.bi-star-half::before{content:"\f587"}.bi-star::before{content:"\f588"}.bi-stars::before{content:"\f589"}.bi-stickies-fill::before{content:"\f58a"}.bi-stickies::before{content:"\f58b"}.bi-sticky-fill::before{content:"\f58c"}.bi-sticky::before{content:"\f58d"}.bi-stop-btn-fill::before{content:"\f58e"}.bi-stop-btn::before{content:"\f58f"}.bi-stop-circle-fill::before{content:"\f590"}.bi-stop-circle::before{content:"\f591"}.bi-stop-fill::before{content:"\f592"}.bi-stop::before{content:"\f593"}.bi-stoplights-fill::before{content:"\f594"}.bi-stoplights::before{content:"\f595"}.bi-stopwatch-fill::before{content:"\f596"}.bi-stopwatch::before{content:"\f597"}.bi-subtract::before{content:"\f598"}.bi-suit-club-fill::before{content:"\f599"}.bi-suit-club::before{content:"\f59a"}.bi-suit-diamond-fill::before{content:"\f59b"}.bi-suit-diamond::before{content:"\f59c"}.bi-suit-heart-fill::before{content:"\f59d"}.bi-suit-heart::before{content:"\f59e"}.bi-suit-spade-fill::before{content:"\f59f"}.bi-suit-spade::before{content:"\f5a0"}.bi-sun-fill::before{content:"\f5a1"}.bi-sun::before{content:"\f5a2"}.bi-sunglasses::before{content:"\f5a3"}.bi-sunrise-fill::before{content:"\f5a4"}.bi-sunrise::before{content:"\f5a5"}.bi-sunset-fill::before{content:"\f5a6"}.bi-sunset::before{content:"\f5a7"}.bi-symmetry-horizontal::before{content:"\f5a8"}.bi-symmetry-vertical::before{content:"\f5a9"}.bi-table::before{content:"\f5aa"}.bi-tablet-fill::before{content:"\f5ab"}.bi-tablet-landscape-fill::before{content:"\f5ac"}.bi-tablet-landscape::before{content:"\f5ad"}.bi-tablet::before{content:"\f5ae"}.bi-tag-fill::before{content:"\f5af"}.bi-tag::before{content:"\f5b0"}.bi-tags-fill::before{content:"\f5b1"}.bi-tags::before{content:"\f5b2"}.bi-telegram::before{content:"\f5b3"}.bi-telephone-fill::before{content:"\f5b4"}.bi-telephone-forward-fill::before{content:"\f5b5"}.bi-telephone-forward::before{content:"\f5b6"}.bi-telephone-inbound-fill::before{content:"\f5b7"}.bi-telephone-inbound::before{content:"\f5b8"}.bi-telephone-minus-fill::before{content:"\f5b9"}.bi-telephone-minus::before{content:"\f5ba"}.bi-telephone-outbound-fill::before{content:"\f5bb"}.bi-telephone-outbound::before{content:"\f5bc"}.bi-telephone-plus-fill::before{content:"\f5bd"}.bi-telephone-plus::before{content:"\f5be"}.bi-telephone-x-fill::before{content:"\f5bf"}.bi-telephone-x::before{content:"\f5c0"}.bi-telephone::before{content:"\f5c1"}.bi-terminal-fill::before{content:"\f5c2"}.bi-terminal::before{content:"\f5c3"}.bi-text-center::before{content:"\f5c4"}.bi-text-indent-left::before{content:"\f5c5"}.bi-text-indent-right::before{content:"\f5c6"}.bi-text-left::before{content:"\f5c7"}.bi-text-paragraph::before{content:"\f5c8"}.bi-text-right::before{content:"\f5c9"}.bi-textarea-resize::before{content:"\f5ca"}.bi-textarea-t::before{content:"\f5cb"}.bi-textarea::before{content:"\f5cc"}.bi-thermometer-half::before{content:"\f5cd"}.bi-thermometer-high::before{content:"\f5ce"}.bi-thermometer-low::before{content:"\f5cf"}.bi-thermometer-snow::before{content:"\f5d0"}.bi-thermometer-sun::before{content:"\f5d1"}.bi-thermometer::before{content:"\f5d2"}.bi-three-dots-vertical::before{content:"\f5d3"}.bi-three-dots::before{content:"\f5d4"}.bi-toggle-off::before{content:"\f5d5"}.bi-toggle-on::before{content:"\f5d6"}.bi-toggle2-off::before{content:"\f5d7"}.bi-toggle2-on::before{content:"\f5d8"}.bi-toggles::before{content:"\f5d9"}.bi-toggles2::before{content:"\f5da"}.bi-tools::before{content:"\f5db"}.bi-tornado::before{content:"\f5dc"}.bi-trash-fill::before{content:"\f5dd"}.bi-trash::before{content:"\f5de"}.bi-trash2-fill::before{content:"\f5df"}.bi-trash2::before{content:"\f5e0"}.bi-tree-fill::before{content:"\f5e1"}.bi-tree::before{content:"\f5e2"}.bi-triangle-fill::before{content:"\f5e3"}.bi-triangle-half::before{content:"\f5e4"}.bi-triangle::before{content:"\f5e5"}.bi-trophy-fill::before{content:"\f5e6"}.bi-trophy::before{content:"\f5e7"}.bi-tropical-storm::before{content:"\f5e8"}.bi-truck-flatbed::before{content:"\f5e9"}.bi-truck::before{content:"\f5ea"}.bi-tsunami::before{content:"\f5eb"}.bi-tv-fill::before{content:"\f5ec"}.bi-tv::before{content:"\f5ed"}.bi-twitch::before{content:"\f5ee"}.bi-twitter::before{content:"\f5ef"}.bi-type-bold::before{content:"\f5f0"}.bi-type-h1::before{content:"\f5f1"}.bi-type-h2::before{content:"\f5f2"}.bi-type-h3::before{content:"\f5f3"}.bi-type-italic::before{content:"\f5f4"}.bi-type-strikethrough::before{content:"\f5f5"}.bi-type-underline::before{content:"\f5f6"}.bi-type::before{content:"\f5f7"}.bi-ui-checks-grid::before{content:"\f5f8"}.bi-ui-checks::before{content:"\f5f9"}.bi-ui-radios-grid::before{content:"\f5fa"}.bi-ui-radios::before{content:"\f5fb"}.bi-umbrella-fill::before{content:"\f5fc"}.bi-umbrella::before{content:"\f5fd"}.bi-union::before{content:"\f5fe"}.bi-unlock-fill::before{content:"\f5ff"}.bi-unlock::before{content:"\f600"}.bi-upc-scan::before{content:"\f601"}.bi-upc::before{content:"\f602"}.bi-upload::before{content:"\f603"}.bi-vector-pen::before{content:"\f604"}.bi-view-list::before{content:"\f605"}.bi-view-stacked::before{content:"\f606"}.bi-vinyl-fill::before{content:"\f607"}.bi-vinyl::before{content:"\f608"}.bi-voicemail::before{content:"\f609"}.bi-volume-down-fill::before{content:"\f60a"}.bi-volume-down::before{content:"\f60b"}.bi-volume-mute-fill::before{content:"\f60c"}.bi-volume-mute::before{content:"\f60d"}.bi-volume-off-fill::before{content:"\f60e"}.bi-volume-off::before{content:"\f60f"}.bi-volume-up-fill::before{content:"\f610"}.bi-volume-up::before{content:"\f611"}.bi-vr::before{content:"\f612"}.bi-wallet-fill::before{content:"\f613"}.bi-wallet::before{content:"\f614"}.bi-wallet2::before{content:"\f615"}.bi-watch::before{content:"\f616"}.bi-water::before{content:"\f617"}.bi-whatsapp::before{content:"\f618"}.bi-wifi-1::before{content:"\f619"}.bi-wifi-2::before{content:"\f61a"}.bi-wifi-off::before{content:"\f61b"}.bi-wifi::before{content:"\f61c"}.bi-wind::before{content:"\f61d"}.bi-window-dock::before{content:"\f61e"}.bi-window-sidebar::before{content:"\f61f"}.bi-window::before{content:"\f620"}.bi-wrench::before{content:"\f621"}.bi-x-circle-fill::before{content:"\f622"}.bi-x-circle::before{content:"\f623"}.bi-x-diamond-fill::before{content:"\f624"}.bi-x-diamond::before{content:"\f625"}.bi-x-octagon-fill::before{content:"\f626"}.bi-x-octagon::before{content:"\f627"}.bi-x-square-fill::before{content:"\f628"}.bi-x-square::before{content:"\f629"}.bi-x::before{content:"\f62a"}.bi-youtube::before{content:"\f62b"}.bi-zoom-in::before{content:"\f62c"}.bi-zoom-out::before{content:"\f62d"}.bi-bank::before{content:"\f62e"}.bi-bank2::before{content:"\f62f"}.bi-bell-slash-fill::before{content:"\f630"}.bi-bell-slash::before{content:"\f631"}.bi-cash-coin::before{content:"\f632"}.bi-check-lg::before{content:"\f633"}.bi-coin::before{content:"\f634"}.bi-currency-bitcoin::before{content:"\f635"}.bi-currency-dollar::before{content:"\f636"}.bi-currency-euro::before{content:"\f637"}.bi-currency-exchange::before{content:"\f638"}.bi-currency-pound::before{content:"\f639"}.bi-currency-yen::before{content:"\f63a"}.bi-dash-lg::before{content:"\f63b"}.bi-exclamation-lg::before{content:"\f63c"}.bi-file-earmark-pdf-fill::before{content:"\f63d"}.bi-file-earmark-pdf::before{content:"\f63e"}.bi-file-pdf-fill::before{content:"\f63f"}.bi-file-pdf::before{content:"\f640"}.bi-gender-ambiguous::before{content:"\f641"}.bi-gender-female::before{content:"\f642"}.bi-gender-male::before{content:"\f643"}.bi-gender-trans::before{content:"\f644"}.bi-headset-vr::before{content:"\f645"}.bi-info-lg::before{content:"\f646"}.bi-mastodon::before{content:"\f647"}.bi-messenger::before{content:"\f648"}.bi-piggy-bank-fill::before{content:"\f649"}.bi-piggy-bank::before{content:"\f64a"}.bi-pin-map-fill::before{content:"\f64b"}.bi-pin-map::before{content:"\f64c"}.bi-plus-lg::before{content:"\f64d"}.bi-question-lg::before{content:"\f64e"}.bi-recycle::before{content:"\f64f"}.bi-reddit::before{content:"\f650"}.bi-safe-fill::before{content:"\f651"}.bi-safe2-fill::before{content:"\f652"}.bi-safe2::before{content:"\f653"}.bi-sd-card-fill::before{content:"\f654"}.bi-sd-card::before{content:"\f655"}.bi-skype::before{content:"\f656"}.bi-slash-lg::before{content:"\f657"}.bi-translate::before{content:"\f658"}.bi-x-lg::before{content:"\f659"}.bi-safe::before{content:"\f65a"}.bi-apple::before{content:"\f65b"}.bi-microsoft::before{content:"\f65d"}.bi-windows::before{content:"\f65e"}.bi-behance::before{content:"\f65c"}.bi-dribbble::before{content:"\f65f"}.bi-line::before{content:"\f660"}.bi-medium::before{content:"\f661"}.bi-paypal::before{content:"\f662"}.bi-pinterest::before{content:"\f663"}.bi-signal::before{content:"\f664"}.bi-snapchat::before{content:"\f665"}.bi-spotify::before{content:"\f666"}.bi-stack-overflow::before{content:"\f667"}.bi-strava::before{content:"\f668"}.bi-wordpress::before{content:"\f669"}.bi-vimeo::before{content:"\f66a"}.bi-activity::before{content:"\f66b"}.bi-easel2-fill::before{content:"\f66c"}.bi-easel2::before{content:"\f66d"}.bi-easel3-fill::before{content:"\f66e"}.bi-easel3::before{content:"\f66f"}.bi-fan::before{content:"\f670"}.bi-fingerprint::before{content:"\f671"}.bi-graph-down-arrow::before{content:"\f672"}.bi-graph-up-arrow::before{content:"\f673"}.bi-hypnotize::before{content:"\f674"}.bi-magic::before{content:"\f675"}.bi-person-rolodex::before{content:"\f676"}.bi-person-video::before{content:"\f677"}.bi-person-video2::before{content:"\f678"}.bi-person-video3::before{content:"\f679"}.bi-person-workspace::before{content:"\f67a"}.bi-radioactive::before{content:"\f67b"}.bi-webcam-fill::before{content:"\f67c"}.bi-webcam::before{content:"\f67d"}.bi-yin-yang::before{content:"\f67e"}.bi-bandaid-fill::before{content:"\f680"}.bi-bandaid::before{content:"\f681"}.bi-bluetooth::before{content:"\f682"}.bi-body-text::before{content:"\f683"}.bi-boombox::before{content:"\f684"}.bi-boxes::before{content:"\f685"}.bi-dpad-fill::before{content:"\f686"}.bi-dpad::before{content:"\f687"}.bi-ear-fill::before{content:"\f688"}.bi-ear::before{content:"\f689"}.bi-envelope-check-fill::before{content:"\f68b"}.bi-envelope-check::before{content:"\f68c"}.bi-envelope-dash-fill::before{content:"\f68e"}.bi-envelope-dash::before{content:"\f68f"}.bi-envelope-exclamation-fill::before{content:"\f691"}.bi-envelope-exclamation::before{content:"\f692"}.bi-envelope-plus-fill::before{content:"\f693"}.bi-envelope-plus::before{content:"\f694"}.bi-envelope-slash-fill::before{content:"\f696"}.bi-envelope-slash::before{content:"\f697"}.bi-envelope-x-fill::before{content:"\f699"}.bi-envelope-x::before{content:"\f69a"}.bi-explicit-fill::before{content:"\f69b"}.bi-explicit::before{content:"\f69c"}.bi-git::before{content:"\f69d"}.bi-infinity::before{content:"\f69e"}.bi-list-columns-reverse::before{content:"\f69f"}.bi-list-columns::before{content:"\f6a0"}.bi-meta::before{content:"\f6a1"}.bi-nintendo-switch::before{content:"\f6a4"}.bi-pc-display-horizontal::before{content:"\f6a5"}.bi-pc-display::before{content:"\f6a6"}.bi-pc-horizontal::before{content:"\f6a7"}.bi-pc::before{content:"\f6a8"}.bi-playstation::before{content:"\f6a9"}.bi-plus-slash-minus::before{content:"\f6aa"}.bi-projector-fill::before{content:"\f6ab"}.bi-projector::before{content:"\f6ac"}.bi-qr-code-scan::before{content:"\f6ad"}.bi-qr-code::before{content:"\f6ae"}.bi-quora::before{content:"\f6af"}.bi-quote::before{content:"\f6b0"}.bi-robot::before{content:"\f6b1"}.bi-send-check-fill::before{content:"\f6b2"}.bi-send-check::before{content:"\f6b3"}.bi-send-dash-fill::before{content:"\f6b4"}.bi-send-dash::before{content:"\f6b5"}.bi-send-exclamation-fill::before{content:"\f6b7"}.bi-send-exclamation::before{content:"\f6b8"}.bi-send-fill::before{content:"\f6b9"}.bi-send-plus-fill::before{content:"\f6ba"}.bi-send-plus::before{content:"\f6bb"}.bi-send-slash-fill::before{content:"\f6bc"}.bi-send-slash::before{content:"\f6bd"}.bi-send-x-fill::before{content:"\f6be"}.bi-send-x::before{content:"\f6bf"}.bi-send::before{content:"\f6c0"}.bi-steam::before{content:"\f6c1"}.bi-terminal-dash::before{content:"\f6c3"}.bi-terminal-plus::before{content:"\f6c4"}.bi-terminal-split::before{content:"\f6c5"}.bi-ticket-detailed-fill::before{content:"\f6c6"}.bi-ticket-detailed::before{content:"\f6c7"}.bi-ticket-fill::before{content:"\f6c8"}.bi-ticket-perforated-fill::before{content:"\f6c9"}.bi-ticket-perforated::before{content:"\f6ca"}.bi-ticket::before{content:"\f6cb"}.bi-tiktok::before{content:"\f6cc"}.bi-window-dash::before{content:"\f6cd"}.bi-window-desktop::before{content:"\f6ce"}.bi-window-fullscreen::before{content:"\f6cf"}.bi-window-plus::before{content:"\f6d0"}.bi-window-split::before{content:"\f6d1"}.bi-window-stack::before{content:"\f6d2"}.bi-window-x::before{content:"\f6d3"}.bi-xbox::before{content:"\f6d4"}.bi-ethernet::before{content:"\f6d5"}.bi-hdmi-fill::before{content:"\f6d6"}.bi-hdmi::before{content:"\f6d7"}.bi-usb-c-fill::before{content:"\f6d8"}.bi-usb-c::before{content:"\f6d9"}.bi-usb-fill::before{content:"\f6da"}.bi-usb-plug-fill::before{content:"\f6db"}.bi-usb-plug::before{content:"\f6dc"}.bi-usb-symbol::before{content:"\f6dd"}.bi-usb::before{content:"\f6de"}.bi-boombox-fill::before{content:"\f6df"}.bi-displayport::before{content:"\f6e1"}.bi-gpu-card::before{content:"\f6e2"}.bi-memory::before{content:"\f6e3"}.bi-modem-fill::before{content:"\f6e4"}.bi-modem::before{content:"\f6e5"}.bi-motherboard-fill::before{content:"\f6e6"}.bi-motherboard::before{content:"\f6e7"}.bi-optical-audio-fill::before{content:"\f6e8"}.bi-optical-audio::before{content:"\f6e9"}.bi-pci-card::before{content:"\f6ea"}.bi-router-fill::before{content:"\f6eb"}.bi-router::before{content:"\f6ec"}.bi-thunderbolt-fill::before{content:"\f6ef"}.bi-thunderbolt::before{content:"\f6f0"}.bi-usb-drive-fill::before{content:"\f6f1"}.bi-usb-drive::before{content:"\f6f2"}.bi-usb-micro-fill::before{content:"\f6f3"}.bi-usb-micro::before{content:"\f6f4"}.bi-usb-mini-fill::before{content:"\f6f5"}.bi-usb-mini::before{content:"\f6f6"}.bi-cloud-haze2::before{content:"\f6f7"}.bi-device-hdd-fill::before{content:"\f6f8"}.bi-device-hdd::before{content:"\f6f9"}.bi-device-ssd-fill::before{content:"\f6fa"}.bi-device-ssd::before{content:"\f6fb"}.bi-displayport-fill::before{content:"\f6fc"}.bi-mortarboard-fill::before{content:"\f6fd"}.bi-mortarboard::before{content:"\f6fe"}.bi-terminal-x::before{content:"\f6ff"}.bi-arrow-through-heart-fill::before{content:"\f700"}.bi-arrow-through-heart::before{content:"\f701"}.bi-badge-sd-fill::before{content:"\f702"}.bi-badge-sd::before{content:"\f703"}.bi-bag-heart-fill::before{content:"\f704"}.bi-bag-heart::before{content:"\f705"}.bi-balloon-fill::before{content:"\f706"}.bi-balloon-heart-fill::before{content:"\f707"}.bi-balloon-heart::before{content:"\f708"}.bi-balloon::before{content:"\f709"}.bi-box2-fill::before{content:"\f70a"}.bi-box2-heart-fill::before{content:"\f70b"}.bi-box2-heart::before{content:"\f70c"}.bi-box2::before{content:"\f70d"}.bi-braces-asterisk::before{content:"\f70e"}.bi-calendar-heart-fill::before{content:"\f70f"}.bi-calendar-heart::before{content:"\f710"}.bi-calendar2-heart-fill::before{content:"\f711"}.bi-calendar2-heart::before{content:"\f712"}.bi-chat-heart-fill::before{content:"\f713"}.bi-chat-heart::before{content:"\f714"}.bi-chat-left-heart-fill::before{content:"\f715"}.bi-chat-left-heart::before{content:"\f716"}.bi-chat-right-heart-fill::before{content:"\f717"}.bi-chat-right-heart::before{content:"\f718"}.bi-chat-square-heart-fill::before{content:"\f719"}.bi-chat-square-heart::before{content:"\f71a"}.bi-clipboard-check-fill::before{content:"\f71b"}.bi-clipboard-data-fill::before{content:"\f71c"}.bi-clipboard-fill::before{content:"\f71d"}.bi-clipboard-heart-fill::before{content:"\f71e"}.bi-clipboard-heart::before{content:"\f71f"}.bi-clipboard-minus-fill::before{content:"\f720"}.bi-clipboard-plus-fill::before{content:"\f721"}.bi-clipboard-pulse::before{content:"\f722"}.bi-clipboard-x-fill::before{content:"\f723"}.bi-clipboard2-check-fill::before{content:"\f724"}.bi-clipboard2-check::before{content:"\f725"}.bi-clipboard2-data-fill::before{content:"\f726"}.bi-clipboard2-data::before{content:"\f727"}.bi-clipboard2-fill::before{content:"\f728"}.bi-clipboard2-heart-fill::before{content:"\f729"}.bi-clipboard2-heart::before{content:"\f72a"}.bi-clipboard2-minus-fill::before{content:"\f72b"}.bi-clipboard2-minus::before{content:"\f72c"}.bi-clipboard2-plus-fill::before{content:"\f72d"}.bi-clipboard2-plus::before{content:"\f72e"}.bi-clipboard2-pulse-fill::before{content:"\f72f"}.bi-clipboard2-pulse::before{content:"\f730"}.bi-clipboard2-x-fill::before{content:"\f731"}.bi-clipboard2-x::before{content:"\f732"}.bi-clipboard2::before{content:"\f733"}.bi-emoji-kiss-fill::before{content:"\f734"}.bi-emoji-kiss::before{content:"\f735"}.bi-envelope-heart-fill::before{content:"\f736"}.bi-envelope-heart::before{content:"\f737"}.bi-envelope-open-heart-fill::before{content:"\f738"}.bi-envelope-open-heart::before{content:"\f739"}.bi-envelope-paper-fill::before{content:"\f73a"}.bi-envelope-paper-heart-fill::before{content:"\f73b"}.bi-envelope-paper-heart::before{content:"\f73c"}.bi-envelope-paper::before{content:"\f73d"}.bi-filetype-aac::before{content:"\f73e"}.bi-filetype-ai::before{content:"\f73f"}.bi-filetype-bmp::before{content:"\f740"}.bi-filetype-cs::before{content:"\f741"}.bi-filetype-css::before{content:"\f742"}.bi-filetype-csv::before{content:"\f743"}.bi-filetype-doc::before{content:"\f744"}.bi-filetype-docx::before{content:"\f745"}.bi-filetype-exe::before{content:"\f746"}.bi-filetype-gif::before{content:"\f747"}.bi-filetype-heic::before{content:"\f748"}.bi-filetype-html::before{content:"\f749"}.bi-filetype-java::before{content:"\f74a"}.bi-filetype-jpg::before{content:"\f74b"}.bi-filetype-js::before{content:"\f74c"}.bi-filetype-jsx::before{content:"\f74d"}.bi-filetype-key::before{content:"\f74e"}.bi-filetype-m4p::before{content:"\f74f"}.bi-filetype-md::before{content:"\f750"}.bi-filetype-mdx::before{content:"\f751"}.bi-filetype-mov::before{content:"\f752"}.bi-filetype-mp3::before{content:"\f753"}.bi-filetype-mp4::before{content:"\f754"}.bi-filetype-otf::before{content:"\f755"}.bi-filetype-pdf::before{content:"\f756"}.bi-filetype-php::before{content:"\f757"}.bi-filetype-png::before{content:"\f758"}.bi-filetype-ppt::before{content:"\f75a"}.bi-filetype-psd::before{content:"\f75b"}.bi-filetype-py::before{content:"\f75c"}.bi-filetype-raw::before{content:"\f75d"}.bi-filetype-rb::before{content:"\f75e"}.bi-filetype-sass::before{content:"\f75f"}.bi-filetype-scss::before{content:"\f760"}.bi-filetype-sh::before{content:"\f761"}.bi-filetype-svg::before{content:"\f762"}.bi-filetype-tiff::before{content:"\f763"}.bi-filetype-tsx::before{content:"\f764"}.bi-filetype-ttf::before{content:"\f765"}.bi-filetype-txt::before{content:"\f766"}.bi-filetype-wav::before{content:"\f767"}.bi-filetype-woff::before{content:"\f768"}.bi-filetype-xls::before{content:"\f76a"}.bi-filetype-xml::before{content:"\f76b"}.bi-filetype-yml::before{content:"\f76c"}.bi-heart-arrow::before{content:"\f76d"}.bi-heart-pulse-fill::before{content:"\f76e"}.bi-heart-pulse::before{content:"\f76f"}.bi-heartbreak-fill::before{content:"\f770"}.bi-heartbreak::before{content:"\f771"}.bi-hearts::before{content:"\f772"}.bi-hospital-fill::before{content:"\f773"}.bi-hospital::before{content:"\f774"}.bi-house-heart-fill::before{content:"\f775"}.bi-house-heart::before{content:"\f776"}.bi-incognito::before{content:"\f777"}.bi-magnet-fill::before{content:"\f778"}.bi-magnet::before{content:"\f779"}.bi-person-heart::before{content:"\f77a"}.bi-person-hearts::before{content:"\f77b"}.bi-phone-flip::before{content:"\f77c"}.bi-plugin::before{content:"\f77d"}.bi-postage-fill::before{content:"\f77e"}.bi-postage-heart-fill::before{content:"\f77f"}.bi-postage-heart::before{content:"\f780"}.bi-postage::before{content:"\f781"}.bi-postcard-fill::before{content:"\f782"}.bi-postcard-heart-fill::before{content:"\f783"}.bi-postcard-heart::before{content:"\f784"}.bi-postcard::before{content:"\f785"}.bi-search-heart-fill::before{content:"\f786"}.bi-search-heart::before{content:"\f787"}.bi-sliders2-vertical::before{content:"\f788"}.bi-sliders2::before{content:"\f789"}.bi-trash3-fill::before{content:"\f78a"}.bi-trash3::before{content:"\f78b"}.bi-valentine::before{content:"\f78c"}.bi-valentine2::before{content:"\f78d"}.bi-wrench-adjustable-circle-fill::before{content:"\f78e"}.bi-wrench-adjustable-circle::before{content:"\f78f"}.bi-wrench-adjustable::before{content:"\f790"}.bi-filetype-json::before{content:"\f791"}.bi-filetype-pptx::before{content:"\f792"}.bi-filetype-xlsx::before{content:"\f793"}.bi-1-circle-fill::before{content:"\f796"}.bi-1-circle::before{content:"\f797"}.bi-1-square-fill::before{content:"\f798"}.bi-1-square::before{content:"\f799"}.bi-2-circle-fill::before{content:"\f79c"}.bi-2-circle::before{content:"\f79d"}.bi-2-square-fill::before{content:"\f79e"}.bi-2-square::before{content:"\f79f"}.bi-3-circle-fill::before{content:"\f7a2"}.bi-3-circle::before{content:"\f7a3"}.bi-3-square-fill::before{content:"\f7a4"}.bi-3-square::before{content:"\f7a5"}.bi-4-circle-fill::before{content:"\f7a8"}.bi-4-circle::before{content:"\f7a9"}.bi-4-square-fill::before{content:"\f7aa"}.bi-4-square::before{content:"\f7ab"}.bi-5-circle-fill::before{content:"\f7ae"}.bi-5-circle::before{content:"\f7af"}.bi-5-square-fill::before{content:"\f7b0"}.bi-5-square::before{content:"\f7b1"}.bi-6-circle-fill::before{content:"\f7b4"}.bi-6-circle::before{content:"\f7b5"}.bi-6-square-fill::before{content:"\f7b6"}.bi-6-square::before{content:"\f7b7"}.bi-7-circle-fill::before{content:"\f7ba"}.bi-7-circle::before{content:"\f7bb"}.bi-7-square-fill::before{content:"\f7bc"}.bi-7-square::before{content:"\f7bd"}.bi-8-circle-fill::before{content:"\f7c0"}.bi-8-circle::before{content:"\f7c1"}.bi-8-square-fill::before{content:"\f7c2"}.bi-8-square::before{content:"\f7c3"}.bi-9-circle-fill::before{content:"\f7c6"}.bi-9-circle::before{content:"\f7c7"}.bi-9-square-fill::before{content:"\f7c8"}.bi-9-square::before{content:"\f7c9"}.bi-airplane-engines-fill::before{content:"\f7ca"}.bi-airplane-engines::before{content:"\f7cb"}.bi-airplane-fill::before{content:"\f7cc"}.bi-airplane::before{content:"\f7cd"}.bi-alexa::before{content:"\f7ce"}.bi-alipay::before{content:"\f7cf"}.bi-android::before{content:"\f7d0"}.bi-android2::before{content:"\f7d1"}.bi-box-fill::before{content:"\f7d2"}.bi-box-seam-fill::before{content:"\f7d3"}.bi-browser-chrome::before{content:"\f7d4"}.bi-browser-edge::before{content:"\f7d5"}.bi-browser-firefox::before{content:"\f7d6"}.bi-browser-safari::before{content:"\f7d7"}.bi-c-circle-fill::before{content:"\f7da"}.bi-c-circle::before{content:"\f7db"}.bi-c-square-fill::before{content:"\f7dc"}.bi-c-square::before{content:"\f7dd"}.bi-capsule-pill::before{content:"\f7de"}.bi-capsule::before{content:"\f7df"}.bi-car-front-fill::before{content:"\f7e0"}.bi-car-front::before{content:"\f7e1"}.bi-cassette-fill::before{content:"\f7e2"}.bi-cassette::before{content:"\f7e3"}.bi-cc-circle-fill::before{content:"\f7e6"}.bi-cc-circle::before{content:"\f7e7"}.bi-cc-square-fill::before{content:"\f7e8"}.bi-cc-square::before{content:"\f7e9"}.bi-cup-hot-fill::before{content:"\f7ea"}.bi-cup-hot::before{content:"\f7eb"}.bi-currency-rupee::before{content:"\f7ec"}.bi-dropbox::before{content:"\f7ed"}.bi-escape::before{content:"\f7ee"}.bi-fast-forward-btn-fill::before{content:"\f7ef"}.bi-fast-forward-btn::before{content:"\f7f0"}.bi-fast-forward-circle-fill::before{content:"\f7f1"}.bi-fast-forward-circle::before{content:"\f7f2"}.bi-fast-forward-fill::before{content:"\f7f3"}.bi-fast-forward::before{content:"\f7f4"}.bi-filetype-sql::before{content:"\f7f5"}.bi-fire::before{content:"\f7f6"}.bi-google-play::before{content:"\f7f7"}.bi-h-circle-fill::before{content:"\f7fa"}.bi-h-circle::before{content:"\f7fb"}.bi-h-square-fill::before{content:"\f7fc"}.bi-h-square::before{content:"\f7fd"}.bi-indent::before{content:"\f7fe"}.bi-lungs-fill::before{content:"\f7ff"}.bi-lungs::before{content:"\f800"}.bi-microsoft-teams::before{content:"\f801"}.bi-p-circle-fill::before{content:"\f804"}.bi-p-circle::before{content:"\f805"}.bi-p-square-fill::before{content:"\f806"}.bi-p-square::before{content:"\f807"}.bi-pass-fill::before{content:"\f808"}.bi-pass::before{content:"\f809"}.bi-prescription::before{content:"\f80a"}.bi-prescription2::before{content:"\f80b"}.bi-r-circle-fill::before{content:"\f80e"}.bi-r-circle::before{content:"\f80f"}.bi-r-square-fill::before{content:"\f810"}.bi-r-square::before{content:"\f811"}.bi-repeat-1::before{content:"\f812"}.bi-repeat::before{content:"\f813"}.bi-rewind-btn-fill::before{content:"\f814"}.bi-rewind-btn::before{content:"\f815"}.bi-rewind-circle-fill::before{content:"\f816"}.bi-rewind-circle::before{content:"\f817"}.bi-rewind-fill::before{content:"\f818"}.bi-rewind::before{content:"\f819"}.bi-train-freight-front-fill::before{content:"\f81a"}.bi-train-freight-front::before{content:"\f81b"}.bi-train-front-fill::before{content:"\f81c"}.bi-train-front::before{content:"\f81d"}.bi-train-lightrail-front-fill::before{content:"\f81e"}.bi-train-lightrail-front::before{content:"\f81f"}.bi-truck-front-fill::before{content:"\f820"}.bi-truck-front::before{content:"\f821"}.bi-ubuntu::before{content:"\f822"}.bi-unindent::before{content:"\f823"}.bi-unity::before{content:"\f824"}.bi-universal-access-circle::before{content:"\f825"}.bi-universal-access::before{content:"\f826"}.bi-virus::before{content:"\f827"}.bi-virus2::before{content:"\f828"}.bi-wechat::before{content:"\f829"}.bi-yelp::before{content:"\f82a"}.bi-sign-stop-fill::before{content:"\f82b"}.bi-sign-stop-lights-fill::before{content:"\f82c"}.bi-sign-stop-lights::before{content:"\f82d"}.bi-sign-stop::before{content:"\f82e"}.bi-sign-turn-left-fill::before{content:"\f82f"}.bi-sign-turn-left::before{content:"\f830"}.bi-sign-turn-right-fill::before{content:"\f831"}.bi-sign-turn-right::before{content:"\f832"}.bi-sign-turn-slight-left-fill::before{content:"\f833"}.bi-sign-turn-slight-left::before{content:"\f834"}.bi-sign-turn-slight-right-fill::before{content:"\f835"}.bi-sign-turn-slight-right::before{content:"\f836"}.bi-sign-yield-fill::before{content:"\f837"}.bi-sign-yield::before{content:"\f838"}.bi-ev-station-fill::before{content:"\f839"}.bi-ev-station::before{content:"\f83a"}.bi-fuel-pump-diesel-fill::before{content:"\f83b"}.bi-fuel-pump-diesel::before{content:"\f83c"}.bi-fuel-pump-fill::before{content:"\f83d"}.bi-fuel-pump::before{content:"\f83e"}.bi-0-circle-fill::before{content:"\f83f"}.bi-0-circle::before{content:"\f840"}.bi-0-square-fill::before{content:"\f841"}.bi-0-square::before{content:"\f842"}.bi-rocket-fill::before{content:"\f843"}.bi-rocket-takeoff-fill::before{content:"\f844"}.bi-rocket-takeoff::before{content:"\f845"}.bi-rocket::before{content:"\f846"}.bi-stripe::before{content:"\f847"}.bi-subscript::before{content:"\f848"}.bi-superscript::before{content:"\f849"}.bi-trello::before{content:"\f84a"}.bi-envelope-at-fill::before{content:"\f84b"}.bi-envelope-at::before{content:"\f84c"}.bi-regex::before{content:"\f84d"}.bi-text-wrap::before{content:"\f84e"}.bi-sign-dead-end-fill::before{content:"\f84f"}.bi-sign-dead-end::before{content:"\f850"}.bi-sign-do-not-enter-fill::before{content:"\f851"}.bi-sign-do-not-enter::before{content:"\f852"}.bi-sign-intersection-fill::before{content:"\f853"}.bi-sign-intersection-side-fill::before{content:"\f854"}.bi-sign-intersection-side::before{content:"\f855"}.bi-sign-intersection-t-fill::before{content:"\f856"}.bi-sign-intersection-t::before{content:"\f857"}.bi-sign-intersection-y-fill::before{content:"\f858"}.bi-sign-intersection-y::before{content:"\f859"}.bi-sign-intersection::before{content:"\f85a"}.bi-sign-merge-left-fill::before{content:"\f85b"}.bi-sign-merge-left::before{content:"\f85c"}.bi-sign-merge-right-fill::before{content:"\f85d"}.bi-sign-merge-right::before{content:"\f85e"}.bi-sign-no-left-turn-fill::before{content:"\f85f"}.bi-sign-no-left-turn::before{content:"\f860"}.bi-sign-no-parking-fill::before{content:"\f861"}.bi-sign-no-parking::before{content:"\f862"}.bi-sign-no-right-turn-fill::before{content:"\f863"}.bi-sign-no-right-turn::before{content:"\f864"}.bi-sign-railroad-fill::before{content:"\f865"}.bi-sign-railroad::before{content:"\f866"}.bi-building-add::before{content:"\f867"}.bi-building-check::before{content:"\f868"}.bi-building-dash::before{content:"\f869"}.bi-building-down::before{content:"\f86a"}.bi-building-exclamation::before{content:"\f86b"}.bi-building-fill-add::before{content:"\f86c"}.bi-building-fill-check::before{content:"\f86d"}.bi-building-fill-dash::before{content:"\f86e"}.bi-building-fill-down::before{content:"\f86f"}.bi-building-fill-exclamation::before{content:"\f870"}.bi-building-fill-gear::before{content:"\f871"}.bi-building-fill-lock::before{content:"\f872"}.bi-building-fill-slash::before{content:"\f873"}.bi-building-fill-up::before{content:"\f874"}.bi-building-fill-x::before{content:"\f875"}.bi-building-fill::before{content:"\f876"}.bi-building-gear::before{content:"\f877"}.bi-building-lock::before{content:"\f878"}.bi-building-slash::before{content:"\f879"}.bi-building-up::before{content:"\f87a"}.bi-building-x::before{content:"\f87b"}.bi-buildings-fill::before{content:"\f87c"}.bi-buildings::before{content:"\f87d"}.bi-bus-front-fill::before{content:"\f87e"}.bi-bus-front::before{content:"\f87f"}.bi-ev-front-fill::before{content:"\f880"}.bi-ev-front::before{content:"\f881"}.bi-globe-americas::before{content:"\f882"}.bi-globe-asia-australia::before{content:"\f883"}.bi-globe-central-south-asia::before{content:"\f884"}.bi-globe-europe-africa::before{content:"\f885"}.bi-house-add-fill::before{content:"\f886"}.bi-house-add::before{content:"\f887"}.bi-house-check-fill::before{content:"\f888"}.bi-house-check::before{content:"\f889"}.bi-house-dash-fill::before{content:"\f88a"}.bi-house-dash::before{content:"\f88b"}.bi-house-down-fill::before{content:"\f88c"}.bi-house-down::before{content:"\f88d"}.bi-house-exclamation-fill::before{content:"\f88e"}.bi-house-exclamation::before{content:"\f88f"}.bi-house-gear-fill::before{content:"\f890"}.bi-house-gear::before{content:"\f891"}.bi-house-lock-fill::before{content:"\f892"}.bi-house-lock::before{content:"\f893"}.bi-house-slash-fill::before{content:"\f894"}.bi-house-slash::before{content:"\f895"}.bi-house-up-fill::before{content:"\f896"}.bi-house-up::before{content:"\f897"}.bi-house-x-fill::before{content:"\f898"}.bi-house-x::before{content:"\f899"}.bi-person-add::before{content:"\f89a"}.bi-person-down::before{content:"\f89b"}.bi-person-exclamation::before{content:"\f89c"}.bi-person-fill-add::before{content:"\f89d"}.bi-person-fill-check::before{content:"\f89e"}.bi-person-fill-dash::before{content:"\f89f"}.bi-person-fill-down::before{content:"\f8a0"}.bi-person-fill-exclamation::before{content:"\f8a1"}.bi-person-fill-gear::before{content:"\f8a2"}.bi-person-fill-lock::before{content:"\f8a3"}.bi-person-fill-slash::before{content:"\f8a4"}.bi-person-fill-up::before{content:"\f8a5"}.bi-person-fill-x::before{content:"\f8a6"}.bi-person-gear::before{content:"\f8a7"}.bi-person-lock::before{content:"\f8a8"}.bi-person-slash::before{content:"\f8a9"}.bi-person-up::before{content:"\f8aa"}.bi-scooter::before{content:"\f8ab"}.bi-taxi-front-fill::before{content:"\f8ac"}.bi-taxi-front::before{content:"\f8ad"}.bi-amd::before{content:"\f8ae"}.bi-database-add::before{content:"\f8af"}.bi-database-check::before{content:"\f8b0"}.bi-database-dash::before{content:"\f8b1"}.bi-database-down::before{content:"\f8b2"}.bi-database-exclamation::before{content:"\f8b3"}.bi-database-fill-add::before{content:"\f8b4"}.bi-database-fill-check::before{content:"\f8b5"}.bi-database-fill-dash::before{content:"\f8b6"}.bi-database-fill-down::before{content:"\f8b7"}.bi-database-fill-exclamation::before{content:"\f8b8"}.bi-database-fill-gear::before{content:"\f8b9"}.bi-database-fill-lock::before{content:"\f8ba"}.bi-database-fill-slash::before{content:"\f8bb"}.bi-database-fill-up::before{content:"\f8bc"}.bi-database-fill-x::before{content:"\f8bd"}.bi-database-fill::before{content:"\f8be"}.bi-database-gear::before{content:"\f8bf"}.bi-database-lock::before{content:"\f8c0"}.bi-database-slash::before{content:"\f8c1"}.bi-database-up::before{content:"\f8c2"}.bi-database-x::before{content:"\f8c3"}.bi-database::before{content:"\f8c4"}.bi-houses-fill::before{content:"\f8c5"}.bi-houses::before{content:"\f8c6"}.bi-nvidia::before{content:"\f8c7"}.bi-person-vcard-fill::before{content:"\f8c8"}.bi-person-vcard::before{content:"\f8c9"}.bi-sina-weibo::before{content:"\f8ca"}.bi-tencent-qq::before{content:"\f8cb"}.bi-wikipedia::before{content:"\f8cc"}.bi-alphabet-uppercase::before{content:"\f2a5"}.bi-alphabet::before{content:"\f68a"}.bi-amazon::before{content:"\f68d"}.bi-arrows-collapse-vertical::before{content:"\f690"}.bi-arrows-expand-vertical::before{content:"\f695"}.bi-arrows-vertical::before{content:"\f698"}.bi-arrows::before{content:"\f6a2"}.bi-ban-fill::before{content:"\f6a3"}.bi-ban::before{content:"\f6b6"}.bi-bing::before{content:"\f6c2"}.bi-cake::before{content:"\f6e0"}.bi-cake2::before{content:"\f6ed"}.bi-cookie::before{content:"\f6ee"}.bi-copy::before{content:"\f759"}.bi-crosshair::before{content:"\f769"}.bi-crosshair2::before{content:"\f794"}.bi-emoji-astonished-fill::before{content:"\f795"}.bi-emoji-astonished::before{content:"\f79a"}.bi-emoji-grimace-fill::before{content:"\f79b"}.bi-emoji-grimace::before{content:"\f7a0"}.bi-emoji-grin-fill::before{content:"\f7a1"}.bi-emoji-grin::before{content:"\f7a6"}.bi-emoji-surprise-fill::before{content:"\f7a7"}.bi-emoji-surprise::before{content:"\f7ac"}.bi-emoji-tear-fill::before{content:"\f7ad"}.bi-emoji-tear::before{content:"\f7b2"}.bi-envelope-arrow-down-fill::before{content:"\f7b3"}.bi-envelope-arrow-down::before{content:"\f7b8"}.bi-envelope-arrow-up-fill::before{content:"\f7b9"}.bi-envelope-arrow-up::before{content:"\f7be"}.bi-feather::before{content:"\f7bf"}.bi-feather2::before{content:"\f7c4"}.bi-floppy-fill::before{content:"\f7c5"}.bi-floppy::before{content:"\f7d8"}.bi-floppy2-fill::before{content:"\f7d9"}.bi-floppy2::before{content:"\f7e4"}.bi-gitlab::before{content:"\f7e5"}.bi-highlighter::before{content:"\f7f8"}.bi-marker-tip::before{content:"\f802"}.bi-nvme-fill::before{content:"\f803"}.bi-nvme::before{content:"\f80c"}.bi-opencollective::before{content:"\f80d"}.bi-pci-card-network::before{content:"\f8cd"}.bi-pci-card-sound::before{content:"\f8ce"}.bi-radar::before{content:"\f8cf"}.bi-send-arrow-down-fill::before{content:"\f8d0"}.bi-send-arrow-down::before{content:"\f8d1"}.bi-send-arrow-up-fill::before{content:"\f8d2"}.bi-send-arrow-up::before{content:"\f8d3"}.bi-sim-slash-fill::before{content:"\f8d4"}.bi-sim-slash::before{content:"\f8d5"}.bi-sourceforge::before{content:"\f8d6"}.bi-substack::before{content:"\f8d7"}.bi-threads-fill::before{content:"\f8d8"}.bi-threads::before{content:"\f8d9"}.bi-transparency::before{content:"\f8da"}.bi-twitter-x::before{content:"\f8db"}.bi-type-h4::before{content:"\f8dc"}.bi-type-h5::before{content:"\f8dd"}.bi-type-h6::before{content:"\f8de"}.bi-backpack-fill::before{content:"\f8df"}.bi-backpack::before{content:"\f8e0"}.bi-backpack2-fill::before{content:"\f8e1"}.bi-backpack2::before{content:"\f8e2"}.bi-backpack3-fill::before{content:"\f8e3"}.bi-backpack3::before{content:"\f8e4"}.bi-backpack4-fill::before{content:"\f8e5"}.bi-backpack4::before{content:"\f8e6"}.bi-brilliance::before{content:"\f8e7"}.bi-cake-fill::before{content:"\f8e8"}.bi-cake2-fill::before{content:"\f8e9"}.bi-duffle-fill::before{content:"\f8ea"}.bi-duffle::before{content:"\f8eb"}.bi-exposure::before{content:"\f8ec"}.bi-gender-neuter::before{content:"\f8ed"}.bi-highlights::before{content:"\f8ee"}.bi-luggage-fill::before{content:"\f8ef"}.bi-luggage::before{content:"\f8f0"}.bi-mailbox-flag::before{content:"\f8f1"}.bi-mailbox2-flag::before{content:"\f8f2"}.bi-noise-reduction::before{content:"\f8f3"}.bi-passport-fill::before{content:"\f8f4"}.bi-passport::before{content:"\f8f5"}.bi-person-arms-up::before{content:"\f8f6"}.bi-person-raised-hand::before{content:"\f8f7"}.bi-person-standing-dress::before{content:"\f8f8"}.bi-person-standing::before{content:"\f8f9"}.bi-person-walking::before{content:"\f8fa"}.bi-person-wheelchair::before{content:"\f8fb"}.bi-shadows::before{content:"\f8fc"}.bi-suitcase-fill::before{content:"\f8fd"}.bi-suitcase-lg-fill::before{content:"\f8fe"}.bi-suitcase-lg::before{content:"\f8ff"}.bi-suitcase::before{content:"\f900"}.bi-suitcase2-fill::before{content:"\f901"}.bi-suitcase2::before{content:"\f902"}.bi-vignette::before{content:"\f903"}.bi-bluesky::before{content:"\f7f9"}.bi-tux::before{content:"\f904"}.bi-beaker-fill::before{content:"\f905"}.bi-beaker::before{content:"\f906"}.bi-flask-fill::before{content:"\f907"}.bi-flask-florence-fill::before{content:"\f908"}.bi-flask-florence::before{content:"\f909"}.bi-flask::before{content:"\f90a"}.bi-leaf-fill::before{content:"\f90b"}.bi-leaf::before{content:"\f90c"}.bi-measuring-cup-fill::before{content:"\f90d"}.bi-measuring-cup::before{content:"\f90e"}.bi-unlock2-fill::before{content:"\f90f"}.bi-unlock2::before{content:"\f910"}.bi-battery-low::before{content:"\f911"}.bi-anthropic::before{content:"\f912"}.bi-apple-music::before{content:"\f913"}.bi-claude::before{content:"\f914"}.bi-openai::before{content:"\f915"}.bi-perplexity::before{content:"\f916"}.bi-css::before{content:"\f917"}.bi-javascript::before{content:"\f918"}.bi-typescript::before{content:"\f919"}.bi-fork-knife::before{content:"\f91a"}.bi-globe-americas-fill::before{content:"\f91b"}.bi-globe-asia-australia-fill::before{content:"\f91c"}.bi-globe-central-south-asia-fill::before{content:"\f91d"}.bi-globe-europe-africa-fill::before{content:"\f91e"} \ No newline at end of file diff --git a/modules/ui-module/src/main/resources/manager/bootstrap/bootstrap-superhero.min.css b/modules/ui-module/src/main/resources/manager/bootstrap/bootstrap-superhero.min.css new file mode 100644 index 000000000..519b08045 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/bootstrap/bootstrap-superhero.min.css @@ -0,0 +1,12 @@ +@charset "UTF-8";/*! + * Bootswatch v5.3.6 (https://bootswatch.com) + * Theme: superhero + * Copyright 2012-2025 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*//*! + * Bootstrap v5.3.6 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */@import url(https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap);:root,[data-bs-theme=light]{--bs-blue:#4c9be8;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#e83e8c;--bs-red:#d9534f;--bs-orange:#df6919;--bs-yellow:#ffc107;--bs-green:#5cb85c;--bs-teal:#20c997;--bs-cyan:#5bc0de;--bs-black:#000;--bs-white:#fff;--bs-gray:#4e5d6c;--bs-gray-dark:#343a40;--bs-gray-100:#ebebeb;--bs-gray-200:#dee2e6;--bs-gray-300:#ced4da;--bs-gray-400:#adb5bd;--bs-gray-500:#868e96;--bs-gray-600:#4e5d6c;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#df6919;--bs-secondary:#4e5d6c;--bs-success:#5cb85c;--bs-info:#5bc0de;--bs-warning:#ffc107;--bs-danger:#d9534f;--bs-light:#abb6c2;--bs-dark:#20374c;--bs-primary-rgb:223,105,25;--bs-secondary-rgb:78,93,108;--bs-success-rgb:92,184,92;--bs-info-rgb:91,192,222;--bs-warning-rgb:255,193,7;--bs-danger-rgb:217,83,79;--bs-light-rgb:171,182,194;--bs-dark-rgb:32,55,76;--bs-primary-text-emphasis:#592a0a;--bs-secondary-text-emphasis:#1f252b;--bs-success-text-emphasis:#254a25;--bs-info-text-emphasis:#244d59;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#572120;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#f9e1d1;--bs-secondary-bg-subtle:#dcdfe2;--bs-success-bg-subtle:#def1de;--bs-info-bg-subtle:#def2f8;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f7dddc;--bs-light-bg-subtle:whitesmoke;--bs-dark-bg-subtle:#adb5bd;--bs-primary-border-subtle:#f2c3a3;--bs-secondary-border-subtle:#b8bec4;--bs-success-border-subtle:#bee3be;--bs-info-border-subtle:#bde6f2;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f0bab9;--bs-light-border-subtle:#dee2e6;--bs-dark-border-subtle:#868e96;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:Lato,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#ebebeb;--bs-body-color-rgb:235,235,235;--bs-body-bg:#0f2537;--bs-body-bg-rgb:15,37,55;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(235, 235, 235, 0.75);--bs-secondary-color-rgb:235,235,235;--bs-secondary-bg:#dee2e6;--bs-secondary-bg-rgb:222,226,230;--bs-tertiary-color:rgba(235, 235, 235, 0.5);--bs-tertiary-color-rgb:235,235,235;--bs-tertiary-bg:#ebebeb;--bs-tertiary-bg-rgb:235,235,235;--bs-heading-color:inherit;--bs-link-color:#df6919;--bs-link-color-rgb:223,105,25;--bs-link-decoration:underline;--bs-link-hover-color:#b25414;--bs-link-hover-color-rgb:178,84,20;--bs-code-color:#e83e8c;--bs-highlight-color:#ebebeb;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#ced4da;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0;--bs-border-radius-sm:0;--bs-border-radius-lg:0;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(223, 105, 25, 0.25);--bs-form-valid-color:#5cb85c;--bs-form-valid-border-color:#5cb85c;--bs-form-invalid-color:#d9534f;--bs-form-invalid-border-color:#d9534f}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#ced4da;--bs-body-color-rgb:206,212,218;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(206, 212, 218, 0.75);--bs-secondary-color-rgb:206,212,218;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(206, 212, 218, 0.5);--bs-tertiary-color-rgb:206,212,218;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#eca575;--bs-secondary-text-emphasis:#959ea7;--bs-success-text-emphasis:#9dd49d;--bs-info-text-emphasis:#9dd9eb;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#e89895;--bs-light-text-emphasis:#ebebeb;--bs-dark-text-emphasis:#ced4da;--bs-primary-bg-subtle:#2d1505;--bs-secondary-bg-subtle:#101316;--bs-success-bg-subtle:#122512;--bs-info-bg-subtle:#12262c;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2b1110;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#863f0f;--bs-secondary-border-subtle:#2f3841;--bs-success-border-subtle:#376e37;--bs-info-border-subtle:#377385;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#82322f;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#eca575;--bs-link-hover-color:#f0b791;--bs-link-color-rgb:236,165,117;--bs-link-hover-color-rgb:240,183,145;--bs-code-color:#f18bba;--bs-highlight-color:#ced4da;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#9dd49d;--bs-form-valid-border-color:#9dd49d;--bs-form-invalid-color:#e89895;--bs-form-invalid-border-color:#e89895}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em;color:inherit}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:0}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;line-height:inherit;font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-weight:300;line-height:1.2;font-size:calc(1.625rem + 4.5vw)}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-weight:300;line-height:1.2;font-size:calc(1.575rem + 3.9vw)}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-weight:300;line-height:1.2;font-size:calc(1.525rem + 3.3vw)}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-weight:300;line-height:1.2;font-size:calc(1.475rem + 2.7vw)}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-weight:300;line-height:1.2;font-size:calc(1.425rem + 2.1vw)}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-weight:300;line-height:1.2;font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#4e5d6c}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:initial;--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:rgba(0, 0, 0, 0.15);--bs-table-accent-bg:rgba(255, 255, 255, 0.05);--bs-table-striped-color:initial;--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:initial;--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:initial;--bs-table-hover-bg:rgba(255, 255, 255, 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#fff;--bs-table-bg:#df6919;--bs-table-border-color:#e58747;--bs-table-striped-bg:#e17125;--bs-table-striped-color:#fff;--bs-table-active-bg:#e27830;--bs-table-active-color:#fff;--bs-table-hover-bg:#e1742a;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#fff;--bs-table-bg:#4e5d6c;--bs-table-border-color:#717d89;--bs-table-striped-bg:#576573;--bs-table-striped-color:#fff;--bs-table-active-bg:#606d7b;--bs-table-active-color:#fff;--bs-table-hover-bg:#5b6977;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#fff;--bs-table-bg:#5cb85c;--bs-table-border-color:#7dc67d;--bs-table-striped-bg:#64bc64;--bs-table-striped-color:#fff;--bs-table-active-bg:#6cbf6c;--bs-table-active-color:#fff;--bs-table-hover-bg:#68bd68;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#fff;--bs-table-bg:#5bc0de;--bs-table-border-color:#7ccde5;--bs-table-striped-bg:#63c3e0;--bs-table-striped-color:#fff;--bs-table-active-bg:#6bc6e1;--bs-table-active-color:#fff;--bs-table-hover-bg:#67c5e0;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#fff;--bs-table-bg:#ffc107;--bs-table-border-color:#ffcd39;--bs-table-striped-bg:#ffc413;--bs-table-striped-color:#000;--bs-table-active-bg:#ffc720;--bs-table-active-color:#000;--bs-table-hover-bg:#ffc61a;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#fff;--bs-table-bg:#d9534f;--bs-table-border-color:#e17572;--bs-table-striped-bg:#db5c58;--bs-table-striped-color:#fff;--bs-table-active-bg:#dd6461;--bs-table-active-color:#fff;--bs-table-hover-bg:#dc605c;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#fff;--bs-table-bg:#abb6c2;--bs-table-border-color:#bcc5ce;--bs-table-striped-bg:#afbac5;--bs-table-striped-color:#fff;--bs-table-active-bg:#b3bdc8;--bs-table-active-color:#fff;--bs-table-hover-bg:#b1bbc7;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#20374c;--bs-table-border-color:#4d5f70;--bs-table-striped-bg:#2b4155;--bs-table-striped-color:#fff;--bs-table-active-bg:#364b5e;--bs-table-active-color:#fff;--bs-table-hover-bg:#314659;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-clip:padding-box;border:var(--bs-border-width) solid transparent;border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#efb48c;outline:0;box-shadow:0 0 0 .25rem rgba(223,105,25,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:#868e96;opacity:1}.form-control::placeholder{color:#868e96;opacity:1}.form-control:disabled{color:#4e5d6c;background-color:#ebebeb;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#ebebeb;background-color:#4e5d6c;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#ebebeb;background-color:#4e5d6c;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#43505d}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#43505d}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid transparent;border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#efb48c;outline:0;box-shadow:0 0 0 .25rem rgba(223,105,25,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{color:#4e5d6c;background-color:#ebebeb}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ced4da' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:#fff;flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:none;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#efb48c;outline:0;box-shadow:0 0 0 .25rem rgba(223,105,25,.25)}.form-check-input:checked{background-color:#df6919;border-color:#df6919}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#df6919;border-color:#df6919;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23efb48c'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #0f2537,0 0 0 .25rem rgba(223,105,25,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #0f2537,0 0 0 .25rem rgba(223,105,25,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#df6919;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#f5d2ba}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#df6919;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#f5d2ba}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;max-width:100%;height:100%;padding:1rem .75rem;overflow:hidden;color:rgba(var(--bs-body-color-rgb),1);text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem;padding-left:.75rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>textarea:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:#fff;border-radius:var(--bs-border-radius)}.form-floating>textarea:focus~label::after,.form-floating>textarea:not(:placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:#fff;border-radius:var(--bs-border-radius)}.form-floating>textarea:disabled~label::after{background-color:#ebebeb}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#4e5d6c}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#ebebeb;text-align:center;white-space:nowrap;background-color:#4e5d6c;border:var(--bs-border-width) solid transparent;border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(-1 * var(--bs-border-width));border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%235cb85c' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%235cb85c' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23d9534f'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23d9534f' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23d9534f'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23d9534f' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#df6919;--bs-btn-border-color:#df6919;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#be5915;--bs-btn-hover-border-color:#b25414;--bs-btn-focus-shadow-rgb:228,128,60;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b25414;--bs-btn-active-border-color:#a74f13;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#df6919;--bs-btn-disabled-border-color:#df6919}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#4e5d6c;--bs-btn-border-color:#4e5d6c;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424f5c;--bs-btn-hover-border-color:#3e4a56;--bs-btn-focus-shadow-rgb:105,117,130;--bs-btn-active-color:#fff;--bs-btn-active-bg:#3e4a56;--bs-btn-active-border-color:#3b4651;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#4e5d6c;--bs-btn-disabled-border-color:#4e5d6c}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#5cb85c;--bs-btn-border-color:#5cb85c;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#4e9c4e;--bs-btn-hover-border-color:#4a934a;--bs-btn-focus-shadow-rgb:116,195,116;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4a934a;--bs-btn-active-border-color:#458a45;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#5cb85c;--bs-btn-disabled-border-color:#5cb85c}.btn-info{--bs-btn-color:#fff;--bs-btn-bg:#5bc0de;--bs-btn-border-color:#5bc0de;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#4da3bd;--bs-btn-hover-border-color:#499ab2;--bs-btn-focus-shadow-rgb:116,201,227;--bs-btn-active-color:#fff;--bs-btn-active-bg:#499ab2;--bs-btn-active-border-color:#4490a7;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#5bc0de;--bs-btn-disabled-border-color:#5bc0de}.btn-warning{--bs-btn-color:#fff;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#d9a406;--bs-btn-hover-border-color:#cc9a06;--bs-btn-focus-shadow-rgb:255,202,44;--bs-btn-active-color:#fff;--bs-btn-active-bg:#cc9a06;--bs-btn-active-border-color:#bf9105;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#d9534f;--bs-btn-border-color:#d9534f;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#b84743;--bs-btn-hover-border-color:#ae423f;--bs-btn-focus-shadow-rgb:223,109,105;--bs-btn-active-color:#fff;--bs-btn-active-bg:#ae423f;--bs-btn-active-border-color:#a33e3b;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#d9534f;--bs-btn-disabled-border-color:#d9534f}.btn-light{--bs-btn-color:#fff;--bs-btn-bg:#abb6c2;--bs-btn-border-color:#abb6c2;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#919ba5;--bs-btn-hover-border-color:#89929b;--bs-btn-focus-shadow-rgb:184,193,203;--bs-btn-active-color:#fff;--bs-btn-active-bg:#89929b;--bs-btn-active-border-color:#808992;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#abb6c2;--bs-btn-disabled-border-color:#abb6c2}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#20374c;--bs-btn-border-color:#20374c;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#415567;--bs-btn-hover-border-color:#364b5e;--bs-btn-focus-shadow-rgb:65,85,103;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5f70;--bs-btn-active-border-color:#364b5e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#20374c;--bs-btn-disabled-border-color:#20374c}.btn-outline-primary{--bs-btn-color:#df6919;--bs-btn-border-color:#df6919;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#df6919;--bs-btn-hover-border-color:#df6919;--bs-btn-focus-shadow-rgb:223,105,25;--bs-btn-active-color:#fff;--bs-btn-active-bg:#df6919;--bs-btn-active-border-color:#df6919;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#df6919;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#df6919;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#4e5d6c;--bs-btn-border-color:#4e5d6c;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#4e5d6c;--bs-btn-hover-border-color:#4e5d6c;--bs-btn-focus-shadow-rgb:78,93,108;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4e5d6c;--bs-btn-active-border-color:#4e5d6c;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#4e5d6c;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#4e5d6c;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#5cb85c;--bs-btn-border-color:#5cb85c;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5cb85c;--bs-btn-hover-border-color:#5cb85c;--bs-btn-focus-shadow-rgb:92,184,92;--bs-btn-active-color:#fff;--bs-btn-active-bg:#5cb85c;--bs-btn-active-border-color:#5cb85c;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#5cb85c;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#5cb85c;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#5bc0de;--bs-btn-border-color:#5bc0de;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5bc0de;--bs-btn-hover-border-color:#5bc0de;--bs-btn-focus-shadow-rgb:91,192,222;--bs-btn-active-color:#fff;--bs-btn-active-bg:#5bc0de;--bs-btn-active-border-color:#5bc0de;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#5bc0de;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#5bc0de;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#fff;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#d9534f;--bs-btn-border-color:#d9534f;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#d9534f;--bs-btn-hover-border-color:#d9534f;--bs-btn-focus-shadow-rgb:217,83,79;--bs-btn-active-color:#fff;--bs-btn-active-bg:#d9534f;--bs-btn-active-border-color:#d9534f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#d9534f;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#d9534f;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#abb6c2;--bs-btn-border-color:#abb6c2;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#abb6c2;--bs-btn-hover-border-color:#abb6c2;--bs-btn-focus-shadow-rgb:171,182,194;--bs-btn-active-color:#fff;--bs-btn-active-bg:#abb6c2;--bs-btn-active-border-color:#abb6c2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#abb6c2;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#abb6c2;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#20374c;--bs-btn-border-color:#20374c;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#20374c;--bs-btn-hover-border-color:#20374c;--bs-btn-focus-shadow-rgb:32,55,76;--bs-btn-active-color:#fff;--bs-btn-active-bg:#20374c;--bs-btn-active-border-color:#20374c;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#20374c;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#20374c;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#4e5d6c;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:228,128,60;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:#4e5d6c;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:rgba(0, 0, 0, 0.15);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:#ebebeb;--bs-dropdown-link-hover-color:#ebebeb;--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.075);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#df6919;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#868e96;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#ced4da;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#ced4da;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:rgba(0, 0, 0, 0.15);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#df6919;--bs-dropdown-link-disabled-color:#868e96;--bs-dropdown-header-color:#868e96}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(-1 * var(--bs-border-width))}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(-1 * var(--bs-border-width))}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:rgba(255, 255, 255, 0.4);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(223,105,25,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:#4e5d6c;--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:#4e5d6c;--bs-nav-tabs-link-active-color:#ebebeb;--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:#4e5d6c;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#df6919}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:#fff;gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-grow:1;flex-basis:0;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28235, 235, 235, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-grow:1;flex-basis:100%;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.75);--bs-navbar-hover-color:#fff;--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:0;--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(255, 255, 255, 0.075);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:#4e5d6c;--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child)>.card-header,.card-group>.card:not(:last-child)>.card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child)>.card-footer,.card-group>.card:not(:last-child)>.card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child)>.card-header,.card-group>.card:not(:first-child)>.card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child)>.card-footer,.card-group>.card:not(:first-child)>.card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:#4e5d6c;--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:0;--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - 0);--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:rgba(255, 255, 255, 0.075);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23ebebeb' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23592a0a' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(223, 105, 25, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:#ebebeb;--bs-accordion-active-bg:#df6919}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-collapse,.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23eca575'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23eca575'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0.75rem;--bs-breadcrumb-padding-y:0.375rem;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg:#4e5d6c;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:#ebebeb;--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:#ebebeb;display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:#fff;--bs-pagination-bg:#4e5d6c;--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:transparent;--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:#fff;--bs-pagination-hover-bg:rgba(255, 255, 255, 0.4);--bs-pagination-hover-border-color:transparent;--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(223, 105, 25, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#df6919;--bs-pagination-active-border-color:#df6919;--bs-pagination-disabled-color:rgba(255, 255, 255, 0.4);--bs-pagination-disabled-bg:#4e5d6c;--bs-pagination-disabled-border-color:transparent;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(-1 * var(--bs-border-width))}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:var(--bs-progress-height)}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:#4e5d6c;--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#df6919;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:#fff;--bs-list-group-bg:#4e5d6c;--bs-list-group-border-color:transparent;--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:#fff;--bs-list-group-action-hover-color:#fff;--bs-list-group-action-hover-bg:rgba(255, 255, 255, 0.4);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:rgba(255, 255, 255, 0.4);--bs-list-group-disabled-bg:#4e5d6c;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#df6919;--bs-list-group-active-border-color:#df6919;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:not(.active):focus,.list-group-item-action:not(.active):hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:not(.active):active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#fff;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:1;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(223, 105, 25, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;filter:var(--bs-btn-close-filter);border:0;border-radius:0;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}:root,[data-bs-theme=light]{--bs-btn-close-filter: }[data-bs-theme=dark]{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:#4e5d6c;--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:rgba(0, 0, 0, 0.2);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:#ebebeb;--bs-toast-header-bg:#4e5d6c;--bs-toast-header-border-color:rgba(0, 0, 0, 0.2);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color:var(--bs-body-color);--bs-modal-bg:#4e5d6c;--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:rgba(0, 0, 0, 0.2);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:rgba(0, 0, 0, 0.2);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0,-50px);transition:transform .3s ease-out}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin-top:calc(-.5 * var(--bs-modal-header-padding-y));margin-right:calc(-.5 * var(--bs-modal-header-padding-x));margin-bottom:calc(-.5 * var(--bs-modal-header-padding-y));margin-left:auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:#4e5d6c;--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:rgba(255, 255, 255, 0.075);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;filter:var(--bs-carousel-control-icon-filter);border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:var(--bs-carousel-indicator-active-bg);background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:var(--bs-carousel-caption-color);text-align:center}.carousel-dark{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}:root,[data-bs-theme=light]{--bs-carousel-indicator-active-bg:#fff;--bs-carousel-caption-color:#fff;--bs-carousel-control-icon-filter: }[data-bs-theme=dark]{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y));margin-left:auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#fff!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#fff!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#fff!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(178,84,20,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(178,84,20,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(178,84,20,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(62,74,86,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(62,74,86,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(62,74,86,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(74,147,74,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(74,147,74,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(74,147,74,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(73,154,178,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(73,154,178,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(73,154,178,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(204,154,6,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(204,154,6,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(204,154,6,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(174,66,63,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(174,66,63,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(174,66,63,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(137,146,155,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(137,146,155,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(137,146,155,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,44,61,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,44,61,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,44,61,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.visually-hidden *,.visually-hidden-focusable:not(:focus):not(:focus-within) *{overflow:hidden!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}.btn-primary{background-color:#df6919}.btn-secondary{background-color:#4e5d6c}.btn-success{background-color:#5cb85c}.btn-info{background-color:#5bc0de}.btn-warning{background-color:#ffc107}.btn-danger{background-color:#d9534f}.btn-light{background-color:#abb6c2}.btn-dark{background-color:#20374c}.text-secondary{color:var(--bs-secondary-color)!important}.dropdown-menu{font-size:.875rem}.dropdown-header{font-size:.875rem}.blockquote-footer{color:#ebebeb}.table{font-size:.875rem}.table .thead-dark th{color:#fff}.table a:not(.btn){color:#fff;text-decoration:underline}.table .dropdown-menu a{text-decoration:none}.table .text-muted{color:rgba(255,255,255,.4)}.checkbox label,.help-block,.radio label,label{font-size:.875rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:#868e96}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>label{color:#868e96}.nav-pills .nav-link,.nav-pills .nav-link:hover,.nav-tabs .nav-link,.nav-tabs .nav-link:hover{color:#ebebeb}.nav-pills .nav-link.disabled,.nav-tabs .nav-link.disabled{color:rgba(255,255,255,.4)}.page-link:focus,.page-link:hover{color:#fff;text-decoration:none}.alert{color:#fff;border:none}.alert .alert-link,.alert a{color:#fff;text-decoration:underline}.alert-primary{background-color:#df6919}.alert-secondary{background-color:#4e5d6c}.alert-success{background-color:#5cb85c}.alert-info{background-color:#5bc0de}.alert-warning{background-color:#ffc107}.alert-danger{background-color:#d9534f}.alert-light{background-color:#abb6c2}.alert-dark{background-color:#20374c}.badge-info,.badge-warning{color:#fff}.tooltip{--bs-tooltip-bg:var(--bs-tertiary-bg);--bs-tooltip-color:var(--bs-emphasis-color)}.popover-header{border-top-left-radius:0;border-top-right-radius:0}.modal-footer,.modal-header{background-color:rgba(255,255,255,.075)} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/modules/ui-module/src/main/resources/manager/bootstrap/bootstrap.bundle.min.js b/modules/ui-module/src/main/resources/manager/bootstrap/bootstrap.bundle.min.js new file mode 100644 index 000000000..3d91751d7 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/bootstrap/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.3.7 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function M(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${M(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${M(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${M(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.7"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>n(t))).join(","):null},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="ArrowLeft",lt="ArrowRight",ct="next",ht="prev",dt="left",ut="right",ft=`slide${ot}`,pt=`slid${ot}`,mt=`keydown${ot}`,gt=`mouseenter${ot}`,_t=`mouseleave${ot}`,bt=`dragstart${ot}`,vt=`load${ot}${rt}`,yt=`click${ot}${rt}`,wt="carousel",At="active",Et=".active",Tt=".carousel-item",Ct=Et+Tt,Ot={[at]:ut,[lt]:dt},xt={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},kt={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class Lt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===wt&&this.cycle()}static get Default(){return xt}static get DefaultType(){return kt}static get NAME(){return"carousel"}next(){this._slide(ct)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(ht)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,pt,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,pt,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?ct:ht;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,mt,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,gt,(()=>this.pause())),N.on(this._element,_t,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,bt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(dt)),rightCallback:()=>this._slide(this._directionToOrder(ut)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Ot[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(Et,this._indicatorsElement);e.classList.remove(At),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(At),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===ct,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(ft).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(At),i.classList.remove(At,c,l),this._isSliding=!1,r(pt)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Ct,this._element)}_getItems(){return z.find(Tt,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===dt?ht:ct:t===dt?ct:ht}_orderToDirection(t){return p()?t===ht?dt:ut:t===ht?ut:dt}static jQueryInterface(t){return this.each((function(){const e=Lt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,yt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(wt))return;t.preventDefault();const i=Lt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,vt,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)Lt.getOrCreateInstance(e)})),m(Lt);const St=".bs.collapse",Dt=`show${St}`,$t=`shown${St}`,It=`hide${St}`,Nt=`hidden${St}`,Pt=`click${St}.data-api`,jt="show",Mt="collapse",Ft="collapsing",Ht=`:scope .${Mt} .${Mt}`,Wt='[data-bs-toggle="collapse"]',Bt={parent:null,toggle:!0},zt={parent:"(null|element)",toggle:"boolean"};class Rt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Wt);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Bt}static get DefaultType(){return zt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Rt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Dt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Mt),this._element.classList.add(Ft),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Ft),this._element.classList.add(Mt,jt),this._element.style[e]="",N.trigger(this._element,$t)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,It).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(Ft),this._element.classList.remove(Mt,jt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Ft),this._element.classList.add(Mt),N.trigger(this._element,Nt)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(jt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Wt);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(Ht,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Rt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,Pt,Wt,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Rt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Rt);var qt="top",Vt="bottom",Kt="right",Qt="left",Xt="auto",Yt=[qt,Vt,Kt,Qt],Ut="start",Gt="end",Jt="clippingParents",Zt="viewport",te="popper",ee="reference",ie=Yt.reduce((function(t,e){return t.concat([e+"-"+Ut,e+"-"+Gt])}),[]),ne=[].concat(Yt,[Xt]).reduce((function(t,e){return t.concat([e,e+"-"+Ut,e+"-"+Gt])}),[]),se="beforeRead",oe="read",re="afterRead",ae="beforeMain",le="main",ce="afterMain",he="beforeWrite",de="write",ue="afterWrite",fe=[se,oe,re,ae,le,ce,he,de,ue];function pe(t){return t?(t.nodeName||"").toLowerCase():null}function me(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function ge(t){return t instanceof me(t).Element||t instanceof Element}function _e(t){return t instanceof me(t).HTMLElement||t instanceof HTMLElement}function be(t){return"undefined"!=typeof ShadowRoot&&(t instanceof me(t).ShadowRoot||t instanceof ShadowRoot)}const ve={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];_e(s)&&pe(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});_e(n)&&pe(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function ye(t){return t.split("-")[0]}var we=Math.max,Ae=Math.min,Ee=Math.round;function Te(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ce(){return!/^((?!chrome|android).)*safari/i.test(Te())}function Oe(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&_e(t)&&(s=t.offsetWidth>0&&Ee(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&Ee(n.height)/t.offsetHeight||1);var r=(ge(t)?me(t):window).visualViewport,a=!Ce()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function xe(t){var e=Oe(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function ke(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&be(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Le(t){return me(t).getComputedStyle(t)}function Se(t){return["table","td","th"].indexOf(pe(t))>=0}function De(t){return((ge(t)?t.ownerDocument:t.document)||window.document).documentElement}function $e(t){return"html"===pe(t)?t:t.assignedSlot||t.parentNode||(be(t)?t.host:null)||De(t)}function Ie(t){return _e(t)&&"fixed"!==Le(t).position?t.offsetParent:null}function Ne(t){for(var e=me(t),i=Ie(t);i&&Se(i)&&"static"===Le(i).position;)i=Ie(i);return i&&("html"===pe(i)||"body"===pe(i)&&"static"===Le(i).position)?e:i||function(t){var e=/firefox/i.test(Te());if(/Trident/i.test(Te())&&_e(t)&&"fixed"===Le(t).position)return null;var i=$e(t);for(be(i)&&(i=i.host);_e(i)&&["html","body"].indexOf(pe(i))<0;){var n=Le(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Pe(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function je(t,e,i){return we(t,Ae(e,i))}function Me(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Fe(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const He={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=ye(i.placement),l=Pe(a),c=[Qt,Kt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Me("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Fe(t,Yt))}(s.padding,i),d=xe(o),u="y"===l?qt:Qt,f="y"===l?Vt:Kt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=Ne(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=je(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&ke(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function We(t){return t.split("-")[1]}var Be={top:"auto",right:"auto",bottom:"auto",left:"auto"};function ze(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Qt,y=qt,w=window;if(c){var A=Ne(i),E="clientHeight",T="clientWidth";A===me(i)&&"static"!==Le(A=De(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===qt||(s===Qt||s===Kt)&&o===Gt)&&(y=Vt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Qt&&(s!==qt&&s!==Vt||o!==Gt)||(v=Kt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&Be),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:Ee(i*s)/s||0,y:Ee(n*s)/s||0}}({x:f,y:m},me(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Re={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:ye(e.placement),variation:We(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,ze(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,ze(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var qe={passive:!0};const Ve={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=me(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,qe)})),a&&l.addEventListener("resize",i.update,qe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,qe)})),a&&l.removeEventListener("resize",i.update,qe)}},data:{}};var Ke={left:"right",right:"left",bottom:"top",top:"bottom"};function Qe(t){return t.replace(/left|right|bottom|top/g,(function(t){return Ke[t]}))}var Xe={start:"end",end:"start"};function Ye(t){return t.replace(/start|end/g,(function(t){return Xe[t]}))}function Ue(t){var e=me(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ge(t){return Oe(De(t)).left+Ue(t).scrollLeft}function Je(t){var e=Le(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ze(t){return["html","body","#document"].indexOf(pe(t))>=0?t.ownerDocument.body:_e(t)&&Je(t)?t:Ze($e(t))}function ti(t,e){var i;void 0===e&&(e=[]);var n=Ze(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=me(n),r=s?[o].concat(o.visualViewport||[],Je(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ti($e(r)))}function ei(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ii(t,e,i){return e===Zt?ei(function(t,e){var i=me(t),n=De(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ce();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ge(t),y:l}}(t,i)):ge(e)?function(t,e){var i=Oe(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):ei(function(t){var e,i=De(t),n=Ue(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=we(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=we(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ge(t),l=-n.scrollTop;return"rtl"===Le(s||i).direction&&(a+=we(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(De(t)))}function ni(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?ye(s):null,r=s?We(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case qt:e={x:a,y:i.y-n.height};break;case Vt:e={x:a,y:i.y+i.height};break;case Kt:e={x:i.x+i.width,y:l};break;case Qt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Pe(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Ut:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Gt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function si(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Jt:a,c=i.rootBoundary,h=void 0===c?Zt:c,d=i.elementContext,u=void 0===d?te:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Me("number"!=typeof g?g:Fe(g,Yt)),b=u===te?ee:te,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ti($e(t)),i=["absolute","fixed"].indexOf(Le(t).position)>=0&&_e(t)?Ne(t):t;return ge(i)?e.filter((function(t){return ge(t)&&ke(t,i)&&"body"!==pe(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ii(t,i,n);return e.top=we(s.top,e.top),e.right=Ae(s.right,e.right),e.bottom=Ae(s.bottom,e.bottom),e.left=we(s.left,e.left),e}),ii(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(ge(y)?y:y.contextElement||De(t.elements.popper),l,h,r),A=Oe(t.elements.reference),E=ni({reference:A,element:v,placement:s}),T=ei(Object.assign({},v,E)),C=u===te?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===te&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[Kt,Vt].indexOf(t)>=0?1:-1,i=[qt,Vt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function oi(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ne:l,h=We(n),d=h?a?ie:ie.filter((function(t){return We(t)===h})):Yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=si(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[ye(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const ri={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=ye(g),b=l||(_!==g&&p?function(t){if(ye(t)===Xt)return[];var e=Qe(t);return[Ye(t),e,Ye(e)]}(g):[Qe(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(ye(i)===Xt?oi(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=si(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?Kt:Qt:k?Vt:qt;y[S]>w[S]&&($=Qe($));var I=Qe($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function ai(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function li(t){return[qt,Kt,Vt,Qt].some((function(e){return t[e]>=0}))}const ci={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=si(e,{elementContext:"reference"}),a=si(e,{altBoundary:!0}),l=ai(r,n),c=ai(a,s,o),h=li(l),d=li(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},hi={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ne.reduce((function(t,i){return t[i]=function(t,e,i){var n=ye(t),s=[Qt,qt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Qt,Kt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},di={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ni({reference:e.rects.reference,element:e.rects.popper,placement:e.placement})},data:{}},ui={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=si(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=ye(e.placement),b=We(e.placement),v=!b,y=Pe(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?qt:Qt,D="y"===y?Vt:Kt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Ut?E[$]:T[$],F=b===Ut?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?xe(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=je(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&Ne(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=je(f?Ae(N,I+V-Y-X):N,I,f?we(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?qt:Qt,tt="x"===y?Vt:Kt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[qt,Qt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=je(t,e,i);return n>i?i:n}(at,et,lt):je(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function fi(t,e,i){void 0===i&&(i=!1);var n,s,o=_e(e),r=_e(e)&&function(t){var e=t.getBoundingClientRect(),i=Ee(e.width)/t.offsetWidth||1,n=Ee(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=De(e),l=Oe(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==pe(e)||Je(a))&&(c=(n=e)!==me(n)&&_e(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Ue(n)),_e(e)?((h=Oe(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ge(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function pi(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var mi={placement:"bottom",modifiers:[],strategy:"absolute"};function gi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[void 0,t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Oi,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=Ki.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(ji);for(const i of e){const e=Ki.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ci,Oi].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Pi)?this:z.prev(this,Pi)[0]||z.next(this,Pi)[0]||z.findOne(Pi,t.delegateTarget.parentNode),o=Ki.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,$i,Pi,Ki.dataApiKeydownHandler),N.on(document,$i,Mi,Ki.dataApiKeydownHandler),N.on(document,Di,Ki.clearMenus),N.on(document,Ii,Ki.clearMenus),N.on(document,Di,Pi,(function(t){t.preventDefault(),Ki.getOrCreateInstance(this).toggle()})),m(Ki);const Qi="backdrop",Xi="show",Yi=`mousedown.bs.${Qi}`,Ui={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Gi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ji extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Ui}static get DefaultType(){return Gi}static get NAME(){return Qi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Xi),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Xi),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Yi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Yi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Zi=".bs.focustrap",tn=`focusin${Zi}`,en=`keydown.tab${Zi}`,nn="backward",sn={autofocus:!0,trapElement:null},on={autofocus:"boolean",trapElement:"element"};class rn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return sn}static get DefaultType(){return on}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Zi),N.on(document,tn,(t=>this._handleFocusin(t))),N.on(document,en,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Zi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===nn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?nn:"forward")}}const an=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",ln=".sticky-top",cn="padding-right",hn="margin-right";class dn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,cn,(e=>e+t)),this._setElementAttributes(an,cn,(e=>e+t)),this._setElementAttributes(ln,hn,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,cn),this._resetElementAttributes(an,cn),this._resetElementAttributes(ln,hn)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const un=".bs.modal",fn=`hide${un}`,pn=`hidePrevented${un}`,mn=`hidden${un}`,gn=`show${un}`,_n=`shown${un}`,bn=`resize${un}`,vn=`click.dismiss${un}`,yn=`mousedown.dismiss${un}`,wn=`keydown.dismiss${un}`,An=`click${un}.data-api`,En="modal-open",Tn="show",Cn="modal-static",On={backdrop:!0,focus:!0,keyboard:!0},xn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class kn extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new dn,this._addEventListeners()}static get Default(){return On}static get DefaultType(){return xn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,gn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(En),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,fn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Tn),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,un),N.off(this._dialog,un),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ji({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new rn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(Tn),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,_n,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,wn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,bn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,yn,(t=>{N.one(this._element,vn,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(En),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,mn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,pn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Cn)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Cn),this._queueCallback((()=>{this._element.classList.remove(Cn),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=kn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,An,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,gn,(t=>{t.defaultPrevented||N.one(e,mn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&kn.getInstance(i).hide(),kn.getOrCreateInstance(e).toggle(this)})),R(kn),m(kn);const Ln=".bs.offcanvas",Sn=".data-api",Dn=`load${Ln}${Sn}`,$n="show",In="showing",Nn="hiding",Pn=".offcanvas.show",jn=`show${Ln}`,Mn=`shown${Ln}`,Fn=`hide${Ln}`,Hn=`hidePrevented${Ln}`,Wn=`hidden${Ln}`,Bn=`resize${Ln}`,zn=`click${Ln}${Sn}`,Rn=`keydown.dismiss${Ln}`,qn={backdrop:!0,keyboard:!0,scroll:!1},Vn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Kn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return qn}static get DefaultType(){return Vn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,jn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new dn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(In),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add($n),this._element.classList.remove(In),N.trigger(this._element,Mn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,Fn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Nn),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove($n,Nn),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new dn).reset(),N.trigger(this._element,Wn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ji({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,Hn)}:null})}_initializeFocusTrap(){return new rn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Rn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,Hn))}))}static jQueryInterface(t){return this.each((function(){const e=Kn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,zn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Wn,(()=>{a(this)&&this.focus()}));const i=z.findOne(Pn);i&&i!==e&&Kn.getInstance(i).hide(),Kn.getOrCreateInstance(e).toggle(this)})),N.on(window,Dn,(()=>{for(const t of z.find(Pn))Kn.getOrCreateInstance(t).show()})),N.on(window,Bn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Kn.getOrCreateInstance(t).hide()})),R(Kn),m(Kn);const Qn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Xn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Yn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Un=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Xn.has(i)||Boolean(Yn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Gn={allowList:Qn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
    "},Jn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Zn={entry:"(string|element|function|null)",selector:"(string|element)"};class ts extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Gn}static get DefaultType(){return Jn}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Zn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Un(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[void 0,this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const es=new Set(["sanitize","allowList","sanitizeFn"]),is="fade",ns="show",ss=".tooltip-inner",os=".modal",rs="hide.bs.modal",as="hover",ls="focus",cs="click",hs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},ds={allowList:Qn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},us={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class fs extends W{constructor(t,e){if(void 0===wi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return ds}static get DefaultType(){return us}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(os),rs,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(ns),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(ns),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger[cs]=!1,this._activeTrigger[ls]=!1,this._activeTrigger[as]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(is,ns),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(is),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new ts({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[ss]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(is)}_isShown(){return this.tip&&this.tip.classList.contains(ns)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=hs[e.toUpperCase()];return yi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element,this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[void 0,e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger[cs]=!(e._isShown()&&e._activeTrigger[cs]),e.toggle()}));else if("manual"!==e){const t=e===as?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===as?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?ls:as]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?ls:as]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(os),rs,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))es.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=fs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(fs);const ps=".popover-header",ms=".popover-body",gs={...fs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},_s={...fs.DefaultType,content:"(null|string|element|function)"};class bs extends fs{static get Default(){return gs}static get DefaultType(){return _s}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ps]:this._getTitle(),[ms]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=bs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(bs);const vs=".bs.scrollspy",ys=`activate${vs}`,ws=`click${vs}`,As=`load${vs}.data-api`,Es="active",Ts="[href]",Cs=".nav-link",Os=`${Cs}, .nav-item > ${Cs}, .list-group-item`,xs={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},ks={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ls extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return xs}static get DefaultType(){return ks}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ws),N.on(this._config.target,ws,Ts,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(Ts,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Es),this._activateParents(t),N.trigger(this._element,ys,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Es);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,Os))t.classList.add(Es)}_clearActiveClass(t){t.classList.remove(Es);const e=z.find(`${Ts}.${Es}`,t);for(const t of e)t.classList.remove(Es)}static jQueryInterface(t){return this.each((function(){const e=Ls.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,As,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Ls.getOrCreateInstance(t)})),m(Ls);const Ss=".bs.tab",Ds=`hide${Ss}`,$s=`hidden${Ss}`,Is=`show${Ss}`,Ns=`shown${Ss}`,Ps=`click${Ss}`,js=`keydown${Ss}`,Ms=`load${Ss}`,Fs="ArrowLeft",Hs="ArrowRight",Ws="ArrowUp",Bs="ArrowDown",zs="Home",Rs="End",qs="active",Vs="fade",Ks="show",Qs=".dropdown-toggle",Xs=`:not(${Qs})`,Ys='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Us=`.nav-link${Xs}, .list-group-item${Xs}, [role="tab"]${Xs}, ${Ys}`,Gs=`.${qs}[data-bs-toggle="tab"], .${qs}[data-bs-toggle="pill"], .${qs}[data-bs-toggle="list"]`;class Js extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,js,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Ds,{relatedTarget:t}):null;N.trigger(t,Is,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(qs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,Ns,{relatedTarget:e})):t.classList.add(Ks)}),t,t.classList.contains(Vs)))}_deactivate(t,e){t&&(t.classList.remove(qs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,$s,{relatedTarget:e})):t.classList.remove(Ks)}),t,t.classList.contains(Vs)))}_keydown(t){if(![Fs,Hs,Ws,Bs,zs,Rs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([zs,Rs].includes(t.key))i=e[t.key===zs?0:e.length-1];else{const n=[Hs,Bs].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Js.getOrCreateInstance(i).show())}_getChildren(){return z.find(Us,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Qs,qs),n(".dropdown-menu",Ks),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(qs)}_getInnerElement(t){return t.matches(Us)?t:z.findOne(Us,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Js.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ps,Ys,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Js.getOrCreateInstance(this).show()})),N.on(window,Ms,(()=>{for(const t of z.find(Gs))Js.getOrCreateInstance(t)})),m(Js);const Zs=".bs.toast",to=`mouseover${Zs}`,eo=`mouseout${Zs}`,io=`focusin${Zs}`,no=`focusout${Zs}`,so=`hide${Zs}`,oo=`hidden${Zs}`,ro=`show${Zs}`,ao=`shown${Zs}`,lo="hide",co="show",ho="showing",uo={animation:"boolean",autohide:"boolean",delay:"number"},fo={animation:!0,autohide:!0,delay:5e3};class po extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return fo}static get DefaultType(){return uo}static get NAME(){return"toast"}show(){N.trigger(this._element,ro).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(lo),d(this._element),this._element.classList.add(co,ho),this._queueCallback((()=>{this._element.classList.remove(ho),N.trigger(this._element,ao),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,so).defaultPrevented||(this._element.classList.add(ho),this._queueCallback((()=>{this._element.classList.add(lo),this._element.classList.remove(ho,co),N.trigger(this._element,oo)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(co),super.dispose()}isShown(){return this._element.classList.contains(co)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,to,(t=>this._onInteraction(t,!0))),N.on(this._element,eo,(t=>this._onInteraction(t,!1))),N.on(this._element,io,(t=>this._onInteraction(t,!0))),N.on(this._element,no,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=po.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(po),m(po),{Alert:Q,Button:Y,Carousel:Lt,Collapse:Rt,Dropdown:Ki,Modal:kn,Offcanvas:Kn,Popover:bs,ScrollSpy:Ls,Tab:Js,Toast:po,Tooltip:fs}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/modules/ui-module/src/main/resources/manager/bootstrap/bootstrap.bundle.min.js.map b/modules/ui-module/src/main/resources/manager/bootstrap/bootstrap.bundle.min.js.map new file mode 100644 index 000000000..27957ead0 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/bootstrap/bootstrap.bundle.min.js.map @@ -0,0 +1 @@ +{"version":3,"names":["elementMap","Map","Data","set","element","key","instance","has","instanceMap","get","size","console","error","Array","from","keys","remove","delete","TRANSITION_END","parseSelector","selector","window","CSS","escape","replace","match","id","triggerTransitionEnd","dispatchEvent","Event","isElement","object","jquery","nodeType","getElement","length","document","querySelector","isVisible","getClientRects","elementIsVisible","getComputedStyle","getPropertyValue","closedDetails","closest","summary","parentNode","isDisabled","Node","ELEMENT_NODE","classList","contains","disabled","hasAttribute","getAttribute","findShadowRoot","documentElement","attachShadow","getRootNode","root","ShadowRoot","noop","reflow","offsetHeight","getjQuery","jQuery","body","DOMContentLoadedCallbacks","isRTL","dir","defineJQueryPlugin","plugin","callback","$","name","NAME","JQUERY_NO_CONFLICT","fn","jQueryInterface","Constructor","noConflict","readyState","addEventListener","push","execute","possibleCallback","args","defaultValue","call","executeAfterTransition","transitionElement","waitForTransition","emulatedDuration","transitionDuration","transitionDelay","floatTransitionDuration","Number","parseFloat","floatTransitionDelay","split","getTransitionDurationFromElement","called","handler","target","removeEventListener","setTimeout","getNextActiveElement","list","activeElement","shouldGetNext","isCycleAllowed","listLength","index","indexOf","Math","max","min","namespaceRegex","stripNameRegex","stripUidRegex","eventRegistry","uidEvent","customEvents","mouseenter","mouseleave","nativeEvents","Set","makeEventUid","uid","getElementEvents","findHandler","events","callable","delegationSelector","Object","values","find","event","normalizeParameters","originalTypeEvent","delegationFunction","isDelegated","typeEvent","getTypeEvent","addHandler","oneOff","wrapFunction","relatedTarget","delegateTarget","this","handlers","previousFunction","domElements","querySelectorAll","domElement","hydrateObj","EventHandler","off","type","apply","bootstrapDelegationHandler","bootstrapHandler","removeHandler","Boolean","removeNamespacedHandlers","namespace","storeElementEvent","handlerKey","entries","includes","on","one","inNamespace","isNamespace","startsWith","elementEvent","slice","keyHandlers","trigger","jQueryEvent","bubbles","nativeDispatch","defaultPrevented","isPropagationStopped","isImmediatePropagationStopped","isDefaultPrevented","evt","cancelable","preventDefault","obj","meta","value","_unused","defineProperty","configurable","normalizeData","toString","JSON","parse","decodeURIComponent","normalizeDataKey","chr","toLowerCase","Manipulator","setDataAttribute","setAttribute","removeDataAttribute","removeAttribute","getDataAttributes","attributes","bsKeys","dataset","filter","pureKey","charAt","getDataAttribute","Config","Default","DefaultType","Error","_getConfig","config","_mergeConfigObj","_configAfterMerge","_typeCheckConfig","jsonConfig","constructor","configTypes","property","expectedTypes","valueType","prototype","RegExp","test","TypeError","toUpperCase","BaseComponent","super","_element","_config","DATA_KEY","dispose","EVENT_KEY","propertyName","getOwnPropertyNames","_queueCallback","isAnimated","getInstance","getOrCreateInstance","VERSION","eventName","getSelector","hrefAttribute","trim","map","sel","join","SelectorEngine","concat","Element","findOne","children","child","matches","parents","ancestor","prev","previous","previousElementSibling","next","nextElementSibling","focusableChildren","focusables","el","getSelectorFromElement","getElementFromSelector","getMultipleElementsFromSelector","enableDismissTrigger","component","method","clickEvent","tagName","EVENT_CLOSE","EVENT_CLOSED","Alert","close","_destroyElement","each","data","undefined","SELECTOR_DATA_TOGGLE","Button","toggle","button","EVENT_TOUCHSTART","EVENT_TOUCHMOVE","EVENT_TOUCHEND","EVENT_POINTERDOWN","EVENT_POINTERUP","endCallback","leftCallback","rightCallback","Swipe","isSupported","_deltaX","_supportPointerEvents","PointerEvent","_initEvents","_start","_eventIsPointerPenTouch","clientX","touches","_end","_handleSwipe","_move","absDeltaX","abs","direction","add","pointerType","navigator","maxTouchPoints","DATA_API_KEY","ARROW_LEFT_KEY","ARROW_RIGHT_KEY","ORDER_NEXT","ORDER_PREV","DIRECTION_LEFT","DIRECTION_RIGHT","EVENT_SLIDE","EVENT_SLID","EVENT_KEYDOWN","EVENT_MOUSEENTER","EVENT_MOUSELEAVE","EVENT_DRAG_START","EVENT_LOAD_DATA_API","EVENT_CLICK_DATA_API","CLASS_NAME_CAROUSEL","CLASS_NAME_ACTIVE","SELECTOR_ACTIVE","SELECTOR_ITEM","SELECTOR_ACTIVE_ITEM","KEY_TO_DIRECTION","ARROW_LEFT_KEY$1","ARROW_RIGHT_KEY$1","interval","keyboard","pause","ride","touch","wrap","Carousel","_interval","_activeElement","_isSliding","touchTimeout","_swipeHelper","_indicatorsElement","_addEventListeners","cycle","_slide","nextWhenVisible","hidden","_clearInterval","_updateInterval","setInterval","_maybeEnableCycle","to","items","_getItems","activeIndex","_getItemIndex","_getActive","order","defaultInterval","_keydown","_addTouchEventListeners","img","swipeConfig","_directionToOrder","endCallBack","clearTimeout","_setActiveIndicatorElement","activeIndicator","newActiveIndicator","elementInterval","parseInt","isNext","nextElement","nextElementIndex","triggerEvent","_orderToDirection","isCycling","directionalClassName","orderClassName","completeCallBack","_isAnimated","clearInterval","carousel","slideIndex","carousels","EVENT_SHOW","EVENT_SHOWN","EVENT_HIDE","EVENT_HIDDEN","CLASS_NAME_SHOW","CLASS_NAME_COLLAPSE","CLASS_NAME_COLLAPSING","CLASS_NAME_DEEPER_CHILDREN","parent","Collapse","_isTransitioning","_triggerArray","toggleList","elem","filterElement","foundElement","_initializeChildren","_addAriaAndCollapsedClass","_isShown","hide","show","activeChildren","_getFirstLevelChildren","activeInstance","dimension","_getDimension","style","scrollSize","complete","getBoundingClientRect","selected","triggerArray","isOpen","top","bottom","right","left","auto","basePlacements","start","end","clippingParents","viewport","popper","reference","variationPlacements","reduce","acc","placement","placements","beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite","modifierPhases","getNodeName","nodeName","getWindow","node","ownerDocument","defaultView","isHTMLElement","HTMLElement","isShadowRoot","applyStyles$1","enabled","phase","_ref","state","elements","forEach","styles","assign","effect","_ref2","initialStyles","position","options","strategy","margin","arrow","hasOwnProperty","attribute","requires","getBasePlacement","round","getUAString","uaData","userAgentData","brands","isArray","item","brand","version","userAgent","isLayoutViewport","includeScale","isFixedStrategy","clientRect","scaleX","scaleY","offsetWidth","width","height","visualViewport","addVisualOffsets","x","offsetLeft","y","offsetTop","getLayoutRect","rootNode","isSameNode","host","isTableElement","getDocumentElement","getParentNode","assignedSlot","getTrueOffsetParent","offsetParent","getOffsetParent","isFirefox","currentNode","css","transform","perspective","contain","willChange","getContainingBlock","getMainAxisFromPlacement","within","mathMax","mathMin","mergePaddingObject","paddingObject","expandToHashMap","hashMap","arrow$1","_state$modifiersData$","arrowElement","popperOffsets","modifiersData","basePlacement","axis","len","padding","rects","toPaddingObject","arrowRect","minProp","maxProp","endDiff","startDiff","arrowOffsetParent","clientSize","clientHeight","clientWidth","centerToReference","center","offset","axisProp","centerOffset","_options$element","requiresIfExists","getVariation","unsetSides","mapToStyles","_Object$assign2","popperRect","variation","offsets","gpuAcceleration","adaptive","roundOffsets","isFixed","_offsets$x","_offsets$y","_ref3","hasX","hasY","sideX","sideY","win","heightProp","widthProp","_Object$assign","commonStyles","_ref4","dpr","devicePixelRatio","roundOffsetsByDPR","computeStyles$1","_ref5","_options$gpuAccelerat","_options$adaptive","_options$roundOffsets","passive","eventListeners","_options$scroll","scroll","_options$resize","resize","scrollParents","scrollParent","update","hash","getOppositePlacement","matched","getOppositeVariationPlacement","getWindowScroll","scrollLeft","pageXOffset","scrollTop","pageYOffset","getWindowScrollBarX","isScrollParent","_getComputedStyle","overflow","overflowX","overflowY","getScrollParent","listScrollParents","_element$ownerDocumen","isBody","updatedList","rectToClientRect","rect","getClientRectFromMixedType","clippingParent","html","layoutViewport","getViewportRect","clientTop","clientLeft","getInnerBoundingClientRect","winScroll","scrollWidth","scrollHeight","getDocumentRect","computeOffsets","commonX","commonY","mainAxis","detectOverflow","_options","_options$placement","_options$strategy","_options$boundary","boundary","_options$rootBoundary","rootBoundary","_options$elementConte","elementContext","_options$altBoundary","altBoundary","_options$padding","altContext","clippingClientRect","mainClippingParents","clipperElement","getClippingParents","firstClippingParent","clippingRect","accRect","getClippingRect","contextElement","referenceClientRect","popperClientRect","elementClientRect","overflowOffsets","offsetData","multiply","computeAutoPlacement","flipVariations","_options$allowedAutoP","allowedAutoPlacements","allPlacements","allowedPlacements","overflows","sort","a","b","flip$1","_skip","_options$mainAxis","checkMainAxis","_options$altAxis","altAxis","checkAltAxis","specifiedFallbackPlacements","fallbackPlacements","_options$flipVariatio","preferredPlacement","oppositePlacement","getExpandedFallbackPlacements","referenceRect","checksMap","makeFallbackChecks","firstFittingPlacement","i","_basePlacement","isStartVariation","isVertical","mainVariationSide","altVariationSide","checks","every","check","_loop","_i","fittingPlacement","reset","getSideOffsets","preventedOffsets","isAnySideFullyClipped","some","side","hide$1","preventOverflow","referenceOverflow","popperAltOverflow","referenceClippingOffsets","popperEscapeOffsets","isReferenceHidden","hasPopperEscaped","offset$1","_options$offset","invertDistance","skidding","distance","distanceAndSkiddingToXY","_data$state$placement","popperOffsets$1","preventOverflow$1","_options$tether","tether","_options$tetherOffset","tetherOffset","isBasePlacement","tetherOffsetValue","normalizedTetherOffsetValue","offsetModifierState","_offsetModifierState$","mainSide","altSide","additive","minLen","maxLen","arrowPaddingObject","arrowPaddingMin","arrowPaddingMax","arrowLen","minOffset","maxOffset","clientOffset","offsetModifierValue","tetherMax","preventedOffset","_offsetModifierState$2","_mainSide","_altSide","_offset","_len","_min","_max","isOriginSide","_offsetModifierValue","_tetherMin","_tetherMax","_preventedOffset","v","withinMaxClamp","getCompositeRect","elementOrVirtualElement","isOffsetParentAnElement","offsetParentIsScaled","isElementScaled","modifiers","visited","result","modifier","dep","depModifier","DEFAULT_OPTIONS","areValidElements","arguments","_key","popperGenerator","generatorOptions","_generatorOptions","_generatorOptions$def","defaultModifiers","_generatorOptions$def2","defaultOptions","pending","orderedModifiers","effectCleanupFns","isDestroyed","setOptions","setOptionsAction","cleanupModifierEffects","merged","orderModifiers","current","existing","m","_ref$options","cleanupFn","forceUpdate","_state$elements","_state$orderedModifie","_state$orderedModifie2","Promise","resolve","then","destroy","onFirstUpdate","createPopper","computeStyles","applyStyles","flip","ARROW_UP_KEY","ARROW_DOWN_KEY","EVENT_KEYDOWN_DATA_API","EVENT_KEYUP_DATA_API","SELECTOR_DATA_TOGGLE_SHOWN","SELECTOR_MENU","PLACEMENT_TOP","PLACEMENT_TOPEND","PLACEMENT_BOTTOM","PLACEMENT_BOTTOMEND","PLACEMENT_RIGHT","PLACEMENT_LEFT","autoClose","display","popperConfig","Dropdown","_popper","_parent","_menu","_inNavbar","_detectNavbar","_createPopper","focus","_completeHide","Popper","referenceElement","_getPopperConfig","_getPlacement","parentDropdown","isEnd","_getOffset","popperData","defaultBsPopperConfig","_selectMenuItem","clearMenus","openToggles","context","composedPath","isMenuTarget","dataApiKeydownHandler","isInput","isEscapeEvent","isUpOrDownEvent","getToggleButton","stopPropagation","EVENT_MOUSEDOWN","className","clickCallback","rootElement","Backdrop","_isAppended","_append","_getElement","_emulateAnimation","backdrop","createElement","append","EVENT_FOCUSIN","EVENT_KEYDOWN_TAB","TAB_NAV_BACKWARD","autofocus","trapElement","FocusTrap","_isActive","_lastTabNavDirection","activate","_handleFocusin","_handleKeydown","deactivate","shiftKey","SELECTOR_FIXED_CONTENT","SELECTOR_STICKY_CONTENT","PROPERTY_PADDING","PROPERTY_MARGIN","ScrollBarHelper","getWidth","documentWidth","innerWidth","_disableOverFlow","_setElementAttributes","calculatedValue","_resetElementAttributes","isOverflowing","_saveInitialAttribute","styleProperty","scrollbarWidth","_applyManipulationCallback","setProperty","actualValue","removeProperty","callBack","EVENT_HIDE_PREVENTED","EVENT_RESIZE","EVENT_CLICK_DISMISS","EVENT_MOUSEDOWN_DISMISS","EVENT_KEYDOWN_DISMISS","CLASS_NAME_OPEN","CLASS_NAME_STATIC","Modal","_dialog","_backdrop","_initializeBackDrop","_focustrap","_initializeFocusTrap","_scrollBar","_adjustDialog","_showElement","_hideModal","handleUpdate","modalBody","transitionComplete","_triggerBackdropTransition","event2","_resetAdjustments","isModalOverflowing","initialOverflowY","isBodyOverflowing","paddingLeft","paddingRight","showEvent","alreadyOpen","CLASS_NAME_SHOWING","CLASS_NAME_HIDING","OPEN_SELECTOR","Offcanvas","blur","completeCallback","DefaultAllowlist","area","br","col","code","dd","div","dl","dt","em","hr","h1","h2","h3","h4","h5","h6","li","ol","p","pre","s","small","span","sub","sup","strong","u","ul","uriAttributes","SAFE_URL_PATTERN","allowedAttribute","allowedAttributeList","attributeName","nodeValue","attributeRegex","regex","allowList","content","extraClass","sanitize","sanitizeFn","template","DefaultContentType","entry","TemplateFactory","getContent","_resolvePossibleFunction","hasContent","changeContent","_checkContent","toHtml","templateWrapper","innerHTML","_maybeSanitize","text","_setContent","arg","templateElement","_putElementInTemplate","textContent","unsafeHtml","sanitizeFunction","createdDocument","DOMParser","parseFromString","elementName","attributeList","allowedAttributes","sanitizeHtml","DISALLOWED_ATTRIBUTES","CLASS_NAME_FADE","SELECTOR_TOOLTIP_INNER","SELECTOR_MODAL","EVENT_MODAL_HIDE","TRIGGER_HOVER","TRIGGER_FOCUS","TRIGGER_CLICK","AttachmentMap","AUTO","TOP","RIGHT","BOTTOM","LEFT","animation","container","customClass","delay","title","Tooltip","_isEnabled","_timeout","_isHovered","_activeTrigger","_templateFactory","_newContent","tip","_setListeners","_fixTitle","enable","disable","toggleEnabled","_leave","_enter","_hideModalHandler","_disposePopper","_isWithContent","isInTheDom","_getTipElement","_isWithActiveTrigger","_getTitle","_createTipElement","_getContentForTemplate","_getTemplateFactory","tipId","prefix","floor","random","getElementById","getUID","setContent","_initializeOnDelegatedTarget","_getDelegateConfig","attachment","triggers","eventIn","eventOut","_setTimeout","timeout","dataAttributes","dataAttribute","SELECTOR_TITLE","SELECTOR_CONTENT","Popover","_getContent","EVENT_ACTIVATE","EVENT_CLICK","SELECTOR_TARGET_LINKS","SELECTOR_NAV_LINKS","SELECTOR_LINK_ITEMS","rootMargin","smoothScroll","threshold","ScrollSpy","_targetLinks","_observableSections","_rootElement","_activeTarget","_observer","_previousScrollData","visibleEntryTop","parentScrollTop","refresh","_initializeTargetsAndObservables","_maybeEnableSmoothScroll","disconnect","_getNewObserver","section","observe","observableSection","scrollTo","behavior","IntersectionObserver","_observerCallback","targetElement","_process","userScrollsDown","isIntersecting","_clearActiveClass","entryIsLowerThanPrevious","targetLinks","anchor","decodeURI","_activateParents","listGroup","activeNodes","spy","HOME_KEY","END_KEY","SELECTOR_DROPDOWN_TOGGLE","NOT_SELECTOR_DROPDOWN_TOGGLE","SELECTOR_INNER_ELEM","SELECTOR_DATA_TOGGLE_ACTIVE","Tab","_setInitialAttributes","_getChildren","innerElem","_elemIsActive","active","_getActiveElem","hideEvent","_deactivate","_activate","relatedElem","_toggleDropDown","nextActiveElement","preventScroll","_setAttributeIfNotExists","_setInitialAttributesOnChild","_getInnerElement","isActive","outerElem","_getOuterElement","_setInitialAttributesOnTargetPanel","open","EVENT_MOUSEOVER","EVENT_MOUSEOUT","EVENT_FOCUSOUT","CLASS_NAME_HIDE","autohide","Toast","_hasMouseInteraction","_hasKeyboardInteraction","_clearTimeout","_maybeScheduleHide","isShown","_onInteraction","isInteracting"],"sources":["../../js/src/dom/data.js","../../js/src/util/index.js","../../js/src/dom/event-handler.js","../../js/src/dom/manipulator.js","../../js/src/util/config.js","../../js/src/base-component.js","../../js/src/dom/selector-engine.js","../../js/src/util/component-functions.js","../../js/src/alert.js","../../js/src/button.js","../../js/src/util/swipe.js","../../js/src/carousel.js","../../js/src/collapse.js","../../node_modules/@popperjs/core/lib/enums.js","../../node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindow.js","../../node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","../../node_modules/@popperjs/core/lib/modifiers/applyStyles.js","../../node_modules/@popperjs/core/lib/utils/getBasePlacement.js","../../node_modules/@popperjs/core/lib/utils/math.js","../../node_modules/@popperjs/core/lib/utils/userAgent.js","../../node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","../../node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","../../node_modules/@popperjs/core/lib/dom-utils/contains.js","../../node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","../../node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","../../node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","../../node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","../../node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","../../node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","../../node_modules/@popperjs/core/lib/utils/within.js","../../node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","../../node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","../../node_modules/@popperjs/core/lib/utils/expandToHashMap.js","../../node_modules/@popperjs/core/lib/modifiers/arrow.js","../../node_modules/@popperjs/core/lib/utils/getVariation.js","../../node_modules/@popperjs/core/lib/modifiers/computeStyles.js","../../node_modules/@popperjs/core/lib/modifiers/eventListeners.js","../../node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","../../node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","../../node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","../../node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","../../node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","../../node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","../../node_modules/@popperjs/core/lib/utils/rectToClientRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","../../node_modules/@popperjs/core/lib/utils/computeOffsets.js","../../node_modules/@popperjs/core/lib/utils/detectOverflow.js","../../node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","../../node_modules/@popperjs/core/lib/modifiers/flip.js","../../node_modules/@popperjs/core/lib/modifiers/hide.js","../../node_modules/@popperjs/core/lib/modifiers/offset.js","../../node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","../../node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","../../node_modules/@popperjs/core/lib/utils/getAltAxis.js","../../node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","../../node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","../../node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","../../node_modules/@popperjs/core/lib/utils/orderModifiers.js","../../node_modules/@popperjs/core/lib/createPopper.js","../../node_modules/@popperjs/core/lib/utils/debounce.js","../../node_modules/@popperjs/core/lib/utils/mergeByName.js","../../node_modules/@popperjs/core/lib/popper-lite.js","../../node_modules/@popperjs/core/lib/popper.js","../../js/src/dropdown.js","../../js/src/util/backdrop.js","../../js/src/util/focustrap.js","../../js/src/util/scrollbar.js","../../js/src/modal.js","../../js/src/offcanvas.js","../../js/src/util/sanitizer.js","../../js/src/util/template-factory.js","../../js/src/tooltip.js","../../js/src/popover.js","../../js/src/scrollspy.js","../../js/src/tab.js","../../js/src/toast.js","../../js/index.umd.js"],"sourcesContent":["/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map()\n\nexport default {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map())\n }\n\n const instanceMap = elementMap.get(element)\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`)\n return\n }\n\n instanceMap.set(key, instance)\n },\n\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null\n }\n\n return null\n },\n\n remove(element, key) {\n if (!elementMap.has(element)) {\n return\n }\n\n const instanceMap = elementMap.get(element)\n\n instanceMap.delete(key)\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element)\n }\n }\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1_000_000\nconst MILLISECONDS_MULTIPLIER = 1000\nconst TRANSITION_END = 'transitionend'\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`)\n }\n\n return selector\n}\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`\n }\n\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase()\n}\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID)\n } while (document.getElementById(prefix))\n\n return prefix\n}\n\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0\n }\n\n // Get transition-duration of the element\n let { transitionDuration, transitionDelay } = window.getComputedStyle(element)\n\n const floatTransitionDuration = Number.parseFloat(transitionDuration)\n const floatTransitionDelay = Number.parseFloat(transitionDelay)\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0]\n transitionDelay = transitionDelay.split(',')[0]\n\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER\n}\n\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END))\n}\n\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false\n }\n\n if (typeof object.jquery !== 'undefined') {\n object = object[0]\n }\n\n return typeof object.nodeType !== 'undefined'\n}\n\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object\n }\n\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object))\n }\n\n return null\n}\n\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false\n }\n\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])')\n\n if (!closedDetails) {\n return elementIsVisible\n }\n\n if (closedDetails !== element) {\n const summary = element.closest('summary')\n if (summary && summary.parentNode !== closedDetails) {\n return false\n }\n\n if (summary === null) {\n return false\n }\n }\n\n return elementIsVisible\n}\n\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true\n }\n\n if (element.classList.contains('disabled')) {\n return true\n }\n\n if (typeof element.disabled !== 'undefined') {\n return element.disabled\n }\n\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'\n}\n\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode()\n return root instanceof ShadowRoot ? root : null\n }\n\n if (element instanceof ShadowRoot) {\n return element\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null\n }\n\n return findShadowRoot(element.parentNode)\n}\n\nconst noop = () => {}\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight // eslint-disable-line no-unused-expressions\n}\n\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery\n }\n\n return null\n}\n\nconst DOMContentLoadedCallbacks = []\n\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback()\n }\n })\n }\n\n DOMContentLoadedCallbacks.push(callback)\n } else {\n callback()\n }\n}\n\nconst isRTL = () => document.documentElement.dir === 'rtl'\n\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery()\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME\n const JQUERY_NO_CONFLICT = $.fn[name]\n $.fn[name] = plugin.jQueryInterface\n $.fn[name].Constructor = plugin\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT\n return plugin.jQueryInterface\n }\n }\n })\n}\n\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue\n}\n\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback)\n return\n }\n\n const durationPadding = 5\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding\n\n let called = false\n\n const handler = ({ target }) => {\n if (target !== transitionElement) {\n return\n }\n\n called = true\n transitionElement.removeEventListener(TRANSITION_END, handler)\n execute(callback)\n }\n\n transitionElement.addEventListener(TRANSITION_END, handler)\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement)\n }\n }, emulatedDuration)\n}\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length\n let index = list.indexOf(activeElement)\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]\n }\n\n index += shouldGetNext ? 1 : -1\n\n if (isCycleAllowed) {\n index = (index + listLength) % listLength\n }\n\n return list[Math.max(0, Math.min(index, listLength - 1))]\n}\n\nexport {\n defineJQueryPlugin,\n execute,\n executeAfterTransition,\n findShadowRoot,\n getElement,\n getjQuery,\n getNextActiveElement,\n getTransitionDurationFromElement,\n getUID,\n isDisabled,\n isElement,\n isRTL,\n isVisible,\n noop,\n onDOMContentLoaded,\n parseSelector,\n reflow,\n triggerTransitionEnd,\n toType\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport { getjQuery } from '../util/index.js'\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/\nconst stripNameRegex = /\\..*/\nconst stripUidRegex = /::\\d+$/\nconst eventRegistry = {} // Events storage\nlet uidEvent = 1\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n}\n\nconst nativeEvents = new Set([\n 'click',\n 'dblclick',\n 'mouseup',\n 'mousedown',\n 'contextmenu',\n 'mousewheel',\n 'DOMMouseScroll',\n 'mouseover',\n 'mouseout',\n 'mousemove',\n 'selectstart',\n 'selectend',\n 'keydown',\n 'keypress',\n 'keyup',\n 'orientationchange',\n 'touchstart',\n 'touchmove',\n 'touchend',\n 'touchcancel',\n 'pointerdown',\n 'pointermove',\n 'pointerup',\n 'pointerleave',\n 'pointercancel',\n 'gesturestart',\n 'gesturechange',\n 'gestureend',\n 'focus',\n 'blur',\n 'change',\n 'reset',\n 'select',\n 'submit',\n 'focusin',\n 'focusout',\n 'load',\n 'unload',\n 'beforeunload',\n 'resize',\n 'move',\n 'DOMContentLoaded',\n 'readystatechange',\n 'error',\n 'abort',\n 'scroll'\n])\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++\n}\n\nfunction getElementEvents(element) {\n const uid = makeEventUid(element)\n\n element.uidEvent = uid\n eventRegistry[uid] = eventRegistry[uid] || {}\n\n return eventRegistry[uid]\n}\n\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, { delegateTarget: element })\n\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn)\n }\n\n return fn.apply(element, [event])\n }\n}\n\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector)\n\n for (let { target } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue\n }\n\n hydrateObj(event, { delegateTarget: target })\n\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn)\n }\n\n return fn.apply(target, [event])\n }\n }\n }\n}\n\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events)\n .find(event => event.callable === callable && event.delegationSelector === delegationSelector)\n}\n\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string'\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : (handler || delegationFunction)\n let typeEvent = getTypeEvent(originalTypeEvent)\n\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent\n }\n\n return [isDelegated, callable, typeEvent]\n}\n\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return\n }\n\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) {\n return fn.call(this, event)\n }\n }\n }\n\n callable = wrapFunction(callable)\n }\n\n const events = getElementEvents(element)\n const handlers = events[typeEvent] || (events[typeEvent] = {})\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null)\n\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff\n\n return\n }\n\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''))\n const fn = isDelegated ?\n bootstrapDelegationHandler(element, handler, callable) :\n bootstrapHandler(element, callable)\n\n fn.delegationSelector = isDelegated ? handler : null\n fn.callable = callable\n fn.oneOff = oneOff\n fn.uidEvent = uid\n handlers[uid] = fn\n\n element.addEventListener(typeEvent, fn, isDelegated)\n}\n\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector)\n\n if (!fn) {\n return\n }\n\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))\n delete events[typeEvent][fn.uidEvent]\n}\n\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {}\n\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)\n }\n }\n}\n\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '')\n return customEvents[event] || event\n}\n\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false)\n },\n\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true)\n },\n\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return\n }\n\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)\n const inNamespace = typeEvent !== originalTypeEvent\n const events = getElementEvents(element)\n const storeElementEvent = events[typeEvent] || {}\n const isNamespace = originalTypeEvent.startsWith('.')\n\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return\n }\n\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null)\n return\n }\n\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))\n }\n }\n\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '')\n\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)\n }\n }\n },\n\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null\n }\n\n const $ = getjQuery()\n const typeEvent = getTypeEvent(event)\n const inNamespace = event !== typeEvent\n\n let jQueryEvent = null\n let bubbles = true\n let nativeDispatch = true\n let defaultPrevented = false\n\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args)\n\n $(element).trigger(jQueryEvent)\n bubbles = !jQueryEvent.isPropagationStopped()\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()\n defaultPrevented = jQueryEvent.isDefaultPrevented()\n }\n\n const evt = hydrateObj(new Event(event, { bubbles, cancelable: true }), args)\n\n if (defaultPrevented) {\n evt.preventDefault()\n }\n\n if (nativeDispatch) {\n element.dispatchEvent(evt)\n }\n\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault()\n }\n\n return evt\n }\n}\n\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value\n } catch {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value\n }\n })\n }\n }\n\n return obj\n}\n\nexport default EventHandler\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true\n }\n\n if (value === 'false') {\n return false\n }\n\n if (value === Number(value).toString()) {\n return Number(value)\n }\n\n if (value === '' || value === 'null') {\n return null\n }\n\n if (typeof value !== 'string') {\n return value\n }\n\n try {\n return JSON.parse(decodeURIComponent(value))\n } catch {\n return value\n }\n}\n\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`)\n}\n\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value)\n },\n\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`)\n },\n\n getDataAttributes(element) {\n if (!element) {\n return {}\n }\n\n const attributes = {}\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'))\n\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '')\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1)\n attributes[pureKey] = normalizeData(element.dataset[key])\n }\n\n return attributes\n },\n\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`))\n }\n}\n\nexport default Manipulator\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Manipulator from '../dom/manipulator.js'\nimport { isElement, toType } from './index.js'\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {}\n }\n\n static get DefaultType() {\n return {}\n }\n\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!')\n }\n\n _getConfig(config) {\n config = this._mergeConfigObj(config)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n _configAfterMerge(config) {\n return config\n }\n\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {} // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n }\n }\n\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property]\n const valueType = isElement(value) ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(\n `${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`\n )\n }\n }\n }\n}\n\nexport default Config\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Data from './dom/data.js'\nimport EventHandler from './dom/event-handler.js'\nimport Config from './util/config.js'\nimport { executeAfterTransition, getElement } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.7'\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super()\n\n element = getElement(element)\n if (!element) {\n return\n }\n\n this._element = element\n this._config = this._getConfig(config)\n\n Data.set(this._element, this.constructor.DATA_KEY, this)\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY)\n EventHandler.off(this._element, this.constructor.EVENT_KEY)\n\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null\n }\n }\n\n // Private\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated)\n }\n\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY)\n }\n\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null)\n }\n\n static get VERSION() {\n return VERSION\n }\n\n static get DATA_KEY() {\n return `bs.${this.NAME}`\n }\n\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`\n }\n\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`\n }\n}\n\nexport default BaseComponent\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport { isDisabled, isVisible, parseSelector } from '../util/index.js'\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target')\n\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href')\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) {\n return null\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`\n }\n\n selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null\n }\n\n return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null\n}\n\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector))\n },\n\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector)\n },\n\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector))\n },\n\n parents(element, selector) {\n const parents = []\n let ancestor = element.parentNode.closest(selector)\n\n while (ancestor) {\n parents.push(ancestor)\n ancestor = ancestor.parentNode.closest(selector)\n }\n\n return parents\n },\n\n prev(element, selector) {\n let previous = element.previousElementSibling\n\n while (previous) {\n if (previous.matches(selector)) {\n return [previous]\n }\n\n previous = previous.previousElementSibling\n }\n\n return []\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling\n\n while (next) {\n if (next.matches(selector)) {\n return [next]\n }\n\n next = next.nextElementSibling\n }\n\n return []\n },\n\n focusableChildren(element) {\n const focusables = [\n 'a',\n 'button',\n 'input',\n 'textarea',\n 'select',\n 'details',\n '[tabindex]',\n '[contenteditable=\"true\"]'\n ].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',')\n\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el))\n },\n\n getSelectorFromElement(element) {\n const selector = getSelector(element)\n\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null\n }\n\n return null\n },\n\n getElementFromSelector(element) {\n const selector = getSelector(element)\n\n return selector ? SelectorEngine.findOne(selector) : null\n },\n\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element)\n\n return selector ? SelectorEngine.find(selector) : []\n }\n}\n\nexport default SelectorEngine\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport { isDisabled } from './index.js'\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`\n const name = component.NAME\n\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n if (isDisabled(this)) {\n return\n }\n\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`)\n const instance = component.getOrCreateInstance(target)\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]()\n })\n}\n\nexport {\n enableDismissTrigger\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'alert'\nconst DATA_KEY = 'bs.alert'\nconst EVENT_KEY = `.${DATA_KEY}`\n\nconst EVENT_CLOSE = `close${EVENT_KEY}`\nconst EVENT_CLOSED = `closed${EVENT_KEY}`\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE)\n\n if (closeEvent.defaultPrevented) {\n return\n }\n\n this._element.classList.remove(CLASS_NAME_SHOW)\n\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE)\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated)\n }\n\n // Private\n _destroyElement() {\n this._element.remove()\n EventHandler.trigger(this._element, EVENT_CLOSED)\n this.dispose()\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](this)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close')\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert)\n\nexport default Alert\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'button'\nconst DATA_KEY = 'bs.button'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst CLASS_NAME_ACTIVE = 'active'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"button\"]'\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE))\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this)\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {\n event.preventDefault()\n\n const button = event.target.closest(SELECTOR_DATA_TOGGLE)\n const data = Button.getOrCreateInstance(button)\n\n data.toggle()\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button)\n\nexport default Button\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport Config from './config.js'\nimport { execute } from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'swipe'\nconst EVENT_KEY = '.bs.swipe'\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY}`\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY}`\nconst POINTER_TYPE_TOUCH = 'touch'\nconst POINTER_TYPE_PEN = 'pen'\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event'\nconst SWIPE_THRESHOLD = 40\n\nconst Default = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n}\n\nconst DefaultType = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n}\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super()\n this._element = element\n\n if (!element || !Swipe.isSupported()) {\n return\n }\n\n this._config = this._getConfig(config)\n this._deltaX = 0\n this._supportPointerEvents = Boolean(window.PointerEvent)\n this._initEvents()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY)\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX\n\n return\n }\n\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX\n }\n }\n\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX\n }\n\n this._handleSwipe()\n execute(this._config.endCallback)\n }\n\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ?\n 0 :\n event.touches[0].clientX - this._deltaX\n }\n\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX)\n\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return\n }\n\n const direction = absDeltaX / this._deltaX\n\n this._deltaX = 0\n\n if (!direction) {\n return\n }\n\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback)\n }\n\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event))\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event))\n\n this._element.classList.add(CLASS_NAME_POINTER_EVENT)\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event))\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event))\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event))\n }\n }\n\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0\n }\n}\n\nexport default Swipe\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n getNextActiveElement,\n isRTL,\n isVisible,\n reflow,\n triggerTransitionEnd\n} from './util/index.js'\nimport Swipe from './util/swipe.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'carousel'\nconst DATA_KEY = 'bs.carousel'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst ARROW_LEFT_KEY = 'ArrowLeft'\nconst ARROW_RIGHT_KEY = 'ArrowRight'\nconst TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next'\nconst ORDER_PREV = 'prev'\nconst DIRECTION_LEFT = 'left'\nconst DIRECTION_RIGHT = 'right'\n\nconst EVENT_SLIDE = `slide${EVENT_KEY}`\nconst EVENT_SLID = `slid${EVENT_KEY}`\nconst EVENT_KEYDOWN = `keydown${EVENT_KEY}`\nconst EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`\nconst EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_CAROUSEL = 'carousel'\nconst CLASS_NAME_ACTIVE = 'active'\nconst CLASS_NAME_SLIDE = 'slide'\nconst CLASS_NAME_END = 'carousel-item-end'\nconst CLASS_NAME_START = 'carousel-item-start'\nconst CLASS_NAME_NEXT = 'carousel-item-next'\nconst CLASS_NAME_PREV = 'carousel-item-prev'\n\nconst SELECTOR_ACTIVE = '.active'\nconst SELECTOR_ITEM = '.carousel-item'\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM\nconst SELECTOR_ITEM_IMG = '.carousel-item img'\nconst SELECTOR_INDICATORS = '.carousel-indicators'\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]'\n\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY]: DIRECTION_LEFT\n}\n\nconst Default = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n}\n\nconst DefaultType = {\n interval: '(number|boolean)', // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._interval = null\n this._activeElement = null\n this._isSliding = false\n this.touchTimeout = null\n this._swipeHelper = null\n\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)\n this._addEventListeners()\n\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT)\n }\n\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next()\n }\n }\n\n prev() {\n this._slide(ORDER_PREV)\n }\n\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element)\n }\n\n this._clearInterval()\n }\n\n cycle() {\n this._clearInterval()\n this._updateInterval()\n\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval)\n }\n\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return\n }\n\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle())\n return\n }\n\n this.cycle()\n }\n\n to(index) {\n const items = this._getItems()\n if (index > items.length - 1 || index < 0) {\n return\n }\n\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index))\n return\n }\n\n const activeIndex = this._getItemIndex(this._getActive())\n if (activeIndex === index) {\n return\n }\n\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV\n\n this._slide(order, items[index])\n }\n\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose()\n }\n\n super.dispose()\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval\n return config\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause())\n EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle())\n }\n\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners()\n }\n }\n\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault())\n }\n\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n }\n\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n }\n\n this._swipeHelper = new Swipe(this._element, swipeConfig)\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return\n }\n\n const direction = KEY_TO_DIRECTION[event.key]\n if (direction) {\n event.preventDefault()\n this._slide(this._directionToOrder(direction))\n }\n }\n\n _getItemIndex(element) {\n return this._getItems().indexOf(element)\n }\n\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return\n }\n\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)\n\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE)\n activeIndicator.removeAttribute('aria-current')\n\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement)\n\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE)\n newActiveIndicator.setAttribute('aria-current', 'true')\n }\n }\n\n _updateInterval() {\n const element = this._activeElement || this._getActive()\n\n if (!element) {\n return\n }\n\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10)\n\n this._config.interval = elementInterval || this._config.defaultInterval\n }\n\n _slide(order, element = null) {\n if (this._isSliding) {\n return\n }\n\n const activeElement = this._getActive()\n const isNext = order === ORDER_NEXT\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap)\n\n if (nextElement === activeElement) {\n return\n }\n\n const nextElementIndex = this._getItemIndex(nextElement)\n\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n })\n }\n\n const slideEvent = triggerEvent(EVENT_SLIDE)\n\n if (slideEvent.defaultPrevented) {\n return\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return\n }\n\n const isCycling = Boolean(this._interval)\n this.pause()\n\n this._isSliding = true\n\n this._setActiveIndicatorElement(nextElementIndex)\n this._activeElement = nextElement\n\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV\n\n nextElement.classList.add(orderClassName)\n\n reflow(nextElement)\n\n activeElement.classList.add(directionalClassName)\n nextElement.classList.add(directionalClassName)\n\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName)\n nextElement.classList.add(CLASS_NAME_ACTIVE)\n\n activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName)\n\n this._isSliding = false\n\n triggerEvent(EVENT_SLID)\n }\n\n this._queueCallback(completeCallBack, activeElement, this._isAnimated())\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE)\n }\n\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)\n }\n\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element)\n }\n\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval)\n this._interval = null\n }\n }\n\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT\n }\n\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV\n }\n\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT\n }\n\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config)\n\n if (typeof config === 'number') {\n data.to(config)\n return\n }\n\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return\n }\n\n event.preventDefault()\n\n const carousel = Carousel.getOrCreateInstance(target)\n const slideIndex = this.getAttribute('data-bs-slide-to')\n\n if (slideIndex) {\n carousel.to(slideIndex)\n carousel._maybeEnableCycle()\n return\n }\n\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next()\n carousel._maybeEnableCycle()\n return\n }\n\n carousel.prev()\n carousel._maybeEnableCycle()\n})\n\nEventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)\n\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel)\n }\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel)\n\nexport default Carousel\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n getElement,\n reflow\n} from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'collapse'\nconst DATA_KEY = 'bs.collapse'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_COLLAPSE = 'collapse'\nconst CLASS_NAME_COLLAPSING = 'collapsing'\nconst CLASS_NAME_COLLAPSED = 'collapsed'\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal'\n\nconst WIDTH = 'width'\nconst HEIGHT = 'height'\n\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"collapse\"]'\n\nconst Default = {\n parent: null,\n toggle: true\n}\n\nconst DefaultType = {\n parent: '(null|element)',\n toggle: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._isTransitioning = false\n this._triggerArray = []\n\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)\n\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem)\n const filterElement = SelectorEngine.find(selector)\n .filter(foundElement => foundElement === this._element)\n\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem)\n }\n }\n\n this._initializeChildren()\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown())\n }\n\n if (this._config.toggle) {\n this.toggle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning || this._isShown()) {\n return\n }\n\n let activeChildren = []\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES)\n .filter(element => element !== this._element)\n .map(element => Collapse.getOrCreateInstance(element, { toggle: false }))\n }\n\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW)\n if (startEvent.defaultPrevented) {\n return\n }\n\n for (const activeInstance of activeChildren) {\n activeInstance.hide()\n }\n\n const dimension = this._getDimension()\n\n this._element.classList.remove(CLASS_NAME_COLLAPSE)\n this._element.classList.add(CLASS_NAME_COLLAPSING)\n\n this._element.style[dimension] = 0\n\n this._addAriaAndCollapsedClass(this._triggerArray, true)\n this._isTransitioning = true\n\n const complete = () => {\n this._isTransitioning = false\n\n this._element.classList.remove(CLASS_NAME_COLLAPSING)\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)\n\n this._element.style[dimension] = ''\n\n EventHandler.trigger(this._element, EVENT_SHOWN)\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n\n this._queueCallback(complete, this._element, true)\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return\n }\n\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n if (startEvent.defaultPrevented) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n reflow(this._element)\n\n this._element.classList.add(CLASS_NAME_COLLAPSING)\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)\n\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger)\n\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false)\n }\n }\n\n this._isTransitioning = true\n\n const complete = () => {\n this._isTransitioning = false\n this._element.classList.remove(CLASS_NAME_COLLAPSING)\n this._element.classList.add(CLASS_NAME_COLLAPSE)\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n }\n\n this._element.style[dimension] = ''\n\n this._queueCallback(complete, this._element, true)\n }\n\n // Private\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW)\n }\n\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle) // Coerce string values\n config.parent = getElement(config.parent)\n return config\n }\n\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT\n }\n\n _initializeChildren() {\n if (!this._config.parent) {\n return\n }\n\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE)\n\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element)\n\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected))\n }\n }\n }\n\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent)\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element))\n }\n\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return\n }\n\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen)\n element.setAttribute('aria-expanded', isOpen)\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {}\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config)\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n }\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || (event.delegateTarget && event.delegateTarget.tagName === 'A')) {\n event.preventDefault()\n }\n\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, { toggle: false }).toggle()\n }\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse)\n\nexport default Collapse\n","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport * as Popper from '@popperjs/core'\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin,\n execute,\n getElement,\n getNextActiveElement,\n isDisabled,\n isElement,\n isRTL,\n isVisible,\n noop\n} from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'dropdown'\nconst DATA_KEY = 'bs.dropdown'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst ESCAPE_KEY = 'Escape'\nconst TAB_KEY = 'Tab'\nconst ARROW_UP_KEY = 'ArrowUp'\nconst ARROW_DOWN_KEY = 'ArrowDown'\nconst RIGHT_MOUSE_BUTTON = 2 // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_DROPUP = 'dropup'\nconst CLASS_NAME_DROPEND = 'dropend'\nconst CLASS_NAME_DROPSTART = 'dropstart'\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center'\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'\n\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)'\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`\nconst SELECTOR_MENU = '.dropdown-menu'\nconst SELECTOR_NAVBAR = '.navbar'\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav'\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'\nconst PLACEMENT_TOPCENTER = 'top'\nconst PLACEMENT_BOTTOMCENTER = 'bottom'\n\nconst Default = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n}\n\nconst DefaultType = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n}\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._popper = null\n this._parent = this._element.parentNode // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] ||\n SelectorEngine.prev(this._element, SELECTOR_MENU)[0] ||\n SelectorEngine.findOne(SELECTOR_MENU, this._parent)\n this._inNavbar = this._detectNavbar()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show()\n }\n\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._createPopper()\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop)\n }\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n this._menu.classList.add(CLASS_NAME_SHOW)\n this._element.classList.add(CLASS_NAME_SHOW)\n EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget)\n }\n\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n\n this._completeHide(relatedTarget)\n }\n\n dispose() {\n if (this._popper) {\n this._popper.destroy()\n }\n\n super.dispose()\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper) {\n this._popper.update()\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)\n if (hideEvent.defaultPrevented) {\n return\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop)\n }\n }\n\n if (this._popper) {\n this._popper.destroy()\n }\n\n this._menu.classList.remove(CLASS_NAME_SHOW)\n this._element.classList.remove(CLASS_NAME_SHOW)\n this._element.setAttribute('aria-expanded', 'false')\n Manipulator.removeDataAttribute(this._menu, 'popper')\n EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)\n\n // Explicitly return focus to the trigger element\n this._element.focus()\n }\n\n _getConfig(config) {\n config = super._getConfig(config)\n\n if (typeof config.reference === 'object' && !isElement(config.reference) &&\n typeof config.reference.getBoundingClientRect !== 'function'\n ) {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`)\n }\n\n return config\n }\n\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org/docs/v2/)')\n }\n\n let referenceElement = this._element\n\n if (this._config.reference === 'parent') {\n referenceElement = this._parent\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference)\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference\n }\n\n const popperConfig = this._getPopperConfig()\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)\n }\n\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW)\n }\n\n _getPlacement() {\n const parentDropdown = this._parent\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER\n }\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'\n\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP\n }\n\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM\n }\n\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null\n }\n\n _getOffset() {\n const { offset } = this._config\n\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10))\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element)\n }\n\n return offset\n }\n\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n },\n {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n }\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static') // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }]\n }\n\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])\n }\n }\n\n _selectMenuItem({ key, target }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element))\n\n if (!items.length) {\n return\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {\n return\n }\n\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN)\n\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle)\n if (!context || context._config.autoClose === false) {\n continue\n }\n\n const composedPath = event.composedPath()\n const isMenuTarget = composedPath.includes(context._menu)\n if (\n composedPath.includes(context._element) ||\n (context._config.autoClose === 'inside' && !isMenuTarget) ||\n (context._config.autoClose === 'outside' && isMenuTarget)\n ) {\n continue\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && ((event.type === 'keyup' && event.key === TAB_KEY) || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue\n }\n\n const relatedTarget = { relatedTarget: context._element }\n\n if (event.type === 'click') {\n relatedTarget.clickEvent = event\n }\n\n context._completeHide(relatedTarget)\n }\n }\n\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName)\n const isEscapeEvent = event.key === ESCAPE_KEY\n const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)\n\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return\n }\n\n if (isInput && !isEscapeEvent) {\n return\n }\n\n event.preventDefault()\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ?\n this :\n (SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] ||\n SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] ||\n SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, event.delegateTarget.parentNode))\n\n const instance = Dropdown.getOrCreateInstance(getToggleButton)\n\n if (isUpOrDownEvent) {\n event.stopPropagation()\n instance.show()\n instance._selectMenuItem(event)\n return\n }\n\n if (instance._isShown()) { // else is escape and we check if it is shown\n event.stopPropagation()\n instance.hide()\n getToggleButton.focus()\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler)\nEventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n event.preventDefault()\n Dropdown.getOrCreateInstance(this).toggle()\n})\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown)\n\nexport default Dropdown\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport Config from './config.js'\nimport {\n execute, executeAfterTransition, getElement, reflow\n} from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'backdrop'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`\n\nconst Default = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true, // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n}\n\nconst DefaultType = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n}\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n this._isAppended = false\n this._element = null\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback)\n return\n }\n\n this._append()\n\n const element = this._getElement()\n if (this._config.isAnimated) {\n reflow(element)\n }\n\n element.classList.add(CLASS_NAME_SHOW)\n\n this._emulateAnimation(() => {\n execute(callback)\n })\n }\n\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback)\n return\n }\n\n this._getElement().classList.remove(CLASS_NAME_SHOW)\n\n this._emulateAnimation(() => {\n this.dispose()\n execute(callback)\n })\n }\n\n dispose() {\n if (!this._isAppended) {\n return\n }\n\n EventHandler.off(this._element, EVENT_MOUSEDOWN)\n\n this._element.remove()\n this._isAppended = false\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div')\n backdrop.className = this._config.className\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE)\n }\n\n this._element = backdrop\n }\n\n return this._element\n }\n\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement)\n return config\n }\n\n _append() {\n if (this._isAppended) {\n return\n }\n\n const element = this._getElement()\n this._config.rootElement.append(element)\n\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback)\n })\n\n this._isAppended = true\n }\n\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated)\n }\n}\n\nexport default Backdrop\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport EventHandler from '../dom/event-handler.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport Config from './config.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'focustrap'\nconst DATA_KEY = 'bs.focustrap'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst EVENT_FOCUSIN = `focusin${EVENT_KEY}`\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}`\n\nconst TAB_KEY = 'Tab'\nconst TAB_NAV_FORWARD = 'forward'\nconst TAB_NAV_BACKWARD = 'backward'\n\nconst Default = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n}\n\nconst DefaultType = {\n autofocus: 'boolean',\n trapElement: 'element'\n}\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n this._isActive = false\n this._lastTabNavDirection = null\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return\n }\n\n if (this._config.autofocus) {\n this._config.trapElement.focus()\n }\n\n EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event))\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event))\n\n this._isActive = true\n }\n\n deactivate() {\n if (!this._isActive) {\n return\n }\n\n this._isActive = false\n EventHandler.off(document, EVENT_KEY)\n }\n\n // Private\n _handleFocusin(event) {\n const { trapElement } = this._config\n\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return\n }\n\n const elements = SelectorEngine.focusableChildren(trapElement)\n\n if (elements.length === 0) {\n trapElement.focus()\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus()\n } else {\n elements[0].focus()\n }\n }\n\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return\n }\n\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD\n }\n}\n\nexport default FocusTrap\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Manipulator from '../dom/manipulator.js'\nimport SelectorEngine from '../dom/selector-engine.js'\nimport { isElement } from './index.js'\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'\nconst SELECTOR_STICKY_CONTENT = '.sticky-top'\nconst PROPERTY_PADDING = 'padding-right'\nconst PROPERTY_MARGIN = 'margin-right'\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth\n return Math.abs(window.innerWidth - documentWidth)\n }\n\n hide() {\n const width = this.getWidth()\n this._disableOverFlow()\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width)\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width)\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width)\n }\n\n reset() {\n this._resetElementAttributes(this._element, 'overflow')\n this._resetElementAttributes(this._element, PROPERTY_PADDING)\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING)\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN)\n }\n\n isOverflowing() {\n return this.getWidth() > 0\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow')\n this._element.style.overflow = 'hidden'\n }\n\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth()\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return\n }\n\n this._saveInitialAttribute(element, styleProperty)\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty)\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`)\n }\n\n this._applyManipulationCallback(selector, manipulationCallBack)\n }\n\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty)\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue)\n }\n }\n\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty)\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty)\n return\n }\n\n Manipulator.removeDataAttribute(element, styleProperty)\n element.style.setProperty(styleProperty, value)\n }\n\n this._applyManipulationCallback(selector, manipulationCallBack)\n }\n\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector)\n return\n }\n\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel)\n }\n }\n}\n\nexport default ScrollBarHelper\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport Backdrop from './util/backdrop.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport FocusTrap from './util/focustrap.js'\nimport {\n defineJQueryPlugin, isRTL, isVisible, reflow\n} from './util/index.js'\nimport ScrollBarHelper from './util/scrollbar.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'modal'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst ESCAPE_KEY = 'Escape'\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_RESIZE = `resize${EVENT_KEY}`\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_OPEN = 'modal-open'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_STATIC = 'modal-static'\n\nconst OPEN_SELECTOR = '.modal.show'\nconst SELECTOR_DIALOG = '.modal-dialog'\nconst SELECTOR_MODAL_BODY = '.modal-body'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"modal\"]'\n\nconst Default = {\n backdrop: true,\n focus: true,\n keyboard: true\n}\n\nconst DefaultType = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)\n this._backdrop = this._initializeBackDrop()\n this._focustrap = this._initializeFocusTrap()\n this._isShown = false\n this._isTransitioning = false\n this._scrollBar = new ScrollBarHelper()\n\n this._addEventListeners()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {\n relatedTarget\n })\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._isShown = true\n this._isTransitioning = true\n\n this._scrollBar.hide()\n\n document.body.classList.add(CLASS_NAME_OPEN)\n\n this._adjustDialog()\n\n this._backdrop.show(() => this._showElement(relatedTarget))\n }\n\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n\n if (hideEvent.defaultPrevented) {\n return\n }\n\n this._isShown = false\n this._isTransitioning = true\n this._focustrap.deactivate()\n\n this._element.classList.remove(CLASS_NAME_SHOW)\n\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated())\n }\n\n dispose() {\n EventHandler.off(window, EVENT_KEY)\n EventHandler.off(this._dialog, EVENT_KEY)\n\n this._backdrop.dispose()\n this._focustrap.deactivate()\n\n super.dispose()\n }\n\n handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n })\n }\n\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n })\n }\n\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element)\n }\n\n this._element.style.display = 'block'\n this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n this._element.setAttribute('role', 'dialog')\n this._element.scrollTop = 0\n\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)\n if (modalBody) {\n modalBody.scrollTop = 0\n }\n\n reflow(this._element)\n\n this._element.classList.add(CLASS_NAME_SHOW)\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate()\n }\n\n this._isTransitioning = false\n EventHandler.trigger(this._element, EVENT_SHOWN, {\n relatedTarget\n })\n }\n\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated())\n }\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return\n }\n\n if (this._config.keyboard) {\n this.hide()\n return\n }\n\n this._triggerBackdropTransition()\n })\n\n EventHandler.on(window, EVENT_RESIZE, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog()\n }\n })\n\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return\n }\n\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition()\n return\n }\n\n if (this._config.backdrop) {\n this.hide()\n }\n })\n })\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._element.removeAttribute('role')\n this._isTransitioning = false\n\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN)\n this._resetAdjustments()\n this._scrollBar.reset()\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n })\n }\n\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE)\n }\n\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n if (hideEvent.defaultPrevented) {\n return\n }\n\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n const initialOverflowY = this._element.style.overflowY\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return\n }\n\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden'\n }\n\n this._element.classList.add(CLASS_NAME_STATIC)\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC)\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY\n }, this._dialog)\n }, this._dialog)\n\n this._element.focus()\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n const scrollbarWidth = this._scrollBar.getWidth()\n const isBodyOverflowing = scrollbarWidth > 0\n\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight'\n this._element.style[property] = `${scrollbarWidth}px`\n }\n\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft'\n this._element.style[property] = `${scrollbarWidth}px`\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](relatedTarget)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n EventHandler.one(target, EVENT_SHOW, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return\n }\n\n EventHandler.one(target, EVENT_HIDDEN, () => {\n if (isVisible(this)) {\n this.focus()\n }\n })\n })\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide()\n }\n\n const data = Modal.getOrCreateInstance(target)\n\n data.toggle(this)\n})\n\nenableDismissTrigger(Modal)\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal)\n\nexport default Modal\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport Backdrop from './util/backdrop.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport FocusTrap from './util/focustrap.js'\nimport {\n defineJQueryPlugin,\n isDisabled,\n isVisible\n} from './util/index.js'\nimport ScrollBarHelper from './util/scrollbar.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'offcanvas'\nconst DATA_KEY = 'bs.offcanvas'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\nconst ESCAPE_KEY = 'Escape'\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_SHOWING = 'showing'\nconst CLASS_NAME_HIDING = 'hiding'\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop'\nconst OPEN_SELECTOR = '.offcanvas.show'\n\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_RESIZE = `resize${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`\n\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"offcanvas\"]'\n\nconst Default = {\n backdrop: true,\n keyboard: true,\n scroll: false\n}\n\nconst DefaultType = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n this._isShown = false\n this._backdrop = this._initializeBackDrop()\n this._focustrap = this._initializeFocusTrap()\n this._addEventListeners()\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget })\n\n if (showEvent.defaultPrevented) {\n return\n }\n\n this._isShown = true\n this._backdrop.show()\n\n if (!this._config.scroll) {\n new ScrollBarHelper().hide()\n }\n\n this._element.setAttribute('aria-modal', true)\n this._element.setAttribute('role', 'dialog')\n this._element.classList.add(CLASS_NAME_SHOWING)\n\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate()\n }\n\n this._element.classList.add(CLASS_NAME_SHOW)\n this._element.classList.remove(CLASS_NAME_SHOWING)\n EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })\n }\n\n this._queueCallback(completeCallBack, this._element, true)\n }\n\n hide() {\n if (!this._isShown) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n\n if (hideEvent.defaultPrevented) {\n return\n }\n\n this._focustrap.deactivate()\n this._element.blur()\n this._isShown = false\n this._element.classList.add(CLASS_NAME_HIDING)\n this._backdrop.hide()\n\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW, CLASS_NAME_HIDING)\n this._element.removeAttribute('aria-modal')\n this._element.removeAttribute('role')\n\n if (!this._config.scroll) {\n new ScrollBarHelper().reset()\n }\n\n EventHandler.trigger(this._element, EVENT_HIDDEN)\n }\n\n this._queueCallback(completeCallback, this._element, true)\n }\n\n dispose() {\n this._backdrop.dispose()\n this._focustrap.deactivate()\n super.dispose()\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n return\n }\n\n this.hide()\n }\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop)\n\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n })\n }\n\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n })\n }\n\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return\n }\n\n if (this._config.keyboard) {\n this.hide()\n return\n }\n\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n })\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config](this)\n })\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this)\n\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault()\n }\n\n if (isDisabled(this)) {\n return\n }\n\n EventHandler.one(target, EVENT_HIDDEN, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus()\n }\n })\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide()\n }\n\n const data = Offcanvas.getOrCreateInstance(target)\n data.toggle(this)\n})\n\nEventHandler.on(window, EVENT_LOAD_DATA_API, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show()\n }\n})\n\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide()\n }\n }\n})\n\nenableDismissTrigger(Offcanvas)\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas)\n\nexport default Offcanvas\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i\n\nexport const DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n dd: [],\n div: [],\n dl: [],\n dt: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n}\n// js-docs-end allow-list\n\nconst uriAttributes = new Set([\n 'background',\n 'cite',\n 'href',\n 'itemtype',\n 'longdesc',\n 'poster',\n 'src',\n 'xlink:href'\n])\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i\n\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase()\n\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue))\n }\n\n return true\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)\n .some(regex => regex.test(attributeName))\n}\n\nexport function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml\n }\n\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml)\n }\n\n const domParser = new window.DOMParser()\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'))\n\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase()\n\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove()\n continue\n }\n\n const attributeList = [].concat(...element.attributes)\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || [])\n\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName)\n }\n }\n }\n\n return createdDocument.body.innerHTML\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport SelectorEngine from '../dom/selector-engine.js'\nimport Config from './config.js'\nimport { DefaultAllowlist, sanitizeHtml } from './sanitizer.js'\nimport { execute, getElement, isElement } from './index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'TemplateFactory'\n\nconst Default = {\n allowList: DefaultAllowlist,\n content: {}, // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
    '\n}\n\nconst DefaultType = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n}\n\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n}\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super()\n this._config = this._getConfig(config)\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content)\n .map(config => this._resolvePossibleFunction(config))\n .filter(Boolean)\n }\n\n hasContent() {\n return this.getContent().length > 0\n }\n\n changeContent(content) {\n this._checkContent(content)\n this._config.content = { ...this._config.content, ...content }\n return this\n }\n\n toHtml() {\n const templateWrapper = document.createElement('div')\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template)\n\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector)\n }\n\n const template = templateWrapper.children[0]\n const extraClass = this._resolvePossibleFunction(this._config.extraClass)\n\n if (extraClass) {\n template.classList.add(...extraClass.split(' '))\n }\n\n return template\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config)\n this._checkContent(config.content)\n }\n\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({ selector, entry: content }, DefaultContentType)\n }\n }\n\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template)\n\n if (!templateElement) {\n return\n }\n\n content = this._resolvePossibleFunction(content)\n\n if (!content) {\n templateElement.remove()\n return\n }\n\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement)\n return\n }\n\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content)\n return\n }\n\n templateElement.textContent = content\n }\n\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg\n }\n\n _resolvePossibleFunction(arg) {\n return execute(arg, [undefined, this])\n }\n\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = ''\n templateElement.append(element)\n return\n }\n\n templateElement.textContent = element.textContent\n }\n}\n\nexport default TemplateFactory\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport * as Popper from '@popperjs/core'\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport Manipulator from './dom/manipulator.js'\nimport {\n defineJQueryPlugin, execute, findShadowRoot, getElement, getUID, isRTL, noop\n} from './util/index.js'\nimport { DefaultAllowlist } from './util/sanitizer.js'\nimport TemplateFactory from './util/template-factory.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'tooltip'\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])\n\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_MODAL = 'modal'\nconst CLASS_NAME_SHOW = 'show'\n\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner'\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`\n\nconst EVENT_MODAL_HIDE = 'hide.bs.modal'\n\nconst TRIGGER_HOVER = 'hover'\nconst TRIGGER_FOCUS = 'focus'\nconst TRIGGER_CLICK = 'click'\nconst TRIGGER_MANUAL = 'manual'\n\nconst EVENT_HIDE = 'hide'\nconst EVENT_HIDDEN = 'hidden'\nconst EVENT_SHOW = 'show'\nconst EVENT_SHOWN = 'shown'\nconst EVENT_INSERTED = 'inserted'\nconst EVENT_CLICK = 'click'\nconst EVENT_FOCUSIN = 'focusin'\nconst EVENT_FOCUSOUT = 'focusout'\nconst EVENT_MOUSEENTER = 'mouseenter'\nconst EVENT_MOUSELEAVE = 'mouseleave'\n\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n}\n\nconst Default = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
    ' +\n '
    ' +\n '
    ' +\n '
    ',\n title: '',\n trigger: 'hover focus'\n}\n\nconst DefaultType = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n}\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org/docs/v2/)')\n }\n\n super(element, config)\n\n // Private\n this._isEnabled = true\n this._timeout = 0\n this._isHovered = null\n this._activeTrigger = {}\n this._popper = null\n this._templateFactory = null\n this._newContent = null\n\n // Protected\n this.tip = null\n\n this._setListeners()\n\n if (!this._config.selector) {\n this._fixTitle()\n }\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle() {\n if (!this._isEnabled) {\n return\n }\n\n if (this._isShown()) {\n this._leave()\n return\n }\n\n this._enter()\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)\n\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'))\n }\n\n this._disposePopper()\n super.dispose()\n }\n\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n if (!(this._isWithContent() && this._isEnabled)) {\n return\n }\n\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW))\n const shadowRoot = findShadowRoot(this._element)\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element)\n\n if (showEvent.defaultPrevented || !isInTheDom) {\n return\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper()\n\n const tip = this._getTipElement()\n\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'))\n\n const { container } = this._config\n\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip)\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))\n }\n\n this._popper = this._createPopper(tip)\n\n tip.classList.add(CLASS_NAME_SHOW)\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop)\n }\n }\n\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN))\n\n if (this._isHovered === false) {\n this._leave()\n }\n\n this._isHovered = false\n }\n\n this._queueCallback(complete, this.tip, this._isAnimated())\n }\n\n hide() {\n if (!this._isShown()) {\n return\n }\n\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE))\n if (hideEvent.defaultPrevented) {\n return\n }\n\n const tip = this._getTipElement()\n tip.classList.remove(CLASS_NAME_SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop)\n }\n }\n\n this._activeTrigger[TRIGGER_CLICK] = false\n this._activeTrigger[TRIGGER_FOCUS] = false\n this._activeTrigger[TRIGGER_HOVER] = false\n this._isHovered = null // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return\n }\n\n if (!this._isHovered) {\n this._disposePopper()\n }\n\n this._element.removeAttribute('aria-describedby')\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN))\n }\n\n this._queueCallback(complete, this.tip, this._isAnimated())\n }\n\n update() {\n if (this._popper) {\n this._popper.update()\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle())\n }\n\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate())\n }\n\n return this.tip\n }\n\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml()\n\n // TODO: remove this check in v6\n if (!tip) {\n return null\n }\n\n tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`)\n\n const tipId = getUID(this.constructor.NAME).toString()\n\n tip.setAttribute('id', tipId)\n\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE)\n }\n\n return tip\n }\n\n setContent(content) {\n this._newContent = content\n if (this._isShown()) {\n this._disposePopper()\n this.show()\n }\n }\n\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content)\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n })\n }\n\n return this._templateFactory\n }\n\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n }\n }\n\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title')\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig())\n }\n\n _isAnimated() {\n return this._config.animation || (this.tip && this.tip.classList.contains(CLASS_NAME_FADE))\n }\n\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW)\n }\n\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element])\n const attachment = AttachmentMap[placement.toUpperCase()]\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))\n }\n\n _getOffset() {\n const { offset } = this._config\n\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10))\n }\n\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element)\n }\n\n return offset\n }\n\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element, this._element])\n }\n\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [\n {\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n },\n {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n },\n {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n },\n {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n },\n {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement)\n }\n }\n ]\n }\n\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])\n }\n }\n\n _setListeners() {\n const triggers = this._config.trigger.split(' ')\n\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK])\n context.toggle()\n })\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ?\n this.constructor.eventName(EVENT_MOUSEENTER) :\n this.constructor.eventName(EVENT_FOCUSIN)\n const eventOut = trigger === TRIGGER_HOVER ?\n this.constructor.eventName(EVENT_MOUSELEAVE) :\n this.constructor.eventName(EVENT_FOCUSOUT)\n\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true\n context._enter()\n })\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event)\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] =\n context._element.contains(event.relatedTarget)\n\n context._leave()\n })\n }\n }\n\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide()\n }\n }\n\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)\n }\n\n _fixTitle() {\n const title = this._element.getAttribute('title')\n\n if (!title) {\n return\n }\n\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title)\n }\n\n this._element.setAttribute('data-bs-original-title', title) // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title')\n }\n\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true\n return\n }\n\n this._isHovered = true\n\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show()\n }\n }, this._config.delay.show)\n }\n\n _leave() {\n if (this._isWithActiveTrigger()) {\n return\n }\n\n this._isHovered = false\n\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide()\n }\n }, this._config.delay.hide)\n }\n\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout)\n this._timeout = setTimeout(handler, timeout)\n }\n\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true)\n }\n\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element)\n\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute]\n }\n }\n\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n }\n config = this._mergeConfigObj(config)\n config = this._configAfterMerge(config)\n this._typeCheckConfig(config)\n return config\n }\n\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container)\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n }\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString()\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString()\n }\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value\n }\n }\n\n config.selector = false\n config.trigger = 'manual'\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config\n }\n\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy()\n this._popper = null\n }\n\n if (this.tip) {\n this.tip.remove()\n this.tip = null\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip)\n\nexport default Tooltip\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport Tooltip from './tooltip.js'\nimport { defineJQueryPlugin } from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'popover'\n\nconst SELECTOR_TITLE = '.popover-header'\nconst SELECTOR_CONTENT = '.popover-body'\n\nconst Default = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
    ' +\n '
    ' +\n '

    ' +\n '
    ' +\n '
    ',\n trigger: 'click'\n}\n\nconst DefaultType = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n}\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent()\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n }\n }\n\n _getContent() {\n return this._resolvePossibleFunction(this._config.content)\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config)\n\n if (typeof config !== 'string') {\n return\n }\n\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n\n data[config]()\n })\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover)\n\nexport default Popover\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport {\n defineJQueryPlugin, getElement, isDisabled, isVisible\n} from './util/index.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'scrollspy'\nconst DATA_KEY = 'bs.scrollspy'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\n\nconst EVENT_ACTIVATE = `activate${EVENT_KEY}`\nconst EVENT_CLICK = `click${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'\nconst CLASS_NAME_ACTIVE = 'active'\n\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]'\nconst SELECTOR_TARGET_LINKS = '[href]'\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'\nconst SELECTOR_NAV_LINKS = '.nav-link'\nconst SELECTOR_NAV_ITEMS = '.nav-item'\nconst SELECTOR_LIST_ITEMS = '.list-group-item'\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`\nconst SELECTOR_DROPDOWN = '.dropdown'\nconst SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'\n\nconst Default = {\n offset: null, // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n}\n\nconst DefaultType = {\n offset: '(number|null)', // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n}\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config)\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map()\n this._observableSections = new Map()\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element\n this._activeTarget = null\n this._observer = null\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n }\n this.refresh() // initialize\n }\n\n // Getters\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get NAME() {\n return NAME\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables()\n this._maybeEnableSmoothScroll()\n\n if (this._observer) {\n this._observer.disconnect()\n } else {\n this._observer = this._getNewObserver()\n }\n\n for (const section of this._observableSections.values()) {\n this._observer.observe(section)\n }\n }\n\n dispose() {\n this._observer.disconnect()\n super.dispose()\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin\n\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value))\n }\n\n return config\n }\n\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK)\n\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash)\n if (observableSection) {\n event.preventDefault()\n const root = this._rootElement || window\n const height = observableSection.offsetTop - this._element.offsetTop\n if (root.scrollTo) {\n root.scrollTo({ top: height, behavior: 'smooth' })\n return\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height\n }\n })\n }\n\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n }\n\n return new IntersectionObserver(entries => this._observerCallback(entries), options)\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`)\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop\n this._process(targetElement(entry))\n }\n\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop\n this._previousScrollData.parentScrollTop = parentScrollTop\n\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null\n this._clearActiveClass(targetElement(entry))\n\n continue\n }\n\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry)\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return\n }\n\n continue\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry)\n }\n }\n }\n\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map()\n this._observableSections = new Map()\n\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target)\n\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue\n }\n\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element)\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor)\n this._observableSections.set(anchor.hash, observableSection)\n }\n }\n }\n\n _process(target) {\n if (this._activeTarget === target) {\n return\n }\n\n this._clearActiveClass(this._config.target)\n this._activeTarget = target\n target.classList.add(CLASS_NAME_ACTIVE)\n this._activateParents(target)\n\n EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target })\n }\n\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN))\n .classList.add(CLASS_NAME_ACTIVE)\n return\n }\n\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both