diff --git a/.gitignore b/.gitignore
index 0e5b2ef..05419ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
__pycache__/
public/
+output.txt
diff --git a/build.sh b/build.sh
index 05fb647..ea63f10 100755
--- a/build.sh
+++ b/build.sh
@@ -1 +1 @@
-python3 src/main.py "docs" "/tolkien-fan-club/"
+python3 -m src "docs" "/tolkien-fan-club/"
diff --git a/main.sh b/main.sh
index 4235831..ae04610 100755
--- a/main.sh
+++ b/main.sh
@@ -1,2 +1,2 @@
-python3 src/main.py "public" "/"
+python3 -m src "public" "/"
cd public && python3 -m http.server 8888
diff --git a/output.txt b/output.txt
deleted file mode 100644
index 415b3a7..0000000
--- a/output.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-
This is text that _should_ remain
-the **same** even with inline stuff
-
-
-This is text that _should_ remain
-the **same** even with inline stuff
-
\ No newline at end of file
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/__main__.py b/src/__main__.py
new file mode 100644
index 0000000..679fb5e
--- /dev/null
+++ b/src/__main__.py
@@ -0,0 +1,3 @@
+from src.cli import main
+
+main()
diff --git a/src/cli.py b/src/cli.py
new file mode 100644
index 0000000..f6a0685
--- /dev/null
+++ b/src/cli.py
@@ -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_dir = sys.argv[1]
+ basepath = sys.argv[2]
+
+ copy_static(destination_dir)
+ generate_pages("content", "template.html", destination_dir, basepath)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/copier.py b/src/copier.py
new file mode 100644
index 0000000..b30c916
--- /dev/null
+++ b/src/copier.py
@@ -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}'")
diff --git a/src/generator.py b/src/generator.py
new file mode 100644
index 0000000..19cb8f6
--- /dev/null
+++ b/src/generator.py
@@ -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}'")
diff --git a/src/main.py b/src/main.py
deleted file mode 100644
index 262f3ec..0000000
--- a/src/main.py
+++ /dev/null
@@ -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_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()
diff --git a/src/markdown/__init__.py b/src/markdown/__init__.py
new file mode 100644
index 0000000..3af9f74
--- /dev/null
+++ b/src/markdown/__init__.py
@@ -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",
+]
diff --git a/src/block.py b/src/markdown/blocks.py
similarity index 83%
rename from src/block.py
rename to src/markdown/blocks.py
index d7a263b..fbebed1 100644
--- a/src/block.py
+++ b/src/markdown/blocks.py
@@ -1,5 +1,4 @@
import re
-import os
from enum import Enum
from typing import Callable
@@ -13,6 +12,7 @@ class BlockType(Enum):
ORDERED_LIST = "ordered list"
UNORDERED_LIST = "unordered list"
+
def block_to_block_type(block: str) -> BlockType:
if is_heading(block):
return BlockType.HEADING
@@ -23,38 +23,45 @@ def block_to_block_type(block: str) -> BlockType:
if is_quote(block):
return BlockType.QUOTE
- if is_unordered(block):
+ if is_unordered_list(block):
return BlockType.UNORDERED_LIST
- if is_ordered(block):
+ if is_ordered_list(block):
return BlockType.ORDERED_LIST
return BlockType.PARAGRAPH
+
def is_heading(block: str) -> bool:
pattern = r"^#{1,6}\s{1}?"
return has_pattern(pattern, block)
+
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)
+
def is_quote(block: str) -> bool:
pattern = r"^>"
return each_line_has_pattern(lambda _: pattern, block)
-def is_unordered(block: str) -> bool:
+
+def is_unordered_list(block: str) -> bool:
pattern = r"^-\s"
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)
+
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:
- lines = block.split(os.linesep)
+ lines = block.split("\n")
for i in range(len(lines)):
line = lines[i]
diff --git a/src/extract.py b/src/markdown/extract.py
similarity index 99%
rename from src/extract.py
rename to src/markdown/extract.py
index d07c291..5aa64b9 100644
--- a/src/extract.py
+++ b/src/markdown/extract.py
@@ -1,11 +1,13 @@
import re
+
def extract_markdown_images(text: str) -> list[tuple[str, str]]:
return extract(r"!\[(.+?)\]\((.+?)\)", text)
+
def extract_markdown_links(text: str) -> list[tuple[str, str]]:
return extract(r"(? 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
diff --git a/src/markdown/to_html.py b/src/markdown/to_html.py
new file mode 100644
index 0000000..d07a4d2
--- /dev/null
+++ b/src/markdown/to_html.py
@@ -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
diff --git a/src/nodes/__init__.py b/src/nodes/__init__.py
new file mode 100644
index 0000000..bb2bb08
--- /dev/null
+++ b/src/nodes/__init__.py
@@ -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",
+]
diff --git a/src/htmlnode.py b/src/nodes/html.py
similarity index 94%
rename from src/htmlnode.py
rename to src/nodes/html.py
index 20dcee7..3b2d25e 100644
--- a/src/htmlnode.py
+++ b/src/nodes/html.py
@@ -7,7 +7,7 @@ class HtmlNode:
tag: str | None = None,
value: str | None = None,
children: list["HtmlNode"] | None = None,
- props: dict | None = None,
+ props: dict[str, str] | None = None,
) -> None:
self.tag = tag
self.value = value
diff --git a/src/leafnode.py b/src/nodes/leaf.py
similarity index 79%
rename from src/leafnode.py
rename to src/nodes/leaf.py
index 8fbeba5..382523b 100644
--- a/src/leafnode.py
+++ b/src/nodes/leaf.py
@@ -1,6 +1,6 @@
from typing import Self
-from htmlnode import HtmlNode
+from src.nodes.html import HtmlNode
class LeafNode(HtmlNode):
@@ -8,9 +8,9 @@ class LeafNode(HtmlNode):
self: Self,
tag: str | None,
value: str | None,
- props: dict | None = None,
+ props: dict[str, str] | None = None,
) -> None:
- super().__init__(tag, value, [], props)
+ super().__init__(tag, value, None, props)
def to_html(self: Self) -> str:
if self.value is None:
diff --git a/src/parentnode.py b/src/nodes/parent.py
similarity index 88%
rename from src/parentnode.py
rename to src/nodes/parent.py
index 9b080a8..c443936 100644
--- a/src/parentnode.py
+++ b/src/nodes/parent.py
@@ -1,6 +1,6 @@
from typing import Self
-from htmlnode import HtmlNode
+from src.nodes.html import HtmlNode
class ParentNode(HtmlNode):
@@ -9,7 +9,7 @@ class ParentNode(HtmlNode):
tag: str,
children: list[HtmlNode],
value: str | None = None,
- props: dict | None = None,
+ props: dict[str, str] | None = None,
) -> None:
super().__init__(tag, value, children, props)
@@ -19,7 +19,7 @@ class ParentNode(HtmlNode):
if self.children is None or len(self.children) == 0:
raise ValueError
-
+
html = []
html.append(f"<{self.tag}{self.props_to_html()}>")
diff --git a/src/textnode.py b/src/nodes/text.py
similarity index 91%
rename from src/textnode.py
rename to src/nodes/text.py
index 115fb03..7db5dc3 100644
--- a/src/textnode.py
+++ b/src/nodes/text.py
@@ -1,6 +1,6 @@
from enum import Enum
-from leafnode import LeafNode
+from src.nodes.leaf import LeafNode
class TextType(Enum):
@@ -43,8 +43,10 @@ def text_node_to_html_node(text_node: TextNode) -> LeafNode:
case TextType.CODE:
return LeafNode("code", text_node.text)
case TextType.LINK:
+ assert text_node.url is not None
return LeafNode("a", text_node.text, {"href": text_node.url})
case TextType.IMAGE:
+ assert text_node.url is not None
return LeafNode("img", "", {"src": text_node.url, "alt": text_node.text})
case _:
raise ValueError
diff --git a/src/splitnodes.py b/src/splitnodes.py
deleted file mode 100644
index d9c0b54..0000000
--- a/src/splitnodes.py
+++ /dev/null
@@ -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
diff --git a/src/template.py b/src/template.py
new file mode 100644
index 0000000..ad57033
--- /dev/null
+++ b/src/template.py
@@ -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
diff --git a/src/tests/__init__.py b/src/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/test_block.py b/src/tests/test_blocks.py
similarity index 98%
rename from src/test_block.py
rename to src/tests/test_blocks.py
index 680fc3b..7a078b7 100644
--- a/src/test_block.py
+++ b/src/tests/test_blocks.py
@@ -1,6 +1,6 @@
import unittest
-from block import BlockType, block_to_block_type
+from src.markdown.blocks import BlockType, block_to_block_type
class TestBlock(unittest.TestCase):
diff --git a/src/test_extract.py b/src/tests/test_extract.py
similarity index 94%
rename from src/test_extract.py
rename to src/tests/test_extract.py
index 4b289b7..7d82be2 100644
--- a/src/test_extract.py
+++ b/src/tests/test_extract.py
@@ -1,6 +1,6 @@
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):
diff --git a/src/test_htmlnode.py b/src/tests/test_html.py
similarity index 98%
rename from src/test_htmlnode.py
rename to src/tests/test_html.py
index 37acc17..7fa2f97 100644
--- a/src/test_htmlnode.py
+++ b/src/tests/test_html.py
@@ -1,6 +1,6 @@
import unittest
-from htmlnode import HtmlNode
+from src.nodes.html import HtmlNode
class TestHtmlNode(unittest.TestCase):
diff --git a/src/test_splitnodes.py b/src/tests/test_inlines.py
similarity index 94%
rename from src/test_splitnodes.py
rename to src/tests/test_inlines.py
index 77d3eb5..894142b 100644
--- a/src/test_splitnodes.py
+++ b/src/tests/test_inlines.py
@@ -1,10 +1,13 @@
-import os
import unittest
-from splitnodes import (markdown_to_blocks, markdown_to_html_node,
- split_nodes_delimiter, split_nodes_image,
- split_nodes_link, text_to_text_node)
-from textnode import TextNode, TextType
+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
+from src.nodes.text import TextNode, TextType
class TestSplitNodes(unittest.TestCase):
@@ -198,9 +201,4 @@ the **same** even with inline stuff
node = markdown_to_html_node(md)
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)
diff --git a/src/test_leafnode.py b/src/tests/test_leaf.py
similarity index 96%
rename from src/test_leafnode.py
rename to src/tests/test_leaf.py
index 78e26fa..512ccb5 100644
--- a/src/test_leafnode.py
+++ b/src/tests/test_leaf.py
@@ -1,6 +1,6 @@
import unittest
-from leafnode import LeafNode
+from src.nodes.leaf import LeafNode
class LeafNodeTest(unittest.TestCase):
diff --git a/src/test_parentnode.py b/src/tests/test_parent.py
similarity index 94%
rename from src/test_parentnode.py
rename to src/tests/test_parent.py
index 5b35ea3..b1b0bc1 100644
--- a/src/test_parentnode.py
+++ b/src/tests/test_parent.py
@@ -1,9 +1,9 @@
import unittest
from typing import cast
-from htmlnode import HtmlNode
-from leafnode import LeafNode
-from parentnode import ParentNode
+from src.nodes.html import HtmlNode
+from src.nodes.leaf import LeafNode
+from src.nodes.parent import ParentNode
class TestParentNode(unittest.TestCase):
diff --git a/src/test_textnode.py b/src/tests/test_text.py
similarity index 97%
rename from src/test_textnode.py
rename to src/tests/test_text.py
index 0de5ea1..38d8af0 100644
--- a/src/test_textnode.py
+++ b/src/tests/test_text.py
@@ -1,7 +1,7 @@
import unittest
-from leafnode import LeafNode
-from textnode import TextNode, TextType, text_node_to_html_node
+from src.nodes.leaf import LeafNode
+from src.nodes.text import TextNode, TextType, text_node_to_html_node
class TestTextNode(unittest.TestCase):
diff --git a/test.sh b/test.sh
index b03cdc0..826f264 100755
--- a/test.sh
+++ b/test.sh
@@ -1,2 +1,2 @@
#!/bin/bash
-python3 -m unittest discover -s src
+python3 -m unittest discover -s src/tests