refactor: make less messy
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
public/
|
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
|
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 re
|
||||||
import os
|
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
@@ -13,6 +12,7 @@ class BlockType(Enum):
|
|||||||
ORDERED_LIST = "ordered list"
|
ORDERED_LIST = "ordered list"
|
||||||
UNORDERED_LIST = "unordered list"
|
UNORDERED_LIST = "unordered list"
|
||||||
|
|
||||||
|
|
||||||
def block_to_block_type(block: str) -> BlockType:
|
def block_to_block_type(block: str) -> BlockType:
|
||||||
if is_heading(block):
|
if is_heading(block):
|
||||||
return BlockType.HEADING
|
return BlockType.HEADING
|
||||||
@@ -23,38 +23,45 @@ def block_to_block_type(block: str) -> BlockType:
|
|||||||
if is_quote(block):
|
if is_quote(block):
|
||||||
return BlockType.QUOTE
|
return BlockType.QUOTE
|
||||||
|
|
||||||
if is_unordered(block):
|
if is_unordered_list(block):
|
||||||
return BlockType.UNORDERED_LIST
|
return BlockType.UNORDERED_LIST
|
||||||
|
|
||||||
if is_ordered(block):
|
if is_ordered_list(block):
|
||||||
return BlockType.ORDERED_LIST
|
return BlockType.ORDERED_LIST
|
||||||
|
|
||||||
return BlockType.PARAGRAPH
|
return BlockType.PARAGRAPH
|
||||||
|
|
||||||
|
|
||||||
def is_heading(block: str) -> bool:
|
def is_heading(block: str) -> bool:
|
||||||
pattern = r"^#{1,6}\s{1}?"
|
pattern = r"^#{1,6}\s{1}?"
|
||||||
return has_pattern(pattern, block)
|
return has_pattern(pattern, block)
|
||||||
|
|
||||||
|
|
||||||
def is_code(block: str) -> bool:
|
def is_code(block: str) -> bool:
|
||||||
pattern = rf"^```.*{os.linesep}[\s\S]*{os.linesep}```$"
|
pattern = r"^```.*\n[\s\S]*\n```$"
|
||||||
return has_pattern(pattern, block)
|
return has_pattern(pattern, block)
|
||||||
|
|
||||||
|
|
||||||
def is_quote(block: str) -> bool:
|
def is_quote(block: str) -> bool:
|
||||||
pattern = r"^>"
|
pattern = r"^>"
|
||||||
return each_line_has_pattern(lambda _: pattern, block)
|
return each_line_has_pattern(lambda _: pattern, block)
|
||||||
|
|
||||||
def is_unordered(block: str) -> bool:
|
|
||||||
|
def is_unordered_list(block: str) -> bool:
|
||||||
pattern = r"^-\s"
|
pattern = r"^-\s"
|
||||||
return each_line_has_pattern(lambda _: pattern, block)
|
return each_line_has_pattern(lambda _: pattern, block)
|
||||||
|
|
||||||
def is_ordered(block: str) -> bool:
|
|
||||||
|
def is_ordered_list(block: str) -> bool:
|
||||||
return each_line_has_pattern(lambda i: rf"{i + 1}\.\s", block)
|
return each_line_has_pattern(lambda i: rf"{i + 1}\.\s", block)
|
||||||
|
|
||||||
|
|
||||||
def has_pattern(pattern: str, block: str) -> bool:
|
def has_pattern(pattern: str, block: str) -> bool:
|
||||||
return re.match(pattern, block) != None
|
return re.match(pattern, block) is not None
|
||||||
|
|
||||||
|
|
||||||
def each_line_has_pattern(pattern_builder: Callable[[int], str], block: str) -> bool:
|
def each_line_has_pattern(pattern_builder: Callable[[int], str], block: str) -> bool:
|
||||||
lines = block.split(os.linesep)
|
lines = block.split("\n")
|
||||||
|
|
||||||
for i in range(len(lines)):
|
for i in range(len(lines)):
|
||||||
line = lines[i]
|
line = lines[i]
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
def extract_markdown_images(text: str) -> list[tuple[str, str]]:
|
def extract_markdown_images(text: str) -> list[tuple[str, str]]:
|
||||||
return extract(r"!\[(.+?)\]\((.+?)\)", text)
|
return extract(r"!\[(.+?)\]\((.+?)\)", text)
|
||||||
|
|
||||||
|
|
||||||
def extract_markdown_links(text: str) -> list[tuple[str, str]]:
|
def extract_markdown_links(text: str) -> list[tuple[str, str]]:
|
||||||
return extract(r"(?<!!)\[(.+?)\]\((.+?)\)", text)
|
return extract(r"(?<!!)\[(.+?)\]\((.+?)\)", text)
|
||||||
|
|
||||||
|
|
||||||
def extract(pattern: str, text: str):
|
def extract(pattern: str, text: str):
|
||||||
return re.findall(pattern, text)
|
return re.findall(pattern, text)
|
||||||
|
|
||||||
@@ -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,
|
tag: str | None = None,
|
||||||
value: str | None = None,
|
value: str | None = None,
|
||||||
children: list["HtmlNode"] | None = None,
|
children: list["HtmlNode"] | None = None,
|
||||||
props: dict | None = None,
|
props: dict[str, str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.tag = tag
|
self.tag = tag
|
||||||
self.value = value
|
self.value = value
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
from htmlnode import HtmlNode
|
from src.nodes.html import HtmlNode
|
||||||
|
|
||||||
|
|
||||||
class LeafNode(HtmlNode):
|
class LeafNode(HtmlNode):
|
||||||
@@ -8,9 +8,9 @@ class LeafNode(HtmlNode):
|
|||||||
self: Self,
|
self: Self,
|
||||||
tag: str | None,
|
tag: str | None,
|
||||||
value: str | None,
|
value: str | None,
|
||||||
props: dict | None = None,
|
props: dict[str, str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(tag, value, [], props)
|
super().__init__(tag, value, None, props)
|
||||||
|
|
||||||
def to_html(self: Self) -> str:
|
def to_html(self: Self) -> str:
|
||||||
if self.value is None:
|
if self.value is None:
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
from htmlnode import HtmlNode
|
from src.nodes.html import HtmlNode
|
||||||
|
|
||||||
|
|
||||||
class ParentNode(HtmlNode):
|
class ParentNode(HtmlNode):
|
||||||
@@ -9,7 +9,7 @@ class ParentNode(HtmlNode):
|
|||||||
tag: str,
|
tag: str,
|
||||||
children: list[HtmlNode],
|
children: list[HtmlNode],
|
||||||
value: str | None = None,
|
value: str | None = None,
|
||||||
props: dict | None = None,
|
props: dict[str, str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(tag, value, children, props)
|
super().__init__(tag, value, children, props)
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from leafnode import LeafNode
|
from src.nodes.leaf import LeafNode
|
||||||
|
|
||||||
|
|
||||||
class TextType(Enum):
|
class TextType(Enum):
|
||||||
@@ -43,8 +43,10 @@ def text_node_to_html_node(text_node: TextNode) -> LeafNode:
|
|||||||
case TextType.CODE:
|
case TextType.CODE:
|
||||||
return LeafNode("code", text_node.text)
|
return LeafNode("code", text_node.text)
|
||||||
case TextType.LINK:
|
case TextType.LINK:
|
||||||
|
assert text_node.url is not None
|
||||||
return LeafNode("a", text_node.text, {"href": text_node.url})
|
return LeafNode("a", text_node.text, {"href": text_node.url})
|
||||||
case TextType.IMAGE:
|
case TextType.IMAGE:
|
||||||
|
assert text_node.url is not None
|
||||||
return LeafNode("img", "", {"src": text_node.url, "alt": text_node.text})
|
return LeafNode("img", "", {"src": text_node.url, "alt": text_node.text})
|
||||||
case _:
|
case _:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
@@ -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
|
import unittest
|
||||||
|
|
||||||
from block import BlockType, block_to_block_type
|
from src.markdown.blocks import BlockType, block_to_block_type
|
||||||
|
|
||||||
|
|
||||||
class TestBlock(unittest.TestCase):
|
class TestBlock(unittest.TestCase):
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from extract import extract_markdown_images, extract_markdown_links
|
from src.markdown.extract import extract_markdown_images, extract_markdown_links
|
||||||
|
|
||||||
|
|
||||||
class TextExtract(unittest.TestCase):
|
class TextExtract(unittest.TestCase):
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from htmlnode import HtmlNode
|
from src.nodes.html import HtmlNode
|
||||||
|
|
||||||
|
|
||||||
class TestHtmlNode(unittest.TestCase):
|
class TestHtmlNode(unittest.TestCase):
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import os
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from splitnodes import (markdown_to_blocks, markdown_to_html_node,
|
from src.markdown.inlines import (
|
||||||
split_nodes_delimiter, split_nodes_image,
|
split_nodes_delimiter,
|
||||||
split_nodes_link, text_to_text_node)
|
split_nodes_image,
|
||||||
from textnode import TextNode, TextType
|
split_nodes_link,
|
||||||
|
text_to_text_node,
|
||||||
|
)
|
||||||
|
from src.markdown.to_html import markdown_to_blocks, markdown_to_html_node
|
||||||
|
from src.nodes.text import TextNode, TextType
|
||||||
|
|
||||||
|
|
||||||
class TestSplitNodes(unittest.TestCase):
|
class TestSplitNodes(unittest.TestCase):
|
||||||
@@ -198,9 +201,4 @@ the **same** even with inline stuff
|
|||||||
node = markdown_to_html_node(md)
|
node = markdown_to_html_node(md)
|
||||||
html = node.to_html()
|
html = node.to_html()
|
||||||
|
|
||||||
with open("output.txt", "w") as file:
|
|
||||||
file.write(expected)
|
|
||||||
file.write("\n\n")
|
|
||||||
file.write(html)
|
|
||||||
|
|
||||||
self.assertEqual(html, expected)
|
self.assertEqual(html, expected)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from leafnode import LeafNode
|
from src.nodes.leaf import LeafNode
|
||||||
|
|
||||||
|
|
||||||
class LeafNodeTest(unittest.TestCase):
|
class LeafNodeTest(unittest.TestCase):
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from htmlnode import HtmlNode
|
from src.nodes.html import HtmlNode
|
||||||
from leafnode import LeafNode
|
from src.nodes.leaf import LeafNode
|
||||||
from parentnode import ParentNode
|
from src.nodes.parent import ParentNode
|
||||||
|
|
||||||
|
|
||||||
class TestParentNode(unittest.TestCase):
|
class TestParentNode(unittest.TestCase):
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from leafnode import LeafNode
|
from src.nodes.leaf import LeafNode
|
||||||
from textnode import TextNode, TextType, text_node_to_html_node
|
from src.nodes.text import TextNode, TextType, text_node_to_html_node
|
||||||
|
|
||||||
|
|
||||||
class TestTextNode(unittest.TestCase):
|
class TestTextNode(unittest.TestCase):
|
||||||
Reference in New Issue
Block a user