lektor-ai-chatbot 1.0.0__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.
- lektor_ai_chatbot-1.0.0/LICENSE +21 -0
- lektor_ai_chatbot-1.0.0/PKG-INFO +91 -0
- lektor_ai_chatbot-1.0.0/README.md +70 -0
- lektor_ai_chatbot-1.0.0/lektor_ai_chatbot.egg-info/PKG-INFO +91 -0
- lektor_ai_chatbot-1.0.0/lektor_ai_chatbot.egg-info/SOURCES.txt +11 -0
- lektor_ai_chatbot-1.0.0/lektor_ai_chatbot.egg-info/dependency_links.txt +1 -0
- lektor_ai_chatbot-1.0.0/lektor_ai_chatbot.egg-info/entry_points.txt +2 -0
- lektor_ai_chatbot-1.0.0/lektor_ai_chatbot.egg-info/top_level.txt +1 -0
- lektor_ai_chatbot-1.0.0/lektor_ai_chatbot.py +149 -0
- lektor_ai_chatbot-1.0.0/pyproject.toml +34 -0
- lektor_ai_chatbot-1.0.0/setup.cfg +4 -0
- lektor_ai_chatbot-1.0.0/setup.py +5 -0
- lektor_ai_chatbot-1.0.0/tests/test_plugin.py +401 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Asyntai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lektor-ai-chatbot
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Add the Asyntai AI chat assistant to your Lektor site. It answers visitor questions from your own content.
|
|
5
|
+
Author-email: Asyntai <hello@asyntai.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://asyntai.com/documentation/integrations/lektor/
|
|
8
|
+
Project-URL: Documentation, https://asyntai.com/documentation/integrations/lektor/
|
|
9
|
+
Project-URL: Source, https://github.com/asyntai/lektor-ai-chatbot
|
|
10
|
+
Project-URL: Issues, https://github.com/asyntai/lektor-ai-chatbot/issues
|
|
11
|
+
Keywords: lektor,lektor-plugin,chatbot,ai,chat,support,asyntai
|
|
12
|
+
Classifier: Framework :: Lektor
|
|
13
|
+
Classifier: Environment :: Plugins
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
|
|
16
|
+
Classifier: Topic :: Communications :: Chat
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# lektor-ai-chatbot
|
|
23
|
+
|
|
24
|
+
Add the [Asyntai](https://asyntai.com) AI chat assistant to your Lektor site.
|
|
25
|
+
It answers visitor questions from your own content, day and night.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
lektor plugins add lektor-ai-chatbot
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then create the file `configs/ai-chatbot.ini` in your project:
|
|
34
|
+
|
|
35
|
+
```ini
|
|
36
|
+
widget_id = asyntai_1a2b3c4d5e6f
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Build the site. The chat bubble is on every page.
|
|
40
|
+
|
|
41
|
+
## Where the widget ID comes from
|
|
42
|
+
|
|
43
|
+
Open the Install page in your [Asyntai dashboard](https://asyntai.com/dashboard).
|
|
44
|
+
The widget ID is the value of `data-asyntai-id` in the snippet shown there. It
|
|
45
|
+
starts with `asyntai_`.
|
|
46
|
+
|
|
47
|
+
Creating an account is free, and the Free plan is enough to try the widget.
|
|
48
|
+
|
|
49
|
+
## Options
|
|
50
|
+
|
|
51
|
+
All options go in `configs/ai-chatbot.ini`.
|
|
52
|
+
|
|
53
|
+
| Option | Default | What it does |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `widget_id` | required | Your Asyntai widget ID |
|
|
56
|
+
| `enable_on_server` | `true` | Set to `false` to keep the widget off the site during `lektor server` |
|
|
57
|
+
| `script_url` | the Asyntai CDN | Advanced. Load the widget from another URL |
|
|
58
|
+
|
|
59
|
+
A missing or malformed `widget_id` stops the build with a message that says how
|
|
60
|
+
to fix it.
|
|
61
|
+
|
|
62
|
+
## What the plugin does
|
|
63
|
+
|
|
64
|
+
- It adds one `<script>` tag in front of the closing body tag of every HTML
|
|
65
|
+
page that Lektor builds, including paginated pages and the 404 page.
|
|
66
|
+
- It marks the tag `async`, so your pages render first.
|
|
67
|
+
- It changes nothing else. No templates are overridden and no CSS is added.
|
|
68
|
+
- Feeds, sitemaps, CSS and images stay as they are. An HTML file in `assets/`
|
|
69
|
+
gets the widget only when it is a full page.
|
|
70
|
+
- If a page already has the Asyntai snippet, for example in a template, the
|
|
71
|
+
plugin leaves that page alone.
|
|
72
|
+
|
|
73
|
+
## Answering from your content
|
|
74
|
+
|
|
75
|
+
Asyntai reads your published pages. Add your site URL in the dashboard, and the
|
|
76
|
+
assistant answers from the same pages the widget sits on. You can also add
|
|
77
|
+
documents, and question and answer pairs.
|
|
78
|
+
|
|
79
|
+
## Removing it
|
|
80
|
+
|
|
81
|
+
Run `lektor plugins remove lektor-ai-chatbot` and build again. The widget is
|
|
82
|
+
gone.
|
|
83
|
+
|
|
84
|
+
## Support
|
|
85
|
+
|
|
86
|
+
Email [hello@asyntai.com](mailto:hello@asyntai.com), or read the
|
|
87
|
+
[Lektor guide](https://asyntai.com/documentation/integrations/lektor/).
|
|
88
|
+
|
|
89
|
+
## Licence
|
|
90
|
+
|
|
91
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# lektor-ai-chatbot
|
|
2
|
+
|
|
3
|
+
Add the [Asyntai](https://asyntai.com) AI chat assistant to your Lektor site.
|
|
4
|
+
It answers visitor questions from your own content, day and night.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
lektor plugins add lektor-ai-chatbot
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Then create the file `configs/ai-chatbot.ini` in your project:
|
|
13
|
+
|
|
14
|
+
```ini
|
|
15
|
+
widget_id = asyntai_1a2b3c4d5e6f
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Build the site. The chat bubble is on every page.
|
|
19
|
+
|
|
20
|
+
## Where the widget ID comes from
|
|
21
|
+
|
|
22
|
+
Open the Install page in your [Asyntai dashboard](https://asyntai.com/dashboard).
|
|
23
|
+
The widget ID is the value of `data-asyntai-id` in the snippet shown there. It
|
|
24
|
+
starts with `asyntai_`.
|
|
25
|
+
|
|
26
|
+
Creating an account is free, and the Free plan is enough to try the widget.
|
|
27
|
+
|
|
28
|
+
## Options
|
|
29
|
+
|
|
30
|
+
All options go in `configs/ai-chatbot.ini`.
|
|
31
|
+
|
|
32
|
+
| Option | Default | What it does |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `widget_id` | required | Your Asyntai widget ID |
|
|
35
|
+
| `enable_on_server` | `true` | Set to `false` to keep the widget off the site during `lektor server` |
|
|
36
|
+
| `script_url` | the Asyntai CDN | Advanced. Load the widget from another URL |
|
|
37
|
+
|
|
38
|
+
A missing or malformed `widget_id` stops the build with a message that says how
|
|
39
|
+
to fix it.
|
|
40
|
+
|
|
41
|
+
## What the plugin does
|
|
42
|
+
|
|
43
|
+
- It adds one `<script>` tag in front of the closing body tag of every HTML
|
|
44
|
+
page that Lektor builds, including paginated pages and the 404 page.
|
|
45
|
+
- It marks the tag `async`, so your pages render first.
|
|
46
|
+
- It changes nothing else. No templates are overridden and no CSS is added.
|
|
47
|
+
- Feeds, sitemaps, CSS and images stay as they are. An HTML file in `assets/`
|
|
48
|
+
gets the widget only when it is a full page.
|
|
49
|
+
- If a page already has the Asyntai snippet, for example in a template, the
|
|
50
|
+
plugin leaves that page alone.
|
|
51
|
+
|
|
52
|
+
## Answering from your content
|
|
53
|
+
|
|
54
|
+
Asyntai reads your published pages. Add your site URL in the dashboard, and the
|
|
55
|
+
assistant answers from the same pages the widget sits on. You can also add
|
|
56
|
+
documents, and question and answer pairs.
|
|
57
|
+
|
|
58
|
+
## Removing it
|
|
59
|
+
|
|
60
|
+
Run `lektor plugins remove lektor-ai-chatbot` and build again. The widget is
|
|
61
|
+
gone.
|
|
62
|
+
|
|
63
|
+
## Support
|
|
64
|
+
|
|
65
|
+
Email [hello@asyntai.com](mailto:hello@asyntai.com), or read the
|
|
66
|
+
[Lektor guide](https://asyntai.com/documentation/integrations/lektor/).
|
|
67
|
+
|
|
68
|
+
## Licence
|
|
69
|
+
|
|
70
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lektor-ai-chatbot
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Add the Asyntai AI chat assistant to your Lektor site. It answers visitor questions from your own content.
|
|
5
|
+
Author-email: Asyntai <hello@asyntai.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://asyntai.com/documentation/integrations/lektor/
|
|
8
|
+
Project-URL: Documentation, https://asyntai.com/documentation/integrations/lektor/
|
|
9
|
+
Project-URL: Source, https://github.com/asyntai/lektor-ai-chatbot
|
|
10
|
+
Project-URL: Issues, https://github.com/asyntai/lektor-ai-chatbot/issues
|
|
11
|
+
Keywords: lektor,lektor-plugin,chatbot,ai,chat,support,asyntai
|
|
12
|
+
Classifier: Framework :: Lektor
|
|
13
|
+
Classifier: Environment :: Plugins
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
|
|
16
|
+
Classifier: Topic :: Communications :: Chat
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# lektor-ai-chatbot
|
|
23
|
+
|
|
24
|
+
Add the [Asyntai](https://asyntai.com) AI chat assistant to your Lektor site.
|
|
25
|
+
It answers visitor questions from your own content, day and night.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
lektor plugins add lektor-ai-chatbot
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then create the file `configs/ai-chatbot.ini` in your project:
|
|
34
|
+
|
|
35
|
+
```ini
|
|
36
|
+
widget_id = asyntai_1a2b3c4d5e6f
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Build the site. The chat bubble is on every page.
|
|
40
|
+
|
|
41
|
+
## Where the widget ID comes from
|
|
42
|
+
|
|
43
|
+
Open the Install page in your [Asyntai dashboard](https://asyntai.com/dashboard).
|
|
44
|
+
The widget ID is the value of `data-asyntai-id` in the snippet shown there. It
|
|
45
|
+
starts with `asyntai_`.
|
|
46
|
+
|
|
47
|
+
Creating an account is free, and the Free plan is enough to try the widget.
|
|
48
|
+
|
|
49
|
+
## Options
|
|
50
|
+
|
|
51
|
+
All options go in `configs/ai-chatbot.ini`.
|
|
52
|
+
|
|
53
|
+
| Option | Default | What it does |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `widget_id` | required | Your Asyntai widget ID |
|
|
56
|
+
| `enable_on_server` | `true` | Set to `false` to keep the widget off the site during `lektor server` |
|
|
57
|
+
| `script_url` | the Asyntai CDN | Advanced. Load the widget from another URL |
|
|
58
|
+
|
|
59
|
+
A missing or malformed `widget_id` stops the build with a message that says how
|
|
60
|
+
to fix it.
|
|
61
|
+
|
|
62
|
+
## What the plugin does
|
|
63
|
+
|
|
64
|
+
- It adds one `<script>` tag in front of the closing body tag of every HTML
|
|
65
|
+
page that Lektor builds, including paginated pages and the 404 page.
|
|
66
|
+
- It marks the tag `async`, so your pages render first.
|
|
67
|
+
- It changes nothing else. No templates are overridden and no CSS is added.
|
|
68
|
+
- Feeds, sitemaps, CSS and images stay as they are. An HTML file in `assets/`
|
|
69
|
+
gets the widget only when it is a full page.
|
|
70
|
+
- If a page already has the Asyntai snippet, for example in a template, the
|
|
71
|
+
plugin leaves that page alone.
|
|
72
|
+
|
|
73
|
+
## Answering from your content
|
|
74
|
+
|
|
75
|
+
Asyntai reads your published pages. Add your site URL in the dashboard, and the
|
|
76
|
+
assistant answers from the same pages the widget sits on. You can also add
|
|
77
|
+
documents, and question and answer pairs.
|
|
78
|
+
|
|
79
|
+
## Removing it
|
|
80
|
+
|
|
81
|
+
Run `lektor plugins remove lektor-ai-chatbot` and build again. The widget is
|
|
82
|
+
gone.
|
|
83
|
+
|
|
84
|
+
## Support
|
|
85
|
+
|
|
86
|
+
Email [hello@asyntai.com](mailto:hello@asyntai.com), or read the
|
|
87
|
+
[Lektor guide](https://asyntai.com/documentation/integrations/lektor/).
|
|
88
|
+
|
|
89
|
+
## Licence
|
|
90
|
+
|
|
91
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
lektor_ai_chatbot.py
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.py
|
|
6
|
+
lektor_ai_chatbot.egg-info/PKG-INFO
|
|
7
|
+
lektor_ai_chatbot.egg-info/SOURCES.txt
|
|
8
|
+
lektor_ai_chatbot.egg-info/dependency_links.txt
|
|
9
|
+
lektor_ai_chatbot.egg-info/entry_points.txt
|
|
10
|
+
lektor_ai_chatbot.egg-info/top_level.txt
|
|
11
|
+
tests/test_plugin.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lektor_ai_chatbot
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Asyntai AI chatbot for Lektor.
|
|
3
|
+
|
|
4
|
+
Adds the Asyntai chat widget to every HTML page that Lektor builds. The plugin
|
|
5
|
+
writes the ordinary embed snippet in front of the closing body tag of each
|
|
6
|
+
page. Nothing else about the site changes: no templates are overridden and no
|
|
7
|
+
CSS is added.
|
|
8
|
+
|
|
9
|
+
Settings live in configs/ai-chatbot.ini:
|
|
10
|
+
|
|
11
|
+
widget_id = asyntai_1a2b3c4d5e6f
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import threading
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
from lektor.assets import Asset
|
|
20
|
+
from lektor.pluginsystem import Plugin
|
|
21
|
+
|
|
22
|
+
__version__ = "1.0.0"
|
|
23
|
+
|
|
24
|
+
DEFAULT_SCRIPT_URL = "https://widget.asyntai.com/static/js/chat-widget.js"
|
|
25
|
+
WIDGET_ID_PATTERN = re.compile(r"^asyntai_[A-Za-z0-9]+$")
|
|
26
|
+
HTML_SUFFIXES = (".html", ".htm")
|
|
27
|
+
|
|
28
|
+
# The last closing body tag of the page. A page can quote "</body>" inside a
|
|
29
|
+
# code block, so only the final one is the real end of the document.
|
|
30
|
+
_BODY_END = re.compile(rb"</body\s*>", re.IGNORECASE)
|
|
31
|
+
_MARKER = b"data-asyntai-id"
|
|
32
|
+
|
|
33
|
+
_FALSE = ("0", "false", "no", "off")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_tag(widget_id, script_url):
|
|
37
|
+
return '<script async src="{src}" data-asyntai-id="{wid}"></script>'.format(
|
|
38
|
+
src=script_url.replace("&", "&").replace('"', """),
|
|
39
|
+
wid=widget_id,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def insert_tag(html, tag):
|
|
44
|
+
"""Put the tag in front of the last </body>, or at the end if there is none.
|
|
45
|
+
|
|
46
|
+
Works on bytes, so the page keeps its own encoding and line endings.
|
|
47
|
+
"""
|
|
48
|
+
tag = tag.encode("utf-8")
|
|
49
|
+
matches = list(_BODY_END.finditer(html))
|
|
50
|
+
if not matches:
|
|
51
|
+
return html + tag
|
|
52
|
+
end = matches[-1].start()
|
|
53
|
+
return html[:end] + tag + b"\n" + html[end:]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def add_to_file(path, tag, full_page_only=False):
|
|
57
|
+
"""Add the tag to one built HTML file. Returns True if the file changed.
|
|
58
|
+
|
|
59
|
+
With full_page_only, a file without a closing body tag stays as it is.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
with open(path, "rb") as f:
|
|
63
|
+
html = f.read()
|
|
64
|
+
except OSError:
|
|
65
|
+
return False
|
|
66
|
+
# The site owner already pasted the snippet in a template. Two loaders
|
|
67
|
+
# would only fight over one chat bubble, so keep theirs. This also makes
|
|
68
|
+
# a second pass over the same file do nothing.
|
|
69
|
+
if _MARKER in html:
|
|
70
|
+
return False
|
|
71
|
+
if full_page_only and not _BODY_END.search(html):
|
|
72
|
+
return False
|
|
73
|
+
new_html = insert_tag(html, tag)
|
|
74
|
+
tmp = path + ".asyntai-tmp"
|
|
75
|
+
with open(tmp, "wb") as f:
|
|
76
|
+
f.write(new_html)
|
|
77
|
+
os.replace(tmp, path)
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def read_settings(config):
|
|
82
|
+
"""Check the ini values and return (tag, enable_on_server)."""
|
|
83
|
+
widget_id = (config.get("widget_id") or "").strip()
|
|
84
|
+
if not widget_id:
|
|
85
|
+
raise click.ClickException(
|
|
86
|
+
'lektor-ai-chatbot: "widget_id" is missing in configs/ai-chatbot.ini. '
|
|
87
|
+
"Copy it from the Install page of your Asyntai dashboard, for "
|
|
88
|
+
"example: widget_id = asyntai_1a2b3c4d5e6f"
|
|
89
|
+
)
|
|
90
|
+
if not WIDGET_ID_PATTERN.match(widget_id):
|
|
91
|
+
raise click.ClickException(
|
|
92
|
+
'lektor-ai-chatbot: "{0}" does not look like a widget ID. It starts '
|
|
93
|
+
"with asyntai_, for example asyntai_1a2b3c4d5e6f.".format(widget_id)
|
|
94
|
+
)
|
|
95
|
+
script_url = (config.get("script_url") or "").strip() or DEFAULT_SCRIPT_URL
|
|
96
|
+
on_server = (config.get("enable_on_server") or "true").strip().lower()
|
|
97
|
+
return build_tag(widget_id, script_url), on_server not in _FALSE
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _in_dev_server():
|
|
101
|
+
# `lektor server` builds in a BackgroundBuilder thread. Checking the
|
|
102
|
+
# thread is reliable even for the very first build, which starts before
|
|
103
|
+
# the server-spawn event.
|
|
104
|
+
try:
|
|
105
|
+
from lektor.devserver import BackgroundBuilder
|
|
106
|
+
except ImportError: # pragma: no cover
|
|
107
|
+
return False
|
|
108
|
+
return isinstance(threading.current_thread(), BackgroundBuilder)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AiChatbotPlugin(Plugin):
|
|
112
|
+
name = "AI Chatbot by Asyntai"
|
|
113
|
+
description = (
|
|
114
|
+
"Adds the Asyntai AI chat assistant to every page. It answers visitor "
|
|
115
|
+
"questions from your own content."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def __init__(self, env, id):
|
|
119
|
+
super().__init__(env, id)
|
|
120
|
+
self._server = False
|
|
121
|
+
|
|
122
|
+
def _settings(self):
|
|
123
|
+
# Read fresh each time, so an edit to the ini file during
|
|
124
|
+
# `lektor server` takes effect on the next build.
|
|
125
|
+
return read_settings(self.get_config(fresh=True))
|
|
126
|
+
|
|
127
|
+
def on_setup_env(self, **extra):
|
|
128
|
+
# Stop `lektor build` at once, with a clear message, when the ID is
|
|
129
|
+
# missing or wrong. Nothing is written in that case.
|
|
130
|
+
self._settings()
|
|
131
|
+
|
|
132
|
+
def on_server_spawn(self, **extra):
|
|
133
|
+
self._server = True
|
|
134
|
+
|
|
135
|
+
def on_after_build(self, builder, build_state, source, prog, **extra):
|
|
136
|
+
tag, on_server = self._settings()
|
|
137
|
+
if not on_server and (self._server or _in_dev_server()):
|
|
138
|
+
return
|
|
139
|
+
for artifact in build_state.updated_artifacts:
|
|
140
|
+
filename = artifact.dst_filename
|
|
141
|
+
if not filename.lower().endswith(HTML_SUFFIXES):
|
|
142
|
+
continue
|
|
143
|
+
# A file copied from assets/ can be an HTML fragment that a script
|
|
144
|
+
# loads into another page. Only a full page gets the widget.
|
|
145
|
+
add_to_file(
|
|
146
|
+
filename,
|
|
147
|
+
tag,
|
|
148
|
+
full_page_only=isinstance(artifact.source_obj, Asset),
|
|
149
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lektor-ai-chatbot"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Add the Asyntai AI chat assistant to your Lektor site. It answers visitor questions from your own content."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{name = "Asyntai", email = "hello@asyntai.com"}]
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
dependencies = []
|
|
15
|
+
keywords = ["lektor", "lektor-plugin", "chatbot", "ai", "chat", "support", "asyntai"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Framework :: Lektor",
|
|
18
|
+
"Environment :: Plugins",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Internet :: WWW/HTTP :: Site Management",
|
|
21
|
+
"Topic :: Communications :: Chat",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://asyntai.com/documentation/integrations/lektor/"
|
|
26
|
+
Documentation = "https://asyntai.com/documentation/integrations/lektor/"
|
|
27
|
+
Source = "https://github.com/asyntai/lektor-ai-chatbot"
|
|
28
|
+
Issues = "https://github.com/asyntai/lektor-ai-chatbot/issues"
|
|
29
|
+
|
|
30
|
+
[project.entry-points."lektor.plugins"]
|
|
31
|
+
ai-chatbot = "lektor_ai_chatbot:AiChatbotPlugin"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
py-modules = ["lektor_ai_chatbot"]
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for lektor-ai-chatbot.
|
|
3
|
+
|
|
4
|
+
They build small real Lektor sites with `lektor build` and `lektor server`.
|
|
5
|
+
Install Lektor and this package first:
|
|
6
|
+
|
|
7
|
+
pip install lektor .
|
|
8
|
+
python tests/test_plugin.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import socket
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import time
|
|
18
|
+
import urllib.request
|
|
19
|
+
|
|
20
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
21
|
+
sys.path.insert(0, os.path.dirname(HERE))
|
|
22
|
+
|
|
23
|
+
import lektor_ai_chatbot as plugin # noqa: E402
|
|
24
|
+
|
|
25
|
+
WIDGET_ID = "asyntai_1a2b3c4d5e6f"
|
|
26
|
+
TAG = plugin.build_tag(WIDGET_ID, plugin.DEFAULT_SCRIPT_URL)
|
|
27
|
+
LEKTOR = [sys.executable, "-m", "lektor"]
|
|
28
|
+
|
|
29
|
+
PAGE_TEMPLATE = """<!doctype html>
|
|
30
|
+
<html>
|
|
31
|
+
<head><meta charset="utf-8"><title>{{ this.title }}</title></head>
|
|
32
|
+
<body>
|
|
33
|
+
<h1>{{ this.title }}</h1>
|
|
34
|
+
{{ this.body }}
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# A template that already carries the snippet, as a site owner would paste it.
|
|
40
|
+
OWN_SNIPPET_TEMPLATE = """<!doctype html>
|
|
41
|
+
<html>
|
|
42
|
+
<head><title>{{ this.title }}</title></head>
|
|
43
|
+
<body>
|
|
44
|
+
{{ this.body }}
|
|
45
|
+
<script async src="https://widget.asyntai.com/static/js/chat-widget.js" data-asyntai-id="asyntai_owner1"></script>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
FEED_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
|
|
51
|
+
<feed><title>{{ this.title }}</title><entry></body></entry></feed>
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
BLOG_TEMPLATE = """<!doctype html>
|
|
55
|
+
<html><head><title>Blog</title></head>
|
|
56
|
+
<body>
|
|
57
|
+
{% for child in this.pagination.items %}<p>{{ child.title }}</p>{% endfor %}
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
PAGE_MODEL = """[model]
|
|
63
|
+
name = Page
|
|
64
|
+
label = {{ this.title }}
|
|
65
|
+
|
|
66
|
+
[fields.title]
|
|
67
|
+
type = string
|
|
68
|
+
|
|
69
|
+
[fields.body]
|
|
70
|
+
type = markdown
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
BLOG_MODEL = """[model]
|
|
74
|
+
name = Blog
|
|
75
|
+
label = Blog
|
|
76
|
+
|
|
77
|
+
[fields.title]
|
|
78
|
+
type = string
|
|
79
|
+
|
|
80
|
+
[children]
|
|
81
|
+
model = page
|
|
82
|
+
order_by = title
|
|
83
|
+
|
|
84
|
+
[pagination]
|
|
85
|
+
enabled = yes
|
|
86
|
+
per_page = 2
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def write(path, text, mode="w"):
|
|
91
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
92
|
+
with open(path, mode, encoding=None if "b" in mode else "utf-8", newline="") as f:
|
|
93
|
+
f.write(text)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def page(title, body="", template=None, slug=None):
|
|
97
|
+
parts = ["title: " + title, "body:\n\n" + body]
|
|
98
|
+
if template:
|
|
99
|
+
parts.append("_template: " + template)
|
|
100
|
+
if slug:
|
|
101
|
+
parts.append("_slug: " + slug)
|
|
102
|
+
return "\n---\n".join(parts) + "\n"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def make_site(root, ini=None):
|
|
106
|
+
write(os.path.join(root, "site.lektorproject"), "[project]\nname = Test\n")
|
|
107
|
+
write(os.path.join(root, "models", "page.ini"), PAGE_MODEL)
|
|
108
|
+
write(os.path.join(root, "models", "blog.ini"), BLOG_MODEL)
|
|
109
|
+
t = os.path.join(root, "templates")
|
|
110
|
+
write(os.path.join(t, "page.html"), PAGE_TEMPLATE)
|
|
111
|
+
write(os.path.join(t, "own.html"), OWN_SNIPPET_TEMPLATE)
|
|
112
|
+
write(os.path.join(t, "feed.xml"), FEED_TEMPLATE)
|
|
113
|
+
write(os.path.join(t, "blog.html"), BLOG_TEMPLATE)
|
|
114
|
+
c = os.path.join(root, "content")
|
|
115
|
+
write(os.path.join(c, "contents.lr"), page("Home", "Welcome."))
|
|
116
|
+
write(os.path.join(c, "about", "contents.lr"), page("About", "About us."))
|
|
117
|
+
write(
|
|
118
|
+
os.path.join(c, "code", "contents.lr"),
|
|
119
|
+
page("Code", "Close the page with:\n\n </body>\n\nThat is all."),
|
|
120
|
+
)
|
|
121
|
+
write(
|
|
122
|
+
os.path.join(c, "unicode", "contents.lr"),
|
|
123
|
+
page("Příliš žluťoučký kůň", "日本語のテキスト. Ünïcödé."),
|
|
124
|
+
)
|
|
125
|
+
write(os.path.join(c, "own", "contents.lr"), page("Own", "Mine.", template="own.html"))
|
|
126
|
+
write(
|
|
127
|
+
os.path.join(c, "feed", "contents.lr"),
|
|
128
|
+
page("Feed", template="feed.xml", slug="feed.xml"),
|
|
129
|
+
)
|
|
130
|
+
write(os.path.join(c, "404", "contents.lr"), page("Not found", "Gone.", slug="404.html"))
|
|
131
|
+
write(os.path.join(c, "blog", "contents.lr"), "_model: blog\n---\ntitle: Blog\n")
|
|
132
|
+
for n in range(1, 6):
|
|
133
|
+
write(
|
|
134
|
+
os.path.join(c, "blog", "post%d" % n, "contents.lr"),
|
|
135
|
+
page("Post %d" % n, "Text %d." % n),
|
|
136
|
+
)
|
|
137
|
+
a = os.path.join(root, "assets")
|
|
138
|
+
write(os.path.join(a, "static", "style.css"), "body { color: black; }\n")
|
|
139
|
+
write(os.path.join(a, "fragment.html"), "<div>Loaded by a script.</div>\n")
|
|
140
|
+
write(
|
|
141
|
+
os.path.join(a, "standalone.html"),
|
|
142
|
+
"<html><body><p>Static page.</p></body></html>\n",
|
|
143
|
+
)
|
|
144
|
+
if ini is not None:
|
|
145
|
+
write(os.path.join(root, "configs", "ai-chatbot.ini"), ini)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build(root, out):
|
|
149
|
+
return subprocess.run(
|
|
150
|
+
LEKTOR + ["--project", root, "build", "-O", out],
|
|
151
|
+
capture_output=True,
|
|
152
|
+
text=True,
|
|
153
|
+
encoding="utf-8",
|
|
154
|
+
errors="replace",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def read(out, *parts):
|
|
159
|
+
with open(os.path.join(out, *parts), "rb") as f:
|
|
160
|
+
return f.read().decode("utf-8")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def free_port():
|
|
164
|
+
with socket.socket() as s:
|
|
165
|
+
s.bind(("127.0.0.1", 0))
|
|
166
|
+
return s.getsockname()[1]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def fetch(url, timeout=60):
|
|
170
|
+
end = time.time() + timeout
|
|
171
|
+
last = None
|
|
172
|
+
while time.time() < end:
|
|
173
|
+
try:
|
|
174
|
+
with urllib.request.urlopen(url, timeout=5) as r:
|
|
175
|
+
return r.read().decode("utf-8")
|
|
176
|
+
except Exception as e: # server still starting or still building
|
|
177
|
+
last = e
|
|
178
|
+
time.sleep(0.5)
|
|
179
|
+
raise AssertionError("no answer from %s: %s" % (url, last))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class Server:
|
|
183
|
+
def __init__(self, root, out):
|
|
184
|
+
self.port = free_port()
|
|
185
|
+
self.out = out
|
|
186
|
+
self.proc = subprocess.Popen(
|
|
187
|
+
LEKTOR
|
|
188
|
+
+ ["--project", root, "server", "-h", "127.0.0.1", "-p", str(self.port), "-O", out],
|
|
189
|
+
stdout=subprocess.DEVNULL,
|
|
190
|
+
stderr=subprocess.DEVNULL,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def url(self, path):
|
|
194
|
+
return "http://127.0.0.1:%d%s" % (self.port, path)
|
|
195
|
+
|
|
196
|
+
def wait_for_file(self, *parts, timeout=60):
|
|
197
|
+
path = os.path.join(self.out, *parts)
|
|
198
|
+
end = time.time() + timeout
|
|
199
|
+
while time.time() < end:
|
|
200
|
+
if os.path.isfile(path):
|
|
201
|
+
time.sleep(0.5)
|
|
202
|
+
return
|
|
203
|
+
time.sleep(0.3)
|
|
204
|
+
raise AssertionError("server never wrote " + path)
|
|
205
|
+
|
|
206
|
+
def stop(self):
|
|
207
|
+
self.proc.terminate()
|
|
208
|
+
try:
|
|
209
|
+
self.proc.wait(10)
|
|
210
|
+
except subprocess.TimeoutExpired:
|
|
211
|
+
self.proc.kill()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
RESULTS = []
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def check(name, cond, detail=""):
|
|
218
|
+
RESULTS.append(bool(cond))
|
|
219
|
+
print(("PASS " if cond else "FAIL ") + name + ("" if cond else " " + detail))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def run():
|
|
223
|
+
tmp = tempfile.mkdtemp(prefix="lektor-ai-chatbot-")
|
|
224
|
+
try:
|
|
225
|
+
# ---- unit checks ------------------------------------------------
|
|
226
|
+
check(
|
|
227
|
+
"insert_tag puts the tag before the last </body>",
|
|
228
|
+
plugin.insert_tag(b"<p></body></p></body>", "X") == b"<p></body></p>X\n</body>",
|
|
229
|
+
)
|
|
230
|
+
check("insert_tag appends when there is no </body>", plugin.insert_tag(b"<p>", "X") == b"<p>X")
|
|
231
|
+
check(
|
|
232
|
+
"build_tag escapes the script URL",
|
|
233
|
+
'src="https://x.test/w.js?a=1&b="2"'
|
|
234
|
+
in plugin.build_tag(WIDGET_ID, 'https://x.test/w.js?a=1&b="2'),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# ---- a normal build ---------------------------------------------
|
|
238
|
+
root = os.path.join(tmp, "site")
|
|
239
|
+
out = os.path.join(tmp, "out")
|
|
240
|
+
make_site(root, "widget_id = %s\n" % WIDGET_ID)
|
|
241
|
+
r = build(root, out)
|
|
242
|
+
check("build succeeds", r.returncode == 0, r.stderr[-800:])
|
|
243
|
+
|
|
244
|
+
home = read(out, "index.html")
|
|
245
|
+
check("home page has one widget tag", home.count("data-asyntai-id") == 1 and TAG in home)
|
|
246
|
+
check("tag sits right before </body>", TAG + "\n</body>" in home)
|
|
247
|
+
check("inner page has the tag", TAG in read(out, "about", "index.html"))
|
|
248
|
+
check("404 page has the tag", TAG in read(out, "404.html"))
|
|
249
|
+
|
|
250
|
+
code = read(out, "code", "index.html")
|
|
251
|
+
check(
|
|
252
|
+
"quoted </body> in a code block is not used",
|
|
253
|
+
code.count("data-asyntai-id") == 1 and code.index(TAG) > code.index("</body>"),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
uni = read(out, "unicode", "index.html")
|
|
257
|
+
check(
|
|
258
|
+
"non-ASCII text survives unchanged",
|
|
259
|
+
"Příliš žluťoučký kůň" in uni and "日本語のテキスト" in uni and TAG in uni,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
own = read(out, "own", "index.html")
|
|
263
|
+
check(
|
|
264
|
+
"a page with the owner's own snippet is left alone",
|
|
265
|
+
own.count("data-asyntai-id") == 1 and "asyntai_owner1" in own,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
check("feed.xml is untouched", "data-asyntai-id" not in read(out, "feed.xml"))
|
|
269
|
+
check("CSS is untouched", read(out, "static", "style.css") == "body { color: black; }\n")
|
|
270
|
+
check(
|
|
271
|
+
"an HTML fragment from assets/ is untouched",
|
|
272
|
+
read(out, "fragment.html") == "<div>Loaded by a script.</div>\n",
|
|
273
|
+
)
|
|
274
|
+
check("a full static page from assets/ gets the tag", TAG in read(out, "standalone.html"))
|
|
275
|
+
|
|
276
|
+
check("blog index page has the tag", TAG in read(out, "blog", "index.html"))
|
|
277
|
+
check("pagination page 2 has the tag", TAG in read(out, "blog", "page", "2", "index.html"))
|
|
278
|
+
check("pagination page 3 has the tag", TAG in read(out, "blog", "page", "3", "index.html"))
|
|
279
|
+
check("blog post has the tag", TAG in read(out, "blog", "post4", "index.html"))
|
|
280
|
+
|
|
281
|
+
# ---- rebuilds ---------------------------------------------------
|
|
282
|
+
r = build(root, out)
|
|
283
|
+
check(
|
|
284
|
+
"second build adds no second tag",
|
|
285
|
+
r.returncode == 0 and read(out, "index.html").count("data-asyntai-id") == 1,
|
|
286
|
+
)
|
|
287
|
+
write(os.path.join(root, "content", "about", "contents.lr"), page("About", "Changed text."))
|
|
288
|
+
r = build(root, out)
|
|
289
|
+
about = read(out, "about", "index.html")
|
|
290
|
+
check(
|
|
291
|
+
"an edited page is rebuilt with exactly one tag",
|
|
292
|
+
r.returncode == 0 and "Changed text." in about and about.count("data-asyntai-id") == 1,
|
|
293
|
+
)
|
|
294
|
+
write(os.path.join(root, "templates", "page.html"), PAGE_TEMPLATE.replace("<h1>", "<h1 class=t>"))
|
|
295
|
+
r = build(root, out)
|
|
296
|
+
check(
|
|
297
|
+
"a template change rebuilds every page with one tag",
|
|
298
|
+
r.returncode == 0
|
|
299
|
+
and 'class=t' in read(out, "blog", "post2", "index.html")
|
|
300
|
+
and read(out, "blog", "post2", "index.html").count("data-asyntai-id") == 1,
|
|
301
|
+
)
|
|
302
|
+
shutil.rmtree(out)
|
|
303
|
+
r = build(root, out)
|
|
304
|
+
check(
|
|
305
|
+
"a fresh build into an empty folder has the tag",
|
|
306
|
+
r.returncode == 0 and TAG in read(out, "index.html"),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# ---- settings ---------------------------------------------------
|
|
310
|
+
r = subprocess.run(
|
|
311
|
+
LEKTOR + ["--project", root, "plugins", "list", "-v"],
|
|
312
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
313
|
+
)
|
|
314
|
+
check(
|
|
315
|
+
"lektor plugins list shows the plugin",
|
|
316
|
+
"AI Chatbot by Asyntai (ai-chatbot)" in r.stdout and "version: 1.0.0" in r.stdout,
|
|
317
|
+
r.stdout,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
custom = os.path.join(tmp, "custom")
|
|
321
|
+
make_site(
|
|
322
|
+
custom,
|
|
323
|
+
"widget_id = %s\nscript_url = https://cdn.example.test/w.js?v=2&x=1\n" % WIDGET_ID,
|
|
324
|
+
)
|
|
325
|
+
r = build(custom, os.path.join(tmp, "custom-out"))
|
|
326
|
+
check(
|
|
327
|
+
"script_url is used and escaped",
|
|
328
|
+
r.returncode == 0
|
|
329
|
+
and 'src="https://cdn.example.test/w.js?v=2&x=1"'
|
|
330
|
+
in read(os.path.join(tmp, "custom-out"), "index.html"),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
for label, ini in (
|
|
334
|
+
("no ini file", None),
|
|
335
|
+
("empty widget_id", "widget_id =\n"),
|
|
336
|
+
("malformed widget_id", "widget_id = 12345\n"),
|
|
337
|
+
):
|
|
338
|
+
bad = os.path.join(tmp, "bad-" + label.replace(" ", "-"))
|
|
339
|
+
bad_out = os.path.join(tmp, "bad-out-" + label.replace(" ", "-"))
|
|
340
|
+
make_site(bad, ini)
|
|
341
|
+
r = build(bad, bad_out)
|
|
342
|
+
text = r.stdout + r.stderr
|
|
343
|
+
check(
|
|
344
|
+
label + " stops the build with a clear message",
|
|
345
|
+
r.returncode != 0 and "lektor-ai-chatbot" in text and "Traceback" not in text
|
|
346
|
+
and not os.path.exists(os.path.join(bad_out, "index.html")),
|
|
347
|
+
text[-600:],
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
off = os.path.join(tmp, "off")
|
|
351
|
+
make_site(off, "widget_id = %s\nenable_on_server = false\n" % WIDGET_ID)
|
|
352
|
+
r = build(off, os.path.join(tmp, "off-out"))
|
|
353
|
+
check(
|
|
354
|
+
"enable_on_server = false still adds the tag on lektor build",
|
|
355
|
+
r.returncode == 0 and TAG in read(os.path.join(tmp, "off-out"), "index.html"),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# ---- lektor server ----------------------------------------------
|
|
359
|
+
srv_out = os.path.join(tmp, "srv-out")
|
|
360
|
+
s = Server(root, srv_out)
|
|
361
|
+
try:
|
|
362
|
+
s.wait_for_file("about", "index.html")
|
|
363
|
+
html = fetch(s.url("/about/"))
|
|
364
|
+
check("lektor server serves the page with the tag", TAG in html and html.count("data-asyntai-id") == 1)
|
|
365
|
+
write(os.path.join(root, "content", "about", "contents.lr"), page("About", "Live edit."))
|
|
366
|
+
end = time.time() + 30
|
|
367
|
+
html = ""
|
|
368
|
+
while time.time() < end:
|
|
369
|
+
html = fetch(s.url("/about/"))
|
|
370
|
+
if "Live edit." in html:
|
|
371
|
+
break
|
|
372
|
+
time.sleep(0.5)
|
|
373
|
+
check(
|
|
374
|
+
"a live edit during lektor server keeps one tag",
|
|
375
|
+
"Live edit." in html and html.count("data-asyntai-id") == 1,
|
|
376
|
+
html[-300:],
|
|
377
|
+
)
|
|
378
|
+
finally:
|
|
379
|
+
s.stop()
|
|
380
|
+
|
|
381
|
+
off_srv_out = os.path.join(tmp, "off-srv-out")
|
|
382
|
+
s = Server(off, off_srv_out)
|
|
383
|
+
try:
|
|
384
|
+
s.wait_for_file("index.html")
|
|
385
|
+
html = fetch(s.url("/"))
|
|
386
|
+
check(
|
|
387
|
+
"enable_on_server = false keeps the widget off lektor server",
|
|
388
|
+
"Home" in html and "data-asyntai-id" not in html,
|
|
389
|
+
)
|
|
390
|
+
finally:
|
|
391
|
+
s.stop()
|
|
392
|
+
finally:
|
|
393
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
394
|
+
|
|
395
|
+
passed = sum(RESULTS)
|
|
396
|
+
print("\n%d/%d checks passed" % (passed, len(RESULTS)))
|
|
397
|
+
return passed == len(RESULTS)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
if __name__ == "__main__":
|
|
401
|
+
sys.exit(0 if run() else 1)
|