diff --git a/examples/code_index.py b/examples/code_index.py new file mode 100644 index 0000000..9c957b3 --- /dev/null +++ b/examples/code_index.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +from pathlib import Path +import typing as t + +from anchovy import ( + AnchovyCSSStep, + AssetMinifierStep, + CSSMinifierStep, + DirectCopyStep, + Matcher, + InputBuildSettings, + JinjaExtendedMarkdownStep, + JinjaRenderStep, + OutputDirPathCalc, + PathCalc, + REMatcher, + ResourcePackerStep, + Rule, + Step, + UnpackArchiveStep, + WorkingDirPathCalc, +) +from anchovy.custody import CustodyEntry + +if t.TYPE_CHECKING: + from jinja2 import Environment + + +MARKDOWN_TEMPLATE = """--- +template = "base.jinja.html" +footnote = "Generated from {path} by Anchovy." +--- +# {path} + +```py +{code} +``` +""" + + +class Code2MarkdownStep(Step): + encoding = 'utf-8' + newline = '\n' + template = MARKDOWN_TEMPLATE + def __call__(self, path: Path, output_paths: list[Path]): + code = path.read_text(self.encoding) + processed = self.template.format(path=path.name, code=code) + for target_path in output_paths: + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_text(processed, self.encoding, newline=self.newline) + + +class CodeIndexStep(JinjaRenderStep): + encoding = 'utf-8' + + def __init__(self, + leaf_matcher: Matcher, + leaf_calc: PathCalc, + env: Environment | None = None, + extra_globals: dict[str, t.Any] | None = None): + super().__init__(env, extra_globals) + self.leaf_matcher = leaf_matcher + self.leaf_calc = leaf_calc + + @classmethod + def get_dependencies(cls): + return super().get_dependencies() + + def __call__(self, path: Path, output_paths: list[Path]): + matched_paths: list[Path] = [path] + leaves: list[tuple[Path, Path]] = [] + for sibling in path.parent.iterdir(): + print('sibling:', sibling) + if sibling == path or sibling.is_dir(): + continue + print('sibling matched thus') + if match := self.leaf_matcher(self.context, sibling): + print('sibling matched', match) + matched_paths.append(sibling) + leaves.append(( + sibling, + self.leaf_calc(self.context, sibling, match).relative_to(self.context['output_dir']), + )) + + print('leaves:', leaves) + self.render_template( + path.relative_to(self.context['working_dir']).as_posix(), + {'leaves': leaves}, + output_paths + ) + return matched_paths, output_paths + + +# Optional, and can be overridden with CLI arguments. +SETTINGS = InputBuildSettings( + input_dir=Path(__file__).parent / 'code_index', + working_dir=Path('working/code_index'), + output_dir=Path('output/code_index'), + custody_cache=Path('output/code_index.json'), +) +RULES = [ + # Ignore dotfiles found in either the input_dir or the working dir. + Rule( + ( + REMatcher(r'(.*/)*\..*', parent_dir='input_dir') + | REMatcher(r'(.*/)*\..*', parent_dir='working_dir') + ), + None + ), + # Unpack archives. + Rule( + REMatcher(r'.*\.zip'), + [WorkingDirPathCalc(transform=lambda p: p.parent), None], + UnpackArchiveStep() + ), + # Embed Python files in Markdown. + Rule( + REMatcher(r'.*\.py'), + [WorkingDirPathCalc('.md')], + Code2MarkdownStep() + ), + # Render markdown files and stop processing. + Rule( + REMatcher(r'.*\.md'), + [WorkingDirPathCalc('.html'), None], + JinjaExtendedMarkdownStep( + default_template='base.j.html', + pygments_params={'classprefix': 'pyg-'}, + ) + ), + # Render Jinja indices through working dir (so there's time for the archive + # to unpack) and stop processing. + Rule( + REMatcher(r'index\.jinja\.html', parent_dir='input_dir'), + [WorkingDirPathCalc(), None], + DirectCopyStep() + ), + Rule( + REMatcher(r'index(?P\.jinja\.html)', parent_dir='working_dir'), + [WorkingDirPathCalc('.html'), None], + CodeIndexStep( + REMatcher(r'.*\.py'), + OutputDirPathCalc('.html') + ) + ), + # Ignore all other Jinja templates. + Rule(REMatcher(r'.*\.jinja\.html'), None), + # Preprocess Anchovy CSS and stop processing. + Rule( + REMatcher(r'.*(?P\.anchovy\.css)'), + [WorkingDirPathCalc('.css'), None], + AnchovyCSSStep() + ), + # Pack CSS. Have to wait for working dir so Anchovy CSS processing is done. + Rule( + REMatcher(r'.*/css_pack.txt', parent_dir='input_dir'), + [WorkingDirPathCalc(), None], + DirectCopyStep() + ), + Rule( + REMatcher(r'.*/css_pack.txt', parent_dir='working_dir'), + [WorkingDirPathCalc('.css'), None], + ResourcePackerStep('working_dir') + ), + # Minify packed CSS and stop processing. + Rule( + REMatcher(r'.*/css_pack\.css', parent_dir='working_dir'), + [OutputDirPathCalc(), None], + CSSMinifierStep() + ), + # Minify HTML/JS and stop processing. + Rule( + REMatcher(r'.*\.(html|js)', parent_dir='working_dir'), + [OutputDirPathCalc(), None], + AssetMinifierStep() + ), +] diff --git a/examples/code_index/base.jinja.html b/examples/code_index/base.jinja.html new file mode 100644 index 0000000..a9f561b --- /dev/null +++ b/examples/code_index/base.jinja.html @@ -0,0 +1,16 @@ + + + + + + + {% block content %} +
+ {{ rendered_markdown | safe }} +
+ {% endblock %} +
+ {{ footnote }} +
+ + diff --git a/examples/code_index/code.zip b/examples/code_index/code.zip new file mode 100644 index 0000000..ffb08e8 Binary files /dev/null and b/examples/code_index/code.zip differ diff --git a/examples/code_index/index.jinja.html b/examples/code_index/index.jinja.html new file mode 100644 index 0000000..f2115e7 --- /dev/null +++ b/examples/code_index/index.jinja.html @@ -0,0 +1,10 @@ +{% set footnote = "An index of Anchovy example configuration code." %} +{% extends "base.jinja.html" %} +{% block content %} +
+

