refactor: make less messy

This commit is contained in:
Stevan Freeborn
2026-07-08 08:32:28 -05:00
parent 090aacdbb8
commit c14c519293
31 changed files with 441 additions and 366 deletions
+1
View File
@@ -1,2 +1,3 @@
__pycache__/ __pycache__/
public/ public/
output.txt
+1 -1
View File
@@ -1 +1 @@
python3 src/main.py "docs" "/tolkien-fan-club/" python3 -m src "docs" "/tolkien-fan-club/"
+1 -1
View File
@@ -1,2 +1,2 @@
python3 src/main.py "public" "/" python3 -m src "public" "/"
cd public && python3 -m http.server 8888 cd public && python3 -m http.server 8888
-7
View File
@@ -1,7 +0,0 @@
<div><pre><code>This is text that _should_ remain
the **same** even with inline stuff
</code></pre></div>
<div><pre><code>This is text that _should_ remain
the **same** even with inline stuff
</code></pre></div>
View File
+3
View File
@@ -0,0 +1,3 @@
from src.cli import main
main()
+19
View File
@@ -0,0 +1,19 @@
import sys
from src.copier import copy_static
from src.generator import generate_pages
def main() -> None:
if len(sys.argv) < 3:
raise RuntimeError("Please provide destination and basepath: <destination> <base_path>")
destination_dir = sys.argv[1]
basepath = sys.argv[2]
copy_static(destination_dir)
generate_pages("content", "template.html", destination_dir, basepath)
if __name__ == "__main__":
main()
+35
View File
@@ -0,0 +1,35 @@
import shutil
from pathlib import Path
def copy_static(destination: str) -> None:
source_path = Path("static")
destination_path = Path(destination)
if not source_path.exists():
raise FileNotFoundError(f"Source directory '{source_path}' does not exist.")
if destination_path.exists():
print(f"Cleaning contents of '{destination_path}'...")
for item in destination_path.iterdir():
if item.is_dir():
shutil.rmtree(item)
else:
item.unlink()
else:
destination_path.mkdir(parents=True, exist_ok=True)
print(f"Copying files from '{source_path}' to '{destination_path}'...")
for item in source_path.rglob("*"):
relative_path = item.relative_to(source_path)
target_path = destination_path / relative_path
if item.is_dir():
target_path.mkdir(parents=True, exist_ok=True)
else:
target_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, target_path)
print(f"Finished copying files from '{source_path}' to '{destination_path}'")
+52
View File
@@ -0,0 +1,52 @@
from pathlib import Path
from src.markdown.to_html import markdown_to_html_node
from src.template import extract_title, render_template
def generate_pages(
dir_path_content: str, template_path: str, dest_dir_path: str, basepath: str
) -> None:
content_dir = Path(dir_path_content)
template = Path(template_path)
dest_dir = Path(dest_dir_path)
print(f"Crawling '{content_dir}' to generate HTML pages in '{dest_dir}'...")
for item in content_dir.rglob("*"):
if item.is_file() and item.suffix.lower() == ".md":
relative_path = item.relative_to(content_dir)
target_path = dest_dir / relative_path.with_suffix(".html")
generate_page(item, template, target_path, basepath)
print("Page generation complete")
def generate_page(
from_path: str | Path, template_path: str | Path, dest_path: str | Path, basepath: str
) -> None:
src = Path(from_path)
template = Path(template_path)
dest = Path(dest_path)
if not src.exists():
raise RuntimeError(f"Source file '{src}' does not exist")
if not src.is_file():
raise RuntimeError(f"Source file '{src}' is not a file")
print(f"Generating page from '{src}' to '{dest}' using '{template}'")
markdown_content = src.read_text(encoding="utf-8")
html_node = markdown_to_html_node(markdown_content)
title = extract_title(markdown_content)
html = html_node.to_html()
rendered_html = render_template(template, title, html, basepath)
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(rendered_html, encoding="utf-8")
print(f"Page successfully generated: '{dest}'")
-123
View File
@@ -1,123 +0,0 @@
import shutil
import sys
from pathlib import Path
from splitnodes import markdown_to_html_node
def main():
if len(sys.argv) < 3:
raise RuntimeError("Please provide destination and basepath: <destination> <base_path>")
destination_dir = sys.argv[1]
basepath = sys.argv[2]
copy_static(destination_dir)
generate_pages("content", "template.html", destination_dir, basepath)
def copy_static(destination: str) -> None:
source_path = Path("static")
destination_path = Path(destination)
if not source_path.exists():
raise FileNotFoundError(f"Source directory '{source_path}' does not exist.")
if destination_path.exists():
print(f"Cleaning contents of '{destination_path}'...")
for item in destination_path.iterdir():
if item.is_dir():
shutil.rmtree(item)
else:
item.unlink()
else:
destination_path.mkdir(parents=True, exist_ok=True)
print(f"Copying files from '{source_path}' to '{destination_path}'...")
for item in source_path.rglob("*"):
relative_path = item.relative_to(source_path)
target_path = destination_path / relative_path
if item.is_dir():
target_path.mkdir(parents=True, exist_ok=True)
else:
target_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, target_path)
print(f"Finished copying files from '{source_path}' to '{destination_path}'")
def generate_pages(
dir_path_content: str, template_path: str, dest_dir_path: str, basepath: str
) -> None:
content_dir = Path(dir_path_content)
template = Path(template_path)
dest_dir = Path(dest_dir_path)
print(f"Crawling '{content_dir}' to generate HTML pages in '{dest_dir}'...")
for item in content_dir.rglob("*"):
if item.is_file() and item.suffix.lower() == ".md":
relative_path = item.relative_to(content_dir)
target_path = dest_dir / relative_path.with_suffix(".html")
generate_page(item, template, target_path, basepath)
print("Page generation complete")
def generate_page(
from_path: str | Path, template_path: str | Path, dest_path: str | Path, basepath: str
) -> None:
src = Path(from_path)
template = Path(template_path)
dest = Path(dest_path)
if not src.exists():
raise RuntimeError(f"Source file '{src}' does not exist")
if not src.is_file():
raise RuntimeError(f"Source file '{src}' is not a file")
if not template.exists():
raise RuntimeError(f"Template file '{template}' does not exist")
if not template.is_file():
raise RuntimeError(f"Template file '{template}' is not a file")
print(f"Generating page from '{src}' to '{dest}' using '{template}'")
markdown_content = src.read_text(encoding="utf-8")
template_content = template.read_text(encoding="utf-8")
html_node = markdown_to_html_node(markdown_content)
title = extract_title(markdown_content)
html = html_node.to_html()
rendered_html = template_content.replace("{{ Title }}", title)
rendered_html = rendered_html.replace("{{ Content }}", html)
rendered_html = rendered_html.replace('href="/', f'href="{basepath}')
rendered_html = rendered_html.replace('src="/', f'src="{basepath}')
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(rendered_html, encoding="utf-8")
print(f"Page successfuly generated: 'dest'")
def extract_title(markdown: str) -> str:
heading_identifier = "# "
lines = markdown.splitlines()
for line in lines:
if line.startswith(heading_identifier):
return line.lstrip(heading_identifier)
raise RuntimeError("No heading found that can be used as title")
if __name__ == "__main__":
main()
+23
View File
@@ -0,0 +1,23 @@
from src.markdown.blocks import BlockType, block_to_block_type
from src.markdown.extract import extract_markdown_images, extract_markdown_links
from src.markdown.inlines import (
split_nodes_delimiter,
split_nodes_image,
split_nodes_link,
text_to_text_node,
)
from src.markdown.to_html import markdown_to_blocks, markdown_to_html_node, text_to_children
__all__ = [
"BlockType",
"block_to_block_type",
"extract_markdown_images",
"extract_markdown_links",
"markdown_to_blocks",
"markdown_to_html_node",
"split_nodes_delimiter",
"split_nodes_image",
"split_nodes_link",
"text_to_children",
"text_to_text_node",
]
+15 -8
View File
@@ -1,5 +1,4 @@
import re import re
import os
from enum import Enum from enum import Enum
from typing import Callable from typing import Callable
@@ -13,6 +12,7 @@ class BlockType(Enum):
ORDERED_LIST = "ordered list" ORDERED_LIST = "ordered list"
UNORDERED_LIST = "unordered list" UNORDERED_LIST = "unordered list"
def block_to_block_type(block: str) -> BlockType: def block_to_block_type(block: str) -> BlockType:
if is_heading(block): if is_heading(block):
return BlockType.HEADING return BlockType.HEADING
@@ -23,38 +23,45 @@ def block_to_block_type(block: str) -> BlockType:
if is_quote(block): if is_quote(block):
return BlockType.QUOTE return BlockType.QUOTE
if is_unordered(block): if is_unordered_list(block):
return BlockType.UNORDERED_LIST return BlockType.UNORDERED_LIST
if is_ordered(block): if is_ordered_list(block):
return BlockType.ORDERED_LIST return BlockType.ORDERED_LIST
return BlockType.PARAGRAPH return BlockType.PARAGRAPH
def is_heading(block: str) -> bool: def is_heading(block: str) -> bool:
pattern = r"^#{1,6}\s{1}?" pattern = r"^#{1,6}\s{1}?"
return has_pattern(pattern, block) return has_pattern(pattern, block)
def is_code(block: str) -> bool: def is_code(block: str) -> bool:
pattern = rf"^```.*{os.linesep}[\s\S]*{os.linesep}```$" pattern = r"^```.*\n[\s\S]*\n```$"
return has_pattern(pattern, block) return has_pattern(pattern, block)
def is_quote(block: str) -> bool: def is_quote(block: str) -> bool:
pattern = r"^>" pattern = r"^>"
return each_line_has_pattern(lambda _: pattern, block) return each_line_has_pattern(lambda _: pattern, block)
def is_unordered(block: str) -> bool:
def is_unordered_list(block: str) -> bool:
pattern = r"^-\s" pattern = r"^-\s"
return each_line_has_pattern(lambda _: pattern, block) return each_line_has_pattern(lambda _: pattern, block)
def is_ordered(block: str) -> bool:
def is_ordered_list(block: str) -> bool:
return each_line_has_pattern(lambda i: rf"{i + 1}\.\s", block) return each_line_has_pattern(lambda i: rf"{i + 1}\.\s", block)
def has_pattern(pattern: str, block: str) -> bool: def has_pattern(pattern: str, block: str) -> bool:
return re.match(pattern, block) != None return re.match(pattern, block) is not None
def each_line_has_pattern(pattern_builder: Callable[[int], str], block: str) -> bool: def each_line_has_pattern(pattern_builder: Callable[[int], str], block: str) -> bool:
lines = block.split(os.linesep) lines = block.split("\n")
for i in range(len(lines)): for i in range(len(lines)):
line = lines[i] line = lines[i]
+3 -1
View File
@@ -1,11 +1,13 @@
import re import re
def extract_markdown_images(text: str) -> list[tuple[str, str]]: def extract_markdown_images(text: str) -> list[tuple[str, str]]:
return extract(r"!\[(.+?)\]\((.+?)\)", text) return extract(r"!\[(.+?)\]\((.+?)\)", text)
def extract_markdown_links(text: str) -> list[tuple[str, str]]: def extract_markdown_links(text: str) -> list[tuple[str, str]]:
return extract(r"(?<!!)\[(.+?)\]\((.+?)\)", text) return extract(r"(?<!!)\[(.+?)\]\((.+?)\)", text)
def extract(pattern: str, text: str): def extract(pattern: str, text: str):
return re.findall(pattern, text) return re.findall(pattern, text)
+92
View File
@@ -0,0 +1,92 @@
from typing import Callable
from src.markdown.extract import extract_markdown_images, extract_markdown_links
from src.nodes.text import TextNode, TextType
def split_nodes_delimiter(
old_nodes: list[TextNode], delimiter: str, text_type: TextType
) -> list[TextNode]:
result = []
for node in old_nodes:
if node.text_type != TextType.PLAIN:
result.append(node)
continue
parts = node.text.split(delimiter)
if len(parts) % 2 == 0:
raise RuntimeError("No matching delimiter found")
for i in range(len(parts)):
current_text = parts[i]
if i % 2 == 0:
result.append(TextNode(current_text, TextType.PLAIN))
else:
result.append(TextNode(current_text, text_type))
return result
def split_nodes_image(old_nodes: list[TextNode]) -> list[TextNode]:
return split_nodes(
old_nodes,
extracter=extract_markdown_images,
pattern_builder=lambda t: f"![{t[0]}]({t[1]})",
node_creator=lambda t: TextNode(t[0], TextType.IMAGE, t[1]),
)
def split_nodes_link(old_nodes: list[TextNode]) -> list[TextNode]:
return split_nodes(
old_nodes,
extracter=extract_markdown_links,
pattern_builder=lambda t: f"[{t[0]}]({t[1]})",
node_creator=lambda t: TextNode(t[0], TextType.LINK, t[1]),
)
def split_nodes(
old_nodes: list[TextNode],
extracter: Callable[[str], list[tuple[str, str]]],
pattern_builder: Callable[[tuple[str, str]], str],
node_creator: Callable[[tuple[str, str]], TextNode],
) -> list[TextNode]:
result = []
for node in old_nodes:
if node.text_type != TextType.PLAIN:
result.append(node)
continue
extracted = extracter(node.text)
node_text = node.text
for match in extracted:
pattern = pattern_builder(match)
parts = node_text.split(pattern, maxsplit=1)
result.append(TextNode(parts[0], TextType.PLAIN))
matched_node = node_creator(match)
result.append(matched_node)
node_text = parts[1]
if len(node_text) > 0:
result.append(TextNode(node_text, TextType.PLAIN))
return result
def text_to_text_node(text: str) -> list[TextNode]:
old_nodes = [TextNode(text, TextType.PLAIN)]
old_nodes = split_nodes_delimiter(old_nodes, "**", TextType.BOLD)
old_nodes = split_nodes_delimiter(old_nodes, "_", TextType.ITALIC)
old_nodes = split_nodes_delimiter(old_nodes, "`", TextType.CODE)
old_nodes = split_nodes_image(old_nodes)
old_nodes = split_nodes_link(old_nodes)
return old_nodes
+119
View File
@@ -0,0 +1,119 @@
from src.markdown.blocks import BlockType, block_to_block_type
from src.markdown.inlines import text_to_text_node
from src.nodes.html import HtmlNode
from src.nodes.parent import ParentNode
from src.nodes.text import TextNode, TextType, text_node_to_html_node
def markdown_to_blocks(markdown: str) -> list[str]:
blocks = markdown.split("\n\n")
trimmed = [block.strip() for block in blocks]
non_empty = [block for block in trimmed if block != ""]
return non_empty
def markdown_to_html_node(markdown: str) -> HtmlNode:
children = []
blocks = markdown_to_blocks(markdown)
for block in blocks:
block_type = block_to_block_type(block)
match block_type:
case BlockType.PARAGRAPH:
children.append(handle_paragraph(block))
case BlockType.HEADING:
children.append(handle_heading(block))
case BlockType.CODE:
children.append(handle_code(block))
case BlockType.QUOTE:
children.append(handle_quote(block))
case BlockType.ORDERED_LIST:
children.append(handle_ordered_list(block))
case BlockType.UNORDERED_LIST:
children.append(handle_unordered_list(block))
case _:
raise NotImplemented("Unknown block type")
return ParentNode("div", children=children)
def handle_paragraph(block: str) -> ParentNode:
no_newlines = block.replace("\n", " ")
paragraph_children = text_to_children(no_newlines)
return ParentNode("p", children=paragraph_children)
def handle_heading(block: str) -> ParentNode:
heading_number = 0
for c in block:
if c != "#":
break
heading_number += 1
heading_children = text_to_children(block.lstrip(f"{'#' * heading_number} "))
return ParentNode(f"h{heading_number}", children=heading_children)
def handle_code(block: str) -> ParentNode:
removed = block.lstrip("```\n").rstrip("```")
text_node = TextNode(removed, TextType.CODE)
code_node = text_node_to_html_node(text_node)
pre_children: list[HtmlNode] = [code_node]
return ParentNode("pre", children=pre_children)
def handle_quote(block: str) -> ParentNode:
replaced_blocks = []
lines = block.splitlines()
for line in lines:
replaced = line.lstrip("> ").lstrip(">")
replaced_blocks.append(replaced)
replaced_block = "\n".join(replaced_blocks)
quote_children = text_to_children(replaced_block)
return ParentNode("blockquote", children=quote_children)
def handle_ordered_list(block: str) -> ParentNode:
ordered_list_children = []
ordered_list_items = block.splitlines()
for i in range(len(ordered_list_items)):
ordered_item = ordered_list_items[i]
ordered_item_children_nodes = text_to_children(ordered_item.lstrip(f"{i + 1}. "))
ordered_item_node = ParentNode(
"li", children=ordered_item_children_nodes
)
ordered_list_children.append(ordered_item_node)
return ParentNode("ol", children=ordered_list_children)
def handle_unordered_list(block: str) -> ParentNode:
unordered_list_children = []
unordered_list_items = block.splitlines()
for unordered_item in unordered_list_items:
unordered_item_children_nodes = text_to_children(unordered_item.lstrip("- "))
unordered_item_node = ParentNode(
"li", children=unordered_item_children_nodes
)
unordered_list_children.append(unordered_item_node)
return ParentNode("ul", children=unordered_list_children)
def text_to_children(markdown: str) -> list[HtmlNode]:
children = []
text_nodes = text_to_text_node(markdown)
for text_node in text_nodes:
html_node = text_node_to_html_node(text_node)
children.append(html_node)
return children
+13
View File
@@ -0,0 +1,13 @@
from src.nodes.html import HtmlNode
from src.nodes.leaf import LeafNode
from src.nodes.parent import ParentNode
from src.nodes.text import TextNode, TextType, text_node_to_html_node
__all__ = [
"HtmlNode",
"LeafNode",
"ParentNode",
"TextNode",
"TextType",
"text_node_to_html_node",
]
+1 -1
View File
@@ -7,7 +7,7 @@ class HtmlNode:
tag: str | None = None, tag: str | None = None,
value: str | None = None, value: str | None = None,
children: list["HtmlNode"] | None = None, children: list["HtmlNode"] | None = None,
props: dict | None = None, props: dict[str, str] | None = None,
) -> None: ) -> None:
self.tag = tag self.tag = tag
self.value = value self.value = value
+3 -3
View File
@@ -1,6 +1,6 @@
from typing import Self from typing import Self
from htmlnode import HtmlNode from src.nodes.html import HtmlNode
class LeafNode(HtmlNode): class LeafNode(HtmlNode):
@@ -8,9 +8,9 @@ class LeafNode(HtmlNode):
self: Self, self: Self,
tag: str | None, tag: str | None,
value: str | None, value: str | None,
props: dict | None = None, props: dict[str, str] | None = None,
) -> None: ) -> None:
super().__init__(tag, value, [], props) super().__init__(tag, value, None, props)
def to_html(self: Self) -> str: def to_html(self: Self) -> str:
if self.value is None: if self.value is None:
+2 -2
View File
@@ -1,6 +1,6 @@
from typing import Self from typing import Self
from htmlnode import HtmlNode from src.nodes.html import HtmlNode
class ParentNode(HtmlNode): class ParentNode(HtmlNode):
@@ -9,7 +9,7 @@ class ParentNode(HtmlNode):
tag: str, tag: str,
children: list[HtmlNode], children: list[HtmlNode],
value: str | None = None, value: str | None = None,
props: dict | None = None, props: dict[str, str] | None = None,
) -> None: ) -> None:
super().__init__(tag, value, children, props) super().__init__(tag, value, children, props)
+3 -1
View File
@@ -1,6 +1,6 @@
from enum import Enum from enum import Enum
from leafnode import LeafNode from src.nodes.leaf import LeafNode
class TextType(Enum): class TextType(Enum):
@@ -43,8 +43,10 @@ def text_node_to_html_node(text_node: TextNode) -> LeafNode:
case TextType.CODE: case TextType.CODE:
return LeafNode("code", text_node.text) return LeafNode("code", text_node.text)
case TextType.LINK: case TextType.LINK:
assert text_node.url is not None
return LeafNode("a", text_node.text, {"href": text_node.url}) return LeafNode("a", text_node.text, {"href": text_node.url})
case TextType.IMAGE: case TextType.IMAGE:
assert text_node.url is not None
return LeafNode("img", "", {"src": text_node.url, "alt": text_node.text}) return LeafNode("img", "", {"src": text_node.url, "alt": text_node.text})
case _: case _:
raise ValueError raise ValueError
-197
View File
@@ -1,197 +0,0 @@
import os
from typing import Callable
from block import BlockType, block_to_block_type
from extract import extract_markdown_images, extract_markdown_links
from htmlnode import HtmlNode
from parentnode import ParentNode
from textnode import TextNode, TextType, text_node_to_html_node
def split_nodes_delimiter(
old_nodes: list[TextNode], delimiter: str, text_type: TextType
) -> list[TextNode]:
result = []
for node in old_nodes:
if node.text_type != TextType.PLAIN:
result.append(node)
continue
parts = node.text.split(delimiter)
if len(parts) % 2 == 0:
raise RuntimeError("No matching delimiter found")
for i in range(len(parts)):
current_text = parts[i]
if i % 2 == 0:
result.append(TextNode(current_text, TextType.PLAIN))
else:
result.append(TextNode(current_text, text_type))
return result
def split_nodes_image(old_nodes: list[TextNode]) -> list[TextNode]:
return split_nodes(
old_nodes,
extracter=extract_markdown_images,
pattern_builder=lambda t: f"![{t[0]}]({t[1]})",
node_creator=lambda t: TextNode(t[0], TextType.IMAGE, t[1]),
)
def split_nodes_link(old_nodes: list[TextNode]) -> list[TextNode]:
return split_nodes(
old_nodes,
extracter=extract_markdown_links,
pattern_builder=lambda t: f"[{t[0]}]({t[1]})",
node_creator=lambda t: TextNode(t[0], TextType.LINK, t[1]),
)
def split_nodes(
old_nodes: list[TextNode],
extracter: Callable[[str], list[tuple[str, str]]],
pattern_builder: Callable[[tuple[str, str]], str],
node_creator: Callable[[tuple[str, str]], TextNode],
) -> list[TextNode]:
result = []
for node in old_nodes:
if node.text_type != TextType.PLAIN:
result.append(node)
continue
extracted = extracter(node.text)
node_text = node.text
for extract in extracted:
pattern = pattern_builder(extract)
parts = node_text.split(pattern, maxsplit=1)
result.append(TextNode(parts[0], TextType.PLAIN))
node = node_creator(extract)
result.append(node)
node_text = parts[1]
if len(node_text) > 0:
result.append(TextNode(node_text, TextType.PLAIN))
return result
def text_to_text_node(text: str) -> list[TextNode]:
old_nodes = [TextNode(text, TextType.PLAIN)]
old_nodes = split_nodes_delimiter(old_nodes, "**", TextType.BOLD)
old_nodes = split_nodes_delimiter(old_nodes, "_", TextType.ITALIC)
old_nodes = split_nodes_delimiter(old_nodes, "`", TextType.CODE)
old_nodes = split_nodes_image(old_nodes)
old_nodes = split_nodes_link(old_nodes)
return old_nodes
def markdown_to_blocks(markdown: str) -> list[str]:
blocks = markdown.split(
"\n\n",
)
trimmed = [block.strip() for block in blocks]
non_empty = [block for block in trimmed if block != ""]
return non_empty
def markdown_to_html_node(markdown: str) -> HtmlNode:
children = []
blocks = markdown_to_blocks(markdown)
for block in blocks:
block_type = block_to_block_type(block)
match block_type:
case BlockType.PARAGRAPH:
no_newlines = block.replace(os.linesep, " ")
paragraph_children = text_to_children(no_newlines)
paragraph_node = ParentNode("p", children=paragraph_children)
children.append(paragraph_node)
case BlockType.HEADING:
heading_number = 0
for c in block:
if c != "#":
break
heading_number += 1
heading_children = text_to_children(block.lstrip(f"{"#" * heading_number} "))
heading_node = ParentNode(
f"h{heading_number}", children=heading_children
)
children.append(heading_node)
case BlockType.CODE:
removed = block.lstrip("```\n").rstrip("```")
text_node = TextNode(removed, TextType.CODE)
code_node = text_node_to_html_node(text_node)
pre_children: list[HtmlNode] = [code_node]
pre_node = ParentNode("pre", children=pre_children)
children.append(pre_node)
case BlockType.QUOTE:
replaced_blocks = []
lines = block.splitlines()
for line in lines:
replaced = line.lstrip("> ").lstrip(">")
replaced_blocks.append(replaced)
replaced_block = os.linesep.join(replaced_blocks)
quote_children = text_to_children(replaced_block)
block_node = ParentNode("blockquote", children=quote_children)
children.append(block_node)
case BlockType.ORDERED_LIST:
ordered_list_children = []
ordered_list_items = block.splitlines()
for i in range(len(ordered_list_items)):
ordered_item = ordered_list_items[i]
ordered_item_children_nodes = text_to_children(ordered_item.lstrip(f"{i + 1}. "))
ordered_item_node = ParentNode(
"li", children=ordered_item_children_nodes
)
ordered_list_children.append(ordered_item_node)
ordered_list_node = ParentNode("ol", children=ordered_list_children)
children.append(ordered_list_node)
case BlockType.UNORDERED_LIST:
unordered_list_children = []
unordered_list_items = block.splitlines()
for unordered_item in unordered_list_items:
unordered_item_children_nodes = text_to_children(unordered_item.lstrip("- "))
unordered_item_node = ParentNode(
"li", children=unordered_item_children_nodes
)
unordered_list_children.append(unordered_item_node)
unordered_list_node = ParentNode("ul", children=unordered_list_children)
children.append(unordered_list_node)
case _:
raise NotImplemented("Unknown block type")
return ParentNode("div", children=children)
def text_to_children(markdown: str) -> list[HtmlNode]:
children = []
text_nodes = text_to_text_node(markdown)
for text_node in text_nodes:
html_node = text_node_to_html_node(text_node)
children.append(html_node)
return children
+36
View File
@@ -0,0 +1,36 @@
from pathlib import Path
def extract_title(markdown: str) -> str:
heading_identifier = "# "
lines = markdown.splitlines()
for line in lines:
if line.startswith(heading_identifier):
return line.lstrip(heading_identifier)
raise RuntimeError("No heading found that can be used as title")
def render_template(
template_path: str | Path,
title: str,
content: str,
basepath: str,
) -> str:
template = Path(template_path)
if not template.exists():
raise RuntimeError(f"Template file '{template}' does not exist")
if not template.is_file():
raise RuntimeError(f"Template file '{template}' is not a file")
template_content = template.read_text(encoding="utf-8")
rendered = template_content.replace("{{ Title }}", title)
rendered = rendered.replace("{{ Content }}", content)
rendered = rendered.replace('href="/', f'href="{basepath}')
rendered = rendered.replace('src="/', f'src="{basepath}')
return rendered
View File
@@ -1,6 +1,6 @@
import unittest import unittest
from block import BlockType, block_to_block_type from src.markdown.blocks import BlockType, block_to_block_type
class TestBlock(unittest.TestCase): class TestBlock(unittest.TestCase):
@@ -1,6 +1,6 @@
import unittest import unittest
from extract import extract_markdown_images, extract_markdown_links from src.markdown.extract import extract_markdown_images, extract_markdown_links
class TextExtract(unittest.TestCase): class TextExtract(unittest.TestCase):
@@ -1,6 +1,6 @@
import unittest import unittest
from htmlnode import HtmlNode from src.nodes.html import HtmlNode
class TestHtmlNode(unittest.TestCase): class TestHtmlNode(unittest.TestCase):
@@ -1,10 +1,13 @@
import os
import unittest import unittest
from splitnodes import (markdown_to_blocks, markdown_to_html_node, from src.markdown.inlines import (
split_nodes_delimiter, split_nodes_image, split_nodes_delimiter,
split_nodes_link, text_to_text_node) split_nodes_image,
from textnode import TextNode, TextType split_nodes_link,
text_to_text_node,
)
from src.markdown.to_html import markdown_to_blocks, markdown_to_html_node
from src.nodes.text import TextNode, TextType
class TestSplitNodes(unittest.TestCase): class TestSplitNodes(unittest.TestCase):
@@ -198,9 +201,4 @@ the **same** even with inline stuff
node = markdown_to_html_node(md) node = markdown_to_html_node(md)
html = node.to_html() html = node.to_html()
with open("output.txt", "w") as file:
file.write(expected)
file.write("\n\n")
file.write(html)
self.assertEqual(html, expected) self.assertEqual(html, expected)
@@ -1,6 +1,6 @@
import unittest import unittest
from leafnode import LeafNode from src.nodes.leaf import LeafNode
class LeafNodeTest(unittest.TestCase): class LeafNodeTest(unittest.TestCase):
@@ -1,9 +1,9 @@
import unittest import unittest
from typing import cast from typing import cast
from htmlnode import HtmlNode from src.nodes.html import HtmlNode
from leafnode import LeafNode from src.nodes.leaf import LeafNode
from parentnode import ParentNode from src.nodes.parent import ParentNode
class TestParentNode(unittest.TestCase): class TestParentNode(unittest.TestCase):
@@ -1,7 +1,7 @@
import unittest import unittest
from leafnode import LeafNode from src.nodes.leaf import LeafNode
from textnode import TextNode, TextType, text_node_to_html_node from src.nodes.text import TextNode, TextType, text_node_to_html_node
class TestTextNode(unittest.TestCase): class TestTextNode(unittest.TestCase):
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/bash #!/bin/bash
python3 -m unittest discover -s src python3 -m unittest discover -s src/tests