spaday-codemirror 0.1.0__py3-none-any.whl
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.
- spaday_codemirror/__init__.py +27 -0
- spaday_codemirror/components.cem.json +132 -0
- spaday_codemirror/components.py +72 -0
- spaday_codemirror/example.py +320 -0
- spaday_codemirror/extension/cdn/index.js +30 -0
- spaday_codemirror/extension/cdn/index.js.map +7 -0
- spaday_codemirror/extension/css/index.css +1 -0
- spaday_codemirror/extension/index.html +28 -0
- spaday_codemirror/extension/versions.json +11 -0
- spaday_codemirror/tests/__init__.py +0 -0
- spaday_codemirror/tests/test_all.py +139 -0
- spaday_codemirror/tests/test_example.py +167 -0
- spaday_codemirror/tests/test_versions.py +24 -0
- spaday_codemirror-0.1.0.dist-info/METADATA +129 -0
- spaday_codemirror-0.1.0.dist-info/RECORD +18 -0
- spaday_codemirror-0.1.0.dist-info/WHEEL +4 -0
- spaday_codemirror-0.1.0.dist-info/entry_points.txt +2 -0
- spaday_codemirror-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from spaday import ComponentPackage
|
|
5
|
+
|
|
6
|
+
from .components import SpadayCodemirror
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
ASSETS_DIR = Path(__file__).parent / "extension"
|
|
11
|
+
MANIFEST = Path(__file__).parent / "components.cem.json"
|
|
12
|
+
|
|
13
|
+
# Exact versions of the JS libraries bundled into cdn/index.js, written by the JS build.
|
|
14
|
+
_VERSIONS_FILE = ASSETS_DIR / "versions.json"
|
|
15
|
+
VERSIONS: dict[str, str] = json.loads(_VERSIONS_FILE.read_text(encoding="utf-8")) if _VERSIONS_FILE.exists() else {}
|
|
16
|
+
|
|
17
|
+
package = ComponentPackage(
|
|
18
|
+
name="codemirror",
|
|
19
|
+
assets_dir=ASSETS_DIR,
|
|
20
|
+
assets=(("css", "css/index.css"), ("js", "cdn/index.js")),
|
|
21
|
+
components=(SpadayCodemirror,),
|
|
22
|
+
provides=VERSIONS,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
CodeMirror = SpadayCodemirror
|
|
26
|
+
|
|
27
|
+
__all__ = ["ASSETS_DIR", "MANIFEST", "VERSIONS", "CodeMirror", "SpadayCodemirror", "__version__", "package"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0.0",
|
|
3
|
+
"modules": [
|
|
4
|
+
{
|
|
5
|
+
"kind": "javascript-module",
|
|
6
|
+
"path": "js/src/ts/index.ts",
|
|
7
|
+
"declarations": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "class",
|
|
10
|
+
"name": "SpadayCodeMirror",
|
|
11
|
+
"tagName": "spaday-codemirror",
|
|
12
|
+
"customElement": true,
|
|
13
|
+
"summary": "CodeMirror 6 code editor.",
|
|
14
|
+
"attributes": [
|
|
15
|
+
{
|
|
16
|
+
"name": "doc",
|
|
17
|
+
"fieldName": "doc",
|
|
18
|
+
"type": { "text": "string" },
|
|
19
|
+
"default": "\"\"",
|
|
20
|
+
"description": "Editor contents."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "language",
|
|
24
|
+
"fieldName": "language",
|
|
25
|
+
"type": { "text": "\"python\" | \"javascript\" | \"json\" | \"markdown\" | \"plain\"" },
|
|
26
|
+
"default": "\"plain\"",
|
|
27
|
+
"description": "Syntax mode."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "theme",
|
|
31
|
+
"fieldName": "theme",
|
|
32
|
+
"type": { "text": "\"light\" | \"dark\"" },
|
|
33
|
+
"default": "\"light\"",
|
|
34
|
+
"description": "Color theme."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "read_only",
|
|
38
|
+
"fieldName": "read_only",
|
|
39
|
+
"type": { "text": "boolean" },
|
|
40
|
+
"default": "false",
|
|
41
|
+
"description": "Disallow user edits."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "line_numbers",
|
|
45
|
+
"fieldName": "line_numbers",
|
|
46
|
+
"type": { "text": "boolean" },
|
|
47
|
+
"default": "true",
|
|
48
|
+
"description": "Show the line-number gutter."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "tab_size",
|
|
52
|
+
"fieldName": "tab_size",
|
|
53
|
+
"type": { "text": "number" },
|
|
54
|
+
"default": "4",
|
|
55
|
+
"description": "Tab width and indent unit, in spaces."
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"members": [
|
|
59
|
+
{
|
|
60
|
+
"kind": "field",
|
|
61
|
+
"name": "doc",
|
|
62
|
+
"type": { "text": "string" },
|
|
63
|
+
"default": "\"\"",
|
|
64
|
+
"attribute": "doc"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"kind": "field",
|
|
68
|
+
"name": "language",
|
|
69
|
+
"type": { "text": "\"python\" | \"javascript\" | \"json\" | \"markdown\" | \"plain\"" },
|
|
70
|
+
"default": "\"plain\"",
|
|
71
|
+
"attribute": "language"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"kind": "field",
|
|
75
|
+
"name": "theme",
|
|
76
|
+
"type": { "text": "\"light\" | \"dark\"" },
|
|
77
|
+
"default": "\"light\"",
|
|
78
|
+
"attribute": "theme"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"kind": "field",
|
|
82
|
+
"name": "read_only",
|
|
83
|
+
"type": { "text": "boolean" },
|
|
84
|
+
"default": "false",
|
|
85
|
+
"attribute": "read_only"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"kind": "field",
|
|
89
|
+
"name": "line_numbers",
|
|
90
|
+
"type": { "text": "boolean" },
|
|
91
|
+
"default": "true",
|
|
92
|
+
"attribute": "line_numbers"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"kind": "field",
|
|
96
|
+
"name": "tab_size",
|
|
97
|
+
"type": { "text": "number" },
|
|
98
|
+
"default": "4",
|
|
99
|
+
"attribute": "tab_size"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"kind": "field",
|
|
103
|
+
"name": "selection",
|
|
104
|
+
"type": { "text": "{ anchor: number; head?: number } | null" },
|
|
105
|
+
"default": "null",
|
|
106
|
+
"description": "Main selection; head defaults to anchor."
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"events": [
|
|
110
|
+
{
|
|
111
|
+
"name": "editor-change",
|
|
112
|
+
"type": { "text": "CustomEvent<{ doc: string; changes: { from: number; to: number; insert: string }[]; selection: { anchor: number; head: number } }>" },
|
|
113
|
+
"description": "User edited the document."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "editor-selection",
|
|
117
|
+
"type": { "text": "CustomEvent<{ selection: { anchor: number; head: number } }>" },
|
|
118
|
+
"description": "User moved the selection without editing."
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"exports": [
|
|
124
|
+
{
|
|
125
|
+
"kind": "custom-element-definition",
|
|
126
|
+
"name": "spaday-codemirror",
|
|
127
|
+
"declaration": { "name": "SpadayCodeMirror", "module": "js/src/ts/index.ts" }
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Generated by spaday.cem from components.cem.json — do not edit by hand.
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from spaday.catalog import ComponentSchema, PropertySchema
|
|
7
|
+
from spaday.component import Child, Component
|
|
8
|
+
|
|
9
|
+
__all__ = ["SpadayCodemirror"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SpadayCodemirror(Component):
|
|
13
|
+
"""CodeMirror 6 code editor."""
|
|
14
|
+
|
|
15
|
+
tag = "spaday-codemirror"
|
|
16
|
+
schema = ComponentSchema(
|
|
17
|
+
tag="spaday-codemirror",
|
|
18
|
+
class_name="SpadayCodemirror",
|
|
19
|
+
summary="CodeMirror 6 code editor.",
|
|
20
|
+
props=(
|
|
21
|
+
PropertySchema(name="doc", kind="string", choices=(), default="", description="Editor contents."),
|
|
22
|
+
PropertySchema(
|
|
23
|
+
name="language",
|
|
24
|
+
kind="enum",
|
|
25
|
+
choices=("python", "javascript", "json", "markdown", "plain"),
|
|
26
|
+
default="plain",
|
|
27
|
+
description="Syntax mode.",
|
|
28
|
+
),
|
|
29
|
+
PropertySchema(name="theme", kind="enum", choices=("light", "dark"), default="light", description="Color theme."),
|
|
30
|
+
PropertySchema(name="read_only", kind="boolean", choices=(), default="false", description="Disallow user edits."),
|
|
31
|
+
PropertySchema(name="line_numbers", kind="boolean", choices=(), default="true", description="Show the line-number gutter."),
|
|
32
|
+
PropertySchema(name="tab_size", kind="number", choices=(), default="4", description="Tab width and indent unit, in spaces."),
|
|
33
|
+
),
|
|
34
|
+
fields=(
|
|
35
|
+
PropertySchema(
|
|
36
|
+
name="selection",
|
|
37
|
+
kind="json",
|
|
38
|
+
choices=(),
|
|
39
|
+
type_text="{ anchor: number; head?: number } | null",
|
|
40
|
+
default=None,
|
|
41
|
+
description="Main selection; head defaults to anchor.",
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
events=("editor-change", "editor-selection"),
|
|
45
|
+
slots=(),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*children: Child,
|
|
51
|
+
key: str | None = None,
|
|
52
|
+
doc: str | None = None,
|
|
53
|
+
language: Literal["python", "javascript", "json", "markdown", "plain"] | None = None,
|
|
54
|
+
theme: Literal["light", "dark"] | None = None,
|
|
55
|
+
read_only: bool | None = None,
|
|
56
|
+
line_numbers: bool | None = None,
|
|
57
|
+
tab_size: float | None = None,
|
|
58
|
+
**props: Any,
|
|
59
|
+
) -> None:
|
|
60
|
+
super().__init__(
|
|
61
|
+
*children,
|
|
62
|
+
key=key,
|
|
63
|
+
props={
|
|
64
|
+
"doc": doc,
|
|
65
|
+
"language": language,
|
|
66
|
+
"theme": theme,
|
|
67
|
+
"read_only": read_only,
|
|
68
|
+
"line_numbers": line_numbers,
|
|
69
|
+
"tab_size": tab_size,
|
|
70
|
+
},
|
|
71
|
+
**props,
|
|
72
|
+
)
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Python-authored CodeMirror page over spaday's worker protocol.
|
|
2
|
+
|
|
3
|
+
A transports-hosted ``Document`` model is the source of truth. The editable source editor sends each
|
|
4
|
+
user edit as a ``spaday:patch`` intent; Python recomputes metrics and a normalized preview, and returns
|
|
5
|
+
a component-tree patch. The same :class:`~spaday.WorkerApp` runs inside Pyodide (``worker_app``) or
|
|
6
|
+
behind a Starlette websocket (``app``), one app per connection.
|
|
7
|
+
|
|
8
|
+
The source editor's ``doc`` prop only changes when Python authors the text (reset / normalize), never to
|
|
9
|
+
echo a user edit back, so in-flight keystrokes are never overwritten by a stale round-trip.
|
|
10
|
+
|
|
11
|
+
Run: ``python -m spaday_codemirror.example`` then open http://127.0.0.1:8031/.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import spaday
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
from spaday import SendPatch, Sequence as Steps, WorkerApp, by_id, element, event_value, lit, prop
|
|
20
|
+
from starlette.applications import Starlette
|
|
21
|
+
from starlette.responses import HTMLResponse
|
|
22
|
+
from starlette.routing import Mount, Route, WebSocketRoute
|
|
23
|
+
from starlette.staticfiles import StaticFiles
|
|
24
|
+
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
25
|
+
from transports import Client, Server, Session, to_value
|
|
26
|
+
|
|
27
|
+
from . import CodeMirror, package
|
|
28
|
+
|
|
29
|
+
HOST = "127.0.0.1"
|
|
30
|
+
PORT = 8031
|
|
31
|
+
TAB_SIZE = 4
|
|
32
|
+
SOURCE_ID = "source-editor"
|
|
33
|
+
|
|
34
|
+
SAMPLE = """def greet(name):
|
|
35
|
+
\tmessage = f"hello, {name}" \n\treturn message
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
print(greet("spaday"))
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
GALLERY = (
|
|
42
|
+
{
|
|
43
|
+
"key": "gallery-python",
|
|
44
|
+
"title": "Python",
|
|
45
|
+
"settings": "light · line numbers · tab 4",
|
|
46
|
+
"props": {"language": "python", "theme": "light", "line_numbers": True, "tab_size": 4},
|
|
47
|
+
"doc": "def area(r):\n return 3.14159 * r ** 2\n",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"key": "gallery-javascript",
|
|
51
|
+
"title": "JavaScript",
|
|
52
|
+
"settings": "dark · line numbers · tab 2",
|
|
53
|
+
"props": {"language": "javascript", "theme": "dark", "line_numbers": True, "tab_size": 2},
|
|
54
|
+
"doc": "const area = (r) => {\n return Math.PI * r ** 2;\n};\n",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"key": "gallery-json",
|
|
58
|
+
"title": "JSON",
|
|
59
|
+
"settings": "light · read-only · tab 2",
|
|
60
|
+
"props": {"language": "json", "theme": "light", "read_only": True, "tab_size": 2},
|
|
61
|
+
"doc": '{\n "name": "spaday-codemirror",\n "private": true\n}\n',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"key": "gallery-markdown",
|
|
65
|
+
"title": "Markdown",
|
|
66
|
+
"settings": "light · no line numbers",
|
|
67
|
+
"props": {"language": "markdown", "theme": "light", "line_numbers": False},
|
|
68
|
+
"doc": "# Notes\n\n- **bold** and *italic*\n- `inline code`\n",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"key": "gallery-plain",
|
|
72
|
+
"title": "Plain text",
|
|
73
|
+
"settings": "dark · read-only · no line numbers · tab 8",
|
|
74
|
+
"props": {"language": "plain", "theme": "dark", "read_only": True, "line_numbers": False, "tab_size": 8},
|
|
75
|
+
"doc": "col\tvalue\nalpha\t1\nbeta\t2\n",
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
styles = """<style>
|
|
80
|
+
:root { color-scheme: light; }
|
|
81
|
+
body { margin: 0; background: #f4f6f8; color: #14212b; font: 15px/1.5 system-ui, sans-serif; }
|
|
82
|
+
#codemirror-example { box-sizing: border-box; max-width: 72rem; margin: 0 auto; padding: 1.5rem 1rem 3rem; }
|
|
83
|
+
#codemirror-example * { box-sizing: border-box; }
|
|
84
|
+
#codemirror-example h1 { margin: 0 0 0.25rem; font-size: 1.6rem; }
|
|
85
|
+
#codemirror-example h2 { margin: 2rem 0 0.75rem; font-size: 1.1rem; }
|
|
86
|
+
#codemirror-example .intro { margin: 0 0 1.25rem; color: #4a5a66; max-width: 46rem; }
|
|
87
|
+
.cm-workspace { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
|
|
88
|
+
.cm-panel { min-width: 0; padding: 0.75rem; border: 1px solid #d8e1e5; border-radius: 8px; background: #fff; }
|
|
89
|
+
.cm-panel h3, .cm-card h3 { margin: 0 0 0.5rem; font-size: 0.95rem; }
|
|
90
|
+
.cm-panel spaday-codemirror { height: 16rem; }
|
|
91
|
+
.cm-toolbar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin: 0.75rem 0 0; }
|
|
92
|
+
.cm-toolbar button { padding: 0.4rem 0.8rem; border: 1px solid #9fb3c0; border-radius: 6px; background: #fff; font: inherit; cursor: pointer; }
|
|
93
|
+
.cm-toolbar button:hover { background: #eef3f6; }
|
|
94
|
+
.cm-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.5rem; margin: 1rem 0 0; }
|
|
95
|
+
.cm-metric { min-width: 0; padding: 0.5rem 0.75rem; border-radius: 6px; background: #eef3f6; }
|
|
96
|
+
.cm-metric span { display: block; color: #4a5a66; font-size: 0.8rem; }
|
|
97
|
+
.cm-metric strong { font-size: 1.2rem; font-variant-numeric: tabular-nums; }
|
|
98
|
+
.cm-status { margin: 0.5rem 0 0; color: #4a5a66; overflow-wrap: anywhere; }
|
|
99
|
+
.cm-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr)); gap: 0.75rem; }
|
|
100
|
+
.cm-card { min-width: 0; padding: 0.75rem; border: 1px solid #d8e1e5; border-radius: 8px; background: #fff; }
|
|
101
|
+
.cm-card p { margin: 0 0 0.5rem; color: #4a5a66; font-size: 0.8rem; }
|
|
102
|
+
.cm-card spaday-codemirror { height: 7.5rem; }
|
|
103
|
+
@media (max-width: 720px) {
|
|
104
|
+
.cm-workspace { grid-template-columns: minmax(0, 1fr); }
|
|
105
|
+
.cm-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
106
|
+
}
|
|
107
|
+
</style>"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Document(BaseModel):
|
|
111
|
+
doc: str = SAMPLE
|
|
112
|
+
revision: int = 0
|
|
113
|
+
status: str = "Ready"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def normalize(doc: str, tab_size: int = TAB_SIZE) -> str:
|
|
117
|
+
"""Expand tabs, strip trailing whitespace, and end with exactly one newline."""
|
|
118
|
+
lines = [line.expandtabs(tab_size).rstrip() for line in doc.split("\n")]
|
|
119
|
+
text = "\n".join(lines).rstrip("\n")
|
|
120
|
+
return f"{text}\n" if text else ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def metrics(doc: str) -> dict[str, int]:
|
|
124
|
+
return {"characters": len(doc), "lines": doc.count("\n") + 1, "words": len(doc.split())}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def python_action(action: str) -> Steps:
|
|
128
|
+
"""Sync the browser's text as Python's baseline, then ask Python to rewrite it."""
|
|
129
|
+
return Steps(
|
|
130
|
+
SendPatch("editor", "sync", prop(by_id(SOURCE_ID), "doc")),
|
|
131
|
+
SendPatch("editor", "action", lit(action)),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class CodeMirrorExample:
|
|
136
|
+
"""One page's state: a transports-hosted ``Document`` plus the editor text Python last authored."""
|
|
137
|
+
|
|
138
|
+
def __init__(self) -> None:
|
|
139
|
+
self.session = Session()
|
|
140
|
+
self.model_id = self.session.host(Document())
|
|
141
|
+
self.server = Server(self.session, default_codec="msgpack")
|
|
142
|
+
self.client = Client(codec="msgpack")
|
|
143
|
+
for frame in self.server.open("browser", "msgpack"):
|
|
144
|
+
self.client.recv(frame)
|
|
145
|
+
# The source editor's `doc` prop. Changes only when Python authors the text.
|
|
146
|
+
self.authored = SAMPLE
|
|
147
|
+
self.app = WorkerApp(self.render, self.on_intent)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def document(self) -> Document:
|
|
151
|
+
return self.client.model(self.model_id, Document)
|
|
152
|
+
|
|
153
|
+
def _commit(self, **changes) -> None:
|
|
154
|
+
proposal = self.document.model_copy(update=changes)
|
|
155
|
+
outbound = self.client.edit(self.model_id, to_value(proposal))
|
|
156
|
+
for frame in self.server.recv("browser", outbound)["browser"]:
|
|
157
|
+
self.client.recv(frame)
|
|
158
|
+
|
|
159
|
+
def on_intent(self, intent: dict) -> None:
|
|
160
|
+
detail = intent["detail"]
|
|
161
|
+
if detail.get("model") != "editor":
|
|
162
|
+
return
|
|
163
|
+
field, value = detail.get("field"), detail.get("value")
|
|
164
|
+
document = self.document
|
|
165
|
+
if field == "doc" and isinstance(value, str) and value != document.doc:
|
|
166
|
+
self._commit(doc=value, revision=document.revision + 1, status=f"Edited in browser · revision {document.revision + 1}")
|
|
167
|
+
elif field == "sync" and isinstance(value, str):
|
|
168
|
+
# Adopt the browser's text as the authored baseline so the following action always patches.
|
|
169
|
+
self.authored = value
|
|
170
|
+
if value != document.doc:
|
|
171
|
+
self._commit(doc=value, revision=document.revision + 1)
|
|
172
|
+
elif field == "action" and value in ("reset", "normalize"):
|
|
173
|
+
target = SAMPLE if value == "reset" else normalize(document.doc)
|
|
174
|
+
label = "Reset" if value == "reset" else "Normalized"
|
|
175
|
+
if target == document.doc:
|
|
176
|
+
self._commit(status=f"{label} in Python · no change")
|
|
177
|
+
else:
|
|
178
|
+
self._commit(doc=target, revision=document.revision + 1, status=f"{label} in Python · revision {document.revision + 1}")
|
|
179
|
+
self.authored = target
|
|
180
|
+
|
|
181
|
+
def render(self):
|
|
182
|
+
document = self.document
|
|
183
|
+
counts = metrics(document.doc)
|
|
184
|
+
source = CodeMirror(
|
|
185
|
+
key=SOURCE_ID,
|
|
186
|
+
id=SOURCE_ID,
|
|
187
|
+
doc=self.authored,
|
|
188
|
+
language="python",
|
|
189
|
+
theme="light",
|
|
190
|
+
tab_size=TAB_SIZE,
|
|
191
|
+
).on("editor-change", SendPatch("editor", "doc", event_value("doc")))
|
|
192
|
+
preview = CodeMirror(
|
|
193
|
+
key="preview-editor",
|
|
194
|
+
id="preview-editor",
|
|
195
|
+
doc=normalize(document.doc),
|
|
196
|
+
language="python",
|
|
197
|
+
theme="dark",
|
|
198
|
+
read_only=True,
|
|
199
|
+
tab_size=TAB_SIZE,
|
|
200
|
+
)
|
|
201
|
+
return (
|
|
202
|
+
element("main", id="codemirror-example")
|
|
203
|
+
.child(element("h1").text("spaday-codemirror"))
|
|
204
|
+
.child(
|
|
205
|
+
element("p")
|
|
206
|
+
.classes("intro")
|
|
207
|
+
.text("Edit the source. Each change goes to Python, which updates the model, the metrics, and the normalized preview.")
|
|
208
|
+
)
|
|
209
|
+
.child(
|
|
210
|
+
element("section")
|
|
211
|
+
.classes("cm-workspace")
|
|
212
|
+
.child(
|
|
213
|
+
element("div")
|
|
214
|
+
.classes("cm-panel")
|
|
215
|
+
.child(element("h3").text("Source · editable"))
|
|
216
|
+
.child(source)
|
|
217
|
+
.child(
|
|
218
|
+
element("div")
|
|
219
|
+
.classes("cm-toolbar")
|
|
220
|
+
.child(element("button", id="normalize", type="button").text("Normalize in Python").on("click", python_action("normalize")))
|
|
221
|
+
.child(element("button", id="reset", type="button").text("Reset").on("click", python_action("reset")))
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
.child(element("div").classes("cm-panel").child(element("h3").text("Normalized preview · read-only, from Python")).child(preview))
|
|
225
|
+
)
|
|
226
|
+
.child(
|
|
227
|
+
element("section", id="metrics")
|
|
228
|
+
.classes("cm-metrics")
|
|
229
|
+
.child(*(self._metric(name, value) for name, value in (*counts.items(), ("revision", document.revision))))
|
|
230
|
+
)
|
|
231
|
+
.child(element("p", id="status").classes("cm-status").text(document.status))
|
|
232
|
+
.child(element("h2").text("Gallery"))
|
|
233
|
+
.child(element("section", id="gallery").classes("cm-gallery").child(*(self._card(entry) for entry in GALLERY)))
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _metric(name: str, value: int):
|
|
238
|
+
return (
|
|
239
|
+
element("div")
|
|
240
|
+
.classes("cm-metric")
|
|
241
|
+
.child(element("span").text(name.capitalize()), element("strong", id=f"metric-{name}").text(str(value)))
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def _card(entry: dict):
|
|
246
|
+
return (
|
|
247
|
+
element("article", id=entry["key"])
|
|
248
|
+
.classes("cm-card")
|
|
249
|
+
.child(element("h3").text(entry["title"]))
|
|
250
|
+
.child(element("p").text(entry["settings"]))
|
|
251
|
+
.child(CodeMirror(key=entry["key"], doc=entry["doc"], **entry["props"]))
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
example = CodeMirrorExample()
|
|
256
|
+
worker_app = example.app
|
|
257
|
+
|
|
258
|
+
PAGE = f"""<!doctype html>
|
|
259
|
+
<html lang="en">
|
|
260
|
+
<head>
|
|
261
|
+
<meta charset="utf-8" />
|
|
262
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
263
|
+
<title>spaday-codemirror</title>
|
|
264
|
+
<link rel="stylesheet" href="/components/codemirror/css/index.css" />
|
|
265
|
+
{styles}
|
|
266
|
+
</head>
|
|
267
|
+
<body>
|
|
268
|
+
<div id="example-root"></div>
|
|
269
|
+
<script type="module">
|
|
270
|
+
import {{ connectWorker, init }} from "/runtime/spaday/cdn/index.js";
|
|
271
|
+
import "/components/codemirror/cdn/index.js";
|
|
272
|
+
|
|
273
|
+
await init({{ module_or_path: "/runtime/spaday/pkg/spaday_bg.wasm" }});
|
|
274
|
+
// Present the websocket as the Worker that connectWorker expects: same snapshot/patch protocol.
|
|
275
|
+
const socket = new WebSocket(`${{location.protocol === "https:" ? "wss" : "ws"}}://${{location.host}}/ws`);
|
|
276
|
+
const opened = new Promise((resolve) => socket.addEventListener("open", resolve, {{ once: true }}));
|
|
277
|
+
const channel = new EventTarget();
|
|
278
|
+
socket.addEventListener("message", (event) => channel.dispatchEvent(new MessageEvent("message", {{ data: JSON.parse(event.data) }})));
|
|
279
|
+
channel.postMessage = (message) => opened.then(() => socket.send(JSON.stringify(message)));
|
|
280
|
+
await connectWorker(document.querySelector("#example-root"), channel).ready;
|
|
281
|
+
document.documentElement.dataset.ready = "true";
|
|
282
|
+
</script>
|
|
283
|
+
</body>
|
|
284
|
+
</html>
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
async def homepage(_request) -> HTMLResponse:
|
|
289
|
+
return HTMLResponse(PAGE)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def socket(websocket: WebSocket) -> None:
|
|
293
|
+
await websocket.accept()
|
|
294
|
+
page = CodeMirrorExample()
|
|
295
|
+
try:
|
|
296
|
+
async for text in websocket.iter_text():
|
|
297
|
+
message = json.loads(text)
|
|
298
|
+
await websocket.send_text(page.app.start_json() if message.get("type") == "start" else page.app.dispatch_json(text))
|
|
299
|
+
except WebSocketDisconnect:
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
app = Starlette(
|
|
304
|
+
routes=[
|
|
305
|
+
Route("/", homepage),
|
|
306
|
+
WebSocketRoute("/ws", socket),
|
|
307
|
+
Mount("/components/codemirror", StaticFiles(directory=package.assets_dir)),
|
|
308
|
+
Mount("/runtime/spaday", StaticFiles(directory=Path(spaday.__file__).parent / "extension")),
|
|
309
|
+
]
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def main() -> None:
|
|
314
|
+
import uvicorn
|
|
315
|
+
|
|
316
|
+
uvicorn.run(app, host=HOST, port=PORT)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
main()
|