diff --git a/package.json b/package.json index 244e85900..ff22fa31f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vanilla-framework", - "version": "4.16.0", + "version": "4.17.0", "author": { "email": "webteam@canonical.com", "name": "Canonical Webteam" @@ -53,7 +53,8 @@ "_index.scss", "/scss", "!/scss/docs", - "!/scss/standalone" + "!/scss/standalone", + "/templates/_macros" ], "devDependencies": { "@canonical/cookie-policy": "3.6.4", diff --git a/releases.yml b/releases.yml index 263bb89c6..ecf20eace 100644 --- a/releases.yml +++ b/releases.yml @@ -1,3 +1,13 @@ +- version: 4.17.0 + features: + - component: Tiered list + url: /docs/patterns/tiered-list + status: Updated + notes: The tiered list pattern macro has been published. + - component: Hero + url: /docs/patterns/hero + status: Updated + notes: The hero pattern macro has been published. - version: 4.16.0 features: - component: CTA Block / Borderless diff --git a/templates/_macros/hero.jinja b/templates/_macros/vf_hero.jinja similarity index 95% rename from templates/_macros/hero.jinja rename to templates/_macros/vf_hero.jinja index e8637c414..6f2f8169d 100644 --- a/templates/_macros/hero.jinja +++ b/templates/_macros/vf_hero.jinja @@ -1,6 +1,6 @@ # Params -# title: Hero title text (required) -# subtitle: Hero subtitle text +# title_text: Hero title text (required) +# subtitle_text: Hero subtitle text # layout: layout of hero section. Options are '50/50', '50/50-full-width-image', '75/25', '25/75', 'fallback' # is_split_on_medium: whether the layout is split on tablet in 50/50, 25/75, and 75/25 layouts. # If false, the layout is stacked on tablet. @@ -10,13 +10,13 @@ # cta: call-to-action block below the description # image: slot for image content # signpost_image: slot for signpost (left column) image content in 25/75 layout. Required for 25/75 layout. -{% macro hero( - title, - subtitle='', +{% macro vf_hero( + title_text, + subtitle_text='', layout="fallback", is_split_on_medium=false ) -%} - {% set has_subtitle = subtitle|trim|length > 0 %} + {% set has_subtitle = subtitle_text|trim|length > 0 %} {% set description_content = caller('description') %} {% set has_description = description_content|trim|length > 0 %} {% set cta_content = caller('cta') %} @@ -79,14 +79,14 @@ {#- Only add a class attribute if needed -#} 0 %} class="{{ title_class }}"{% endif %}> - {{ title }} + {{ title_text }} {%- endmacro %} {%- macro _hero_subtitle_block() -%} {% if has_subtitle %}

- {{ subtitle }} + {{ subtitle_text }}

