199 lines
5.8 KiB
Python
199 lines
5.8 KiB
Python
import html
|
|
import json
|
|
|
|
from anytree import Node
|
|
from anytree.importer import DictImporter
|
|
from anytree.exporter import DictExporter
|
|
from sys import argv
|
|
|
|
|
|
class TableCell:
|
|
def __init__(self, name, rowspan="", colspan="", description="", is_header=False):
|
|
self.name = name
|
|
self.rowspan = rowspan
|
|
self.colspan = colspan
|
|
self.description = description
|
|
self.is_header = is_header
|
|
|
|
def to_html(self):
|
|
if self.is_header:
|
|
output = f"<th title='{html.escape(self.description)}'>{self.name}</th>"
|
|
elif not self.name:
|
|
output = f'<td colspan="{self.colspan}" class="filler"></td>'
|
|
else:
|
|
output = f"<td rowspan='{self.rowspan}' colspan='{self.colspan}' title='{html.escape(self.description)}'>{self.name}</td>"
|
|
|
|
return output
|
|
|
|
|
|
class Recipe:
|
|
def __init__(self, name: str, desc: str, servings: str, prep_time: str, cook_time: str, sub_recipes: list[Node]=[]):
|
|
self.name = name
|
|
self.desc = desc
|
|
self.servings = servings
|
|
self.prep_time = prep_time
|
|
self.cook_time = cook_time
|
|
|
|
self._sub_recipes = sub_recipes
|
|
|
|
def add_sub_recipe(self, tree_root: Node):
|
|
self._sub_recipes.append(tree_root)
|
|
|
|
def generate_ingredient_list(self):
|
|
ingredients = []
|
|
|
|
for sub_recipe in self._sub_recipes:
|
|
for ingredient in sub_recipe.leaves:
|
|
if ingredient.description:
|
|
ingredients.append(f"{ingredient.name} -- {ingredient.description}")
|
|
else:
|
|
ingredients.append(ingredient.name)
|
|
|
|
return ingredients
|
|
|
|
def generate_task_list(self):
|
|
tasks = []
|
|
|
|
for sub_recipe in self._sub_recipes:
|
|
tasks.extend(self.generate_steps_depth_first(sub_recipe, steps=[]))
|
|
|
|
return tasks
|
|
|
|
def generate_steps_depth_first(self, node, steps=[]):
|
|
for child in sorted(node.children, key=lambda x: x.height, reverse=True):
|
|
steps = self.generate_steps_depth_first(child, steps)
|
|
|
|
if not node.is_leaf:
|
|
steps.append(node.description if node.description else node.name)
|
|
|
|
return steps
|
|
|
|
def generate_tables(self):
|
|
tables = []
|
|
|
|
for sub_recipe in self._sub_recipes:
|
|
tables.append(self.build_table_rows_depth_first(sub_recipe))
|
|
|
|
return tables
|
|
|
|
def build_table_rows_depth_first(self, node, rows=None):
|
|
if rows is None:
|
|
rows = []
|
|
|
|
for child in sorted(node.children, key=lambda x: x.height, reverse=True):
|
|
rows = self.build_table_rows_depth_first(child, rows)
|
|
|
|
colspan = 0
|
|
if node.siblings:
|
|
max_sibling_height = max([s.height for s in node.siblings])
|
|
|
|
if max_sibling_height > node.height:
|
|
colspan = max_sibling_height - node.height + 1
|
|
|
|
if node.is_leaf:
|
|
rows.append([TableCell(node.name, description=node.description, is_header=True)])
|
|
|
|
if colspan > 1:
|
|
rows[-1].append(TableCell("", colspan=colspan-1))
|
|
|
|
else:
|
|
child_leaves = len(node.leaves)
|
|
cur_row = len(rows) - child_leaves
|
|
rows[cur_row].append(TableCell(node.name, rowspan=child_leaves, colspan=colspan, description=node.description))
|
|
|
|
return rows
|
|
|
|
@classmethod
|
|
def from_json(cls, json_string: str):
|
|
recipe_dict = json.loads(json_string)
|
|
|
|
name = recipe_dict["name"]
|
|
desc = recipe_dict.get("description")
|
|
servings = recipe_dict.get("servings")
|
|
prep_time = recipe_dict.get("prep_time")
|
|
cook_time = recipe_dict.get("cook_time")
|
|
importer = DictImporter()
|
|
sub_recipes = [importer.import_(data) for data in recipe_dict["sub_recipes"]]
|
|
|
|
return cls(name, desc, servings, prep_time, cook_time, sub_recipes)
|
|
|
|
def to_dict(self):
|
|
sub_recipes_dict = []
|
|
exporter = DictExporter()
|
|
for sub_recipe in self._sub_recipes:
|
|
sub_recipes_dict.append(exporter.export(sub_recipe))
|
|
|
|
return {
|
|
"name": self.name,
|
|
"description": self.desc,
|
|
"servings": self.servings,
|
|
"prep_time": self.prep_time,
|
|
"cook_time": self.cook_time,
|
|
"sub_recipes": sub_recipes_dict
|
|
}
|
|
|
|
def to_json(self):
|
|
return json.dumps(self.to_dict(), indent=2)
|
|
|
|
def to_html(self):
|
|
return f"""<!DOCTYPE html>
|
|
<html lang='en'>
|
|
<head><title>{self.name}</title>
|
|
<link rel='stylesheet' type='text/css' href='style.css'>
|
|
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<nav><a href='./'>Recipe Index</a></nav>
|
|
<h1>{self.name}</h1>
|
|
<p>{self.desc}</p>
|
|
<p>
|
|
{f"<span><strong>Servings: </strong>{self.servings}</span><br>" if self.servings else ""}
|
|
{f"<span><strong>Preparation Time: </strong>{self.prep_time}</span><br>" if self.prep_time else ""}
|
|
{f"<span><strong>Cook Time: </strong>{self.cook_time}</span><br>" if self.cook_time else ""}
|
|
</p>
|
|
</header>
|
|
|
|
<main>
|
|
<div id='needs'>
|
|
<h2>Needs</h2>
|
|
<ul>
|
|
{"\n".join([f" <li>{ingredient}</li>" for ingredient in self.generate_ingredient_list()])}
|
|
</ul>
|
|
</div>
|
|
|
|
<div id='preparation'>
|
|
<h2>Preparation</h2>
|
|
<ol>
|
|
{"\n".join([f" <li>{task}</li>" for task in self.generate_task_list()])}
|
|
</ol>
|
|
</div>
|
|
</main>
|
|
|
|
<summary>
|
|
<h2>Tabular Layout</h2>
|
|
{"\n".join(
|
|
[f"<table cellspacing=0 border=true>\n{"".join(
|
|
[f"<tr>\n{"".join(
|
|
[f" {cell.to_html()}\n" for cell in row]
|
|
)}</tr>\n" for row in table]
|
|
)}</table>" for table in self.generate_tables()]
|
|
)}
|
|
</summary>
|
|
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(argv) < 2:
|
|
print(f"usage: {argv[0]} <json file>")
|
|
exit(1)
|
|
|
|
|
|
with open(argv[1], 'r') as f:
|
|
print(Recipe.from_json(f.read()).to_html())
|
|
|