diff --git a/.gitignore b/.gitignore
index 07d4c0cd..ffabb7fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
# ReactPy-Django Build Artifacts
-src/reactpy_django/static/*
+src/reactpy_django/static/reactpy_django/client.js
+src/reactpy_django/static/reactpy_django/pyscript
+src/reactpy_django/static/reactpy_django/morphdom
# Django #
logs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c9b047f..1e1e5b29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,13 +34,21 @@ Using the following categories, list your changes in this order:
## [Unreleased]
+### Added
+
+- Client-side Python components can now be rendered via the new `{% pyscript_component %}` template tag
+ - You must first call the `{% pyscript_setup %}` template tag to load PyScript dependencies
+- Client-side components can be embedded into existing server-side components via `reactpy_django.components.pyscript_component`.
+- Tired of writing JavaScript? You can now write PyScript code that runs directly within client browser via the `reactpy_django.html.pyscript` element.
+ - This is a viable substitution for most JavaScript code.
+
### Changed
- New syntax for `use_query` and `use_mutation` hooks. Here's a quick comparison of the changes:
```python
- query = use_query(QueryOptions(thread_sensitive=True), get_items, value=123456, foo="bar") # Old
- query = use_query(get_items, {"value":12356, "foo":"bar"}, thread_sensitive=True) # New
+ query = use_query(QueryOptions(thread_sensitive=True), get_items, foo="bar") # Old
+ query = use_query(get_items, {"foo":"bar"}, thread_sensitive=True) # New
mutation = use_mutation(MutationOptions(thread_sensitive=True), remove_item) # Old
mutation = use_mutation(remove_item, thread_sensitive=True) # New
@@ -48,7 +56,11 @@ Using the following categories, list your changes in this order:
### Removed
-- `QueryOptions` and `MutationOptions` have been removed. Their values are now passed direct into the hook.
+- `QueryOptions` and `MutationOptions` have been removed. The value contained within these objects are now passed directly into the hook.
+
+### Fixed
+
+- Resolved a bug where Django-ReactPy would not properly detect `settings.py:DEBUG`.
## [3.8.1] - 2024-05-07
diff --git a/README.md b/README.md
index ce11472f..817e684b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# ReactPy Django
+# ReactPy-Django
@@ -21,6 +21,7 @@
[ReactPy-Django](https://github.com/reactive-python/reactpy-django) is used to add [ReactPy](https://reactpy.dev/) support to an existing **Django project**. This package also turbocharges ReactPy with features such as...
- [SEO compatible rendering](https://reactive-python.github.io/reactpy-django/latest/reference/settings/#reactpy_prerender)
+- [Client-Side Python components](https://reactive-python.github.io/reactpy-django/latest/reference/template-tag/#pyscript-component)
- [Single page application (SPA) capabilities](https://reactive-python.github.io/reactpy-django/latest/reference/router/#django-router)
- [Distributed computing](https://reactive-python.github.io/reactpy-django/latest/reference/settings/#reactpy_default_hosts)
- [Performance enhancements](https://reactive-python.github.io/reactpy-django/latest/reference/settings/#performance-settings)
@@ -82,7 +83,7 @@ def hello_world(recipient: str):
-## [`my_app/templates/my-template.html`](https://docs.djangoproject.com/en/dev/topics/templates/)
+## [`my_app/templates/my_template.html`](https://docs.djangoproject.com/en/dev/topics/templates/)
diff --git a/docs/examples/html/pyscript-component.html b/docs/examples/html/pyscript-component.html
new file mode 100644
index 00000000..3f21e3fa
--- /dev/null
+++ b/docs/examples/html/pyscript-component.html
@@ -0,0 +1,14 @@
+{% load reactpy %}
+
+
+
+
+ ReactPy
+ {% pyscript_setup %}
+
+
+
+ {% pyscript_component "./example_project/my_app/components/hello_world.py" %}
+
+
+
diff --git a/docs/examples/html/pyscript-initial-object.html b/docs/examples/html/pyscript-initial-object.html
new file mode 100644
index 00000000..0e0a35c3
--- /dev/null
+++ b/docs/examples/html/pyscript-initial-object.html
@@ -0,0 +1,3 @@
+
+ {% pyscript_component "./example_project/my_app/components/root.py" initial=my_initial_object %}
+
diff --git a/docs/examples/html/pyscript-initial-string.html b/docs/examples/html/pyscript-initial-string.html
new file mode 100644
index 00000000..8e062d6a
--- /dev/null
+++ b/docs/examples/html/pyscript-initial-string.html
@@ -0,0 +1,3 @@
+
+ {% pyscript_component "./example_project/my_app/components/root.py" initial=" Loading ...
" %}
+
diff --git a/docs/examples/html/pyscript-js-module.html b/docs/examples/html/pyscript-js-module.html
new file mode 100644
index 00000000..2d0130fb
--- /dev/null
+++ b/docs/examples/html/pyscript-js-module.html
@@ -0,0 +1,14 @@
+{% load reactpy %}
+
+
+
+
+ ReactPy
+ {% pyscript_setup extra_js='{"/static/moment.js":"moment"}' %}
+
+
+
+ {% component "example_project.my_app.components.root.py" %}
+
+
+
diff --git a/docs/examples/html/pyscript-multiple-files.html b/docs/examples/html/pyscript-multiple-files.html
new file mode 100644
index 00000000..1f9267a8
--- /dev/null
+++ b/docs/examples/html/pyscript-multiple-files.html
@@ -0,0 +1,15 @@
+{% load reactpy %}
+
+
+
+
+ ReactPy
+ {% pyscript_setup %}
+
+
+
+ {% pyscript_component "./example_project/my_app/components/root.py"
+ "./example_project/my_app/components/child.py" %}
+
+
+
diff --git a/docs/examples/html/pyscript-root.html b/docs/examples/html/pyscript-root.html
new file mode 100644
index 00000000..e89a5369
--- /dev/null
+++ b/docs/examples/html/pyscript-root.html
@@ -0,0 +1,3 @@
+
+ {% pyscript_component "./example_project/my_app/components/main.py" root="main" %}
+
diff --git a/docs/examples/html/pyscript-setup-config-object.html b/docs/examples/html/pyscript-setup-config-object.html
new file mode 100644
index 00000000..70b408b1
--- /dev/null
+++ b/docs/examples/html/pyscript-setup-config-object.html
@@ -0,0 +1,4 @@
+
+ ReactPy
+ {% pyscript_setup config=my_config_object %}
+
diff --git a/docs/examples/html/pyscript-setup-config-string.html b/docs/examples/html/pyscript-setup-config-string.html
new file mode 100644
index 00000000..842bb769
--- /dev/null
+++ b/docs/examples/html/pyscript-setup-config-string.html
@@ -0,0 +1,4 @@
+
+ ReactPy
+ {% pyscript_setup config='{"experimental_create_proxy":"auto"}' %}
+
diff --git a/docs/examples/html/pyscript-setup-dependencies.html b/docs/examples/html/pyscript-setup-dependencies.html
new file mode 100644
index 00000000..f982b8fb
--- /dev/null
+++ b/docs/examples/html/pyscript-setup-dependencies.html
@@ -0,0 +1,4 @@
+
+ ReactPy
+ {% pyscript_setup "dill==0.3.5" "markdown<=3.6.0" "nest_asyncio" "titlecase" %}
+
diff --git a/docs/examples/html/pyscript-setup-extra-js-object.html b/docs/examples/html/pyscript-setup-extra-js-object.html
new file mode 100644
index 00000000..815cb040
--- /dev/null
+++ b/docs/examples/html/pyscript-setup-extra-js-object.html
@@ -0,0 +1,14 @@
+{% load reactpy %}
+
+
+
+
+ ReactPy
+ {% pyscript_setup extra_js=my_extra_js_object %}
+
+
+
+ {% component "example_project.my_app.components.root.py" %}
+
+
+
diff --git a/docs/examples/html/pyscript-setup-extra-js-string.html b/docs/examples/html/pyscript-setup-extra-js-string.html
new file mode 100644
index 00000000..2d0130fb
--- /dev/null
+++ b/docs/examples/html/pyscript-setup-extra-js-string.html
@@ -0,0 +1,14 @@
+{% load reactpy %}
+
+
+
+
+ ReactPy
+ {% pyscript_setup extra_js='{"/static/moment.js":"moment"}' %}
+
+
+
+ {% component "example_project.my_app.components.root.py" %}
+
+
+
diff --git a/docs/examples/html/pyscript-setup.html b/docs/examples/html/pyscript-setup.html
new file mode 100644
index 00000000..20bb2b09
--- /dev/null
+++ b/docs/examples/html/pyscript-setup.html
@@ -0,0 +1,6 @@
+{% load reactpy %}
+
+
+ ReactPy
+ {% pyscript_setup %}
+
diff --git a/docs/examples/html/pyscript-ssr-parent.html b/docs/examples/html/pyscript-ssr-parent.html
new file mode 100644
index 00000000..bf0f47ae
--- /dev/null
+++ b/docs/examples/html/pyscript-ssr-parent.html
@@ -0,0 +1,14 @@
+{% load reactpy %}
+
+
+
+
+ ReactPy
+ {% pyscript_setup %}
+
+
+
+ {% component "example_project.my_app.components.server_side_component" %}
+
+
+
diff --git a/docs/examples/html/pyscript-tag.html b/docs/examples/html/pyscript-tag.html
new file mode 100644
index 00000000..6ca71085
--- /dev/null
+++ b/docs/examples/html/pyscript-tag.html
@@ -0,0 +1,14 @@
+{% load reactpy %}
+
+
+
+
+ ReactPy
+ {% pyscript_setup %}
+
+
+
+ {% component "example_project.my_app.components.server_side_component.py" %}
+
+
+
diff --git a/docs/examples/python/example/views.py b/docs/examples/python/example/views.py
index a8ed7fdb..23e21130 100644
--- a/docs/examples/python/example/views.py
+++ b/docs/examples/python/example/views.py
@@ -2,4 +2,4 @@
def index(request):
- return render(request, "my-template.html")
+ return render(request, "my_template.html")
diff --git a/docs/examples/python/pyscript-component-initial-object.py b/docs/examples/python/pyscript-component-initial-object.py
new file mode 100644
index 00000000..222a568b
--- /dev/null
+++ b/docs/examples/python/pyscript-component-initial-object.py
@@ -0,0 +1,12 @@
+from reactpy import component, html
+from reactpy_django.components import pyscript_component
+
+
+@component
+def server_side_component():
+ return html.div(
+ pyscript_component(
+ "./example_project/my_app/components/root.py",
+ initial=html.div("Loading ..."),
+ ),
+ )
diff --git a/docs/examples/python/pyscript-component-initial-string.py b/docs/examples/python/pyscript-component-initial-string.py
new file mode 100644
index 00000000..664b9f9b
--- /dev/null
+++ b/docs/examples/python/pyscript-component-initial-string.py
@@ -0,0 +1,12 @@
+from reactpy import component, html
+from reactpy_django.components import pyscript_component
+
+
+@component
+def server_side_component():
+ return html.div(
+ pyscript_component(
+ "./example_project/my_app/components/root.py",
+ initial=" Loading ...
",
+ ),
+ )
diff --git a/docs/examples/python/pyscript-component-multiple-files-root.py b/docs/examples/python/pyscript-component-multiple-files-root.py
new file mode 100644
index 00000000..776b26b2
--- /dev/null
+++ b/docs/examples/python/pyscript-component-multiple-files-root.py
@@ -0,0 +1,12 @@
+from reactpy import component, html
+from reactpy_django.components import pyscript_component
+
+
+@component
+def server_side_component():
+ return html.div(
+ pyscript_component(
+ "./example_project/my_app/components/root.py",
+ "./example_project/my_app/components/child.py",
+ ),
+ )
diff --git a/docs/examples/python/pyscript-component-root.py b/docs/examples/python/pyscript-component-root.py
new file mode 100644
index 00000000..9880b740
--- /dev/null
+++ b/docs/examples/python/pyscript-component-root.py
@@ -0,0 +1,12 @@
+from reactpy import component, html
+from reactpy_django.components import pyscript_component
+
+
+@component
+def server_side_component():
+ return html.div(
+ pyscript_component(
+ "./example_project/my_app/components/main.py",
+ root="main",
+ ),
+ )
diff --git a/docs/examples/python/pyscript-hello-world.py b/docs/examples/python/pyscript-hello-world.py
new file mode 100644
index 00000000..d5737421
--- /dev/null
+++ b/docs/examples/python/pyscript-hello-world.py
@@ -0,0 +1,6 @@
+from reactpy import component, html
+
+
+@component
+def root():
+ return html.div("Hello, World!")
diff --git a/docs/examples/python/pyscript-initial-object.py b/docs/examples/python/pyscript-initial-object.py
new file mode 100644
index 00000000..1742ff87
--- /dev/null
+++ b/docs/examples/python/pyscript-initial-object.py
@@ -0,0 +1,10 @@
+from django.shortcuts import render
+from reactpy import html
+
+
+def index(request):
+ return render(
+ request,
+ "my_template.html",
+ context={"my_initial_object": html.div("Loading ...")},
+ )
diff --git a/docs/examples/python/pyscript-js-execution.py b/docs/examples/python/pyscript-js-execution.py
new file mode 100644
index 00000000..a96ef65b
--- /dev/null
+++ b/docs/examples/python/pyscript-js-execution.py
@@ -0,0 +1,11 @@
+import js
+from reactpy import component, html
+
+
+@component
+def root():
+
+ def onClick(event):
+ js.document.title = "New window title"
+
+ return html.button({"onClick": onClick}, "Click Me!")
diff --git a/docs/examples/python/pyscript-js-module.py b/docs/examples/python/pyscript-js-module.py
new file mode 100644
index 00000000..221b5bae
--- /dev/null
+++ b/docs/examples/python/pyscript-js-module.py
@@ -0,0 +1,12 @@
+from reactpy import component, html
+
+
+@component
+def root():
+ from pyscript.js_modules import moment
+
+ return html.div(
+ {"id": "moment"},
+ "Using the JavaScript package 'moment' to calculate time: ",
+ moment.default().format("YYYY-MM-DD HH:mm:ss"),
+ )
diff --git a/docs/examples/python/pyscript-multiple-files-child.py b/docs/examples/python/pyscript-multiple-files-child.py
new file mode 100644
index 00000000..73dbb189
--- /dev/null
+++ b/docs/examples/python/pyscript-multiple-files-child.py
@@ -0,0 +1,6 @@
+from reactpy import component, html
+
+
+@component
+def child_component():
+ return html.div("This is a child component from a different file.")
diff --git a/docs/examples/python/pyscript-multiple-files-root.py b/docs/examples/python/pyscript-multiple-files-root.py
new file mode 100644
index 00000000..dc17e7ad
--- /dev/null
+++ b/docs/examples/python/pyscript-multiple-files-root.py
@@ -0,0 +1,11 @@
+from typing import TYPE_CHECKING
+
+from reactpy import component, html
+
+if TYPE_CHECKING:
+ from .child import child_component
+
+
+@component
+def root():
+ return html.div("This text is from the root component.", child_component())
diff --git a/docs/examples/python/pyscript-root.py b/docs/examples/python/pyscript-root.py
new file mode 100644
index 00000000..f39fd01e
--- /dev/null
+++ b/docs/examples/python/pyscript-root.py
@@ -0,0 +1,6 @@
+from reactpy import component, html
+
+
+@component
+def main():
+ return html.div("Hello, World!")
diff --git a/docs/examples/python/pyscript-setup-config-object.py b/docs/examples/python/pyscript-setup-config-object.py
new file mode 100644
index 00000000..85db2751
--- /dev/null
+++ b/docs/examples/python/pyscript-setup-config-object.py
@@ -0,0 +1,9 @@
+from django.shortcuts import render
+
+
+def index(request):
+ return render(
+ request,
+ "my_template.html",
+ context={"my_config_object": {"experimental_create_proxy": "auto"}},
+ )
diff --git a/docs/examples/python/pyscript-setup-extra-js-object.py b/docs/examples/python/pyscript-setup-extra-js-object.py
new file mode 100644
index 00000000..805365cf
--- /dev/null
+++ b/docs/examples/python/pyscript-setup-extra-js-object.py
@@ -0,0 +1,10 @@
+from django.shortcuts import render
+from django.templatetags.static import static
+
+
+def index(request):
+ return render(
+ request,
+ "my_template.html",
+ context={"my_extra_js_object": {static("moment.js"): "moment"}},
+ )
diff --git a/docs/examples/python/pyscript-ssr-child.py b/docs/examples/python/pyscript-ssr-child.py
new file mode 100644
index 00000000..d2566c88
--- /dev/null
+++ b/docs/examples/python/pyscript-ssr-child.py
@@ -0,0 +1,6 @@
+from reactpy import component, html
+
+
+@component
+def root():
+ return html.div("This text is from my client-side component")
diff --git a/docs/examples/python/pyscript-ssr-parent.py b/docs/examples/python/pyscript-ssr-parent.py
new file mode 100644
index 00000000..b51aa110
--- /dev/null
+++ b/docs/examples/python/pyscript-ssr-parent.py
@@ -0,0 +1,10 @@
+from reactpy import component, html
+from reactpy_django.components import pyscript_component
+
+
+@component
+def server_side_component():
+ return html.div(
+ "This text is from my server-side component",
+ pyscript_component("./example_project/my_app/components/root.py"),
+ )
diff --git a/docs/examples/python/pyscript-tag.py b/docs/examples/python/pyscript-tag.py
new file mode 100644
index 00000000..6455e9da
--- /dev/null
+++ b/docs/examples/python/pyscript-tag.py
@@ -0,0 +1,15 @@
+from reactpy import component, html
+from reactpy_django.html import pyscript
+
+example_source_code = """
+import js
+
+js.console.log("Hello, World!")
+"""
+
+
+@component
+def server_side_component():
+ return html.div(
+ pyscript(example_source_code.strip()),
+ )
diff --git a/docs/examples/python/template-tag-bad-view.py b/docs/examples/python/template-tag-bad-view.py
index 00d0d9f7..ef16c845 100644
--- a/docs/examples/python/template-tag-bad-view.py
+++ b/docs/examples/python/template-tag-bad-view.py
@@ -2,5 +2,8 @@
def example_view(request):
- context_vars = {"my_variable": "example_project.my_app.components.hello_world"}
- return render(request, "my-template.html", context_vars)
+ return render(
+ request,
+ "my_template.html",
+ context={"my_variable": "example_project.my_app.components.hello_world"},
+ )
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index bee85cc1..e4159640 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -7,6 +7,7 @@ nav:
- Reference:
- Components: reference/components.md
- Hooks: reference/hooks.md
+ - HTML: reference/html.md
- URL Router: reference/router.md
- Decorators: reference/decorators.md
- Utilities: reference/utils.md
diff --git a/docs/src/assets/css/admonition.css b/docs/src/assets/css/admonition.css
index 8b3f06ef..c93892a8 100644
--- a/docs/src/assets/css/admonition.css
+++ b/docs/src/assets/css/admonition.css
@@ -1,45 +1,45 @@
[data-md-color-scheme="slate"] {
- --admonition-border-color: transparent;
- --admonition-expanded-border-color: rgba(255, 255, 255, 0.1);
- --note-bg-color: rgba(43, 110, 98, 0.2);
- --terminal-bg-color: #0c0c0c;
- --terminal-title-bg-color: #000;
- --deep-dive-bg-color: rgba(43, 52, 145, 0.2);
- --you-will-learn-bg-color: #353a45;
- --pitfall-bg-color: rgba(182, 87, 0, 0.2);
+ --admonition-border-color: transparent;
+ --admonition-expanded-border-color: rgba(255, 255, 255, 0.1);
+ --note-bg-color: rgba(43, 110, 98, 0.2);
+ --terminal-bg-color: #0c0c0c;
+ --terminal-title-bg-color: #000;
+ --deep-dive-bg-color: rgba(43, 52, 145, 0.2);
+ --you-will-learn-bg-color: #353a45;
+ --pitfall-bg-color: rgba(182, 87, 0, 0.2);
}
[data-md-color-scheme="default"] {
- --admonition-border-color: rgba(0, 0, 0, 0.08);
- --admonition-expanded-border-color: var(--admonition-border-color);
- --note-bg-color: rgb(244, 251, 249);
- --terminal-bg-color: rgb(64, 71, 86);
- --terminal-title-bg-color: rgb(35, 39, 47);
- --deep-dive-bg-color: rgb(243, 244, 253);
- --you-will-learn-bg-color: rgb(246, 247, 249);
- --pitfall-bg-color: rgb(254, 245, 231);
+ --admonition-border-color: rgba(0, 0, 0, 0.08);
+ --admonition-expanded-border-color: var(--admonition-border-color);
+ --note-bg-color: rgb(244, 251, 249);
+ --terminal-bg-color: rgb(64, 71, 86);
+ --terminal-title-bg-color: rgb(35, 39, 47);
+ --deep-dive-bg-color: rgb(243, 244, 253);
+ --you-will-learn-bg-color: rgb(246, 247, 249);
+ --pitfall-bg-color: rgb(254, 245, 231);
}
.md-typeset details,
.md-typeset .admonition {
- border-color: var(--admonition-border-color) !important;
- box-shadow: none;
+ border-color: var(--admonition-border-color) !important;
+ box-shadow: none;
}
.md-typeset :is(.admonition, details) {
- margin: 0.55em 0;
+ margin: 0 0;
}
.md-typeset .admonition {
- font-size: 0.7rem;
+ font-size: 0.7rem;
}
.md-typeset .admonition:focus-within,
.md-typeset details:focus-within {
- box-shadow: none !important;
+ box-shadow: none !important;
}
.md-typeset details[open] {
- border-color: var(--admonition-expanded-border-color) !important;
+ border-color: var(--admonition-expanded-border-color) !important;
}
/*
@@ -47,24 +47,24 @@ Admonition: "summary"
React Name: "You will learn"
*/
.md-typeset .admonition.summary {
- background: var(--you-will-learn-bg-color);
- padding: 0.8rem 1.4rem;
- border-radius: 0.8rem;
+ background: var(--you-will-learn-bg-color);
+ padding: 0.8rem 1.4rem;
+ border-radius: 0.8rem;
}
.md-typeset .summary .admonition-title {
- font-size: 1rem;
- background: transparent;
- padding-left: 0.6rem;
- padding-bottom: 0;
+ font-size: 1rem;
+ background: transparent;
+ padding-left: 0.6rem;
+ padding-bottom: 0;
}
.md-typeset .summary .admonition-title:before {
- display: none;
+ display: none;
}
.md-typeset .admonition.summary {
- border-color: #ffffff17 !important;
+ border-color: #ffffff17 !important;
}
/*
@@ -72,21 +72,21 @@ Admonition: "abstract"
React Name: "Note"
*/
.md-typeset .admonition.abstract {
- background: var(--note-bg-color);
- padding: 0.8rem 1.4rem;
- border-radius: 0.8rem;
+ background: var(--note-bg-color);
+ padding: 0.8rem 1.4rem;
+ border-radius: 0.8rem;
}
.md-typeset .abstract .admonition-title {
- font-size: 1rem;
- background: transparent;
- padding-bottom: 0;
- color: rgb(68, 172, 153);
+ font-size: 1rem;
+ background: transparent;
+ padding-bottom: 0;
+ color: rgb(68, 172, 153);
}
.md-typeset .abstract .admonition-title:before {
- font-size: 1.1rem;
- background: rgb(68, 172, 153);
+ font-size: 1.1rem;
+ background: rgb(68, 172, 153);
}
/*
@@ -94,21 +94,21 @@ Admonition: "warning"
React Name: "Pitfall"
*/
.md-typeset .admonition.warning {
- background: var(--pitfall-bg-color);
- padding: 0.8rem 1.4rem;
- border-radius: 0.8rem;
+ background: var(--pitfall-bg-color);
+ padding: 0.8rem 1.4rem;
+ border-radius: 0.8rem;
}
.md-typeset .warning .admonition-title {
- font-size: 1rem;
- background: transparent;
- padding-bottom: 0;
- color: rgb(219, 125, 39);
+ font-size: 1rem;
+ background: transparent;
+ padding-bottom: 0;
+ color: rgb(219, 125, 39);
}
.md-typeset .warning .admonition-title:before {
- font-size: 1.1rem;
- background: rgb(219, 125, 39);
+ font-size: 1.1rem;
+ background: rgb(219, 125, 39);
}
/*
@@ -116,21 +116,21 @@ Admonition: "info"
React Name: "Deep Dive"
*/
.md-typeset .admonition.info {
- background: var(--deep-dive-bg-color);
- padding: 0.8rem 1.4rem;
- border-radius: 0.8rem;
+ background: var(--deep-dive-bg-color);
+ padding: 0.8rem 1.4rem;
+ border-radius: 0.8rem;
}
.md-typeset .info .admonition-title {
- font-size: 1rem;
- background: transparent;
- padding-bottom: 0;
- color: rgb(136, 145, 236);
+ font-size: 1rem;
+ background: transparent;
+ padding-bottom: 0;
+ color: rgb(136, 145, 236);
}
.md-typeset .info .admonition-title:before {
- font-size: 1.1rem;
- background: rgb(136, 145, 236);
+ font-size: 1.1rem;
+ background: rgb(136, 145, 236);
}
/*
@@ -138,23 +138,24 @@ Admonition: "example"
React Name: "Terminal"
*/
.md-typeset .admonition.example {
- background: var(--terminal-bg-color);
- border-radius: 0.4rem;
- overflow: hidden;
- border: none;
+ background: var(--terminal-bg-color);
+ border-radius: 0.4rem;
+ overflow: hidden;
+ border: none;
+ margin: 0.5rem 0;
}
.md-typeset .example .admonition-title {
- background: var(--terminal-title-bg-color);
- color: rgb(246, 247, 249);
+ background: var(--terminal-title-bg-color);
+ color: rgb(246, 247, 249);
}
.md-typeset .example .admonition-title:before {
- background: rgb(246, 247, 249);
+ background: rgb(246, 247, 249);
}
.md-typeset .admonition.example code {
- background: transparent;
- color: #fff;
- box-shadow: none;
+ background: transparent;
+ color: #fff;
+ box-shadow: none;
}
diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt
index dee45011..66265e78 100644
--- a/docs/src/dictionary.txt
+++ b/docs/src/dictionary.txt
@@ -39,3 +39,5 @@ misconfigurations
backhaul
sublicense
broadcasted
+hello_world
+my_template
diff --git a/docs/src/learn/add-reactpy-to-a-django-project.md b/docs/src/learn/add-reactpy-to-a-django-project.md
index a0cca013..dd258737 100644
--- a/docs/src/learn/add-reactpy-to-a-django-project.md
+++ b/docs/src/learn/add-reactpy-to-a-django-project.md
@@ -125,6 +125,6 @@ Prefer a quick summary? Read the **At a Glance** section below.
---
- **`my_app/templates/my-template.html`**
+ **`my_app/templates/my_template.html`**
{% include-markdown "../../../README.md" start="" end="" %}
diff --git a/docs/src/learn/your-first-component.md b/docs/src/learn/your-first-component.md
index b0749c41..08df6a57 100644
--- a/docs/src/learn/your-first-component.md
+++ b/docs/src/learn/your-first-component.md
@@ -43,7 +43,7 @@ Within this file, you can define your component functions using ReactPy's `#!pyt
We recommend creating a `components.py` for small **Django apps**. If your app has a lot of components, you should consider breaking them apart into individual modules such as `components/navbar.py`.
- Ultimately, components are referenced by Python dotted path in `my-template.html` ([_see next step_](#embedding-in-a-template)). This path must be valid to Python's `#!python importlib`.
+ Ultimately, components are referenced by Python dotted path in `my_template.html` ([_see next step_](#embedding-in-a-template)). This path must be valid to Python's `#!python importlib`.
??? question "What does the decorator actually do?"
@@ -62,7 +62,7 @@ In your **Django app**'s HTML template, you can now embed your ReactPy component
Additionally, you can pass in `#!python args` and `#!python kwargs` into your component function. After reading the code below, pay attention to how the function definition for `#!python hello_world` ([_from the previous step_](#defining-a-component)) accepts a `#!python recipient` argument.
-=== "my-template.html"
+=== "my_template.html"
{% include-markdown "../../../README.md" start="" end="" %}
@@ -76,7 +76,7 @@ Additionally, you can pass in `#!python args` and `#!python kwargs` into your co
## Setting up a Django view
-Within your **Django app**'s `views.py` file, you will need to [create a view function](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) to render the HTML template `my-template.html` ([_from the previous step_](#embedding-in-a-template)).
+Within your **Django app**'s `views.py` file, you will need to [create a view function](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) to render the HTML template `my_template.html` ([_from the previous step_](#embedding-in-a-template)).
=== "views.py"
diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md
index aa0e75d4..aaeabba7 100644
--- a/docs/src/reference/components.md
+++ b/docs/src/reference/components.md
@@ -8,6 +8,114 @@ We supply some pre-designed that components can be used to help simplify develop
---
+## PyScript Component
+
+This allows you to embedded any number of client-side PyScript components within traditional ReactPy components.
+
+{% include-markdown "../reference/template-tag.md" start="" end="" %}
+
+=== "components.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-ssr-parent.py" %}
+ ```
+
+=== "root.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-ssr-child.py" %}
+ ```
+
+=== "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-ssr-parent.html" %}
+ ```
+
+??? example "See Interface"
+
+ **Parameters**
+
+ | Name | Type | Description | Default |
+ | --- | --- | --- | --- |
+ | `#!python *file_paths` | `#!python str` | File path to your client-side component. If multiple paths are provided, the contents are automatically merged. | N/A |
+ | `#!python initial` | `#!python str | VdomDict | ComponentType` | The initial HTML that is displayed prior to the PyScript component loads. This can either be a string containing raw HTML, a `#!python reactpy.html` snippet, or a non-interactive component. | `#!python ""` |
+ | `#!python root` | `#!python str` | The name of the root component function. | `#!python "root"` |
+
+
+
+??? warning "You must call `pyscript_setup` in your Django template before using this tag!"
+
+ This requires using of the [`#!jinja {% pyscript_setup %}` template tag](./template-tag.md#pyscript-setup) to initialize PyScript on the client.
+
+ === "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-setup.html" %}
+ ```
+
+
+
+{% include-markdown "../reference/template-tag.md" start="" end="" %}
+
+{% include-markdown "../reference/template-tag.md" start="" end="" trailing-newlines=false preserve-includer-indent=false %}
+
+ === "components.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-component-multiple-files-root.py" %}
+ ```
+
+ === "root.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-multiple-files-root.py" %}
+ ```
+
+ === "child.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-multiple-files-child.py" %}
+ ```
+
+??? question "How do I display something while the component is loading?"
+
+ You can configure the `#!python initial` keyword to display HTML while your PyScript component is loading.
+
+ The value for `#!python initial` is most commonly be a `#!python reactpy.html` snippet or a non-interactive `#!python @component`.
+
+ === "components.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-component-initial-object.py" %}
+ ```
+
+ However, you can also use a string containing raw HTML.
+
+ === "components.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-component-initial-string.py" %}
+ ```
+
+??? question "Can I use a different name for my root component?"
+
+ Yes, you can use the `#!python root` keyword to specify a different name for your root function.
+
+ === "components.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-component-root.py" %}
+ ```
+
+ === "main.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-root.py" %}
+ ```
+
+---
+
## View To Component
Automatically convert a Django view into a component.
diff --git a/docs/src/reference/html.md b/docs/src/reference/html.md
new file mode 100644
index 00000000..fd63c033
--- /dev/null
+++ b/docs/src/reference/html.md
@@ -0,0 +1,31 @@
+## Overview
+
+
+
+We supply some pre-generated that HTML nodes can be used to help simplify development.
+
+
+
+---
+
+## PyScript
+
+Primitive HTML tag that is leveraged by [`reactpy_django.components.pyscript_component`](./components.md#pyscript-component).
+
+This can be used as an alternative to the `#!python reactpy.html.script` tag to execute JavaScript and run client-side Python code.
+
+Additionally, this tag functions identically to any other tag contained within `#!python reactpy.html`, and can be used in the same way.
+
+=== "components.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-tag.py" %}
+ ```
+
+=== "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-tag.html" %}
+ ```
+
+{% include-markdown "../reference/components.md" start="" end="" %}
diff --git a/docs/src/reference/router.md b/docs/src/reference/router.md
index af5353e8..66ad0f9e 100644
--- a/docs/src/reference/router.md
+++ b/docs/src/reference/router.md
@@ -18,6 +18,13 @@ A Single Page Application URL router, which is a variant of [`reactpy-router`](h
URL router that enables the ability to conditionally render other components based on the client's current URL `#!python path`.
+!!! warning "Pitfall"
+
+ All pages where `django_router` is used must have identical, or more permissive URL exposure within Django's [URL patterns](https://docs.djangoproject.com/en/5.0/topics/http/urls/#example). You can think of the router component as a secondary, client-side router. Django still handles the primary server-side routes.
+
+ We recommend creating a route with a wildcard `.*` to forward routes to ReactPy. For example...
+ `#!python re_path(r"^/router/.*$", my_reactpy_view)`
+
=== "components.py"
```python
diff --git a/docs/src/reference/template-tag.md b/docs/src/reference/template-tag.md
index 759aa8cf..434c81d0 100644
--- a/docs/src/reference/template-tag.md
+++ b/docs/src/reference/template-tag.md
@@ -14,7 +14,7 @@ This template tag can be used to insert any number of ReactPy components onto yo
Each component loaded via this template tag will receive a dedicated WebSocket connection to the server.
-=== "my-template.html"
+=== "my_template.html"
{% include-markdown "../../../README.md" start="" end="" %}
@@ -33,23 +33,17 @@ Each component loaded via this template tag will receive a dedicated WebSocket c
| `#!python offline` | `#!python str` | The dotted path to a component that will be displayed if your root component loses connection to the server. Keep in mind, this `offline` component will be non-interactive (hooks won't operate). | `#!python ""` |
| `#!python **kwargs` | `#!python Any` | The keyword arguments to provide to the component. | N/A |
- **Returns**
-
- | Type | Description |
- | --- | --- |
- | `#!python Component` | A ReactPy component. |
-
??? warning "Do not use context variables for the component path"
The ReactPy component finder requires that your component path is a string.
- **Do not** use Django template/context variables for the component path. Failure to follow this warning can result in unexpected behavior, such as components that will not render.
+ **Do not** use Django template/context variables for the component path. Failure to follow this warning will result in render failures.
For example, **do not** do the following:
- === "my-template.html"
+ === "my_template.html"
```jinja
@@ -75,7 +69,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c
You can add as many components to a webpage as needed by using the template tag multiple times. Retrofitting legacy sites to use ReactPy will typically involve many components on one page.
- === "my-template.html"
+ === "my_template.html"
```jinja
{% load reactpy %}
@@ -99,7 +93,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c
You can use any combination of `#!python *args`/`#!python **kwargs` in your template tag.
- === "my-template.html"
+ === "my_template.html"
```jinja
{% component "example_project.my_app.components.frog_greeter" 123 "Mr. Froggles" species="Grey Treefrog" %}
@@ -115,7 +109,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c
Yes! This is most commonly done through [`settings.py:REACTPY_HOSTS`](../reference/settings.md#reactpy_default_hosts). However, you can use the `#!python host` keyword to render components on a specific ASGI server.
- === "my-template.html"
+ === "my_template.html"
```jinja
...
@@ -135,7 +129,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c
This is most commonly done through [`settings.py:REACTPY_PRERENDER`](../reference/settings.md#reactpy_prerender). However, you can use the `#!python prerender` keyword to pre-render a specific component.
- === "my-template.html"
+ === "my_template.html"
```jinja
...
@@ -143,11 +137,11 @@ Each component loaded via this template tag will receive a dedicated WebSocket c
...
```
-??? question "How do I show something when the client disconnects?"
+??? question "How do I display something when the client disconnects?"
You can use the `#!python offline` keyword to display a specific component when the client disconnects from the server.
- === "my-template.html"
+ === "my_template.html"
```jinja
...
@@ -156,3 +150,229 @@ Each component loaded via this template tag will receive a dedicated WebSocket c
```
_Note: The `#!python offline` component will be non-interactive (hooks won't operate)._
+
+## PyScript Component
+
+This template tag can be used to insert any number of **client-side** ReactPy components onto your page.
+
+
+
+By default, the only dependencies available are the Python standard library, `pyscript`, `pyodide`, `reactpy` core.
+
+Your PyScript component file requires a `#!python def root()` component to function as the entry point.
+
+
+
+!!! warning "Pitfall"
+
+ Your provided Python file is loaded directly into the client (web browser) **as raw text**, and ran using a PyScript interpreter. Be cautious about what you include in your Python file.
+
+ As a result of running client-side, Python packages within your local environment (such as those installed via `pip install ...`) are **not accessible** within PyScript components.
+
+=== "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-component.html" %}
+ ```
+
+=== "hello_world.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-hello-world.py" %}
+ ```
+
+??? example "See Interface"
+
+ **Parameters**
+
+ | Name | Type | Description | Default |
+ | --- | --- | --- | --- |
+ | `#!python *file_paths` | `#!python str` | File path to your client-side component. If multiple paths are provided, the contents are automatically merged. | N/A |
+ | `#!python initial` | `#!python str | VdomDict | ComponentType` | The initial HTML that is displayed prior to the PyScript component loads. This can either be a string containing raw HTML, a `#!python reactpy.html` snippet, or a non-interactive component. | `#!python ""` |
+ | `#!python root` | `#!python str` | The name of the root component function. | `#!python "root"` |
+
+
+
+??? question "How do I execute JavaScript within PyScript components?"
+
+ PyScript components have the ability to directly execute standard library JavaScript using the [`pyodide` `js` module](https://pyodide.org/en/stable/usage/type-conversions.html#importing-javascript-objects-into-python) or [`pyscript` foreign function interface](https://docs.pyscript.net/2024.6.1/user-guide/dom/).
+
+ The `#!python js` module has access to everything within the browser's JavaScript environment. Therefore, any global JavaScript functions loaded within your HTML `#!html ` can be called as well. However, be mindful of JavaScript load order!
+
+ === "root.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-js-execution.py" %}
+ ```
+
+ To import JavaScript modules in a fashion similar to `#!javascript import {moment} from 'static/moment.js'`, you will need to configure your `#!jinja {% pyscript_setup %}` block to make the module available to PyScript. This module will be accessed within `#!python pyscript.js_modules.*`. For more information, see the [PyScript JS modules docs](https://docs.pyscript.net/2024.6.2/user-guide/configuration/#javascript-modules).
+
+ === "root.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-js-module.py" %}
+ ```
+
+ === "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-js-module.html" %}
+ ```
+
+
+
+
+
+??? question "Does my entire component need to be contained in one file?"
+
+ Splitting a large file into multiple files is a common practice in software development.
+
+ However, PyScript components are run on the client browser. As such, they do not have access to your local development environment, and thus cannot `#!python import` any local Python files.
+
+ If your PyScript component file gets too large, you can declare multiple file paths instead. These files will automatically combined by ReactPy.
+
+ Here is how we recommend splitting your component into multiple files while avoiding local imports but retaining type hints.
+
+
+
+ === "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-multiple-files.html" %}
+ ```
+
+ === "root.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-multiple-files-root.py" %}
+ ```
+
+ === "child.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-multiple-files-child.py" %}
+ ```
+
+??? question "How do I display something while the component is loading?"
+
+ You can configure the `#!python initial` keyword to display HTML while your PyScript component is loading.
+
+ The value for `#!python initial` is most commonly be a string containing raw HTML.
+
+ === "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-initial-string.html" %}
+ ```
+
+ However, you can also insert a `#!python reactpy.html` snippet or a non-interactive `#!python @component` via template context.
+
+ === "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-initial-object.html" %}
+ ```
+
+ === "views.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-initial-object.py" %}
+ ```
+
+??? question "Can I use a different name for my root component?"
+
+ Yes, you can use the `#!python root` keyword to specify a different name for your root function.
+
+ === "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-root.html" %}
+ ```
+
+ === "main.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-root.py" %}
+ ```
+
+## PyScript Setup
+
+This template tag configures the current page to be able to run `pyscript`.
+
+You can optionally use this tag to configure the current PyScript environment. For example, you can include a list of Python packages to automatically install within the PyScript environment.
+
+=== "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-setup.html" %}
+ ```
+
+??? example "See Interface"
+
+ **Parameters**
+
+ | Name | Type | Description | Default |
+ | --- | --- | --- | --- |
+ | `#!python *extra_py` | `#!python str` | Dependencies that need to be loaded on the page for your PyScript components. Each dependency must be contained within it's own string and written in Python requirements file syntax. | N/A |
+ | `#!python extra_js` | `#!python str | dict` | A JSON string or Python dictionary containing a vanilla JavaScript module URL and the `#!python name: str` to access it within `#!python pyscript.js_modules.*`. | `#!python ""` |
+ | `#!python config` | `#!python str | dict` | A JSON string or Python dictionary containing PyScript configuration values. | `#!python ""` |
+
+??? question "How do I install additional Python dependencies?"
+
+ Dependencies must be available on [`pypi`](https://pypi.org/) and declared in your `#!jinja {% pyscript_setup %}` block using Python requirements file syntax.
+
+ These dependencies are automatically downloaded and installed into the PyScript client-side environment when the page is loaded.
+
+ === "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-setup-dependencies.html" %}
+ ```
+
+??? question "How do I install additional Javascript dependencies?"
+
+ You can use the `#!python extra_js` keyword to load additional JavaScript modules into your PyScript environment.
+
+ === "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-setup-extra-js-object.html" %}
+ ```
+
+ === "views.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-setup-extra-js-object.py" %}
+ ```
+
+ The value for `#!python extra_js` is most commonly a Python dictionary, but JSON strings are also supported.
+
+ === "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-setup-extra-js-string.html" %}
+ ```
+
+??? question "How do I modify the `pyscript` default configuration?"
+
+ You can modify the default [PyScript configuration](https://docs.pyscript.net/2024.6.2/user-guide/configuration/) by providing a value to the `#!python config` keyword.
+
+ === "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-setup-config-string.html" %}
+ ```
+
+ While this value is most commonly a JSON string, Python dictionary objects are also supported.
+
+ === "my_template.html"
+
+ ```jinja
+ {% include "../../examples/html/pyscript-setup-config-object.html" %}
+ ```
+
+ === "views.py"
+
+ ```python
+ {% include "../../examples/python/pyscript-setup-config-object.py" %}
+ ```
diff --git a/docs/src/reference/utils.md b/docs/src/reference/utils.md
index 461d9df5..6590012c 100644
--- a/docs/src/reference/utils.md
+++ b/docs/src/reference/utils.md
@@ -76,7 +76,7 @@ Typically, this function is automatically called on all components contained wit
For security reasons, ReactPy requires all root components to be registered. However, all components contained within Django templates are automatically registered.
- This function is needed when you have configured your [`host`](../reference/template-tag.md#component) to a dedicated Django rendering application that doesn't have templates.
+ This function is commonly needed when you have configured your [`host`](../reference/template-tag.md#component) to a dedicated Django rendering application that doesn't have templates.
---
diff --git a/setup.py b/setup.py
index b99d550b..76a91edf 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,6 @@
from __future__ import annotations, print_function
+import shutil
import sys
import traceback
from distutils import log
@@ -16,7 +17,9 @@
name = "reactpy_django"
root_dir = Path(__file__).parent
src_dir = root_dir / "src"
+js_dir = src_dir / "js"
package_dir = src_dir / name
+static_dir = package_dir / "static" / name
# -----------------------------------------------------------------------------
@@ -97,22 +100,37 @@
def build_javascript_first(build_cls: type):
class Command(build_cls):
def run(self):
- js_dir = str(src_dir / "js")
log.info("Installing Javascript...")
- result = npm.call(["install"], cwd=js_dir)
+ result = npm.call(["install"], cwd=str(js_dir))
if result != 0:
log.error(traceback.format_exc())
log.error("Failed to install Javascript")
raise RuntimeError("Failed to install Javascript")
log.info("Building Javascript...")
- result = npm.call(["run", "build"], cwd=js_dir)
+ result = npm.call(["run", "build"], cwd=str(js_dir))
if result != 0:
log.error(traceback.format_exc())
log.error("Failed to build Javascript")
raise RuntimeError("Failed to build Javascript")
+ log.info("Copying @pyscript/core distribution")
+ pyscript_dist = js_dir / "node_modules" / "@pyscript" / "core" / "dist"
+ pyscript_static_dir = static_dir / "pyscript"
+ if not pyscript_static_dir.exists():
+ pyscript_static_dir.mkdir()
+ for file in pyscript_dist.iterdir():
+ shutil.copy(file, pyscript_static_dir / file.name)
+
+ log.info("Copying Morphdom distribution")
+ morphdom_dist = js_dir / "node_modules" / "morphdom" / "dist"
+ morphdom_static_dir = static_dir / "morphdom"
+ if not morphdom_static_dir.exists():
+ morphdom_static_dir.mkdir()
+ for file in morphdom_dist.iterdir():
+ shutil.copy(file, morphdom_static_dir / file.name)
+
log.info("Successfully built Javascript")
super().run()
diff --git a/src/js/package-lock.json b/src/js/package-lock.json
index b0390203..d4cb1c0b 100644
--- a/src/js/package-lock.json
+++ b/src/js/package-lock.json
@@ -5,8 +5,10 @@
"packages": {
"": {
"dependencies": {
+ "@pyscript/core": "^0.4.48",
"@reactpy/client": "^0.3.1",
"@rollup/plugin-typescript": "^11.1.6",
+ "morphdom": "^2.7.3",
"tslib": "^2.6.2"
},
"devDependencies": {
@@ -205,6 +207,19 @@
"node": ">= 8"
}
},
+ "node_modules/@pyscript/core": {
+ "version": "0.4.48",
+ "resolved": "https://registry.npmjs.org/@pyscript/core/-/core-0.4.48.tgz",
+ "integrity": "sha512-cVZ//1WDkWhjZ1tOjUB1YJ5mKxDf3kMpzS/pw7Oe9/BMrB/NM3TxxCQ9Oyvq7Fkfv1F+srIcsi1xZ5gQeP+5Tg==",
+ "dependencies": {
+ "@ungap/with-resolvers": "^0.1.0",
+ "basic-devtools": "^0.1.6",
+ "polyscript": "^0.13.5",
+ "sticky-module": "^0.1.1",
+ "to-json-callback": "^0.1.1",
+ "type-checked-collections": "^0.1.7"
+ }
+ },
"node_modules/@reactpy/client": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@reactpy/client/-/client-0.3.1.tgz",
@@ -550,8 +565,22 @@
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
- "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
- "dev": true
+ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
+ },
+ "node_modules/@ungap/with-resolvers": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@ungap/with-resolvers/-/with-resolvers-0.1.0.tgz",
+ "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw=="
+ },
+ "node_modules/@webreflection/fetch": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz",
+ "integrity": "sha512-zCcqCJoNLvdeF41asAK71XPlwSPieeRDsE09albBunJEksuYPYNillKNQjf8p5BqSoTKTuKrW3lUm3MNodUC4g=="
+ },
+ "node_modules/@webreflection/idb-map": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@webreflection/idb-map/-/idb-map-0.3.1.tgz",
+ "integrity": "sha512-lRCanqwR7tHHFohJHAMSMEZnoNPvgjcKr0f5e4y+lTJA+fctT61EZ+f5pT5/+8+wlSsMAvXjzfKRLT6o9aqxbA=="
},
"node_modules/acorn": {
"version": "8.11.3",
@@ -749,6 +778,11 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "node_modules/basic-devtools": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/basic-devtools/-/basic-devtools-0.1.6.tgz",
+ "integrity": "sha512-g9zJ63GmdUesS3/Fwv0B5SYX6nR56TQXmGr+wE5PRTNCnGQMYWhUx/nZB/mMWnQJVLPPAp89oxDNlasdtNkW5Q=="
+ },
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -809,6 +843,28 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/codedent": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/codedent/-/codedent-0.1.2.tgz",
+ "integrity": "sha512-qEqzcy5viM3UoCN0jYHZeXZoyd4NZQzYFg0kOBj8O1CgoGG9WYYTF+VeQRsN0OSKFjF3G1u4WDUOtOsWEx6N2w==",
+ "dependencies": {
+ "plain-tag": "^0.1.3"
+ }
+ },
+ "node_modules/coincident": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/coincident/-/coincident-1.2.3.tgz",
+ "integrity": "sha512-Uxz3BMTWIslzeWjuQnizGWVg0j6khbvHUQ8+5BdM7WuJEm4ALXwq3wluYoB+uF68uPBz/oUOeJnYURKyfjexlA==",
+ "dependencies": {
+ "@ungap/structured-clone": "^1.2.0",
+ "@ungap/with-resolvers": "^0.1.0",
+ "gc-hook": "^0.3.1",
+ "proxy-target": "^3.0.2"
+ },
+ "optionalDependencies": {
+ "ws": "^8.16.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1463,6 +1519,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/gc-hook": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz",
+ "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A=="
+ },
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
@@ -1653,6 +1714,11 @@
"node": ">= 0.4"
}
},
+ "node_modules/html-escaper": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
+ },
"node_modules/ignore": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
@@ -2212,6 +2278,11 @@
"node": ">=10"
}
},
+ "node_modules/morphdom": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.3.tgz",
+ "integrity": "sha512-rvGK92GxSuPEZLY8D/JH07cG3BxyA+/F0Bxg32OoGAEFFhGWA3OqVpqPZlOgZTCR52clXrmz+z2pYSJ6gOig1w=="
+ },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -2440,6 +2511,30 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/plain-tag": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/plain-tag/-/plain-tag-0.1.3.tgz",
+ "integrity": "sha512-yyVAOFKTAElc7KdLt2+UKGExNYwYb/Y/WE9i+1ezCQsJE8gbKSjewfpRqK2nQgZ4d4hhAAGgDCOcIZVilqE5UA=="
+ },
+ "node_modules/polyscript": {
+ "version": "0.13.5",
+ "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.13.5.tgz",
+ "integrity": "sha512-PwXWnhLbOMtvZWFIN271JhaN7KnxESaMtv9Rcdrq1TKTCMnkz9idvYb3Od1iumBJlr49lLlwyUKeGb423rFR4w==",
+ "dependencies": {
+ "@ungap/structured-clone": "^1.2.0",
+ "@ungap/with-resolvers": "^0.1.0",
+ "@webreflection/fetch": "^0.1.5",
+ "@webreflection/idb-map": "^0.3.1",
+ "basic-devtools": "^0.1.6",
+ "codedent": "^0.1.2",
+ "coincident": "^1.2.3",
+ "gc-hook": "^0.3.1",
+ "html-escaper": "^3.0.3",
+ "proxy-target": "^3.0.2",
+ "sticky-module": "^0.1.1",
+ "to-json-callback": "^0.1.1"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -2475,6 +2570,11 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proxy-target": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/proxy-target/-/proxy-target-3.0.2.tgz",
+ "integrity": "sha512-FFE1XNwXX/FNC3/P8HiKaJSy/Qk68RitG/QEcLy/bVnTAPlgTAWPZKh0pARLAnpfXQPKyalBhk009NRTgsk8vQ=="
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2840,6 +2940,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/sticky-module": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/sticky-module/-/sticky-module-0.1.1.tgz",
+ "integrity": "sha512-IuYgnyIMUx/m6rtu14l/LR2MaqOLtpXcWkxPmtPsiScRHEo+S4Tojk+DWFHOncSdFX/OsoLOM4+T92yOmI1AMw=="
+ },
"node_modules/string.prototype.matchall": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz",
@@ -2958,6 +3063,11 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
+ "node_modules/to-json-callback": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/to-json-callback/-/to-json-callback-0.1.1.tgz",
+ "integrity": "sha512-BzOeinTT3NjE+FJ2iCvWB8HvyuyBzoH3WlSnJ+AYVC4tlePyZWSYdkQIFOARWiq0t35/XhmI0uQsFiUsRksRqg=="
+ },
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
@@ -2975,6 +3085,11 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/type-checked-collections": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/type-checked-collections/-/type-checked-collections-0.1.7.tgz",
+ "integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A=="
+ },
"node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -3185,6 +3300,27 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
+ "node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "optional": true,
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/src/js/package.json b/src/js/package.json
index 8d2d9ff5..949b6cf9 100644
--- a/src/js/package.json
+++ b/src/js/package.json
@@ -13,15 +13,17 @@
"@rollup/plugin-replace": "^5.0.5",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
- "prettier": "^3.2.3",
"eslint": "^8.38.0",
"eslint-plugin-react": "^7.32.2",
+ "prettier": "^3.2.3",
"rollup": "^4.9.5",
"typescript": "^5.3.3"
},
"dependencies": {
+ "@pyscript/core": "^0.4.48",
"@reactpy/client": "^0.3.1",
"@rollup/plugin-typescript": "^11.1.6",
+ "morphdom": "^2.7.3",
"tslib": "^2.6.2"
}
}
diff --git a/src/reactpy_django/__init__.py b/src/reactpy_django/__init__.py
index 8598ed0c..0bbff9d1 100644
--- a/src/reactpy_django/__init__.py
+++ b/src/reactpy_django/__init__.py
@@ -2,7 +2,16 @@
import nest_asyncio
-from reactpy_django import checks, components, decorators, hooks, router, types, utils
+from reactpy_django import (
+ checks,
+ components,
+ decorators,
+ hooks,
+ html,
+ router,
+ types,
+ utils,
+)
from reactpy_django.websocket.paths import (
REACTPY_WEBSOCKET_PATH,
REACTPY_WEBSOCKET_ROUTE,
@@ -12,6 +21,7 @@
__all__ = [
"REACTPY_WEBSOCKET_PATH",
"REACTPY_WEBSOCKET_ROUTE",
+ "html",
"hooks",
"components",
"decorators",
diff --git a/src/reactpy_django/components.py b/src/reactpy_django/components.py
index 75b0c321..579c73e3 100644
--- a/src/reactpy_django/components.py
+++ b/src/reactpy_django/components.py
@@ -4,6 +4,7 @@
import os
from typing import Any, Callable, Sequence, Union, cast, overload
from urllib.parse import urlencode
+from uuid import uuid4
from warnings import warn
from django.contrib.staticfiles.finders import find
@@ -12,10 +13,17 @@
from django.urls import reverse
from django.views import View
from reactpy import component, hooks, html, utils
-from reactpy.types import Key, VdomDict
+from reactpy.types import ComponentType, Key, VdomDict
from reactpy_django.exceptions import ViewNotRegisteredError
-from reactpy_django.utils import generate_obj_name, import_module, render_view
+from reactpy_django.html import pyscript
+from reactpy_django.utils import (
+ generate_obj_name,
+ import_module,
+ render_pyscript_template,
+ render_view,
+ vdom_or_component_to_string,
+)
# Type hints for:
@@ -27,8 +35,7 @@ def view_to_component(
compatibility: bool = False,
transforms: Sequence[Callable[[VdomDict], Any]] = (),
strict_parsing: bool = True,
-) -> Any:
- ...
+) -> Any: ...
# Type hints for:
@@ -39,8 +46,7 @@ def view_to_component(
compatibility: bool = False,
transforms: Sequence[Callable[[VdomDict], Any]] = (),
strict_parsing: bool = True,
-) -> Callable[[Callable], Any]:
- ...
+) -> Callable[[Callable], Any]: ...
def view_to_component(
@@ -148,6 +154,29 @@ def django_js(static_path: str, key: Key | None = None):
return _django_js(static_path=static_path, key=key)
+def pyscript_component(
+ *file_paths: str,
+ initial: str | VdomDict | ComponentType = "",
+ root: str = "root",
+):
+ """
+ Args:
+ file_paths: File path to your client-side component. If multiple paths are \
+ provided, the contents are automatically merged.
+
+ Kwargs:
+ initial: The initial HTML that is displayed prior to the PyScript component \
+ loads. This can either be a string containing raw HTML, a \
+ `#!python reactpy.html` snippet, or a non-interactive component.
+ root: The name of the root component function.
+ """
+ return _pyscript_component(
+ *file_paths,
+ initial=initial,
+ root=root,
+ )
+
+
@component
def _view_to_component(
view: Callable | View | str,
@@ -284,3 +313,29 @@ def _cached_static_contents(static_path: str) -> str:
)
return file_contents
+
+
+@component
+def _pyscript_component(
+ *file_paths: str,
+ initial: str | VdomDict | ComponentType = "",
+ root: str = "root",
+):
+ rendered, set_rendered = hooks.use_state(False)
+ uuid = uuid4().hex.replace("-", "")
+ initial = vdom_or_component_to_string(initial, uuid=uuid)
+ executor = render_pyscript_template(file_paths, uuid, root)
+
+ if not rendered:
+ # FIXME: This is needed to properly re-render PyScript during a WebSocket
+ # disconnection / reconnection. There may be a better way to do this in the future.
+ set_rendered(True)
+ return None
+
+ return html._(
+ html.div(
+ {"id": f"pyscript-{uuid}", "className": "pyscript", "data-uuid": uuid},
+ initial,
+ ),
+ pyscript({"async": ""}, executor),
+ )
diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py
index cabb61a4..21a30a32 100644
--- a/src/reactpy_django/config.py
+++ b/src/reactpy_django/config.py
@@ -7,7 +7,7 @@
from django.core.cache import DEFAULT_CACHE_ALIAS
from django.db import DEFAULT_DB_ALIAS
from django.views import View
-from reactpy.config import REACTPY_DEBUG_MODE
+from reactpy.config import REACTPY_DEBUG_MODE as _REACTPY_DEBUG_MODE
from reactpy.core.types import ComponentConstructor
from reactpy_django.types import (
@@ -17,7 +17,8 @@
from reactpy_django.utils import import_dotted_path
# Non-configurable values
-REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG"))
+_REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG"))
+REACTPY_DEBUG_MODE = _REACTPY_DEBUG_MODE.current
REACTPY_REGISTERED_COMPONENTS: dict[str, ComponentConstructor] = {}
REACTPY_FAILED_COMPONENTS: set[str] = set()
REACTPY_REGISTERED_IFRAME_VIEWS: dict[str, Callable | View] = {}
diff --git a/src/reactpy_django/html.py b/src/reactpy_django/html.py
new file mode 100644
index 00000000..d35daf43
--- /dev/null
+++ b/src/reactpy_django/html.py
@@ -0,0 +1,3 @@
+from reactpy.core.vdom import make_vdom_constructor
+
+pyscript = make_vdom_constructor("py-script")
diff --git a/src/reactpy_django/management/commands/clean_reactpy.py b/src/reactpy_django/management/commands/clean_reactpy.py
index bfde6f2f..0c5dc308 100644
--- a/src/reactpy_django/management/commands/clean_reactpy.py
+++ b/src/reactpy_django/management/commands/clean_reactpy.py
@@ -28,10 +28,10 @@ def add_arguments(self, parser):
parser.add_argument(
"--sessions",
action="store_true",
- help="Configure this clean to only clean session data (and other configured cleaning options).",
+ help="Clean session data. This value can be combined with other cleaning options.",
)
parser.add_argument(
"--user-data",
action="store_true",
- help="Configure this clean to only clean user data (and other configured cleaning options).",
+ help="Clean user data. This value can be combined with other cleaning options.",
)
diff --git a/src/reactpy_django/pyscript/__init__.py b/src/reactpy_django/pyscript/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/reactpy_django/pyscript/component_template.py b/src/reactpy_django/pyscript/component_template.py
new file mode 100644
index 00000000..59442571
--- /dev/null
+++ b/src/reactpy_django/pyscript/component_template.py
@@ -0,0 +1,26 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ import asyncio
+
+ from reactpy_django.pyscript.layout_handler import ReactPyLayoutHandler
+
+
+# User component is inserted below by regex replacement
+def user_workspace_UUID():
+ """Encapsulate the user's code with a completely unique function (workspace)
+ to prevent overlapping imports and variable names between different components.
+
+ This code is designed to be run directly by PyScript, and is not intended to be run
+ in a normal Python environment.
+
+ ReactPy-Django performs string substitutions to turn this file into valid PyScript.
+ """
+
+ def root(): ...
+
+ return root()
+
+
+# Create a task to run the user's component workspace
+task_UUID = asyncio.create_task(ReactPyLayoutHandler("UUID").run(user_workspace_UUID))
diff --git a/src/reactpy_django/pyscript/layout_handler.py b/src/reactpy_django/pyscript/layout_handler.py
new file mode 100644
index 00000000..da5bfb1b
--- /dev/null
+++ b/src/reactpy_django/pyscript/layout_handler.py
@@ -0,0 +1,138 @@
+# mypy: disable-error-code=attr-defined
+import asyncio
+
+
+class ReactPyLayoutHandler:
+ """Encapsulate the entire layout handler with a class to prevent overlapping
+ variable names between user code.
+
+ This code is designed to be run directly by PyScript, and is not intended to be run
+ in a normal Python environment.
+ """
+
+ def __init__(self, uuid):
+ self.uuid = uuid
+
+ @staticmethod
+ def apply_update(update, root_model):
+ """Apply an update ReactPy's internal DOM model."""
+ from jsonpointer import set_pointer
+
+ if update["path"]:
+ set_pointer(root_model, update["path"], update["model"])
+ else:
+ root_model.update(update["model"])
+
+ def render(self, layout, model):
+ """Submit ReactPy's internal DOM model into the HTML DOM."""
+ import js
+ from pyscript.js_modules import morphdom
+
+ # Create a new container to render the layout into
+ container = js.document.getElementById(f"pyscript-{self.uuid}")
+ temp_container = container.cloneNode(False)
+ self.build_element_tree(layout, temp_container, model)
+
+ # Use morphdom to update the DOM
+ morphdom.default(container, temp_container)
+
+ # Remove the cloned container to prevent memory leaks
+ temp_container.remove()
+
+ def build_element_tree(self, layout, parent, model):
+ """Recursively build an element tree, starting from the root component."""
+ import js
+
+ if isinstance(model, str):
+ parent.appendChild(js.document.createTextNode(model))
+ elif isinstance(model, dict):
+ if not model["tagName"]:
+ for child in model.get("children", []):
+ self.build_element_tree(layout, parent, child)
+ return
+ tag = model["tagName"]
+ attributes = model.get("attributes", {})
+ children = model.get("children", [])
+ element = js.document.createElement(tag)
+ for key, value in attributes.items():
+ if key == "style":
+ for style_key, style_value in value.items():
+ setattr(element.style, style_key, style_value)
+ elif key == "className":
+ element.className = value
+ else:
+ element.setAttribute(key, value)
+ for event_name, event_handler_model in model.get(
+ "eventHandlers", {}
+ ).items():
+ self.create_event_handler(
+ layout, element, event_name, event_handler_model
+ )
+ for child in children:
+ self.build_element_tree(layout, element, child)
+ parent.appendChild(element)
+ else:
+ raise ValueError(f"Unknown model type: {type(model)}")
+
+ @staticmethod
+ def create_event_handler(layout, element, event_name, event_handler_model):
+ """Create an event handler for an element. This function is used as an
+ adapter between ReactPy and browser events."""
+ from pyodide.ffi.wrappers import add_event_listener
+
+ target = event_handler_model["target"]
+
+ def event_handler(*args):
+ asyncio.create_task(
+ layout.deliver({"type": "layout-event", "target": target, "data": args})
+ )
+
+ event_name = event_name.lstrip("on_").lower().replace("_", "")
+ add_event_listener(element, event_name, event_handler)
+
+ @staticmethod
+ def delete_old_workspaces():
+ """To prevent memory leaks, we must delete all user generated Python code when
+ it is no longer in use (removed from the page). To do this, we compare what
+ UUIDs exist on the DOM, versus what UUIDs exist within the PyScript global
+ interpreter."""
+ import js
+
+ dom_workspaces = js.document.querySelectorAll(".pyscript")
+ dom_uuids = {element.dataset.uuid for element in dom_workspaces}
+ python_uuids = {
+ value.split("_")[-1]
+ for value in globals()
+ if value.startswith("user_workspace_")
+ }
+
+ # Delete the workspace if it exists at the moment when we check
+ for uuid in python_uuids - dom_uuids:
+ task_name = f"task_{uuid}"
+ if task_name in globals():
+ task: asyncio.Task = globals()[task_name]
+ task.cancel()
+ del globals()[task_name]
+ else:
+ print(f"Warning: Could not auto delete PyScript task {task_name}")
+
+ workspace_name = f"user_workspace_{uuid}"
+ if workspace_name in globals():
+ del globals()[workspace_name]
+ else:
+ print(
+ f"Warning: Could not auto delete PyScript workspace {workspace_name}"
+ )
+
+ async def run(self, workspace_function):
+ """Run the layout handler. This function is main executor for all user generated code."""
+ from reactpy.core.layout import Layout
+
+ self.delete_old_workspaces()
+ root_model: dict = {}
+
+ async with Layout(workspace_function()) as layout:
+ while True:
+ update = await layout.render()
+ self.apply_update(update, root_model)
+ self.render(layout, root_model)
diff --git a/src/reactpy_django/static/reactpy_django/pyscript-custom.css b/src/reactpy_django/static/reactpy_django/pyscript-custom.css
new file mode 100644
index 00000000..5793fd52
--- /dev/null
+++ b/src/reactpy_django/static/reactpy_django/pyscript-custom.css
@@ -0,0 +1,3 @@
+py-script {
+ display: none;
+}
diff --git a/src/reactpy_django/static/reactpy_django/pyscript-hide-debug.css b/src/reactpy_django/static/reactpy_django/pyscript-hide-debug.css
new file mode 100644
index 00000000..9cd8541e
--- /dev/null
+++ b/src/reactpy_django/static/reactpy_django/pyscript-hide-debug.css
@@ -0,0 +1,3 @@
+.py-error {
+ display: none;
+}
diff --git a/src/reactpy_django/templates/reactpy/pyscript_component.html b/src/reactpy_django/templates/reactpy/pyscript_component.html
new file mode 100644
index 00000000..a4767040
--- /dev/null
+++ b/src/reactpy_django/templates/reactpy/pyscript_component.html
@@ -0,0 +1,2 @@
+{{pyscript_initial_html}}
+{{pyscript_executor}}
diff --git a/src/reactpy_django/templates/reactpy/pyscript_setup.html b/src/reactpy_django/templates/reactpy/pyscript_setup.html
new file mode 100644
index 00000000..e258cf08
--- /dev/null
+++ b/src/reactpy_django/templates/reactpy/pyscript_setup.html
@@ -0,0 +1,8 @@
+{% load static %}
+
+
+{% if not reactpy_debug_mode %}
+
+{% endif %}
+
+{{pyscript_layout_handler}}
diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py
index e33d8387..1fdfa6af 100644
--- a/src/reactpy_django/templatetags/reactpy.py
+++ b/src/reactpy_django/templatetags/reactpy.py
@@ -3,16 +3,12 @@
from logging import getLogger
from uuid import uuid4
-import dill as pickle
from django import template
from django.http import HttpRequest
from django.urls import NoReverseMatch, reverse
-from reactpy.backend.hooks import ConnectionContext
-from reactpy.backend.types import Connection, Location
-from reactpy.core.types import ComponentConstructor
-from reactpy.utils import vdom_to_html
+from reactpy.core.types import ComponentConstructor, ComponentType, VdomDict
-from reactpy_django import config, models
+from reactpy_django import config as reactpy_config
from reactpy_django.exceptions import (
ComponentCarrierError,
ComponentDoesNotExistError,
@@ -20,8 +16,17 @@
InvalidHostError,
OfflineComponentMissing,
)
-from reactpy_django.types import ComponentParams
-from reactpy_django.utils import SyncLayout, strtobool, validate_component_args
+from reactpy_django.utils import (
+ PYSCRIPT_LAYOUT_HANDLER,
+ extend_pyscript_config,
+ prerender_component,
+ render_pyscript_template,
+ save_component_params,
+ strtobool,
+ validate_component_args,
+ validate_host,
+ vdom_or_component_to_string,
+)
try:
RESOLVED_WEB_MODULES_PATH = reverse("reactpy:web_modules", args=["/"]).strip("/")
@@ -37,7 +42,7 @@ def component(
dotted_path: str,
*args,
host: str | None = None,
- prerender: str = str(config.REACTPY_PRERENDER),
+ prerender: str = str(reactpy_config.REACTPY_PRERENDER),
offline: str = "",
**kwargs,
):
@@ -73,7 +78,11 @@ def component(
perceived_host = (request.get_host() if request else "").strip("/")
host = (
host
- or (next(config.REACTPY_DEFAULT_HOSTS) if config.REACTPY_DEFAULT_HOSTS else "")
+ or (
+ next(reactpy_config.REACTPY_DEFAULT_HOSTS)
+ if reactpy_config.REACTPY_DEFAULT_HOSTS
+ else ""
+ )
).strip("/")
is_local = not host or host.startswith(perceived_host)
uuid = str(uuid4())
@@ -84,7 +93,7 @@ def component(
_offline_html = ""
# Validate the host
- if host and config.REACTPY_DEBUG_MODE:
+ if host and reactpy_config.REACTPY_DEBUG_MODE:
try:
validate_host(host)
except InvalidHostError as e:
@@ -92,14 +101,14 @@ def component(
# Fetch the component
if is_local:
- user_component = config.REACTPY_REGISTERED_COMPONENTS.get(dotted_path)
+ user_component = reactpy_config.REACTPY_REGISTERED_COMPONENTS.get(dotted_path)
if not user_component:
msg = f"Component '{dotted_path}' is not registered as a root component. "
_logger.error(msg)
return failure_context(dotted_path, ComponentDoesNotExistError(msg))
# Validate the component args & kwargs
- if is_local and config.REACTPY_DEBUG_MODE:
+ if is_local and reactpy_config.REACTPY_DEBUG_MODE:
try:
validate_component_args(user_component, *args, **kwargs)
except ComponentParamError as e:
@@ -140,7 +149,7 @@ def component(
# Fetch the offline component's HTML, if requested
if offline:
- offline_component = config.REACTPY_REGISTERED_COMPONENTS.get(offline)
+ offline_component = reactpy_config.REACTPY_REGISTERED_COMPONENTS.get(offline)
if not offline_component:
msg = f"Cannot render offline component '{offline}'. It is not registered as a component."
_logger.error(msg)
@@ -159,63 +168,83 @@ def component(
"reactpy_class": class_,
"reactpy_uuid": uuid,
"reactpy_host": host or perceived_host,
- "reactpy_url_prefix": config.REACTPY_URL_PREFIX,
+ "reactpy_url_prefix": reactpy_config.REACTPY_URL_PREFIX,
"reactpy_component_path": f"{dotted_path}/{uuid}/{int(has_args)}/",
"reactpy_resolved_web_modules_path": RESOLVED_WEB_MODULES_PATH,
- "reactpy_reconnect_interval": config.REACTPY_RECONNECT_INTERVAL,
- "reactpy_reconnect_max_interval": config.REACTPY_RECONNECT_MAX_INTERVAL,
- "reactpy_reconnect_backoff_multiplier": config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER,
- "reactpy_reconnect_max_retries": config.REACTPY_RECONNECT_MAX_RETRIES,
+ "reactpy_reconnect_interval": reactpy_config.REACTPY_RECONNECT_INTERVAL,
+ "reactpy_reconnect_max_interval": reactpy_config.REACTPY_RECONNECT_MAX_INTERVAL,
+ "reactpy_reconnect_backoff_multiplier": reactpy_config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER,
+ "reactpy_reconnect_max_retries": reactpy_config.REACTPY_RECONNECT_MAX_RETRIES,
"reactpy_prerender_html": _prerender_html,
"reactpy_offline_html": _offline_html,
}
-def failure_context(dotted_path: str, error: Exception):
- return {
- "reactpy_failure": True,
- "reactpy_debug_mode": config.REACTPY_DEBUG_MODE,
- "reactpy_dotted_path": dotted_path,
- "reactpy_error": type(error).__name__,
- }
-
-
-def save_component_params(args, kwargs, uuid):
- params = ComponentParams(args, kwargs)
- model = models.ComponentSession(uuid=uuid, params=pickle.dumps(params))
- model.full_clean()
- model.save()
+@register.inclusion_tag("reactpy/pyscript_component.html", takes_context=True)
+def pyscript_component(
+ context: template.RequestContext,
+ *file_paths: str,
+ initial: str | VdomDict | ComponentType = "",
+ root: str = "root",
+):
+ """
+ Args:
+ file_paths: File path to your client-side component. If multiple paths are \
+ provided, the contents are automatically merged.
+
+ Kwargs:
+ initial: The initial HTML that is displayed prior to the PyScript component \
+ loads. This can either be a string containing raw HTML, a \
+ `#!python reactpy.html` snippet, or a non-interactive component.
+ root: The name of the root component function.
+ """
+ if not file_paths:
+ raise ValueError(
+ "At least one file path must be provided to the 'pyscript_component' tag."
+ )
+ uuid = uuid4().hex
+ request: HttpRequest | None = context.get("request")
+ initial = vdom_or_component_to_string(initial, request=request, uuid=uuid)
+ executor = render_pyscript_template(file_paths, uuid, root)
-def validate_host(host: str):
- if "://" in host:
- protocol = host.split("://")[0]
- msg = (
- f"Invalid host provided to component. Contains a protocol '{protocol}://'."
- )
- _logger.error(msg)
- raise InvalidHostError(msg)
+ return {
+ "pyscript_executor": executor,
+ "pyscript_uuid": uuid,
+ "pyscript_initial_html": initial,
+ }
-def prerender_component(
- user_component: ComponentConstructor, args, kwargs, uuid, request: HttpRequest
+@register.inclusion_tag("reactpy/pyscript_setup.html")
+def pyscript_setup(
+ *extra_py: str,
+ extra_js: str | dict = "",
+ config: str | dict = "",
):
- search = request.GET.urlencode()
- scope = getattr(request, "scope", {})
- scope["reactpy"] = {"id": str(uuid)}
-
- with SyncLayout(
- ConnectionContext(
- user_component(*args, **kwargs),
- value=Connection(
- scope=scope,
- location=Location(
- pathname=request.path, search=f"?{search}" if search else ""
- ),
- carrier=request,
- ),
- )
- ) as layout:
- vdom_tree = layout.render()["model"]
+ """
+ Args:
+ extra_py: Dependencies that need to be loaded on the page for \
+ your PyScript components. Each dependency must be contained \
+ within it's own string and written in Python requirements file syntax.
+
+ Kwargs:
+ extra_js: A JSON string or Python dictionary containing a vanilla \
+ JavaScript module URL and the `name: str` to access it within \
+ `pyscript.js_modules.*`.
+ config: A JSON string or Python dictionary containing PyScript \
+ configuration values.
+ """
+ return {
+ "pyscript_config": extend_pyscript_config(extra_py, extra_js, config),
+ "pyscript_layout_handler": PYSCRIPT_LAYOUT_HANDLER,
+ "reactpy_debug_mode": reactpy_config.REACTPY_DEBUG_MODE,
+ }
+
- return vdom_to_html(vdom_tree)
+def failure_context(dotted_path: str, error: Exception):
+ return {
+ "reactpy_failure": True,
+ "reactpy_debug_mode": reactpy_config.REACTPY_DEBUG_MODE,
+ "reactpy_dotted_path": dotted_path,
+ "reactpy_error": type(error).__name__,
+ }
diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py
index 73538ad7..48559e84 100644
--- a/src/reactpy_django/utils.py
+++ b/src/reactpy_django/utils.py
@@ -2,14 +2,23 @@
import contextlib
import inspect
+import json
import logging
import os
import re
+import textwrap
from asyncio import iscoroutinefunction
+from copy import deepcopy
from fnmatch import fnmatch
from importlib import import_module
-from typing import Any, Callable, Sequence
-
+from pathlib import Path
+from typing import Any, Callable, Mapping, Sequence
+from uuid import UUID, uuid4
+
+import dill
+import jsonpointer
+import orjson
+import reactpy
from asgiref.sync import async_to_sync
from channels.db import database_sync_to_async
from django.db.models import ManyToManyField, ManyToOneRel, prefetch_related_objects
@@ -17,14 +26,19 @@
from django.db.models.query import QuerySet
from django.http import HttpRequest, HttpResponse
from django.template import engines
+from django.templatetags.static import static
from django.utils.encoding import smart_str
from django.views import View
+from reactpy import vdom_to_html
+from reactpy.backend.hooks import ConnectionContext
+from reactpy.backend.types import Connection, Location
from reactpy.core.layout import Layout
from reactpy.types import ComponentConstructor
from reactpy_django.exceptions import (
ComponentDoesNotExistError,
ComponentParamError,
+ InvalidHostError,
ViewDoesNotExistError,
)
@@ -44,6 +58,13 @@
+ rf"({_OFFLINE_KWARG_PATTERN}|{_GENERIC_KWARG_PATTERN})*?"
+ r"\s*%}"
)
+PYSCRIPT_COMPONENT_TEMPLATE = (
+ Path(__file__).parent / "pyscript" / "component_template.py"
+).read_text(encoding="utf-8")
+PYSCRIPT_LAYOUT_HANDLER = (
+ Path(__file__).parent / "pyscript" / "layout_handler.py"
+).read_text(encoding="utf-8")
+PYSCRIPT_DEFAULT_CONFIG: dict[str, Any] = {}
async def render_view(
@@ -381,3 +402,159 @@ def strtobool(val):
return 0
else:
raise ValueError(f"invalid truth value {val}")
+
+
+def prerender_component(
+ user_component: ComponentConstructor,
+ args: Sequence,
+ kwargs: Mapping,
+ uuid: str | UUID,
+ request: HttpRequest,
+) -> str:
+ """Prerenders a ReactPy component and returns the HTML string."""
+ search = request.GET.urlencode()
+ scope = getattr(request, "scope", {})
+ scope["reactpy"] = {"id": str(uuid)}
+
+ with SyncLayout(
+ ConnectionContext(
+ user_component(*args, **kwargs),
+ value=Connection(
+ scope=scope,
+ location=Location(
+ pathname=request.path, search=f"?{search}" if search else ""
+ ),
+ carrier=request,
+ ),
+ )
+ ) as layout:
+ vdom_tree = layout.render()["model"]
+
+ return vdom_to_html(vdom_tree)
+
+
+def vdom_or_component_to_string(
+ vdom_or_component: Any, request: HttpRequest | None = None, uuid: str | None = None
+) -> str:
+ """Converts a VdomDict or component to an HTML string. If a string is provided instead, it will be
+ automatically returned."""
+ if isinstance(vdom_or_component, dict):
+ return vdom_to_html(vdom_or_component) # type: ignore
+
+ if hasattr(vdom_or_component, "render"):
+ if not request:
+ request = HttpRequest()
+ request.method = "GET"
+ if not uuid:
+ uuid = uuid4().hex
+ return prerender_component(vdom_or_component, [], {}, uuid, request)
+
+ if isinstance(vdom_or_component, str):
+ return vdom_or_component
+
+ raise ValueError(
+ f"Invalid type for vdom_or_component: {type(vdom_or_component)}. "
+ "Expected a VdomDict, component, or string."
+ )
+
+
+def render_pyscript_template(file_paths: Sequence[str], uuid: str, root: str):
+ """Inserts the user's code into the PyScript template using pattern matching."""
+ from django.core.cache import caches
+
+ from reactpy_django.config import REACTPY_CACHE
+
+ # Create a valid PyScript executor by replacing the template values
+ executor = PYSCRIPT_COMPONENT_TEMPLATE.replace("UUID", uuid)
+ executor = executor.replace("return root()", f"return {root}()")
+
+ # Fetch the user's PyScript code
+ all_file_contents: list[str] = []
+ for file_path in file_paths:
+ # Try to get user code from cache
+ cache_key = create_cache_key("pyscript", file_path)
+ last_modified_time = os.stat(file_path).st_mtime
+ file_contents: str = caches[REACTPY_CACHE].get(
+ cache_key, version=int(last_modified_time)
+ )
+ if file_contents:
+ all_file_contents.append(file_contents)
+
+ # If not cached, read from file system
+ else:
+ file_contents = Path(file_path).read_text(encoding="utf-8").strip()
+ all_file_contents.append(file_contents)
+ caches[REACTPY_CACHE].set(
+ cache_key, file_contents, version=int(last_modified_time)
+ )
+
+ # Prepare the PyScript code block
+ user_code = "\n".join(all_file_contents) # Combine all user code
+ user_code = user_code.replace("\t", " ") # Normalize the text
+ user_code = textwrap.indent(user_code, " ") # Add indentation to match template
+
+ # Insert the user code into the PyScript template
+ return executor.replace(" def root(): ...", user_code)
+
+
+def extend_pyscript_config(
+ extra_py: Sequence, extra_js: dict | str, config: dict | str
+) -> str:
+ """Extends ReactPy's default PyScript config with user provided values."""
+ # Lazily set up the initial config in to wait for Django's static file system
+ if not PYSCRIPT_DEFAULT_CONFIG:
+ PYSCRIPT_DEFAULT_CONFIG.update(
+ {
+ "packages": [
+ f"reactpy=={reactpy.__version__}",
+ f"jsonpointer=={jsonpointer.__version__}",
+ "ssl",
+ ],
+ "js_modules": {
+ "main": {
+ static("reactpy_django/morphdom/morphdom-esm.js"): "morphdom"
+ }
+ },
+ }
+ )
+
+ # Extend the Python dependency list
+ pyscript_config = deepcopy(PYSCRIPT_DEFAULT_CONFIG)
+ pyscript_config["packages"].extend(extra_py)
+
+ # Extend the JavaScript dependency list
+ if extra_js and isinstance(extra_js, str):
+ pyscript_config["js_modules"]["main"].update(json.loads(extra_js))
+ elif extra_js and isinstance(extra_js, dict):
+ pyscript_config["js_modules"]["main"].update(extra_py)
+
+ # Update the config
+ if config and isinstance(config, str):
+ pyscript_config.update(json.loads(config))
+ elif config and isinstance(config, dict):
+ pyscript_config.update(config)
+ return orjson.dumps(pyscript_config).decode("utf-8")
+
+
+def save_component_params(args, kwargs, uuid) -> None:
+ """Saves the component parameters to the database.
+ This is used within our template tag in order to propogate
+ the parameters between the HTTP and WebSocket stack."""
+ from reactpy_django import models
+ from reactpy_django.types import ComponentParams
+
+ params = ComponentParams(args, kwargs)
+ model = models.ComponentSession(uuid=uuid, params=dill.dumps(params))
+ model.full_clean()
+ model.save()
+
+
+def validate_host(host: str) -> None:
+ """Validates the host string to ensure it does not contain a protocol."""
+ if "://" in host:
+ protocol = host.split("://")[0]
+ msg = (
+ f"Invalid host provided to component. Contains a protocol '{protocol}://'."
+ )
+ _logger.error(msg)
+ raise InvalidHostError(msg)
diff --git a/tests/test_app/__init__.py b/tests/test_app/__init__.py
index 86dbd107..392e6066 100644
--- a/tests/test_app/__init__.py
+++ b/tests/test_app/__init__.py
@@ -1,3 +1,4 @@
+import shutil
from pathlib import Path
from nodejs import npm
@@ -6,3 +7,33 @@
js_dir = Path(__file__).parent.parent.parent / "src" / "js"
assert npm.call(["install"], cwd=str(js_dir)) == 0
assert npm.call(["run", "build"], cwd=str(js_dir)) == 0
+
+# Make sure the current PyScript distribution is always available
+pyscript_dist = js_dir / "node_modules" / "@pyscript" / "core" / "dist"
+pyscript_static_dir = (
+ Path(__file__).parent.parent.parent
+ / "src"
+ / "reactpy_django"
+ / "static"
+ / "reactpy_django"
+ / "pyscript"
+)
+if not pyscript_static_dir.exists():
+ pyscript_static_dir.mkdir()
+for file in pyscript_dist.iterdir():
+ shutil.copy(file, pyscript_static_dir / file.name)
+
+# Make sure the current Morphdom distrubiton is always available
+morphdom_dist = js_dir / "node_modules" / "morphdom" / "dist"
+morphdom_static_dir = (
+ Path(__file__).parent.parent.parent
+ / "src"
+ / "reactpy_django"
+ / "static"
+ / "reactpy_django"
+ / "morphdom"
+)
+if not morphdom_static_dir.exists():
+ morphdom_static_dir.mkdir()
+for file in morphdom_dist.iterdir():
+ shutil.copy(file, morphdom_static_dir / file.name)
diff --git a/tests/test_app/components.py b/tests/test_app/components.py
index fe6df2f0..69b1541c 100644
--- a/tests/test_app/components.py
+++ b/tests/test_app/components.py
@@ -724,10 +724,10 @@ async def on_submit(event):
),
},
html.div("use_user_data"),
- html.button({"class": "login-1", "on_click": login_user1}, "Login 1"),
- html.button({"class": "login-2", "on_click": login_user2}, "Login 2"),
- html.button({"class": "logout", "on_click": logout_user}, "Logout"),
- html.button({"class": "clear", "on_click": clear_data}, "Clear Data"),
+ html.button({"className": "login-1", "on_click": login_user1}, "Login 1"),
+ html.button({"className": "login-2", "on_click": login_user2}, "Login 2"),
+ html.button({"className": "logout", "on_click": logout_user}, "Logout"),
+ html.button({"className": "clear", "on_click": clear_data}, "Clear Data"),
html.div(f"User: {current_user}"),
html.div(f"Data: {user_data_query.data}"),
html.div(
@@ -792,8 +792,8 @@ async def on_submit(event):
),
},
html.div("use_user_data_with_default"),
- html.button({"class": "login-3", "on_click": login_user3}, "Login 3"),
- html.button({"class": "clear", "on_click": clear_data}, "Clear Data"),
+ html.button({"className": "login-3", "on_click": login_user3}, "Login 3"),
+ html.button({"className": "clear", "on_click": clear_data}, "Clear Data"),
html.div(f"User: {current_user}"),
html.div(f"Data: {user_data_query.data}"),
html.div(
diff --git a/tests/test_app/pyscript/__init__.py b/tests/test_app/pyscript/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_app/pyscript/components/__init__.py b/tests/test_app/pyscript/components/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_app/pyscript/components/child.py b/tests/test_app/pyscript/components/child.py
new file mode 100644
index 00000000..1f4a7824
--- /dev/null
+++ b/tests/test_app/pyscript/components/child.py
@@ -0,0 +1,25 @@
+from reactpy import component, html, use_state
+
+
+@component
+def root():
+ value, set_value = use_state(0)
+ return html.article(
+ {"id": "child"},
+ "This was embedded via a server-side component.",
+ html.div(
+ {"className": "grid"},
+ html.button(
+ {"className": "plus", "on_click": lambda event: set_value(value + 1)},
+ "+",
+ ),
+ html.button(
+ {"className": "minus", "on_click": lambda event: set_value(value - 1)},
+ "-",
+ ),
+ ),
+ "Current value",
+ html.pre(
+ {"style": {"font-style": "bold"}, "data-value": str(value)}, str(value)
+ ),
+ )
diff --git a/tests/test_app/pyscript/components/counter.py b/tests/test_app/pyscript/components/counter.py
new file mode 100644
index 00000000..31df55a1
--- /dev/null
+++ b/tests/test_app/pyscript/components/counter.py
@@ -0,0 +1,24 @@
+from reactpy import component, html, use_state
+
+
+@component
+def root():
+ value, set_value = use_state(0)
+ return html.article(
+ {"id": "counter"},
+ html.div(
+ {"className": "grid"},
+ html.button(
+ {"className": "plus", "on_click": lambda event: set_value(value + 1)},
+ "+",
+ ),
+ html.button(
+ {"className": "minus", "on_click": lambda event: set_value(value - 1)},
+ "-",
+ ),
+ ),
+ "Current value",
+ html.pre(
+ {"style": {"font-style": "bold"}, "data-value": str(value)}, str(value)
+ ),
+ )
diff --git a/tests/test_app/pyscript/components/custom_root.py b/tests/test_app/pyscript/components/custom_root.py
new file mode 100644
index 00000000..ee44fde4
--- /dev/null
+++ b/tests/test_app/pyscript/components/custom_root.py
@@ -0,0 +1,6 @@
+from reactpy import component, html
+
+
+@component
+def main():
+ return html.div({"id": "custom-root"}, "Component with a custom root name.")
diff --git a/tests/test_app/pyscript/components/hello_world.py b/tests/test_app/pyscript/components/hello_world.py
new file mode 100644
index 00000000..d8c36ee8
--- /dev/null
+++ b/tests/test_app/pyscript/components/hello_world.py
@@ -0,0 +1,6 @@
+from reactpy import component, html
+
+
+@component
+def root():
+ return html.div({"id": "hello-world"}, "hello world")
diff --git a/tests/test_app/pyscript/components/multifile_child.py b/tests/test_app/pyscript/components/multifile_child.py
new file mode 100644
index 00000000..4658e8a2
--- /dev/null
+++ b/tests/test_app/pyscript/components/multifile_child.py
@@ -0,0 +1,6 @@
+from reactpy import component, html
+
+
+@component
+def child():
+ return html.div({"id": "multifile-child"}, "Multifile child")
diff --git a/tests/test_app/pyscript/components/multifile_parent.py b/tests/test_app/pyscript/components/multifile_parent.py
new file mode 100644
index 00000000..48a1b1d8
--- /dev/null
+++ b/tests/test_app/pyscript/components/multifile_parent.py
@@ -0,0 +1,11 @@
+from typing import TYPE_CHECKING
+
+from reactpy import component, html
+
+if TYPE_CHECKING:
+ from .multifile_child import child
+
+
+@component
+def root():
+ return html.div({"id": "multifile-parent"}, "Multifile root", child())
diff --git a/tests/test_app/pyscript/components/remote_js_module.py b/tests/test_app/pyscript/components/remote_js_module.py
new file mode 100644
index 00000000..26eccf03
--- /dev/null
+++ b/tests/test_app/pyscript/components/remote_js_module.py
@@ -0,0 +1,14 @@
+from reactpy import component, html
+
+
+@component
+def root():
+ from pyscript.js_modules import moment
+
+ time: str = moment.default().format("YYYY-MM-DD HH:mm:ss")
+
+ return html.div(
+ {"id": "moment", "data-success": bool(time)},
+ "Using the JavaScript package 'moment' to calculate time: ",
+ time,
+ )
diff --git a/tests/test_app/pyscript/components/server_side.py b/tests/test_app/pyscript/components/server_side.py
new file mode 100644
index 00000000..fe31d527
--- /dev/null
+++ b/tests/test_app/pyscript/components/server_side.py
@@ -0,0 +1,33 @@
+from reactpy import component, html, use_state
+from reactpy_django.components import pyscript_component
+
+
+@component
+def parent():
+ return html.div(
+ {"id": "parent"},
+ pyscript_component("./test_app/pyscript/components/child.py"),
+ )
+
+
+@component
+def parent_toggle():
+ state, set_state = use_state(False)
+
+ if not state:
+ return html.div(
+ {"id": "parent-toggle"},
+ html.button(
+ {"onClick": lambda x: set_state(not state)},
+ "Click to show/hide",
+ ),
+ )
+
+ return html.div(
+ {"id": "parent-toggle"},
+ html.button(
+ {"onClick": lambda x: set_state(not state)},
+ "Click to show/hide",
+ ),
+ pyscript_component("./test_app/pyscript/components/child.py"),
+ )
diff --git a/tests/test_app/pyscript/urls.py b/tests/test_app/pyscript/urls.py
new file mode 100644
index 00000000..e71c18e5
--- /dev/null
+++ b/tests/test_app/pyscript/urls.py
@@ -0,0 +1,7 @@
+from django.urls import re_path
+
+from test_app.pyscript.views import pyscript
+
+urlpatterns = [
+ re_path(r"^pyscript/(?P.*)/?$", pyscript),
+]
diff --git a/tests/test_app/pyscript/views.py b/tests/test_app/pyscript/views.py
new file mode 100644
index 00000000..f4891be1
--- /dev/null
+++ b/tests/test_app/pyscript/views.py
@@ -0,0 +1,5 @@
+from django.shortcuts import render
+
+
+def pyscript(request, path=None):
+ return render(request, "pyscript.html", {})
diff --git a/tests/test_app/static/moment.js b/tests/test_app/static/moment.js
new file mode 100644
index 00000000..956eeb3d
--- /dev/null
+++ b/tests/test_app/static/moment.js
@@ -0,0 +1,5680 @@
+//! moment.js
+//! version : 2.30.1
+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+//! license : MIT
+//! momentjs.com
+
+var hookCallback;
+
+function hooks() {
+ return hookCallback.apply(null, arguments);
+}
+
+// This is done to register the method called with moment()
+// without creating circular dependencies.
+function setHookCallback(callback) {
+ hookCallback = callback;
+}
+
+function isArray(input) {
+ return (
+ input instanceof Array ||
+ Object.prototype.toString.call(input) === '[object Array]'
+ );
+}
+
+function isObject(input) {
+ // IE8 will treat undefined and null as object if it wasn't for
+ // input != null
+ return (
+ input != null &&
+ Object.prototype.toString.call(input) === '[object Object]'
+ );
+}
+
+function hasOwnProp(a, b) {
+ return Object.prototype.hasOwnProperty.call(a, b);
+}
+
+function isObjectEmpty(obj) {
+ if (Object.getOwnPropertyNames) {
+ return Object.getOwnPropertyNames(obj).length === 0;
+ } else {
+ var k;
+ for (k in obj) {
+ if (hasOwnProp(obj, k)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
+function isUndefined(input) {
+ return input === void 0;
+}
+
+function isNumber(input) {
+ return (
+ typeof input === 'number' ||
+ Object.prototype.toString.call(input) === '[object Number]'
+ );
+}
+
+function isDate(input) {
+ return (
+ input instanceof Date ||
+ Object.prototype.toString.call(input) === '[object Date]'
+ );
+}
+
+function map(arr, fn) {
+ var res = [],
+ i,
+ arrLen = arr.length;
+ for (i = 0; i < arrLen; ++i) {
+ res.push(fn(arr[i], i));
+ }
+ return res;
+}
+
+function extend(a, b) {
+ for (var i in b) {
+ if (hasOwnProp(b, i)) {
+ a[i] = b[i];
+ }
+ }
+
+ if (hasOwnProp(b, 'toString')) {
+ a.toString = b.toString;
+ }
+
+ if (hasOwnProp(b, 'valueOf')) {
+ a.valueOf = b.valueOf;
+ }
+
+ return a;
+}
+
+function createUTC(input, format, locale, strict) {
+ return createLocalOrUTC(input, format, locale, strict, true).utc();
+}
+
+function defaultParsingFlags() {
+ // We need to deep clone this object.
+ return {
+ empty: false,
+ unusedTokens: [],
+ unusedInput: [],
+ overflow: -2,
+ charsLeftOver: 0,
+ nullInput: false,
+ invalidEra: null,
+ invalidMonth: null,
+ invalidFormat: false,
+ userInvalidated: false,
+ iso: false,
+ parsedDateParts: [],
+ era: null,
+ meridiem: null,
+ rfc2822: false,
+ weekdayMismatch: false,
+ };
+}
+
+function getParsingFlags(m) {
+ if (m._pf == null) {
+ m._pf = defaultParsingFlags();
+ }
+ return m._pf;
+}
+
+var some;
+if (Array.prototype.some) {
+ some = Array.prototype.some;
+} else {
+ some = function (fun) {
+ var t = Object(this),
+ len = t.length >>> 0,
+ i;
+
+ for (i = 0; i < len; i++) {
+ if (i in t && fun.call(this, t[i], i, t)) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+}
+
+function isValid(m) {
+ var flags = null,
+ parsedParts = false,
+ isNowValid = m._d && !isNaN(m._d.getTime());
+ if (isNowValid) {
+ flags = getParsingFlags(m);
+ parsedParts = some.call(flags.parsedDateParts, function (i) {
+ return i != null;
+ });
+ isNowValid =
+ flags.overflow < 0 &&
+ !flags.empty &&
+ !flags.invalidEra &&
+ !flags.invalidMonth &&
+ !flags.invalidWeekday &&
+ !flags.weekdayMismatch &&
+ !flags.nullInput &&
+ !flags.invalidFormat &&
+ !flags.userInvalidated &&
+ (!flags.meridiem || (flags.meridiem && parsedParts));
+ if (m._strict) {
+ isNowValid =
+ isNowValid &&
+ flags.charsLeftOver === 0 &&
+ flags.unusedTokens.length === 0 &&
+ flags.bigHour === undefined;
+ }
+ }
+ if (Object.isFrozen == null || !Object.isFrozen(m)) {
+ m._isValid = isNowValid;
+ } else {
+ return isNowValid;
+ }
+ return m._isValid;
+}
+
+function createInvalid(flags) {
+ var m = createUTC(NaN);
+ if (flags != null) {
+ extend(getParsingFlags(m), flags);
+ } else {
+ getParsingFlags(m).userInvalidated = true;
+ }
+
+ return m;
+}
+
+// Plugins that add properties should also add the key here (null value),
+// so we can properly clone ourselves.
+var momentProperties = (hooks.momentProperties = []),
+ updateInProgress = false;
+
+function copyConfig(to, from) {
+ var i,
+ prop,
+ val,
+ momentPropertiesLen = momentProperties.length;
+
+ if (!isUndefined(from._isAMomentObject)) {
+ to._isAMomentObject = from._isAMomentObject;
+ }
+ if (!isUndefined(from._i)) {
+ to._i = from._i;
+ }
+ if (!isUndefined(from._f)) {
+ to._f = from._f;
+ }
+ if (!isUndefined(from._l)) {
+ to._l = from._l;
+ }
+ if (!isUndefined(from._strict)) {
+ to._strict = from._strict;
+ }
+ if (!isUndefined(from._tzm)) {
+ to._tzm = from._tzm;
+ }
+ if (!isUndefined(from._isUTC)) {
+ to._isUTC = from._isUTC;
+ }
+ if (!isUndefined(from._offset)) {
+ to._offset = from._offset;
+ }
+ if (!isUndefined(from._pf)) {
+ to._pf = getParsingFlags(from);
+ }
+ if (!isUndefined(from._locale)) {
+ to._locale = from._locale;
+ }
+
+ if (momentPropertiesLen > 0) {
+ for (i = 0; i < momentPropertiesLen; i++) {
+ prop = momentProperties[i];
+ val = from[prop];
+ if (!isUndefined(val)) {
+ to[prop] = val;
+ }
+ }
+ }
+
+ return to;
+}
+
+// Moment prototype object
+function Moment(config) {
+ copyConfig(this, config);
+ this._d = new Date(config._d != null ? config._d.getTime() : NaN);
+ if (!this.isValid()) {
+ this._d = new Date(NaN);
+ }
+ // Prevent infinite loop in case updateOffset creates new moment
+ // objects.
+ if (updateInProgress === false) {
+ updateInProgress = true;
+ hooks.updateOffset(this);
+ updateInProgress = false;
+ }
+}
+
+function isMoment(obj) {
+ return (
+ obj instanceof Moment || (obj != null && obj._isAMomentObject != null)
+ );
+}
+
+function warn(msg) {
+ if (
+ hooks.suppressDeprecationWarnings === false &&
+ typeof console !== 'undefined' &&
+ console.warn
+ ) {
+ console.warn('Deprecation warning: ' + msg);
+ }
+}
+
+function deprecate(msg, fn) {
+ var firstTime = true;
+
+ return extend(function () {
+ if (hooks.deprecationHandler != null) {
+ hooks.deprecationHandler(null, msg);
+ }
+ if (firstTime) {
+ var args = [],
+ arg,
+ i,
+ key,
+ argLen = arguments.length;
+ for (i = 0; i < argLen; i++) {
+ arg = '';
+ if (typeof arguments[i] === 'object') {
+ arg += '\n[' + i + '] ';
+ for (key in arguments[0]) {
+ if (hasOwnProp(arguments[0], key)) {
+ arg += key + ': ' + arguments[0][key] + ', ';
+ }
+ }
+ arg = arg.slice(0, -2); // Remove trailing comma and space
+ } else {
+ arg = arguments[i];
+ }
+ args.push(arg);
+ }
+ warn(
+ msg +
+ '\nArguments: ' +
+ Array.prototype.slice.call(args).join('') +
+ '\n' +
+ new Error().stack
+ );
+ firstTime = false;
+ }
+ return fn.apply(this, arguments);
+ }, fn);
+}
+
+var deprecations = {};
+
+function deprecateSimple(name, msg) {
+ if (hooks.deprecationHandler != null) {
+ hooks.deprecationHandler(name, msg);
+ }
+ if (!deprecations[name]) {
+ warn(msg);
+ deprecations[name] = true;
+ }
+}
+
+hooks.suppressDeprecationWarnings = false;
+hooks.deprecationHandler = null;
+
+function isFunction(input) {
+ return (
+ (typeof Function !== 'undefined' && input instanceof Function) ||
+ Object.prototype.toString.call(input) === '[object Function]'
+ );
+}
+
+function set(config) {
+ var prop, i;
+ for (i in config) {
+ if (hasOwnProp(config, i)) {
+ prop = config[i];
+ if (isFunction(prop)) {
+ this[i] = prop;
+ } else {
+ this['_' + i] = prop;
+ }
+ }
+ }
+ this._config = config;
+ // Lenient ordinal parsing accepts just a number in addition to
+ // number + (possibly) stuff coming from _dayOfMonthOrdinalParse.
+ // TODO: Remove "ordinalParse" fallback in next major release.
+ this._dayOfMonthOrdinalParseLenient = new RegExp(
+ (this._dayOfMonthOrdinalParse.source || this._ordinalParse.source) +
+ '|' +
+ /\d{1,2}/.source
+ );
+}
+
+function mergeConfigs(parentConfig, childConfig) {
+ var res = extend({}, parentConfig),
+ prop;
+ for (prop in childConfig) {
+ if (hasOwnProp(childConfig, prop)) {
+ if (isObject(parentConfig[prop]) && isObject(childConfig[prop])) {
+ res[prop] = {};
+ extend(res[prop], parentConfig[prop]);
+ extend(res[prop], childConfig[prop]);
+ } else if (childConfig[prop] != null) {
+ res[prop] = childConfig[prop];
+ } else {
+ delete res[prop];
+ }
+ }
+ }
+ for (prop in parentConfig) {
+ if (
+ hasOwnProp(parentConfig, prop) &&
+ !hasOwnProp(childConfig, prop) &&
+ isObject(parentConfig[prop])
+ ) {
+ // make sure changes to properties don't modify parent config
+ res[prop] = extend({}, res[prop]);
+ }
+ }
+ return res;
+}
+
+function Locale(config) {
+ if (config != null) {
+ this.set(config);
+ }
+}
+
+var keys;
+
+if (Object.keys) {
+ keys = Object.keys;
+} else {
+ keys = function (obj) {
+ var i,
+ res = [];
+ for (i in obj) {
+ if (hasOwnProp(obj, i)) {
+ res.push(i);
+ }
+ }
+ return res;
+ };
+}
+
+var defaultCalendar = {
+ sameDay: '[Today at] LT',
+ nextDay: '[Tomorrow at] LT',
+ nextWeek: 'dddd [at] LT',
+ lastDay: '[Yesterday at] LT',
+ lastWeek: '[Last] dddd [at] LT',
+ sameElse: 'L',
+};
+
+function calendar(key, mom, now) {
+ var output = this._calendar[key] || this._calendar['sameElse'];
+ return isFunction(output) ? output.call(mom, now) : output;
+}
+
+function zeroFill(number, targetLength, forceSign) {
+ var absNumber = '' + Math.abs(number),
+ zerosToFill = targetLength - absNumber.length,
+ sign = number >= 0;
+ return (
+ (sign ? (forceSign ? '+' : '') : '-') +
+ Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) +
+ absNumber
+ );
+}
+
+var formattingTokens =
+ /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,
+ localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,
+ formatFunctions = {},
+ formatTokenFunctions = {};
+
+// token: 'M'
+// padded: ['MM', 2]
+// ordinal: 'Mo'
+// callback: function () { this.month() + 1 }
+function addFormatToken(token, padded, ordinal, callback) {
+ var func = callback;
+ if (typeof callback === 'string') {
+ func = function () {
+ return this[callback]();
+ };
+ }
+ if (token) {
+ formatTokenFunctions[token] = func;
+ }
+ if (padded) {
+ formatTokenFunctions[padded[0]] = function () {
+ return zeroFill(func.apply(this, arguments), padded[1], padded[2]);
+ };
+ }
+ if (ordinal) {
+ formatTokenFunctions[ordinal] = function () {
+ return this.localeData().ordinal(
+ func.apply(this, arguments),
+ token
+ );
+ };
+ }
+}
+
+function removeFormattingTokens(input) {
+ if (input.match(/\[[\s\S]/)) {
+ return input.replace(/^\[|\]$/g, '');
+ }
+ return input.replace(/\\/g, '');
+}
+
+function makeFormatFunction(format) {
+ var array = format.match(formattingTokens),
+ i,
+ length;
+
+ for (i = 0, length = array.length; i < length; i++) {
+ if (formatTokenFunctions[array[i]]) {
+ array[i] = formatTokenFunctions[array[i]];
+ } else {
+ array[i] = removeFormattingTokens(array[i]);
+ }
+ }
+
+ return function (mom) {
+ var output = '',
+ i;
+ for (i = 0; i < length; i++) {
+ output += isFunction(array[i])
+ ? array[i].call(mom, format)
+ : array[i];
+ }
+ return output;
+ };
+}
+
+// format date using native date object
+function formatMoment(m, format) {
+ if (!m.isValid()) {
+ return m.localeData().invalidDate();
+ }
+
+ format = expandFormat(format, m.localeData());
+ formatFunctions[format] =
+ formatFunctions[format] || makeFormatFunction(format);
+
+ return formatFunctions[format](m);
+}
+
+function expandFormat(format, locale) {
+ var i = 5;
+
+ function replaceLongDateFormatTokens(input) {
+ return locale.longDateFormat(input) || input;
+ }
+
+ localFormattingTokens.lastIndex = 0;
+ while (i >= 0 && localFormattingTokens.test(format)) {
+ format = format.replace(
+ localFormattingTokens,
+ replaceLongDateFormatTokens
+ );
+ localFormattingTokens.lastIndex = 0;
+ i -= 1;
+ }
+
+ return format;
+}
+
+var defaultLongDateFormat = {
+ LTS: 'h:mm:ss A',
+ LT: 'h:mm A',
+ L: 'MM/DD/YYYY',
+ LL: 'MMMM D, YYYY',
+ LLL: 'MMMM D, YYYY h:mm A',
+ LLLL: 'dddd, MMMM D, YYYY h:mm A',
+};
+
+function longDateFormat(key) {
+ var format = this._longDateFormat[key],
+ formatUpper = this._longDateFormat[key.toUpperCase()];
+
+ if (format || !formatUpper) {
+ return format;
+ }
+
+ this._longDateFormat[key] = formatUpper
+ .match(formattingTokens)
+ .map(function (tok) {
+ if (
+ tok === 'MMMM' ||
+ tok === 'MM' ||
+ tok === 'DD' ||
+ tok === 'dddd'
+ ) {
+ return tok.slice(1);
+ }
+ return tok;
+ })
+ .join('');
+
+ return this._longDateFormat[key];
+}
+
+var defaultInvalidDate = 'Invalid date';
+
+function invalidDate() {
+ return this._invalidDate;
+}
+
+var defaultOrdinal = '%d',
+ defaultDayOfMonthOrdinalParse = /\d{1,2}/;
+
+function ordinal(number) {
+ return this._ordinal.replace('%d', number);
+}
+
+var defaultRelativeTime = {
+ future: 'in %s',
+ past: '%s ago',
+ s: 'a few seconds',
+ ss: '%d seconds',
+ m: 'a minute',
+ mm: '%d minutes',
+ h: 'an hour',
+ hh: '%d hours',
+ d: 'a day',
+ dd: '%d days',
+ w: 'a week',
+ ww: '%d weeks',
+ M: 'a month',
+ MM: '%d months',
+ y: 'a year',
+ yy: '%d years',
+};
+
+function relativeTime(number, withoutSuffix, string, isFuture) {
+ var output = this._relativeTime[string];
+ return isFunction(output)
+ ? output(number, withoutSuffix, string, isFuture)
+ : output.replace(/%d/i, number);
+}
+
+function pastFuture(diff, output) {
+ var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
+ return isFunction(format) ? format(output) : format.replace(/%s/i, output);
+}
+
+var aliases = {
+ D: 'date',
+ dates: 'date',
+ date: 'date',
+ d: 'day',
+ days: 'day',
+ day: 'day',
+ e: 'weekday',
+ weekdays: 'weekday',
+ weekday: 'weekday',
+ E: 'isoWeekday',
+ isoweekdays: 'isoWeekday',
+ isoweekday: 'isoWeekday',
+ DDD: 'dayOfYear',
+ dayofyears: 'dayOfYear',
+ dayofyear: 'dayOfYear',
+ h: 'hour',
+ hours: 'hour',
+ hour: 'hour',
+ ms: 'millisecond',
+ milliseconds: 'millisecond',
+ millisecond: 'millisecond',
+ m: 'minute',
+ minutes: 'minute',
+ minute: 'minute',
+ M: 'month',
+ months: 'month',
+ month: 'month',
+ Q: 'quarter',
+ quarters: 'quarter',
+ quarter: 'quarter',
+ s: 'second',
+ seconds: 'second',
+ second: 'second',
+ gg: 'weekYear',
+ weekyears: 'weekYear',
+ weekyear: 'weekYear',
+ GG: 'isoWeekYear',
+ isoweekyears: 'isoWeekYear',
+ isoweekyear: 'isoWeekYear',
+ w: 'week',
+ weeks: 'week',
+ week: 'week',
+ W: 'isoWeek',
+ isoweeks: 'isoWeek',
+ isoweek: 'isoWeek',
+ y: 'year',
+ years: 'year',
+ year: 'year',
+};
+
+function normalizeUnits(units) {
+ return typeof units === 'string'
+ ? aliases[units] || aliases[units.toLowerCase()]
+ : undefined;
+}
+
+function normalizeObjectUnits(inputObject) {
+ var normalizedInput = {},
+ normalizedProp,
+ prop;
+
+ for (prop in inputObject) {
+ if (hasOwnProp(inputObject, prop)) {
+ normalizedProp = normalizeUnits(prop);
+ if (normalizedProp) {
+ normalizedInput[normalizedProp] = inputObject[prop];
+ }
+ }
+ }
+
+ return normalizedInput;
+}
+
+var priorities = {
+ date: 9,
+ day: 11,
+ weekday: 11,
+ isoWeekday: 11,
+ dayOfYear: 4,
+ hour: 13,
+ millisecond: 16,
+ minute: 14,
+ month: 8,
+ quarter: 7,
+ second: 15,
+ weekYear: 1,
+ isoWeekYear: 1,
+ week: 5,
+ isoWeek: 5,
+ year: 1,
+};
+
+function getPrioritizedUnits(unitsObj) {
+ var units = [],
+ u;
+ for (u in unitsObj) {
+ if (hasOwnProp(unitsObj, u)) {
+ units.push({ unit: u, priority: priorities[u] });
+ }
+ }
+ units.sort(function (a, b) {
+ return a.priority - b.priority;
+ });
+ return units;
+}
+
+var match1 = /\d/, // 0 - 9
+ match2 = /\d\d/, // 00 - 99
+ match3 = /\d{3}/, // 000 - 999
+ match4 = /\d{4}/, // 0000 - 9999
+ match6 = /[+-]?\d{6}/, // -999999 - 999999
+ match1to2 = /\d\d?/, // 0 - 99
+ match3to4 = /\d\d\d\d?/, // 999 - 9999
+ match5to6 = /\d\d\d\d\d\d?/, // 99999 - 999999
+ match1to3 = /\d{1,3}/, // 0 - 999
+ match1to4 = /\d{1,4}/, // 0 - 9999
+ match1to6 = /[+-]?\d{1,6}/, // -999999 - 999999
+ matchUnsigned = /\d+/, // 0 - inf
+ matchSigned = /[+-]?\d+/, // -inf - inf
+ matchOffset = /Z|[+-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
+ matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi, // +00 -00 +00:00 -00:00 +0000 -0000 or Z
+ matchTimestamp = /[+-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
+ // any word (or two) characters or numbers including two/three word month in arabic.
+ // includes scottish gaelic two word and hyphenated months
+ matchWord =
+ /[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,
+ match1to2NoLeadingZero = /^[1-9]\d?/, // 1-99
+ match1to2HasZero = /^([1-9]\d|\d)/, // 0-99
+ regexes;
+
+regexes = {};
+
+function addRegexToken(token, regex, strictRegex) {
+ regexes[token] = isFunction(regex)
+ ? regex
+ : function (isStrict, localeData) {
+ return isStrict && strictRegex ? strictRegex : regex;
+ };
+}
+
+function getParseRegexForToken(token, config) {
+ if (!hasOwnProp(regexes, token)) {
+ return new RegExp(unescapeFormat(token));
+ }
+
+ return regexes[token](config._strict, config._locale);
+}
+
+// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+function unescapeFormat(s) {
+ return regexEscape(
+ s
+ .replace('\\', '')
+ .replace(
+ /\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,
+ function (matched, p1, p2, p3, p4) {
+ return p1 || p2 || p3 || p4;
+ }
+ )
+ );
+}
+
+function regexEscape(s) {
+ return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+}
+
+function absFloor(number) {
+ if (number < 0) {
+ // -0 -> 0
+ return Math.ceil(number) || 0;
+ } else {
+ return Math.floor(number);
+ }
+}
+
+function toInt(argumentForCoercion) {
+ var coercedNumber = +argumentForCoercion,
+ value = 0;
+
+ if (coercedNumber !== 0 && isFinite(coercedNumber)) {
+ value = absFloor(coercedNumber);
+ }
+
+ return value;
+}
+
+var tokens = {};
+
+function addParseToken(token, callback) {
+ var i,
+ func = callback,
+ tokenLen;
+ if (typeof token === 'string') {
+ token = [token];
+ }
+ if (isNumber(callback)) {
+ func = function (input, array) {
+ array[callback] = toInt(input);
+ };
+ }
+ tokenLen = token.length;
+ for (i = 0; i < tokenLen; i++) {
+ tokens[token[i]] = func;
+ }
+}
+
+function addWeekParseToken(token, callback) {
+ addParseToken(token, function (input, array, config, token) {
+ config._w = config._w || {};
+ callback(input, config._w, config, token);
+ });
+}
+
+function addTimeToArrayFromToken(token, input, config) {
+ if (input != null && hasOwnProp(tokens, token)) {
+ tokens[token](input, config._a, config, token);
+ }
+}
+
+function isLeapYear(year) {
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+}
+
+var YEAR = 0,
+ MONTH = 1,
+ DATE = 2,
+ HOUR = 3,
+ MINUTE = 4,
+ SECOND = 5,
+ MILLISECOND = 6,
+ WEEK = 7,
+ WEEKDAY = 8;
+
+// FORMATTING
+
+addFormatToken('Y', 0, 0, function () {
+ var y = this.year();
+ return y <= 9999 ? zeroFill(y, 4) : '+' + y;
+});
+
+addFormatToken(0, ['YY', 2], 0, function () {
+ return this.year() % 100;
+});
+
+addFormatToken(0, ['YYYY', 4], 0, 'year');
+addFormatToken(0, ['YYYYY', 5], 0, 'year');
+addFormatToken(0, ['YYYYYY', 6, true], 0, 'year');
+
+// PARSING
+
+addRegexToken('Y', matchSigned);
+addRegexToken('YY', match1to2, match2);
+addRegexToken('YYYY', match1to4, match4);
+addRegexToken('YYYYY', match1to6, match6);
+addRegexToken('YYYYYY', match1to6, match6);
+
+addParseToken(['YYYYY', 'YYYYYY'], YEAR);
+addParseToken('YYYY', function (input, array) {
+ array[YEAR] =
+ input.length === 2 ? hooks.parseTwoDigitYear(input) : toInt(input);
+});
+addParseToken('YY', function (input, array) {
+ array[YEAR] = hooks.parseTwoDigitYear(input);
+});
+addParseToken('Y', function (input, array) {
+ array[YEAR] = parseInt(input, 10);
+});
+
+// HELPERS
+
+function daysInYear(year) {
+ return isLeapYear(year) ? 366 : 365;
+}
+
+// HOOKS
+
+hooks.parseTwoDigitYear = function (input) {
+ return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
+};
+
+// MOMENTS
+
+var getSetYear = makeGetSet('FullYear', true);
+
+function getIsLeapYear() {
+ return isLeapYear(this.year());
+}
+
+function makeGetSet(unit, keepTime) {
+ return function (value) {
+ if (value != null) {
+ set$1(this, unit, value);
+ hooks.updateOffset(this, keepTime);
+ return this;
+ } else {
+ return get(this, unit);
+ }
+ };
+}
+
+function get(mom, unit) {
+ if (!mom.isValid()) {
+ return NaN;
+ }
+
+ var d = mom._d,
+ isUTC = mom._isUTC;
+
+ switch (unit) {
+ case 'Milliseconds':
+ return isUTC ? d.getUTCMilliseconds() : d.getMilliseconds();
+ case 'Seconds':
+ return isUTC ? d.getUTCSeconds() : d.getSeconds();
+ case 'Minutes':
+ return isUTC ? d.getUTCMinutes() : d.getMinutes();
+ case 'Hours':
+ return isUTC ? d.getUTCHours() : d.getHours();
+ case 'Date':
+ return isUTC ? d.getUTCDate() : d.getDate();
+ case 'Day':
+ return isUTC ? d.getUTCDay() : d.getDay();
+ case 'Month':
+ return isUTC ? d.getUTCMonth() : d.getMonth();
+ case 'FullYear':
+ return isUTC ? d.getUTCFullYear() : d.getFullYear();
+ default:
+ return NaN; // Just in case
+ }
+}
+
+function set$1(mom, unit, value) {
+ var d, isUTC, year, month, date;
+
+ if (!mom.isValid() || isNaN(value)) {
+ return;
+ }
+
+ d = mom._d;
+ isUTC = mom._isUTC;
+
+ switch (unit) {
+ case 'Milliseconds':
+ return void (isUTC
+ ? d.setUTCMilliseconds(value)
+ : d.setMilliseconds(value));
+ case 'Seconds':
+ return void (isUTC ? d.setUTCSeconds(value) : d.setSeconds(value));
+ case 'Minutes':
+ return void (isUTC ? d.setUTCMinutes(value) : d.setMinutes(value));
+ case 'Hours':
+ return void (isUTC ? d.setUTCHours(value) : d.setHours(value));
+ case 'Date':
+ return void (isUTC ? d.setUTCDate(value) : d.setDate(value));
+ // case 'Day': // Not real
+ // return void (isUTC ? d.setUTCDay(value) : d.setDay(value));
+ // case 'Month': // Not used because we need to pass two variables
+ // return void (isUTC ? d.setUTCMonth(value) : d.setMonth(value));
+ case 'FullYear':
+ break; // See below ...
+ default:
+ return; // Just in case
+ }
+
+ year = value;
+ month = mom.month();
+ date = mom.date();
+ date = date === 29 && month === 1 && !isLeapYear(year) ? 28 : date;
+ void (isUTC
+ ? d.setUTCFullYear(year, month, date)
+ : d.setFullYear(year, month, date));
+}
+
+// MOMENTS
+
+function stringGet(units) {
+ units = normalizeUnits(units);
+ if (isFunction(this[units])) {
+ return this[units]();
+ }
+ return this;
+}
+
+function stringSet(units, value) {
+ if (typeof units === 'object') {
+ units = normalizeObjectUnits(units);
+ var prioritized = getPrioritizedUnits(units),
+ i,
+ prioritizedLen = prioritized.length;
+ for (i = 0; i < prioritizedLen; i++) {
+ this[prioritized[i].unit](units[prioritized[i].unit]);
+ }
+ } else {
+ units = normalizeUnits(units);
+ if (isFunction(this[units])) {
+ return this[units](value);
+ }
+ }
+ return this;
+}
+
+function mod(n, x) {
+ return ((n % x) + x) % x;
+}
+
+var indexOf;
+
+if (Array.prototype.indexOf) {
+ indexOf = Array.prototype.indexOf;
+} else {
+ indexOf = function (o) {
+ // I know
+ var i;
+ for (i = 0; i < this.length; ++i) {
+ if (this[i] === o) {
+ return i;
+ }
+ }
+ return -1;
+ };
+}
+
+function daysInMonth(year, month) {
+ if (isNaN(year) || isNaN(month)) {
+ return NaN;
+ }
+ var modMonth = mod(month, 12);
+ year += (month - modMonth) / 12;
+ return modMonth === 1
+ ? isLeapYear(year)
+ ? 29
+ : 28
+ : 31 - ((modMonth % 7) % 2);
+}
+
+// FORMATTING
+
+addFormatToken('M', ['MM', 2], 'Mo', function () {
+ return this.month() + 1;
+});
+
+addFormatToken('MMM', 0, 0, function (format) {
+ return this.localeData().monthsShort(this, format);
+});
+
+addFormatToken('MMMM', 0, 0, function (format) {
+ return this.localeData().months(this, format);
+});
+
+// PARSING
+
+addRegexToken('M', match1to2, match1to2NoLeadingZero);
+addRegexToken('MM', match1to2, match2);
+addRegexToken('MMM', function (isStrict, locale) {
+ return locale.monthsShortRegex(isStrict);
+});
+addRegexToken('MMMM', function (isStrict, locale) {
+ return locale.monthsRegex(isStrict);
+});
+
+addParseToken(['M', 'MM'], function (input, array) {
+ array[MONTH] = toInt(input) - 1;
+});
+
+addParseToken(['MMM', 'MMMM'], function (input, array, config, token) {
+ var month = config._locale.monthsParse(input, token, config._strict);
+ // if we didn't find a month name, mark the date as invalid.
+ if (month != null) {
+ array[MONTH] = month;
+ } else {
+ getParsingFlags(config).invalidMonth = input;
+ }
+});
+
+// LOCALES
+
+var defaultLocaleMonths =
+ 'January_February_March_April_May_June_July_August_September_October_November_December'.split(
+ '_'
+ ),
+ defaultLocaleMonthsShort =
+ 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
+ MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,
+ defaultMonthsShortRegex = matchWord,
+ defaultMonthsRegex = matchWord;
+
+function localeMonths(m, format) {
+ if (!m) {
+ return isArray(this._months)
+ ? this._months
+ : this._months['standalone'];
+ }
+ return isArray(this._months)
+ ? this._months[m.month()]
+ : this._months[
+ (this._months.isFormat || MONTHS_IN_FORMAT).test(format)
+ ? 'format'
+ : 'standalone'
+ ][m.month()];
+}
+
+function localeMonthsShort(m, format) {
+ if (!m) {
+ return isArray(this._monthsShort)
+ ? this._monthsShort
+ : this._monthsShort['standalone'];
+ }
+ return isArray(this._monthsShort)
+ ? this._monthsShort[m.month()]
+ : this._monthsShort[
+ MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'
+ ][m.month()];
+}
+
+function handleStrictParse(monthName, format, strict) {
+ var i,
+ ii,
+ mom,
+ llc = monthName.toLocaleLowerCase();
+ if (!this._monthsParse) {
+ // this is not used
+ this._monthsParse = [];
+ this._longMonthsParse = [];
+ this._shortMonthsParse = [];
+ for (i = 0; i < 12; ++i) {
+ mom = createUTC([2000, i]);
+ this._shortMonthsParse[i] = this.monthsShort(
+ mom,
+ ''
+ ).toLocaleLowerCase();
+ this._longMonthsParse[i] = this.months(mom, '').toLocaleLowerCase();
+ }
+ }
+
+ if (strict) {
+ if (format === 'MMM') {
+ ii = indexOf.call(this._shortMonthsParse, llc);
+ return ii !== -1 ? ii : null;
+ } else {
+ ii = indexOf.call(this._longMonthsParse, llc);
+ return ii !== -1 ? ii : null;
+ }
+ } else {
+ if (format === 'MMM') {
+ ii = indexOf.call(this._shortMonthsParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._longMonthsParse, llc);
+ return ii !== -1 ? ii : null;
+ } else {
+ ii = indexOf.call(this._longMonthsParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._shortMonthsParse, llc);
+ return ii !== -1 ? ii : null;
+ }
+ }
+}
+
+function localeMonthsParse(monthName, format, strict) {
+ var i, mom, regex;
+
+ if (this._monthsParseExact) {
+ return handleStrictParse.call(this, monthName, format, strict);
+ }
+
+ if (!this._monthsParse) {
+ this._monthsParse = [];
+ this._longMonthsParse = [];
+ this._shortMonthsParse = [];
+ }
+
+ // TODO: add sorting
+ // Sorting makes sure if one month (or abbr) is a prefix of another
+ // see sorting in computeMonthsParse
+ for (i = 0; i < 12; i++) {
+ // make the regex if we don't have it already
+ mom = createUTC([2000, i]);
+ if (strict && !this._longMonthsParse[i]) {
+ this._longMonthsParse[i] = new RegExp(
+ '^' + this.months(mom, '').replace('.', '') + '$',
+ 'i'
+ );
+ this._shortMonthsParse[i] = new RegExp(
+ '^' + this.monthsShort(mom, '').replace('.', '') + '$',
+ 'i'
+ );
+ }
+ if (!strict && !this._monthsParse[i]) {
+ regex =
+ '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
+ this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (
+ strict &&
+ format === 'MMMM' &&
+ this._longMonthsParse[i].test(monthName)
+ ) {
+ return i;
+ } else if (
+ strict &&
+ format === 'MMM' &&
+ this._shortMonthsParse[i].test(monthName)
+ ) {
+ return i;
+ } else if (!strict && this._monthsParse[i].test(monthName)) {
+ return i;
+ }
+ }
+}
+
+// MOMENTS
+
+function setMonth(mom, value) {
+ if (!mom.isValid()) {
+ // No op
+ return mom;
+ }
+
+ if (typeof value === 'string') {
+ if (/^\d+$/.test(value)) {
+ value = toInt(value);
+ } else {
+ value = mom.localeData().monthsParse(value);
+ // TODO: Another silent failure?
+ if (!isNumber(value)) {
+ return mom;
+ }
+ }
+ }
+
+ var month = value,
+ date = mom.date();
+
+ date = date < 29 ? date : Math.min(date, daysInMonth(mom.year(), month));
+ void (mom._isUTC
+ ? mom._d.setUTCMonth(month, date)
+ : mom._d.setMonth(month, date));
+ return mom;
+}
+
+function getSetMonth(value) {
+ if (value != null) {
+ setMonth(this, value);
+ hooks.updateOffset(this, true);
+ return this;
+ } else {
+ return get(this, 'Month');
+ }
+}
+
+function getDaysInMonth() {
+ return daysInMonth(this.year(), this.month());
+}
+
+function monthsShortRegex(isStrict) {
+ if (this._monthsParseExact) {
+ if (!hasOwnProp(this, '_monthsRegex')) {
+ computeMonthsParse.call(this);
+ }
+ if (isStrict) {
+ return this._monthsShortStrictRegex;
+ } else {
+ return this._monthsShortRegex;
+ }
+ } else {
+ if (!hasOwnProp(this, '_monthsShortRegex')) {
+ this._monthsShortRegex = defaultMonthsShortRegex;
+ }
+ return this._monthsShortStrictRegex && isStrict
+ ? this._monthsShortStrictRegex
+ : this._monthsShortRegex;
+ }
+}
+
+function monthsRegex(isStrict) {
+ if (this._monthsParseExact) {
+ if (!hasOwnProp(this, '_monthsRegex')) {
+ computeMonthsParse.call(this);
+ }
+ if (isStrict) {
+ return this._monthsStrictRegex;
+ } else {
+ return this._monthsRegex;
+ }
+ } else {
+ if (!hasOwnProp(this, '_monthsRegex')) {
+ this._monthsRegex = defaultMonthsRegex;
+ }
+ return this._monthsStrictRegex && isStrict
+ ? this._monthsStrictRegex
+ : this._monthsRegex;
+ }
+}
+
+function computeMonthsParse() {
+ function cmpLenRev(a, b) {
+ return b.length - a.length;
+ }
+
+ var shortPieces = [],
+ longPieces = [],
+ mixedPieces = [],
+ i,
+ mom,
+ shortP,
+ longP;
+ for (i = 0; i < 12; i++) {
+ // make the regex if we don't have it already
+ mom = createUTC([2000, i]);
+ shortP = regexEscape(this.monthsShort(mom, ''));
+ longP = regexEscape(this.months(mom, ''));
+ shortPieces.push(shortP);
+ longPieces.push(longP);
+ mixedPieces.push(longP);
+ mixedPieces.push(shortP);
+ }
+ // Sorting makes sure if one month (or abbr) is a prefix of another it
+ // will match the longer piece.
+ shortPieces.sort(cmpLenRev);
+ longPieces.sort(cmpLenRev);
+ mixedPieces.sort(cmpLenRev);
+
+ this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i');
+ this._monthsShortRegex = this._monthsRegex;
+ this._monthsStrictRegex = new RegExp(
+ '^(' + longPieces.join('|') + ')',
+ 'i'
+ );
+ this._monthsShortStrictRegex = new RegExp(
+ '^(' + shortPieces.join('|') + ')',
+ 'i'
+ );
+}
+
+function createDate(y, m, d, h, M, s, ms) {
+ // can't just apply() to create a date:
+ // https://stackoverflow.com/q/181348
+ var date;
+ // the date constructor remaps years 0-99 to 1900-1999
+ if (y < 100 && y >= 0) {
+ // preserve leap years using a full 400 year cycle, then reset
+ date = new Date(y + 400, m, d, h, M, s, ms);
+ if (isFinite(date.getFullYear())) {
+ date.setFullYear(y);
+ }
+ } else {
+ date = new Date(y, m, d, h, M, s, ms);
+ }
+
+ return date;
+}
+
+function createUTCDate(y) {
+ var date, args;
+ // the Date.UTC function remaps years 0-99 to 1900-1999
+ if (y < 100 && y >= 0) {
+ args = Array.prototype.slice.call(arguments);
+ // preserve leap years using a full 400 year cycle, then reset
+ args[0] = y + 400;
+ date = new Date(Date.UTC.apply(null, args));
+ if (isFinite(date.getUTCFullYear())) {
+ date.setUTCFullYear(y);
+ }
+ } else {
+ date = new Date(Date.UTC.apply(null, arguments));
+ }
+
+ return date;
+}
+
+// start-of-first-week - start-of-year
+function firstWeekOffset(year, dow, doy) {
+ var // first-week day -- which january is always in the first week (4 for iso, 1 for other)
+ fwd = 7 + dow - doy,
+ // first-week day local weekday -- which local weekday is fwd
+ fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7;
+
+ return -fwdlw + fwd - 1;
+}
+
+// https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
+function dayOfYearFromWeeks(year, week, weekday, dow, doy) {
+ var localWeekday = (7 + weekday - dow) % 7,
+ weekOffset = firstWeekOffset(year, dow, doy),
+ dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset,
+ resYear,
+ resDayOfYear;
+
+ if (dayOfYear <= 0) {
+ resYear = year - 1;
+ resDayOfYear = daysInYear(resYear) + dayOfYear;
+ } else if (dayOfYear > daysInYear(year)) {
+ resYear = year + 1;
+ resDayOfYear = dayOfYear - daysInYear(year);
+ } else {
+ resYear = year;
+ resDayOfYear = dayOfYear;
+ }
+
+ return {
+ year: resYear,
+ dayOfYear: resDayOfYear,
+ };
+}
+
+function weekOfYear(mom, dow, doy) {
+ var weekOffset = firstWeekOffset(mom.year(), dow, doy),
+ week = Math.floor((mom.dayOfYear() - weekOffset - 1) / 7) + 1,
+ resWeek,
+ resYear;
+
+ if (week < 1) {
+ resYear = mom.year() - 1;
+ resWeek = week + weeksInYear(resYear, dow, doy);
+ } else if (week > weeksInYear(mom.year(), dow, doy)) {
+ resWeek = week - weeksInYear(mom.year(), dow, doy);
+ resYear = mom.year() + 1;
+ } else {
+ resYear = mom.year();
+ resWeek = week;
+ }
+
+ return {
+ week: resWeek,
+ year: resYear,
+ };
+}
+
+function weeksInYear(year, dow, doy) {
+ var weekOffset = firstWeekOffset(year, dow, doy),
+ weekOffsetNext = firstWeekOffset(year + 1, dow, doy);
+ return (daysInYear(year) - weekOffset + weekOffsetNext) / 7;
+}
+
+// FORMATTING
+
+addFormatToken('w', ['ww', 2], 'wo', 'week');
+addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek');
+
+// PARSING
+
+addRegexToken('w', match1to2, match1to2NoLeadingZero);
+addRegexToken('ww', match1to2, match2);
+addRegexToken('W', match1to2, match1to2NoLeadingZero);
+addRegexToken('WW', match1to2, match2);
+
+addWeekParseToken(
+ ['w', 'ww', 'W', 'WW'],
+ function (input, week, config, token) {
+ week[token.substr(0, 1)] = toInt(input);
+ }
+);
+
+// HELPERS
+
+// LOCALES
+
+function localeWeek(mom) {
+ return weekOfYear(mom, this._week.dow, this._week.doy).week;
+}
+
+var defaultLocaleWeek = {
+ dow: 0, // Sunday is the first day of the week.
+ doy: 6, // The week that contains Jan 6th is the first week of the year.
+};
+
+function localeFirstDayOfWeek() {
+ return this._week.dow;
+}
+
+function localeFirstDayOfYear() {
+ return this._week.doy;
+}
+
+// MOMENTS
+
+function getSetWeek(input) {
+ var week = this.localeData().week(this);
+ return input == null ? week : this.add((input - week) * 7, 'd');
+}
+
+function getSetISOWeek(input) {
+ var week = weekOfYear(this, 1, 4).week;
+ return input == null ? week : this.add((input - week) * 7, 'd');
+}
+
+// FORMATTING
+
+addFormatToken('d', 0, 'do', 'day');
+
+addFormatToken('dd', 0, 0, function (format) {
+ return this.localeData().weekdaysMin(this, format);
+});
+
+addFormatToken('ddd', 0, 0, function (format) {
+ return this.localeData().weekdaysShort(this, format);
+});
+
+addFormatToken('dddd', 0, 0, function (format) {
+ return this.localeData().weekdays(this, format);
+});
+
+addFormatToken('e', 0, 0, 'weekday');
+addFormatToken('E', 0, 0, 'isoWeekday');
+
+// PARSING
+
+addRegexToken('d', match1to2);
+addRegexToken('e', match1to2);
+addRegexToken('E', match1to2);
+addRegexToken('dd', function (isStrict, locale) {
+ return locale.weekdaysMinRegex(isStrict);
+});
+addRegexToken('ddd', function (isStrict, locale) {
+ return locale.weekdaysShortRegex(isStrict);
+});
+addRegexToken('dddd', function (isStrict, locale) {
+ return locale.weekdaysRegex(isStrict);
+});
+
+addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) {
+ var weekday = config._locale.weekdaysParse(input, token, config._strict);
+ // if we didn't get a weekday name, mark the date as invalid
+ if (weekday != null) {
+ week.d = weekday;
+ } else {
+ getParsingFlags(config).invalidWeekday = input;
+ }
+});
+
+addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) {
+ week[token] = toInt(input);
+});
+
+// HELPERS
+
+function parseWeekday(input, locale) {
+ if (typeof input !== 'string') {
+ return input;
+ }
+
+ if (!isNaN(input)) {
+ return parseInt(input, 10);
+ }
+
+ input = locale.weekdaysParse(input);
+ if (typeof input === 'number') {
+ return input;
+ }
+
+ return null;
+}
+
+function parseIsoWeekday(input, locale) {
+ if (typeof input === 'string') {
+ return locale.weekdaysParse(input) % 7 || 7;
+ }
+ return isNaN(input) ? null : input;
+}
+
+// LOCALES
+function shiftWeekdays(ws, n) {
+ return ws.slice(n, 7).concat(ws.slice(0, n));
+}
+
+var defaultLocaleWeekdays =
+ 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
+ defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
+ defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
+ defaultWeekdaysRegex = matchWord,
+ defaultWeekdaysShortRegex = matchWord,
+ defaultWeekdaysMinRegex = matchWord;
+
+function localeWeekdays(m, format) {
+ var weekdays = isArray(this._weekdays)
+ ? this._weekdays
+ : this._weekdays[
+ m && m !== true && this._weekdays.isFormat.test(format)
+ ? 'format'
+ : 'standalone'
+ ];
+ return m === true
+ ? shiftWeekdays(weekdays, this._week.dow)
+ : m
+ ? weekdays[m.day()]
+ : weekdays;
+}
+
+function localeWeekdaysShort(m) {
+ return m === true
+ ? shiftWeekdays(this._weekdaysShort, this._week.dow)
+ : m
+ ? this._weekdaysShort[m.day()]
+ : this._weekdaysShort;
+}
+
+function localeWeekdaysMin(m) {
+ return m === true
+ ? shiftWeekdays(this._weekdaysMin, this._week.dow)
+ : m
+ ? this._weekdaysMin[m.day()]
+ : this._weekdaysMin;
+}
+
+function handleStrictParse$1(weekdayName, format, strict) {
+ var i,
+ ii,
+ mom,
+ llc = weekdayName.toLocaleLowerCase();
+ if (!this._weekdaysParse) {
+ this._weekdaysParse = [];
+ this._shortWeekdaysParse = [];
+ this._minWeekdaysParse = [];
+
+ for (i = 0; i < 7; ++i) {
+ mom = createUTC([2000, 1]).day(i);
+ this._minWeekdaysParse[i] = this.weekdaysMin(
+ mom,
+ ''
+ ).toLocaleLowerCase();
+ this._shortWeekdaysParse[i] = this.weekdaysShort(
+ mom,
+ ''
+ ).toLocaleLowerCase();
+ this._weekdaysParse[i] = this.weekdays(mom, '').toLocaleLowerCase();
+ }
+ }
+
+ if (strict) {
+ if (format === 'dddd') {
+ ii = indexOf.call(this._weekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ } else if (format === 'ddd') {
+ ii = indexOf.call(this._shortWeekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ } else {
+ ii = indexOf.call(this._minWeekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ }
+ } else {
+ if (format === 'dddd') {
+ ii = indexOf.call(this._weekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._shortWeekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._minWeekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ } else if (format === 'ddd') {
+ ii = indexOf.call(this._shortWeekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._weekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._minWeekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ } else {
+ ii = indexOf.call(this._minWeekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._weekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._shortWeekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ }
+ }
+}
+
+function localeWeekdaysParse(weekdayName, format, strict) {
+ var i, mom, regex;
+
+ if (this._weekdaysParseExact) {
+ return handleStrictParse$1.call(this, weekdayName, format, strict);
+ }
+
+ if (!this._weekdaysParse) {
+ this._weekdaysParse = [];
+ this._minWeekdaysParse = [];
+ this._shortWeekdaysParse = [];
+ this._fullWeekdaysParse = [];
+ }
+
+ for (i = 0; i < 7; i++) {
+ // make the regex if we don't have it already
+
+ mom = createUTC([2000, 1]).day(i);
+ if (strict && !this._fullWeekdaysParse[i]) {
+ this._fullWeekdaysParse[i] = new RegExp(
+ '^' + this.weekdays(mom, '').replace('.', '\\.?') + '$',
+ 'i'
+ );
+ this._shortWeekdaysParse[i] = new RegExp(
+ '^' + this.weekdaysShort(mom, '').replace('.', '\\.?') + '$',
+ 'i'
+ );
+ this._minWeekdaysParse[i] = new RegExp(
+ '^' + this.weekdaysMin(mom, '').replace('.', '\\.?') + '$',
+ 'i'
+ );
+ }
+ if (!this._weekdaysParse[i]) {
+ regex =
+ '^' +
+ this.weekdays(mom, '') +
+ '|^' +
+ this.weekdaysShort(mom, '') +
+ '|^' +
+ this.weekdaysMin(mom, '');
+ this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (
+ strict &&
+ format === 'dddd' &&
+ this._fullWeekdaysParse[i].test(weekdayName)
+ ) {
+ return i;
+ } else if (
+ strict &&
+ format === 'ddd' &&
+ this._shortWeekdaysParse[i].test(weekdayName)
+ ) {
+ return i;
+ } else if (
+ strict &&
+ format === 'dd' &&
+ this._minWeekdaysParse[i].test(weekdayName)
+ ) {
+ return i;
+ } else if (!strict && this._weekdaysParse[i].test(weekdayName)) {
+ return i;
+ }
+ }
+}
+
+// MOMENTS
+
+function getSetDayOfWeek(input) {
+ if (!this.isValid()) {
+ return input != null ? this : NaN;
+ }
+
+ var day = get(this, 'Day');
+ if (input != null) {
+ input = parseWeekday(input, this.localeData());
+ return this.add(input - day, 'd');
+ } else {
+ return day;
+ }
+}
+
+function getSetLocaleDayOfWeek(input) {
+ if (!this.isValid()) {
+ return input != null ? this : NaN;
+ }
+ var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7;
+ return input == null ? weekday : this.add(input - weekday, 'd');
+}
+
+function getSetISODayOfWeek(input) {
+ if (!this.isValid()) {
+ return input != null ? this : NaN;
+ }
+
+ // behaves the same as moment#day except
+ // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
+ // as a setter, sunday should belong to the previous week.
+
+ if (input != null) {
+ var weekday = parseIsoWeekday(input, this.localeData());
+ return this.day(this.day() % 7 ? weekday : weekday - 7);
+ } else {
+ return this.day() || 7;
+ }
+}
+
+function weekdaysRegex(isStrict) {
+ if (this._weekdaysParseExact) {
+ if (!hasOwnProp(this, '_weekdaysRegex')) {
+ computeWeekdaysParse.call(this);
+ }
+ if (isStrict) {
+ return this._weekdaysStrictRegex;
+ } else {
+ return this._weekdaysRegex;
+ }
+ } else {
+ if (!hasOwnProp(this, '_weekdaysRegex')) {
+ this._weekdaysRegex = defaultWeekdaysRegex;
+ }
+ return this._weekdaysStrictRegex && isStrict
+ ? this._weekdaysStrictRegex
+ : this._weekdaysRegex;
+ }
+}
+
+function weekdaysShortRegex(isStrict) {
+ if (this._weekdaysParseExact) {
+ if (!hasOwnProp(this, '_weekdaysRegex')) {
+ computeWeekdaysParse.call(this);
+ }
+ if (isStrict) {
+ return this._weekdaysShortStrictRegex;
+ } else {
+ return this._weekdaysShortRegex;
+ }
+ } else {
+ if (!hasOwnProp(this, '_weekdaysShortRegex')) {
+ this._weekdaysShortRegex = defaultWeekdaysShortRegex;
+ }
+ return this._weekdaysShortStrictRegex && isStrict
+ ? this._weekdaysShortStrictRegex
+ : this._weekdaysShortRegex;
+ }
+}
+
+function weekdaysMinRegex(isStrict) {
+ if (this._weekdaysParseExact) {
+ if (!hasOwnProp(this, '_weekdaysRegex')) {
+ computeWeekdaysParse.call(this);
+ }
+ if (isStrict) {
+ return this._weekdaysMinStrictRegex;
+ } else {
+ return this._weekdaysMinRegex;
+ }
+ } else {
+ if (!hasOwnProp(this, '_weekdaysMinRegex')) {
+ this._weekdaysMinRegex = defaultWeekdaysMinRegex;
+ }
+ return this._weekdaysMinStrictRegex && isStrict
+ ? this._weekdaysMinStrictRegex
+ : this._weekdaysMinRegex;
+ }
+}
+
+function computeWeekdaysParse() {
+ function cmpLenRev(a, b) {
+ return b.length - a.length;
+ }
+
+ var minPieces = [],
+ shortPieces = [],
+ longPieces = [],
+ mixedPieces = [],
+ i,
+ mom,
+ minp,
+ shortp,
+ longp;
+ for (i = 0; i < 7; i++) {
+ // make the regex if we don't have it already
+ mom = createUTC([2000, 1]).day(i);
+ minp = regexEscape(this.weekdaysMin(mom, ''));
+ shortp = regexEscape(this.weekdaysShort(mom, ''));
+ longp = regexEscape(this.weekdays(mom, ''));
+ minPieces.push(minp);
+ shortPieces.push(shortp);
+ longPieces.push(longp);
+ mixedPieces.push(minp);
+ mixedPieces.push(shortp);
+ mixedPieces.push(longp);
+ }
+ // Sorting makes sure if one weekday (or abbr) is a prefix of another it
+ // will match the longer piece.
+ minPieces.sort(cmpLenRev);
+ shortPieces.sort(cmpLenRev);
+ longPieces.sort(cmpLenRev);
+ mixedPieces.sort(cmpLenRev);
+
+ this._weekdaysRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i');
+ this._weekdaysShortRegex = this._weekdaysRegex;
+ this._weekdaysMinRegex = this._weekdaysRegex;
+
+ this._weekdaysStrictRegex = new RegExp(
+ '^(' + longPieces.join('|') + ')',
+ 'i'
+ );
+ this._weekdaysShortStrictRegex = new RegExp(
+ '^(' + shortPieces.join('|') + ')',
+ 'i'
+ );
+ this._weekdaysMinStrictRegex = new RegExp(
+ '^(' + minPieces.join('|') + ')',
+ 'i'
+ );
+}
+
+// FORMATTING
+
+function hFormat() {
+ return this.hours() % 12 || 12;
+}
+
+function kFormat() {
+ return this.hours() || 24;
+}
+
+addFormatToken('H', ['HH', 2], 0, 'hour');
+addFormatToken('h', ['hh', 2], 0, hFormat);
+addFormatToken('k', ['kk', 2], 0, kFormat);
+
+addFormatToken('hmm', 0, 0, function () {
+ return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2);
+});
+
+addFormatToken('hmmss', 0, 0, function () {
+ return (
+ '' +
+ hFormat.apply(this) +
+ zeroFill(this.minutes(), 2) +
+ zeroFill(this.seconds(), 2)
+ );
+});
+
+addFormatToken('Hmm', 0, 0, function () {
+ return '' + this.hours() + zeroFill(this.minutes(), 2);
+});
+
+addFormatToken('Hmmss', 0, 0, function () {
+ return (
+ '' +
+ this.hours() +
+ zeroFill(this.minutes(), 2) +
+ zeroFill(this.seconds(), 2)
+ );
+});
+
+function meridiem(token, lowercase) {
+ addFormatToken(token, 0, 0, function () {
+ return this.localeData().meridiem(
+ this.hours(),
+ this.minutes(),
+ lowercase
+ );
+ });
+}
+
+meridiem('a', true);
+meridiem('A', false);
+
+// PARSING
+
+function matchMeridiem(isStrict, locale) {
+ return locale._meridiemParse;
+}
+
+addRegexToken('a', matchMeridiem);
+addRegexToken('A', matchMeridiem);
+addRegexToken('H', match1to2, match1to2HasZero);
+addRegexToken('h', match1to2, match1to2NoLeadingZero);
+addRegexToken('k', match1to2, match1to2NoLeadingZero);
+addRegexToken('HH', match1to2, match2);
+addRegexToken('hh', match1to2, match2);
+addRegexToken('kk', match1to2, match2);
+
+addRegexToken('hmm', match3to4);
+addRegexToken('hmmss', match5to6);
+addRegexToken('Hmm', match3to4);
+addRegexToken('Hmmss', match5to6);
+
+addParseToken(['H', 'HH'], HOUR);
+addParseToken(['k', 'kk'], function (input, array, config) {
+ var kInput = toInt(input);
+ array[HOUR] = kInput === 24 ? 0 : kInput;
+});
+addParseToken(['a', 'A'], function (input, array, config) {
+ config._isPm = config._locale.isPM(input);
+ config._meridiem = input;
+});
+addParseToken(['h', 'hh'], function (input, array, config) {
+ array[HOUR] = toInt(input);
+ getParsingFlags(config).bigHour = true;
+});
+addParseToken('hmm', function (input, array, config) {
+ var pos = input.length - 2;
+ array[HOUR] = toInt(input.substr(0, pos));
+ array[MINUTE] = toInt(input.substr(pos));
+ getParsingFlags(config).bigHour = true;
+});
+addParseToken('hmmss', function (input, array, config) {
+ var pos1 = input.length - 4,
+ pos2 = input.length - 2;
+ array[HOUR] = toInt(input.substr(0, pos1));
+ array[MINUTE] = toInt(input.substr(pos1, 2));
+ array[SECOND] = toInt(input.substr(pos2));
+ getParsingFlags(config).bigHour = true;
+});
+addParseToken('Hmm', function (input, array, config) {
+ var pos = input.length - 2;
+ array[HOUR] = toInt(input.substr(0, pos));
+ array[MINUTE] = toInt(input.substr(pos));
+});
+addParseToken('Hmmss', function (input, array, config) {
+ var pos1 = input.length - 4,
+ pos2 = input.length - 2;
+ array[HOUR] = toInt(input.substr(0, pos1));
+ array[MINUTE] = toInt(input.substr(pos1, 2));
+ array[SECOND] = toInt(input.substr(pos2));
+});
+
+// LOCALES
+
+function localeIsPM(input) {
+ // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
+ // Using charAt should be more compatible.
+ return (input + '').toLowerCase().charAt(0) === 'p';
+}
+
+var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i,
+ // Setting the hour should keep the time, because the user explicitly
+ // specified which hour they want. So trying to maintain the same hour (in
+ // a new timezone) makes sense. Adding/subtracting hours does not follow
+ // this rule.
+ getSetHour = makeGetSet('Hours', true);
+
+function localeMeridiem(hours, minutes, isLower) {
+ if (hours > 11) {
+ return isLower ? 'pm' : 'PM';
+ } else {
+ return isLower ? 'am' : 'AM';
+ }
+}
+
+var baseConfig = {
+ calendar: defaultCalendar,
+ longDateFormat: defaultLongDateFormat,
+ invalidDate: defaultInvalidDate,
+ ordinal: defaultOrdinal,
+ dayOfMonthOrdinalParse: defaultDayOfMonthOrdinalParse,
+ relativeTime: defaultRelativeTime,
+
+ months: defaultLocaleMonths,
+ monthsShort: defaultLocaleMonthsShort,
+
+ week: defaultLocaleWeek,
+
+ weekdays: defaultLocaleWeekdays,
+ weekdaysMin: defaultLocaleWeekdaysMin,
+ weekdaysShort: defaultLocaleWeekdaysShort,
+
+ meridiemParse: defaultLocaleMeridiemParse,
+};
+
+// internal storage for locale config files
+var locales = {},
+ localeFamilies = {},
+ globalLocale;
+
+function commonPrefix(arr1, arr2) {
+ var i,
+ minl = Math.min(arr1.length, arr2.length);
+ for (i = 0; i < minl; i += 1) {
+ if (arr1[i] !== arr2[i]) {
+ return i;
+ }
+ }
+ return minl;
+}
+
+function normalizeLocale(key) {
+ return key ? key.toLowerCase().replace('_', '-') : key;
+}
+
+// pick the locale from the array
+// try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
+// substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
+function chooseLocale(names) {
+ var i = 0,
+ j,
+ next,
+ locale,
+ split;
+
+ while (i < names.length) {
+ split = normalizeLocale(names[i]).split('-');
+ j = split.length;
+ next = normalizeLocale(names[i + 1]);
+ next = next ? next.split('-') : null;
+ while (j > 0) {
+ locale = loadLocale(split.slice(0, j).join('-'));
+ if (locale) {
+ return locale;
+ }
+ if (
+ next &&
+ next.length >= j &&
+ commonPrefix(split, next) >= j - 1
+ ) {
+ //the next array item is better than a shallower substring of this one
+ break;
+ }
+ j--;
+ }
+ i++;
+ }
+ return globalLocale;
+}
+
+function isLocaleNameSane(name) {
+ // Prevent names that look like filesystem paths, i.e contain '/' or '\'
+ // Ensure name is available and function returns boolean
+ return !!(name && name.match('^[^/\\\\]*$'));
+}
+
+function loadLocale(name) {
+ var oldLocale = null,
+ aliasedRequire;
+ // TODO: Find a better way to register and load all the locales in Node
+ if (
+ locales[name] === undefined &&
+ typeof module !== 'undefined' &&
+ module &&
+ module.exports &&
+ isLocaleNameSane(name)
+ ) {
+ try {
+ oldLocale = globalLocale._abbr;
+ aliasedRequire = require;
+ aliasedRequire('./locale/' + name);
+ getSetGlobalLocale(oldLocale);
+ } catch (e) {
+ // mark as not found to avoid repeating expensive file require call causing high CPU
+ // when trying to find en-US, en_US, en-us for every format call
+ locales[name] = null; // null means not found
+ }
+ }
+ return locales[name];
+}
+
+// This function will load locale and then set the global locale. If
+// no arguments are passed in, it will simply return the current global
+// locale key.
+function getSetGlobalLocale(key, values) {
+ var data;
+ if (key) {
+ if (isUndefined(values)) {
+ data = getLocale(key);
+ } else {
+ data = defineLocale(key, values);
+ }
+
+ if (data) {
+ // moment.duration._locale = moment._locale = data;
+ globalLocale = data;
+ } else {
+ if (typeof console !== 'undefined' && console.warn) {
+ //warn user if arguments are passed but the locale could not be set
+ console.warn(
+ 'Locale ' + key + ' not found. Did you forget to load it?'
+ );
+ }
+ }
+ }
+
+ return globalLocale._abbr;
+}
+
+function defineLocale(name, config) {
+ if (config !== null) {
+ var locale,
+ parentConfig = baseConfig;
+ config.abbr = name;
+ if (locales[name] != null) {
+ deprecateSimple(
+ 'defineLocaleOverride',
+ 'use moment.updateLocale(localeName, config) to change ' +
+ 'an existing locale. moment.defineLocale(localeName, ' +
+ 'config) should only be used for creating a new locale ' +
+ 'See http://momentjs.com/guides/#/warnings/define-locale/ for more info.'
+ );
+ parentConfig = locales[name]._config;
+ } else if (config.parentLocale != null) {
+ if (locales[config.parentLocale] != null) {
+ parentConfig = locales[config.parentLocale]._config;
+ } else {
+ locale = loadLocale(config.parentLocale);
+ if (locale != null) {
+ parentConfig = locale._config;
+ } else {
+ if (!localeFamilies[config.parentLocale]) {
+ localeFamilies[config.parentLocale] = [];
+ }
+ localeFamilies[config.parentLocale].push({
+ name: name,
+ config: config,
+ });
+ return null;
+ }
+ }
+ }
+ locales[name] = new Locale(mergeConfigs(parentConfig, config));
+
+ if (localeFamilies[name]) {
+ localeFamilies[name].forEach(function (x) {
+ defineLocale(x.name, x.config);
+ });
+ }
+
+ // backwards compat for now: also set the locale
+ // make sure we set the locale AFTER all child locales have been
+ // created, so we won't end up with the child locale set.
+ getSetGlobalLocale(name);
+
+ return locales[name];
+ } else {
+ // useful for testing
+ delete locales[name];
+ return null;
+ }
+}
+
+function updateLocale(name, config) {
+ if (config != null) {
+ var locale,
+ tmpLocale,
+ parentConfig = baseConfig;
+
+ if (locales[name] != null && locales[name].parentLocale != null) {
+ // Update existing child locale in-place to avoid memory-leaks
+ locales[name].set(mergeConfigs(locales[name]._config, config));
+ } else {
+ // MERGE
+ tmpLocale = loadLocale(name);
+ if (tmpLocale != null) {
+ parentConfig = tmpLocale._config;
+ }
+ config = mergeConfigs(parentConfig, config);
+ if (tmpLocale == null) {
+ // updateLocale is called for creating a new locale
+ // Set abbr so it will have a name (getters return
+ // undefined otherwise).
+ config.abbr = name;
+ }
+ locale = new Locale(config);
+ locale.parentLocale = locales[name];
+ locales[name] = locale;
+ }
+
+ // backwards compat for now: also set the locale
+ getSetGlobalLocale(name);
+ } else {
+ // pass null for config to unupdate, useful for tests
+ if (locales[name] != null) {
+ if (locales[name].parentLocale != null) {
+ locales[name] = locales[name].parentLocale;
+ if (name === getSetGlobalLocale()) {
+ getSetGlobalLocale(name);
+ }
+ } else if (locales[name] != null) {
+ delete locales[name];
+ }
+ }
+ }
+ return locales[name];
+}
+
+// returns locale data
+function getLocale(key) {
+ var locale;
+
+ if (key && key._locale && key._locale._abbr) {
+ key = key._locale._abbr;
+ }
+
+ if (!key) {
+ return globalLocale;
+ }
+
+ if (!isArray(key)) {
+ //short-circuit everything else
+ locale = loadLocale(key);
+ if (locale) {
+ return locale;
+ }
+ key = [key];
+ }
+
+ return chooseLocale(key);
+}
+
+function listLocales() {
+ return keys(locales);
+}
+
+function checkOverflow(m) {
+ var overflow,
+ a = m._a;
+
+ if (a && getParsingFlags(m).overflow === -2) {
+ overflow =
+ a[MONTH] < 0 || a[MONTH] > 11
+ ? MONTH
+ : a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH])
+ ? DATE
+ : a[HOUR] < 0 ||
+ a[HOUR] > 24 ||
+ (a[HOUR] === 24 &&
+ (a[MINUTE] !== 0 ||
+ a[SECOND] !== 0 ||
+ a[MILLISECOND] !== 0))
+ ? HOUR
+ : a[MINUTE] < 0 || a[MINUTE] > 59
+ ? MINUTE
+ : a[SECOND] < 0 || a[SECOND] > 59
+ ? SECOND
+ : a[MILLISECOND] < 0 || a[MILLISECOND] > 999
+ ? MILLISECOND
+ : -1;
+
+ if (
+ getParsingFlags(m)._overflowDayOfYear &&
+ (overflow < YEAR || overflow > DATE)
+ ) {
+ overflow = DATE;
+ }
+ if (getParsingFlags(m)._overflowWeeks && overflow === -1) {
+ overflow = WEEK;
+ }
+ if (getParsingFlags(m)._overflowWeekday && overflow === -1) {
+ overflow = WEEKDAY;
+ }
+
+ getParsingFlags(m).overflow = overflow;
+ }
+
+ return m;
+}
+
+// iso 8601 regex
+// 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00)
+var extendedIsoRegex =
+ /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,
+ basicIsoRegex =
+ /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d|))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,
+ tzRegex = /Z|[+-]\d\d(?::?\d\d)?/,
+ isoDates = [
+ ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/],
+ ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/],
+ ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/],
+ ['GGGG-[W]WW', /\d{4}-W\d\d/, false],
+ ['YYYY-DDD', /\d{4}-\d{3}/],
+ ['YYYY-MM', /\d{4}-\d\d/, false],
+ ['YYYYYYMMDD', /[+-]\d{10}/],
+ ['YYYYMMDD', /\d{8}/],
+ ['GGGG[W]WWE', /\d{4}W\d{3}/],
+ ['GGGG[W]WW', /\d{4}W\d{2}/, false],
+ ['YYYYDDD', /\d{7}/],
+ ['YYYYMM', /\d{6}/, false],
+ ['YYYY', /\d{4}/, false],
+ ],
+ // iso time formats and regexes
+ isoTimes = [
+ ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/],
+ ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/],
+ ['HH:mm:ss', /\d\d:\d\d:\d\d/],
+ ['HH:mm', /\d\d:\d\d/],
+ ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/],
+ ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/],
+ ['HHmmss', /\d\d\d\d\d\d/],
+ ['HHmm', /\d\d\d\d/],
+ ['HH', /\d\d/],
+ ],
+ aspNetJsonRegex = /^\/?Date\((-?\d+)/i,
+ // RFC 2822 regex: For details see https://tools.ietf.org/html/rfc2822#section-3.3
+ rfc2822 =
+ /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/,
+ obsOffsets = {
+ UT: 0,
+ GMT: 0,
+ EDT: -4 * 60,
+ EST: -5 * 60,
+ CDT: -5 * 60,
+ CST: -6 * 60,
+ MDT: -6 * 60,
+ MST: -7 * 60,
+ PDT: -7 * 60,
+ PST: -8 * 60,
+ };
+
+// date from iso format
+function configFromISO(config) {
+ var i,
+ l,
+ string = config._i,
+ match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string),
+ allowTime,
+ dateFormat,
+ timeFormat,
+ tzFormat,
+ isoDatesLen = isoDates.length,
+ isoTimesLen = isoTimes.length;
+
+ if (match) {
+ getParsingFlags(config).iso = true;
+ for (i = 0, l = isoDatesLen; i < l; i++) {
+ if (isoDates[i][1].exec(match[1])) {
+ dateFormat = isoDates[i][0];
+ allowTime = isoDates[i][2] !== false;
+ break;
+ }
+ }
+ if (dateFormat == null) {
+ config._isValid = false;
+ return;
+ }
+ if (match[3]) {
+ for (i = 0, l = isoTimesLen; i < l; i++) {
+ if (isoTimes[i][1].exec(match[3])) {
+ // match[2] should be 'T' or space
+ timeFormat = (match[2] || ' ') + isoTimes[i][0];
+ break;
+ }
+ }
+ if (timeFormat == null) {
+ config._isValid = false;
+ return;
+ }
+ }
+ if (!allowTime && timeFormat != null) {
+ config._isValid = false;
+ return;
+ }
+ if (match[4]) {
+ if (tzRegex.exec(match[4])) {
+ tzFormat = 'Z';
+ } else {
+ config._isValid = false;
+ return;
+ }
+ }
+ config._f = dateFormat + (timeFormat || '') + (tzFormat || '');
+ configFromStringAndFormat(config);
+ } else {
+ config._isValid = false;
+ }
+}
+
+function extractFromRFC2822Strings(
+ yearStr,
+ monthStr,
+ dayStr,
+ hourStr,
+ minuteStr,
+ secondStr
+) {
+ var result = [
+ untruncateYear(yearStr),
+ defaultLocaleMonthsShort.indexOf(monthStr),
+ parseInt(dayStr, 10),
+ parseInt(hourStr, 10),
+ parseInt(minuteStr, 10),
+ ];
+
+ if (secondStr) {
+ result.push(parseInt(secondStr, 10));
+ }
+
+ return result;
+}
+
+function untruncateYear(yearStr) {
+ var year = parseInt(yearStr, 10);
+ if (year <= 49) {
+ return 2000 + year;
+ } else if (year <= 999) {
+ return 1900 + year;
+ }
+ return year;
+}
+
+function preprocessRFC2822(s) {
+ // Remove comments and folding whitespace and replace multiple-spaces with a single space
+ return s
+ .replace(/\([^()]*\)|[\n\t]/g, ' ')
+ .replace(/(\s\s+)/g, ' ')
+ .replace(/^\s\s*/, '')
+ .replace(/\s\s*$/, '');
+}
+
+function checkWeekday(weekdayStr, parsedInput, config) {
+ if (weekdayStr) {
+ // TODO: Replace the vanilla JS Date object with an independent day-of-week check.
+ var weekdayProvided = defaultLocaleWeekdaysShort.indexOf(weekdayStr),
+ weekdayActual = new Date(
+ parsedInput[0],
+ parsedInput[1],
+ parsedInput[2]
+ ).getDay();
+ if (weekdayProvided !== weekdayActual) {
+ getParsingFlags(config).weekdayMismatch = true;
+ config._isValid = false;
+ return false;
+ }
+ }
+ return true;
+}
+
+function calculateOffset(obsOffset, militaryOffset, numOffset) {
+ if (obsOffset) {
+ return obsOffsets[obsOffset];
+ } else if (militaryOffset) {
+ // the only allowed military tz is Z
+ return 0;
+ } else {
+ var hm = parseInt(numOffset, 10),
+ m = hm % 100,
+ h = (hm - m) / 100;
+ return h * 60 + m;
+ }
+}
+
+// date and time from ref 2822 format
+function configFromRFC2822(config) {
+ var match = rfc2822.exec(preprocessRFC2822(config._i)),
+ parsedArray;
+ if (match) {
+ parsedArray = extractFromRFC2822Strings(
+ match[4],
+ match[3],
+ match[2],
+ match[5],
+ match[6],
+ match[7]
+ );
+ if (!checkWeekday(match[1], parsedArray, config)) {
+ return;
+ }
+
+ config._a = parsedArray;
+ config._tzm = calculateOffset(match[8], match[9], match[10]);
+
+ config._d = createUTCDate.apply(null, config._a);
+ config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);
+
+ getParsingFlags(config).rfc2822 = true;
+ } else {
+ config._isValid = false;
+ }
+}
+
+// date from 1) ASP.NET, 2) ISO, 3) RFC 2822 formats, or 4) optional fallback if parsing isn't strict
+function configFromString(config) {
+ var matched = aspNetJsonRegex.exec(config._i);
+ if (matched !== null) {
+ config._d = new Date(+matched[1]);
+ return;
+ }
+
+ configFromISO(config);
+ if (config._isValid === false) {
+ delete config._isValid;
+ } else {
+ return;
+ }
+
+ configFromRFC2822(config);
+ if (config._isValid === false) {
+ delete config._isValid;
+ } else {
+ return;
+ }
+
+ if (config._strict) {
+ config._isValid = false;
+ } else {
+ // Final attempt, use Input Fallback
+ hooks.createFromInputFallback(config);
+ }
+}
+
+hooks.createFromInputFallback = deprecate(
+ 'value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), ' +
+ 'which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are ' +
+ 'discouraged. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.',
+ function (config) {
+ config._d = new Date(config._i + (config._useUTC ? ' UTC' : ''));
+ }
+);
+
+// Pick the first defined of two or three arguments.
+function defaults(a, b, c) {
+ if (a != null) {
+ return a;
+ }
+ if (b != null) {
+ return b;
+ }
+ return c;
+}
+
+function currentDateArray(config) {
+ // hooks is actually the exported moment object
+ var nowValue = new Date(hooks.now());
+ if (config._useUTC) {
+ return [
+ nowValue.getUTCFullYear(),
+ nowValue.getUTCMonth(),
+ nowValue.getUTCDate(),
+ ];
+ }
+ return [nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()];
+}
+
+// convert an array to a date.
+// the array should mirror the parameters below
+// note: all values past the year are optional and will default to the lowest possible value.
+// [year, month, day , hour, minute, second, millisecond]
+function configFromArray(config) {
+ var i,
+ date,
+ input = [],
+ currentDate,
+ expectedWeekday,
+ yearToUse;
+
+ if (config._d) {
+ return;
+ }
+
+ currentDate = currentDateArray(config);
+
+ //compute day of the year from weeks and weekdays
+ if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
+ dayOfYearFromWeekInfo(config);
+ }
+
+ //if the day of the year is set, figure out what it is
+ if (config._dayOfYear != null) {
+ yearToUse = defaults(config._a[YEAR], currentDate[YEAR]);
+
+ if (
+ config._dayOfYear > daysInYear(yearToUse) ||
+ config._dayOfYear === 0
+ ) {
+ getParsingFlags(config)._overflowDayOfYear = true;
+ }
+
+ date = createUTCDate(yearToUse, 0, config._dayOfYear);
+ config._a[MONTH] = date.getUTCMonth();
+ config._a[DATE] = date.getUTCDate();
+ }
+
+ // Default to current date.
+ // * if no year, month, day of month are given, default to today
+ // * if day of month is given, default month and year
+ // * if month is given, default only year
+ // * if year is given, don't default anything
+ for (i = 0; i < 3 && config._a[i] == null; ++i) {
+ config._a[i] = input[i] = currentDate[i];
+ }
+
+ // Zero out whatever was not defaulted, including time
+ for (; i < 7; i++) {
+ config._a[i] = input[i] =
+ config._a[i] == null ? (i === 2 ? 1 : 0) : config._a[i];
+ }
+
+ // Check for 24:00:00.000
+ if (
+ config._a[HOUR] === 24 &&
+ config._a[MINUTE] === 0 &&
+ config._a[SECOND] === 0 &&
+ config._a[MILLISECOND] === 0
+ ) {
+ config._nextDay = true;
+ config._a[HOUR] = 0;
+ }
+
+ config._d = (config._useUTC ? createUTCDate : createDate).apply(
+ null,
+ input
+ );
+ expectedWeekday = config._useUTC
+ ? config._d.getUTCDay()
+ : config._d.getDay();
+
+ // Apply timezone offset from input. The actual utcOffset can be changed
+ // with parseZone.
+ if (config._tzm != null) {
+ config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);
+ }
+
+ if (config._nextDay) {
+ config._a[HOUR] = 24;
+ }
+
+ // check for mismatching day of week
+ if (
+ config._w &&
+ typeof config._w.d !== 'undefined' &&
+ config._w.d !== expectedWeekday
+ ) {
+ getParsingFlags(config).weekdayMismatch = true;
+ }
+}
+
+function dayOfYearFromWeekInfo(config) {
+ var w, weekYear, week, weekday, dow, doy, temp, weekdayOverflow, curWeek;
+
+ w = config._w;
+ if (w.GG != null || w.W != null || w.E != null) {
+ dow = 1;
+ doy = 4;
+
+ // TODO: We need to take the current isoWeekYear, but that depends on
+ // how we interpret now (local, utc, fixed offset). So create
+ // a now version of current config (take local/utc/offset flags, and
+ // create now).
+ weekYear = defaults(
+ w.GG,
+ config._a[YEAR],
+ weekOfYear(createLocal(), 1, 4).year
+ );
+ week = defaults(w.W, 1);
+ weekday = defaults(w.E, 1);
+ if (weekday < 1 || weekday > 7) {
+ weekdayOverflow = true;
+ }
+ } else {
+ dow = config._locale._week.dow;
+ doy = config._locale._week.doy;
+
+ curWeek = weekOfYear(createLocal(), dow, doy);
+
+ weekYear = defaults(w.gg, config._a[YEAR], curWeek.year);
+
+ // Default to current week.
+ week = defaults(w.w, curWeek.week);
+
+ if (w.d != null) {
+ // weekday -- low day numbers are considered next week
+ weekday = w.d;
+ if (weekday < 0 || weekday > 6) {
+ weekdayOverflow = true;
+ }
+ } else if (w.e != null) {
+ // local weekday -- counting starts from beginning of week
+ weekday = w.e + dow;
+ if (w.e < 0 || w.e > 6) {
+ weekdayOverflow = true;
+ }
+ } else {
+ // default to beginning of week
+ weekday = dow;
+ }
+ }
+ if (week < 1 || week > weeksInYear(weekYear, dow, doy)) {
+ getParsingFlags(config)._overflowWeeks = true;
+ } else if (weekdayOverflow != null) {
+ getParsingFlags(config)._overflowWeekday = true;
+ } else {
+ temp = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy);
+ config._a[YEAR] = temp.year;
+ config._dayOfYear = temp.dayOfYear;
+ }
+}
+
+// constant that refers to the ISO standard
+hooks.ISO_8601 = function () {};
+
+// constant that refers to the RFC 2822 form
+hooks.RFC_2822 = function () {};
+
+// date from string and format string
+function configFromStringAndFormat(config) {
+ // TODO: Move this to another part of the creation flow to prevent circular deps
+ if (config._f === hooks.ISO_8601) {
+ configFromISO(config);
+ return;
+ }
+ if (config._f === hooks.RFC_2822) {
+ configFromRFC2822(config);
+ return;
+ }
+ config._a = [];
+ getParsingFlags(config).empty = true;
+
+ // This array is used to make a Date, either with `new Date` or `Date.UTC`
+ var string = '' + config._i,
+ i,
+ parsedInput,
+ tokens,
+ token,
+ skipped,
+ stringLength = string.length,
+ totalParsedInputLength = 0,
+ era,
+ tokenLen;
+
+ tokens =
+ expandFormat(config._f, config._locale).match(formattingTokens) || [];
+ tokenLen = tokens.length;
+ for (i = 0; i < tokenLen; i++) {
+ token = tokens[i];
+ parsedInput = (string.match(getParseRegexForToken(token, config)) ||
+ [])[0];
+ if (parsedInput) {
+ skipped = string.substr(0, string.indexOf(parsedInput));
+ if (skipped.length > 0) {
+ getParsingFlags(config).unusedInput.push(skipped);
+ }
+ string = string.slice(
+ string.indexOf(parsedInput) + parsedInput.length
+ );
+ totalParsedInputLength += parsedInput.length;
+ }
+ // don't parse if it's not a known token
+ if (formatTokenFunctions[token]) {
+ if (parsedInput) {
+ getParsingFlags(config).empty = false;
+ } else {
+ getParsingFlags(config).unusedTokens.push(token);
+ }
+ addTimeToArrayFromToken(token, parsedInput, config);
+ } else if (config._strict && !parsedInput) {
+ getParsingFlags(config).unusedTokens.push(token);
+ }
+ }
+
+ // add remaining unparsed input length to the string
+ getParsingFlags(config).charsLeftOver =
+ stringLength - totalParsedInputLength;
+ if (string.length > 0) {
+ getParsingFlags(config).unusedInput.push(string);
+ }
+
+ // clear _12h flag if hour is <= 12
+ if (
+ config._a[HOUR] <= 12 &&
+ getParsingFlags(config).bigHour === true &&
+ config._a[HOUR] > 0
+ ) {
+ getParsingFlags(config).bigHour = undefined;
+ }
+
+ getParsingFlags(config).parsedDateParts = config._a.slice(0);
+ getParsingFlags(config).meridiem = config._meridiem;
+ // handle meridiem
+ config._a[HOUR] = meridiemFixWrap(
+ config._locale,
+ config._a[HOUR],
+ config._meridiem
+ );
+
+ // handle era
+ era = getParsingFlags(config).era;
+ if (era !== null) {
+ config._a[YEAR] = config._locale.erasConvertYear(era, config._a[YEAR]);
+ }
+
+ configFromArray(config);
+ checkOverflow(config);
+}
+
+function meridiemFixWrap(locale, hour, meridiem) {
+ var isPm;
+
+ if (meridiem == null) {
+ // nothing to do
+ return hour;
+ }
+ if (locale.meridiemHour != null) {
+ return locale.meridiemHour(hour, meridiem);
+ } else if (locale.isPM != null) {
+ // Fallback
+ isPm = locale.isPM(meridiem);
+ if (isPm && hour < 12) {
+ hour += 12;
+ }
+ if (!isPm && hour === 12) {
+ hour = 0;
+ }
+ return hour;
+ } else {
+ // this is not supposed to happen
+ return hour;
+ }
+}
+
+// date from string and array of format strings
+function configFromStringAndArray(config) {
+ var tempConfig,
+ bestMoment,
+ scoreToBeat,
+ i,
+ currentScore,
+ validFormatFound,
+ bestFormatIsValid = false,
+ configfLen = config._f.length;
+
+ if (configfLen === 0) {
+ getParsingFlags(config).invalidFormat = true;
+ config._d = new Date(NaN);
+ return;
+ }
+
+ for (i = 0; i < configfLen; i++) {
+ currentScore = 0;
+ validFormatFound = false;
+ tempConfig = copyConfig({}, config);
+ if (config._useUTC != null) {
+ tempConfig._useUTC = config._useUTC;
+ }
+ tempConfig._f = config._f[i];
+ configFromStringAndFormat(tempConfig);
+
+ if (isValid(tempConfig)) {
+ validFormatFound = true;
+ }
+
+ // if there is any input that was not parsed add a penalty for that format
+ currentScore += getParsingFlags(tempConfig).charsLeftOver;
+
+ //or tokens
+ currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10;
+
+ getParsingFlags(tempConfig).score = currentScore;
+
+ if (!bestFormatIsValid) {
+ if (
+ scoreToBeat == null ||
+ currentScore < scoreToBeat ||
+ validFormatFound
+ ) {
+ scoreToBeat = currentScore;
+ bestMoment = tempConfig;
+ if (validFormatFound) {
+ bestFormatIsValid = true;
+ }
+ }
+ } else {
+ if (currentScore < scoreToBeat) {
+ scoreToBeat = currentScore;
+ bestMoment = tempConfig;
+ }
+ }
+ }
+
+ extend(config, bestMoment || tempConfig);
+}
+
+function configFromObject(config) {
+ if (config._d) {
+ return;
+ }
+
+ var i = normalizeObjectUnits(config._i),
+ dayOrDate = i.day === undefined ? i.date : i.day;
+ config._a = map(
+ [i.year, i.month, dayOrDate, i.hour, i.minute, i.second, i.millisecond],
+ function (obj) {
+ return obj && parseInt(obj, 10);
+ }
+ );
+
+ configFromArray(config);
+}
+
+function createFromConfig(config) {
+ var res = new Moment(checkOverflow(prepareConfig(config)));
+ if (res._nextDay) {
+ // Adding is smart enough around DST
+ res.add(1, 'd');
+ res._nextDay = undefined;
+ }
+
+ return res;
+}
+
+function prepareConfig(config) {
+ var input = config._i,
+ format = config._f;
+
+ config._locale = config._locale || getLocale(config._l);
+
+ if (input === null || (format === undefined && input === '')) {
+ return createInvalid({ nullInput: true });
+ }
+
+ if (typeof input === 'string') {
+ config._i = input = config._locale.preparse(input);
+ }
+
+ if (isMoment(input)) {
+ return new Moment(checkOverflow(input));
+ } else if (isDate(input)) {
+ config._d = input;
+ } else if (isArray(format)) {
+ configFromStringAndArray(config);
+ } else if (format) {
+ configFromStringAndFormat(config);
+ } else {
+ configFromInput(config);
+ }
+
+ if (!isValid(config)) {
+ config._d = null;
+ }
+
+ return config;
+}
+
+function configFromInput(config) {
+ var input = config._i;
+ if (isUndefined(input)) {
+ config._d = new Date(hooks.now());
+ } else if (isDate(input)) {
+ config._d = new Date(input.valueOf());
+ } else if (typeof input === 'string') {
+ configFromString(config);
+ } else if (isArray(input)) {
+ config._a = map(input.slice(0), function (obj) {
+ return parseInt(obj, 10);
+ });
+ configFromArray(config);
+ } else if (isObject(input)) {
+ configFromObject(config);
+ } else if (isNumber(input)) {
+ // from milliseconds
+ config._d = new Date(input);
+ } else {
+ hooks.createFromInputFallback(config);
+ }
+}
+
+function createLocalOrUTC(input, format, locale, strict, isUTC) {
+ var c = {};
+
+ if (format === true || format === false) {
+ strict = format;
+ format = undefined;
+ }
+
+ if (locale === true || locale === false) {
+ strict = locale;
+ locale = undefined;
+ }
+
+ if (
+ (isObject(input) && isObjectEmpty(input)) ||
+ (isArray(input) && input.length === 0)
+ ) {
+ input = undefined;
+ }
+ // object construction must be done this way.
+ // https://github.com/moment/moment/issues/1423
+ c._isAMomentObject = true;
+ c._useUTC = c._isUTC = isUTC;
+ c._l = locale;
+ c._i = input;
+ c._f = format;
+ c._strict = strict;
+
+ return createFromConfig(c);
+}
+
+function createLocal(input, format, locale, strict) {
+ return createLocalOrUTC(input, format, locale, strict, false);
+}
+
+var prototypeMin = deprecate(
+ 'moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/',
+ function () {
+ var other = createLocal.apply(null, arguments);
+ if (this.isValid() && other.isValid()) {
+ return other < this ? this : other;
+ } else {
+ return createInvalid();
+ }
+ }
+ ),
+ prototypeMax = deprecate(
+ 'moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/',
+ function () {
+ var other = createLocal.apply(null, arguments);
+ if (this.isValid() && other.isValid()) {
+ return other > this ? this : other;
+ } else {
+ return createInvalid();
+ }
+ }
+ );
+
+// Pick a moment m from moments so that m[fn](other) is true for all
+// other. This relies on the function fn to be transitive.
+//
+// moments should either be an array of moment objects or an array, whose
+// first element is an array of moment objects.
+function pickBy(fn, moments) {
+ var res, i;
+ if (moments.length === 1 && isArray(moments[0])) {
+ moments = moments[0];
+ }
+ if (!moments.length) {
+ return createLocal();
+ }
+ res = moments[0];
+ for (i = 1; i < moments.length; ++i) {
+ if (!moments[i].isValid() || moments[i][fn](res)) {
+ res = moments[i];
+ }
+ }
+ return res;
+}
+
+// TODO: Use [].sort instead?
+function min() {
+ var args = [].slice.call(arguments, 0);
+
+ return pickBy('isBefore', args);
+}
+
+function max() {
+ var args = [].slice.call(arguments, 0);
+
+ return pickBy('isAfter', args);
+}
+
+var now = function () {
+ return Date.now ? Date.now() : +new Date();
+};
+
+var ordering = [
+ 'year',
+ 'quarter',
+ 'month',
+ 'week',
+ 'day',
+ 'hour',
+ 'minute',
+ 'second',
+ 'millisecond',
+];
+
+function isDurationValid(m) {
+ var key,
+ unitHasDecimal = false,
+ i,
+ orderLen = ordering.length;
+ for (key in m) {
+ if (
+ hasOwnProp(m, key) &&
+ !(
+ indexOf.call(ordering, key) !== -1 &&
+ (m[key] == null || !isNaN(m[key]))
+ )
+ ) {
+ return false;
+ }
+ }
+
+ for (i = 0; i < orderLen; ++i) {
+ if (m[ordering[i]]) {
+ if (unitHasDecimal) {
+ return false; // only allow non-integers for smallest unit
+ }
+ if (parseFloat(m[ordering[i]]) !== toInt(m[ordering[i]])) {
+ unitHasDecimal = true;
+ }
+ }
+ }
+
+ return true;
+}
+
+function isValid$1() {
+ return this._isValid;
+}
+
+function createInvalid$1() {
+ return createDuration(NaN);
+}
+
+function Duration(duration) {
+ var normalizedInput = normalizeObjectUnits(duration),
+ years = normalizedInput.year || 0,
+ quarters = normalizedInput.quarter || 0,
+ months = normalizedInput.month || 0,
+ weeks = normalizedInput.week || normalizedInput.isoWeek || 0,
+ days = normalizedInput.day || 0,
+ hours = normalizedInput.hour || 0,
+ minutes = normalizedInput.minute || 0,
+ seconds = normalizedInput.second || 0,
+ milliseconds = normalizedInput.millisecond || 0;
+
+ this._isValid = isDurationValid(normalizedInput);
+
+ // representation for dateAddRemove
+ this._milliseconds =
+ +milliseconds +
+ seconds * 1e3 + // 1000
+ minutes * 6e4 + // 1000 * 60
+ hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978
+ // Because of dateAddRemove treats 24 hours as different from a
+ // day when working around DST, we need to store them separately
+ this._days = +days + weeks * 7;
+ // It is impossible to translate months into days without knowing
+ // which months you are are talking about, so we have to store
+ // it separately.
+ this._months = +months + quarters * 3 + years * 12;
+
+ this._data = {};
+
+ this._locale = getLocale();
+
+ this._bubble();
+}
+
+function isDuration(obj) {
+ return obj instanceof Duration;
+}
+
+function absRound(number) {
+ if (number < 0) {
+ return Math.round(-1 * number) * -1;
+ } else {
+ return Math.round(number);
+ }
+}
+
+// compare two arrays, return the number of differences
+function compareArrays(array1, array2, dontConvert) {
+ var len = Math.min(array1.length, array2.length),
+ lengthDiff = Math.abs(array1.length - array2.length),
+ diffs = 0,
+ i;
+ for (i = 0; i < len; i++) {
+ if (
+ (dontConvert && array1[i] !== array2[i]) ||
+ (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))
+ ) {
+ diffs++;
+ }
+ }
+ return diffs + lengthDiff;
+}
+
+// FORMATTING
+
+function offset(token, separator) {
+ addFormatToken(token, 0, 0, function () {
+ var offset = this.utcOffset(),
+ sign = '+';
+ if (offset < 0) {
+ offset = -offset;
+ sign = '-';
+ }
+ return (
+ sign +
+ zeroFill(~~(offset / 60), 2) +
+ separator +
+ zeroFill(~~offset % 60, 2)
+ );
+ });
+}
+
+offset('Z', ':');
+offset('ZZ', '');
+
+// PARSING
+
+addRegexToken('Z', matchShortOffset);
+addRegexToken('ZZ', matchShortOffset);
+addParseToken(['Z', 'ZZ'], function (input, array, config) {
+ config._useUTC = true;
+ config._tzm = offsetFromString(matchShortOffset, input);
+});
+
+// HELPERS
+
+// timezone chunker
+// '+10:00' > ['10', '00']
+// '-1530' > ['-15', '30']
+var chunkOffset = /([\+\-]|\d\d)/gi;
+
+function offsetFromString(matcher, string) {
+ var matches = (string || '').match(matcher),
+ chunk,
+ parts,
+ minutes;
+
+ if (matches === null) {
+ return null;
+ }
+
+ chunk = matches[matches.length - 1] || [];
+ parts = (chunk + '').match(chunkOffset) || ['-', 0, 0];
+ minutes = +(parts[1] * 60) + toInt(parts[2]);
+
+ return minutes === 0 ? 0 : parts[0] === '+' ? minutes : -minutes;
+}
+
+// Return a moment from input, that is local/utc/zone equivalent to model.
+function cloneWithOffset(input, model) {
+ var res, diff;
+ if (model._isUTC) {
+ res = model.clone();
+ diff =
+ (isMoment(input) || isDate(input)
+ ? input.valueOf()
+ : createLocal(input).valueOf()) - res.valueOf();
+ // Use low-level api, because this fn is low-level api.
+ res._d.setTime(res._d.valueOf() + diff);
+ hooks.updateOffset(res, false);
+ return res;
+ } else {
+ return createLocal(input).local();
+ }
+}
+
+function getDateOffset(m) {
+ // On Firefox.24 Date#getTimezoneOffset returns a floating point.
+ // https://github.com/moment/moment/pull/1871
+ return -Math.round(m._d.getTimezoneOffset());
+}
+
+// HOOKS
+
+// This function will be called whenever a moment is mutated.
+// It is intended to keep the offset in sync with the timezone.
+hooks.updateOffset = function () {};
+
+// MOMENTS
+
+// keepLocalTime = true means only change the timezone, without
+// affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]-->
+// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset
+// +0200, so we adjust the time as needed, to be valid.
+//
+// Keeping the time actually adds/subtracts (one hour)
+// from the actual represented time. That is why we call updateOffset
+// a second time. In case it wants us to change the offset again
+// _changeInProgress == true case, then we have to adjust, because
+// there is no such time in the given timezone.
+function getSetOffset(input, keepLocalTime, keepMinutes) {
+ var offset = this._offset || 0,
+ localAdjust;
+ if (!this.isValid()) {
+ return input != null ? this : NaN;
+ }
+ if (input != null) {
+ if (typeof input === 'string') {
+ input = offsetFromString(matchShortOffset, input);
+ if (input === null) {
+ return this;
+ }
+ } else if (Math.abs(input) < 16 && !keepMinutes) {
+ input = input * 60;
+ }
+ if (!this._isUTC && keepLocalTime) {
+ localAdjust = getDateOffset(this);
+ }
+ this._offset = input;
+ this._isUTC = true;
+ if (localAdjust != null) {
+ this.add(localAdjust, 'm');
+ }
+ if (offset !== input) {
+ if (!keepLocalTime || this._changeInProgress) {
+ addSubtract(
+ this,
+ createDuration(input - offset, 'm'),
+ 1,
+ false
+ );
+ } else if (!this._changeInProgress) {
+ this._changeInProgress = true;
+ hooks.updateOffset(this, true);
+ this._changeInProgress = null;
+ }
+ }
+ return this;
+ } else {
+ return this._isUTC ? offset : getDateOffset(this);
+ }
+}
+
+function getSetZone(input, keepLocalTime) {
+ if (input != null) {
+ if (typeof input !== 'string') {
+ input = -input;
+ }
+
+ this.utcOffset(input, keepLocalTime);
+
+ return this;
+ } else {
+ return -this.utcOffset();
+ }
+}
+
+function setOffsetToUTC(keepLocalTime) {
+ return this.utcOffset(0, keepLocalTime);
+}
+
+function setOffsetToLocal(keepLocalTime) {
+ if (this._isUTC) {
+ this.utcOffset(0, keepLocalTime);
+ this._isUTC = false;
+
+ if (keepLocalTime) {
+ this.subtract(getDateOffset(this), 'm');
+ }
+ }
+ return this;
+}
+
+function setOffsetToParsedOffset() {
+ if (this._tzm != null) {
+ this.utcOffset(this._tzm, false, true);
+ } else if (typeof this._i === 'string') {
+ var tZone = offsetFromString(matchOffset, this._i);
+ if (tZone != null) {
+ this.utcOffset(tZone);
+ } else {
+ this.utcOffset(0, true);
+ }
+ }
+ return this;
+}
+
+function hasAlignedHourOffset(input) {
+ if (!this.isValid()) {
+ return false;
+ }
+ input = input ? createLocal(input).utcOffset() : 0;
+
+ return (this.utcOffset() - input) % 60 === 0;
+}
+
+function isDaylightSavingTime() {
+ return (
+ this.utcOffset() > this.clone().month(0).utcOffset() ||
+ this.utcOffset() > this.clone().month(5).utcOffset()
+ );
+}
+
+function isDaylightSavingTimeShifted() {
+ if (!isUndefined(this._isDSTShifted)) {
+ return this._isDSTShifted;
+ }
+
+ var c = {},
+ other;
+
+ copyConfig(c, this);
+ c = prepareConfig(c);
+
+ if (c._a) {
+ other = c._isUTC ? createUTC(c._a) : createLocal(c._a);
+ this._isDSTShifted =
+ this.isValid() && compareArrays(c._a, other.toArray()) > 0;
+ } else {
+ this._isDSTShifted = false;
+ }
+
+ return this._isDSTShifted;
+}
+
+function isLocal() {
+ return this.isValid() ? !this._isUTC : false;
+}
+
+function isUtcOffset() {
+ return this.isValid() ? this._isUTC : false;
+}
+
+function isUtc() {
+ return this.isValid() ? this._isUTC && this._offset === 0 : false;
+}
+
+// ASP.NET json date format regex
+var aspNetRegex = /^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/,
+ // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
+ // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
+ // and further modified to allow for strings containing both week and day
+ isoRegex =
+ /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;
+
+function createDuration(input, key) {
+ var duration = input,
+ // matching against regexp is expensive, do it on demand
+ match = null,
+ sign,
+ ret,
+ diffRes;
+
+ if (isDuration(input)) {
+ duration = {
+ ms: input._milliseconds,
+ d: input._days,
+ M: input._months,
+ };
+ } else if (isNumber(input) || !isNaN(+input)) {
+ duration = {};
+ if (key) {
+ duration[key] = +input;
+ } else {
+ duration.milliseconds = +input;
+ }
+ } else if ((match = aspNetRegex.exec(input))) {
+ sign = match[1] === '-' ? -1 : 1;
+ duration = {
+ y: 0,
+ d: toInt(match[DATE]) * sign,
+ h: toInt(match[HOUR]) * sign,
+ m: toInt(match[MINUTE]) * sign,
+ s: toInt(match[SECOND]) * sign,
+ ms: toInt(absRound(match[MILLISECOND] * 1000)) * sign, // the millisecond decimal point is included in the match
+ };
+ } else if ((match = isoRegex.exec(input))) {
+ sign = match[1] === '-' ? -1 : 1;
+ duration = {
+ y: parseIso(match[2], sign),
+ M: parseIso(match[3], sign),
+ w: parseIso(match[4], sign),
+ d: parseIso(match[5], sign),
+ h: parseIso(match[6], sign),
+ m: parseIso(match[7], sign),
+ s: parseIso(match[8], sign),
+ };
+ } else if (duration == null) {
+ // checks for null or undefined
+ duration = {};
+ } else if (
+ typeof duration === 'object' &&
+ ('from' in duration || 'to' in duration)
+ ) {
+ diffRes = momentsDifference(
+ createLocal(duration.from),
+ createLocal(duration.to)
+ );
+
+ duration = {};
+ duration.ms = diffRes.milliseconds;
+ duration.M = diffRes.months;
+ }
+
+ ret = new Duration(duration);
+
+ if (isDuration(input) && hasOwnProp(input, '_locale')) {
+ ret._locale = input._locale;
+ }
+
+ if (isDuration(input) && hasOwnProp(input, '_isValid')) {
+ ret._isValid = input._isValid;
+ }
+
+ return ret;
+}
+
+createDuration.fn = Duration.prototype;
+createDuration.invalid = createInvalid$1;
+
+function parseIso(inp, sign) {
+ // We'd normally use ~~inp for this, but unfortunately it also
+ // converts floats to ints.
+ // inp may be undefined, so careful calling replace on it.
+ var res = inp && parseFloat(inp.replace(',', '.'));
+ // apply sign while we're at it
+ return (isNaN(res) ? 0 : res) * sign;
+}
+
+function positiveMomentsDifference(base, other) {
+ var res = {};
+
+ res.months =
+ other.month() - base.month() + (other.year() - base.year()) * 12;
+ if (base.clone().add(res.months, 'M').isAfter(other)) {
+ --res.months;
+ }
+
+ res.milliseconds = +other - +base.clone().add(res.months, 'M');
+
+ return res;
+}
+
+function momentsDifference(base, other) {
+ var res;
+ if (!(base.isValid() && other.isValid())) {
+ return { milliseconds: 0, months: 0 };
+ }
+
+ other = cloneWithOffset(other, base);
+ if (base.isBefore(other)) {
+ res = positiveMomentsDifference(base, other);
+ } else {
+ res = positiveMomentsDifference(other, base);
+ res.milliseconds = -res.milliseconds;
+ res.months = -res.months;
+ }
+
+ return res;
+}
+
+// TODO: remove 'name' arg after deprecation is removed
+function createAdder(direction, name) {
+ return function (val, period) {
+ var dur, tmp;
+ //invert the arguments, but complain about it
+ if (period !== null && !isNaN(+period)) {
+ deprecateSimple(
+ name,
+ 'moment().' +
+ name +
+ '(period, number) is deprecated. Please use moment().' +
+ name +
+ '(number, period). ' +
+ 'See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.'
+ );
+ tmp = val;
+ val = period;
+ period = tmp;
+ }
+
+ dur = createDuration(val, period);
+ addSubtract(this, dur, direction);
+ return this;
+ };
+}
+
+function addSubtract(mom, duration, isAdding, updateOffset) {
+ var milliseconds = duration._milliseconds,
+ days = absRound(duration._days),
+ months = absRound(duration._months);
+
+ if (!mom.isValid()) {
+ // No op
+ return;
+ }
+
+ updateOffset = updateOffset == null ? true : updateOffset;
+
+ if (months) {
+ setMonth(mom, get(mom, 'Month') + months * isAdding);
+ }
+ if (days) {
+ set$1(mom, 'Date', get(mom, 'Date') + days * isAdding);
+ }
+ if (milliseconds) {
+ mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding);
+ }
+ if (updateOffset) {
+ hooks.updateOffset(mom, days || months);
+ }
+}
+
+var add = createAdder(1, 'add'),
+ subtract = createAdder(-1, 'subtract');
+
+function isString(input) {
+ return typeof input === 'string' || input instanceof String;
+}
+
+// type MomentInput = Moment | Date | string | number | (number | string)[] | MomentInputObject | void; // null | undefined
+function isMomentInput(input) {
+ return (
+ isMoment(input) ||
+ isDate(input) ||
+ isString(input) ||
+ isNumber(input) ||
+ isNumberOrStringArray(input) ||
+ isMomentInputObject(input) ||
+ input === null ||
+ input === undefined
+ );
+}
+
+function isMomentInputObject(input) {
+ var objectTest = isObject(input) && !isObjectEmpty(input),
+ propertyTest = false,
+ properties = [
+ 'years',
+ 'year',
+ 'y',
+ 'months',
+ 'month',
+ 'M',
+ 'days',
+ 'day',
+ 'd',
+ 'dates',
+ 'date',
+ 'D',
+ 'hours',
+ 'hour',
+ 'h',
+ 'minutes',
+ 'minute',
+ 'm',
+ 'seconds',
+ 'second',
+ 's',
+ 'milliseconds',
+ 'millisecond',
+ 'ms',
+ ],
+ i,
+ property,
+ propertyLen = properties.length;
+
+ for (i = 0; i < propertyLen; i += 1) {
+ property = properties[i];
+ propertyTest = propertyTest || hasOwnProp(input, property);
+ }
+
+ return objectTest && propertyTest;
+}
+
+function isNumberOrStringArray(input) {
+ var arrayTest = isArray(input),
+ dataTypeTest = false;
+ if (arrayTest) {
+ dataTypeTest =
+ input.filter(function (item) {
+ return !isNumber(item) && isString(input);
+ }).length === 0;
+ }
+ return arrayTest && dataTypeTest;
+}
+
+function isCalendarSpec(input) {
+ var objectTest = isObject(input) && !isObjectEmpty(input),
+ propertyTest = false,
+ properties = [
+ 'sameDay',
+ 'nextDay',
+ 'lastDay',
+ 'nextWeek',
+ 'lastWeek',
+ 'sameElse',
+ ],
+ i,
+ property;
+
+ for (i = 0; i < properties.length; i += 1) {
+ property = properties[i];
+ propertyTest = propertyTest || hasOwnProp(input, property);
+ }
+
+ return objectTest && propertyTest;
+}
+
+function getCalendarFormat(myMoment, now) {
+ var diff = myMoment.diff(now, 'days', true);
+ return diff < -6
+ ? 'sameElse'
+ : diff < -1
+ ? 'lastWeek'
+ : diff < 0
+ ? 'lastDay'
+ : diff < 1
+ ? 'sameDay'
+ : diff < 2
+ ? 'nextDay'
+ : diff < 7
+ ? 'nextWeek'
+ : 'sameElse';
+}
+
+function calendar$1(time, formats) {
+ // Support for single parameter, formats only overload to the calendar function
+ if (arguments.length === 1) {
+ if (!arguments[0]) {
+ time = undefined;
+ formats = undefined;
+ } else if (isMomentInput(arguments[0])) {
+ time = arguments[0];
+ formats = undefined;
+ } else if (isCalendarSpec(arguments[0])) {
+ formats = arguments[0];
+ time = undefined;
+ }
+ }
+ // We want to compare the start of today, vs this.
+ // Getting start-of-today depends on whether we're local/utc/offset or not.
+ var now = time || createLocal(),
+ sod = cloneWithOffset(now, this).startOf('day'),
+ format = hooks.calendarFormat(this, sod) || 'sameElse',
+ output =
+ formats &&
+ (isFunction(formats[format])
+ ? formats[format].call(this, now)
+ : formats[format]);
+
+ return this.format(
+ output || this.localeData().calendar(format, this, createLocal(now))
+ );
+}
+
+function clone() {
+ return new Moment(this);
+}
+
+function isAfter(input, units) {
+ var localInput = isMoment(input) ? input : createLocal(input);
+ if (!(this.isValid() && localInput.isValid())) {
+ return false;
+ }
+ units = normalizeUnits(units) || 'millisecond';
+ if (units === 'millisecond') {
+ return this.valueOf() > localInput.valueOf();
+ } else {
+ return localInput.valueOf() < this.clone().startOf(units).valueOf();
+ }
+}
+
+function isBefore(input, units) {
+ var localInput = isMoment(input) ? input : createLocal(input);
+ if (!(this.isValid() && localInput.isValid())) {
+ return false;
+ }
+ units = normalizeUnits(units) || 'millisecond';
+ if (units === 'millisecond') {
+ return this.valueOf() < localInput.valueOf();
+ } else {
+ return this.clone().endOf(units).valueOf() < localInput.valueOf();
+ }
+}
+
+function isBetween(from, to, units, inclusivity) {
+ var localFrom = isMoment(from) ? from : createLocal(from),
+ localTo = isMoment(to) ? to : createLocal(to);
+ if (!(this.isValid() && localFrom.isValid() && localTo.isValid())) {
+ return false;
+ }
+ inclusivity = inclusivity || '()';
+ return (
+ (inclusivity[0] === '('
+ ? this.isAfter(localFrom, units)
+ : !this.isBefore(localFrom, units)) &&
+ (inclusivity[1] === ')'
+ ? this.isBefore(localTo, units)
+ : !this.isAfter(localTo, units))
+ );
+}
+
+function isSame(input, units) {
+ var localInput = isMoment(input) ? input : createLocal(input),
+ inputMs;
+ if (!(this.isValid() && localInput.isValid())) {
+ return false;
+ }
+ units = normalizeUnits(units) || 'millisecond';
+ if (units === 'millisecond') {
+ return this.valueOf() === localInput.valueOf();
+ } else {
+ inputMs = localInput.valueOf();
+ return (
+ this.clone().startOf(units).valueOf() <= inputMs &&
+ inputMs <= this.clone().endOf(units).valueOf()
+ );
+ }
+}
+
+function isSameOrAfter(input, units) {
+ return this.isSame(input, units) || this.isAfter(input, units);
+}
+
+function isSameOrBefore(input, units) {
+ return this.isSame(input, units) || this.isBefore(input, units);
+}
+
+function diff(input, units, asFloat) {
+ var that, zoneDelta, output;
+
+ if (!this.isValid()) {
+ return NaN;
+ }
+
+ that = cloneWithOffset(input, this);
+
+ if (!that.isValid()) {
+ return NaN;
+ }
+
+ zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4;
+
+ units = normalizeUnits(units);
+
+ switch (units) {
+ case 'year':
+ output = monthDiff(this, that) / 12;
+ break;
+ case 'month':
+ output = monthDiff(this, that);
+ break;
+ case 'quarter':
+ output = monthDiff(this, that) / 3;
+ break;
+ case 'second':
+ output = (this - that) / 1e3;
+ break; // 1000
+ case 'minute':
+ output = (this - that) / 6e4;
+ break; // 1000 * 60
+ case 'hour':
+ output = (this - that) / 36e5;
+ break; // 1000 * 60 * 60
+ case 'day':
+ output = (this - that - zoneDelta) / 864e5;
+ break; // 1000 * 60 * 60 * 24, negate dst
+ case 'week':
+ output = (this - that - zoneDelta) / 6048e5;
+ break; // 1000 * 60 * 60 * 24 * 7, negate dst
+ default:
+ output = this - that;
+ }
+
+ return asFloat ? output : absFloor(output);
+}
+
+function monthDiff(a, b) {
+ if (a.date() < b.date()) {
+ // end-of-month calculations work correct when the start month has more
+ // days than the end month.
+ return -monthDiff(b, a);
+ }
+ // difference in months
+ var wholeMonthDiff = (b.year() - a.year()) * 12 + (b.month() - a.month()),
+ // b is in (anchor - 1 month, anchor + 1 month)
+ anchor = a.clone().add(wholeMonthDiff, 'months'),
+ anchor2,
+ adjust;
+
+ if (b - anchor < 0) {
+ anchor2 = a.clone().add(wholeMonthDiff - 1, 'months');
+ // linear across the month
+ adjust = (b - anchor) / (anchor - anchor2);
+ } else {
+ anchor2 = a.clone().add(wholeMonthDiff + 1, 'months');
+ // linear across the month
+ adjust = (b - anchor) / (anchor2 - anchor);
+ }
+
+ //check for negative zero, return zero if negative zero
+ return -(wholeMonthDiff + adjust) || 0;
+}
+
+hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ';
+hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]';
+
+function toString() {
+ return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ');
+}
+
+function toISOString(keepOffset) {
+ if (!this.isValid()) {
+ return null;
+ }
+ var utc = keepOffset !== true,
+ m = utc ? this.clone().utc() : this;
+ if (m.year() < 0 || m.year() > 9999) {
+ return formatMoment(
+ m,
+ utc
+ ? 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'
+ : 'YYYYYY-MM-DD[T]HH:mm:ss.SSSZ'
+ );
+ }
+ if (isFunction(Date.prototype.toISOString)) {
+ // native implementation is ~50x faster, use it when we can
+ if (utc) {
+ return this.toDate().toISOString();
+ } else {
+ return new Date(this.valueOf() + this.utcOffset() * 60 * 1000)
+ .toISOString()
+ .replace('Z', formatMoment(m, 'Z'));
+ }
+ }
+ return formatMoment(
+ m,
+ utc ? 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' : 'YYYY-MM-DD[T]HH:mm:ss.SSSZ'
+ );
+}
+
+/**
+ * Return a human readable representation of a moment that can
+ * also be evaluated to get a new moment which is the same
+ *
+ * @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects
+ */
+function inspect() {
+ if (!this.isValid()) {
+ return 'moment.invalid(/* ' + this._i + ' */)';
+ }
+ var func = 'moment',
+ zone = '',
+ prefix,
+ year,
+ datetime,
+ suffix;
+ if (!this.isLocal()) {
+ func = this.utcOffset() === 0 ? 'moment.utc' : 'moment.parseZone';
+ zone = 'Z';
+ }
+ prefix = '[' + func + '("]';
+ year = 0 <= this.year() && this.year() <= 9999 ? 'YYYY' : 'YYYYYY';
+ datetime = '-MM-DD[T]HH:mm:ss.SSS';
+ suffix = zone + '[")]';
+
+ return this.format(prefix + year + datetime + suffix);
+}
+
+function format(inputString) {
+ if (!inputString) {
+ inputString = this.isUtc()
+ ? hooks.defaultFormatUtc
+ : hooks.defaultFormat;
+ }
+ var output = formatMoment(this, inputString);
+ return this.localeData().postformat(output);
+}
+
+function from(time, withoutSuffix) {
+ if (
+ this.isValid() &&
+ ((isMoment(time) && time.isValid()) || createLocal(time).isValid())
+ ) {
+ return createDuration({ to: this, from: time })
+ .locale(this.locale())
+ .humanize(!withoutSuffix);
+ } else {
+ return this.localeData().invalidDate();
+ }
+}
+
+function fromNow(withoutSuffix) {
+ return this.from(createLocal(), withoutSuffix);
+}
+
+function to(time, withoutSuffix) {
+ if (
+ this.isValid() &&
+ ((isMoment(time) && time.isValid()) || createLocal(time).isValid())
+ ) {
+ return createDuration({ from: this, to: time })
+ .locale(this.locale())
+ .humanize(!withoutSuffix);
+ } else {
+ return this.localeData().invalidDate();
+ }
+}
+
+function toNow(withoutSuffix) {
+ return this.to(createLocal(), withoutSuffix);
+}
+
+// If passed a locale key, it will set the locale for this
+// instance. Otherwise, it will return the locale configuration
+// variables for this instance.
+function locale(key) {
+ var newLocaleData;
+
+ if (key === undefined) {
+ return this._locale._abbr;
+ } else {
+ newLocaleData = getLocale(key);
+ if (newLocaleData != null) {
+ this._locale = newLocaleData;
+ }
+ return this;
+ }
+}
+
+var lang = deprecate(
+ 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.',
+ function (key) {
+ if (key === undefined) {
+ return this.localeData();
+ } else {
+ return this.locale(key);
+ }
+ }
+);
+
+function localeData() {
+ return this._locale;
+}
+
+var MS_PER_SECOND = 1000,
+ MS_PER_MINUTE = 60 * MS_PER_SECOND,
+ MS_PER_HOUR = 60 * MS_PER_MINUTE,
+ MS_PER_400_YEARS = (365 * 400 + 97) * 24 * MS_PER_HOUR;
+
+// actual modulo - handles negative numbers (for dates before 1970):
+function mod$1(dividend, divisor) {
+ return ((dividend % divisor) + divisor) % divisor;
+}
+
+function localStartOfDate(y, m, d) {
+ // the date constructor remaps years 0-99 to 1900-1999
+ if (y < 100 && y >= 0) {
+ // preserve leap years using a full 400 year cycle, then reset
+ return new Date(y + 400, m, d) - MS_PER_400_YEARS;
+ } else {
+ return new Date(y, m, d).valueOf();
+ }
+}
+
+function utcStartOfDate(y, m, d) {
+ // Date.UTC remaps years 0-99 to 1900-1999
+ if (y < 100 && y >= 0) {
+ // preserve leap years using a full 400 year cycle, then reset
+ return Date.UTC(y + 400, m, d) - MS_PER_400_YEARS;
+ } else {
+ return Date.UTC(y, m, d);
+ }
+}
+
+function startOf(units) {
+ var time, startOfDate;
+ units = normalizeUnits(units);
+ if (units === undefined || units === 'millisecond' || !this.isValid()) {
+ return this;
+ }
+
+ startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate;
+
+ switch (units) {
+ case 'year':
+ time = startOfDate(this.year(), 0, 1);
+ break;
+ case 'quarter':
+ time = startOfDate(
+ this.year(),
+ this.month() - (this.month() % 3),
+ 1
+ );
+ break;
+ case 'month':
+ time = startOfDate(this.year(), this.month(), 1);
+ break;
+ case 'week':
+ time = startOfDate(
+ this.year(),
+ this.month(),
+ this.date() - this.weekday()
+ );
+ break;
+ case 'isoWeek':
+ time = startOfDate(
+ this.year(),
+ this.month(),
+ this.date() - (this.isoWeekday() - 1)
+ );
+ break;
+ case 'day':
+ case 'date':
+ time = startOfDate(this.year(), this.month(), this.date());
+ break;
+ case 'hour':
+ time = this._d.valueOf();
+ time -= mod$1(
+ time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE),
+ MS_PER_HOUR
+ );
+ break;
+ case 'minute':
+ time = this._d.valueOf();
+ time -= mod$1(time, MS_PER_MINUTE);
+ break;
+ case 'second':
+ time = this._d.valueOf();
+ time -= mod$1(time, MS_PER_SECOND);
+ break;
+ }
+
+ this._d.setTime(time);
+ hooks.updateOffset(this, true);
+ return this;
+}
+
+function endOf(units) {
+ var time, startOfDate;
+ units = normalizeUnits(units);
+ if (units === undefined || units === 'millisecond' || !this.isValid()) {
+ return this;
+ }
+
+ startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate;
+
+ switch (units) {
+ case 'year':
+ time = startOfDate(this.year() + 1, 0, 1) - 1;
+ break;
+ case 'quarter':
+ time =
+ startOfDate(
+ this.year(),
+ this.month() - (this.month() % 3) + 3,
+ 1
+ ) - 1;
+ break;
+ case 'month':
+ time = startOfDate(this.year(), this.month() + 1, 1) - 1;
+ break;
+ case 'week':
+ time =
+ startOfDate(
+ this.year(),
+ this.month(),
+ this.date() - this.weekday() + 7
+ ) - 1;
+ break;
+ case 'isoWeek':
+ time =
+ startOfDate(
+ this.year(),
+ this.month(),
+ this.date() - (this.isoWeekday() - 1) + 7
+ ) - 1;
+ break;
+ case 'day':
+ case 'date':
+ time = startOfDate(this.year(), this.month(), this.date() + 1) - 1;
+ break;
+ case 'hour':
+ time = this._d.valueOf();
+ time +=
+ MS_PER_HOUR -
+ mod$1(
+ time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE),
+ MS_PER_HOUR
+ ) -
+ 1;
+ break;
+ case 'minute':
+ time = this._d.valueOf();
+ time += MS_PER_MINUTE - mod$1(time, MS_PER_MINUTE) - 1;
+ break;
+ case 'second':
+ time = this._d.valueOf();
+ time += MS_PER_SECOND - mod$1(time, MS_PER_SECOND) - 1;
+ break;
+ }
+
+ this._d.setTime(time);
+ hooks.updateOffset(this, true);
+ return this;
+}
+
+function valueOf() {
+ return this._d.valueOf() - (this._offset || 0) * 60000;
+}
+
+function unix() {
+ return Math.floor(this.valueOf() / 1000);
+}
+
+function toDate() {
+ return new Date(this.valueOf());
+}
+
+function toArray() {
+ var m = this;
+ return [
+ m.year(),
+ m.month(),
+ m.date(),
+ m.hour(),
+ m.minute(),
+ m.second(),
+ m.millisecond(),
+ ];
+}
+
+function toObject() {
+ var m = this;
+ return {
+ years: m.year(),
+ months: m.month(),
+ date: m.date(),
+ hours: m.hours(),
+ minutes: m.minutes(),
+ seconds: m.seconds(),
+ milliseconds: m.milliseconds(),
+ };
+}
+
+function toJSON() {
+ // new Date(NaN).toJSON() === null
+ return this.isValid() ? this.toISOString() : null;
+}
+
+function isValid$2() {
+ return isValid(this);
+}
+
+function parsingFlags() {
+ return extend({}, getParsingFlags(this));
+}
+
+function invalidAt() {
+ return getParsingFlags(this).overflow;
+}
+
+function creationData() {
+ return {
+ input: this._i,
+ format: this._f,
+ locale: this._locale,
+ isUTC: this._isUTC,
+ strict: this._strict,
+ };
+}
+
+addFormatToken('N', 0, 0, 'eraAbbr');
+addFormatToken('NN', 0, 0, 'eraAbbr');
+addFormatToken('NNN', 0, 0, 'eraAbbr');
+addFormatToken('NNNN', 0, 0, 'eraName');
+addFormatToken('NNNNN', 0, 0, 'eraNarrow');
+
+addFormatToken('y', ['y', 1], 'yo', 'eraYear');
+addFormatToken('y', ['yy', 2], 0, 'eraYear');
+addFormatToken('y', ['yyy', 3], 0, 'eraYear');
+addFormatToken('y', ['yyyy', 4], 0, 'eraYear');
+
+addRegexToken('N', matchEraAbbr);
+addRegexToken('NN', matchEraAbbr);
+addRegexToken('NNN', matchEraAbbr);
+addRegexToken('NNNN', matchEraName);
+addRegexToken('NNNNN', matchEraNarrow);
+
+addParseToken(
+ ['N', 'NN', 'NNN', 'NNNN', 'NNNNN'],
+ function (input, array, config, token) {
+ var era = config._locale.erasParse(input, token, config._strict);
+ if (era) {
+ getParsingFlags(config).era = era;
+ } else {
+ getParsingFlags(config).invalidEra = input;
+ }
+ }
+);
+
+addRegexToken('y', matchUnsigned);
+addRegexToken('yy', matchUnsigned);
+addRegexToken('yyy', matchUnsigned);
+addRegexToken('yyyy', matchUnsigned);
+addRegexToken('yo', matchEraYearOrdinal);
+
+addParseToken(['y', 'yy', 'yyy', 'yyyy'], YEAR);
+addParseToken(['yo'], function (input, array, config, token) {
+ var match;
+ if (config._locale._eraYearOrdinalRegex) {
+ match = input.match(config._locale._eraYearOrdinalRegex);
+ }
+
+ if (config._locale.eraYearOrdinalParse) {
+ array[YEAR] = config._locale.eraYearOrdinalParse(input, match);
+ } else {
+ array[YEAR] = parseInt(input, 10);
+ }
+});
+
+function localeEras(m, format) {
+ var i,
+ l,
+ date,
+ eras = this._eras || getLocale('en')._eras;
+ for (i = 0, l = eras.length; i < l; ++i) {
+ switch (typeof eras[i].since) {
+ case 'string':
+ // truncate time
+ date = hooks(eras[i].since).startOf('day');
+ eras[i].since = date.valueOf();
+ break;
+ }
+
+ switch (typeof eras[i].until) {
+ case 'undefined':
+ eras[i].until = +Infinity;
+ break;
+ case 'string':
+ // truncate time
+ date = hooks(eras[i].until).startOf('day').valueOf();
+ eras[i].until = date.valueOf();
+ break;
+ }
+ }
+ return eras;
+}
+
+function localeErasParse(eraName, format, strict) {
+ var i,
+ l,
+ eras = this.eras(),
+ name,
+ abbr,
+ narrow;
+ eraName = eraName.toUpperCase();
+
+ for (i = 0, l = eras.length; i < l; ++i) {
+ name = eras[i].name.toUpperCase();
+ abbr = eras[i].abbr.toUpperCase();
+ narrow = eras[i].narrow.toUpperCase();
+
+ if (strict) {
+ switch (format) {
+ case 'N':
+ case 'NN':
+ case 'NNN':
+ if (abbr === eraName) {
+ return eras[i];
+ }
+ break;
+
+ case 'NNNN':
+ if (name === eraName) {
+ return eras[i];
+ }
+ break;
+
+ case 'NNNNN':
+ if (narrow === eraName) {
+ return eras[i];
+ }
+ break;
+ }
+ } else if ([name, abbr, narrow].indexOf(eraName) >= 0) {
+ return eras[i];
+ }
+ }
+}
+
+function localeErasConvertYear(era, year) {
+ var dir = era.since <= era.until ? +1 : -1;
+ if (year === undefined) {
+ return hooks(era.since).year();
+ } else {
+ return hooks(era.since).year() + (year - era.offset) * dir;
+ }
+}
+
+function getEraName() {
+ var i,
+ l,
+ val,
+ eras = this.localeData().eras();
+ for (i = 0, l = eras.length; i < l; ++i) {
+ // truncate time
+ val = this.clone().startOf('day').valueOf();
+
+ if (eras[i].since <= val && val <= eras[i].until) {
+ return eras[i].name;
+ }
+ if (eras[i].until <= val && val <= eras[i].since) {
+ return eras[i].name;
+ }
+ }
+
+ return '';
+}
+
+function getEraNarrow() {
+ var i,
+ l,
+ val,
+ eras = this.localeData().eras();
+ for (i = 0, l = eras.length; i < l; ++i) {
+ // truncate time
+ val = this.clone().startOf('day').valueOf();
+
+ if (eras[i].since <= val && val <= eras[i].until) {
+ return eras[i].narrow;
+ }
+ if (eras[i].until <= val && val <= eras[i].since) {
+ return eras[i].narrow;
+ }
+ }
+
+ return '';
+}
+
+function getEraAbbr() {
+ var i,
+ l,
+ val,
+ eras = this.localeData().eras();
+ for (i = 0, l = eras.length; i < l; ++i) {
+ // truncate time
+ val = this.clone().startOf('day').valueOf();
+
+ if (eras[i].since <= val && val <= eras[i].until) {
+ return eras[i].abbr;
+ }
+ if (eras[i].until <= val && val <= eras[i].since) {
+ return eras[i].abbr;
+ }
+ }
+
+ return '';
+}
+
+function getEraYear() {
+ var i,
+ l,
+ dir,
+ val,
+ eras = this.localeData().eras();
+ for (i = 0, l = eras.length; i < l; ++i) {
+ dir = eras[i].since <= eras[i].until ? +1 : -1;
+
+ // truncate time
+ val = this.clone().startOf('day').valueOf();
+
+ if (
+ (eras[i].since <= val && val <= eras[i].until) ||
+ (eras[i].until <= val && val <= eras[i].since)
+ ) {
+ return (
+ (this.year() - hooks(eras[i].since).year()) * dir +
+ eras[i].offset
+ );
+ }
+ }
+
+ return this.year();
+}
+
+function erasNameRegex(isStrict) {
+ if (!hasOwnProp(this, '_erasNameRegex')) {
+ computeErasParse.call(this);
+ }
+ return isStrict ? this._erasNameRegex : this._erasRegex;
+}
+
+function erasAbbrRegex(isStrict) {
+ if (!hasOwnProp(this, '_erasAbbrRegex')) {
+ computeErasParse.call(this);
+ }
+ return isStrict ? this._erasAbbrRegex : this._erasRegex;
+}
+
+function erasNarrowRegex(isStrict) {
+ if (!hasOwnProp(this, '_erasNarrowRegex')) {
+ computeErasParse.call(this);
+ }
+ return isStrict ? this._erasNarrowRegex : this._erasRegex;
+}
+
+function matchEraAbbr(isStrict, locale) {
+ return locale.erasAbbrRegex(isStrict);
+}
+
+function matchEraName(isStrict, locale) {
+ return locale.erasNameRegex(isStrict);
+}
+
+function matchEraNarrow(isStrict, locale) {
+ return locale.erasNarrowRegex(isStrict);
+}
+
+function matchEraYearOrdinal(isStrict, locale) {
+ return locale._eraYearOrdinalRegex || matchUnsigned;
+}
+
+function computeErasParse() {
+ var abbrPieces = [],
+ namePieces = [],
+ narrowPieces = [],
+ mixedPieces = [],
+ i,
+ l,
+ erasName,
+ erasAbbr,
+ erasNarrow,
+ eras = this.eras();
+
+ for (i = 0, l = eras.length; i < l; ++i) {
+ erasName = regexEscape(eras[i].name);
+ erasAbbr = regexEscape(eras[i].abbr);
+ erasNarrow = regexEscape(eras[i].narrow);
+
+ namePieces.push(erasName);
+ abbrPieces.push(erasAbbr);
+ narrowPieces.push(erasNarrow);
+ mixedPieces.push(erasName);
+ mixedPieces.push(erasAbbr);
+ mixedPieces.push(erasNarrow);
+ }
+
+ this._erasRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i');
+ this._erasNameRegex = new RegExp('^(' + namePieces.join('|') + ')', 'i');
+ this._erasAbbrRegex = new RegExp('^(' + abbrPieces.join('|') + ')', 'i');
+ this._erasNarrowRegex = new RegExp(
+ '^(' + narrowPieces.join('|') + ')',
+ 'i'
+ );
+}
+
+// FORMATTING
+
+addFormatToken(0, ['gg', 2], 0, function () {
+ return this.weekYear() % 100;
+});
+
+addFormatToken(0, ['GG', 2], 0, function () {
+ return this.isoWeekYear() % 100;
+});
+
+function addWeekYearFormatToken(token, getter) {
+ addFormatToken(0, [token, token.length], 0, getter);
+}
+
+addWeekYearFormatToken('gggg', 'weekYear');
+addWeekYearFormatToken('ggggg', 'weekYear');
+addWeekYearFormatToken('GGGG', 'isoWeekYear');
+addWeekYearFormatToken('GGGGG', 'isoWeekYear');
+
+// ALIASES
+
+// PARSING
+
+addRegexToken('G', matchSigned);
+addRegexToken('g', matchSigned);
+addRegexToken('GG', match1to2, match2);
+addRegexToken('gg', match1to2, match2);
+addRegexToken('GGGG', match1to4, match4);
+addRegexToken('gggg', match1to4, match4);
+addRegexToken('GGGGG', match1to6, match6);
+addRegexToken('ggggg', match1to6, match6);
+
+addWeekParseToken(
+ ['gggg', 'ggggg', 'GGGG', 'GGGGG'],
+ function (input, week, config, token) {
+ week[token.substr(0, 2)] = toInt(input);
+ }
+);
+
+addWeekParseToken(['gg', 'GG'], function (input, week, config, token) {
+ week[token] = hooks.parseTwoDigitYear(input);
+});
+
+// MOMENTS
+
+function getSetWeekYear(input) {
+ return getSetWeekYearHelper.call(
+ this,
+ input,
+ this.week(),
+ this.weekday() + this.localeData()._week.dow,
+ this.localeData()._week.dow,
+ this.localeData()._week.doy
+ );
+}
+
+function getSetISOWeekYear(input) {
+ return getSetWeekYearHelper.call(
+ this,
+ input,
+ this.isoWeek(),
+ this.isoWeekday(),
+ 1,
+ 4
+ );
+}
+
+function getISOWeeksInYear() {
+ return weeksInYear(this.year(), 1, 4);
+}
+
+function getISOWeeksInISOWeekYear() {
+ return weeksInYear(this.isoWeekYear(), 1, 4);
+}
+
+function getWeeksInYear() {
+ var weekInfo = this.localeData()._week;
+ return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
+}
+
+function getWeeksInWeekYear() {
+ var weekInfo = this.localeData()._week;
+ return weeksInYear(this.weekYear(), weekInfo.dow, weekInfo.doy);
+}
+
+function getSetWeekYearHelper(input, week, weekday, dow, doy) {
+ var weeksTarget;
+ if (input == null) {
+ return weekOfYear(this, dow, doy).year;
+ } else {
+ weeksTarget = weeksInYear(input, dow, doy);
+ if (week > weeksTarget) {
+ week = weeksTarget;
+ }
+ return setWeekAll.call(this, input, week, weekday, dow, doy);
+ }
+}
+
+function setWeekAll(weekYear, week, weekday, dow, doy) {
+ var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy),
+ date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear);
+
+ this.year(date.getUTCFullYear());
+ this.month(date.getUTCMonth());
+ this.date(date.getUTCDate());
+ return this;
+}
+
+// FORMATTING
+
+addFormatToken('Q', 0, 'Qo', 'quarter');
+
+// PARSING
+
+addRegexToken('Q', match1);
+addParseToken('Q', function (input, array) {
+ array[MONTH] = (toInt(input) - 1) * 3;
+});
+
+// MOMENTS
+
+function getSetQuarter(input) {
+ return input == null
+ ? Math.ceil((this.month() + 1) / 3)
+ : this.month((input - 1) * 3 + (this.month() % 3));
+}
+
+// FORMATTING
+
+addFormatToken('D', ['DD', 2], 'Do', 'date');
+
+// PARSING
+
+addRegexToken('D', match1to2, match1to2NoLeadingZero);
+addRegexToken('DD', match1to2, match2);
+addRegexToken('Do', function (isStrict, locale) {
+ // TODO: Remove "ordinalParse" fallback in next major release.
+ return isStrict
+ ? locale._dayOfMonthOrdinalParse || locale._ordinalParse
+ : locale._dayOfMonthOrdinalParseLenient;
+});
+
+addParseToken(['D', 'DD'], DATE);
+addParseToken('Do', function (input, array) {
+ array[DATE] = toInt(input.match(match1to2)[0]);
+});
+
+// MOMENTS
+
+var getSetDayOfMonth = makeGetSet('Date', true);
+
+// FORMATTING
+
+addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear');
+
+// PARSING
+
+addRegexToken('DDD', match1to3);
+addRegexToken('DDDD', match3);
+addParseToken(['DDD', 'DDDD'], function (input, array, config) {
+ config._dayOfYear = toInt(input);
+});
+
+// HELPERS
+
+// MOMENTS
+
+function getSetDayOfYear(input) {
+ var dayOfYear =
+ Math.round(
+ (this.clone().startOf('day') - this.clone().startOf('year')) / 864e5
+ ) + 1;
+ return input == null ? dayOfYear : this.add(input - dayOfYear, 'd');
+}
+
+// FORMATTING
+
+addFormatToken('m', ['mm', 2], 0, 'minute');
+
+// PARSING
+
+addRegexToken('m', match1to2, match1to2HasZero);
+addRegexToken('mm', match1to2, match2);
+addParseToken(['m', 'mm'], MINUTE);
+
+// MOMENTS
+
+var getSetMinute = makeGetSet('Minutes', false);
+
+// FORMATTING
+
+addFormatToken('s', ['ss', 2], 0, 'second');
+
+// PARSING
+
+addRegexToken('s', match1to2, match1to2HasZero);
+addRegexToken('ss', match1to2, match2);
+addParseToken(['s', 'ss'], SECOND);
+
+// MOMENTS
+
+var getSetSecond = makeGetSet('Seconds', false);
+
+// FORMATTING
+
+addFormatToken('S', 0, 0, function () {
+ return ~~(this.millisecond() / 100);
+});
+
+addFormatToken(0, ['SS', 2], 0, function () {
+ return ~~(this.millisecond() / 10);
+});
+
+addFormatToken(0, ['SSS', 3], 0, 'millisecond');
+addFormatToken(0, ['SSSS', 4], 0, function () {
+ return this.millisecond() * 10;
+});
+addFormatToken(0, ['SSSSS', 5], 0, function () {
+ return this.millisecond() * 100;
+});
+addFormatToken(0, ['SSSSSS', 6], 0, function () {
+ return this.millisecond() * 1000;
+});
+addFormatToken(0, ['SSSSSSS', 7], 0, function () {
+ return this.millisecond() * 10000;
+});
+addFormatToken(0, ['SSSSSSSS', 8], 0, function () {
+ return this.millisecond() * 100000;
+});
+addFormatToken(0, ['SSSSSSSSS', 9], 0, function () {
+ return this.millisecond() * 1000000;
+});
+
+// PARSING
+
+addRegexToken('S', match1to3, match1);
+addRegexToken('SS', match1to3, match2);
+addRegexToken('SSS', match1to3, match3);
+
+var token, getSetMillisecond;
+for (token = 'SSSS'; token.length <= 9; token += 'S') {
+ addRegexToken(token, matchUnsigned);
+}
+
+function parseMs(input, array) {
+ array[MILLISECOND] = toInt(('0.' + input) * 1000);
+}
+
+for (token = 'S'; token.length <= 9; token += 'S') {
+ addParseToken(token, parseMs);
+}
+
+getSetMillisecond = makeGetSet('Milliseconds', false);
+
+// FORMATTING
+
+addFormatToken('z', 0, 0, 'zoneAbbr');
+addFormatToken('zz', 0, 0, 'zoneName');
+
+// MOMENTS
+
+function getZoneAbbr() {
+ return this._isUTC ? 'UTC' : '';
+}
+
+function getZoneName() {
+ return this._isUTC ? 'Coordinated Universal Time' : '';
+}
+
+var proto = Moment.prototype;
+
+proto.add = add;
+proto.calendar = calendar$1;
+proto.clone = clone;
+proto.diff = diff;
+proto.endOf = endOf;
+proto.format = format;
+proto.from = from;
+proto.fromNow = fromNow;
+proto.to = to;
+proto.toNow = toNow;
+proto.get = stringGet;
+proto.invalidAt = invalidAt;
+proto.isAfter = isAfter;
+proto.isBefore = isBefore;
+proto.isBetween = isBetween;
+proto.isSame = isSame;
+proto.isSameOrAfter = isSameOrAfter;
+proto.isSameOrBefore = isSameOrBefore;
+proto.isValid = isValid$2;
+proto.lang = lang;
+proto.locale = locale;
+proto.localeData = localeData;
+proto.max = prototypeMax;
+proto.min = prototypeMin;
+proto.parsingFlags = parsingFlags;
+proto.set = stringSet;
+proto.startOf = startOf;
+proto.subtract = subtract;
+proto.toArray = toArray;
+proto.toObject = toObject;
+proto.toDate = toDate;
+proto.toISOString = toISOString;
+proto.inspect = inspect;
+if (typeof Symbol !== 'undefined' && Symbol.for != null) {
+ proto[Symbol.for('nodejs.util.inspect.custom')] = function () {
+ return 'Moment<' + this.format() + '>';
+ };
+}
+proto.toJSON = toJSON;
+proto.toString = toString;
+proto.unix = unix;
+proto.valueOf = valueOf;
+proto.creationData = creationData;
+proto.eraName = getEraName;
+proto.eraNarrow = getEraNarrow;
+proto.eraAbbr = getEraAbbr;
+proto.eraYear = getEraYear;
+proto.year = getSetYear;
+proto.isLeapYear = getIsLeapYear;
+proto.weekYear = getSetWeekYear;
+proto.isoWeekYear = getSetISOWeekYear;
+proto.quarter = proto.quarters = getSetQuarter;
+proto.month = getSetMonth;
+proto.daysInMonth = getDaysInMonth;
+proto.week = proto.weeks = getSetWeek;
+proto.isoWeek = proto.isoWeeks = getSetISOWeek;
+proto.weeksInYear = getWeeksInYear;
+proto.weeksInWeekYear = getWeeksInWeekYear;
+proto.isoWeeksInYear = getISOWeeksInYear;
+proto.isoWeeksInISOWeekYear = getISOWeeksInISOWeekYear;
+proto.date = getSetDayOfMonth;
+proto.day = proto.days = getSetDayOfWeek;
+proto.weekday = getSetLocaleDayOfWeek;
+proto.isoWeekday = getSetISODayOfWeek;
+proto.dayOfYear = getSetDayOfYear;
+proto.hour = proto.hours = getSetHour;
+proto.minute = proto.minutes = getSetMinute;
+proto.second = proto.seconds = getSetSecond;
+proto.millisecond = proto.milliseconds = getSetMillisecond;
+proto.utcOffset = getSetOffset;
+proto.utc = setOffsetToUTC;
+proto.local = setOffsetToLocal;
+proto.parseZone = setOffsetToParsedOffset;
+proto.hasAlignedHourOffset = hasAlignedHourOffset;
+proto.isDST = isDaylightSavingTime;
+proto.isLocal = isLocal;
+proto.isUtcOffset = isUtcOffset;
+proto.isUtc = isUtc;
+proto.isUTC = isUtc;
+proto.zoneAbbr = getZoneAbbr;
+proto.zoneName = getZoneName;
+proto.dates = deprecate(
+ 'dates accessor is deprecated. Use date instead.',
+ getSetDayOfMonth
+);
+proto.months = deprecate(
+ 'months accessor is deprecated. Use month instead',
+ getSetMonth
+);
+proto.years = deprecate(
+ 'years accessor is deprecated. Use year instead',
+ getSetYear
+);
+proto.zone = deprecate(
+ 'moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/',
+ getSetZone
+);
+proto.isDSTShifted = deprecate(
+ 'isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information',
+ isDaylightSavingTimeShifted
+);
+
+function createUnix(input) {
+ return createLocal(input * 1000);
+}
+
+function createInZone() {
+ return createLocal.apply(null, arguments).parseZone();
+}
+
+function preParsePostFormat(string) {
+ return string;
+}
+
+var proto$1 = Locale.prototype;
+
+proto$1.calendar = calendar;
+proto$1.longDateFormat = longDateFormat;
+proto$1.invalidDate = invalidDate;
+proto$1.ordinal = ordinal;
+proto$1.preparse = preParsePostFormat;
+proto$1.postformat = preParsePostFormat;
+proto$1.relativeTime = relativeTime;
+proto$1.pastFuture = pastFuture;
+proto$1.set = set;
+proto$1.eras = localeEras;
+proto$1.erasParse = localeErasParse;
+proto$1.erasConvertYear = localeErasConvertYear;
+proto$1.erasAbbrRegex = erasAbbrRegex;
+proto$1.erasNameRegex = erasNameRegex;
+proto$1.erasNarrowRegex = erasNarrowRegex;
+
+proto$1.months = localeMonths;
+proto$1.monthsShort = localeMonthsShort;
+proto$1.monthsParse = localeMonthsParse;
+proto$1.monthsRegex = monthsRegex;
+proto$1.monthsShortRegex = monthsShortRegex;
+proto$1.week = localeWeek;
+proto$1.firstDayOfYear = localeFirstDayOfYear;
+proto$1.firstDayOfWeek = localeFirstDayOfWeek;
+
+proto$1.weekdays = localeWeekdays;
+proto$1.weekdaysMin = localeWeekdaysMin;
+proto$1.weekdaysShort = localeWeekdaysShort;
+proto$1.weekdaysParse = localeWeekdaysParse;
+
+proto$1.weekdaysRegex = weekdaysRegex;
+proto$1.weekdaysShortRegex = weekdaysShortRegex;
+proto$1.weekdaysMinRegex = weekdaysMinRegex;
+
+proto$1.isPM = localeIsPM;
+proto$1.meridiem = localeMeridiem;
+
+function get$1(format, index, field, setter) {
+ var locale = getLocale(),
+ utc = createUTC().set(setter, index);
+ return locale[field](utc, format);
+}
+
+function listMonthsImpl(format, index, field) {
+ if (isNumber(format)) {
+ index = format;
+ format = undefined;
+ }
+
+ format = format || '';
+
+ if (index != null) {
+ return get$1(format, index, field, 'month');
+ }
+
+ var i,
+ out = [];
+ for (i = 0; i < 12; i++) {
+ out[i] = get$1(format, i, field, 'month');
+ }
+ return out;
+}
+
+// ()
+// (5)
+// (fmt, 5)
+// (fmt)
+// (true)
+// (true, 5)
+// (true, fmt, 5)
+// (true, fmt)
+function listWeekdaysImpl(localeSorted, format, index, field) {
+ if (typeof localeSorted === 'boolean') {
+ if (isNumber(format)) {
+ index = format;
+ format = undefined;
+ }
+
+ format = format || '';
+ } else {
+ format = localeSorted;
+ index = format;
+ localeSorted = false;
+
+ if (isNumber(format)) {
+ index = format;
+ format = undefined;
+ }
+
+ format = format || '';
+ }
+
+ var locale = getLocale(),
+ shift = localeSorted ? locale._week.dow : 0,
+ i,
+ out = [];
+
+ if (index != null) {
+ return get$1(format, (index + shift) % 7, field, 'day');
+ }
+
+ for (i = 0; i < 7; i++) {
+ out[i] = get$1(format, (i + shift) % 7, field, 'day');
+ }
+ return out;
+}
+
+function listMonths(format, index) {
+ return listMonthsImpl(format, index, 'months');
+}
+
+function listMonthsShort(format, index) {
+ return listMonthsImpl(format, index, 'monthsShort');
+}
+
+function listWeekdays(localeSorted, format, index) {
+ return listWeekdaysImpl(localeSorted, format, index, 'weekdays');
+}
+
+function listWeekdaysShort(localeSorted, format, index) {
+ return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort');
+}
+
+function listWeekdaysMin(localeSorted, format, index) {
+ return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin');
+}
+
+getSetGlobalLocale('en', {
+ eras: [
+ {
+ since: '0001-01-01',
+ until: +Infinity,
+ offset: 1,
+ name: 'Anno Domini',
+ narrow: 'AD',
+ abbr: 'AD',
+ },
+ {
+ since: '0000-12-31',
+ until: -Infinity,
+ offset: 1,
+ name: 'Before Christ',
+ narrow: 'BC',
+ abbr: 'BC',
+ },
+ ],
+ dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/,
+ ordinal: function (number) {
+ var b = number % 10,
+ output =
+ toInt((number % 100) / 10) === 1
+ ? 'th'
+ : b === 1
+ ? 'st'
+ : b === 2
+ ? 'nd'
+ : b === 3
+ ? 'rd'
+ : 'th';
+ return number + output;
+ },
+});
+
+// Side effect imports
+
+hooks.lang = deprecate(
+ 'moment.lang is deprecated. Use moment.locale instead.',
+ getSetGlobalLocale
+);
+hooks.langData = deprecate(
+ 'moment.langData is deprecated. Use moment.localeData instead.',
+ getLocale
+);
+
+var mathAbs = Math.abs;
+
+function abs() {
+ var data = this._data;
+
+ this._milliseconds = mathAbs(this._milliseconds);
+ this._days = mathAbs(this._days);
+ this._months = mathAbs(this._months);
+
+ data.milliseconds = mathAbs(data.milliseconds);
+ data.seconds = mathAbs(data.seconds);
+ data.minutes = mathAbs(data.minutes);
+ data.hours = mathAbs(data.hours);
+ data.months = mathAbs(data.months);
+ data.years = mathAbs(data.years);
+
+ return this;
+}
+
+function addSubtract$1(duration, input, value, direction) {
+ var other = createDuration(input, value);
+
+ duration._milliseconds += direction * other._milliseconds;
+ duration._days += direction * other._days;
+ duration._months += direction * other._months;
+
+ return duration._bubble();
+}
+
+// supports only 2.0-style add(1, 's') or add(duration)
+function add$1(input, value) {
+ return addSubtract$1(this, input, value, 1);
+}
+
+// supports only 2.0-style subtract(1, 's') or subtract(duration)
+function subtract$1(input, value) {
+ return addSubtract$1(this, input, value, -1);
+}
+
+function absCeil(number) {
+ if (number < 0) {
+ return Math.floor(number);
+ } else {
+ return Math.ceil(number);
+ }
+}
+
+function bubble() {
+ var milliseconds = this._milliseconds,
+ days = this._days,
+ months = this._months,
+ data = this._data,
+ seconds,
+ minutes,
+ hours,
+ years,
+ monthsFromDays;
+
+ // if we have a mix of positive and negative values, bubble down first
+ // check: https://github.com/moment/moment/issues/2166
+ if (
+ !(
+ (milliseconds >= 0 && days >= 0 && months >= 0) ||
+ (milliseconds <= 0 && days <= 0 && months <= 0)
+ )
+ ) {
+ milliseconds += absCeil(monthsToDays(months) + days) * 864e5;
+ days = 0;
+ months = 0;
+ }
+
+ // The following code bubbles up values, see the tests for
+ // examples of what that means.
+ data.milliseconds = milliseconds % 1000;
+
+ seconds = absFloor(milliseconds / 1000);
+ data.seconds = seconds % 60;
+
+ minutes = absFloor(seconds / 60);
+ data.minutes = minutes % 60;
+
+ hours = absFloor(minutes / 60);
+ data.hours = hours % 24;
+
+ days += absFloor(hours / 24);
+
+ // convert days to months
+ monthsFromDays = absFloor(daysToMonths(days));
+ months += monthsFromDays;
+ days -= absCeil(monthsToDays(monthsFromDays));
+
+ // 12 months -> 1 year
+ years = absFloor(months / 12);
+ months %= 12;
+
+ data.days = days;
+ data.months = months;
+ data.years = years;
+
+ return this;
+}
+
+function daysToMonths(days) {
+ // 400 years have 146097 days (taking into account leap year rules)
+ // 400 years have 12 months === 4800
+ return (days * 4800) / 146097;
+}
+
+function monthsToDays(months) {
+ // the reverse of daysToMonths
+ return (months * 146097) / 4800;
+}
+
+function as(units) {
+ if (!this.isValid()) {
+ return NaN;
+ }
+ var days,
+ months,
+ milliseconds = this._milliseconds;
+
+ units = normalizeUnits(units);
+
+ if (units === 'month' || units === 'quarter' || units === 'year') {
+ days = this._days + milliseconds / 864e5;
+ months = this._months + daysToMonths(days);
+ switch (units) {
+ case 'month':
+ return months;
+ case 'quarter':
+ return months / 3;
+ case 'year':
+ return months / 12;
+ }
+ } else {
+ // handle milliseconds separately because of floating point math errors (issue #1867)
+ days = this._days + Math.round(monthsToDays(this._months));
+ switch (units) {
+ case 'week':
+ return days / 7 + milliseconds / 6048e5;
+ case 'day':
+ return days + milliseconds / 864e5;
+ case 'hour':
+ return days * 24 + milliseconds / 36e5;
+ case 'minute':
+ return days * 1440 + milliseconds / 6e4;
+ case 'second':
+ return days * 86400 + milliseconds / 1000;
+ // Math.floor prevents floating point math errors here
+ case 'millisecond':
+ return Math.floor(days * 864e5) + milliseconds;
+ default:
+ throw new Error('Unknown unit ' + units);
+ }
+ }
+}
+
+function makeAs(alias) {
+ return function () {
+ return this.as(alias);
+ };
+}
+
+var asMilliseconds = makeAs('ms'),
+ asSeconds = makeAs('s'),
+ asMinutes = makeAs('m'),
+ asHours = makeAs('h'),
+ asDays = makeAs('d'),
+ asWeeks = makeAs('w'),
+ asMonths = makeAs('M'),
+ asQuarters = makeAs('Q'),
+ asYears = makeAs('y'),
+ valueOf$1 = asMilliseconds;
+
+function clone$1() {
+ return createDuration(this);
+}
+
+function get$2(units) {
+ units = normalizeUnits(units);
+ return this.isValid() ? this[units + 's']() : NaN;
+}
+
+function makeGetter(name) {
+ return function () {
+ return this.isValid() ? this._data[name] : NaN;
+ };
+}
+
+var milliseconds = makeGetter('milliseconds'),
+ seconds = makeGetter('seconds'),
+ minutes = makeGetter('minutes'),
+ hours = makeGetter('hours'),
+ days = makeGetter('days'),
+ months = makeGetter('months'),
+ years = makeGetter('years');
+
+function weeks() {
+ return absFloor(this.days() / 7);
+}
+
+var round = Math.round,
+ thresholds = {
+ ss: 44, // a few seconds to seconds
+ s: 45, // seconds to minute
+ m: 45, // minutes to hour
+ h: 22, // hours to day
+ d: 26, // days to month/week
+ w: null, // weeks to month
+ M: 11, // months to year
+ };
+
+// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) {
+ return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
+}
+
+function relativeTime$1(posNegDuration, withoutSuffix, thresholds, locale) {
+ var duration = createDuration(posNegDuration).abs(),
+ seconds = round(duration.as('s')),
+ minutes = round(duration.as('m')),
+ hours = round(duration.as('h')),
+ days = round(duration.as('d')),
+ months = round(duration.as('M')),
+ weeks = round(duration.as('w')),
+ years = round(duration.as('y')),
+ a =
+ (seconds <= thresholds.ss && ['s', seconds]) ||
+ (seconds < thresholds.s && ['ss', seconds]) ||
+ (minutes <= 1 && ['m']) ||
+ (minutes < thresholds.m && ['mm', minutes]) ||
+ (hours <= 1 && ['h']) ||
+ (hours < thresholds.h && ['hh', hours]) ||
+ (days <= 1 && ['d']) ||
+ (days < thresholds.d && ['dd', days]);
+
+ if (thresholds.w != null) {
+ a =
+ a ||
+ (weeks <= 1 && ['w']) ||
+ (weeks < thresholds.w && ['ww', weeks]);
+ }
+ a = a ||
+ (months <= 1 && ['M']) ||
+ (months < thresholds.M && ['MM', months]) ||
+ (years <= 1 && ['y']) || ['yy', years];
+
+ a[2] = withoutSuffix;
+ a[3] = +posNegDuration > 0;
+ a[4] = locale;
+ return substituteTimeAgo.apply(null, a);
+}
+
+// This function allows you to set the rounding function for relative time strings
+function getSetRelativeTimeRounding(roundingFunction) {
+ if (roundingFunction === undefined) {
+ return round;
+ }
+ if (typeof roundingFunction === 'function') {
+ round = roundingFunction;
+ return true;
+ }
+ return false;
+}
+
+// This function allows you to set a threshold for relative time strings
+function getSetRelativeTimeThreshold(threshold, limit) {
+ if (thresholds[threshold] === undefined) {
+ return false;
+ }
+ if (limit === undefined) {
+ return thresholds[threshold];
+ }
+ thresholds[threshold] = limit;
+ if (threshold === 's') {
+ thresholds.ss = limit - 1;
+ }
+ return true;
+}
+
+function humanize(argWithSuffix, argThresholds) {
+ if (!this.isValid()) {
+ return this.localeData().invalidDate();
+ }
+
+ var withSuffix = false,
+ th = thresholds,
+ locale,
+ output;
+
+ if (typeof argWithSuffix === 'object') {
+ argThresholds = argWithSuffix;
+ argWithSuffix = false;
+ }
+ if (typeof argWithSuffix === 'boolean') {
+ withSuffix = argWithSuffix;
+ }
+ if (typeof argThresholds === 'object') {
+ th = Object.assign({}, thresholds, argThresholds);
+ if (argThresholds.s != null && argThresholds.ss == null) {
+ th.ss = argThresholds.s - 1;
+ }
+ }
+
+ locale = this.localeData();
+ output = relativeTime$1(this, !withSuffix, th, locale);
+
+ if (withSuffix) {
+ output = locale.pastFuture(+this, output);
+ }
+
+ return locale.postformat(output);
+}
+
+var abs$1 = Math.abs;
+
+function sign(x) {
+ return (x > 0) - (x < 0) || +x;
+}
+
+function toISOString$1() {
+ // for ISO strings we do not use the normal bubbling rules:
+ // * milliseconds bubble up until they become hours
+ // * days do not bubble at all
+ // * months bubble up until they become years
+ // This is because there is no context-free conversion between hours and days
+ // (think of clock changes)
+ // and also not between days and months (28-31 days per month)
+ if (!this.isValid()) {
+ return this.localeData().invalidDate();
+ }
+
+ var seconds = abs$1(this._milliseconds) / 1000,
+ days = abs$1(this._days),
+ months = abs$1(this._months),
+ minutes,
+ hours,
+ years,
+ s,
+ total = this.asSeconds(),
+ totalSign,
+ ymSign,
+ daysSign,
+ hmsSign;
+
+ if (!total) {
+ // this is the same as C#'s (Noda) and python (isodate)...
+ // but not other JS (goog.date)
+ return 'P0D';
+ }
+
+ // 3600 seconds -> 60 minutes -> 1 hour
+ minutes = absFloor(seconds / 60);
+ hours = absFloor(minutes / 60);
+ seconds %= 60;
+ minutes %= 60;
+
+ // 12 months -> 1 year
+ years = absFloor(months / 12);
+ months %= 12;
+
+ // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
+ s = seconds ? seconds.toFixed(3).replace(/\.?0+$/, '') : '';
+
+ totalSign = total < 0 ? '-' : '';
+ ymSign = sign(this._months) !== sign(total) ? '-' : '';
+ daysSign = sign(this._days) !== sign(total) ? '-' : '';
+ hmsSign = sign(this._milliseconds) !== sign(total) ? '-' : '';
+
+ return (
+ totalSign +
+ 'P' +
+ (years ? ymSign + years + 'Y' : '') +
+ (months ? ymSign + months + 'M' : '') +
+ (days ? daysSign + days + 'D' : '') +
+ (hours || minutes || seconds ? 'T' : '') +
+ (hours ? hmsSign + hours + 'H' : '') +
+ (minutes ? hmsSign + minutes + 'M' : '') +
+ (seconds ? hmsSign + s + 'S' : '')
+ );
+}
+
+var proto$2 = Duration.prototype;
+
+proto$2.isValid = isValid$1;
+proto$2.abs = abs;
+proto$2.add = add$1;
+proto$2.subtract = subtract$1;
+proto$2.as = as;
+proto$2.asMilliseconds = asMilliseconds;
+proto$2.asSeconds = asSeconds;
+proto$2.asMinutes = asMinutes;
+proto$2.asHours = asHours;
+proto$2.asDays = asDays;
+proto$2.asWeeks = asWeeks;
+proto$2.asMonths = asMonths;
+proto$2.asQuarters = asQuarters;
+proto$2.asYears = asYears;
+proto$2.valueOf = valueOf$1;
+proto$2._bubble = bubble;
+proto$2.clone = clone$1;
+proto$2.get = get$2;
+proto$2.milliseconds = milliseconds;
+proto$2.seconds = seconds;
+proto$2.minutes = minutes;
+proto$2.hours = hours;
+proto$2.days = days;
+proto$2.weeks = weeks;
+proto$2.months = months;
+proto$2.years = years;
+proto$2.humanize = humanize;
+proto$2.toISOString = toISOString$1;
+proto$2.toString = toISOString$1;
+proto$2.toJSON = toISOString$1;
+proto$2.locale = locale;
+proto$2.localeData = localeData;
+
+proto$2.toIsoString = deprecate(
+ 'toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)',
+ toISOString$1
+);
+proto$2.lang = lang;
+
+// FORMATTING
+
+addFormatToken('X', 0, 0, 'unix');
+addFormatToken('x', 0, 0, 'valueOf');
+
+// PARSING
+
+addRegexToken('x', matchSigned);
+addRegexToken('X', matchTimestamp);
+addParseToken('X', function (input, array, config) {
+ config._d = new Date(parseFloat(input) * 1000);
+});
+addParseToken('x', function (input, array, config) {
+ config._d = new Date(toInt(input));
+});
+
+//! moment.js
+
+hooks.version = '2.30.1';
+
+setHookCallback(createLocal);
+
+hooks.fn = proto;
+hooks.min = min;
+hooks.max = max;
+hooks.now = now;
+hooks.utc = createUTC;
+hooks.unix = createUnix;
+hooks.months = listMonths;
+hooks.isDate = isDate;
+hooks.locale = getSetGlobalLocale;
+hooks.invalid = createInvalid;
+hooks.duration = createDuration;
+hooks.isMoment = isMoment;
+hooks.weekdays = listWeekdays;
+hooks.parseZone = createInZone;
+hooks.localeData = getLocale;
+hooks.isDuration = isDuration;
+hooks.monthsShort = listMonthsShort;
+hooks.weekdaysMin = listWeekdaysMin;
+hooks.defineLocale = defineLocale;
+hooks.updateLocale = updateLocale;
+hooks.locales = listLocales;
+hooks.weekdaysShort = listWeekdaysShort;
+hooks.normalizeUnits = normalizeUnits;
+hooks.relativeTimeRounding = getSetRelativeTimeRounding;
+hooks.relativeTimeThreshold = getSetRelativeTimeThreshold;
+hooks.calendarFormat = getCalendarFormat;
+hooks.prototype = proto;
+
+// currently HTML5 input type only supports 24-hour formats
+hooks.HTML5_FMT = {
+ DATETIME_LOCAL: 'YYYY-MM-DDTHH:mm', //
+ DATETIME_LOCAL_SECONDS: 'YYYY-MM-DDTHH:mm:ss', //
+ DATETIME_LOCAL_MS: 'YYYY-MM-DDTHH:mm:ss.SSS', //
+ DATE: 'YYYY-MM-DD', //
+ TIME: 'HH:mm', //
+ TIME_SECONDS: 'HH:mm:ss', //
+ TIME_MS: 'HH:mm:ss.SSS', //
+ WEEK: 'GGGG-[W]WW', //
+ MONTH: 'YYYY-MM', //
+};
+
+export default hooks;
diff --git a/tests/test_app/templates/pyscript.html b/tests/test_app/templates/pyscript.html
new file mode 100644
index 00000000..57a5dd15
--- /dev/null
+++ b/tests/test_app/templates/pyscript.html
@@ -0,0 +1,33 @@
+{% load static %} {% load reactpy %}
+
+
+
+
+
+
+
+
+ ReactPy
+ {% pyscript_setup extra_js='{"/static/moment.js":"moment"}' config="{}" %}
+
+
+
+ ReactPy PyScript Test Page
+
+ {% pyscript_component "./test_app/pyscript/components/hello_world.py" initial="Loading...
" %}
+
+ {% pyscript_component "./test_app/pyscript/components/custom_root.py" root="main" %}
+
+ {% pyscript_component "./test_app/pyscript/components/multifile_parent.py" "./test_app/pyscript/components/multifile_child.py" %}
+
+ {% pyscript_component "./test_app/pyscript/components/counter.py" %}
+
+ {% component "test_app.pyscript.components.server_side.parent" %}
+
+ {% component "test_app.pyscript.components.server_side.parent_toggle" %}
+
+ {% pyscript_component "./test_app/pyscript/components/remote_js_module.py" %}
+
+
+
+
diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py
index d92867cd..11fdc390 100644
--- a/tests/test_app/tests/test_components.py
+++ b/tests/test_app/tests/test_components.py
@@ -678,3 +678,47 @@ def test_channel_layer_components(self):
finally:
new_page.close()
+
+ def test_pyscript_components(self):
+ new_page = self.browser.new_page()
+ try:
+ new_page.goto(f"{self.live_server_url}/pyscript/")
+ new_page.wait_for_selector("#hello-world-loading")
+ new_page.wait_for_selector("#hello-world")
+ new_page.wait_for_selector("#custom-root")
+ new_page.wait_for_selector("#multifile-parent")
+ new_page.wait_for_selector("#multifile-child")
+
+ new_page.wait_for_selector("#counter")
+ new_page.wait_for_selector("#counter pre[data-value='0']")
+ new_page.wait_for_selector("#counter .plus").click(delay=CLICK_DELAY)
+ new_page.wait_for_selector("#counter pre[data-value='1']")
+ new_page.wait_for_selector("#counter .plus").click(delay=CLICK_DELAY)
+ new_page.wait_for_selector("#counter pre[data-value='2']")
+ new_page.wait_for_selector("#counter .minus").click(delay=CLICK_DELAY)
+ new_page.wait_for_selector("#counter pre[data-value='1']")
+
+ new_page.wait_for_selector("#parent")
+ new_page.wait_for_selector("#child")
+ new_page.wait_for_selector("#child pre[data-value='0']")
+ new_page.wait_for_selector("#child .plus").click(delay=CLICK_DELAY)
+ new_page.wait_for_selector("#child pre[data-value='1']")
+ new_page.wait_for_selector("#child .plus").click(delay=CLICK_DELAY)
+ new_page.wait_for_selector("#child pre[data-value='2']")
+ new_page.wait_for_selector("#child .minus").click(delay=CLICK_DELAY)
+ new_page.wait_for_selector("#child pre[data-value='1']")
+
+ new_page.wait_for_selector("#parent-toggle")
+ new_page.wait_for_selector("#parent-toggle button").click(delay=CLICK_DELAY)
+ new_page.wait_for_selector("#parent-toggle")
+ new_page.wait_for_selector("#parent-toggle pre[data-value='0']")
+ new_page.wait_for_selector("#parent-toggle .plus").click(delay=CLICK_DELAY)
+ new_page.wait_for_selector("#parent-toggle pre[data-value='1']")
+ new_page.wait_for_selector("#parent-toggle .plus").click(delay=CLICK_DELAY)
+ new_page.wait_for_selector("#parent-toggle pre[data-value='2']")
+ new_page.wait_for_selector("#parent-toggle .minus").click(delay=CLICK_DELAY)
+ new_page.wait_for_selector("#parent-toggle pre[data-value='1']")
+
+ new_page.wait_for_selector("#moment[data-success=true]")
+ finally:
+ new_page.close()
diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py
index 05acb163..070b74f1 100644
--- a/tests/test_app/urls.py
+++ b/tests/test_app/urls.py
@@ -14,6 +14,7 @@
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
+
from django.contrib import admin
from django.urls import include, path
@@ -30,6 +31,7 @@
path("", include("test_app.prerender.urls")),
path("", include("test_app.performance.urls")),
path("", include("test_app.router.urls")),
+ path("", include("test_app.pyscript.urls")),
path("", include("test_app.offline.urls")),
path("", include("test_app.channel_layers.urls")),
path("reactpy/", include("reactpy_django.http.urls")),