tpolecat / tut

doc/tutorial generator for scala
MIT License
579 stars 63 forks source link

Feature Request: Ability to import external content into tut blocks #187

Open metasim opened 7 years ago

metasim commented 7 years ago

I have a number of (Spark-based) tutorials that require the same front-matter to be run along with a shutdown routine and the end. I have to copy/paste these blocks of code into each of my tut documents. It would be great if we could have a modifier that indicates that the code should be read from an external file. For example:

```tut:import(baz/MySetup.scala)
``` 

Even better would be the ability to select subsections of the file by line:

```tut:import(baz/MySetup.scala)
2-10
12-15
20
``` 

I started work on implementing this myself, but the code structure is beyond my FP comfort zone, and am not quite sure where to inject the new functionality.

tpolecat commented 7 years ago

You can trick sbt into doing this for you, with a task that preprocesses your source and adds the header/footer material. But that's kind of awful to deal with. I like your idea. 👍

metasim commented 7 years ago

Your comment on #172 makes me think you were talking about this item. Unlike that one, I'm not comfortable doing this one, as I really haven't grokked the order of operations, state scoping, sequencing etc. with the State and IO monads. Still don't have the right mental model. As I stated above I got this far and then gave up. :-/

olafurpg commented 6 years ago

@metasim What is the difference between this feature and something like here below?

```tut:silent
val init = baz.Example()
import init._


assuming that `baz.Example` is on your classpath 
metasim commented 6 years ago

@olafurpg

You're suggested technique is interesting, but doesn't allow for selective tut execution of code fragments interspersed with explanatory Markdown. Still, I might be able to get some mileage from it.

The original impetus was to allow sections from one file to be selectively injected into the tut stream. If one were to use file names and line numbers as the mechanism to do this, it would take the form outlined below. That said, I actually think using line numbers is clumsy and too fragile. Something like Code Externalization in knitr is closer to the mark, where marker comments in the source trigger the inclusion blocks.

Another way of looking at it is finding a balance between where tut is today (Markdown as the record of authority) and literator (Scala source as the record of authority). I'd like Markdown to be the record of authority for the prose, and Scala source as the record of authority on the code fragments.


Given this Scala source:

object Baloney {
  val line2 = "line2"
  val line3 = "line3"
  val line4 = "line4"
  def line5(something: String): Any = {
    something + " is now on line 6"
  }
  line5("line8")
}

...and this Markdown source:

The start

```tut:import(baz/Baloney.scala)
2
```

```tut
line2
```

```tut:fail
line3
```

```tut:import(baz/Baloney.scala)
3
4
```

```tut:import(baz/Baloney.scala)
5-8
```

```tut:book
line5("tut block")
```

The end

I'd expect something like this output (hand evaluating here...):

The start

```scala
scala>  val line2 = "line2"
line2: String = line2
```

```scala
scala> line2
res0: String = line2
```

```scala
scala> line3
:12: error: not found: value line3
       line3
       ^
```

```scala
scala>   val line3 = "line3"
line3: String = line3

scala>   val line4 = "line4"
line4: String = line4
```

```scala
scala>   def line5(something: String): Any = {
     |     something + " is now on line 6"
     |   }
line5: (something: String)Any

scala>   line5("line8")
res2: Any = line8 is now on line 6
```

```scala
line5("tut block")
// res3: Any = tut block is now on line 6
```

The end
olafurpg commented 6 years ago

I think I see what you mean. Yes, my workaround is insufficient if you want to inline the declarations. However, building on top of line numbers seems fragile and cryptic to read, to me it's not obvious what the following does

```tut:import(baz/Baloney.scala)
2

I don't have any good suggestion. One hacky way might be to inline the code manually with passthrough

```scala
@ scala.io.Source.fromFile("Example.scala").getLines.drop(1).take(1).mkString
res3: String = "  val line2 = \"2\""

but that won't explicitly annotate all types and runtime values for each computed value.

metasim commented 6 years ago

Yeh, I think line numbers is the wrong way to go. Playing off the knitr Code Externalization, I wonder if something like this would be closer to the mark:

Scala:

object Baloney {
  // tut:begin:a
  val line2 = "line2"
  // tut:end
  // tut:begin:b
  val line3 = "line3"
  val line4 = "line4"
  // tut:end
  // tut:begin:c
  def line5(something: String): Any = {
    something + " is now on line 6"
  }
  // tut:end
  line5("line8")
}

Markdown:


```tut:import(baz/Baloney.scala):a
```

```tut:book:import(baz/Baloney.scala):b
```

```tut:silent:import(baz/Baloney.scala):c
```