webtimeline 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: webtimeline
3
+ Version: 0.0.1
4
+ Summary: Python Web served time graph
5
+ Keywords: chart,line,plot,graph,web,served,time
6
+ Author-email: Bernard Czenkusz <bernie@skipole.co.uk>
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-Expression: Unlicense
10
+ Classifier: Topic :: Scientific/Engineering
11
+ Classifier: Topic :: Office/Business
12
+ Classifier: Topic :: Education
13
+ License-File: LICENSE
14
+ Requires-Dist: litestar[standard]>=2.19.0
15
+ Requires-Dist: Mako>=1.3.10
16
+ Requires-Dist: minilineplot>=0.0.8
17
+ Project-URL: Source, https://github.com/bernie-skipole/webtimeline
18
+
19
+ # webtimeline
20
+
21
+ Python Web served time graph
22
+
23
+ This provides a class 'WebTimeLine' which generates a web server, serving a page with a lineplot.
24
+
25
+ This is an example, using HTMX, MAKO and LITESTAR and is primarily intended as a record for myself.
26
+
27
+ A coroutine method of the class can be used to add points, in the form of (time.time(), value)
28
+
29
+ These are dynamically added to the plot, and the web page will be updated as points are added.
30
+
31
+ An example browser image is:
32
+
33
+ ![Terminal screenshot](https://github.com/bernie-skipole/webtimeline/raw/main/Screenshot.png)
34
+
35
+ The program can be installed from Pypi into a virtual environment, which will automaticall pull in dependencies.
36
+
37
+ Running "python3 -m webtimeline" will then run the following script and will serve the above chart:
38
+
39
+ import asyncio, time, random
40
+
41
+ from webtimeline import WebTimeLine
42
+
43
+ ## This creates an example web service, with random measurements every ten seconds
44
+
45
+ tline = WebTimeLine(host='localhost', port=8000, basepath=None, hours=1, title="My Title", description="Data display")
46
+
47
+ # set a y axis, lowest value 0.0
48
+ # highest 80.0
49
+ # with four intervals up the axis (five values shown at grid lines)
50
+ # and axis numbers printed with one decimal point
51
+
52
+ tline.set_y_axis(ymin=0.0, ymax=80.0, yintervals=4, yformat=".1f")
53
+
54
+ async def my_function(tline):
55
+ "Create data and send it using tline.putpoint()"
56
+ while True:
57
+ value = random.uniform(30, 70) # random value used here
58
+ await tline.putpoint(time.time(), value)
59
+ await asyncio.sleep(10) # pause 10 seconds between readings
60
+
61
+ ## create two tasks, one runs the web server, one gathers data
62
+
63
+ async def runchart():
64
+ async with asyncio.TaskGroup() as tg:
65
+ tg.create_task( tline.serve(tg) )
66
+ tg.create_task( my_function(tline) )
67
+ print("Now serving at localhost:8000")
68
+
69
+ asyncio.run(runchart())
70
+
71
+
72
+ The web page is purposely minimal, without any CSS. The code could be obtained from github and the Mako templates altered for a more pleasant view.
73
+
74
+ Dependencies are:
75
+
76
+ litestar[standard]
77
+ mako
78
+ minilineplot
79
+
80
+ Details of the WebTimeLine class are:
81
+
82
+ **WebTimeLine(host, port, basepath, hours, height, width, title, description)**
83
+
84
+ host is a string, default 'localhost'
85
+
86
+ port is an integer, default 8000
87
+
88
+ basepath is either None, or a string such as '/graph/' which will set a path segment which will be prepended to the URL path.
89
+
90
+ hours is the hours, between 1 and 48, displayed along the x axis.
91
+
92
+ height is the height of the image, default 600.
93
+
94
+ width is the width of the image, default 800.
95
+
96
+ title, if given is a string shown at the top of the graph.
97
+
98
+ description, if given, is a string shown at the bottom of the graph.
99
+
100
+ **Methods**
101
+
102
+ **serve(tg)**
103
+
104
+ (async method) Set this as a task to serve the web page, tg should be a taskgroup
105
+
106
+ **putpoint(t, v)**
107
+
108
+ (async method) await this to add a point to the graph.
109
+
110
+ t must be a time.time() point.
111
+
112
+ v is the value to be plotted.
113
+
114
+ **set_colors(backcol, gridcol, axiscol, chartbackcol, linecol)**
115
+
116
+ If called sets chart colours.
117
+
118
+ backcol is default "white", The background colour of the whole image
119
+
120
+ gridcol is default "grey", The colour of the chart grid
121
+
122
+ axiscol is default "black", The colour of axis, title and description
123
+
124
+ chartbackcol is default "white", The background colour of the chart
125
+
126
+ linecol is default "blue", The colour of the line being plotted
127
+
128
+ All these colour names are SVG names and can be set as:
129
+
130
+ Color Names: "red", "blue" etc.
131
+
132
+ Hex Codes: "#FF0000" for red.
133
+
134
+ RGB/RGBA: "rgb(255,0,0)" or "rgba(255,0,0,0.5)" (with opacity).
135
+
136
+ HSL/HSLA: "hsl(0,100%,50%)" or "hsla(0,100%,50%,0.5)" (hue, saturation, lightness, alpha)
137
+
138
+ **set_title(title)**
139
+
140
+ Sets the title of the graph, and updates the graph with the new title.
141
+
142
+ **set_description(description)**
143
+
144
+ Sets the description of the graph, and updates the graph with the new description.
145
+
146
+ **set_y_axis(ymin, ymax, yintervals, yformat)**
147
+
148
+ If this is not called, an automatic y scaling will be used.
149
+
150
+ If it is called, then these values will be set, however if any y point exceeds these values, then the chart will revert to auto-scaling.
151
+
152
+ If you wish to revert to autoscaling, call this with None values.
153
+
154
+
155
+
156
+
157
+
158
+
159
+
160
+
161
+
162
+
@@ -0,0 +1,143 @@
1
+ # webtimeline
2
+
3
+ Python Web served time graph
4
+
5
+ This provides a class 'WebTimeLine' which generates a web server, serving a page with a lineplot.
6
+
7
+ This is an example, using HTMX, MAKO and LITESTAR and is primarily intended as a record for myself.
8
+
9
+ A coroutine method of the class can be used to add points, in the form of (time.time(), value)
10
+
11
+ These are dynamically added to the plot, and the web page will be updated as points are added.
12
+
13
+ An example browser image is:
14
+
15
+ ![Terminal screenshot](https://github.com/bernie-skipole/webtimeline/raw/main/Screenshot.png)
16
+
17
+ The program can be installed from Pypi into a virtual environment, which will automaticall pull in dependencies.
18
+
19
+ Running "python3 -m webtimeline" will then run the following script and will serve the above chart:
20
+
21
+ import asyncio, time, random
22
+
23
+ from webtimeline import WebTimeLine
24
+
25
+ ## This creates an example web service, with random measurements every ten seconds
26
+
27
+ tline = WebTimeLine(host='localhost', port=8000, basepath=None, hours=1, title="My Title", description="Data display")
28
+
29
+ # set a y axis, lowest value 0.0
30
+ # highest 80.0
31
+ # with four intervals up the axis (five values shown at grid lines)
32
+ # and axis numbers printed with one decimal point
33
+
34
+ tline.set_y_axis(ymin=0.0, ymax=80.0, yintervals=4, yformat=".1f")
35
+
36
+ async def my_function(tline):
37
+ "Create data and send it using tline.putpoint()"
38
+ while True:
39
+ value = random.uniform(30, 70) # random value used here
40
+ await tline.putpoint(time.time(), value)
41
+ await asyncio.sleep(10) # pause 10 seconds between readings
42
+
43
+ ## create two tasks, one runs the web server, one gathers data
44
+
45
+ async def runchart():
46
+ async with asyncio.TaskGroup() as tg:
47
+ tg.create_task( tline.serve(tg) )
48
+ tg.create_task( my_function(tline) )
49
+ print("Now serving at localhost:8000")
50
+
51
+ asyncio.run(runchart())
52
+
53
+
54
+ The web page is purposely minimal, without any CSS. The code could be obtained from github and the Mako templates altered for a more pleasant view.
55
+
56
+ Dependencies are:
57
+
58
+ litestar[standard]
59
+ mako
60
+ minilineplot
61
+
62
+ Details of the WebTimeLine class are:
63
+
64
+ **WebTimeLine(host, port, basepath, hours, height, width, title, description)**
65
+
66
+ host is a string, default 'localhost'
67
+
68
+ port is an integer, default 8000
69
+
70
+ basepath is either None, or a string such as '/graph/' which will set a path segment which will be prepended to the URL path.
71
+
72
+ hours is the hours, between 1 and 48, displayed along the x axis.
73
+
74
+ height is the height of the image, default 600.
75
+
76
+ width is the width of the image, default 800.
77
+
78
+ title, if given is a string shown at the top of the graph.
79
+
80
+ description, if given, is a string shown at the bottom of the graph.
81
+
82
+ **Methods**
83
+
84
+ **serve(tg)**
85
+
86
+ (async method) Set this as a task to serve the web page, tg should be a taskgroup
87
+
88
+ **putpoint(t, v)**
89
+
90
+ (async method) await this to add a point to the graph.
91
+
92
+ t must be a time.time() point.
93
+
94
+ v is the value to be plotted.
95
+
96
+ **set_colors(backcol, gridcol, axiscol, chartbackcol, linecol)**
97
+
98
+ If called sets chart colours.
99
+
100
+ backcol is default "white", The background colour of the whole image
101
+
102
+ gridcol is default "grey", The colour of the chart grid
103
+
104
+ axiscol is default "black", The colour of axis, title and description
105
+
106
+ chartbackcol is default "white", The background colour of the chart
107
+
108
+ linecol is default "blue", The colour of the line being plotted
109
+
110
+ All these colour names are SVG names and can be set as:
111
+
112
+ Color Names: "red", "blue" etc.
113
+
114
+ Hex Codes: "#FF0000" for red.
115
+
116
+ RGB/RGBA: "rgb(255,0,0)" or "rgba(255,0,0,0.5)" (with opacity).
117
+
118
+ HSL/HSLA: "hsl(0,100%,50%)" or "hsla(0,100%,50%,0.5)" (hue, saturation, lightness, alpha)
119
+
120
+ **set_title(title)**
121
+
122
+ Sets the title of the graph, and updates the graph with the new title.
123
+
124
+ **set_description(description)**
125
+
126
+ Sets the description of the graph, and updates the graph with the new description.
127
+
128
+ **set_y_axis(ymin, ymax, yintervals, yformat)**
129
+
130
+ If this is not called, an automatic y scaling will be used.
131
+
132
+ If it is called, then these values will be set, however if any y point exceeds these values, then the chart will revert to auto-scaling.
133
+
134
+ If you wish to revert to autoscaling, call this with None values.
135
+
136
+
137
+
138
+
139
+
140
+
141
+
142
+
143
+
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["flit_core >=3.2,<4"]
3
+ build-backend = "flit_core.buildapi"
4
+
5
+ [project]
6
+ name = "webtimeline"
7
+ authors = [{name = "Bernard Czenkusz", email = "bernie@skipole.co.uk"}]
8
+ license = "Unlicense"
9
+ classifiers = ["Topic :: Scientific/Engineering", "Topic :: Office/Business", "Topic :: Education"]
10
+ version = "0.0.1"
11
+ description="Python Web served time graph"
12
+ readme = "README.md"
13
+ requires-python = ">=3.10"
14
+ keywords=['chart', 'line', 'plot', 'graph', 'web', 'served', 'time']
15
+ dependencies = ["litestar[standard]>=2.19.0", "Mako>=1.3.10", "minilineplot>=0.0.8"]
16
+
17
+ [project.urls]
18
+ Source = "https://github.com/bernie-skipole/webtimeline"
19
+
20
+ [project.scripts]
21
+ webtimeline = "webtimeline.__main__:main"
22
+
23
+
@@ -0,0 +1,55 @@
1
+
2
+
3
+ from .wtl import WebTimeLine
4
+
5
+
6
+ # Usage
7
+
8
+ # Create instance of WebTimeLine class
9
+
10
+ # tline = WebTimeLine(host='localhost', port=8000, basepath=None, hours=4, title="My Title")
11
+
12
+ # hours should be between 1 and 48
13
+ # a task tline.serve(tg) should be created to run the web server, tg being a taskgroup
14
+ # tline.putpoint(t, v) should be awaited to insert points
15
+
16
+
17
+ ####### Example
18
+
19
+ # install dependencies
20
+ # pip install litestar[standard]
21
+ # pip install mako
22
+ # pip install minilineplot
23
+
24
+
25
+ ######### and the script would be something like :
26
+
27
+
28
+ # import asyncio, time
29
+ #
30
+ # from webtimeline import WebTimeLine
31
+ #
32
+ ## Create WebTimeLine object
33
+ # tline = WebTimeLine(host='localhost', port=8000, basepath=None, hours=4, title="My Title")
34
+ #
35
+ # async def my_function(tline):
36
+ # while True:
37
+ # # Get value from somewhere ....
38
+ # # and puts the measurement into the plot
39
+ # await tline.putpoint(time.time(), v)
40
+ # await asyncio.sleep(10) # pause between readings, and allow webserver to work
41
+ #
42
+ #
43
+ ## create two tasks, one runs the web server, one runs my_function(tline) gathering data
44
+ #
45
+ # async def runchart():
46
+ # async with asyncio.TaskGroup() as tg:
47
+ # tg.create_task( tline.serve(tg) )
48
+ # tg.create_task( my_function(tline) )
49
+ #
50
+ ## And run the loop
51
+ # asyncio.run(runchart())
52
+
53
+
54
+
55
+
@@ -0,0 +1,42 @@
1
+ import asyncio, time, random
2
+
3
+ from . import WebTimeLine
4
+
5
+
6
+ ### This creates an example web service, with random measurements every ten seconds
7
+
8
+ ## Create WebTimeLine object
9
+ tline = WebTimeLine(host='localhost', port=8000, basepath=None, hours=1, title="My Title", description="Data display")
10
+
11
+ # set a y axis, lowest value 0.0
12
+ # highest 80.0
13
+ # with four intervals up the axis (five values shown at grid lines)
14
+ # and axis numbers printed with one decimal point
15
+
16
+ tline.set_y_axis(ymin=0.0, ymax=80.0, yintervals=4, yformat=".1f")
17
+
18
+ async def my_function(tline):
19
+ "Create data and send it using tline.putpoint()"
20
+ while True:
21
+ value = random.uniform(30, 70) # random value used here
22
+ await tline.putpoint(time.time(), value)
23
+ await asyncio.sleep(10) # pause 10 seconds between readings
24
+
25
+ ## create two tasks, one runs the web server, one gathers data
26
+
27
+ async def runchart():
28
+ async with asyncio.TaskGroup() as tg:
29
+ tg.create_task( tline.serve(tg) )
30
+ tg.create_task( my_function(tline) )
31
+ print("Now serving at localhost:8000")
32
+
33
+
34
+
35
+ def main():
36
+ "Run the program"
37
+ asyncio.run(runchart())
38
+
39
+
40
+ if __name__ == "__main__":
41
+ # And run main
42
+ main()
File without changes
@@ -0,0 +1,143 @@
1
+
2
+
3
+ import asyncio
4
+
5
+ from os import listdir, remove
6
+ from os.path import isfile, join
7
+
8
+ from pathlib import Path
9
+
10
+ from collections.abc import AsyncGenerator
11
+
12
+ from asyncio.exceptions import TimeoutError
13
+
14
+ from litestar import Litestar, get, post, Request
15
+ from litestar.plugins.htmx import HTMXPlugin, HTMXTemplate, ClientRedirect, ClientRefresh
16
+ from litestar.contrib.mako import MakoTemplateEngine
17
+ from litestar.template.config import TemplateConfig
18
+ from litestar.response import Template, Redirect, File
19
+ from litestar.static_files import create_static_files_router
20
+ from litestar.datastructures import Cookie, State
21
+
22
+ from litestar.connection import ASGIConnection
23
+ from litestar.exceptions import NotFoundException
24
+
25
+ from litestar.response import ServerSentEvent, ServerSentEventMessage
26
+
27
+
28
+ # location of static files, for CSS and javascript
29
+ STATICFILES = Path(__file__).parent.resolve() / "static"
30
+
31
+ # location of template files
32
+ TEMPLATEFILES = Path(__file__).parent.resolve() / "templates"
33
+
34
+
35
+ # Dictionary of Global variables
36
+ PARAMETERS = {}
37
+
38
+
39
+ class CheckChange:
40
+ """Iterate whenever a chart change occurs."""
41
+
42
+
43
+ def __aiter__(self):
44
+ return self
45
+
46
+ async def __anext__(self):
47
+ "Whenever there is a change, return a ServerSentEventMessage"
48
+ global PARAMETERS
49
+ mkchart = PARAMETERS['mkchart']
50
+ while True:
51
+ try:
52
+ await asyncio.wait_for(mkchart.chart_event.wait(), timeout=5.0)
53
+ except TimeoutError:
54
+ # perhaps check for a stop flage here
55
+ continue
56
+ # a chart_event has occurred
57
+ return ServerSentEventMessage(event="newchart")
58
+
59
+
60
+
61
+ # SSE Handler
62
+ @get(path="/check", sync_to_thread=False)
63
+ def check() -> ServerSentEvent:
64
+ return ServerSentEvent(CheckChange())
65
+
66
+
67
+ def gotonotfound_error_handler(request: Request, exc: Exception) -> ClientRedirect|Redirect:
68
+ """If a NotFoundException is raised, this handles it, and redirects
69
+ the caller to the not found page"""
70
+ global PARAMETERS
71
+ basepath = PARAMETERS["basepath"]
72
+ if basepath:
73
+ redirectpath = basepath + "notfound"
74
+ else:
75
+ redirectpath = "/notfound"
76
+ if request.htmx:
77
+ return ClientRedirect(redirectpath)
78
+ return Redirect(redirectpath)
79
+
80
+
81
+ @get("/notfound", sync_to_thread=False )
82
+ def notfound(request: Request) -> Template:
83
+ "This is the not found page of your site"
84
+ return Template("notfound.html")
85
+
86
+
87
+ @get("/")
88
+ async def publicroot(request: Request) -> ClientRedirect|Redirect:
89
+ "This is the public root folder of your site"
90
+ global PARAMETERS
91
+ basepath = PARAMETERS["basepath"]
92
+ if basepath:
93
+ redirectpath = basepath + "chartpage.html"
94
+ else:
95
+ redirectpath = "/chartpage.html"
96
+ if request.htmx:
97
+ return ClientRedirect(redirectpath)
98
+ return Redirect(redirectpath)
99
+
100
+
101
+ @get("/chartpage.html" )
102
+ async def chartpage(request: Request) -> Template:
103
+ "This is the chart page of your site"
104
+ global PARAMETERS
105
+ mkchart = PARAMETERS['mkchart']
106
+ if mkchart.chart is None:
107
+ return Template("chartpage.html", context={"chart":None})
108
+ return Template("chartpage.html", context={"chart":mkchart.chart.to_string()})
109
+
110
+
111
+ @get("/getchart" )
112
+ async def getchart(request: Request) -> Template:
113
+ "This is just the chart"
114
+ global PARAMETERS
115
+ mkchart = PARAMETERS['mkchart']
116
+ if mkchart.chart is None:
117
+ return HTMXTemplate(None, template_str="<p>No chart generated</p>")
118
+ return HTMXTemplate(None, template_str=mkchart.chart.to_string())
119
+
120
+
121
+
122
+ def make_app(basepath, mkchart):
123
+ # Initialize the Litestar app with a Mako template engine and register the routes
124
+ global PARAMETERS, STATICFILES, TEMPLATEFILES
125
+ PARAMETERS['basepath'] = basepath
126
+ PARAMETERS['mkchart'] = mkchart
127
+
128
+ app = Litestar( path = basepath,
129
+ route_handlers=[publicroot,
130
+ notfound,
131
+ chartpage,
132
+ check,
133
+ getchart,
134
+ create_static_files_router(path="/static", directories=[STATICFILES]),
135
+ ],
136
+ exception_handlers={ NotFoundException: gotonotfound_error_handler},
137
+ plugins=[HTMXPlugin()],
138
+ template_config=TemplateConfig(directory=TEMPLATEFILES,
139
+ engine=MakoTemplateEngine
140
+ ),
141
+ openapi_config=None
142
+ )
143
+ return app
@@ -0,0 +1 @@
1
+ var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=dn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true,historyRestoreAsHxRequest:true},parseInterval:null,location:location,_:null,version:"2.0.6"};Q.onLoad=j;Q.process=Ft;Q.on=xe;Q.off=be;Q.trigger=ae;Q.ajax=Ln;Q.find=f;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=zn;Q.removeExtension=$n;Q.logAll=V;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:se,canAccessLocalStorage:B,findThisElement:Se,filterValues:yn,swap:$e,hasAttribute:s,getAttributeValue:a,getClosestAttributeValue:ne,getClosestMatch:q,getExpressionVars:Tn,getHeaders:mn,getInputValues:dn,getInternalData:oe,getSwapSpecification:bn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:le,makeSettleInfo:Sn,oobSwap:He,querySelectorExt:ue,settleImmediately:Yt,shouldCancel:ht,triggerEvent:ae,triggerErrorEvent:fe,withExtensions:jt};const de=["get","post","put","delete","patch"];const T=de.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function a(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function te(){return document}function y(e,t){return e.getRootNode?e.getRootNode({composed:t}):te()}function q(e,t){while(e&&!t(e)){e=u(e)}return e||null}function o(e,t,n){const r=a(t,n);const o=a(t,"hx-disinherit");var i=a(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function ne(t,n){let r=null;q(t,function(e){return!!(r=o(t,ce(e),n))});if(r!=="unset"){return r}}function h(e,t){return e instanceof Element&&e.matches(t)}function A(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function L(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function N(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function r(e){const t=te().createElement("script");ie(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function i(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(i(e)){const t=r(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){R(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,"");const n=A(t);let r;if(n==="html"){r=new DocumentFragment;const i=L(e);N(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=L(t);N(r,i.body);r.title=i.title}else{const i=L('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function re(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function D(e){return typeof e==="function"}function k(e){return t(e,"Object")}function oe(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e<t.length;e++){n.push(t[e])}}return n}function ie(t,n){if(t){for(let e=0;e<t.length;e++){n(t[e])}}}function F(e){const t=e.getBoundingClientRect();const n=t.top;const r=t.bottom;return n<window.innerHeight&&r>=0}function se(e){return e.getRootNode({composed:true})===document}function X(e){return e.trim().split(/\s+/)}function le(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function v(e){try{return JSON.parse(e)}catch(e){R(e);return null}}function B(){const e="htmx:sessionStorageTest";try{sessionStorage.setItem(e,e);sessionStorage.removeItem(e);return true}catch(e){return false}}function U(e){const t=new URL(e,"http://x");if(t){e=t.pathname+t.search}if(e!="/"){e=e.replace(/\/+$/,"")}return e}function e(e){return On(te().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function f(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return f(te(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(te(),e)}}function b(){return window}function z(e,t){e=w(e);if(t){b().setTimeout(function(){z(e);e=null},t)}else{u(e).removeChild(e)}}function ce(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function p(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ce(w(e));if(!e){return}if(n){b().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ce(w(e));if(!r){return}if(n){b().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=w(e);e.classList.toggle(t)}function Z(e,t){e=w(e);ie(e.parentElement.children,function(e){G(e,t)});K(ce(e),t)}function g(e,t){e=ce(w(e));if(e){return e.closest(t)}return null}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function pe(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function m(t,r,n){if(r.indexOf("global ")===0){return m(t,r.slice(7),true)}t=w(t);const o=[];{let t=0;let n=0;for(let e=0;e<r.length;e++){const l=r[e];if(l===","&&t===0){o.push(r.substring(n,e));n=e+1;continue}if(l==="<"){t++}else if(l==="/"&&e<r.length-1&&r[e+1]===">"){t--}}if(n<r.length){o.push(r.substring(n))}}const i=[];const s=[];while(o.length>0){const r=pe(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ce(t),pe(r.slice(8)))}else if(r.indexOf("find ")===0){e=f(p(t),pe(r.slice(5)))}else if(r==="next"||r==="nextElementSibling"){e=ce(t).nextElementSibling}else if(r.indexOf("next ")===0){e=ge(t,pe(r.slice(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ce(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,pe(r.slice(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=y(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=p(y(t,!!n));i.push(...M(c.querySelectorAll(e)))}return i}var ge=function(t,e,n){const r=p(y(t,n)).querySelectorAll(e);for(let e=0;e<r.length;e++){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_PRECEDING){return o}}};var me=function(t,e,n){const r=p(y(t,n)).querySelectorAll(e);for(let e=r.length-1;e>=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ue(e,t){if(typeof e!=="string"){return m(e,t)[0]}else{return m(te().body,e)[0]}}function w(e,t){if(typeof e==="string"){return f(p(t)||document,e)}else{return e}}function ye(e,t,n,r){if(D(t)){return{target:te().body,event:J(e),listener:t,options:n}}else{return{target:w(e),event:J(t),listener:n,options:r}}}function xe(t,n,r,o){Gn(function(){const e=ye(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=D(n);return e?n:r}function be(t,n,r){Gn(function(){const e=ye(t,n,r);e.target.removeEventListener(e.event,e.listener)});return D(n)?n:r}const ve=te().createElement("output");function we(t,n){const e=ne(t,n);if(e){if(e==="this"){return[Se(t,n)]}else{const r=m(t,e);const o=/(^|,)(\s*)inherit(\s*)($|,)/.test(e);if(o){const i=ce(q(t,function(e){return e!==t&&s(ce(e),n)}));if(i){r.push(...we(i,n))}}if(r.length===0){R('The selector "'+e+'" on '+n+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ce(q(e,function(e){return a(ce(e),t)!=null}))}function Ee(e){const t=ne(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ue(e,t)}}else{const n=oe(e);if(n.boosted){return te().body}else{return e}}}function Ce(e){return Q.config.attributesToSettle.includes(e)}function Oe(t,n){ie(t.attributes,function(e){if(!n.hasAttribute(e.name)&&Ce(e.name)){t.removeAttribute(e.name)}});ie(n.attributes,function(e){if(Ce(e.name)){t.setAttribute(e.name,e.value)}})}function Re(t,e){const n=Jn(e);for(let e=0;e<n.length;e++){const r=n[e];try{if(r.isInlineSwap(t)){return true}}catch(e){R(e)}}return t==="outerHTML"}function He(e,o,i,t){t=t||te();let n="#"+CSS.escape(ee(o,"id"));let s="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=m(t,n,false);if(r.length){ie(r,function(e){let t;const n=o.cloneNode(true);t=te().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=p(n)}const r={shouldSwap:true,target:e,fragment:t};if(!ae(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}ie(i.elts,function(e){ae(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(te().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=f("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=f("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){ie(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=a(e,"id");const n=te().getElementById(t);if(n!=null){if(e.moveBefore){let e=f("#--htmx-preserve-pantry--");if(e==null){te().body.insertAdjacentHTML("afterend","<div id='--htmx-preserve-pantry--'></div>");e=f("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Ae(l,e,c){ie(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=p(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Le(e){return function(){G(e,Q.config.addedClass);Ft(ce(e));Ne(p(e));ae(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=$(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function c(e,t,n,r){Ae(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ce(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Le(o))}}}function Ie(e,t){let n=0;while(n<e.length){t=(t<<5)-t+e.charCodeAt(n++)|0}return t}function Pe(t){let n=0;for(let e=0;e<t.attributes.length;e++){const r=t.attributes[e];if(r.value){n=Ie(r.name,n);n=Ie(r.value,n)}}return n}function De(t){const n=oe(t);if(n.onHandlers){for(let e=0;e<n.onHandlers.length;e++){const r=n.onHandlers[e];be(t,r.event,r.listener)}delete n.onHandlers}}function ke(e){const t=oe(e);if(t.timeout){clearTimeout(t.timeout)}if(t.listenerInfos){ie(t.listenerInfos,function(e){if(e.on){be(e.on,e.trigger,e.listener)}})}De(e);ie(Object.keys(t),function(e){if(e!=="firstInitCompleted")delete t[e]})}function S(e){ae(e,"htmx:beforeCleanupElement");ke(e);ie(e.children,function(e){S(e)})}function Me(t,e,n){if(t.tagName==="BODY"){return Ve(t,e,n)}let r;const o=t.previousSibling;const i=u(t);if(!i){return}c(i,t,e,n);if(o==null){r=i.firstChild}else{r=o.nextSibling}n.elts=n.elts.filter(function(e){return e!==t});while(r&&r!==t){if(r instanceof Element){n.elts.push(r)}r=r.nextSibling}S(t);t.remove()}function Fe(e,t,n){return c(e,e.firstChild,t,n)}function Xe(e,t,n){return c(u(e),e,t,n)}function Be(e,t,n){return c(e,null,t,n)}function Ue(e,t,n){return c(u(e),e.nextSibling,t,n)}function je(e){S(e);const t=u(e);if(t){return t.removeChild(e)}}function Ve(e,t,n){const r=e.firstChild;c(e,r,t,n);if(r){while(r.nextSibling){S(r.nextSibling);e.removeChild(r.nextSibling)}S(r);e.removeChild(r)}}function _e(t,e,n,r,o){switch(t){case"none":return;case"outerHTML":Me(n,r,o);return;case"afterbegin":Fe(n,r,o);return;case"beforebegin":Xe(n,r,o);return;case"beforeend":Be(n,r,o);return;case"afterend":Ue(n,r,o);return;case"delete":je(n);return;default:var i=Jn(e);for(let e=0;e<i.length;e++){const s=i[e];try{const l=s.handleSwap(t,n,r,o);if(l){if(Array.isArray(l)){for(let e=0;e<l.length;e++){const c=l[e];if(c.nodeType!==Node.TEXT_NODE&&c.nodeType!==Node.COMMENT_NODE){o.tasks.push(Le(c))}}}return}}catch(e){R(e)}}if(t==="innerHTML"){Ve(n,r,o)}else{_e(Q.config.defaultSwapStyle,e,n,r,o)}}}function ze(e,n,r){var t=x(e,"[hx-swap-oob], [data-hx-swap-oob]");ie(t,function(e){if(Q.config.allowNestedOobSwaps||e.parentElement===null){const t=a(e,"hx-swap-oob");if(t!=null){He(t,e,n,r)}}else{e.removeAttribute("hx-swap-oob");e.removeAttribute("data-hx-swap-oob")}});return t.length>0}function $e(h,d,p,g){if(!g){g={}}let m=null;let n=null;let e=function(){re(g.beforeSwapCallback);h=w(h);const r=g.contextElement?y(g.contextElement,false):te();const e=document.activeElement;let t={};t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null};const o=Sn(h);if(p.swapStyle==="textContent"){h.textContent=d}else{let n=P(d);o.title=g.title||n.title;if(g.historyRequest){n=n.querySelector("[hx-history-elt],[data-hx-history-elt]")||n}if(g.selectOOB){const i=g.selectOOB.split(",");for(let t=0;t<i.length;t++){const s=i[t].split(":",2);let e=s[0].trim();if(e.indexOf("#")===0){e=e.substring(1)}const l=s[1]||"true";const c=n.querySelector("#"+e);if(c){He(l,c,o,r)}}}ze(n,o,r);ie(x(n,"template"),function(e){if(e.content&&ze(e.content,o,r)){e.remove()}});if(g.select){const u=te().createDocumentFragment();ie(n.querySelectorAll(g.select),function(e){u.appendChild(e)});n=u}qe(n);_e(p.swapStyle,g.contextElement,h,n,o);Te()}if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){const f=document.getElementById(ee(t.elt,"id"));const a={preventScroll:p.focusScroll!==undefined?!p.focusScroll:!Q.config.defaultFocusScroll};if(f){if(t.start&&f.setSelectionRange){try{f.setSelectionRange(t.start,t.end)}catch(e){}}f.focus(a)}}h.classList.remove(Q.config.swappingClass);ie(o.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ae(e,"htmx:afterSwap",g.eventInfo)});re(g.afterSwapCallback);if(!p.ignoreTitle){Bn(o.title)}const n=function(){ie(o.tasks,function(e){e.call()});ie(o.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ae(e,"htmx:afterSettle",g.eventInfo)});if(g.anchor){const e=ce(w("#"+g.anchor));if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}En(o.elts,p);re(g.afterSettleCallback);re(m)};if(p.settleDelay>0){b().setTimeout(n,p.settleDelay)}else{n()}};let t=Q.config.globalViewTransitions;if(p.hasOwnProperty("transition")){t=p.transition}const r=g.contextElement||te();if(t&&ae(r,"htmx:beforeTransition",g.eventInfo)&&typeof Promise!=="undefined"&&document.startViewTransition){const o=new Promise(function(e,t){m=e;n=t});const i=e;e=function(){document.startViewTransition(function(){i();return o})}}try{if(p?.swapDelay&&p.swapDelay>0){b().setTimeout(e,p.swapDelay)}else{e()}}catch(e){fe(r,"htmx:swapError",g.eventInfo);re(n);throw e}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=v(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(k(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}ae(n,i,e)}}}else{const s=r.split(",");for(let e=0;e<s.length;e++){ae(n,s[e].trim(),[])}}}const Ke=/\s/;const E=/[\s,]/;const Ge=/[_$a-zA-Z]/;const We=/[_$a-zA-Z0-9]/;const Ze=['"',"'","/"];const C=/[^\s]/;const Ye=/[{(]/;const Qe=/[})]/;function et(e){const t=[];let n=0;while(n<e.length){if(Ge.exec(e.charAt(n))){var r=n;while(We.exec(e.charAt(n+1))){n++}t.push(e.substring(r,n+1))}else if(Ze.indexOf(e.charAt(n))!==-1){const o=e.charAt(n);var r=n;n++;while(n<e.length&&e.charAt(n)!==o){if(e.charAt(n)==="\\"){n++}n++}t.push(e.substring(r,n+1))}else{const i=e.charAt(n);t.push(i)}n++}return t}function tt(e,t,n){return Ge.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==n&&t!=="."}function nt(r,o,i){if(o[0]==="["){o.shift();let e=1;let t=" return (function("+i+"){ return (";let n=null;while(o.length>0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=On(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(te().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function O(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=O(e,Qe).trim();e.shift()}else{t=O(e,E)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{O(o,C);const l=o.length;const c=O(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};O(o,C);u.pollInterval=d(O(o,/[,\[\s]/));O(o,C);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const f={trigger:c};var i=nt(e,o,"event");if(i){f.eventFilter=i}O(o,C);while(o.length>0&&o[0]!==","){const a=o.shift();if(a==="changed"){f.changed=true}else if(a==="once"){f.once=true}else if(a==="consume"){f.consume=true}else if(a==="delay"&&o[0]===":"){o.shift();f.delay=d(O(o,E))}else if(a==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=O(o,E);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}f.from=s}else if(a==="target"&&o[0]===":"){o.shift();f.target=rt(o)}else if(a==="throttle"&&o[0]===":"){o.shift();f.throttle=d(O(o,E))}else if(a==="queue"&&o[0]===":"){o.shift();f.queue=O(o,E)}else if(a==="root"&&o[0]===":"){o.shift();f[a]=rt(o)}else if(a==="threshold"&&o[0]===":"){o.shift();f[a]=O(o,E)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}r.push(f)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=a(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){oe(e).cancelled=true}function ct(e,t,n){const r=oe(e);r.timeout=b().setTimeout(function(){if(se(e)&&r.cancelled!==true){if(!pt(n,e,Bt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function ft(e){return g(e,Q.config.disableSelector)}function at(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){gt(t,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)},n,e,true)})}}function ht(e,t){if(e.type==="submit"||e.type==="click"){t=ce(e.target)||t;if(t.tagName==="FORM"){return true}if(t.form&&t.type==="submit"){return true}t=t.closest("a");if(t&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function dt(e,t){return oe(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function pt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(te().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function gt(l,c,e,u,f){const a=oe(l);let t;if(u.from){t=m(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in a)){a.lastValue=new WeakMap}t.forEach(function(e){if(!a.lastValue.has(u)){a.lastValue.set(u,new WeakMap)}a.lastValue.get(u).set(e,e.value)})}ie(t,function(i){const s=function(e){if(!se(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(f||ht(e,l)){e.preventDefault()}if(pt(u,l,e)){return}const t=oe(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ce(e.target),u.target)){return}}if(u.once){if(a.triggeredOnce){return}else{a.triggeredOnce=true}}if(u.changed){const n=e.target;const r=n.value;const o=a.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(a.delayed){clearTimeout(a.delayed)}if(a.throttle){return}if(u.throttle>0){if(!a.throttle){ae(l,"htmx:trigger");c(l,e);a.throttle=b().setTimeout(function(){a.throttle=null},u.throttle)}}else if(u.delay>0){a.delayed=b().setTimeout(function(){ae(l,"htmx:trigger");c(l,e)},u.delay)}else{ae(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let yt=null;function xt(){if(!yt){yt=function(){mt=true};window.addEventListener("scroll",yt);window.addEventListener("resize",yt);setInterval(function(){if(mt){mt=false;ie(te().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&F(e)){e.setAttribute("data-hx-revealed","true");const t=oe(e);if(t.initHash){ae(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){ae(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;ae(e,"htmx:trigger");t(e)}};if(r>0){b().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;ie(de,function(r){if(s(t,"hx-"+r)){const o=a(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){xt();gt(r,n,t,e);bt(ce(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ue(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e<t.length;e++){const n=t[e];if(n.isIntersecting){ae(r,"intersect");break}}},o);i.observe(ce(r));gt(ce(r),n,t,e)}else if(!t.firstInitCompleted&&e.trigger==="load"){if(!pt(e,r,Bt("load",{elt:r}))){vt(ce(r),n,t,e.delay)}}else if(e.pollInterval>0){t.polling=true;ct(ce(r),n,e)}else{gt(r,n,t,e)}}function Et(e){const t=ce(e);if(!t){return false}const n=t.attributes;for(let e=0;e<n.length;e++){const r=n[e].name;if(l(r,"hx-on:")||l(r,"data-hx-on:")||l(r,"hx-on-")||l(r,"data-hx-on-")){return true}}return false}const Ct=(new XPathEvaluator).createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or'+' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]');function Ot(e,t){if(Et(e)){t.push(ce(e))}const n=Ct.evaluate(e);let r=null;while(r=n.iterateNext())t.push(ce(r))}function Rt(e){const t=[];if(e instanceof DocumentFragment){for(const n of e.childNodes){Ot(n,t)}}else{Ot(e,t)}return t}function Ht(e){if(e.querySelectorAll){const n=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]";const r=[];for(const i in Vn){const s=Vn[i];if(s.getSelectors){var t=s.getSelectors();if(t){r.push(t)}}}const o=e.querySelectorAll(T+n+", form, [type='submit'],"+" [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"+r.flat().map(e=>", "+e).join(""));return o}else{return[]}}function Tt(e){const t=At(e.target);const n=Nt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Nt(e);if(t){t.lastButtonClicked=null}}function At(e){return g(ce(e),"button, input[type='submit']")}function Lt(e){return e.form||g(e,"form")}function Nt(e){const t=At(e.target);if(!t){return}const n=Lt(t);return oe(n)}function It(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function Pt(t,e,n){const r=oe(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){On(t,function(){if(ft(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function Dt(t){De(t);for(let e=0;e<t.attributes.length;e++){const n=t.attributes[e].name;const r=t.attributes[e].value;if(l(n,"hx-on")||l(n,"data-hx-on")){const o=n.indexOf("-on")+3;const i=n.slice(o,o+1);if(i==="-"||i===":"){let e=n.slice(o+1);if(l(e,":")){e="htmx"+e}else if(l(e,"-")){e="htmx:"+e.slice(1)}else if(l(e,"htmx-")){e="htmx:"+e.slice(5)}Pt(t,e,r)}}}}function kt(t){ae(t,"htmx:beforeProcessNode");const n=oe(t);const e=st(t);const r=wt(t,n,e);if(!r){if(ne(t,"hx-boost")==="true"){at(t,n,e)}else if(s(t,"hx-trigger")){e.forEach(function(e){St(t,e,n,function(){})})}}if(t.tagName==="FORM"||ee(t,"type")==="submit"&&s(t,"form")){It(t)}n.firstInitCompleted=true;ae(t,"htmx:afterProcessNode")}function Mt(e){if(!(e instanceof Element)){return false}const t=oe(e);const n=Pe(e);if(t.initHash!==n){ke(e);t.initHash=n;return true}return false}function Ft(e){e=w(e);if(ft(e)){S(e);return}const t=[];if(Mt(e)){t.push(e)}ie(Ht(e),function(e){if(ft(e)){S(e);return}if(Mt(e)){t.push(e)}});ie(Rt(e),Dt);ie(t,kt)}function Xt(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Bt(e,t){return new CustomEvent(e,{bubbles:true,cancelable:true,composed:true,detail:t})}function fe(e,t,n){ae(e,t,le({error:t},n))}function Ut(e){return e==="htmx:afterProcessNode"}function jt(e,t,n){ie(Jn(e,[],n),function(e){try{t(e)}catch(e){R(e)}})}function R(e){console.error(e)}function ae(e,t,n){e=w(e);if(n==null){n={}}n.elt=e;const r=Bt(t,n);if(Q.logger&&!Ut(t)){Q.logger(e,t,n)}if(n.error){R(n.error);ae(e,"htmx:error",{errorInfo:n})}let o=e.dispatchEvent(r);const i=Xt(t);if(o&&i!==t){const s=Bt(i,r.detail);o=o&&e.dispatchEvent(s)}jt(ce(e),function(e){o=o&&(e.onEvent(t,r)!==false&&!r.defaultPrevented)});return o}let Vt=location.pathname+location.search;function _t(e){Vt=e;if(B()){sessionStorage.setItem("htmx-current-path-for-history",e)}}function zt(){const e=te().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||te().body}function $t(t,e){if(!B()){return}const n=Kt(e);const r=te().title;const o=window.scrollY;if(Q.config.historyCacheSize<=0){sessionStorage.removeItem("htmx-history-cache");return}t=U(t);const i=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<i.length;e++){if(i[e].url===t){i.splice(e,1);break}}const s={url:t,content:n,title:r,scroll:o};ae(te().body,"htmx:historyItemCreated",{item:s,cache:i});i.push(s);while(i.length>Q.config.historyCacheSize){i.shift()}while(i.length>0){try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Jt(t){if(!B()){return null}t=U(t);const n=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<n.length;e++){if(n[e].url===t){return n[e]}}return null}function Kt(e){const t=Q.config.requestClass;const n=e.cloneNode(true);ie(x(n,"."+t),function(e){G(e,t)});ie(x(n,"[data-disabled-by-htmx]"),function(e){e.removeAttribute("disabled")});return n.innerHTML}function Gt(){const e=zt();let t=Vt;if(B()){t=sessionStorage.getItem("htmx-current-path-for-history")}t=t||location.pathname+location.search;const n=te().querySelector('[hx-history="false" i],[data-hx-history="false" i]');if(!n){ae(te().body,"htmx:beforeHistorySave",{path:t,historyElt:e});$t(t,e)}if(Q.config.historyEnabled)history.replaceState({htmx:true},te().title,location.href)}function Wt(e){if(Q.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(Y(e,"&")||Y(e,"?")){e=e.slice(0,-1)}}if(Q.config.historyEnabled){history.pushState({htmx:true},"",e)}_t(e)}function Zt(e){if(Q.config.historyEnabled)history.replaceState({htmx:true},"",e);_t(e)}function Yt(e){ie(e,function(e){e.call(undefined)})}function Qt(e){const t=new XMLHttpRequest;const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0};const r={path:e,xhr:t,historyElt:zt(),swapSpec:n};t.open("GET",e,true);if(Q.config.historyRestoreAsHxRequest){t.setRequestHeader("HX-Request","true")}t.setRequestHeader("HX-History-Restore-Request","true");t.setRequestHeader("HX-Current-URL",location.href);t.onload=function(){if(this.status>=200&&this.status<400){r.response=this.response;ae(te().body,"htmx:historyCacheMissLoad",r);$e(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:true});_t(r.path);ae(te().body,"htmx:historyRestore",{path:e,cacheMiss:true,serverResponse:r.response})}else{fe(te().body,"htmx:historyCacheMissLoadError",r)}};if(ae(te().body,"htmx:historyCacheMiss",r)){t.send()}}function en(e){Gt();e=e||location.pathname+location.search;const t=Jt(e);if(t){const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll};const r={path:e,item:t,historyElt:zt(),swapSpec:n};if(ae(te().body,"htmx:historyCacheHit",r)){$e(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title});_t(r.path);ae(te().body,"htmx:historyRestore",r)}}else{if(Q.config.refreshOnHistoryMiss){Q.location.reload(true)}else{Qt(e)}}}function tn(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function nn(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function rn(e,t){ie(e.concat(t),function(e){const t=oe(e);t.requestCount=(t.requestCount||1)-1});ie(e,function(e){const t=oe(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});ie(t,function(e){const t=oe(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function on(t,n){for(let e=0;e<t.length;e++){const r=t[e];if(r.isSameNode(n)){return true}}return false}function sn(e){const t=e;if(t.name===""||t.name==null||t.disabled||g(t,"fieldset[disabled]")){return false}if(t.type==="button"||t.type==="submit"||t.tagName==="image"||t.tagName==="reset"||t.tagName==="file"){return false}if(t.type==="checkbox"||t.type==="radio"){return t.checked}return true}function ln(t,e,n){if(t!=null&&e!=null){if(Array.isArray(e)){e.forEach(function(e){n.append(t,e)})}else{n.append(t,e)}}}function cn(t,n,r){if(t!=null&&n!=null){let e=r.getAll(t);if(Array.isArray(n)){e=e.filter(e=>n.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);ie(e,e=>r.append(t,e))}}function un(e){if(e instanceof HTMLSelectElement&&e.multiple){return M(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e instanceof HTMLInputElement&&e.files){return M(e.files)}return e.value}function fn(t,n,r,e,o){if(e==null||on(t,e)){return}else{t.push(e)}if(sn(e)){const i=ee(e,"name");ln(i,un(e),n);if(o){an(e,r)}}if(e instanceof HTMLFormElement){ie(e.elements,function(e){if(t.indexOf(e)>=0){cn(e.name,un(e),n)}else{t.push(e)}if(o){an(e,r)}});new FormData(e).forEach(function(e,t){if(e instanceof File&&e.name===""){return}ln(t,e,n)})}}function an(e,t){const n=e;if(n.willValidate){ae(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});ae(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function hn(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function dn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=oe(e);if(s.lastButtonClicked&&!se(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||a(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){fn(n,o,i,Lt(e),l)}fn(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const f=ee(u,"name");ln(f,u.value,o)}const c=we(e,"hx-include");ie(c,function(e){fn(n,r,i,ce(e),l);if(!h(e,"form")){ie(p(e).querySelectorAll(ot),function(e){fn(n,r,i,e,l)})}});hn(r,o);return{errors:i,formData:r,values:kn(r)}}function pn(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function gn(e){e=Pn(e);let n="";e.forEach(function(e,t){n=pn(n,t,e)});return n}function mn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":a(t,"id"),"HX-Current-URL":location.href};Cn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(oe(e).boosted){r["HX-Boosted"]="true"}return r}function yn(n,e){const t=ne(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){ie(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;ie(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function xn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function bn(e,t){const n=t||ne(e,"hx-swap");const r={swapStyle:oe(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&oe(e).boosted&&!xn(e)){r.show="top"}if(n){const s=X(n);if(s.length>0){for(let e=0;e<s.length;e++){const l=s[e];if(l.indexOf("swap:")===0){r.swapDelay=d(l.slice(5))}else if(l.indexOf("settle:")===0){r.settleDelay=d(l.slice(7))}else if(l.indexOf("transition:")===0){r.transition=l.slice(11)==="true"}else if(l.indexOf("ignoreTitle:")===0){r.ignoreTitle=l.slice(12)==="true"}else if(l.indexOf("scroll:")===0){const c=l.slice(7);var o=c.split(":");const u=o.pop();var i=o.length>0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const f=l.slice(5);var o=f.split(":");const a=o.pop();var i=o.length>0?o.join(":"):null;r.show=a;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{R("Unknown modifier in hx-swap: "+l)}}}}return r}function vn(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function wn(t,n,r){let o=null;jt(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(vn(n)){return hn(new FormData,Pn(r))}else{return gn(r)}}}function Sn(e){return{tasks:[],elts:[e]}}function En(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ce(ue(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}if(typeof t.scroll==="number"){b().setTimeout(function(){window.scrollTo(0,t.scroll)},0)}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ce(ue(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Cn(r,e,o,i,s){if(i==null){i={}}if(r==null){return i}const l=a(r,e);if(l){let e=l.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=On(r,function(){if(s){return Function("event","return ("+e+")").call(r,s)}else{return Function("return ("+e+")").call(r)}},{})}else{n=v(e)}for(const c in n){if(n.hasOwnProperty(c)){if(i[c]==null){i[c]=n[c]}}}}return Cn(ce(u(r)),e,o,i,s)}function On(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function Rn(e,t,n){return Cn(e,"hx-vars",true,n,t)}function Hn(e,t,n){return Cn(e,"hx-vals",false,n,t)}function Tn(e,t){return le(Rn(e,t),Hn(e,t))}function qn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function An(t){if(t.responseURL){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function H(e,t){return t.test(e.getAllResponseHeaders())}function Ln(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:w(r)||ve,returnPromise:true})}else{let e=w(r.target);if(r.target&&!e||r.source&&!e&&!w(r.source)){e=ve}return he(t,n,w(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(t,n,null,null,{returnPromise:true})}}function Nn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function In(e,t,n){const r=new URL(t,location.protocol!=="about:"?location.href:window.origin);const o=location.protocol!=="about:"?location.origin:window.origin;const i=o===r.origin;if(Q.config.selfRequestsOnly){if(!i){return false}}return ae(e,"htmx:validateUrl",le({url:r,sameHost:i},n))}function Pn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Dn(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function kn(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Dn(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,k){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=te().body}const M=i.handler||jn;const F=i.select||null;if(!se(r)){re(s);return e}const c=i.targetOverride||ce(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:ne(r,"hx-target")});re(l);return e}let u=oe(r);const f=u.lastButtonClicked;if(f){const A=ee(f,"formaction");if(A!=null){n=A}const L=ee(f,"formmethod");if(L!=null){if(de.includes(L.toLowerCase())){t=L}else{re(s);return e}}}const a=ne(r,"hx-confirm");if(k===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:a};if(ae(r,"htmx:confirm",G)===false){re(s);return e}}let h=r;let d=ne(r,"hx-sync");let p=null;let X=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ce(ue(r,I))}d=(N[1]||"drop").trim();u=oe(h);if(d==="drop"&&u.xhr&&u.abortable!==true){re(s);return e}else if(d==="abort"){if(u.xhr){re(s);return e}else{X=true}}else if(d==="replace"){ae(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");p=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){ae(h,"htmx:abort")}else{if(p==null){if(o){const P=oe(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){p=P.triggerSpec.queue}}if(p==null){p="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(p==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="all"){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){he(t,n,r,o,i)})}re(s);return e}}const g=new XMLHttpRequest;u.xhr=g;u.abortable=X;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=ne(r,"hx-prompt");if(B){var y=prompt(B);if(y===null||!ae(r,"htmx:prompt",{prompt:y,target:c})){re(s);m();return e}}if(a&&!k){if(!confirm(a)){re(s);m();return e}}let x=mn(r,c,y);if(t!=="get"&&!vn(r)){x["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){x=le(x,i.headers)}const U=dn(r,t);let b=U.errors;const j=U.formData;if(i.values){hn(j,Pn(i.values))}const V=Pn(Tn(r,o));const v=hn(j,V);let w=yn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=location.href}const S=Cn(r,"hx-request");const _=oe(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:kn(w),unfilteredFormData:v,unfilteredParameters:kn(v),headers:x,elt:r,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!ae(r,"htmx:configRequest",C)){re(s);m();return e}n=C.path;t=C.verb;x=C.headers;w=Pn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){ae(r,"htmx:validation:halted",C);re(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=gn(w);if(O){R+="#"+O}}}if(!In(r,R,C)){fe(r,"htmx:invalidPath",C);re(l);m();return e}g.open(t.toUpperCase(),R,true);g.overrideMimeType("text/html");g.withCredentials=C.withCredentials;g.timeout=C.timeout;if(S.noHeaders){}else{for(const D in x){if(x.hasOwnProperty(D)){const Y=x[D];qn(g,D,Y)}}}const H={xhr:g,target:c,requestConfig:C,etc:i,boosted:_,select:F,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};g.onload=function(){try{const t=Nn(r);H.pathInfo.responsePath=An(g);M(r,H);if(H.keepIndicators!==true){rn(T,q)}ae(r,"htmx:afterRequest",H);ae(r,"htmx:afterOnLoad",H);if(!se(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(se(n)){e=n}}if(e){ae(e,"htmx:afterRequest",H);ae(e,"htmx:afterOnLoad",H)}}re(s)}catch(e){fe(r,"htmx:onLoadError",le({error:e},H));throw e}finally{m()}};g.onerror=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);re(l);m()};g.onabort=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);re(l);m()};g.ontimeout=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);re(l);m()};if(!ae(r,"htmx:beforeRequest",H)){re(s);m();return e}var T=tn(r);var q=nn(r);ie(["loadstart","loadend","progress","abort"],function(t){ie([g,g.upload],function(e){e.addEventListener(t,function(e){ae(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ae(r,"htmx:beforeSend",H);const J=E?null:wn(g,r,w);g.send(J);return e}function Mn(e,t){const n=t.xhr;let r=null;let o=null;if(H(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(H(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(H(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=ne(e,"hx-push-url");const c=ne(e,"hx-replace-url");const u=oe(e).boosted;let f=null;let a=null;if(l){f="push";a=l}else if(c){f="replace";a=c}else if(u){f="push";a=s||i}if(a){if(a==="false"){return{}}if(a==="true"){a=s||i}if(t.pathInfo.anchor&&a.indexOf("#")===-1){a=a+"#"+t.pathInfo.anchor}return{type:f,path:a}}else{return{}}}function Fn(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Xn(e){for(var t=0;t<Q.config.responseHandling.length;t++){var n=Q.config.responseHandling[t];if(Fn(n,e.status)){return n}}return{swap:false}}function Bn(e){if(e){const t=f("title");if(t){t.textContent=e}else{window.document.title=e}}}function Un(e,t){if(t==="this"){return e}const n=ce(ue(e,t));if(n==null){fe(e,"htmx:targetError",{target:t});throw new Error(`Invalid re-target ${t}`)}return n}function jn(t,e){const n=e.xhr;let r=e.target;const o=e.etc;const i=e.select;if(!ae(t,"htmx:beforeOnLoad",e))return;if(H(n,/HX-Trigger:/i)){Je(n,"HX-Trigger",t)}if(H(n,/HX-Location:/i)){Gt();let e=n.getResponseHeader("HX-Location");var s;if(e.indexOf("{")===0){s=v(e);e=s.path;delete s.path}Ln("get",e,s).then(function(){Wt(e)});return}const l=H(n,/HX-Refresh:/i)&&n.getResponseHeader("HX-Refresh")==="true";if(H(n,/HX-Redirect:/i)){e.keepIndicators=true;Q.location.href=n.getResponseHeader("HX-Redirect");l&&Q.location.reload();return}if(l){e.keepIndicators=true;Q.location.reload();return}const c=Mn(t,e);const u=Xn(n);const f=u.swap;let a=!!u.error;let h=Q.config.ignoreTitle||u.ignoreTitle;let d=u.select;if(u.target){e.target=Un(t,u.target)}var p=o.swapOverride;if(p==null&&u.swapOverride){p=u.swapOverride}if(H(n,/HX-Retarget:/i)){e.target=Un(t,n.getResponseHeader("HX-Retarget"))}if(H(n,/HX-Reswap:/i)){p=n.getResponseHeader("HX-Reswap")}var g=n.response;var m=le({shouldSwap:f,serverResponse:g,isError:a,ignoreTitle:h,selectOverride:d,swapOverride:p},e);if(u.event&&!ae(r,u.event,m))return;if(!ae(r,"htmx:beforeSwap",m))return;r=m.target;g=m.serverResponse;a=m.isError;h=m.ignoreTitle;d=m.selectOverride;p=m.swapOverride;e.target=r;e.failed=a;e.successful=!a;if(m.shouldSwap){if(n.status===286){lt(t)}jt(t,function(e){g=e.transformResponse(g,n,t)});if(c.type){Gt()}var y=bn(t,p);if(!y.hasOwnProperty("ignoreTitle")){y.ignoreTitle=h}r.classList.add(Q.config.swappingClass);if(i){d=i}if(H(n,/HX-Reselect:/i)){d=n.getResponseHeader("HX-Reselect")}const x=ne(t,"hx-select-oob");const b=ne(t,"hx-select");$e(r,g,y,{select:d==="unset"?null:d||b,selectOOB:x,eventInfo:e,anchor:e.pathInfo.anchor,contextElement:t,afterSwapCallback:function(){if(H(n,/HX-Trigger-After-Swap:/i)){let e=t;if(!se(t)){e=te().body}Je(n,"HX-Trigger-After-Swap",e)}},afterSettleCallback:function(){if(H(n,/HX-Trigger-After-Settle:/i)){let e=t;if(!se(t)){e=te().body}Je(n,"HX-Trigger-After-Settle",e)}},beforeSwapCallback:function(){if(c.type){ae(te().body,"htmx:beforeHistoryUpdate",le({history:c},e));if(c.type==="push"){Wt(c.path);ae(te().body,"htmx:pushedIntoHistory",{path:c.path})}else{Zt(c.path);ae(te().body,"htmx:replacedInHistory",{path:c.path})}}}})}if(a){fe(t,"htmx:responseError",le({error:"Response Status Error Code "+n.status+" from "+e.pathInfo.requestPath},e))}}const Vn={};function _n(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function zn(e,t){if(t.init){t.init(n)}Vn[e]=le(_n(),t)}function $n(e){delete Vn[e]}function Jn(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=a(e,"hx-ext");if(t){ie(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Vn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Jn(ce(u(e)),n,r)}var Kn=false;te().addEventListener("DOMContentLoaded",function(){Kn=true});function Gn(e){if(Kn||te().readyState==="complete"){e()}else{te().addEventListener("DOMContentLoaded",e)}}function Wn(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";te().head.insertAdjacentHTML("beforeend","<style"+e+"> ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} </style>")}}function Zn(){const e=te().querySelector('meta[name="htmx-config"]');if(e){return v(e.content)}else{return null}}function Yn(){const e=Zn();if(e){Q.config=le(Q.config,e)}}Gn(function(){Yn();Wn();let e=te().body;Ft(e);const t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=oe(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){en();ie(t,function(e){ae(e,"htmx:restored",{document:te(),triggerEvent:ae})})}else{if(n){n(e)}}};b().setTimeout(function(){ae(e,"htmx:load",{});e=null},0)});return Q}();
@@ -0,0 +1,290 @@
1
+ /*
2
+ Server Sent Events Extension
3
+ ============================
4
+ This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
5
+
6
+ */
7
+
8
+ (function() {
9
+ /** @type {import("../htmx").HtmxInternalApi} */
10
+ var api
11
+
12
+ htmx.defineExtension('sse', {
13
+
14
+ /**
15
+ * Init saves the provided reference to the internal HTMX API.
16
+ *
17
+ * @param {import("../htmx").HtmxInternalApi} api
18
+ * @returns void
19
+ */
20
+ init: function(apiRef) {
21
+ // store a reference to the internal API.
22
+ api = apiRef
23
+
24
+ // set a function in the public API for creating new EventSource objects
25
+ if (htmx.createEventSource == undefined) {
26
+ htmx.createEventSource = createEventSource
27
+ }
28
+ },
29
+
30
+ getSelectors: function() {
31
+ return ['[sse-connect]', '[data-sse-connect]', '[sse-swap]', '[data-sse-swap]']
32
+ },
33
+
34
+ /**
35
+ * onEvent handles all events passed to this extension.
36
+ *
37
+ * @param {string} name
38
+ * @param {Event} evt
39
+ * @returns void
40
+ */
41
+ onEvent: function(name, evt) {
42
+ var parent = evt.target || evt.detail.elt
43
+ switch (name) {
44
+ case 'htmx:beforeCleanupElement':
45
+ var internalData = api.getInternalData(parent)
46
+ // Try to remove remove an EventSource when elements are removed
47
+ var source = internalData.sseEventSource
48
+ if (source) {
49
+ api.triggerEvent(parent, 'htmx:sseClose', {
50
+ source,
51
+ type: 'nodeReplaced',
52
+ })
53
+ internalData.sseEventSource.close()
54
+ }
55
+
56
+ return
57
+
58
+ // Try to create EventSources when elements are processed
59
+ case 'htmx:afterProcessNode':
60
+ ensureEventSourceOnElement(parent)
61
+ }
62
+ }
63
+ })
64
+
65
+ /// ////////////////////////////////////////////
66
+ // HELPER FUNCTIONS
67
+ /// ////////////////////////////////////////////
68
+
69
+ /**
70
+ * createEventSource is the default method for creating new EventSource objects.
71
+ * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
72
+ *
73
+ * @param {string} url
74
+ * @returns EventSource
75
+ */
76
+ function createEventSource(url) {
77
+ return new EventSource(url, { withCredentials: true })
78
+ }
79
+
80
+ /**
81
+ * registerSSE looks for attributes that can contain sse events, right
82
+ * now hx-trigger and sse-swap and adds listeners based on these attributes too
83
+ * the closest event source
84
+ *
85
+ * @param {HTMLElement} elt
86
+ */
87
+ function registerSSE(elt) {
88
+ // Add message handlers for every `sse-swap` attribute
89
+ if (api.getAttributeValue(elt, 'sse-swap')) {
90
+ // Find closest existing event source
91
+ var sourceElement = api.getClosestMatch(elt, hasEventSource)
92
+ if (sourceElement == null) {
93
+ // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
94
+ return null // no eventsource in parentage, orphaned element
95
+ }
96
+
97
+ // Set internalData and source
98
+ var internalData = api.getInternalData(sourceElement)
99
+ var source = internalData.sseEventSource
100
+
101
+ var sseSwapAttr = api.getAttributeValue(elt, 'sse-swap')
102
+ var sseEventNames = sseSwapAttr.split(',')
103
+
104
+ for (var i = 0; i < sseEventNames.length; i++) {
105
+ const sseEventName = sseEventNames[i].trim()
106
+ const listener = function(event) {
107
+ // If the source is missing then close SSE
108
+ if (maybeCloseSSESource(sourceElement)) {
109
+ return
110
+ }
111
+
112
+ // If the body no longer contains the element, remove the listener
113
+ if (!api.bodyContains(elt)) {
114
+ source.removeEventListener(sseEventName, listener)
115
+ return
116
+ }
117
+
118
+ // swap the response into the DOM and trigger a notification
119
+ if (!api.triggerEvent(elt, 'htmx:sseBeforeMessage', event)) {
120
+ return
121
+ }
122
+ swap(elt, event.data)
123
+ api.triggerEvent(elt, 'htmx:sseMessage', event)
124
+ }
125
+
126
+ // Register the new listener
127
+ api.getInternalData(elt).sseEventListener = listener
128
+ source.addEventListener(sseEventName, listener)
129
+ }
130
+ }
131
+
132
+ // Add message handlers for every `hx-trigger="sse:*"` attribute
133
+ if (api.getAttributeValue(elt, 'hx-trigger')) {
134
+ // Find closest existing event source
135
+ var sourceElement = api.getClosestMatch(elt, hasEventSource)
136
+ if (sourceElement == null) {
137
+ // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
138
+ return null // no eventsource in parentage, orphaned element
139
+ }
140
+
141
+ // Set internalData and source
142
+ var internalData = api.getInternalData(sourceElement)
143
+ var source = internalData.sseEventSource
144
+
145
+ var triggerSpecs = api.getTriggerSpecs(elt)
146
+ triggerSpecs.forEach(function(ts) {
147
+ if (ts.trigger.slice(0, 4) !== 'sse:') {
148
+ return
149
+ }
150
+
151
+ var listener = function (event) {
152
+ if (maybeCloseSSESource(sourceElement)) {
153
+ return
154
+ }
155
+ if (!api.bodyContains(elt)) {
156
+ source.removeEventListener(ts.trigger.slice(4), listener)
157
+ }
158
+ // Trigger events to be handled by the rest of htmx
159
+ htmx.trigger(elt, ts.trigger, event)
160
+ htmx.trigger(elt, 'htmx:sseMessage', event)
161
+ }
162
+
163
+ // Register the new listener
164
+ api.getInternalData(elt).sseEventListener = listener
165
+ source.addEventListener(ts.trigger.slice(4), listener)
166
+ })
167
+ }
168
+ }
169
+
170
+ /**
171
+ * ensureEventSourceOnElement creates a new EventSource connection on the provided element.
172
+ * If a usable EventSource already exists, then it is returned. If not, then a new EventSource
173
+ * is created and stored in the element's internalData.
174
+ * @param {HTMLElement} elt
175
+ * @param {number} retryCount
176
+ * @returns {EventSource | null}
177
+ */
178
+ function ensureEventSourceOnElement(elt, retryCount) {
179
+ if (elt == null) {
180
+ return null
181
+ }
182
+
183
+ // handle extension source creation attribute
184
+ if (api.getAttributeValue(elt, 'sse-connect')) {
185
+ var sseURL = api.getAttributeValue(elt, 'sse-connect')
186
+ if (sseURL == null) {
187
+ return
188
+ }
189
+
190
+ ensureEventSource(elt, sseURL, retryCount)
191
+ }
192
+
193
+ registerSSE(elt)
194
+ }
195
+
196
+ function ensureEventSource(elt, url, retryCount) {
197
+ var source = htmx.createEventSource(url)
198
+
199
+ source.onerror = function(err) {
200
+ // Log an error event
201
+ api.triggerErrorEvent(elt, 'htmx:sseError', { error: err, source })
202
+
203
+ // If parent no longer exists in the document, then clean up this EventSource
204
+ if (maybeCloseSSESource(elt)) {
205
+ return
206
+ }
207
+
208
+ // Otherwise, try to reconnect the EventSource
209
+ if (source.readyState === EventSource.CLOSED) {
210
+ retryCount = retryCount || 0
211
+ retryCount = Math.max(Math.min(retryCount * 2, 128), 1)
212
+ var timeout = retryCount * 500
213
+ window.setTimeout(function() {
214
+ ensureEventSourceOnElement(elt, retryCount)
215
+ }, timeout)
216
+ }
217
+ }
218
+
219
+ source.onopen = function(evt) {
220
+ api.triggerEvent(elt, 'htmx:sseOpen', { source })
221
+
222
+ if (retryCount && retryCount > 0) {
223
+ const childrenToFix = elt.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]")
224
+ for (let i = 0; i < childrenToFix.length; i++) {
225
+ registerSSE(childrenToFix[i])
226
+ }
227
+ // We want to increase the reconnection delay for consecutive failed attempts only
228
+ retryCount = 0
229
+ }
230
+ }
231
+
232
+ api.getInternalData(elt).sseEventSource = source
233
+
234
+
235
+ var closeAttribute = api.getAttributeValue(elt, "sse-close");
236
+ if (closeAttribute) {
237
+ // close eventsource when this message is received
238
+ source.addEventListener(closeAttribute, function() {
239
+ api.triggerEvent(elt, 'htmx:sseClose', {
240
+ source,
241
+ type: 'message',
242
+ })
243
+ source.close()
244
+ });
245
+ }
246
+ }
247
+
248
+ /**
249
+ * maybeCloseSSESource confirms that the parent element still exists.
250
+ * If not, then any associated SSE source is closed and the function returns true.
251
+ *
252
+ * @param {HTMLElement} elt
253
+ * @returns boolean
254
+ */
255
+ function maybeCloseSSESource(elt) {
256
+ if (!api.bodyContains(elt)) {
257
+ var source = api.getInternalData(elt).sseEventSource
258
+ if (source != undefined) {
259
+ api.triggerEvent(elt, 'htmx:sseClose', {
260
+ source,
261
+ type: 'nodeMissing',
262
+ })
263
+ source.close()
264
+ // source = null
265
+ return true
266
+ }
267
+ }
268
+ return false
269
+ }
270
+
271
+
272
+ /**
273
+ * @param {HTMLElement} elt
274
+ * @param {string} content
275
+ */
276
+ function swap(elt, content) {
277
+ api.withExtensions(elt, function(extension) {
278
+ content = extension.transformResponse(content, null, elt)
279
+ })
280
+
281
+ var swapSpec = api.getSwapSpecification(elt)
282
+ var target = api.getTarget(elt)
283
+ api.swap(target, content, swapSpec)
284
+ }
285
+
286
+
287
+ function hasEventSource(node) {
288
+ return api.getInternalData(node).sseEventSource != null
289
+ }
290
+ })()
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <meta charset="UTF-8">
4
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
5
+ <title>Chart</title>
6
+ <script src="static/htmx.min.js"></script>
7
+ <script src="static/sse.js"></script>
8
+
9
+
10
+ <body>
11
+
12
+ <div hx-ext="sse" sse-connect="check">
13
+
14
+ <div style="margin-top:8vh" hx-get="getchart" hx-trigger="sse:newchart" hx-target=this>
15
+ <div>
16
+ % if chart:
17
+ <div>${chart|n}</div>
18
+ % else:
19
+ <p>No data yet: Waiting..</p>
20
+ % endif
21
+ </div>
22
+ </div>
23
+
24
+ </div>
25
+
26
+ </body>
27
+ </html>
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ ## notfound.html - The page showing a not found message
5
+
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>Not Found</title>
9
+
10
+ <body>
11
+
12
+ <div style="margin-top:8vh">
13
+ <h3>Sorry, the page you requested has not been found</h3>
14
+ </div>
15
+
16
+
17
+ </body>
18
+ </html>
@@ -0,0 +1,171 @@
1
+
2
+ import asyncio
3
+
4
+ import uvicorn
5
+
6
+ import minilineplot
7
+
8
+ from .web.app import make_app
9
+
10
+
11
+ class WebTimeLine:
12
+
13
+ def __init__(self, host='localhost', port=8000, basepath=None, hours=4, height=600, width=800, title="", description=""):
14
+ self._host = host
15
+ self._port = port
16
+ self._queue = asyncio.Queue()
17
+ self._mkchart = MakeChart(hours=hours, height=height, width=width, title=title, description=description, queue = self._queue)
18
+ self._server = None
19
+
20
+ # ensure basepath is either None, or a string with leading and tailing '/' characters
21
+ if basepath:
22
+ basepath = basepath.strip("/. ")
23
+ if basepath:
24
+ self._basepath = f"/{basepath}/"
25
+ else:
26
+ self._basepath = None
27
+
28
+
29
+
30
+ async def serve(self, tg):
31
+ app = make_app(self._basepath, self._mkchart)
32
+ config = uvicorn.Config(app=app, host=self._host, port=self._port, log_level="error")
33
+ self._server = uvicorn.Server(config)
34
+ tg.create_task( self._server.serve() )
35
+ tg.create_task( self._mkchart.run() )
36
+
37
+
38
+ async def putpoint(self, t, v):
39
+ "Put a point timestamp, value"
40
+ item = (t,v)
41
+ await self._queue.put(item)
42
+
43
+ def set_colors(self,
44
+ backcol = "white", # The background colour of the whole image
45
+ gridcol = "grey", # Color of the chart grid
46
+ axiscol = "black", # Color of axis, title and description
47
+ chartbackcol = "white", # Background colour of the chart
48
+ linecol = "blue" # Color of the line being plotted
49
+ ):
50
+ self._mkchart.set_colors(backcol, gridcol, axiscol, chartbackcol, linecol)
51
+
52
+
53
+ def set_title(self, title):
54
+ self._mkchart.set_title(title)
55
+
56
+ def set_description(self, description):
57
+ self._mkchart.set_description(description)
58
+
59
+ def set_y_axis(self, ymin, ymax, yintervals, yformat):
60
+ """If this is not called, an automatic y scaling will be used.
61
+ If it is called, then these values will be set, however if any y point
62
+ exceeds the values, then the chart will revert to auto-scaling.
63
+ If you wish to revert to autoscaling, call this with None values."""
64
+ self._mkchart.set_y_axis(ymin, ymax, yintervals, yformat)
65
+
66
+
67
+ class MakeChart:
68
+
69
+ def __init__(self, hours, height, width, title, description, queue):
70
+ self.hours = hours
71
+ self.height = height
72
+ self.width = width
73
+ self.title = title
74
+ self.description=description
75
+ self.queue = queue
76
+ self.backcol = "white"
77
+ self.gridcol = "grey"
78
+ self.axiscol = "black"
79
+ self.chartbackcol = "white"
80
+ self.linecol = "blue"
81
+ self.points = []
82
+ self.chart = None
83
+
84
+ self.ymin = None
85
+ self.ymax = None
86
+ self.yintervals = None
87
+ self.yformat = None
88
+
89
+ # this event is triggered when a chart event occurs
90
+ self.chart_event = asyncio.Event()
91
+
92
+ async def run(self):
93
+ "Creates the chart when a new point added"
94
+ while True:
95
+ item = await self.queue.get()
96
+ self.points.append(item)
97
+ last_t = item[0]
98
+ first_t = self.points[0][0]
99
+ tspan = last_t - first_t
100
+ plotted_span = self.hours * 3600
101
+ if first_t < last_t - plotted_span:
102
+ self.points.pop(0)
103
+ self.make_chart()
104
+
105
+
106
+ def make_chart(self):
107
+ if len(self.points)<2:
108
+ return
109
+ line = minilineplot.Line(values=self.points,
110
+ color = self.linecol)
111
+ self.chart = minilineplot.Axis(lines=[line],
112
+ imagewidth=self.width,
113
+ imageheight=self.height,
114
+ title = self.title,
115
+ description = self.description,
116
+ gridcol = self.gridcol,
117
+ axiscol = self.axiscol,
118
+ chartbackcol = self.chartbackcol,
119
+ backcol = self.backcol)
120
+ ymax = max(p[1] for p in self.points)
121
+ ymin = min(p[1] for p in self.points)
122
+ if self.ymin is None or self.ymax is None or self.yformat is None or self.yintervals is None :
123
+ self.chart.auto_y()
124
+ elif ymin<self.ymin or ymax>self.ymax:
125
+ self.chart.auto_y()
126
+ else:
127
+ self.chart.ymax = self.ymax
128
+ self.chart.ymin = self.ymin
129
+ self.chart.yintervals = self.yintervals
130
+ self.chart.yformat = self.yformat
131
+ self.chart.auto_time_x(hourspan = self.hours)
132
+ # flag a chart event
133
+ self.chart_event.set()
134
+ self.chart_event.clear()
135
+
136
+
137
+ def set_colors(self, backcol, gridcol, axiscol, chartbackcol, linecol):
138
+ self.backcol = backcol
139
+ self.gridcol = gridcol
140
+ self.axiscol = axiscol
141
+ self.chartbackcol = chartbackcol
142
+ self.linecol = linecol
143
+ self.make_chart()
144
+
145
+
146
+ def set_title(self, title):
147
+ self.title = title
148
+ self.make_chart()
149
+
150
+
151
+ def set_description(self, description):
152
+ self.description = description
153
+ self.make_chart()
154
+
155
+ def set_y_axis(self, ymin, ymax, yintervals, yformat):
156
+ """If this is not called, an automatic y scaling will be used.
157
+ If it is called, then these values will be set, however if any y point
158
+ exceeds the values, then the chart will revert to auto-scaling.
159
+ If you wish to revert to autoscaling, call this with None values."""
160
+ self.ymin = ymin
161
+ self.ymax = ymax
162
+ self.yintervals = yintervals
163
+ self.yformat = yformat
164
+ self.make_chart()
165
+
166
+
167
+
168
+
169
+
170
+
171
+