{% endif %} {%- endmacro %} @@ -127,7 +127,7 @@ {% endif -%}
- {%- if is_fallback %} + {%- if is_fallback -%}
{{- _hero_description_block() -}} {{ _hero_cta_block() -}} diff --git a/templates/_macros/tiered-list.jinja b/templates/_macros/vf_tiered-list.jinja similarity index 95% rename from templates/_macros/tiered-list.jinja rename to templates/_macros/vf_tiered-list.jinja index 842b24554..c68b2e3f8 100644 --- a/templates/_macros/tiered-list.jinja +++ b/templates/_macros/vf_tiered-list.jinja @@ -9,7 +9,7 @@ # list_item_title_[1-25]: title element of each child list item # list_item_description_[1-25]: description element of each child list item # cta: CTA block element -{% macro tiered_list( +{% macro vf_tiered_list( is_description_full_width_on_desktop=true, is_list_full_width_on_tablet=true) -%} {%- set title_content = caller('title') -%} @@ -62,6 +62,8 @@ {%- set list_item_description_content = caller('list_item_description_' + number|string) -%} {%- set has_description_content = list_item_description_content|trim|length > 0 -%} + {#- Check to see if title/description content exist, since we're + iterating through 25 potential list items -#} {%- if has_title_content and has_description_content -%} {%- if is_list_full_width_on_tablet == true %}
diff --git a/templates/docs/building-vanilla.md b/templates/docs/building-vanilla.md index ddaa7a814..b69e0f81d 100644 --- a/templates/docs/building-vanilla.md +++ b/templates/docs/building-vanilla.md @@ -4,7 +4,8 @@ context: title: Building with Vanilla --- -Here you will find information on how you can use different tools to build Vanilla into production CSS. +Here you will find information on how you can use different tools to build +Vanilla into production HTML and CSS. ## Sass @@ -73,6 +74,39 @@ To watch for changes in your Sass files, add the following script to your `packa Now if you open an extra terminal and run `yarn watch-css`, the CSS will be rebuilt every time your Sass files are edited and saved. +## Jinja Macros + +A variety of Vanilla's components and patterns are offered as +[Jinja macros](https://jinja.palletsprojects.com/templates/#macros), which may +be useful to you if you build sites using the +[Jinja](https://jinja.palletsprojects.com/) templating engine. These macros can +help abstract away some of the complexity of Vanilla's HTML, making producing +complex page layouts simpler and faster. + +In order to pull Vanilla's macros into your project, you may need to expose them +to your webserver or templating engine. An example of this using Flask and Jinja +might look like the following: + +```python +from flask import Flask +from jinja2 import ChoiceLoader, FileSystemLoader + +app = Flask(__name__) + +# ChoiceLoader attempts loading templates from each path in successive order +loader = ChoiceLoader([ + FileSystemLoader('templates'), + FileSystemLoader('node_modules/vanilla-framework/templates/') +]) + +# Loader supplied to jinja_loader overwrites default jinja_loader +app.jinja_loader = loader + +``` + +After making the macros available to your webserver/templating engine, see the +individual component/pattern docs for import and usage instructions. + ## Webpack [Webpack](https://webpack.js.org/) is used to compile JavaScript modules, and can be used to inject Vanilla styles to the DOM. To get set up using Vanilla with Webpack, add the `webpack` and `vanilla-framework` packages to your project dependencies: diff --git a/templates/docs/examples/patterns/hero/hero-50-50-full-width-image.html b/templates/docs/examples/patterns/hero/hero-50-50-full-width-image.html index 9d3e13404..ef18d3fbf 100644 --- a/templates/docs/examples/patterns/hero/hero-50-50-full-width-image.html +++ b/templates/docs/examples/patterns/hero/hero-50-50-full-width-image.html @@ -1,5 +1,5 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/hero.jinja" import hero %} +{% from "_macros/vf_hero.jinja" import vf_hero %} {% block title %}Hero / 50/50 / Full width image{% endblock %} {% block standalone_css %}patterns_all{% endblock %} @@ -7,9 +7,9 @@ {% set is_paper = true %} {% block content %} -{% call(slot) hero( - title='H1 - ideally one line, up to two', - subtitle='H2 placeholder - aim for one line, 2 is acceptable.', +{% call(slot) vf_hero( + title_text='H1 - ideally one line, up to two', + subtitle_text='H2 placeholder - aim for one line, 2 is acceptable.', layout='50/50-full-width-image' ) -%} {%- if slot == 'description' -%} diff --git a/templates/docs/examples/patterns/hero/hero-50-50-split-on-medium.html b/templates/docs/examples/patterns/hero/hero-50-50-split-on-medium.html index a3d5ddafa..27d483f23 100644 --- a/templates/docs/examples/patterns/hero/hero-50-50-split-on-medium.html +++ b/templates/docs/examples/patterns/hero/hero-50-50-split-on-medium.html @@ -1,5 +1,5 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/hero.jinja" import hero %} +{% from "_macros/vf_hero.jinja" import vf_hero %} {% block title %}Hero / 50/50 / Split on Medium{% endblock %} {% block standalone_css %}patterns_all{% endblock %} @@ -7,9 +7,9 @@ {% set is_paper = true %} {% block content %} -{% call(slot) hero( - title='H1 - ideally one line, up to two', - subtitle='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', +{% call(slot) vf_hero( + title_text='H1 - ideally one line, up to two', + subtitle_text='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', layout='50/50', is_split_on_medium=true ) -%} diff --git a/templates/docs/examples/patterns/hero/hero-50-50.html b/templates/docs/examples/patterns/hero/hero-50-50.html index 5e960e748..b80299440 100644 --- a/templates/docs/examples/patterns/hero/hero-50-50.html +++ b/templates/docs/examples/patterns/hero/hero-50-50.html @@ -1,5 +1,5 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/hero.jinja" import hero %} +{% from "_macros/vf_hero.jinja" import vf_hero %} {% block title %}Hero / 50/50{% endblock %} {% block standalone_css %}patterns_all{% endblock %} @@ -7,9 +7,9 @@ {% set is_paper = true %} {% block content %} -{% call(slot) hero( - title='H1 - ideally one line, up to two', - subtitle='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', +{% call(slot) vf_hero( + title_text='H1 - ideally one line, up to two', + subtitle_text='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', layout='50/50' ) -%} {%- if slot == 'description' -%} diff --git a/templates/docs/examples/patterns/hero/hero-75-25.html b/templates/docs/examples/patterns/hero/hero-75-25.html index 2d4fc1681..04caa5430 100644 --- a/templates/docs/examples/patterns/hero/hero-75-25.html +++ b/templates/docs/examples/patterns/hero/hero-75-25.html @@ -1,14 +1,14 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/hero.jinja" import hero %} +{% from "_macros/vf_hero.jinja" import vf_hero %} {% block title %}Hero / 75/25{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% set is_paper = true %} {% block content %} -{% call(slot) hero( - title='H1 - ideally one line, up to two', - subtitle='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', +{% call(slot) vf_hero( + title_text='H1 - ideally one line, up to two', + subtitle_text='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', layout='75/25', is_split_on_medium=true ) -%} diff --git a/templates/docs/examples/patterns/hero/hero-fallback.html b/templates/docs/examples/patterns/hero/hero-fallback.html index cfb746157..35e92ce1e 100644 --- a/templates/docs/examples/patterns/hero/hero-fallback.html +++ b/templates/docs/examples/patterns/hero/hero-fallback.html @@ -1,14 +1,14 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/hero.jinja" import hero %} +{% from "_macros/vf_hero.jinja" import vf_hero %} {% block title %}Hero / Fallback{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% set is_paper = true %} {% block content %} -{% call(slot) hero( - title='H1 - ideally one line, up to two', - subtitle='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', +{% call(slot) vf_hero( + title_text='H1 - ideally one line, up to two', + subtitle_text='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', layout='fallback', is_split_on_medium=true ) -%} diff --git a/templates/docs/examples/patterns/hero/hero-signpost-full-width-image.html b/templates/docs/examples/patterns/hero/hero-signpost-full-width-image.html index 524ff4b0f..720dc9ae8 100644 --- a/templates/docs/examples/patterns/hero/hero-signpost-full-width-image.html +++ b/templates/docs/examples/patterns/hero/hero-signpost-full-width-image.html @@ -1,14 +1,14 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/hero.jinja" import hero %} +{% from "_macros/vf_hero.jinja" import vf_hero %} {% block title %}Hero / Signpost logo / Full width image{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% set is_paper = true %} {% block content %} -{% call(slot) hero( - title='H1 - ideally one line, up to two', - subtitle='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', +{% call(slot) vf_hero( + title_text='H1 - ideally one line, up to two', + subtitle_text='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', layout='25/75' ) -%} {%- if slot == 'signpost_image' -%} diff --git a/templates/docs/examples/patterns/hero/hero-signpost.html b/templates/docs/examples/patterns/hero/hero-signpost.html index f9d1b3c39..9d70e52eb 100644 --- a/templates/docs/examples/patterns/hero/hero-signpost.html +++ b/templates/docs/examples/patterns/hero/hero-signpost.html @@ -1,14 +1,14 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/hero.jinja" import hero %} +{% from "_macros/vf_hero.jinja" import vf_hero %} {% block title %}Hero / Signpost logo{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% set is_paper = True %} {% block content %} -{% call(slot) hero( - title='H1 - ideally one line, up to two', - subtitle='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', +{% call(slot) vf_hero( + title_text='H1 - ideally one line, up to two', + subtitle_text='H2 placeholder - aim for one line, 2 is acceptable, more - use a paragraph', layout='25/75' ) -%} {%- if slot == 'signpost_image' -%} diff --git a/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-description-cta.html b/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-description-cta.html index b70737b0c..3671d85e4 100644 --- a/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-description-cta.html +++ b/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-description-cta.html @@ -1,12 +1,12 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/tiered-list.jinja" import tiered_list %} +{% from "_macros/vf_tiered-list.jinja" import vf_tiered_list %} {% block title %}Tiered list / 50/50 on desktop with description CTA{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% block content %} -{%- call(slot) tiered_list(is_description_full_width_on_desktop=false, is_list_full_width_on_tablet=true) -%} +{%- call(slot) vf_tiered_list(is_description_full_width_on_desktop=false, is_list_full_width_on_tablet=true) -%} {%- if slot == 'title' -%}

