diff --git a/.gitignore b/.gitignore index 95ab1bd..edf4096 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *~ \#*\# .\#* +examples/*/*/public # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..1bf3cdb --- /dev/null +++ b/examples/README.md @@ -0,0 +1,17 @@ +# Examples + +This directory contains runnable `httk-web` examples. + +- `modern/`: recommended v2-first examples (Jinja2 templates, modern metadata). +- `legacy/`: compatibility-mode examples that use deprecated legacy syntax/features. + +Modern includes: +- `minimal/` +- `rst_site/` (modern replacement for `rst_templator`) +- `blog/` (modern replacement for legacy blog example) +- `search_app/` (modern replacement for legacy search example) + +Legacy now includes migrated variants of the original `6_website` examples, +including blog/search-style examples. + +Legacy examples are kept to validate migration behavior and should not be used as the default style for new projects. diff --git a/examples/legacy/README.md b/examples/legacy/README.md new file mode 100644 index 0000000..9624a04 --- /dev/null +++ b/examples/legacy/README.md @@ -0,0 +1,14 @@ +# Legacy Examples + +These examples intentionally use deprecated legacy features and require compatibility mode. + +- `static_simple/` +- `hello_world_app/` +- `rst_templator/` +- `blog/` +- `search_app/` + +Use the provided `serve_legacy_*.py` and `publish_legacy_*.py` scripts. + +`search_app/` is included as a migrated legacy example, but it depends on legacy +`httk` database modules that are not part of `httk-web`. diff --git a/examples/legacy/blog/publish_legacy_blog.py b/examples/legacy/blog/publish_legacy_blog.py new file mode 100644 index 0000000..c654cfc --- /dev/null +++ b/examples/legacy/blog/publish_legacy_blog.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import publish + +ROOT = Path(__file__).parent +publish(ROOT / "src", ROOT / "public", "http://127.0.0.1/", compatibility_mode=True) diff --git a/examples/legacy/blog/serve_legacy_blog.py b/examples/legacy/blog/serve_legacy_blog.py new file mode 100644 index 0000000..e3f984f --- /dev/null +++ b/examples/legacy/blog/serve_legacy_blog.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import serve + +ROOT = Path(__file__).parent +serve(ROOT / "src", port=8080, compatibility_mode=True, config_name="config_dynamic") diff --git a/examples/legacy/blog/src/config.httkweb b/examples/legacy/blog/src/config.httkweb new file mode 100644 index 0000000..7a7c446 --- /dev/null +++ b/examples/legacy/blog/src/config.httkweb @@ -0,0 +1,4 @@ +--- +maintitle: My formidable website +menuitems-list: index, blog, contact +--- diff --git a/examples/legacy/blog/src/config_dynamic.httkweb b/examples/legacy/blog/src/config_dynamic.httkweb new file mode 100644 index 0000000..6ace46b --- /dev/null +++ b/examples/legacy/blog/src/config_dynamic.httkweb @@ -0,0 +1,5 @@ +--- +maintitle: My formidable website +menuitems-list: index, blog, contact +urls_without_ext: true +--- diff --git a/examples/legacy/blog/src/content/404.md b/examples/legacy/blog/src/content/404.md new file mode 100644 index 0000000..313b364 --- /dev/null +++ b/examples/legacy/blog/src/content/404.md @@ -0,0 +1,6 @@ +--- +Title: My Website - Missing page +Name: "404" +Template: "404" +--- +The page could not be found. diff --git a/examples/legacy/blog/src/content/blog.md b/examples/legacy/blog/src/content/blog.md new file mode 100644 index 0000000..3bd0295 --- /dev/null +++ b/examples/legacy/blog/src/content/blog.md @@ -0,0 +1,12 @@ +-------- +Title: My Website - Blog posts +Name: Blog +Template: blog +Base_template: base_default +-------- + +Blog +==== + +Below is a list of all blog posts available here. + diff --git a/examples/legacy/blog/src/content/blogposts/another_post.md b/examples/legacy/blog/src/content/blogposts/another_post.md new file mode 100644 index 0000000..f8acc9e --- /dev/null +++ b/examples/legacy/blog/src/content/blogposts/another_post.md @@ -0,0 +1,13 @@ +-------- +Title: My Website - Another post +Author: Rickard Armiento +Date: 2023-10-04 +Name: Another post +Template: blogpost +Base_template: base_default +-------- + +Another post +============ + +This is another post diff --git a/examples/legacy/blog/src/content/blogposts/hello-everyone.md b/examples/legacy/blog/src/content/blogposts/hello-everyone.md new file mode 100644 index 0000000..a4608d5 --- /dev/null +++ b/examples/legacy/blog/src/content/blogposts/hello-everyone.md @@ -0,0 +1,13 @@ +-------- +Title: My Website - Hello Everyone +Author: Rickard Armiento +Date: 2023-10-03 +Name: Hello Everyone +Template: blogpost +Base_template: base_default +-------- + +Hello everyone +============== + +Hello everyone, this is the first blog post. diff --git a/examples/legacy/blog/src/content/blogposts/third_post.md b/examples/legacy/blog/src/content/blogposts/third_post.md new file mode 100644 index 0000000..7b424b5 --- /dev/null +++ b/examples/legacy/blog/src/content/blogposts/third_post.md @@ -0,0 +1,13 @@ +-------- +Title: My Website - Third post +Author: Rickard Armiento +Date: 2023-10-05 +Name: Third post +Template: blogpost +Base_template: base_default +-------- + +Third post +========== + +This is the third post diff --git a/examples/legacy/blog/src/content/contact.md b/examples/legacy/blog/src/content/contact.md new file mode 100644 index 0000000..74eef13 --- /dev/null +++ b/examples/legacy/blog/src/content/contact.md @@ -0,0 +1,12 @@ +------- +Title: My Website - Contact +Name: Contact +Template: default +Base_template: base_default +------ + +Contact +======= + +Contact me here: someone@example.com + diff --git a/examples/legacy/blog/src/content/index.md b/examples/legacy/blog/src/content/index.md new file mode 100644 index 0000000..702570e --- /dev/null +++ b/examples/legacy/blog/src/content/index.md @@ -0,0 +1,33 @@ +----------- +Title: My Website +Name: Main +Date: 2023-09-27 +Version: 1 +Author: Rickard Armiento +Template: default +Base_template: base_default +----------- + +My website +========== + +This is my formidable website. It contains the following sections: + +* [Blog](blog) +* [Contact](contact) + +Subheading +---------- + +Here is some nice math: + +\(\int (x+y) dx\) + +And a code segment: + +``` python +x = 1 +if x == 1: + # indented four spaces + print("x is 1.") +``` diff --git a/examples/legacy/blog/src/functions/init.py b/examples/legacy/blog/src/functions/init.py new file mode 100644 index 0000000..3fb9ea3 --- /dev/null +++ b/examples/legacy/blog/src/functions/init.py @@ -0,0 +1,39 @@ +import datetime +import os +from pathlib import Path + + +def execute(global_data, **kargs): + prefix = Path(__file__).resolve().parents[1] / "content" + filterlist = [".md", ".rst", ".html"] + path = "blogposts" + + # Chicken-or-egg problem of having to partially render blog posts to sort them, but their renders refer to the other blogposts + global_data['blogposts'] = [] + global_data['blogposts_latest'] = [] + + def listdirsorted(path): + return [ + x[0] + for x in sorted( + [ + ( + fn, + datetime.datetime.strptime( + str(global_data['pages'](os.path.join(path, fn), 'date')), "%Y-%m-%d" + ), + ) + for fn in os.listdir(prefix / path) + ], + key=lambda x: x[1], + reverse=True, + ) + ] + + global_data['blogposts'] = [ + os.path.join(path, f) + for f in listdirsorted(path) + if os.path.isfile(prefix / path / f) and any([f.endswith(t) for t in filterlist]) + ] + + global_data['blogposts_latest'] = global_data['blogposts'][:5] diff --git a/examples/legacy/blog/src/static/favicon.ico b/examples/legacy/blog/src/static/favicon.ico new file mode 100644 index 0000000..89273a9 Binary files /dev/null and b/examples/legacy/blog/src/static/favicon.ico differ diff --git a/examples/dummy.py b/examples/legacy/blog/src/static/img/.gitignore similarity index 100% rename from examples/dummy.py rename to examples/legacy/blog/src/static/img/.gitignore diff --git a/examples/legacy/blog/src/static/js/paginate.js b/examples/legacy/blog/src/static/js/paginate.js new file mode 100644 index 0000000..fc77e63 --- /dev/null +++ b/examples/legacy/blog/src/static/js/paginate.js @@ -0,0 +1,104 @@ +// Based on https://stackoverflow.com/a/72308579 by rootShiv + +function setup_pagination(table) { + var filter = $("#filter").val().toLowerCase(); + var lastPage = 1; + $("#maxRows").on("change", function (evt) { + lastPage = 1; + $(".pagination").find("li").slice(1, -1).remove(); + var trnum = 0; + var maxRows = parseInt($(this).val()); + $(table + " tr").each(function (el) { + a = $(this); + txtValue = a.text().toLowerCase(); + if (!filter || txtValue.indexOf(filter) > -1) { + trnum++; + } + }); + var pagenum = Math.ceil(trnum / maxRows); + for (var i = 1; i <= pagenum; i++) { + $(".pagination #insert-pages-here") + .before( + '
  • '+(i)+ + '(current)
  • ') + .show(); + } + $('.pagination [data-page="1"]').addClass("active"); + $(".pagination li").on("click", function (evt) { + evt.stopImmediatePropagation(); + evt.preventDefault(); + var pageNum = $(this).attr("data-page"); + activate_page(pageNum, table, filter); + }); + pagination_limits(); + }).val(10).change(); + activate_page(1, table, filter); +} + +function activate_page(pageNum, table, filter) { + var maxRows = parseInt($("#maxRows").val()); + if (pageNum == "prev") { + if (lastPage == 1) { + return; + } + pageNum = --lastPage; + } + if (pageNum == "next") { + if (lastPage == $(".pagination li").length - 2) { + return; + } + pageNum = ++lastPage; + } + lastPage = pageNum; + var trIndex = 0; + $(".pagination li").removeClass("active"); + $('.pagination [data-page="' + lastPage + '"]').addClass("active"); + pagination_limits(); + $(table + " tr").each(function () { + + a = $(this); + txtValue = a.text().toLowerCase(); + if (!filter || txtValue.indexOf(filter) > -1) { + trIndex++; + if (trIndex > maxRows * pageNum || trIndex <= maxRows * pageNum - maxRows) { + $(this).hide(); + } else { + $(this).show(); + } + } else { + $(this).hide(); + } + }); +} + +function pagination_limits() { + if ($(".pagination li").length > 7) { + if ($(".pagination li.active").attr("data-page") <= 3) { + $(".pagination li:gt(5)").hide(); + $(".pagination li:lt(5)").show(); + $('.pagination [data-page="next"]').show(); + } + if ($(".pagination li.active").attr("data-page") > 3) { + $(".pagination li:gt(0)").hide(); + $('.pagination [data-page="next"]').show(); + for ( + let i = parseInt($(".pagination li.active").attr("data-page")) - 2; + i <= parseInt($(".pagination li.active").attr("data-page")) + 2; + i++ + ) { + $('.pagination [data-page="' + i + '"]').show(); + } + } + } +} + +$('#filterform').on('reset', function(e) +{ + setTimeout(function() { setup_pagination("#table-id"); }); +}); + +$('#filterform').on('submit',function(event) { + event.preventDefault(); +}); + +setup_pagination("#table-id"); diff --git a/examples/legacy/blog/src/static/resources/css/httk.css b/examples/legacy/blog/src/static/resources/css/httk.css new file mode 100644 index 0000000..a3dd0e3 --- /dev/null +++ b/examples/legacy/blog/src/static/resources/css/httk.css @@ -0,0 +1,342 @@ +body { + padding-top: 0rem; +} +.navbar-custom { +} +.navbar-custom .navbar-nav .nav-link { + color: grey; +} +.navbar-custom .nav-link.active, +.navbar-custom .nav-link:hover { + color: black; +} +.border-3 { + border-width:3px !important; +} +.border-orange { + border-color: #f80 !important; +} +.border-darkgrey { + border-color: #888 !important; +} +.orange { + color: #f80 !important; +} +.dropdown .dropdown-menu .dropdown-item:active, .dropdown .dropdown-menu .dropdown-item:hover{background-color: white; color: black;} +.dropdown .dropdown-menu .dropdown-item {background-color: white; color: grey;} + +h1, h2, h3 { + padding-top: 1em; +} + +.publist a { + font-style: italic; +} + +* { + box-sizing: border-box; } + +body { + padding: 0; + margin: 0; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 1.5; + color: #606c71; } + +a { + color: #1e6bb8; + text-decoration: none; } + a:hover { + text-decoration: underline; } + +.page-header .btn { + display: inline-block; + margin-bottom: 1rem; + color: rgba(255, 255, 255, 0.7); + background-color: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.2); + border-style: solid; + border-width: 1px; + border-radius: 0.3rem; + transition: color 0.2s, background-color 0.2s, border-color 0.2s; } +.page-header .btn:hover { + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + background-color: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.3); } +.page-header .btn + .btn { + margin-left: 1rem; } + @media screen and (min-width: 64em) { + .page-header .btn { + padding: 0.75rem 1rem; } } + @media screen and (min-width: 42em) and (max-width: 64em) { + .page-header .btn { + padding: 0.6rem 0.9rem; + font-size: 0.9rem; } } + @media screen and (max-width: 42em) { + .page-header .btn { + display: block; + width: 100%; + padding: 0.75rem; + font-size: 0.9rem; } + .btn + .btn { + margin-top: 1rem; + margin-left: 0; } } + +.page-header { + color: #fff; + text-align: center; + #background-color: #155799; + background-color: #114a85; + #background-image: linear-gradient(120deg, #80b8df, #155799); + #background-image: linear-gradient(120deg, #80b8df, #103481); + #background-image: linear-gradient(120deg, #103481, #80b8df); + #background-image: linear-gradient(120deg, #A03481, #FA380f); + background-image: linear-gradient(120deg, #703481, #6A0000); + } + .page-header img { + height:96px; + } + @media screen and (min-width: 64em) { + .page-header { + padding: 3rem 4rem; } } + @media screen and (min-width: 42em) and (max-width: 64em) { + .page-header { + padding: 3rem 4rem; } } + @media screen and (max-width: 42em) { + .page-header { + padding: 2rem 1rem; } } + +.project-name { + margin-top: 0; + margin-bottom: 0.1rem; } + @media screen and (min-width: 64em) { + .project-name { + font-size: 3.25rem; } } + @media screen and (min-width: 42em) and (max-width: 64em) { + .project-name { + font-size: 2.25rem; } } + @media screen and (max-width: 42em) { + .project-name { + font-size: 1.75rem; } } + +.project-tagline { + margin-bottom: 2rem; + font-weight: normal; + opacity: 0.7; } + @media screen and (min-width: 64em) { + .project-tagline { + font-size: 1.25rem; } } + @media screen and (min-width: 42em) and (max-width: 64em) { + .project-tagline { + font-size: 1.15rem; } } + @media screen and (max-width: 42em) { + .project-tagline { + font-size: 1rem; } } + +.main-content { + word-wrap: break-word; } + .main-content :first-child { + margin-top: 0; } + @media screen and (min-width: 64em) { + .main-content { + max-width: 64rem; + padding: 2rem 6rem; + margin: 0 auto; + font-size: 1.1rem; } } + @media screen and (min-width: 42em) and (max-width: 64em) { + .main-content { + padding: 2rem 4rem; + font-size: 1.1rem; } } + @media screen and (max-width: 42em) { + .main-content { + padding: 2rem 1rem; + font-size: 1rem; } } + .main-content img { + max-width: 100%; } + .main-content h1, + .main-content h2, + .main-content h3, + .main-content h4, + .main-content h5, + .main-content h6 { + margin-top: 2rem; + margin-bottom: 1rem; + font-weight: normal; + color: #2255AA; } + .main-content p { + margin-bottom: 1em; } + .main-content code { + padding: 2px 4px; + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 0.9rem; + color: #567482; + background-color: #f3f6fa; + border-radius: 0.3rem; } + .main-content pre { + padding: 0.8rem; + margin-top: 0; + margin-bottom: 1rem; + font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace; + color: #567482; + word-wrap: normal; + background-color: #f3f6fa; + border: solid 1px #dce6f0; + border-radius: 0.3rem; } + .main-content pre > code { + padding: 0; + margin: 0; + font-size: 0.9rem; + color: #567482; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; } + .main-content .highlight { + margin-bottom: 1rem; } + .main-content .highlight pre { + margin-bottom: 0; + word-break: normal; } + .main-content .highlight pre, + .main-content pre { + padding: 0.8rem; + overflow: auto; + font-size: 0.9rem; + line-height: 1.45; + border-radius: 0.3rem; + -webkit-overflow-scrolling: touch; } + .main-content pre code, + .main-content pre tt { + display: inline; + max-width: initial; + padding: 0; + margin: 0; + overflow: initial; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; } + .main-content pre code:before, .main-content pre code:after, + .main-content pre tt:before, + .main-content pre tt:after { + content: normal; } + .main-content ul, + .main-content ol { + margin-top: 0; } + .main-content blockquote { + padding: 0 1rem; + margin-left: 0; + color: #819198; + border-left: 0.3rem solid #dce6f0; } + .main-content blockquote > :first-child { + margin-top: 0; } + .main-content blockquote > :last-child { + margin-bottom: 0; } + .main-content table { + display: block; + width: 100%; + overflow: auto; + word-break: normal; + word-break: keep-all; + -webkit-overflow-scrolling: touch; } + .main-content table th { + font-weight: bold; } + .main-content table th, + .main-content table td { + padding: 0.5rem 1rem; + border: 1px solid #e9ebec; } + .main-content dl { + padding: 0; } + .main-content dl dt { + padding: 0; + margin-top: 1rem; + font-size: 1rem; + font-weight: bold; } + .main-content dl dd { + padding: 0; + margin-bottom: 1rem; } + .main-content hr { + height: 2px; + padding: 0; + margin: 1rem 0; + background-color: #eff0f1; + border: 0; } + +.site-footer { + padding-top: 2rem; + margin-top: 2rem; + border-top: solid 1px #eff0f1; } + @media screen and (min-width: 64em) { + .site-footer { + font-size: 1rem; } } + @media screen and (min-width: 42em) and (max-width: 64em) { + .site-footer { + font-size: 1rem; } } + @media screen and (max-width: 42em) { + .site-footer { + font-size: 0.9rem; } } + +.site-footer-owner { + display: block; + font-weight: bold; } + +.site-footer-credits { + color: #819198; } + +.project-name { + font-weight: bold; + display: inline; +} +.top-left-dropdown { + background-color: #eff0f1; + border-color: #dfe0e1; + color: grey; +} +.dropdown-toggle { + background-color: #ffffff; + border-color: #dfe0e1; + color: grey; +} +@media all and (min-width: 992px) { + .navbar .nav-item .dropdown-menu{ display: none; } + .navbar .nav-item:hover .dropdown-menu{ display: block; } + .navbar .nav-item .dropdown-menu{ margin-top:0; } +} +blockquote { + margin: 1em 0em 1em 5em; + color: grey; +} + +.main-content img { + display: block; + margin: 2em; + margin-left: auto; + margin-right: auto; + width: 100%; +} + +.main-content img.align-left { + float: left; + margin-top: 0.5ex; + margin-bottom: 0.5ex; + margin-right: 2em; + margin-left: -5em; +} + +.main-content img.align-right { + float: right; + margin-top: 0.5ex; + margin-bottom: 0.5ex; + margin-right: -5em; + margin-left: 2em; +} + +.main-content img.oversize { + max-width: none; + width: 135%; + margin-top: 0.5ex; + margin-bottom: 0.5ex; + margin-left: -5em; + margin-right: -5em; +} diff --git a/examples/legacy/blog/src/templates/404.httkweb.html b/examples/legacy/blog/src/templates/404.httkweb.html new file mode 100644 index 0000000..912ade8 --- /dev/null +++ b/examples/legacy/blog/src/templates/404.httkweb.html @@ -0,0 +1,2 @@ +{content} +{error_404_reason:unquoted:if:Reason given from webserver: {error_404_reason}} diff --git a/examples/legacy/blog/src/templates/base_default.httkweb.html b/examples/legacy/blog/src/templates/base_default.httkweb.html new file mode 100644 index 0000000..a8f839b --- /dev/null +++ b/examples/legacy/blog/src/templates/base_default.httkweb.html @@ -0,0 +1,43 @@ + + + + + {page.title} + + + + + + + + + + + +
    +{content} +
    + + + + + + + diff --git a/examples/legacy/blog/src/templates/blog.httkweb.html b/examples/legacy/blog/src/templates/blog.httkweb.html new file mode 100644 index 0000000..c9e5c08 --- /dev/null +++ b/examples/legacy/blog/src/templates/blog.httkweb.html @@ -0,0 +1,46 @@ +
    + +
    +
    +
    + {content} + + + {blogposts:repeat:: + + } +
    {{pages:call:{{item}}:name}} - {{pages:call:{{item}}:author}} ({{pages:call:{{item}}:date}})
    +
    + +
    +
    + +
    +
    +
    +
    +
    + diff --git a/examples/legacy/blog/src/templates/blogpost.httkweb.html b/examples/legacy/blog/src/templates/blogpost.httkweb.html new file mode 100644 index 0000000..f41c654 --- /dev/null +++ b/examples/legacy/blog/src/templates/blogpost.httkweb.html @@ -0,0 +1,26 @@ +
    + +
    +
    +
    + + ← blog / {page.name} + + {content} +
    +
    +
    +
    diff --git a/examples/legacy/blog/src/templates/default.httkweb.html b/examples/legacy/blog/src/templates/default.httkweb.html new file mode 100644 index 0000000..8b13b37 --- /dev/null +++ b/examples/legacy/blog/src/templates/default.httkweb.html @@ -0,0 +1,23 @@ +
    + +
    +
    +
    + {content} +
    +
    +
    +
    diff --git a/examples/legacy/hello_world_app/publish_legacy_hello_world.py b/examples/legacy/hello_world_app/publish_legacy_hello_world.py new file mode 100755 index 0000000..5b6218d --- /dev/null +++ b/examples/legacy/hello_world_app/publish_legacy_hello_world.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import publish + +ROOT = Path(__file__).parent +publish(ROOT / "src", ROOT / "public", "./", compatibility_mode=True) diff --git a/examples/legacy/hello_world_app/serve_legacy_hello_world.py b/examples/legacy/hello_world_app/serve_legacy_hello_world.py new file mode 100755 index 0000000..a59305e --- /dev/null +++ b/examples/legacy/hello_world_app/serve_legacy_hello_world.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import serve + +ROOT = Path(__file__).parent +serve(ROOT / "src", port=8080, compatibility_mode=True) diff --git a/examples/legacy/hello_world_app/src/config.httkweb b/examples/legacy/hello_world_app/src/config.httkweb new file mode 100644 index 0000000..519c199 --- /dev/null +++ b/examples/legacy/hello_world_app/src/config.httkweb @@ -0,0 +1,3 @@ +--- +website_name: Httk hello world demo +--- diff --git a/examples/legacy/hello_world_app/src/content/404.httkweb b/examples/legacy/hello_world_app/src/content/404.httkweb new file mode 100644 index 0000000..eb5118f --- /dev/null +++ b/examples/legacy/hello_world_app/src/content/404.httkweb @@ -0,0 +1,10 @@ +--- +Title: 404 +Date: 2018-08-16 +Version: 1 +Author: Rickard Armiento +Template: default +--- +The page could not be found. + + diff --git a/examples/legacy/hello_world_app/src/content/index.httkweb b/examples/legacy/hello_world_app/src/content/index.httkweb new file mode 100644 index 0000000..c4950ca --- /dev/null +++ b/examples/legacy/hello_world_app/src/content/index.httkweb @@ -0,0 +1,13 @@ +--- +Title: Search +Date: 2018-08-16 +Version: 1 +Author: Rickard Armiento +hello_world-function: hello_world::hello_world_result +--- + +============================================ +This example runs a function that greets you +============================================ + +The output is shown below. diff --git a/examples/legacy/hello_world_app/src/functions/hello_world.py b/examples/legacy/hello_world_app/src/functions/hello_world.py new file mode 100644 index 0000000..16f7e7a --- /dev/null +++ b/examples/legacy/hello_world_app/src/functions/hello_world.py @@ -0,0 +1,3 @@ +def execute(global_data, **kargs): + print("Debug: running hello_world function.") + return "Hello " + global_data['greeter'] diff --git a/examples/legacy/hello_world_app/src/functions/init.py b/examples/legacy/hello_world_app/src/functions/init.py new file mode 100644 index 0000000..0a65fbe --- /dev/null +++ b/examples/legacy/hello_world_app/src/functions/init.py @@ -0,0 +1,3 @@ +def execute(global_data, **kargs): + print("Debug: running website initialization function.") + global_data['greeter'] = "world" diff --git a/examples/legacy/hello_world_app/src/static/img/Example.png b/examples/legacy/hello_world_app/src/static/img/Example.png new file mode 100644 index 0000000..aa61359 Binary files /dev/null and b/examples/legacy/hello_world_app/src/static/img/Example.png differ diff --git a/examples/legacy/hello_world_app/src/static/resources/css/httkdemo.css b/examples/legacy/hello_world_app/src/static/resources/css/httkdemo.css new file mode 100644 index 0000000..262c8e8 --- /dev/null +++ b/examples/legacy/hello_world_app/src/static/resources/css/httkdemo.css @@ -0,0 +1,40 @@ +body { + padding-top: 0rem; +} +.navbar-custom { +} +.navbar-custom .navbar-nav .nav-link { + color: rgba(0,0,0,.5); +} +.navbar-custom .nav-link.active, +.navbar-custom .nav-link:hover { + color: #000000; +} +.border-3 { + border-width:3px !important; +} +.border-orange { + border-color: #f80 !important; +} +.border-darkgrey { + border-color: #888 !important; +} +.orange { + color: #f80 !important; +} +.dropdown .dropdown-menu .dropdown-item:active, .dropdown .dropdown-menu .dropdown-item:hover{background-color: white; color: black;} +.dropdown .dropdown-menu .dropdown-item {background-color: white; color: grey;} + +.align-right { + float: right; + margin: 2em; +} + +h1, h2, h3 { + padding-top: 1em; +} + +.publist a { + font-style: italic; +} + diff --git a/examples/legacy/hello_world_app/src/templates/base_default.httkweb.html b/examples/legacy/hello_world_app/src/templates/base_default.httkweb.html new file mode 100644 index 0000000..8c6a4d8 --- /dev/null +++ b/examples/legacy/hello_world_app/src/templates/base_default.httkweb.html @@ -0,0 +1,45 @@ + + + + + + + + + +{page.title} + + + + + + + +
    +
    + {content} +
    +
    + + + + + diff --git a/examples/legacy/hello_world_app/src/templates/default.httkweb.html b/examples/legacy/hello_world_app/src/templates/default.httkweb.html new file mode 100644 index 0000000..390c2fc --- /dev/null +++ b/examples/legacy/hello_world_app/src/templates/default.httkweb.html @@ -0,0 +1,2 @@ +{content} +{page.hello_world} diff --git a/examples/legacy/hello_world_app/src/templates/hello_world_result.httkweb.html b/examples/legacy/hello_world_app/src/templates/hello_world_result.httkweb.html new file mode 100644 index 0000000..a9ed31a --- /dev/null +++ b/examples/legacy/hello_world_app/src/templates/hello_world_result.httkweb.html @@ -0,0 +1 @@ +Result of hello world function: {result} diff --git a/examples/legacy/rst_templator/publish_legacy_rst_templator.py b/examples/legacy/rst_templator/publish_legacy_rst_templator.py new file mode 100644 index 0000000..c654cfc --- /dev/null +++ b/examples/legacy/rst_templator/publish_legacy_rst_templator.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import publish + +ROOT = Path(__file__).parent +publish(ROOT / "src", ROOT / "public", "http://127.0.0.1/", compatibility_mode=True) diff --git a/examples/legacy/rst_templator/serve_legacy_rst_templator.py b/examples/legacy/rst_templator/serve_legacy_rst_templator.py new file mode 100644 index 0000000..e3f984f --- /dev/null +++ b/examples/legacy/rst_templator/serve_legacy_rst_templator.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import serve + +ROOT = Path(__file__).parent +serve(ROOT / "src", port=8080, compatibility_mode=True, config_name="config_dynamic") diff --git a/examples/legacy/rst_templator/src/config.rst b/examples/legacy/rst_templator/src/config.rst new file mode 100644 index 0000000..b1ba0d0 --- /dev/null +++ b/examples/legacy/rst_templator/src/config.rst @@ -0,0 +1,6 @@ +:menuitems-list: + - index + - contact + - bare + + diff --git a/examples/legacy/rst_templator/src/config_dynamic.rst b/examples/legacy/rst_templator/src/config_dynamic.rst new file mode 100644 index 0000000..5821c3d --- /dev/null +++ b/examples/legacy/rst_templator/src/config_dynamic.rst @@ -0,0 +1,6 @@ +:menuitems-list: + - index + - contact + - bare +:urls_without_ext: true + diff --git a/examples/legacy/rst_templator/src/content/404.rst b/examples/legacy/rst_templator/src/content/404.rst new file mode 100644 index 0000000..733dd19 --- /dev/null +++ b/examples/legacy/rst_templator/src/content/404.rst @@ -0,0 +1,12 @@ +:Title: Contact +:Date: 2018-08-16 +:Version: 1 +:Author: Rickard Armiento +:Layout: default + +Not found +========= + +The page could not be found. + + diff --git a/examples/legacy/rst_templator/src/content/bare.rst b/examples/legacy/rst_templator/src/content/bare.rst new file mode 100644 index 0000000..faac02a --- /dev/null +++ b/examples/legacy/rst_templator/src/content/bare.rst @@ -0,0 +1,13 @@ +:Title: Bare +:Date: 2018-08-16 +:Version: 1 +:Author: Rickard Armiento +:Layout: default +:Base_template: bare + +Bare +==== + +This page has no header, etc. + + diff --git a/examples/legacy/rst_templator/src/content/contact.rst b/examples/legacy/rst_templator/src/content/contact.rst new file mode 100644 index 0000000..9e9047f --- /dev/null +++ b/examples/legacy/rst_templator/src/content/contact.rst @@ -0,0 +1,11 @@ +:Title: Contact +:Date: 2018-08-16 +:Version: 1 +:Author: Rickard Armiento +:Layout: default + +Contact +======= + +You should contact us + diff --git a/examples/legacy/rst_templator/src/content/index.rst b/examples/legacy/rst_templator/src/content/index.rst new file mode 100644 index 0000000..bacedab --- /dev/null +++ b/examples/legacy/rst_templator/src/content/index.rst @@ -0,0 +1,509 @@ +:Title: Front page +:Date: 2018-08-16 +:Version: 1 +:Author: Rickard Armiento +:Layout: default + +:Address: Example Street 123 + Sweden +:Contact: example@example.com +:organization: httk +:field name: This is a generic bibliographic field. +:field name 2: + Generic bibliographic fields may contain multiple body elements. + + Like this. + +====================== + httk example website +====================== + +Below follows some example content from the docutils demo.txt file. + +.. Above is the document title, and below is the subtitle. + They are transformed from section titles after parsing. + +-------------------------------- + Examples of Syntax Constructs +-------------------------------- + +.. meta:: + :keywords: reStructuredText, demonstration, demo, parser + :description lang=en: A demonstration of the reStructuredText + markup language, containing examples of all basic + constructs and many advanced constructs. + +.. To get numbered headings, use this: + .. contents:: Table of Contents + .. section-numbering:: + +Structural Elements +=================== + +Section Title +------------- + +That's it, the text just above this line. + +Transitions +----------- + +Here's a transition: + +--------- + +It divides the section. + +Body Elements +============= + +Paragraphs +---------- + +A paragraph. + +Inline Markup +````````````` + +Paragraphs contain text and may contain inline markup: *emphasis*, +**strong emphasis**, ``inline literals``, standalone hyperlinks +(http://www.python.org), external hyperlinks (Python_), internal +cross-references (example_), external hyperlinks with embedded URIs +(`Python web site `__), footnote references +(manually numbered [1]_, anonymous auto-numbered [#]_, labeled +auto-numbered [#label]_, or symbolic [*]_), citation references +([CIT2002]_), substitution references, and _`inline +hyperlink targets` (see Targets_ below for a reference back to here). +Character-level inline markup is also possible (although exceedingly +ugly!) in *re*\ ``Structured``\ *Text*. + +The default role for interpreted text is `Title Reference`. Here are +some explicit interpreted text roles: a PEP reference (:PEP:`287`); an +RFC reference (:RFC:`2822`); a :sub:`subscript`; a :sup:`superscript`; +and explicit roles for :emphasis:`standard` :strong:`inline` +:literal:`markup`. + +.. DO NOT RE-WRAP THE FOLLOWING PARAGRAPH! + +Let's test wrapping and whitespace significance in inline literals: +``This is an example of --inline-literal --text, --including some-- +strangely--hyphenated-words. Adjust-the-width-of-your-browser-window +to see how the text is wrapped. -- ---- -------- Now note the +spacing between the words of this sentence (words +should be grouped in pairs).`` + +If the ``--pep-references`` option was supplied, there should be a +live link to PEP 258 here. + +Bullet Lists +------------ + +- A bullet list + + + Nested bullet list. + + Nested item 2. + +- Item 2. + + Paragraph 2 of item 2. + + * Nested bullet list. + * Nested item 2. + + - Third level. + - Item 2. + + * Nested item 3. + +Enumerated Lists +---------------- + +1. Arabic numerals. + + a) lower alpha) + + (i) (lower roman) + + A. upper alpha. + + I) upper roman) + +2. Lists that don't start at 1: + + 3. Three + + 4. Four + + C. C + + D. D + + iii. iii + + iv. iv + +#. List items may also be auto-enumerated. + +Definition Lists +---------------- + +Term + Definition +Term : classifier + Definition paragraph 1. + + Definition paragraph 2. +Term + Definition + +Field Lists +----------- + +:what: Field lists map field names to field bodies, like database + records. They are often part of an extension syntax. They are + an unambiguous variant of RFC 2822 fields. + +:how arg1 arg2: + + The field marker is a colon, the field name, and a colon. + + The field body may contain one or more body elements, indented + relative to the field marker. + +Option Lists +------------ + +For listing command-line options: + +-a command-line option "a" +-b file options can have arguments + and long descriptions +--long options can be long also +--input=file long options can also have + arguments + +--very-long-option + The description can also start on the next line. + + The description may contain multiple body elements, + regardless of where it starts. + +-x, -y, -z Multiple options are an "option group". +-v, --verbose Commonly-seen: short & long options. +-1 file, --one=file, --two file + Multiple options with arguments. +/V DOS/VMS-style options too + +There must be at least two spaces between the option and the +description. + +Literal Blocks +-------------- + +Literal blocks are indicated with a double-colon ("::") at the end of +the preceding paragraph (over there ``-->``). They can be indented:: + + if literal_block: + text = 'is left as-is' + spaces_and_linebreaks = 'are preserved' + markup_processing = None + +Or they can be quoted without indentation:: + +>> Great idea! +> +> Why didn't I think of that? + +Line Blocks +----------- + +| This is a line block. It ends with a blank line. +| Each new line begins with a vertical bar ("|"). +| Line breaks and initial indents are preserved. +| Continuation lines are wrapped portions of long lines; + they begin with a space in place of the vertical bar. +| The left edge of a continuation line need not be aligned with + the left edge of the text above it. + +| This is a second line block. +| +| Blank lines are permitted internally, but they must begin with a "|". + +Take it away, Eric the Orchestra Leader! + + | A one, two, a one two three four + | + | Half a bee, philosophically, + | must, *ipso facto*, half not be. + | But half the bee has got to be, + | *vis a vis* its entity. D'you see? + | + | But can a bee be said to be + | or not to be an entire bee, + | when half the bee is not a bee, + | due to some ancient injury? + | + | Singing... + +Block Quotes +------------ + +Block quotes consist of indented body elements: + + My theory by A. Elk. Brackets Miss, brackets. This theory goes + as follows and begins now. All brontosauruses are thin at one + end, much much thicker in the middle and then thin again at the + far end. That is my theory, it is mine, and belongs to me and I + own it, and what it is too. + + -- Anne Elk (Miss) + +Doctest Blocks +-------------- + +>>> print 'Python-specific usage examples; begun with ">>>"' +Python-specific usage examples; begun with ">>>" +>>> print '(cut and pasted from interactive Python sessions)' +(cut and pasted from interactive Python sessions) + +Tables +------ + +Here's a grid table followed by a simple table: + ++------------------------+------------+----------+----------+ +| Header row, column 1 | Header 2 | Header 3 | Header 4 | +| (header rows optional) | | | | ++========================+============+==========+==========+ +| body row 1, column 1 | column 2 | column 3 | column 4 | ++------------------------+------------+----------+----------+ +| body row 2 | Cells may span columns. | ++------------------------+------------+---------------------+ +| body row 3 | Cells may | - Table cells | ++------------------------+ span rows. | - contain | +| body row 4 | | - body elements. | ++------------------------+------------+----------+----------+ +| body row 5 | Cells may also be | | +| | empty: ``-->`` | | ++------------------------+-----------------------+----------+ + +===== ===== ====== + Inputs Output +------------ ------ + A B A or B +===== ===== ====== +False False False +True False True +False True True +True True True +===== ===== ====== + +Footnotes +--------- + +.. [1] A footnote contains body elements, consistently indented by at + least 3 spaces. + + This is the footnote's second paragraph. + +.. [#label] Footnotes may be numbered, either manually (as in [1]_) or + automatically using a "#"-prefixed label. This footnote has a + label so it can be referred to from multiple places, both as a + footnote reference ([#label]_) and as a hyperlink reference + (label_). + +.. [#] This footnote is numbered automatically and anonymously using a + label of "#" only. + +(Remark: one example was removed here, because it caused unicode error.) + +.. [*] This footnote shows the next symbol in the sequence. + +Citations +--------- + +.. [CIT2002] Citations are text-labeled footnotes. They may be + rendered separately and differently from footnotes. + +Here's a reference to the above, [CIT2002]_. + +Targets +------- + +.. _example: + +This paragraph is pointed to by the explicit "example" target. A +reference can be found under `Inline Markup`_, above. `Inline +hyperlink targets`_ are also possible. + +Section headers are implicit targets, referred to by name. See +Targets_, which is a subsection of `Body Elements`_. + +Explicit external targets are interpolated into references such as +"Python_". + +.. _Python: http://www.python.org/ + +Targets may be indirect and anonymous. Thus `this phrase`__ may also +refer to the Targets_ section. + +__ Targets_ + +Duplicate Target Names +`````````````````````` + +Duplicate names in section headers or other implicit targets will +generate "info" (level-1) system messages. Duplicate names in +explicit targets will generate "warning" (level-2) system messages. + +Duplicate Target Names +`````````````````````` + +Since there are two "Duplicate Target Names" section headers, we +cannot uniquely refer to either of them by name. + +Directives +---------- + +.. contents:: :local: + +These are just a sample of the many reStructuredText Directives. For +others, please see +http://docutils.sourceforge.net/docs/ref/rst/directives.html. + +Document Parts +`````````````` + +An example of the "contents" directive can be seen above this section +(a local, untitled table of contents_). + +Images +`````` + +An image directive (also clickable -- a hyperlink reference): + +.. image:: img/Example.png + :target: directives_ + +A figure directive: + +.. figure:: img/Example.png + :alt: reStructuredText, the markup syntax + + A figure is an image with a caption and/or a legend: + + +------------+-----------------------------------------------+ + | re | Revised, revisited, based on 're' module. | + +------------+-----------------------------------------------+ + | Structured | Structure-enhanced text, structuredtext. | + +------------+-----------------------------------------------+ + | Text | Well it is, isn't it? | + +------------+-----------------------------------------------+ + + This paragraph is also part of the legend. + +Admonitions +``````````` + +.. Attention:: Directives at large. + +.. Caution:: + + Don't take any wooden nickels. + +.. DANGER:: Mad scientist at work! + +.. Error:: Does not compute. + +.. Hint:: It's bigger than a bread box. + +.. Important:: + - Wash behind your ears. + - Clean up your room. + - Call your mother. + - Back up your data. + +.. Note:: This is a note. + +.. Tip:: 15% if the service is good. + +.. WARNING:: Strong prose may provoke extreme mental exertion. + Reader discretion is strongly advised. + +.. admonition:: And, by the way... + + You can make up your own admonition too. + +Topics, Sidebars, and Rubrics +````````````````````````````` + +.. sidebar:: Sidebar Title + :subtitle: Optional Subtitle + + This is a sidebar. It is for text outside the flow of the main + text. + + .. rubric:: This is a rubric inside a sidebar + + Sidebars often appears beside the main text with a border and + background color. + +.. topic:: Topic Title + + This is a topic. + +.. rubric:: This is a rubric + +Target Footnotes +```````````````` + +.. target-notes:: + +Replacement Text +```````````````` + +I recommend you try |Python|_. + +.. |Python| replace:: Python, *the* best language around + +Compound Paragraph +`````````````````` + +.. compound:: + + This paragraph contains a literal block:: + + Connecting... OK + Transmitting data... OK + Disconnecting... OK + + and thus consists of a simple paragraph, a literal block, and + another simple paragraph. Nonetheless it is semantically *one* + paragraph. + +This construct is called a *compound paragraph* and can be produced +with the "compound" directive. + +Substitution Definitions +------------------------ + +An inline image (|example|) example: + +.. |EXAMPLE| image:: img/Example.png + +(Substitution definitions are not visible in the HTML source.) + +Comments +-------- + +Here's one: + +.. Comments begin with two dots and a space. Anything may + follow, except for the syntax of footnotes, hyperlink + targets, directives, or substitution definitions. + + Double-dashes -- "--" -- must be escaped somehow in HTML output. + +(View the HTML source to see the comment.) + + diff --git a/examples/legacy/rst_templator/src/static/img/Example.png b/examples/legacy/rst_templator/src/static/img/Example.png new file mode 100644 index 0000000..aa61359 Binary files /dev/null and b/examples/legacy/rst_templator/src/static/img/Example.png differ diff --git a/examples/legacy/rst_templator/src/static/resources/css/httkdemo.css b/examples/legacy/rst_templator/src/static/resources/css/httkdemo.css new file mode 100644 index 0000000..262c8e8 --- /dev/null +++ b/examples/legacy/rst_templator/src/static/resources/css/httkdemo.css @@ -0,0 +1,40 @@ +body { + padding-top: 0rem; +} +.navbar-custom { +} +.navbar-custom .navbar-nav .nav-link { + color: rgba(0,0,0,.5); +} +.navbar-custom .nav-link.active, +.navbar-custom .nav-link:hover { + color: #000000; +} +.border-3 { + border-width:3px !important; +} +.border-orange { + border-color: #f80 !important; +} +.border-darkgrey { + border-color: #888 !important; +} +.orange { + color: #f80 !important; +} +.dropdown .dropdown-menu .dropdown-item:active, .dropdown .dropdown-menu .dropdown-item:hover{background-color: white; color: black;} +.dropdown .dropdown-menu .dropdown-item {background-color: white; color: grey;} + +.align-right { + float: right; + margin: 2em; +} + +h1, h2, h3 { + padding-top: 1em; +} + +.publist a { + font-style: italic; +} + diff --git a/examples/legacy/rst_templator/src/templates/bare.httkweb.html b/examples/legacy/rst_templator/src/templates/bare.httkweb.html new file mode 100644 index 0000000..edaf100 --- /dev/null +++ b/examples/legacy/rst_templator/src/templates/bare.httkweb.html @@ -0,0 +1,25 @@ + + + + + + + + + + {page.title} + + + + + + +
    +
    + {content} +
    +
    + + + + diff --git a/examples/legacy/rst_templator/src/templates/bare.templator.html b/examples/legacy/rst_templator/src/templates/bare.templator.html new file mode 100644 index 0000000..8ec27be --- /dev/null +++ b/examples/legacy/rst_templator/src/templates/bare.templator.html @@ -0,0 +1,26 @@ +$def with (content) + + + + + + + + + + $page.title + + + + + + +
    +
    + $:content +
    +
    + + + + diff --git a/examples/legacy/rst_templator/src/templates/base_default.httkweb.html b/examples/legacy/rst_templator/src/templates/base_default.httkweb.html new file mode 100644 index 0000000..c0b6804 --- /dev/null +++ b/examples/legacy/rst_templator/src/templates/base_default.httkweb.html @@ -0,0 +1,52 @@ + + + + + + + + + + {page.title} + + + + + + + + +
    +
    + {content} +
    +
    + + + + diff --git a/examples/legacy/rst_templator/src/templates/base_default.templator.html b/examples/legacy/rst_templator/src/templates/base_default.templator.html new file mode 100644 index 0000000..e721fe6 --- /dev/null +++ b/examples/legacy/rst_templator/src/templates/base_default.templator.html @@ -0,0 +1,52 @@ +$def with (content) + + + + + + + + + + $page.title + + + + + + + + +
    +
    + $:content +
    +
    + + + + diff --git a/examples/legacy/rst_templator/src/templates/default.httkweb.html b/examples/legacy/rst_templator/src/templates/default.httkweb.html new file mode 100644 index 0000000..24db9fc --- /dev/null +++ b/examples/legacy/rst_templator/src/templates/default.httkweb.html @@ -0,0 +1 @@ +{content} diff --git a/examples/legacy/rst_templator/src/templates/default.templator.html b/examples/legacy/rst_templator/src/templates/default.templator.html new file mode 100644 index 0000000..9ad37b9 --- /dev/null +++ b/examples/legacy/rst_templator/src/templates/default.templator.html @@ -0,0 +1,2 @@ +$def with(content) +$:content diff --git a/examples/legacy/search_app/example.sqlite b/examples/legacy/search_app/example.sqlite new file mode 100644 index 0000000..532e3bd Binary files /dev/null and b/examples/legacy/search_app/example.sqlite differ diff --git a/examples/legacy/search_app/publish_legacy_search_app.py b/examples/legacy/search_app/publish_legacy_search_app.py new file mode 100644 index 0000000..c654cfc --- /dev/null +++ b/examples/legacy/search_app/publish_legacy_search_app.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import publish + +ROOT = Path(__file__).parent +publish(ROOT / "src", ROOT / "public", "http://127.0.0.1/", compatibility_mode=True) diff --git a/examples/legacy/search_app/serve_legacy_search_app.py b/examples/legacy/search_app/serve_legacy_search_app.py new file mode 100644 index 0000000..a59305e --- /dev/null +++ b/examples/legacy/search_app/serve_legacy_search_app.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import serve + +ROOT = Path(__file__).parent +serve(ROOT / "src", port=8080, compatibility_mode=True) diff --git a/examples/legacy/search_app/src/config.httkweb b/examples/legacy/search_app/src/config.httkweb new file mode 100644 index 0000000..df3a977 --- /dev/null +++ b/examples/legacy/search_app/src/config.httkweb @@ -0,0 +1,6 @@ +--- +website_name: Httk search demo +other_websites_names-list: Another website, Open Materials Database +other_websites_urls-list: https://example.com, http://openmaterialsdb.se +menuitems-list: index +--- diff --git a/examples/legacy/search_app/src/content/404.httkweb b/examples/legacy/search_app/src/content/404.httkweb new file mode 100644 index 0000000..eb5118f --- /dev/null +++ b/examples/legacy/search_app/src/content/404.httkweb @@ -0,0 +1,10 @@ +--- +Title: 404 +Date: 2018-08-16 +Version: 1 +Author: Rickard Armiento +Template: default +--- +The page could not be found. + + diff --git a/examples/legacy/search_app/src/content/index.httkweb b/examples/legacy/search_app/src/content/index.httkweb new file mode 100644 index 0000000..6926f53 --- /dev/null +++ b/examples/legacy/search_app/src/content/index.httkweb @@ -0,0 +1,10 @@ +--- +Title: Search +Date: 2018-08-16 +Version: 1 +Author: Rickard Armiento +Template: search_page +search_result-function: search:q,!compound:search_result +material_details-function: details:compound:material_details +base_template_app: base_app +--- diff --git a/examples/legacy/search_app/src/functions/details.py b/examples/legacy/search_app/src/functions/details.py new file mode 100644 index 0000000..b6e67bd --- /dev/null +++ b/examples/legacy/search_app/src/functions/details.py @@ -0,0 +1,29 @@ +# mypy: ignore-errors + +from httk.atomistic import Compound + + +def execute(compound, global_data, **kargs): + + search = global_data['store'].searcher() + search_compound = search.variable(Compound) + search.add(search_compound.element_wyckoff_sequence == compound) + + search.output(search_compound, 'compound') + + compound = list(search)[0][0][0] + tags = compound.get_tags() + tags = [{'tag': tags[tag].tag, 'value': tags[tag].value} for tag in tags] + + names = ", ".join([x.name for x in compound.get_names()]) + + output = { + 'id': compound.element_wyckoff_sequence, + 'formula': compound.formula, + 'anonymous_formula': compound.anonymous_formula, + 'spacegroup_number': compound.spacegroup_number, + 'tags': tags, + 'names': names, + } + + return output diff --git a/examples/legacy/search_app/src/functions/init.py b/examples/legacy/search_app/src/functions/init.py new file mode 100644 index 0000000..8e5436f --- /dev/null +++ b/examples/legacy/search_app/src/functions/init.py @@ -0,0 +1,13 @@ +# mypy: ignore-errors + +import httk.db + +import httk + + +def execute(global_data, **kargs): + + backend = httk.db.backend.Sqlite('../../../Tutorial/tutorial_data/tutorial.sqlite') + store = httk.db.store.SqlStore(backend) + + global_data['store'] = store diff --git a/examples/legacy/search_app/src/functions/search.py b/examples/legacy/search_app/src/functions/search.py new file mode 100644 index 0000000..42222a6 --- /dev/null +++ b/examples/legacy/search_app/src/functions/search.py @@ -0,0 +1,49 @@ +# mypy: ignore-errors + +import shlex + +from httk.atomistic import Compound +from httk.atomistic.compound import CompoundName + + +def execute(q, global_data, **kargs): + + output = [] + + parts = shlex.split(q) + + search = global_data['store'].searcher() + search_compound = search.variable(Compound) + + # TODO: this should work: + # search_compound_tag = search.variable(CompoundTag,parent=search_compound, subkey='compound') + search_compound_name = search.variable( + CompoundName, parent=search_compound, parentkey='Compound_id', subkey='compound_Compound_sid' + ) + + # search.add(search_compound_tag.compound == search_compound) + + for part in parts: + criterion = search_compound_name.name.like('%' + part + '%') + criterion = criterion or search_compound.formula_symbols.is_in(part) + search.add(criterion) + search.output(search_compound, 'compound') + + i = 0 + for match, header in list(search): + compound = match[0] + names = ", ".join([x.name for x in compound.get_names()]) + + i += 1 + output += [ + { + 'index': i, + 'id': compound.element_wyckoff_sequence, + 'formula': compound.formula, + 'anonymous_formula': compound.anonymous_formula, + 'spacegroup': compound.spacegroup_number, + 'names': names, + } + ] + + return output diff --git a/examples/legacy/search_app/src/static/img/Example.png b/examples/legacy/search_app/src/static/img/Example.png new file mode 100644 index 0000000..aa61359 Binary files /dev/null and b/examples/legacy/search_app/src/static/img/Example.png differ diff --git a/examples/legacy/search_app/src/static/resources/css/httkdemo.css b/examples/legacy/search_app/src/static/resources/css/httkdemo.css new file mode 100644 index 0000000..262c8e8 --- /dev/null +++ b/examples/legacy/search_app/src/static/resources/css/httkdemo.css @@ -0,0 +1,40 @@ +body { + padding-top: 0rem; +} +.navbar-custom { +} +.navbar-custom .navbar-nav .nav-link { + color: rgba(0,0,0,.5); +} +.navbar-custom .nav-link.active, +.navbar-custom .nav-link:hover { + color: #000000; +} +.border-3 { + border-width:3px !important; +} +.border-orange { + border-color: #f80 !important; +} +.border-darkgrey { + border-color: #888 !important; +} +.orange { + color: #f80 !important; +} +.dropdown .dropdown-menu .dropdown-item:active, .dropdown .dropdown-menu .dropdown-item:hover{background-color: white; color: black;} +.dropdown .dropdown-menu .dropdown-item {background-color: white; color: grey;} + +.align-right { + float: right; + margin: 2em; +} + +h1, h2, h3 { + padding-top: 1em; +} + +.publist a { + font-style: italic; +} + diff --git a/examples/legacy/search_app/src/templates/base_app.httkweb.html b/examples/legacy/search_app/src/templates/base_app.httkweb.html new file mode 100644 index 0000000..4de0943 --- /dev/null +++ b/examples/legacy/search_app/src/templates/base_app.httkweb.html @@ -0,0 +1,22 @@ + + + + + + + + + + {page.title} + + + + + +
    + {content} +
    + + + + diff --git a/examples/legacy/search_app/src/templates/base_default.httkweb.html b/examples/legacy/search_app/src/templates/base_default.httkweb.html new file mode 100644 index 0000000..bd3fad4 --- /dev/null +++ b/examples/legacy/search_app/src/templates/base_default.httkweb.html @@ -0,0 +1,60 @@ + + + + + + + + + +{page.title} + + + + + + + +
    + {content} +
    + + + + diff --git a/examples/legacy/search_app/src/templates/default.httkweb.html b/examples/legacy/search_app/src/templates/default.httkweb.html new file mode 100644 index 0000000..24db9fc --- /dev/null +++ b/examples/legacy/search_app/src/templates/default.httkweb.html @@ -0,0 +1 @@ +{content} diff --git a/examples/legacy/search_app/src/templates/material_details.httkweb.html b/examples/legacy/search_app/src/templates/material_details.httkweb.html new file mode 100644 index 0000000..d17d10d --- /dev/null +++ b/examples/legacy/search_app/src/templates/material_details.httkweb.html @@ -0,0 +1,16 @@ +Materials info + +Tags: + + {result[tags]:repeat: + + + + + } +
    {{item[tag]}}{{item[value]}}
    diff --git a/examples/legacy/search_app/src/templates/search_page.httkweb.html b/examples/legacy/search_app/src/templates/search_page.httkweb.html new file mode 100644 index 0000000..c09cc78 --- /dev/null +++ b/examples/legacy/search_app/src/templates/search_page.httkweb.html @@ -0,0 +1,15 @@ +
    +
    + +

    Search for Materials

    + (Can enter names and symbols)
    +
    +
    +
    +
    +
    +
    +
    +{page.search_result} +{page.material_details} +
    diff --git a/examples/legacy/search_app/src/templates/search_result.httkweb.html b/examples/legacy/search_app/src/templates/search_result.httkweb.html new file mode 100644 index 0000000..6eed70a --- /dev/null +++ b/examples/legacy/search_app/src/templates/search_result.httkweb.html @@ -0,0 +1,19 @@ +

    Search results:

    + + + + + + + +{result:repeat:: + + + + + + + + +} +
    #Compound idFormulaAnonymous formulaNamesSpacegroup
    {{item[index]}}{{item[id]}}{{item[formula]}}{{item[anonymous_formula]}}{{item[names]}}{{item[spacegroup]}}
    diff --git a/examples/legacy/static_simple/publish_legacy_static_simple.py b/examples/legacy/static_simple/publish_legacy_static_simple.py new file mode 100755 index 0000000..c654cfc --- /dev/null +++ b/examples/legacy/static_simple/publish_legacy_static_simple.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import publish + +ROOT = Path(__file__).parent +publish(ROOT / "src", ROOT / "public", "http://127.0.0.1/", compatibility_mode=True) diff --git a/examples/legacy/static_simple/serve_legacy_static_simple.py b/examples/legacy/static_simple/serve_legacy_static_simple.py new file mode 100755 index 0000000..e3f984f --- /dev/null +++ b/examples/legacy/static_simple/serve_legacy_static_simple.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import serve + +ROOT = Path(__file__).parent +serve(ROOT / "src", port=8080, compatibility_mode=True, config_name="config_dynamic") diff --git a/examples/legacy/static_simple/src/config.httkweb b/examples/legacy/static_simple/src/config.httkweb new file mode 100644 index 0000000..d97f29d --- /dev/null +++ b/examples/legacy/static_simple/src/config.httkweb @@ -0,0 +1,4 @@ +--- +menuitems-list: index, contact, bare +--- + diff --git a/examples/legacy/static_simple/src/config_dynamic.httkweb b/examples/legacy/static_simple/src/config_dynamic.httkweb new file mode 100644 index 0000000..dcc358d --- /dev/null +++ b/examples/legacy/static_simple/src/config_dynamic.httkweb @@ -0,0 +1,4 @@ +--- +menuitems-list: index, contact, bare +urls_without_ext: true +--- diff --git a/examples/legacy/static_simple/src/content/404.httkweb b/examples/legacy/static_simple/src/content/404.httkweb new file mode 100644 index 0000000..9d62814 --- /dev/null +++ b/examples/legacy/static_simple/src/content/404.httkweb @@ -0,0 +1,10 @@ +--- +Title: Contact +Date: 2018-08-16 +Version: 1 +Author: Rickard Armiento +Template: 404 +--- +The page could not be found. + + diff --git a/examples/legacy/static_simple/src/content/bare.httkweb b/examples/legacy/static_simple/src/content/bare.httkweb new file mode 100644 index 0000000..11495ac --- /dev/null +++ b/examples/legacy/static_simple/src/content/bare.httkweb @@ -0,0 +1,15 @@ +--- +Title: Bare +Date: 2018-08-16 +Version: 1 +Author: Rickard Armiento +Template: default +Base_template: bare +--- + +Bare +==== + +This page has no header, etc. + + diff --git a/examples/legacy/static_simple/src/content/contact.httkweb b/examples/legacy/static_simple/src/content/contact.httkweb new file mode 100644 index 0000000..e32967a --- /dev/null +++ b/examples/legacy/static_simple/src/content/contact.httkweb @@ -0,0 +1,13 @@ +--- +Title: Contact +Date: 2018-08-16 +Version: 1 +Author: Rickard Armiento +Template: default +--- + +Contact +======= + +You should contact us + diff --git a/examples/legacy/static_simple/src/content/index.httkweb b/examples/legacy/static_simple/src/content/index.httkweb new file mode 100644 index 0000000..6608dc5 --- /dev/null +++ b/examples/legacy/static_simple/src/content/index.httkweb @@ -0,0 +1,34 @@ +--- +Title: Front page +Date: 2018-08-16 +Version: 1 +Author: Rickard Armiento +Template: default + +Address: Example Street 123 + Sweden +Contact: example@example.com +organization: httk +field name: This is a generic bibliographic field. +field name 2: + Generic bibliographic fields may contain multiple body elements. + + Like this. +--- + +This is the front page +====================== + +You can write with *emphasis*, **strong emphasis**, and as `interpreted text`. + +This is an anchor link: `You can do subtitles`_ + +You escape, e.g., \* and \`` with \\ + + +You can do subtitles +-------------------- +Welcome to this page. + +You can do external hyperlinks, like `Python `_. + diff --git a/examples/legacy/static_simple/src/static/img/Example.png b/examples/legacy/static_simple/src/static/img/Example.png new file mode 100644 index 0000000..aa61359 Binary files /dev/null and b/examples/legacy/static_simple/src/static/img/Example.png differ diff --git a/examples/legacy/static_simple/src/static/resources/css/httkdemo.css b/examples/legacy/static_simple/src/static/resources/css/httkdemo.css new file mode 100644 index 0000000..262c8e8 --- /dev/null +++ b/examples/legacy/static_simple/src/static/resources/css/httkdemo.css @@ -0,0 +1,40 @@ +body { + padding-top: 0rem; +} +.navbar-custom { +} +.navbar-custom .navbar-nav .nav-link { + color: rgba(0,0,0,.5); +} +.navbar-custom .nav-link.active, +.navbar-custom .nav-link:hover { + color: #000000; +} +.border-3 { + border-width:3px !important; +} +.border-orange { + border-color: #f80 !important; +} +.border-darkgrey { + border-color: #888 !important; +} +.orange { + color: #f80 !important; +} +.dropdown .dropdown-menu .dropdown-item:active, .dropdown .dropdown-menu .dropdown-item:hover{background-color: white; color: black;} +.dropdown .dropdown-menu .dropdown-item {background-color: white; color: grey;} + +.align-right { + float: right; + margin: 2em; +} + +h1, h2, h3 { + padding-top: 1em; +} + +.publist a { + font-style: italic; +} + diff --git a/examples/legacy/static_simple/src/templates/404.httkweb.html b/examples/legacy/static_simple/src/templates/404.httkweb.html new file mode 100644 index 0000000..912ade8 --- /dev/null +++ b/examples/legacy/static_simple/src/templates/404.httkweb.html @@ -0,0 +1,2 @@ +{content} +{error_404_reason:unquoted:if:Reason given from webserver: {error_404_reason}} diff --git a/examples/legacy/static_simple/src/templates/bare.httkweb.html b/examples/legacy/static_simple/src/templates/bare.httkweb.html new file mode 100644 index 0000000..efd99da --- /dev/null +++ b/examples/legacy/static_simple/src/templates/bare.httkweb.html @@ -0,0 +1,25 @@ + + + + + + + + + + {page.title} + + + + + + +
    +
    + {content} +
    +
    + + + + diff --git a/examples/legacy/static_simple/src/templates/base_default.httkweb.html b/examples/legacy/static_simple/src/templates/base_default.httkweb.html new file mode 100644 index 0000000..e14e145 --- /dev/null +++ b/examples/legacy/static_simple/src/templates/base_default.httkweb.html @@ -0,0 +1,52 @@ + + + + + + + + + + {page.title} + + + + + + + + +
    +
    + {content} +
    +
    + + + + diff --git a/examples/legacy/static_simple/src/templates/default.httkweb.html b/examples/legacy/static_simple/src/templates/default.httkweb.html new file mode 100644 index 0000000..24db9fc --- /dev/null +++ b/examples/legacy/static_simple/src/templates/default.httkweb.html @@ -0,0 +1 @@ +{content} diff --git a/examples/modern/blog/README.md b/examples/modern/blog/README.md new file mode 100644 index 0000000..23ceb15 --- /dev/null +++ b/examples/modern/blog/README.md @@ -0,0 +1,19 @@ +# Modern Blog Example + +Modernized replacement for the old legacy blog example. + +- Markdown content +- Jinja2 templates +- Python function bootstrap via `src/functions/init.py` + +Run: + +```bash +python serve.py +``` + +Publish: + +```bash +python publish.py +``` diff --git a/examples/modern/blog/publish.py b/examples/modern/blog/publish.py new file mode 100644 index 0000000..2b958af --- /dev/null +++ b/examples/modern/blog/publish.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import publish + +ROOT = Path(__file__).parent +publish(ROOT / "src", ROOT / "public", "http://127.0.0.1/") diff --git a/examples/modern/blog/serve.py b/examples/modern/blog/serve.py new file mode 100644 index 0000000..ce3cd1a --- /dev/null +++ b/examples/modern/blog/serve.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import serve + +ROOT = Path(__file__).parent +serve(ROOT / "src", port=8080) diff --git a/examples/modern/blog/src/content/blog.md b/examples/modern/blog/src/content/blog.md new file mode 100644 index 0000000..fc1672c --- /dev/null +++ b/examples/modern/blog/src/content/blog.md @@ -0,0 +1,6 @@ +--- +title: All Blog Posts +template: blog_index +--- + +Browse all posts below. diff --git a/examples/modern/blog/src/content/blogposts/hello-modern.md b/examples/modern/blog/src/content/blogposts/hello-modern.md new file mode 100644 index 0000000..6c59aa5 --- /dev/null +++ b/examples/modern/blog/src/content/blogposts/hello-modern.md @@ -0,0 +1,8 @@ +--- +title: Hello Modern Blog +author: httk-web +date: 2026-03-10 +template: blog_post +--- + +This is the first modern blog post. diff --git a/examples/modern/blog/src/content/blogposts/migration-notes.md b/examples/modern/blog/src/content/blogposts/migration-notes.md new file mode 100644 index 0000000..2c21baa --- /dev/null +++ b/examples/modern/blog/src/content/blogposts/migration-notes.md @@ -0,0 +1,8 @@ +--- +title: Migration Notes +author: httk-web +date: 2026-03-20 +template: blog_post +--- + +This post summarizes migration from legacy templates to Jinja2. diff --git a/examples/modern/blog/src/content/contact.md b/examples/modern/blog/src/content/contact.md new file mode 100644 index 0000000..750a90b --- /dev/null +++ b/examples/modern/blog/src/content/contact.md @@ -0,0 +1,6 @@ +--- +title: Contact +template: default +--- + +Contact us at `contact@example.com`. diff --git a/examples/modern/blog/src/content/index.md b/examples/modern/blog/src/content/index.md new file mode 100644 index 0000000..934a1de --- /dev/null +++ b/examples/modern/blog/src/content/index.md @@ -0,0 +1,6 @@ +--- +title: Modern Blog Home +template: blog_home +--- + +Welcome to the modern blog example. diff --git a/examples/modern/blog/src/functions/init.py b/examples/modern/blog/src/functions/init.py new file mode 100644 index 0000000..0be7c01 --- /dev/null +++ b/examples/modern/blog/src/functions/init.py @@ -0,0 +1,14 @@ +from pathlib import Path + + +def execute(global_data, **kwargs): + content_root = Path(__file__).resolve().parents[1] / "content" / "blogposts" + posts = [] + for path in sorted(content_root.glob("*.md")): + rel = f"blogposts/{path.stem}" + date = global_data["pages"](rel, "date") + posts.append((str(date or ""), rel)) + + posts.sort(reverse=True) + global_data["blogposts"] = [rel for _, rel in posts] + global_data["blogposts_latest"] = global_data["blogposts"][:5] diff --git a/examples/modern/blog/src/templates/base_default.html.j2 b/examples/modern/blog/src/templates/base_default.html.j2 new file mode 100644 index 0000000..cc46843 --- /dev/null +++ b/examples/modern/blog/src/templates/base_default.html.j2 @@ -0,0 +1,21 @@ + + + + + + {{ title or "Modern Blog" }} + + +
    +

    Modern Blog

    + +
    +
    + {{ content }} +
    + + diff --git a/examples/modern/blog/src/templates/blog_home.html.j2 b/examples/modern/blog/src/templates/blog_home.html.j2 new file mode 100644 index 0000000..eea2349 --- /dev/null +++ b/examples/modern/blog/src/templates/blog_home.html.j2 @@ -0,0 +1,13 @@ +
    +

    {{ title }}

    + {{ content }} +

    Latest posts

    + +
    diff --git a/examples/modern/blog/src/templates/blog_index.html.j2 b/examples/modern/blog/src/templates/blog_index.html.j2 new file mode 100644 index 0000000..b429883 --- /dev/null +++ b/examples/modern/blog/src/templates/blog_index.html.j2 @@ -0,0 +1,12 @@ +
    +

    {{ title }}

    + {{ content }} +
      + {% for rel in blogposts %} +
    • + {{ pages(rel, 'title') }} + by {{ pages(rel, 'author') }} ({{ pages(rel, 'date') }}) +
    • + {% endfor %} +
    +
    diff --git a/examples/modern/blog/src/templates/blog_post.html.j2 b/examples/modern/blog/src/templates/blog_post.html.j2 new file mode 100644 index 0000000..0e92ad1 --- /dev/null +++ b/examples/modern/blog/src/templates/blog_post.html.j2 @@ -0,0 +1,5 @@ +
    +

    {{ title }}

    +

    {{ date }} by {{ author }}

    + {{ content }} +
    diff --git a/examples/modern/blog/src/templates/default.html.j2 b/examples/modern/blog/src/templates/default.html.j2 new file mode 100644 index 0000000..6711c79 --- /dev/null +++ b/examples/modern/blog/src/templates/default.html.j2 @@ -0,0 +1,4 @@ +
    +

    {{ title }}

    + {{ content }} +
    diff --git a/examples/modern/minimal/README.md b/examples/modern/minimal/README.md new file mode 100644 index 0000000..b958aaa --- /dev/null +++ b/examples/modern/minimal/README.md @@ -0,0 +1,13 @@ +# Minimal Modern Example + +Serve: + +```bash +python serve.py +``` + +Publish: + +```bash +python publish.py +``` diff --git a/examples/modern/minimal/publish.py b/examples/modern/minimal/publish.py new file mode 100644 index 0000000..2b958af --- /dev/null +++ b/examples/modern/minimal/publish.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import publish + +ROOT = Path(__file__).parent +publish(ROOT / "src", ROOT / "public", "http://127.0.0.1/") diff --git a/examples/modern/minimal/serve.py b/examples/modern/minimal/serve.py new file mode 100644 index 0000000..ce3cd1a --- /dev/null +++ b/examples/modern/minimal/serve.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import serve + +ROOT = Path(__file__).parent +serve(ROOT / "src", port=8080) diff --git a/examples/modern/minimal/src/content/index.md b/examples/modern/minimal/src/content/index.md new file mode 100644 index 0000000..a0542ac --- /dev/null +++ b/examples/modern/minimal/src/content/index.md @@ -0,0 +1,8 @@ +--- +title: Minimal Modern Example +template: default +--- + +# Hello from httk-web + +This is the modern example using Jinja2 templates. diff --git a/examples/modern/minimal/src/templates/base_default.html.j2 b/examples/modern/minimal/src/templates/base_default.html.j2 new file mode 100644 index 0000000..f92656a --- /dev/null +++ b/examples/modern/minimal/src/templates/base_default.html.j2 @@ -0,0 +1,13 @@ + + + + + + {{ title }} + + +
    + {{ content }} +
    + + diff --git a/examples/modern/minimal/src/templates/default.html.j2 b/examples/modern/minimal/src/templates/default.html.j2 new file mode 100644 index 0000000..1050544 --- /dev/null +++ b/examples/modern/minimal/src/templates/default.html.j2 @@ -0,0 +1,4 @@ +
    +

    {{ title }}

    + {{ content }} +
    diff --git a/examples/modern/rst_site/README.md b/examples/modern/rst_site/README.md new file mode 100644 index 0000000..93f36e4 --- /dev/null +++ b/examples/modern/rst_site/README.md @@ -0,0 +1,19 @@ +# Modern RST Site + +Modernized replacement for the old `rst_templator` example. + +- Uses `.rst` content. +- Uses Jinja2 templates (`.html.j2`). +- No legacy templator/web.py syntax. + +Run: + +```bash +python serve.py +``` + +Publish: + +```bash +python publish.py +``` diff --git a/examples/modern/rst_site/publish.py b/examples/modern/rst_site/publish.py new file mode 100644 index 0000000..2b958af --- /dev/null +++ b/examples/modern/rst_site/publish.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import publish + +ROOT = Path(__file__).parent +publish(ROOT / "src", ROOT / "public", "http://127.0.0.1/") diff --git a/examples/modern/rst_site/serve.py b/examples/modern/rst_site/serve.py new file mode 100644 index 0000000..ce3cd1a --- /dev/null +++ b/examples/modern/rst_site/serve.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import serve + +ROOT = Path(__file__).parent +serve(ROOT / "src", port=8080) diff --git a/examples/modern/rst_site/src/content/about.rst b/examples/modern/rst_site/src/content/about.rst new file mode 100644 index 0000000..d3c19b0 --- /dev/null +++ b/examples/modern/rst_site/src/content/about.rst @@ -0,0 +1,7 @@ +:Title: About This Example +:Template: default + +About +===== + +This is a small multi-page RST site. diff --git a/examples/modern/rst_site/src/content/index.rst b/examples/modern/rst_site/src/content/index.rst new file mode 100644 index 0000000..70e1455 --- /dev/null +++ b/examples/modern/rst_site/src/content/index.rst @@ -0,0 +1,14 @@ +:Title: Modern RST Front Page +:Author: httk-web +:Date: 2026-03-27 +:Template: default + +Modern RST Example +================== + +This page is written in **reStructuredText** and rendered through the modern +Jinja2 template layer. + +- No web.py templator syntax +- No `.templator.html` templates +- Python 3.11+ only diff --git a/examples/modern/rst_site/src/templates/base_default.html.j2 b/examples/modern/rst_site/src/templates/base_default.html.j2 new file mode 100644 index 0000000..266e78b --- /dev/null +++ b/examples/modern/rst_site/src/templates/base_default.html.j2 @@ -0,0 +1,19 @@ + + + + + + {{ title or "Modern RST Site" }} + + +
    + +
    +
    + {{ content }} +
    + + diff --git a/examples/modern/rst_site/src/templates/default.html.j2 b/examples/modern/rst_site/src/templates/default.html.j2 new file mode 100644 index 0000000..1050544 --- /dev/null +++ b/examples/modern/rst_site/src/templates/default.html.j2 @@ -0,0 +1,4 @@ +
    +

    {{ title }}

    + {{ content }} +
    diff --git a/examples/modern/search_app/README.md b/examples/modern/search_app/README.md new file mode 100644 index 0000000..333458f --- /dev/null +++ b/examples/modern/search_app/README.md @@ -0,0 +1,15 @@ +# Modern Search Example + +Modernized replacement for the old search app example. + +- Dynamic function injection from query parameters +- Jinja2 templates for page and fragment rendering +- In-memory sample dataset (no legacy `httk.db` dependency) + +Run: + +```bash +python serve.py +``` + +Then open `http://127.0.0.1:8080/` and search for terms like `si` or `oxide`. diff --git a/examples/modern/search_app/publish.py b/examples/modern/search_app/publish.py new file mode 100644 index 0000000..2b958af --- /dev/null +++ b/examples/modern/search_app/publish.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import publish + +ROOT = Path(__file__).parent +publish(ROOT / "src", ROOT / "public", "http://127.0.0.1/") diff --git a/examples/modern/search_app/serve.py b/examples/modern/search_app/serve.py new file mode 100644 index 0000000..ce3cd1a --- /dev/null +++ b/examples/modern/search_app/serve.py @@ -0,0 +1,6 @@ +from pathlib import Path + +from httk.web import serve + +ROOT = Path(__file__).parent +serve(ROOT / "src", port=8080) diff --git a/examples/modern/search_app/src/content/index.md b/examples/modern/search_app/src/content/index.md new file mode 100644 index 0000000..6434025 --- /dev/null +++ b/examples/modern/search_app/src/content/index.md @@ -0,0 +1,8 @@ +--- +title: Modern Search Demo +template: search_page +results-function: search:q:search_results +details-function: details:material_id:material_details +--- + +Use the search box to query the sample material dataset. diff --git a/examples/modern/search_app/src/functions/details.py b/examples/modern/search_app/src/functions/details.py new file mode 100644 index 0000000..943bc78 --- /dev/null +++ b/examples/modern/search_app/src/functions/details.py @@ -0,0 +1,5 @@ +def execute(material_id, global_data, **kwargs): + for row in global_data.get("materials", []): + if row.get("id") == material_id: + return row + return None diff --git a/examples/modern/search_app/src/functions/init.py b/examples/modern/search_app/src/functions/init.py new file mode 100644 index 0000000..e0c9304 --- /dev/null +++ b/examples/modern/search_app/src/functions/init.py @@ -0,0 +1,6 @@ +def execute(global_data, **kwargs): + global_data["materials"] = [ + {"id": "mp-149", "formula": "Si", "name": "Silicon", "spacegroup": "Fd-3m"}, + {"id": "mp-13", "formula": "Fe", "name": "Iron", "spacegroup": "Im-3m"}, + {"id": "mp-22862", "formula": "Al2O3", "name": "Aluminum Oxide", "spacegroup": "R-3c"}, + ] diff --git a/examples/modern/search_app/src/functions/search.py b/examples/modern/search_app/src/functions/search.py new file mode 100644 index 0000000..282a0b7 --- /dev/null +++ b/examples/modern/search_app/src/functions/search.py @@ -0,0 +1,12 @@ +def execute(q, global_data, **kwargs): + needle = str(q).strip().lower() + if not needle: + return [] + + materials = global_data.get("materials", []) + out = [] + for row in materials: + text = " ".join([str(row.get("id", "")), str(row.get("formula", "")), str(row.get("name", ""))]).lower() + if needle in text: + out.append(row) + return out diff --git a/examples/modern/search_app/src/templates/base_default.html.j2 b/examples/modern/search_app/src/templates/base_default.html.j2 new file mode 100644 index 0000000..ce6296a --- /dev/null +++ b/examples/modern/search_app/src/templates/base_default.html.j2 @@ -0,0 +1,13 @@ + + + + + + {{ title or "Modern Search" }} + + +
    + {{ content }} +
    + + diff --git a/examples/modern/search_app/src/templates/material_details.html.j2 b/examples/modern/search_app/src/templates/material_details.html.j2 new file mode 100644 index 0000000..f353aa9 --- /dev/null +++ b/examples/modern/search_app/src/templates/material_details.html.j2 @@ -0,0 +1,10 @@ +{% if result %} + +{% else %} +

    No details found.

    +{% endif %} diff --git a/examples/modern/search_app/src/templates/search_page.html.j2 b/examples/modern/search_app/src/templates/search_page.html.j2 new file mode 100644 index 0000000..42cb258 --- /dev/null +++ b/examples/modern/search_app/src/templates/search_page.html.j2 @@ -0,0 +1,21 @@ +
    +

    {{ title }}

    + {{ content }} + +
    + + +
    + + {% if results %} + {{ results }} + {% endif %} + + {% if details %} +

    Material details

    + {{ details }} + {% endif %} +
    diff --git a/examples/modern/search_app/src/templates/search_results.html.j2 b/examples/modern/search_app/src/templates/search_results.html.j2 new file mode 100644 index 0000000..d925a6a --- /dev/null +++ b/examples/modern/search_app/src/templates/search_results.html.j2 @@ -0,0 +1,24 @@ + + + + + + + + + + + {% for row in result %} + + + + + + + {% endfor %} + +
    IDFormulaNameSpacegroup
    + + {{ row['id'] }} + + {{ row['formula'] }}{{ row['name'] }}{{ row['spacegroup'] }}
    diff --git a/src/httk/web/api.py b/src/httk/web/api.py index efa64f8..0b5472c 100644 --- a/src/httk/web/api.py +++ b/src/httk/web/api.py @@ -15,9 +15,15 @@ def create_asgi_app( *, baseurl: str | None = None, compatibility_mode: bool = False, + config_name: str = "config", debug: bool = False, ) -> Starlette: - config = SiteConfig.from_srcdir(srcdir=srcdir, baseurl=baseurl, compatibility_mode=compatibility_mode) + config = SiteConfig.from_srcdir( + srcdir=srcdir, + baseurl=baseurl, + compatibility_mode=compatibility_mode, + config_name=config_name, + ) engine = SiteEngine(config) return create_app(engine=engine, debug=debug) @@ -29,9 +35,16 @@ def serve( port: int = 8080, baseurl: str | None = None, compatibility_mode: bool = False, + config_name: str = "config", debug: bool = False, ) -> None: - app = create_asgi_app(srcdir=srcdir, baseurl=baseurl, compatibility_mode=compatibility_mode, debug=debug) + app = create_asgi_app( + srcdir=srcdir, + baseurl=baseurl, + compatibility_mode=compatibility_mode, + config_name=config_name, + debug=debug, + ) run_dev_server(app=app, host=host, port=port) @@ -41,7 +54,13 @@ def publish( baseurl: str, *, compatibility_mode: bool = False, + config_name: str = "config", ) -> PublishReport: - config = SiteConfig.from_srcdir(srcdir=srcdir, baseurl=baseurl, compatibility_mode=compatibility_mode) + config = SiteConfig.from_srcdir( + srcdir=srcdir, + baseurl=baseurl, + compatibility_mode=compatibility_mode, + config_name=config_name, + ) engine = SiteEngine(config) return publish_site(engine=engine, outdir=outdir) diff --git a/src/httk/web/engine/site_engine.py b/src/httk/web/engine/site_engine.py index 2352de1..5537234 100644 --- a/src/httk/web/engine/site_engine.py +++ b/src/httk/web/engine/site_engine.py @@ -26,6 +26,8 @@ def __init__(self, config: SiteConfig) -> None: else: self.template_engine = JinjaTemplateEngine(template_dir=config.template_dir) self.function_handler = PythonFunctionHandler(functions_dir=config.functions_dir) + self.global_data: dict[str, object] = self._load_global_config_metadata() + self._run_init_function() def resolve(self, route: str) -> ResolvedRoute: return resolve_route(config=self.config, route=route) @@ -65,8 +67,17 @@ def render( route_key = normalize_route(route) warnings: list[str] = [] - template_name = self._metadata_string(metadata, "template", default="default") - base_template_name = self._metadata_string(metadata, "base_template", default="base_default") + render_mode = "serve" if request is not None else "publish" + template_name = self._metadata_string( + metadata, + f"template_{render_mode}", + default=self._metadata_string(metadata, "template", default="default"), + ) + base_template_name = self._metadata_string( + metadata, + f"base_template_{render_mode}", + default=self._metadata_string(metadata, "base_template", default="base_default"), + ) context = self._build_template_context( route_key=route_key, @@ -121,7 +132,8 @@ def _build_template_context( postvars: dict[str, str], request: HttpRequestContext, ) -> dict[str, object]: - context: dict[str, object] = dict(metadata) + context: dict[str, object] = dict(self.global_data) + context.update(metadata) page_cache: dict[str, tuple[str, dict[str, object]]] = {} def first_value(*values: object) -> object: @@ -170,8 +182,9 @@ def pages(path: str, field: str) -> object: page_html, page_metadata = self._render_content_without_templates(target) page_cache[normalized] = (page_html, page_metadata) - if field in page_metadata: - return page_metadata[field] + metadata_value = self._metadata_field_value(page_metadata, field) + if metadata_value is not None: + return metadata_value if field in {"content", "html"}: return page_html if field == "relurl": @@ -184,11 +197,20 @@ def pages(path: str, field: str) -> object: context["query"] = dict(query) context["postvars"] = dict(postvars) context["request"] = request - context["page"] = { - "relurl": route_key, - "absurl": self._absolute_url(route_key), - "relbaseurl": self._relative_base(route_key), + page_data = { + key: value + for key, value in metadata.items() + if isinstance(key, str) and key and not key.startswith("_") and not key.lower().endswith("-function") } + page_data.update( + { + "relurl": route_key, + "absurl": self._absolute_url(route_key), + "relbaseurl": self._relative_base(route_key), + "functionurl": self._absolute_url(route_key), + } + ) + context["page"] = page_data return context def _apply_function_injections( @@ -200,7 +222,7 @@ def _apply_function_injections( route_key: str, warnings: list[str], ) -> None: - function_keys = [key for key in metadata if key.endswith("-function")] + function_keys = [key for key in metadata if isinstance(key, str) and key.lower().endswith("-function")] for function_key in function_keys: try: @@ -291,3 +313,67 @@ def _metadata_string(self, metadata: dict[str, object], key: str, *, default: st stripped = value.strip() return stripped if stripped else default return default + + def _load_global_config_metadata(self) -> dict[str, object]: + config_name = self.config.config_name.strip() + if not config_name: + return {} + + for suffix, renderer in RENDERERS_BY_SUFFIX.items(): + candidate = self.config.srcdir / f"{config_name}{suffix}" + if not candidate.exists() or not candidate.is_file(): + continue + rendered = renderer.render(candidate) + metadata = dict(rendered.metadata) + return self._normalize_legacy_list_keys(metadata) + + return {} + + def _run_init_function(self) -> None: + init_file = self.config.functions_dir / "init.py" + if not init_file.exists() or not init_file.is_file(): + return + + init_context = dict(self.global_data) + init_context["pages"] = self._global_pages_helper + self.function_handler.execute(function_name="init", params={}, global_data=init_context) + self.global_data.update(init_context) + + def _normalize_legacy_list_keys(self, metadata: dict[str, object]) -> dict[str, object]: + normalized = dict(metadata) + for key, value in list(metadata.items()): + if not isinstance(key, str) or not key.endswith("-list"): + continue + base_key = key[: -len("-list")] + if isinstance(value, list): + normalized[base_key] = value + elif isinstance(value, str): + normalized[base_key] = [x.strip() for x in value.split(",") if x.strip()] + elif value is None: + normalized[base_key] = [] + else: + normalized[base_key] = [value] + return normalized + + def _global_pages_helper(self, path: str, field: str) -> object: + target = self.resolve(path) + if target.kind != "content" or target.source_path is None: + return None + + page_html, page_metadata = self._render_content_without_templates(target) + metadata_value = self._metadata_field_value(page_metadata, field) + if metadata_value is not None: + return metadata_value + if field in {"content", "html"}: + return page_html + if field == "relurl": + return normalize_route(path) + return None + + def _metadata_field_value(self, metadata: dict[str, object], field: str) -> object: + if field in metadata: + return metadata[field] + for key, value in metadata.items(): + if isinstance(key, str) and key.lower() == field.lower(): + return value + return None diff --git a/src/httk/web/model/config.py b/src/httk/web/model/config.py index ee1fcec..221aa10 100644 --- a/src/httk/web/model/config.py +++ b/src/httk/web/model/config.py @@ -12,6 +12,7 @@ class SiteConfig: functions_subdir: str = "functions" baseurl: str | None = None compatibility_mode: bool = False + config_name: str = "config" @classmethod def from_srcdir( @@ -20,8 +21,14 @@ def from_srcdir( *, baseurl: str | None = None, compatibility_mode: bool = False, + config_name: str = "config", ) -> Self: - return cls(srcdir=Path(srcdir).resolve(), baseurl=baseurl, compatibility_mode=compatibility_mode) + return cls( + srcdir=Path(srcdir).resolve(), + baseurl=baseurl, + compatibility_mode=compatibility_mode, + config_name=config_name, + ) @property def content_dir(self) -> Path: @@ -37,4 +44,11 @@ def template_dir(self) -> Path: @property def functions_dir(self) -> Path: - return self.srcdir / self.functions_subdir + primary = self.srcdir / self.functions_subdir + if primary.exists(): + return primary + if self.compatibility_mode: + legacy = self.srcdir / "_functions" + if legacy.exists(): + return legacy + return primary diff --git a/src/httk/web/renderers/_frontmatter.py b/src/httk/web/renderers/_frontmatter.py index 3d886ca..0307356 100644 --- a/src/httk/web/renderers/_frontmatter.py +++ b/src/httk/web/renderers/_frontmatter.py @@ -39,6 +39,8 @@ def split_front_matter(text: str) -> tuple[dict[str, Any], str]: target_list = normalized[base_key] if isinstance(value, list): target_list.extend(value) + elif isinstance(value, str): + target_list.extend([x.strip() for x in value.split(",") if x.strip()]) elif value is not None: target_list.append(value) else: diff --git a/src/httk/web/templating/_legacy_formatter.py b/src/httk/web/templating/_legacy_formatter.py new file mode 100644 index 0000000..b2a7e69 --- /dev/null +++ b/src/httk/web/templating/_legacy_formatter.py @@ -0,0 +1,185 @@ +import ast +import string +from html import escape +from typing import Any, Mapping, Sequence + +from markupsafe import Markup + + +class UnquotedText(str): + """Marker type for values that should bypass auto-escaping.""" + + +class HttkTemplateFormatter(string.Formatter): + def __init__(self) -> None: + super().__init__() + self._current_args: Sequence[Any] = () + self._current_kwargs: Mapping[str, Any] = {} + + def format_field(self, value: Any, spec: str) -> str: + return self._format_field(value, spec, quote=None) + + def _format_field(self, value: Any, spec: str, quote: bool | None) -> str: + if spec == "unquoted" or spec.startswith("unquoted:"): + return self._format_field(value, spec[len("unquoted:") :], quote=False) + if spec == "quote" or spec.startswith("quote:"): + return self._format_field(value, spec[len("quote:") :], quote=True) + + if spec.startswith("repeat:"): + template = spec.partition("::")[-1] + new_kwargs: dict[str, Any] = dict(self._current_kwargs) + if "item" in new_kwargs: + prior_items = new_kwargs.get("items") + if isinstance(prior_items, list): + new_kwargs["items"] = [new_kwargs["item"]] + prior_items + else: + new_kwargs["items"] = [new_kwargs["item"]] + if "index" in new_kwargs: + prior_indices = new_kwargs.get("indices") + if isinstance(prior_indices, list): + new_kwargs["indices"] = [new_kwargs["index"]] + prior_indices + else: + new_kwargs["indices"] = [new_kwargs["index"]] + if "index1" in new_kwargs: + prior_indices = new_kwargs.get("indices") + if isinstance(prior_indices, list): + new_kwargs["indices"] = [new_kwargs["index1"]] + prior_indices + else: + new_kwargs["indices"] = [new_kwargs["index1"]] + + def update_and_return(update: dict[str, Any]) -> dict[str, Any]: + new_kwargs.update(update) + return new_kwargs + + if value is None: + raise ValueError(f"HttkTemplateFormatter: asked to loop over None for spec: {spec}") + if isinstance(value, dict): + return "".join( + [ + self.format( + template, + **(update_and_return({"item": value[key], "index": key, "index1": key})), + ) + for key in value + ] + ) + if not isinstance(value, Sequence): + return "" + return "".join( + [ + self.format( + template, + **(update_and_return({"item": value[i], "index": i, "index1": i + 1})), + ) + for i in range(len(value)) + ] + ) + + if spec == "call" or spec.startswith("call:"): + callargs, _sep, newspec = spec.partition("::") + callargs_list = callargs.split(":") + parsed_callargs: list[Any] = [] + for arg in callargs_list: + if arg.startswith("{") and arg.endswith("}"): + parsed_callargs.append(self.get_field(arg[1:-1], self._current_args, self._current_kwargs)[0]) + else: + parsed_callargs.append(arg) + + if hasattr(value, "__repr__") and ("of float object" in repr(value) or "of int object" in repr(value)): + for callarg_idx in range(1, len(parsed_callargs)): + try: + if parsed_callargs[callarg_idx] == "nan": + parsed_callargs[callarg_idx] = float("nan") + else: + parsed_callargs[callarg_idx] = ast.literal_eval(str(parsed_callargs[callarg_idx])) + except (ValueError, SyntaxError): + pass + + if not callable(value): + raise TypeError(f"Templateengine_httk: tried to call non-callable value: {value!r}") + result = value(*parsed_callargs[1:]) + return self._format_field(result, newspec, quote=quote) + + if spec.startswith("getitem:") or spec.startswith("getattr:"): + x, _dummy, newspec = spec.partition(":")[2].partition("::") + call_func: str | None = None + if ":call" in x: + call_func = x.partition(".")[-1].split(":")[0] + x = x.partition(".")[0] + idx: Any + if x.startswith("{") and x.endswith("}"): + idx = self.get_field(x[1:-1], self._current_args, self._current_kwargs)[0] + else: + idx = x + try: + val = value[idx] if spec.startswith("getitem:") else getattr(value, str(idx)) + except (TypeError, KeyError, AttributeError, IndexError): + try: + int_idx = int(str(idx)) + val = value[int_idx] if spec.startswith("getitem:") else "" + except (TypeError, ValueError, KeyError, IndexError): + return "" + + if newspec == "" and call_func is None: + return str(val) + + if call_func is not None: + val = getattr(val, call_func) + newspec = "call" + spec.partition(":call")[-1] + return self._format_field(val, newspec, quote=quote) + + if ( + spec.startswith("if:") + or spec.startswith("if-not:") + or spec.startswith("if-set:") + or spec.startswith("if-unset:") + ): + outcome = ( + (spec.startswith("if:") and bool(value)) + or (spec.startswith("if-not:") and not bool(value)) + or (spec.startswith("if-set:") and value is not None) + or (spec.startswith("if-unset:") and value is None) + ) + + if "::else::" in spec: + if not outcome: + template = spec.partition("::else::")[-1] + else: + template = spec.partition("::else::")[0].partition("::")[-1] + else: + if not outcome: + return "" + template = spec.partition("::")[-1] + return self.format(template, **dict(self._current_kwargs)) + + if value is None: + return "" + + output = super().format_field(value, spec) + if quote is None: + quote = not isinstance(value, (UnquotedText, Markup)) + if quote: + output = escape(output, quote=True).replace("'", "'") + return output + + def get_field( + self, + field_name: str, + args: Sequence[Any], + kwargs: Mapping[str, Any], + ) -> tuple[Any, Any]: + try: + return super().get_field(field_name, args, kwargs) + except (KeyError, AttributeError): + return None, field_name + + def vformat(self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]) -> str: + previous_args = self._current_args + previous_kwargs = self._current_kwargs + self._current_args = args + self._current_kwargs = kwargs + try: + return super().vformat(format_string, args, kwargs) + finally: + self._current_args = previous_args + self._current_kwargs = previous_kwargs diff --git a/src/httk/web/templating/httk_compat.py b/src/httk/web/templating/httk_compat.py index f69ea1d..0cfecdb 100644 --- a/src/httk/web/templating/httk_compat.py +++ b/src/httk/web/templating/httk_compat.py @@ -1,5 +1,9 @@ from pathlib import Path +from markupsafe import Markup + +from ._legacy_formatter import HttkTemplateFormatter, UnquotedText +from .base import TemplateRenderInput from .jinja2_engine import JinjaTemplateEngine LEGACY_TEMPLATE_SUFFIXES: tuple[str, ...] = ( @@ -21,3 +25,42 @@ class HttkCompatTemplateEngine(JinjaTemplateEngine): def __init__(self, template_dir: Path) -> None: super().__init__(template_dir, template_suffixes=LEGACY_TEMPLATE_SUFFIXES) + self._legacy_formatter = HttkTemplateFormatter() + + def render(self, render_input: TemplateRenderInput) -> str: + template_key = self._resolve_template(render_input.template_name) + base_key = self._resolve_template(render_input.base_template_name) + + context = dict(render_input.context) + content = render_input.content_html + if template_key is not None: + content = self._render_with_resolved_template(template_key, content=content, context=context) + + if base_key is not None: + content = self._render_with_resolved_template(base_key, content=content, context=context) + + return content + + def render_fragment(self, *, template_name: str, context: dict[str, object]) -> str | None: + template_key = self._resolve_fragment_template(template_name) + if template_key is None: + return None + + if template_key.endswith(".httkweb.html"): + template_text = (self.template_dir / template_key).read_text(encoding="utf-8") + return self._legacy_formatter.format(template_text, **dict(context)) + + template = self._environment.get_template(template_key) + return template.render(**context) + + def _render_with_resolved_template(self, template_key: str, *, content: str, context: dict[str, object]) -> str: + if not template_key.endswith(".httkweb.html"): + template = self._environment.get_template(template_key) + working = dict(context) + working["content"] = Markup(content) + return template.render(**working) + + template_text = (self.template_dir / template_key).read_text(encoding="utf-8") + working = dict(context) + working["content"] = UnquotedText(content) + return self._legacy_formatter.format(template_text, **working) diff --git a/tests/test_api.py b/tests/test_api.py index 33540ba..1caf399 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -72,3 +72,111 @@ def test_publish_compatibility_mode_prefers_httkweb_templates(tmp_path: Path) -> rendered = (out / "index.html").read_text(encoding="utf-8") assert "legacy=" in rendered assert "modern=" not in rendered + + +def test_create_asgi_app_compatibility_mode_supports_legacy_repeat(tmp_path: Path) -> None: + src = tmp_path / "src" + (src / "content").mkdir(parents=True) + (src / "static").mkdir(parents=True) + (src / "templates").mkdir(parents=True) + + (src / "content" / "index.md").write_text( + "---\n" + "template: default\n" + "menuitems:\n" + " - alpha\n" + " - beta\n" + "---\n\n" + "hello", + encoding="utf-8", + ) + (src / "templates" / "default.httkweb.html").write_text( + "{content}", + encoding="utf-8", + ) + + app = create_asgi_app(src, compatibility_mode=True) + + with TestClient(app) as client: + response = client.get("/") + + assert response.status_code == 200 + assert "
  • alpha
  • " in response.text + assert "
  • beta
  • " in response.text + assert "hello" in response.text + + +def test_create_asgi_app_compatibility_mode_supports_legacy_pages_call(tmp_path: Path) -> None: + src = tmp_path / "src" + (src / "content").mkdir(parents=True) + (src / "static").mkdir(parents=True) + (src / "templates").mkdir(parents=True) + + (src / "content" / "about.md").write_text("---\ntitle: About Page\n---\n\nAbout body", encoding="utf-8") + (src / "content" / "index.md").write_text( + "---\n" + "template: default\n" + "menuitems:\n" + " - about\n" + "---\n\n" + "home", + encoding="utf-8", + ) + (src / "templates" / "default.httkweb.html").write_text( + "{menuitems:repeat::{{pages:call:{{item}}:title}}}{content}", + encoding="utf-8", + ) + + app = create_asgi_app(src, compatibility_mode=True) + + with TestClient(app) as client: + response = client.get("/") + + assert response.status_code == 200 + assert "About Page" in response.text + + +def test_create_asgi_app_compatibility_mode_loads_config_frontmatter(tmp_path: Path) -> None: + src = tmp_path / "src" + (src / "content").mkdir(parents=True) + (src / "static").mkdir(parents=True) + (src / "templates").mkdir(parents=True) + + (src / "config.httkweb").write_text("---\nmenuitems-list: index, contact, bare\n---\n", encoding="utf-8") + (src / "content" / "index.md").write_text("---\ntemplate: default\n---\n\nhello", encoding="utf-8") + (src / "templates" / "default.httkweb.html").write_text( + "{content}", + encoding="utf-8", + ) + + app = create_asgi_app(src, compatibility_mode=True) + with TestClient(app) as client: + response = client.get("/") + + assert response.status_code == 200 + assert "
  • index
  • " in response.text + assert "
  • contact
  • " in response.text + assert "
  • bare
  • " in response.text + + +def test_create_asgi_app_compatibility_mode_runs_init_function(tmp_path: Path) -> None: + src = tmp_path / "src" + (src / "content").mkdir(parents=True) + (src / "static").mkdir(parents=True) + (src / "templates").mkdir(parents=True) + (src / "functions").mkdir(parents=True) + + (src / "functions" / "init.py").write_text( + "def execute(global_data, **kwargs):\n" + " global_data['website_name'] = 'Legacy Site'\n", + encoding="utf-8", + ) + (src / "content" / "index.md").write_text("---\ntemplate: default\n---\n\nhello", encoding="utf-8") + (src / "templates" / "default.httkweb.html").write_text("{website_name}:{content}", encoding="utf-8") + + app = create_asgi_app(src, compatibility_mode=True) + with TestClient(app) as client: + response = client.get("/") + + assert response.status_code == 200 + assert "Legacy Site:" in response.text