Skip to content

Commit 8343379

Browse files
author
Olafur Pall Geirsson
committed
Support multiple input/output file/directory pairs.
Previously, mdoc only supported a single --in and --out argument and they both had to be directories. Now, users can specify a list of --in and --out arguments of both files and directories. This change enables nice use-cases like generating a single `readme.md` file in the root directory of a git repo or generating the blog directory of a Docusaurus website. ``` mdoc --in readme.template.md --out readme.md mdoc --in docs --out website/target/docs --in blog --out website/blog ``` This change turned out to be tricky to implement: * the public API for custom modifiers needs to correctly handle the case when --in and --out are files. For example, the Scala.js modifier needs to be able to know where to generate JavaScript files. * we need to report helpful and actionable error messages when validating user input. For example, it's not valid to pair an input directory with an output file.
1 parent 264ee02 commit 8343379

33 files changed

+620
-297
lines changed

build.sbt

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ inThisBuild(
99
scalaVersion := scala212,
1010
crossScalaVersions := List(scala212, scala211, scala213),
1111
scalacOptions ++= List(
12+
"-Yrangepos",
1213
"-Xexperimental",
1314
"-deprecation"
1415
),

docs/installation.md

+3-4
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ The sbt-mdoc plugin supports the following settings.
119119

120120
```
121121

122-
123122
## Command-line
124123

125124
Use [coursier](https://github.com/coursier/coursier/#command-line) to launch
@@ -167,16 +166,16 @@ markdown as `@@VARIABLE@`.
167166
+ --site.SCALA_VERSION @SCALA_VERSION@
168167
```
169168

170-
Use `--out` to customize where your markdown sources are generated, by
171-
default the `out/` directory is used.
169+
Use `--out` to customize where your markdown sources are generated, by default
170+
the `out/` directory is used.
172171

173172
```diff
174173
coursier launch org.scalameta:mdoc_@SCALA_BINARY_VERSION@:@VERSION@ -- \
175174
+ --out target/docs
176175
```
177176

178177
The `--out` flag doesn't have to be a directory, it can also be an individual
179-
file. However, this assume that your `--in` was also an individual file.
178+
file. However, this assumes that your `--in` was also an individual file.
180179

181180
```diff
182181
coursier launch org.scalameta:mdoc_@SCALA_BINARY_VERSION@:@VERSION@ -- \

mdoc-docs/src/main/scala/mdoc/docs/MdocModifier.scala

+4-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import mdoc.internal.markdown.GitHubIdGenerator
1414
import mdoc.internal.markdown.LinkHygiene
1515
import mdoc.internal.pos.PositionSyntax._
1616
import mdoc.internal.markdown.MarkdownFile
17+
import mdoc.internal.cli.InputFile
1718

1819
class MdocModifier(context: Context) extends StringModifier {
1920
private val myStdout = new ByteArrayOutputStream()
@@ -24,16 +25,16 @@ class MdocModifier(context: Context) extends StringModifier {
2425
myStdout.reset()
2526
myReporter.reset()
2627
val cleanInput = Input.VirtualFile(code.filename, code.text)
27-
val relpath = RelativePath(code.filename)
28+
val file = InputFile.fromSettings(code.filename, context.settings)
2829
val markdown = Markdown.toMarkdown(
2930
cleanInput,
3031
myContext,
31-
relpath,
32+
file,
3233
myContext.settings.site,
3334
myReporter,
3435
myContext.settings
3536
)
36-
val links = DocumentLinks.fromMarkdown(GitHubIdGenerator, relpath, cleanInput)
37+
val links = DocumentLinks.fromMarkdown(GitHubIdGenerator, file.relpath, cleanInput)
3738
LinkHygiene.lint(List(links), myReporter, verbose = false)
3839
val stdout = fansi.Str(myStdout.toString()).plainText
3940
if (myReporter.hasErrors || myReporter.hasWarnings) {

mdoc-js/src/main/scala-2.12/mdoc/modifiers/JsConfig.scala

+4-9
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ case class JsConfig(
1515
libraries: List[AbsolutePath] = Nil,
1616
mountNode: String = "node",
1717
minLevel: Level = Level.Info,
18-
outDirectory: AbsolutePath = PathIO.workingDirectory,
18+
outDirectories: List[AbsolutePath] = List(PathIO.workingDirectory),
19+
outPrefix: Option[String] = None,
1920
fullOpt: Boolean = true,
2021
htmlPrefix: String = "",
2122
relativeLinkPrefix: String = ""
@@ -65,14 +66,8 @@ object JsConfig {
6566
ctx.site.getOrElse("js-html-header", ""),
6667
Classpath(ctx.site.getOrElse("js-libraries", "")).entries,
6768
mountNode = ctx.site.getOrElse("js-mount-node", base.mountNode),
68-
outDirectory = ctx.site.get("js-out-prefix") match {
69-
case Some(value) =>
70-
// This is needed for Docusaurus that requires assets (non markdown) files to live under
71-
// `docs/assets/`: https://docusaurus.io/docs/en/doc-markdown#linking-to-images-and-other-assets
72-
ctx.settings.out.resolve(value)
73-
case None =>
74-
ctx.settings.out
75-
},
69+
outDirectories = ctx.settings.out,
70+
outPrefix = ctx.site.get("js-out-prefix"),
7671
minLevel = ctx.site.get("js-level") match {
7772
case None => Level.Info
7873
case Some("info") => Level.Info

mdoc-js/src/main/scala-2.12/mdoc/modifiers/JsModifier.scala

+35-12
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import scala.meta.Term
2525
import scala.meta.inputs.Input
2626
import scala.meta.io.Classpath
2727
import scala.reflect.io.VirtualDirectory
28+
import mdoc.internal.cli.InputFile
29+
import scala.meta.io.AbsolutePath
2830

2931
class JsModifier extends mdoc.PreModifier {
3032
override val name = "js"
@@ -153,19 +155,40 @@ class JsModifier extends mdoc.PreModifier {
153155
} else {
154156
val output = WritableMemVirtualJSFile("output.js")
155157
linker.link(virtualIrFiles ++ sjsir, Nil, output, sjsLogger)
156-
val outjsfile = config.outDirectory.resolve(ctx.relativePath.resolveSibling(_ + ".js"))
157-
outjsfile.write(output.content)
158-
val outmdoc = outjsfile.resolveSibling(_ => "mdoc.js")
159-
outmdoc.write(Resources.readPath("/mdoc.js"))
160-
val relfile = outjsfile.toRelativeLinkFrom(ctx.outputFile, config.relativeLinkPrefix)
161-
val relmdoc = outmdoc.toRelativeLinkFrom(ctx.outputFile, config.relativeLinkPrefix)
162-
new CodeBuilder()
163-
.println(config.htmlHeader)
164-
.lines(config.libraryScripts(outjsfile, ctx))
165-
.println(s"""<script type="text/javascript" src="$relfile" defer></script>""")
166-
.println(s"""<script type="text/javascript" src="$relmdoc" defer></script>""")
167-
.toString
158+
ctx.settings.toInputFile(ctx.inputFile) match {
159+
case None =>
160+
ctx.reporter.error(
161+
s"unable to find output file matching the input file '${ctx.inputFile}'. " +
162+
s"To fix this problem, make sure that --in points to a directory that contains the file ${ctx.inputFile}."
163+
)
164+
""
165+
case Some(inputFile) =>
166+
val outjsfile = resolveOutputJsFile(inputFile)
167+
outjsfile.write(output.content)
168+
val outmdoc = outjsfile.resolveSibling(_ => "mdoc.js")
169+
outmdoc.write(Resources.readPath("/mdoc.js"))
170+
val relfile = outjsfile.toRelativeLinkFrom(ctx.outputFile, config.relativeLinkPrefix)
171+
val relmdoc = outmdoc.toRelativeLinkFrom(ctx.outputFile, config.relativeLinkPrefix)
172+
new CodeBuilder()
173+
.println(config.htmlHeader)
174+
.lines(config.libraryScripts(outjsfile, ctx))
175+
.println(s"""<script type="text/javascript" src="$relfile" defer></script>""")
176+
.println(s"""<script type="text/javascript" src="$relmdoc" defer></script>""")
177+
.toString
178+
}
179+
}
180+
}
181+
182+
private def resolveOutputJsFile(file: InputFile): AbsolutePath = {
183+
val outputDirectory = config.outPrefix match {
184+
case None =>
185+
file.outputDirectory
186+
case Some(prefix) =>
187+
// This is needed for Docusaurus that requires assets (non markdown) files to live under
188+
// `docs/assets/`: https://docusaurus.io/docs/en/doc-markdown#linking-to-images-and-other-assets
189+
file.outputDirectory.resolve(prefix)
168190
}
191+
outputDirectory.resolve(file.relpath).resolveSibling(_ + ".js")
169192
}
170193

171194
override def process(ctx: PreModifierContext): String = {

mdoc/src/main/scala/mdoc/MainSettings.scala

+8-2
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,16 @@ final class MainSettings private (
4444
copy(settings.withWorkingDirectory(AbsolutePath(cwd)))
4545
}
4646
def withOut(out: Path): MainSettings = {
47-
copy(settings.copy(out = AbsolutePath(out)))
47+
withOutputPaths(List(out))
48+
}
49+
def withOutputPaths(out: List[Path]): MainSettings = {
50+
copy(settings.copy(out = out.map(AbsolutePath(_)(settings.cwd))))
4851
}
4952
def withIn(in: Path): MainSettings = {
50-
copy(settings.copy(in = AbsolutePath(in)))
53+
withInputPaths(List(in))
54+
}
55+
def withInputPaths(in: List[Path]): MainSettings = {
56+
copy(settings.copy(in = in.map(AbsolutePath(_)(settings.cwd))))
5157
}
5258
def withClasspath(classpath: String): MainSettings = {
5359
copy(settings.copy(classpath = classpath))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package mdoc
2+
3+
import mdoc.internal.cli.Settings
4+
5+
final class OnLoadContext private[mdoc] (
6+
val reporter: Reporter,
7+
private[mdoc] val settings: Settings
8+
) {
9+
def site: Map[String, String] = settings.site
10+
}

mdoc/src/main/scala/mdoc/PostModifier.scala

+7-5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import scala.meta.inputs.Input
1010
import scala.meta.io.AbsolutePath
1111
import scala.collection.JavaConverters._
1212
import scala.meta.io.RelativePath
13+
import mdoc.internal.cli.InputFile
1314

1415
trait PostModifier {
1516
val name: String
@@ -33,12 +34,13 @@ final class PostModifierContext private[mdoc] (
3334
val outputCode: String,
3435
val variables: List[Variable],
3536
val reporter: Reporter,
36-
val relativePath: RelativePath,
37+
private[mdoc] val file: InputFile,
3738
private[mdoc] val settings: Settings
3839
) {
39-
def inputFile: AbsolutePath = inDirectory.resolve(relativePath)
40-
def outputFile: AbsolutePath = outDirectory.resolve(relativePath)
4140
def lastValue: Any = variables.lastOption.map(_.runtimeValue).orNull
42-
def inDirectory: AbsolutePath = settings.in
43-
def outDirectory: AbsolutePath = settings.out
41+
def relativePath: RelativePath = file.relpath
42+
def inputFile: AbsolutePath = file.inputFile
43+
def outputFile: AbsolutePath = file.outputFile
44+
def inDirectory: AbsolutePath = file.inputDirectory
45+
def outDirectory: AbsolutePath = file.outputDirectory
4446
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package mdoc
2+
3+
import scala.meta.io.AbsolutePath
4+
import scala.meta.io.RelativePath
5+
import mdoc.internal.cli.Settings
6+
import mdoc.internal.cli.InputFile
7+
8+
final class PostProcessContext private[mdoc] (
9+
val reporter: Reporter,
10+
private[mdoc] val file: InputFile,
11+
private[mdoc] val settings: Settings
12+
) {
13+
def relativePath: RelativePath = file.relpath
14+
def inputFile: AbsolutePath = file.inputFile
15+
def outputFile: AbsolutePath = file.outputFile
16+
def inDirectory: AbsolutePath = file.inputDirectory
17+
def outDirectory: AbsolutePath = file.outputDirectory
18+
}

mdoc/src/main/scala/mdoc/PreModifier.scala

-37
Original file line numberDiff line numberDiff line change
@@ -30,40 +30,3 @@ object PreModifier {
3030
implicit val encoder: ConfEncoder[PreModifier] =
3131
ConfEncoder.StringEncoder.contramap(mod => s"<${mod.name}>")
3232
}
33-
34-
final class OnLoadContext private[mdoc] (
35-
val reporter: Reporter,
36-
private[mdoc] val settings: Settings
37-
) {
38-
def site: Map[String, String] = settings.site
39-
}
40-
41-
final class PostProcessContext private[mdoc] (
42-
val reporter: Reporter,
43-
val relativePath: RelativePath,
44-
private[mdoc] val settings: Settings
45-
) {
46-
def inputFile: AbsolutePath = inDirectory.resolve(relativePath)
47-
def outputFile: AbsolutePath = outDirectory.resolve(relativePath)
48-
def inDirectory: AbsolutePath = settings.in
49-
def outDirectory: AbsolutePath = settings.out
50-
}
51-
52-
final class PreModifierContext private[mdoc] (
53-
val info: String,
54-
val originalCode: Input,
55-
val reporter: Reporter,
56-
val relativePath: RelativePath,
57-
private[mdoc] val settings: Settings
58-
) {
59-
def infoInput: Input = {
60-
val cpos = originalCode.toPosition.toUnslicedPosition
61-
val start = cpos.start - info.length - 1
62-
val end = cpos.start - 1
63-
Input.Slice(cpos.input, start, end)
64-
}
65-
def inputFile: AbsolutePath = inDirectory.resolve(relativePath)
66-
def outputFile: AbsolutePath = outDirectory.resolve(relativePath)
67-
def inDirectory: AbsolutePath = settings.in
68-
def outDirectory: AbsolutePath = settings.out
69-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package mdoc
2+
3+
import scala.meta.inputs.Input
4+
import scala.meta.io.RelativePath
5+
import mdoc.internal.cli.Settings
6+
import mdoc.internal.pos.PositionSyntax._
7+
import scala.meta.io.AbsolutePath
8+
import mdoc.internal.cli.InputFile
9+
10+
final class PreModifierContext private[mdoc] (
11+
val info: String,
12+
val originalCode: Input,
13+
val reporter: Reporter,
14+
private[mdoc] val file: InputFile,
15+
private[mdoc] val settings: Settings
16+
) {
17+
def infoInput: Input = {
18+
val cpos = originalCode.toPosition.toUnslicedPosition
19+
val start = cpos.start - info.length - 1
20+
val end = cpos.start - 1
21+
Input.Slice(cpos.input, start, end)
22+
}
23+
def relativePath: RelativePath = file.relpath
24+
def inputFile: AbsolutePath = file.inputFile
25+
def outputFile: AbsolutePath = file.outputFile
26+
def inDirectory: AbsolutePath = file.inputDirectory
27+
def outDirectory: AbsolutePath = file.outputDirectory
28+
}

mdoc/src/main/scala/mdoc/internal/cli/Feedback.scala

+21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mdoc.internal.cli
22

33
import java.nio.file.Path
4+
import scala.meta.io.AbsolutePath
45

56
object Feedback {
67
def outSubdirectoryOfIn(in: Path, out: Path): String = {
@@ -9,4 +10,24 @@ object Feedback {
910
s" --in=$in\n" +
1011
s" --out=$out"
1112
}
13+
def mustBeNonEmpty(what: String): String = {
14+
s"--$what must be non-empty. To fix this problem, add an argument for the `--$what <value>` argument."
15+
}
16+
def inputDifferentLengthOutput(input: List[AbsolutePath], output: List[AbsolutePath]): String = {
17+
val diff = math.abs(input.length - output.length)
18+
val toFix = if (input.length > output.length) "out" else "in"
19+
s"--in and --out must have the same length but found ${input.length} --in argument(s) and ${output.length} --out argument(s). " +
20+
s"To fix this problem, add $diff more $toFix arguments."
21+
}
22+
def outputCannotBeRegularFile(input: AbsolutePath, output: AbsolutePath): String = {
23+
s"--out argument '$output' cannot be a regular file when --in argument '$input' is a directory."
24+
}
25+
def outputCannotBeDirectory(input: AbsolutePath, output: AbsolutePath): String = {
26+
s"--out argument '$output' cannot be a directory when --in argument '$input' is a regular file. " +
27+
"To fix this problem, change the --out argument to point to a regular file or an empty path."
28+
}
29+
def inputEqualOutput(input: AbsolutePath): String = {
30+
s"--in and --out cannot be the same path '$input'. " +
31+
"To fix this problem, change the --out argument to another path."
32+
}
1233
}

mdoc/src/main/scala/mdoc/internal/cli/InputFile.scala

+24-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,31 @@ package mdoc.internal.cli
22

33
import scala.meta.io.AbsolutePath
44
import scala.meta.io.RelativePath
5+
import scala.meta.internal.io.PathIO
6+
import metaconfig.Input
7+
import java.nio.file.Files
8+
import mdoc.internal.pos.PositionSyntax._
59

10+
/**
11+
* @param relpath the input filename relativized by its input directory.
12+
* @param inputFile the input file to read from.
13+
* @param outputFile the output file to write to.
14+
* @param inputDirectory directory enclosing the input file.
15+
* @param outputDirectory directory enclosing the output file.
16+
*/
617
case class InputFile(
718
relpath: RelativePath,
8-
in: AbsolutePath,
9-
out: AbsolutePath
19+
inputFile: AbsolutePath,
20+
outputFile: AbsolutePath,
21+
inputDirectory: AbsolutePath,
22+
outputDirectory: AbsolutePath
1023
)
24+
25+
object InputFile {
26+
def fromSettings(filename: String, settings: Settings): InputFile = {
27+
val relpath = RelativePath(filename)
28+
val inputDir = settings.in.head
29+
val outputDir = settings.out.head
30+
InputFile(relpath, inputDir.resolve(filename), outputDir.resolve(filename), inputDir, outputDir)
31+
}
32+
}

0 commit comments

Comments
 (0)