H2 - up to two lines; ideally one.

{%- endif -%} diff --git a/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-description.html b/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-description.html index 6ab5ca82a..a27d04ee0 100644 --- a/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-description.html +++ b/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-description.html @@ -1,12 +1,12 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/tiered-list.jinja" import tiered_list %} +{% from "_macros/vf_tiered-list.jinja" import vf_tiered_list %} {% block title %}Tiered list / 50/50 on desktop with description{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% block content %} -{%- call(slot) tiered_list(is_description_full_width_on_desktop=false, is_list_full_width_on_tablet=true) -%} +{%- call(slot) vf_tiered_list(is_description_full_width_on_desktop=false, is_list_full_width_on_tablet=true) -%} {%- if slot == 'title' -%}

H2 - up to two lines; ideally one.

{%- endif -%} diff --git a/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-list-item-cta.html b/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-list-item-cta.html index d932da2b9..7c305e86c 100644 --- a/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-list-item-cta.html +++ b/templates/docs/examples/patterns/tiered-list/50-50-desktop-with-list-item-cta.html @@ -1,12 +1,12 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/tiered-list.jinja" import tiered_list %} +{% from "_macros/vf_tiered-list.jinja" import vf_tiered_list %} {% block title %}Tiered list / 50/50 on desktop with list item CTA{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% block content %} -{%- call(slot) tiered_list(is_description_full_width_on_desktop=false, is_list_full_width_on_tablet=true) -%} +{%- call(slot) vf_tiered_list(is_description_full_width_on_desktop=false, is_list_full_width_on_tablet=true) -%} {%- if slot == 'title' -%}

H2 - up to two lines; ideally one.

{%- endif -%} diff --git a/templates/docs/examples/patterns/tiered-list/50-50-tablet-with-description.html b/templates/docs/examples/patterns/tiered-list/50-50-tablet-with-description.html index 9aabdd0e7..eb92ddd65 100644 --- a/templates/docs/examples/patterns/tiered-list/50-50-tablet-with-description.html +++ b/templates/docs/examples/patterns/tiered-list/50-50-tablet-with-description.html @@ -1,12 +1,12 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/tiered-list.jinja" import tiered_list %} +{% from "_macros/vf_tiered-list.jinja" import vf_tiered_list %} {% block title %}Tiered list / 50/50 on tablet with description{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% block content %} -{%- call(slot) tiered_list(is_description_full_width_on_desktop=true, is_list_full_width_on_tablet=false) -%} +{%- call(slot) vf_tiered_list(is_description_full_width_on_desktop=true, is_list_full_width_on_tablet=false) -%} {%- if slot == 'title' -%}

H2 - up to two lines; ideally one.

{%- endif -%} diff --git a/templates/docs/examples/patterns/tiered-list/50-50-tablet-without-description.html b/templates/docs/examples/patterns/tiered-list/50-50-tablet-without-description.html index 4c1a72c21..69a8278e9 100644 --- a/templates/docs/examples/patterns/tiered-list/50-50-tablet-without-description.html +++ b/templates/docs/examples/patterns/tiered-list/50-50-tablet-without-description.html @@ -1,12 +1,12 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/tiered-list.jinja" import tiered_list %} +{% from "_macros/vf_tiered-list.jinja" import vf_tiered_list %} {% block title %}Tiered list / 50/50 on tablet without description{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% block content %} -{%- call(slot) tiered_list(is_list_full_width_on_tablet=false) -%} +{%- call(slot) vf_tiered_list(is_list_full_width_on_tablet=false) -%} {%- if slot == 'title' -%}

H2 - up to two lines; ideally one.

{%- endif -%} diff --git a/templates/docs/examples/patterns/tiered-list/50-50-with-description.html b/templates/docs/examples/patterns/tiered-list/50-50-with-description.html index ebd1e3bf3..39e695298 100644 --- a/templates/docs/examples/patterns/tiered-list/50-50-with-description.html +++ b/templates/docs/examples/patterns/tiered-list/50-50-with-description.html @@ -1,12 +1,12 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/tiered-list.jinja" import tiered_list %} +{% from "_macros/vf_tiered-list.jinja" import vf_tiered_list %} {% block title %}Tiered list / 50/50 with description{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% block content %} -{%- call(slot) tiered_list(is_description_full_width_on_desktop=false, is_list_full_width_on_tablet=false) -%} +{%- call(slot) vf_tiered_list(is_description_full_width_on_desktop=false, is_list_full_width_on_tablet=false) -%} {%- if slot == 'title' -%}

H2 - up to two lines; ideally one.

