From 8d709fdc0ee3b6e8b030ad54aa11d7d35ce4a0d5 Mon Sep 17 00:00:00 2001 From: specCon18 Date: Sat, 1 Feb 2025 00:56:50 -0500 Subject: [PATCH] added markdown id --- .gitignore | 1 + main.sh | 1 + out.txt | 0 public/index.html | 20 +++ public/styles.css | 23 ++++ src/conversions.py | 224 ++++++++++++++++++++++++++++++ src/htmlnode.py | 56 ++++++++ src/main.py | 15 ++ src/test_conversions.py | 293 ++++++++++++++++++++++++++++++++++++++++ src/test_htmlnode.py | 93 +++++++++++++ src/test_textnode.py | 29 ++++ src/textnode.py | 23 ++++ test.sh | 1 + 13 files changed, 779 insertions(+) create mode 100644 .gitignore create mode 100755 main.sh create mode 100644 out.txt create mode 100644 public/index.html create mode 100644 public/styles.css create mode 100644 src/conversions.py create mode 100644 src/htmlnode.py create mode 100644 src/main.py create mode 100644 src/test_conversions.py create mode 100644 src/test_htmlnode.py create mode 100644 src/test_textnode.py create mode 100644 src/textnode.py create mode 100755 test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/main.sh b/main.sh new file mode 100755 index 0000000..6b0ad88 --- /dev/null +++ b/main.sh @@ -0,0 +1 @@ +python3 src/main.py diff --git a/out.txt b/out.txt new file mode 100644 index 0000000..e69de29 diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..1ed2aab --- /dev/null +++ b/public/index.html @@ -0,0 +1,20 @@ + + + Why Frontend Development Sucks + + + +

Front-end Development is the Worst

+

+ Look, front-end development is for script kiddies and soydevs who can't handle the real programming. I mean, + it's just a bunch of divs and spans, right? And css??? It's like, "Oh, I want this to be red, but not thaaaaat + red." What a joke. +

+

+ Real programmers code, not silly markup languages. They code on Arch Linux, not macOS, and certainly not + Windows. They use Vim, not VS Code. They use C, not HTML. Come to the + backend, where the real programming + happens. +

+ + diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..69b0e5b --- /dev/null +++ b/public/styles.css @@ -0,0 +1,23 @@ +body { + font-family: Arial, sans-serif; + line-height: 1.6; + margin: 0; + padding: 0; + background-color: #1f1f23; +} +body { + max-width: 600px; + margin: 0 auto; + padding: 20px; +} +h1 { + color: #ffffff; + margin-bottom: 20px; +} +p { + color: #999999; + margin-bottom: 20px; +} +a { + color: #6568ff; +} diff --git a/src/conversions.py b/src/conversions.py new file mode 100644 index 0000000..533553f --- /dev/null +++ b/src/conversions.py @@ -0,0 +1,224 @@ +from textnode import TextType, TextNode +from htmlnode import LeafNode +import re + + +def block_to_block_type(markdown): + markdown = markdown.strip() # Remove leading/trailing whitespace + + if markdown.startswith("#"): + # Count the number of # characters + num_hashes = len(markdown.split(" ", 1)[0]) + + # Ensure the number of # is between 1 and 6 and followed by a space + if 1 <= num_hashes <= 6 and markdown[num_hashes:].startswith(" "): + return "heading" + return "paragraph" # Invalid heading (wrong number of # or no space after #) + + elif markdown.startswith("```"): + if markdown.endswith("```") and len(markdown.strip("`").strip()) > 0: + return "code" + return "paragraph" # Invalid code block (empty or not closed properly) + + elif markdown.startswith(">"): + split_lines = markdown.split("\n") + if all(line.startswith("> ") and line.strip("> ").strip() for line in split_lines): + return "quote" + return "paragraph" # Invalid quote (empty or missing space after >) + + elif markdown.startswith(("*", "-")): + split_lines = markdown.split("\n") + startswith = "*" if markdown.startswith("*") else "-" + if all(line.startswith(f"{startswith} ") and line.strip(f"{startswith} ").strip() for line in split_lines): + return "unordered_list" + return "paragraph" # Invalid unordered list (empty or missing space) + + elif markdown.startswith("1.") or markdown.startswith("2.") or markdown.startswith("3.") or markdown.startswith("4.") or markdown.startswith("5.") or markdown.startswith("6.") or markdown.startswith("7.") or markdown.startswith("8.") or markdown.startswith("9."): + split_lines = markdown.split("\n") + list_counter = 1 + + for line in split_lines: + expected_prefix = f"{list_counter}. " + if not line.startswith(expected_prefix) or not line[len(expected_prefix):].strip(): + return "paragraph" # Invalid ordered list (incorrect numbering or empty item) + list_counter += 1 + + return "ordered_list" + + else: + return "paragraph" + + +def markdown_to_blocks(markdown): + lines = markdown.split('\n') + blocks = [] + current_block = [] + for line in lines: + stripped = line.strip() + if stripped == "": + if current_block == []: + continue + else: + blocks.append("\n".join(current_block).strip()) + current_block = [] + else: + current_block.append(line) + if current_block != []: + blocks.append("\n".join(current_block).strip()) + current_block = [] + return blocks + +def text_node_to_html_node(text_node): + match text_node.text_type: + case TextType.NORMAL_TEXT: + return LeafNode(value=text_node.text) + case TextType.BOLD_TEXT: + return LeafNode(value=text_node.text,tag="b") + case TextType.ITALIC_TEXT: + return LeafNode(value=text_node.text,tag="i") + case TextType.CODE_TEXT: + return LeafNode(value=text_node.text,tag="code") + case TextType.LINK_TEXT: + node = LeafNode(value=text_node.text,tag="a") + node.props = {"href":text_node.url} + return node + case TextType.IMAGE_TEXT: + node = LeafNode(value="",tag="img") + node.props = {"src":text_node.url ,"alt":text_node.text} + return node + case _: + raise Exception("NOT_A_VALID_TEXT_TYPE") + +def split_nodes_delimiter(old_nodes, delimiter, text_type): + new_nodes = [] + + for node in old_nodes: + # Skip nodes that are not NORMAL_TEXT + if node.text_type != TextType.NORMAL_TEXT: + new_nodes.append(node) + continue + + # If no delimiter is found in the node's text + if delimiter not in node.text: + new_nodes.append(node) + continue + + # Process text while delimiters exist in the string + text = node.text + while delimiter in text: + # Find the first and second delimiters + first_delim = text.find(delimiter) + if first_delim > 0: + prefix = text[:first_delim] # Clean prefix from whitespace + if prefix: + new_nodes.append(TextNode(prefix, TextType.NORMAL_TEXT)) + + # Find the next delimiter + last_delim = text.find(delimiter, first_delim + len(delimiter)) + if last_delim == -1: + raise Exception("Invalid Markdown: only one delimiter") + + # Extract and append middle section, ensuring validity + middle = text[first_delim + len(delimiter):last_delim].strip() + if middle: # Only add non-blank nodes for the styled text + new_nodes.append(TextNode(middle, text_type)) + + # Update text to the remaining suffix after the second delimiter + text = text[last_delim + len(delimiter):] + + # Once all delimiters are processed, handle the remaining suffix + suffix = text # Ensure suffix is stripped + if suffix: + new_nodes.append(TextNode(suffix, TextType.NORMAL_TEXT)) + + return new_nodes + +def extract_markdown_images(text): + pattern = r"!\[(.*?)\]\((.*?\.(?:png|jpg|jpeg|gif|svg|webp|bmp|tiff|ico)[^)]*)\)" + matches = re.findall(pattern, text) + return matches + +def extract_markdown_links(text): + pattern = r"\[(.*?)\]\((.*?)\)" + matches = re.findall(pattern,text) + return matches + +def split_nodes_image(old_nodes): + new_nodes = [] + for node in old_nodes: + if node.text_type != TextType.NORMAL_TEXT: + new_nodes.append(node) + continue + + # Start with the node's full text + text = node.text + + # Keep processing while we can find images + while True: + images = extract_markdown_images(text) + if not images: + # No more images in text + if text: # only append if not empty + new_nodes.append(TextNode(text, TextType.NORMAL_TEXT)) + break + + # Process the first image found + image = images[0] + text_parts = text.split(f"![{image[0]}]({image[1]})", 1) + + # Add prefix text if not empty + if text_parts[0]: + new_nodes.append(TextNode(text_parts[0], TextType.NORMAL_TEXT)) + + # Add the image node + new_nodes.append(TextNode(image[0], TextType.IMAGE_TEXT, image[1])) + + # Update text to remaining portion + text = text_parts[1] + + return new_nodes + +def split_nodes_link(old_nodes): + new_nodes = [] + for node in old_nodes: + if node.text_type != TextType.NORMAL_TEXT: + new_nodes.append(node) + continue + + # Start with the node's full text + text = node.text + + # Keep processing while we can find images + while True: + links = extract_markdown_links(text) + if not links: + # No more images in text + if text: # only append if not empty + new_nodes.append(TextNode(text, TextType.NORMAL_TEXT)) + break + + link = links[0] + text_parts = text.split(f"[{link[0]}]({link[1]})", 1) + + if text_parts[0]: + new_nodes.append(TextNode(text_parts[0], TextType.NORMAL_TEXT)) + + new_nodes.append(TextNode(link[0], TextType.LINK_TEXT, link[1])) + + text = text_parts[1] + + return new_nodes + +def text_to_textnodes(text): + nodes = [TextNode(text, TextType.NORMAL_TEXT)] + + nodes = split_nodes_delimiter(nodes, "**", TextType.BOLD_TEXT) + nodes = split_nodes_delimiter(nodes, "*", TextType.ITALIC_TEXT) + nodes = split_nodes_image(nodes) + nodes = split_nodes_link(nodes) + nodes = split_nodes_delimiter(nodes, "`", TextType.CODE_TEXT) + + # Remove any empty or whitespace-only nodes + nodes = [node for node in nodes if node.text.strip()] + return nodes + diff --git a/src/htmlnode.py b/src/htmlnode.py new file mode 100644 index 0000000..70c97e8 --- /dev/null +++ b/src/htmlnode.py @@ -0,0 +1,56 @@ +class HTMLNode: + def __init__(self, tag=None, value=None, children=None, props=None): + self.tag = tag + self.value = value + self.children = children or [] + self.props = props or {} + + def to_html(self): + raise NotImplementedError + + def props_to_html(self): + if not self.props: + return "" + return " ".join([f'{key}="{value}"' for key, value in self.props.items()]) + + def __repr__(self): + return f"Tag: {self.tag}, Val: {self.value}, Children: {self.children}, Props: {self.props}" + +class LeafNode(HTMLNode): + def __init__(self, value,tag=None): + super().__init__(tag,value,children=[]) + + def to_html(self): + props_as_html = self.props_to_html() + + if not self.value: + raise ValueError("Value is missing") + + if not self.tag: + return f"{self.value}" + + if props_as_html == "": + return f"<{self.tag}>{self.value}" + else: + return f"<{self.tag} {props_as_html}>{self.value}" + +class ParentNode(HTMLNode): + def __init__(self, tag, children, props=None): + super().__init__(tag=tag, children=children, props=props or {}) + + def to_html(self): + if not self.tag: + raise ValueError("Tag cannot be None!") + if not self.children: + raise ValueError("Children are missing!") + + props_html = self.props_to_html() + children_html = "".join( + child.to_html() if isinstance(child, HTMLNode) else child + for child in self.children + ) + + if props_html: + return f"<{self.tag} {props_html}>{children_html}" + else: + return f"<{self.tag}>{children_html}" diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..983f032 --- /dev/null +++ b/src/main.py @@ -0,0 +1,15 @@ +from textnode import TextNode,TextType +from conversions import markdown_to_blocks +def main(): + md = """# This is a heading + + This is a paragraph of text. It has some **bold** and *italic* words inside of it. + + * This is the first list item in a list block + * This is a list item + * This is another list item + """ + print(markdown_to_blocks(md)) + +if __name__=="__main__": + main() diff --git a/src/test_conversions.py b/src/test_conversions.py new file mode 100644 index 0000000..ffa564d --- /dev/null +++ b/src/test_conversions.py @@ -0,0 +1,293 @@ +import unittest +from htmlnode import LeafNode +from conversions import text_node_to_html_node,split_nodes_delimiter,extract_markdown_images,extract_markdown_links,split_nodes_image,split_nodes_link,text_to_textnodes,markdown_to_blocks,block_to_block_type + +from textnode import TextType,TextNode + +class TestConversions(unittest.TestCase): + + def test_text_node_to_html_node(self): + text_node = TextNode("Hello, world!", TextType.NORMAL_TEXT) + html_node = text_node_to_html_node(text_node) + assert html_node.tag == None + assert html_node.value == "Hello, world!" + assert html_node.props == {} + + def test_bold_node_to_html_node(self): + bold_node = TextNode("Hello, world!", TextType.BOLD_TEXT) + html_node = text_node_to_html_node(bold_node) + assert html_node.tag == "b" + assert html_node.value == "Hello, world!" + + def test_italic_node_to_html_node(self): + italic_node = TextNode("Hello, world!", TextType.ITALIC_TEXT) + html_node = text_node_to_html_node(italic_node) + assert html_node.tag == "i" + assert html_node.value == "Hello, world!" + + def test_code_node_to_html_node(self): + code_node = TextNode("var hw = \"Hello, world!\"", TextType.CODE_TEXT) + html_node = text_node_to_html_node(code_node) + assert html_node.tag == "code" + assert html_node.value == "var hw = \"Hello, world!\"" + + def test_link_node_to_html_node(self): + link_node = TextNode("google", TextType.LINK_TEXT,"https://google.com") + html_node = text_node_to_html_node(link_node) + assert html_node.tag == "a" + assert html_node.value == "google" + assert html_node.props == {"href":"https://google.com"} + + def test_img_node_to_html_node(self): + img_node = TextNode("an image of an image", TextType.IMAGE_TEXT, "https://google.com") + html_node = text_node_to_html_node(img_node) + assert html_node.tag == "img" + assert html_node.value == "" # Remember, image nodes have empty value + assert html_node.props == { + "src": "https://google.com", + "alt": "an image of an image" + } + +class TestSplitNodesDelimiter(unittest.TestCase): + + def test_basic_split(self): + old_nodes = [TextNode("Hello *world*", TextType.NORMAL_TEXT)] + new_nodes = split_nodes_delimiter(old_nodes, "*", TextType.ITALIC_TEXT) + self.assertEqual(len(new_nodes), 2) + self.assertEqual(new_nodes[0].text, "Hello ") + self.assertEqual(new_nodes[0].text_type, TextType.NORMAL_TEXT) + self.assertEqual(new_nodes[1].text, "world") + self.assertEqual(new_nodes[1].text_type, TextType.ITALIC_TEXT) + + def test_unbalanced_delimiter(self): + old_nodes = [TextNode("Hello *world", TextType.NORMAL_TEXT)] + with self.assertRaises(Exception) as context: + split_nodes_delimiter(old_nodes, "*", TextType.BOLD_TEXT) + self.assertEqual(str(context.exception), "Invalid Markdown: only one delimiter") + + def test_multiple_splits(self): + old_nodes = [TextNode("Hello *bold* and *italic* world", TextType.NORMAL_TEXT)] + new_nodes = split_nodes_delimiter(old_nodes, "*", TextType.BOLD_TEXT) + self.assertEqual(len(new_nodes), 5) + self.assertEqual(new_nodes[0].text, "Hello ") + self.assertEqual(new_nodes[1].text, "bold") + self.assertEqual(new_nodes[1].text_type, TextType.BOLD_TEXT) + self.assertEqual(new_nodes[2].text, " and ") + self.assertEqual(new_nodes[3].text, "italic") + self.assertEqual(new_nodes[3].text_type, TextType.BOLD_TEXT) + self.assertEqual(new_nodes[4].text, " world") + self.assertEqual(new_nodes[4].text_type, TextType.NORMAL_TEXT) + +class TestExtractMarkdownImages(unittest.TestCase): + + def test_single_image(self): + text = "Here is an image ![alt text](image.jpg) in markdown." + expected = [("alt text", "image.jpg")] + self.assertEqual(extract_markdown_images(text), expected) + + def test_multiple_images(self): + text = "![first](first.jpg) and ![second](second.png)" + expected = [("first", "first.jpg"), ("second", "second.png")] + self.assertEqual(extract_markdown_images(text), expected) + + def test_no_images(self): + text = "This is a plain text without images." + expected = [] + self.assertEqual(extract_markdown_images(text), expected) + + def test_image_with_special_characters(self): + text = "Check this out ![cool image](path/to/image-with-hyphen.jpg)" + expected = [("cool image", "path/to/image-with-hyphen.jpg")] + self.assertEqual(extract_markdown_images(text), expected) + + def test_image_with_parentheses_in_url(self): + text = "![example](https://example.com/image(special).png)" + expected = [("example", "https://example.com/image(special).png")] + self.assertEqual(extract_markdown_images(text), expected) + + def test_image_with_no_alt_text(self): + text = "![](no-alt.jpg)" + expected = [("", "no-alt.jpg")] + self.assertEqual(extract_markdown_images(text), expected) + +class TestExtractMarkdownLinks(unittest.TestCase): + + def test_single_link(self): + text = "This is a [link](https://example.com)." + self.assertEqual(extract_markdown_links(text), [("link", "https://example.com")]) + + def test_multiple_links(self): + text = "Check [this](https://example1.com) and [that](https://example2.com)." + self.assertEqual(extract_markdown_links(text), [("this", "https://example1.com"), ("that", "https://example2.com")]) + + def test_no_links(self): + text = "This is just plain text with no links." + self.assertEqual(extract_markdown_links(text), []) + + def test_link_with_special_chars(self): + text = "Click [here](https://example.com/path?query=value&other=true)." + self.assertEqual(extract_markdown_links(text), [("here", "https://example.com/path?query=value&other=true")]) + + def test_nested_brackets(self): + text = "This is a [complex [link]](https://example.com)." + self.assertEqual(extract_markdown_links(text), [("complex [link]", "https://example.com")]) + + def test_unmatched_brackets(self): + text = "This is a [broken link(https://example.com)." + self.assertEqual(extract_markdown_links(text), []) + + def test_unmatched_parentheses(self): + text = "This is a [broken](https://example.com link." + self.assertEqual(extract_markdown_links(text), []) + +class TestSplitNodesImage(unittest.TestCase): + + def test_no_images(self): + node = TextNode("Just plain text", TextType.NORMAL_TEXT) + assert split_nodes_image([node]) == [node] + + def test_one_image(self): + node = TextNode("Text with ![image](example.png)", TextType.NORMAL_TEXT) + nodes = split_nodes_image([node]) + assert nodes[0] == TextNode("Text with ", TextType.NORMAL_TEXT) + assert nodes[1] == TextNode("image",TextType.IMAGE_TEXT,"example.png") + + def test_multiple_images(self): + node = TextNode("Start ![one](test1.jpg) middle ![two](test2.png) end",TextType.NORMAL_TEXT) + nodes = split_nodes_image([node]) + assert nodes[0] == TextNode("Start ",TextType.NORMAL_TEXT) + assert nodes[1] == TextNode("one",TextType.IMAGE_TEXT,"test1.jpg") + assert nodes[2] == TextNode(" middle ",TextType.NORMAL_TEXT) + assert nodes[3] == TextNode("two",TextType.IMAGE_TEXT,"test2.png") + assert nodes[4] == TextNode(" end",TextType.NORMAL_TEXT) + + def test_non_text_node(self): + node = TextNode("![image](url)", TextType.LINK_TEXT) + nodes = split_nodes_image([node]) + assert nodes[0] == node + +class TestSplitNodesLink(unittest.TestCase): + + def test_no_links(self): + node = TextNode("Just plain text", TextType.NORMAL_TEXT) + assert split_nodes_link([node]) == [node] + + def test_one_link(self): + node = TextNode("Text with [link](url)", TextType.NORMAL_TEXT) + nodes = split_nodes_link([node]) + assert nodes[0] == TextNode("Text with ",TextType.NORMAL_TEXT) + assert nodes[1] == TextNode("link",TextType.LINK_TEXT,"url") + + def test_multiple_links(self): + node = TextNode("Start [one](url1) middle [two](url2) end", TextType.NORMAL_TEXT) + nodes = split_nodes_link([node]) + assert nodes[0] == TextNode("Start ",TextType.NORMAL_TEXT) + assert nodes[1] == TextNode("one",TextType.LINK_TEXT,"url1") + assert nodes[2] == TextNode(" middle ",TextType.NORMAL_TEXT) + assert nodes[3] == TextNode("two",TextType.LINK_TEXT,"url2") + assert nodes[4] == TextNode(" end",TextType.NORMAL_TEXT) + + def test_non_text_node(self): + node = TextNode("![text](url)", TextType.IMAGE_TEXT) + nodes = split_nodes_link([node]) + assert nodes[0] == node + +class TestTextToTextNodes(unittest.TestCase): + def test_text_to_textnodes(self): + text = "This is a **text** *node* ![dog](../dog.jpg) and ![cat](../cat.jpg) are [dogs](http://localhost/dogs) and [cats](http://localhost/cats) this is code`codecodecodeycodecode`" + expected_output = [ + TextNode("This is a ", TextType.NORMAL_TEXT, None), + TextNode("text", TextType.BOLD_TEXT, None), + TextNode("node", TextType.ITALIC_TEXT, None), + TextNode("dog", TextType.IMAGE_TEXT, "../dog.jpg"), + TextNode(" and ", TextType.NORMAL_TEXT, None), + TextNode("cat", TextType.IMAGE_TEXT, "../cat.jpg"), + TextNode(" are ", TextType.NORMAL_TEXT, None), + TextNode("dogs", TextType.LINK_TEXT, "http://localhost/dogs"), + TextNode(" and ", TextType.NORMAL_TEXT, None), + TextNode("cats", TextType.LINK_TEXT, "http://localhost/cats"), + TextNode(" this is code", TextType.NORMAL_TEXT, None), + TextNode("codecodecodeycodecode", TextType.CODE_TEXT, None) + ] + self.assertEqual(text_to_textnodes(text), expected_output) + +class TestMarkdownToBlocks(unittest.TestCase): + def test_basic_paragraphs(self): + markdown = """This is a paragraph. + +Another paragraph.""" + expected = ["This is a paragraph.", "Another paragraph."] + self.assertEqual(markdown_to_blocks(markdown), expected) + + def test_extra_whitespace(self): + markdown = """ This is indented. + + Another indented paragraph. """ + expected = ["This is indented.", "Another indented paragraph."] + self.assertEqual(markdown_to_blocks(markdown), expected) + + def test_empty_input(self): + markdown = "" + expected = [] + self.assertEqual(markdown_to_blocks(markdown), expected) + + def test_only_whitespace(self): + markdown = " \n \n " + expected = [] + self.assertEqual(markdown_to_blocks(markdown), expected) + + def test_multiple_paragraphs_with_blank_lines(self): + markdown = """First paragraph. + + +Second paragraph. + + +Third paragraph.""" + expected = ["First paragraph.", "Second paragraph.", "Third paragraph."] + self.assertEqual(markdown_to_blocks(markdown), expected) + + def test_mixed_newline_formats(self): + markdown = """Line one.\n\n Line two.\n \n\n Line three. """ + expected = ["Line one.", "Line two.", "Line three."] + self.assertEqual(markdown_to_blocks(markdown), expected) + +class TestBlockToBlockType(unittest.TestCase): + + def test_heading(self): + self.assertEqual(block_to_block_type("# Heading"), "heading") + self.assertEqual(block_to_block_type("## Subheading"), "heading") + self.assertEqual(block_to_block_type("### Another heading"), "heading") + self.assertEqual(block_to_block_type("###### Small heading"), "heading") + self.assertEqual(block_to_block_type("#NoSpaceAfterHash"), "paragraph") # Invalid heading + self.assertEqual(block_to_block_type("####### TooManyHashes"), "paragraph") # Invalid heading + + def test_code(self): + self.assertEqual(block_to_block_type("```code block```"), "code") + self.assertEqual(block_to_block_type("``` python\nprint('Hello')\n```"), "code") + self.assertEqual(block_to_block_type("```"), "paragraph") # Invalid code block (not closed properly) + self.assertEqual(block_to_block_type("``` "), "paragraph") # Invalid code block (empty) + + def test_quote(self): + self.assertEqual(block_to_block_type("> This is a quote"), "quote") + self.assertEqual(block_to_block_type("> Another quote"), "quote") + self.assertEqual(block_to_block_type(">Invalid quote without space"), "paragraph") # Invalid quote + self.assertEqual(block_to_block_type("> Multiple lines\n> with proper format"), "quote") + self.assertEqual(block_to_block_type(">MultipleLinesNoSpace"), "paragraph") # Invalid quote + + def test_unordered_list(self): + self.assertEqual(block_to_block_type("* Item 1"), "unordered_list") + self.assertEqual(block_to_block_type("- Item 2"), "unordered_list") + self.assertEqual(block_to_block_type("* Item 1\n* Item 2"), "unordered_list") + self.assertEqual(block_to_block_type("- Item 1\n- Item 2"), "unordered_list") + self.assertEqual(block_to_block_type("* Item 1\n Item 2"), "paragraph") # Invalid unordered list (no space) + self.assertEqual(block_to_block_type("- Item 1\n-Item2"), "paragraph") # Invalid unordered list (no space) + + def test_paragraph(self): + self.assertEqual(block_to_block_type("This is a simple paragraph"), "paragraph") + self.assertEqual(block_to_block_type("Random text without markdown"), "paragraph") + self.assertEqual(block_to_block_type("Another paragraph"), "paragraph") + + +if __name__ == "__main__": + unittest.main() diff --git a/src/test_htmlnode.py b/src/test_htmlnode.py new file mode 100644 index 0000000..33c71da --- /dev/null +++ b/src/test_htmlnode.py @@ -0,0 +1,93 @@ +import unittest +from htmlnode import HTMLNode, LeafNode, ParentNode + +class TestHTMLNode(unittest.TestCase): + + def test_initialization(self): + node = HTMLNode(tag="div", value="Hello, World!", props={"class": "container"}) + self.assertEqual(node.tag, "div") + self.assertEqual(node.value, "Hello, World!") + self.assertEqual(node.children, []) + self.assertEqual(node.props, {"class": "container"}) + + def test_props_to_html(self): + node = HTMLNode(tag="input", props={"type": "text", "placeholder": "Enter name"}) + props_html = node.props_to_html() + self.assertEqual(props_html, 'type="text" placeholder="Enter name"') + + def test_repr(self): + node = HTMLNode(tag="p", value="Sample text", props={"id": "text1"}) + expected_repr = "Tag: p, Val: Sample text, Children: [], Props: {'id': 'text1'}" + self.assertEqual(repr(node), expected_repr) + +class TestParentNode(unittest.TestCase): + + def test_initialization(self): + node = ParentNode(tag="div", children=[]) + self.assertEqual(node.tag, "div") + self.assertEqual(node.children, []) + self.assertEqual(node.props, {}) + + def test_to_html_with_one_child(self): + child = LeafNode(value="Hello, World!", tag="p") + node = ParentNode(tag="div", children=[child]) + self.assertEqual(node.to_html(), "

Hello, World!

") + + def test_to_html_with_multiple_children(self): + child1 = LeafNode(value="First", tag="span") + child2 = LeafNode(value="Second", tag="span") + node = ParentNode(tag="div", children=[child1, child2]) + self.assertEqual(node.to_html(), "
FirstSecond
") + + def test_to_html_with_nested_parent(self): + inner_child = LeafNode(value="Nested", tag="b") + inner_parent = ParentNode(tag="p", children=[inner_child]) + outer_parent = ParentNode(tag="div", children=[inner_parent]) + + expected_html = "

Nested

" + self.assertEqual(outer_parent.to_html(), expected_html) + + def test_to_html_with_props(self): + child = LeafNode(value="Text", tag="p") + node = ParentNode(tag="section", children=[child], props={"class": "content"}) + self.assertEqual(node.to_html(), '

Text

') + + def test_to_html_with_empty_children_raises_error(self): + with self.assertRaises(ValueError): + ParentNode(tag="div", children=[]).to_html() + + def test_to_html_raises_error_without_tag(self): + child = LeafNode(value="Test", tag="p") + with self.assertRaises(ValueError): + ParentNode(tag=None, children=[child]).to_html() + +class TestLeafNode(unittest.TestCase): + + def test_initialization(self): + node = LeafNode(value="Hello, World!", tag="p") + self.assertEqual(node.tag, "p") + self.assertEqual(node.value, "Hello, World!") + self.assertEqual(node.children, []) + self.assertEqual(node.props, {}) + + def test_to_html_with_tag(self): + node = LeafNode(value="Hello, World!", tag="p") + self.assertEqual(node.to_html(), "

Hello, World!

") + + def test_to_html_without_tag(self): + node = LeafNode(value="Just text") + self.assertEqual(node.to_html(), "Just text") + + def test_to_html_with_props(self): + node = LeafNode(value="Click me", tag="button") + node.props = {"class": "btn", "id": "submit-btn"} + self.assertEqual(node.to_html(), '') + + def test_to_html_raises_value_error(self): + with self.assertRaises(ValueError): + LeafNode(value=None, tag="p").to_html() + +if __name__ == "__main__": + unittest.main() + + diff --git a/src/test_textnode.py b/src/test_textnode.py new file mode 100644 index 0000000..99fa212 --- /dev/null +++ b/src/test_textnode.py @@ -0,0 +1,29 @@ +import unittest +from textnode import TextNode, TextType + +class TestTextNode(unittest.TestCase): + def test_eq(self): + # Test equality between two identical nodes + node1 = TextNode("This is a text node", TextType.BOLD_TEXT) + node2 = TextNode("This is a text node", TextType.BOLD_TEXT) + self.assertEqual(node1, node2) + + def test_url(self): + # Test that the URL is correctly assigned + node = TextNode("This is link text", TextType.LINK_TEXT, "https://google.com") + self.assertEqual(node.url, "https://google.com") + + def test_repr(self): + # Test the string representation of a TextNode + node = TextNode("Sample text", TextType.LINK_TEXT, "https://example.com") + expected_repr = "TextNode(Sample text, link, https://example.com)" + self.assertEqual(repr(node), expected_repr) + + def test_invalid_text_type(self): + # Test that an invalid text_type raises an error + with self.assertRaises(ValueError): + TextNode("Invalid test", "invalid_type") + +if __name__ == "__main__": + unittest.main() + diff --git a/src/textnode.py b/src/textnode.py new file mode 100644 index 0000000..2a7aecf --- /dev/null +++ b/src/textnode.py @@ -0,0 +1,23 @@ +from enum import Enum + +class TextType(Enum): + NORMAL_TEXT = "normal" + BOLD_TEXT = "bold" + ITALIC_TEXT = "italic" + CODE_TEXT = "code" + LINK_TEXT = "link" + IMAGE_TEXT = "image" + +class TextNode: + def __init__(self, text, text_type, url=None): + if not isinstance(text_type, TextType): + raise ValueError(f"text_type must be an instance of TextType. Got: {text_type}") + self.text = text + self.text_type = text_type + self.url = url + + def __eq__(self, other): + return self.text == other.text and self.text_type == other.text_type and self.url == other.url + + def __repr__(self): + return f"TextNode({self.text}, {self.text_type.value}, {self.url})" diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..7a38f55 --- /dev/null +++ b/test.sh @@ -0,0 +1 @@ +python3 -m unittest discover -s src