Merge pull request 'fix: restore KaTeX assets and deployment checks' (#16) from fix/latex-assets-and-deploy into main
Reviewed-on: http://117.72.28.96:8765/Jacky/Hulumath-Web/pulls/16
This commit was merged in pull request #16.
This commit is contained in:
@@ -60,6 +60,16 @@ jobs:
|
||||
- name: ASGI import check
|
||||
working-directory: backend
|
||||
run: ../.venv-ci/bin/python -c "from config.asgi import application; print(type(application).__name__)"
|
||||
- name: Production static assets check
|
||||
working-directory: backend
|
||||
env:
|
||||
DJANGO_DEBUG: "false"
|
||||
DJANGO_USE_HTTPS: "false"
|
||||
DJANGO_SECRET_KEY: ci-static-assets-check
|
||||
DJANGO_ALLOWED_HOSTS: localhost
|
||||
DATABASE_URL: mysql://root:ci-root-password@mysql:3306/hulumath
|
||||
REDIS_URL: redis://127.0.0.1:6379/0
|
||||
run: ../.venv-ci/bin/python manage.py collectstatic --noinput --clear
|
||||
- name: SQLite tests and coverage
|
||||
run: |
|
||||
.venv-ci/bin/python -m pytest -q \
|
||||
|
||||
@@ -66,6 +66,17 @@ jobs:
|
||||
working-directory: backend
|
||||
run: ../.venv-release/bin/python -c "from config.asgi import application; print(type(application).__name__)"
|
||||
|
||||
- name: 生产静态资源完整性检查
|
||||
working-directory: backend
|
||||
env:
|
||||
DJANGO_DEBUG: "false"
|
||||
DJANGO_USE_HTTPS: "false"
|
||||
DJANGO_SECRET_KEY: release-static-assets-check
|
||||
DJANGO_ALLOWED_HOSTS: localhost
|
||||
DATABASE_URL: mysql://root:ci-root-password@mysql:3306/hulumath
|
||||
REDIS_URL: redis://127.0.0.1:6379/0
|
||||
run: ../.venv-release/bin/python manage.py collectstatic --noinput --clear
|
||||
|
||||
- name: MySQL 发布迁移检查
|
||||
env:
|
||||
DATABASE_URL: mysql://root:ci-root-password@mysql:3306/hulumath
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.test import override_settings
|
||||
|
||||
STATIC_ROOT = Path(settings.BASE_DIR) / "static"
|
||||
|
||||
@@ -71,6 +74,41 @@ def test_toolbox_and_games_资源入口与移动端触控样式存在():
|
||||
assert "@media (max-width: 700px)" in styles
|
||||
|
||||
|
||||
def test_latex_默认源码可渲染且_katex_静态资源完整():
|
||||
template = (settings.BASE_DIR / "templates" / "index.html").read_text(encoding="utf-8")
|
||||
app = (STATIC_ROOT / "js" / "app.js").read_text(encoding="utf-8")
|
||||
katex_root = STATIC_ROOT / "vendor" / "katex"
|
||||
katex_css = (katex_root / "katex.min.css").read_text(encoding="utf-8")
|
||||
font_paths = set(re.findall(r"url\(fonts/([^)]+)\)", katex_css))
|
||||
|
||||
assert (
|
||||
'<textarea id="latex-source" spellcheck="false">'
|
||||
r"\int_{0}^{1} x^2\,dx = \frac{1}{3}</textarea>"
|
||||
) in template
|
||||
assert r"\\int_{0}^{1}" not in template
|
||||
assert "function normalizeLatexSource(source)" in app
|
||||
assert "globalThis.katex.render(normalized, target" in app
|
||||
assert "throwOnError: true" in app
|
||||
assert len(font_paths) == 60
|
||||
assert all((katex_root / "fonts" / path).is_file() for path in font_paths)
|
||||
assert (katex_root / "LICENSE.txt").is_file()
|
||||
|
||||
|
||||
def test_production_manifest_可处理全部静态资源(tmp_path):
|
||||
storage = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
with override_settings(
|
||||
STATIC_ROOT=tmp_path,
|
||||
STORAGES={
|
||||
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||
"staticfiles": {"BACKEND": storage},
|
||||
},
|
||||
):
|
||||
call_command("collectstatic", interactive=False, clear=True, verbosity=0)
|
||||
|
||||
assert (tmp_path / "staticfiles.json").is_file()
|
||||
assert (tmp_path / "vendor" / "katex" / "katex.min.css").is_file()
|
||||
|
||||
|
||||
def test_realtime_match_联机码与_websocket_前端资源存在():
|
||||
template = (settings.BASE_DIR / "templates" / "index.html").read_text(encoding="utf-8")
|
||||
realtime = (STATIC_ROOT / "js" / "realtime.js").read_text(encoding="utf-8")
|
||||
|
||||
@@ -161,7 +161,9 @@ button { color: inherit; }
|
||||
.editor-pane label, .preview-pane > span { display: block; color: #9eaaa2; font-size: 10px; letter-spacing: .16em; text-transform: uppercase; }
|
||||
.editor-pane textarea { width: 100%; height: 330px; margin-top: 22px; resize: none; border: 0; outline: 0; background: transparent; color: #dcefa1; font: 16px/1.8 "SFMono-Regular", Consolas, monospace; }
|
||||
.preview-pane { display: flex; flex-direction: column; }
|
||||
#latex-preview { margin: auto; max-width: 100%; overflow-wrap: anywhere; font: 28px/1.6 Georgia, serif; }
|
||||
#latex-preview { margin: auto; max-width: 100%; overflow-x: auto; overflow-y: hidden; padding: 18px 4px; font-size: 28px; line-height: 1.6; }
|
||||
#latex-preview .katex-display { margin: 0; overflow-x: auto; overflow-y: hidden; }
|
||||
#latex-preview.latex-preview-error { color: #b84136; font: 14px/1.7 system-ui, sans-serif; white-space: normal; }
|
||||
.preview-pane button { align-self: flex-end; }
|
||||
.profile-panel { padding: 35px; }
|
||||
.metric-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; }
|
||||
|
||||
@@ -1060,15 +1060,40 @@ function switchTool(tool) {
|
||||
window.HuluToolbox?.activate(tool);
|
||||
}
|
||||
|
||||
function normalizeLatexSource(source) {
|
||||
let normalized = String(source || "").trim();
|
||||
if (normalized.startsWith("$$") && normalized.endsWith("$$")) {
|
||||
normalized = normalized.slice(2, -2).trim();
|
||||
} else if (normalized.startsWith("\\[") && normalized.endsWith("\\]")) {
|
||||
normalized = normalized.slice(2, -2).trim();
|
||||
} else if (normalized.startsWith("$") && normalized.endsWith("$")) {
|
||||
normalized = normalized.slice(1, -1).trim();
|
||||
}
|
||||
return normalized.replace(/\\\\(?=[A-Za-z,;:!])/g, "\\");
|
||||
}
|
||||
|
||||
function renderLatexPreview(source) {
|
||||
const target = $("#latex-preview");
|
||||
target.replaceChildren();
|
||||
if (!source.trim()) return;
|
||||
if (typeof katex === "undefined") { target.textContent = source; return; }
|
||||
target.classList.remove("latex-preview-error");
|
||||
const normalized = normalizeLatexSource(source);
|
||||
if (!normalized) return;
|
||||
if (typeof globalThis.katex?.render !== "function") {
|
||||
target.classList.add("latex-preview-error");
|
||||
target.textContent = "公式渲染组件加载失败,请刷新页面后重试。";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
katex.render(source, target, { throwOnError: false, displayMode: true });
|
||||
} catch {
|
||||
target.textContent = source;
|
||||
globalThis.katex.render(normalized, target, {
|
||||
displayMode: true,
|
||||
throwOnError: true,
|
||||
trust: false,
|
||||
maxExpand: 500,
|
||||
maxSize: 20,
|
||||
});
|
||||
} catch (error) {
|
||||
target.classList.add("latex-preview-error");
|
||||
target.textContent = `公式语法错误:${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,7 +1102,10 @@ async function saveFormula() {
|
||||
try {
|
||||
await api("latex/documents/", {
|
||||
method: "POST",
|
||||
body: { title: `公式 ${new Date().toLocaleString()}`, source: $("#latex-source").value },
|
||||
body: {
|
||||
title: `公式 ${new Date().toLocaleString()}`,
|
||||
source: normalizeLatexSource($("#latex-source").value),
|
||||
},
|
||||
});
|
||||
showToast("公式及首个版本已保存");
|
||||
} catch (error) {
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2020 Khan Academy and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -328,8 +328,8 @@
|
||||
<div class="tool-workspace" id="tool-latex">
|
||||
<div class="workspace-heading"><div><span class="kicker">LATEX LAB</span><h2>数学表达实验室</h2></div><p>源码、实时预览与版本保存。</p></div>
|
||||
<div class="editor-shell">
|
||||
<div class="editor-pane"><label for="latex-source">LaTeX 源码</label><textarea id="latex-source" spellcheck="false">\\int_{0}^{1} x^2\\,dx = \\frac{1}{3}</textarea></div>
|
||||
<div class="preview-pane"><span>实时预览</span><div id="latex-preview">\int_{0}^{1} x^2\,dx = \frac{1}{3}</div><button id="save-formula" class="primary-button">保存到我的公式</button></div>
|
||||
<div class="editor-pane"><label for="latex-source">LaTeX 源码</label><textarea id="latex-source" spellcheck="false">\int_{0}^{1} x^2\,dx = \frac{1}{3}</textarea></div>
|
||||
<div class="preview-pane"><span>实时预览</span><div id="latex-preview" role="status" aria-live="polite"></div><button id="save-formula" class="primary-button">保存到我的公式</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -228,7 +228,7 @@ test -s "$BACKUP_FILE" || fail "数据库备份为空: $BACKUP_FILE"
|
||||
find "$BACKUP_DIR" -type f -name 'hulumath_*.sql.gz' -mtime +14 -delete
|
||||
log "数据库备份完成: $BACKUP_FILE"
|
||||
|
||||
log "执行 Django 发布检查、迁移和静态资源收集"
|
||||
log "执行 Django 发布检查、静态资源收集和迁移"
|
||||
if [ -d "$PROJECT_DIR/backend/staticfiles" ]; then
|
||||
$SUDO chown -R "$DEPLOY_OWNER" "$PROJECT_DIR/backend/staticfiles"
|
||||
fi
|
||||
@@ -240,8 +240,8 @@ else
|
||||
"$VENV_DIR/bin/python" manage.py check --deploy --fail-level ERROR --database default
|
||||
fi
|
||||
"$VENV_DIR/bin/python" manage.py check_mysql
|
||||
"$VENV_DIR/bin/python" manage.py migrate --noinput
|
||||
"$VENV_DIR/bin/python" manage.py collectstatic --noinput
|
||||
"$VENV_DIR/bin/python" manage.py migrate --noinput
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
log "安装或更新 systemd 服务"
|
||||
|
||||
Reference in New Issue
Block a user