{%- endif -%} diff --git a/templates/docs/examples/patterns/tiered-list/full-width-with-description.html b/templates/docs/examples/patterns/tiered-list/full-width-with-description.html index c258ebbaa..f35f5226c 100644 --- a/templates/docs/examples/patterns/tiered-list/full-width-with-description.html +++ b/templates/docs/examples/patterns/tiered-list/full-width-with-description.html @@ -1,12 +1,12 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/tiered-list.jinja" import tiered_list %} +{% from "_macros/vf_tiered-list.jinja" import vf_tiered_list %} {% block title %}Tiered list / Full-width with description{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% block content %} -{%- call(slot) tiered_list(is_description_full_width_on_desktop=true, is_list_full_width_on_tablet=true) -%} +{%- call(slot) vf_tiered_list(is_description_full_width_on_desktop=true, is_list_full_width_on_tablet=true) -%} {%- if slot == 'title' -%}

H2 - up to two lines; ideally one.

{%- endif -%} diff --git a/templates/docs/examples/patterns/tiered-list/full-width-without-description.html b/templates/docs/examples/patterns/tiered-list/full-width-without-description.html index 503a2b131..99ba74f4a 100644 --- a/templates/docs/examples/patterns/tiered-list/full-width-without-description.html +++ b/templates/docs/examples/patterns/tiered-list/full-width-without-description.html @@ -1,12 +1,12 @@ {% extends "_layouts/examples.html" %} -{% from "_macros/tiered-list.jinja" import tiered_list %} +{% from "_macros/vf_tiered-list.jinja" import vf_tiered_list %} {% block title %}Tiered list / Full-width without description{% endblock %} {% block standalone_css %}patterns_all{% endblock %} {% block content %} -{%- call(slot) tiered_list(is_list_full_width_on_tablet=true) -%} +{%- call(slot) vf_tiered_list(is_list_full_width_on_tablet=true) -%} {%- if slot == 'title' -%}

H2 - up to two lines; ideally one.

