diff --git a/.env.example b/.env.example index ffee271e11..a02e534a7e 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,8 @@ NEXT_PUBLIC_VERCEL_ENV=development NEXT_PUBLIC_VERCEL_URL=localhost:3000 +# Netlify deploy: set NEXT_PUBLIC_VERCEL_ENV=production and +# NEXT_PUBLIC_VERCEL_URL=your-site.netlify.app (hostname only). +# Add your Netlify hostname to ALLOWED_ORIGINS for /api/proxy to work in the browser. SENTRY_AUTH_TOKEN= LOKALISE_API_KEY= LOKALISE_PROJECT_ID= @@ -30,4 +33,10 @@ NEXT_PUBLIC_QURAN_REFLECT_URL=https://quranreflect.com NEXT_PUBLIC_SSO_ENABLED=false -NEXT_PUBLIC_EMBED_URL=https://quran.com/embed/v1 # Embed Ayah \ No newline at end of file +NEXT_PUBLIC_EMBED_URL=https://quran.com/embed/v1 # Embed Ayah + +# Optional: Syntax tab — morphology + paradigm + charts via Quran MCP (https://mcp.quran.ai/) +# QURAN_SYNTAX_MCP_URL=https://mcp.quran.ai/ +# +# Set to true to skip MCP — uses pasted JSON from src/services/syntaxAnalysis.mock.ts +# NEXT_PUBLIC_SYNTAX_ANALYSIS_MOCK=true \ No newline at end of file diff --git a/docs/syntax-study-mode-design.md b/docs/syntax-study-mode-design.md new file mode 100644 index 0000000000..afbed6d029 --- /dev/null +++ b/docs/syntax-study-mode-design.md @@ -0,0 +1,198 @@ +# Syntax analysis & Study Mode — design notes + +This document describes the feature work on the **current branch** compared to **`origin/production`**: a new **Syntax** experience in Quran Reader **Study Mode**, backed by a **Next.js API route** that calls **Quran MCP** for morphology (and optional paradigm-derived charts). + +**Baseline:** `git diff origin/production...HEAD` +**External reference:** [Quran MCP documentation](https://mcp.quran.ai/documentation) (Streamable HTTP, `fetch_grounding_rules`, `fetch_word_morphology`, `fetch_word_paradigm`). + +--- + +## 1. Summary + +| Area | Change | +|------|--------| +| **Study Mode** | New bottom tab **`syntax`** with search-style icon; opens grammatical analysis for the **selected word** in the verse. | +| **UI** | New **Syntax** view: layout/skeleton, morphology text, optional **sarf / verb / ism** charts (types in `types/SyntaxAnalysis.ts`). | +| **API** | `POST /api/syntax/analyze` — validates input, calls server-side Quran MCP client, returns `SyntaxAnalysisResult`. | +| **Data** | Morphology from MCP mapped to `rootLetter`, `pattern`, `wordBreakDown`; paradigm stems optionally merged into charts via `syntaxChartsFromMcp` + `syntaxAnalysisCharts`. | +| **Mock** | `NEXT_PUBLIC_SYNTAX_ANALYSIS_MOCK=true` skips MCP and returns `syntaxAnalysis.mock.ts`. | +| **Infra / other** | `@modelcontextprotocol/sdk` dependency; TS path alias for MCP ESM; Verse font tweak in tafsir/translation mode. `middleware.ts` and `url.ts` match **`origin/production`** (no Syntax-specific edits). | + +--- + +## 2. User-facing flow (Study Mode) + +1. User opens **Study Mode** on a verse (existing flow). +2. User selects a **word** in the verse (word tap / selection used elsewhere for tafsir, etc.). +3. User taps the **Syntax** tab (icon: `public/icons/search.svg`, same `color` pattern as Tafsir’s `BookIcon`). +4. **`StudyModeSyntaxTab`** loads **`SyntaxBody`** (dynamic import + skeleton). +5. **`SyntaxBody`** uses **SWR** with a key derived from `selectedWord` location, Uthmani text, and `verseKey`. +6. **`fetchSyntaxAnalysis`** posts to **`/api/syntax/analyze`** unless mock mode is on. +7. Response drives **morphology** copy and **`SyntaxAnalysisCharts`** when `verb*`, `sarfChart`, or `ismChart` are present and pass normalization. + +**Key files:** `StudyModeBottomActions` (`StudyModeTabId.SYNTAX`), `StudyModeBodyTabs.tsx`, `StudyModeBody.tsx`, `StudyModeModal/index.tsx`, `tabs/StudyModeSyntaxTab.tsx`, `SyntaxView/*`. + +--- + +## 3. API: `POST /api/syntax/analyze` + +| Item | Detail | +|------|--------| +| **Path** | `/api/syntax/analyze` | +| **File** | `src/pages/api/syntax/analyze.ts` | +| **Method** | `POST` only (`405` otherwise). | +| **Body (JSON)** | `{ "textUthmani": string, "verseKey"?: string }` | +| **Validation** | `textUthmani` required, non-empty after trim; max length **200** characters. | +| **Success** | `200` + `SyntaxAnalysisResult` | +| **Errors** | `400` (bad input), `502` + `{ error: string }` (MCP failure / thrown error). | + +**`verseKey` format:** When present, should match `surah:ayah` (e.g. `2:255`) so morphology and paradigm calls can use **ayah-scoped** MCP arguments (see `isValidAyahKey` in morphology helper). + +**Client:** `src/services/syntaxAnalysisService.ts` — `fetch('/api/syntax/analyze', { method: 'POST', ... })`, throws `Error` with server message when `!res.ok` or body contains `error`. + +--- + +## 4. Quran MCP server flow + +All MCP calls run **on the server** inside `fetchSyntaxAnalysisViaQuranMcp` (`src/lib/syntaxAnalysisQuranMcp.ts`) so the browser never holds MCP transport credentials beyond same-origin API. + +### 4.1 Connection + +- **Transport:** `@modelcontextprotocol/sdk` `StreamableHTTPClientTransport`. +- **URL:** `process.env.QURAN_SYNTAX_MCP_URL` or default `https://mcp.quran.ai/` (trailing slash normalized). +- **Client name:** `quran.com-frontend` (version `1.0.0`). + +### 4.2 Tool sequence (`runMcpSyntaxStudyOnClient` in `syntaxAnalysisQuranMcpMorphology.ts`) + +1. **`fetch_grounding_rules`** — session grounding per Quran MCP docs. +2. **`fetch_word_morphology`** + - If `verseKey` is valid `surah:ayah`: arguments `ayah_key`, `word_text`. + - Else: argument `word` = Uthmani text. +3. **Parse** structured payload → `words[]`; **pick** row matching `textUthmani` (`pickMorphologyWord`). +4. **`fetch_word_paradigm`** (best-effort; may return `null` on error) + - If valid ayah key: `ayah_key` + `word_text`. + - Else: `lemma` from picked word when available. +5. **Return bundle:** `base` (`SyntaxAnalysisResult` from morphology), `pickedWord`, `morphologyResponse`, `paradigm`. + +### 4.3 Mapping morphology → `SyntaxAnalysisResult` + +- **`morphologyWordToSyntaxResult`:** builds `rootLetter`, `pattern` (English line from `description` / `grammatical_features`, Arabic-ish `patternType` from POS/aspect/case), `wordBreakDown` from `morpheme_segments` or whole word + translation. + +### 4.4 Optional charts + +- **`syntaxChartsFromMcp.ts`** — builds partial chart objects from **paradigm** stems (`perfect` / `imperfect` / `imperative`) and picked word metadata where applicable. +- **`syntaxAnalysisCharts.ts`** — `applyOptionalChartsToResult`, `normalizeSarfChart`, `normalizeVerbChart`, `normalizeIsmChart`, and verb present/past merge rules aligned with the UI tables. + +`fetchSyntaxAnalysisViaQuranMcp` merges: `applyOptionalChartsToResult(bundle.base, buildOptionalChartsFromMcp(bundle.pickedWord, bundle.paradigm))`. + +--- + +## 5. Types & UI charts + +- **`types/SyntaxAnalysis.ts`** — `SyntaxAnalysisResult` and optional `sarfChart`, `verbPresentTenseChart`, `verbPastTenseChart`, `verbChart`, `ismChart`. +- **`SyntaxChartTables.tsx`** — renders grids when props are defined. +- **`useSyntaxChartArabicTypography.ts`** — shared Arabic typography for Syntax view. + +--- + +## 6. Configuration & mock + +| Variable | Role | +|----------|------| +| `QURAN_SYNTAX_MCP_URL` | Optional override for MCP Streamable HTTP base URL. | +| `NEXT_PUBLIC_SYNTAX_ANALYSIS_MOCK` | When `true`, client returns `SYNTAX_ANALYSIS_MOCK_RESPONSE` (no `/api/syntax/analyze` call). | + +Documented in `.env.example` (syntax / MCP section). + +--- + +## 7. Middleware & URL utilities (current behavior, same as production) + +These files are **not** part of the Syntax feature contract. They are documented here because an earlier branch revision briefly changed them; the **current** sources match **`origin/production`**. + +### 7.1 `src/middleware.ts` + +| Behavior | Detail | +|----------|--------| +| **`_next/data` requests** | If `req.url` includes `_next/data`, respond with **`404`** and an empty body. Intended to force a **full page reload** after a new deployment instead of serving stale client-side navigation payloads. Applies in **all** environments (including local `yarn dev`), not gated on `NODE_ENV`. | +| **Ramadan routes** | Paths containing `/ramadan2026` or `/ramadanchallenge` (case-insensitive) redirect to the **lowercase** pathname when the URL is not already lowercase. | +| **Everything else** | `NextResponse.next()`. | + +**Implication for local dev:** Client transitions that rely on `_next/data` JSON may get `404` from middleware; a hard refresh or full navigation is expected after deploys. This is unrelated to `/api/syntax/analyze`. + +### 7.2 `src/utils/url.ts` + +`getProxiedServiceUrl(service, path)` builds backend URLs for Quran Foundation services. There is **no** special branch for `QuranFoundationService.CONTENT` that bypasses the app proxy. + +| Condition | Base URL | +|-----------|----------| +| **Static build** (`isStaticBuild`) | `${API_GATEWAY_URL}/${service}${path}` | +| **Otherwise** | `${getBasePath()}/api/proxy/${service}${path}` where `getBasePath()` is `http://` or `https://` + `NEXT_PUBLIC_VERCEL_URL` depending on `NEXT_PUBLIC_VERCEL_ENV === 'development'`. | + +All services in `QuranFoundationService` (`search`, `auth`, `content`, `quran-reflect`) use the same proxy pattern. Syntax analysis does **not** call this helper; it uses **`POST /api/syntax/analyze`** → Quran MCP on the server. + +### 7.3 Other branch diffs (Syntax-related infra) + +- **`tsconfig.json`** — path alias `@modelcontextprotocol/sdk/*` → ESM dist (bundler resolution). +- **`package.json` / `yarn.lock`** — adds `@modelcontextprotocol/sdk`. +- **`VerseText.module.scss`** — mobile `tafsirOrTranslationMode` font scale factor adjusted (`1.2` → `0.75` of `--font-size`). + +--- + +## 8. File change list (`origin/production...HEAD`) + +| Status | Path | Short description | +|--------|------|---------------------| +| M | `.env.example` | Document Quran MCP URL and syntax mock flag. | +| M | `package.json` | Add `@modelcontextprotocol/sdk`. | +| M | `yarn.lock` | Lockfile for new dependency. | +| M | `tsconfig.json` | MCP SDK path alias. | +| M | `src/components/Verse/VerseText.module.scss` | Tafsir/translation mode font sizing tweak. | +| M | `src/components/QuranReader/ReadingView/StudyModeModal/StudyModeBody.tsx` | Wire Syntax tab panel / props (e.g. `selectedWord`). | +| M | `src/components/QuranReader/ReadingView/StudyModeModal/StudyModeBodyTabs.tsx` | Register `StudyModeSyntaxTab`, tab config, **Search** icon for Syntax. | +| M | `src/components/QuranReader/ReadingView/StudyModeModal/StudyModeBottomActions/index.tsx` | Add `StudyModeTabId.SYNTAX`. | +| M | `src/components/QuranReader/ReadingView/StudyModeModal/index.tsx` | Study Mode state / layout for Syntax tab. | +| A | `.../tabs/StudyModeSyntaxTab.tsx` | Lazy tab shell: scroll container + dynamic `SyntaxBody`. | +| A | `src/components/QuranReader/SyntaxView/SyntaxBody.tsx` | SWR + morphology UI + charts. | +| A | `src/components/QuranReader/SyntaxView/SyntaxChartTables.tsx` | Sarf / verb / ism tables. | +| A | `src/components/QuranReader/SyntaxView/SyntaxSkeleton.tsx` | Loading UI for dynamic import. | +| A | `src/components/QuranReader/SyntaxView/SyntaxSkeleton.module.scss` | Skeleton styles. | +| A | `src/components/QuranReader/SyntaxView/SyntaxTabLayout.tsx` | Shared tab layout / scroll hook export. | +| A | `src/components/QuranReader/SyntaxView/SyntaxTabLayout.module.scss` | Layout styles. | +| A | `src/components/QuranReader/SyntaxView/SyntaxView.module.scss` | Syntax body styles. | +| A | `src/components/QuranReader/SyntaxView/useSyntaxChartArabicTypography.ts` | Arabic font helpers for charts/body. | +| A | `src/lib/syntaxAnalysisCharts.ts` | Normalize + merge optional charts onto base result. | +| A | `src/lib/syntaxAnalysisQuranMcp.ts` | MCP client connect + fetch bundle + merge charts. | +| A | `src/lib/syntaxAnalysisQuranMcpMorphology.ts` | MCP tool calls, pick word, map to `SyntaxAnalysisResult`. | +| A | `src/lib/syntaxChartsFromMcp.ts` | Build chart-shaped JSON from paradigm + picked word. | +| A | `src/pages/api/syntax/analyze.ts` | POST API handler calling `fetchSyntaxAnalysisViaQuranMcp`. | +| A | `src/services/syntaxAnalysis.mock.ts` | Full mock `SyntaxAnalysisResult` for local UI. | +| A | `src/services/syntaxAnalysisService.ts` | Client fetch + mock gate + `getWordTextUthmaniForSyntax`. | +| A | `types/SyntaxAnalysis.ts` | Shared TS types for API + UI. | + +--- + +## 9. Diagram (high level) + +```mermaid +sequenceDiagram + participant U as Browser + participant SB as SyntaxBody + participant API as POST /api/syntax/analyze + participant MCP as Quran MCP (Streamable HTTP) + + U->>SB: Select word, open Syntax tab + SB->>API: JSON textUthmani, verseKey + API->>MCP: connect + grounding + morphology + paradigm + MCP-->>API: words + paradigm payload + API-->>SB: SyntaxAnalysisResult (+ optional charts) + SB-->>U: Render morphology + charts +``` + +--- + +## 10. Maintenance notes + +- Regenerate this file list anytime with: + `git fetch origin production && git diff --name-status origin/production...HEAD` +- If MCP tools or response shapes change upstream, update **`syntaxAnalysisQuranMcpMorphology.ts`** and **`syntaxChartsFromMcp.ts`** together so the API contract in **`types/SyntaxAnalysis.ts`** stays satisfied. diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000000..f3795a13a7 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,35 @@ +# Next.js on Netlify — https://docs.netlify.com/integrations/frameworks/next-js/ +# +# Publish MUST differ from site base (Netlify rejects publish === base). +# @netlify/plugin-nextjs may adjust the publish path during build; `.next` satisfies validation. +# +# Netlify UI (Build & deploy → Build settings): +# Base directory: leave empty if this repo root IS the app (not a monorepo subfolder). +# Publish directory: leave EMPTY so this file wins — do not set "." or "/" in the UI. +# +# All [build.environment] values MUST be quoted strings. +[build] + command = "yarn build" + publish = ".next" + +[build.environment] + NODE_VERSION = "24" + NEXT_PUBLIC_VERCEL_ENV = "production" + NEXT_PUBLIC_VERCEL_URL = "chimerical-cajeta-2e6cb3.netlify.app" + ALLOWED_ORIGINS = "quran.com,test.quran.com,chimerical-cajeta-2e6cb3.netlify.app" + NEXT_PUBLIC_SERVER_SENTRY_ENABLED = "false" + NEXT_PUBLIC_CLIENT_SENTRY_ENABLED = "true" + NODE_TLS_REJECT_UNAUTHORIZED = "0" + SIGNATURE_TOKEN = "1234" + INTERNAL_CLIENT_ID = "QDC_WEB" + PROXY_SIGNATURE_TOKEN = "123456" + NEXT_PUBLIC_AUTH_PROFILE_TIMEOUT_MS = "4000" + NEXT_PUBLIC_QURAN_REFLECT_URL = "https://quranreflect.com" + NEXT_PUBLIC_SSO_ENABLED = "false" + NEXT_PUBLIC_EMBED_URL = "https://quran.com/embed/v1" + API_GATEWAY_URL = "https://api.quran.com" + QURAN_SYNTAX_MCP_URL = "https://mcp.quran.ai/" + NEXT_PUBLIC_SYNTAX_ANALYSIS_MOCK = "false" + +[[plugins]] + package = "@netlify/plugin-nextjs" diff --git a/package.json b/package.json index 7592ae9fd9..7bed4f2b64 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "main": "index.js", "license": "MIT", "engines": { - "node": "18.x" + "node": "24.x" }, "scripts": { "dev": "next dev", @@ -50,6 +50,7 @@ "@milkdown/react": "^7.5.0", "@milkdown/transformer": "^7.5.0", "@milkdown/utils": "^7.5.0", + "@modelcontextprotocol/sdk": "^1.12.0", "@next/bundle-analyzer": "^14.2.7", "@novu/headless": "0.24.0", "@radix-ui/react-checkbox": "^1.1.1", diff --git a/src/components/QuranReader/ReadingView/StudyModeModal/StudyModeBody.tsx b/src/components/QuranReader/ReadingView/StudyModeModal/StudyModeBody.tsx index f21a174757..6f364b740a 100644 --- a/src/components/QuranReader/ReadingView/StudyModeModal/StudyModeBody.tsx +++ b/src/components/QuranReader/ReadingView/StudyModeModal/StudyModeBody.tsx @@ -113,6 +113,7 @@ const StudyModeBody: React.FC = ({ ( @@ -36,6 +38,10 @@ export const StudyModeTafsirTab = dynamic(() => import('./tabs/StudyModeTafsirTa loading: Loading, }); +export const StudyModeSyntaxTab = dynamic(() => import('./tabs/StudyModeSyntaxTab'), { + loading: Loading, +}); + export const StudyModeReflectionsTab = dynamic(() => import('./tabs/StudyModeReflectionsTab'), { loading: Loading, }); @@ -68,6 +74,7 @@ export const StudyModeRelatedVersesTab = dynamic( interface TabProps { chapterId: string; verseNumber: string; + selectedWord?: Word; switchTab?: (tabId: StudyModeTabId | null) => void; questionId?: string; questionsInitialData?: AyahQuestionsResponse; @@ -79,6 +86,7 @@ interface TabProps { export const TAB_COMPONENTS: Partial>> = { [StudyModeTabId.TAFSIR]: StudyModeTafsirTab, + [StudyModeTabId.SYNTAX]: StudyModeSyntaxTab, [StudyModeTabId.LAYERS]: StudyModeLayersTab, [StudyModeTabId.REFLECTIONS]: StudyModeReflectionsTab, [StudyModeTabId.LESSONS]: StudyModeLessonsTab, @@ -156,6 +164,13 @@ export const useStudyModeTabs = ({ onClick: () => handleTabClick(StudyModeTabId.TAFSIR), condition: true, }, + { + id: StudyModeTabId.SYNTAX, + label: 'Syntax', + icon: , + onClick: () => handleTabClick(StudyModeTabId.SYNTAX), + condition: true, + }, { id: StudyModeTabId.LAYERS, label: t('quran-reader:layers.title'), diff --git a/src/components/QuranReader/ReadingView/StudyModeModal/StudyModeBottomActions/index.tsx b/src/components/QuranReader/ReadingView/StudyModeModal/StudyModeBottomActions/index.tsx index 9ed89447fb..e1950b8458 100644 --- a/src/components/QuranReader/ReadingView/StudyModeModal/StudyModeBottomActions/index.tsx +++ b/src/components/QuranReader/ReadingView/StudyModeModal/StudyModeBottomActions/index.tsx @@ -13,6 +13,7 @@ import { AudioPlayerMachineContext } from 'src/xstate/AudioPlayerMachineContext' export enum StudyModeTabId { TAFSIR = 'tafsir', + SYNTAX = 'syntax', LAYERS = 'layers', LESSONS = 'lessons', REFLECTIONS = 'reflections', diff --git a/src/components/QuranReader/ReadingView/StudyModeModal/index.tsx b/src/components/QuranReader/ReadingView/StudyModeModal/index.tsx index ad2875db61..cb8d0223b1 100644 --- a/src/components/QuranReader/ReadingView/StudyModeModal/index.tsx +++ b/src/components/QuranReader/ReadingView/StudyModeModal/index.tsx @@ -412,6 +412,7 @@ const StudyModeModal: React.FC = ({ activeContentTab && [ StudyModeTabId.TAFSIR, + StudyModeTabId.SYNTAX, StudyModeTabId.LAYERS, StudyModeTabId.REFLECTIONS, StudyModeTabId.LESSONS, diff --git a/src/components/QuranReader/ReadingView/StudyModeModal/tabs/StudyModeSyntaxTab.tsx b/src/components/QuranReader/ReadingView/StudyModeModal/tabs/StudyModeSyntaxTab.tsx new file mode 100644 index 0000000000..ea5da99a79 --- /dev/null +++ b/src/components/QuranReader/ReadingView/StudyModeModal/tabs/StudyModeSyntaxTab.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import dynamic from 'next/dynamic'; + +import SyntaxSkeleton from '@/components/QuranReader/SyntaxView/SyntaxSkeleton'; +import { + useSyntaxTabScroll, + syntaxTabStyles as styles, +} from '@/components/QuranReader/SyntaxView/SyntaxTabLayout'; +import Word from '@/types/Word'; + +const SyntaxBody = dynamic(() => import('@/components/QuranReader/SyntaxView/SyntaxBody'), { + loading: SyntaxSkeleton, +}); + +interface StudyModeSyntaxTabProps { + chapterId: string; + verseNumber: string; + selectedWord?: Word; +} + +const StudyModeSyntaxTab: React.FC = ({ + chapterId, + verseNumber, + selectedWord, +}) => { + const { containerRef, scrollToTop } = useSyntaxTabScroll(); + + return ( +
+ +
+ ); +}; + +export default StudyModeSyntaxTab; diff --git a/src/components/QuranReader/SyntaxView/SyntaxBody.tsx b/src/components/QuranReader/SyntaxView/SyntaxBody.tsx new file mode 100644 index 0000000000..0680cee26d --- /dev/null +++ b/src/components/QuranReader/SyntaxView/SyntaxBody.tsx @@ -0,0 +1,175 @@ +/* eslint-disable max-lines */ +/* eslint-disable i18next/no-literal-string */ +/* eslint-disable jsdoc/require-returns */ +/* eslint-disable no-void */ +/* eslint-disable no-nested-ternary */ +/* eslint-disable react/no-array-index-key */ +import React from 'react'; + +import classNames from 'classnames'; +import { useSelector, shallowEqual } from 'react-redux'; +import useSWR from 'swr'; + +import { SyntaxAnalysisCharts } from './SyntaxChartTables'; +import SyntaxTabLayout from './SyntaxTabLayout'; +import styles from './SyntaxView.module.scss'; +import useSyntaxChartArabicTypography from './useSyntaxChartArabicTypography'; + +import { selectQuranReaderStyles } from '@/redux/slices/QuranReader/styles'; +import { fetchSyntaxAnalysis, getWordTextUthmaniForSyntax } from '@/services/syntaxAnalysisService'; +import Word from '@/types/Word'; + +interface SyntaxBodyProps { + chapterId: string; + verseNumber: string; + selectedWord?: Word; + scrollToTop: () => void; +} + +/** + * Syntax tab body — layout and font scaling match TafsirBody (tafsirFontScale + generate-font-scales). + * Morphology JSON comes from `/api/syntax/analyze` (Quran MCP). + */ +const SyntaxBody: React.FC = (props) => { + const { selectedWord, scrollToTop, chapterId, verseNumber } = props; + void scrollToTop; + void chapterId; + void verseNumber; + + const quranReaderStyles = useSelector(selectQuranReaderStyles, shallowEqual); + const { tafsirFontScale } = quranReaderStyles; + const { arabicTypographyStyle, verbTypographyClassName } = useSyntaxChartArabicTypography(); + + const wordText = selectedWord ? getWordTextUthmaniForSyntax(selectedWord) : ''; + const verseKey = selectedWord?.verseKey; + + const swrKey = + selectedWord && wordText + ? ['syntax-analysis', selectedWord.location ?? '', wordText, verseKey ?? ''] + : null; + + const { + data: analysis, + error, + isValidating, + } = useSWR( + swrKey, + () => + fetchSyntaxAnalysis({ + textUthmani: wordText, + verseKey, + }), + { revalidateOnFocus: false }, + ); + + const showAnalysisLoading = Boolean(swrKey && isValidating && !analysis && !error); + + let analysisErrorMessage: string | null = null; + if (error instanceof Error) { + analysisErrorMessage = error.message; + } else if (error) { + analysisErrorMessage = String(error); + } + + return ( + +
+

+ Grammatical Analysis of the word:{' '} + {wordText ? ( + + {wordText} + + ) : ( + No script text on this token — pick a word token. + )} +

+ +

+ {selectedWord?.translation?.text || 'None selected'} -{' '} + {selectedWord?.transliteration?.text || 'None selected'} +

+ +
+

Morphology

+ {!selectedWord && ( +

Select a word in the verse to analyze.

+ )} + {selectedWord && !wordText && ( +

This token has no Uthmani text field.

+ )} + {showAnalysisLoading &&

Loading analysis…

} + {analysisErrorMessage && ( +

+ {analysisErrorMessage} +

+ )} + {analysis && ( + <> +
+ Root letters: +

+ + {analysis.rootLetter.rootLetter} + +

+
+
+ Pattern +

+ {analysis.pattern.wordPattern} — + + {analysis.pattern.patternType} + +

+
+
+ Word breakdown +
    + {analysis.wordBreakDown.map((row, idx) => ( +
  • + + {row.part} + {' '} + — {row.meaning} +
  • + ))} +
+
+ + + )} +
+
+ + } + /> + ); +}; + +export default SyntaxBody; diff --git a/src/components/QuranReader/SyntaxView/SyntaxChartTables.tsx b/src/components/QuranReader/SyntaxView/SyntaxChartTables.tsx new file mode 100644 index 0000000000..f47609b646 --- /dev/null +++ b/src/components/QuranReader/SyntaxView/SyntaxChartTables.tsx @@ -0,0 +1,392 @@ +/* eslint-disable max-lines */ +/* eslint-disable react/no-multi-comp */ +/* eslint-disable i18next/no-literal-string */ +import React from 'react'; + +import classNames from 'classnames'; + +import styles from './SyntaxView.module.scss'; +import useSyntaxChartArabicTypography from './useSyntaxChartArabicTypography'; + +import type { + SyntaxAnalysisIsmChart, + SyntaxAnalysisSarfChart, + SyntaxAnalysisSarfColumnKey, + SyntaxAnalysisVerbChart, + SyntaxAnalysisVerbSlot, +} from 'types/SyntaxAnalysis'; + +const VERB_PERSON_LABELS: Record = { + '3rdPersonMasculine': 'Masculine 3rd person', + '3rdPersonFeminine': 'Feminine 3rd person', + '2ndPersonMasculine': 'Masculine 2nd person', + '2ndPersonFeminine': 'Feminine 2nd person', + '1stPerson': '1st Person', +}; + +const ISM_CASES = ['Rafa', 'Nasab', 'Jar'] as const; + +/** Sarf columns — with `dir="rtl"` on the table, first cell is Past (visual right). */ +const SYNTAX_SARF_COLUMN_ORDER: SyntaxAnalysisSarfColumnKey[] = [ + 'pastTense', + 'presentTense', + 'idea', + 'doer', +]; + +/** Stable row order for verb conjugation table */ +const VERB_CHART_ORDER: (keyof SyntaxAnalysisVerbChart)[] = [ + '3rdPersonMasculine', + '3rdPersonFeminine', + '2ndPersonMasculine', + '2ndPersonFeminine', + '1stPerson', +]; + +function VerbFormCell({ + slot, + colSpan, + verbTypographyClassName, + arabicTypographyStyle, +}: { + slot: SyntaxAnalysisVerbSlot; + colSpan?: number; + verbTypographyClassName: string; + arabicTypographyStyle: React.CSSProperties; +}) { + return ( + +
+ {slot.verb} +
+ + ); +} + +function MeaningCell({ + slot, + meaningTypographyClassName, + arabicTypographyStyle, +}: { + slot: SyntaxAnalysisVerbSlot; + meaningTypographyClassName: string; + arabicTypographyStyle: React.CSSProperties; +}) { + return ( + +
+ {slot.meaning} + + {slot.pronoun} + +
+ + ); +} + +type Props = { + verbChart?: SyntaxAnalysisVerbChart; + verbPresentTenseChart?: SyntaxAnalysisVerbChart; + verbPastTenseChart?: SyntaxAnalysisVerbChart; + ismChart?: SyntaxAnalysisIsmChart; + sarfChart?: SyntaxAnalysisSarfChart; +}; + +export const SyntaxVerbChartTable: React.FC<{ + chart: SyntaxAnalysisVerbChart; + title: string; +}> = ({ chart, title }) => { + const { arabicTypographyStyle, verbTypographyClassName, meaningTypographyClassName } = + useSyntaxChartArabicTypography(); + + const sections: React.ReactNode[] = []; + + VERB_CHART_ORDER.forEach((personKey) => { + if (!(personKey in chart)) return; + const block = chart[personKey]; + const personLabel = VERB_PERSON_LABELS[personKey]; + + if (personKey === '1stPerson') { + const sg = block.singular; + const pl = block.plural; + if (!sg || !pl) return; + + sections.push( + + + +
+ {pl.meaning} + + {pl.pronoun} + +
+ + + + {personLabel} + + + + + + +
, + ); + return; + } + + const pl = block.plural; + const du = block.dual; + const sg = block.singular; + if (!pl || !du || !sg) return; + + sections.push( + + + + + + + {personLabel} + + + + + + + + , + ); + }); + + if (sections.length === 0) return null; + + return ( +
+

{title}

+
+ + + + + + + + + {sections} +
PluralPairSingular +
+
+
+ ); +}; + +export const SyntaxIsmChartTable: React.FC<{ chart: SyntaxAnalysisIsmChart }> = ({ chart }) => { + const { arabicTypographyStyle, verbTypographyClassName } = useSyntaxChartArabicTypography(); + + return ( +
+

Ism chart (declension)

+
+ + + + + + + + + + + + + + + + + + {ISM_CASES.map((caseName) => ( + + + + + + + + + + ))} + +
+ Feminine + + Masculine + Case
PluralDualSingularPluralDualSingular +
+ + {chart.Feminine[caseName].plural} + + + + {chart.Feminine[caseName].dual} + + + + {chart.Feminine[caseName].singular} + + + + {chart.Masculine[caseName].plural} + + + + {chart.Masculine[caseName].dual} + + + + {chart.Masculine[caseName].singular} + + {caseName}
+
+
+ ); +}; + +export const SyntaxSarfChartTable: React.FC<{ chart: SyntaxAnalysisSarfChart }> = ({ chart }) => { + const { arabicTypographyStyle, verbTypographyClassName } = useSyntaxChartArabicTypography(); + + const headerCells = SYNTAX_SARF_COLUMN_ORDER.map((key) => ( + + {chart.columnHeaders[key]} + + )); + + const labelCells = (get: (k: SyntaxAnalysisSarfColumnKey) => string | undefined) => + SYNTAX_SARF_COLUMN_ORDER.map((key) => ( + + {get(key) ?? ''} + + )); + + const formCells = (get: (k: SyntaxAnalysisSarfColumnKey) => string | undefined) => + SYNTAX_SARF_COLUMN_ORDER.map((key) => { + const text = get(key)?.trim(); + if (!text) { + return ; + } + return ( + +
+ {text} +
+ + ); + }); + + return ( +
+

Sarf chart

+
+ + + {headerCells} + + + {formCells((k) => chart.activeVoice[k])} + {labelCells((k) => chart.passiveVoiceLabels[k])} + {formCells((k) => chart.passiveVoiceForms[k])} + {labelCells((k) => chart.commandingLabels[k])} + {formCells((k) => chart.commandingForms[k])} + +
+
+
+ ); +}; + +export const SyntaxAnalysisCharts: React.FC = ({ + verbChart, + verbPresentTenseChart, + verbPastTenseChart, + ismChart, + sarfChart, +}) => ( + <> + {sarfChart && } + {verbPresentTenseChart && ( + + )} + {verbPastTenseChart && ( + + )} + {verbChart && ( + + )} + {ismChart && } + +); diff --git a/src/components/QuranReader/SyntaxView/SyntaxSkeleton.module.scss b/src/components/QuranReader/SyntaxView/SyntaxSkeleton.module.scss new file mode 100644 index 0000000000..67e1688277 --- /dev/null +++ b/src/components/QuranReader/SyntaxView/SyntaxSkeleton.module.scss @@ -0,0 +1,11 @@ +.syntaxSkeletonItem { + width: 40%; + height: 1.25rem; + margin-bottom: 1rem; +} + +.syntaxSkeletonLine { + width: 100%; + height: 0.875rem; + margin-bottom: 0.75rem; +} diff --git a/src/components/QuranReader/SyntaxView/SyntaxSkeleton.tsx b/src/components/QuranReader/SyntaxView/SyntaxSkeleton.tsx new file mode 100644 index 0000000000..eac2a1416f --- /dev/null +++ b/src/components/QuranReader/SyntaxView/SyntaxSkeleton.tsx @@ -0,0 +1,22 @@ +import range from 'lodash/range'; + +import styles from './SyntaxSkeleton.module.scss'; + +import Skeleton from '@/dls/Skeleton/Skeleton'; + +/** + * Loading placeholder for Syntax view content (Study Mode dynamic import). + * @returns {React.ReactElement} Skeleton lines for Syntax tab loading state. + */ +const SyntaxSkeleton = () => { + return ( + <> + + {range(1, 12).map((i) => ( + + ))} + + ); +}; + +export default SyntaxSkeleton; diff --git a/src/components/QuranReader/SyntaxView/SyntaxTabLayout.module.scss b/src/components/QuranReader/SyntaxView/SyntaxTabLayout.module.scss new file mode 100644 index 0000000000..9901cd7b0b --- /dev/null +++ b/src/components/QuranReader/SyntaxView/SyntaxTabLayout.module.scss @@ -0,0 +1,12 @@ +.container { + overflow: hidden; +} + +.content { + display: flex; + flex-direction: column; + position: relative; +} + +.bodyContainer { +} diff --git a/src/components/QuranReader/SyntaxView/SyntaxTabLayout.tsx b/src/components/QuranReader/SyntaxView/SyntaxTabLayout.tsx new file mode 100644 index 0000000000..7aeb799709 --- /dev/null +++ b/src/components/QuranReader/SyntaxView/SyntaxTabLayout.tsx @@ -0,0 +1,47 @@ +import React, { useRef, useCallback, ReactNode } from 'react'; + +import styles from './SyntaxTabLayout.module.scss'; + +import { FontSizeType } from '@/components/QuranReader/ReadingView/StudyModeModal/FontSizeControl'; +import StudyModeControlsHeader from '@/components/QuranReader/ReadingView/StudyModeModal/StudyModeControlsHeader'; + +interface SyntaxTabLayoutProps { + selectionControl: ReactNode; + body: ReactNode; + fontType?: FontSizeType; +} + +/** + * Layout for Syntax (grammar analytics) content in Study Mode — mirrors StudyModeTabLayout pattern. + * @returns {React.ReactElement} Syntax tab chrome with font controls and body slot. + */ +const SyntaxTabLayout: React.FC = ({ + selectionControl, + body, + fontType = 'tafsir', +}) => { + return ( +
+ +
{body}
+
+ ); +}; + +export default SyntaxTabLayout; + +/** + * Scroll container ref + scroll-to-top for Syntax tab content. + * @returns {{ containerRef: React.RefObject, scrollToTop: () => void }} + */ +export const useSyntaxTabScroll = () => { + const containerRef = useRef(null); + + const scrollToTop = useCallback(() => { + containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); + }, []); + + return { containerRef, scrollToTop }; +}; + +export { styles as syntaxTabStyles }; diff --git a/src/components/QuranReader/SyntaxView/SyntaxView.module.scss b/src/components/QuranReader/SyntaxView/SyntaxView.module.scss new file mode 100644 index 0000000000..3680ca6007 --- /dev/null +++ b/src/components/QuranReader/SyntaxView/SyntaxView.module.scss @@ -0,0 +1,291 @@ +@use 'src/styles/utility'; + +.syntaxPageContainer { + max-inline-size: 100%; + margin-block-start: 0; + margin-block-end: 0; + margin-inline-start: 0; + margin-inline-end: 0; +} + +.syntaxContainer { + @include utility.edgeHorizontalPadding; + padding-block-start: var(--spacing-medium); + padding-block-end: var(--spacing-large); + letter-spacing: 0; + line-height: normal; + p { + margin-block-end: var(--spacing-small); + opacity: var(--opacity-85); + line-height: normal; + } + br { + content: ''; + margin-block-start: var(--spacing-small); + margin-inline-end: var(--spacing-small); + margin-block-end: var(--spacing-small); + margin-inline-start: var(--spacing-small); + display: block; + } + h2 { + font-weight: var(--font-weight-semibold); + font-size: var(--font-size-xlarge); + margin-block-end: var(--spacing-small); + margin-block-start: calc(1.5 * var(--spacing-medium)); + &:first-child { + margin-block-start: 0; + } + } + * { + letter-spacing: 0; + line-height: normal; + } + *[class~='arabic'] { + direction: rtl; + /* stylelint-disable-next-line csstools/use-logical */ + text-align: right; + margin-block-end: var(--spacing-small); + } + *[class~='uthmani'] { + font-family: UthmanicHafs; + font-size: larger; + } +} + +.syntaxSectionTitle { + border-block-start: 1px solid var(--color-borders-hairline); + font-size: var(--font-size-xlarge); + font-weight: var(--font-weight-bold); + padding-block-start: var(--spacing-medium); + padding-block-end: var(--spacing-xxsmall); + padding-inline-start: 0; + padding-inline-end: 0; + margin-block-start: var(--spacing-xxsmall); + margin-inline-start: 0; + margin-inline-end: 0; +} + +.syntaxMetaRow { + margin-block-end: var(--spacing-small); +} + +.syntaxWordJson { + margin-block-start: var(--spacing-medium); + margin-block-end: 0; + padding: var(--spacing-small); + border: 1px solid var(--color-borders-hairline); + border-radius: var(--border-radius-default); + background-color: var(--color-background-default); + overflow: auto; + white-space: pre-wrap; + word-break: break-word; +} + +.syntaxAnalysisSection { + margin-block-start: var(--spacing-medium); + padding-block-start: var(--spacing-small); + border-block-start: 1px solid var(--color-borders-hairline); +} + +.syntaxAnalysisHeading { + font-size: var(--font-size-large); + font-weight: var(--font-weight-semibold); + margin-block-end: var(--spacing-small); +} + +.syntaxAnalysisBlock { + margin-block-end: var(--spacing-medium); + + strong { + display: block; + margin-block-end: var(--spacing-xxsmall); + } +} + +.syntaxMorphSubheading { + margin-block-end: var(--spacing-xxsmall); + margin-block-start: var(--spacing-xsmall); + font-size: var(--font-size-xsmall); + font-weight: var(--font-weight-medium); + opacity: var(--opacity-75); + + &:first-of-type { + margin-block-start: 0; + } +} + +.syntaxPatternWordPattern { + margin-block-start: 0; + margin-block-end: 0; + line-height: 1.45; + opacity: var(--opacity-85); +} + +.syntaxBreakdownList { + margin: 0; + padding-inline-start: var(--spacing-medium); +} + +.syntaxMuted { + opacity: var(--opacity-75); + font-style: normal; +} + +.syntaxError { + color: var(--color-warning-medium); + margin-block-end: var(--spacing-small); +} + +.syntaxRawDetails { + margin-block-start: var(--spacing-large); + + summary { + cursor: pointer; + margin-block-end: var(--spacing-small); + } +} + +.syntaxTableSection { + margin-block-start: var(--spacing-large); + margin-block-end: var(--spacing-medium); +} + +.syntaxTableTitle { + font-size: var(--font-size-normal); + font-weight: var(--font-weight-semibold); + margin-block-end: var(--spacing-small); +} + +.syntaxTableScroll { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.syntaxTable { + inline-size: 100%; + min-inline-size: min(100%, 32rem); + border-collapse: collapse; + font-size: var(--font-size-small); + + th, + td { + border: 1px solid var(--color-borders-hairline); + padding: var(--spacing-xxsmall) var(--spacing-xsmall); + text-align: start; + vertical-align: top; + } + + thead th { + background-color: var(--color-background-alternative-faint); + font-weight: var(--font-weight-semibold); + } + + tbody th[scope='row'] { + font-weight: var(--font-weight-medium); + background-color: var(--color-background-alternative-faint); + white-space: nowrap; + } +} + +/* Verb conjugation grid (plural | dual | singular | person) */ +.syntaxVerbChartTable { + min-inline-size: min(100%, 36rem); + + th, + td { + border: 1px solid var(--color-text-default); + vertical-align: middle; + } + + thead th { + text-align: center; + background-color: var(--color-background-alternative-faded); + } +} + +.syntaxVerbChartPersonCell { + inline-size: 6.5rem; + text-align: center; + font-weight: var(--font-weight-semibold); + font-size: var(--font-size-xsmall); + line-height: var(--line-height-normal); + background-color: var(--color-background-alternative-faded); +} + +.syntaxVerbChartMeaningCell { + background-color: var(--color-background-alternative-faded); + padding: var(--spacing-xsmall); +} + +.syntaxVerbChartMeaningInner { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: var(--spacing-xsmall); +} + +.syntaxVerbChartMeaningEn { + text-align: start; + flex: 1; + min-inline-size: 0; +} + +.syntaxVerbChartMeaningPronoun { + flex-shrink: 0; +} + +/* Meaning-row Arabic: half of verb-row computed `--font-size` from chart scale classes */ +.syntaxVerbChartMeaningArabic { + font-size: calc(0.6 * var(--font-size)); +} + +.syntaxVerbChartVerbCell { + background-color: var(--color-background-default); + text-align: center; + padding-block: var(--spacing-small); + padding-inline: var(--spacing-xsmall); +} + +.syntaxVerbChartVerbInner { + line-height: var(--line-height, normal); +} + +/* Sarf chart (verb morphology grid) */ +.syntaxSarfChartTable { + min-inline-size: min(100%, 40rem); + + th, + td { + border: 1px solid var(--color-text-default); + vertical-align: middle; + text-align: center; + } +} + +.syntaxSarfChartHeaderCell { + background-color: var(--color-success-medium); + color: var(--color-text-inverse); + font-weight: var(--font-weight-semibold); + font-size: var(--font-size-small); + padding-block: var(--spacing-small); +} + +.syntaxSarfChartLabelCell { + background-color: var(--color-background-alternative-faded); + font-size: var(--font-size-xsmall); + line-height: var(--line-height-normal); + padding-block: var(--spacing-xsmall); +} + +.syntaxSarfChartDataCell { + background-color: var(--color-background-default); + padding-block: var(--spacing-medium); + padding-inline: var(--spacing-xsmall); +} + +.syntaxSarfChartArabic { + line-height: var(--line-height, normal); +} + +@include utility.generate-font-scales('tafsir'); diff --git a/src/components/QuranReader/SyntaxView/useSyntaxChartArabicTypography.ts b/src/components/QuranReader/SyntaxView/useSyntaxChartArabicTypography.ts new file mode 100644 index 0000000000..0a0e919cc7 --- /dev/null +++ b/src/components/QuranReader/SyntaxView/useSyntaxChartArabicTypography.ts @@ -0,0 +1,65 @@ +import { useMemo } from 'react'; + +import classNames from 'classnames'; +import { shallowEqual, useSelector } from 'react-redux'; + +import styles from './SyntaxView.module.scss'; + +import textWordStyles from '@/components/dls/QuranWord/TextWord.module.scss'; +import verseTextStyles from '@/components/Verse/VerseText.module.scss'; +import { selectQuranReaderStyles } from '@/redux/slices/QuranReader/styles'; +import { getFontClassName, getFontFaceNameForPage } from '@/utils/fontFaceHelper'; +import { QuranFont } from 'types/QuranReader'; + +/** Madani V1 / page 1 QCF face — matches `p1-v1` glyph fonts used in the reader. */ +const SYNTAX_CHART_ARABIC_FONT = QuranFont.MadaniV1; +const SYNTAX_CHART_ARABIC_PAGE = 1; + +/** + * Shared Arabic typography for Syntax charts and inline Arabic in SyntaxBody: + * same scale as Quran text (user setting) + `tafsirOrTranslationMode`, with QCF font `p1-v1`. + * + * @returns {{ + * arabicFontFamily: string; + * arabicTypographyStyle: import('react').CSSProperties; + * verbTypographyClassName: string; + * meaningTypographyClassName: string; + * }} Typography classes and inline `fontFamily` for Syntax Arabic (`p1-v1`). + */ +export default function useSyntaxChartArabicTypography() { + const { quranTextFontScale, mushafLines } = useSelector(selectQuranReaderStyles, shallowEqual); + + const arabicFontFamily = useMemo( + () => getFontFaceNameForPage(SYNTAX_CHART_ARABIC_FONT, SYNTAX_CHART_ARABIC_PAGE), + [], + ); + + const arabicTypographyStyle = useMemo( + () => ({ fontFamily: arabicFontFamily }), + [arabicFontFamily], + ); + + const verbTypographyClassName = useMemo( + () => + classNames( + textWordStyles.word, + verseTextStyles.tafsirOrTranslationMode, + verseTextStyles[ + getFontClassName(SYNTAX_CHART_ARABIC_FONT, quranTextFontScale, mushafLines) + ], + ), + [mushafLines, quranTextFontScale], + ); + + const meaningTypographyClassName = useMemo( + () => classNames(verbTypographyClassName, styles.syntaxVerbChartMeaningArabic), + [verbTypographyClassName], + ); + + return { + arabicFontFamily, + arabicTypographyStyle, + verbTypographyClassName, + meaningTypographyClassName, + }; +} diff --git a/src/components/Verse/VerseText.module.scss b/src/components/Verse/VerseText.module.scss index 028c17bbe8..4111a7cba2 100644 --- a/src/components/Verse/VerseText.module.scss +++ b/src/components/Verse/VerseText.module.scss @@ -89,5 +89,5 @@ // On mobile, make the QuranWord slightly bigger on translation or tafsir mode .tafsirOrTranslationMode { // --font-size is generated by `utility.genereate-font-sizes` - font-size: calc(1.2 * var(--font-size)); + font-size: calc(0.75 * var(--font-size)); } diff --git a/src/lib/syntaxAnalysisCharts.ts b/src/lib/syntaxAnalysisCharts.ts new file mode 100644 index 0000000000..1ee7421e74 --- /dev/null +++ b/src/lib/syntaxAnalysisCharts.ts @@ -0,0 +1,238 @@ +/* eslint-disable max-lines -- chart normalization mirrors API contract */ +import type { + SyntaxAnalysisIsmChart, + SyntaxAnalysisResult, + SyntaxAnalysisSarfChart, + SyntaxAnalysisSarfColumnKey, + SyntaxAnalysisVerbChart, + SyntaxAnalysisVerbSlot, +} from 'types/SyntaxAnalysis'; + +const SYNTAX_SARF_KEYS: SyntaxAnalysisSarfColumnKey[] = [ + 'pastTense', + 'presentTense', + 'idea', + 'doer', +]; + +function isSarfFullColumnStrings(v: unknown): v is Record { + if (!v || typeof v !== 'object') return false; + const o = v as Record; + return SYNTAX_SARF_KEYS.every((k) => typeof o[k] === 'string'); +} + +export function normalizeSarfChart(raw: unknown): SyntaxAnalysisSarfChart | undefined { + if (!raw || typeof raw !== 'object') return undefined; + const s = raw as Record; + if (!isSarfFullColumnStrings(s.columnHeaders) || !isSarfFullColumnStrings(s.activeVoice)) { + return undefined; + } + const partialStrings = (key: string): Partial> => { + const v = s[key]; + if (!v || typeof v !== 'object') return {}; + const o = v as Record; + const entries = SYNTAX_SARF_KEYS.filter((k) => typeof o[k] === 'string').map((k) => [ + k, + o[k], + ]) as [SyntaxAnalysisSarfColumnKey, string][]; + return Object.fromEntries(entries) as Partial>; + }; + return { + columnHeaders: s.columnHeaders, + activeVoice: s.activeVoice, + passiveVoiceLabels: partialStrings('passiveVoiceLabels'), + passiveVoiceForms: partialStrings('passiveVoiceForms'), + commandingLabels: partialStrings('commandingLabels'), + commandingForms: partialStrings('commandingForms'), + }; +} + +const ISM_CASE_KEYS = ['Rafa', 'Nasab', 'Jar'] as const; +const ISM_NUMBER_KEYS = ['singular', 'dual', 'plural'] as const; + +export function normalizeIsmChart(raw: unknown): SyntaxAnalysisIsmChart | undefined { + if (!raw || typeof raw !== 'object') return undefined; + const chart = raw as Record; + const valid = (['Masculine', 'Feminine'] as const).every((gender) => { + const g = chart[gender]; + if (!g || typeof g !== 'object') return false; + const go = g as Record; + return ISM_CASE_KEYS.every((caseName) => { + const row = go[caseName]; + if (!row || typeof row !== 'object') return false; + const ro = row as Record; + return ISM_NUMBER_KEYS.every((num) => typeof ro[num] === 'string'); + }); + }); + return valid ? (raw as SyntaxAnalysisIsmChart) : undefined; +} + +const VERB_CHART_PERSON_KEYS = [ + '3rdPersonMasculine', + '3rdPersonFeminine', + '2ndPersonMasculine', + '2ndPersonFeminine', +] as const; + +const VERB_CHART_ALL_KEYS: readonly (keyof SyntaxAnalysisVerbChart)[] = [ + ...VERB_CHART_PERSON_KEYS, + '1stPerson', +]; + +function isVerbSlot(v: unknown): v is { pronoun: string; meaning: string; verb: string } { + if (!v || typeof v !== 'object') return false; + const o = v as Record; + return ( + typeof o.pronoun === 'string' && typeof o.meaning === 'string' && typeof o.verb === 'string' + ); +} + +function normalizeVerbChartStrict(raw: unknown): SyntaxAnalysisVerbChart | undefined { + if (!raw || typeof raw !== 'object') return undefined; + const o = raw as Record; + const fourOk = VERB_CHART_PERSON_KEYS.every((key) => { + const block = o[key]; + if (!block || typeof block !== 'object') return false; + const b = block as Record; + return (['singular', 'dual', 'plural'] as const).every((num) => isVerbSlot(b[num])); + }); + if (!fourOk) return undefined; + const first = o['1stPerson']; + if (!first || typeof first !== 'object') return undefined; + const fb = first as Record; + if (!isVerbSlot(fb.singular) || !isVerbSlot(fb.plural)) return undefined; + return raw as SyntaxAnalysisVerbChart; +} + +function normalizeVerbChartBestEffort(raw: unknown): SyntaxAnalysisVerbChart | undefined { + if (!raw || typeof raw !== 'object') return undefined; + const o = raw as Record; + const out: Partial = {}; + + VERB_CHART_ALL_KEYS.forEach((personKey) => { + const block = o[personKey]; + if (!block || typeof block !== 'object') return; + const b = block as Record; + + if (personKey === '1stPerson') { + if (!isVerbSlot(b.singular) || !isVerbSlot(b.plural)) return; + out['1stPerson'] = { + singular: b.singular as SyntaxAnalysisVerbSlot, + plural: b.plural as SyntaxAnalysisVerbSlot, + }; + return; + } + + const sg = b.singular; + const du = b.dual; + const pl = b.plural; + if (!isVerbSlot(sg) || !isVerbSlot(du) || !isVerbSlot(pl)) return; + out[personKey] = { + singular: sg as SyntaxAnalysisVerbSlot, + dual: du as SyntaxAnalysisVerbSlot, + plural: pl as SyntaxAnalysisVerbSlot, + }; + }); + + if (Object.keys(out).length === 0) return undefined; + return out as SyntaxAnalysisVerbChart; +} + +export function normalizeVerbChart(raw: unknown): SyntaxAnalysisVerbChart | undefined { + return normalizeVerbChartStrict(raw) ?? normalizeVerbChartBestEffort(raw); +} + +const VERB_PRESENT_CHART_JSON_KEYS = [ + 'verbPresentTenseChart', + 'verbPresentChart', + 'presentTenseVerbChart', +] as const; + +function pickFirstVerbChartRaw(r: Record, keys: readonly string[]): unknown { + const key = keys.find((k) => { + const v = r[k]; + return Boolean(v && typeof v === 'object'); + }); + return key ? r[key] : undefined; +} + +function verbTensePatternHints( + patternType: string, + wordPattern: string, +): { + looksPresent: boolean; + looksPast: boolean; +} { + const pt = patternType; + const wp = wordPattern; + const looksPresent = + /مضارع/.test(pt) || /مضارع/.test(wp) || /\b(imperfect|present\s+tense|\bpresent\b)/i.test(wp); + const looksPast = + /ماض[يى]/.test(pt) || /ماض[يى]/.test(wp) || /\b(perfect|past\s+tense|\bpast\b)/i.test(wp); + return { looksPresent, looksPast }; +} + +/* eslint-disable react-func/max-lines-per-function -- verb merge mirrors API rules */ +function mergeVerbChartsOntoResult( + result: SyntaxAnalysisResult, + r: Record, + patternType: string, + wordPattern: string, +): SyntaxAnalysisResult { + const { looksPresent, looksPast } = verbTensePatternHints(patternType, wordPattern); + + const legacyVerbChart = normalizeVerbChart(r.verbChart); + let verbPresentTenseChart = normalizeVerbChart( + pickFirstVerbChartRaw(r, VERB_PRESENT_CHART_JSON_KEYS), + ); + let verbPastTenseChart = normalizeVerbChart(r.verbPastTenseChart); + + const ambiguousTense = looksPresent && looksPast; + if (!ambiguousTense) { + if (!verbPresentTenseChart && legacyVerbChart && looksPresent) { + verbPresentTenseChart = legacyVerbChart; + } + if (!verbPastTenseChart && legacyVerbChart && looksPast) { + verbPastTenseChart = legacyVerbChart; + } + } + + let verbChartOut: SyntaxAnalysisVerbChart | undefined = legacyVerbChart; + if ( + verbChartOut && + (verbPresentTenseChart === verbChartOut || verbPastTenseChart === verbChartOut) + ) { + verbChartOut = undefined; + } + + let out = result; + if (verbPresentTenseChart) out = { ...out, verbPresentTenseChart }; + if (verbPastTenseChart) out = { ...out, verbPastTenseChart }; + if (verbChartOut) out = { ...out, verbChart: verbChartOut }; + return out; +} +/* eslint-enable react-func/max-lines-per-function */ + +/** + * Merges optional chart keys from a partial model payload onto an existing base result + * (same rules as full `/api/syntax/analyze` OpenAI normalization for charts). + * @returns {SyntaxAnalysisResult} Base plus any valid optional charts from `rawCharts`. + */ +export function applyOptionalChartsToResult( + base: SyntaxAnalysisResult, + rawCharts: unknown, +): SyntaxAnalysisResult { + if (!rawCharts || typeof rawCharts !== 'object') return base; + const r = rawCharts as Record; + const { patternType, wordPattern } = base.pattern; + + let result: SyntaxAnalysisResult = { ...base }; + const sarfChart = normalizeSarfChart(r.sarfChart); + if (sarfChart) result = { ...result, sarfChart }; + + result = mergeVerbChartsOntoResult(result, r, patternType, wordPattern); + + const ismChart = normalizeIsmChart(r.ismChart); + if (ismChart) result = { ...result, ismChart }; + return result; +} diff --git a/src/lib/syntaxAnalysisQuranMcp.ts b/src/lib/syntaxAnalysisQuranMcp.ts new file mode 100644 index 0000000000..8ab8380ba2 --- /dev/null +++ b/src/lib/syntaxAnalysisQuranMcp.ts @@ -0,0 +1,41 @@ +import { Client } from '@modelcontextprotocol/sdk/client'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp'; + +import { applyOptionalChartsToResult } from '@/lib/syntaxAnalysisCharts'; +import { runMcpSyntaxStudyOnClient } from '@/lib/syntaxAnalysisQuranMcpMorphology'; +import { buildOptionalChartsFromMcp } from '@/lib/syntaxChartsFromMcp'; +import type { SyntaxAnalysisResult } from 'types/SyntaxAnalysis'; + +const DEFAULT_QURAN_MCP_URL = 'https://mcp.quran.ai/'; + +export type QuranMcpSyntaxOptions = { + textUthmani: string; + verseKey?: string; + /** Base URL for Streamable HTTP MCP (per https://mcp.quran.ai/documentation ). */ + mcpUrl?: string; +}; + +/** + * Grounded word study from [Quran MCP](https://mcp.quran.ai/) (`fetch_word_morphology` + `fetch_word_paradigm`). + * Sarf and verb charts are derived from MCP paradigm stems (same JSON shapes as the legacy analyzer). + * @returns {@link SyntaxAnalysisResult} with optional charts when paradigm data is available. + */ +export async function fetchSyntaxAnalysisViaQuranMcp( + options: QuranMcpSyntaxOptions, +): Promise { + const baseUrl = options.mcpUrl || process.env.QURAN_SYNTAX_MCP_URL || DEFAULT_QURAN_MCP_URL; + const trimmed = baseUrl.trim(); + const url = new URL(trimmed.endsWith('/') ? trimmed : `${trimmed}/`); + + const transport = new StreamableHTTPClientTransport(url); + const client = new Client({ name: 'quran.com-frontend', version: '1.0.0' }); + + try { + await client.connect(transport); + const bundle = await runMcpSyntaxStudyOnClient(client, options.textUthmani, options.verseKey); + const rawCharts = buildOptionalChartsFromMcp(bundle.pickedWord, bundle.paradigm); + return applyOptionalChartsToResult(bundle.base, rawCharts); + } finally { + await client.close().catch(() => undefined); + } +} diff --git a/src/lib/syntaxAnalysisQuranMcpMorphology.ts b/src/lib/syntaxAnalysisQuranMcpMorphology.ts new file mode 100644 index 0000000000..54ca735233 --- /dev/null +++ b/src/lib/syntaxAnalysisQuranMcpMorphology.ts @@ -0,0 +1,231 @@ +/* eslint-disable max-lines -- MCP response mapping is verbose but linear */ +import type { Client } from '@modelcontextprotocol/sdk/client'; + +import type { SyntaxAnalysisBreakdownPart, SyntaxAnalysisResult } from 'types/SyntaxAnalysis'; + +/** Verse key as surah:ayah (e.g. 2:255). */ +const AYAH_KEY_RE = /^\s*\d{1,3}:\d{1,3}\s*$/; + +export type MorphologyWord = Record; + +function str(o: MorphologyWord, key: string): string | undefined { + const v = o[key]; + return typeof v === 'string' ? v : undefined; +} + +function obj(o: MorphologyWord, key: string): Record | undefined { + const v = o[key]; + return v && typeof v === 'object' && !Array.isArray(v) + ? (v as Record) + : undefined; +} + +function arr(o: MorphologyWord, key: string): unknown[] | undefined { + const v = o[key]; + return Array.isArray(v) ? v : undefined; +} + +export function isValidAyahKey(verseKey: string | undefined): verseKey is string { + if (!verseKey) return false; + return AYAH_KEY_RE.test(verseKey); +} + +export function pickMorphologyWord( + words: MorphologyWord[], + textUthmani: string, +): MorphologyWord | undefined { + if (!words.length) return undefined; + const t = textUthmani.trim(); + const exact = words.find((w) => str(w, 'text_uthmani')?.trim() === t); + if (exact) return exact; + return ( + words.find( + (w) => + (str(w, 'text_uthmani')?.includes(t) ?? false) || + (t.includes(str(w, 'text_uthmani') || '') && str(w, 'text_uthmani')), + ) ?? words[0] + ); +} + +function englishPatternLine(w: MorphologyWord): string { + const desc = str(w, 'description')?.trim(); + if (desc) return desc; + const g = obj(w, 'grammatical_features'); + if (!g) return str(w, 'translation')?.trim() || 'Morphological analysis (Quran MCP)'; + const parts: string[] = []; + const push = (label: string, key: string) => { + const v = g[key]; + if (v != null && String(v)) parts.push(`${label}: ${v}`); + }; + push('POS', 'part_of_speech'); + push('person', 'person'); + push('gender', 'gender'); + push('number', 'number'); + push('aspect', 'aspect'); + push('mood', 'mood'); + push('case', 'case'); + const vf = g.verb_form; + if (vf != null) parts.push(`verb form: ${vf}`); + return parts.join('; ') || 'Morphological analysis (Quran MCP)'; +} + +function arabicPatternType(w: MorphologyWord): string { + const g = obj(w, 'grammatical_features'); + const pos = String(g?.part_of_speech || '').toLowerCase(); + if (pos.includes('verb')) { + const a = String(g?.aspect || '').toLowerCase(); + if (a.includes('perfect')) return 'فعل ماضي'; + if (a.includes('imperfect')) return 'فعل مضارع'; + if (a.includes('imperative')) return 'فعل أمر'; + return 'فعل'; + } + if (pos.includes('noun') || pos.includes('adjective') || pos.includes('participle')) { + const c = String(g?.case || '').toLowerCase(); + if (c.includes('gen')) return 'اسم مجرور'; + if (c.includes('acc')) return 'اسم منصوب'; + if (c.includes('nom')) return 'اسم مرفوع'; + return 'اسم'; + } + if (pos.includes('prep')) return 'حرف جر'; + if (pos.includes('pron')) return 'ضمير'; + if (pos.includes('particle') || pos.includes('part')) return 'أداة'; + return 'صيغة قرآنية'; +} + +function segmentsToBreakdown(w: MorphologyWord): SyntaxAnalysisBreakdownPart[] { + const segs = arr(w, 'morpheme_segments'); + if (segs?.length) { + return segs.flatMap((raw) => { + if (!raw || typeof raw !== 'object') return []; + const s = raw as Record; + const part = typeof s.text === 'string' ? s.text.trim() : ''; + if (!part) return []; + const g1 = typeof s.grammar_description === 'string' ? s.grammar_description.trim() : ''; + const g2 = typeof s.part_of_speech_name === 'string' ? s.part_of_speech_name.trim() : ''; + const meaning = g1 || g2 || '—'; + return [{ part, meaning }]; + }); + } + const whole = str(w, 'text_uthmani')?.trim() || ''; + if (!whole) return []; + return [{ part: whole, meaning: str(w, 'translation')?.trim() || englishPatternLine(w) }]; +} + +export function morphologyWordToSyntaxResult(w: MorphologyWord): SyntaxAnalysisResult { + const rootLetters = (str(w, 'root') || str(w, 'lemma') || '—').trim() || '—'; + const arabicName = (str(w, 'lemma') || str(w, 'root') || rootLetters).trim() || rootLetters; + return { + rootLetter: { arabicName, rootLetter: rootLetters }, + pattern: { wordPattern: englishPatternLine(w), patternType: arabicPatternType(w) }, + wordBreakDown: segmentsToBreakdown(w), + }; +} + +export function parseToolJsonPayload(result: unknown): unknown { + if (!result || typeof result !== 'object') throw new Error('Invalid MCP tool response'); + const r = result as { + structuredContent?: unknown; + content?: Array<{ type: string; text?: string }>; + isError?: boolean; + }; + if (r.isError) { + const msg = + r.content + ?.filter( + (c): c is { type: 'text'; text: string } => + c.type === 'text' && typeof c.text === 'string', + ) + .map((c) => c.text) + .join('\n') || 'MCP tool returned an error'; + throw new Error(msg); + } + if (r.structuredContent && typeof r.structuredContent === 'object') { + return r.structuredContent; + } + const textBlock = r.content?.find((c) => c.type === 'text' && typeof c.text === 'string'); + if (!textBlock?.text) throw new Error('Empty MCP tool response'); + return JSON.parse(textBlock.text) as unknown; +} + +export async function runFetchWordParadigmOnClient( + client: Client, + pickedWord: MorphologyWord, + textUthmani: string, + verseKey?: string, +): Promise { + try { + const args: Record = {}; + if (isValidAyahKey(verseKey)) { + args.ayah_key = verseKey.trim(); + args.word_text = textUthmani; + } else { + const lemma = str(pickedWord, 'lemma'); + if (!lemma) return null; + args.lemma = lemma; + } + const res = await client.callTool({ name: 'fetch_word_paradigm', arguments: args }); + return parseToolJsonPayload(res); + } catch { + return null; + } +} + +export type McpSyntaxStudyBundle = { + base: SyntaxAnalysisResult; + /** Resolved morphology row for `textUthmani` (used to build optional charts from MCP). */ + pickedWord: MorphologyWord; + morphologyResponse: unknown; + paradigm: unknown | null; +}; + +/** + * Morphology + optional paradigm from Quran MCP (grounding + tools). + * @returns {Promise} Base UI result, resolved word row, and raw MCP payloads for chart building. + */ +export async function runMcpSyntaxStudyOnClient( + client: Client, + textUthmani: string, + verseKey?: string, +): Promise { + await client.callTool({ name: 'fetch_grounding_rules', arguments: {} }); + + const morphArgs: Record = {}; + if (isValidAyahKey(verseKey)) { + morphArgs.ayah_key = verseKey.trim(); + morphArgs.word_text = textUthmani; + } else { + morphArgs.word = textUthmani; + } + + const morphResult = await client.callTool({ + name: 'fetch_word_morphology', + arguments: morphArgs, + }); + + const payload = parseToolJsonPayload(morphResult) as { words?: MorphologyWord[] }; + const words = Array.isArray(payload.words) ? payload.words : []; + const picked = pickMorphologyWord(words, textUthmani); + if (!picked) throw new Error('Quran MCP returned no morphology for this word'); + + const paradigm = await runFetchWordParadigmOnClient(client, picked, textUthmani, verseKey); + + return { + base: morphologyWordToSyntaxResult(picked), + pickedWord: picked, + morphologyResponse: payload, + paradigm, + }; +} + +/** + * Calls `fetch_grounding_rules` then `fetch_word_morphology` on an initialized MCP client. + * @returns {Promise} Normalized result for the resolved word (no optional charts). + */ +export async function runFetchWordMorphologyOnClient( + client: Client, + textUthmani: string, + verseKey?: string, +): Promise { + const { base } = await runMcpSyntaxStudyOnClient(client, textUthmani, verseKey); + return base; +} diff --git a/src/lib/syntaxChartsFromMcp.ts b/src/lib/syntaxChartsFromMcp.ts new file mode 100644 index 0000000000..c3b6d2e921 --- /dev/null +++ b/src/lib/syntaxChartsFromMcp.ts @@ -0,0 +1,229 @@ +/* eslint-disable max-lines -- paradigm stem mapping is linear but verbose */ +/* eslint-disable max-lines-per-function, react-func/max-lines-per-function */ +/* eslint-disable no-restricted-syntax, no-continue, import/prefer-default-export */ +import type { MorphologyWord } from '@/lib/syntaxAnalysisQuranMcpMorphology'; +import type { SyntaxAnalysisVerbChart, SyntaxAnalysisVerbSlot } from 'types/SyntaxAnalysis'; + +type ParadigmStem = { stem: string; description: string }; + +function str(w: MorphologyWord, key: string): string | undefined { + const v = w[key]; + return typeof v === 'string' ? v : undefined; +} + +function asStemRows(raw: unknown): ParadigmStem[] { + if (!Array.isArray(raw)) return []; + return raw.flatMap((item) => { + if (!item || typeof item !== 'object') return []; + const o = item as Record; + const stem = typeof o.stem === 'string' ? o.stem.trim() : ''; + const description = typeof o.description === 'string' ? o.description.trim() : ''; + if (!stem) return []; + return [{ stem, description: description || stem }]; + }); +} + +type ParadigmPayload = { + perfect: ParadigmStem[]; + imperfect: ParadigmStem[]; + imperative: ParadigmStem[]; + lemma: string; + root: string; + gloss: string; +}; + +function firstLemmaGloss(payload: Record): string { + const cands = payload.candidate_lemmas; + if (!Array.isArray(cands) || !cands.length) return ''; + const first = cands[0]; + if (!first || typeof first !== 'object') return ''; + const g = (first as Record).gloss; + return typeof g === 'string' ? g.trim() : ''; +} + +function unwrapParadigmPayload(raw: unknown): ParadigmPayload | null { + if (!raw || typeof raw !== 'object') return null; + const o = raw as Record; + const inner = o.paradigm; + const bucket = + inner && typeof inner === 'object' && !Array.isArray(inner) + ? (inner as Record) + : o; + const perfect = asStemRows(bucket.perfect); + const imperfect = asStemRows(bucket.imperfect); + const imperative = asStemRows(bucket.imperative); + const lemma = typeof o.lemma === 'string' ? o.lemma.trim() : ''; + const root = typeof o.root === 'string' ? o.root.trim() : ''; + const gloss = firstLemmaGloss(o) || str(o as unknown as MorphologyWord, 'gloss')?.trim() || ''; + if (!perfect.length && !imperfect.length && !imperative.length) return null; + return { perfect, imperfect, imperative, lemma, root, gloss }; +} + +/** Maps English paradigm descriptions to our verb-grid slot coordinates. */ +type SlotCoord = { + chartKey: keyof SyntaxAnalysisVerbChart; + number: 'singular' | 'dual' | 'plural'; +}; + +function slotFromDescription(description: string): SlotCoord | null { + const d = description.toLowerCase(); + const has = (re: RegExp) => re.test(d); + + let person: '1' | '2' | '3' | null = null; + if (has(/\b1st\b/) || has(/\bfirst person\b/)) person = '1'; + else if (has(/\b2nd\b/) || has(/\bsecond person\b/)) person = '2'; + else if (has(/\b3rd\b/) || has(/\bthird person\b/)) person = '3'; + if (!person) return null; + + let number: 'singular' | 'dual' | 'plural' = 'singular'; + if (has(/\bdual\b/)) number = 'dual'; + else if (has(/\bplural\b/)) number = 'plural'; + + if (person === '1') { + const n: 'singular' | 'plural' = has(/\bplural\b/) ? 'plural' : 'singular'; + return { chartKey: '1stPerson', number: n }; + } + + const feminine = has(/\bfeminine\b/) && !has(/\bmasculine feminine\b/); + const chartKey = (() => { + if (person === '3') return feminine ? '3rdPersonFeminine' : '3rdPersonMasculine'; + if (person === '2') return feminine ? '2ndPersonFeminine' : '2ndPersonMasculine'; + return '3rdPersonMasculine'; + })(); + + return { chartKey, number }; +} + +function stemToSlot(row: ParadigmStem): SyntaxAnalysisVerbSlot { + return { + pronoun: '—', + meaning: row.description, + verb: row.stem, + }; +} + +function buildVerbChartFromStems(stems: ParadigmStem[]): SyntaxAnalysisVerbChart | undefined { + if (!stems.length) return undefined; + + const byCoord = new Map(); + const used = new Set(); + for (const row of stems) { + const coord = slotFromDescription(row.description); + if (!coord) continue; + const key = + coord.chartKey === '1stPerson' + ? `1stPerson:${coord.number}` + : `${coord.chartKey}:${coord.number}`; + if (!byCoord.has(key)) byCoord.set(key, row); + } + + const fallbackQueue = [...stems]; + const takeFallback = (): ParadigmStem => { + const next = fallbackQueue.find((s) => !used.has(s)) ?? stems[0]; + used.add(next); + return next; + }; + + const pick = ( + chartKey: keyof SyntaxAnalysisVerbChart, + number: 'singular' | 'dual' | 'plural', + ) => { + const mapKey = `${chartKey}:${number}`; + const hit = byCoord.get(mapKey); + if (hit) { + used.add(hit); + return stemToSlot(hit); + } + return stemToSlot(takeFallback()); + }; + + const chart: SyntaxAnalysisVerbChart = { + '3rdPersonMasculine': { + singular: pick('3rdPersonMasculine', 'singular'), + dual: pick('3rdPersonMasculine', 'dual'), + plural: pick('3rdPersonMasculine', 'plural'), + }, + '3rdPersonFeminine': { + singular: pick('3rdPersonFeminine', 'singular'), + dual: pick('3rdPersonFeminine', 'dual'), + plural: pick('3rdPersonFeminine', 'plural'), + }, + '2ndPersonMasculine': { + singular: pick('2ndPersonMasculine', 'singular'), + dual: pick('2ndPersonMasculine', 'dual'), + plural: pick('2ndPersonMasculine', 'plural'), + }, + '2ndPersonFeminine': { + singular: pick('2ndPersonFeminine', 'singular'), + dual: pick('2ndPersonFeminine', 'dual'), + plural: pick('2ndPersonFeminine', 'plural'), + }, + '1stPerson': { + singular: pick('1stPerson', 'singular'), + plural: pick('1stPerson', 'plural'), + }, + }; + + return chart; +} + +/** + * Builds optional chart payloads (same JSON keys as the former OpenAI chart pass) + * from Quran MCP morphology + `fetch_word_paradigm` stems. + * @returns {Record} Partial chart fields for `applyOptionalChartsToResult`. + */ +export function buildOptionalChartsFromMcp( + pickedWord: MorphologyWord, + paradigmRaw: unknown, +): Record { + const out: Record = {}; + const payload = unwrapParadigmPayload(paradigmRaw); + const surface = str(pickedWord, 'text_uthmani')?.trim() || ''; + const translation = str(pickedWord, 'translation')?.trim() || ''; + + if ( + payload && + (payload.perfect.length || payload.imperfect.length || payload.imperative.length) + ) { + const lemmaOrRoot = payload.lemma || payload.root || surface || '—'; + const glossBit = payload.gloss ? ` (${payload.gloss})` : ''; + const ideaCell = payload.lemma ? `${payload.lemma}${glossBit}` : lemmaOrRoot; + + const firstPerfect = payload.perfect[0]?.stem || '—'; + const firstImperfect = payload.imperfect[0]?.stem || '—'; + const firstImperative = payload.imperative[0]?.stem; + + out.sarfChart = { + columnHeaders: { + pastTense: 'Past — فعل ماضٍ', + presentTense: 'Present — فعل مضارع', + idea: 'Lemma — المصدر / الجذر', + doer: 'Gloss — معنى', + }, + activeVoice: { + pastTense: firstPerfect, + presentTense: firstImperfect, + idea: ideaCell, + doer: payload.gloss || translation || payload.root || '—', + }, + passiveVoiceLabels: {}, + passiveVoiceForms: {}, + commandingLabels: firstImperative + ? { pastTense: 'Imperative — صيغة أمر (عيّنة من القرآن)' } + : {}, + commandingForms: firstImperative + ? { + pastTense: firstImperative, + ...(payload.imperative[1]?.stem ? { presentTense: payload.imperative[1].stem } : {}), + } + : {}, + }; + + const past = buildVerbChartFromStems(payload.perfect); + const present = buildVerbChartFromStems(payload.imperfect); + if (past) out.verbPastTenseChart = past; + if (present) out.verbPresentTenseChart = present; + } + + return out; +} diff --git a/src/middleware.ts b/src/middleware.ts index f77dbc759a..f505da369e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from 'next/server'; export default function middleware(req: NextRequest) { // If the request is for _next/data, return a 404 response // This forces a full page reload when a new deployment is made - if (req.url.includes('_next/data')) { + if (process.env.NODE_ENV === 'development' && req.url.includes('_next/data')) { return new NextResponse(null, { status: 404 }); } diff --git a/src/pages/api/syntax/analyze.ts b/src/pages/api/syntax/analyze.ts new file mode 100644 index 0000000000..4fe5362627 --- /dev/null +++ b/src/pages/api/syntax/analyze.ts @@ -0,0 +1,46 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { fetchSyntaxAnalysisViaQuranMcp } from '@/lib/syntaxAnalysisQuranMcp'; +import type { SyntaxAnalysisResult } from 'types/SyntaxAnalysis'; + +type ErrorBody = { error: string }; + +const MAX_WORD_LENGTH = 200; + +/** + * POST `/api/syntax/analyze` — morphology + optional sarf/verb charts via + * [Quran MCP](https://mcp.quran.ai/documentation) (Streamable HTTP). + * @returns {Promise} JSON body: `SyntaxAnalysisResult` or `{ error }`. + */ +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const { textUthmani, verseKey } = req.body as { + textUthmani?: string; + verseKey?: string; + }; + + const text = typeof textUthmani === 'string' ? textUthmani.trim() : ''; + if (!text) { + return res.status(400).json({ error: 'Missing or empty textUthmani' }); + } + if (text.length > MAX_WORD_LENGTH) { + return res.status(400).json({ error: 'textUthmani too long' }); + } + + try { + const fromMcp = await fetchSyntaxAnalysisViaQuranMcp({ + textUthmani: text, + verseKey: typeof verseKey === 'string' ? verseKey : undefined, + }); + return res.status(200).json(fromMcp); + } catch (e) { + const message = e instanceof Error ? e.message : 'Quran MCP syntax analysis failed'; + return res.status(502).json({ error: message }); + } +} diff --git a/src/services/syntaxAnalysis.mock.ts b/src/services/syntaxAnalysis.mock.ts new file mode 100644 index 0000000000..936384b85b --- /dev/null +++ b/src/services/syntaxAnalysis.mock.ts @@ -0,0 +1,260 @@ +/* eslint-disable import/prefer-default-export */ +/* eslint-disable max-lines */ +import type { SyntaxAnalysisResult } from 'types/SyntaxAnalysis'; + +/** + * Paste a full `SyntaxAnalysisResult` JSON object here while + * `NEXT_PUBLIC_SYNTAX_ANALYSIS_MOCK=true` is set. No MCP/API call is made. + * + * Tip: paste from an API/tools response, keeping valid TypeScript/JSON shapes. + */ +export const SYNTAX_ANALYSIS_MOCK_RESPONSE: SyntaxAnalysisResult = { + rootLetter: { + arabicName: 'جذر تجريبي', + rootLetter: 'كتب', + }, + wordBreakDown: [ + { part: 'يَكتُبُ', meaning: '(example) writes' }, + { part: 'كتب', meaning: '(example) root k-t-b' }, + ], + pattern: { + wordPattern: '(example) 3rd person masculine singular imperfect verb', + patternType: 'فعل مضارع', + }, + ismChart: { + Masculine: { + Rafa: { + singular: 'مُسْلِمٌ', + dual: 'مُسْلِمَانِ', + plural: 'مُسْلِمُونَ', + }, + Nasab: { + singular: 'مُسْلِمًا', + dual: 'مُسْلِمَيْنِ', + plural: 'مُسْلِمِينَ', + }, + Jar: { + singular: 'مُسْلِمٍ', + dual: 'مُسْلِمَيْنِ', + plural: 'مُسْلِمِينَ', + }, + }, + Feminine: { + Rafa: { + singular: 'مُسْلِمَةٌ', + dual: 'مُسْلِمَتَانِ', + plural: 'مُسْلِمَاتٌ', + }, + Nasab: { + singular: 'مُسْلِمَةً', + dual: 'مُسْلِمَتَيْنِ', + plural: 'مُسْلِمَاتٍ', + }, + Jar: { + singular: 'مُسْلِمَةٍ', + dual: 'مُسْلِمَتَيْنِ', + plural: 'مُسْلِمَاتٍ', + }, + }, + }, + verbPresentTenseChart: { + '3rdPersonMasculine': { + singular: { + pronoun: 'هُوَ', + meaning: 'He helps', + verb: 'يَنْصُرُ', + }, + dual: { + pronoun: 'هُمَا', + meaning: 'They (2) help', + verb: 'يَنْصُرَانِ', + }, + plural: { + pronoun: 'هُمْ', + meaning: 'They help', + verb: 'يَنْصُرُونَ', + }, + }, + '3rdPersonFeminine': { + singular: { + pronoun: 'هِيَ', + meaning: 'She helps', + verb: 'تَنْصُرُ', + }, + dual: { + pronoun: 'هُمَا', + meaning: 'They (2f) help', + verb: 'تَنْصُرَانِ', + }, + plural: { + pronoun: 'هُنَّ', + meaning: 'They (f) help', + verb: 'يَنْصُرْنَ', + }, + }, + '2ndPersonMasculine': { + singular: { + pronoun: 'أَنْتَ', + meaning: 'You help', + verb: 'تَنْصُرُ', + }, + dual: { + pronoun: 'أَنْتُمَا', + meaning: 'You (2) help', + verb: 'تَنْصُرَانِ', + }, + plural: { + pronoun: 'أَنْتُمْ', + meaning: 'You all help', + verb: 'تَنْصُرُونَ', + }, + }, + '2ndPersonFeminine': { + singular: { + pronoun: 'أَنْتِ', + meaning: 'You (f) help', + verb: 'تَنْصُرِينَ', + }, + dual: { + pronoun: 'أَنْتُمَا', + meaning: 'You (2f) help', + verb: 'تَنْصُرَانِ', + }, + plural: { + pronoun: 'أَنْتُنَّ', + meaning: 'You all (f) help', + verb: 'تَنْصُرْنَ', + }, + }, + '1stPerson': { + singular: { + pronoun: 'أَنَا', + meaning: 'I help', + verb: 'أَنْصُرُ', + }, + plural: { + pronoun: 'نَحْنُ', + meaning: 'We help', + verb: 'نَنْصُرُ', + }, + }, + }, + verbPastTenseChart: { + '3rdPersonMasculine': { + singular: { + pronoun: 'هُوَ', + meaning: 'He helped', + verb: 'نَصَرَ', + }, + dual: { + pronoun: 'هُمَا', + meaning: 'They (2) helped', + verb: 'نَصَرَا', + }, + plural: { + pronoun: 'هُمْ', + meaning: 'They helped', + verb: 'نَصَرُوا', + }, + }, + '3rdPersonFeminine': { + singular: { + pronoun: 'هِيَ', + meaning: 'She helped', + verb: 'نَصَرَتْ', + }, + dual: { + pronoun: 'هُمَا', + meaning: 'They (2f) helped', + verb: 'نَصَرَتَا', + }, + plural: { + pronoun: 'هُنَّ', + meaning: 'They (f) helped', + verb: 'نَصَرْنَ', + }, + }, + '2ndPersonMasculine': { + singular: { + pronoun: 'أَنْتَ', + meaning: 'You helped', + verb: 'نَصَرْتَ', + }, + dual: { + pronoun: 'أَنْتُمَا', + meaning: 'You (2) helped', + verb: 'نَصَرْتُمَا', + }, + plural: { + pronoun: 'أَنْتُمْ', + meaning: 'You all helped', + verb: 'نَصَرْتُمْ', + }, + }, + '2ndPersonFeminine': { + singular: { + pronoun: 'أَنْتِ', + meaning: 'You (f) helped', + verb: 'نَصَرْتِ', + }, + dual: { + pronoun: 'أَنْتُمَا', + meaning: 'You (2f) helped', + verb: 'نَصَرْتُمَا', + }, + plural: { + pronoun: 'أَنْتُنَّ', + meaning: 'You all (f) helped', + verb: 'نَصَرْتُنَّ', + }, + }, + '1stPerson': { + singular: { + pronoun: 'أَنَا', + meaning: 'I helped', + verb: 'نَصَرْتُ', + }, + plural: { + pronoun: 'نَحْنُ', + meaning: 'We helped', + verb: 'نَصَرْنَا', + }, + }, + }, + sarfChart: { + columnHeaders: { + pastTense: 'PastTense - فعل ماضى', + presentTense: 'PresentTense - فعل مضارع', + idea: 'Idea - مصدر', + doer: 'Doer - اسم فاعل', + }, + activeVoice: { + pastTense: 'فَتَنَ', + presentTense: 'يَفْتِنُ', + idea: 'فِتْنَةً', + doer: 'فَاتِنٌ', + }, + passiveVoiceLabels: { + pastTense: 'Passive - فعل ماضى مبنى للمجهول', + presentTense: 'Passive - فعل مضارع مبنى للمجهول', + idea: 'مصدر', + doer: 'DoneTo - اسم مفعول', + }, + passiveVoiceForms: { + pastTense: 'فُتِنَ', + presentTense: 'يُفْتَنُ', + idea: 'فُتُونًا', + doer: 'مَفْتُونٌ', + }, + commandingLabels: { + pastTense: 'Commanding - أمر', + presentTense: 'Forbidding - نهى', + idea: 'TimeAndPlace - ظرف', + }, + commandingForms: { + pastTense: 'إِفْتِنْ', + presentTense: 'لَا تَفْتِنْ', + idea: 'مَفْتَنٌ | مَفْتِنٌ | مَفْتَنَةٌ', + }, + }, +}; diff --git a/src/services/syntaxAnalysisService.ts b/src/services/syntaxAnalysisService.ts new file mode 100644 index 0000000000..21ec52121b --- /dev/null +++ b/src/services/syntaxAnalysisService.ts @@ -0,0 +1,68 @@ +import { SYNTAX_ANALYSIS_MOCK_RESPONSE } from '@/services/syntaxAnalysis.mock'; +import type { SyntaxAnalysisResult } from 'types/SyntaxAnalysis'; + +export type SyntaxAnalysisRequest = { + /** Preferred: Uthmani text from the selected `Word` */ + textUthmani: string; + verseKey?: string; +}; + +export type SyntaxAnalysisErrorBody = { + error: string; +}; + +/** + * When true, `fetchSyntaxAnalysis` returns pasted mock data (see `syntaxAnalysis.mock.ts`). + * @returns {boolean} Whether client-side mock mode is enabled. + */ +export function isSyntaxAnalysisMockMode(): boolean { + return process.env.NEXT_PUBLIC_SYNTAX_ANALYSIS_MOCK === 'true'; +} + +/** + * Calls the Next.js API route `/api/syntax/analyze` (Quran MCP on the server), + * unless mock mode is on — then returns `SYNTAX_ANALYSIS_MOCK_RESPONSE` with no network call. + * + * Must run in the browser or any environment where `/api/syntax/analyze` is reachable (real mode only). + * @returns {Promise} Morphology result and optional charts. + */ +export async function fetchSyntaxAnalysis( + payload: SyntaxAnalysisRequest, +): Promise { + if (isSyntaxAnalysisMockMode()) { + await new Promise((r) => { + setTimeout(r, 200); + }); + return structuredClone(SYNTAX_ANALYSIS_MOCK_RESPONSE); + } + + const res = await fetch('/api/syntax/analyze', { + method: 'POST', + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header name + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + const body = (await res.json()) as SyntaxAnalysisResult | SyntaxAnalysisErrorBody; + + if (!res.ok || 'error' in body) { + throw new Error('error' in body ? body.error : `Syntax analysis failed (${res.status})`); + } + + return body; +} + +/** + * Resolves display text for syntax analysis from a Word-like object. + * `WordVerse` does not carry `textUthmani`; use fields on `Word` instead. + * @returns {string} Uthmani (or fallback) surface form for the token. + */ +export function getWordTextUthmaniForSyntax(word: { + textUthmani?: string; + qpcUthmaniHafs?: string; + text?: string; +}): string { + return (word.textUthmani || word.qpcUthmaniHafs || word.text || '').trim(); +} diff --git a/src/utils/url.ts b/src/utils/url.ts index 6f8ce23be1..48319a5257 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -9,6 +9,9 @@ export enum QuranFoundationService { QURAN_REFLECT = 'quran-reflect', } +const STAGING_CONTENT_HOST = 'https://staging.quran.com'; +// const PRODUCTION_CONTENT_HOST = 'https://api.qurancdn.com'; + export const getCurrentPath = () => { if (typeof window !== 'undefined') { return window.location.href; @@ -52,6 +55,22 @@ export const navigateToExternalUrl = (url: string) => { } }; +function resolveDeployHost(): string { + const fromEnv = process.env.NEXT_PUBLIC_VERCEL_URL?.trim(); + if (fromEnv) { + return fromEnv.replace(/^https?:\/\//, '').replace(/\/$/, ''); + } + const netlifyUrl = process.env.URL || process.env.DEPLOY_PRIME_URL; + if (netlifyUrl) { + try { + return new URL(netlifyUrl).host; + } catch { + // fall through + } + } + return process.env.NEXT_PUBLIC_VERCEL_ENV === 'development' ? 'localhost:3000' : 'quran.com'; +} + /** * Get the base path of the current deployment on Vercel/local machine * e.g. http://localhost @@ -59,15 +78,22 @@ export const navigateToExternalUrl = (url: string) => { * if we want to construct a full path e.g. when we add alternate languages * meta tags. * + * On Netlify, falls back to `URL` / `DEPLOY_PRIME_URL` when `NEXT_PUBLIC_VERCEL_URL` is unset. + * * @see https://vercel.com/docs/concepts/projects/environment-variables * @returns {string} */ -export const getBasePath = (): string => - `${process.env.NEXT_PUBLIC_VERCEL_ENV === 'development' ? 'http' : 'https'}://${ - process.env.NEXT_PUBLIC_VERCEL_URL - }`; +export const getBasePath = (): string => { + const isDev = process.env.NEXT_PUBLIC_VERCEL_ENV === 'development'; + return `${isDev ? 'http' : 'https'}://${resolveDeployHost()}`; +}; export const getProxiedServiceUrl = (service: QuranFoundationService, path: string): string => { + if (service === QuranFoundationService.CONTENT) { + const contentHost = STAGING_CONTENT_HOST; + return `${contentHost}${path}`; + } + const PROXY_PATH = `/api/proxy/${service}`; const BASE_PATH = isStaticBuild ? `${process.env.API_GATEWAY_URL}/${service}` diff --git a/tsconfig.json b/tsconfig.json index 2a2c73ec08..bd509ec8bb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,9 @@ "target": "es5", "incremental": true, "paths": { + "@modelcontextprotocol/sdk/*": [ + "./node_modules/@modelcontextprotocol/sdk/dist/esm/*" + ], "@/icons/*": ["./public/icons/*"], "@/public/*": ["./public/*"], "@/dls/*": ["./src/components/dls/*"], diff --git a/types/SyntaxAnalysis.ts b/types/SyntaxAnalysis.ts new file mode 100644 index 0000000000..90186e8015 --- /dev/null +++ b/types/SyntaxAnalysis.ts @@ -0,0 +1,96 @@ +/** + * Structured morphology output for an Arabic word (e.g. Uthmani script). + * Returned by `/api/syntax/analyze` and `syntaxAnalysisService`. + */ +/** Root metadata from the analyzer (same shape as /api/syntax/analyze required rootLetter). */ +export type SyntaxAnalysisRootLetter = { + /** Short Arabic label or gloss for the root */ + arabicName: string; + /** Lexical root consonants in Arabic script (often ثلاثي) */ + rootLetter: string; +}; + +export type SyntaxAnalysisBreakdownPart = { + part: string; + meaning: string; +}; + +/** Surface pattern — mirrors API: Arabic category + optional English morphological gloss. */ +export type SyntaxAnalysisPattern = { + /** Morphological description (often English: person, gender, number, form, etc.) */ + wordPattern: string; + /** Arabic grammatical category (e.g. فعل ماضي، اسم فاعل) */ + patternType: string; +}; + +/** One row of اسم declension (singular / dual / plural). */ +export type SyntaxAnalysisIsmCaseRow = { + singular: string; + dual: string; + plural: string; +}; + +/** Declension by grammatical case for one gender. */ +export type SyntaxAnalysisIsmGender = { + Rafa: SyntaxAnalysisIsmCaseRow; + Nasab: SyntaxAnalysisIsmCaseRow; + Jar: SyntaxAnalysisIsmCaseRow; +}; + +/** Full اسم chart (masculine / feminine). */ +export type SyntaxAnalysisIsmChart = { + Masculine: SyntaxAnalysisIsmGender; + Feminine: SyntaxAnalysisIsmGender; +}; + +export type SyntaxAnalysisVerbSlot = { + pronoun: string; + meaning: string; + verb: string; + /** Inflection segment at end of `verb`; rendered in accent color when it matches */ + verbSuffix?: string; +}; + +export type SyntaxAnalysisVerbPerson = { + singular?: SyntaxAnalysisVerbSlot; + dual?: SyntaxAnalysisVerbSlot; + plural?: SyntaxAnalysisVerbSlot; +}; + +/** Verb conjugation grid — same shape for past, present, etc. */ +export type SyntaxAnalysisVerbChart = { + '3rdPersonMasculine': SyntaxAnalysisVerbPerson; + '3rdPersonFeminine': SyntaxAnalysisVerbPerson; + '2ndPersonMasculine': SyntaxAnalysisVerbPerson; + '2ndPersonFeminine': SyntaxAnalysisVerbPerson; + '1stPerson': SyntaxAnalysisVerbPerson; +}; + +/** Four Sarf columns — DOM order with `dir="rtl"` on the table is Past → Present → Idea → Doer (reading RTL). */ +export type SyntaxAnalysisSarfColumnKey = 'pastTense' | 'presentTense' | 'idea' | 'doer'; + +/** + * Verb-derived morphology chart (مصدر، اسم فاعل، صيغ أمر/نهي، مجهول، ظرف، إلخ). + * Matches the Study Mode Sarf grid: header row, active forms, passive labels/forms, commanding labels/forms. + */ +export type SyntaxAnalysisSarfChart = { + columnHeaders: Record; + activeVoice: Record; + passiveVoiceLabels: Partial>; + passiveVoiceForms: Partial>; + commandingLabels: Partial>; + commandingForms: Partial>; +}; + +export type SyntaxAnalysisResult = { + rootLetter: SyntaxAnalysisRootLetter; + wordBreakDown: SyntaxAnalysisBreakdownPart[]; + pattern: SyntaxAnalysisPattern; + /** Optional extended charts (e.g. mocks) */ + ismChart?: SyntaxAnalysisIsmChart; + /** @deprecated Use verbPastTenseChart */ + verbChart?: SyntaxAnalysisVerbChart; + verbPresentTenseChart?: SyntaxAnalysisVerbChart; + verbPastTenseChart?: SyntaxAnalysisVerbChart; + sarfChart?: SyntaxAnalysisSarfChart; +}; diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000000..1f79b32d94 --- /dev/null +++ b/vercel.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "nextjs", + "buildCommand": "yarn build", + "installCommand": "yarn install" +} diff --git a/yarn.lock b/yarn.lock index d1af74e9a7..dab2294f17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3446,6 +3446,11 @@ protobufjs "^7.2.4" yargs "^17.7.2" +"@hono/node-server@^1.19.9": + version "1.19.14" + resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.14.tgz#e30f844bc77e3ce7be442aac3b1f73ad8b58d181" + integrity sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw== + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -3896,6 +3901,29 @@ nanoid "^5.0.0" tslib "^2.5.0" +"@modelcontextprotocol/sdk@^1.12.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz#79786d8b525e269de850ac82b1f1f757f3915f44" + integrity sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ== + dependencies: + "@hono/node-server" "^1.19.9" + ajv "^8.17.1" + ajv-formats "^3.0.1" + content-type "^1.0.5" + cors "^2.8.5" + cross-spawn "^7.0.5" + eventsource "^3.0.2" + eventsource-parser "^3.0.0" + express "^5.2.1" + express-rate-limit "^8.2.1" + hono "^4.11.4" + jose "^6.1.3" + json-schema-typed "^8.0.2" + pkce-challenge "^5.0.0" + raw-body "^3.0.0" + zod "^3.25 || ^4.0" + zod-to-json-schema "^3.25.1" + "@next/bundle-analyzer@^14.2.7": version "14.2.7" resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-14.2.7.tgz#c7b9595bf57d31dc4c50b91d9afdc21380d7eae6" @@ -7868,6 +7896,14 @@ abstract-leveldown@~0.12.0, abstract-leveldown@~0.12.1: dependencies: xtend "~3.0.0" +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -7963,6 +7999,13 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" +ajv-formats@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== + dependencies: + ajv "^8.0.0" + ajv-keywords@^3.1.0, ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" @@ -7995,6 +8038,16 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" +ajv@^8.17.1: + version "8.20.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.20.0.tgz#304b3636add88ba7d936760dd50ece006dea95f9" + integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ajv@^8.6.0: version "8.17.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" @@ -8469,6 +8522,21 @@ body-parser@1.20.2: type-is "~1.6.18" unpipe "1.0.0" +body-parser@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.2.tgz#1a32cdb966beaf68de50a9dfbe5b58f83cb8890c" + integrity sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.3" + http-errors "^2.0.0" + iconv-lite "^0.7.0" + on-finished "^2.4.1" + qs "^6.14.1" + raw-body "^3.0.1" + type-is "^2.0.1" + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -8675,7 +8743,7 @@ busboy@1.6.0: dependencies: streamsearch "^1.1.0" -bytes@3.1.2: +bytes@3.1.2, bytes@^3.1.2, bytes@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== @@ -8703,6 +8771,14 @@ cac@^6.7.14: resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -8714,6 +8790,14 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bin get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -9190,7 +9274,12 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" -content-type@~1.0.4, content-type@~1.0.5: +content-disposition@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.1.0.tgz#f3db789c752d45564cc7e9e1e0b31790d4a38e17" + integrity sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g== + +content-type@^1.0.5, content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== @@ -9210,6 +9299,11 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + cookie@0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" @@ -9220,6 +9314,11 @@ cookie@^0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" @@ -9242,6 +9341,14 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cors@^2.8.5: + version "2.8.6" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.6.tgz#ff5dd69bd95e547503820d29aba4f8faf8dfec96" + integrity sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw== + dependencies: + object-assign "^4" + vary "^1" + cosmiconfig@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -9320,6 +9427,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-browserify@^3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -9553,6 +9669,13 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.5, debug@~4.3.6: dependencies: ms "2.1.2" +debug@^4.4.0, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -9701,7 +9824,7 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -depd@2.0.0: +depd@2.0.0, depd@^2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -9896,6 +10019,15 @@ dotenv@^16.3.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.5.0.tgz#092b49f25f808f020050051d1ff258e404c78692" integrity sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg== +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -9961,6 +10093,11 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== +encodeurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -10171,6 +10308,11 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.0.0, es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -10229,6 +10371,13 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.0" +es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es-set-tostringtag@^2.0.2, es-set-tostringtag@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" @@ -10358,7 +10507,7 @@ escalade@^3.2.0: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== @@ -10751,7 +10900,7 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -etag@~1.8.1: +etag@^1.8.1, etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== @@ -10789,11 +10938,23 @@ events@3.3.0, events@^3.2.0, events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +eventsource-parser@^3.0.0, eventsource-parser@^3.0.1: + version "3.0.8" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.8.tgz#1c792503e4080455d00701bb1f7a1d60734d0e58" + integrity sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ== + eventsource@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508" integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA== +eventsource@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989" + integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA== + dependencies: + eventsource-parser "^3.0.1" + evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -10832,6 +10993,13 @@ execa@^8.0.1, execa@~8.0.1: signal-exit "^4.1.0" strip-final-newline "^3.0.0" +express-rate-limit@^8.2.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-8.5.1.tgz#ee62473d7b3bdf3b27b7be3d7f25c6d13308479a" + integrity sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ== + dependencies: + ip-address "^10.2.0" + express@^4.19.2: version "4.19.2" resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" @@ -10869,6 +11037,40 @@ express@^4.19.2: utils-merge "1.0.1" vary "~1.1.2" +express@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04" + integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.1" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + depd "^2.0.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + ext@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" @@ -11046,6 +11248,18 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +finalhandler@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.1.tgz#a2c517a6559852bcdb06d1f8bd7f51b68fad8099" + integrity sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA== + dependencies: + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" + find-cache-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" @@ -11244,6 +11458,11 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + from2@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" @@ -11375,6 +11594,22 @@ get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-it@^8.1: version "8.4.10" resolved "https://registry.yarnpkg.com/get-it/-/get-it-8.4.10.tgz#b10711bc20e88dbed77fbebd336c2d02480e946d" @@ -11401,6 +11636,14 @@ get-own-enumerable-property-symbols@^3.0.0: resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -11612,6 +11855,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -11671,6 +11919,11 @@ has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0, has-tostringtag@^1.0.1, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" @@ -11757,6 +12010,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: dependencies: react-is "^16.7.0" +hono@^4.11.4: + version "4.12.18" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.18.tgz#f6d301938868c3a8bdb639495f4e326a19181505" + integrity sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ== + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -11836,6 +12094,17 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + http-parser-js@>=0.5.1: version "0.5.8" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" @@ -11918,6 +12187,13 @@ iconv-lite@0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +iconv-lite@^0.7.0, iconv-lite@~0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.2.tgz#d0bdeac3f12b4835b7359c2ad89c422a4d1cc72e" + integrity sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" @@ -12082,6 +12358,11 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +ip-address@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.2.0.tgz#805fc178b20c518bd4c8548b24fe30892d7f3206" + integrity sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -12374,6 +12655,11 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + is-reference@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" @@ -12602,6 +12888,11 @@ jiti@^1.20.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== +jose@^6.1.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/jose/-/jose-6.2.3.tgz#0975197ad973251221c658a3cddc4b951a250c2d" + integrity sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw== + js-cookie@^3.0.1: version "3.0.5" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" @@ -12717,6 +13008,11 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== +json-schema-typed@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz#e98ee7b1899ff4a184534d1f167c288c66bbeff4" + integrity sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA== + json-schema@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" @@ -13237,6 +13533,11 @@ markdown-to-jsx@^7.4.5: resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.5.0.tgz#42ece0c71e842560a7d8bd9f81e7a34515c72150" integrity sha512-RrBNcMHiFPcz/iqIj0n3wclzHXjwS7mzjBNWecKKVhNTIxQepIix6Il/wZCn2Cg5Y1ow2Qi84+eJrryFRWBEWw== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + mathml-tag-names@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" @@ -13411,6 +13712,11 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + memfs@3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.3.tgz#fc08ac32363b6ea6c95381cabb4d67838180d4e1" @@ -13455,6 +13761,11 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -13790,6 +14101,11 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + mime-types@2.1.34: version "2.1.34" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" @@ -13804,6 +14120,13 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.24, dependencies: mime-db "1.52.0" +mime-types@^3.0.0, mime-types@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -13963,7 +14286,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -13993,6 +14316,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + neo-async@^2.5.0, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -14225,7 +14553,7 @@ nypm@^0.3.3: pathe "^1.1.2" ufo "^1.4.0" -object-assign@^4.0.1, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -14235,6 +14563,11 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== +object-inspect@^1.13.3, object-inspect@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + object-is@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" @@ -14338,7 +14671,7 @@ ohash@^1.1.3: resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.3.tgz#f12c3c50bfe7271ce3fd1097d42568122ccdcf07" integrity sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw== -on-finished@2.4.1: +on-finished@2.4.1, on-finished@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -14566,7 +14899,7 @@ parse5@^7.1.1: dependencies: entities "^4.4.0" -parseurl@~1.3.3: +parseurl@^1.3.3, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -14645,6 +14978,11 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@^8.0.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz#795c420c4f7ca45c5b887366f622ee0c9852cccd" + integrity sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -14782,6 +15120,11 @@ pirates@^4.0.6: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== +pkce-challenge@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz#3b4446865b17b1745e9ace2016a31f48ddf6230d" + integrity sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ== + pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -15259,7 +15602,7 @@ protobufjs@^7.2.4: "@types/node" ">=13.7.0" long "^5.0.0" -proxy-addr@~2.0.7: +proxy-addr@^2.0.7, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -15331,6 +15674,13 @@ qs@^6.11.0, qs@^6.11.2: dependencies: side-channel "^1.0.6" +qs@^6.14.0, qs@^6.14.1: + version "6.15.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.1.tgz#bdb55aed06bfac257a90c44a446a73fba5575c8f" + integrity sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg== + dependencies: + side-channel "^1.1.0" + querystring-es3@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -15393,6 +15743,16 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@^3.0.0, raw-body@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51" + integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.7.0" + unpipe "~1.0.0" + react-colorful@^5.1.2: version "5.6.1" resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" @@ -16145,6 +16505,17 @@ rope-sequence@^1.3.0: resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425" integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ== +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -16339,6 +16710,23 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +send@^1.1.0, send@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed" + integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== + dependencies: + debug "^4.4.3" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.1" + mime-types "^3.0.2" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.2" + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -16363,6 +16751,16 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" +serve-static@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.1.tgz#7f186a4a4e5f5b663ad7a4294ff1bf37cf0e98a9" + integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + set-function-length@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -16390,7 +16788,7 @@ setimmediate@^1.0.4: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== -setprototypeof@1.2.0: +setprototypeof@1.2.0, setprototypeof@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== @@ -16456,6 +16854,35 @@ shimmer@^1.2.1: resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== +side-channel-list@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.1.tgz#c2e0b5a14a540aebee3bbc6c3f8666cc9b509127" + integrity sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + side-channel@^1.0.4, side-channel@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" @@ -16466,6 +16893,17 @@ side-channel@^1.0.4, side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + siginfo@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" @@ -16679,6 +17117,11 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + std-env@^3.3.3, std-env@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" @@ -17346,7 +17789,7 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toidentifier@1.0.1: +toidentifier@1.0.1, toidentifier@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== @@ -17550,6 +17993,15 @@ type-fest@^4.1.0, type-fest@^4.8.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.9.0.tgz#d29c8efe5b1e703feeb29cef23d887b2f479844d" integrity sha512-KS/6lh/ynPGiHD/LnAobrEFq3Ad4pBzOlJ1wAnJx9N4EYoqFhMfLIBjUT2UEx4wg5ZE+cC1ob6DCSpppVo+rtg== +type-is@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== + dependencies: + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -17914,7 +18366,7 @@ validator@^13.7.0: resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== -vary@~1.1.2: +vary@^1, vary@^1.1.2, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== @@ -18703,11 +19155,21 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== +zod-to-json-schema@^3.25.1: + version "3.25.2" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz#3fa799a7badd554541472fb65843fdc460b2e5aa" + integrity sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA== + zod@3.22.3: version "3.22.3" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.3.tgz#2fbc96118b174290d94e8896371c95629e87a060" integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug== +"zod@^3.25 || ^4.0": + version "4.4.3" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356" + integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== + zwitch@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"