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