diff --git a/build.sbt b/build.sbt index edb8675bc..b6f608a97 100644 --- a/build.sbt +++ b/build.sbt @@ -154,7 +154,8 @@ lazy val io = project.in(file("io")) .settings( name := "laika-io", libraryDependencies ++= Seq(catsEffect, fs2IO, munit, munitCE3), - Test / scalacOptions ~= disableMissingInterpolatorWarning + Test / scalacOptions ~= disableMissingInterpolatorWarning, + mimaBinaryIssueFilters ++= MimaFilters.includeRefactoring ) lazy val pdf = project.in(file("pdf")) diff --git a/io/src/main/scala/laika/helium/config/api.scala b/io/src/main/scala/laika/helium/config/api.scala index c5e6ac8a8..7a148025f 100644 --- a/io/src/main/scala/laika/helium/config/api.scala +++ b/io/src/main/scala/laika/helium/config/api.scala @@ -26,6 +26,8 @@ import laika.theme.config.{ Color, DocumentMetadata, FontDefinition, + IncludeCSSConfig, + IncludeJSConfig, ScriptAttributes, StyleAttributes } @@ -445,12 +447,12 @@ private[helium] trait SiteOps extends SingleConfigOps with CopyOps { } - private def withStyleIncludes(newValue: StyleIncludes): Helium = { + private def withStyleIncludes(newValue: IncludeCSSConfig): Helium = { val newContent = currentContent.copy(styleIncludes = newValue) copyWith(helium.siteSettings.copy(content = newContent)) } - private def withScriptIncludes(newValue: ScriptIncludes): Helium = { + private def withScriptIncludes(newValue: IncludeJSConfig): Helium = { val newContent = currentContent.copy(scriptIncludes = newValue) copyWith(helium.siteSettings.copy(content = newContent)) } @@ -464,8 +466,7 @@ private[helium] trait SiteOps extends SingleConfigOps with CopyOps { attributes: StyleAttributes = StyleAttributes.defaults, condition: Document => Boolean = _ => true ): Helium = { - val newInclude = ExternalCSS(url, attributes, condition) - val styleIncludes = currentContent.styleIncludes.add(newInclude) + val styleIncludes = currentContent.styleIncludes.externalCSS(url, attributes, condition) withStyleIncludes(styleIncludes) } @@ -481,8 +482,7 @@ private[helium] trait SiteOps extends SingleConfigOps with CopyOps { attributes: StyleAttributes = StyleAttributes.defaults, condition: Document => Boolean = _ => true ): Helium = { - val newInclude = InternalCSS(searchPath, attributes, condition) - val styleIncludes = currentContent.styleIncludes.add(newInclude) + val styleIncludes = currentContent.styleIncludes.internalCSS(searchPath, attributes, condition) withStyleIncludes(styleIncludes) } @@ -494,8 +494,7 @@ private[helium] trait SiteOps extends SingleConfigOps with CopyOps { content: String, condition: Document => Boolean = _ => true ): Helium = { - val newInclude = InlineCSS(content, condition) - val styleIncludes = currentContent.styleIncludes.add(newInclude) + val styleIncludes = currentContent.styleIncludes.inlineCSS(content, condition) withStyleIncludes(styleIncludes) } @@ -508,8 +507,7 @@ private[helium] trait SiteOps extends SingleConfigOps with CopyOps { attributes: ScriptAttributes = ScriptAttributes.defaults, condition: Document => Boolean = _ => true ): Helium = { - val newInclude = ExternalJS(url, attributes, condition) - val scriptIncludes = currentContent.scriptIncludes.add(newInclude) + val scriptIncludes = currentContent.scriptIncludes.externalJS(url, attributes, condition) withScriptIncludes(scriptIncludes) } @@ -525,8 +523,7 @@ private[helium] trait SiteOps extends SingleConfigOps with CopyOps { attributes: ScriptAttributes = ScriptAttributes.defaults, condition: Document => Boolean = _ => true ): Helium = { - val newInclude = InternalJS(searchPath, attributes, condition) - val scriptIncludes = currentContent.scriptIncludes.add(newInclude) + val scriptIncludes = currentContent.scriptIncludes.internalJS(searchPath, attributes, condition) withScriptIncludes(scriptIncludes) } @@ -539,8 +536,7 @@ private[helium] trait SiteOps extends SingleConfigOps with CopyOps { isModule: Boolean = false, condition: Document => Boolean = _ => true ): Helium = { - val newInclude = InlineJS(content, isModule, condition) - val scriptIncludes = currentContent.scriptIncludes.add(newInclude) + val scriptIncludes = currentContent.scriptIncludes.inlineJS(content, isModule, condition) withScriptIncludes(scriptIncludes) } @@ -976,10 +972,10 @@ private[helium] trait EPUBOps extends SingleConfigOps with CopyOps { copyWith(helium.epubSettings.copy(layout = newLayout)) } - private def withStyleIncludes(newValue: StyleIncludes): Helium = + private def withStyleIncludes(newValue: IncludeCSSConfig): Helium = copyWith(helium.epubSettings.copy(styleIncludes = newValue)) - private def withScriptIncludes(newValue: ScriptIncludes): Helium = + private def withScriptIncludes(newValue: IncludeJSConfig): Helium = copyWith(helium.epubSettings.copy(scriptIncludes = newValue)) /** Auto-links CSS documents from the specified path, which may point to a single CSS document @@ -994,8 +990,8 @@ private[helium] trait EPUBOps extends SingleConfigOps with CopyOps { attributes: StyleAttributes = StyleAttributes.defaults, condition: Document => Boolean = _ => true ): Helium = { - val newInclude = InternalCSS(searchPath, attributes, condition) - val styleIncludes = helium.epubSettings.styleIncludes.add(newInclude) + val styleIncludes = + helium.epubSettings.styleIncludes.internalCSS(searchPath, attributes, condition) withStyleIncludes(styleIncludes) } @@ -1007,8 +1003,7 @@ private[helium] trait EPUBOps extends SingleConfigOps with CopyOps { content: String, condition: Document => Boolean = _ => true ): Helium = { - val newInclude = InlineCSS(content, condition) - val styleIncludes = helium.epubSettings.styleIncludes.add(newInclude) + val styleIncludes = helium.epubSettings.styleIncludes.inlineCSS(content, condition) withStyleIncludes(styleIncludes) } @@ -1024,8 +1019,8 @@ private[helium] trait EPUBOps extends SingleConfigOps with CopyOps { attributes: ScriptAttributes = ScriptAttributes.defaults, condition: Document => Boolean = _ => true ): Helium = { - val newInclude = InternalJS(searchPath, attributes, condition) - val scriptIncludes = helium.epubSettings.scriptIncludes.add(newInclude) + val scriptIncludes = + helium.epubSettings.scriptIncludes.internalJS(searchPath, attributes, condition) withScriptIncludes(scriptIncludes) } @@ -1038,8 +1033,7 @@ private[helium] trait EPUBOps extends SingleConfigOps with CopyOps { isModule: Boolean = false, condition: Document => Boolean = _ => true ): Helium = { - val newInclude = InlineJS(content, isModule, condition) - val scriptIncludes = helium.epubSettings.scriptIncludes.add(newInclude) + val scriptIncludes = helium.epubSettings.scriptIncludes.inlineJS(content, isModule, condition) withScriptIncludes(scriptIncludes) } diff --git a/io/src/main/scala/laika/helium/internal/builder/HeliumDirectives.scala b/io/src/main/scala/laika/helium/internal/builder/HeliumDirectives.scala index 47c9ae154..48ba26058 100644 --- a/io/src/main/scala/laika/helium/internal/builder/HeliumDirectives.scala +++ b/io/src/main/scala/laika/helium/internal/builder/HeliumDirectives.scala @@ -21,6 +21,7 @@ import laika.api.bundle.{ PathTranslator, TemplateDirectives } import laika.ast.{ TemplateSpanSequence, TemplateString } import laika.config.{ LaikaKeys, Versions } import laika.helium.Helium +import laika.theme.config.IncludeDirective /** @author Jens Halm */ @@ -68,11 +69,11 @@ private[helium] object HeliumDirectives { Seq( initVersions, initPreview, - HeliumHeadDirectives.includeCSS( + IncludeDirective.forCSS( helium.siteSettings.content.styleIncludes, helium.epubSettings.styleIncludes ), - HeliumHeadDirectives.includeJS( + IncludeDirective.forJS( helium.siteSettings.content.scriptIncludes, helium.epubSettings.scriptIncludes ) diff --git a/io/src/main/scala/laika/helium/internal/config/layout.scala b/io/src/main/scala/laika/helium/internal/config/layout.scala index 4844bcf2d..0979bdbe3 100644 --- a/io/src/main/scala/laika/helium/internal/config/layout.scala +++ b/io/src/main/scala/laika/helium/internal/config/layout.scala @@ -4,6 +4,7 @@ import laika.ast.Path.Root import laika.ast.* import laika.helium.config.* import laika.parse.{ SourceCursor, SourceFragment } +import laika.theme.config.{ IncludeCSSConfig, IncludeJSConfig } private[helium] sealed trait CommonLayout { def defaultBlockSpacing: Length @@ -21,8 +22,8 @@ private[helium] case class WebLayout( private[helium] case class WebContent( favIcons: Seq[Favicon] = Nil, - styleIncludes: StyleIncludes = StyleIncludes.empty, - scriptIncludes: ScriptIncludes = ScriptIncludes.empty, + styleIncludes: IncludeCSSConfig = IncludeCSSConfig.empty, + scriptIncludes: IncludeJSConfig = IncludeJSConfig.empty, topNavigationBar: TopNavigationBar = TopNavigationBar.default, mainNavigation: MainNavigation = MainNavigation(), pageNavigation: PageNavigation = PageNavigation(), diff --git a/io/src/main/scala/laika/helium/internal/config/settings.scala b/io/src/main/scala/laika/helium/internal/config/settings.scala index aeb920c3a..729c34c60 100644 --- a/io/src/main/scala/laika/helium/internal/config/settings.scala +++ b/io/src/main/scala/laika/helium/internal/config/settings.scala @@ -1,7 +1,13 @@ package laika.helium.internal.config import laika.config.{ CoverImage, Versions } -import laika.theme.config.{ BookConfig, DocumentMetadata, FontDefinition } +import laika.theme.config.{ + BookConfig, + DocumentMetadata, + FontDefinition, + IncludeCSSConfig, + IncludeJSConfig +} private[helium] trait CommonSettings { def themeFonts: ThemeFonts @@ -45,8 +51,8 @@ private[helium] case class EPUBSettings( fontSizes: FontSizes, colors: ColorSet, darkMode: Option[ColorSet], - styleIncludes: StyleIncludes = StyleIncludes.empty, - scriptIncludes: ScriptIncludes = ScriptIncludes.empty, + styleIncludes: IncludeCSSConfig = IncludeCSSConfig.empty, + scriptIncludes: IncludeJSConfig = IncludeJSConfig.empty, layout: EPUBLayout, coverImages: Seq[CoverImage] ) extends DarkModeSupport { diff --git a/io/src/main/scala/laika/theme/config/IncludeDirective.scala b/io/src/main/scala/laika/theme/config/IncludeDirective.scala new file mode 100644 index 000000000..83a765202 --- /dev/null +++ b/io/src/main/scala/laika/theme/config/IncludeDirective.scala @@ -0,0 +1,181 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package laika.theme.config + +import laika.api.bundle.TemplateDirectives +import laika.ast.{ Document, Path } +import laika.theme.config.internal.{ + ExternalCSS, + ExternalJS, + IncludeDirectiveBuilder, + InlineCSS, + InlineJS, + InternalCSS, + InternalJS, + ScriptIncludes, + StyleIncludes +} + +/** Represents the configuration for the `@:includeCSS` directive + * for a single format (e.g. HTML or EPUB). + */ +class IncludeCSSConfig private ( + private[config] val includes: StyleIncludes = StyleIncludes.empty +) { + + /** Links an external CSS resource from the specified URL. + * + * The `condition` attribute can be used to only include the CSS when some user-defined predicates are satisfied. + */ + def externalCSS( + url: String, + attributes: StyleAttributes = StyleAttributes.defaults, + condition: Document => Boolean = _ => true + ): IncludeCSSConfig = { + val newInclude = ExternalCSS(url, attributes, condition) + new IncludeCSSConfig(includes.add(newInclude)) + } + + /** Auto-links CSS documents from the specified path, which may point to a single CSS document + * or a directory. + * In case of a directory it will be searched recursively and all CSS files found within it + * will be linked in the HTML head. + * + * The `condition` attribute can be used to only include the CSS when some user-defined predicates are satisfied. + */ + def internalCSS( + searchPath: Path, + attributes: StyleAttributes = StyleAttributes.defaults, + condition: Document => Boolean = _ => true + ): IncludeCSSConfig = { + val newInclude = InternalCSS(searchPath, attributes, condition) + new IncludeCSSConfig(includes.add(newInclude)) + } + + /** Inserts inline style declarations into the HTML head. + * + * The `condition` attribute can be used to only include the CSS when some user-defined predicates are satisfied. + */ + def inlineCSS( + content: String, + condition: Document => Boolean = _ => true + ): IncludeCSSConfig = { + val newInclude = InlineCSS(content, condition) + new IncludeCSSConfig(includes.add(newInclude)) + } + +} + +object IncludeCSSConfig { + + val empty: IncludeCSSConfig = new IncludeCSSConfig() + +} + +/** Represents the configuration for the `@:includeJS` directive + * for a single format (e.g. HTML or EPUB). + */ +class IncludeJSConfig private ( + private[config] val includes: ScriptIncludes = ScriptIncludes.empty +) { + + /** Links an external JavaScript resource from the specified URL. + * + * The `condition` attribute can be used to only include the CSS when some user-defined predicates are satisfied. + */ + def externalJS( + url: String, + attributes: ScriptAttributes = ScriptAttributes.defaults, + condition: Document => Boolean = _ => true + ): IncludeJSConfig = { + val newInclude = ExternalJS(url, attributes, condition) + new IncludeJSConfig(includes.add(newInclude)) + } + + /** Auto-links JavaScript documents from the specified path, which may point to a single JS document + * or a directory. + * In case of a directory it will be searched recursively and all `*.js` files found within it + * will be linked in the HTML head. + * + * The `condition` attribute can be used to only include the CSS when some user-defined predicates are satisfied. + */ + def internalJS( + searchPath: Path, + attributes: ScriptAttributes = ScriptAttributes.defaults, + condition: Document => Boolean = _ => true + ): IncludeJSConfig = { + val newInclude = InternalJS(searchPath, attributes, condition) + new IncludeJSConfig(includes.add(newInclude)) + } + + /** Inserts inline scripts into the HTML head. + * + * The `condition` attribute can be used to only include the CSS when some user-defined predicates are satisfied. + */ + def inlineJS( + content: String, + isModule: Boolean = false, + condition: Document => Boolean = _ => true + ): IncludeJSConfig = { + val newInclude = InlineJS(content, isModule, condition) + new IncludeJSConfig(includes.add(newInclude)) + } + +} + +object IncludeJSConfig { + + val empty: IncludeJSConfig = new IncludeJSConfig() + +} + +/** Builders for the `@:includeCSS` and `@:includeJS` directives, + * which can be used in templates for HTML and EPUB output. + */ +object IncludeDirective { + + /** Creates an instance of the `@:includeCSS` directive that + * can be added to any `ExtensionBundle`. + * + * The configurations for HTML and EPUB are separate and either + * of the two can be empty. + * + * They can also point to the same instance in case the two formats + * should use the same style includes. + */ + def forCSS( + htmlConfig: IncludeCSSConfig, + epubConfig: IncludeCSSConfig = IncludeCSSConfig.empty + ): TemplateDirectives.Directive = + IncludeDirectiveBuilder.includeCSS(htmlConfig.includes, epubConfig.includes) + + /** Creates an instance of the `@:includeJS` directive that + * can be added to any `ExtensionBundle`. + * + * The configurations for HTML and EPUB are separate and either + * of the two can be empty. + * + * They can also point to the same instance in case the two formats + * should use the same script includes. + */ + def forJS( + htmlConfig: IncludeJSConfig, + epubConfig: IncludeJSConfig = IncludeJSConfig.empty + ): TemplateDirectives.Directive = + IncludeDirectiveBuilder.includeJS(htmlConfig.includes, epubConfig.includes) + +} diff --git a/io/src/main/scala/laika/helium/internal/builder/HeliumHeadDirectives.scala b/io/src/main/scala/laika/theme/config/internal/IncludeDirectiveBuilder.scala similarity index 96% rename from io/src/main/scala/laika/helium/internal/builder/HeliumHeadDirectives.scala rename to io/src/main/scala/laika/theme/config/internal/IncludeDirectiveBuilder.scala index 4a9d8a45c..444af895f 100644 --- a/io/src/main/scala/laika/helium/internal/builder/HeliumHeadDirectives.scala +++ b/io/src/main/scala/laika/theme/config/internal/IncludeDirectiveBuilder.scala @@ -14,16 +14,15 @@ * limitations under the License. */ -package laika.helium.internal.builder +package laika.theme.config.internal import cats.syntax.all.* import laika.api.bundle.TemplateDirectives -import laika.ast.Path.Root import laika.ast.* -import laika.helium.internal.config.{ InlineCSS, InlineJS, ScriptIncludes, StyleIncludes } +import laika.ast.Path.Root import laika.theme.config.{ CrossOrigin, ScriptAttributes, StyleAttributes } -private[helium] object HeliumHeadDirectives { +private[config] object IncludeDirectiveBuilder { private type Attributes = Seq[(String, String)] @@ -165,7 +164,7 @@ private[helium] object HeliumHeadDirectives { epubIncludes: StyleIncludes ): TemplateDirectives.Directive = TemplateDirectives.create("includeCSS") { - import TemplateDirectives.dsl._ + import TemplateDirectives.dsl.* val templateStart = """ Boolean ) -private[helium] case class ExternalJS( +private[config] case class ExternalJS( url: String, attributes: ScriptAttributes, condition: Document => Boolean ) -private[helium] case class InlineJS( +private[config] case class InlineJS( content: String, isModule: Boolean, condition: Document => Boolean ) -private[helium] case class ScriptIncludes( +private[config] case class ScriptIncludes( internal: Seq[InternalJS], external: Seq[ExternalJS], inlined: Seq[InlineJS] @@ -41,28 +57,28 @@ private[helium] case class ScriptIncludes( def isEmpty: Boolean = internal.isEmpty && external.isEmpty && inlined.isEmpty } -private[helium] object ScriptIncludes { +private[config] object ScriptIncludes { val empty: ScriptIncludes = ScriptIncludes(Nil, Nil, Nil) } -private[helium] case class InternalCSS( +private[config] case class InternalCSS( searchPath: Path, attributes: StyleAttributes, condition: Document => Boolean ) -private[helium] case class ExternalCSS( +private[config] case class ExternalCSS( url: String, attributes: StyleAttributes, condition: Document => Boolean ) -private[helium] case class InlineCSS( +private[config] case class InlineCSS( content: String, condition: Document => Boolean ) -private[helium] case class StyleIncludes( +private[config] case class StyleIncludes( internal: Seq[InternalCSS], external: Seq[ExternalCSS], inlined: Seq[InlineCSS] @@ -83,6 +99,6 @@ private[helium] case class StyleIncludes( } -private[helium] object StyleIncludes { +private[config] object StyleIncludes { val empty: StyleIncludes = StyleIncludes(Nil, Nil, Nil) } diff --git a/project/MimaFilters.scala b/project/MimaFilters.scala new file mode 100644 index 000000000..0afbdd90e --- /dev/null +++ b/project/MimaFilters.scala @@ -0,0 +1,30 @@ +import com.typesafe.tools.mima.core.{ MissingClassProblem, ProblemFilter, ProblemFilters } + +object MimaFilters { + + val includeRefactoring: Seq[ProblemFilter] = Seq( + ProblemFilters.exclude[MissingClassProblem]( + "laika.helium.internal.builder.HeliumHeadDirectives" + ), + ProblemFilters.exclude[MissingClassProblem]( + "laika.helium.internal.builder.HeliumHeadDirectives$" + ), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.StyleIncludes"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.StyleIncludes$"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.ScriptIncludes"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.ScriptIncludes$"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.ExternalCSS"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.ExternalCSS$"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.InternalCSS"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.InternalCSS$"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.InlineCSS"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.InlineCSS$"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.ExternalJS"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.ExternalJS$"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.InternalJS"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.InternalJS$"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.InlineJS"), + ProblemFilters.exclude[MissingClassProblem]("laika.helium.internal.config.InlineJS$") + ) + +}