mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(reply modal): add sci tex preview button (#1170)
* feat(sci): add 4chan-style TeX support with MathJax on /sci/ - [math]/[eqn] tags typeset with MathJax 3 (lazy chunk, only on /sci/ with math present) - 4chan-identical config: Safe mode, left-aligned eqn, neutered \color/\newcommand macros - TeX button in reply modal title bar opens live TeX Preview modal - /sci/ post form rules bullets for [math]/[eqn] usage and right-click source - MathJax context menu on right-click (Show Math As > TeX Commands) - woff fonts served from node_modules in dev and emitted into build * feat(sci): finish TeX support: configmacros fix, preview preload, translations, tests - add configmacros package so the 4chan macro neutering (\color, \newcommand, ...) applies - preload MathJax when the TeX Preview opens, like 4chan - stable closeModal callback for the preview modal - pre-bundle mathjax components in vite optimizeDeps to avoid dev mid-session reload - translate the 6 new TeX keys into all 35 languages - markdown math segment component tests + math-tags unit tests * feat(reply modal): add sci tex preview button * fix(tex-preview): clear MathJax bookkeeping and pending typeset on close Addresses Cursor Bugbot: the preview output was typeset via typesetMathElement but never passed to clearMathElement on unmount, so repeated open/close cycles kept detached nodes in MathJax's math list. Also cancels the pending debounce timer. * fix(reply-modal): reset TeX preview state when the reply modal closes Addresses CodeRabbit: showTexPreview persisted across close/reopen like the bbcode preview flags, so the TeX preview would auto-open on the next reply.
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
"i18next-http-backend": "3.0.5",
|
||||
"json-stringify-pretty-compact": "4.0.0",
|
||||
"lodash": "4.18.0",
|
||||
"mathjax-full": "3.2.2",
|
||||
"memoizee": "0.4.15",
|
||||
"react": "19.2.7",
|
||||
"react-ace": "14.0.1",
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "هذا المستخدم يستخدم Tor",
|
||||
"copy_share_link": "نسخ رابط المشاركة",
|
||||
"directory_submit_board": "أرسل لوحتك",
|
||||
"all_subtitle": "المنتدى الأعلى تصنيفاً من كل دليل"
|
||||
"all_subtitle": "المنتدى الأعلى تصنيفاً من كل دليل",
|
||||
"tex_preview_protip": "استخدم وسوم [math][/math] للمعادلات المضمّنة، و [eqn][/eqn] للمعادلات الكتلية.",
|
||||
"post_form_math_tags_prompt": "استخدم <tex>TeX</tex> مع علامات [math] للمعادلات المضمنة وعلامات [eqn] للمعادلات الكاملة.",
|
||||
"post_form_math_right_click_prompt": "انقر بزر الماوس الأيمن على المعادلات لعرض المصدر.",
|
||||
"tex_preview_title": "<tex>TeX</tex> معاينة",
|
||||
"tex_preview_input_label": "TeX مصدر",
|
||||
"preview_tex_equations": "معاينة معادلات TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "এই ব্যবহারকারী Tor ব্যবহার করছেন",
|
||||
"copy_share_link": "শেয়ার লিংক কপি করুন",
|
||||
"directory_submit_board": "আপনার বোর্ড জমা দিন",
|
||||
"all_subtitle": "প্রতিটি ডিরেক্টরি থেকে সর্বোচ্চ স্কোর করা বোর্ড"
|
||||
"all_subtitle": "প্রতিটি ডিরেক্টরি থেকে সর্বোচ্চ স্কোর করা বোর্ড",
|
||||
"tex_preview_protip": "ইনলাইন সমীকরণের জন্য [math][/math] ট্যাগ ব্যবহার করুন, এবং ব্লক সমীকরণের জন্য [eqn][/eqn] ট্যাগ ব্যবহার করুন।",
|
||||
"post_form_math_tags_prompt": "<tex>TeX</tex> ব্যবহার করুন ইনলাইন সমীকরণের জন্য [math] ট্যাগ এবং ব্লক সমীকরণের জন্য [eqn] ট্যাগ সহ।",
|
||||
"post_form_math_right_click_prompt": "সমীকরণ দেখতে ডান-ক্লিক করুন।",
|
||||
"tex_preview_title": "<tex>TeX</tex> প্রিভিউ",
|
||||
"tex_preview_input_label": "TeX উৎস",
|
||||
"preview_tex_equations": "TeX সমীকরণগুলির পূর্বরূপ দেখুন"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Tento uživatel je na síti Tor",
|
||||
"copy_share_link": "Kopírovat odkaz pro sdílení",
|
||||
"directory_submit_board": "Odeslat svou nástěnku",
|
||||
"all_subtitle": "Fórum s nejvyšším skóre z každého adresáře"
|
||||
"all_subtitle": "Fórum s nejvyšším skóre z každého adresáře",
|
||||
"tex_preview_protip": "Použijte značky [math][/math] pro vložené rovnice a značky [eqn][/eqn] pro blokové rovnice.",
|
||||
"post_form_math_tags_prompt": "Použijte <tex>TeX</tex> s tagy [math] pro vložené rovnice a tagy [eqn] pro blokové rovnice.",
|
||||
"post_form_math_right_click_prompt": "Kliknutím pravým tlačítkem na rovnice zobrazíte zdroj.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Náhled",
|
||||
"tex_preview_input_label": "TeX zdroj",
|
||||
"preview_tex_equations": "Náhled TeX rovnic"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Denne bruger er på Tor",
|
||||
"copy_share_link": "Kopier delingslinkk",
|
||||
"directory_submit_board": "Indsend dit board",
|
||||
"all_subtitle": "Forummet med højeste pointtal fra hvert katalog"
|
||||
"all_subtitle": "Forummet med højeste pointtal fra hvert katalog",
|
||||
"tex_preview_protip": "Brug [math][/math]-tags til indlejrede ligninger og [eqn][/eqn]-tags til blokligninger.",
|
||||
"post_form_math_tags_prompt": "Brug <tex>TeX</tex> med [math] tags til inline ligninger og [eqn] tags til blok ligninger.",
|
||||
"post_form_math_right_click_prompt": "Højreklik på ligninger for at se kilden.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Forhåndsvisning",
|
||||
"tex_preview_input_label": "TeX kilde",
|
||||
"preview_tex_equations": "Forhåndsvisning af TeX-ligninger"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Dieser Benutzer ist auf Tor",
|
||||
"copy_share_link": "Freigabelink kopieren",
|
||||
"directory_submit_board": "Reiche dein Board ein",
|
||||
"all_subtitle": "Das höchstbewertete Forum aus jedem Verzeichnis"
|
||||
"all_subtitle": "Das höchstbewertete Forum aus jedem Verzeichnis",
|
||||
"tex_preview_protip": "Verwenden Sie [math][/math]-Tags für Inline-Gleichungen und [eqn][/eqn]-Tags für Block-Gleichungen.",
|
||||
"post_form_math_tags_prompt": "Nutze <tex>TeX</tex> mit [math]-Tags für Inline-Gleichungen und [eqn]-Tags für Block-Gleichungen.",
|
||||
"post_form_math_right_click_prompt": "Rechtsklick auf Gleichungen zum Anzeigen der Quelle.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Vorschau",
|
||||
"tex_preview_input_label": "TeX-Quelle",
|
||||
"preview_tex_equations": "TeX-Gleichungen in der Vorschau anzeigen"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Αυτός ο χρήστης είναι στο Tor",
|
||||
"copy_share_link": "Αντιγραφή ενός συνδέσμου",
|
||||
"directory_submit_board": "Υπόβαλε το board σου",
|
||||
"all_subtitle": "Το Φόρουμ με την υψηλότερη βαθμολογία από κάθε κατάλογο"
|
||||
"all_subtitle": "Το Φόρουμ με την υψηλότερη βαθμολογία από κάθε κατάλογο",
|
||||
"tex_preview_protip": "Χρησιμοποιήστε [math][/math] ετικέτες για εξισώσεις σε γραμμή και [eqn][/eqn] ετικέτες για εξισώσεις μπλοκ.",
|
||||
"post_form_math_tags_prompt": "Χρησιμοποιήστε <tex>TeX</tex> με ετικέτες [math] για εξισώσεις ενδιάμεσου κειμένου και ετικέτες [eqn] για εξισώσεις σε μπλοκ.",
|
||||
"post_form_math_right_click_prompt": "Κάντε δεξί κλικ στις εξισώσεις για να δείτε την πηγή.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Προεπισκόπηση",
|
||||
"tex_preview_input_label": "TeX πηγή",
|
||||
"preview_tex_equations": "Προεπισκόπηση εξισώσεων TeX"
|
||||
}
|
||||
|
||||
@@ -426,5 +426,11 @@
|
||||
"tor_author_flag_label": "This user is on Tor",
|
||||
"copy_share_link": "Copy share link",
|
||||
"directory_submit_board": "Submit Your Board",
|
||||
"all_subtitle": "The highest-scoring board from each directory"
|
||||
"all_subtitle": "The highest-scoring board from each directory",
|
||||
"post_form_math_tags_prompt": "Use <tex>TeX</tex> with [math] tags for inline and [eqn] tags for block equations.",
|
||||
"post_form_math_right_click_prompt": "Right-click equations to view the source.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Preview",
|
||||
"tex_preview_protip": "Use [math][/math] tags for inline, and [eqn][/eqn] tags for block equations.",
|
||||
"tex_preview_input_label": "TeX source",
|
||||
"preview_tex_equations": "Preview TeX equations"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Este usuario está en Tor",
|
||||
"copy_share_link": "Copiar enlace compartido",
|
||||
"directory_submit_board": "Envía tu board",
|
||||
"all_subtitle": "El foro de mayor puntuación de cada directorio"
|
||||
"all_subtitle": "El foro de mayor puntuación de cada directorio",
|
||||
"tex_preview_protip": "Usa etiquetas [math][/math] para ecuaciones en línea y etiquetas [eqn][/eqn] para ecuaciones en bloque.",
|
||||
"post_form_math_tags_prompt": "Usa <tex>TeX</tex> con etiquetas [math] para ecuaciones en línea y etiquetas [eqn] para ecuaciones en bloque.",
|
||||
"post_form_math_right_click_prompt": "Haz clic derecho en las ecuaciones para ver la fuente.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Vista Previa",
|
||||
"tex_preview_input_label": "TeX fuente",
|
||||
"preview_tex_equations": "Previsualizar ecuaciones TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "این کاربر روی Tor است",
|
||||
"copy_share_link": "کپی کردن لینک اشتراک",
|
||||
"directory_submit_board": "برد خود را ارسال کنید",
|
||||
"all_subtitle": "تالار بالاترین امتیاز از هر دایرکتوری"
|
||||
"all_subtitle": "تالار بالاترین امتیاز از هر دایرکتوری",
|
||||
"tex_preview_protip": "برای معادلات درونخطی از برچسبهای [math][/math] و برای معادلات بلوکی از برچسبهای [eqn][/eqn] استفاده کنید.",
|
||||
"post_form_math_tags_prompt": "از <tex>TeX</tex> با برچسبهای [math] برای معادلات درونمتنی و برچسبهای [eqn] برای معادلات بلاکی استفاده کنید.",
|
||||
"post_form_math_right_click_prompt": "برای مشاهده منبع، بر روی معادلات کلیک راست کنید.",
|
||||
"tex_preview_title": "<tex>TeX</tex> پیشنمایش",
|
||||
"tex_preview_input_label": "TeX منبع",
|
||||
"preview_tex_equations": "پیش نمایش معادلات TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Tämä käyttäjä on Torissa",
|
||||
"copy_share_link": "Kopioi jakolinkkii",
|
||||
"directory_submit_board": "Lähetä boardisi",
|
||||
"all_subtitle": "Korkeimman pistemäärän saanut foorumi jokaisesta hakemistosta"
|
||||
"all_subtitle": "Korkeimman pistemäärän saanut foorumi jokaisesta hakemistosta",
|
||||
"tex_preview_protip": "Käytä [math][/math]-tunnisteita sisäisille yhtälöille ja [eqn][/eqn]-tunnisteita lohkoyhtälöille.",
|
||||
"post_form_math_tags_prompt": "Käytä <tex>TeX</tex>-ohjelmaa [math]-tageilla sisäisille yhtälöille ja [eqn]-tageilla lohkoyhtälöille.",
|
||||
"post_form_math_right_click_prompt": "Napsauta yhtälöitä hiiren oikealla painikkeella nähdäksesi lähteen.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Esikatselu",
|
||||
"tex_preview_input_label": "TeX lähde",
|
||||
"preview_tex_equations": "Esikatsele TeX-yhtälöitä"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Ang user na ito ay nasa Tor",
|
||||
"copy_share_link": "Kopyahin ang link sa pagbabahagi",
|
||||
"directory_submit_board": "Isumite ang board mo",
|
||||
"all_subtitle": "Ang pinakamataas na nakakuha ng puntos na Board mula sa bawat direktoryo"
|
||||
"all_subtitle": "Ang pinakamataas na nakakuha ng puntos na Board mula sa bawat direktoryo",
|
||||
"tex_preview_protip": "Gamitin ang [math][/math] tags para sa inline equations, at [eqn][/eqn] tags para sa block equations.",
|
||||
"post_form_math_tags_prompt": "Gumamit ng <tex>TeX</tex> na may [math] tags para sa inline at [eqn] tags para sa block equations.",
|
||||
"post_form_math_right_click_prompt": "I-right-click ang mga equation upang makita ang source.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Pagpapakita",
|
||||
"tex_preview_input_label": "TeX pinagmulan",
|
||||
"preview_tex_equations": "Magpakita ng preview ng TeX equations"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Cet utilisateur est sur Tor",
|
||||
"copy_share_link": "Copier le lien de partage",
|
||||
"directory_submit_board": "Soumettez votre board",
|
||||
"all_subtitle": "Le forum de plus haute note de chaque répertoire"
|
||||
"all_subtitle": "Le forum de plus haute note de chaque répertoire",
|
||||
"tex_preview_protip": "Utilisez les balises [math][/math] pour les équations en ligne et les balises [eqn][/eqn] pour les équations en bloc.",
|
||||
"post_form_math_tags_prompt": "Utilisez <tex>TeX</tex> avec les balises [math] pour les équations en ligne et les balises [eqn] pour les équations de bloc.",
|
||||
"post_form_math_right_click_prompt": "Cliquez avec le bouton droit sur les équations pour voir la source.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Aperçu",
|
||||
"tex_preview_input_label": "TeX source",
|
||||
"preview_tex_equations": "Aperçu des équations TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "המשתמש הזה נמצא ב-Tor",
|
||||
"copy_share_link": "העתק קישור שיתוף",
|
||||
"directory_submit_board": "שלח את הבורד שלך",
|
||||
"all_subtitle": "הפורום בעל הציון הגבוה ביותר מכל ספריה"
|
||||
"all_subtitle": "הפורום בעל הציון הגבוה ביותר מכל ספריה",
|
||||
"tex_preview_protip": "השתמש בתגיות [math][/math] לתוך-שורה, והשתמש בתגיות [eqn][/eqn] לתוך-בלוק.",
|
||||
"post_form_math_tags_prompt": "השתמשו ב-<tex>TeX</tex> עם תגי [math] למשוואות בשורה ותגי [eqn] למשוואות בבלוק.",
|
||||
"post_form_math_right_click_prompt": "לחץ לחיצה ימנית על משוואות כדי לצפות בקוד.",
|
||||
"tex_preview_title": "<tex>TeX</tex> תצוגה מקדימה",
|
||||
"tex_preview_input_label": "TeX מקור",
|
||||
"preview_tex_equations": "תצוגה מקדימה של משוואות TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "यह उपयोगकर्ता Tor पर है",
|
||||
"copy_share_link": "शेयर लिंक कॉपी करें",
|
||||
"directory_submit_board": "अपना बोर्ड सबमिट करें",
|
||||
"all_subtitle": "हर डायरेक्टरी से सबसे अधिक स्कोर किया गया फोरम"
|
||||
"all_subtitle": "हर डायरेक्टरी से सबसे अधिक स्कोर किया गया फोरम",
|
||||
"tex_preview_protip": "[math][/math] टैग को इनलाइन समीकरणों के लिए और [eqn][/eqn] टैग को ब्लॉक समीकरणों के लिए उपयोग करें।",
|
||||
"post_form_math_tags_prompt": "<tex>TeX</tex> का उपयोग करें [math] टैग के साथ इनलाइन समीकरणों के लिए और [eqn] टैग के साथ ब्लॉक समीकरणों के लिए।",
|
||||
"post_form_math_right_click_prompt": "समीकरणों को देखने के लिए दाईं ओर क्लिक करें।",
|
||||
"tex_preview_title": "<tex>TeX</tex> पूर्वावलोकन",
|
||||
"tex_preview_input_label": "TeX स्रोत",
|
||||
"preview_tex_equations": "TeX समीकरणों का पूर्वावलोकन करें"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Ez a felhasználó a Tor hálózatán van",
|
||||
"copy_share_link": "Megosztási hivatkozás másolása",
|
||||
"directory_submit_board": "Küldd be a boardodat",
|
||||
"all_subtitle": "A legmagasabb pontszámú Fórum minden könyvtárból"
|
||||
"all_subtitle": "A legmagasabb pontszámú Fórum minden könyvtárból",
|
||||
"tex_preview_protip": "Használja a [math][/math] címkéket beágyazott egyenletekhez és a [eqn][/eqn] címkéket blokk egyenletekhez.",
|
||||
"post_form_math_tags_prompt": "Használjon <tex>TeX</tex>-et [math] címkékkel az inline egyenletekhez és [eqn] címkékkel a blokk egyenletekhez.",
|
||||
"post_form_math_right_click_prompt": "Kattintson a jobb gombbal az egyenletekre a forrás megtekintéséhez.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Előnézet",
|
||||
"tex_preview_input_label": "TeX forrás",
|
||||
"preview_tex_equations": "TeX-egyenletek előnézete"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Pengguna ini ada di Tor",
|
||||
"copy_share_link": "Salin tautan bagian",
|
||||
"directory_submit_board": "Kirim board Anda",
|
||||
"all_subtitle": "Forum dengan skor tertinggi dari setiap direktori"
|
||||
"all_subtitle": "Forum dengan skor tertinggi dari setiap direktori",
|
||||
"tex_preview_protip": "Gunakan tag [math][/math] untuk persamaan sebaris dan tag [eqn][/eqn] untuk persamaan blok.",
|
||||
"post_form_math_tags_prompt": "Gunakan <tex>TeX</tex> dengan tag [math] untuk persamaan sebaris dan tag [eqn] untuk persamaan blok.",
|
||||
"post_form_math_right_click_prompt": "Klik kanan pada persamaan untuk melihat sumbernya.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Pratinjau",
|
||||
"tex_preview_input_label": "TeX sumber",
|
||||
"preview_tex_equations": "Pratinjau persamaan TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Questo utente è su Tor",
|
||||
"copy_share_link": "Copia link condiviso",
|
||||
"directory_submit_board": "Invia la tua board",
|
||||
"all_subtitle": "Il board con il punteggio più alto da ogni directory"
|
||||
"all_subtitle": "Il board con il punteggio più alto da ogni directory",
|
||||
"tex_preview_protip": "Usa i tag [math][/math] per le equazioni in linea e i tag [eqn][/eqn] per le equazioni di blocco.",
|
||||
"post_form_math_tags_prompt": "Usa <tex>TeX</tex> con tag [math] per le equazioni in linea e tag [eqn] per le equazioni in blocco.",
|
||||
"post_form_math_right_click_prompt": "Fai clic destro sulle equazioni per visualizzare il codice sorgente.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Anteprima",
|
||||
"tex_preview_input_label": "TeX sorgente",
|
||||
"preview_tex_equations": "Anteprima delle equazioni TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "このユーザーはTorを使用しています",
|
||||
"copy_share_link": "シェアリンクをコピー",
|
||||
"directory_submit_board": "掲示板を提出する",
|
||||
"all_subtitle": "各ディレクトリから最もスコアの高い掲示板"
|
||||
"all_subtitle": "各ディレクトリから最もスコアの高い掲示板",
|
||||
"tex_preview_protip": "[math][/math] タグはインライン数式に、[eqn][/eqn] タグはブロック数式に使用してください。",
|
||||
"post_form_math_tags_prompt": "<tex>TeX</tex>をインライン方程式には[math]タグ、ブロック方程式には[eqn]タグと一緒に使用してください。",
|
||||
"post_form_math_right_click_prompt": "方程式を右クリックしてソースを表示します。",
|
||||
"tex_preview_title": "<tex>TeX</tex> プレビュー",
|
||||
"tex_preview_input_label": "TeX ソース",
|
||||
"preview_tex_equations": "TeX方程式のプレビュー"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "이 사용자는 Tor를 사용 중입니다",
|
||||
"copy_share_link": "공유 링크 복사",
|
||||
"directory_submit_board": "게시판 제출하기",
|
||||
"all_subtitle": "각 디렉토리의 가장 높은 점수 게시판"
|
||||
"all_subtitle": "각 디렉토리의 가장 높은 점수 게시판",
|
||||
"tex_preview_protip": "[math][/math] 태그를 인라인 방정식에, [eqn][/eqn] 태그를 블록 방정식에 사용하세요.",
|
||||
"post_form_math_tags_prompt": "<tex>TeX</tex>를 인라인 방정식에는 [math] 태그와, 블록 방정식에는 [eqn] 태그와 함께 사용하세요.",
|
||||
"post_form_math_right_click_prompt": "우클릭하여 방정식 소스를 봅니다.",
|
||||
"tex_preview_title": "<tex>TeX</tex> 미리보기",
|
||||
"tex_preview_input_label": "TeX 소스",
|
||||
"preview_tex_equations": "TeX 방정식 미리보기"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "हा वापरकर्ता Tor वर आहे",
|
||||
"copy_share_link": "शेअर लिंक कॉपी करा",
|
||||
"directory_submit_board": "तुमचा बोर्ड सबमिट करा",
|
||||
"all_subtitle": "प्रत्येक डायरेक्टरीमधील सर्वोच्च स्कोर केलेले फोरम"
|
||||
"all_subtitle": "प्रत्येक डायरेक्टरीमधील सर्वोच्च स्कोर केलेले फोरम",
|
||||
"tex_preview_protip": "इनलाइन समीकरणांसाठी [math][/math] टॅग्ज आणि ब्लॉक समीकरणांसाठी [eqn][/eqn] टॅग्ज वापरा.",
|
||||
"post_form_math_tags_prompt": "<tex>TeX</tex> चा वापर [math] टॅग्ज सह इनलाइन समीकरणांसाठी आणि [eqn] टॅग्ज सह ब्लॉक समीकरणांसाठी करा.",
|
||||
"post_form_math_right_click_prompt": "समीकरणांवर उजव्या क्लिकने स्रोत पाहा.",
|
||||
"tex_preview_title": "<tex>TeX</tex> पूर्वावलोकन",
|
||||
"tex_preview_input_label": "TeX स्रोत",
|
||||
"preview_tex_equations": "TeX समीकरणांचे पूर्वावलोकन"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Deze gebruiker zit op Tor",
|
||||
"copy_share_link": "Delen link kopiëren",
|
||||
"directory_submit_board": "Dien je board in",
|
||||
"all_subtitle": "Het Forum met de hoogste score van elke directory"
|
||||
"all_subtitle": "Het Forum met de hoogste score van elke directory",
|
||||
"tex_preview_protip": "Gebruik [math][/math]-tags voor inline-vergelijkingen en [eqn][/eqn]-tags voor blok-vergelijkingen.",
|
||||
"post_form_math_tags_prompt": "Gebruik <tex>TeX</tex> met [math] tags voor inline vergelijkingen en [eqn] tags voor blok vergelijkingen.",
|
||||
"post_form_math_right_click_prompt": "Klik met de rechtermuisknop op vergelijkingen om de bron te zien.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Voorbeeld",
|
||||
"tex_preview_input_label": "TeX bron",
|
||||
"preview_tex_equations": "Voorbeeld van TeX-vergelijkingen"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Denne brukeren er på Tor",
|
||||
"copy_share_link": "Kopier delingslenke",
|
||||
"directory_submit_board": "Send inn boardet ditt",
|
||||
"all_subtitle": "Forummet med høyest poengsum fra hver katalog"
|
||||
"all_subtitle": "Forummet med høyest poengsum fra hver katalog",
|
||||
"tex_preview_protip": "Bruk [math][/math]-tagger for innebygde ligninger og [eqn][/eqn]-tagger for blokkligninger.",
|
||||
"post_form_math_tags_prompt": "Bruk <tex>TeX</tex> med [math] tags for inline ligninger og [eqn] tags for blokk ligninger.",
|
||||
"post_form_math_right_click_prompt": "Høyreklikk på ligninger for å se kilden.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Forhåndsvisning",
|
||||
"tex_preview_input_label": "TeX kilde",
|
||||
"preview_tex_equations": "Forhåndsvisning av TeX-ligninger"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Ten użytkownik jest w sieci Tor",
|
||||
"copy_share_link": "Kopiuj link udostępniania",
|
||||
"directory_submit_board": "Prześlij swój board",
|
||||
"all_subtitle": "Forum o najwyższej punktacji z każdego katalogu"
|
||||
"all_subtitle": "Forum o najwyższej punktacji z każdego katalogu",
|
||||
"tex_preview_protip": "Użyj tagów [math][/math] dla równań wbudowanych i tagów [eqn][/eqn] dla równań blokowych.",
|
||||
"post_form_math_tags_prompt": "Użyj <tex>TeX</tex> ze znacznikami [math] dla równań wbudowanych i znacznikami [eqn] dla równań blokowych.",
|
||||
"post_form_math_right_click_prompt": "Kliknij prawym przyciskiem myszy na równania, aby wyświetlić kod źródłowy.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Podgląd",
|
||||
"tex_preview_input_label": "TeX źródło",
|
||||
"preview_tex_equations": "Podgląd równań TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Este usuário está no Tor",
|
||||
"copy_share_link": "Copiar link compartilhado",
|
||||
"directory_submit_board": "Envie seu board",
|
||||
"all_subtitle": "O Fórum de mais alta pontuação de cada diretório"
|
||||
"all_subtitle": "O Fórum de mais alta pontuação de cada diretório",
|
||||
"tex_preview_protip": "Use tags [math][/math] para equações inline e tags [eqn][/eqn] para equações em bloco.",
|
||||
"post_form_math_tags_prompt": "Use <tex>TeX</tex> com tags [math] para equações inline e tags [eqn] para equações em bloco.",
|
||||
"post_form_math_right_click_prompt": "Clique com o botão direito nas equações para ver o código-fonte.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Visualização",
|
||||
"tex_preview_input_label": "TeX fonte",
|
||||
"preview_tex_equations": "Visualizar equações TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Acest utilizator este pe Tor",
|
||||
"copy_share_link": "Copiezi link-ul de partajare",
|
||||
"directory_submit_board": "Trimite boardul tău",
|
||||
"all_subtitle": "Forumul cu cea mai mare punctaj din fiecare director"
|
||||
"all_subtitle": "Forumul cu cea mai mare punctaj din fiecare director",
|
||||
"tex_preview_protip": "Utilizează etichetele [math][/math] pentru ecuații inline și etichetele [eqn][/eqn] pentru ecuații bloc.",
|
||||
"post_form_math_tags_prompt": "Utilizați <tex>TeX</tex> cu taguri [math] pentru ecuații inline și taguri [eqn] pentru ecuații în bloc.",
|
||||
"post_form_math_right_click_prompt": "Faceți clic dreapta pe ecuații pentru a vedea sursa.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Previzualizare",
|
||||
"tex_preview_input_label": "TeX sursă",
|
||||
"preview_tex_equations": "Previzualizare ecuații TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Этот пользователь находится в Tor",
|
||||
"copy_share_link": "Копировать ссылку для общего доступа",
|
||||
"directory_submit_board": "Отправить свою доску",
|
||||
"all_subtitle": "Форум с наивысшим рейтингом из каждого каталога"
|
||||
"all_subtitle": "Форум с наивысшим рейтингом из каждого каталога",
|
||||
"tex_preview_protip": "Используйте теги [math][/math] для встроенных уравнений и теги [eqn][/eqn] для блочных уравнений.",
|
||||
"post_form_math_tags_prompt": "Используйте <tex>TeX</tex> с тегами [math] для встроенных уравнений и [eqn] для блочных уравнений.",
|
||||
"post_form_math_right_click_prompt": "Щелкните правой кнопкой мыши на уравнениях, чтобы просмотреть исходный код.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Предпросмотр",
|
||||
"tex_preview_input_label": "TeX источник",
|
||||
"preview_tex_equations": "Предпросмотр уравнений TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Ky përdorues është në Tor",
|
||||
"copy_share_link": "Kopjo lidhjen e ndarjes",
|
||||
"directory_submit_board": "Dërgo bordin tënd",
|
||||
"all_subtitle": "Forumi me pikëzimin më të lartë nga secila drejtori"
|
||||
"all_subtitle": "Forumi me pikëzimin më të lartë nga secila drejtori",
|
||||
"tex_preview_protip": "Përdorni etiketat [math][/math] për ekuacione inline dhe etiketat [eqn][/eqn] për ekuacione bloku.",
|
||||
"post_form_math_tags_prompt": "Përdorni <tex>TeX</tex> me etiketat [math] për ekuacionet inline dhe etiketat [eqn] për ekuacionet e bllokut.",
|
||||
"post_form_math_right_click_prompt": "Kliko me butonin e djathtë në ekuacione për të parë burimin.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Pamja paraprake",
|
||||
"tex_preview_input_label": "TeX burim",
|
||||
"preview_tex_equations": "Paraqitja paraprakese e ekuacioneve TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Den här användaren är på Tor",
|
||||
"copy_share_link": "Kopiera delningslänk",
|
||||
"directory_submit_board": "Skicka in ditt board",
|
||||
"all_subtitle": "Forummet med högst poäng från varje katalog"
|
||||
"all_subtitle": "Forummet med högst poäng från varje katalog",
|
||||
"tex_preview_protip": "Använd [math][/math]-taggar för inline-ekvationer och [eqn][/eqn]-taggar för block-ekvationer.",
|
||||
"post_form_math_tags_prompt": "Använd <tex>TeX</tex> med [math] tags för inline ekvationer och [eqn] tags för block ekvationer.",
|
||||
"post_form_math_right_click_prompt": "Högerklicka på ekvationer för att se källan.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Förhandsvisning",
|
||||
"tex_preview_input_label": "TeX källa",
|
||||
"preview_tex_equations": "Förhandsvisning av TeX-ekvationer"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "ఈ వినియోగదారు Tor లో ఉన్నారు",
|
||||
"copy_share_link": "భాగస్వామ్య లింక్ను కాపీ చేయండి",
|
||||
"directory_submit_board": "మీ బోర్డును సమర్పించండి",
|
||||
"all_subtitle": "ప్రతిదిరెక్టరీ నుండి అత్యధిక స్కోర్ చేసిన ఫోరం"
|
||||
"all_subtitle": "ప్రతిదిరెక్టరీ నుండి అత్యధిక స్కోర్ చేసిన ఫోరం",
|
||||
"tex_preview_protip": "ఇన్లైన్ సమీకరణాల కోసం [math][/math] ట్యాగ్లను ఉపయోగించండి, మరియు బ్లాక్ సమీకరణాల కోసం [eqn][/eqn] ట్యాగ్లను ఉపయోగించండి.",
|
||||
"post_form_math_tags_prompt": "[math] ట్యాగ్లతో ఇన్లైన్ సమీకరణాల కోసం మరియు [eqn] ట్యాగ్లతో బ్లాక్ సమీకరణాల కోసం <tex>TeX</tex>ని ఉపయోగించండి.",
|
||||
"post_form_math_right_click_prompt": "సమీకరణాలపై కుడి-క్లిక్ చేసి మూలాన్ని చూడండి.",
|
||||
"tex_preview_title": "<tex>TeX</tex> ప్రీవ్యూ",
|
||||
"tex_preview_input_label": "TeX మూలం",
|
||||
"preview_tex_equations": "TeX సమీకరణల ప్రిన్ట్కు"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "ผู้ใช้นี้อยู่บน Tor",
|
||||
"copy_share_link": "คัดลอกลิงก์แชร์",
|
||||
"directory_submit_board": "ส่งบอร์ดของคุณ",
|
||||
"all_subtitle": "กระดานที่ได้คะแนนสูงสุดจากแต่ละไดเรกทอรี่"
|
||||
"all_subtitle": "กระดานที่ได้คะแนนสูงสุดจากแต่ละไดเรกทอรี่",
|
||||
"tex_preview_protip": "ใช้แท็ก [math][/math] สำหรับสมการแบบอินไลน์ และใช้แท็ก [eqn][/eqn] สำหรับสมการแบบบล็อก",
|
||||
"post_form_math_tags_prompt": "ใช้ <tex>TeX</tex> พร้อมกับแท็ก [math] สำหรับสมการแบบอินไลน์ และแท็ก [eqn] สำหรับสมการแบบบล็อก",
|
||||
"post_form_math_right_click_prompt": "คลิกขวาที่สมการเพื่อดูแหล่งที่มา",
|
||||
"tex_preview_title": "<tex>TeX</tex> ตัวอย่าง",
|
||||
"tex_preview_input_label": "TeX แหล่งที่มา",
|
||||
"preview_tex_equations": "ตัวอย่างสมการ TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Bu kullanıcı Tor'da",
|
||||
"copy_share_link": "Paylaşma bağlantısını kopyala",
|
||||
"directory_submit_board": "Panonu Gönder",
|
||||
"all_subtitle": "Her dizinden en yüksek skorlu Forum"
|
||||
"all_subtitle": "Her dizinden en yüksek skorlu Forum",
|
||||
"tex_preview_protip": "[math][/math] etiketlerini satır içi denklemler için ve [eqn][/eqn] etiketlerini blok denklemler için kullanın.",
|
||||
"post_form_math_tags_prompt": "İnline denklemler için [math] etiketleriyle ve blok denklemler için [eqn] etiketleriyle <tex>TeX</tex> kullanın.",
|
||||
"post_form_math_right_click_prompt": "Denklemlere sağ tıklayarak kaynağını görüntüleyin.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Ön İzleme",
|
||||
"tex_preview_input_label": "TeX kaynak",
|
||||
"preview_tex_equations": "TeX denklemlerini ön izle"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Цей користувач у Tor",
|
||||
"copy_share_link": "Копіювати посилання для спільного доступу",
|
||||
"directory_submit_board": "Надіслати свою дошку",
|
||||
"all_subtitle": "Форум з найвищим рейтингом з кожного каталогу"
|
||||
"all_subtitle": "Форум з найвищим рейтингом з кожного каталогу",
|
||||
"tex_preview_protip": "Використовуйте теги [math][/math] для вбудованих рівнянь та теги [eqn][/eqn] для блокових рівнянь.",
|
||||
"post_form_math_tags_prompt": "Використовуйте <tex>TeX</tex> з тегами [math] для вбудованих рівнянь та [eqn] для блокових рівнянь.",
|
||||
"post_form_math_right_click_prompt": "Клацніть правою кнопкою миші на рівняння, щоб переглянути вихідний код.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Попередній перегляд",
|
||||
"tex_preview_input_label": "TeX джерело",
|
||||
"preview_tex_equations": "Попередній перегляд рівнянь TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "یہ صارف Tor پر ہے",
|
||||
"copy_share_link": "شیئر لنک کاپی کریں",
|
||||
"directory_submit_board": "اپنا بورڈ جمع کرائیں",
|
||||
"all_subtitle": "ہر ڈائریکٹری سے سب سے زیادہ سکور پیغام بورڈ"
|
||||
"all_subtitle": "ہر ڈائریکٹری سے سب سے زیادہ سکور پیغام بورڈ",
|
||||
"tex_preview_protip": "انلائن مساوات کے لیے [math][/math] ٹیگز اور بلاک مساوات کے لیے [eqn][/eqn] ٹیگز استعمال کریں۔",
|
||||
"post_form_math_tags_prompt": "<tex>TeX</tex> کو [math] ٹیگز کے ساتھ انلائن مساوات کے لیے اور [eqn] ٹیگز کے ساتھ بلاک مساوات کے لیے استعمال کریں۔",
|
||||
"post_form_math_right_click_prompt": "مساوات پر دائیں کلک کریں تاکہ منبع دیکھ سکیں۔",
|
||||
"tex_preview_title": "<tex>TeX</tex> پیش نمائش",
|
||||
"tex_preview_input_label": "TeX منبع",
|
||||
"preview_tex_equations": "TeX مساوات کا پیش نمائش دیکھیں"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "Người dùng này đang ở trên Tor",
|
||||
"copy_share_link": "Sao chép liên kết chia sẻ",
|
||||
"directory_submit_board": "Gửi bảng của bạn",
|
||||
"all_subtitle": "Diễn đàn có điểm cao nhất từ mỗi thư mục"
|
||||
"all_subtitle": "Diễn đàn có điểm cao nhất từ mỗi thư mục",
|
||||
"tex_preview_protip": "Sử dụng thẻ [math][/math] cho các phương trình nội dòng và thẻ [eqn][/eqn] cho các phương trình khối.",
|
||||
"post_form_math_tags_prompt": "Sử dụng <tex>TeX</tex> với thẻ [math] cho phương trình nội dòng và thẻ [eqn] cho phương trình khối.",
|
||||
"post_form_math_right_click_prompt": "Nhấp chuột phải vào các phương trình để xem nguồn.",
|
||||
"tex_preview_title": "<tex>TeX</tex> Xem Trước",
|
||||
"tex_preview_input_label": "TeX nguồn",
|
||||
"preview_tex_equations": "Xem trước các phương trình TeX"
|
||||
}
|
||||
|
||||
@@ -407,5 +407,11 @@
|
||||
"tor_author_flag_label": "该用户正在使用 Tor",
|
||||
"copy_share_link": "复制分享链接",
|
||||
"directory_submit_board": "提交你的版块",
|
||||
"all_subtitle": "来自每个目录的最高评分论坛"
|
||||
"all_subtitle": "来自每个目录的最高评分论坛",
|
||||
"tex_preview_protip": "对于行内方程使用 [math][/math] 标签,对于块级方程使用 [eqn][/eqn] 标签。",
|
||||
"post_form_math_tags_prompt": "在<tex>TeX</tex>中使用[math]标签表示行内公式,使用[eqn]标签表示块级公式。",
|
||||
"post_form_math_right_click_prompt": "右键单击方程式查看源代码。",
|
||||
"tex_preview_title": "<tex>TeX</tex> 预览",
|
||||
"tex_preview_input_label": "TeX 源代码",
|
||||
"preview_tex_equations": "预览 TeX 方程"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ const testState = vi.hoisted(() => ({
|
||||
name?: string;
|
||||
title?: string;
|
||||
}>,
|
||||
typesetCalls: [] as string[],
|
||||
embeddableHosts: new Set<string>(),
|
||||
cidToNumber: {} as Record<string, number>,
|
||||
internalPathByHref: {} as Record<string, string | null>,
|
||||
@@ -89,6 +90,15 @@ vi.mock('../../../hooks/use-is-mobile', () => ({
|
||||
default: () => testState.isMobile,
|
||||
}));
|
||||
|
||||
vi.mock('../../../lib/mathjax/mathjax-typeset', () => ({
|
||||
typesetMathElement: (_element: HTMLElement, source: string) => {
|
||||
testState.typesetCalls.push(source);
|
||||
return Promise.resolve();
|
||||
},
|
||||
clearMathElement: () => undefined,
|
||||
preloadMathJax: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock('../../../lib/utils/media-utils', () => ({
|
||||
getHasThumbnail: (linkMediaInfo?: { patternThumbnailUrl?: string; thumbnail?: string; type?: string }, href?: string) =>
|
||||
Boolean(
|
||||
@@ -236,6 +246,7 @@ describe('Markdown', () => {
|
||||
vi.clearAllMocks();
|
||||
testState.comments = {};
|
||||
testState.directories = [{ address: 'music-posting.eth', name: 'music-posting.bso', title: '/mu/ - Music' }];
|
||||
testState.typesetCalls = [];
|
||||
testState.embeddableHosts = new Set<string>();
|
||||
testState.cidToNumber = {};
|
||||
testState.internalPathByHref = {};
|
||||
@@ -685,4 +696,57 @@ describe('Markdown', () => {
|
||||
|
||||
expect(container.querySelector('[data-testid="comment-media"]')).toBeNull();
|
||||
});
|
||||
|
||||
describe('math tags on /sci/', () => {
|
||||
const useSciDirectory = () => {
|
||||
testState.directories = [
|
||||
{ address: 'music-posting.eth', name: 'music-posting.bso', title: '/mu/ - Music' },
|
||||
{ address: 'science-and-math.eth', directoryCode: 'sci', name: 'science-and-math.bso', title: '/sci/ - Science & Math' },
|
||||
];
|
||||
};
|
||||
|
||||
it('hands [math] and [eqn] segments to MathJax on /sci/ routes, keeping the delimiters visible', async () => {
|
||||
useSciDirectory();
|
||||
|
||||
await renderMarkdown({ content: 'inline [math]x_1[/math] then\n[eqn]\\int x\\\\dx[/eqn] end' }, '/sci/thread/post-1');
|
||||
|
||||
expect(container.textContent).toContain('[math]x_1[/math]');
|
||||
expect(container.textContent).toContain('[eqn]\\int x\\\\dx[/eqn]');
|
||||
expect(testState.typesetCalls).toEqual(['[math]x_1[/math]', '[eqn]\\int x\\\\dx[/eqn]']);
|
||||
});
|
||||
|
||||
it('keeps multi-line math in a single segment instead of splitting on line breaks', async () => {
|
||||
useSciDirectory();
|
||||
|
||||
await renderMarkdown({ content: '[math]a\\\\\nb[/math]' }, '/sci/thread/post-1');
|
||||
|
||||
expect(testState.typesetCalls).toEqual(['[math]a\\\\\nb[/math]']);
|
||||
});
|
||||
|
||||
it('does not typeset math on non-math boards', async () => {
|
||||
useSciDirectory();
|
||||
|
||||
await renderMarkdown({ content: '[math]x_1[/math]' }, '/mu/thread/post-1');
|
||||
|
||||
expect(container.textContent).toContain('[math]x_1[/math]');
|
||||
expect(testState.typesetCalls).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not typeset math in the catalog, like 4chan teasers', async () => {
|
||||
useSciDirectory();
|
||||
|
||||
await renderMarkdown({ content: '[math]x_1[/math]' }, '/sci/catalog');
|
||||
|
||||
expect(container.textContent).toContain('[math]x_1[/math]');
|
||||
expect(testState.typesetCalls).toEqual([]);
|
||||
});
|
||||
|
||||
it('typesets math for /sci/ posts rendered outside /sci/ routes via their community address', async () => {
|
||||
useSciDirectory();
|
||||
|
||||
await renderMarkdown({ content: '[math]x_1[/math]', communityAddress: 'science-and-math.bso' }, '/all');
|
||||
|
||||
expect(testState.typesetCalls).toEqual(['[math]x_1[/math]']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { isCatalogView } from '../../lib/utils/view-utils';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import CommentMedia from '../comment-media/comment-media';
|
||||
import CodeBlock from '../code-block/code-block';
|
||||
import TexMath from '../tex-math/tex-math';
|
||||
import styles from './markdown.module.css';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { canEmbed } from '../embed/embed-utils';
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
getMatchingFortuneEntry,
|
||||
isFortuneDirectoryCode,
|
||||
} from '../../lib/utils/post-options-utils';
|
||||
import { HAS_MATH_TAG_REGEX, isMathDirectoryCode, splitMathSegments } from '../../lib/math-tags';
|
||||
|
||||
const safeParseUrl = (href: string): URL | null => {
|
||||
try {
|
||||
@@ -812,11 +814,32 @@ const Markdown = ({ content, title, postCid, communityAddress, parseSpoilers = t
|
||||
const enableCodeTags =
|
||||
getDirectoryCodeForIdentifier(getRouteBoardIdentifier(location.pathname), directories) === CODE_DIRECTORY_CODE ||
|
||||
getDirectoryCodeForIdentifier(communityAddress, directories) === CODE_DIRECTORY_CODE;
|
||||
// [math]/[eqn] TeX tags are a /sci/ feature (like 4chan). The catalog is excluded, matching
|
||||
// 4chan, where teasers show the raw tags and MathJax only runs on board and thread pages.
|
||||
const enableMathTags =
|
||||
!isInCatalogView &&
|
||||
(isMathDirectoryCode(getDirectoryCodeForIdentifier(getRouteBoardIdentifier(location.pathname), directories)) ||
|
||||
isMathDirectoryCode(getDirectoryCodeForIdentifier(communityAddress, directories)));
|
||||
|
||||
const rendered = useMemo(() => {
|
||||
const context = { isInCatalogView, postCid, communityAddress, enableFortuneMarkup, enableQstBbcode, parseSpoilers };
|
||||
const raw = content || '';
|
||||
|
||||
if (enableMathTags && HAS_MATH_TAG_REGEX.test(raw)) {
|
||||
const elements: React.ReactNode[] = [];
|
||||
splitMathSegments(raw).forEach((segment) => {
|
||||
if (segment.type === 'math') {
|
||||
elements.push(<TexMath key={`math-${segment.start}`} source={segment.value} />);
|
||||
return;
|
||||
}
|
||||
if (!segment.value) return;
|
||||
elements.push(
|
||||
<React.Fragment key={`text-${segment.start}`}>{renderTextLines(normalizeContent(segment.value), context, `${segment.start}:`)}</React.Fragment>,
|
||||
);
|
||||
});
|
||||
return elements;
|
||||
}
|
||||
|
||||
if (!enableCodeTags || !HAS_CODE_TAG_REGEX.test(raw)) {
|
||||
return renderTextLines(normalizeContent(raw), context, '');
|
||||
}
|
||||
@@ -832,7 +855,7 @@ const Markdown = ({ content, title, postCid, communityAddress, parseSpoilers = t
|
||||
});
|
||||
|
||||
return elements;
|
||||
}, [content, isInCatalogView, postCid, communityAddress, enableFortuneMarkup, enableQstBbcode, parseSpoilers, enableCodeTags]);
|
||||
}, [content, isInCatalogView, postCid, communityAddress, enableFortuneMarkup, enableQstBbcode, parseSpoilers, enableCodeTags, enableMathTags]);
|
||||
|
||||
return (
|
||||
<span className={styles.markdown}>
|
||||
|
||||
@@ -34,6 +34,7 @@ import { getBoardPath, isDirectoryRoute } from '../../lib/utils/route-utils';
|
||||
import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsForDirectory, type CommentFlagSelectOption } from '../../lib/comment-flag-selection';
|
||||
import { FLASH_TAG_OPTIONS, getFlashTagPublishOptionsForDirectoryCode, isFlashDirectoryCode, type FlashTagOption } from '../../lib/flash-tags';
|
||||
import { isMathDirectoryCode } from '../../lib/math-tags';
|
||||
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
|
||||
import { useDirectories } from '../../hooks/use-directories';
|
||||
import { useDirectoryEntry } from '../../hooks/use-directory-entry';
|
||||
@@ -54,6 +55,7 @@ import BoardOfflineAlert from '../board-offline-alert/board-offline-alert';
|
||||
import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar';
|
||||
import LoadingEllipsis from '../loading-ellipsis/loading-ellipsis';
|
||||
import OekakiDrawingControls from '../oekaki-drawing-controls/oekaki-drawing-controls';
|
||||
import TexLogo from '../tex-logo/tex-logo';
|
||||
import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message';
|
||||
import styles from './post-form.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
@@ -181,6 +183,7 @@ interface PostFormFieldsProps {
|
||||
flashTagOptions: FlashTagOption[];
|
||||
showFlashTagSelector: boolean;
|
||||
showFlashUploadPrompt: boolean;
|
||||
showMathTagsPrompt: boolean;
|
||||
showBbcodeToolbar: boolean;
|
||||
onBbcodePreviewToggle: () => void;
|
||||
onPublishReply: () => void;
|
||||
@@ -233,6 +236,7 @@ const PostFormFields = ({
|
||||
flashTagOptions,
|
||||
showFlashTagSelector,
|
||||
showFlashUploadPrompt,
|
||||
showMathTagsPrompt,
|
||||
showBbcodeToolbar,
|
||||
onBbcodePreviewToggle,
|
||||
onPublishReply,
|
||||
@@ -495,6 +499,19 @@ const PostFormFields = ({
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
{showMathTagsPrompt && (
|
||||
<>
|
||||
<li>
|
||||
<Trans
|
||||
i18nKey='post_form_math_tags_prompt'
|
||||
components={{
|
||||
tex: <TexLogo />,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>{t('post_form_math_right_click_prompt')}</li>
|
||||
</>
|
||||
)}
|
||||
{showOekakiControls && isWebRuntime() ? <li>{OEKAKI_WEB_WARNING_TEXT}</li> : null}
|
||||
</ul>
|
||||
</td>
|
||||
@@ -557,6 +574,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry);
|
||||
const showFlashUploadPrompt = isFlashDirectoryCode(postOptionsDirectoryCode);
|
||||
const showFlashTagSelector = showFlashUploadPrompt && !isInPostView;
|
||||
const showMathTagsPrompt = isMathDirectoryCode(postOptionsDirectoryCode) || isMathDirectoryCode(directoryEntry?.directoryCode);
|
||||
|
||||
const accountCommunityAddresses = useAccountCommunityAddresses();
|
||||
const accountAddress = account?.author?.address;
|
||||
@@ -940,6 +958,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
flashTagOptions={FLASH_TAG_OPTIONS}
|
||||
showFlashTagSelector={showFlashTagSelector}
|
||||
showFlashUploadPrompt={showFlashUploadPrompt}
|
||||
showMathTagsPrompt={showMathTagsPrompt}
|
||||
showBbcodeToolbar={showBbcodeToolbar}
|
||||
onBbcodePreviewToggle={handleBbcodePreviewToggle}
|
||||
onPublishReply={onPublishReply}
|
||||
|
||||
@@ -227,6 +227,12 @@ vi.mock('../../loading-ellipsis/loading-ellipsis', () => ({
|
||||
default: ({ string }: { string: string }) => createElement('span', { 'data-testid': 'loading-ellipsis' }, string),
|
||||
}));
|
||||
|
||||
vi.mock('../../../lib/mathjax/mathjax-typeset', () => ({
|
||||
clearMathElement: () => undefined,
|
||||
preloadMathJax: () => undefined,
|
||||
typesetMathElement: () => Promise.resolve(),
|
||||
}));
|
||||
|
||||
vi.mock('lodash/debounce', () => ({
|
||||
default: <T extends (...args: any[]) => void>(fn: T, wait = 0) => {
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
@@ -514,6 +520,30 @@ describe('ReplyModal', () => {
|
||||
expect(container.textContent).not.toContain(OEKAKI_WEB_WARNING_TEXT);
|
||||
});
|
||||
|
||||
it('opens the TeX preview modal from the /sci/ reply modal button', async () => {
|
||||
testState.directoryByAddress['science-and-math.bso'] = {
|
||||
address: 'science-and-math.bso',
|
||||
directoryCode: 'sci',
|
||||
features: {},
|
||||
title: '/sci/ - Science & Math',
|
||||
};
|
||||
testState.communities['science-and-math.bso'] = { address: 'science-and-math.bso' };
|
||||
|
||||
await renderReplyModal('/sci/thread/post-1', 'science-and-math.bso');
|
||||
|
||||
const texButton = container.querySelector<HTMLButtonElement>('button[aria-label="preview_tex_equations"]');
|
||||
expect(texButton?.textContent).toBe('TEX');
|
||||
expect(texButton?.querySelector('sub')?.textContent).toBe('E');
|
||||
|
||||
await act(async () => {
|
||||
texButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
const texPreview = container.querySelector('dialog[aria-labelledby="tex-preview-title"]');
|
||||
expect(texPreview).toBeTruthy();
|
||||
expect(texPreview?.textContent).toContain('tex_preview_title');
|
||||
});
|
||||
|
||||
it('shows a flag selector on flag boards and publishes the default geographic request', async () => {
|
||||
await renderReplyModal('/pol/thread/post-1', 'politically-incorrect.bso');
|
||||
|
||||
|
||||
@@ -39,6 +39,41 @@
|
||||
background-image: var(--close-button-background-image);
|
||||
}
|
||||
|
||||
.texButtonTooltip {
|
||||
float: left;
|
||||
display: block;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
margin: 0 0 0 3px;
|
||||
}
|
||||
|
||||
.texButton {
|
||||
all: unset;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
color: var(--button-desktop-text-color);
|
||||
font-size: 10.6667px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.texButton:hover,
|
||||
.texButton:focus-visible {
|
||||
color: var(--button-desktop-text-color-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.texButton:focus-visible {
|
||||
outline: 1px dotted currentColor;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.texButton sub {
|
||||
font-size: 80%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.container input[type="text"], .container textarea, .container select:not(.flagSelector) {
|
||||
border: var(--reply-modal-field-input-border, revert);
|
||||
font-family: var(--post-form-field-font-family, revert);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { TFunction } from 'i18next';
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
isPostOptionsValidationError,
|
||||
} from '../../lib/utils/post-options-utils';
|
||||
import { isValidPublishURL } from '../../lib/utils/url-utils';
|
||||
import { isMathDirectoryCode } from '../../lib/math-tags';
|
||||
import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
|
||||
import { getModerationPostingRoleLabel } from '../../lib/utils/author-display-utils';
|
||||
import { isAllView, isModView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
@@ -39,6 +40,10 @@ import BoardOfflineAlert from '../board-offline-alert/board-offline-alert';
|
||||
import LoadingEllipsis from '../loading-ellipsis/loading-ellipsis';
|
||||
import OekakiDrawingControls from '../oekaki-drawing-controls/oekaki-drawing-controls';
|
||||
import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message';
|
||||
import TexLogo from '../tex-logo/tex-logo';
|
||||
import TexPreviewModal from '../tex-preview-modal/tex-preview-modal';
|
||||
import Tooltip from '../tooltip/tooltip';
|
||||
import { preloadMathJax } from '../../lib/mathjax/mathjax-typeset';
|
||||
import styles from './reply-modal.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import debounce from 'lodash/debounce';
|
||||
@@ -71,6 +76,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true;
|
||||
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
|
||||
const showOekakiControls = postOptionsDirectoryCode === 'i' || directoryEntry?.directoryCode === 'i';
|
||||
const showTexButton = isMathDirectoryCode(postOptionsDirectoryCode) || isMathDirectoryCode(directoryEntry?.directoryCode);
|
||||
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
|
||||
const requirePostLinkIsMedia = requirePostLinkIsMediaFeature === true || (requirePostLinkIsMediaFeature === undefined && (isInAllView || isInSubscriptionsView));
|
||||
const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry);
|
||||
@@ -120,6 +126,8 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const [url, setUrl] = useState('');
|
||||
const [isBbcodePreviewing, setIsBbcodePreviewing] = useState(false);
|
||||
const [bbcodePreviewContent, setBbcodePreviewContent] = useState('');
|
||||
const [showTexPreview, setShowTexPreview] = useState(false);
|
||||
const closeTexPreview = useCallback(() => setShowTexPreview(false), []);
|
||||
|
||||
const checkContentLengthRef = useRef(
|
||||
debounce((content: string, t: TFunction, options: string, directoryCode: string | undefined) => {
|
||||
@@ -349,6 +357,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
checkPostOptionsRef.current.cancel();
|
||||
setIsBbcodePreviewing(false);
|
||||
setBbcodePreviewContent('');
|
||||
setShowTexPreview(false);
|
||||
}
|
||||
}, [showReplyModal]);
|
||||
|
||||
@@ -538,6 +547,22 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
}}
|
||||
>
|
||||
<div id='reply-modal-title' className={`replyModalHandle ${styles.title}`} {...(!isMobile ? bind() : {})}>
|
||||
{showTexButton && !isMobile && (
|
||||
<Tooltip content={t('preview_tex_equations')} className={styles.texButtonTooltip}>
|
||||
<button
|
||||
type='button'
|
||||
className={styles.texButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
preloadMathJax();
|
||||
setShowTexPreview(true);
|
||||
}}
|
||||
aria-label={t('preview_tex_equations')}
|
||||
>
|
||||
<TexLogo />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{t('reply_to_no', { no: threadNumber ?? '?' })}
|
||||
<button
|
||||
type='button'
|
||||
@@ -688,7 +713,14 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
</animated.div>
|
||||
);
|
||||
|
||||
return showReplyModal && modalContent;
|
||||
return (
|
||||
showReplyModal && (
|
||||
<>
|
||||
{modalContent}
|
||||
{showTexPreview && <TexPreviewModal closeModal={closeTexPreview} />}
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default ReplyModal;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// The classic "TeX" wordmark with a subscripted "E", used by the /sci/ post form rules, the
|
||||
// reply modal's TeX button and the TeX Preview modal title.
|
||||
const TexLogo = ({ className }: { className?: string }) => (
|
||||
<span className={className}>
|
||||
T<sub>E</sub>X
|
||||
</span>
|
||||
);
|
||||
|
||||
export default TexLogo;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { clearMathElement, typesetMathElement } from '../../lib/mathjax/mathjax-typeset';
|
||||
|
||||
// Renders a [math]/[eqn] segment. The raw TeX source (delimiters included) stays visible until
|
||||
// MathJax lazy-loads and typesets it in place, like 4chan. MathJax owns this span's subtree after
|
||||
// typesetting, so React must never re-render children inside it: the source text is rendered once
|
||||
// and the effect below (a legit external-DOM-library sync, not data fetching) hands the node over.
|
||||
const TexMath = ({ source }: { source: string }) => {
|
||||
const elementRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
typesetMathElement(element, source);
|
||||
return () => clearMathElement(element);
|
||||
}, [source]);
|
||||
|
||||
return <span ref={elementRef}>{source}</span>;
|
||||
};
|
||||
|
||||
export default memo(TexMath);
|
||||
@@ -0,0 +1,106 @@
|
||||
/* Layout mirrors 4chan's TeX Preview panel: fullscreen dimmed overlay, 600px centered panel,
|
||||
bold bordered header, hint line, 75px monospace input and a live preview area below it. */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.overlayButton {
|
||||
all: unset;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
cursor: default;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: 600px;
|
||||
max-width: calc(100% - 20px);
|
||||
max-height: calc(100vh - 20px);
|
||||
overflow: auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
margin: 0;
|
||||
padding: 2px;
|
||||
background-color: var(--challenge-modal-background-color, rgb(240, 224, 214));
|
||||
border: var(--challenge-modal-border, 1px solid rgb(217, 191, 183));
|
||||
color: var(--body-font-color, rgb(0, 0, 0));
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
margin: 0 0 5px;
|
||||
padding: 5px 18px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.titleText {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.texLogo {
|
||||
font-size: 80%;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.texLogo sub {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
all: unset;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
image-rendering: pixelated;
|
||||
background-image: var(--close-button-background-image);
|
||||
}
|
||||
|
||||
.protip {
|
||||
font-size: 11px;
|
||||
margin: 5px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
height: 75px;
|
||||
margin: 0 0 5px;
|
||||
padding: 2px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
border: 1px solid #aaa;
|
||||
outline: none;
|
||||
background-color: var(--post-form-field-input-background-color, rgb(255, 255, 255));
|
||||
color: var(--post-form-field-input-color, rgb(0, 0, 0));
|
||||
}
|
||||
|
||||
.output {
|
||||
box-sizing: border-box;
|
||||
min-height: 75px;
|
||||
white-space: pre;
|
||||
padding: 0 3px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { clearMathElement, typesetMathElement } from '../../lib/mathjax/mathjax-typeset';
|
||||
import TexLogo from '../tex-logo/tex-logo';
|
||||
import styles from './tex-preview-modal.module.css';
|
||||
|
||||
const TYPESET_DEBOUNCE_MS = 50;
|
||||
|
||||
// Live TeX preview opened from the reply modal's TeX button: a textarea over a preview area that
|
||||
// re-typesets what you type, so equations can be checked before posting.
|
||||
const TexPreviewModal = ({ closeModal }: { closeModal: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const outputRef = useRef<HTMLDivElement>(null);
|
||||
const typesetTimeoutRef = useRef<number>(undefined);
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const { value } = event.target;
|
||||
window.clearTimeout(typesetTimeoutRef.current);
|
||||
typesetTimeoutRef.current = window.setTimeout(() => {
|
||||
if (outputRef.current) {
|
||||
typesetMathElement(outputRef.current, value);
|
||||
}
|
||||
}, TYPESET_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const closeOnEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.stopPropagation();
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Capture phase so Escape closes this modal before the reply modal's own Escape handler.
|
||||
document.addEventListener('keydown', closeOnEscape, true);
|
||||
return () => document.removeEventListener('keydown', closeOnEscape, true);
|
||||
}, [closeModal]);
|
||||
|
||||
// On close, cancel any pending typeset and drop MathJax's bookkeeping for the preview output,
|
||||
// like TexMath does, so repeated open/close cycles do not accumulate detached math items.
|
||||
useEffect(() => {
|
||||
const output = outputRef.current;
|
||||
return () => {
|
||||
window.clearTimeout(typesetTimeoutRef.current);
|
||||
if (output) {
|
||||
clearMathElement(output);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.overlay}>
|
||||
<button type='button' className={styles.overlayButton} aria-label={t('close')} onClick={closeModal} />
|
||||
<dialog open className={styles.panel} aria-modal='true' aria-labelledby='tex-preview-title'>
|
||||
<div id='tex-preview-title' className={styles.header}>
|
||||
<span className={styles.titleText}>
|
||||
<Trans
|
||||
i18nKey='tex_preview_title'
|
||||
components={{
|
||||
tex: <TexLogo className={styles.texLogo} />,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<button type='button' className={styles.closeIcon} title={t('close')} aria-label={t('close')} onClick={closeModal} />
|
||||
</div>
|
||||
<div className={styles.protip}>{t('tex_preview_protip')}</div>
|
||||
<textarea className={styles.input} aria-label={t('tex_preview_input_label')} spellCheck={false} onChange={handleInputChange} />
|
||||
<div className={styles.output} ref={outputRef} />
|
||||
</dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TexPreviewModal;
|
||||
@@ -5,10 +5,11 @@ import styles from './tooltip.module.css';
|
||||
interface TooltipProps {
|
||||
content: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
const Tooltip = ({ content, children, showTooltip = true }: TooltipProps) => {
|
||||
const Tooltip = ({ content, children, className, showTooltip = true }: TooltipProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
@@ -28,7 +29,7 @@ const Tooltip = ({ content, children, showTooltip = true }: TooltipProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<span ref={refs.setReference} {...getReferenceProps()}>
|
||||
<span className={className} ref={refs.setReference} {...getReferenceProps()}>
|
||||
{children}
|
||||
</span>
|
||||
{showTooltip && (
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { HAS_MATH_TAG_REGEX, isMathDirectoryCode, splitMathSegments } from '../math-tags';
|
||||
|
||||
describe('math-tags', () => {
|
||||
it('enables math tags only for /sci/', () => {
|
||||
expect(isMathDirectoryCode('sci')).toBe(true);
|
||||
expect(isMathDirectoryCode('SCI')).toBe(true);
|
||||
expect(isMathDirectoryCode('g')).toBe(false);
|
||||
expect(isMathDirectoryCode('b')).toBe(false);
|
||||
expect(isMathDirectoryCode(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('detects closed math and eqn tags', () => {
|
||||
expect(HAS_MATH_TAG_REGEX.test('[math]x[/math]')).toBe(true);
|
||||
expect(HAS_MATH_TAG_REGEX.test('[eqn]\\int x dx[/eqn]')).toBe(true);
|
||||
expect(HAS_MATH_TAG_REGEX.test('[math]unclosed')).toBe(false);
|
||||
expect(HAS_MATH_TAG_REGEX.test('[math]mismatch[/eqn]')).toBe(false);
|
||||
expect(HAS_MATH_TAG_REGEX.test('no tags at all')).toBe(false);
|
||||
});
|
||||
|
||||
it('splits text and math segments with offsets, keeping the delimiters', () => {
|
||||
expect(splitMathSegments('a [math]x_1[/math] b')).toEqual([
|
||||
{ type: 'text', value: 'a ', start: 0 },
|
||||
{ type: 'math', value: '[math]x_1[/math]', start: 2 },
|
||||
{ type: 'text', value: ' b', start: 18 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps multi-line eqn content in one math segment', () => {
|
||||
const raw = 'before\n[eqn]\\begin{pmatrix}a & b\\\\c & d\\end{pmatrix}[/eqn]\nafter';
|
||||
const segments = splitMathSegments(raw);
|
||||
expect(segments.map((segment) => segment.type)).toEqual(['text', 'math', 'text']);
|
||||
expect(segments[1].value).toContain('pmatrix');
|
||||
expect(segments[1].value).toContain('\\\\');
|
||||
});
|
||||
|
||||
it('leaves unclosed or mismatched tags as plain text', () => {
|
||||
expect(splitMathSegments('[math]x')).toEqual([{ type: 'text', value: '[math]x', start: 0 }]);
|
||||
expect(splitMathSegments('[math]x[/eqn]')).toEqual([{ type: 'text', value: '[math]x[/eqn]', start: 0 }]);
|
||||
});
|
||||
|
||||
it('handles back-to-back and repeated math segments', () => {
|
||||
expect(splitMathSegments('[math]a[/math][eqn]b[/eqn]')).toEqual([
|
||||
{ type: 'math', value: '[math]a[/math]', start: 0 },
|
||||
{ type: 'math', value: '[eqn]b[/eqn]', start: 14 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not extract math inside spoilers so spoiler parsing keeps working', () => {
|
||||
expect(splitMathSegments('[spoiler]a [math]x[/math][/spoiler]')).toEqual([
|
||||
{ type: 'text', value: '[spoiler]a [math]x[/math][/spoiler]', start: 0 },
|
||||
]);
|
||||
const mixed = splitMathSegments('[spoiler][math]a[/math][/spoiler] [math]b[/math]');
|
||||
expect(mixed.map((segment) => segment.type)).toEqual(['text', 'math']);
|
||||
expect(mixed[1].value).toBe('[math]b[/math]');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
// [math]/[eqn] TeX tags are a /sci/ feature (like 4chan): typeset client-side with MathJax,
|
||||
// enabled only when the post's board or the current route resolves to /sci/.
|
||||
export const MATH_DIRECTORY_CODE = 'sci';
|
||||
|
||||
export const isMathDirectoryCode = (directoryCode: string | undefined): boolean => directoryCode?.toLowerCase() === MATH_DIRECTORY_CODE;
|
||||
|
||||
// Tags are matched case-sensitively and must be properly closed, like 4chan's MathJax delimiters.
|
||||
// [\s\S] lets a single [math]/[eqn] region span multiple lines (e.g. pmatrix rows).
|
||||
const MATH_SEGMENT_REGEX = /\[(math|eqn)\]([\s\S]*?)\[\/\1\]/g;
|
||||
export const HAS_MATH_TAG_REGEX = /\[(math|eqn)\][\s\S]*?\[\/\1\]/;
|
||||
|
||||
// Math is not extracted inside [spoiler] regions so existing spoiler parsing keeps working there.
|
||||
const SPOILER_RANGE_REGEX = /\[[sS][pP][oO][iI][lL][eE][rR]\][\s\S]*?\[\/[sS][pP][oO][iI][lL][eE][rR]\]/g;
|
||||
|
||||
export type MathSegment = { type: 'text' | 'math'; value: string; start: number };
|
||||
|
||||
const getSpoilerRanges = (raw: string): { start: number; end: number }[] => {
|
||||
const ranges: { start: number; end: number }[] = [];
|
||||
const regex = new RegExp(SPOILER_RANGE_REGEX.source, 'g');
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = regex.exec(raw)) !== null) {
|
||||
ranges.push({ start: match.index, end: regex.lastIndex });
|
||||
}
|
||||
return ranges;
|
||||
};
|
||||
|
||||
// Splits content into text segments (normal markdown pipeline) and math segments (typeset as-is,
|
||||
// delimiters included). Unclosed tags stay plain text.
|
||||
export const splitMathSegments = (raw: string): MathSegment[] => {
|
||||
const segments: MathSegment[] = [];
|
||||
const spoilerRanges = getSpoilerRanges(raw);
|
||||
const regex = new RegExp(MATH_SEGMENT_REGEX.source, 'g');
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(raw)) !== null) {
|
||||
const matchStart = match.index;
|
||||
const matchEnd = regex.lastIndex;
|
||||
|
||||
if (spoilerRanges.some((range) => matchStart >= range.start && matchEnd <= range.end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchStart > lastIndex) {
|
||||
segments.push({ type: 'text', value: raw.slice(lastIndex, matchStart), start: lastIndex });
|
||||
}
|
||||
segments.push({ type: 'math', value: match[0], start: matchStart });
|
||||
lastIndex = matchEnd;
|
||||
}
|
||||
|
||||
if (lastIndex < raw.length) {
|
||||
segments.push({ type: 'text', value: raw.slice(lastIndex), start: lastIndex });
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
// MathJax v3 configuration matching 4chan's /sci/ setup: [math] inline and [eqn] display
|
||||
// delimiters only, left-aligned display equations, Safe mode, and shitposting-prone macros
|
||||
// (\color, \newcommand, \def, ...) neutered into no-ops so they silently do nothing.
|
||||
// Must run before the MathJax component modules are imported.
|
||||
|
||||
// Font files are emitted under <base>/mathjax/woff-v2/ by the vite plugin (see vite.config.js),
|
||||
// resolved like the Ruffle runtime so it works in dev, web builds, and Electron.
|
||||
const fontURL = new URL('mathjax/woff-v2', document.baseURI).href;
|
||||
|
||||
(window as unknown as { MathJax: object }).MathJax = {
|
||||
loader: { load: [] },
|
||||
startup: { typeset: false },
|
||||
tex: {
|
||||
// configmacros provides the `macros` option used to neuter the disallowed macros below
|
||||
packages: ['base', 'ams', 'noerrors', 'noundefined', 'configmacros'],
|
||||
inlineMath: [['[math]', '[/math]']],
|
||||
displayMath: [['[eqn]', '[/eqn]']],
|
||||
processEscapes: false,
|
||||
processEnvironments: false,
|
||||
processRefs: false,
|
||||
macros: {
|
||||
color: '{}',
|
||||
newcommand: '{}',
|
||||
renewcommand: '{}',
|
||||
newenvironment: '{}',
|
||||
renewenvironment: '{}',
|
||||
def: '{}',
|
||||
let: '{}',
|
||||
},
|
||||
},
|
||||
chtml: {
|
||||
fontURL,
|
||||
displayAlign: 'left',
|
||||
},
|
||||
options: {
|
||||
enableMenu: true,
|
||||
safeOptions: {
|
||||
allow: {
|
||||
URLs: 'none',
|
||||
classes: 'none',
|
||||
cssIDs: 'none',
|
||||
styles: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
// Custom MathJax v3 component build (the official "making a custom build" recipe for bundlers):
|
||||
// the config module must execute first, then the components, then startup wires everything up.
|
||||
// Imported lazily (dynamic import) so MathJax stays out of the main bundle and only loads on
|
||||
// math-enabled boards that actually display equations.
|
||||
import './mathjax-config';
|
||||
import 'mathjax-full/components/src/startup/lib/startup.js';
|
||||
import 'mathjax-full/components/src/core/core.js';
|
||||
import 'mathjax-full/components/src/input/tex-base/tex-base.js';
|
||||
import 'mathjax-full/components/src/input/tex/extensions/ams/ams.js';
|
||||
import 'mathjax-full/components/src/input/tex/extensions/configmacros/configmacros.js';
|
||||
import 'mathjax-full/components/src/input/tex/extensions/noerrors/noerrors.js';
|
||||
import 'mathjax-full/components/src/input/tex/extensions/noundefined/noundefined.js';
|
||||
import 'mathjax-full/components/src/output/chtml/chtml.js';
|
||||
import 'mathjax-full/components/src/output/chtml/fonts/tex/tex.js';
|
||||
import 'mathjax-full/components/src/ui/safe/safe.js';
|
||||
import 'mathjax-full/components/src/ui/menu/menu.js';
|
||||
import 'mathjax-full/components/src/a11y/assistive-mml/assistive-mml.js';
|
||||
import 'mathjax-full/components/src/startup/startup.js';
|
||||
@@ -0,0 +1,62 @@
|
||||
// Lazy MathJax entry point: the heavy setup chunk is fetched once, on the first equation that
|
||||
// actually needs it, and typeset calls are serialized because MathJax's typesetPromise must not
|
||||
// run concurrently with itself.
|
||||
interface MathJaxApi {
|
||||
startup: { promise: Promise<void> };
|
||||
typesetPromise: (elements: HTMLElement[]) => Promise<void>;
|
||||
typesetClear: (elements: HTMLElement[]) => void;
|
||||
}
|
||||
|
||||
let mathJaxPromise: Promise<MathJaxApi | undefined> | undefined;
|
||||
let typesetQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
const loadMathJax = (): Promise<MathJaxApi | undefined> => {
|
||||
if (!mathJaxPromise) {
|
||||
mathJaxPromise = import('./mathjax-setup')
|
||||
.then(async () => {
|
||||
const mathJax = (window as unknown as { MathJax: MathJaxApi }).MathJax;
|
||||
await mathJax.startup.promise;
|
||||
return mathJax;
|
||||
})
|
||||
.catch((error) => {
|
||||
// Allow a retry on the next equation (e.g. the chunk failed to download while offline).
|
||||
mathJaxPromise = undefined;
|
||||
console.error('failed to load MathJax', error);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return mathJaxPromise;
|
||||
};
|
||||
|
||||
// Starts fetching the MathJax chunk ahead of the first typeset (e.g. when the TeX Preview modal
|
||||
// opens), like 4chan loading MathJax as soon as the preview panel is created.
|
||||
export const preloadMathJax = (): void => {
|
||||
loadMathJax();
|
||||
};
|
||||
|
||||
// Resets the element to the raw TeX source, then typesets it in place. Re-running on the same
|
||||
// element is safe (the source reset makes it idempotent), so re-mounts and StrictMode double
|
||||
// effects just re-typeset.
|
||||
export const typesetMathElement = (element: HTMLElement, source: string): Promise<void> => {
|
||||
typesetQueue = typesetQueue.then(async () => {
|
||||
const mathJax = await loadMathJax();
|
||||
if (!mathJax || !element.isConnected) {
|
||||
return;
|
||||
}
|
||||
element.textContent = source;
|
||||
try {
|
||||
await mathJax.typesetPromise([element]);
|
||||
} catch (error) {
|
||||
console.error('failed to typeset math', error);
|
||||
}
|
||||
});
|
||||
return typesetQueue;
|
||||
};
|
||||
|
||||
// Drops MathJax's internal bookkeeping for an element that is being unmounted.
|
||||
export const clearMathElement = (element: HTMLElement): void => {
|
||||
if (!mathJaxPromise) {
|
||||
return;
|
||||
}
|
||||
mathJaxPromise.then((mathJax) => mathJax?.typesetClear([element])).catch(() => {});
|
||||
};
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
// mathjax-full ships its component entry modules as plain untyped JS.
|
||||
declare module 'mathjax-full/components/src/*';
|
||||
+73
-1
@@ -58,6 +58,8 @@ const baselineAppShellUrls = new Set([
|
||||
]);
|
||||
const ruffleRuntimeDirectory = new URL('./node_modules/@ruffle-rs/ruffle/', import.meta.url);
|
||||
const ruffleRuntimeFilePattern = /^(?:ruffle\.js|core\.ruffle\.[\da-f]+\.js|[\da-f]+\.wasm)$/;
|
||||
const mathjaxFontDirectory = new URL('./node_modules/mathjax-full/es5/output/chtml/fonts/woff-v2/', import.meta.url);
|
||||
const mathjaxFontFilePattern = /^MathJax_[\w-]+\.woff$/;
|
||||
|
||||
function readGitRef(args) {
|
||||
try {
|
||||
@@ -236,6 +238,47 @@ function ruffleRuntimeAssetsPlugin() {
|
||||
};
|
||||
}
|
||||
|
||||
// MathJax's CHTML output loads its TeX web fonts from <base>/mathjax/woff-v2/ (see
|
||||
// src/lib/mathjax/mathjax-config.ts). Like the Ruffle runtime, the fonts are served straight
|
||||
// from node_modules in dev and emitted into the build output, so nothing is committed to git.
|
||||
function getMathjaxFontFileNames() {
|
||||
return readdirSync(mathjaxFontDirectory).filter((fileName) => mathjaxFontFilePattern.test(fileName));
|
||||
}
|
||||
|
||||
function mathjaxFontAssetsPlugin() {
|
||||
return {
|
||||
name: 'fivechan-mathjax-font-assets',
|
||||
configureServer(server) {
|
||||
server.middlewares.use('/mathjax/woff-v2/', (req, res, next) => {
|
||||
const fileName = new URL(req.url || '', 'https://example.invalid/').pathname.split('/').pop() || '';
|
||||
|
||||
if (!mathjaxFontFilePattern.test(fileName)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const source = readFileSync(new URL(fileName, mathjaxFontDirectory));
|
||||
res.setHeader('Content-Type', 'font/woff');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.end(source);
|
||||
} catch {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
generateBundle() {
|
||||
for (const fileName of getMathjaxFontFileNames()) {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: `mathjax/woff-v2/${fileName}`,
|
||||
source: readFileSync(new URL(fileName, mathjaxFontDirectory)),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getVercelContentSecurityPolicy() {
|
||||
const vercelConfig = JSON.parse(readFileSync(new URL('./vercel.json', import.meta.url), 'utf8'));
|
||||
const cspHeader = vercelConfig.headers
|
||||
@@ -323,6 +366,7 @@ export default defineConfig({
|
||||
plugins: [
|
||||
appVersionMetadataPlugin(),
|
||||
ruffleRuntimeAssetsPlugin(),
|
||||
mathjaxFontAssetsPlugin(),
|
||||
...react({
|
||||
babel: {
|
||||
plugins: [
|
||||
@@ -496,6 +540,9 @@ export default defineConfig({
|
||||
if (/[\\/]node_modules[\\/](react-virtuoso)[\\/]/.test(id)) {
|
||||
return 'virtuoso';
|
||||
}
|
||||
if (/[\\/]node_modules[\\/](mathjax-full|mj-context-menu)[\\/]/.test(id)) {
|
||||
return 'mathjax';
|
||||
}
|
||||
if (/[\\/]node_modules[\\/](@floating-ui)[\\/]/.test(id)) {
|
||||
return 'floating-ui';
|
||||
}
|
||||
@@ -505,7 +552,32 @@ export default defineConfig({
|
||||
},
|
||||
base: publicBase,
|
||||
optimizeDeps: {
|
||||
include: ['ethers', 'assert', 'buffer', 'process', 'util', 'stream-browserify', 'isomorphic-fetch', 'workbox-core', 'workbox-precaching'],
|
||||
include: [
|
||||
'ethers',
|
||||
'assert',
|
||||
'buffer',
|
||||
'process',
|
||||
'util',
|
||||
'stream-browserify',
|
||||
'isomorphic-fetch',
|
||||
'workbox-core',
|
||||
'workbox-precaching',
|
||||
// The lazy MathJax chunk (src/lib/mathjax/mathjax-setup.ts): pre-bundle so the first
|
||||
// equation does not trigger a mid-session "optimized dependencies changed" full reload.
|
||||
'mathjax-full/components/src/startup/lib/startup.js',
|
||||
'mathjax-full/components/src/core/core.js',
|
||||
'mathjax-full/components/src/input/tex-base/tex-base.js',
|
||||
'mathjax-full/components/src/input/tex/extensions/ams/ams.js',
|
||||
'mathjax-full/components/src/input/tex/extensions/configmacros/configmacros.js',
|
||||
'mathjax-full/components/src/input/tex/extensions/noerrors/noerrors.js',
|
||||
'mathjax-full/components/src/input/tex/extensions/noundefined/noundefined.js',
|
||||
'mathjax-full/components/src/output/chtml/chtml.js',
|
||||
'mathjax-full/components/src/output/chtml/fonts/tex/tex.js',
|
||||
'mathjax-full/components/src/ui/safe/safe.js',
|
||||
'mathjax-full/components/src/ui/menu/menu.js',
|
||||
'mathjax-full/components/src/a11y/assistive-mml/assistive-mml.js',
|
||||
'mathjax-full/components/src/startup/startup.js',
|
||||
],
|
||||
},
|
||||
define: {
|
||||
'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion),
|
||||
|
||||
@@ -67,6 +67,7 @@ __metadata:
|
||||
knip: "npm:6.1.0"
|
||||
lint-staged: "npm:12.3.8"
|
||||
lodash: "npm:4.18.0"
|
||||
mathjax-full: "npm:3.2.2"
|
||||
memoizee: "npm:0.4.15"
|
||||
node-fetch: "npm:3.3.2"
|
||||
oxfmt: "npm:0.20.0"
|
||||
@@ -8615,6 +8616,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:13.1.0, commander@npm:^13.1.0":
|
||||
version: 13.1.0
|
||||
resolution: "commander@npm:13.1.0"
|
||||
checksum: 10c0/7b8c5544bba704fbe84b7cab2e043df8586d5c114a4c5b607f83ae5060708940ed0b5bd5838cf8ce27539cde265c1cbd59ce3c8c6b017ed3eec8943e3a415164
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^11.1.0":
|
||||
version: 11.1.0
|
||||
resolution: "commander@npm:11.1.0"
|
||||
@@ -8629,13 +8637,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^13.1.0":
|
||||
version: 13.1.0
|
||||
resolution: "commander@npm:13.1.0"
|
||||
checksum: 10c0/7b8c5544bba704fbe84b7cab2e043df8586d5c114a4c5b607f83ae5060708940ed0b5bd5838cf8ce27539cde265c1cbd59ce3c8c6b017ed3eec8943e3a415164
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^14.0.0, commander@npm:^14.0.3":
|
||||
version: 14.0.3
|
||||
resolution: "commander@npm:14.0.3"
|
||||
@@ -10391,6 +10392,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esm@npm:^3.2.25":
|
||||
version: 3.2.25
|
||||
resolution: "esm@npm:3.2.25"
|
||||
checksum: 10c0/8e60e8075506a7ce28681c30c8f54623fe18a251c364cd481d86719fc77f58aa055b293d80632d9686d5408aaf865ffa434897dc9fd9153c8b3f469fad23f094
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esniff@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "esniff@npm:2.0.1"
|
||||
@@ -14487,6 +14495,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mathjax-full@npm:3.2.2":
|
||||
version: 3.2.2
|
||||
resolution: "mathjax-full@npm:3.2.2"
|
||||
dependencies:
|
||||
esm: "npm:^3.2.25"
|
||||
mhchemparser: "npm:^4.1.0"
|
||||
mj-context-menu: "npm:^0.6.1"
|
||||
speech-rule-engine: "npm:^4.0.6"
|
||||
checksum: 10c0/48f55747bd91aa6bba905bab8654045e4d654a0a65869b1d446e25a89284ef37dc722c6c4a585e7fc14c5f87bb6954291f066e08e5a8245bb5ff0c4f4c54c8d4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"md5.js@npm:^1.3.4":
|
||||
version: 1.3.5
|
||||
resolution: "md5.js@npm:1.3.5"
|
||||
@@ -14596,6 +14616,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mhchemparser@npm:^4.1.0":
|
||||
version: 4.2.1
|
||||
resolution: "mhchemparser@npm:4.2.1"
|
||||
checksum: 10c0/5afe8b5ed918fda71a17875227050fd7e8e706305df28bd0beb5a7cd07886a393d041317e3c4b9105fc518ab75065b65886b2574cbd8dec49520de04d119e64a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.8":
|
||||
version: 4.0.8
|
||||
resolution: "micromatch@npm:4.0.8"
|
||||
@@ -14886,6 +14913,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mj-context-menu@npm:^0.6.1":
|
||||
version: 0.6.1
|
||||
resolution: "mj-context-menu@npm:0.6.1"
|
||||
checksum: 10c0/2bf3564bb5cbd3915e14417e8df4a4e3013f1e281d431ee1e88976ae6529cc88e6eea7d9b3e217d1d79666f89070c3e1a193383ba9c7bf82d63075e616664ce9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3":
|
||||
version: 0.5.3
|
||||
resolution: "mkdirp-classic@npm:0.5.3"
|
||||
@@ -18451,6 +18485,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"speech-rule-engine@npm:^4.0.6":
|
||||
version: 4.1.4
|
||||
resolution: "speech-rule-engine@npm:4.1.4"
|
||||
dependencies:
|
||||
"@xmldom/xmldom": "npm:0.9.10"
|
||||
commander: "npm:13.1.0"
|
||||
wicked-good-xpath: "npm:1.3.0"
|
||||
bin:
|
||||
sre: bin/sre
|
||||
checksum: 10c0/905c9e063e066eef0b5bbb64a9a3f30242860bdd5447fdbc3a7796fb62bdfed9ca62369861406f3dfe07cc8d3329866ee525fad5070b9ddf08920d9604223d8d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"split2@npm:^4.0.0, split2@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "split2@npm:4.2.0"
|
||||
@@ -20405,6 +20452,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"wicked-good-xpath@npm:1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "wicked-good-xpath@npm:1.3.0"
|
||||
checksum: 10c0/6837e1027e75e2ad7fe0620d11def4c42235cbe6c649727258973d7d77c2c5066a5376595e0283d3ba8e2fa22deb3cfe7358db5a02a938bde273b55b9ac57832
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"word-wrap@npm:^1.0.3":
|
||||
version: 1.2.5
|
||||
resolution: "word-wrap@npm:1.2.5"
|
||||
|
||||
Reference in New Issue
Block a user