Avatar of evan-buss

evan-buss's solution

to Markdown in the Java Track

Published at Sep 02 2019 · 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 boolean activeList = false;
  //    Calls the specialized functions for different parts of the markdown
  String parse(String markdown) {

    StringBuilder output = new StringBuilder();

    //    Loop over each line in the markdown source
    for (String line : markdown.split("\n")) {

      if (line.startsWith("#")) {
        output.append(parseHeader(line));
      } else if (line.startsWith("*")) {
        output.append(parseListItem(line));
      } else {
        if (activeList) {
          activeList = false;
          output.append("</ul>");
        }
        output.append(parseParagraph(line));
      }
    }

    //    If the last line is a list item we still need to close it
    if (activeList) {
      output.append("</ul>");
    }

    return output.toString();
  }

  //  Parse the header levels
  private String parseHeader(String markdown) {
    int count = 0;

    for (int i = 0; i < markdown.length() && markdown.charAt(i) == '#'; i++) {
      count++;
    }

    return "<h" + count + ">" + markdown.substring(count + 1) + "</h" + count + ">";
  }

  //  Parse list into HTML list
  private String parseListItem(String markdown) {
    String output = "";
    if (!activeList) {
      activeList = true;
      output += "<ul>";
    }
    String listItemString = parseSomeSymbols(markdown.substring(2));
    output += "<li>" + listItemString + "</li>";
    return output;
  }

  // Create paragraphs
  private String parseParagraph(String markdown) {
    return "<p>" + parseSomeSymbols(markdown) + "</p>";
  }

  //  Parse text emphasis
  private String parseSomeSymbols(String markdown) {
    String workingOn = markdown.replaceAll("__(.+)__", "<strong>$1</strong>");
    return workingOn.replaceAll("_(.+)_", "<em>$1</em>");
  }
}

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?