diff --git a/.env.example b/.env.example index 1bd505d..fdcb85d 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,17 @@ -# 复制为 .env 并填写密钥,或直接在下方编辑(请勿将真实密钥提交到 Git) -QWEN_API_KEY= +# 复制为 .env 并填写密钥(start.sh 会从未存在的 .env 复制本文件) +# QWEN_API_KEY 填 DashScope 控制台申请的 sk- 开头密钥,不要加引号;修改后需重启 backend 容器 +QWEN_API_KEY=sk-85880595fc714d63bfd0b025e917bd26 QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 -QWEN_MODEL=qwen-plus +QWEN_MODEL=qwen3.5-plus +# OCR / 视觉识别(与 DashScope 控制台开通的模型一致,如 qwen-vl-plus、qwen-vl-max) +# QWEN_VL_MODEL=qwen-vl-plus + +# 访问千问 API 时是否信任 HTTP_PROXY/HTTPS_PROXY(默认 0,避免 Docker 误用代理导致 httpx ConnectError) +# HTTPX_TRUST_ENV=0 + +# —— 可选:Docker 构建时的国内镜像(docker compose build 会读取本文件同名字段) +# PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ +# PIP_TRUSTED_HOST=mirrors.aliyun.com +# DEBIAN_MIRROR_HOST=mirrors.aliyun.com +# NPM_REGISTRY=https://registry.npmmirror.com +# ALPINE_MIRROR_HOST=mirrors.aliyun.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d313cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +!.env.example + +# 本地代理等个性化合并(参见 docker-compose.override.example.yml) +docker-compose.override.yml diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..2e194bc --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,9 @@ +__pycache__ +*.pyc +.git +.gitignore +*.md +.env +.venv +.pytest_cache +.mypy_cache diff --git a/backend/Dockerfile b/backend/Dockerfile index 08aa47a..304defe 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,14 +1,26 @@ -# 基础镜像来自 Docker Hub;请在本机配置阿里云镜像加速器后再构建(见仓库 docker-compose.yml 注释) FROM python:3.12-slim WORKDIR /app ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 -ENV PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ +# 默认直连外网 API,避免宿主机/Docker Desktop 传入的 *_PROXY 影响 httpx(需代理时在 compose 中覆盖) +ENV HTTPX_TRUST_ENV=0 +ENV HTTP_PROXY= +ENV HTTPS_PROXY= +ENV ALL_PROXY= +ENV http_proxy= +ENV https_proxy= +ENV all_proxy= +ENV NO_PROXY="*" + +ARG PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ +ARG PIP_TRUSTED_HOST=mirrors.aliyun.com +ARG DEBIAN_MIRROR_HOST=mirrors.aliyun.com +RUN sed -i "s/deb.debian.org/${DEBIAN_MIRROR_HOST}/g; s/security.debian.org/${DEBIAN_MIRROR_HOST}/g" /etc/apt/sources.list.d/debian.sources COPY requirements.txt /app/requirements.txt -RUN pip install --no-cache-dir -r /app/requirements.txt +RUN pip install --no-cache-dir -i "${PIP_INDEX_URL}" --trusted-host "${PIP_TRUSTED_HOST}" -r /app/requirements.txt COPY app /app/app diff --git a/backend/app/main.py b/backend/app/main.py index 6c6c44b..e2645a4 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,5 +1,9 @@ import os +import json import uuid +import zipfile +import base64 +from typing import Any from datetime import date, datetime, timedelta from io import BytesIO from pathlib import Path @@ -14,7 +18,7 @@ from reportlab.lib.pagesizes import A4 from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.cidfonts import UnicodeCIDFont from reportlab.pdfgen import canvas -from sqlalchemy import asc, desc, func, or_, select +from sqlalchemy import asc, desc, func, inspect, or_, select, text from sqlalchemy.orm import Session from .database import Base, engine, get_db @@ -27,6 +31,8 @@ from .schemas import ( MistakeCreate, MistakeOut, MistakeUpdate, + OcrParseIn, + OcrParseOut, ResourceBatchUpdate, ResourceCreate, ResourceOut, @@ -39,6 +45,23 @@ from .schemas import ( Base.metadata.create_all(bind=engine) + +def _migrate_mistake_columns() -> None: + inspector = inspect(engine) + if "mistakes" not in inspector.get_table_names(): + return + existed = {col["name"] for col in inspector.get_columns("mistakes")} + with engine.begin() as conn: + if "question_content" not in existed: + conn.execute(text("ALTER TABLE mistakes ADD COLUMN question_content TEXT")) + if "answer" not in existed: + conn.execute(text("ALTER TABLE mistakes ADD COLUMN answer TEXT")) + if "explanation" not in existed: + conn.execute(text("ALTER TABLE mistakes ADD COLUMN explanation TEXT")) + + +_migrate_mistake_columns() + app = FastAPI(title="公考助手 API", version="1.0.0") UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", "/app/uploads")) UPLOAD_DIR.mkdir(parents=True, exist_ok=True) @@ -69,6 +92,7 @@ def _query_mistakes_for_export( category: str | None, start_date: date | None, end_date: date | None, + ids: list[int] | None = None, ): stmt = select(Mistake) if category: @@ -77,12 +101,175 @@ def _query_mistakes_for_export( stmt = stmt.where(Mistake.created_at >= datetime.combine(start_date, datetime.min.time())) if end_date: stmt = stmt.where(Mistake.created_at <= datetime.combine(end_date, datetime.max.time())) + if ids: + stmt = stmt.where(Mistake.id.in_(ids)) items = db.scalars(stmt.order_by(desc(Mistake.created_at))).all() if len(items) > 200: raise HTTPException(status_code=400, detail="单次最多导出 200 题") return items +def _validate_mistake_payload(payload: MistakeCreate | MistakeUpdate) -> None: + has_image = bool((payload.image_url or "").strip()) + has_question = bool((payload.question_content or "").strip()) + has_answer = bool((payload.answer or "").strip()) + if not has_image and not has_question and not has_answer: + raise HTTPException(status_code=400, detail="请上传题目图片或填写试题/答案后再保存") + + +def _normalize_multiline_text(value: str | None) -> str: + if not value: + return "" + text_value = value.replace("\r\n", "\n").replace("\r", "\n") + lines = [line.strip() for line in text_value.split("\n")] + compact = [line for line in lines if line] + return "\n".join(compact).strip() + + +def _wrap_pdf_text(text: str, max_width: float, font_name: str = "STSong-Light", font_size: int = 12) -> list[str]: + normalized = _normalize_multiline_text(text) + if not normalized: + return [] + wrapped: list[str] = [] + for raw_line in normalized.split("\n"): + current = "" + for ch in raw_line: + candidate = f"{current}{ch}" + if pdfmetrics.stringWidth(candidate, font_name, font_size) <= max_width: + current = candidate + else: + if current: + wrapped.append(current) + current = ch + if current: + wrapped.append(current) + return wrapped + + +def _mistake_export_blocks(item: Mistake, content_mode: str) -> list[str]: + question = _normalize_multiline_text(item.question_content) + answer = _normalize_multiline_text(item.answer) + explanation = _normalize_multiline_text(item.explanation) + if not question: + question = "无题干与选项内容" + + blocks: list[str] = [question] + if content_mode == "full": + # 「答案:」「解析:」与正文同一行开头,避免标签单独成行(与题号+题干规则一致) + blocks.append(f"答案: {answer or '无'}") + blocks.append(f"解析: {explanation or '无'}") + return blocks + + +def _extract_upload_filename(url: str | None) -> str | None: + if not url or not url.startswith("/uploads/"): + return None + return Path(url).name + + +def _safe_datetime(value: str | None) -> datetime: + if not value: + return datetime.utcnow() + try: + return datetime.fromisoformat(value.replace("Z", "+00:00")).replace(tzinfo=None) + except ValueError: + return datetime.utcnow() + + +def _safe_date(value: str | None) -> date: + if not value: + return date.today() + try: + return date.fromisoformat(value) + except ValueError: + return date.today() + + +def _extract_json_text(raw_text: str) -> str: + content = raw_text.strip() + if content.startswith("```"): + lines = content.splitlines() + if lines: + lines = lines[1:] + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + content = "\n".join(lines).strip() + return content + + +def _dump_all_data(db: Session) -> dict: + resources = db.scalars(select(Resource).order_by(asc(Resource.id))).all() + mistakes = db.scalars(select(Mistake).order_by(asc(Mistake.id))).all() + scores = db.scalars(select(ScoreRecord).order_by(asc(ScoreRecord.id))).all() + return { + "meta": { + "exported_at": datetime.utcnow().isoformat(), + "version": "1.1.0", + }, + "resources": [ + { + "id": item.id, + "title": item.title, + "resource_type": item.resource_type, + "url": item.url, + "file_name": item.file_name, + "category": item.category, + "tags": item.tags, + "created_at": item.created_at.isoformat() if item.created_at else None, + } + for item in resources + ], + "mistakes": [ + { + "id": item.id, + "title": item.title, + "image_url": item.image_url, + "category": item.category, + "difficulty": item.difficulty, + "question_content": item.question_content, + "answer": item.answer, + "explanation": item.explanation, + "note": item.note, + "wrong_count": item.wrong_count, + "created_at": item.created_at.isoformat() if item.created_at else None, + } + for item in mistakes + ], + "scores": [ + { + "id": item.id, + "exam_name": item.exam_name, + "exam_date": item.exam_date.isoformat() if item.exam_date else None, + "total_score": item.total_score, + "module_scores": item.module_scores, + "created_at": item.created_at.isoformat() if item.created_at else None, + } + for item in scores + ], + } + + +def _restore_upload_url_from_zip(url: str | None, zip_ref: zipfile.ZipFile) -> str | None: + if not url: + return None + file_name = _extract_upload_filename(url) + if not file_name: + return url + + zip_path = f"uploads/{file_name}" + if zip_path not in zip_ref.namelist(): + return url + + data = zip_ref.read(zip_path) + target_name = file_name + target_path = UPLOAD_DIR / target_name + if target_path.exists(): + target_name = f"{uuid.uuid4().hex}_{file_name}" + target_path = UPLOAD_DIR / target_name + target_path.write_bytes(data) + return f"/uploads/{target_name}" + + @app.post("/api/upload") async def upload_file(file: UploadFile = File(...)): suffix = Path(file.filename or "").suffix.lower() @@ -197,7 +384,16 @@ def list_mistakes( if category: stmt = stmt.where(Mistake.category == category) if keyword: - stmt = stmt.where(or_(Mistake.note.ilike(f"%{keyword}%"), Mistake.title.ilike(f"%{keyword}%"))) + stmt = stmt.where( + or_( + Mistake.note.ilike(f"%{keyword}%"), + Mistake.title.ilike(f"%{keyword}%"), + Mistake.question_content.ilike(f"%{keyword}%"), + Mistake.answer.ilike(f"%{keyword}%"), + Mistake.explanation.ilike(f"%{keyword}%"), + Mistake.image_url.ilike(f"%{keyword}%"), + ) + ) sort_col = Mistake.created_at if sort_by == "created_at" else Mistake.wrong_count stmt = stmt.order_by(desc(sort_col) if order == "desc" else asc(sort_col)) return db.scalars(stmt).all() @@ -205,6 +401,7 @@ def list_mistakes( @app.post("/api/mistakes", response_model=MistakeOut) def create_mistake(payload: MistakeCreate, db: Session = Depends(get_db)): + _validate_mistake_payload(payload) item = Mistake(**payload.model_dump()) db.add(item) db.commit() @@ -214,6 +411,7 @@ def create_mistake(payload: MistakeCreate, db: Session = Depends(get_db)): @app.put("/api/mistakes/{item_id}", response_model=MistakeOut) def update_mistake(item_id: int, payload: MistakeUpdate, db: Session = Depends(get_db)): + _validate_mistake_payload(payload) item = db.get(Mistake, item_id) if not item: raise HTTPException(status_code=404, detail="Mistake not found") @@ -239,32 +437,44 @@ def export_mistakes_pdf( category: str | None = None, start_date: date | None = None, end_date: date | None = None, + ids: str | None = None, + content_mode: str = Query("full", pattern="^(full|question_only)$"), db: Session = Depends(get_db), ): - items = _query_mistakes_for_export(db, category, start_date, end_date) + id_list = [int(x) for x in ids.split(",") if x.strip().isdigit()] if ids else None + items = _query_mistakes_for_export(db, category, start_date, end_date, id_list) buf = BytesIO() pdf = canvas.Canvas(buf, pagesize=A4) pdfmetrics.registerFont(UnicodeCIDFont("STSong-Light")) pdf.setFont("STSong-Light", 12) y = 800 - pdf.drawString(50, y, "公考助手 - 错题导出") - y -= 30 + left = 48 + right = 560 + max_width = right - left + pdf.drawString(left, y, "公考助手 - 错题导出") + y -= 28 for idx, item in enumerate(items, start=1): - lines = [ - f"{idx}. {item.title}", - f"分类: {item.category} 难度: {item.difficulty or '未设置'} 错误频次: {item.wrong_count}", - f"备注: {item.note or '无'}", - "答题区: _______________________________", - ] - for line in lines: - if y < 70: - pdf.showPage() - pdf.setFont("STSong-Light", 12) - y = 800 - pdf.drawString(50, y, line[:90]) - y -= 22 - y -= 6 + if y < 90: + pdf.showPage() + pdf.setFont("STSong-Light", 12) + y = 800 + blocks = _mistake_export_blocks(item, content_mode) + for bi, block in enumerate(blocks): + # 题号与题干同一行开头,避免「1.」单独成行 + text = f"{idx}. {block}" if bi == 0 else block + lines = _wrap_pdf_text(text, max_width=max_width) + if not lines: + continue + for line in lines: + if y < 70: + pdf.showPage() + pdf.setFont("STSong-Light", 12) + y = 800 + pdf.drawString(left, y, line) + y -= 18 + y -= 6 + y -= 8 pdf.save() buf.seek(0) @@ -280,16 +490,20 @@ def export_mistakes_docx( category: str | None = None, start_date: date | None = None, end_date: date | None = None, + ids: str | None = None, + content_mode: str = Query("full", pattern="^(full|question_only)$"), db: Session = Depends(get_db), ): - items = _query_mistakes_for_export(db, category, start_date, end_date) + id_list = [int(x) for x in ids.split(",") if x.strip().isdigit()] if ids else None + items = _query_mistakes_for_export(db, category, start_date, end_date, id_list) doc = Document() doc.add_heading("公考助手 - 错题导出", level=1) for idx, item in enumerate(items, start=1): - doc.add_paragraph(f"{idx}. {item.title}") - doc.add_paragraph(f"分类: {item.category} | 难度: {item.difficulty or '未设置'} | 错误频次: {item.wrong_count}") - doc.add_paragraph(f"备注: {item.note or '无'}") - doc.add_paragraph("答题区: ________________________________________") + blocks = _mistake_export_blocks(item, content_mode) + for bi, block in enumerate(blocks): + # 题号与题干同段,避免单独一行只有「1.」 + para = f"{idx}. {block}" if bi == 0 else block + doc.add_paragraph(para) buf = BytesIO() doc.save(buf) @@ -362,8 +576,108 @@ def score_stats(db: Session = Depends(get_db)): return ScoreStats(highest=highest, lowest=lowest, average=round(float(avg), 2), improvement=improvement) +def _qwen_base_url() -> str: + return os.getenv("QWEN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1").strip().rstrip("/") + + +def _get_qwen_api_key() -> str: + """去除首尾空白与常见误加的引号,避免 .env 里写成 'sk-xxx' 导致鉴权失败。""" + raw = os.getenv("QWEN_API_KEY", "") or "" + return raw.strip().strip('"').strip("'").strip() + + +def _raise_for_qwen_http_error(resp: httpx.Response, prefix: str) -> None: + """HTTP 非 2xx 时解析 DashScope 错误体,对 invalid_api_key 返回 401 + 明确说明。""" + if resp.status_code < 300: + return + text = resp.text + try: + data = resp.json() + err = data.get("error") + if isinstance(err, dict): + code = str(err.get("code") or "") + if code == "invalid_api_key": + raise HTTPException( + status_code=401, + detail=( + "阿里云 DashScope API Key 无效或未生效。" + "请到阿里云百炼 / Model Studio 控制台创建 API Key(通常以 sk- 开头)," + "写入项目根目录 .env 的 QWEN_API_KEY=,勿加引号;" + "修改后执行: docker compose up -d --build backend" + ), + ) + msg = err.get("message") or text + raise HTTPException(status_code=502, detail=f"{prefix}: {msg}") + except HTTPException: + raise + except (ValueError, TypeError, KeyError): + pass + raise HTTPException(status_code=502, detail=f"{prefix}: {text[:1200]}") + + +def _httpx_trust_env() -> bool: + """默认不信任环境变量中的代理,避免 Docker/IDE 注入空代理导致 ConnectError;需走系统代理时设 HTTPX_TRUST_ENV=1。""" + return os.getenv("HTTPX_TRUST_ENV", "0").lower() in ("1", "true", "yes") + + +def _qwen_http_client(timeout_sec: float = 60.0) -> httpx.AsyncClient: + return httpx.AsyncClient( + timeout=httpx.Timeout(timeout_sec, connect=20.0), + trust_env=_httpx_trust_env(), + limits=httpx.Limits(max_keepalive_connections=5, max_connections=10), + ) + + +def _message_content_to_str(content: Any) -> str: + """OpenAI 兼容接口里 message.content 可能是 str 或多段结构。""" + if content is None: + return "" + if isinstance(content, str): + return content + if isinstance(content, list): + parts: list[str] = [] + for part in content: + if isinstance(part, dict): + if part.get("type") == "text" and "text" in part: + parts.append(str(part["text"])) + elif "text" in part: + parts.append(str(part["text"])) + elif isinstance(part, str): + parts.append(part) + return "".join(parts) + return str(content) + + +def _openai_completion_assistant_text(data: dict) -> str: + """从 chat/completions JSON 中取出助手文本;若含 error 或无 choices 则抛错。""" + err = data.get("error") + if err is not None: + if isinstance(err, dict): + code = str(err.get("code") or "") + if code == "invalid_api_key": + raise HTTPException( + status_code=401, + detail=( + "阿里云 DashScope API Key 无效。" + "请在 .env 中填写正确的 QWEN_API_KEY 并重启 backend。" + ), + ) + msg = err.get("message") or err.get("code") or json.dumps(err, ensure_ascii=False) + else: + msg = str(err) + raise HTTPException(status_code=502, detail=f"千问接口错误: {msg}") + choices = data.get("choices") + if not choices: + raise HTTPException( + status_code=502, + detail=f"千问返回异常(无 choices),请检查模型名与权限。原始片段: {json.dumps(data, ensure_ascii=False)[:800]}", + ) + msg = choices[0].get("message") or {} + return _message_content_to_str(msg.get("content")) + + async def _call_qwen(system_prompt: str, user_prompt: str) -> str: - api_key = os.getenv("QWEN_API_KEY", "") + api_key = _get_qwen_api_key() if not api_key: return ( "当前未配置千问 API Key,已返回本地降级提示。\n" @@ -373,7 +687,7 @@ async def _call_qwen(system_prompt: str, user_prompt: str) -> str: "QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1\n" "QWEN_MODEL=qwen-plus" ) - base_url = os.getenv("QWEN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1") + base_url = _qwen_base_url() model = os.getenv("QWEN_MODEL", "qwen-plus") headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} payload = { @@ -384,12 +698,333 @@ async def _call_qwen(system_prompt: str, user_prompt: str) -> str: ], "temperature": 0.4, } - async with httpx.AsyncClient(timeout=40) as client: - resp = await client.post(f"{base_url}/chat/completions", headers=headers, json=payload) - if resp.status_code >= 300: - raise HTTPException(status_code=502, detail=f"千问请求失败: {resp.text}") - data = resp.json() - return data["choices"][0]["message"]["content"] + url = f"{base_url}/chat/completions" + try: + async with _qwen_http_client(40.0) as client: + resp = await client.post(url, headers=headers, json=payload) + except httpx.ConnectError as e: + raise HTTPException( + status_code=502, + detail=( + f"无法连接千问接口({url})。请检查本机/容器能否访问外网、DNS 是否正常;" + "若在 Docker 中可尝试为 backend 配置 dns 或关闭错误代理。" + "默认已忽略 HTTP(S)_PROXY,若需代理请设置 HTTPX_TRUST_ENV=1。" + f" 原始错误: {e!s}" + ), + ) from e + except httpx.TimeoutException as e: + raise HTTPException(status_code=504, detail=f"千问请求超时: {e!s}") from e + _raise_for_qwen_http_error(resp, "千问请求失败") + try: + data = resp.json() + except ValueError: + raise HTTPException(status_code=502, detail=f"千问返回非 JSON: {resp.text[:600]}") + return _openai_completion_assistant_text(data) + + +async def _call_qwen_vision(system_prompt: str, user_prompt: str, image_data_url: str) -> str: + api_key = _get_qwen_api_key() + if not api_key: + return ( + "当前未配置千问 API Key,无法执行 OCR。\n" + "请在 .env 中配置 QWEN_API_KEY 后重试。" + ) + base_url = _qwen_base_url() + model = os.getenv("QWEN_VL_MODEL", "qwen-vl-plus") + headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + # 与 DashScope 文档一致:先图后文,利于多模态路由 + payload = { + "model": model, + "messages": [ + {"role": "system", "content": system_prompt}, + { + "role": "user", + "content": [ + {"type": "image_url", "image_url": {"url": image_data_url}}, + {"type": "text", "text": user_prompt}, + ], + }, + ], + "temperature": 0.2, + } + url = f"{base_url}/chat/completions" + try: + async with _qwen_http_client(60.0) as client: + resp = await client.post(url, headers=headers, json=payload) + except httpx.ConnectError as e: + raise HTTPException( + status_code=502, + detail=( + f"无法连接千问接口(OCR,{url})。请检查网络与 DNS;" + "默认已忽略 HTTP(S)_PROXY,若需代理请设置 HTTPX_TRUST_ENV=1。" + f" 原始错误: {e!s}" + ), + ) from e + except httpx.TimeoutException as e: + raise HTTPException(status_code=504, detail=f"OCR 请求超时: {e!s}") from e + except httpx.RequestError as e: + raise HTTPException(status_code=502, detail=f"OCR 网络请求失败: {e!s}") from e + _raise_for_qwen_http_error(resp, "OCR 请求失败") + try: + data = resp.json() + except ValueError: + raise HTTPException(status_code=502, detail=f"OCR 返回非 JSON: {resp.text[:600]}") + return _openai_completion_assistant_text(data) + + +@app.get("/api/data/export") +def export_user_data( + format: str = Query("zip", pattern="^(zip|json)$"), + include_files: bool = True, + db: Session = Depends(get_db), +): + payload = _dump_all_data(db) + timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S") + + if format == "json": + buf = BytesIO(json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")) + return StreamingResponse( + buf, + media_type="application/json", + headers={"Content-Disposition": f'attachment; filename="exam_helper_backup_{timestamp}.json"'}, + ) + + used_upload_files: set[str] = set() + for item in payload["resources"]: + name = _extract_upload_filename(item.get("url")) + if name: + used_upload_files.add(name) + for item in payload["mistakes"]: + name = _extract_upload_filename(item.get("image_url")) + if name: + used_upload_files.add(name) + + buf = BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zip_ref: + zip_ref.writestr("data.json", json.dumps(payload, ensure_ascii=False, indent=2)) + if include_files: + for file_name in sorted(used_upload_files): + path = UPLOAD_DIR / file_name + if path.exists() and path.is_file(): + zip_ref.write(path, arcname=f"uploads/{file_name}") + buf.seek(0) + return StreamingResponse( + buf, + media_type="application/zip", + headers={"Content-Disposition": f'attachment; filename="exam_helper_backup_{timestamp}.zip"'}, + ) + + +@app.post("/api/data/import") +async def import_user_data( + file: UploadFile = File(...), + mode: str = Query("merge", pattern="^(merge|replace)$"), + db: Session = Depends(get_db), +): + content = await file.read() + if not content: + raise HTTPException(status_code=400, detail="导入文件为空") + if len(content) > 100 * 1024 * 1024: + raise HTTPException(status_code=400, detail="导入文件不能超过 100MB") + + suffix = Path(file.filename or "").suffix.lower() + payload: dict + zip_ref: zipfile.ZipFile | None = None + + if suffix == ".json": + try: + payload = json.loads(content.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise HTTPException(status_code=400, detail=f"JSON 解析失败: {exc}") from exc + elif suffix == ".zip": + try: + zip_ref = zipfile.ZipFile(BytesIO(content)) + except zipfile.BadZipFile as exc: + raise HTTPException(status_code=400, detail="ZIP 文件损坏或格式错误") from exc + if "data.json" not in zip_ref.namelist(): + raise HTTPException(status_code=400, detail="ZIP 中缺少 data.json") + try: + payload = json.loads(zip_ref.read("data.json").decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise HTTPException(status_code=400, detail=f"data.json 解析失败: {exc}") from exc + else: + raise HTTPException(status_code=400, detail="仅支持 .json 或 .zip 导入") + + resources = payload.get("resources", []) + mistakes = payload.get("mistakes", []) + scores = payload.get("scores", []) + if not isinstance(resources, list) or not isinstance(mistakes, list) or not isinstance(scores, list): + raise HTTPException(status_code=400, detail="导入文件结构错误") + + if mode == "replace": + for item in db.scalars(select(Resource)).all(): + db.delete(item) + for item in db.scalars(select(Mistake)).all(): + db.delete(item) + for item in db.scalars(select(ScoreRecord)).all(): + db.delete(item) + db.commit() + + imported = {"resources": 0, "mistakes": 0, "scores": 0} + + for item in resources: + url = item.get("url") + if zip_ref is not None: + url = _restore_upload_url_from_zip(url, zip_ref) + obj = Resource( + title=item.get("title") or "未命名资源", + resource_type=item.get("resource_type") if item.get("resource_type") in {"link", "file"} else "link", + url=url, + file_name=item.get("file_name"), + category=item.get("category") or "未分类", + tags=item.get("tags"), + created_at=_safe_datetime(item.get("created_at")), + ) + db.add(obj) + imported["resources"] += 1 + + for item in mistakes: + image_url = item.get("image_url") + if zip_ref is not None: + image_url = _restore_upload_url_from_zip(image_url, zip_ref) + difficulty = item.get("difficulty") + obj = Mistake( + title=item.get("title") or "未命名错题", + image_url=image_url, + category=item.get("category") or "其他", + difficulty=difficulty if difficulty in {"easy", "medium", "hard"} else None, + question_content=item.get("question_content"), + answer=item.get("answer"), + explanation=item.get("explanation"), + note=item.get("note"), + wrong_count=max(int(item.get("wrong_count") or 1), 1), + created_at=_safe_datetime(item.get("created_at")), + ) + db.add(obj) + imported["mistakes"] += 1 + + for item in scores: + score = float(item.get("total_score") or 0) + obj = ScoreRecord( + exam_name=item.get("exam_name") or "未命名考试", + exam_date=_safe_date(item.get("exam_date")), + total_score=max(min(score, 200), 0), + module_scores=item.get("module_scores"), + created_at=_safe_datetime(item.get("created_at")), + ) + db.add(obj) + imported["scores"] += 1 + + db.commit() + return {"success": True, "mode": mode, "imported": imported} + + +@app.post("/api/ocr/parse", response_model=OcrParseOut) +async def parse_ocr(payload: OcrParseIn): + file_name = _extract_upload_filename(payload.image_url) + if not file_name: + raise HTTPException(status_code=400, detail="仅支持 /uploads 下的图片做 OCR") + target = UPLOAD_DIR / file_name + if not target.exists() or not target.is_file(): + raise HTTPException(status_code=404, detail="图片不存在或已删除") + + suffix = target.suffix.lower() + mime = { + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".webp": "image/webp", + }.get(suffix) + if not mime: + raise HTTPException(status_code=400, detail="仅支持 JPG/PNG/WebP OCR") + + b64 = base64.b64encode(target.read_bytes()).decode("utf-8") + image_data_url = f"data:{mime};base64,{b64}" + ocr_prompt = ( + "请识别图片中的题目,返回严格 JSON。" + "字段说明:text 为整题完整纯文本(含材料、提问句、全部选项);" + "question_content 必须与 text 一致地表示「完整题干」,须包含阅读材料、填空/提问句、所有选项(A B C D 等)," + "禁止只填写「依次填入…」等短提示句而省略材料和选项。" + "另含 title_suggestion、category_suggestion、difficulty_suggestion、answer、explanation。" + "无法确认的字段可填空字符串。" + ) + if payload.prompt: + ocr_prompt = f"{ocr_prompt}\n补充要求:{payload.prompt}" + + raw_text = await _call_qwen_vision( + "你是公考题目OCR与结构化助手。输出必须是 JSON,不要额外解释。", + ocr_prompt, + image_data_url, + ) + + try: + parsed = json.loads(_extract_json_text(raw_text)) + data = ( + parsed + if isinstance(parsed, dict) + else { + "text": raw_text.strip(), + "title_suggestion": None, + "category_suggestion": None, + "difficulty_suggestion": None, + "question_content": raw_text.strip(), + "answer": "", + "explanation": "", + } + ) + except json.JSONDecodeError: + data = { + "text": raw_text.strip(), + "title_suggestion": None, + "category_suggestion": None, + "difficulty_suggestion": None, + "question_content": raw_text.strip(), + "answer": "", + "explanation": "", + } + + def _opt_str(val: Any) -> str | None: + if val is None: + return None + if isinstance(val, (dict, list)): + return None + s = str(val).strip() + return s if s else None + + def _merge_question_body(text_raw: str, qc_raw: str | None) -> str | None: + """模型常把全文放在 text,却只把短问句放在 question_content;合并时以更长、更完整的文本为准。""" + t = (text_raw or "").strip() + q = (qc_raw or "").strip() + if not t and not q: + return None + if not q: + return t or None + if not t: + return q or None + if len(t) > len(q): + return t + if len(q) > len(t): + return q + # 长度接近或相等:若一方包含另一方,取更长;否则保留 text(整页 OCR 通常更全) + if t in q: + return q + if q in t: + return t + return t + + text_out = str(data.get("text", "") or "").strip() + qc_model = _opt_str(data.get("question_content")) + question_merged = _merge_question_body(text_out, qc_model) + + return OcrParseOut( + text=text_out, + title_suggestion=_opt_str(data.get("title_suggestion")), + category_suggestion=_opt_str(data.get("category_suggestion")), + difficulty_suggestion=_opt_str(data.get("difficulty_suggestion")), + question_content=_opt_str(question_merged), + answer=_opt_str(data.get("answer")), + explanation=_opt_str(data.get("explanation")), + ) @app.post("/api/ai/mistakes/{item_id}/analyze", response_model=AiMistakeAnalysisOut) @@ -403,6 +1038,9 @@ async def ai_analyze_mistake(item_id: int, db: Session = Depends(get_db)): f"错题标题: {item.title}\n" f"分类: {item.category}\n" f"难度: {item.difficulty or '未设置'}\n" + f"题目内容: {item.question_content or '无'}\n" + f"答案: {item.answer or '无'}\n" + f"解析: {item.explanation or '无'}\n" f"错误频次: {item.wrong_count}\n" f"备注: {item.note or '无'}\n\n" "请按以下结构输出:\n" diff --git a/backend/app/models.py b/backend/app/models.py index 1c9b527..181ec36 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -27,6 +27,9 @@ class Mistake(Base): image_url: Mapped[str | None] = mapped_column(String(1024), nullable=True) category: Mapped[str] = mapped_column(String(50), nullable=False) difficulty: Mapped[str | None] = mapped_column(String(20), nullable=True) # easy/medium/hard + question_content: Mapped[str | None] = mapped_column(Text, nullable=True) + answer: Mapped[str | None] = mapped_column(Text, nullable=True) + explanation: Mapped[str | None] = mapped_column(Text, nullable=True) note: Mapped[str | None] = mapped_column(Text, nullable=True) wrong_count: Mapped[int] = mapped_column(Integer, default=1) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 2e67a0f..9087a09 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -31,7 +31,10 @@ class MistakeBase(BaseModel): image_url: str | None = None category: str difficulty: str | None = Field(None, pattern="^(easy|medium|hard)$") - note: str | None = Field(None, max_length=500) + question_content: str | None = Field(None, max_length=8000) + answer: str | None = Field(None, max_length=4000) + explanation: str | None = Field(None, max_length=8000) + note: str | None = Field(None, max_length=4000) wrong_count: int = Field(1, ge=1) @@ -99,3 +102,18 @@ class AiStudyPlanIn(BaseModel): class AiStudyPlanOut(BaseModel): plan: str + + +class OcrParseIn(BaseModel): + image_url: str = Field(..., max_length=1024) + prompt: str | None = Field(None, max_length=500) + + +class OcrParseOut(BaseModel): + text: str + title_suggestion: str | None = None + category_suggestion: str | None = None + difficulty_suggestion: str | None = None + question_content: str | None = None + answer: str | None = None + explanation: str | None = None diff --git a/docker-compose.override.example.yml b/docker-compose.override.example.yml new file mode 100644 index 0000000..369d561 --- /dev/null +++ b/docker-compose.override.example.yml @@ -0,0 +1,13 @@ +# 若访问 DashScope 必须经过宿主机代理(如 Clash 端口),复制本文件为 docker-compose.override.yml 再按需修改。 +# docker compose 会自动合并 override,无需改主文件。 +# +# cp docker-compose.override.example.yml docker-compose.override.yml +# +services: + backend: + environment: + HTTPX_TRUST_ENV: "1" + HTTPS_PROXY: http://host.docker.internal:7890 + HTTP_PROXY: http://host.docker.internal:7890 + ALL_PROXY: http://host.docker.internal:7890 + NO_PROXY: localhost,127.0.0.1,db,frontend,.aliyuncs.com diff --git a/docker-compose.yml b/docker-compose.yml index a19c260..627b165 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,8 @@ -# Docker Hub 官方镜像:请在本机 Docker 中配置「阿里云镜像加速器」后再构建/拉取 -# (控制台:容器镜像服务 ACR → 镜像工具 → 镜像加速器,将地址写入 Docker Engine 的 registry-mirrors) +# 构建参数默认走国内源;可在项目根 .env 中覆盖(Compose 会自动加载 .env) +# 基础镜像 postgres/nginx/python/node 仍从 Docker Hub 拉取,请在 Docker Desktop 配置 registry-mirrors(见 scripts/configure-mirrors.sh) +# +# backend 已默认清空 HTTP(S)_PROXY,避免宿主机/IDE 代理注入容器导致访问 dashscope 失败;若必须走代理访问外网,请用 +# docker-compose.override.yml 覆盖 HTTPX_TRUST_ENV=1 并设置代理变量(见 .env.example 说明) services: db: image: postgres:16-alpine @@ -20,8 +23,16 @@ services: backend: build: context: ./backend + args: + PIP_INDEX_URL: ${PIP_INDEX_URL:-https://mirrors.aliyun.com/pypi/simple/} + PIP_TRUSTED_HOST: ${PIP_TRUSTED_HOST:-mirrors.aliyun.com} + DEBIAN_MIRROR_HOST: ${DEBIAN_MIRROR_HOST:-mirrors.aliyun.com} container_name: exam-helper-backend restart: unless-stopped + # 改善容器内解析 dashscope.aliyuncs.com(若仍 ConnectError,再检查宿主机网络/代理) + dns: + - 223.5.5.5 + - 223.6.6.6 healthcheck: test: [ @@ -34,7 +45,7 @@ services: timeout: 5s retries: 12 start_period: 25s - # AI 相关变量优先从 .env.example 注入;若存在 .env 则覆盖同名项(见 Compose env_file 顺序) + # 先加载 .env.example;若存在 .env 则合并覆盖(推荐用 start.sh 或 scripts/bootstrap-env.sh 生成 .env) env_file: - .env.example - path: .env @@ -44,17 +55,31 @@ services: # 本地开发 Vite + Docker 前端均需允许 CORS_ORIGINS: http://localhost:5173,http://localhost:8173 UPLOAD_DIR: /app/uploads + # 覆盖 .env 中可能误带的代理,保证访问千问与 pip 行为解耦(千问用 httpx trust_env=false + 此处清空) + HTTPX_TRUST_ENV: "0" + HTTP_PROXY: "" + HTTPS_PROXY: "" + ALL_PROXY: "" + http_proxy: "" + https_proxy: "" + all_proxy: "" + NO_PROXY: "*" volumes: - uploads_data:/app/uploads depends_on: db: condition: service_healthy + # 仅绑定本机回环;需手机/局域网访问可改为 "8866:8000" ports: - "127.0.0.1:8866:8000" frontend: build: context: ./frontend + args: + NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmmirror.com} + DEBIAN_MIRROR_HOST: ${DEBIAN_MIRROR_HOST:-mirrors.aliyun.com} + ALPINE_MIRROR_HOST: ${ALPINE_MIRROR_HOST:-mirrors.aliyun.com} container_name: exam-helper-frontend restart: unless-stopped depends_on: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index f9f7458..c5ff851 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,20 +1,26 @@ -# 基础镜像来自 Docker Hub;请在本机配置阿里云镜像加速器后再构建(见仓库 docker-compose.yml 注释) -FROM node:22-alpine AS builder +# builder:Debian bookworm + glibc,避免 Alpine 下 Vite/esbuild 段错误 +FROM node:22-bookworm-slim AS builder WORKDIR /app -RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories -COPY package.json /app/package.json -# 阿里云镜像站指定的 npm 同步源(原淘宝镜像域名已迁移至 npmmirror) -RUN npm config set registry https://registry.npmmirror.com && npm install +ARG NPM_REGISTRY=https://registry.npmmirror.com +ARG DEBIAN_MIRROR_HOST=mirrors.aliyun.com +RUN sed -i "s/deb.debian.org/${DEBIAN_MIRROR_HOST}/g; s/security.debian.org/${DEBIAN_MIRROR_HOST}/g" /etc/apt/sources.list.d/debian.sources + +RUN npm config set registry "${NPM_REGISTRY}" + +COPY package.json package-lock.json /app/ +RUN npm ci COPY . /app RUN npm run build \ && test -f /app/dist/index.html \ && ls -la /app/dist +# 运行阶段:Nginx Alpine,apk 使用国内镜像加速 FROM nginx:1.27-alpine -RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories +ARG ALPINE_MIRROR_HOST=mirrors.aliyun.com +RUN sed -i "s/dl-cdn.alpinelinux.org/${ALPINE_MIRROR_HOST}/g" /etc/apk/repositories COPY nginx.conf /etc/nginx/conf.d/default.conf COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 diff --git a/frontend/index.html b/frontend/index.html index 8febbc0..b8f8de5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ - + diff --git a/frontend/node_modules/.bin/baseline-browser-mapping 2 b/frontend/node_modules/.bin/baseline-browser-mapping 2 new file mode 120000 index 0000000..8e9a12d --- /dev/null +++ b/frontend/node_modules/.bin/baseline-browser-mapping 2 @@ -0,0 +1 @@ +../baseline-browser-mapping/dist/cli.cjs \ No newline at end of file diff --git a/frontend/node_modules/.bin/browserslist 2 b/frontend/node_modules/.bin/browserslist 2 new file mode 120000 index 0000000..3cd991b --- /dev/null +++ b/frontend/node_modules/.bin/browserslist 2 @@ -0,0 +1 @@ +../browserslist/cli.js \ No newline at end of file diff --git a/frontend/node_modules/.bin/esbuild 2 b/frontend/node_modules/.bin/esbuild 2 new file mode 120000 index 0000000..c83ac07 --- /dev/null +++ b/frontend/node_modules/.bin/esbuild 2 @@ -0,0 +1 @@ +../esbuild/bin/esbuild \ No newline at end of file diff --git a/frontend/node_modules/.bin/jsesc 2 b/frontend/node_modules/.bin/jsesc 2 new file mode 120000 index 0000000..7237604 --- /dev/null +++ b/frontend/node_modules/.bin/jsesc 2 @@ -0,0 +1 @@ +../jsesc/bin/jsesc \ No newline at end of file diff --git a/frontend/node_modules/.bin/json5 2 b/frontend/node_modules/.bin/json5 2 new file mode 120000 index 0000000..217f379 --- /dev/null +++ b/frontend/node_modules/.bin/json5 2 @@ -0,0 +1 @@ +../json5/lib/cli.js \ No newline at end of file diff --git a/frontend/node_modules/.bin/loose-envify 2 b/frontend/node_modules/.bin/loose-envify 2 new file mode 120000 index 0000000..ed9009c --- /dev/null +++ b/frontend/node_modules/.bin/loose-envify 2 @@ -0,0 +1 @@ +../loose-envify/cli.js \ No newline at end of file diff --git a/frontend/node_modules/.bin/nanoid 2 b/frontend/node_modules/.bin/nanoid 2 new file mode 120000 index 0000000..e2be547 --- /dev/null +++ b/frontend/node_modules/.bin/nanoid 2 @@ -0,0 +1 @@ +../nanoid/bin/nanoid.cjs \ No newline at end of file diff --git a/frontend/node_modules/.bin/parser 2 b/frontend/node_modules/.bin/parser 2 new file mode 120000 index 0000000..ce7bf97 --- /dev/null +++ b/frontend/node_modules/.bin/parser 2 @@ -0,0 +1 @@ +../@babel/parser/bin/babel-parser.js \ No newline at end of file diff --git a/frontend/node_modules/.bin/rollup 2 b/frontend/node_modules/.bin/rollup 2 new file mode 120000 index 0000000..5939621 --- /dev/null +++ b/frontend/node_modules/.bin/rollup 2 @@ -0,0 +1 @@ +../rollup/dist/bin/rollup \ No newline at end of file diff --git a/frontend/node_modules/.bin/semver 2 b/frontend/node_modules/.bin/semver 2 new file mode 120000 index 0000000..5aaadf4 --- /dev/null +++ b/frontend/node_modules/.bin/semver 2 @@ -0,0 +1 @@ +../semver/bin/semver.js \ No newline at end of file diff --git a/frontend/node_modules/.bin/update-browserslist-db 2 b/frontend/node_modules/.bin/update-browserslist-db 2 new file mode 120000 index 0000000..b11e16f --- /dev/null +++ b/frontend/node_modules/.bin/update-browserslist-db 2 @@ -0,0 +1 @@ +../update-browserslist-db/cli.js \ No newline at end of file diff --git a/frontend/node_modules/.bin/vite 2 b/frontend/node_modules/.bin/vite 2 new file mode 120000 index 0000000..6d1e3be --- /dev/null +++ b/frontend/node_modules/.bin/vite 2 @@ -0,0 +1 @@ +../vite/bin/vite.js \ No newline at end of file diff --git a/frontend/node_modules/.package-lock.json b/frontend/node_modules/.package-lock.json index df3068f..06d9f6c 100644 --- a/frontend/node_modules/.package-lock.json +++ b/frontend/node_modules/.package-lock.json @@ -1002,6 +1002,21 @@ "node": ">= 6" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/frontend/node_modules/call-bind-apply-helpers/.eslintrc 2 b/frontend/node_modules/call-bind-apply-helpers/.eslintrc 2 new file mode 100644 index 0000000..201e859 --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/.eslintrc 2 @@ -0,0 +1,17 @@ +{ + "root": true, + + "extends": "@ljharb", + + "rules": { + "func-name-matching": 0, + "id-length": 0, + "new-cap": [2, { + "capIsNewExceptions": [ + "GetIntrinsic", + ], + }], + "no-extra-parens": 0, + "no-magic-numbers": 0, + }, +} diff --git a/frontend/node_modules/call-bind-apply-helpers/.nycrc 2 b/frontend/node_modules/call-bind-apply-helpers/.nycrc 2 new file mode 100644 index 0000000..bdd626c --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/.nycrc 2 @@ -0,0 +1,9 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "text", "html", "json"], + "exclude": [ + "coverage", + "test" + ] +} diff --git a/frontend/node_modules/call-bind-apply-helpers/CHANGELOG 2.md b/frontend/node_modules/call-bind-apply-helpers/CHANGELOG 2.md new file mode 100644 index 0000000..2484942 --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/CHANGELOG 2.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v1.0.2](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.1...v1.0.2) - 2025-02-12 + +### Commits + +- [types] improve inferred types [`e6f9586`](https://github.com/ljharb/call-bind-apply-helpers/commit/e6f95860a3c72879cb861a858cdfb8138fbedec1) +- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `@types/tape`, `es-value-fixtures`, `for-each`, `has-strict-mode`, `object-inspect` [`e43d540`](https://github.com/ljharb/call-bind-apply-helpers/commit/e43d5409f97543bfbb11f345d47d8ce4e066d8c1) + +## [v1.0.1](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.0...v1.0.1) - 2024-12-08 + +### Commits + +- [types] `reflectApply`: fix types [`4efc396`](https://github.com/ljharb/call-bind-apply-helpers/commit/4efc3965351a4f02cc55e836fa391d3d11ef2ef8) +- [Fix] `reflectApply`: oops, Reflect is not a function [`83cc739`](https://github.com/ljharb/call-bind-apply-helpers/commit/83cc7395de6b79b7730bdf092f1436f0b1263c75) +- [Dev Deps] update `@arethetypeswrong/cli` [`80bd5d3`](https://github.com/ljharb/call-bind-apply-helpers/commit/80bd5d3ae58b4f6b6995ce439dd5a1bcb178a940) + +## v1.0.0 - 2024-12-05 + +### Commits + +- Initial implementation, tests, readme [`7879629`](https://github.com/ljharb/call-bind-apply-helpers/commit/78796290f9b7430c9934d6f33d94ae9bc89fce04) +- Initial commit [`3f1dc16`](https://github.com/ljharb/call-bind-apply-helpers/commit/3f1dc164afc43285631b114a5f9dd9137b2b952f) +- npm init [`081df04`](https://github.com/ljharb/call-bind-apply-helpers/commit/081df048c312fcee400922026f6e97281200a603) +- Only apps should have lockfiles [`5b9ca0f`](https://github.com/ljharb/call-bind-apply-helpers/commit/5b9ca0fe8101ebfaf309c549caac4e0a017ed930) diff --git a/frontend/node_modules/call-bind-apply-helpers/LICENSE 2 b/frontend/node_modules/call-bind-apply-helpers/LICENSE 2 new file mode 100644 index 0000000..f82f389 --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/LICENSE 2 @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Jordan Harband + +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. diff --git a/frontend/node_modules/call-bind-apply-helpers/README 2.md b/frontend/node_modules/call-bind-apply-helpers/README 2.md new file mode 100644 index 0000000..8fc0dae --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/README 2.md @@ -0,0 +1,62 @@ +# call-bind-apply-helpers [![Version Badge][npm-version-svg]][package-url] + +[![github actions][actions-image]][actions-url] +[![coverage][codecov-image]][codecov-url] +[![dependency status][deps-svg]][deps-url] +[![dev dependency status][dev-deps-svg]][dev-deps-url] +[![License][license-image]][license-url] +[![Downloads][downloads-image]][downloads-url] + +[![npm badge][npm-badge-png]][package-url] + +Helper functions around Function call/apply/bind, for use in `call-bind`. + +The only packages that should likely ever use this package directly are `call-bind` and `get-intrinsic`. +Please use `call-bind` unless you have a very good reason not to. + +## Getting started + +```sh +npm install --save call-bind-apply-helpers +``` + +## Usage/Examples + +```js +const assert = require('assert'); +const callBindBasic = require('call-bind-apply-helpers'); + +function f(a, b) { + assert.equal(this, 1); + assert.equal(a, 2); + assert.equal(b, 3); + assert.equal(arguments.length, 2); +} + +const fBound = callBindBasic([f, 1]); + +delete Function.prototype.call; +delete Function.prototype.bind; + +fBound(2, 3); +``` + +## Tests + +Clone the repo, `npm install`, and run `npm test` + +[package-url]: https://npmjs.org/package/call-bind-apply-helpers +[npm-version-svg]: https://versionbadg.es/ljharb/call-bind-apply-helpers.svg +[deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers.svg +[deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers +[dev-deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers/dev-status.svg +[dev-deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers#info=devDependencies +[npm-badge-png]: https://nodei.co/npm/call-bind-apply-helpers.png?downloads=true&stars=true +[license-image]: https://img.shields.io/npm/l/call-bind-apply-helpers.svg +[license-url]: LICENSE +[downloads-image]: https://img.shields.io/npm/dm/call-bind-apply-helpers.svg +[downloads-url]: https://npm-stat.com/charts.html?package=call-bind-apply-helpers +[codecov-image]: https://codecov.io/gh/ljharb/call-bind-apply-helpers/branch/main/graphs/badge.svg +[codecov-url]: https://app.codecov.io/gh/ljharb/call-bind-apply-helpers/ +[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/call-bind-apply-helpers +[actions-url]: https://github.com/ljharb/call-bind-apply-helpers/actions diff --git a/frontend/node_modules/call-bind-apply-helpers/actualApply 2.js b/frontend/node_modules/call-bind-apply-helpers/actualApply 2.js new file mode 100644 index 0000000..ffa5135 --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/actualApply 2.js @@ -0,0 +1,10 @@ +'use strict'; + +var bind = require('function-bind'); + +var $apply = require('./functionApply'); +var $call = require('./functionCall'); +var $reflectApply = require('./reflectApply'); + +/** @type {import('./actualApply')} */ +module.exports = $reflectApply || bind.call($call, $apply); diff --git a/frontend/node_modules/call-bind-apply-helpers/actualApply.d 2.ts b/frontend/node_modules/call-bind-apply-helpers/actualApply.d 2.ts new file mode 100644 index 0000000..b87286a --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/actualApply.d 2.ts @@ -0,0 +1 @@ +export = Reflect.apply; \ No newline at end of file diff --git a/frontend/node_modules/call-bind-apply-helpers/applyBind 2.js b/frontend/node_modules/call-bind-apply-helpers/applyBind 2.js new file mode 100644 index 0000000..d2b7723 --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/applyBind 2.js @@ -0,0 +1,10 @@ +'use strict'; + +var bind = require('function-bind'); +var $apply = require('./functionApply'); +var actualApply = require('./actualApply'); + +/** @type {import('./applyBind')} */ +module.exports = function applyBind() { + return actualApply(bind, $apply, arguments); +}; diff --git a/frontend/node_modules/call-bind-apply-helpers/applyBind.d 2.ts b/frontend/node_modules/call-bind-apply-helpers/applyBind.d 2.ts new file mode 100644 index 0000000..d176c1a --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/applyBind.d 2.ts @@ -0,0 +1,19 @@ +import actualApply from './actualApply'; + +type TupleSplitHead = T['length'] extends N + ? T + : T extends [...infer R, any] + ? TupleSplitHead + : never + +type TupleSplitTail = O['length'] extends N + ? T + : T extends [infer F, ...infer R] + ? TupleSplitTail<[...R], N, [...O, F]> + : never + +type TupleSplit = [TupleSplitHead, TupleSplitTail] + +declare function applyBind(...args: TupleSplit, 2>[1]): ReturnType; + +export = applyBind; \ No newline at end of file diff --git a/frontend/node_modules/call-bind-apply-helpers/functionApply 2.js b/frontend/node_modules/call-bind-apply-helpers/functionApply 2.js new file mode 100644 index 0000000..c71df9c --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/functionApply 2.js @@ -0,0 +1,4 @@ +'use strict'; + +/** @type {import('./functionApply')} */ +module.exports = Function.prototype.apply; diff --git a/frontend/node_modules/call-bind-apply-helpers/functionApply.d 2.ts b/frontend/node_modules/call-bind-apply-helpers/functionApply.d 2.ts new file mode 100644 index 0000000..1f6e11b --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/functionApply.d 2.ts @@ -0,0 +1 @@ +export = Function.prototype.apply; \ No newline at end of file diff --git a/frontend/node_modules/call-bind-apply-helpers/functionCall 2.js b/frontend/node_modules/call-bind-apply-helpers/functionCall 2.js new file mode 100644 index 0000000..7a8d873 --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/functionCall 2.js @@ -0,0 +1,4 @@ +'use strict'; + +/** @type {import('./functionCall')} */ +module.exports = Function.prototype.call; diff --git a/frontend/node_modules/call-bind-apply-helpers/functionCall.d 2.ts b/frontend/node_modules/call-bind-apply-helpers/functionCall.d 2.ts new file mode 100644 index 0000000..15e93df --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/functionCall.d 2.ts @@ -0,0 +1 @@ +export = Function.prototype.call; \ No newline at end of file diff --git a/frontend/node_modules/call-bind-apply-helpers/index 2.js b/frontend/node_modules/call-bind-apply-helpers/index 2.js new file mode 100644 index 0000000..2f6dab4 --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/index 2.js @@ -0,0 +1,15 @@ +'use strict'; + +var bind = require('function-bind'); +var $TypeError = require('es-errors/type'); + +var $call = require('./functionCall'); +var $actualApply = require('./actualApply'); + +/** @type {(args: [Function, thisArg?: unknown, ...args: unknown[]]) => Function} TODO FIXME, find a way to use import('.') */ +module.exports = function callBindBasic(args) { + if (args.length < 1 || typeof args[0] !== 'function') { + throw new $TypeError('a function is required'); + } + return $actualApply(bind, $call, args); +}; diff --git a/frontend/node_modules/call-bind-apply-helpers/index.d 2.ts b/frontend/node_modules/call-bind-apply-helpers/index.d 2.ts new file mode 100644 index 0000000..541516b --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/index.d 2.ts @@ -0,0 +1,64 @@ +type RemoveFromTuple< + Tuple extends readonly unknown[], + RemoveCount extends number, + Index extends 1[] = [] +> = Index["length"] extends RemoveCount + ? Tuple + : Tuple extends [infer First, ...infer Rest] + ? RemoveFromTuple + : Tuple; + +type ConcatTuples< + Prefix extends readonly unknown[], + Suffix extends readonly unknown[] +> = [...Prefix, ...Suffix]; + +type ExtractFunctionParams = T extends (this: infer TThis, ...args: infer P extends readonly unknown[]) => infer R + ? { thisArg: TThis; params: P; returnType: R } + : never; + +type BindFunction< + T extends (this: any, ...args: any[]) => any, + TThis, + TBoundArgs extends readonly unknown[], + ReceiverBound extends boolean +> = ExtractFunctionParams extends { + thisArg: infer OrigThis; + params: infer P extends readonly unknown[]; + returnType: infer R; +} + ? ReceiverBound extends true + ? (...args: RemoveFromTuple>) => R extends [OrigThis, ...infer Rest] + ? [TThis, ...Rest] // Replace `this` with `thisArg` + : R + : >>( + thisArg: U, + ...args: RemainingArgs + ) => R extends [OrigThis, ...infer Rest] + ? [U, ...ConcatTuples] // Preserve bound args in return type + : R + : never; + +declare function callBind< + const T extends (this: any, ...args: any[]) => any, + Extracted extends ExtractFunctionParams, + const TBoundArgs extends Partial & readonly unknown[], + const TThis extends Extracted["thisArg"] +>( + args: [fn: T, thisArg: TThis, ...boundArgs: TBoundArgs] +): BindFunction; + +declare function callBind< + const T extends (this: any, ...args: any[]) => any, + Extracted extends ExtractFunctionParams, + const TBoundArgs extends Partial & readonly unknown[] +>( + args: [fn: T, ...boundArgs: TBoundArgs] +): BindFunction; + +declare function callBind( + args: [fn: Exclude, ...rest: TArgs] +): never; + +// export as namespace callBind; +export = callBind; diff --git a/frontend/node_modules/call-bind-apply-helpers/package 2.json b/frontend/node_modules/call-bind-apply-helpers/package 2.json new file mode 100644 index 0000000..923b8be --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/package 2.json @@ -0,0 +1,85 @@ +{ + "name": "call-bind-apply-helpers", + "version": "1.0.2", + "description": "Helper functions around Function call/apply/bind, for use in `call-bind`", + "main": "index.js", + "exports": { + ".": "./index.js", + "./actualApply": "./actualApply.js", + "./applyBind": "./applyBind.js", + "./functionApply": "./functionApply.js", + "./functionCall": "./functionCall.js", + "./reflectApply": "./reflectApply.js", + "./package.json": "./package.json" + }, + "scripts": { + "prepack": "npmignore --auto --commentLines=auto", + "prepublish": "not-in-publish || npm run prepublishOnly", + "prepublishOnly": "safe-publish-latest", + "prelint": "evalmd README.md", + "lint": "eslint --ext=.js,.mjs .", + "postlint": "tsc -p . && attw -P", + "pretest": "npm run lint", + "tests-only": "nyc tape 'test/**/*.js'", + "test": "npm run tests-only", + "posttest": "npx npm@'>=10.2' audit --production", + "version": "auto-changelog && git add CHANGELOG.md", + "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ljharb/call-bind-apply-helpers.git" + }, + "author": "Jordan Harband ", + "license": "MIT", + "bugs": { + "url": "https://github.com/ljharb/call-bind-apply-helpers/issues" + }, + "homepage": "https://github.com/ljharb/call-bind-apply-helpers#readme", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.17.3", + "@ljharb/eslint-config": "^21.1.1", + "@ljharb/tsconfig": "^0.2.3", + "@types/for-each": "^0.3.3", + "@types/function-bind": "^1.1.10", + "@types/object-inspect": "^1.13.0", + "@types/tape": "^5.8.1", + "auto-changelog": "^2.5.0", + "encoding": "^0.1.13", + "es-value-fixtures": "^1.7.1", + "eslint": "=8.8.0", + "evalmd": "^0.0.19", + "for-each": "^0.3.5", + "has-strict-mode": "^1.1.0", + "in-publish": "^2.0.1", + "npmignore": "^0.3.1", + "nyc": "^10.3.2", + "object-inspect": "^1.13.4", + "safe-publish-latest": "^2.0.0", + "tape": "^5.9.0", + "typescript": "next" + }, + "testling": { + "files": "test/index.js" + }, + "auto-changelog": { + "output": "CHANGELOG.md", + "template": "keepachangelog", + "unreleased": false, + "commitLimit": false, + "backfillLimit": false, + "hideCredit": true + }, + "publishConfig": { + "ignore": [ + ".github/workflows" + ] + }, + "engines": { + "node": ">= 0.4" + } +} diff --git a/frontend/node_modules/call-bind-apply-helpers/reflectApply 2.js b/frontend/node_modules/call-bind-apply-helpers/reflectApply 2.js new file mode 100644 index 0000000..3d03caa --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/reflectApply 2.js @@ -0,0 +1,4 @@ +'use strict'; + +/** @type {import('./reflectApply')} */ +module.exports = typeof Reflect !== 'undefined' && Reflect && Reflect.apply; diff --git a/frontend/node_modules/call-bind-apply-helpers/reflectApply.d 2.ts b/frontend/node_modules/call-bind-apply-helpers/reflectApply.d 2.ts new file mode 100644 index 0000000..6b2ae76 --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/reflectApply.d 2.ts @@ -0,0 +1,3 @@ +declare const reflectApply: false | typeof Reflect.apply; + +export = reflectApply; diff --git a/frontend/node_modules/call-bind-apply-helpers/tsconfig 2.json b/frontend/node_modules/call-bind-apply-helpers/tsconfig 2.json new file mode 100644 index 0000000..aef9993 --- /dev/null +++ b/frontend/node_modules/call-bind-apply-helpers/tsconfig 2.json @@ -0,0 +1,9 @@ +{ + "extends": "@ljharb/tsconfig", + "compilerOptions": { + "target": "es2021", + }, + "exclude": [ + "coverage", + ], +} \ No newline at end of file diff --git a/frontend/node_modules/caniuse-lite/LICENSE 2 b/frontend/node_modules/caniuse-lite/LICENSE 2 new file mode 100644 index 0000000..06c608d --- /dev/null +++ b/frontend/node_modules/caniuse-lite/LICENSE 2 @@ -0,0 +1,395 @@ +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/frontend/node_modules/caniuse-lite/README 2.md b/frontend/node_modules/caniuse-lite/README 2.md new file mode 100644 index 0000000..f2c67bc --- /dev/null +++ b/frontend/node_modules/caniuse-lite/README 2.md @@ -0,0 +1,6 @@ +# caniuse-lite + +A smaller version of caniuse-db, with only the essentials! + +## Docs +Read full docs **[here](https://github.com/browserslist/caniuse-lite#readme)**. diff --git a/frontend/node_modules/caniuse-lite/package 2.json b/frontend/node_modules/caniuse-lite/package 2.json new file mode 100644 index 0000000..e9e28b3 --- /dev/null +++ b/frontend/node_modules/caniuse-lite/package 2.json @@ -0,0 +1,34 @@ +{ + "name": "caniuse-lite", + "version": "1.0.30001781", + "description": "A smaller version of caniuse-db, with only the essentials!", + "main": "dist/unpacker/index.js", + "files": [ + "data", + "dist" + ], + "keywords": [ + "support" + ], + "author": { + "name": "Ben Briggs", + "email": "beneb.info@gmail.com", + "url": "http://beneb.info" + }, + "repository": "browserslist/caniuse-lite", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" +} diff --git a/frontend/node_modules/csstype/LICENSE 2 b/frontend/node_modules/csstype/LICENSE 2 new file mode 100644 index 0000000..ac06f62 --- /dev/null +++ b/frontend/node_modules/csstype/LICENSE 2 @@ -0,0 +1,19 @@ +Copyright (c) 2017-2018 Fredrik Nicol + +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. diff --git a/frontend/node_modules/csstype/README 2.md b/frontend/node_modules/csstype/README 2.md new file mode 100644 index 0000000..9b3391a --- /dev/null +++ b/frontend/node_modules/csstype/README 2.md @@ -0,0 +1,291 @@ +# CSSType + +[![npm](https://img.shields.io/npm/v/csstype.svg)](https://www.npmjs.com/package/csstype) + +TypeScript and Flow definitions for CSS, generated by [data from MDN](https://github.com/mdn/data). It provides autocompletion and type checking for CSS properties and values. + +**TypeScript** + +```ts +import type * as CSS from 'csstype'; + +const style: CSS.Properties = { + colour: 'white', // Type error on property + textAlign: 'middle', // Type error on value +}; +``` + +**Flow** + +```js +// @flow strict +import * as CSS from 'csstype'; + +const style: CSS.Properties<> = { + colour: 'white', // Type error on property + textAlign: 'middle', // Type error on value +}; +``` + +_Further examples below will be in TypeScript!_ + +## Getting started + +```sh +$ npm install csstype +``` + +## Table of content + +- [Style types](#style-types) +- [At-rule types](#at-rule-types) +- [Pseudo types](#pseudo-types) +- [Generics](#generics) +- [Usage](#usage) +- [What should I do when I get type errors?](#what-should-i-do-when-i-get-type-errors) +- [Version 3.0](#version-30) +- [Contributing](#contributing) + +## Style types + +Properties are categorized in different uses and in several technical variations to provide typings that suits as many as possible. + +| | Default | `Hyphen` | `Fallback` | `HyphenFallback` | +| -------------- | -------------------- | -------------------------- | ---------------------------- | ---------------------------------- | +| **All** | `Properties` | `PropertiesHyphen` | `PropertiesFallback` | `PropertiesHyphenFallback` | +| **`Standard`** | `StandardProperties` | `StandardPropertiesHyphen` | `StandardPropertiesFallback` | `StandardPropertiesHyphenFallback` | +| **`Vendor`** | `VendorProperties` | `VendorPropertiesHyphen` | `VendorPropertiesFallback` | `VendorPropertiesHyphenFallback` | +| **`Obsolete`** | `ObsoleteProperties` | `ObsoletePropertiesHyphen` | `ObsoletePropertiesFallback` | `ObsoletePropertiesHyphenFallback` | +| **`Svg`** | `SvgProperties` | `SvgPropertiesHyphen` | `SvgPropertiesFallback` | `SvgPropertiesHyphenFallback` | + +Categories: + +- **All** - Includes `Standard`, `Vendor`, `Obsolete` and `Svg` +- **`Standard`** - Current properties and extends subcategories `StandardLonghand` and `StandardShorthand` _(e.g. `StandardShorthandProperties`)_ +- **`Vendor`** - Vendor prefixed properties and extends subcategories `VendorLonghand` and `VendorShorthand` _(e.g. `VendorShorthandProperties`)_ +- **`Obsolete`** - Removed or deprecated properties +- **`Svg`** - SVG-specific properties + +Variations: + +- **Default** - JavaScript (camel) cased property names +- **`Hyphen`** - CSS (kebab) cased property names +- **`Fallback`** - Also accepts array of values e.g. `string | string[]` + +## At-rule types + +At-rule interfaces with descriptors. + +**TypeScript**: These will be found in the `AtRule` namespace, e.g. `AtRule.Viewport`. +**Flow**: These will be prefixed with `AtRule$`, e.g. `AtRule$Viewport`. + +| | Default | `Hyphen` | `Fallback` | `HyphenFallback` | +| -------------------- | -------------- | -------------------- | ---------------------- | ---------------------------- | +| **`@counter-style`** | `CounterStyle` | `CounterStyleHyphen` | `CounterStyleFallback` | `CounterStyleHyphenFallback` | +| **`@font-face`** | `FontFace` | `FontFaceHyphen` | `FontFaceFallback` | `FontFaceHyphenFallback` | +| **`@viewport`** | `Viewport` | `ViewportHyphen` | `ViewportFallback` | `ViewportHyphenFallback` | + +## Pseudo types + +String literals of pseudo classes and pseudo elements + +- `Pseudos` + + Extends: + - `AdvancedPseudos` + + Function-like pseudos e.g. `:not(:first-child)`. The string literal contains the value excluding the parenthesis: `:not`. These are separated because they require an argument that results in infinite number of variations. + + - `SimplePseudos` + + Plain pseudos e.g. `:hover` that can only be **one** variation. + +## Generics + +All interfaces has two optional generic argument to define length and time: `CSS.Properties` + +- **Length** is the first generic parameter and defaults to `string | 0` because `0` is the only [length where the unit identifier is optional](https://drafts.csswg.org/css-values-3/#lengths). You can specify this, e.g. `string | number`, for platforms and libraries that accepts any numeric value as length with a specific unit. + ```tsx + const style: CSS.Properties = { + width: 100, + }; + ``` +- **Time** is the second generic argument and defaults to `string`. You can specify this, e.g. `string | number`, for platforms and libraries that accepts any numeric value as length with a specific unit. + ```tsx + const style: CSS.Properties = { + transitionDuration: 1000, + }; + ``` + +## Usage + +```ts +import type * as CSS from 'csstype'; + +const style: CSS.Properties = { + width: '10px', + margin: '1em', +}; +``` + +In some cases, like for CSS-in-JS libraries, an array of values is a way to provide fallback values in CSS. Using `CSS.PropertiesFallback` instead of `CSS.Properties` will add the possibility to use any property value as an array of values. + +```ts +import type * as CSS from 'csstype'; + +const style: CSS.PropertiesFallback = { + display: ['-webkit-flex', 'flex'], + color: 'white', +}; +``` + +There's even string literals for pseudo selectors and elements. + +```ts +import type * as CSS from 'csstype'; + +const pseudos: { [P in CSS.SimplePseudos]?: CSS.Properties } = { + ':hover': { + display: 'flex', + }, +}; +``` + +Hyphen cased (kebab cased) properties are provided in `CSS.PropertiesHyphen` and `CSS.PropertiesHyphenFallback`. It's not **not** added by default in `CSS.Properties`. To allow both of them, you can simply extend with `CSS.PropertiesHyphen` or/and `CSS.PropertiesHyphenFallback`. + +```ts +import type * as CSS from 'csstype'; + +interface Style extends CSS.Properties, CSS.PropertiesHyphen {} + +const style: Style = { + 'flex-grow': 1, + 'flex-shrink': 0, + 'font-weight': 'normal', + backgroundColor: 'white', +}; +``` + +Adding type checked CSS properties to a `HTMLElement`. + +```ts +import type * as CSS from 'csstype'; + +const style: CSS.Properties = { + color: 'red', + margin: '1em', +}; + +let button = document.createElement('button'); + +Object.assign(button.style, style); +``` + +## What should I do when I get type errors? + +The goal is to have as perfect types as possible and we're trying to do our best. But with CSS Custom Properties, the CSS specification changing frequently and vendors implementing their own specifications with new releases sometimes causes type errors even if it should work. Here's some steps you could take to get it fixed: + +_If you're using CSS Custom Properties you can step directly to step 3._ + +1. **First of all, make sure you're doing it right.** A type error could also indicate that you're not :wink: + - Some CSS specs that some vendors has implemented could have been officially rejected or haven't yet received any official acceptance and are therefor not included + - If you're using TypeScript, [type widening](https://blog.mariusschulz.com/2017/02/04/TypeScript-2-1-literal-type-widening) could be the reason you get `Type 'string' is not assignable to...` errors + +2. **Have a look in [issues](https://github.com/frenic/csstype/issues) to see if an issue already has been filed. If not, create a new one.** To help us out, please refer to any information you have found. +3. Fix the issue locally with **TypeScript** (Flow further down): + - The recommended way is to use **module augmentation**. Here's a few examples: + + ```ts + // My css.d.ts file + import type * as CSS from 'csstype'; + + declare module 'csstype' { + interface Properties { + // Add a missing property + WebkitRocketLauncher?: string; + + // Add a CSS Custom Property + '--theme-color'?: 'black' | 'white'; + + // Allow namespaced CSS Custom Properties + [index: `--theme-${string}`]: any; + + // Allow any CSS Custom Properties + [index: `--${string}`]: any; + + // ...or allow any other property + [index: string]: any; + } + } + ``` + + - The alternative way is to use **type assertion**. Here's a few examples: + + ```ts + const style: CSS.Properties = { + // Add a missing property + ['WebkitRocketLauncher' as any]: 'launching', + + // Add a CSS Custom Property + ['--theme-color' as any]: 'black', + }; + ``` + + Fix the issue locally with **Flow**: + - Use **type assertion**. Here's a few examples: + + ```js + const style: $Exact> = { + // Add a missing property + [('WebkitRocketLauncher': any)]: 'launching', + + // Add a CSS Custom Property + [('--theme-color': any)]: 'black', + }; + ``` + +## Version 3.2 + +- **No longer compatible with version 2** + Conflicts may occur when both version ^3.2.0 and ^2.0.0 are installed. Potential fix for Npm would be to force resolution in `package.json`: + ```json + { + "overrides": { + "csstype": "^3.2.0" + } + } + ``` + +## Version 3.1 + +- **Data types are exposed** + TypeScript: `DataType.Color` + Flow: `DataType$Color` + +## Version 3.0 + +- **All property types are exposed with namespace** + TypeScript: `Property.AlignContent` (was `AlignContentProperty` before) + Flow: `Property$AlignContent` +- **All at-rules are exposed with namespace** + TypeScript: `AtRule.FontFace` (was `FontFace` before) + Flow: `AtRule$FontFace` +- **Data types are NOT exposed** + E.g. `Color` and `Box`. Because the generation of data types may suddenly be removed or renamed. +- **TypeScript hack for autocompletion** + Uses `(string & {})` for literal string unions and `(number & {})` for literal number unions ([related issue](https://github.com/microsoft/TypeScript/issues/29729)). Utilize `PropertyValue` to unpack types from e.g. `(string & {})` to `string`. +- **New generic for time** + Read more on the ["Generics"](#generics) section. +- **Flow types improvements** + Flow Strict enabled and exact types are used. + +## Contributing + +**Never modify `index.d.ts` and `index.js.flow` directly. They are generated automatically and committed so that we can easily follow any change it results in.** Therefor it's important that you run `$ git config merge.ours.driver true` after you've forked and cloned. That setting prevents merge conflicts when doing rebase. + +### Commands + +- `npm run build` Generates typings and type checks them +- `npm run watch` Runs build on each save +- `npm run test` Runs the tests +- `npm run lazy` Type checks, lints and formats everything diff --git a/frontend/node_modules/csstype/index.d 2.ts b/frontend/node_modules/csstype/index.d 2.ts new file mode 100644 index 0000000..da0affd --- /dev/null +++ b/frontend/node_modules/csstype/index.d 2.ts @@ -0,0 +1,22569 @@ +export {}; + +export type PropertyValue = + TValue extends Array ? Array : TValue extends infer TUnpacked & {} ? TUnpacked : TValue; + +export type Fallback = { [P in keyof T]: T[P] | readonly NonNullable[] }; + +export interface StandardLonghandProperties { + /** + * This feature is not Baseline because it does not work in some of the most widely-used browsers. + * + * **Syntax**: `auto | ` + * + * **Initial value**: `auto` + * + * | Chrome | Firefox | Safari | Edge | IE | + * | :----: | :-----: | :------: | :----: | :-: | + * | **93** | **92** | **15.4** | **93** | No | + * + * @see https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/accent-color + */ + accentColor?: Property.AccentColor | undefined; + /** + * This feature is well established and works across many devices and browser versions. It’s been available across browsers since September 2015. + * + * **Syntax**: `normal | | | ? ` + * + * **Initial value**: `normal` + * + * | Chrome | Firefox | Safari | Edge | IE | + * | :------: | :-----: | :-----: | :----: | :----: | + * | **29** | **28** | **9** | **12** | **11** | + * | 21 _-x-_ | | 7 _-x-_ | | | + * + * @see https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/align-content + */ + alignContent?: Property.AlignContent | undefined; + /** + * This feature is well established and works across many devices and browser versions. It’s been available across browsers since September 2015. + * + * **Syntax**: `normal | stretch | | [ ? ] | anchor-center` + * + * **Initial value**: `normal` + * + * | Chrome | Firefox | Safari | Edge | IE | + * | :------: | :-----: | :-----: | :----: | :----: | + * | **29** | **20** | **9** | **12** | **11** | + * | 21 _-x-_ | | 7 _-x-_ | | | + * + * @see https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/align-items + */ + alignItems?: Property.AlignItems | undefined; + /** + * This feature is well established and works across many devices and browser versions. It’s been available across browsers since September 2015. + * + * **Syntax**: `auto | normal | stretch | | ? | anchor-center` + * + * **Initial value**: `auto` + * + * | Chrome | Firefox | Safari | Edge | IE | + * | :------: | :-----: | :-----: | :----: | :----: | + * | **29** | **20** | **9** | **12** | **10** | + * | 21 _-x-_ | | 7 _-x-_ | | | + * + * @see https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/align-self + */ + alignSelf?: Property.AlignSelf | undefined; + /** + * **Syntax**: `[ normal | | | ? ]#` + * + * **Initial value**: `normal` + */ + alignTracks?: Property.AlignTracks | undefined; + /** + * This feature is not Baseline because it does not work in some of the most widely-used browsers. + * + * **Syntax**: `baseline | alphabetic | ideographic | middle | central | mathematical | text-before-edge | text-after-edge` + * + * **Initial value**: `baseline` + * + * | Chrome | Firefox | Safari | Edge | IE | + * | :----: | :-----: | :-----: | :----: | :-: | + * | **1** | No | **5.1** | **79** | No | + * + * @see https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/alignment-baseline + */ + alignmentBaseline?: Property.AlignmentBaseline | undefined; + /** + * This feature is not Baseline because it does not work in some of the most widely-used browsers. + * + * **Syntax**: `none | #` + * + * **Initial value**: `none` + * + * | Chrome | Firefox | Safari | Edge | IE | + * | :-----: | :---------: | :----: | :-----: | :-: | + * | **125** | **preview** | **26** | **125** | No | + * + * @see https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/anchor-name + */ + anchorName?: Property.AnchorName | undefined; + /** + * **Syntax**: `none | all | #` + * + * **Initial value**: `none` + * + * | Chrome | Firefox | Safari | Edge | IE | + * | :-----: | :---------: | :----: | :-----: | :-: | + * | **131** | **preview** | **26** | **131** | No | + */ + anchorScope?: Property.AnchorScope | undefined; + /** + * Since July 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers. + * + * **Syntax**: `#` + * + * **Initial value**: `replace` + * + * | Chrome | Firefox | Safari | Edge | IE | + * | :-----: | :-----: | :----: | :-----: | :-: | + * | **112** | **115** | **16** | **112** | No | + * + * @see https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/animation-composition + */ + animationComposition?: Property.AnimationComposition | undefined; + /** + * This feature is well established and works across many devices and browser versions. It’s been available across browsers since September 2015. + * + * **Syntax**: `