def test_from_unscaled_recipe_page(self, tmp_path: Path, link: str, exp_link: str) -> None: filename_to_asset_paths: MutableMapping[str, str] = {} assert (self.run( t("a", "Link", href=link), tmp_path=tmp_path, source=tmp_path / "foo" / "bar" / "unscaled.md", from_path="/categories/bar/unscaled.html", filename_to_asset_paths=filename_to_asset_paths, ) == t("a", "Link", href=exp_link)) assert filename_to_asset_paths == {}
def test_valid(self, tmp_path: Path) -> None: (tmp_path / "bar.txt").open("w").write("foobar") assert (postprocess_html( t("a", "foo", href="bar.txt"), False, [ partial( embed_local_links_as_data_urls, source=tmp_path / "foo.md", root=tmp_path, ), ], ) == t("a", "foo", href="data:text/plain;base64,Zm9vYmFy"))
def test_from_scaled_category_page( self, tmp_path: Path, link: str, exp_link: str, ) -> None: filename_to_asset_paths: MutableMapping[str, str] = {} assert (self.run( t("a", "Link", href=link), tmp_path=tmp_path, source=tmp_path / "foo" / "bar" / "README.md", from_path="/serves123/bar/index.html", filename_to_asset_paths=filename_to_asset_paths, ) == t("a", "Link", href=exp_link)) assert filename_to_asset_paths == {}
def visit_pending_recipe_node(self: HTMLTranslator, node: nodes.Node) -> None: self.body.append( t( "div", "\n".join( render_recipe_tree(recipe_tree, node.id_prefix) for recipe_tree in node.recipe.scale(node.scale).recipe_trees), class_="rg-recipe-block", ))
def test_resolve_local_links_stage( self, input_path: Path, make_directory: MakeDirectoryFn ) -> None: h = HomePage.from_root_directory( make_directory({"foo/recipe.md": "# Recipe for 2", "foo/file.txt": "..."}), ) filename_to_asset_paths: MutableMapping[Path, str] = {} stage = ( h.scaled_categories[3] .subcategories[0] .recipes[0] .get_resolve_local_links_stage( source=input_path / "foo" / "recipe.md", source_to_page_paths={ input_path / "README.md": ("/categories/index.html", True), input_path / "foo" / "recipe.md": ("/serves2/foo/recipe.html", True), }, filename_to_asset_paths=filename_to_asset_paths, ) ) for href, exp_href in [ # Check directory is correct ("recipe.md", "recipe.html"), # Check path is right (i.e. uses same number of servings ("../README.md", "../index.html"), # Check asset paths updated and asset dir is correct ("file.txt", "../../assets/foo/file.txt"), ]: assert postprocess_html(t("a", "foo", href=href), False, [stage]) == t( "a", "foo", href=exp_href ) assert filename_to_asset_paths == { input_path / "foo" / "file.txt": "/assets/foo/file.txt", } # Check root is correctly set with pytest.raises(LinkToExternalFileError): postprocess_html(t("a", "foo", href="../../../foo.bar"), False, [stage])
def test_any_page_cases( self, tmp_path: Path, link: str, exp_link: str, exp_filename_to_asset_paths: Mapping[str, str], source: Path, from_path: str, ) -> None: filename_to_asset_paths: MutableMapping[str, str] = {} assert (self.run( t("a", "Link", href=link), tmp_path=tmp_path, source=tmp_path / source, from_path=from_path, filename_to_asset_paths=filename_to_asset_paths, ) == t("a", "Link", href=exp_link)) assert filename_to_asset_paths == { tmp_path / file: path for file, path in exp_filename_to_asset_paths.items() }
def test_path_does_not_exist(self, tmp_path: Path) -> None: with pytest.raises(LinkToNonExistentFileError): postprocess_html( t("a", "foo", href="bar.txt"), False, [ partial( embed_local_links_as_data_urls, source=tmp_path / "foo.md", root=tmp_path, ), ], )
def test_path_outside_root(self, tmp_path: Path) -> None: with pytest.raises(LinkToExternalFileError): postprocess_html( t("a", "foo", href="../bar.txt"), False, [ partial( embed_local_links_as_data_urls, source=tmp_path / "foo.md", root=tmp_path, ), ], )
def test_ignore_external_or_page_local_links(self, tmp_path: Path, url: str) -> None: html = t("a", "foo", href=url) assert (postprocess_html( html, False, [ partial( embed_local_links_as_data_urls, source=tmp_path / "foo.md", root=tmp_path, ), ], ) == html)
def render_heading(self, element: block.Heading) -> str: """ Render headings, also capturing the first heading if it contains a serving count. """ level = element.level text = self.render_children(element) # type: ignore pre = "" # To prepend before <h*> tag post = "" # To append after </h*> tag attrs = "" # Attributes to be inserted into the <h*> tag. # Capture the first title as recipe title. Ignore it if it isn't a H1 # level title or if it contains HTML or a scaled value substitution # since we can't easily turn these into a plain text title. if self.first_heading and level == 1 and "<" not in text and "%" not in text: match = self.title_serving_count_pattern.search(text) if match is None: self.output.title = html.unescape(text.strip()) attrs = ' class="rg-title-unscalable"' else: title = text[: match.start()] self.output.title = html.unescape(title.strip()) self.output.servings = int(match["servings"]) placeholder = generate_placeholder() self.output.scaled_value_strings[placeholder] = SVS( int(match["servings"]) ) text = title + t( "span", match["preposition"] + placeholder, class_="rg-serving-count", ) attrs = ' class="rg-title-scalable"' pre = self.output.pre_title_placeholder = generate_placeholder() post = self.output.post_title_placeholder = generate_placeholder() self.first_heading = False return f"{pre}<h{level}{attrs}>{text}</h{level}>{post}\n"
def test_escape_attr_values(self) -> None: assert (t( "foo", bar="in 'quotes\" here") == '<foo bar="in \'quotes" here"/>')
def test_multi_line_body(self) -> None: assert t("foo", "bar\nbaz") == "<foo>\n bar\n baz\n</foo>"
def test_single_line_body_with_trailing_newline(self) -> None: assert t("foo", "bar\n") == "<foo>\n bar\n</foo>"
def test_single_line_body(self) -> None: assert t("foo", "bar") == "<foo>bar</foo>"
def test_with_body_and_attrs(self) -> None: assert t("foo", "", bar="baz") == '<foo bar="baz"></foo>' assert t("foo", "", bar="baz", quo="qux") == '<foo bar="baz" quo="qux"></foo>'
def test_empty_body(self) -> None: assert t("foo", "") == "<foo></foo>"
def test_dunderscore_to_hyphen_substitution(self) -> None: assert t("foo", data__foo="bar") == '<foo data-foo="bar"/>'
def test_underscore_postfix_removal(self) -> None: assert t("foo", class_="baz") == '<foo class="baz"/>'
def test_no_child_with_attrs(self) -> None: assert t("foo", bar="baz") == '<foo bar="baz"/>' assert t("foo", bar="baz", quo="qux") == '<foo bar="baz" quo="qux"/>'
def test_minimal_tag(self) -> None: assert t("foo") == "<foo />"
def render(self, scale: Union[int, float, Fraction] = 1) -> str: """ Render this recipe scaled by a given factor. """ html = self.html # Substitute scaled value strings for placeholder, svs in self.scaled_value_strings.items(): html = html.replace( placeholder, render_scaled_value_string(svs.scale(scale)), ) # Substitute scaled recipes id_prefix_index = 0 for placeholder, recipe in self.recipe_placeholders.items(): if recipe.follows is None: id_prefix_index += 1 if id_prefix_index > 1: id_prefix = f"recipe{id_prefix_index}-" else: id_prefix = "recipe-" html = html.replace( placeholder, t( "div", "\n".join( render_recipe_tree(recipe_tree, id_prefix) for recipe_tree in recipe.scale(scale).recipe_trees ), class_="rg-recipe-block", ), ) # Modify heading to include scaling info if ( self.title is not None and self.pre_title_placeholder is not None and self.post_title_placeholder is not None ): html = html.replace(self.pre_title_placeholder, "<header>") post_title_text = "" if scale != 1: if self.servings is not None: orig_servings = t( "span", f"{self.servings} serving{'s' if self.servings != 1 else ''}", class_="rg-original-servings", ) post_title_text = t("p", f"Rescaled from {orig_servings}.") else: scale_str = t( "span", f"{render_number(scale)}×", class_="rg-scaling-factor", ) post_title_text = t("p", f"Scaled {scale_str}") html = html.replace( self.post_title_placeholder, f"{post_title_text}</header>" ) return html