Deciding which tags to apply by using the chain-of-responsibility pattern
Now that we have the means to transform a simple line into an HTML encoded line, we need a way to decide which tags we should apply. Right from the start, I knew that we would be applying yet another pattern, one that is eminently suitable for asking the question, "Should I handle this tag?" If no, then I will forward this on so that something else can decide whether or not it should handle the tag.
We are going to use another behavioral pattern to handle this—the chain-of-responsibility pattern. This pattern lets us chain together a series of classes by creating a class that accepts the next class in the chain, along with a method to handle a request. Depending on the internal logic of the request handler, it may pass processing onto the next class in the chain.
If we start off with our base class, we can see what this pattern gives us and how we are going to use it:
abstract class Handler<T> {
protected next : Handler<T> | null = null;
public SetNext(next : Handler<T>) : void {
this.next = next;
}
public HandleRequest(request : T) : void {
if (!this.CanHandle(request)) {
if (this.next !== null) {
this.next.HandleRequest(request);
}
return;
}
}
protected abstract CanHandle(request : T) : boolean;
}
The next class in our chain is set using SetNext. HandleRequest works by calling our abstract CanHandle method to see whether the current class can handle the request. If it cannot handle the request and if this.next is not null (note the use of union types here), we forward the request onto the next class. This is repeated until we can either handle the request or this.next is null.
We can now add a concrete implementation of our Handler class. First, we will add our constructor and member variables, as follows:
class ParseChainHandler extends Handler<ParseElement> {
private readonly visitable : IVisitable = new Visitable();
constructor(private readonly document : IMarkdownDocument,
private readonly tagType : string,
private readonly visitor : IVisitor) {
super();
}
}
Our constructor accepts the instance of the markdown document; the string that represents our tagType, for example, #; and the relevant visitor will visit the class if we get a matching tag. Before we see what the code for CanHandle looks like, we need to take a slight detour and introduce a class that will help us parse the current line and see if the tag is present at the start.
We are going to create a class that exists purely to parse the string, and looks to see if it starts with the relevant markdown tag. What is special about our Parse method is that we are returning something called a tuple. We can think of a tuple as a fixed-size array that can have different types at different positions in the array. In our case, we are going to return a boolean type and a string type. The boolean type indicates whether or not the tag was found, and the string type will return the text without the tag at the start; for example, if the string was # Hello and the tag was # , we would want to return Hello. The code that checks for the tag is very straightforward; it simply looks to see if the text starts with the tag. If it does, we set the boolean part of our tuple to true and use substr to get the remainder of our text. Consider the following code:
class LineParser {
public Parse(value : string, tag : string) : [boolean, string] {
let output : [boolean, string] = [false, ""];
output[1] = value;
if (value === "") {
return output;
}
let split = value.startsWith(`${tag}`);
if (split) {
output[0] = true;
output[1] = value.substr(tag.length);
}
return output;
}
}
Now that we have our LineParser class, we can apply that in our CanHandle method as follows:
protected CanHandle(request: ParseElement): boolean {
let split = new LineParser().Parse(request.CurrentLine, this.tagType);
if (split[0]){
request.CurrentLine = split[1];
this.visitable.Accept(this.visitor, request, this.document);
}
return split[0];
}
Here, we are using our parser to build a tuple where the first parameter states whether or not the tag was present, and the second parameter contains the text without the tag if the tag was present. If the markdown tag was present in our string, we call the Accept method on our Visitable implementation.
This is what our ParseChainHandler looks like now:
class ParseChainHandler extends Handler<ParseElement> {
private readonly visitable : IVisitable = new Visitable();
protected CanHandle(request: ParseElement): boolean {
let split = new LineParser().Parse(request.CurrentLine, this.tagType);
if (split[0]){
request.CurrentLine = split[1];
this.visitable.Accept(this.visitor, request, this.document);
}
return split[0];
}
constructor(private readonly document : IMarkdownDocument,
private readonly tagType : string,
private readonly visitor : IVisitor) {
super();
}
}
We have a special case that we need to handle. We know that the paragraph has no tag associated with it—if there are no matches through the rest of the chain, by default, it's a paragraph. This means that we need a slightly different handler to cope with paragraphs, shown as follows:
class ParagraphHandler extends Handler<ParseElement> {
private readonly visitable : IVisitable = new Visitable();
private readonly visitor : IVisitor = new ParagraphVisitor()
protected CanHandle(request: ParseElement): boolean {
this.visitable.Accept(this.visitor, request, this.document);
return true;
}
constructor(private readonly document : IMarkdownDocument) {
super();
}
}
With this infrastructure in place, we are now ready to create the concrete handlers for the appropriate tags as follows:
class Header1ChainHandler extends ParseChainHandler {
constructor(document : IMarkdownDocument) {
super(document, "# ", new Header1Visitor());
}
}
class Header2ChainHandler extends ParseChainHandler {
constructor(document : IMarkdownDocument) {
super(document, "## ", new Header2Visitor());
}
}
class Header3ChainHandler extends ParseChainHandler {
constructor(document : IMarkdownDocument) {
super(document, "### ", new Header3Visitor());
}
}
class HorizontalRuleHandler extends ParseChainHandler {
constructor(document : IMarkdownDocument) {
super(document, "---", new HorizontalRuleVisitor());
}
}
We now have a route through from the tag, for example, ---,to the appropriate visitor. We have now linked our chain-of-responsibility pattern to our visitor pattern. We have one final thing that we need to do: set up the chain. To do this, let's use a separate class that builds our chain:
class ChainOfResponsibilityFactory {
Build(document : IMarkdownDocument) : ParseChainHandler {
let header1 : Header1ChainHandler = new Header1ChainHandler(document);
let header2 : Header2ChainHandler = new Header2ChainHandler(document);
let header3 : Header3ChainHandler = new Header3ChainHandler(document);
let horizontalRule : HorizontalRuleHandler = new HorizontalRuleHandler(document);
let paragraph : ParagraphHandler = new ParagraphHandler(document);
header1.SetNext(header2);
header2.SetNext(header3);
header3.SetNext(horizontalRule);
horizontalRule.SetNext(paragraph);
return header1;
}
}
This simple-looking method accomplishes a lot for us. The first few statements initialize the chain-of-responsibility handlers for us; first for the headers, then for the horizontal rule, and finally for the paragraph handler. Remembering that this is only part of what we need to do here, we then go through the headers and the horizontal rule and set up the next item in the chain. Header 1 will forward calls on to header 2, header 2 forwards to header 3, and so on. The reason we don't set any further chained items after the paragraph handler is because that is the last case we want to handle. If the user isn't typing header1, header2, header3, or horizontalRule, then we're going to treat this as a paragraph.