Skip to content

Commit 929dfc3

Browse files
Experimental support for Markdown (#1268)
* Markdown support PoC * Markdown support fixes - Hide markdown support behind a feature flag (`--enable-markdown`) - still process explicitly passed Markdown files - remove some PoC simplifications and workarounds - misc refactors * Add Markdown guide Co-authored-by: Marcin K <github@anubis.email>
1 parent a07a2d4 commit 929dfc3

File tree

17 files changed

+940
-15
lines changed

17 files changed

+940
-15
lines changed

modules/build/src/main/scala/scala/build/CrossSources.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ object CrossSources {
156156
.flatMap(_.options)
157157
.flatMap(_.internal.extraSourceFiles)
158158
.distinct
159-
val inputsElemFromDirectives = value(resolveInputsFromSources(sourcesFromDirectives))
159+
val inputsElemFromDirectives =
160+
value(resolveInputsFromSources(sourcesFromDirectives, inputs.enableMarkdown))
160161
val preprocessedSourcesFromDirectives = value(preprocessSources(inputsElemFromDirectives))
161162
val allInputs = inputs.add(inputsElemFromDirectives)
162163

@@ -228,15 +229,17 @@ object CrossSources {
228229
(CrossSources(paths, inMemory, defaultMainClassOpt, resourceDirs, buildOptions), allInputs)
229230
}
230231

231-
private def resolveInputsFromSources(sources: Seq[Positioned[os.Path]]) =
232+
private def resolveInputsFromSources(sources: Seq[Positioned[os.Path]], enableMarkdown: Boolean) =
232233
sources.map { source =>
233234
val sourcePath = source.value
234235
lazy val dir = sourcePath / os.up
235236
lazy val subPath = sourcePath.subRelativeTo(dir)
236-
if (os.isDir(sourcePath)) Right(Inputs.singleFilesFromDirectory(Inputs.Directory(sourcePath)))
237+
if (os.isDir(sourcePath))
238+
Right(Inputs.singleFilesFromDirectory(Inputs.Directory(sourcePath), enableMarkdown))
237239
else if (sourcePath.ext == "scala") Right(Seq(Inputs.ScalaFile(dir, subPath)))
238240
else if (sourcePath.ext == "sc") Right(Seq(Inputs.Script(dir, subPath)))
239241
else if (sourcePath.ext == "java") Right(Seq(Inputs.JavaFile(dir, subPath)))
242+
else if (sourcePath.ext == "md") Right(Seq(Inputs.MarkdownFile(dir, subPath)))
240243
else {
241244
val msg =
242245
if (os.exists(sourcePath))

modules/build/src/main/scala/scala/build/Inputs.scala

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ final case class Inputs(
2222
baseProjectName: String,
2323
mayAppendHash: Boolean,
2424
workspaceOrigin: Option[WorkspaceOrigin],
25+
enableMarkdown: Boolean,
2526
withRestrictedFeatures: Boolean
2627
) {
2728

@@ -31,7 +32,7 @@ final case class Inputs(
3132
def singleFiles(): Seq[Inputs.SingleFile] =
3233
elements.flatMap {
3334
case f: Inputs.SingleFile => Seq(f)
34-
case d: Inputs.Directory => Inputs.singleFilesFromDirectory(d)
35+
case d: Inputs.Directory => Inputs.singleFilesFromDirectory(d, enableMarkdown)
3536
case _: Inputs.ResourceDirectory => Nil
3637
case _: Inputs.Virtual => Nil
3738
}
@@ -52,7 +53,7 @@ final case class Inputs(
5253
def flattened(): Seq[Inputs.SingleElement] =
5354
elements.flatMap {
5455
case f: Inputs.SingleFile => Seq(f)
55-
case d: Inputs.Directory => Inputs.singleFilesFromDirectory(d)
56+
case d: Inputs.Directory => Inputs.singleFilesFromDirectory(d, enableMarkdown)
5657
case _: Inputs.ResourceDirectory => Nil
5758
case v: Inputs.Virtual => Seq(v)
5859
}
@@ -110,7 +111,7 @@ final case class Inputs(
110111
case elem: Inputs.OnDisk =>
111112
val content = elem match {
112113
case dirInput: Inputs.Directory =>
113-
Seq("dir:") ++ Inputs.singleFilesFromDirectory(dirInput)
114+
Seq("dir:") ++ Inputs.singleFilesFromDirectory(dirInput, enableMarkdown)
114115
.map(file => s"${file.path}:" + os.read(file.path))
115116
case resDirInput: Inputs.ResourceDirectory =>
116117
// Resource changes for SN require relinking, so they should also be hashed
@@ -199,6 +200,10 @@ object Inputs {
199200
extends OnDisk with SourceFile with Compiled {
200201
lazy val path: os.Path = base / subPath
201202
}
203+
final case class MarkdownFile(base: os.Path, subPath: os.SubPath)
204+
extends OnDisk with SourceFile {
205+
lazy val path: os.Path = base / subPath
206+
}
202207
final case class Directory(path: os.Path) extends OnDisk with Compiled
203208
final case class ResourceDirectory(path: os.Path) extends OnDisk
204209

@@ -218,7 +223,10 @@ object Inputs {
218223
final case class VirtualData(content: Array[Byte], source: String)
219224
extends Virtual
220225

221-
def singleFilesFromDirectory(d: Inputs.Directory): Seq[Inputs.SingleFile] = {
226+
def singleFilesFromDirectory(
227+
d: Inputs.Directory,
228+
enableMarkdown: Boolean
229+
): Seq[Inputs.SingleFile] = {
222230
import Ordering.Implicits.seqOrdering
223231
os.walk.stream(d.path, skip = _.last.startsWith("."))
224232
.filter(os.isFile(_))
@@ -229,6 +237,8 @@ object Inputs {
229237
Inputs.ScalaFile(d.path, p.subRelativeTo(d.path))
230238
case p if p.last.endsWith(".sc") =>
231239
Inputs.Script(d.path, p.subRelativeTo(d.path))
240+
case p if p.last.endsWith(".md") && enableMarkdown =>
241+
Inputs.MarkdownFile(d.path, p.subRelativeTo(d.path))
232242
}
233243
.toVector
234244
.sortBy(_.subPath.segments)
@@ -244,6 +254,7 @@ object Inputs {
244254
case _: Inputs.JavaFile => "java:"
245255
case _: Inputs.ScalaFile => "scala:"
246256
case _: Inputs.Script => "sc:"
257+
case _: Inputs.MarkdownFile => "md:"
247258
}
248259
Iterator(prefix, elem.path.toString, "\n").map(bytes)
249260
case v: Inputs.Virtual =>
@@ -268,6 +279,7 @@ object Inputs {
268279
baseProjectName: String,
269280
directories: Directories,
270281
forcedWorkspace: Option[os.Path],
282+
enableMarkdown: Boolean,
271283
withRestrictedFeatures: Boolean
272284
): Inputs = {
273285

@@ -313,7 +325,8 @@ object Inputs {
313325
baseProjectName,
314326
mayAppendHash = needsHash,
315327
workspaceOrigin = Some(workspaceOrigin0),
316-
withRestrictedFeatures
328+
enableMarkdown = enableMarkdown,
329+
withRestrictedFeatures = withRestrictedFeatures
317330
)
318331
}
319332

@@ -410,6 +423,7 @@ object Inputs {
410423
else if (arg.endsWith(".sc")) Right(Seq(Script(dir, subPath)))
411424
else if (arg.endsWith(".scala")) Right(Seq(ScalaFile(dir, subPath)))
412425
else if (arg.endsWith(".java")) Right(Seq(JavaFile(dir, subPath)))
426+
else if (arg.endsWith(".md")) Right(Seq(MarkdownFile(dir, subPath)))
413427
else if (os.isDir(path)) Right(Seq(Directory(path)))
414428
else if (acceptFds && arg.startsWith("/dev/fd/")) {
415429
val content = os.read.bytes(os.Path(arg, cwd))
@@ -436,6 +450,7 @@ object Inputs {
436450
javaSnippetList: List[String],
437451
acceptFds: Boolean,
438452
forcedWorkspace: Option[os.Path],
453+
enableMarkdown: Boolean,
439454
withRestrictedFeatures: Boolean
440455
): Either[BuildException, Inputs] = {
441456
val validatedArgs: Seq[Either[String, Seq[Element]]] =
@@ -457,6 +472,7 @@ object Inputs {
457472
baseProjectName,
458473
directories,
459474
forcedWorkspace,
475+
enableMarkdown,
460476
withRestrictedFeatures
461477
))
462478
}
@@ -477,13 +493,14 @@ object Inputs {
477493
javaSnippetList: List[String] = List.empty,
478494
acceptFds: Boolean = false,
479495
forcedWorkspace: Option[os.Path] = None,
496+
enableMarkdown: Boolean = false,
480497
withRestrictedFeatures: Boolean
481498
): Either[BuildException, Inputs] =
482499
if (
483500
args.isEmpty && scriptSnippetList.isEmpty && scalaSnippetList.isEmpty && javaSnippetList.isEmpty
484501
)
485502
defaultInputs().toRight(new InputsException(
486-
"No inputs provided (expected files with .scala or .sc extensions, and / or directories)."
503+
"No inputs provided (expected files with .scala, .sc, .java or .md extensions, and / or directories)."
487504
))
488505
else
489506
forNonEmptyArgs(
@@ -498,22 +515,25 @@ object Inputs {
498515
javaSnippetList,
499516
acceptFds,
500517
forcedWorkspace,
518+
enableMarkdown,
501519
withRestrictedFeatures
502520
)
503521

504522
def default(): Option[Inputs] =
505523
None
506524

507-
def empty(workspace: os.Path): Inputs =
525+
def empty(workspace: os.Path, enableMarkdown: Boolean): Inputs =
508526
Inputs(
509527
elements = Nil,
510528
defaultMainClassElement = None,
511529
workspace = workspace,
512530
baseProjectName = "project",
513531
mayAppendHash = true,
514532
workspaceOrigin = None,
533+
enableMarkdown = enableMarkdown,
515534
withRestrictedFeatures = false
516535
)
517536

518-
def empty(projectName: String) = Inputs(Nil, None, os.pwd, projectName, false, None, false)
537+
def empty(projectName: String): Inputs =
538+
Inputs(Nil, None, os.pwd, projectName, false, None, true, false)
519539
}

modules/build/src/main/scala/scala/build/Sources.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ object Sources {
9292
): Seq[Preprocessor] =
9393
Seq(
9494
ScriptPreprocessor(codeWrapper),
95+
MarkdownPreprocessor,
9596
JavaPreprocessor(archiveCache, javaClassNameVersionOpt, javaCommand),
9697
ScalaPreprocessor,
9798
DataPreprocessor
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package scala.build.internal.markdown
2+
3+
import scala.annotation.tailrec
4+
import scala.collection.mutable
5+
import scala.jdk.CollectionConverters.*
6+
7+
/** Representation for a (closed) code block contained in Markdown
8+
*
9+
* @param info
10+
* a list of tags tied to a given code block
11+
* @param body
12+
* the code block content
13+
* @param startLine
14+
* starting line on which the code block was defined (excluding backticks)
15+
* @param endLine
16+
* end line on which the code block was closed (excluding backticks)
17+
*/
18+
case class MarkdownCodeBlock(
19+
info: Seq[String],
20+
body: String,
21+
startLine: Int,
22+
endLine: Int
23+
) {
24+
25+
/** @return
26+
* `true` if this snippet should be ignored, `false` otherwise
27+
*/
28+
def shouldIgnore: Boolean = info.head != "scala" || info.contains("ignore")
29+
30+
/** @return
31+
* `true` if this snippet should have its scope reset, `false` otherwise
32+
*/
33+
def resetScope: Boolean = info.contains("reset")
34+
35+
/** @return
36+
* `true` if this snippet is a test snippet, `false` otherwise
37+
*/
38+
def isTest: Boolean = info.contains("test")
39+
40+
/** @return
41+
* `true` if this snippet is a raw snippet, `false` otherwise
42+
*/
43+
def isRaw: Boolean = info.contains("raw")
44+
}
45+
46+
object MarkdownCodeBlock {
47+
48+
/** Finds all code snippets in given input
49+
*
50+
* @param md
51+
* Markdown file in a `String` format
52+
* @return
53+
* list of all found snippets
54+
*/
55+
def findCodeBlocks(md: String): Seq[MarkdownCodeBlock] = {
56+
val allLines = md
57+
.lines()
58+
.toList
59+
.asScala
60+
@tailrec
61+
def findCodeBlocksRec(
62+
lines: Seq[String],
63+
closedFences: Seq[MarkdownCodeBlock] = Seq.empty,
64+
maybeOpenFence: Option[MarkdownOpenFence] = None,
65+
currentIndex: Int = 0
66+
): Seq[MarkdownCodeBlock] = if lines.isEmpty then closedFences
67+
else {
68+
val currentLine = lines.head
69+
val (newClosedFences, newOpenFence) = maybeOpenFence match {
70+
case None => closedFences -> MarkdownOpenFence.maybeFence(currentLine, currentIndex)
71+
case mof @ Some(openFence) =>
72+
val backticksStart = currentLine.indexOf(openFence.backticks)
73+
if backticksStart == openFence.indent &&
74+
currentLine.forall(c => c == '`' || c.isWhitespace)
75+
then (closedFences :+ openFence.closeFence(currentIndex, allLines.toArray)) -> None
76+
else closedFences -> mof
77+
}
78+
findCodeBlocksRec(lines.tail, newClosedFences, newOpenFence, currentIndex + 1)
79+
}
80+
findCodeBlocksRec(allLines.toSeq).filter(!_.shouldIgnore)
81+
}
82+
}

0 commit comments

Comments
 (0)