🎉 Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io 🎉
Avatar of VicenteFreire

VicenteFreire's solution

to Markdown in the Java Track

Published at Jul 13 2018 · 0 comments
Instructions
Test suite
Solution

Note:

This solution was written on an old version of Exercism. The tests below might not correspond to the solution code, and the exercise may have changed since this code was written.

Refactor a Markdown parser.

The markdown exercise is a refactoring exercise. There is code that parses a given string with Markdown syntax and returns the associated HTML for that string. Even though this code is confusingly written and hard to follow, somehow it works and all the tests are passing! Your challenge is to re-write this code to make it easier to read and maintain while still making sure that all the tests keep passing.

It would be helpful if you made notes of what you did in your refactoring in comments so reviewers can see that, but it isn't strictly necessary. The most important thing is to make the code better!

Running the tests

You can run all the tests for an exercise by entering

$ gradle test

in your terminal.

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

MarkdownTest.java

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class MarkdownTest {

    private Markdown markdown;

    @Before
    public void setup() {
        markdown = new Markdown();
    }

    @Test
    public void normalTextAsAParagraph() {
        String input = "This will be a paragraph";
        String expected = "<p>This will be a paragraph</p>";

        assertEquals(expected, markdown.parse(input));
    }

    @Ignore("Remove to run test")
    @Test
    public void italics() {
        String input = "_This will be italic_";
        String expected = "<p><em>This will be italic</em></p>";

        assertEquals(expected, markdown.parse(input));
    }

    @Ignore("Remove to run test")
    @Test
    public void boldText() {
        String input = "__This will be bold__";
        String expected = "<p><strong>This will be bold</strong></p>";

        assertEquals(expected, markdown.parse(input));
    }

    @Ignore("Remove to run test")
    @Test
    public void normalItalicsAndBoldText() {
        String input = "This will _be_ __mixed__";
        String expected = "<p>This will <em>be</em> <strong>mixed</strong></p>";

        assertEquals(expected, markdown.parse(input));
    }

    @Ignore("Remove to run test")
    @Test
    public void withH1HeaderLevel() {
        String input = "# This will be an h1";
        String expected = "<h1>This will be an h1</h1>";

        assertEquals(expected, markdown.parse(input));
    }

    @Ignore("Remove to run test")
    @Test
    public void withH2HeaderLevel() {
        String input = "## This will be an h2";
        String expected = "<h2>This will be an h2</h2>";

        assertEquals(expected, markdown.parse(input));
    }

    @Ignore("Remove to run test")
    @Test
    public void withH6HeaderLevel() {
        String input = "###### This will be an h6";
        String expected = "<h6>This will be an h6</h6>";

        assertEquals(expected, markdown.parse(input));
    }

    @Ignore("Remove to run test")
    @Test
    public void unorderedLists() {
        String input = "* Item 1\n* Item 2";
        String expected = "<ul><li>Item 1</li><li>Item 2</li></ul>";

        assertEquals(expected, markdown.parse(input));
    }

    @Ignore("Remove to run test")
    @Test
    public void aLittleBitOfEverything() {
        String input = "# Header!\n* __Bold Item__\n* _Italic Item_";
        String expected = "<h1>Header!</h1><ul><li><strong>Bold Item</strong></li><li><em>Italic Item</em></li></ul>";

        assertEquals(expected, markdown.parse(input));
    }
}

src/main/java/HeaderTag.java

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class HeaderTag implements Tag {

	private static final String TAG_NAME = "h";

	private static final String HEADER_REGEX = "^(#{1,} )(.+)$";

	@Override
	public Boolean matches(String line) {
		return line.matches(HEADER_REGEX);
	}

	@Override
	public String parse(String line, Object... options) {
		Pattern pattern = Pattern.compile(HEADER_REGEX);
		Matcher matcher = pattern.matcher(line);
		matcher.find();

		String tagKey = matcher.group(1);
		String content = matcher.group(2);
		String tagName = TAG_NAME.concat(String.valueOf(tagKey.trim().length()));

		return Tag.getOpenTag(tagName).concat(content).concat(Tag.getCloseTag(tagName));
	}

}

src/main/java/InlineTag.java

public enum InlineTag implements Tag {

	ITALIC("em", "([^_]*)_([^_]+)_([^_])"), BOLD("strong", "([^_]*)__([^_]+)__([^_])");

	private String tagName;
	private String regex;

	private InlineTag(String tagName, String regex) {
		this.tagName = tagName;
		this.regex = regex;
	}

	@Override
	public Boolean matches(String line) {
		return line.matches(this.regex);
	}

	@Override
	public String parse(String line, Object... options) {
		String openTag = Tag.getOpenTag(this.tagName);
		String closeTag = Tag.getCloseTag(this.tagName);
		String replacement = "$1".concat(openTag).concat("$2").concat(closeTag).concat("$3");
		String parsed = line.replaceAll(this.regex, replacement);

		return parsed;
	}

}

src/main/java/Markdown.java

import java.util.stream.Collectors;
import java.util.stream.IntStream;

class Markdown {

	private static final String LINES_SPLITTER = "\n";
	private static final String EMPTY = "";

	String parse(String markdown) {
		String[] lines = markdown.split(LINES_SPLITTER);
		String result = IntStream.range(0, lines.length).mapToObj(index -> parseLine(lines, index))
				.collect(Collectors.joining());

		return result;
	}

