From 0380c01a276360fa85ba855b84c829795250bb87 Mon Sep 17 00:00:00 2001
From: nudoragon
Date: Sun, 17 May 2026 22:11:46 +0800
Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E6=9E=B6=E6=A7=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
build_editor.py | 102 ++
main.py | 295 +++-
pyproject.toml | 6 +-
static/css/my-style.css | 624 ++++++++
static/js/my-editor.js | 168 +++
templates/dashboard.html | 502 +++++++
templates/editor.html | 2321 ++++++++++++++++++++++++++++++
uv.lock | 146 ++
websites/my-website/index.html | 33 +
websites/my-website/project.json | 6 +
websites/second-web/project.json | 6 +
11 files changed, 4204 insertions(+), 5 deletions(-)
create mode 100644 build_editor.py
create mode 100644 templates/dashboard.html
create mode 100644 uv.lock
create mode 100644 websites/my-website/index.html
create mode 100644 websites/my-website/project.json
create mode 100644 websites/second-web/project.json
diff --git a/build_editor.py b/build_editor.py
new file mode 100644
index 0000000..597412d
--- /dev/null
+++ b/build_editor.py
@@ -0,0 +1,102 @@
+"""Build editor.html template from vvvebjs source with Jinja2 escaping."""
+from pathlib import Path
+
+BASE = Path(__file__).parent
+SRC = BASE / "static" / "Vvvebjs" / "editor.html"
+OUT = BASE / "templates" / "editor.html"
+
+content = SRC.read_text(encoding="utf-8")
+
+# ── 1. Fix static asset paths ────────────────────────────────────
+VBASE = "/static/Vvvebjs/"
+replacements = [
+ ('href="css/', f'href="{VBASE}css/'),
+ ('href="libs/', f'href="{VBASE}libs/'),
+ ('href="img/', f'href="{VBASE}img/'),
+ ('href="favicon.ico"', f'href="{VBASE}favicon.ico"'),
+ ('src="libs/', f'src="{VBASE}libs/'),
+ ('src="js/', f'src="{VBASE}js/'),
+ ('src="img/', f'src="{VBASE}img/'),
+ ('src="media/', f'src="{VBASE}media/'),
+ ('src="fonts/', f'src="{VBASE}fonts/'),
+ ('src="demo/', f'src="{VBASE}demo/'),
+]
+for old, new in replacements:
+ content = content.replace(old, new)
+
+# ── 2. Fix PHP save URL → Flask API ─────────────────────────────
+content = content.replace('data-vvveb-url="save.php"', 'data-vvveb-url="/api/save"')
+
+# ── 3. Fix JS url variables ──────────────────────────────────────
+JS_VARS = [
+ ("let renameUrl = 'save.php?action=rename';",
+ "let renameUrl = '/api/save?action=rename';"),
+ ("let deleteUrl = 'save.php?action=delete';",
+ "let deleteUrl = '/api/save?action=delete';"),
+ ("let saveReusableUrl = 'save.php?action=saveReusable';",
+ "let saveReusableUrl = '/api/save?action=saveReusable';"),
+ ("let oEmbedProxyUrl = 'save.php?action=oembedProxy';",
+ "let oEmbedProxyUrl = '/api/save?action=oembedProxy';"),
+]
+for old, new in JS_VARS:
+ content = content.replace(old, new)
+
+# ── 4. Wrap everything in {% raw %} ... {% endraw %} to avoid Jinja2 parsing conflicts ──
+# We do this first so Jinja2 ignores VvvebJS's frontend micro-templates ({%= %}, {% %}).
+# Then we selectively exit the {% raw %} block using {% endraw %} ... {% raw %} for our dynamic values.
+content = "{% raw %}" + content + "{% endraw %}"
+
+# ── 5. Replace defaultPages block with Escaped Jinja2 Injection ──
+# Since we are inside a {% raw %} block, we use {% endraw %}{{ pages_json | safe }}{% raw %}
+start = content.find("let defaultPages = {")
+end = content.find("};\n\n\nlet pages = defaultPages;")
+if start != -1 and end != -1:
+ end += len("};\n\n\nlet pages = defaultPages;")
+ content = (
+ content[:start]
+ + "let defaultPages = {% endraw %}{{ pages_json | safe }}{% raw %};\nlet pages = defaultPages;"
+ + content[end:]
+ )
+
+# ── 6. Add back button + slug variable before
───────────
+# We break out of {% raw %} using {% endraw %} to interpolate {{ slug | safe }}
+BACK_BTN = """
+
+
+ ← 返回管理器
+
+
+
+"""
+content = content.replace("", BACK_BTN + "\n")
+
+OUT.write_text(content, encoding="utf-8")
+print(f"Successfully generated escaped Jinja2 template! Size: {len(content)} bytes.")
diff --git a/main.py b/main.py
index fb9c894..13394e5 100644
--- a/main.py
+++ b/main.py
@@ -1,6 +1,295 @@
-def main():
- print("Hello from vvvebjs!")
+"""VvvebJS 網頁管理器 — Flask 主程式."""
+from __future__ import annotations
+
+import json
+import re
+import shutil
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any
+
+from flask import Flask, Response, abort, jsonify, render_template, request, send_from_directory
+
+app = Flask(__name__)
+
+# ── 路徑常數 ────────────────────────────────────────────────────────────────
+BASE_DIR = Path(__file__).parent
+WEBSITES_DIR = BASE_DIR / "websites"
+BLANK_TEMPLATE = BASE_DIR / "static" / "Vvvebjs" / "new-page-blank-template.html"
+
+
+# ── 輔助函式 ────────────────────────────────────────────────────────────────
+
+def slugify(text: str) -> str:
+ """將文字轉成 URL-safe slug."""
+ text = text.lower().strip()
+ text = re.sub(r"[^\w\s-]", "", text, flags=re.UNICODE)
+ text = re.sub(r"[\s_-]+", "-", text)
+ text = re.sub(r"^-+|-+$", "", text)
+ return text or "untitled"
+
+
+def _now_iso() -> str:
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
+
+
+def _project_dir(slug: str) -> Path:
+ return WEBSITES_DIR / slug
+
+
+def _project_json_path(slug: str) -> Path:
+ return _project_dir(slug) / "project.json"
+
+
+def _load_project(slug: str) -> dict[str, Any]:
+ """讀取專案設定,若不存在則回傳預設值."""
+ path = _project_json_path(slug)
+ if path.exists():
+ data: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
+ return data
+ return {
+ "name": slug,
+ "slug": slug,
+ "description": "",
+ "created_at": _now_iso(),
+ }
+
+
+def _save_project(slug: str, data: dict[str, Any]) -> None:
+ path = _project_json_path(slug)
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
+
+
+def _list_pages(slug: str) -> list[str]:
+ """列出專案目錄下所有 .html 頁面."""
+ proj_dir = _project_dir(slug)
+ if not proj_dir.exists():
+ return []
+ return sorted(p.name for p in proj_dir.glob("*.html"))
+
+
+def _project_summary(slug: str) -> dict[str, Any]:
+ data = _load_project(slug)
+ pages = _list_pages(slug)
+ proj_dir = _project_dir(slug)
+ html_files = list(proj_dir.glob("*.html"))
+ if html_files:
+ last_mod = max(f.stat().st_mtime for f in html_files)
+ last_mod_str = datetime.fromtimestamp(last_mod, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
+ else:
+ last_mod_str = str(data.get("created_at", _now_iso()))
+ return {
+ **data,
+ "slug": slug,
+ "page_count": len(pages),
+ "pages": pages,
+ "last_modified": last_mod_str,
+ }
+
+
+def _sanitize_file_path(slug: str, filename: str) -> Path | None:
+ """驗證並回傳安全的檔案路徑(防止路徑遍歷)."""
+ proj_dir = _project_dir(slug).resolve()
+ target = (proj_dir / Path(filename).name).resolve()
+ if not str(target).startswith(str(proj_dir)):
+ return None
+ if target.suffix.lower() != ".html":
+ return None
+ return target
+
+
+def _copy_blank_template(dest: Path) -> None:
+ """複製空白範本或建立最小 HTML."""
+ if BLANK_TEMPLATE.exists():
+ shutil.copy(BLANK_TEMPLATE, dest)
+ else:
+ dest.write_text(
+ "
My Page