Block Elements
Block
Blocks are kind of the grouping elements of a system. Other things are laid out
inside of Blocks: namely Spans, but also, other blocks. In the markdown, blocks
are usually seen as separated by whitespace lines.
// In com/tristanhunt/knockoff/Block.scala
// See the Block package and imports
trait Block extends BlockSeq {
/**
The actual content of each block.
*/
val span : SpanSeq
/**
A markdown representation of this block - it may not equal the original
source.
*/
def markdown : String
/**
An HTML rendering of the Block element.
*/
def xml : Node
/**
The original source position used to make up this block.
*/
val position : Position
}
In many cases, the Block can not contain other blocks.
// In com/tristanhunt/knockoff/SimpleBlock.scala
package com.tristanhunt.knockoff
trait SimpleBlock extends Block {
override def theSeq : Seq[ Block ] = List( this )
}
In some other cases, the block is a pretty complex thing:
// In com/tristanhunt/knockoff/ComplexBlock.scala
package com.tristanhunt.knockoff
trait ComplexBlock extends Block {
val children : Seq[ Block ]
val span = new SpanSeq{ def theSeq = children.flatMap( _.span ) }
def theSeq = children
def childrenMarkdown = children.map( _.markdown ).mkString("\n") + "\n"
def childrenXML = children.map( _.xml )
}
There are moments when we need to capture a series of blocks as a ComplexBlock
without any other real information.
// In com/tristanhunt/knockoff/GroupBlock.scala
package com.tristanhunt.knockoff
import scala.xml.{ Node, Group }
import scala.util.parsing.input.{ NoPosition, Position }
class GroupBlock( val children : Seq[ Block ] )
extends ComplexBlock {
val position : Position = children.firstOption match {
case None => NoPosition
case Some( child ) => child.position
}
def markdown = childrenMarkdown
def xml : Node = Group( children.map( _.xml ) )
override def toString = "GroupBlock(" + markdown + ")"
}
BlockSeq
The BlockSeq adds searching methods over Block elements. It is also the
main return type of the KnockOff.parse method.
One of the shorthand filter expressions allows for you to indicate the "BlockType".
// In com/tristanhunt/knockoff/BlockType.scala
package com.tristanhunt.knockoff
/**
* Used to indicate the type of blocks you are interested in when filtering via
* the BlockSeq.filterType method.
*/
trait BlockType[T <: Block] { def wrappedClass : Class[ T ] }
case object Paragraphs
extends BlockType[ Paragraph ] { def wrappedClass = classOf[ Paragraph ] }
case object Headers
extends BlockType[ Header ] { def wrappedClass = classOf[ Header ] }
case object LinkDefinitions
extends BlockType[ LinkDefinition ] {
def wrappedClass = classOf[ LinkDefinition ]
}
case object Blockquotes
extends BlockType[ Blockquote ] { def wrappedClass = classOf[ Blockquote ] }
case object CodeBlocks
extends BlockType[ CodeBlock ] { def wrappedClass = classOf[ CodeBlock ] }
case object HorizontalRules
extends BlockType[ HorizontalRule ] {
def wrappedClass = classOf[ HorizontalRule ]
}
case object OrderedItems
extends BlockType[ OrderedItem ] { def wrappedClass = classOf[ OrderedItem ] }
case object UnorderedItems
extends BlockType[ UnorderedItem ] {
def wrappedClass = classOf[ UnorderedItem ]
}
case object MarkdownLists
extends BlockType[ MarkdownList ] { def wrappedClass = classOf[ MarkdownList ] }
This is used only as a short hand for query expressions.
// In com/tristanhunt/knockoff/BlockSeq.scala
// See the BlockSeq package and imports
trait BlockSeq extends Seq[ Block ] {
def theSeq : Seq[ Block ]
override def length : Int = theSeq.length
override def elements = theSeq.elements
override def apply( ii : Int ) = theSeq(ii)
def toXML : Node = Group( theSeq.map( _.xml ) )
/**
* Returns a BlockSeq that contains only that particular block type.
*/
def filterType [ T <: Block ] ( blockType : BlockType[T] ) : BlockSeq = {
BlockSeq.fromSeq( filter( blockType.wrappedClass.isInstance( _ ) ) )
}
/** Shorthand for the filter method. */
def filterType ( query : Block => Boolean ) : BlockSeq =
BlockSeq.fromSeq( filter( query ) )
}
object BlockSeq {
def fromSeq( seq : Seq[ Block ] ) = new BlockSeq {
override def theSeq = seq
}
}
Paragraph
The text block element that is represented inside a <p> tag. Does not
contain any other useful metadata.
Otherwise the paragraph is a simple Block type (does not contain other
Blocks).
// In com/tristanhunt/knockoff/Paragraph.scala
// See the Paragraph package and imports
class Paragraph( val span : SpanSeq, val position : Position )
extends SimpleBlock {
def markdown = span.toMarkdown
/**
If this paragraph only contains HTMLSpan elements, then just pass that
information through without a paragraph marker.
*/
def xml =
if ( isHTML ) span.toXML else <p>{ span.toXML }</p>
def isHTML : Boolean = ! span.exists(
s => s match {
case html : HTMLSpan => false
case text : Text => ! text.content.trim.isEmpty
case _ => true
}
)
// See the Paragraph toString, equals, hashCode implementations
}
Header
Represents an <h1>, <h2>, etc., element in the markdown document.
The header element is a simple Block type, but can contain any kind of
SpanSeq.
The markdown representation of the Header should always be the ATX-style
headers - # Header #.
// In com/tristanhunt/knockoff/Header.scala
// See the Header package and imports
class Header( val level : Int, val span : SpanSeq, val position : Position )
extends SimpleBlock {
def markdown = {
val sb = new StringBuilder
( 0 until level ).foreach( _ => sb.append("#") )
sb.append(" ").append( span.toMarkdown ).append(" ")
( 0 until level ).foreach( _ => sb.append("#") )
sb.toString
}
def xml = level match {
case 1 => <h1>{ span.toXML }</h1>
case 2 => <h2>{ span.toXML }</h2>
case 3 => <h3>{ span.toXML }</h3>
case 4 => <h4>{ span.toXML }</h4>
case 5 => <h5>{ span.toXML }</h5>
case 6 => <h6>{ span.toXML }</h6>
case _ => <div class={ "header" + level }>{ span.toXML }</div>
}
// See the Header toString, equals, hashCode implementations
}
LinkDefinition
When a link is created with the definition like [something][id], that id can
be defined later on a string like [id]: url "optional title".
// In com/tristanhunt/knockoff/LinkDefinition.scala
// See the LinkDefinition package and imports
class LinkDefinition(
val id : String,
val url : String,
val title : Option[ String ],
val position : Position
)
extends SimpleBlock {
val span = Span.empty
def xml : Node = Group( Nil )
def markdown = {
val sb = new StringBuilder
sb.append("[").append( id ).append("]: ").append( url ).append(
title match {
case None => ""
// Remember that "This "Title" is Valid" as a single title.
case Some( titleValue ) => "\"" + titleValue + "\""
}
)
sb.toString
}
// See the LinkDefinition toString, equals, hashCode implementations
}
Blockquote
A block quote is really another markdown document, quoted.
// In com/tristanhunt/knockoff/Blockquote.scala
// See the Blockquote package and imports
class Blockquote(
val children : Seq[ Block ],
val position : Position
)
extends ComplexBlock {
def markdown : String = {
Source.fromString( childrenMarkdown )
.getLines.map( l => "> " + l ).mkString("")
}
def xml : Elem = <blockquote>{ childrenXML }</blockquote>
// See the Blockquote toString, equals, hashCode implementations
}
CodeBlock
The code block is a chunk of preformatted text to this system. Note that this means we completely ignore all formatting, and do escaping. This means that sequences like
<later>
become this in the output:
<pre><code>≤later></pre></code>
This means that in order to inject actual HTML inside the final code, you'll have to
write up an HTML code element. This could be seen as a later transformation, say,
if you want to inject a series of line numbers via <span> elements.
// In com/tristanhunt/knockoff/CodeBlock.scala
// See the CodeBlock package and imports
class CodeBlock( val text : Text, val position : Position )
extends SimpleBlock {
def this( preformatted : String, position : Position ) =
this( new Text( preformatted ), position )
val span = text
val preformatted = text.markdown
lazy val preformattedLines =
Source.fromString( preformatted ).getLines
def markdown =
preformattedLines.map{ line => " " + line }.mkString("")
def xml : Node = <pre><code>{ preformatted }</code></pre>
// See the CodeBlock toString, equals, hashCode implementations
}
HorizontalRule
Represents a <hr/> injected into content. Note that this does not happen to do
anything but replace a line of asterixes, underscores, or hyphens.
// In com/tristanhunt/knockoff/HorizontalRule.scala
package com.tristanhunt.knockoff
// See the HorizontalRule imports
class HorizontalRule( val position : Position ) extends SimpleBlock {
def markdown = "* * *"
val span = new Text( markdown )
def xml = <hr/>
// See the HorizontalRule toString, equals, hashCode implementations
}
Lists - Unordered, Ordered, Simple, and Complex
Lists in markdown can be simple or complex. It becomes complex when any one of the
list items have more than one Block. They are either tagged as ordered or
unordered, though the ElementFactory has helper methods to make this decision.
How this might look in code:
val olist = olist(
oi( para("some text") ),
oi( para("uno"), para("dos") )
)
val ulist = ulist(
ui("list item 1"),
ui("list item 2")
)
Note the difference between the list items oi of the olist and the ui of the
ulist. This makes discovery of the list much simpler during the parsing process,
and prevents you from mistakenly globbing inconsistent lists together.
These codes would be represented as:
<ol>
<li><p>some text</p></li>
<li><p>uno</p><p>dos</p><li>
</ol>
<ul>
<li>list item 1</li>
<li>list item 2</li>
</ul>
Only in the simple list case do we let the <li> operate as a simple implicit block
container.
In implementation terms, we don't have a single list.
ListItem
// In com/tristanhunt/knockoff/ListItem.scala
package com.tristanhunt.knockoff
// See the ListItem imports
abstract class ListItem(
val items : Seq[ Block ],
val position : Position
)
extends ComplexBlock {
val children = new GroupBlock( items )
def itemPrefix : String
def markdown : String = {
if ( children.isEmpty ) return ""
return (
( itemPrefix + children.first.markdown + " \n" ) +
children.drop(1).map( " " + _.markdown + " \n" ).mkString( "" )
)
}
def xml( complex : Boolean ) : Node = <li>{
if ( isComplex )
childrenXML
else
children.first.span.toXML
}</li>
def xml : Node = xml( isComplex )
def isComplex = items.length > 1
def + ( block : Block ) : ListItem
// See the ListItem toString, equals, hashCode implementation
}
OrderedItem
// In com/tristanhunt/knockoff/OrderedItem.scala
package com.tristanhunt.knockoff
import scala.util.parsing.input.Position
class OrderedItem( items : Seq[ Block ], position : Position )
extends ListItem( items, position ) {
def this( block : Block, position : Position ) =
this( Seq( block ), position )
def itemPrefix = "1. "
def + ( b : Block ) : ListItem =
new OrderedItem( children ++ Seq(b), children.first.position )
}
UnorderedItem
// In com/tristanhunt/knockoff/UnorderedItem.scala
package com.tristanhunt.knockoff
import scala.util.parsing.input.Position
class UnorderedItem( items : Seq[ Block ], position : Position )
extends ListItem( items, position ) {
def this( block : Block, position : Position ) =
this( Seq( block ), position )
def itemPrefix = "* "
def + ( b : Block ) : ListItem =
new UnorderedItem( children ++ Seq(b), children.first.position )
}
MarkdownList
Lists just contain items, and are either ordered or unordered.
The position of a list is a little spurious: the start of the list should be the position of the first item, however, it's elements may not contain the entire content; whitespace will be missing in complex cases.
// In com/tristanhunt/knockoff/MarkdownList.scala
package com.tristanhunt.knockoff
// See the MarkdownList imports
/**
* @param ordered Alters the output, mostly.
*/
abstract class MarkdownList(
val items : Seq[ ListItem ]
) extends ComplexBlock {
val children = items
val position = children.firstOption match {
case None => NoPosition
case Some( child ) => child.position
}
def markdown = childrenMarkdown
/**
Create a new list with the block appended to the last item.
*/
def + ( block : Block ) : MarkdownList
// See the MarkdownList toString, equals, hashCode implementations
}
class OrderedList( items : Seq[ ListItem ] )
extends MarkdownList( items ) {
def xml = <ol>{ childrenXML }</ol>
def + ( item : OrderedItem ) : OrderedList =
new OrderedList( children ++ Seq(item) )
def + ( block : Block ) : MarkdownList = new OrderedList(
children.take( children.length - 1 ) ++ Seq(children.last + block)
)
}
class UnorderedList( items : Seq[ ListItem ] )
extends MarkdownList( items ) {
def xml = <ul>{ childrenXML }</ul>
def + ( item : UnorderedItem ) : UnorderedList =
new UnorderedList( children ++ Seq(item) )
def + ( block : Block ) : MarkdownList = new UnorderedList(
children.take( children.length - 1 ) ++ Seq(children.last + block)
)
}
Block Specification
// In test com/tristanhunt/knockoff/BlockSuite.scala
package com.tristanhunt.knockoff
import scala.util.parsing.input.NoPosition
import org.scalatest._
import matchers._
class BlockSuite extends Spec with ShouldMatchers with HasElementFactory {
val factory = elementFactory
import factory._
describe("BlockSeq") {
it( "should filter Paragraphs and Headers properly with filterType" ) {
val p1 = para( t("p1"), NoPosition )
val h1 = head( 1, t("h1"), NoPosition )
val blocks = BlockSeq.fromSeq( List( p1, h1 ) )
( blocks filterType Paragraphs ) should have length (1)
assert( ( blocks filterType Paragraphs ) contains p1 )
( blocks filterType Headers ) should have length (1)
assert( ( blocks filterType Headers ) contains h1 )
}
}
}
References
Block
// The Block package and imports
package com.tristanhunt.knockoff
import scala.xml.Node
import scala.util.parsing.input.Position
BlockSeq
// The BlockSeq package and imports
package com.tristanhunt.knockoff
import scala.xml.{ Node, Elem, Group }
Paragraph
// The Paragraph package and imports
package com.tristanhunt.knockoff
import scala.xml.Elem
import scala.util.parsing.input.Position
// The Paragraph toString, equals, hashCode implementations
override def toString = "Paragraph(" + markdown + ")"
override def equals( rhs : Any ):Boolean = rhs match {
case oth : Paragraph => ( oth canEqual this ) && ( this sameElements oth )
case _ => false
}
def canEqual( p : Paragraph ) : Boolean = ( getClass == p.getClass )
def sameElements( p : Paragraph ) : Boolean = {
( span == p.span ) &&
( position == p.position )
}
override def hashCode : Int = span.hashCode + position.hashCode
Header
// The Header package and imports
package com.tristanhunt.knockoff
import scala.xml.Elem
import scala.util.parsing.input.Position
// The Header toString, equals, hashCode implementations
override def toString = "Header(" + markdown + ")"
override def equals( rhs : Any ):Boolean = rhs match {
case oth : Header => ( oth canEqual this ) && ( this sameElements oth )
case _ => false
}
def canEqual( p : Header ) : Boolean = ( getClass == p.getClass )
def sameElements( p : Header ) : Boolean = {
( level == p.level ) &&
( span == p.span ) &&
( position == p.position )
}
override def hashCode : Int =
43 + level + span.hashCode + position.hashCode
LinkDefinition
// The LinkDefinition package and imports
package com.tristanhunt.knockoff
import scala.xml.{ Node, Group }
import scala.util.parsing.input.Position
// The LinkDefinition toString, equals, hashCode implementations
override def toString = "LinkDefinition(" + markdown + ")"
override def equals( rhs : Any ):Boolean = rhs match {
case oth : LinkDefinition => ( oth canEqual this ) && ( this sameElements oth )
case _ => false
}
def canEqual( p : LinkDefinition ) : Boolean = ( getClass == p.getClass )
def sameElements( p : LinkDefinition ) : Boolean = {
( id == p.id ) &&
( url == p.url ) &&
( title == p.title ) &&
( position == p.position )
}
override def hashCode : Int =
43 + id.hashCode + url.hashCode + span.hashCode + position.hashCode
Blockquote
// The Blockquote package and imports
package com.tristanhunt.knockoff
import scala.io.Source
import scala.xml.Elem
import scala.util.parsing.input.Position
// The Blockquote toString, equals, hashCode implementations
override def toString = "Blockquote(" + markdown + ")"
override def equals( rhs : Any ):Boolean = rhs match {
case oth : Blockquote => ( oth canEqual this ) && ( this sameElements oth )
case _ => false
}
def canEqual( b : Blockquote ) : Boolean = ( getClass == b.getClass )
def sameElements( b : Blockquote ) = {
( children sameElements b.children ) &&
( position == b.position )
}
override def hashCode : Int = {
43 + (
( 3 /: children ){
(sum, child) => { sum + 43 + 3 * child.hashCode }
}
)
}
CodeBlock
// The CodeBlock package and imports
package com.tristanhunt.knockoff
import scala.xml.{ Node, Unparsed }
import scala.io.Source
import scala.util.parsing.input.Position
// The CodeBlock toString, equals, hashCode implementations
override def toString = "CodeBlock(" + preformatted + ")"
override def hashCode : Int = preformatted.hashCode
override def equals( rhs : Any ) : Boolean = rhs match {
case t : CodeBlock => t.canEqual( this ) && ( this sameElements t )
case _ => false
}
def sameElements( cb : CodeBlock ) : Boolean = {
( cb.preformatted == preformatted ) &&
( cb.position == position )
}
def canEqual( t : CodeBlock ) : Boolean = t.getClass == getClass
HorizontalRule
// The HorizontalRule imports
import scala.xml.{ Node, Unparsed }
import scala.io.Source
import scala.util.parsing.input.Position
// The HorizontalRule toString, equals, hashCode implementations
override def toString = "HorizontalRule"
override def hashCode : Int = position.hashCode + 47
override def equals( rhs : Any ) : Boolean = rhs match {
case t : HorizontalRule => t.canEqual( this ) && ( t.position == position )
case _ => false
}
def canEqual( t : HorizontalRule ) : Boolean = t.getClass == getClass
ListItem
// The ListItem imports
import scala.util.parsing.input.Position
import scala.xml.Node
// The ListItem toString, equals, hashCode implementations
override def toString = "ListItem(" + markdown + ")"
override def hashCode : Int = {
( 11 /: children )( (total, child) => total + 51 + 3 * child.hashCode ) +
position.hashCode + 47
}
override def equals( rhs : Any ) : Boolean = rhs match {
case t : ListItem => t.canEqual( this ) && ( this sameElements t )
case _ => false
}
def sameElements( ci : ListItem ) : Boolean = {
( children == ci.children ) &&
( position == ci.position )
}
def canEqual( t : ListItem ) : Boolean = t.getClass == getClass
MarkdownList
// The MarkdownList imports
import scala.io.Source
import scala.util.parsing.input.{ NoPosition, Position }
// The MarkdownList toString, equals, hashCode implementations
override def toString = "MarkdownList(" + markdown + ")"
override def hashCode : Int = {
( 13 /: children )( (total, child) => total + 51 + 3 * child.hashCode ) +
position.hashCode + 47
}
override def equals( rhs : Any ) : Boolean = rhs match {
case t : MarkdownList => t.canEqual( this ) && ( t sameElements this )
case _ => false
}
def sameElements( ml : MarkdownList ) : Boolean = {
( children sameElements ml.children )
}
def canEqual( t : MarkdownList ) : Boolean = t.getClass == getClass