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..b1311a0c8 100644 --- a/mdoc-docs/src/main/scala/mdoc/docs/EvilplotModifier.scala +++ b/mdoc-docs/src/main/scala/mdoc/docs/EvilplotModifier.scala @@ -3,7 +3,10 @@ package mdoc.docs import com.cibo.evilplot.geometry.Drawable import java.nio.file.Files import java.nio.file.Paths + import mdoc._ +import mdoc.internal.cli.{Exit, Settings} + import scala.meta.inputs.Position class EvilplotModifier extends PostModifier { @@ -36,4 +39,12 @@ class EvilplotModifier extends PostModifier { "" } } + + override def onStart(settings: Settings): Unit = () + + override def preProcess(ctx: PostModifierContext): Unit = () + + override def postProcess(ctx: PostModifierContext): 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..7220ae5f4 --- /dev/null +++ b/mdoc-docs/src/main/scala/mdoc/docs/LifeCycleModifier.scala @@ -0,0 +1,50 @@ +package mdoc.docs + +import mdoc._ +import mdoc.internal.cli.{Exit, Settings} + +/** + * 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 + // Pre and post processing per instance + var numberOfPreProcess = 0 + var numberOfPostProcess = 0 + + def process(ctx: PostModifierContext): String = { + // Used for checking the counting + s"numberOfStarts = $numberOfStarts ; numberOfExists = $numberOfExists ; numberOfPreProcess = $numberOfPreProcess ; numberOfPostProcess = $numberOfPostProcess" + } + + /** + * This is called once when the [[mdoc.Main]] process starts + * @param settings CLI or API settings used by mdoc + */ + override def onStart(settings: Settings): Unit = { + numberOfStarts += 1 + LifeCycleCounter.numberOfStarts.set(LifeCycleCounter.numberOfStarts.get() + 1) + } + + override def preProcess(ctx: PostModifierContext): Unit = { numberOfPreProcess += 1} + + override def postProcess(ctx: PostModifierContext): Unit = { numberOfPostProcess += 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/PostModifier.scala b/mdoc/src/main/scala/mdoc/PostModifier.scala index 9ec3053ee..6abed5dd7 100644 --- a/mdoc/src/main/scala/mdoc/PostModifier.scala +++ b/mdoc/src/main/scala/mdoc/PostModifier.scala @@ -1,11 +1,13 @@ package mdoc import java.util.ServiceLoader -import mdoc.internal.cli.Settings + +import mdoc.internal.cli.{Exit, 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._ @@ -13,7 +15,11 @@ import scala.meta.io.RelativePath trait PostModifier { val name: String + def onStart(settings: Settings): Unit + def preProcess(ctx: PostModifierContext): Unit def process(ctx: PostModifierContext): String + def postProcess(ctx: PostModifierContext): Unit + 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..36d709250 100644 --- a/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala +++ b/mdoc/src/main/scala/mdoc/internal/cli/MainOps.scala @@ -146,6 +146,7 @@ final class MainOps( if (settings.watch) { startLivereload() } + context.settings.postModifiers.foreach(_.onStart(settings)) val isOk = generateCompleteSite() if (settings.isFileWatching) { waitingForFileChanges() @@ -153,6 +154,7 @@ final class MainOps( // exit code doesn't matter when file watching Exit.success } else { + context.settings.postModifiers.foreach(_.onExit(isOk)) isOk } } diff --git a/mdoc/src/main/scala/mdoc/internal/markdown/Processor.scala b/mdoc/src/main/scala/mdoc/internal/markdown/Processor.scala index 23e61eb69..337f5e461 100644 --- a/mdoc/src/main/scala/mdoc/internal/markdown/Processor.scala +++ b/mdoc/src/main/scala/mdoc/internal/markdown/Processor.scala @@ -171,7 +171,9 @@ class Processor(implicit ctx: Context) { inputFile, ctx.settings ) + modifier.preProcess(postCtx) val postRender = modifier.process(postCtx) + modifier.postProcess(postCtx) replaceNodeWithText(doc, block, postRender) case m: Modifier.Builtin => if (m.isPassthrough) { 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..2b2f12284 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 @@ -6,4 +6,12 @@ import mdoc.PostModifierContext class EvilplotPostModifier extends PostModifier { val name = "evilplot" def process(ctx: PostModifierContext): String = "" + + override def onStart(ctx: PostModifierContext): Unit = () + + override def preProcess(ctx: PostModifierContext): Unit = () + + override def postProcess(ctx: PostModifierContext): Unit = () + + override def onExit(ctx: PostModifierContext): 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..8514e3312 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,10 @@ package tests.markdown import com.cibo.evilplot.geometry.Drawable import java.nio.file.Files + import mdoc.PostModifier import mdoc.PostModifierContext +import mdoc.internal.cli.{Exit, Settings} class EvilplotPostModifier extends PostModifier { val name = "evilplot" @@ -28,4 +30,13 @@ class EvilplotPostModifier extends PostModifier { "" } } + + override def onStart(settings: Settings): Unit = () + + override def preProcess(ctx: PostModifierContext): Unit = () + + override def postProcess(ctx: PostModifierContext): 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..56fa88d81 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,26 @@ 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 ; numberOfPreProcess = 1 ; numberOfPostProcess = 0" + ) + + // Process counts are per PostModifier instance, starts and exists per mdoc.Main process + // Because each test runs its own mdoc.Main process, the process counts are the same + check( + "lifecycle-2", + """ + |```scala mdoc:lifecycle + |val x = "message" + |``` + """.stripMargin, + "numberOfStarts = 0 ; numberOfExists = 0 ; numberOfPreProcess = 1 ; numberOfPostProcess = 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..6c7bd75df 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 @@ -2,8 +2,17 @@ package tests.markdown import mdoc.PostModifier import mdoc.PostModifierContext +import mdoc.internal.cli.{Exit, Settings} class EvilplotPostModifier extends PostModifier { val name = "evilplot" def process(ctx: PostModifierContext): String = "" + + override def onStart(settings: Settings): Unit = () + + override def preProcess(ctx: PostModifierContext): Unit = () + + override def postProcess(ctx: PostModifierContext): 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..102816f34 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,48 @@ class CliSuite extends BaseCliSuite { |""".stripMargin ) + checkCli( + "lifeCycle", + """ + |/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 ; numberOfPreProcess = 1 ; numberOfPostProcess = 0 + |/file2.md + |# file 2 + |Two + |numberOfStarts = 1 ; numberOfExists = 0 ; numberOfPreProcess = 2 ; numberOfPostProcess = 1 + """.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.numberOfStarts.set(0) + LifeCycleCounter.numberOfExists.set(0) + }, + 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..085174e35 100644 --- a/tests/unit/src/test/scala/tests/markdown/BulletPostModifier.scala +++ b/tests/unit/src/test/scala/tests/markdown/BulletPostModifier.scala @@ -2,6 +2,7 @@ package tests.markdown import mdoc.PostModifier import mdoc.PostModifierContext +import mdoc.internal.cli.{Exit, Settings} class BulletPostModifier extends PostModifier { val name = "bullet" @@ -14,4 +15,13 @@ class BulletPostModifier extends PostModifier { "" } } + + override def onStart(settings: Settings): Unit = () + + override def preProcess(ctx: PostModifierContext): Unit = () + + override def postProcess(ctx: PostModifierContext): 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..a1d760378 --- /dev/null +++ b/tests/unit/src/test/scala/tests/markdown/LifeCycleModifier.scala @@ -0,0 +1,50 @@ +package tests.markdown + +import mdoc._ +import mdoc.internal.cli.{Exit, Settings} + +/** + * 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 + // Pre and post processing per instance + var numberOfPreProcess = 0 + var numberOfPostProcess = 0 + + def process(ctx: PostModifierContext): String = { + // Used for checking the counting + s"numberOfStarts = $numberOfStarts ; numberOfExists = $numberOfExists ; numberOfPreProcess = $numberOfPreProcess ; numberOfPostProcess = $numberOfPostProcess" + } + + /** + * This is called once when the [[mdoc.Main]] process starts + * @param settings CLI or API settings used by mdoc + */ + override def onStart(settings: Settings): Unit = { + numberOfStarts += 1 + LifeCycleCounter.numberOfStarts.set(LifeCycleCounter.numberOfStarts.get() + 1) + } + + override def preProcess(ctx: PostModifierContext): Unit = { numberOfPreProcess += 1} + + override def postProcess(ctx: PostModifierContext): Unit = { numberOfPostProcess += 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) + } +}