diff --git a/docs/modifiers.md b/docs/modifiers.md index 99cade539..639597b33 100644 --- a/docs/modifiers.md +++ b/docs/modifiers.md @@ -301,6 +301,19 @@ val message = "hello world!" ``` ```` +In addition to the `process` method, the `PostModifier` trait also has several +life-cycle methods that signal when a `PostModifier` instance: +* Has started for the first time (`onStart`) when MDoc is launched; +* Just before compilation and processing occurs (`preProcess`) on each source +document file; +* Just after compilation and processing has finished (`postProcess`) on each +source document file; +* Has finished after processing the last source document file (`onExit`) +before MDoc terminates. +These methods can be used to initialize and deactivate resources required by +the `PostModifier` instances. + + ## StringModifier A `StringModifier` is a custom modifier that processes the plain text contents diff --git a/mdoc-docs/src/main/resources/META-INF/services/mdoc.PostModifier b/mdoc-docs/src/main/resources/META-INF/services/mdoc.PostModifier index eaf64a23e..b1ee8407f 100644 --- a/mdoc-docs/src/main/resources/META-INF/services/mdoc.PostModifier +++ b/mdoc-docs/src/main/resources/META-INF/services/mdoc.PostModifier @@ -1 +1,2 @@ mdoc.docs.EvilplotModifier +mdoc.docs.LifeCycleModifier diff --git a/mdoc-docs/src/main/scala/mdoc/docs/EvilplotModifier.scala b/mdoc-docs/src/main/scala/mdoc/docs/EvilplotModifier.scala index 31e38745e..5b1e7435a 100644 --- a/mdoc-docs/src/main/scala/mdoc/docs/EvilplotModifier.scala +++ b/mdoc-docs/src/main/scala/mdoc/docs/EvilplotModifier.scala @@ -3,7 +3,9 @@ package mdoc.docs import com.cibo.evilplot.geometry.Drawable import java.nio.file.Files import java.nio.file.Paths + import mdoc._ + import scala.meta.inputs.Position class EvilplotModifier extends PostModifier { @@ -36,4 +38,8 @@ class EvilplotModifier extends PostModifier { "" } } + + override def onStart(settings: MainSettings): Unit = () + + override def onExit(exit: Exit): Unit = () } diff --git a/mdoc-docs/src/main/scala/mdoc/docs/LifeCycleModifier.scala b/mdoc-docs/src/main/scala/mdoc/docs/LifeCycleModifier.scala new file mode 100644 index 000000000..d373557cb --- /dev/null +++ b/mdoc-docs/src/main/scala/mdoc/docs/LifeCycleModifier.scala @@ -0,0 +1,42 @@ +package mdoc.docs + +import mdoc._ + +/** + * Global counter used to test the [[mdoc.Main]] process counting. + */ +object LifeCycleCounter { + val numberOfStarts: ThreadLocal[Integer] = ThreadLocal.withInitial(() => 0) + val numberOfExists: ThreadLocal[Integer] = ThreadLocal.withInitial(() => 0) +} + +class LifeCycleModifier extends PostModifier { + val name = "lifecycle" + + // Starts and stops per instance + var numberOfStarts = 0 + var numberOfExists = 0 + + def process(ctx: PostModifierContext): String = { + // Used for checking the counting + s"numberOfStarts = $numberOfStarts ; numberOfExists = $numberOfExists" + } + + /** + * This is called once when the [[mdoc.Main]] process starts + * @param settings CLI or API settings used by mdoc + */ + override def onStart(settings: MainSettings): Unit = { + numberOfStarts += 1 + LifeCycleCounter.numberOfStarts.set(LifeCycleCounter.numberOfStarts.get() + 1) + } + + /** + * This is called once when the [[mdoc.Main]] process finsihes + * @param exit is the exit code returned by mdoc's processing + */ + override def onExit(exit: Exit): Unit = { + numberOfExists += 1 + LifeCycleCounter.numberOfExists.set(LifeCycleCounter.numberOfExists.get() + 1) + } +} diff --git a/mdoc/src/main/scala/mdoc/Exit.scala b/mdoc/src/main/scala/mdoc/Exit.scala new file mode 100644 index 000000000..b62e9ffa0 --- /dev/null +++ b/mdoc/src/main/scala/mdoc/Exit.scala @@ -0,0 +1,18 @@ +package mdoc + +import mdoc.internal.cli.{Exit => CliExit} + +final class Exit private[mdoc] (val exit: CliExit) { + def merge(other: Exit): Exit = Exit(exit.merge(other.exit)) + def isSuccess: Boolean = exit == CliExit.success + def isError: Boolean = exit == CliExit.error +} + +object Exit { + private[mdoc] def apply(exit: CliExit): Exit = { + new Exit(exit) + } + def apply(): Exit = { + Exit(CliExit.success) + } +} diff --git a/mdoc/src/main/scala/mdoc/Main.scala b/mdoc/src/main/scala/mdoc/Main.scala index a83ed7c37..e9a1f7aa2 100644 --- a/mdoc/src/main/scala/mdoc/Main.scala +++ b/mdoc/src/main/scala/mdoc/Main.scala @@ -8,7 +8,6 @@ import scala.meta.io.AbsolutePath import mdoc.internal.cli.MainOps import mdoc.internal.cli.Settings import mdoc.internal.io.ConsoleReporter -import mdoc.internal.markdown.Markdown object Main { @@ -23,10 +22,11 @@ object Main { def process(args: Array[String], reporter: Reporter, cwd: Path): Int = { val base = Settings.default(AbsolutePath(cwd)) val ctx = Settings.fromCliArgs(args.toList, base) - MainOps.process(ctx, reporter) + val mainSettings = ctx.andThen(s => Configured.ok(new MainSettings(s, reporter))) + MainOps.process(mainSettings, reporter) } def process(settings: MainSettings): Int = { - MainOps.process(Configured.ok(settings.settings), settings.reporter) + MainOps.process(Configured.ok(settings), settings.reporter) } } diff --git a/mdoc/src/main/scala/mdoc/MainSettings.scala b/mdoc/src/main/scala/mdoc/MainSettings.scala index 53ba30f43..d8f5624ad 100644 --- a/mdoc/src/main/scala/mdoc/MainSettings.scala +++ b/mdoc/src/main/scala/mdoc/MainSettings.scala @@ -11,7 +11,7 @@ import scala.meta.io.AbsolutePath import mdoc.internal.cli.Settings import mdoc.internal.io.ConsoleReporter -final class MainSettings private ( +final class MainSettings private[mdoc] ( private[mdoc] val settings: Settings, private[mdoc] val reporter: Reporter ) { diff --git a/mdoc/src/main/scala/mdoc/PostModifier.scala b/mdoc/src/main/scala/mdoc/PostModifier.scala index 9ec3053ee..6533907b6 100644 --- a/mdoc/src/main/scala/mdoc/PostModifier.scala +++ b/mdoc/src/main/scala/mdoc/PostModifier.scala @@ -1,19 +1,47 @@ package mdoc import java.util.ServiceLoader + import mdoc.internal.cli.Settings import metaconfig.ConfDecoder import metaconfig.ConfEncoder import metaconfig.ConfError import metaconfig.generic.Surface + import scala.meta.inputs.Input import scala.meta.io.AbsolutePath import scala.collection.JavaConverters._ import scala.meta.io.RelativePath +/** + * Interface of classes used for processing Markdown code fences. + * It provides method calls to set-up resources before processing + * sources, process code fences of all source files and release + * resource just before mdoc terminates. + * + */ trait PostModifier { val name: String + + /** + * This methods is called once just before mdoc starts processing all of the + * source files. Use this to set-up resources required by the post-modifier. + * + * @param settings setting set via the command line or directly vi the API + */ + def onStart(settings: MainSettings): Unit = () def process(ctx: PostModifierContext): String + + /** + * This methods is called once just after mdoc finished processing all of the + * source files. Use this to release or deactivate any resources that are not + * required by the post-modifier anymore. + * + * @param exit a value of 0 indicates mdoc processed all files with no error. + * a value of 1 indicates mdoc processing resulted in at least + * one error. + */ + def onExit(exit: Exit): Unit = () } object PostModifier { diff --git a/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala b/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala index 4c5fc3974..a5dfcfe58 100644 --- a/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala +++ b/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala @@ -6,7 +6,8 @@ import io.methvin.watcher.DirectoryChangeEvent import java.nio.file.Files import java.nio.file.StandardCopyOption import java.util.concurrent.Executors -import mdoc.Reporter + +import mdoc.{MainSettings, Reporter} import mdoc.internal.BuildInfo import mdoc.internal.io.IO import mdoc.internal.io.MdocFileListener @@ -17,6 +18,7 @@ import mdoc.internal.markdown.LinkHygiene import mdoc.internal.markdown.Markdown import mdoc.internal.pos.DiffUtils import metaconfig.Configured + import scala.meta.Input import scala.meta.internal.io.FileIO import scala.meta.internal.io.PathIO @@ -209,19 +211,19 @@ final class MainOps( } object MainOps { - def process(settings: Configured[Settings], reporter: Reporter): Int = { + def process(settings: Configured[MainSettings], reporter: Reporter): Int = { settings match { - case Configured.Ok(setting) if setting.help => + case Configured.Ok(setting) if setting.settings.help => reporter.println(Settings.help(BuildInfo.version, 80)) 0 - case Configured.Ok(setting) if setting.usage => + case Configured.Ok(setting) if setting.settings.usage => reporter.println(Settings.usage) 0 - case Configured.Ok(setting) if setting.version => + case Configured.Ok(setting) if setting.settings.version => reporter.println(Settings.version(BuildInfo.version)) 0 case els => - els.andThen(_.validate(reporter)) match { + els.andThen(_.settings.validate(reporter)) match { case Configured.NotOk(error) => error.all.foreach(message => reporter.error(message)) 1 @@ -229,8 +231,11 @@ object MainOps { if (ctx.settings.verbose) { ctx.reporter.setDebugEnabled(true) } + ctx.settings.postModifiers.foreach(_.onStart(settings.get)) val runner = new MainOps(ctx) val exit = runner.run() + val exitCode = mdoc.Exit(exit) + ctx.settings.postModifiers.foreach(_.onExit(exitCode)) if (exit.isSuccess) { 0 } else { diff --git a/tests/unit/src/test/resources/META-INF/services/mdoc.PostModifier b/tests/unit/src/test/resources/META-INF/services/mdoc.PostModifier index d806d0ae8..7bf54ea85 100644 --- a/tests/unit/src/test/resources/META-INF/services/mdoc.PostModifier +++ b/tests/unit/src/test/resources/META-INF/services/mdoc.PostModifier @@ -1,2 +1,4 @@ tests.markdown.EvilplotPostModifier tests.markdown.BulletPostModifier +tests.markdown.LifeCycleModifier + diff --git a/tests/unit/src/test/scala-2.11/tests/markdown/EvilplotPostModifier.scala b/tests/unit/src/test/scala-2.11/tests/markdown/EvilplotPostModifier.scala index 127f1b7b2..024f2d330 100644 --- a/tests/unit/src/test/scala-2.11/tests/markdown/EvilplotPostModifier.scala +++ b/tests/unit/src/test/scala-2.11/tests/markdown/EvilplotPostModifier.scala @@ -1,9 +1,13 @@ package tests.markdown -import mdoc.PostModifier -import mdoc.PostModifierContext +import mdoc.{MainSettings, PostModifier, PostModifierContext, Exit} class EvilplotPostModifier extends PostModifier { val name = "evilplot" + def process(ctx: PostModifierContext): String = "" + + override def onStart(settings: MainSettings): Unit = () + + override def onExit(exit: Exit): Unit = () } diff --git a/tests/unit/src/test/scala-2.12/tests/markdown/EvilplotPostModifier.scala b/tests/unit/src/test/scala-2.12/tests/markdown/EvilplotPostModifier.scala index bfd3e3191..b78eba353 100644 --- a/tests/unit/src/test/scala-2.12/tests/markdown/EvilplotPostModifier.scala +++ b/tests/unit/src/test/scala-2.12/tests/markdown/EvilplotPostModifier.scala @@ -2,8 +2,8 @@ package tests.markdown import com.cibo.evilplot.geometry.Drawable import java.nio.file.Files -import mdoc.PostModifier -import mdoc.PostModifierContext + +import mdoc.{Exit, MainSettings, PostModifier, PostModifierContext} class EvilplotPostModifier extends PostModifier { val name = "evilplot" @@ -28,4 +28,9 @@ class EvilplotPostModifier extends PostModifier { "" } } + + override def onStart(settings: MainSettings): Unit = () + + override def onExit(exit: Exit): Unit = () + } diff --git a/tests/unit/src/test/scala-2.12/tests/markdown/PostModifierSuite.scala b/tests/unit/src/test/scala-2.12/tests/markdown/PostModifierSuite.scala index 3f7be5aac..47e07b1b2 100644 --- a/tests/unit/src/test/scala-2.12/tests/markdown/PostModifierSuite.scala +++ b/tests/unit/src/test/scala-2.12/tests/markdown/PostModifierSuite.scala @@ -53,4 +53,14 @@ class PostModifierSuite extends BaseMarkdownSuite { "error: expected int runtime value. Obtained message" ) + check( + "lifecycle-1", + """ + |```scala mdoc:lifecycle + |val x = "message" + |``` + """.stripMargin, + "numberOfStarts = 0 ; numberOfExists = 0" + ) + } diff --git a/tests/unit/src/test/scala-2.13/tests/markdown/EvilplotPostModifier.scala b/tests/unit/src/test/scala-2.13/tests/markdown/EvilplotPostModifier.scala index 127f1b7b2..ad7517edd 100644 --- a/tests/unit/src/test/scala-2.13/tests/markdown/EvilplotPostModifier.scala +++ b/tests/unit/src/test/scala-2.13/tests/markdown/EvilplotPostModifier.scala @@ -1,9 +1,12 @@ package tests.markdown -import mdoc.PostModifier -import mdoc.PostModifierContext +import mdoc.{MainSettings, PostModifier, PostModifierContext, Exit} class EvilplotPostModifier extends PostModifier { val name = "evilplot" def process(ctx: PostModifierContext): String = "" + + override def onStart(settings: MainSettings): Unit = () + + override def onExit(exit: Exit): Unit = () } diff --git a/tests/unit/src/test/scala/tests/cli/CliSuite.scala b/tests/unit/src/test/scala/tests/cli/CliSuite.scala index f13a03071..0034ce70d 100644 --- a/tests/unit/src/test/scala/tests/cli/CliSuite.scala +++ b/tests/unit/src/test/scala/tests/cli/CliSuite.scala @@ -1,7 +1,9 @@ package tests.cli import java.nio.file.Files + import mdoc.internal.BuildInfo +import tests.markdown.{LifeCycleCounter, LifeCycleModifier} class CliSuite extends BaseCliSuite { @@ -191,4 +193,46 @@ class CliSuite extends BaseCliSuite { |""".stripMargin ) + checkCli( + "lifeCycle-0", + """ + |/file1.md + |# file 1 + |One + |```scala mdoc:lifecycle + |val x1 = 1 + |``` + |/file2.md + |# file 2 + |Two + |```scala mdoc:lifecycle + |val x2 = 2 + |``` + | """.stripMargin, + """ + |/file1.md + |# file 1 + |One + |numberOfStarts = 1 ; numberOfExists = 0 + |/file2.md + |# file 2 + |Two + |numberOfStarts = 1 ; numberOfExists = 0 + """.stripMargin, // process counts per PostModifier instance, starts and exists per mdoc.Main process + setup = { fixture => + // Global thread local counter updated by all mdoc.Main process + // All tests in this test suite run sequentially but change the counter + // So make sure we start anew for this test + LifeCycleCounter.reset() + }, + onStdout = { out => + assert(out.contains("Compiling 2 files to")) + assert(out.contains("Compiled in")) + assert(out.contains("(0 errors")) + // Should start and stop one only once in this test (several times for test-suite) + assert(LifeCycleCounter.numberOfExists.get() == 1) + assert(LifeCycleCounter.numberOfStarts.get() == 1) + } + ) + } diff --git a/tests/unit/src/test/scala/tests/markdown/BulletPostModifier.scala b/tests/unit/src/test/scala/tests/markdown/BulletPostModifier.scala index 28a2710f0..7c10f08d6 100644 --- a/tests/unit/src/test/scala/tests/markdown/BulletPostModifier.scala +++ b/tests/unit/src/test/scala/tests/markdown/BulletPostModifier.scala @@ -1,7 +1,6 @@ package tests.markdown -import mdoc.PostModifier -import mdoc.PostModifierContext +import mdoc.{Exit, MainSettings, PostModifier, PostModifierContext} class BulletPostModifier extends PostModifier { val name = "bullet" @@ -14,4 +13,9 @@ class BulletPostModifier extends PostModifier { "" } } + + override def onStart(settings: MainSettings): Unit = () + + override def onExit(exit: Exit): Unit = () + } diff --git a/tests/unit/src/test/scala/tests/markdown/LifeCycleModifier.scala b/tests/unit/src/test/scala/tests/markdown/LifeCycleModifier.scala new file mode 100644 index 000000000..2759099b3 --- /dev/null +++ b/tests/unit/src/test/scala/tests/markdown/LifeCycleModifier.scala @@ -0,0 +1,56 @@ +package tests.markdown + +import mdoc._ + +/** + * Global counter used to test the [[mdoc.Main]] process counting. + * Because tests can be executed concurrently, these need to be + * thread local. The following counters are used for testing within + * the same thread. + */ +object LifeCycleCounter { + val numberOfStarts: ThreadLocal[Integer] = ThreadLocal.withInitial(() => 0) + val numberOfExists: ThreadLocal[Integer] = ThreadLocal.withInitial(() => 0) + + /** + * Reset counters to zero. + */ + def reset(): Unit = { + LifeCycleCounter.numberOfStarts.set(0) + LifeCycleCounter.numberOfExists.set(0) + } +} + +/** + * PostModifier used for testing the [[onStart()]] and [[onExit()]] calls. + */ +class LifeCycleModifier extends PostModifier { + val name = "lifecycle" + + // Starts and stops per instance + var numberOfStarts = 0 + var numberOfExists = 0 + + def process(ctx: PostModifierContext): String = { + // Used for checking the counting between threads + s"numberOfStarts = $numberOfStarts ; numberOfExists = $numberOfExists" + } + + /** + * This is called once when the [[mdoc.Main]] process starts + * @param settings CLI or API settings used by mdoc + */ + override def onStart(settings: MainSettings): Unit = { + numberOfStarts += 1 + LifeCycleCounter.numberOfStarts.set(LifeCycleCounter.numberOfStarts.get() + 1) + } + + /** + * This is called once when the [[mdoc.Main]] process finishes + * @param exit is the exit code returned by mdoc's processing + */ + override def onExit(exit: Exit): Unit = { + numberOfExists += 1 + LifeCycleCounter.numberOfExists.set(LifeCycleCounter.numberOfExists.get() + 1) + } +}