	/**
	 * Parses a single line, searching, in a given order of priorities, for any tag
	 * that matches the definition of the line. The first tag that matches it will
	 * be applied.
	 * 
	 * @param allLines
	 *            the full set of lines, for multi-line tags that need this
	 *            information
	 * @param lineIndex
	 *            the index of the current line
	 * @return the parsed line
	 */
	private String parseLine(String[] allLines, Integer lineIndex) {
		String parsed = allLines[lineIndex];

		if (TagsFactory.getHeaderTag().matches(parsed)) {
			parsed = TagsFactory.getHeaderTag().parse(parsed);
		}
		else if (TagsFactory.getUnorderdListTag().matches(parsed)) {
			parsed = parseUnorderedLists(allLines, lineIndex, parsed);
		}
		else {
			parsed = TagsFactory.getParagraphTag().parse(parsed);
		}

		return parseInlineTags(parsed);
	}

	/**
	 * Parse lists, which can affect to several lines, so we have to look at the
	 * previous and the next line.
	 * 
	 * @param allLines
	 *            the set of all lines.
	 * @param lineIndex
	 *            the index of the current line.
	 * @param line
	 *            the current line.
	 * @return the line parsed as a list item, and the start / end of the list if
	 *         required.
	 */
	private String parseUnorderedLists(String[] allLines, Integer lineIndex, String line) {
		String previousLine = (lineIndex > 0) ? allLines[lineIndex - 1] : EMPTY;
		String nextLine = (lineIndex < (allLines.length - 1)) ? allLines[lineIndex + 1] : EMPTY;

		return TagsFactory.getUnorderdListTag().parse(line, previousLine, nextLine);
	}

	/**
	 * Parse those tags which apply to parts of single lines and can be combined
	 * with other higher tags.
	 * 
	 * @param line
	 *            the line to be parsed
	 * @return the parsed line
	 */
	private String parseInlineTags(String line) {
		String parsed = line;

		for (Tag inlineTag : TagsFactory.getInlineTags()) {
			parsed = inlineTag.parse(parsed);
		}

		return parsed;
	}

}

src/main/java/ParagraphTag.java

public class ParagraphTag implements Tag {

	private static final String TAG_NAME = "p";

	@Override
	public Boolean matches(String line) {
		return Boolean.TRUE;
	}

	@Override
	public String parse(String line, Object... options) {
		return Tag.getOpenTag(TAG_NAME).concat(line).concat(Tag.getCloseTag(TAG_NAME));
	}

}

src/main/java/Tag.java

public interface Tag {

	static final String INIT_TAG = "<";
	static final String END_TAG = ">";
	static final String BAR = "/";

	static String getOpenTag(String tagName) {
		return INIT_TAG.concat(tagName).concat(END_TAG);
	}

	static String getCloseTag(String tagName) {
		return INIT_TAG.concat(BAR).concat(tagName).concat(END_TAG);
	}

	Boolean matches(String line);

	String parse(String line, Object... options);

}

src/main/java/TagsFactory.java

import java.util.Arrays;
import java.util.List;

public class TagsFactory {

	private static final Tag HEADER_TAG = new HeaderTag();
	private static final Tag UNORDERED_LIST_TAG = new UnorderedListTag();
	private static final Tag PARAGRAPH_TAG = new ParagraphTag();
	private static final List<Tag> INLINE_TAGS = Arrays.asList(InlineTag.values());

	private TagsFactory() {
	}

	public static Tag getHeaderTag() {
		return HEADER_TAG;
	}

	public static Tag getUnorderdListTag() {
		return UNORDERED_LIST_TAG;
	}

	public static Tag getParagraphTag() {
		return PARAGRAPH_TAG;
	}

	public static List<Tag> getInlineTags() {
		return INLINE_TAGS;
	}

}

src/main/java/UnorderedListTag.java

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class UnorderedListTag implements Tag {

	private static final String LIST_TAG_NAME = "ul";
	private static final String LIST_ITEM_TAG_NAME = "li";

	private static final String LIST_ITEM_REGEX = "^\\* (.+)$";

	@Override
	public Boolean matches(String line) {
		return line.matches(LIST_ITEM_REGEX);
	}

	@Override
	public String parse(String line, Object... options) {
		if (options.length != 2) {
			throw new IllegalArgumentException("Previous and next line are required");
		}

		String previousLine = options[0].toString();
		String nextLine = options[1].toString();
		String listItem = parseListItem(line);
		listItem = parseListStart(listItem, previousLine);
		listItem = parseListEnd(listItem, nextLine);

		return listItem;
	}

	private String parseListItem(String line) {
		Pattern pattern = Pattern.compile(LIST_ITEM_REGEX);
		Matcher matcher = pattern.matcher(line);
		matcher.find();

		return Tag.getOpenTag(LIST_ITEM_TAG_NAME).concat(matcher.group(1)).concat(Tag.getCloseTag(LIST_ITEM_TAG_NAME));
	}

	private String parseListStart(String line, String previousLine) {
		return (matches(previousLine)) ? line : Tag.getOpenTag(LIST_TAG_NAME).concat(line);
	}

	private String parseListEnd(String line, String nextLine) {
		return (matches(nextLine)) ? line : line.concat(Tag.getCloseTag(LIST_TAG_NAME));
	}

}

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?