added markdown id
This commit is contained in:
commit
8d709fdc0e
13 changed files with 779 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
__pycache__/
|
||||||
1
main.sh
Executable file
1
main.sh
Executable file
|
|
@ -0,0 +1 @@
|
||||||
|
python3 src/main.py
|
||||||
0
out.txt
Normal file
0
out.txt
Normal file
20
public/index.html
Normal file
20
public/index.html
Normal 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
23
public/styles.css
Normal 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
224
src/conversions.py
Normal 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
56
src/htmlnode.py
Normal 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
15
src/main.py
Normal 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
293
src/test_conversions.py
Normal 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  in markdown."
|
||||||
|
expected = [("alt text", "image.jpg")]
|
||||||
|
self.assertEqual(extract_markdown_images(text), expected)
|
||||||
|
|
||||||
|
def test_multiple_images(self):
|
||||||
|
text = " and "
|
||||||
|
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 "
|
||||||
|
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 = ".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 = ""
|
||||||
|
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 ", 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  middle  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("", 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("", 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*  and  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
93
src/test_htmlnode.py
Normal 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
29
src/test_textnode.py
Normal 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
23
src/textnode.py
Normal 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
1
test.sh
Executable file
|
|
@ -0,0 +1 @@
|
||||||
|
python3 -m unittest discover -s src
|
||||||
Loading…
Add table
Add a link
Reference in a new issue