Avatar of pdmoore

pdmoore's solution

to Markdown in the Java Track

Published at Jan 30 2020 · 0 comments
Instructions
Test suite
Solution

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!

Setup

Go through the setup instructions for Java to install the necessary dependencies:

https://exercism.io/tracks/java/installation

Running the tests

You can run all the tests for an exercise by entering the following in your terminal:

$ gradle test

Use gradlew.bat if you're on Windows

In the test suites all tests but the first have been skipped.

Once you get a test passing, you can enable the next one by removing the @Ignore("Remove to run test") annotation.

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));
    }

    @Ignore("Remove to run test")
    @Test
    public void markdownSymbolsInTheHeaderShouldNotBeInterpreted() {
        String input = "# This is a header with # and * in the text";
        String expected = "<h1>This is a header with # and * in the text</h1>";

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

    @Ignore("Remove to run test")
    @Test
    public void markdownSymbolsInTheListItemTextShouldNotBeInterpreted() {
        String input = "* Item 1 with a # in the text\n* Item 2 with * in the text";
        String expected = "<ul><li>Item 1 with a # in the text</li><li>Item 2 with * in the text</li></ul>";

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

    @Ignore("Remove to run test")
    @Test
    public void markdownSymbolsInTheParagraphTextShouldNotBeInterpreted() {
        String input = "This is a paragraph with # and * in the text";
        String expected = "<p>This is a paragraph with # and * in the text</p>";

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

    @Ignore("Remove to run test")
    @Test
    public void markdownUnorderedListsCloseProperlyWithPrecedingAndFollowingLines() {
        String input = "# Start a list\n* Item 1\n* Item 2\nEnd a list";
        String expected = "<h1>Start a list</h1><ul><li>Item 1</li><li>Item 2</li></ul><p>End a list</p>";

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

}
class Markdown {
    private static final String MARKDOWN_STRONG = "__(.+)__";
    private static final String MARKDOWN_EMPHASIS = "_(.+)_";
    private static final String HTML_STRONG = "<strong>$1</strong>";
    private static final String HTML_EMPHASIS = "<em>$1</em>";

    public static final String TAG_LIST_ITEM = "ul";
    public static final String TAG_LIST = "li";
    public static final String TAG_PARAGRAPH = "p";

    private boolean activeList;

    String parse(String markdown) {
        activeList = false;

        String result = "";
        for (String line : markdown.split("\n")) {
            result = processLine(result, line);
        }

        result += closeActiveList();
        result = convertModifiersToHtml(result);

        return result;
    }

    private String processLine(String resultInProgress, String line) {
        String htmlForThisLine = null;
        if (isHeader(line)) {
            resultInProgress += closeActiveList();
            activeList = false;

            htmlForThisLine = parseHeader(line);
        } else if (isList(line)) {
            resultInProgress += openActiveList();
            activeList = true;

            htmlForThisLine = parseListItem(line);
        } else {
            resultInProgress += closeActiveList();
            activeList = false;

            htmlForThisLine = parseParagraph(line);
        }

        return resultInProgress + htmlForThisLine;
    }

    private String openActiveList() {
        return activeList ? "" : openTag(TAG_LIST_ITEM);
    }

    private String closeActiveList() {
        return activeList ? closeTag(TAG_LIST_ITEM) : "";
    }

    private boolean isList(String line) {
        return line.startsWith("*");
    }

    private boolean isHeader(String line) {
        return line.startsWith("#");
    }

    private String parseHeader(String markdown) {
        int headerTagCount = countHeaderTags(markdown);
        String markdownPastTheHeader = markdown.substring(headerTagCount + 1);
        String headerTag = "h" + Integer.toString(headerTagCount);

        return wrapWithTag(markdownPastTheHeader, headerTag);
    }

    private int countHeaderTags(String markdown) {
        int headerTagCount = -1;
        while (markdown.charAt(++headerTagCount) == '#') {
            // clever - is there a better way?
        }
        return headerTagCount;
    }

    private String parseListItem(String markdown) {
        String markdownPastTheListItem = markdown.substring(2);
        return wrapWithTag(markdownPastTheListItem, TAG_LIST);
    }

    private String parseParagraph(String markdown) {
        return wrapWithTag(markdown, TAG_PARAGRAPH);
    }

    private String convertModifiersToHtml(String markdown) {
        return markdown.replaceAll(MARKDOWN_STRONG, HTML_STRONG).replaceAll(MARKDOWN_EMPHASIS, HTML_EMPHASIS);
    }

    private String wrapWithTag(String markdown, String tag) {
        return openTag(tag) + markdown + closeTag(tag);
    }

    private String openTag(String tag) {
        return "<" + tag + ">";
    }

    private String closeTag(String tag) {
        return "</" + tag + ">";
    }
}

Community comments

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

pdmoore's Reflection

- Felt like there were some missing tests based on coverage (list followed by a paragraph)
- Spent time refactoring down to single-responsibility methods, then removing duplication
- Want to tackle processLine more - hierarchy of similar behavior
- Started it recently but seeing a "exercise has been updated message"?