Advanced TypeScript Programming Projects
上QQ阅读APP看书,第一时间看更新

Mapping our markdown tag types to HTML tag types

In our requirements, we set out a master list of tags that our parser is going to handle. In order to identify these tags, we are going to add an enumeration consisting of the tags we are making available to our users:

enum TagType {
Paragraph,
Header1,
Header2,
Header3,
HorizontalRule
}

From our requirements, we also know that we need to translate between these tags and their equivalent opening and closing HTML tags. The way that we are going to do this is to map tagType to an equivalent HTML tag. To do this, we are going to create a class that has the sole responsibility of handling this mapping for us. The following code shows this:

class TagTypeToHtml {
private readonly tagType : Map<TagType, string> = new Map<TagType, string>();
constructor() {
this.tagType.set(TagType.Header1, "h1");
this.tagType.set(TagType.Header2, "h2");
this.tagType.set(TagType.Header3, "h3");
this.tagType.set(TagType.Paragraph, "p");
this.tagType.set(TagType.HorizontalRule, "hr")
}
}
At first, the use of readonly on a type can appear confusing. What this keyword means is that, after the class has been instantiated, tagType cannot be recreated elsewhere in the class. This means that we can set up our mappings in the constructor safe, knowing that we are not going to call this.tagType = new Map<TagType, string>(); later on.

We also need a way to retrieve opening and closing tags from this class. We're going to start by creating a method to get the opening tag from tagType, as follows:

public OpeningTag(tagType : TagType) : string {
let tag = this.tagType.get(tagType);
if (tag !== null) {
return `<${tag}>`;
}
return `<p>`;
}

This method is pretty straightforward. It starts by trying to get tagType from the map. With the code we currently have, we will always have an entry in the map, but we could extend the enumeration in the future and forget to add the tag to the list of tags. That is why we check to see if the tag is present; if it is, we return the tag enclosed in <>. If the tag is not present, we return a paragraph tag as a default.

Now, let's look at ClosingTag:

public ClosingTag(tagType : TagType) : string {
let tag = this.tagType.get(tagType);
if (tag !== null) {
return `</${tag}>`;
}
return `</p>`;
}

Looking at these two methods, we can see that they are almost identical. When we think about the problem of creating our HTML tag, we realize that the only difference between an opening and a closing tag is that the closing tag has a / in it. With that in mind, we can change the code to use a helper method that accepts whether the tag starts with < or </:

private GetTag(tagType : TagType, openingTagPattern : string) : string {
let tag = this.tagType.get(tagType);
if (tag !== null) {
return `${openingTagPattern}${tag}>`;
}
return `${openingTagPattern}p>`;
}

All that remains is for us to add methods to retrieve the opening and closing tags:

public OpeningTag(tagType : TagType) : string {
return this.GetTag(tagType, `<`);
}

public ClosingTag(tagType : TagType) : string {
return this.GetTag(tagType, `</`);
}

Pulling this all together, the code for our TagTypeToHtml class now looks like this:

class TagTypeToHtml {
private readonly tagType : Map<TagType, string> = new Map<TagType, string>();
constructor() {
this.tagType.set(TagType.Header1, "h1");
this.tagType.set(TagType.Header2, "h2");
this.tagType.set(TagType.Header3, "h3");
this.tagType.set(TagType.Paragraph, "p");
this.tagType.set(TagType.HorizontalRule, "hr")
}

public OpeningTag(tagType : TagType) : string {
return this.GetTag(tagType, `<`);
}

public ClosingTag(tagType : TagType) : string {
return this.GetTag(tagType, `</`);
}

private GetTag(tagType : TagType, openingTagPattern : string) : string {
let tag = this.tagType.get(tagType);
if (tag !== null) {
return `${openingTagPattern}${tag}>`;
}
return `${openingTagPattern}p>`;
}
}
The single responsibility of our TagTypeToHtml class is mapping tagType to an HTML tag. Something that we are going to keep coming back to throughout this chapter is that we want classes to have a single responsibility. In OO theory, this is known as one of the principles of SOLID (short for Single Responsibility PrincipleOpen/Closed PrincipleLiskov Substitution PrincipleInterface Segregation PrincipleDependency Inversion Principle) design. The acronym refers to a set of complementary development techniques to create more robust code.
This handy acronym serves to guide us on how to structure classes and the most important part, in my opinion, is the Single Responsibility Principle, which states that a class should do one thing and one thing only. While I would certainly recommend reading about this topic (and we will touch on other aspects of it as we progress), in my opinion, the most important part of SOLID design is that classes are responsible for one thing and one thing only; everything else flows out of that principle. Classes that only do one thing are generally much easier to test and they are a lot easier to understand. That does not mean that they should only have one method. They can have many methods, as long as they are all related to the purpose of the class. We will cover this topic again and again throughout the book because it is so important.