{%- endif -%} diff --git a/templates/docs/patterns/hero/index.md b/templates/docs/patterns/hero/index.md index c5dea7452..0748d75c0 100644 --- a/templates/docs/patterns/hero/index.md +++ b/templates/docs/patterns/hero/index.md @@ -15,13 +15,13 @@ Depending on the size and composition of your content, you can choose from a var The hero pattern is composed of the following elements: -| Element | Description | -| -------------------- | ------------------------------------------------------------------------ | -| Title (**required**) | h1 title text | -| Subtitle | h2 subtitle text | -| Description | p description text | -| Call to action block | [Call to action block](/docs/patterns/cta-block) beneath the description | -| Image | Main hero visual | +| Element | Description | +| -------------------- | ------------------------------------------------------------- | +| Title (**required**) | Title text (to be placed in `h1` heading) | +| Subtitle | Subtitle text (using `h2` style) | +| Description | Description text (one or more paragraphs) | +| Call to action block | [CTA block](/docs/patterns/cta-block) beneath the description | +| Image | Main hero visual | ## 50/50 @@ -33,7 +33,7 @@ This is useful when your hero contents, especially your image, are not suitably This makes your hero somewhat safer to use, as it helps to avoid awkward content sizing on medium screens, making all content stack vertically. -
+ @@ -43,7 +43,7 @@ You can use .row--50-50 to create a 50/50 hero that is split on lar This is useful when your available vertical space is limited, and your hero contents are suitably balanced to be viewed side-by-side on medium screens. -
+ @@ -52,10 +52,11 @@ View example of the hero pattern in 50/50 that is split on medium and small The above hero layouts place the hero image in the right column by default. However, this is not suitable for very wide images. If you have a very wide image or otherwise want your image to take up the full hero width, place the title by itself in -the first column and place the image in a .p-image-container .is-cover at the same level as the grid columns. +the first column and place the image in a .p-image-container .is-cover at the same level as the grid +columns. This will make the image take up the full width of the hero. -
+ @@ -64,7 +65,7 @@ View example of the hero pattern in 50/50 split with a full-width image If you have a small image that you want to associate with the hero title, you can use the "signpost" layout. This places the image in a small column beside the primary hero content. -
+ @@ -72,7 +73,7 @@ This layout also supports a full-width image. Place the image in a .p-imag level as the hero grid columns to make it take full width beneath the rest of the hero. This is identical to the full-width image layout for the [50/50 layout](#50-50-with-full-width-image). -
+ @@ -85,7 +86,7 @@ The .row--75-25 class is used to maintain the 75/25 split on medium If you find that the image is too tall on small screens, you can use .u-hide--small to hide the image on small screens. -
+ @@ -95,11 +96,184 @@ If you have a very large amount of text content that is difficult to balance wit fallback layout. This places the title and subtitle in their own row above the rest of the hero content. -
+ +## Jinja Macro + +The `vf_hero` Jinja macro can be used to generate a hero pattern. The API for the macro is shown below. + +### Parameters + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRequired?TypeDefaultDescription
+ layout + + Yes + + One of:
+ '50/50',
+ '50/50-full-width-image',
+ '75/25',
+ '25/75',
+ 'fallback' +
+ 'fallback' + + Choice of hero layout +
+ title_text + + Yes + + string + + N/A + + h1 title text +
+ subtitle_text + + No + + string + + N/A + + h2 subtitle text +
+ is_split_on_medium + + Yes + + boolean + + false + + Whether the layout is split on tablet-sized devices +
+
+ +### Slots + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRequired?Description
+ description + + No + + Paragraph-style content below the title and subtitle +
+ cta + + Yes + + Contents of a CTA block beneath + the title and description +
+ image + + Yes, when layout='50/50-full-width-image' + + Image content +
+ signpost_image + + Yes, when layout='25/75' + + Small image (such as a logo) to place in the left column of the + 25/75 Hero +
+
+ ## Import +### Jinja Macro + +To import the Hero Jinja macro, copy the following import statement into your +Jinja template. + +```jinja +{% raw -%} +{% from "_macros/vf_hero.jinja" import vf_hero %} +{%- endraw -%} +``` + +View the [building with Jinja macros guide](/docs/building-vanilla#jinja-macros) +for macro installation instructions. + +### SCSS + Since Patterns leverage many other parts of Vanilla in their composition and content, we recommend [importing the entirety of Vanilla](/docs#install) for full support. diff --git a/templates/docs/patterns/tiered-list/index.md b/templates/docs/patterns/tiered-list/index.md index 9487731e0..916711b67 100644 --- a/templates/docs/patterns/tiered-list/index.md +++ b/templates/docs/patterns/tiered-list/index.md @@ -32,7 +32,7 @@ The tiered list pattern is composed of the following elements: This variant contains a top-level description which is presented side-by-side with its title on desktop screen sizes. -
+ @@ -42,7 +42,7 @@ This variant does not contain a top-level description and its child list is presented with its titles side-by-side with its descriptions on tablet screen sizes. -
+ @@ -51,7 +51,7 @@ View example of the tiered list pattern This variant contains a top-level description and its child list is presented with its titles side-by-side with its descriptions on tablet screen sizes. -
+ @@ -61,7 +61,7 @@ This variant contains a top-level description. Its title and description are presented side-by-side on desktop screen sizes, and its child list is presented side-by-side on tablet screen sizes. -
+ @@ -71,7 +71,7 @@ This variant does not contain a top-level description, and both its title and child list are presented full-width on desktop and tablet screen sizes respectively. -
+ @@ -81,7 +81,7 @@ This variant contains a top-level description, and its title, description, and child list are presented full-width on desktop and tablet screen sizes respectively. -
+ @@ -99,8 +99,150 @@ View example of the tiered list pattern View example of the tiered list pattern
+## Jinja Macro + +The `vf_tiered_list` Jinja macro can be used to generate a tiered list pattern. The API for the macro is shown below. + +### Parameters + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRequired?TypeDefaultDescription
+ is_description_full_width_on_desktop + + Yes + + boolean + + true + + Whether the description element should be full-width on desktop +
+ is_list_full_width_on_tablet + + Yes + + boolean + + true + + Whether the list element should be full-width on tablet +
+
+ +### Slots + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameRequired?Description
+ title + + Yes + + Title sentence displayed at the top of the pattern +
+ description + + No + + Description paragraph displayed below the title +
+ list_item_title_[1-25] + + Yes, at least 1 + + Title element of each child list item; max of 25 +
+ list_item_description_[1-25] + + Yes, at least 1 + + Description element of each child list item; max of 25 +
+ cta + + No + + Contents of a CTA block at the + bottom of the pattern +
+
+ ## Import +### Jinja Macro + +To import the Tiered List Jinja macro, copy the following import statement into +your Jinja template: + +```jinja +{% raw -%} +{% from "_macros/vf_tiered-list.jinja" import vf_tiered_list %} +{%- endraw -%} +``` + +View the [building with Jinja macros guide](/docs/building-vanilla#jinja-macros) +for macro installation instructions. + Since Patterns leverage many other parts of Vanilla in their composition and content, we recommend [importing the entirety of Vanilla](/docs#install) for full support. diff --git a/templates/static/js/example.js b/templates/static/js/example.js index f214336e8..125f01180 100644 --- a/templates/static/js/example.js +++ b/templates/static/js/example.js @@ -5,10 +5,10 @@ // throttling function calls, by Remy Sharp // http://remysharp.com/2010/07/21/throttling-function-calls/ - var throttle = function (fn, delay) { - var timer = null; + const throttle = function (fn, delay) { + let timer = null; return function () { - var context = this, + const context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function () { @@ -17,7 +17,49 @@ }; }; - var CODEPEN_CONFIG = { + /** + * Mapping of example keys to the regex patterns used to strip them out of an example + * @type {{body: RegExp, jinja: RegExp, title: RegExp, head: RegExp}} + */ + const EXAMPLE_CONTENT_PATTERNS = { + body: /]*>((.|[\n\r])*)<\/body>/im, + jinja: /{% block content %}([\s\S]*?){% endblock %}/, + title: /]*>((.|[\n\r])*)<\/title>/im, + head: /]*>((.|[\n\r])*)<\/head>/im, + }; + + /** + * Object representing the structure for language option mappings. + * @typedef {Object} ExampleLanguageConfig + * @property {string} label - Human-readable label. + * @property {string} langIdentifier - Prism language identifier. + */ + + /** + * Mapping of example keys to their configurations. + * @type {{jinja: ExampleLanguageConfig, css: ExampleLanguageConfig, js: ExampleLanguageConfig, html: ExampleLanguageConfig}} + */ + const EXAMPLE_LANGUAGE_OPTION_CONFIG = { + html: { + label: 'HTML', + langIdentifier: 'html', + }, + css: { + label: 'CSS', + langIdentifier: 'css', + }, + js: { + label: 'JS', + langIdentifier: 'js', + }, + jinja: { + label: 'Jinja', + // While `jinja2` is an option on Prism, it does not seem to highlight syntax properly. So use HTML instead. + langIdentifier: 'html', + }, + }; + + const CODEPEN_CONFIG = { title: 'Vanilla framework example', head: "", stylesheets: [ @@ -33,37 +75,56 @@ }; document.addEventListener('DOMContentLoaded', function () { - var examples = document.querySelectorAll('.js-example'); + const examples = document.querySelectorAll('.js-example'); - [].slice.call(examples).forEach(fetchExample); + [].slice.call(examples).forEach((placementElement) => { + renderExample(placementElement).catch((error) => { + console.error('Failed to render example', {placementElement, error}); + }); + }); }); - function fetchExample(exampleElement) { - var link = exampleElement.href; - var request = new XMLHttpRequest(); - - request.onreadystatechange = function () { - if (request.status === 200 && request.readyState === 4) { - var html = request.responseText; - renderExample(exampleElement, html); - exampleElement.style.display = 'none'; + /** + * `fetch()` wrapper that throws an error if the response is not OK. + * @param {String} url Address to fetch + * @param {RequestInit} opts Options for the fetch request + * @returns {Promise} Response object + * @throws {Error} If the response is not in the 200 (OK) range + */ + const fetchResponse = async function (url, opts = {}) { + try { + const response = await fetch(url, opts); + if (!response.ok) { + throw new Error(`Failed to fetch example ${url} with status ${response.status}`); } - }; + return response; + } catch (err) { + console.error('An error occurred while performing a fetch request', err); + throw err; + } + }; - request.open('GET', link, true); - request.send(null); - } + /** + * Fetch the response text of a URL. + * @param {String} url Address to fetch + * @returns {Promise} Response text + * @throws {Error} If the response is not in the 200 (OK) range + */ + const fetchResponseText = async function (url) { + return fetchResponse(url).then((response) => response.text()); + }; /** * Format source code based on language * @param {String} source - source code to format - * @param {String} lang - language of the source code + * @param {'html'|'jinja'|'js'|'css'} lang - language of the source code * @returns {String} formatted source code */ function formatSource(source, lang) { try { switch (lang) { case 'html': + case 'jinja': return window.html_beautify(source, {indent_size: 2}); case 'js': return window.js_beautify(source, {indent_size: 2}); @@ -79,128 +140,221 @@ } } - function createPreCode(source, lang) { - var code = document.createElement('code'); - code.appendChild(document.createTextNode(formatSource(source, lang))); + /** + * Create `pre`-formatted code for a block of source + * @param {String} source Formatted source code + * @param {'html'|'jinja'|'js'|'css'} lang Language of the source code + * @param {Boolean} isHidden Whether the pre-code should be hidden initially + * @returns {HTMLPreElement} Code snippet containing the source code + */ + function createPreCode(source, lang, isHidden = true) { + const code = document.createElement('code'); + code.appendChild(document.createTextNode(source)); - var pre = document.createElement('pre'); + const pre = document.createElement('pre'); pre.classList.add('p-code-snippet__block'); // TODO: move max-height elsewhere to CSS? pre.style.maxHeight = '300px'; - if (lang !== 'html') { + if (isHidden) { pre.classList.add('u-hide'); } if (lang) { pre.setAttribute('data-lang', lang); - pre.classList.add('language-' + lang); + pre.classList.add('language-' + (EXAMPLE_LANGUAGE_OPTION_CONFIG[lang]?.langIdentifier || lang)); } pre.appendChild(code); return pre; } - function renderExample(placementElement, html) { - var bodyPattern = /]*>((.|[\n\r])*)<\/body>/im; - var titlePattern = /]*>((.|[\n\r])*)<\/title>/im; - var headPattern = /]*>((.|[\n\r])*)<\/head>/im; - - var title = titlePattern.exec(html)[1].trim(); - var bodyHTML = bodyPattern.exec(html)[1].trim(); - var headHTML = headPattern.exec(html)[1].trim(); - - var htmlSource = stripScriptsFromSource(bodyHTML); - var jsSource = getScriptFromSource(bodyHTML); - var cssSource = getStyleFromSource(headHTML); - var externalScripts = getExternalScriptsFromSource(html); - var codePenData = { - html: htmlSource, - css: cssSource, - js: jsSource, - externalJS: externalScripts, - }; + /** + * Extract a section of HTML from the document + * @param {'body'|'jinja'|'title'|'head'} sectionKey The key/type of content to be extracted + * @param {String} documentHTML The example's full HTML content. This may be rendered or raw Jinja template. + * @returns {String} The requested section of the document, or an empty string if it was not found. + */ + function getExampleSection(sectionKey, documentHTML) { + const pattern = EXAMPLE_CONTENT_PATTERNS[sectionKey]; + return pattern?.exec(documentHTML)?.[1]?.trim() || ''; + } + + /** + * Fetches the rendered HTML of an example and extracts the relevant sections for rendering and code snippets. + * @param {HTMLAnchorElement} placementElement The placeholder element for the example + * @returns {Promise<{renderedHtml: String, bodyHtml: String, title: String, jsSource: String, externalScripts: NodeListOf, cssSource: String}>} The extracted sections of the example + */ + async function fetchRenderedHtml(placementElement) { + const renderedHtml = await fetchResponseText(placementElement.href); + let bodyHtml = getExampleSection('body', renderedHtml); + + // Extract JS from the body before we strip it out + const jsSource = formatSource(getScriptFromSource(bodyHtml), 'js'); + bodyHtml = formatSource(stripScriptsFromSource(bodyHtml), 'html'); + + const title = getExampleSection('title', renderedHtml).split('|')[0]; + const headHtml = getExampleSection('head', renderedHtml); + const cssSource = formatSource(getStyleFromSource(headHtml), 'css'); + const externalScripts = getExternalScriptsFromSource(renderedHtml); - var height = placementElement.getAttribute('data-height'); + return {renderedHtml, bodyHtml, title, jsSource, externalScripts, cssSource}; + } - var codeSnippet = document.createElement('div'); + /** + * Fetches the raw Jinja template of an example and returns the Jinja content block + * @param {HTMLElement} placementElement The placeholder element for the example + * @returns {Promise} The Jinja content block of the example + */ + async function fetchJinjaContentBlock(placementElement) { + // Raw templates are not served at standalone paths, so strip it from the URL if it was found. + const exampleUrl = new URL(`${placementElement.href.replace(/standalone/, '/')}`); + // Add `?raw=true` query parameter to the URL to request the raw Jinja template + const queryParams = new URLSearchParams(exampleUrl.search); + queryParams.set('raw', true); + exampleUrl.search = queryParams.toString(); + + const rawJinjaTemplate = await fetchResponseText(exampleUrl.toString()); + return formatSource(getExampleSection('jinja', rawJinjaTemplate), 'jinja'); + } + + /** + * Replaces an example placeholder element with its rendered result and code snippet. + * @param {HTMLAnchorElement} placementElement `a.js-example` element used as a placeholder for the example to render + */ + async function renderExample(placementElement) { + const codeSnippet = document.createElement('div'); codeSnippet.classList.add('p-code-snippet', 'is-bordered'); - var header = document.createElement('div'); + const header = document.createElement('div'); header.classList.add('p-code-snippet__header'); - var titleEl = document.createElement('h5'); + + const titleEl = document.createElement('h5'); titleEl.classList.add('p-code-snippet__title'); - // example page title is structured as "... | Examples | Vanilla documentation" - // we want to strip anything after first | pipe - titleEl.innerText = title.split('|')[0]; + // Example data will be asynchronously fetched and placed here on promise resolution. + const srcData = { + html: undefined, + renderedHtml: undefined, + jinja: undefined, + css: undefined, + js: undefined, + codePen: undefined, + title: undefined, + }; + + const exampleRequests = []; + const fetchHtml = fetchRenderedHtml(placementElement).then(({renderedHtml, bodyHtml, title, jsSource, externalScripts, cssSource}) => { + // There are required, so throw if they failed + if (renderedHtml && bodyHtml && title) { + srcData.renderedHtml = renderedHtml; + srcData.html = bodyHtml; + srcData.title = title; + } else { + throw new Error('Failed to fetch HTML for example iframe and HTML source.'); + } + + // The rest of the views are optional + srcData.js = jsSource; + srcData.css = cssSource; + srcData.codePen = { + html: bodyHtml, + css: cssSource, + js: jsSource, + externalJS: externalScripts, + }; + }); + exampleRequests.push(fetchHtml); + + if (placementElement.getAttribute('data-lang') === 'jinja') { + // Perform jinja template fetching if the example was marked as a Jinja template + const fetchJinja = fetchJinjaContentBlock(placementElement).then((contentBlock) => { + const hasJinjaTemplate = contentBlock?.length > 0; + if (hasJinjaTemplate) { + srcData.jinja = contentBlock; + } + }); + exampleRequests.push(fetchJinja); + } + + // Perform as much of the data fetching and processing as possible in parallel + await Promise.all(exampleRequests); + // Code after this point depends on the data above being fully fetched, so must come after an `await` + + titleEl.innerText = srcData.title; header.appendChild(titleEl); codeSnippet.appendChild(header); - placementElement.parentNode.insertBefore(codeSnippet, placementElement); - renderIframe(codeSnippet, html, height); - - // Build code block structure - var options = ['html']; - codeSnippet.appendChild(createPreCode(htmlSource, 'html')); - if (jsSource) { - codeSnippet.appendChild(createPreCode(jsSource, 'js')); - options.push('js'); - } - if (cssSource) { - codeSnippet.appendChild(createPreCode(cssSource, 'css')); - options.push('css'); - } + renderIframe(codeSnippet, srcData.renderedHtml, placementElement.getAttribute('data-height')); - renderDropdown(header, options); - renderCodePenEditLink(codeSnippet, codePenData); + // Gather the languages that have source code available, in the order they should be displayed + // We can't rely on order of these languages being made available in the promise blocks above due to async nature + const languageOptions = ['jinja', 'html', 'js', 'css'].filter((lang) => srcData[lang]); + const sourceBlocks = languageOptions + // THe first language option that was found is displayed by default. The rest are viewable using dropdown. + .map((lang, idx) => createPreCode(srcData[lang], lang, idx > 0)); + // Code snippet must be populated with code before Prism can highlight it + sourceBlocks.forEach((block) => codeSnippet.appendChild(block)); if (Prism) { Prism.highlightAllUnder(codeSnippet); } + + if (srcData.codePen) { + renderCodePenEditLink(codeSnippet, srcData.codePen); + } + + renderDropdown(header, languageOptions); + + // The example has been rendered successfully, hide the placeholder element. + placementElement.style.display = 'none'; } - function renderDropdown(header, options) { + /** + * Renders a dropdown containing the code snippet options, allowing user to switch between multiple views. + * @param {HTMLDivElement} codeSnippetHeader The header element of the code snippet + * @param {('html'|'jinja'|'js'|'css')[]} codeSnippetModes List of code snippet mode options + */ + function renderDropdown(codeSnippetHeader, codeSnippetModes) { // only add dropdown if there is more than one code block - if (options.length > 1) { - var dropdownsEl = document.createElement('div'); - dropdownsEl.classList.add('p-code-snippet__dropdowns'); - - var selectEl = document.createElement('select'); - selectEl.classList.add('p-code-snippet__dropdown'); - - options.forEach(function (option) { - var optionHTML = document.createElement('option'); - optionHTML.value = option.toLowerCase(); - optionHTML.innerText = option.toUpperCase(); - selectEl.appendChild(optionHTML); - }); + if (codeSnippetModes.length <= 1) return; - dropdownsEl.appendChild(selectEl); - header.appendChild(dropdownsEl); - attachDropdownEvents(selectEl); - } + const dropdownsEl = document.createElement('div'); + dropdownsEl.classList.add('p-code-snippet__dropdowns'); + + const selectEl = document.createElement('select'); + selectEl.classList.add('p-code-snippet__dropdown'); + + codeSnippetModes.forEach(function (option) { + const optionHTML = document.createElement('option'); + optionHTML.value = option.toLowerCase(); + optionHTML.innerText = EXAMPLE_LANGUAGE_OPTION_CONFIG[option]?.label || option.toLowerCase(); + selectEl.appendChild(optionHTML); + }); + + dropdownsEl.appendChild(selectEl); + codeSnippetHeader.appendChild(dropdownsEl); + attachDropdownEvents(selectEl); } function resizeIframe(iframe) { if (iframe.contentDocument.readyState == 'complete') { - var frameHeight = iframe.contentDocument.body.scrollHeight; - var style = iframe.contentWindow.getComputedStyle(iframe.contentDocument.body); + const frameHeight = iframe.contentDocument.body.scrollHeight; iframe.height = frameHeight + 32 + 'px'; // accommodate for body margin } } function renderIframe(container, html, height) { - var iframe = document.createElement('iframe'); + const iframe = document.createElement('iframe'); if (height) { iframe.height = height + 'px'; } container.appendChild(iframe); - var doc = iframe.contentWindow.document; + const doc = iframe.contentWindow.document; doc.open(); doc.write(html); doc.close(); @@ -208,7 +362,7 @@ // if height wasn't specified, try to determine it from example content if (!height) { // Wait for content to load before determining height - var resizeInterval = setInterval(function () { + const resizeInterval = setInterval(function () { if (iframe.contentDocument.readyState == 'complete') { resizeIframe(iframe); clearInterval(resizeInterval); @@ -233,16 +387,16 @@ } function renderCodePenEditLink(snippet, sourceData) { - var html = sourceData.html === null ? '' : sourceData.html; - var css = sourceData.css === null ? '' : sourceData.css; - var js = sourceData.js === null ? '' : sourceData.js; + const html = sourceData.html === null ? '' : sourceData.html; + const css = sourceData.css === null ? '' : sourceData.css; + const js = sourceData.js === null ? '' : sourceData.js; if (html || css || js) { - var container = document.createElement('div'); - var form = document.createElement('form'); - var input = document.createElement('input'); - var link = document.createElement('a'); - var data = { + const container = document.createElement('div'); + const form = document.createElement('form'); + const input = document.createElement('input'); + const link = document.createElement('a'); + const data = { title: CODEPEN_CONFIG.title, head: CODEPEN_CONFIG.head, html: html, @@ -252,7 +406,7 @@ js_external: sourceData.externalJS.join(';'), }; // Replace double quotes to avoid errors on CodePen - var JSONstring = JSON.stringify(data).replace(/"/g, '"').replace(/'/g, '''); + const JSONstring = JSON.stringify(data).replace(/"/g, '"').replace(/'/g, '''); container.classList.add('p-code-snippet__header'); @@ -290,17 +444,17 @@ } function getStyleFromSource(source) { - var div = document.createElement('div'); + const div = document.createElement('div'); div.innerHTML = source; - var style = div.querySelector('style'); + const style = div.querySelector('style'); return style ? style.innerHTML.trim() : null; } function stripScriptsFromSource(source) { - var div = document.createElement('div'); + const div = document.createElement('div'); div.innerHTML = source; - var scripts = div.getElementsByTagName('script'); - var i = scripts.length; + const scripts = div.getElementsByTagName('script'); + let i = scripts.length; while (i--) { scripts[i].parentNode.removeChild(scripts[i]); } @@ -308,16 +462,16 @@ } function getScriptFromSource(source) { - var div = document.createElement('div'); + const div = document.createElement('div'); div.innerHTML = source; - var script = div.querySelector('script'); + const script = div.querySelector('script'); return script ? script.innerHTML.trim() : null; } function getExternalScriptsFromSource(source) { - var div = document.createElement('div'); + const div = document.createElement('div'); div.innerHTML = source; - var scripts = div.querySelectorAll('script[src]'); + let scripts = div.querySelectorAll('script[src]'); scripts = [].slice.apply(scripts).map(function (s) { return s.src; }); @@ -338,12 +492,12 @@ */ function attachDropdownEvents(dropdown) { dropdown.addEventListener('change', function (e) { - var snippet = e.target.closest('.p-code-snippet'); + const snippet = e.target.closest('.p-code-snippet'); // toggle code blocks visibility based on selected language - for (var i = 0; i < dropdown.options.length; i++) { - var lang = dropdown.options[i].value; - var block = snippet && snippet.querySelector("[data-lang='" + lang + "']"); + for (let i = 0; i < dropdown.options.length; i++) { + const lang = dropdown.options[i].value; + const block = snippet && snippet.querySelector("[data-lang='" + lang + "']"); if (lang === e.target.value) { block.classList.remove('u-hide'); diff --git a/templates/static/js/scripts.js b/templates/static/js/scripts.js index 2330abf55..8ab701ca6 100644 --- a/templates/static/js/scripts.js +++ b/templates/static/js/scripts.js @@ -2,10 +2,10 @@ (function () { // throttling function calls, by Remy Sharp // http://remysharp.com/2010/07/21/throttling-function-calls/ - var throttle = function (fn, delay) { - var timer = null; + const throttle = function (fn, delay) { + let timer = null; return function () { - var context = this, + let context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function () { diff --git a/webapp/app.py b/webapp/app.py index fc6a24b89..504d8ac1c 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -3,6 +3,7 @@ import json import os import random +import werkzeug import yaml import urllib import markupsafe @@ -286,16 +287,30 @@ def standalone_examples_index(): ) -@app.route("/docs/examples/standalone/") -def standalone_example(example_path): +@app.route("/docs/examples/") +def example(example_path, is_standalone=False): try: + is_raw = (flask.request.args.get("raw") or "").lower() == "true" + # If the user has requested the raw template, serve it directly + if is_raw: + raw_example_path = f"../templates/docs/examples/{example_path}.html" + # separate directory from file name so that flask.send_from_directory() can prevent malicious file access + raw_example_directory = os.path.dirname(raw_example_path) + raw_example_file_name = os.path.basename(raw_example_path) + return flask.send_from_directory(raw_example_directory, raw_example_file_name, mimetype="text/raw") + return flask.render_template( - f"docs/examples/{example_path}.html", is_standalone=True + f"docs/examples/{example_path}.html", is_standalone=is_standalone ) - except jinja2.exceptions.TemplateNotFound: + except (jinja2.exceptions.TemplateNotFound, werkzeug.exceptions.NotFound): return flask.abort(404) +@app.route("/docs/examples/standalone/") +def standalone_example(example_path): + return example(example_path, is_standalone=True) + + @app.route("/contribute") def contribute_index(): all_contributors = _get_contributors()