refactor: make less messy
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
__pycache__/
|
||||
public/
|
||||
output.txt
|
||||
|
||||
@@ -1 +1 @@
|
||||
python3 src/main.py "docs" "/tolkien-fan-club/"
|
||||
python3 -m src "docs" "/tolkien-fan-club/"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
python3 src/main.py "public" "/"
|
||||
python3 -m src "public" "/"
|
||||
cd public && python3 -m http.server 8888
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
from src.cli import main
|
||||
|
||||
main()
|
||||
+19
@@ -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()
|
||||
@@ -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}'")
|
||||
@@ -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
@@ -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()
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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]
|
||||
@@ -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"(?<!!)\[(.+?)\]\((.+?)\)", text)
|
||||
|
||||
|
||||
def extract(pattern: str, text: str):
|
||||
return re.findall(pattern, text)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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()}>")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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):
|
||||
@@ -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):
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from htmlnode import HtmlNode
|
||||
from src.nodes.html import HtmlNode
|
||||
|
||||
|
||||
class TestHtmlNode(unittest.TestCase):
|
||||
@@ -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)
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from leafnode import LeafNode
|
||||
from src.nodes.leaf import LeafNode
|
||||
|
||||
|
||||
class LeafNodeTest(unittest.TestCase):
|
||||
@@ -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):
|
||||
@@ -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):
|
||||
Reference in New Issue
Block a user