Avatar of cjvolkert

cjvolkert's solution

to Markdown in the Java Track

Published at May 13 2019 · 0 comments
Instructions
Test suite
Solution

Note:

This exercise has changed since this solution 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));
    }

    @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));
    }
}
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

class Markdown {

	class Document {
		List<MDElement> elements = new ArrayList<>();

		public void add(MDElement element) {
			if (element.isListRow()) {
				if (elements.isEmpty() || !elements.get(elements.size() - 1).isList()) {
					elements.add(new MDList(""));
				}
				elements.get(elements.size() - 1).add(element);
			}
			else {
				elements.add(element);
			}
		}

		public String get() {
			return this.elements.stream().map(e -> e.get()).collect(Collectors.joining());
		}
	}

	abstract class MDElement {
		List<MDElement> childs = new ArrayList< >();

		public abstract String get();

		public void  add(MDElement element) {} 

		public boolean isListRow() {
			return false;
		}

		public boolean isList() {
			return false;
		}
	}

	class MDText extends MDElement {
		String text;

		public MDText(String value) {
			text = value;
		}

		@Override
		public String get() {
			return parseSomeSymbols(this.text).trim();
		}

		private String parseSomeSymbols(String markdown) {

			String lookingFor = "__(.+)__";
			String update = "<strong>$1</strong>";
			String workingOn = markdown.replaceAll(lookingFor, update);

			lookingFor = "_(.+)_";
			update = "<em>$1</em>";
			return workingOn.replaceAll(lookingFor, update);
		}
	}

	class MDList extends MDElement {
		public MDList(String value) {
			String content = value.replaceFirst("\\*", "");
			this.childs.add(new MDText(content));
		}

		List<MDElement> rows = new ArrayList<>();

		@Override
		public boolean isList() {
			return true;
		}

		@Override
		public void add(MDElement row) {
			rows.add(row);
		}

		public String get() {
			return "<ul>" + this.rows.stream().map(e -> e.get()).collect(Collectors.joining()) + "</ul>";
		}

	}

	class MDParagraph extends MDElement {
		public MDParagraph(String value) {
			this.childs.add(new MDText(value));
		}

		public String get() {
			return "<p>" + childs.stream().map(m -> m.get()).collect(Collectors.joining()) + "</p>";
		}

	}

	class MDListRow extends MDElement {
		public MDListRow(String value) {
			String content = value.replaceFirst("\\*", "");
			this.childs.add(new MDText(content));
		}

		@Override
		public boolean isListRow() {
			return true;
		}

		public String get() {
			return "<li>" + childs.stream().map(m -> m.get()).collect(Collectors.joining()) + "</li>";
		}

	}

	class Header extends MDElement {
		int indention;

		public Header(String markdown) {
			String content = markdown.replaceFirst("\\#*", "");
			indention = (markdown.length() - content.length());
			this.childs.add(new MDText(markdown.substring(indention + 1)));
		}

		public String get() {
			String startTag = "<h" + indention + ">";
			String endTag = "</h" + indention + ">";
			return startTag + childs.stream().map(m -> m.get()).collect(Collectors.joining()) + endTag;
		}
	}

	class Parser {
		public MDElement build(String markdown) {
			if (markdown.startsWith("#")) {
				return new Header(markdown);
			}
			else if (markdown.startsWith("*")) {
				return new MDListRow(markdown);
			}
			else {
				return new MDParagraph(markdown);
			}
		}
	}

	String parse(String markdown) {
		String[] lines = markdown.split("\n");

		Document document = new Document();
		Parser parser = new Parser();

		for (int i = 0; i < lines.length; i++) {
			MDElement e = parser.build(lines[i]);
			document.add(e);
		}

		return document.get();
	}
}

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?