Examples

+ {% for source, target in leaves %} +

{{ source.name }}

+ {% endfor %} +
+{% endblock %} diff --git a/examples/code_index/static/core.anchovy.css b/examples/code_index/static/core.anchovy.css new file mode 100644 index 0000000..a28b42b --- /dev/null +++ b/examples/code_index/static/core.anchovy.css @@ -0,0 +1,28 @@ +body { + font-family: 'Arial Narrow', sans-serif; + color: #eee; + background-color: #333; + padding: 10px; +} + +main, footer { + margin: 0 auto; + max-width: 800px; +} + +footer { + border-top: 1px #eee dotted; + padding-top: 10px; +} + +.highlight { + padding: 10px; + border-radius: 5px; +} + +a { + color: #eee; + :hover { + color: #aaf; + } +} diff --git a/examples/code_index/static/css_pack.txt b/examples/code_index/static/css_pack.txt new file mode 100644 index 0000000..7b87636 --- /dev/null +++ b/examples/code_index/static/css_pack.txt @@ -0,0 +1,2 @@ +static/core.css +static/pygments.css diff --git a/examples/code_index/static/pygments.anchovy.css b/examples/code_index/static/pygments.anchovy.css new file mode 100644 index 0000000..1915618 --- /dev/null +++ b/examples/code_index/static/pygments.anchovy.css @@ -0,0 +1,85 @@ +.highlight { + line-height: 1.2em; + background: #272822; color: #f8f8f2; + span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } + .linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } + .hll { background-color: #49483e } + .pyg-c { color: #959077 } /* Comment */ + .pyg-err { color: #ed007e; background-color: #1e0010 } /* Error */ + .pyg-esc { color: #f8f8f2 } /* Escape */ + .pyg-g { color: #f8f8f2 } /* Generic */ + .pyg-k { color: #66d9ef } /* Keyword */ + .pyg-l { color: #ae81ff } /* Literal */ + .pyg-n { color: #f8f8f2 } /* Name */ + .pyg-o { color: #ff4689 } /* Operator */ + .pyg-x { color: #f8f8f2 } /* Other */ + .pyg-p { color: #f8f8f2 } /* Punctuation */ + .pyg-ch { color: #959077 } /* Comment.Hashbang */ + .pyg-cm { color: #959077 } /* Comment.Multiline */ + .pyg-cp { color: #959077 } /* Comment.Preproc */ + .pyg-cpf { color: #959077 } /* Comment.PreprocFile */ + .pyg-c1 { color: #959077 } /* Comment.Single */ + .pyg-cs { color: #959077 } /* Comment.Special */ + .pyg-gd { color: #ff4689 } /* Generic.Deleted */ + .pyg-ge { color: #f8f8f2; font-style: italic } /* Generic.Emph */ + .pyg-ges { color: #f8f8f2; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ + .pyg-gr { color: #f8f8f2 } /* Generic.Error */ + .pyg-gh { color: #f8f8f2 } /* Generic.Heading */ + .pyg-gi { color: #a6e22e } /* Generic.Inserted */ + .pyg-go { color: #66d9ef } /* Generic.Output */ + .pyg-gp { color: #ff4689; font-weight: bold } /* Generic.Prompt */ + .pyg-gs { color: #f8f8f2; font-weight: bold } /* Generic.Strong */ + .pyg-gu { color: #959077 } /* Generic.Subheading */ + .pyg-gt { color: #f8f8f2 } /* Generic.Traceback */ + .pyg-kc { color: #66d9ef } /* Keyword.Constant */ + .pyg-kd { color: #66d9ef } /* Keyword.Declaration */ + .pyg-kn { color: #ff4689 } /* Keyword.Namespace */ + .pyg-kp { color: #66d9ef } /* Keyword.Pseudo */ + .pyg-kr { color: #66d9ef } /* Keyword.Reserved */ + .pyg-kt { color: #66d9ef } /* Keyword.Type */ + .pyg-ld { color: #e6db74 } /* Literal.Date */ + .pyg-m { color: #ae81ff } /* Literal.Number */ + .pyg-s { color: #e6db74 } /* Literal.String */ + .pyg-na { color: #a6e22e } /* Name.Attribute */ + .pyg-nb { color: #f8f8f2 } /* Name.Builtin */ + .pyg-nc { color: #a6e22e } /* Name.Class */ + .pyg-no { color: #66d9ef } /* Name.Constant */ + .pyg-nd { color: #a6e22e } /* Name.Decorator */ + .pyg-ni { color: #f8f8f2 } /* Name.Entity */ + .pyg-ne { color: #a6e22e } /* Name.Exception */ + .pyg-nf { color: #a6e22e } /* Name.Function */ + .pyg-nl { color: #f8f8f2 } /* Name.Label */ + .pyg-nn { color: #f8f8f2 } /* Name.Namespace */ + .pyg-nx { color: #a6e22e } /* Name.Other */ + .pyg-py { color: #f8f8f2 } /* Name.Property */ + .pyg-nt { color: #ff4689 } /* Name.Tag */ + .pyg-nv { color: #f8f8f2 } /* Name.Variable */ + .pyg-ow { color: #ff4689 } /* Operator.Word */ + .pyg-pm { color: #f8f8f2 } /* Punctuation.Marker */ + .pyg-w { color: #f8f8f2 } /* Text.Whitespace */ + .pyg-mb { color: #ae81ff } /* Literal.Number.Bin */ + .pyg-mf { color: #ae81ff } /* Literal.Number.Float */ + .pyg-mh { color: #ae81ff } /* Literal.Number.Hex */ + .pyg-mi { color: #ae81ff } /* Literal.Number.Integer */ + .pyg-mo { color: #ae81ff } /* Literal.Number.Oct */ + .pyg-sa { color: #e6db74 } /* Literal.String.Affix */ + .pyg-sb { color: #e6db74 } /* Literal.String.Backtick */ + .pyg-sc { color: #e6db74 } /* Literal.String.Char */ + .pyg-dl { color: #e6db74 } /* Literal.String.Delimiter */ + .pyg-sd { color: #e6db74 } /* Literal.String.Doc */ + .pyg-s2 { color: #e6db74 } /* Literal.String.Double */ + .pyg-se { color: #ae81ff } /* Literal.String.Escape */ + .pyg-sh { color: #e6db74 } /* Literal.String.Heredoc */ + .pyg-si { color: #e6db74 } /* Literal.String.Interpol */ + .pyg-sx { color: #e6db74 } /* Literal.String.Other */ + .pyg-sr { color: #e6db74 } /* Literal.String.Regex */ + .pyg-s1 { color: #e6db74 } /* Literal.String.Single */ + .pyg-ss { color: #e6db74 } /* Literal.String.Symbol */ + .pyg-bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ + .pyg-fm { color: #a6e22e } /* Name.Function.Magic */ + .pyg-vc { color: #f8f8f2 } /* Name.Variable.Class */ + .pyg-vg { color: #f8f8f2 } /* Name.Variable.Global */ + .pyg-vi { color: #f8f8f2 } /* Name.Variable.Instance */ + .pyg-vm { color: #f8f8f2 } /* Name.Variable.Magic */ + .pyg-il { color: #ae81ff } /* Literal.Number.Integer.Long */ +} diff --git a/test/artifacts/code_index.json b/test/artifacts/code_index.json new file mode 100644 index 0000000..2d8f3f9 --- /dev/null +++ b/test/artifacts/code_index.json @@ -0,0 +1,292 @@ +{ + "info": { + "input_dir": "examples\\code_index", + "output_dir": "output\\code_index", + "working_dir": "working\\code_index", + "custody_cache": "output\\code_index.json", + "purge_dirs": "True" + }, + "parameters": { + "anchovy_version": "0.12.1.dev3+g0b211d8.d20230817" + }, + "graph": { + "working_dir/basic_site.py": { + "input_dir/code.zip": [ + "working_dir/basic_site.py", + "working_dir/gallery.py" + ] + }, + "working_dir/gallery.py": { + "input_dir/code.zip": [ + "working_dir/basic_site.py", + "working_dir/gallery.py" + ] + }, + "working_dir/index.jinja.html": { + "input_dir/index.jinja.html": [ + "working_dir/index.jinja.html" + ] + }, + "working_dir/static/core.css": { + "input_dir/static/core.anchovy.css": [ + "working_dir/static/core.css" + ] + }, + "working_dir/static/pygments.css": { + "input_dir/static/pygments.anchovy.css": [ + "working_dir/static/pygments.css" + ] + }, + "working_dir/static/css_pack.txt": { + "input_dir/static/css_pack.txt": [ + "working_dir/static/css_pack.txt" + ] + }, + "working_dir/basic_site.md": { + "working_dir/basic_site.py": [ + "working_dir/basic_site.md" + ] + }, + "working_dir/gallery.md": { + "working_dir/gallery.py": [ + "working_dir/gallery.md" + ] + }, + "working_dir/index.html": { + "working_dir/index.jinja.html": [ + "working_dir/index.html" + ], + "working_dir/basic_site.py": [ + "working_dir/index.html" + ], + "working_dir/gallery.py": [ + "working_dir/index.html" + ] + }, + "working_dir/static/css_pack.css": { + "working_dir/static/css_pack.txt": [ + "working_dir/static/css_pack.css" + ], + "working_dir/static/core.css": [ + "working_dir/static/css_pack.css" + ], + "working_dir/static/pygments.css": [ + "working_dir/static/css_pack.css" + ] + }, + "working_dir/basic_site.html": { + "working_dir/basic_site.md": [ + "working_dir/basic_site.html" + ], + "input_dir/base.jinja.html": [ + "working_dir/basic_site.html" + ] + }, + "working_dir/gallery.html": { + "working_dir/gallery.md": [ + "working_dir/gallery.html" + ], + "input_dir/base.jinja.html": [ + "working_dir/gallery.html" + ] + }, + "output_dir/static/css_pack.css": { + "working_dir/static/css_pack.css": [ + "output_dir/static/css_pack.css" + ] + }, + "output_dir/index.html": { + "working_dir/index.html": [ + "output_dir/index.html" + ] + }, + "output_dir/basic_site.html": { + "working_dir/basic_site.html": [ + "output_dir/basic_site.html" + ] + }, + "output_dir/gallery.html": { + "working_dir/gallery.html": [ + "output_dir/gallery.html" + ] + } + }, + "meta": { + "working_dir/basic_site.py": [ + "path", + { + "sha1": "c4983436e017a238d94796250a749d74cbc76835", + "m_time": 1699590586.2190921, + "size": 1071 + } + ], + "working_dir/gallery.py": [ + "path", + { + "sha1": "3cdd5e3cc41664211b63f98b94a0791282f1ea4d", + "m_time": 1699590586.2200894, + "size": 1407 + } + ], + "input_dir/code.zip": [ + "path", + { + "sha1": "212d13b73c1b6579519c38b019eae828358cc98d", + "m_time": 1699583562.867972, + "size": 1233 + } + ], + "working_dir/index.jinja.html": [ + "path", + { + "sha1": "c361ccf60628e76f571f8229d9c896603f7042ab", + "m_time": 1699590586.232628, + "size": 298 + } + ], + "input_dir/index.jinja.html": [ + "path", + { + "sha1": "c361ccf60628e76f571f8229d9c896603f7042ab", + "m_time": 1699590558.3379586, + "size": 298 + } + ], + "working_dir/static/core.css": [ + "path", + { + "sha1": "23b855543fd50b49b743413cc221e1bee266c6c9", + "m_time": 1699590586.2366588, + "size": 306 + } + ], + "input_dir/static/core.anchovy.css": [ + "path", + { + "sha1": "0c537d96df6aa891be2ba27af58975eb8fff9e2b", + "m_time": 1699588391.7623892, + "size": 369 + } + ], + "working_dir/static/pygments.css": [ + "path", + { + "sha1": "16d39823b42dfc58eb0b12d04cc54c233575f398", + "m_time": 1699590586.241645, + "size": 5551 + } + ], + "input_dir/static/pygments.anchovy.css": [ + "path", + { + "sha1": "e81fa6d29a89ef0f69f06e235b0ce9b9e1f593be", + "m_time": 1699584264.714627, + "size": 4606 + } + ], + "working_dir/static/css_pack.txt": [ + "path", + { + "sha1": "0cc6ab8a6e988e4f3ff88a30dca17f4b8087a37b", + "m_time": 1699590586.2446487, + "size": 36 + } + ], + "input_dir/static/css_pack.txt": [ + "path", + { + "sha1": "0cc6ab8a6e988e4f3ff88a30dca17f4b8087a37b", + "m_time": 1699587279.0891676, + "size": 36 + } + ], + "working_dir/basic_site.md": [ + "path", + { + "sha1": "016d62d4ad6398e04173910a39c330fbcf4f7e9e", + "m_time": 1699590586.2566335, + "size": 1190 + } + ], + "working_dir/gallery.md": [ + "path", + { + "sha1": "de520a2e79c3312bbcbbc746f6f1aabdc9dce919", + "m_time": 1699590586.2696455, + "size": 1520 + } + ], + "working_dir/index.html": [ + "path", + { + "sha1": "e653663027dfc58550559eaa9d2ba816dd94b8e0", + "m_time": 1699590586.2947295, + "size": 427 + } + ], + "working_dir/static/css_pack.css": [ + "path", + { + "sha1": "5620127ab0d2261762dac0c30d1fe79e86dcac03", + "m_time": 1699590586.3048718, + "size": 5859 + } + ], + "working_dir/basic_site.html": [ + "path", + { + "sha1": "ffe41a786d2d6e21e277aa1db53db621384aa76f", + "m_time": 1699590586.3675473, + "size": 4852 + } + ], + "input_dir/base.jinja.html": [ + "path", + { + "sha1": "cd1fad850af896b28d5cd43b64837e1360a9b0a9", + "m_time": 1699587994.624042, + "size": 349 + } + ], + "working_dir/gallery.html": [ + "path", + { + "sha1": "607b78c4b55e9b7598d74c7dae26847536932758", + "m_time": 1699590586.3774774, + "size": 6386 + } + ], + "output_dir/static/css_pack.css": [ + "path", + { + "sha1": "a8253d222c06b0cf4391d9b95471bc67d4033663", + "m_time": 1699590586.3884788, + "size": 2794 + } + ], + "output_dir/index.html": [ + "path", + { + "sha1": "239ba6c1e7802f77005bcef7ce3eae4ef59df4ae", + "m_time": 1699590586.421747, + "size": 263 + } + ], + "output_dir/basic_site.html": [ + "path", + { + "sha1": "f217f536a707d91b90b6d50ab94fb5e9c310a5f2", + "m_time": 1699590586.451815, + "size": 4439 + } + ], + "output_dir/gallery.html": [ + "path", + { + "sha1": "ddac506b836ee7fffaed7f1dad18967bd11acf0e", + "m_time": 1699590586.4674623, + "size": 5887 + } + ] + } +} diff --git a/test/test_examples.py b/test/test_examples.py index f218acf..8ce997f 100644 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -48,6 +48,7 @@ def compare_artifacts(old: dict, new: dict): @pytest.mark.parametrize('name', [ 'basic_site', 'gallery', + 'code_index', ]) def test_example(name, tmp_path): module_items = load_example(name)