added markdown id

This commit is contained in:
specCon18 2025-02-01 00:56:50 -05:00
commit 8d709fdc0e
13 changed files with 779 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
__pycache__/

1
main.sh Executable file
View file

@ -0,0 +1 @@
python3 src/main.py

0
out.txt Normal file
View file

20
public/index.html Normal file
View file

@ -0,0 +1,20 @@
<html>
<head>
<title>Why Frontend Development Sucks</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Front-end Development is the Worst</h1>
<p>
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.
</p>
<p>
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
<a href="https://www.boot.dev">backend</a>, where the real programming
happens.
</p>
</body>
</html>

23
public/styles.css Normal file
View file

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

224
src/conversions.py Normal file
View file

@ -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

56
src/htmlnode.py Normal file
View file

@ -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}</{self.tag}>"
else:
return f"<{self.tag} {props_as_html}>{self.value}</{self.tag}>"
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}</{self.tag}>"
else:
return f"<{self.tag}>{children_html}</{self.tag}>"

15
src/main.py Normal file
View file

@ -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()

293
src/test_conversions.py Normal file
View file

@ -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()

93
src/test_htmlnode.py Normal file
View file

@ -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(), "<div><p>Hello, World!</p></div>")
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(), "<div><span>First</span><span>Second</span></div>")
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 = "<div><p><b>Nested</b></p></div>"
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(), '<section class="content"><p>Text</p></section>')
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(), "<p>Hello, World!</p>")
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(), '<button class="btn" id="submit-btn">Click me</button>')
def test_to_html_raises_value_error(self):
with self.assertRaises(ValueError):
LeafNode(value=None, tag="p").to_html()
if __name__ == "__main__":
unittest.main()

29
src/test_textnode.py Normal file
View file

@ -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()

23
src/textnode.py Normal file
View file

@ -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})"

1
test.sh Executable file
View file

@ -0,0 +1 @@
python3 -m unittest discover -s src