diff --git a/cli/src/main/scala/mdoc/Variable.scala b/cli/src/main/scala/mdoc/Variable.scala index 524709e15..cff2d1736 100644 --- a/cli/src/main/scala/mdoc/Variable.scala +++ b/cli/src/main/scala/mdoc/Variable.scala @@ -1,6 +1,8 @@ package mdoc +import mdoc.internal.markdown.GenModifier import mdoc.internal.markdown.Modifier +import mdoc.internal.markdown.ModifierInline import mdoc.internal.markdown.ReplVariablePrinter import scala.meta.inputs.Position @@ -58,9 +60,13 @@ final class Variable private[mdoc] ( val totalVariablesInStatement: Int, val indexOfStatementInCodeFence: Int, val totalStatementsInCodeFence: Int, - private[mdoc] val mods: Modifier + private[mdoc] val mods: GenModifier ) { - def isToString: Boolean = mods.isToString + def isToString: Boolean = + mods match { + case fenceModifier: Modifier => fenceModifier.isToString + case modifierInline: ModifierInline => false + } def isUnit: Boolean = staticType.endsWith("Unit") override def toString: String = { ReplVariablePrinter(this) diff --git a/cli/src/main/scala/mdoc/internal/markdown/GenModifier.scala b/cli/src/main/scala/mdoc/internal/markdown/GenModifier.scala new file mode 100644 index 000000000..d7c8fdb25 --- /dev/null +++ b/cli/src/main/scala/mdoc/internal/markdown/GenModifier.scala @@ -0,0 +1 @@ +package mdoc.internal.markdown \ No newline at end of file diff --git a/cli/src/main/scala/mdoc/internal/markdown/ModInline.scala b/cli/src/main/scala/mdoc/internal/markdown/ModInline.scala new file mode 100644 index 000000000..121b17d74 --- /dev/null +++ b/cli/src/main/scala/mdoc/internal/markdown/ModInline.scala @@ -0,0 +1,20 @@ +package mdoc.internal.markdown + +import scala.util.Try + +sealed abstract class ModInline extends Product with Serializable +object ModInline { + // The default behavior will be CompileOnly, so we don't need that Mod + case object Fail extends ModInline + case object Warn extends ModInline + + def static: List[ModInline] = + List( + Fail, + Warn, + ) + + def unapply(string: String): Option[ModInline] = { + static.find(_.toString.equalsIgnoreCase(string)) + } +} diff --git a/cli/src/main/scala/mdoc/internal/markdown/Modifier.scala b/cli/src/main/scala/mdoc/internal/markdown/Modifier.scala index 80f5f1cf2..7ff8f5720 100644 --- a/cli/src/main/scala/mdoc/internal/markdown/Modifier.scala +++ b/cli/src/main/scala/mdoc/internal/markdown/Modifier.scala @@ -3,6 +3,15 @@ package mdoc.internal.markdown import mdoc.StringModifier import mdoc.internal.markdown.Mod._ + +sealed trait GenModifier { + val mods: Set[Mod] + def isDefault: Boolean = mods.isEmpty + def isFailOrWarn: Boolean = isFail || isWarn + def isFail: Boolean = mods(Fail) + def isWarn: Boolean = mods(Warn) +} + /** A mdoc code fence modifier. * * Modifiers are parsed from code blocks like here @@ -13,11 +22,7 @@ import mdoc.internal.markdown.Mod._ * * Currently, only supports parsing one modifier per code block. */ -sealed abstract class Modifier(val mods: Set[Mod]) { - def isDefault: Boolean = mods.isEmpty - def isFailOrWarn: Boolean = isFail || isWarn - def isFail: Boolean = mods(Fail) - def isWarn: Boolean = mods(Warn) +sealed abstract class Modifier(val mods: Set[Mod]) extends GenModifier { def isPassthrough: Boolean = mods(Passthrough) def isString: Boolean = this.isInstanceOf[Modifier.Str] def isPre: Boolean = this.isInstanceOf[Modifier.Pre] @@ -82,3 +87,39 @@ object Modifier { case class Pre(mod: mdoc.PreModifier, info: String) extends Modifier(Set.empty) } + +/** An mdoc inline code modifier. + * + * Modifiers are parsed from inline code blocks like here + * + * `scala mdoc:passthrough println("# Header")` + * + * Currently, only supports parsing one modifier per code block. + */ +case class ModifierInline(val mods: Set[Mod]) extends GenModifier +object ModifierInline { + object Default { + def apply(): ModifierInline = ModifierInline(Set.empty) + } + object Fail { + def unapply(m: ModifierInline): Boolean = + m.isFailOrWarn + } + object Warn { + def unapply(m: ModifierInline): Boolean = + m.isWarn + } + + def apply(string: String): Option[ModifierInline] = { + val mods = string.split(":").map { + case Mod(m) => Some(m) + case _ => None + } + if (mods.forall(_.isDefined)) { + Some(ModifierInline(mods.iterator.map(_.get).toSet)) + } else { + None + } + } + +} diff --git a/cli/src/main/scala/mdoc/internal/markdown/ModifierInline.scala b/cli/src/main/scala/mdoc/internal/markdown/ModifierInline.scala new file mode 100644 index 000000000..96a472d6e --- /dev/null +++ b/cli/src/main/scala/mdoc/internal/markdown/ModifierInline.scala @@ -0,0 +1,4 @@ +package mdoc.internal.markdown + +import mdoc.StringModifier +import mdoc.internal.markdown.Mod._ diff --git a/cli/src/main/scala/mdoc/internal/markdown/ReplVariablePrinter.scala b/cli/src/main/scala/mdoc/internal/markdown/ReplVariablePrinter.scala index 44a8b9990..17e363d53 100644 --- a/cli/src/main/scala/mdoc/internal/markdown/ReplVariablePrinter.scala +++ b/cli/src/main/scala/mdoc/internal/markdown/ReplVariablePrinter.scala @@ -27,8 +27,13 @@ class ReplVariablePrinter( if (binder.isToString) { appendMultiline(sb, binder.runtimeValue.toString) } else { - val heightOverride = binder.mods.heightOverride - val widthOverride = binder.mods.widthOverride + val (heightOverride, widthOverride) = + binder.mods match { + case fenceModifier: Modifier => + (fenceModifier.heightOverride, fenceModifier.widthOverride) + case modifierInline: ModifierInline => + (None, None) + } val lines = pprint.PPrinter.BlackWhite.tokenize( binder.runtimeValue, diff --git a/mdoc/src/main/scala-2/mdoc/internal/markdown/FailInstrumenter.scala b/mdoc/src/main/scala-2/mdoc/internal/markdown/FailInstrumenter.scala index 0bf8bbc1d..92bc2232d 100644 --- a/mdoc/src/main/scala-2/mdoc/internal/markdown/FailInstrumenter.scala +++ b/mdoc/src/main/scala-2/mdoc/internal/markdown/FailInstrumenter.scala @@ -21,31 +21,63 @@ final class FailInstrumenter(sections: List[SectionInput], i: Int) { sections.zipWithIndex.foreach { case (section, j) => if (j > i) () else { - if (section.mod.isReset) { - nest.unnest() - sb.print(Instrumenter.reset(section.mod, gensym.fresh("App"))) - } else if (section.mod.isNest) { - nest.nest() - } - if (j == i || !section.mod.isFailOrWarn) { - section.source.stats.foreach { stat => - stat match { - case i: Import => - i.importers.foreach { - case Importer( - Term.Name(name), - List(Importee.Name(_: Name.Indeterminate)) + section.mod match { + case fenceModifier: Modifier => + if (fenceModifier.isReset) { + nest.unnest() + sb.print(Instrumenter.reset(fenceModifier, gensym.fresh("App"))) + } else if (fenceModifier.isNest) { + nest.nest() + } + if (j == i || !fenceModifier.isFailOrWarn) { + println("Should proceed for fence: " + section.source) + section.source.stats.foreach { stat => + stat match { + case i: Import => + i.importers.foreach { + case Importer( + Term.Name(name), + List(Importee.Name(_: Name.Indeterminate)) + ) if Instrumenter.magicImports(name) => + case importer => + sb.print("import ") + sb.print(importer.pos.text) + sb.print(";") + } + case _ => + sb.println(stat.pos.text) + } + } + } + case modifierInline: ModifierInline => + println("FailInstrumenter.modifierInline branch") + nest.nest() + sb.println(section.input) + if (j == i || !modifierInline.isFailOrWarn) { + println("Should proceed: " + section.source) + section.source.stats.foreach { stat => + println("stat: " + stat) + stat match { + case i: Import => + println("Uh? Import?") + i.importers.foreach { + case Importer( + Term.Name(name), + List(Importee.Name(_: Name.Indeterminate)) ) if Instrumenter.magicImports(name) => - case importer => - sb.print("import ") - sb.print(importer.pos.text) - sb.print(";") + case importer => + sb.print("import ") + sb.print(importer.pos.text) + sb.print(";") + } + case _ => + println("stat.pos.text: " + stat.pos.text) + sb.println(stat.pos.text) } - case _ => - sb.println(stat.pos.text) + } } - } } + } } sb.println("\n }\n}") diff --git a/mdoc/src/main/scala-2/mdoc/internal/markdown/Instrumenter.scala b/mdoc/src/main/scala-2/mdoc/internal/markdown/Instrumenter.scala index d36217b55..4c46d99a5 100644 --- a/mdoc/src/main/scala-2/mdoc/internal/markdown/Instrumenter.scala +++ b/mdoc/src/main/scala-2/mdoc/internal/markdown/Instrumenter.scala @@ -37,67 +37,101 @@ class Instrumenter( private val sb = new PrintStream(out) val gensym = new Gensym() val nest = new Nesting(sb) - private def printAsScript(): Unit = { - sections.zipWithIndex.foreach { case (section, i) => - if (section.mod.isReset) { - nest.unnest() - sb.print(Instrumenter.reset(section.mod, gensym.fresh("App"))) - } else if (section.mod.isNest) { - nest.nest() - } - sb.println("\n$doc.startSection();") - if (section.mod.isFailOrWarn) { - sb.println(s"$$doc.startStatement(${position(section.source.pos)});") - val out = new FailInstrumenter(sections, i).instrument() - val literal = Instrumenter.stringLiteral(out) - val binder = gensym.fresh("res") - sb.append("val ") - .append(binder) - .append(" = _root_.mdoc.internal.document.FailSection(") - .append(literal) - .append(", ") - .append(position(section.source.pos)) - .append(");") - printBinder(binder, section.source.pos) - sb.println("\n$doc.endStatement();") - } else if (section.mod.isCompileOnly) { - section.source.stats.foreach { stat => - sb.println(s"$$doc.startStatement(${position(stat.pos)});") - sb.println("\n$doc.endStatement();") - } - sb.println(s"""object ${gensym.fresh("compile")} {""") - sb.println(section.source.pos.text) - sb.println("\n}") - } else if (section.mod.isCrash) { - section.source.stats match { - case head :: _ => - sb.println(s"$$doc.startStatement(${position(head.pos)});") - - sb.append("$doc.crash(") - .append(position(head.pos)) - .append(") {\n") + private def printAsScript(): Unit = + sections.zipWithIndex.foreach { case (section: SectionInput, i) => + section.mod match { + case fenceModifier: Modifier => { + if (fenceModifier.isReset) { + nest.unnest() + sb.print(Instrumenter.reset(fenceModifier, gensym.fresh("App"))) + } else if (fenceModifier.isNest) { + nest.nest() + } + sb.println("\n$doc.startSection();") + if (fenceModifier.isFailOrWarn) { + sb.println(s"$$doc.startStatement(${position(section.source.pos)});") + val out = new FailInstrumenter(sections, i).instrument() + val literal = Instrumenter.stringLiteral(out) + val binder = gensym.fresh("res") + sb.append("val ") + .append(binder) + .append(" = _root_.mdoc.internal.document.FailSection(") + .append(literal) + .append(", ") + .append(position(section.source.pos)) + .append(");") + printBinder(binder, section.source.pos) + sb.println("\n$doc.endStatement();") + } else if (fenceModifier.isCompileOnly) { section.source.stats.foreach { stat => - sb.append(stat.pos.text).append(";\n") + sb.println(s"$$doc.startStatement(${position(stat.pos)});") + sb.println("\n$doc.endStatement();") } - // closing the $doc.crash {... block - sb.append("\n}\n") + sb.println(s"""object ${gensym.fresh("compile")} {""") + sb.println(section.source.pos.text) + sb.println("\n}") + } else if (fenceModifier.isCrash) { + section.source.stats match { + case head :: _ => + sb.println(s"$$doc.startStatement(${position(head.pos)});") - sb.println("\n$doc.endStatement();") + sb.append("$doc.crash(") + .append(position(head.pos)) + .append(") {\n") + + section.source.stats.foreach { stat => + sb.append(stat.pos.text).append(";\n") + } + // closing the $doc.crash {... block + sb.append("\n}\n") + + sb.println("\n$doc.endStatement();") - case Nil => + case Nil => + } + } else { + section.source.stats.foreach { stat => + sb.println(s"$$doc.startStatement(${position(stat.pos)});") + printStatement(stat, fenceModifier, sb) + sb.println("\n$doc.endStatement();") + } + } + sb.println("$doc.endSection();") } - } else { - section.source.stats.foreach { stat => - sb.println(s"$$doc.startStatement(${position(stat.pos)});") - printStatement(stat, section.mod, sb) - sb.println("\n$doc.endStatement();") + nest.unnest() + case modifierInline: ModifierInline => { + nest.nest() + sb.println("\n$doc.startSection();") + if (modifierInline.isFailOrWarn) { + sb.println(s"$$doc.startStatement(${position(section.source.pos)});") + val out = new FailInstrumenter(sections, i).instrument() + val literal = Instrumenter.stringLiteral(out) + val binder = gensym.fresh("res") + sb.append("val ") + .append(binder) + .append(" = _root_.mdoc.internal.document.FailSection(") + .append(literal) + .append(", ") + .append(position(section.source.pos)) + .append(");") + printBinder(binder, section.source.pos) + sb.println("\n$doc.endStatement();") + } else { + section.source.stats.foreach { stat => + sb.println(s"$$doc.startStatement(${position(stat.pos)});") + sb.println("\n$doc.endStatement();") + } + sb.println(s"""object ${gensym.fresh("compile")} {""") + sb.println(section.source.pos.text) + sb.println("\n}") + } + nest.unnest() } } - sb.println("$doc.endSection();") + } - nest.unnest() - } + private def printBinder(name: String, pos: Position): Unit = { sb.print(s"; $$doc.binder($name, ${position(pos)})") @@ -134,6 +168,7 @@ class Instrumenter( } } } + } object Instrumenter { val magicImports = Set( diff --git a/mdoc/src/main/scala/mdoc/internal/markdown/EvaluatedSection.scala b/mdoc/src/main/scala/mdoc/internal/markdown/EvaluatedSection.scala index 67bbd3e78..c1d3b48b8 100644 --- a/mdoc/src/main/scala/mdoc/internal/markdown/EvaluatedSection.scala +++ b/mdoc/src/main/scala/mdoc/internal/markdown/EvaluatedSection.scala @@ -4,6 +4,6 @@ import scala.meta.Source import scala.meta.inputs.Input import mdoc.document.Section -case class EvaluatedSection(section: Section, input: Input, source: ParsedSource, mod: Modifier) { +case class EvaluatedSection(section: Section, input: Input, source: ParsedSource, mod: GenModifier) { def out: String = section.statements.map(_.out).mkString } diff --git a/mdoc/src/main/scala/mdoc/internal/markdown/FenceInput.scala b/mdoc/src/main/scala/mdoc/internal/markdown/FenceInput.scala index 447c35c2c..589519755 100644 --- a/mdoc/src/main/scala/mdoc/internal/markdown/FenceInput.scala +++ b/mdoc/src/main/scala/mdoc/internal/markdown/FenceInput.scala @@ -1,6 +1,5 @@ package mdoc.internal.markdown -import com.vladsch.flexmark.ast.FencedCodeBlock import scala.meta.inputs.Input import scala.meta.inputs.Position import mdoc.internal.cli.Context diff --git a/mdoc/src/main/scala/mdoc/internal/markdown/InlineInput.scala b/mdoc/src/main/scala/mdoc/internal/markdown/InlineInput.scala new file mode 100644 index 000000000..242e9204c --- /dev/null +++ b/mdoc/src/main/scala/mdoc/internal/markdown/InlineInput.scala @@ -0,0 +1,93 @@ +package mdoc.internal.markdown + +import scala.meta.inputs.Input +import scala.meta.inputs.Position +import mdoc.internal.cli.Context +import mdoc.internal.markdown.Modifier.Str +import mdoc.internal.markdown.Modifier.Default +import mdoc.internal.markdown.Modifier.Post +import mdoc.internal.markdown.Modifier.Pre + +case class PreInlineInput(block: CodeFence, input: Input, mod: Pre) // TODO Delete? +case class StringInlineInput(block: InlineCode, input: Input, mod: Str) +case class ScalaInlineInput(block: InlineMdoc, input: Input, mod: ModifierInline) + +class InlineInput(ctx: Context, baseInput: Input) { + def getModifier(info: Text): Option[ModifierInline] = { + val string = info.value.stripLineEnd + println("InlineInput string: " + string) + if (!string.startsWith("scala mdoc")) None + else { + if (!string.contains(':')) { + println("InlineInput Default modifiers") + Some(ModifierInline.Default()) + } + else { + val mode = string.stripPrefix("scala mdoc:") + println("InlineInput custom mode: " + mode) + ModifierInline(mode) + .orElse { + invalid(info, s"Invalid mode '$mode'") + None + } + } + } + } + + private def invalid(info: Text, message: String): Unit = { + val offset = "scala mdoc:".length + val start = info.pos.start + offset + val end = info.pos.end - 1 + val pos = Position.Range(baseInput, start, end) + ctx.reporter.error(pos, message) + } + private def invalidCombination(info: Text, mod1: String, mod2: String): Boolean = { + invalid(info, s"invalid combination of modifiers '$mod1' and '$mod2'") + false + } + + private def isValid(info: Text, mod: ModifierInline): Boolean = { + true + // TODO Pick out relevant logic below to restore this method + /* if (mod.isFailOrWarn && mod.isCrash) { + invalidCombination(info, "crash", "fail") + } else if (mod.isSilent && mod.isInvisible) { + invalidCombination(info, "silent", "invisible") + } else if (mod.isReset && mod.isNest) { + invalid( + info, + "the modifier 'nest' is redundant when used in combination with 'reset'. " + + "To fix this error, remove 'nest'" + ) + false + } else if (mod.isCompileOnly) { + val others = mod.mods - Mod.CompileOnly + if (others.isEmpty) { + true + } else { + val all = others.map(_.toString.toLowerCase).mkString(", ") + invalid( + info, + s"""compile-only cannot be used in combination with $all""" + ) + false + } + } else { + true + } + */ + } + def unapply(block: InlineMdoc): Option[ScalaInlineInput] = { + println("InlineInput.unapply") + getModifier(block.info) match { + case Some(mod) => + if (isValid(block.info, mod)) { + val input = Input.Slice(baseInput, block.body.pos.start, block.body.pos.end) + Some(ScalaInlineInput(block, input, mod)) + } else { + None + } + case _ => None + } + } +} diff --git a/mdoc/src/main/scala/mdoc/internal/markdown/Markdown.scala b/mdoc/src/main/scala/mdoc/internal/markdown/Markdown.scala index 71175416b..9b25999f1 100644 --- a/mdoc/src/main/scala/mdoc/internal/markdown/Markdown.scala +++ b/mdoc/src/main/scala/mdoc/internal/markdown/Markdown.scala @@ -129,6 +129,7 @@ object Markdown { ) val file = MarkdownFile.parse(textWithVariables, inputFile, reporter) val processor = new Processor()(context) + // TODO Or look into this processDocument call? processor.processDocument(file) file.renderToString } diff --git a/mdoc/src/main/scala/mdoc/internal/markdown/MarkdownBuilder.scala b/mdoc/src/main/scala/mdoc/internal/markdown/MarkdownBuilder.scala index 00041499e..42eaa6e52 100644 --- a/mdoc/src/main/scala/mdoc/internal/markdown/MarkdownBuilder.scala +++ b/mdoc/src/main/scala/mdoc/internal/markdown/MarkdownBuilder.scala @@ -25,6 +25,7 @@ object MarkdownBuilder { instrumented: Instrumented, filename: String ): EvaluatedDocument = { + println("building document") val instrumentedInput = InstrumentedInput(filename, instrumented.source) reporter.debug(s"$filename: instrumented code\n$instrumented") val compileInput = Input.VirtualFile(filename, instrumented.source) diff --git a/mdoc/src/main/scala/mdoc/internal/markdown/MarkdownFile.scala b/mdoc/src/main/scala/mdoc/internal/markdown/MarkdownFile.scala index 95225549b..ff460ccc1 100644 --- a/mdoc/src/main/scala/mdoc/internal/markdown/MarkdownFile.scala +++ b/mdoc/src/main/scala/mdoc/internal/markdown/MarkdownFile.scala @@ -63,7 +63,8 @@ object MarkdownFile { val info = line.substring(backticks.length()) state = State.CodeFence(curr, backticks, info) } else { - parts += newText(curr, end) + // TODO Consider putting conditional inside the block + parts ++= parseLineWithInlineCode(line) } case s: State.CodeFence => if ( @@ -89,6 +90,46 @@ object MarkdownFile { val parts = parser.acceptParts() MarkdownFile(input, file, parts) } + + def parseLineWithInlineCode(line: String): List[MarkdownPart] = { + if (!line.contains("`")) { + List(Text(line)) + } else { + /** TODO + * - How should we handle: + * - Multiple ticks in a row + * - Especially when at the beginning of the line, as other tests already focus on that case. + * - Unbalanced tick marks + * - + */ + val prefix = if (line.startsWith("`")) " " else "" + val suffix = if (line.endsWith("`")) " " else "" + val tickSections = (prefix + line + suffix).split("`")//.filterNot(s => s.isBlank) + if (line.contains("Inline")) { + tickSections.foreach(section => println("Section: " + section)) + } + if (tickSections.nonEmpty && tickSections.size % 2 == 0) + throw new RuntimeException("TODO How to handle Unbalanced ticks!") + + tickSections.toList.zipWithIndex.map { case (piece, index) => + // TODO This might be dangerous. If the paragraph starts with "scala mdoc", outside of ticks, this + // could go haywire + if (index % 2 != 0) { + if (piece.startsWith("scala mdoc")) { + val wordsInMdocPiece = piece.split("\\s+") + val (info, body) = wordsInMdocPiece.splitAt(2) + InlineMdoc(Text(info.mkString(" ")), Text(body.mkString(" "))) + } else { + Text(s"`$piece`") // TODO Any cleaner way of avoiding re-adding backticks here? + + } + } + else + Text(s"$piece") // TODO Any cleaner way of avoiding re-adding backticks here? + } + } + } + } sealed abstract class MarkdownPart { @@ -120,6 +161,11 @@ sealed abstract class MarkdownPart { } fence.closeBackticks.renderToString(out) } + case inlineMdoc: InlineMdoc => + out.append("`") + inlineMdoc.body.renderToString(out) + out.append("`") +// out.append(inlineMdoc.body) } } final case class Text(value: String) extends MarkdownPart @@ -129,3 +175,14 @@ final case class CodeFence(openBackticks: Text, info: Text, body: Text, closeBac var newInfo = Option.empty[String] var newBody = Option.empty[String] } + +// TODO Info/modifiers +final case class InlineCode(body: Text) extends MarkdownPart +final case class InlineMdoc(info: Text, body: Text) extends MarkdownPart { + val closeTick = "`" + // TODO See which vars are necessary + // Since we're not messing with output, I think these can actually go away + var newPart = Option.empty[String] + var newInfo = Option.empty[String] + var newBody = Option.empty[String] +} diff --git a/mdoc/src/main/scala/mdoc/internal/markdown/Processor.scala b/mdoc/src/main/scala/mdoc/internal/markdown/Processor.scala index 8a006a23c..e1cb621a0 100644 --- a/mdoc/src/main/scala/mdoc/internal/markdown/Processor.scala +++ b/mdoc/src/main/scala/mdoc/internal/markdown/Processor.scala @@ -32,6 +32,7 @@ import mdoc.internal.BuildInfo object MdocDialect { def parse(path: AbsolutePath): Parsed[Source] = { + println("Parsing with MdocDialect") (Input.VirtualFile(path.toString, path.readText), scala).parse[Source] } @@ -45,7 +46,7 @@ class Processor(implicit ctx: Context) { def processDocument(doc: MarkdownFile): MarkdownFile = { val docInput = doc.input - val (scalaInputs, customInputs, preInputs) = collectFenceInputs(doc) + val (scalaInputs, customInputs, preInputs, scalaInlineInputs) = collectFenceInputs(doc) val filename = docInput.toFilename(ctx.settings) val inputFile = doc.file.relpath customInputs.foreach { block => processStringInput(doc, block) } @@ -53,6 +54,9 @@ class Processor(implicit ctx: Context) { if (scalaInputs.nonEmpty) { processScalaInputs(doc, scalaInputs, inputFile, filename) } + if (scalaInlineInputs.nonEmpty) { + processScalaInlineInputs(doc, scalaInlineInputs, inputFile, filename) + } if (preInputs.nonEmpty) { val post = new PostProcessContext(ctx.reporter, doc.file, ctx.settings) ctx.settings.preModifiers.foreach { pre => @@ -156,6 +160,51 @@ class Processor(implicit ctx: Context) { compiler ) } + + def processScalaInlineInputs( + doc: MarkdownFile, + inputs: List[ScalaInlineInput], + relpath: RelativePath, + filename: String + ): Unit = { + val sectionInputs = inputs.map { case ScalaInlineInput(_, input, mod) => + import scala.meta._ + + (input, MdocDialect.scala).parse[Source] match { + case parsers.Parsed.Success(source) => + SectionInput(input, ParsedSource(source), mod) + case parsers.Parsed.Error(pos, msg, _) => + ctx.reporter.error(pos.toUnslicedPosition, msg) + SectionInput(input, ParsedSource.empty, mod) + } + } + val instrumented = Instrumenter.instrument(doc.file, sectionInputs, ctx.settings, ctx.reporter) + + if (ctx.reporter.hasErrors) { + return + } + if (ctx.settings.verbose) { + ctx.reporter.info(s"Instrumented $filename") + ctx.reporter.println(instrumented.source) + } + val compiler = + try { + ctx.compiler(instrumented) + } catch { + case e: CoursierError => + handleCoursierError(instrumented, e) + ctx.compiler + } + processScalaInlineInputs( + doc, + inputs, + relpath, + filename, + sectionInputs, + instrumented, + compiler + ) + } def handleCoursierError(instrumented: Instrumented, e: CoursierError): Unit = { e match { case m: MultipleResolutionError => @@ -181,6 +230,7 @@ class Processor(implicit ctx: Context) { instrumented: Instrumented, markdownCompiler: MarkdownCompiler ): Unit = { + // TODO Possibly hook in here? val rendered = MarkdownBuilder.buildDocument( markdownCompiler, ctx.reporter, @@ -266,6 +316,41 @@ class Processor(implicit ctx: Context) { } } + def processScalaInlineInputs( + doc: MarkdownFile, + inputs: List[ScalaInlineInput], + relpath: RelativePath, + filename: String, + sectionInputs: List[SectionInput], + instrumented: Instrumented, + markdownCompiler: MarkdownCompiler + ): Unit = { + val rendered = MarkdownBuilder.buildDocument( + markdownCompiler, + ctx.reporter, + sectionInputs, + instrumented, + filename + ) +// println("Rendered: " + rendered) + rendered.sections.foreach { section => + println("Rendered Section: " + section) + } + rendered.sections.zip(inputs).foreach { case (section, ScalaInlineInput(block, _, mod)) => + block.newInfo = Some("scala") + def defaultRender: String = + Renderer.renderEvaluatedSection( + rendered, + section, + ctx.reporter, + ctx.settings.variablePrinter, + markdownCompiler + ) + implicit val pprintColor = TPrintColors.BlackWhite + block.newBody = Some(defaultRender) + } + } + def appendChild(doc: MarkdownFile, text: String): Unit = { if (text.nonEmpty) { doc.appendText(text) @@ -276,13 +361,19 @@ class Processor(implicit ctx: Context) { toReplace.newPart = Some(text) } + def replaceNodeWithText(doc: MarkdownFile, toReplace: InlineMdoc, text: String): Unit = { + toReplace.newPart = Some(text) + } + def collectFenceInputs( doc: MarkdownFile - ): (List[ScalaFenceInput], List[StringFenceInput], List[PreFenceInput]) = { + ): (List[ScalaFenceInput], List[StringFenceInput], List[PreFenceInput], List[ScalaInlineInput]) = { val InterestingCodeFence = new FenceInput(ctx, doc.input) + val InterestingInlineCode = new InlineInput(ctx, doc.input) val inputs = List.newBuilder[ScalaFenceInput] val strings = List.newBuilder[StringFenceInput] val pres = List.newBuilder[PreFenceInput] + val inlineInputs = List.newBuilder[ScalaInlineInput] doc.parts.foreach { case InterestingCodeFence(input) => input.mod match { @@ -293,8 +384,19 @@ class Processor(implicit ctx: Context) { case _ => inputs += input } + case InterestingInlineCode(input) => +// println("Collecting inline: " + input) + inlineInputs += input +// input.mod match { +// case string: Str => +// strings += StringFenceInput(input.block, input.input, string) +// case pre: Pre => +// pres += PreFenceInput(input.block, input.input, pre) +// case _ => +// inputs += input +// } case _ => } - (inputs.result(), strings.result(), pres.result()) + (inputs.result(), strings.result(), pres.result(), inlineInputs.result()) } } diff --git a/mdoc/src/main/scala/mdoc/internal/markdown/Renderer.scala b/mdoc/src/main/scala/mdoc/internal/markdown/Renderer.scala index 3f5957ce5..9cfd71d8a 100644 --- a/mdoc/src/main/scala/mdoc/internal/markdown/Renderer.scala +++ b/mdoc/src/main/scala/mdoc/internal/markdown/Renderer.scala @@ -53,29 +53,34 @@ object Renderer { reporter: Reporter, edit: TokenEditDistance ): String = { - require(section.mod.isCrash, section.mod) - val out = new ByteArrayOutputStream() - val ps = new PrintStream(out) - ps.println("```scala") - ps.println(section.source.pos.text) - val crashes = for { - statement <- section.section.statements - binder <- statement.binders - if binder.value.isInstanceOf[Crashed] - } yield binder.value.asInstanceOf[Crashed] - crashes.headOption match { - case Some(CrashResult.Crashed(e, _)) => - MdocExceptions.trimStacktrace(e) - val stacktrace = new ByteArrayOutputStream() - e.printStackTrace(new PrintStream(stacktrace)) - appendFreshMultiline(ps, stacktrace.toString()) - ps.append('\n') - case None => - val mpos = section.source.pos.toUnslicedPosition - reporter.error(mpos, "Expected runtime exception but program completed successfully") + section.mod match { + case fenceModifier: Modifier => + require(fenceModifier.isCrash, fenceModifier) + val out = new ByteArrayOutputStream() + val ps = new PrintStream(out) + ps.println("```scala") + ps.println(section.source.pos.text) + val crashes = for { + statement <- section.section.statements + binder <- statement.binders + if binder.value.isInstanceOf[Crashed] + } yield binder.value.asInstanceOf[Crashed] + crashes.headOption match { + case Some(CrashResult.Crashed(e, _)) => + MdocExceptions.trimStacktrace(e) + val stacktrace = new ByteArrayOutputStream() + e.printStackTrace(new PrintStream(stacktrace)) + appendFreshMultiline(ps, stacktrace.toString()) + ps.append('\n') + case None => + val mpos = section.source.pos.toUnslicedPosition + reporter.error(mpos, "Expected runtime exception but program completed successfully") + } + ps.println("```") + out.toString() + case modifierInline: ModifierInline => + throw new RuntimeException("TODO Should never happen. How to guarantee this earlier?") } - ps.println("```") - out.toString() } @deprecated("this method will be removed", "2020-06-01") diff --git a/mdoc/src/main/scala/mdoc/internal/markdown/SectionInput.scala b/mdoc/src/main/scala/mdoc/internal/markdown/SectionInput.scala index e6b14b255..dd05c1348 100644 --- a/mdoc/src/main/scala/mdoc/internal/markdown/SectionInput.scala +++ b/mdoc/src/main/scala/mdoc/internal/markdown/SectionInput.scala @@ -9,11 +9,17 @@ import mdoc.internal.cli.{Context => MContext} import scala.meta.parsers.Parsed.Success import mdoc.internal.pos.PositionSyntax._ -case class SectionInput(input: Input, source: ParsedSource, mod: Modifier) +case class SectionInput(input: Input, source: ParsedSource, mod: GenModifier) object SectionInput { def tokenEdit(sections: List[SectionInput], instrumented: Input): TokenEditDistance = { + sections.foreach { section => + println("SectionInput.tokenEdit.section: " + section) + } + sections.foreach { section => + println("SectionInput.tokenEdit.instrumented: " + instrumented) + } TokenEditDistance.fromTrees(sections.map(_.source.source), instrumented) } diff --git a/tests/unit/src/test/scala-2/tests/markdown/CompileOnlySuite.scala b/tests/unit/src/test/scala-2/tests/markdown/CompileOnlySuite.scala index 1e76d8f1b..2aaf81f50 100644 --- a/tests/unit/src/test/scala-2/tests/markdown/CompileOnlySuite.scala +++ b/tests/unit/src/test/scala-2/tests/markdown/CompileOnlySuite.scala @@ -1,5 +1,7 @@ package tests.markdown +// TODO Replicate these types of tests for inline. +// Also: why does this only exists for Scala-2? class CompileOnlySuite extends BaseMarkdownSuite { check( "compile-only", diff --git a/tests/unit/src/test/scala/tests/markdown/FailSuite.scala b/tests/unit/src/test/scala/tests/markdown/FailSuite.scala index fb0e3b2dc..775829e39 100644 --- a/tests/unit/src/test/scala/tests/markdown/FailSuite.scala +++ b/tests/unit/src/test/scala/tests/markdown/FailSuite.scala @@ -377,4 +377,30 @@ class FailSuite extends BaseMarkdownSuite { """.stripMargin ) ) + + check( + "mismatchInline", + """`scala mdoc:fail val x: Int = "Inline"`""", + """|```scala + |val x: Int = "String" + |// error: type mismatch; + |// found : String("String") + |// required: Int + |// val x: Int = "String" + |// ^^^^^^^^ + |``` + """.stripMargin, + compat = Map( + Compat.Scala3 -> + """|```scala + |val x: Int = "String" + |// error: + |// Found: String("String") + |// Required: Int + |// val x: Int = "String" + |// ^^^^^^^^ + |``` + """.stripMargin + ) + ) } diff --git a/tests/unit/src/test/scala/tests/markdown/MarkdownFileInlineSuite.scala b/tests/unit/src/test/scala/tests/markdown/MarkdownFileInlineSuite.scala new file mode 100644 index 000000000..c1b79c6d4 --- /dev/null +++ b/tests/unit/src/test/scala/tests/markdown/MarkdownFileInlineSuite.scala @@ -0,0 +1,61 @@ +package tests.markdown + +import munit.FunSuite +import mdoc.internal.markdown.MarkdownFile +import scala.meta.inputs.Input +import mdoc.internal.io.ConsoleReporter +import mdoc.internal.markdown.InlineCode +import mdoc.internal.markdown.InlineMdoc +import mdoc.internal.markdown.Text +import mdoc.internal.markdown.MarkdownPart +import mdoc.internal.markdown.CodeFence +import scala.meta.io.RelativePath +import mdoc.internal.cli.InputFile +import scala.meta.io.AbsolutePath +import java.nio.file.Files +import mdoc.internal.cli.Settings +import scala.meta.internal.io.PathIO + +class MarkdownFileInlineSuite extends FunSuite { + val reporter = new ConsoleReporter(System.out) + + def check(name: String, original: String, expected: MarkdownPart*): Unit = { + test(name) { + reporter.reset() + val input = Input.VirtualFile(name, original) + val file = InputFile.fromRelativeFilename(name, Settings.default(PathIO.workingDirectory)) + val obtained = MarkdownFile.parse(input, file, reporter).parts + require(!reporter.hasErrors) + val expectedParts = expected.toList + assertNoDiff( + pprint.tokenize(obtained).mkString, + pprint.tokenize(expectedParts).mkString + ) + } + } + + check( + "inlineSmall", + """Hello `scala mdoc println(42)` World""".stripMargin, + Text("Hello "), + InlineMdoc(Text("scala mdoc"), Text("println(42)")), + Text(" World"), + ) + + check( + "inlineCrash", + """Hello `scala mdoc:crash println(42)` World""".stripMargin, + Text("Hello "), + InlineMdoc(Text("scala mdoc:crash"), Text("println(42)")), + Text(" World"), + ) + + check( + "inlineIgnoreNonMdoc", + """Hello `println("Unevaluated code")` World""".stripMargin, + Text("Hello "), + Text("""`println("Unevaluated code")`"""), + Text(" World"), + ) + +}