-
Notifications
You must be signed in to change notification settings - Fork 114
Add include_javascript(), include_css(), and include_html()
#127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 9 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
b836344
wip
cpsievert ecb3ce3
wip2
cpsievert 1c3d465
Merge branch 'main' into include-helpers
49d162b
Linting
0f85347
PR Comments
06de6d3
Pass attrributes to tags.script
8c33228
linting
5427d77
Typing
bab42a8
Merge remote-tracking branch 'origin/main' into include-helpers
7c0868a
Merge branch 'main' into include-helpers
3b5b9c0
Update shiny/ui/_include_helpers.py
7de3978
Update shiny/ui/_include_helpers.py
ee30073
Update shiny/ui/_include_helpers.py
2b5010f
Update shiny/ui/_include_helpers.py
2c3bd1f
PR comments
e325acb
Check if asset files exist.
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import os | ||
|
|
||
| from shiny import * | ||
|
|
||
| css_file = os.path.join(os.path.dirname(__file__), "css/styles.css") | ||
|
|
||
| app_ui = ui.page_fluid( | ||
| "Almost before we knew it, we had left the ground!!!", | ||
| ui.include_css(css_file, method="link_files"), | ||
| ui.div( | ||
| # Style individual elements with an attribute dictionary. | ||
| {"style": "font-weight: bold"}, | ||
| ui.p("Bold text"), | ||
| ), | ||
| ) | ||
|
|
||
| app = App(app_ui, None) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| @font-face { | ||
| font-family: 'Square Peg'; | ||
| src: url('SquarePeg-Regular.ttf'); | ||
gshotwell marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| body { | ||
| font-size: 3rem; | ||
| background-color: pink | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import os | ||
|
|
||
| from shiny import * | ||
|
|
||
| js_file = os.path.join(os.path.dirname(__file__), "js/app.js") | ||
gshotwell marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| app_ui = ui.page_fluid( | ||
| "If you see this page before 'OK'-ing the alert box, something went wrong", | ||
| ui.include_js(js_file), | ||
| ) | ||
|
|
||
|
|
||
| app = App(app_ui, None) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| alert("If you're seeing this, the javascript file was included successfully."); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| from __future__ import annotations | ||
|
|
||
| __all__ = ("include_js", "include_css") | ||
|
|
||
| import glob | ||
| import hashlib | ||
| import os | ||
| import shutil | ||
| import tempfile | ||
|
|
||
| # TODO: maybe these include_*() functions should actually live in htmltools? | ||
| from htmltools import HTMLDependency, Tag, TagAttrValue, tags | ||
|
|
||
| from .._docstring import add_example | ||
| from .._typing_extensions import Literal | ||
|
|
||
| # TODO: it's bummer that, when method="link_files" and path is in the same directory | ||
| # as the app, the app's source will be included. Should we just not copy .py/.r files? | ||
|
|
||
|
|
||
| @add_example() | ||
| def include_js( | ||
| path: str, | ||
gshotwell marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| *, | ||
| method: Literal["link", "link_files", "inline"] = "link", | ||
| **kwargs: TagAttrValue, | ||
| ) -> Tag: | ||
| """ | ||
| Include a JavaScript file | ||
|
|
||
| Parameters | ||
| ---------- | ||
| path | ||
| A path to a JS file. | ||
| method | ||
| One of the following: | ||
| * ``"link"``: Link to the JS file via a :func:`~ui.tags.script` tag. This | ||
| method is generally preferrable to ``"inline"`` since it allows the browser | ||
| to cache the file. | ||
| * ``"link_files"``: Same as ``"link"``, but also allow for the CSS file to | ||
| request other files within ``path``'s immediate parent directory (e.g., | ||
| ``fetch()`` the contents of another file). Note that this isn't the default | ||
gshotwell marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| behavior because you should **be careful not to include files in the same | ||
| directory as ``path`` that contain sensitive information**. A good general | ||
| rule of thumb to follow is to have ``path`` be located in a subdirectory of | ||
| the app directory. For example, if the app's source is located at | ||
| ``/app/app.py``, then ``path`` should be somewhere like | ||
| ``/app/js/custom.js`` (and all the other relevant accompanying 'safe' files | ||
| should be located under ``/app/js/``). | ||
| * ``"inline"``: Inline the JS file contents within a :func:`~ui.tags.script` | ||
| tag. | ||
| *kwargs | ||
gshotwell marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Attributes which are passed on to `~ui.tags.script` | ||
|
|
||
|
|
||
| Returns | ||
| ------- | ||
| A :func:`~ui.tags.script` tag. | ||
gshotwell marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Note | ||
| ---- | ||
| This places a :func:`~ui.tags.script` tag in the :func:`~ui.tags.body` of the | ||
| document. If instead, you want to place the tag in the :func:`~ui.tags.head` of the | ||
| document, you can wrap it in ``head_content`` (in this case, just make sure you're | ||
| aware that the DOM probably won't be ready when the script is executed). | ||
|
|
||
| .. code-block:: python | ||
|
|
||
|
|
||
|
|
||
| ui.fluidPage( | ||
gshotwell marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| head_content(ui.include_js("custom.js")), | ||
| ) | ||
|
|
||
| # Alternately you can inline Javscript by changing the method. | ||
| ui.fluidPage( | ||
| head_content(ui.include_js("custom.js", method = "inline")), | ||
| ) | ||
|
|
||
| See Also | ||
| -------- | ||
| ~ui.tags.script | ||
| ~include_css | ||
| """ | ||
|
|
||
| if method == "inline": | ||
| return tags.script(read_utf8(path), type="text/javascript", **kwargs) | ||
gshotwell marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| include_files = method == "link_files" | ||
| path_dest, hash = maybe_copy_files(path, include_files) | ||
|
|
||
| dep, src = create_include_dependency("include-js-" + hash, path_dest, include_files) | ||
|
|
||
| return tags.script(dep, src=src, **kwargs) | ||
|
|
||
|
|
||
| @add_example() | ||
| def include_css( | ||
| path: str, *, method: Literal["link", "link_files", "inline"] = "link" | ||
| ) -> Tag: | ||
| """ | ||
| Include a CSS file | ||
|
|
||
| Parameters | ||
| ---------- | ||
| path | ||
| A path to a CSS file. | ||
| method | ||
| One of the following: | ||
| * ``"link"``: Link to the CSS file via a :func:`~ui.tags.link` tag. This | ||
| method is generally preferrable to ``"inline"`` since it allows the browser | ||
| to cache the file. | ||
| * ``"link_files"``: Same as ``"link"``, but also allow for the CSS file to | ||
| request other files within ``path``'s immediate parent directory (e.g., | ||
| ``@import()`` another file). Note that this isn't the default behavior | ||
| because you should **be careful not to include files in the same directory | ||
| as ``path`` that contain sensitive information**. A good general rule of | ||
| thumb to follow is to have ``path`` be located in a subdirectory of the app | ||
| directory. For example, if the app's source is located at ``/app/app.py``, | ||
| then ``path`` should be somewhere like ``/app/css/custom.css`` (and all the | ||
| other relevant accompanying 'safe' files should be located under | ||
| ``/app/css/``). | ||
| * ``"inline"``: Inline the CSS file contents within a :func:`~ui.tags.style` | ||
| tag. | ||
|
|
||
| Returns | ||
| ------- | ||
| If ``method="inline"``, returns a :func:`~ui.tags.style` tag; otherwise, returns a | ||
| :func:`~ui.tags.link` tag. | ||
gshotwell marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Note | ||
| ---- | ||
| By default this places a :func:`~ui.tags.link` (or :func:`~ui.tags.style`) tag in | ||
| the :func:`~ui.tags.body` of the document, which isn't optimal for performance, and | ||
| may result in a Flash of Unstyled Content (FOUC). To instead place the CSS in the | ||
| :func:`~ui.tags.head` of the document, you can wrap it in ``head_content``: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from htmltools import head_content from shiny import ui | ||
|
|
||
| ui.fluidPage( | ||
| head_content(ui.include_css("custom.css")), | ||
|
|
||
| # You can also inline css by passing a dictionary with a `style` element. | ||
| ui.div( | ||
| {"style": "font-weight: bold;"}, | ||
| ui.p("Some text!"), | ||
| ) | ||
| ) | ||
gshotwell marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| See Also | ||
| -------- | ||
| ~ui.tags.style | ||
| ~ui.tags.link | ||
| ~include_js | ||
| """ | ||
|
|
||
| if method == "inline": | ||
| return tags.style(read_utf8(path), type="text/css") | ||
|
|
||
| include_files = method == "link_files" | ||
| path_dest, hash = maybe_copy_files(path, include_files) | ||
|
|
||
| dep, src = create_include_dependency( | ||
| "include-css-" + hash, path_dest, include_files | ||
| ) | ||
|
|
||
| return tags.link(dep, href=src, rel="stylesheet") | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Include helpers | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def create_include_dependency( | ||
| name: str, path: str, include_files: bool | ||
| ) -> tuple[HTMLDependency, str]: | ||
| dep = HTMLDependency( | ||
| name, | ||
| DEFAULT_VERSION, | ||
| source={"subdir": os.path.dirname(path)}, | ||
| all_files=include_files, | ||
| ) | ||
|
|
||
| # source_path_map() tells us where the source subdir is mapped to on the client | ||
| # (i.e., session._register_web_dependency() uses the same thing to determine where | ||
| # to mount the subdir, but we can't assume an active session at this point). | ||
| src = os.path.join(dep.source_path_map()["href"], os.path.basename(path)) | ||
|
|
||
| return dep, src | ||
|
|
||
|
|
||
| def maybe_copy_files(path: str, include_files: bool) -> tuple[str, str]: | ||
| hash = get_hash(path, include_files) | ||
|
|
||
| # To avoid unnecessary work when the same file is included multiple times, | ||
| # use a directory scoped by a hash of the file. | ||
| tmpdir = os.path.join(tempfile.gettempdir(), "shiny_include_files", hash) | ||
| path_dest = os.path.join(tmpdir, os.path.basename(path)) | ||
gshotwell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Since the hash/tmpdir should represent all the files in the path's directory, | ||
| # we can simply return here | ||
| if os.path.exists(path_dest): | ||
| return path_dest, hash | ||
|
|
||
| # Otherwise, make sure we have a clean slate | ||
| if os.path.exists(tmpdir): | ||
| shutil.rmtree(tmpdir) | ||
|
|
||
| if include_files: | ||
| shutil.copytree(os.path.dirname(path), tmpdir) | ||
| else: | ||
| os.makedirs(tmpdir, exist_ok=True) | ||
| shutil.copy(path, path_dest) | ||
|
|
||
| return path_dest, hash | ||
|
|
||
|
|
||
| def get_hash(path: str, include_files: bool) -> str: | ||
| if include_files: | ||
| key = get_file_key(path) | ||
| else: | ||
| dir = os.path.dirname(path) | ||
| files = glob.iglob(os.path.join(dir, "**"), recursive=True) | ||
| key = "\n".join([get_file_key(x) for x in files]) | ||
| return hash_deterministic(key) | ||
|
|
||
|
|
||
| def get_file_key(path: str) -> str: | ||
| return path + "-" + str(os.path.getmtime(path)) | ||
|
|
||
|
|
||
| def hash_deterministic(s: str) -> str: | ||
| """ | ||
| Returns a deterministic hash of the given string. | ||
| """ | ||
| return hashlib.sha1(s.encode("utf-8")).hexdigest() | ||
|
|
||
|
|
||
| def read_utf8(path: str) -> str: | ||
| with open(path, "r", encoding="utf-8") as f: | ||
| return f.read() | ||
|
|
||
|
|
||
| DEFAULT_VERSION = "0.0" | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.