From ae789a5b4019fea0f73b4a32215d650bfcb4a529 Mon Sep 17 00:00:00 2001 From: retoor Date: Thu, 4 Dec 2025 20:29:35 +0100 Subject: [PATCH] Initial commit. --- README.md | 56 ++ assets/icons/icon-128.png | Bin 0 -> 3110 bytes assets/icons/icon-128.svg | 4 + assets/icons/icon-144.png | Bin 0 -> 3489 bytes assets/icons/icon-144.svg | 4 + assets/icons/icon-152.png | Bin 0 -> 3642 bytes assets/icons/icon-152.svg | 4 + assets/icons/icon-16.png | Bin 0 -> 422 bytes assets/icons/icon-16.svg | 4 + assets/icons/icon-192.png | Bin 0 -> 4668 bytes assets/icons/icon-192.svg | 4 + assets/icons/icon-32.png | Bin 0 -> 848 bytes assets/icons/icon-32.svg | 4 + assets/icons/icon-384.png | Bin 0 -> 9938 bytes assets/icons/icon-384.svg | 4 + assets/icons/icon-512.png | Bin 0 -> 14026 bytes assets/icons/icon-512.svg | 4 + assets/icons/icon-72.png | Bin 0 -> 1865 bytes assets/icons/icon-72.svg | 4 + assets/icons/icon-96.png | Bin 0 -> 2364 bytes assets/icons/icon-96.svg | 4 + css/base.css | 219 +++++ css/components/app-header.css | 214 +++++ css/components/app-nav.css | 104 +++ css/components/comment.css | 152 ++++ css/components/components.css | 487 +++++++++++ css/components/form.css | 245 ++++++ css/components/notification.css | 122 +++ css/components/pages.css | 151 ++++ css/components/rant.css | 342 ++++++++ css/components/user.css | 166 ++++ css/themes/black.css | 31 + css/themes/dark.css | 31 + css/themes/forest.css | 31 + css/themes/light.css | 31 + css/themes/ocean.css | 31 + css/themes/sunset.css | 31 + css/themes/white.css | 31 + css/variables.css | 57 ++ index.html | 59 ++ js/api/client.js | 282 +++++++ js/app.js | 315 +++++++ js/components/app-header.js | 149 ++++ js/components/app-nav.js | 207 +++++ js/components/base-component.js | 220 +++++ js/components/comment-form.js | 180 ++++ js/components/comment-item.js | 232 +++++ js/components/image-preview.js | 146 ++++ js/components/link-preview.js | 72 ++ js/components/loading-spinner.js | 48 ++ js/components/login-form.js | 133 +++ js/components/notification-list.js | 232 +++++ js/components/post-form.js | 249 ++++++ js/components/rant-card.js | 169 ++++ js/components/rant-content.js | 78 ++ js/components/rant-detail.js | 243 ++++++ js/components/rant-feed.js | 242 ++++++ js/components/theme-selector.js | 62 ++ js/components/toast-notification.js | 124 +++ js/components/user-avatar.js | 103 +++ js/components/user-profile.js | 224 +++++ js/components/vote-buttons.js | 113 +++ js/components/youtube-embed.js | 97 +++ js/pages/collabs-page.js | 36 + js/pages/home-page.js | 48 ++ js/pages/login-page.js | 93 ++ js/pages/notifications-page.js | 33 + js/pages/profile-page.js | 49 ++ js/pages/rant-page.js | 62 ++ js/pages/search-page.js | 88 ++ js/pages/settings-page.js | 116 +++ js/pages/stories-page.js | 36 + js/pages/weekly-page.js | 36 + js/services/auth.js | 100 +++ js/services/router.js | 177 ++++ js/services/storage.js | 155 ++++ js/services/theme.js | 156 ++++ js/utils/date.js | 90 ++ js/utils/markdown.js | 148 ++++ js/utils/template.js | 154 ++++ js/utils/url.js | 119 +++ lib/highlight.css | 10 + lib/highlight.min.js | 1213 +++++++++++++++++++++++++++ lib/marked.min.js | 6 + manifest.json | 65 ++ proxy.py | 125 +++ sw.js | 132 +++ 87 files changed, 9798 insertions(+) create mode 100644 README.md create mode 100644 assets/icons/icon-128.png create mode 100644 assets/icons/icon-128.svg create mode 100644 assets/icons/icon-144.png create mode 100644 assets/icons/icon-144.svg create mode 100644 assets/icons/icon-152.png create mode 100644 assets/icons/icon-152.svg create mode 100644 assets/icons/icon-16.png create mode 100644 assets/icons/icon-16.svg create mode 100644 assets/icons/icon-192.png create mode 100644 assets/icons/icon-192.svg create mode 100644 assets/icons/icon-32.png create mode 100644 assets/icons/icon-32.svg create mode 100644 assets/icons/icon-384.png create mode 100644 assets/icons/icon-384.svg create mode 100644 assets/icons/icon-512.png create mode 100644 assets/icons/icon-512.svg create mode 100644 assets/icons/icon-72.png create mode 100644 assets/icons/icon-72.svg create mode 100644 assets/icons/icon-96.png create mode 100644 assets/icons/icon-96.svg create mode 100644 css/base.css create mode 100644 css/components/app-header.css create mode 100644 css/components/app-nav.css create mode 100644 css/components/comment.css create mode 100644 css/components/components.css create mode 100644 css/components/form.css create mode 100644 css/components/notification.css create mode 100644 css/components/pages.css create mode 100644 css/components/rant.css create mode 100644 css/components/user.css create mode 100644 css/themes/black.css create mode 100644 css/themes/dark.css create mode 100644 css/themes/forest.css create mode 100644 css/themes/light.css create mode 100644 css/themes/ocean.css create mode 100644 css/themes/sunset.css create mode 100644 css/themes/white.css create mode 100644 css/variables.css create mode 100644 index.html create mode 100644 js/api/client.js create mode 100644 js/app.js create mode 100644 js/components/app-header.js create mode 100644 js/components/app-nav.js create mode 100644 js/components/base-component.js create mode 100644 js/components/comment-form.js create mode 100644 js/components/comment-item.js create mode 100644 js/components/image-preview.js create mode 100644 js/components/link-preview.js create mode 100644 js/components/loading-spinner.js create mode 100644 js/components/login-form.js create mode 100644 js/components/notification-list.js create mode 100644 js/components/post-form.js create mode 100644 js/components/rant-card.js create mode 100644 js/components/rant-content.js create mode 100644 js/components/rant-detail.js create mode 100644 js/components/rant-feed.js create mode 100644 js/components/theme-selector.js create mode 100644 js/components/toast-notification.js create mode 100644 js/components/user-avatar.js create mode 100644 js/components/user-profile.js create mode 100644 js/components/vote-buttons.js create mode 100644 js/components/youtube-embed.js create mode 100644 js/pages/collabs-page.js create mode 100644 js/pages/home-page.js create mode 100644 js/pages/login-page.js create mode 100644 js/pages/notifications-page.js create mode 100644 js/pages/profile-page.js create mode 100644 js/pages/rant-page.js create mode 100644 js/pages/search-page.js create mode 100644 js/pages/settings-page.js create mode 100644 js/pages/stories-page.js create mode 100644 js/pages/weekly-page.js create mode 100644 js/services/auth.js create mode 100644 js/services/router.js create mode 100644 js/services/storage.js create mode 100644 js/services/theme.js create mode 100644 js/utils/date.js create mode 100644 js/utils/markdown.js create mode 100644 js/utils/template.js create mode 100644 js/utils/url.js create mode 100644 lib/highlight.css create mode 100644 lib/highlight.min.js create mode 100644 lib/marked.min.js create mode 100644 manifest.json create mode 100644 proxy.py create mode 100644 sw.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d5ccb3 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Rantii + +DevRant client built with vanilla JavaScript, Web Components, and CSS. + +## Requirements + +- Python 3.8+ +- aiohttp (`pip install aiohttp`) + +## Run + +```bash +python3 proxy.py +``` + +Open `http://localhost:8000` + +## Features + +- Browse rants, weekly challenges, collabs, stories +- User profiles and notifications +- Voting, commenting, posting +- Search +- 7 themes: dark, light, black, white, ocean, forest, sunset +- PWA installable +- Offline support via service worker + +## Structure + +``` +rantii/ +├── index.html +├── proxy.py +├── manifest.json +├── sw.js +├── css/ +│ ├── base.css +│ ├── variables.css +│ ├── themes/ +│ └── components/ +├── js/ +│ ├── app.js +│ ├── api/ +│ ├── services/ +│ ├── components/ +│ ├── pages/ +│ └── utils/ +├── lib/ +│ ├── marked.min.js +│ └── highlight.min.js +└── assets/ +``` + +## Author + +retoor diff --git a/assets/icons/icon-128.png b/assets/icons/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..7043253680aa52581d7e720819fa3c4efb62ef7b GIT binary patch literal 3110 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 R9z}NmW^7?pW_a)Ee*vw2`Jw;- literal 0 HcmV?d00001 diff --git a/assets/icons/icon-128.svg b/assets/icons/icon-128.svg new file mode 100644 index 0000000..55d571f --- /dev/null +++ b/assets/icons/icon-128.svg @@ -0,0 +1,4 @@ + + + R + diff --git a/assets/icons/icon-144.png b/assets/icons/icon-144.png new file mode 100644 index 0000000000000000000000000000000000000000..dc5cb8ceb8e7ddf6ca689625cd650e5f21074ce2 GIT binary patch literal 3489 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 CFqDk| literal 0 HcmV?d00001 diff --git a/assets/icons/icon-144.svg b/assets/icons/icon-144.svg new file mode 100644 index 0000000..55d571f --- /dev/null +++ b/assets/icons/icon-144.svg @@ -0,0 +1,4 @@ + + + R + diff --git a/assets/icons/icon-152.png b/assets/icons/icon-152.png new file mode 100644 index 0000000000000000000000000000000000000000..313051982d8f1366cc2bff299f22de57747a67a1 GIT binary patch literal 3642 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 literal 0 HcmV?d00001 diff --git a/assets/icons/icon-152.svg b/assets/icons/icon-152.svg new file mode 100644 index 0000000..55d571f --- /dev/null +++ b/assets/icons/icon-152.svg @@ -0,0 +1,4 @@ + + + R + diff --git a/assets/icons/icon-16.png b/assets/icons/icon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..752f9ef94ed2fbe5935fab8e37785016a660c86d GIT binary patch literal 422 zcmV;X0a^ZuP)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv|NsC0|NjYC_uK#g z00(qQO+^Rk3Cxu3I~>qd*xGuKBVX<84i z-JYrD%khQNfxm{?oS7OHsN7*M=2&?{@W<9~(L(!}pR%-bs4;C~`tjx1-?G2F`1LXp zNTwGVv>A-HcdNb8k$7P7Gwnw(=Xa(%Oj=FrpB;Wwd+*rKCqMR3Kxom)>grVfOtKMC8cEFO{G2otXKy349{P0zz>>F&O$edFWdJ09kczshxk4 QyZ`_I07*qoM6N<$g0Y0Zh5!Hn literal 0 HcmV?d00001 diff --git a/assets/icons/icon-16.svg b/assets/icons/icon-16.svg new file mode 100644 index 0000000..55d571f --- /dev/null +++ b/assets/icons/icon-16.svg @@ -0,0 +1,4 @@ + + + R + diff --git a/assets/icons/icon-192.png b/assets/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..7fea47c95a7a4772370b78abbbf0c2acc3cfba06 GIT binary patch literal 4668 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 literal 0 HcmV?d00001 diff --git a/assets/icons/icon-192.svg b/assets/icons/icon-192.svg new file mode 100644 index 0000000..55d571f --- /dev/null +++ b/assets/icons/icon-192.svg @@ -0,0 +1,4 @@ + + + R + diff --git a/assets/icons/icon-32.png b/assets/icons/icon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..f795cf7553187fc5d8f0e723c372997222d9ca59 GIT binary patch literal 848 zcmV-W1F!svP)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv|NsC0|NjYC_uK#g z00(qQO+^Rk3! zqaGM&2aI?Ruu#{y#9tz+;<kaGUM;UzjoIhesq4*Tc`9iZrD~>XJ;rygYiH%f?0sb0hbLPELB5c`gzL#RZw|@PFspj;JuUcQy z|GfRL{+~tY9rI3>P|EG4ng_HEIe&8JL)aI4zD0dKj;Th}4pa72+LsldFZ4>hRC;1}Yx588?+2(Aa8yV_!u>1)EIT1iU%32@ z(aRr(hF7niJ7+3-?dJJEUylDR`^&?4kx_@ih`LUvng@`x?{&u?o!_9v;vbp+fBs)O zB=A}3BbSK=*HxaGA_*+ktZxRN2NF2o+4qLy_s(w`$g+ENKgoZ%!?=(!oH5m78~-!G zpuy(>L(U)EdEYYro&8(*@aC^QKer>x9&P@z + + R + diff --git a/assets/icons/icon-384.png b/assets/icons/icon-384.png new file mode 100644 index 0000000000000000000000000000000000000000..1d51321803b68cd411090d0c0b932847f0bada5d GIT binary patch literal 9938 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 AHUIzs literal 0 HcmV?d00001 diff --git a/assets/icons/icon-384.svg b/assets/icons/icon-384.svg new file mode 100644 index 0000000..55d571f --- /dev/null +++ b/assets/icons/icon-384.svg @@ -0,0 +1,4 @@ + + + R + diff --git a/assets/icons/icon-512.png b/assets/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..702d3dcd83d239c43f30be673848b24a3df7ba54 GIT binary patch literal 14026 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 literal 0 HcmV?d00001 diff --git a/assets/icons/icon-512.svg b/assets/icons/icon-512.svg new file mode 100644 index 0000000..55d571f --- /dev/null +++ b/assets/icons/icon-512.svg @@ -0,0 +1,4 @@ + + + R + diff --git a/assets/icons/icon-72.png b/assets/icons/icon-72.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b34c8bd55f0a7da6d9a57b3c08d9fe0e9cdadc GIT binary patch literal 1865 zcmbVN_dnYU7yhDll)83nwKXf|>nfU7)vUy(!-zD-MXiXfy>_J)E^ajGLN0GQwaKsua2ak|DC@IA1z zwmAK72;SXea2S5HEzFXi1v(=xDUseeKg7{cZ7puP#?E}hCqP~9TN9ixY!frN8oL%T5dEM}1g`ybTh)Gz=c;r-77Yo@>ze z|NUOf_gaej8%Lpcdp~Zr-Uw>4^JuIKC94^i+@S8w!P)gC1bV_KM61eG6Z?!uz{bbm z5s$!zS+^38U@GV`NdGro+W*|>62m%UMt;?Pq3%0M?H68oVxLH~45=qeW-G{E`2+ly z)V*M^avjsPLT`iE>7!WeMv7^%C#Mnu${(uX*sOsDABLgir#|*#?R$Nbtxg>-U%5IE8gRvY5`kfy$gXdCd`Q{< z7F!jrU4tV%-Bmsw_QRC*=FPQMVDDH{uk)Z6lVfn~30d`Y4c8XiRT+fFPFp3kX04pE zjL*RZ^?7yj0OLu{V|;)*FK{Fg^?QXhdYNx%u-x8(z7Vu{`FKa<-l(5tk^z>omV8D` z6yFs>G(C~#cu41X@K_zlOw>Zv)(RV~6rrwFjUD91U6Z+W7A^gm1JHr*3-SySuPwnKu8%zG4PdmDo9~jO}9fl>ta6`VUrOMBD z?%|I8#=eAl-uI2JX-{Tl7bnAHiZY|+5Z_B>8KGGQFAYBY?QZZuFig*34IlgrOezo~ znnv>}Aa>2T#EGX$>dFG~jP#oufe29?7B9OXidhsdst*$a`r-l#2xUW0{m`R0gY~IN1u9 z2croZ<($Vh_BHdpnVOK>B&WkXdU}TZ4n1=U+VnQP;F@&#R(?5z(DTmA=XU&<6F$%mwnpX6S!i}&dp-}etSzVfp8d#$F2A7{Y} zU*?jh@wn6?L%(+1gpT4;|FIl&zB3OPRS!X`hZY)?4uqtyOv$uzkyA{`1@FF;Ku+ThGW{yK59zEsLey-s6yt;Ww82G?=Nvu>4GQB}q6d zWP?qf;}@XCpA6wac(j?;_6jWI?S7SRFEU!;XX#wQjG8(3;BQ2efi~@AjbD%40oJ2A z#K}n7JSK+cXeTDT*CEVxpnIuXe5&o>?0^w_ug>3(dKf|MP_90w6F@;ae0MtRx^EJBYp3lEjUH2viKwNw yF0XOtT7kNXWY5g^h@p={)09>D{{P#5!nM(Gds(C7Ux%FU2H0BKTf8;*N%;q^D1BA{ literal 0 HcmV?d00001 diff --git a/assets/icons/icon-72.svg b/assets/icons/icon-72.svg new file mode 100644 index 0000000..55d571f --- /dev/null +++ b/assets/icons/icon-72.svg @@ -0,0 +1,4 @@ + + + R + diff --git a/assets/icons/icon-96.png b/assets/icons/icon-96.png new file mode 100644 index 0000000000000000000000000000000000000000..e9210c98fd1e014125443f9a3376aa679c700959 GIT binary patch literal 2364 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 AU;qFB literal 0 HcmV?d00001 diff --git a/assets/icons/icon-96.svg b/assets/icons/icon-96.svg new file mode 100644 index 0000000..55d571f --- /dev/null +++ b/assets/icons/icon-96.svg @@ -0,0 +1,4 @@ + + + R + diff --git a/css/base.css b/css/base.css new file mode 100644 index 0000000..b9c11cd --- /dev/null +++ b/css/base.css @@ -0,0 +1,219 @@ +/** + * @fileoverview Base Styles for Rantii + * @author retoor + * @description Reset and foundational styles + * @keywords css, base, reset, foundation, styles + */ + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: transparent; +} + +body { + font-family: var(--font-family); + font-size: var(--font-size-md); + line-height: 1.5; + color: var(--color-text); + background-color: var(--color-bg); + min-height: 100vh; + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body.nav-open { + overflow: hidden; +} + +a { + color: var(--color-link); + text-decoration: none; + transition: color var(--transition-fast); +} + +a:hover { + text-decoration: underline; +} + +img, video { + max-width: 100%; + height: auto; + display: block; +} + +button, input, textarea, select { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; +} + +button { + cursor: pointer; + background: none; + border: none; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +input, textarea { + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--spacing-sm) var(--spacing-md); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); +} + +input:focus, textarea:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(233, 69, 96, 0.2); +} + +textarea { + resize: vertical; + min-height: 80px; +} + +ul, ol { + list-style: none; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + line-height: 1.2; +} + +h1 { font-size: var(--font-size-3xl); } +h2 { font-size: var(--font-size-2xl); } +h3 { font-size: var(--font-size-xl); } +h4 { font-size: var(--font-size-lg); } + +code, pre { + font-family: var(--font-mono); + font-size: var(--font-size-sm); +} + +code { + background-color: var(--color-code-bg); + padding: 0.125em 0.375em; + border-radius: var(--radius-sm); +} + +pre { + background-color: var(--color-code-bg); + padding: var(--spacing-md); + border-radius: var(--radius-md); + overflow-x: auto; +} + +pre code { + background: none; + padding: 0; +} + +::selection { + background-color: var(--color-primary); + color: white; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-text-muted); +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-lg); + border-radius: var(--radius-md); + font-weight: 500; + transition: all var(--transition-fast); +} + +.btn-primary { + background-color: var(--color-primary); + color: white; +} + +.btn-primary:hover { + background-color: var(--color-primary-hover); +} + +.btn-secondary { + background-color: var(--color-secondary); + color: var(--color-text); +} + +.btn-secondary:hover { + background-color: var(--color-surface-hover); +} + +.btn-danger { + background-color: var(--color-error); + color: white; +} + +.btn-danger:hover { + opacity: 0.9; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +[hidden] { + display: none !important; +} + +.app-main { + margin-top: var(--header-height); + min-height: calc(100vh - var(--header-height)); + padding: var(--spacing-md); +} + +@media (min-width: 769px) { + .app-main { + margin-left: var(--nav-width); + padding: var(--spacing-lg); + } +} + +.page { + max-width: var(--max-content-width); + margin: 0 auto; +} diff --git a/css/components/app-header.css b/css/components/app-header.css new file mode 100644 index 0000000..1d3bc00 --- /dev/null +++ b/css/components/app-header.css @@ -0,0 +1,214 @@ +/** + * @fileoverview Header Styles for Rantii + * @author retoor + * @description Application header component styles + * @keywords css, header, navigation, styles + */ + +app-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--header-height); + background-color: var(--color-surface); + border-bottom: 1px solid var(--color-border); + z-index: var(--z-sticky); +} + +.header-container { + display: flex; + align-items: center; + height: 100%; + padding: 0 var(--spacing-md); + gap: var(--spacing-md); +} + +.header-left { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.menu-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: var(--radius-md); + color: var(--color-text); +} + +.menu-toggle:hover { + background-color: var(--color-surface-hover); +} + +.menu-icon { + display: block; + width: 20px; + height: 2px; + background-color: currentColor; + position: relative; +} + +.menu-icon::before, +.menu-icon::after { + content: ''; + position: absolute; + left: 0; + width: 100%; + height: 2px; + background-color: currentColor; +} + +.menu-icon::before { + top: -6px; +} + +.menu-icon::after { + bottom: -6px; +} + +.logo { + display: flex; + align-items: center; + text-decoration: none; +} + +.logo-text { + font-size: var(--font-size-xl); + font-weight: 700; + color: var(--color-primary); +} + +.header-center { + flex: 1; + max-width: 400px; + margin: 0 auto; +} + +.search-container { + position: relative; + display: flex; + align-items: center; +} + +.search-input { + width: 100%; + padding-right: 40px; +} + +.search-btn { + position: absolute; + right: 4px; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + color: var(--color-text-muted); + border-radius: var(--radius-sm); +} + +.search-btn:hover { + color: var(--color-text); + background-color: var(--color-surface-hover); +} + +.header-right { + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +.header-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + color: var(--color-text-secondary); + border-radius: var(--radius-md); + transition: all var(--transition-fast); +} + +.header-btn:hover { + color: var(--color-text); + background-color: var(--color-surface-hover); +} + +.login-btn { + width: auto; + padding: 0 var(--spacing-md); + background-color: var(--color-primary); + color: white; +} + +.login-btn:hover { + background-color: var(--color-primary-hover); + color: white; +} + +.notifications-btn { + position: relative; +} + +.notification-badge { + position: absolute; + top: 4px; + right: 4px; + min-width: 18px; + height: 18px; + padding: 0 4px; + font-size: var(--font-size-xs); + font-weight: 600; + background-color: var(--color-primary); + color: white; + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; +} + +.theme-icon-light { + display: none; +} + +.theme-dark .theme-icon-dark, +.theme-black .theme-icon-dark, +.theme-ocean .theme-icon-dark, +.theme-forest .theme-icon-dark, +.theme-sunset .theme-icon-dark { + display: block; +} + +.theme-dark .theme-icon-light, +.theme-black .theme-icon-light, +.theme-ocean .theme-icon-light, +.theme-forest .theme-icon-light, +.theme-sunset .theme-icon-light { + display: none; +} + +.theme-light .theme-icon-dark, +.theme-white .theme-icon-dark { + display: none; +} + +.theme-light .theme-icon-light, +.theme-white .theme-icon-light { + display: block; +} + +@media (min-width: 769px) { + .menu-toggle { + display: none; + } +} + +@media (max-width: 480px) { + .header-center { + display: none; + } +} diff --git a/css/components/app-nav.css b/css/components/app-nav.css new file mode 100644 index 0000000..3840607 --- /dev/null +++ b/css/components/app-nav.css @@ -0,0 +1,104 @@ +/** + * @fileoverview Navigation Styles for Rantii + * @author retoor + * @description Side navigation component styles + * @keywords css, navigation, sidebar, menu, styles + */ + +app-nav { + position: fixed; + top: var(--header-height); + left: 0; + bottom: 0; + width: var(--nav-width); + background-color: var(--color-surface); + border-right: 1px solid var(--color-border); + overflow-y: auto; + z-index: var(--z-sticky); + transform: translateX(-100%); + transition: transform var(--transition-normal); +} + +app-nav.nav-open { + transform: translateX(0); +} + +@media (min-width: 769px) { + app-nav { + transform: translateX(0); + } +} + +.nav-container { + padding: var(--spacing-md); +} + +.nav-list { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.nav-item { + display: block; +} + +.nav-link { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-md); + color: var(--color-text-secondary); + border-radius: var(--radius-md); + text-decoration: none; + transition: all var(--transition-fast); +} + +.nav-link:hover { + color: var(--color-text); + background-color: var(--color-surface-hover); + text-decoration: none; +} + +.nav-item.active .nav-link { + color: var(--color-primary); + background-color: rgba(233, 69, 96, 0.1); +} + +.nav-link svg { + flex-shrink: 0; +} + +.nav-label { + font-weight: 500; +} + +.nav-badge { + margin-left: auto; + min-width: 20px; + height: 20px; + padding: 0 6px; + font-size: var(--font-size-xs); + font-weight: 600; + background-color: var(--color-primary); + color: white; + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; +} + +.nav-divider { + height: 1px; + background-color: var(--color-border); + margin: var(--spacing-md) 0; +} + +.nav-link-danger { + color: var(--color-error); +} + +.nav-link-danger:hover { + color: var(--color-error); + background-color: rgba(239, 68, 68, 0.1); +} diff --git a/css/components/comment.css b/css/components/comment.css new file mode 100644 index 0000000..d5af877 --- /dev/null +++ b/css/components/comment.css @@ -0,0 +1,152 @@ +/** + * @fileoverview Comment Component Styles for Rantii + * @author retoor + * @description Styles for comment items and forms + * @keywords css, comment, form, styles + */ + +comment-item { + display: block; + background-color: var(--color-surface); + border-radius: var(--radius-md); + transition: all var(--transition-fast); +} + +comment-item.highlight { + box-shadow: 0 0 0 2px var(--color-primary); +} + +.comment-content { + padding: var(--spacing-md); +} + +.comment-header { + display: flex; + align-items: center; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-sm); +} + +.comment-meta { + display: flex; + align-items: center; + gap: var(--spacing-sm); + flex: 1; + flex-wrap: wrap; +} + +.comment-username { + font-weight: 600; + cursor: pointer; +} + +.comment-username:hover { + color: var(--color-primary); +} + +.comment-score { + font-size: var(--font-size-sm); + color: var(--color-upvote); +} + +.comment-time { + font-size: var(--font-size-sm); + color: var(--color-text-muted); +} + +.comment-actions { + display: flex; + gap: var(--spacing-xs); +} + +.action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + color: var(--color-text-muted); + border-radius: var(--radius-sm); + transition: all var(--transition-fast); +} + +.action-btn:hover { + color: var(--color-text); + background-color: var(--color-surface-hover); +} + +.action-btn.delete-btn:hover { + color: var(--color-error); +} + +.comment-body { + margin-bottom: var(--spacing-sm); +} + +.comment-footer { + display: flex; + align-items: center; +} + +.comment-skeleton { + height: 100px; + background: linear-gradient(90deg, var(--color-surface) 25%, var(--color-surface-hover) 50%, var(--color-surface) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: var(--radius-md); +} + +.comment-edit { + padding: var(--spacing-md); +} + +.comment-edit-input { + width: 100%; + min-height: 100px; + margin-bottom: var(--spacing-md); +} + +.comment-edit-actions { + display: flex; + gap: var(--spacing-sm); + justify-content: flex-end; +} + +comment-form { + display: block; +} + +.comment-form-auth { + text-align: center; + padding: var(--spacing-lg); + background-color: var(--color-surface); + border-radius: var(--radius-md); +} + +.comment-form-auth p { + margin-bottom: var(--spacing-md); + color: var(--color-text-secondary); +} + +.comment-form-inner { + background-color: var(--color-surface); + border-radius: var(--radius-md); + padding: var(--spacing-md); +} + +.comment-input { + width: 100%; + min-height: 80px; + margin-bottom: var(--spacing-sm); +} + +.form-actions { + display: flex; + align-items: center; + justify-content: space-between; +} + +.char-count { + font-size: var(--font-size-sm); + color: var(--color-text-muted); +} diff --git a/css/components/components.css b/css/components/components.css new file mode 100644 index 0000000..756527e --- /dev/null +++ b/css/components/components.css @@ -0,0 +1,487 @@ +/** + * @fileoverview Component Styles for Rantii + * @author retoor + * @description Styles for all UI components + * @keywords css, components, ui, styles + */ + +loading-spinner { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-sm); +} + +.spinner-circle { + width: 40px; + height: 40px; + border: 3px solid var(--color-border); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.spinner-small .spinner-circle { + width: 20px; + height: 20px; + border-width: 2px; +} + +.spinner-large .spinner-circle { + width: 60px; + height: 60px; + border-width: 4px; +} + +.spinner-text { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +toast-container { + position: fixed; + bottom: var(--spacing-lg); + right: var(--spacing-lg); + z-index: var(--z-toast); + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + max-width: 400px; +} + +toast-notification { + display: block; + animation: slideIn 0.3s ease; +} + +toast-notification.toast-hiding { + animation: slideOut 0.3s ease forwards; +} + +.toast-content { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-md); + background-color: var(--color-surface); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + border-left: 4px solid; +} + +.toast-success .toast-content { border-left-color: var(--color-success); } +.toast-error .toast-content { border-left-color: var(--color-error); } +.toast-warning .toast-content { border-left-color: var(--color-warning); } +.toast-info .toast-content { border-left-color: var(--color-info); } + +.toast-icon { + flex-shrink: 0; +} + +.toast-success .toast-icon { color: var(--color-success); } +.toast-error .toast-icon { color: var(--color-error); } +.toast-warning .toast-icon { color: var(--color-warning); } +.toast-info .toast-icon { color: var(--color-info); } + +.toast-message { + flex: 1; + font-size: var(--font-size-sm); +} + +.toast-close { + flex-shrink: 0; + color: var(--color-text-muted); + padding: var(--spacing-xs); + border-radius: var(--radius-sm); +} + +.toast-close:hover { + color: var(--color-text); + background-color: var(--color-surface-hover); +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } +} + +user-avatar { + display: inline-block; +} + +.avatar-wrapper { + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + overflow: hidden; +} + +.avatar-small .avatar-wrapper { + width: 32px; + height: 32px; +} + +.avatar-medium .avatar-wrapper { + width: 48px; + height: 48px; +} + +.avatar-large .avatar-wrapper { + width: 80px; + height: 80px; +} + +.avatar-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.avatar-initials { + color: white; + font-weight: 600; +} + +.avatar-small .avatar-initials { font-size: var(--font-size-xs); } +.avatar-medium .avatar-initials { font-size: var(--font-size-md); } +.avatar-large .avatar-initials { font-size: var(--font-size-xl); } + +vote-buttons { + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +.vote-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + color: var(--color-text-muted); + border-radius: var(--radius-sm); + transition: all var(--transition-fast); +} + +.vote-btn:hover { + background-color: var(--color-surface-hover); +} + +.vote-btn.upvote:hover, +.vote-btn.upvote.active { + color: var(--color-upvote); +} + +.vote-btn.downvote:hover, +.vote-btn.downvote.active { + color: var(--color-downvote); +} + +.vote-score { + min-width: 40px; + text-align: center; + font-weight: 600; + font-size: var(--font-size-sm); +} + +.vote-score.positive { color: var(--color-upvote); } +.vote-score.negative { color: var(--color-downvote); } + +theme-selector { + display: block; +} + +.theme-options { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: var(--spacing-sm); + margin-top: var(--spacing-sm); +} + +.theme-option { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-xs); + padding: var(--spacing-sm); + border: 2px solid transparent; + border-radius: var(--radius-md); + transition: all var(--transition-fast); +} + +.theme-option:hover { + background-color: var(--color-surface-hover); +} + +.theme-option.active { + border-color: var(--color-primary); +} + +.theme-preview { + width: 40px; + height: 40px; + border-radius: var(--radius-sm); + border: 1px solid var(--color-border); +} + +.theme-preview-dark { background: linear-gradient(135deg, #1a1a2e 50%, #e94560 50%); } +.theme-preview-light { background: linear-gradient(135deg, #f5f5f7 50%, #d63651 50%); } +.theme-preview-black { background: linear-gradient(135deg, #000000 50%, #e94560 50%); } +.theme-preview-white { background: linear-gradient(135deg, #ffffff 50%, #c52d47 50%); } +.theme-preview-ocean { background: linear-gradient(135deg, #0a1929 50%, #5090d3 50%); } +.theme-preview-forest { background: linear-gradient(135deg, #0d1f0d 50%, #4caf50 50%); } +.theme-preview-sunset { background: linear-gradient(135deg, #1f1410 50%, #ff7043 50%); } + +.theme-name { + font-size: var(--font-size-xs); + color: var(--color-text-secondary); +} + +.theme-label { + font-weight: 500; + margin-bottom: var(--spacing-sm); +} + +image-preview { + display: block; +} + +.image-container { + position: relative; + max-width: 100%; + border-radius: var(--radius-md); + overflow: hidden; + background-color: var(--color-bg-tertiary); +} + +.preview-image { + width: 100%; + height: auto; + display: block; + cursor: pointer; +} + +.gif-badge { + position: absolute; + top: var(--spacing-sm); + left: var(--spacing-sm); + padding: 2px 6px; + font-size: var(--font-size-xs); + font-weight: 600; + background-color: rgba(0, 0, 0, 0.7); + color: white; + border-radius: var(--radius-sm); +} + +.expand-btn { + position: absolute; + bottom: var(--spacing-sm); + right: var(--spacing-sm); + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background-color: rgba(0, 0, 0, 0.7); + color: white; + border-radius: var(--radius-sm); + opacity: 0; + transition: opacity var(--transition-fast); +} + +.image-container:hover .expand-btn { + opacity: 1; +} + +image-lightbox { + position: fixed; + inset: 0; + z-index: var(--z-modal); + opacity: 0; + transition: opacity var(--transition-normal); +} + +image-lightbox.lightbox-visible { + opacity: 1; +} + +.lightbox-backdrop { + position: absolute; + inset: 0; + background-color: var(--color-overlay); +} + +.lightbox-content { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-lg); +} + +.lightbox-image { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.lightbox-close { + position: absolute; + top: var(--spacing-md); + right: var(--spacing-md); + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background-color: rgba(0, 0, 0, 0.5); + color: white; + border-radius: var(--radius-full); +} + +.lightbox-close:hover { + background-color: rgba(0, 0, 0, 0.7); +} + +youtube-embed { + display: block; + max-width: 100%; +} + +.youtube-preview { + position: relative; + aspect-ratio: 16/9; + border-radius: var(--radius-md); + overflow: hidden; + cursor: pointer; + background-color: var(--color-bg-tertiary); +} + +.youtube-thumbnail { + width: 100%; + height: 100%; + object-fit: cover; +} + +.youtube-play { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: none; + border: none; + padding: 0; + transition: transform var(--transition-fast); +} + +.youtube-preview:hover .youtube-play { + transform: translate(-50%, -50%) scale(1.1); +} + +.youtube-badge { + position: absolute; + bottom: var(--spacing-sm); + right: var(--spacing-sm); + padding: 2px 6px; + font-size: var(--font-size-xs); + font-weight: 600; + background-color: #ff0000; + color: white; + border-radius: var(--radius-sm); +} + +.youtube-player { + aspect-ratio: 16/9; + border-radius: var(--radius-md); + overflow: hidden; +} + +.youtube-player iframe { + width: 100%; + height: 100%; + border: none; +} + +link-preview { + display: block; +} + +.link-card { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-md); + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + text-decoration: none; + transition: all var(--transition-fast); +} + +.link-card:hover { + background-color: var(--color-surface-hover); + border-color: var(--color-primary); + text-decoration: none; +} + +.link-icon { + flex-shrink: 0; + color: var(--color-text-muted); +} + +.link-info { + flex: 1; + min-width: 0; +} + +.link-title { + display: block; + font-weight: 500; + color: var(--color-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.link-domain { + display: block; + font-size: var(--font-size-sm); + color: var(--color-text-muted); +} + +.link-external { + flex-shrink: 0; + color: var(--color-text-muted); +} + +@media (max-width: 480px) { + toast-container { + left: var(--spacing-md); + right: var(--spacing-md); + bottom: var(--spacing-md); + max-width: none; + } +} diff --git a/css/components/form.css b/css/components/form.css new file mode 100644 index 0000000..ea903d3 --- /dev/null +++ b/css/components/form.css @@ -0,0 +1,245 @@ +/** + * @fileoverview Form Component Styles for Rantii + * @author retoor + * @description Styles for forms, inputs and modals + * @keywords css, form, input, modal, styles + */ + +login-form, +post-form { + display: block; +} + +.login-form, +.post-form-inner { + background-color: var(--color-surface); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); +} + +.form-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--spacing-lg); +} + +.form-title { + font-size: var(--font-size-xl); +} + +.form-subtitle { + color: var(--color-text-secondary); + margin-top: var(--spacing-xs); +} + +.close-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + color: var(--color-text-secondary); + border-radius: var(--radius-md); +} + +.close-btn:hover { + color: var(--color-text); + background-color: var(--color-surface-hover); +} + +.form-group { + margin-bottom: var(--spacing-md); +} + +.form-label { + display: block; + font-weight: 500; + margin-bottom: var(--spacing-xs); +} + +.form-input { + width: 100%; +} + +.form-checkbox { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.form-checkbox input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--color-primary); +} + +.form-error { + padding: var(--spacing-sm) var(--spacing-md); + background-color: rgba(248, 113, 113, 0.1); + color: var(--color-error); + border-radius: var(--radius-md); + margin-bottom: var(--spacing-md); + font-size: var(--font-size-sm); +} + +.form-submit, +.submit-btn { + width: 100%; + padding: var(--spacing-md); + background-color: var(--color-primary); + color: white; + border-radius: var(--radius-md); + font-weight: 600; + transition: all var(--transition-fast); +} + +.form-submit:hover, +.submit-btn:hover { + background-color: var(--color-primary-hover); +} + +.form-submit:disabled, +.submit-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.post-form-auth { + text-align: center; + padding: var(--spacing-lg); + background-color: var(--color-surface); + border-radius: var(--radius-lg); +} + +.post-form-auth p { + margin-bottom: var(--spacing-md); + color: var(--color-text-secondary); +} + +.post-input { + width: 100%; + min-height: 150px; +} + +.tags-input { + width: 100%; +} + +.modal { + position: fixed; + inset: 0; + z-index: var(--z-modal); + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-md); + opacity: 0; + transition: opacity var(--transition-normal); +} + +.modal.modal-visible { + opacity: 1; +} + +.modal-backdrop { + position: absolute; + inset: 0; + background-color: var(--color-overlay); +} + +.modal-content { + position: relative; + width: 100%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; +} + +login-page { + display: block; +} + +.login-container { + max-width: 400px; + margin: 0 auto; + padding: var(--spacing-lg); +} + +.login-header { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: var(--spacing-xl); + position: relative; +} + +.login-header .back-btn { + position: absolute; + left: 0; + top: 0; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + color: var(--color-text-secondary); + border-radius: var(--radius-md); +} + +.login-header .back-btn:hover { + color: var(--color-text); + background-color: var(--color-surface-hover); +} + +.login-logo { + font-size: var(--font-size-3xl); + font-weight: 700; + color: var(--color-primary); +} + +.logged-in-info { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + background-color: var(--color-surface); + border-radius: var(--radius-lg); + padding: var(--spacing-xl); +} + +.logged-in-info user-avatar { + margin-bottom: var(--spacing-md); +} + +.logged-in-text { + font-size: var(--font-size-lg); + margin-bottom: var(--spacing-lg); +} + +.logged-in-actions { + display: flex; + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); + width: 100%; +} + +.logged-in-actions .btn { + flex: 1; +} + +.switch-text { + color: var(--color-text-muted); + font-size: var(--font-size-sm); + margin-bottom: var(--spacing-sm); +} + +.btn-link { + background: none; + color: var(--color-link); + padding: var(--spacing-sm); +} + +.btn-link:hover { + text-decoration: underline; +} diff --git a/css/components/notification.css b/css/components/notification.css new file mode 100644 index 0000000..8b2a5da --- /dev/null +++ b/css/components/notification.css @@ -0,0 +1,122 @@ +/** + * @fileoverview Notification Component Styles for Rantii + * @author retoor + * @description Styles for notification list and items + * @keywords css, notification, alert, styles + */ + +notification-list { + display: block; +} + +.notification-auth, +.notification-loading, +.notification-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 200px; + text-align: center; + gap: var(--spacing-md); + color: var(--color-text-secondary); +} + +.notification-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-md); + background-color: var(--color-surface); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + border-bottom: 1px solid var(--color-border); +} + +.notification-header h2 { + font-size: var(--font-size-lg); +} + +.clear-btn { + font-size: var(--font-size-sm); + color: var(--color-primary); + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--radius-sm); +} + +.clear-btn:hover { + background-color: rgba(233, 69, 96, 0.1); +} + +.notification-items { + background-color: var(--color-surface); + border-radius: 0 0 var(--radius-lg) var(--radius-lg); +} + +notification-item { + display: block; + cursor: pointer; + border-bottom: 1px solid var(--color-border); + transition: all var(--transition-fast); +} + +notification-item:last-child { + border-bottom: none; + border-radius: 0 0 var(--radius-lg) var(--radius-lg); +} + +notification-item:hover { + background-color: var(--color-surface-hover); +} + +notification-item.unread { + background-color: rgba(233, 69, 96, 0.05); +} + +notification-item.unread::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background-color: var(--color-primary); +} + +.notif-content { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-md); + position: relative; +} + +.notif-icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background-color: var(--color-bg-tertiary); + border-radius: var(--radius-full); + color: var(--color-text-secondary); +} + +.notif-body { + flex: 1; + min-width: 0; +} + +.notif-username { + font-weight: 600; +} + +.notif-action { + color: var(--color-text-secondary); +} + +.notif-time { + flex-shrink: 0; + font-size: var(--font-size-sm); + color: var(--color-text-muted); +} diff --git a/css/components/pages.css b/css/components/pages.css new file mode 100644 index 0000000..2c4c5c9 --- /dev/null +++ b/css/components/pages.css @@ -0,0 +1,151 @@ +/** + * @fileoverview Page Component Styles for Rantii + * @author retoor + * @description Styles for page-level components + * @keywords css, page, layout, styles + */ + +home-page, +weekly-page, +collabs-page, +stories-page { + display: block; +} + +rant-page, +profile-page, +search-page, +notifications-page, +settings-page, +login-page { + display: block; +} + +.page-header { + margin-bottom: var(--spacing-lg); + padding: var(--spacing-md); + background-color: var(--color-surface); + border-radius: var(--radius-lg); +} + +.page-header h1 { + font-size: var(--font-size-xl); +} + +search-page .search-header { + margin-bottom: var(--spacing-lg); +} + +search-page .search-form { + display: flex; + gap: var(--spacing-sm); +} + +search-page .search-input { + flex: 1; +} + +search-page .search-btn { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + background-color: var(--color-primary); + color: white; + border-radius: var(--radius-md); +} + +search-page .search-btn:hover { + background-color: var(--color-primary-hover); +} + +.search-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 300px; + text-align: center; + color: var(--color-text-secondary); + gap: var(--spacing-md); +} + +settings-page .settings-header { + display: flex; + align-items: center; + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); + padding: var(--spacing-md); + background-color: var(--color-surface); + border-radius: var(--radius-lg); +} + +settings-page .settings-header .back-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + color: var(--color-text-secondary); + border-radius: var(--radius-md); +} + +settings-page .settings-header .back-btn:hover { + color: var(--color-text); + background-color: var(--color-surface-hover); +} + +settings-page .settings-header h1 { + font-size: var(--font-size-xl); +} + +.settings-content { + display: flex; + flex-direction: column; + gap: var(--spacing-lg); +} + +.settings-section { + background-color: var(--color-surface); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); +} + +.settings-section h2 { + font-size: var(--font-size-lg); + margin-bottom: var(--spacing-md); +} + +.setting-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-sm) 0; + border-bottom: 1px solid var(--color-border); +} + +.setting-item:last-of-type { + border-bottom: none; + margin-bottom: var(--spacing-md); +} + +.setting-label { + color: var(--color-text-secondary); +} + +.setting-value { + font-weight: 500; +} + +.about-info { + color: var(--color-text-secondary); +} + +.about-info p { + margin-bottom: var(--spacing-xs); +} + +.about-info strong { + color: var(--color-text); +} diff --git a/css/components/rant.css b/css/components/rant.css new file mode 100644 index 0000000..a510d26 --- /dev/null +++ b/css/components/rant.css @@ -0,0 +1,342 @@ +/** + * @fileoverview Rant Component Styles for Rantii + * @author retoor + * @description Styles for rant card, detail and feed + * @keywords css, rant, card, feed, styles + */ + +rant-content { + display: block; +} + +.content-text { + word-wrap: break-word; + overflow-wrap: break-word; +} + +.content-text p { + margin-bottom: var(--spacing-sm); +} + +.content-text p:last-child { + margin-bottom: 0; +} + +.content-text a { + color: var(--color-link); +} + +.content-text code { + background-color: var(--color-code-bg); + padding: 0.125em 0.375em; + border-radius: var(--radius-sm); + font-size: 0.875em; +} + +.content-text pre { + background-color: var(--color-code-bg); + padding: var(--spacing-md); + border-radius: var(--radius-md); + overflow-x: auto; + margin: var(--spacing-md) 0; +} + +.content-text pre code { + background: none; + padding: 0; + font-size: var(--font-size-sm); +} + +.content-images, +.content-videos, +.content-links { + margin-top: var(--spacing-md); + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +rant-card { + display: block; + background-color: var(--color-surface); + border-radius: var(--radius-lg); + cursor: pointer; + transition: all var(--transition-fast); +} + +rant-card:hover { + background-color: var(--color-surface-hover); +} + +.card-content { + padding: var(--spacing-md); +} + +.card-header { + display: flex; + align-items: center; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-sm); +} + +.card-meta { + display: flex; + align-items: center; + gap: var(--spacing-sm); + flex-wrap: wrap; +} + +.card-username { + font-weight: 600; + color: var(--color-text); +} + +.card-score { + font-size: var(--font-size-sm); + color: var(--color-upvote); +} + +.card-time { + font-size: var(--font-size-sm); + color: var(--color-text-muted); +} + +.card-body { + margin-bottom: var(--spacing-md); +} + +.card-image { + margin-top: var(--spacing-md); +} + +.card-footer { + display: flex; + align-items: center; + gap: var(--spacing-md); + flex-wrap: wrap; +} + +.card-comments { + display: flex; + align-items: center; + gap: var(--spacing-xs); + color: var(--color-text-muted); + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--radius-sm); + transition: all var(--transition-fast); +} + +.card-comments:hover { + color: var(--color-text); + background-color: var(--color-surface-hover); +} + +.card-tags { + display: flex; + gap: var(--spacing-xs); + flex-wrap: wrap; + margin-left: auto; +} + +.tag { + padding: 2px 8px; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + background-color: var(--color-bg-tertiary); + border-radius: var(--radius-full); + border: none; + cursor: pointer; + transition: all var(--transition-fast); +} + +.tag:hover { + background-color: var(--color-primary); + color: white; +} + +.rant-card-skeleton { + height: 200px; + background: linear-gradient(90deg, var(--color-surface) 25%, var(--color-surface-hover) 50%, var(--color-surface) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: var(--radius-lg); +} + +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +rant-detail { + display: block; +} + +.rant-detail-loading, +.rant-detail-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 200px; + text-align: center; + gap: var(--spacing-md); +} + +.detail-content { + background-color: var(--color-surface); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); +} + +.detail-header { + display: flex; + align-items: center; + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); +} + +.detail-header .back-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + color: var(--color-text-secondary); + border-radius: var(--radius-md); +} + +.detail-header .back-btn:hover { + color: var(--color-text); + background-color: var(--color-surface-hover); +} + +.detail-title { + font-size: var(--font-size-lg); + font-weight: 600; +} + +.detail-author { + display: flex; + align-items: center; + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); +} + +.author-info { + flex: 1; +} + +.author-username { + display: block; + font-weight: 600; + cursor: pointer; +} + +.author-username:hover { + color: var(--color-primary); +} + +.author-score { + font-size: var(--font-size-sm); + color: var(--color-upvote); +} + +.detail-time { + font-size: var(--font-size-sm); + color: var(--color-text-muted); +} + +.detail-body { + margin-bottom: var(--spacing-lg); +} + +.detail-image { + margin-top: var(--spacing-lg); +} + +.detail-tags { + display: flex; + gap: var(--spacing-sm); + flex-wrap: wrap; + margin-bottom: var(--spacing-lg); +} + +.detail-footer { + display: flex; + align-items: center; + gap: var(--spacing-lg); + padding-top: var(--spacing-md); + border-top: 1px solid var(--color-border); +} + +.detail-comments-count { + color: var(--color-text-secondary); +} + +.comments-section { + margin-top: var(--spacing-lg); +} + +.comments-list { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + margin-top: var(--spacing-lg); +} + +rant-feed { + display: block; +} + +.feed-controls { + margin-bottom: var(--spacing-md); +} + +.sort-tabs { + display: flex; + gap: var(--spacing-xs); + background-color: var(--color-surface); + padding: var(--spacing-xs); + border-radius: var(--radius-md); +} + +.sort-tab { + flex: 1; + padding: var(--spacing-sm) var(--spacing-md); + color: var(--color-text-secondary); + border-radius: var(--radius-sm); + font-weight: 500; + transition: all var(--transition-fast); +} + +.sort-tab:hover { + color: var(--color-text); +} + +.sort-tab.active { + background-color: var(--color-primary); + color: white; +} + +.feed-list { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.feed-loading, +.feed-loadmore { + display: flex; + justify-content: center; + padding: var(--spacing-lg); +} + +.feed-empty { + text-align: center; + padding: var(--spacing-2xl); + color: var(--color-text-secondary); +} + +.load-more-btn { + min-width: 120px; +} diff --git a/css/components/user.css b/css/components/user.css new file mode 100644 index 0000000..e6c9a8a --- /dev/null +++ b/css/components/user.css @@ -0,0 +1,166 @@ +/** + * @fileoverview User Component Styles for Rantii + * @author retoor + * @description Styles for user profile and related components + * @keywords css, user, profile, avatar, styles + */ + +user-profile { + display: block; +} + +.profile-loading, +.profile-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 300px; + text-align: center; + gap: var(--spacing-md); +} + +.profile-header { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-lg); + background-color: var(--color-surface); + border-radius: var(--radius-lg); + margin-bottom: var(--spacing-lg); + position: relative; +} + +.profile-header .back-btn { + position: absolute; + top: var(--spacing-md); + left: var(--spacing-md); + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + color: var(--color-text-secondary); + border-radius: var(--radius-md); +} + +.profile-header .back-btn:hover { + color: var(--color-text); + background-color: var(--color-surface-hover); +} + +.profile-info { + text-align: center; +} + +.profile-username { + font-size: var(--font-size-2xl); + margin-bottom: var(--spacing-xs); +} + +.profile-score { + font-size: var(--font-size-xl); + font-weight: 600; + color: var(--color-upvote); +} + +.profile-details { + background-color: var(--color-surface); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + margin-bottom: var(--spacing-lg); +} + +.detail-item { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + padding: var(--spacing-sm) 0; + border-bottom: 1px solid var(--color-border); +} + +.detail-item:last-child { + border-bottom: none; +} + +.detail-label { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.detail-value { + color: var(--color-text); +} + +.detail-link { + color: var(--color-link); +} + +.profile-content { + background-color: var(--color-surface); + border-radius: var(--radius-lg); +} + +.content-tabs { + display: flex; + border-bottom: 1px solid var(--color-border); +} + +.content-tabs .tab { + flex: 1; + padding: var(--spacing-md); + color: var(--color-text-secondary); + font-weight: 500; + text-align: center; + border-bottom: 2px solid transparent; + transition: all var(--transition-fast); +} + +.content-tabs .tab:hover { + color: var(--color-text); +} + +.content-tabs .tab.active { + color: var(--color-primary); + border-bottom-color: var(--color-primary); +} + +.content-panel { + padding: var(--spacing-md); +} + +.rants-list, +.comments-list { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.empty-message { + text-align: center; + padding: var(--spacing-lg); + color: var(--color-text-secondary); +} + +.profile-comment { + padding: var(--spacing-md); + background-color: var(--color-bg-secondary); + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition-fast); +} + +.profile-comment:hover { + background-color: var(--color-bg-tertiary); +} + +.profile-comment .comment-meta { + display: flex; + gap: var(--spacing-sm); + margin-top: var(--spacing-sm); + font-size: var(--font-size-sm); + color: var(--color-text-muted); +} diff --git a/css/themes/black.css b/css/themes/black.css new file mode 100644 index 0000000..8fd1e72 --- /dev/null +++ b/css/themes/black.css @@ -0,0 +1,31 @@ +/** + * @fileoverview Black Theme for Rantii + * @author retoor + * @description Pure black OLED theme + * @keywords theme, black, oled, colors + */ + +:root.theme-black, +.theme-black { + --color-bg: #000000; + --color-bg-secondary: #0a0a0a; + --color-bg-tertiary: #141414; + --color-surface: #0a0a0a; + --color-surface-hover: #1a1a1a; + --color-border: #2a2a2a; + --color-text: #ffffff; + --color-text-secondary: #a0a0a0; + --color-text-muted: #666666; + --color-primary: #e94560; + --color-primary-hover: #f85070; + --color-secondary: #1a1a1a; + --color-success: #4ade80; + --color-warning: #fbbf24; + --color-error: #f87171; + --color-info: #60a5fa; + --color-upvote: #4ade80; + --color-downvote: #f87171; + --color-link: #60a5fa; + --color-code-bg: #0a0a0a; + --color-overlay: rgba(0, 0, 0, 0.8); +} diff --git a/css/themes/dark.css b/css/themes/dark.css new file mode 100644 index 0000000..40edfeb --- /dev/null +++ b/css/themes/dark.css @@ -0,0 +1,31 @@ +/** + * @fileoverview Dark Theme for Rantii + * @author retoor + * @description Dark color scheme + * @keywords theme, dark, colors, scheme + */ + +:root.theme-dark, +.theme-dark { + --color-bg: #1a1a2e; + --color-bg-secondary: #16213e; + --color-bg-tertiary: #0f3460; + --color-surface: #1f1f38; + --color-surface-hover: #2a2a4a; + --color-border: #3a3a5a; + --color-text: #eaeaea; + --color-text-secondary: #a0a0b0; + --color-text-muted: #707080; + --color-primary: #e94560; + --color-primary-hover: #f85070; + --color-secondary: #0f3460; + --color-success: #4ade80; + --color-warning: #fbbf24; + --color-error: #f87171; + --color-info: #60a5fa; + --color-upvote: #4ade80; + --color-downvote: #f87171; + --color-link: #60a5fa; + --color-code-bg: #0d1117; + --color-overlay: rgba(0, 0, 0, 0.7); +} diff --git a/css/themes/forest.css b/css/themes/forest.css new file mode 100644 index 0000000..7dd856d --- /dev/null +++ b/css/themes/forest.css @@ -0,0 +1,31 @@ +/** + * @fileoverview Forest Theme for Rantii + * @author retoor + * @description Deep forest green theme + * @keywords theme, forest, green, colors + */ + +:root.theme-forest, +.theme-forest { + --color-bg: #0d1f0d; + --color-bg-secondary: #122912; + --color-bg-tertiary: #1a3a1a; + --color-surface: #122912; + --color-surface-hover: #1e451e; + --color-border: #2a5a2a; + --color-text: #e8f5e8; + --color-text-secondary: #a8c8a8; + --color-text-muted: #6a8a6a; + --color-primary: #4caf50; + --color-primary-hover: #66bb6a; + --color-secondary: #1a3a1a; + --color-success: #81c784; + --color-warning: #ffb74d; + --color-error: #e57373; + --color-info: #64b5f6; + --color-upvote: #81c784; + --color-downvote: #e57373; + --color-link: #64b5f6; + --color-code-bg: #0a160a; + --color-overlay: rgba(13, 31, 13, 0.8); +} diff --git a/css/themes/light.css b/css/themes/light.css new file mode 100644 index 0000000..db9feae --- /dev/null +++ b/css/themes/light.css @@ -0,0 +1,31 @@ +/** + * @fileoverview Light Theme for Rantii + * @author retoor + * @description Light color scheme + * @keywords theme, light, colors, scheme + */ + +:root.theme-light, +.theme-light { + --color-bg: #f5f5f7; + --color-bg-secondary: #ffffff; + --color-bg-tertiary: #e8e8ec; + --color-surface: #ffffff; + --color-surface-hover: #f0f0f4; + --color-border: #d1d1d6; + --color-text: #1c1c1e; + --color-text-secondary: #636366; + --color-text-muted: #8e8e93; + --color-primary: #d63651; + --color-primary-hover: #c52d47; + --color-secondary: #e8e8ec; + --color-success: #34c759; + --color-warning: #ff9500; + --color-error: #ff3b30; + --color-info: #007aff; + --color-upvote: #34c759; + --color-downvote: #ff3b30; + --color-link: #007aff; + --color-code-bg: #f6f8fa; + --color-overlay: rgba(0, 0, 0, 0.5); +} diff --git a/css/themes/ocean.css b/css/themes/ocean.css new file mode 100644 index 0000000..89a954e --- /dev/null +++ b/css/themes/ocean.css @@ -0,0 +1,31 @@ +/** + * @fileoverview Ocean Theme for Rantii + * @author retoor + * @description Deep ocean blue theme + * @keywords theme, ocean, blue, colors + */ + +:root.theme-ocean, +.theme-ocean { + --color-bg: #0a1929; + --color-bg-secondary: #0d2137; + --color-bg-tertiary: #132f4c; + --color-surface: #0d2137; + --color-surface-hover: #173a5e; + --color-border: #1e4976; + --color-text: #e7ebf0; + --color-text-secondary: #b2bac2; + --color-text-muted: #6f7e8c; + --color-primary: #5090d3; + --color-primary-hover: #66a6e8; + --color-secondary: #132f4c; + --color-success: #66bb6a; + --color-warning: #ffa726; + --color-error: #ef5350; + --color-info: #42a5f5; + --color-upvote: #66bb6a; + --color-downvote: #ef5350; + --color-link: #42a5f5; + --color-code-bg: #001e3c; + --color-overlay: rgba(10, 25, 41, 0.8); +} diff --git a/css/themes/sunset.css b/css/themes/sunset.css new file mode 100644 index 0000000..efb6b7a --- /dev/null +++ b/css/themes/sunset.css @@ -0,0 +1,31 @@ +/** + * @fileoverview Sunset Theme for Rantii + * @author retoor + * @description Warm sunset orange theme + * @keywords theme, sunset, orange, colors + */ + +:root.theme-sunset, +.theme-sunset { + --color-bg: #1f1410; + --color-bg-secondary: #2a1a14; + --color-bg-tertiary: #3d261c; + --color-surface: #2a1a14; + --color-surface-hover: #453024; + --color-border: #5a4030; + --color-text: #fef3e8; + --color-text-secondary: #d4b8a0; + --color-text-muted: #8a7060; + --color-primary: #ff7043; + --color-primary-hover: #ff8a65; + --color-secondary: #3d261c; + --color-success: #aed581; + --color-warning: #ffca28; + --color-error: #ef5350; + --color-info: #4fc3f7; + --color-upvote: #aed581; + --color-downvote: #ef5350; + --color-link: #4fc3f7; + --color-code-bg: #170e0a; + --color-overlay: rgba(31, 20, 16, 0.8); +} diff --git a/css/themes/white.css b/css/themes/white.css new file mode 100644 index 0000000..73c306a --- /dev/null +++ b/css/themes/white.css @@ -0,0 +1,31 @@ +/** + * @fileoverview White Theme for Rantii + * @author retoor + * @description Pure white minimalist theme + * @keywords theme, white, minimal, colors + */ + +:root.theme-white, +.theme-white { + --color-bg: #ffffff; + --color-bg-secondary: #fafafa; + --color-bg-tertiary: #f0f0f0; + --color-surface: #ffffff; + --color-surface-hover: #f5f5f5; + --color-border: #e0e0e0; + --color-text: #000000; + --color-text-secondary: #505050; + --color-text-muted: #909090; + --color-primary: #c52d47; + --color-primary-hover: #a82540; + --color-secondary: #f0f0f0; + --color-success: #2e8b57; + --color-warning: #cc7700; + --color-error: #cc3333; + --color-info: #0066cc; + --color-upvote: #2e8b57; + --color-downvote: #cc3333; + --color-link: #0066cc; + --color-code-bg: #f5f5f5; + --color-overlay: rgba(0, 0, 0, 0.4); +} diff --git a/css/variables.css b/css/variables.css new file mode 100644 index 0000000..fa67e90 --- /dev/null +++ b/css/variables.css @@ -0,0 +1,57 @@ +/** + * @fileoverview CSS Variables for Rantii + * @author retoor + * @description Design tokens and CSS custom properties + * @keywords css, variables, tokens, design, colors + */ + +:root { + --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + --font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace; + + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-md: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 2rem; + + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; + + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-full: 9999px; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15); + + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; + --transition-slow: 350ms ease; + + --z-dropdown: 100; + --z-sticky: 200; + --z-modal: 300; + --z-toast: 400; + + --header-height: 56px; + --nav-width: 240px; + --max-content-width: 680px; +} + +@media (max-width: 768px) { + :root { + --header-height: 52px; + --nav-width: 280px; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..67d269e --- /dev/null +++ b/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + Rantii - DevRant Client + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + + diff --git a/js/api/client.js b/js/api/client.js new file mode 100644 index 0000000..b8e6bd3 --- /dev/null +++ b/js/api/client.js @@ -0,0 +1,282 @@ +/** + * @fileoverview DevRant API Client for Rantii + * @author retoor + * @description HTTP client for communicating with the DevRant API + * @keywords api, client, devrant, http, fetch + */ + +const API_BASE_URL = window.location.hostname === 'localhost' ? '/api/' : 'https://dr.molodetz.nl/api/'; +const APP_ID = 3; + +class ApiClient { + constructor() { + this.tokenId = null; + this.tokenKey = null; + this.userId = null; + } + + setAuth(tokenId, tokenKey, userId) { + this.tokenId = tokenId; + this.tokenKey = tokenKey; + this.userId = userId; + } + + clearAuth() { + this.tokenId = null; + this.tokenKey = null; + this.userId = null; + } + + isAuthenticated() { + return this.tokenId !== null && this.tokenKey !== null; + } + + buildParams(params = {}) { + const result = { app: APP_ID, ...params }; + if (this.isAuthenticated()) { + result.token_id = this.tokenId; + result.token_key = this.tokenKey; + result.user_id = this.userId; + } + return result; + } + + buildUrl(endpoint) { + const path = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint; + return API_BASE_URL + path; + } + + async request(method, endpoint, params = {}, timeout = 30000) { + const url = this.buildUrl(endpoint); + const allParams = this.buildParams(params); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const config = { + method, + headers: {}, + signal: controller.signal + }; + + try { + let fullUrl = url; + if (method === 'GET' || method === 'DELETE') { + const queryString = new URLSearchParams(allParams).toString(); + fullUrl = queryString ? `${url}?${queryString}` : url; + } else { + config.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + config.body = new URLSearchParams(allParams).toString(); + } + + const response = await fetch(fullUrl, config); + clearTimeout(timeoutId); + + if (!response.ok && response.status >= 500) { + return { success: false, error: `Server error: ${response.status}` }; + } + + const data = await response.json(); + return data; + } catch (error) { + clearTimeout(timeoutId); + if (error.name === 'AbortError') { + return { success: false, error: 'Request timed out' }; + } + return { success: false, error: error.message }; + } + } + + async get(endpoint, params = {}) { + return this.request('GET', endpoint, params); + } + + async post(endpoint, params = {}) { + return this.request('POST', endpoint, params); + } + + async delete(endpoint, params = {}) { + return this.request('DELETE', endpoint, params); + } + + async login(username, password) { + const response = await this.post('/users/auth-token', { + username, + password + }); + if (response.success && response.auth_token) { + this.setAuth( + response.auth_token.id, + response.auth_token.key, + response.auth_token.user_id + ); + return { + success: true, + authToken: response.auth_token + }; + } + return { success: false, error: response.error || 'Login failed' }; + } + + async getRants(sort = 'recent', limit = 20, skip = 0) { + const response = await this.get('/devrant/rants', { sort, limit, skip }); + if (response.success) { + return { success: true, rants: response.rants || [] }; + } + return { success: false, rants: [], error: response.error }; + } + + async getRant(rantId) { + const response = await this.get(`/devrant/rants/${rantId}`); + if (response.success) { + return { + success: true, + rant: response.rant, + comments: response.comments || [] + }; + } + return { success: false, error: response.error }; + } + + async voteRant(rantId, vote, reason = null) { + const params = { vote }; + if (reason !== null) { + params.reason = reason; + } + const response = await this.post(`/devrant/rants/${rantId}/vote`, params); + return { success: response.success, rant: response.rant }; + } + + async postComment(rantId, comment) { + const response = await this.post(`/devrant/rants/${rantId}/comments`, { + comment, + plat: 2 + }); + return { success: response.success, comment: response.comment }; + } + + async updateComment(commentId, comment) { + const response = await this.post(`/comments/${commentId}`, { comment }); + return { success: response.success }; + } + + async deleteComment(commentId) { + const response = await this.delete(`/comments/${commentId}`); + return { success: response.success }; + } + + async voteComment(commentId, vote, reason = null) { + const params = { vote }; + if (reason !== null) { + params.reason = reason; + } + const response = await this.post(`/comments/${commentId}/vote`, params); + return { success: response.success, comment: response.comment }; + } + + async getComment(commentId) { + const response = await this.get(`/comments/${commentId}`); + if (response.success) { + return { success: true, comment: response.comment }; + } + return { success: false, error: response.error }; + } + + async getUserId(username) { + const response = await this.get('/get-user-id', { username }); + if (response.success) { + return { success: true, userId: response.user_id }; + } + return { success: false, error: response.error }; + } + + async getProfile(userId) { + const response = await this.get(`/users/${userId}`); + if (response.success) { + return { success: true, profile: response.profile }; + } + return { success: false, error: response.error }; + } + + async getProfileByUsername(username) { + const userIdResponse = await this.getUserId(username); + if (!userIdResponse.success) { + return { success: false, error: userIdResponse.error }; + } + return this.getProfile(userIdResponse.userId); + } + + async search(term) { + const response = await this.get('/devrant/search', { term }); + if (response.success) { + return { success: true, rants: response.results || [] }; + } + return { success: false, rants: [], error: response.error }; + } + + async getNotifications() { + const response = await this.get('/users/me/notif-feed', { ext_prof: 1 }); + if (response.success) { + const items = response.data?.items || []; + const users = response.data?.username_map || {}; + + const notifications = items.map(item => ({ + ...item, + username: users[item.uid] || item.username || 'Someone' + })); + + return { + success: true, + notifications, + unread: response.data?.unread || { total: 0 } + }; + } + return { success: false, notifications: [], error: response.error }; + } + + async clearNotifications() { + const response = await this.delete('/users/me/notif-feed'); + return { success: response.success }; + } + + async postRant(text, tags = '', type = 1) { + const response = await this.post('/devrant/rants', { + rant: text, + tags, + type, + plat: 2 + }); + return { success: response.success, rantId: response.rant_id }; + } + + async deleteRant(rantId) { + const response = await this.delete(`/devrant/rants/${rantId}`); + return { success: response.success }; + } + + async getWeeklyRants(sort = 'recent', limit = 20, skip = 0) { + const response = await this.get('/devrant/weekly-rants', { sort, limit, skip }); + if (response.success) { + return { success: true, rants: response.rants || [] }; + } + return { success: false, rants: [], error: response.error }; + } + + async getCollabs(sort = 'recent', limit = 20, skip = 0) { + const response = await this.get('/devrant/collabs', { sort, limit, skip }); + if (response.success) { + return { success: true, rants: response.rants || [] }; + } + return { success: false, rants: [], error: response.error }; + } + + async getStories(sort = 'recent', limit = 20, skip = 0) { + const response = await this.get('/devrant/story-rants', { sort, limit, skip }); + if (response.success) { + return { success: true, rants: response.rants || [] }; + } + return { success: false, rants: [], error: response.error }; + } +} + +export { ApiClient, API_BASE_URL, APP_ID }; diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..00d9dd6 --- /dev/null +++ b/js/app.js @@ -0,0 +1,315 @@ +/** + * @fileoverview Main Application Class for Rantii + * @author retoor + * @description Core application controller and initialization + * @keywords app, application, main, controller, core + */ + +import { ApiClient } from './api/client.js'; +import { StorageService } from './services/storage.js'; +import { AuthService } from './services/auth.js'; +import { Router } from './services/router.js'; +import { ThemeService } from './services/theme.js'; +import { markdownRenderer } from './utils/markdown.js'; + +import { BaseComponent } from './components/base-component.js'; +import { AppHeader } from './components/app-header.js'; +import { AppNav } from './components/app-nav.js'; +import { LoadingSpinner } from './components/loading-spinner.js'; +import { ToastNotification, ToastContainer } from './components/toast-notification.js'; +import { UserAvatar } from './components/user-avatar.js'; +import { VoteButtons } from './components/vote-buttons.js'; +import { ThemeSelector } from './components/theme-selector.js'; +import { LoginForm } from './components/login-form.js'; +import { ImagePreview, ImageLightbox } from './components/image-preview.js'; +import { YoutubeEmbed } from './components/youtube-embed.js'; +import { LinkPreview } from './components/link-preview.js'; +import { RantContent } from './components/rant-content.js'; +import { RantCard } from './components/rant-card.js'; +import { RantDetail } from './components/rant-detail.js'; +import { RantFeed } from './components/rant-feed.js'; +import { CommentItem } from './components/comment-item.js'; +import { CommentForm } from './components/comment-form.js'; +import { UserProfile } from './components/user-profile.js'; +import { NotificationList, NotificationItem } from './components/notification-list.js'; +import { PostForm, PostModal } from './components/post-form.js'; + +import { HomePage } from './pages/home-page.js'; +import { RantPage } from './pages/rant-page.js'; +import { ProfilePage } from './pages/profile-page.js'; +import { SearchPage } from './pages/search-page.js'; +import { NotificationsPage } from './pages/notifications-page.js'; +import { SettingsPage } from './pages/settings-page.js'; +import { LoginPage } from './pages/login-page.js'; +import { WeeklyPage } from './pages/weekly-page.js'; +import { CollabsPage } from './pages/collabs-page.js'; +import { StoriesPage } from './pages/stories-page.js'; + +class Application { + constructor() { + this.api = new ApiClient(); + this.storage = new StorageService(); + this.auth = new AuthService(this.api, this.storage); + this.router = new Router(); + this.theme = new ThemeService(this.storage); + this.toast = null; + this.header = null; + this.nav = null; + this.main = null; + this.currentPage = null; + this.isNavOpen = false; + } + + async init() { + await this.auth.init(); + this.theme.init(); + await markdownRenderer.init(); + + this.setupToast(); + this.setupHeader(); + this.setupNav(); + this.setupMain(); + this.setupRouter(); + this.setupEventListeners(); + + this.router.init(); + + if ('serviceWorker' in navigator) { + this.registerServiceWorker(); + } + } + + setupToast() { + this.toast = document.createElement('toast-container'); + document.body.appendChild(this.toast); + } + + setupHeader() { + this.header = document.querySelector('app-header'); + if (!this.header) { + this.header = document.createElement('app-header'); + document.body.insertBefore(this.header, document.body.firstChild); + } + } + + setupNav() { + this.nav = document.querySelector('app-nav'); + if (!this.nav) { + this.nav = document.createElement('app-nav'); + this.header.after(this.nav); + } + } + + setupMain() { + this.main = document.querySelector('main'); + if (!this.main) { + this.main = document.createElement('main'); + this.main.id = 'app-main'; + this.main.className = 'app-main'; + this.nav.after(this.main); + } + } + + setupRouter() { + this.router.register('home', (params) => this.showPage('home', params)); + this.router.register('rant', (params) => this.showPage('rant', params)); + this.router.register('user', (params) => this.showPage('user', params)); + this.router.register('search', (params) => this.showPage('search', params)); + this.router.register('notifications', (params) => this.showPage('notifications', params)); + this.router.register('settings', (params) => this.showPage('settings', params)); + this.router.register('login', (params) => this.showPage('login', params)); + this.router.register('weekly', (params) => this.showPage('weekly', params)); + this.router.register('collabs', (params) => this.showPage('collabs', params)); + this.router.register('stories', (params) => this.showPage('stories', params)); + } + + setupEventListeners() { + window.addEventListener('rantii:auth-change', () => this.onAuthChange()); + window.addEventListener('rantii:require-auth', () => this.router.goToLogin()); + + if (this.header) { + this.header.addEventListener('menu-toggle', () => this.toggleNav()); + this.header.addEventListener('create-post', () => this.openPostModal()); + this.header.addEventListener('user-menu', () => this.toggleUserMenu()); + } + + if (this.nav) { + this.nav.addEventListener('nav-click', () => this.closeNav()); + } + + document.addEventListener('click', (e) => { + if (this.isNavOpen && !e.target.closest('app-nav') && !e.target.closest('.menu-toggle')) { + this.closeNav(); + } + }); + } + + showPage(routeName, params) { + if (this.currentPage) { + this.currentPage.remove(); + } + + let page; + switch (routeName) { + case 'home': + page = document.createElement('home-page'); + break; + case 'rant': + page = document.createElement('rant-page'); + page.setAttribute('rant-id', params.rant); + if (params.comment) { + page.setAttribute('comment-id', params.comment); + } + break; + case 'user': + page = document.createElement('profile-page'); + page.setAttribute('username', params.user); + break; + case 'search': + page = document.createElement('search-page'); + if (params.search) { + page.setAttribute('query', params.search); + } + break; + case 'notifications': + page = document.createElement('notifications-page'); + break; + case 'settings': + page = document.createElement('settings-page'); + break; + case 'login': + page = document.createElement('login-page'); + break; + case 'weekly': + page = document.createElement('weekly-page'); + break; + case 'collabs': + page = document.createElement('collabs-page'); + break; + case 'stories': + page = document.createElement('stories-page'); + break; + default: + page = document.createElement('home-page'); + } + + this.main.appendChild(page); + this.currentPage = page; + this.nav?.setActive(routeName); + this.closeNav(); + + window.scrollTo(0, 0); + } + + toggleNav() { + this.isNavOpen = !this.isNavOpen; + if (this.nav) { + this.nav.toggleClass('nav-open', this.isNavOpen); + } + document.body.classList.toggle('nav-open', this.isNavOpen); + } + + openNav() { + this.isNavOpen = true; + if (this.nav) { + this.nav.addClass('nav-open'); + } + document.body.classList.add('nav-open'); + } + + closeNav() { + this.isNavOpen = false; + if (this.nav) { + this.nav.removeClass('nav-open'); + } + document.body.classList.remove('nav-open'); + } + + openPostModal() { + if (!this.auth.isLoggedIn()) { + this.router.goToLogin(); + return; + } + const modal = document.createElement('post-modal'); + modal.open(); + } + + toggleUserMenu() { + const username = this.auth.getUsername(); + if (username) { + this.router.goToUser(username); + } + } + + onAuthChange() { + if (this.header) { + this.header.render(); + } + if (this.nav) { + this.nav.render(); + } + this.updateNotificationBadge(); + } + + async updateNotificationBadge() { + if (!this.auth.isLoggedIn()) return; + + try { + const result = await this.api.getNotifications(); + if (result?.success) { + const count = result.unread?.total || 0; + if (this.header) { + this.header.setNotificationCount(count); + } + if (this.nav) { + this.nav.setNotificationBadge(count); + } + } + } catch (error) {} + } + + async registerServiceWorker() { + try { + const basePath = this.getBasePath(); + await navigator.serviceWorker.register(`${basePath}sw.js`); + } catch (error) {} + } + + getBasePath() { + const path = window.location.pathname; + const lastSlash = path.lastIndexOf('/'); + return path.substring(0, lastSlash + 1); + } + + showToast(message, type = 'info') { + if (this.toast) { + this.toast.show(message, type); + } + } + + success(message) { + this.showToast(message, 'success'); + } + + error(message) { + this.showToast(message, 'error'); + } + + warning(message) { + this.showToast(message, 'warning'); + } + + info(message) { + this.showToast(message, 'info'); + } +} + +const app = new Application(); + +document.addEventListener('DOMContentLoaded', () => { + app.init(); +}); + +window.app = app; + +export { Application, app }; diff --git a/js/components/app-header.js b/js/components/app-header.js new file mode 100644 index 0000000..d79c4a1 --- /dev/null +++ b/js/components/app-header.js @@ -0,0 +1,149 @@ +/** + * @fileoverview Application Header Component for Rantii + * @author retoor + * @description Main navigation header with search and user controls + * @keywords header, navigation, search, menu, toolbar + */ + +import { BaseComponent } from './base-component.js'; + +class AppHeader extends BaseComponent { + static get observedAttributes() { + return ['logged-in']; + } + + init() { + this.render(); + this.bindEvents(); + } + + render() { + const isLoggedIn = this.isLoggedIn(); + const user = this.getCurrentUser(); + + this.setHtml(` +
+ +
+
+ + +
+
+
+ ${isLoggedIn ? ` + + + + ` : ` + + `} + +
+
+ `); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + this.on(this, 'submit', this.handleSubmit); + + const searchInput = this.$('.search-input'); + if (searchInput) { + this.on(searchInput, 'keydown', this.handleSearchKeydown); + } + + window.addEventListener('rantii:auth-change', () => this.render()); + } + + handleClick(e) { + const target = e.target.closest('button, a'); + if (!target) return; + + if (target.classList.contains('menu-toggle')) { + e.preventDefault(); + this.emit('menu-toggle'); + } else if (target.classList.contains('logo') || target.closest('.logo')) { + e.preventDefault(); + this.getRouter()?.goHome(); + } else if (target.classList.contains('search-btn')) { + this.performSearch(); + } else if (target.classList.contains('notifications-btn')) { + this.getRouter()?.goToNotifications(); + } else if (target.classList.contains('post-btn')) { + this.emit('create-post'); + } else if (target.classList.contains('user-btn')) { + this.emit('user-menu'); + } else if (target.classList.contains('login-btn')) { + this.getRouter()?.goToLogin(); + } else if (target.classList.contains('theme-btn')) { + this.getTheme()?.toggleDarkLight(); + } + } + + handleSearchKeydown(e) { + if (e.key === 'Enter') { + e.preventDefault(); + this.performSearch(); + } + } + + handleSubmit(e) { + e.preventDefault(); + } + + performSearch() { + const input = this.$('.search-input'); + if (input && input.value.trim()) { + this.getRouter()?.goToSearch(input.value.trim()); + } + } + + setNotificationCount(count) { + const badge = this.$('.notification-badge'); + if (badge) { + badge.textContent = count > 99 ? '99+' : count; + badge.hidden = count === 0; + } + } + + clearSearch() { + const input = this.$('.search-input'); + if (input) { + input.value = ''; + } + } +} + +customElements.define('app-header', AppHeader); + +export { AppHeader }; diff --git a/js/components/app-nav.js b/js/components/app-nav.js new file mode 100644 index 0000000..13e2947 --- /dev/null +++ b/js/components/app-nav.js @@ -0,0 +1,207 @@ +/** + * @fileoverview Application Navigation Component for Rantii + * @author retoor + * @description Side navigation menu with main app sections + * @keywords navigation, menu, sidebar, nav, links + */ + +import { BaseComponent } from './base-component.js'; + +class AppNav extends BaseComponent { + static get observedAttributes() { + return ['active', 'expanded']; + } + + init() { + this.render(); + this.bindEvents(); + } + + render() { + const active = this.getAttr('active') || 'home'; + const isLoggedIn = this.isLoggedIn(); + + this.setHtml(` + + `); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + window.addEventListener('rantii:auth-change', () => this.render()); + window.addEventListener('rantii:route-change', (e) => { + this.setAttr('active', e.detail.route); + this.render(); + }); + } + + handleClick(e) { + const link = e.target.closest('.nav-link'); + if (!link) return; + + e.preventDefault(); + const route = link.dataset.route; + + switch (route) { + case 'home': + this.getRouter()?.goHome(); + break; + case 'weekly': + this.getRouter()?.goToWeekly(); + break; + case 'collabs': + this.getRouter()?.goToCollabs(); + break; + case 'stories': + this.getRouter()?.goToStories(); + break; + case 'search': + this.getRouter()?.goToSearch(); + break; + case 'notifications': + this.getRouter()?.goToNotifications(); + break; + case 'profile': + const username = this.getCurrentUser()?.username; + if (username) { + this.getRouter()?.goToUser(username); + } + break; + case 'settings': + this.getRouter()?.goToSettings(); + break; + case 'login': + this.getRouter()?.goToLogin(); + break; + case 'logout': + this.getAuth()?.logout(); + this.getRouter()?.goHome(); + break; + } + + this.emit('nav-click', { route }); + } + + setActive(route) { + this.setAttr('active', route); + this.render(); + } + + setNotificationBadge(count) { + const badge = this.$('.nav-badge'); + if (badge) { + badge.textContent = count > 99 ? '99+' : count; + badge.hidden = count === 0; + } + } + + expand() { + this.setAttr('expanded', ''); + } + + collapse() { + this.removeAttribute('expanded'); + } + + toggle() { + if (this.hasAttr('expanded')) { + this.collapse(); + } else { + this.expand(); + } + } +} + +customElements.define('app-nav', AppNav); + +export { AppNav }; diff --git a/js/components/base-component.js b/js/components/base-component.js new file mode 100644 index 0000000..3486c37 --- /dev/null +++ b/js/components/base-component.js @@ -0,0 +1,220 @@ +/** + * @fileoverview Base Component Class for Rantii + * @author retoor + * @description Foundation class for all custom HTML elements + * @keywords component, web component, custom element, base, foundation + */ + +class BaseComponent extends HTMLElement { + constructor() { + super(); + this.isInitialized = false; + this.eventListeners = []; + } + + connectedCallback() { + if (!this.isInitialized) { + this.init(); + this.isInitialized = true; + } + this.onConnected(); + } + + disconnectedCallback() { + this.cleanup(); + this.onDisconnected(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue !== newValue) { + this.onAttributeChanged(name, oldValue, newValue); + } + } + + init() {} + + onConnected() {} + + onDisconnected() {} + + onAttributeChanged(name, oldValue, newValue) {} + + render() {} + + update(data) { + Object.assign(this, data); + this.render(); + } + + $(selector) { + return this.querySelector(selector); + } + + $$(selector) { + return this.querySelectorAll(selector); + } + + on(target, event, handler, options = {}) { + const boundHandler = handler.bind(this); + target.addEventListener(event, boundHandler, options); + this.eventListeners.push({ target, event, handler: boundHandler, options }); + return boundHandler; + } + + off(target, event, handler) { + target.removeEventListener(event, handler); + this.eventListeners = this.eventListeners.filter( + l => !(l.target === target && l.event === event && l.handler === handler) + ); + } + + cleanup() { + this.eventListeners.forEach(({ target, event, handler, options }) => { + target.removeEventListener(event, handler, options); + }); + this.eventListeners = []; + } + + emit(eventName, detail = {}) { + const event = new CustomEvent(eventName, { + bubbles: true, + composed: true, + detail + }); + this.dispatchEvent(event); + } + + setHtml(html) { + this.innerHTML = html; + } + + setText(text) { + this.textContent = text; + } + + show() { + this.style.display = ''; + this.removeAttribute('hidden'); + } + + hide() { + this.style.display = 'none'; + } + + toggle(visible) { + if (visible !== undefined) { + visible ? this.show() : this.hide(); + } else { + this.style.display === 'none' ? this.show() : this.hide(); + } + } + + addClass(...classNames) { + this.classList.add(...classNames); + } + + removeClass(...classNames) { + this.classList.remove(...classNames); + } + + toggleClass(className, force) { + this.classList.toggle(className, force); + } + + hasClass(className) { + return this.classList.contains(className); + } + + setData(key, value) { + this.dataset[key] = value; + } + + getData(key) { + return this.dataset[key]; + } + + getAttr(name) { + return this.getAttribute(name); + } + + setAttr(name, value) { + if (value === null || value === undefined) { + this.removeAttribute(name); + } else { + this.setAttribute(name, value); + } + } + + hasAttr(name) { + return this.hasAttribute(name); + } + + async waitForInit() { + return new Promise(resolve => { + if (this.isInitialized) { + resolve(); + } else { + const observer = new MutationObserver(() => { + if (this.isInitialized) { + observer.disconnect(); + resolve(); + } + }); + observer.observe(this, { childList: true, subtree: true }); + } + }); + } + + debounce(func, wait) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; + } + + throttle(func, limit) { + let inThrottle; + return (...args) => { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + } + + getApp() { + return window.app; + } + + getApi() { + return window.app?.api; + } + + getAuth() { + return window.app?.auth; + } + + getRouter() { + return window.app?.router; + } + + getStorage() { + return window.app?.storage; + } + + getTheme() { + return window.app?.theme; + } + + isLoggedIn() { + return window.app?.auth?.isLoggedIn() || false; + } + + getCurrentUser() { + return window.app?.auth?.getUser() || null; + } +} + +export { BaseComponent }; diff --git a/js/components/comment-form.js b/js/components/comment-form.js new file mode 100644 index 0000000..92103e4 --- /dev/null +++ b/js/components/comment-form.js @@ -0,0 +1,180 @@ +/** + * @fileoverview Comment Form Component for Rantii + * @author retoor + * @description Form for posting new comments + * @keywords comment, form, post, reply, input + */ + +import { BaseComponent } from './base-component.js'; + +class CommentForm extends BaseComponent { + static get observedAttributes() { + return ['rant-id']; + } + + init() { + this.isSubmitting = false; + this.render(); + this.bindEvents(); + this.loadDraft(); + } + + render() { + const rantId = this.getAttr('rant-id'); + const isLoggedIn = this.isLoggedIn(); + + this.addClass('comment-form'); + + if (!isLoggedIn) { + this.setHtml(` +
+

Sign in to comment

+ +
+ `); + return; + } + + this.setHtml(` +
+
+ +
+
+ 0 / 5000 + +
+
+ `); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + this.on(this, 'submit', this.handleSubmit); + this.on(this, 'input', this.handleInput); + } + + handleClick(e) { + const loginBtn = e.target.closest('.login-btn'); + if (loginBtn) { + this.getRouter()?.goToLogin(); + } + } + + async handleSubmit(e) { + e.preventDefault(); + + if (this.isSubmitting) return; + + const textarea = this.$('.comment-input'); + if (!textarea) return; + + const text = textarea.value.trim(); + if (!text) return; + + const rantId = this.getAttr('rant-id'); + if (!rantId) return; + + this.isSubmitting = true; + this.render(); + + try { + const result = await this.getApi()?.postComment(rantId, text); + if (result?.success) { + this.clearDraft(); + this.emit('comment-posted', { rantId, comment: result.comment }); + this.isSubmitting = false; + this.render(); + } else { + this.getApp()?.toast?.error(result?.error || 'Failed to post comment'); + this.isSubmitting = false; + this.render(); + } + } catch (error) { + this.getApp()?.toast?.error('Failed to post comment'); + this.isSubmitting = false; + this.render(); + } + } + + handleInput(e) { + if (e.target.classList.contains('comment-input')) { + const textarea = e.target; + const count = textarea.value.length; + const countEl = this.$('.char-count'); + if (countEl) { + countEl.textContent = `${count} / 5000`; + } + this.saveDraft(textarea.value); + } + } + + loadDraft() { + const rantId = this.getAttr('rant-id'); + if (rantId) { + const draft = this.getStorage()?.getDraftComment(rantId); + if (draft) { + const textarea = this.$('.comment-input'); + if (textarea) { + textarea.value = draft; + const countEl = this.$('.char-count'); + if (countEl) { + countEl.textContent = `${draft.length} / 5000`; + } + } + } + } + } + + saveDraft(text) { + const rantId = this.getAttr('rant-id'); + if (rantId) { + this.getStorage()?.setDraftComment(rantId, text); + } + } + + clearDraft() { + const rantId = this.getAttr('rant-id'); + if (rantId) { + this.getStorage()?.clearDraftComment(rantId); + } + } + + reset() { + const textarea = this.$('.comment-input'); + if (textarea) { + textarea.value = ''; + } + const countEl = this.$('.char-count'); + if (countEl) { + countEl.textContent = '0 / 5000'; + } + this.clearDraft(); + } + + focus() { + const textarea = this.$('.comment-input'); + if (textarea) { + textarea.focus(); + } + } + + onAttributeChanged(name, oldValue, newValue) { + if (name === 'rant-id') { + this.loadDraft(); + } + } +} + +customElements.define('comment-form', CommentForm); + +export { CommentForm }; diff --git a/js/components/comment-item.js b/js/components/comment-item.js new file mode 100644 index 0000000..a2a04f4 --- /dev/null +++ b/js/components/comment-item.js @@ -0,0 +1,232 @@ +/** + * @fileoverview Comment Item Component for Rantii + * @author retoor + * @description Single comment display with voting + * @keywords comment, item, reply, discussion, vote + */ + +import { BaseComponent } from './base-component.js'; +import { formatRelativeTime } from '../utils/date.js'; + +class CommentItem extends BaseComponent { + static get observedAttributes() { + return ['comment-id']; + } + + init() { + this.commentData = null; + this.isEditing = false; + this.render(); + this.bindEvents(); + } + + setComment(comment) { + this.commentData = comment; + this.setAttr('comment-id', comment.id); + this.render(); + } + + render() { + if (!this.commentData) { + this.setHtml('
'); + return; + } + + const comment = this.commentData; + const isOwner = this.getCurrentUser()?.id === comment.user_id; + + this.addClass('comment-item'); + + if (this.isEditing) { + this.setHtml(` +
+ +
+ + +
+
+ `); + return; + } + + this.setHtml(` +
+
+ + +
+ ${comment.user_username} + +${comment.user_score} + +
+ ${isOwner ? ` +
+ + +
+ ` : ''} +
+
+ +
+
+ + +
+
+ `); + } + + escapeAttr(str) { + if (!str) return ''; + return str + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + this.on(this, 'vote', this.handleVote); + } + + handleClick(e) { + const editBtn = e.target.closest('.edit-btn'); + const deleteBtn = e.target.closest('.delete-btn'); + const cancelBtn = e.target.closest('.cancel-edit-btn'); + const saveBtn = e.target.closest('.save-edit-btn'); + const username = e.target.closest('.comment-username'); + const avatar = e.target.closest('user-avatar'); + + if (editBtn) { + this.startEditing(); + return; + } + + if (deleteBtn) { + this.confirmDelete(); + return; + } + + if (cancelBtn) { + this.cancelEditing(); + return; + } + + if (saveBtn) { + this.saveEdit(); + return; + } + + if (username || avatar) { + this.getRouter()?.goToUser(this.commentData.user_username); + } + } + + async handleVote(e) { + const { vote, itemId } = e.detail; + + const voteButtons = this.$('vote-buttons'); + if (voteButtons) { + voteButtons.disable(); + } + + try { + const result = await this.getApi()?.voteComment(itemId, vote); + if (result?.success && result.comment) { + this.commentData.score = result.comment.score; + this.commentData.vote_state = result.comment.vote_state; + if (voteButtons) { + voteButtons.updateVote(result.comment.score, result.comment.vote_state); + voteButtons.enable(); + } + } + } catch (error) { + if (voteButtons) { + voteButtons.enable(); + } + } + } + + startEditing() { + this.isEditing = true; + this.render(); + const textarea = this.$('.comment-edit-input'); + if (textarea) { + textarea.focus(); + textarea.setSelectionRange(textarea.value.length, textarea.value.length); + } + } + + cancelEditing() { + this.isEditing = false; + this.render(); + } + + async saveEdit() { + const textarea = this.$('.comment-edit-input'); + if (!textarea) return; + + const newBody = textarea.value.trim(); + if (!newBody || newBody === this.commentData.body) { + this.cancelEditing(); + return; + } + + try { + const result = await this.getApi()?.updateComment(this.commentData.id, newBody); + if (result?.success) { + this.commentData.body = newBody; + this.isEditing = false; + this.render(); + this.emit('comment-updated', { comment: this.commentData }); + } + } catch (error) { + this.getApp()?.toast?.error('Failed to update comment'); + } + } + + confirmDelete() { + if (confirm('Delete this comment?')) { + this.deleteComment(); + } + } + + async deleteComment() { + try { + const result = await this.getApi()?.deleteComment(this.commentData.id); + if (result?.success) { + this.emit('comment-deleted', { commentId: this.commentData.id }); + this.remove(); + } + } catch (error) { + this.getApp()?.toast?.error('Failed to delete comment'); + } + } + + getCommentId() { + return this.commentData?.id || this.getAttr('comment-id'); + } +} + +customElements.define('comment-item', CommentItem); + +export { CommentItem }; diff --git a/js/components/image-preview.js b/js/components/image-preview.js new file mode 100644 index 0000000..10b1934 --- /dev/null +++ b/js/components/image-preview.js @@ -0,0 +1,146 @@ +/** + * @fileoverview Image Preview Component for Rantii + * @author retoor + * @description Displays images with lightbox functionality + * @keywords image, preview, lightbox, gallery, media + */ + +import { BaseComponent } from './base-component.js'; +import { buildDevrantImageUrl, isGifUrl } from '../utils/url.js'; + +class ImagePreview extends BaseComponent { + static get observedAttributes() { + return ['src', 'width', 'height', 'alt']; + } + + init() { + this.render(); + this.bindEvents(); + } + + render() { + const src = this.getAttr('src'); + const width = this.getAttr('width'); + const height = this.getAttr('height'); + const alt = this.getAttr('alt') || 'Image'; + + if (!src) { + this.setHtml(''); + return; + } + + const imageUrl = buildDevrantImageUrl(src); + const isGif = isGifUrl(imageUrl); + const aspectRatio = width && height ? width / height : null; + + this.addClass('image-preview'); + + this.setHtml(` +
+ ${alt} + ${isGif ? 'GIF' : ''} + +
+ `); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + } + + handleClick(e) { + const expandBtn = e.target.closest('.expand-btn'); + const img = e.target.closest('.preview-image'); + + if (expandBtn || img) { + e.preventDefault(); + this.openLightbox(); + } + } + + openLightbox() { + const src = this.getAttr('src'); + if (!src) return; + + const imageUrl = buildDevrantImageUrl(src); + + const lightbox = document.createElement('image-lightbox'); + lightbox.setAttribute('src', imageUrl); + document.body.appendChild(lightbox); + } + + onAttributeChanged(name, oldValue, newValue) { + this.render(); + } +} + +customElements.define('image-preview', ImagePreview); + +class ImageLightbox extends BaseComponent { + static get observedAttributes() { + return ['src']; + } + + init() { + this.keydownHandler = (e) => this.handleKeydown(e); + this.render(); + this.bindEvents(); + } + + render() { + const src = this.getAttr('src'); + + this.addClass('lightbox'); + + this.setHtml(` + + + `); + + requestAnimationFrame(() => this.addClass('lightbox-visible')); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + document.addEventListener('keydown', this.keydownHandler); + } + + handleClick(e) { + if (e.target.classList.contains('lightbox-backdrop') || + e.target.closest('.lightbox-close')) { + this.close(); + } + } + + handleKeydown(e) { + if (e.key === 'Escape') { + this.close(); + } + } + + close() { + document.removeEventListener('keydown', this.keydownHandler); + this.removeClass('lightbox-visible'); + setTimeout(() => this.remove(), 300); + } +} + +customElements.define('image-lightbox', ImageLightbox); + +export { ImagePreview, ImageLightbox }; diff --git a/js/components/link-preview.js b/js/components/link-preview.js new file mode 100644 index 0000000..feec192 --- /dev/null +++ b/js/components/link-preview.js @@ -0,0 +1,72 @@ +/** + * @fileoverview Link Preview Component for Rantii + * @author retoor + * @description Displays link previews with domain info + * @keywords link, preview, url, external, domain + */ + +import { BaseComponent } from './base-component.js'; +import { getDomain, sanitizeUrl } from '../utils/url.js'; + +class LinkPreview extends BaseComponent { + static get observedAttributes() { + return ['url', 'title']; + } + + init() { + this.render(); + } + + render() { + const url = this.getAttr('url'); + const title = this.getAttr('title'); + + if (!url) { + this.setHtml(''); + return; + } + + const safeUrl = sanitizeUrl(url); + if (!safeUrl) { + this.setHtml(''); + return; + } + + const domain = getDomain(safeUrl); + const displayTitle = title || safeUrl; + + this.addClass('link-preview'); + + this.setHtml(` + + + + + + `); + } + + truncate(text, length) { + if (text.length <= length) return text; + return text.substring(0, length) + '...'; + } + + onAttributeChanged(name, oldValue, newValue) { + this.render(); + } +} + +customElements.define('link-preview', LinkPreview); + +export { LinkPreview }; diff --git a/js/components/loading-spinner.js b/js/components/loading-spinner.js new file mode 100644 index 0000000..59a7862 --- /dev/null +++ b/js/components/loading-spinner.js @@ -0,0 +1,48 @@ +/** + * @fileoverview Loading Spinner Component for Rantii + * @author retoor + * @description Animated loading indicator for async operations + * @keywords loading, spinner, animation, progress, indicator + */ + +import { BaseComponent } from './base-component.js'; + +class LoadingSpinner extends BaseComponent { + static get observedAttributes() { + return ['size', 'text']; + } + + init() { + this.render(); + } + + render() { + const size = this.getAttr('size') || 'medium'; + const text = this.getAttr('text') || ''; + + this.addClass('spinner', `spinner-${size}`); + + this.setHtml(` +
+
+
+ ${text ? `${text}` : ''} + `); + } + + onAttributeChanged(name, oldValue, newValue) { + this.render(); + } + + setText(text) { + this.setAttr('text', text); + } + + setSize(size) { + this.setAttr('size', size); + } +} + +customElements.define('loading-spinner', LoadingSpinner); + +export { LoadingSpinner }; diff --git a/js/components/login-form.js b/js/components/login-form.js new file mode 100644 index 0000000..59bc051 --- /dev/null +++ b/js/components/login-form.js @@ -0,0 +1,133 @@ +/** + * @fileoverview Login Form Component for Rantii + * @author retoor + * @description User authentication form with validation + * @keywords login, form, authentication, credentials, signin + */ + +import { BaseComponent } from './base-component.js'; + +class LoginForm extends BaseComponent { + init() { + this.isLoading = false; + this.render(); + this.bindEvents(); + } + + render() { + this.setHtml(` + + `); + } + + bindEvents() { + const form = this.$('form'); + if (form) { + this.on(form, 'submit', this.handleSubmit); + } + } + + async handleSubmit(e) { + e.preventDefault(); + + if (this.isLoading) return; + + const username = this.$('#login-username').value.trim(); + const password = this.$('#login-password').value; + const remember = this.$('#login-remember').checked; + + if (!username || !password) { + this.showError('Please enter username and password'); + return; + } + + this.setLoading(true); + this.hideError(); + + try { + const result = await this.getAuth()?.login(username, password, remember); + if (result?.success) { + this.emit('login-success', { user: result.user }); + this.getRouter()?.goHome(); + } else { + this.showError(result?.error || 'Login failed'); + } + } catch (error) { + this.showError('Connection error. Please try again.'); + } finally { + this.setLoading(false); + } + } + + setLoading(loading) { + this.isLoading = loading; + this.render(); + this.bindEvents(); + } + + showError(message) { + const errorEl = this.$('.form-error'); + if (errorEl) { + errorEl.textContent = message; + errorEl.hidden = false; + } + } + + hideError() { + const errorEl = this.$('.form-error'); + if (errorEl) { + errorEl.hidden = true; + } + } + + reset() { + const form = this.$('form'); + if (form) { + form.reset(); + } + this.hideError(); + } +} + +customElements.define('login-form', LoginForm); + +export { LoginForm }; diff --git a/js/components/notification-list.js b/js/components/notification-list.js new file mode 100644 index 0000000..cd90181 --- /dev/null +++ b/js/components/notification-list.js @@ -0,0 +1,232 @@ +/** + * @fileoverview Notification List Component for Rantii + * @author retoor + * @description Displays user notifications and mentions + * @keywords notification, list, alerts, mentions, updates + */ + +import { BaseComponent } from './base-component.js'; +import { formatRelativeTime } from '../utils/date.js'; + +class NotificationList extends BaseComponent { + init() { + this.notifications = []; + this.isLoading = false; + this.unreadCount = 0; + this.render(); + this.bindEvents(); + } + + async load() { + if (!this.isLoggedIn()) { + this.render(); + return; + } + + this.isLoading = true; + this.render(); + + try { + const result = await this.getApi()?.getNotifications(); + if (result?.success) { + this.notifications = result.notifications || []; + this.unreadCount = result.unread?.total || 0; + } + } catch (error) { + this.notifications = []; + } finally { + this.isLoading = false; + this.render(); + } + } + + render() { + this.addClass('notification-list'); + + if (!this.isLoggedIn()) { + this.setHtml(` +
+

Sign in to view notifications

+ +
+ `); + return; + } + + if (this.isLoading) { + this.setHtml(` +
+ +
+ `); + return; + } + + if (this.notifications.length === 0) { + this.setHtml(` +
+ + + +

No notifications

+
+ `); + return; + } + + this.setHtml(` +
+

Notifications

+ ${this.unreadCount > 0 ? ` + + ` : ''} +
+
+ ${this.notifications.map(notif => ` + + + `).join('')} +
+ `); + + this.initNotificationItems(); + } + + initNotificationItems() { + const items = this.$$('notification-item'); + items.forEach(item => { + const notifData = item.dataset.notif; + if (notifData) { + try { + const notif = JSON.parse(notifData.replace(/'/g, "'")); + item.setNotification(notif); + } catch (e) {} + } + }); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + } + + handleClick(e) { + const loginBtn = e.target.closest('.login-btn'); + const clearBtn = e.target.closest('.clear-btn'); + + if (loginBtn) { + this.getRouter()?.goToLogin(); + return; + } + + if (clearBtn) { + this.clearNotifications(); + } + } + + async clearNotifications() { + try { + await this.getApi()?.clearNotifications(); + this.unreadCount = 0; + this.emit('notifications-cleared'); + await this.load(); + } catch (error) {} + } + + onConnected() { + this.load(); + } + + onDisconnected() { + this.isLoading = false; + } + + getUnreadCount() { + return this.unreadCount; + } + + refresh() { + this.load(); + } +} + +customElements.define('notification-list', NotificationList); + +class NotificationItem extends BaseComponent { + init() { + this.notifData = null; + this.render(); + this.bindEvents(); + } + + setNotification(notif) { + this.notifData = notif; + this.render(); + } + + render() { + if (!this.notifData) { + this.setHtml(''); + return; + } + + const notif = this.notifData; + const isUnread = notif.read === 0; + const typeLabel = this.getTypeLabel(notif.type); + const username = notif.username || notif.user_username || notif.name || 'Someone'; + + this.addClass('notification-item'); + if (isUnread) { + this.addClass('unread'); + } + + this.setHtml(` +
+
${this.getTypeIcon(notif.type)}
+
+ ${username} + ${typeLabel} +
+ +
+ `); + } + + getTypeLabel(type) { + const labels = { + 'comment_mention': 'mentioned you', + 'comment_content': 'commented on your rant', + 'comment_vote': 'upvoted your comment', + 'rant_vote': 'upvoted your rant', + 'comment_discuss': 'replied to a discussion' + }; + return labels[type] || 'interacted'; + } + + getTypeIcon(type) { + if (type.includes('mention')) { + return ``; + } + if (type.includes('vote')) { + return ``; + } + return ``; + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + } + + handleClick() { + if (this.notifData?.rant_id) { + this.getRouter()?.goToRant( + this.notifData.rant_id, + this.notifData.comment_id + ); + } + } +} + +customElements.define('notification-item', NotificationItem); + +export { NotificationList, NotificationItem }; diff --git a/js/components/post-form.js b/js/components/post-form.js new file mode 100644 index 0000000..3b30b03 --- /dev/null +++ b/js/components/post-form.js @@ -0,0 +1,249 @@ +/** + * @fileoverview Post Form Component for Rantii + * @author retoor + * @description Form for creating new rants + * @keywords post, form, create, rant, new + */ + +import { BaseComponent } from './base-component.js'; + +class PostForm extends BaseComponent { + init() { + this.isSubmitting = false; + this.render(); + this.bindEvents(); + this.loadDraft(); + } + + render() { + const isLoggedIn = this.isLoggedIn(); + + this.addClass('post-form'); + + if (!isLoggedIn) { + this.setHtml(` +
+

Sign in to post

+ +
+ `); + return; + } + + this.setHtml(` +
+
+

Create Rant

+ +
+
+ +
+
+ +
+
+ 0 / 5000 + +
+
+ `); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + this.on(this, 'submit', this.handleSubmit); + this.on(this, 'input', this.handleInput); + } + + handleClick(e) { + const loginBtn = e.target.closest('.login-btn'); + const closeBtn = e.target.closest('.close-btn'); + + if (loginBtn) { + this.getRouter()?.goToLogin(); + return; + } + + if (closeBtn) { + this.emit('close'); + } + } + + async handleSubmit(e) { + e.preventDefault(); + + if (this.isSubmitting) return; + + const textarea = this.$('.post-input'); + const tagsInput = this.$('.tags-input'); + + if (!textarea) return; + + const text = textarea.value.trim(); + if (!text) { + this.getApp()?.toast?.error('Please enter some text'); + return; + } + + const tags = tagsInput ? tagsInput.value.trim() : ''; + + this.isSubmitting = true; + this.render(); + + try { + const result = await this.getApi()?.postRant(text, tags); + if (result?.success) { + this.clearDraft(); + this.emit('post-created', { rantId: result.rantId }); + this.getApp()?.toast?.success('Rant posted successfully'); + this.isSubmitting = false; + this.render(); + this.getRouter()?.goToRant(result.rantId); + } else { + this.getApp()?.toast?.error(result?.error || 'Failed to post'); + this.isSubmitting = false; + this.render(); + } + } catch (error) { + this.getApp()?.toast?.error('Failed to post'); + this.isSubmitting = false; + this.render(); + } + } + + handleInput(e) { + if (e.target.classList.contains('post-input')) { + const textarea = e.target; + const count = textarea.value.length; + const countEl = this.$('.char-count'); + if (countEl) { + countEl.textContent = `${count} / 5000`; + } + this.saveDraft(textarea.value); + } + } + + loadDraft() { + const draft = this.getStorage()?.getDraftRant(); + if (draft) { + const textarea = this.$('.post-input'); + if (textarea) { + textarea.value = draft; + const countEl = this.$('.char-count'); + if (countEl) { + countEl.textContent = `${draft.length} / 5000`; + } + } + } + } + + saveDraft(text) { + this.getStorage()?.setDraftRant(text); + } + + clearDraft() { + this.getStorage()?.clearDraftRant(); + } + + reset() { + const textarea = this.$('.post-input'); + if (textarea) { + textarea.value = ''; + } + const tagsInput = this.$('.tags-input'); + if (tagsInput) { + tagsInput.value = ''; + } + const countEl = this.$('.char-count'); + if (countEl) { + countEl.textContent = '0 / 5000'; + } + this.clearDraft(); + } + + focus() { + const textarea = this.$('.post-input'); + if (textarea) { + textarea.focus(); + } + } +} + +customElements.define('post-form', PostForm); + +class PostModal extends BaseComponent { + init() { + this.keydownHandler = (e) => this.handleKeydown(e); + this.render(); + this.bindEvents(); + } + + render() { + this.addClass('modal', 'post-modal'); + + this.setHtml(` + + + `); + + requestAnimationFrame(() => this.addClass('modal-visible')); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + this.on(this, 'close', this.close); + this.on(this, 'post-created', this.close); + document.addEventListener('keydown', this.keydownHandler); + } + + handleClick(e) { + if (e.target.classList.contains('modal-backdrop')) { + this.close(); + } + } + + handleKeydown(e) { + if (e.key === 'Escape') { + this.close(); + } + } + + close() { + document.removeEventListener('keydown', this.keydownHandler); + this.removeClass('modal-visible'); + setTimeout(() => this.remove(), 300); + } + + open() { + document.body.appendChild(this); + const form = this.$('post-form'); + if (form) { + form.focus(); + } + } +} + +customElements.define('post-modal', PostModal); + +export { PostForm, PostModal }; diff --git a/js/components/rant-card.js b/js/components/rant-card.js new file mode 100644 index 0000000..eef6808 --- /dev/null +++ b/js/components/rant-card.js @@ -0,0 +1,169 @@ +/** + * @fileoverview Rant Card Component for Rantii + * @author retoor + * @description Compact rant display for feed listings + * @keywords rant, card, feed, listing, preview + */ + +import { BaseComponent } from './base-component.js'; +import { formatRelativeTime } from '../utils/date.js'; +import { buildDevrantImageUrl } from '../utils/url.js'; + +class RantCard extends BaseComponent { + static get observedAttributes() { + return ['rant-id']; + } + + init() { + this.rantData = null; + this.render(); + this.bindEvents(); + } + + setRant(rant) { + this.rantData = rant; + this.setAttr('rant-id', rant.id); + this.render(); + } + + render() { + if (!this.rantData) { + this.setHtml('
'); + return; + } + + const rant = this.rantData; + const hasImage = rant.attached_image && typeof rant.attached_image === 'object'; + const imageUrl = hasImage ? buildDevrantImageUrl(rant.attached_image.url) : null; + + this.addClass('rant-card'); + + this.setHtml(` +
+
+ + +
+ ${rant.user_username} + +${rant.user_score} + ${formatRelativeTime(rant.created_time)} +
+
+
+ + ${imageUrl ? ` +
+ + +
+ ` : ''} +
+
+ + + + ${rant.tags && rant.tags.length > 0 ? ` +
+ ${rant.tags.slice(0, 3).map(tag => ` + ${tag} + `).join('')} +
+ ` : ''} +
+
+ `); + } + + escapeAttr(str) { + if (!str) return ''; + return str + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + this.on(this, 'vote', this.handleVote); + } + + handleClick(e) { + const username = e.target.closest('.card-username'); + const avatar = e.target.closest('user-avatar'); + const commentsBtn = e.target.closest('.card-comments'); + const voteBtn = e.target.closest('.vote-btn'); + const tag = e.target.closest('.tag'); + const imagePreview = e.target.closest('image-preview'); + const youtubeEmbed = e.target.closest('youtube-embed'); + const linkPreview = e.target.closest('link-preview'); + + if (voteBtn || imagePreview || youtubeEmbed || linkPreview) { + return; + } + + if (username || avatar) { + e.stopPropagation(); + this.getRouter()?.goToUser(this.rantData.user_username); + return; + } + + if (tag) { + e.stopPropagation(); + this.getRouter()?.goToSearch(tag.textContent); + return; + } + + this.getRouter()?.goToRant(this.rantData.id); + } + + async handleVote(e) { + e.stopPropagation(); + const { vote, itemId } = e.detail; + + const voteButtons = this.$('vote-buttons'); + if (voteButtons) { + voteButtons.disable(); + } + + try { + const result = await this.getApi()?.voteRant(itemId, vote); + if (result?.success && result.rant) { + this.rantData.score = result.rant.score; + this.rantData.vote_state = result.rant.vote_state; + if (voteButtons) { + voteButtons.updateVote(result.rant.score, result.rant.vote_state); + voteButtons.enable(); + } + } + } catch (error) { + if (voteButtons) { + voteButtons.enable(); + } + } + } + + getRantId() { + return this.rantData?.id || this.getAttr('rant-id'); + } +} + +customElements.define('rant-card', RantCard); + +export { RantCard }; diff --git a/js/components/rant-content.js b/js/components/rant-content.js new file mode 100644 index 0000000..986922b --- /dev/null +++ b/js/components/rant-content.js @@ -0,0 +1,78 @@ +/** + * @fileoverview Rant Content Component for Rantii + * @author retoor + * @description Renders rant text with markdown, images, and media + * @keywords rant, content, markdown, media, render + */ + +import { BaseComponent } from './base-component.js'; +import { markdownRenderer } from '../utils/markdown.js'; +import { extractImageUrls, extractYoutubeUrls, extractNonMediaUrls } from '../utils/url.js'; + +class RantContent extends BaseComponent { + static get observedAttributes() { + return ['text']; + } + + init() { + this.render(); + } + + render() { + const text = this.getAttr('text') || ''; + + this.addClass('rant-content'); + + const images = extractImageUrls(text); + const youtubeLinks = extractYoutubeUrls(text); + const otherLinks = extractNonMediaUrls(text); + + const renderedText = markdownRenderer.render(text); + + let html = `
${renderedText}
`; + + if (images.length > 0) { + html += ` +
+ ${images.map(url => ` + + `).join('')} +
+ `; + } + + if (youtubeLinks.length > 0) { + html += ` +
+ ${youtubeLinks.map(url => ` + + `).join('')} +
+ `; + } + + if (otherLinks.length > 0) { + html += ` + + `; + } + + this.setHtml(html); + } + + onAttributeChanged(name, oldValue, newValue) { + this.render(); + } + + setText(text) { + this.setAttr('text', text); + } +} + +customElements.define('rant-content', RantContent); + +export { RantContent }; diff --git a/js/components/rant-detail.js b/js/components/rant-detail.js new file mode 100644 index 0000000..bfcd18f --- /dev/null +++ b/js/components/rant-detail.js @@ -0,0 +1,243 @@ +/** + * @fileoverview Rant Detail Component for Rantii + * @author retoor + * @description Full rant view with comments + * @keywords rant, detail, view, full, comments + */ + +import { BaseComponent } from './base-component.js'; +import { formatRelativeTime, formatFullDate } from '../utils/date.js'; +import { buildDevrantImageUrl } from '../utils/url.js'; + +class RantDetail extends BaseComponent { + static get observedAttributes() { + return ['rant-id']; + } + + init() { + this.rantData = null; + this.comments = []; + this.isLoading = false; + this.render(); + this.bindEvents(); + } + + async load(rantId) { + this.setAttr('rant-id', rantId); + this.isLoading = true; + this.render(); + + try { + const result = await this.getApi()?.getRant(rantId); + if (result?.success) { + this.rantData = result.rant; + this.comments = result.comments || []; + } + } catch (error) { + this.rantData = null; + this.comments = []; + } finally { + this.isLoading = false; + this.render(); + } + } + + setRant(rant, comments = []) { + this.rantData = rant; + this.comments = comments; + this.setAttr('rant-id', rant.id); + this.render(); + } + + render() { + if (this.isLoading) { + this.setHtml(` +
+ +
+ `); + return; + } + + if (!this.rantData) { + this.setHtml(` +
+

Rant not found

+ +
+ `); + return; + } + + const rant = this.rantData; + const hasImage = rant.attached_image && typeof rant.attached_image === 'object'; + const imageUrl = hasImage ? buildDevrantImageUrl(rant.attached_image.url) : null; + + this.addClass('rant-detail'); + + this.setHtml(` +
+
+ +

Rant

+
+
+ + +
+ ${rant.user_username} + +${rant.user_score} +
+ +
+
+ + ${imageUrl ? ` +
+ + +
+ ` : ''} +
+ ${rant.tags && rant.tags.length > 0 ? ` +
+ ${rant.tags.map(tag => ` + + `).join('')} +
+ ` : ''} +
+ + + ${this.comments.length} comments +
+
+
+ +
+ ${this.comments.map(comment => ` + + + `).join('')} +
+
+ `); + + this.initComments(); + } + + escapeAttr(str) { + if (!str) return ''; + return str + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + initComments() { + const commentItems = this.$$('comment-item'); + commentItems.forEach(item => { + const commentData = item.dataset.comment; + if (commentData) { + try { + const comment = JSON.parse(commentData.replace(/'/g, "'")); + item.setComment(comment); + } catch (e) {} + } + }); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + this.on(this, 'vote', this.handleVote); + this.on(this, 'comment-posted', this.handleCommentPosted); + } + + handleClick(e) { + const backBtn = e.target.closest('.back-btn'); + const username = e.target.closest('.author-username'); + const avatar = e.target.closest('user-avatar'); + const tag = e.target.closest('.tag'); + + if (backBtn) { + e.preventDefault(); + window.history.back(); + return; + } + + if (username || avatar) { + this.getRouter()?.goToUser(this.rantData.user_username); + return; + } + + if (tag) { + const tagText = tag.dataset.tag || tag.textContent; + this.getRouter()?.goToSearch(tagText); + } + } + + async handleVote(e) { + const { vote, itemId, type } = e.detail; + + if (type === 'rant') { + const result = await this.getApi()?.voteRant(itemId, vote); + if (result?.success && result.rant) { + this.rantData.score = result.rant.score; + this.rantData.vote_state = result.rant.vote_state; + const voteButtons = this.$('vote-buttons[type="rant"]'); + if (voteButtons) { + voteButtons.updateVote(result.rant.score, result.rant.vote_state); + } + } + } + } + + async handleCommentPosted(e) { + await this.load(this.rantData.id); + this.scrollToComments(); + } + + scrollToComment(commentId) { + const commentEl = this.$(`comment-item[comment-id="${commentId}"]`); + if (commentEl) { + commentEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); + commentEl.classList.add('highlight'); + setTimeout(() => commentEl.classList.remove('highlight'), 2000); + } + } + + scrollToComments() { + const commentsSection = this.$('.comments-section'); + if (commentsSection) { + commentsSection.scrollIntoView({ behavior: 'smooth' }); + } + } + + getRantId() { + return this.rantData?.id || this.getAttr('rant-id'); + } +} + +customElements.define('rant-detail', RantDetail); + +export { RantDetail }; diff --git a/js/components/rant-feed.js b/js/components/rant-feed.js new file mode 100644 index 0000000..95f5fcb --- /dev/null +++ b/js/components/rant-feed.js @@ -0,0 +1,242 @@ +/** + * @fileoverview Rant Feed Component for Rantii + * @author retoor + * @description Infinite scrolling list of rants + * @keywords feed, list, rants, infinite, scroll + */ + +import { BaseComponent } from './base-component.js'; + +class RantFeed extends BaseComponent { + static get observedAttributes() { + return ['sort', 'feed-type']; + } + + init() { + this.rants = []; + this.skip = 0; + this.limit = 20; + this.isLoading = false; + this.hasMore = true; + this.sort = this.getAttr('sort') || 'recent'; + this.feedType = this.getAttr('feed-type') || 'rants'; + + this.render(); + this.bindEvents(); + } + + async load(reset = false) { + if (this.isLoading) return; + if (!reset && !this.hasMore) return; + + if (reset) { + this.rants = []; + this.skip = 0; + this.hasMore = true; + } + + this.isLoading = true; + this.updateLoadingState(); + + try { + let result; + const api = this.getApi(); + + switch (this.feedType) { + case 'weekly': + result = await api?.getWeeklyRants(this.sort, this.limit, this.skip); + break; + case 'collabs': + result = await api?.getCollabs(this.sort, this.limit, this.skip); + break; + case 'stories': + result = await api?.getStories(this.sort, this.limit, this.skip); + break; + case 'search': + break; + default: + result = await api?.getRants(this.sort, this.limit, this.skip); + } + + if (result?.success) { + const newRants = result.rants || []; + this.rants = [...this.rants, ...newRants]; + this.skip += newRants.length; + this.hasMore = newRants.length >= this.limit; + } else { + this.hasMore = false; + } + } catch (error) { + this.hasMore = false; + } finally { + this.isLoading = false; + this.render(); + } + } + + async search(term) { + if (this.isLoading) return; + + this.isLoading = true; + this.rants = []; + this.updateLoadingState(); + + try { + const result = await this.getApi()?.search(term); + if (result?.success) { + this.rants = result.rants || []; + } + this.hasMore = false; + } catch (error) { + this.rants = []; + } finally { + this.isLoading = false; + this.render(); + } + } + + render() { + this.addClass('rant-feed'); + + if (this.rants.length === 0 && !this.isLoading) { + this.setHtml(` +
+

No rants found

+
+ `); + return; + } + + this.setHtml(` +
+
+ + + +
+
+
+ ${this.rants.map(rant => ` + + `).join('')} +
+ ${this.isLoading ? ` +
+ +
+ ` : ''} + ${this.hasMore && !this.isLoading ? ` +
+ +
+ ` : ''} + `); + + this.initRantCards(); + } + + initRantCards() { + const cards = this.$$('rant-card'); + cards.forEach((card, index) => { + if (this.rants[index]) { + card.setRant(this.rants[index]); + } + }); + } + + updateLoadingState() { + const loadingEl = this.$('.feed-loading'); + if (this.isLoading && !loadingEl && this.rants.length > 0) { + const loadMore = this.$('.feed-loadmore'); + if (loadMore) { + loadMore.innerHTML = ''; + } + } + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + this.setupInfiniteScroll(); + } + + handleClick(e) { + const sortTab = e.target.closest('.sort-tab'); + const loadMoreBtn = e.target.closest('.load-more-btn'); + + if (sortTab) { + const newSort = sortTab.dataset.sort; + if (newSort !== this.sort) { + this.sort = newSort; + this.setAttr('sort', newSort); + this.load(true); + } + return; + } + + if (loadMoreBtn) { + this.load(); + } + } + + setupInfiniteScroll() { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach(entry => { + if (entry.isIntersecting && !this.isLoading && this.hasMore) { + this.load(); + } + }); + }, + { rootMargin: '200px' } + ); + + this.intersectionObserver = observer; + } + + onConnected() { + this.load(true); + } + + onDisconnected() { + if (this.intersectionObserver) { + this.intersectionObserver.disconnect(); + } + this.isLoading = false; + this.hasMore = true; + } + + onAttributeChanged(name, oldValue, newValue) { + if (name === 'sort' && oldValue !== newValue) { + this.sort = newValue; + this.load(true); + } + if (name === 'feed-type' && oldValue !== newValue) { + this.feedType = newValue; + this.load(true); + } + } + + setSort(sort) { + this.sort = sort; + this.setAttr('sort', sort); + this.load(true); + } + + setFeedType(type) { + this.feedType = type; + this.setAttr('feed-type', type); + this.load(true); + } + + refresh() { + this.load(true); + } + + getRants() { + return this.rants; + } +} + +customElements.define('rant-feed', RantFeed); + +export { RantFeed }; diff --git a/js/components/theme-selector.js b/js/components/theme-selector.js new file mode 100644 index 0000000..75a099f --- /dev/null +++ b/js/components/theme-selector.js @@ -0,0 +1,62 @@ +/** + * @fileoverview Theme Selector Component for Rantii + * @author retoor + * @description UI for selecting application color themes + * @keywords theme, selector, dark, light, appearance + */ + +import { BaseComponent } from './base-component.js'; +import { THEMES } from '../services/theme.js'; + +class ThemeSelector extends BaseComponent { + init() { + this.themeChangeHandler = () => this.render(); + this.render(); + this.bindEvents(); + } + + render() { + const currentTheme = this.getTheme()?.getTheme() || 'dark'; + const themes = Object.entries(THEMES); + + this.setHtml(` +
+ +
+ ${themes.map(([key, theme]) => ` + + `).join('')} +
+
+ `); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + window.addEventListener('rantii:theme-change', this.themeChangeHandler); + } + + onDisconnected() { + window.removeEventListener('rantii:theme-change', this.themeChangeHandler); + } + + handleClick(e) { + const option = e.target.closest('.theme-option'); + if (!option) return; + + const theme = option.dataset.theme; + if (theme) { + this.getTheme()?.setTheme(theme); + this.render(); + } + } +} + +customElements.define('theme-selector', ThemeSelector); + +export { ThemeSelector }; diff --git a/js/components/toast-notification.js b/js/components/toast-notification.js new file mode 100644 index 0000000..d66c9c7 --- /dev/null +++ b/js/components/toast-notification.js @@ -0,0 +1,124 @@ +/** + * @fileoverview Toast Notification Component for Rantii + * @author retoor + * @description Non-intrusive notification messages for user feedback + * @keywords toast, notification, alert, message, feedback + */ + +import { BaseComponent } from './base-component.js'; + +class ToastNotification extends BaseComponent { + static get observedAttributes() { + return ['type', 'message', 'duration']; + } + + init() { + this.autoHideTimer = null; + this.render(); + } + + render() { + const type = this.getAttr('type') || 'info'; + const message = this.getAttr('message') || ''; + + this.addClass('toast', `toast-${type}`); + + this.setHtml(` +
+ ${this.getIcon(type)} + ${message} + +
+ `); + + this.bindEvents(); + this.startAutoHide(); + } + + getIcon(type) { + const icons = { + success: ``, + error: ``, + warning: ``, + info: `` + }; + return icons[type] || icons.info; + } + + bindEvents() { + const closeBtn = this.$('.toast-close'); + if (closeBtn) { + this.on(closeBtn, 'click', () => this.dismiss()); + } + } + + startAutoHide() { + const duration = parseInt(this.getAttr('duration') || '4000', 10); + if (duration > 0) { + this.autoHideTimer = setTimeout(() => this.dismiss(), duration); + } + } + + dismiss() { + if (this.autoHideTimer) { + clearTimeout(this.autoHideTimer); + } + this.addClass('toast-hiding'); + setTimeout(() => { + this.emit('toast-dismissed'); + this.remove(); + }, 300); + } + + show(message, type = 'info', duration = 4000) { + this.setAttr('message', message); + this.setAttr('type', type); + this.setAttr('duration', duration.toString()); + this.render(); + } +} + +customElements.define('toast-notification', ToastNotification); + +class ToastContainer extends BaseComponent { + init() { + this.addClass('toast-container'); + } + + show(message, type = 'info', duration = 4000) { + const toast = document.createElement('toast-notification'); + toast.setAttribute('message', message); + toast.setAttribute('type', type); + toast.setAttribute('duration', duration.toString()); + this.appendChild(toast); + return toast; + } + + success(message, duration = 4000) { + return this.show(message, 'success', duration); + } + + error(message, duration = 5000) { + return this.show(message, 'error', duration); + } + + warning(message, duration = 4000) { + return this.show(message, 'warning', duration); + } + + info(message, duration = 4000) { + return this.show(message, 'info', duration); + } + + clearAll() { + this.innerHTML = ''; + } +} + +customElements.define('toast-container', ToastContainer); + +export { ToastNotification, ToastContainer }; diff --git a/js/components/user-avatar.js b/js/components/user-avatar.js new file mode 100644 index 0000000..a162800 --- /dev/null +++ b/js/components/user-avatar.js @@ -0,0 +1,103 @@ +/** + * @fileoverview User Avatar Component for Rantii + * @author retoor + * @description Displays user avatar with fallback to initials + * @keywords avatar, user, profile, image, picture + */ + +import { BaseComponent } from './base-component.js'; +import { buildAvatarUrl } from '../utils/url.js'; + +class UserAvatar extends BaseComponent { + static get observedAttributes() { + return ['avatar', 'username', 'size', 'user-id']; + } + + init() { + this.render(); + } + + render() { + const avatarData = this.getAttr('avatar'); + const username = this.getAttr('username') || ''; + const size = this.getAttr('size') || 'medium'; + const userId = this.getAttr('user-id'); + + this.addClass('avatar', `avatar-${size}`); + + let avatar = null; + if (avatarData) { + try { + avatar = JSON.parse(avatarData); + } catch (e) { + avatar = null; + } + } + + const bgColor = avatar?.b || '#54556e'; + const imageUrl = buildAvatarUrl(avatar); + + if (imageUrl) { + this.setHtml(` +
+ ${username} +
+ `); + } else { + const initials = this.getInitials(username); + this.setHtml(` +
+ ${initials} +
+ `); + } + + if (userId || username) { + this.style.cursor = 'pointer'; + this.setAttribute('role', 'button'); + this.setAttribute('tabindex', '0'); + } + } + + getInitials(username) { + if (!username) return '?'; + return username.substring(0, 2).toUpperCase(); + } + + onAttributeChanged(name, oldValue, newValue) { + this.render(); + } + + onConnected() { + this.on(this, 'click', this.handleClick); + this.on(this, 'keydown', this.handleKeydown); + } + + handleClick(e) { + const username = this.getAttr('username'); + if (username) { + this.getRouter()?.goToUser(username); + } + } + + handleKeydown(e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.handleClick(e); + } + } + + setAvatar(avatar, username) { + if (avatar && typeof avatar === 'object') { + this.setAttr('avatar', JSON.stringify(avatar)); + } + if (username) { + this.setAttr('username', username); + } + this.render(); + } +} + +customElements.define('user-avatar', UserAvatar); + +export { UserAvatar }; diff --git a/js/components/user-profile.js b/js/components/user-profile.js new file mode 100644 index 0000000..92f253b --- /dev/null +++ b/js/components/user-profile.js @@ -0,0 +1,224 @@ +/** + * @fileoverview User Profile Component for Rantii + * @author retoor + * @description Complete user profile display with stats and content + * @keywords user, profile, stats, about, content + */ + +import { BaseComponent } from './base-component.js'; +import { formatDate } from '../utils/date.js'; + +class UserProfile extends BaseComponent { + static get observedAttributes() { + return ['username', 'user-id']; + } + + init() { + this.profileData = null; + this.isLoading = false; + this.activeTab = 'rants'; + this.render(); + this.bindEvents(); + } + + async load(username) { + if (!username) return; + + this.isLoading = true; + this.setAttr('username', username); + this.render(); + + try { + const result = await this.getApi()?.getProfileByUsername(username); + if (result?.success) { + this.profileData = result.profile; + } + } catch (error) { + this.profileData = null; + } finally { + this.isLoading = false; + this.render(); + } + } + + render() { + if (this.isLoading) { + this.setHtml(` +
+ +
+ `); + return; + } + + if (!this.profileData) { + this.setHtml(` +
+

User not found

+ +
+ `); + return; + } + + const profile = this.profileData; + const rants = profile.content?.content?.rants || []; + const comments = profile.content?.content?.comments || []; + + this.addClass('user-profile'); + + this.setHtml(` +
+ +
+ + +
+
+

${profile.username}

+
+${profile.score}
+
+
+
+ ${profile.about ? ` +
+ About +

${profile.about}

+
+ ` : ''} + ${profile.location ? ` +
+ Location + ${profile.location} +
+ ` : ''} + ${profile.skills ? ` +
+ Skills + ${profile.skills} +
+ ` : ''} + ${profile.github ? ` +
+ GitHub + ${profile.github} +
+ ` : ''} + ${profile.website ? ` +
+ Website + ${profile.website} +
+ ` : ''} +
+ Joined + ${formatDate(profile.created_time)} +
+
+
+
+ + +
+
+ ${this.activeTab === 'rants' ? ` +
+ ${rants.length > 0 ? rants.map(rant => ` + + `).join('') : '

No rants yet

'} +
+ ` : ''} + ${this.activeTab === 'comments' ? ` +
+ ${comments.length > 0 ? comments.map(comment => ` +
+ +
+ +${comment.score} +
+
+ `).join('') : '

No comments yet

'} +
+ ` : ''} +
+
+ `); + + this.initRantCards(rants); + } + + escapeAttr(str) { + if (!str) return ''; + return str + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + initRantCards(rants) { + const cards = this.$$('rant-card'); + cards.forEach((card, index) => { + if (rants[index]) { + card.setRant(rants[index]); + } + }); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + } + + handleClick(e) { + const backBtn = e.target.closest('.back-btn'); + const tab = e.target.closest('.tab'); + const profileComment = e.target.closest('.profile-comment'); + + if (backBtn) { + e.preventDefault(); + window.history.back(); + return; + } + + if (tab) { + this.activeTab = tab.dataset.tab; + this.render(); + const rants = this.profileData?.content?.content?.rants || []; + this.initRantCards(rants); + return; + } + + if (profileComment) { + const rantId = profileComment.dataset.rantId; + if (rantId) { + this.getRouter()?.goToRant(rantId); + } + } + } + + onAttributeChanged(name, oldValue, newValue) { + if (name === 'username' && newValue && oldValue !== newValue) { + this.load(newValue); + } + } + + getUsername() { + return this.profileData?.username || this.getAttr('username'); + } +} + +customElements.define('user-profile', UserProfile); + +export { UserProfile }; diff --git a/js/components/vote-buttons.js b/js/components/vote-buttons.js new file mode 100644 index 0000000..32cee96 --- /dev/null +++ b/js/components/vote-buttons.js @@ -0,0 +1,113 @@ +/** + * @fileoverview Vote Buttons Component for Rantii + * @author retoor + * @description Upvote and downvote controls for rants and comments + * @keywords vote, upvote, downvote, score, rating + */ + +import { BaseComponent } from './base-component.js'; + +class VoteButtons extends BaseComponent { + static get observedAttributes() { + return ['score', 'vote-state', 'type', 'item-id', 'disabled']; + } + + init() { + this.render(); + this.bindEvents(); + } + + render() { + const score = parseInt(this.getAttr('score') || '0', 10); + const voteState = parseInt(this.getAttr('vote-state') || '0', 10); + const disabled = this.hasAttr('disabled'); + + this.addClass('vote-buttons'); + + this.setHtml(` + + ${score} + + `); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + } + + handleClick(e) { + const btn = e.target.closest('.vote-btn'); + if (!btn || btn.disabled) return; + + if (!this.isLoggedIn()) { + this.getRouter()?.goToLogin(); + return; + } + + const currentState = parseInt(this.getAttr('vote-state') || '0', 10); + let newVote; + + if (btn.classList.contains('upvote')) { + newVote = currentState === 1 ? 0 : 1; + } else if (btn.classList.contains('downvote')) { + newVote = currentState === -1 ? 0 : -1; + } + + if (newVote !== undefined) { + this.emit('vote', { + vote: newVote, + type: this.getAttr('type'), + itemId: this.getAttr('item-id') + }); + } + } + + setScore(score) { + this.setAttr('score', score.toString()); + const scoreEl = this.$('.vote-score'); + if (scoreEl) { + scoreEl.textContent = score; + } + } + + setVoteState(state) { + this.setAttr('vote-state', state.toString()); + this.render(); + } + + updateVote(score, voteState) { + this.setAttr('score', score.toString()); + this.setAttr('vote-state', voteState.toString()); + this.render(); + } + + disable() { + this.setAttr('disabled', ''); + this.render(); + } + + enable() { + this.removeAttribute('disabled'); + this.render(); + } + + onAttributeChanged(name, oldValue, newValue) { + this.render(); + } +} + +customElements.define('vote-buttons', VoteButtons); + +export { VoteButtons }; diff --git a/js/components/youtube-embed.js b/js/components/youtube-embed.js new file mode 100644 index 0000000..c473ab3 --- /dev/null +++ b/js/components/youtube-embed.js @@ -0,0 +1,97 @@ +/** + * @fileoverview YouTube Embed Component for Rantii + * @author retoor + * @description Embeds YouTube videos with preview thumbnail + * @keywords youtube, video, embed, media, player + */ + +import { BaseComponent } from './base-component.js'; +import { getYoutubeVideoId, getYoutubeThumbnail, getYoutubeEmbedUrl } from '../utils/url.js'; + +class YoutubeEmbed extends BaseComponent { + static get observedAttributes() { + return ['url', 'video-id']; + } + + init() { + this.isPlaying = false; + this.render(); + this.bindEvents(); + } + + render() { + let videoId = this.getAttr('video-id'); + const url = this.getAttr('url'); + + if (!videoId && url) { + videoId = getYoutubeVideoId(url); + } + + if (!videoId) { + this.setHtml(''); + return; + } + + this.addClass('youtube-embed'); + + if (this.isPlaying) { + const embedUrl = getYoutubeEmbedUrl(videoId); + this.setHtml(` +
+ +
+ `); + } else { + const thumbnail = getYoutubeThumbnail(videoId); + this.setHtml(` +
+ Video thumbnail + + YouTube +
+ `); + } + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + } + + handleClick(e) { + const playBtn = e.target.closest('.youtube-play'); + const preview = e.target.closest('.youtube-preview'); + + if (playBtn || preview) { + e.preventDefault(); + this.play(); + } + } + + play() { + this.isPlaying = true; + this.render(); + } + + stop() { + this.isPlaying = false; + this.render(); + } + + onAttributeChanged(name, oldValue, newValue) { + this.isPlaying = false; + this.render(); + } +} + +customElements.define('youtube-embed', YoutubeEmbed); + +export { YoutubeEmbed }; diff --git a/js/pages/collabs-page.js b/js/pages/collabs-page.js new file mode 100644 index 0000000..03e883a --- /dev/null +++ b/js/pages/collabs-page.js @@ -0,0 +1,36 @@ +/** + * @fileoverview Collabs Page Component for Rantii + * @author retoor + * @description Collaboration projects page + * @keywords collabs, page, projects, collaboration, feed + */ + +import { BaseComponent } from '../components/base-component.js'; + +class CollabsPage extends BaseComponent { + init() { + this.render(); + } + + render() { + this.addClass('page', 'collabs-page'); + + this.setHtml(` + + + `); + } + + refresh() { + const feed = this.$('rant-feed'); + if (feed) { + feed.refresh(); + } + } +} + +customElements.define('collabs-page', CollabsPage); + +export { CollabsPage }; diff --git a/js/pages/home-page.js b/js/pages/home-page.js new file mode 100644 index 0000000..be22f10 --- /dev/null +++ b/js/pages/home-page.js @@ -0,0 +1,48 @@ +/** + * @fileoverview Home Page Component for Rantii + * @author retoor + * @description Main feed page displaying latest rants + * @keywords home, page, feed, main, landing + */ + +import { BaseComponent } from '../components/base-component.js'; + +class HomePage extends BaseComponent { + static get observedAttributes() { + return ['sort']; + } + + init() { + this.render(); + } + + render() { + const sort = this.getAttr('sort') || 'recent'; + + this.addClass('page', 'home-page'); + + this.setHtml(` + + `); + } + + onAttributeChanged(name, oldValue, newValue) { + if (name === 'sort') { + const feed = this.$('rant-feed'); + if (feed) { + feed.setSort(newValue); + } + } + } + + refresh() { + const feed = this.$('rant-feed'); + if (feed) { + feed.refresh(); + } + } +} + +customElements.define('home-page', HomePage); + +export { HomePage }; diff --git a/js/pages/login-page.js b/js/pages/login-page.js new file mode 100644 index 0000000..899ea5c --- /dev/null +++ b/js/pages/login-page.js @@ -0,0 +1,93 @@ +/** + * @fileoverview Login Page Component for Rantii + * @author retoor + * @description User authentication page + * @keywords login, page, auth, signin, authentication + */ + +import { BaseComponent } from '../components/base-component.js'; + +class LoginPage extends BaseComponent { + init() { + this.render(); + this.bindEvents(); + } + + render() { + const isLoggedIn = this.isLoggedIn(); + const user = this.getCurrentUser(); + + this.addClass('page', 'login-page'); + + if (isLoggedIn) { + this.setHtml(` + + `); + } else { + this.setHtml(` + + `); + } + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + this.on(this, 'login-success', this.handleLoginSuccess); + } + + handleClick(e) { + const backBtn = e.target.closest('.back-btn'); + const logoutBtn = e.target.closest('.logout-btn'); + const switchBtn = e.target.closest('.switch-btn'); + const homeBtn = e.target.closest('.home-btn'); + + if (backBtn) { + window.history.back(); + } else if (logoutBtn) { + this.getAuth()?.logout(); + this.render(); + } else if (switchBtn) { + this.getAuth()?.logout(); + this.render(); + } else if (homeBtn) { + this.getRouter()?.goHome(); + } + } + + handleLoginSuccess() { + this.getRouter()?.goHome(); + } +} + +customElements.define('login-page', LoginPage); + +export { LoginPage }; diff --git a/js/pages/notifications-page.js b/js/pages/notifications-page.js new file mode 100644 index 0000000..9bcf8b2 --- /dev/null +++ b/js/pages/notifications-page.js @@ -0,0 +1,33 @@ +/** + * @fileoverview Notifications Page Component for Rantii + * @author retoor + * @description User notifications view page + * @keywords notifications, page, alerts, mentions, updates + */ + +import { BaseComponent } from '../components/base-component.js'; + +class NotificationsPage extends BaseComponent { + init() { + this.render(); + } + + render() { + this.addClass('page', 'notifications-page'); + + this.setHtml(` + + `); + } + + refresh() { + const list = this.$('notification-list'); + if (list) { + list.refresh(); + } + } +} + +customElements.define('notifications-page', NotificationsPage); + +export { NotificationsPage }; diff --git a/js/pages/profile-page.js b/js/pages/profile-page.js new file mode 100644 index 0000000..9c3bc00 --- /dev/null +++ b/js/pages/profile-page.js @@ -0,0 +1,49 @@ +/** + * @fileoverview Profile Page Component for Rantii + * @author retoor + * @description User profile view page + * @keywords profile, page, user, view, account + */ + +import { BaseComponent } from '../components/base-component.js'; + +class ProfilePage extends BaseComponent { + static get observedAttributes() { + return ['username']; + } + + init() { + this.render(); + } + + render() { + const username = this.getAttr('username'); + + this.addClass('page', 'profile-page'); + + this.setHtml(` + + `); + + if (username) { + this.load(username); + } + } + + async load(username) { + const profile = this.$('user-profile'); + if (profile) { + await profile.load(username); + } + } + + onAttributeChanged(name, oldValue, newValue) { + if (name === 'username' && newValue) { + this.load(newValue); + } + } +} + +customElements.define('profile-page', ProfilePage); + +export { ProfilePage }; diff --git a/js/pages/rant-page.js b/js/pages/rant-page.js new file mode 100644 index 0000000..50c4214 --- /dev/null +++ b/js/pages/rant-page.js @@ -0,0 +1,62 @@ +/** + * @fileoverview Rant Page Component for Rantii + * @author retoor + * @description Single rant view page with comments + * @keywords rant, page, detail, view, single + */ + +import { BaseComponent } from '../components/base-component.js'; + +class RantPage extends BaseComponent { + static get observedAttributes() { + return ['rant-id', 'comment-id']; + } + + init() { + this.render(); + } + + render() { + const rantId = this.getAttr('rant-id'); + + this.addClass('page', 'rant-page'); + + this.setHtml(` + + `); + + if (rantId) { + this.load(rantId); + } + } + + async load(rantId) { + const detail = this.$('rant-detail'); + if (detail) { + await detail.load(rantId); + + const commentId = this.getAttr('comment-id'); + if (commentId) { + requestAnimationFrame(() => { + detail.scrollToComment(commentId); + }); + } + } + } + + onAttributeChanged(name, oldValue, newValue) { + if (name === 'rant-id' && newValue) { + this.load(newValue); + } + if (name === 'comment-id' && newValue) { + const detail = this.$('rant-detail'); + if (detail) { + detail.scrollToComment(newValue); + } + } + } +} + +customElements.define('rant-page', RantPage); + +export { RantPage }; diff --git a/js/pages/search-page.js b/js/pages/search-page.js new file mode 100644 index 0000000..355661c --- /dev/null +++ b/js/pages/search-page.js @@ -0,0 +1,88 @@ +/** + * @fileoverview Search Page Component for Rantii + * @author retoor + * @description Search results and search interface page + * @keywords search, page, results, find, query + */ + +import { BaseComponent } from '../components/base-component.js'; + +class SearchPage extends BaseComponent { + static get observedAttributes() { + return ['query']; + } + + init() { + this.render(); + this.bindEvents(); + } + + render() { + const query = this.getAttr('query') || ''; + + this.addClass('page', 'search-page'); + + this.setHtml(` +
+
+ + +
+
+
+ ${query ? `` : ` +
+ + + +

Enter a search term

+
+ `} +
+ `); + + if (query) { + this.search(query); + } + } + + bindEvents() { + this.on(this, 'submit', this.handleSubmit); + } + + handleSubmit(e) { + e.preventDefault(); + const input = this.$('.search-input'); + if (input && input.value.trim()) { + this.getRouter()?.goToSearch(input.value.trim()); + } + } + + async search(query) { + const feed = this.$('rant-feed'); + if (feed) { + await feed.search(query); + } + } + + onAttributeChanged(name, oldValue, newValue) { + if (name === 'query') { + this.render(); + if (newValue) { + this.search(newValue); + } + } + } +} + +customElements.define('search-page', SearchPage); + +export { SearchPage }; diff --git a/js/pages/settings-page.js b/js/pages/settings-page.js new file mode 100644 index 0000000..305ebf7 --- /dev/null +++ b/js/pages/settings-page.js @@ -0,0 +1,116 @@ +/** + * @fileoverview Settings Page Component for Rantii + * @author retoor + * @description Application settings and preferences page + * @keywords settings, page, preferences, options, config + */ + +import { BaseComponent } from '../components/base-component.js'; + +class SettingsPage extends BaseComponent { + init() { + this.render(); + this.bindEvents(); + } + + render() { + const isLoggedIn = this.isLoggedIn(); + const user = this.getCurrentUser(); + + this.addClass('page', 'settings-page'); + + this.setHtml(` +
+ +

Settings

+
+
+ ${isLoggedIn ? ` +
+

Account

+
+ Logged in as + ${user?.username || ''} +
+ +
+ ` : ` +
+

Account

+

Sign in to access all features

+ +
+ `} +
+

Appearance

+ +
+
+

About

+
+

Rantii - A DevRant Client

+

Version 1.0.0

+

Made with care by retoor

+
+
+
+

Data

+ +
+
+ `); + } + + bindEvents() { + this.on(this, 'click', this.handleClick); + } + + handleClick(e) { + const backBtn = e.target.closest('.back-btn'); + const loginBtn = e.target.closest('.login-btn'); + const logoutBtn = e.target.closest('.logout-btn'); + const clearCacheBtn = e.target.closest('.clear-cache-btn'); + + if (backBtn) { + window.history.back(); + return; + } + + if (loginBtn) { + this.getRouter()?.goToLogin(); + return; + } + + if (logoutBtn) { + this.logout(); + return; + } + + if (clearCacheBtn) { + this.clearCache(); + } + } + + logout() { + this.getAuth()?.logout(); + this.render(); + this.getApp()?.toast?.success('Signed out successfully'); + } + + clearCache() { + const storage = this.getStorage(); + if (storage) { + storage.clear(); + storage.setTheme(this.getTheme()?.getTheme() || 'dark'); + } + this.getApp()?.toast?.success('Cache cleared'); + } +} + +customElements.define('settings-page', SettingsPage); + +export { SettingsPage }; diff --git a/js/pages/stories-page.js b/js/pages/stories-page.js new file mode 100644 index 0000000..d97ecc7 --- /dev/null +++ b/js/pages/stories-page.js @@ -0,0 +1,36 @@ +/** + * @fileoverview Stories Page Component for Rantii + * @author retoor + * @description Developer stories page + * @keywords stories, page, devstories, feed, articles + */ + +import { BaseComponent } from '../components/base-component.js'; + +class StoriesPage extends BaseComponent { + init() { + this.render(); + } + + render() { + this.addClass('page', 'stories-page'); + + this.setHtml(` + + + `); + } + + refresh() { + const feed = this.$('rant-feed'); + if (feed) { + feed.refresh(); + } + } +} + +customElements.define('stories-page', StoriesPage); + +export { StoriesPage }; diff --git a/js/pages/weekly-page.js b/js/pages/weekly-page.js new file mode 100644 index 0000000..8326009 --- /dev/null +++ b/js/pages/weekly-page.js @@ -0,0 +1,36 @@ +/** + * @fileoverview Weekly Page Component for Rantii + * @author retoor + * @description Weekly rant challenge page + * @keywords weekly, page, challenge, rants, feed + */ + +import { BaseComponent } from '../components/base-component.js'; + +class WeeklyPage extends BaseComponent { + init() { + this.render(); + } + + render() { + this.addClass('page', 'weekly-page'); + + this.setHtml(` + + + `); + } + + refresh() { + const feed = this.$('rant-feed'); + if (feed) { + feed.refresh(); + } + } +} + +customElements.define('weekly-page', WeeklyPage); + +export { WeeklyPage }; diff --git a/js/services/auth.js b/js/services/auth.js new file mode 100644 index 0000000..e18db59 --- /dev/null +++ b/js/services/auth.js @@ -0,0 +1,100 @@ +/** + * @fileoverview Authentication Service for Rantii + * @author retoor + * @description Handles user authentication and session management + * @keywords auth, authentication, login, session, token + */ + +class AuthService { + constructor(apiClient, storageService) { + this.api = apiClient; + this.storage = storageService; + this.currentUser = null; + this.onAuthChange = null; + } + + async init() { + const authData = this.storage.getAuth(); + if (authData && authData.tokenId && authData.tokenKey) { + this.api.setAuth(authData.tokenId, authData.tokenKey, authData.userId); + this.currentUser = { + id: authData.userId, + username: authData.username + }; + return true; + } + return false; + } + + async login(username, password, remember = true) { + const result = await this.api.login(username, password); + if (result.success) { + const authData = { + tokenId: result.authToken.id, + tokenKey: result.authToken.key, + userId: result.authToken.user_id, + username: username, + expireTime: result.authToken.expire_time + }; + if (remember) { + this.storage.setAuth(authData); + } + this.currentUser = { + id: authData.userId, + username: username + }; + this.notifyAuthChange(); + return { success: true, user: this.currentUser }; + } + return { success: false, error: result.error }; + } + + logout() { + this.api.clearAuth(); + this.storage.clearAuth(); + this.currentUser = null; + this.notifyAuthChange(); + } + + isLoggedIn() { + return this.api.isAuthenticated() && this.currentUser !== null; + } + + getUser() { + return this.currentUser; + } + + getUserId() { + return this.currentUser?.id || null; + } + + getUsername() { + return this.currentUser?.username || null; + } + + setAuthChangeCallback(callback) { + this.onAuthChange = callback; + } + + notifyAuthChange() { + if (this.onAuthChange) { + this.onAuthChange(this.isLoggedIn(), this.currentUser); + } + window.dispatchEvent(new CustomEvent('rantii:auth-change', { + detail: { + isLoggedIn: this.isLoggedIn(), + user: this.currentUser + } + })); + } + + requireAuth() { + if (!this.isLoggedIn()) { + window.dispatchEvent(new CustomEvent('rantii:require-auth')); + return false; + } + return true; + } +} + +export { AuthService }; diff --git a/js/services/router.js b/js/services/router.js new file mode 100644 index 0000000..8f9cef8 --- /dev/null +++ b/js/services/router.js @@ -0,0 +1,177 @@ +/** + * @fileoverview URL Router Service for Rantii + * @author retoor + * @description Handles URL routing and navigation state management + * @keywords router, navigation, url, history, routing + */ + +class Router { + constructor() { + this.routes = new Map(); + this.currentRoute = null; + this.currentParams = {}; + this.onRouteChange = null; + this.basePath = this.detectBasePath(); + } + + detectBasePath() { + const path = window.location.pathname; + const htmlIndex = path.lastIndexOf('.html'); + if (htmlIndex !== -1) { + return path.substring(0, path.lastIndexOf('/') + 1); + } + return path.endsWith('/') ? path : path + '/'; + } + + init() { + window.addEventListener('popstate', () => this.handleRoute()); + this.handleRoute(); + } + + register(name, handler) { + this.routes.set(name, handler); + } + + getParams() { + const params = new URLSearchParams(window.location.search); + const result = {}; + for (const [key, value] of params) { + result[key] = value; + } + return result; + } + + handleRoute() { + const params = this.getParams(); + this.currentParams = params; + + let routeName = 'home'; + if (params.rant) { + routeName = 'rant'; + } else if (params.user) { + routeName = 'user'; + } else if (params.search !== undefined) { + routeName = 'search'; + } else if (params.notifications !== undefined) { + routeName = 'notifications'; + } else if (params.settings !== undefined) { + routeName = 'settings'; + } else if (params.login !== undefined) { + routeName = 'login'; + } else if (params.weekly !== undefined) { + routeName = 'weekly'; + } else if (params.collabs !== undefined) { + routeName = 'collabs'; + } else if (params.stories !== undefined) { + routeName = 'stories'; + } + + this.currentRoute = routeName; + const handler = this.routes.get(routeName); + if (handler) { + handler(params); + } + if (this.onRouteChange) { + this.onRouteChange(routeName, params); + } + window.dispatchEvent(new CustomEvent('rantii:route-change', { + detail: { route: routeName, params } + })); + } + + navigate(routeName, params = {}) { + const url = this.buildUrl(params); + window.history.pushState({ route: routeName, params }, '', url); + this.handleRoute(); + } + + replace(routeName, params = {}) { + const url = this.buildUrl(params); + window.history.replaceState({ route: routeName, params }, '', url); + this.handleRoute(); + } + + buildUrl(params = {}) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + searchParams.set(key, value); + } else if (value === '') { + searchParams.set(key, ''); + } + }); + const queryString = searchParams.toString(); + const currentPath = window.location.pathname; + return queryString ? `${currentPath}?${queryString}` : currentPath; + } + + goHome() { + this.navigate('home', {}); + } + + goToRant(rantId, commentId = null) { + const params = { rant: rantId }; + if (commentId) { + params.comment = commentId; + } + this.navigate('rant', params); + } + + goToUser(username) { + this.navigate('user', { user: username }); + } + + goToSearch(term = '') { + this.navigate('search', { search: term }); + } + + goToNotifications() { + this.navigate('notifications', { notifications: '' }); + } + + goToSettings() { + this.navigate('settings', { settings: '' }); + } + + goToLogin() { + this.navigate('login', { login: '' }); + } + + goToWeekly() { + this.navigate('weekly', { weekly: '' }); + } + + goToCollabs() { + this.navigate('collabs', { collabs: '' }); + } + + goToStories() { + this.navigate('stories', { stories: '' }); + } + + back() { + window.history.back(); + } + + forward() { + window.history.forward(); + } + + getCurrentRoute() { + return this.currentRoute; + } + + getCurrentParams() { + return this.currentParams; + } + + setRouteChangeCallback(callback) { + this.onRouteChange = callback; + } + + isCurrentRoute(routeName) { + return this.currentRoute === routeName; + } +} + +export { Router }; diff --git a/js/services/storage.js b/js/services/storage.js new file mode 100644 index 0000000..687658d --- /dev/null +++ b/js/services/storage.js @@ -0,0 +1,155 @@ +/** + * @fileoverview Local Storage Service for Rantii + * @author retoor + * @description Manages persistent storage of user data and settings + * @keywords storage, localStorage, persistence, settings + */ + +const STORAGE_PREFIX = 'rantii_'; + +class StorageService { + constructor() { + this.prefix = STORAGE_PREFIX; + } + + key(name) { + return `${this.prefix}${name}`; + } + + set(name, value) { + try { + const serialized = JSON.stringify(value); + localStorage.setItem(this.key(name), serialized); + return true; + } catch (error) { + return false; + } + } + + get(name, defaultValue = null) { + try { + const item = localStorage.getItem(this.key(name)); + if (item === null) { + return defaultValue; + } + return JSON.parse(item); + } catch (error) { + return defaultValue; + } + } + + remove(name) { + try { + localStorage.removeItem(this.key(name)); + return true; + } catch (error) { + return false; + } + } + + clear() { + try { + const keys = Object.keys(localStorage).filter(k => k.startsWith(this.prefix)); + keys.forEach(key => localStorage.removeItem(key)); + return true; + } catch (error) { + return false; + } + } + + has(name) { + return localStorage.getItem(this.key(name)) !== null; + } + + getAuth() { + return this.get('auth', null); + } + + setAuth(authData) { + return this.set('auth', authData); + } + + clearAuth() { + return this.remove('auth'); + } + + getTheme() { + return this.get('theme', 'dark'); + } + + setTheme(theme) { + return this.set('theme', theme); + } + + getSettings() { + return this.get('settings', { + theme: 'dark', + fontSize: 'medium', + notifications: true + }); + } + + setSettings(settings) { + return this.set('settings', settings); + } + + updateSettings(partial) { + const current = this.getSettings(); + return this.setSettings({ ...current, ...partial }); + } + + getRecentSearches() { + return this.get('recent_searches', []); + } + + addRecentSearch(term) { + const searches = this.getRecentSearches(); + const filtered = searches.filter(s => s !== term); + filtered.unshift(term); + return this.set('recent_searches', filtered.slice(0, 10)); + } + + getDraftRant() { + return this.get('draft_rant', ''); + } + + setDraftRant(text) { + return this.set('draft_rant', text); + } + + clearDraftRant() { + return this.remove('draft_rant'); + } + + getDraftComment(rantId) { + return this.get(`draft_comment_${rantId}`, ''); + } + + setDraftComment(rantId, text) { + return this.set(`draft_comment_${rantId}`, text); + } + + clearDraftComment(rantId) { + return this.remove(`draft_comment_${rantId}`); + } + + getCachedProfile(userId) { + return this.get(`profile_cache_${userId}`, null); + } + + setCachedProfile(userId, profile) { + return this.set(`profile_cache_${userId}`, { + data: profile, + timestamp: Date.now() + }); + } + + isCacheValid(cacheEntry, maxAge = 300000) { + if (!cacheEntry || !cacheEntry.timestamp) { + return false; + } + return Date.now() - cacheEntry.timestamp < maxAge; + } +} + +export { StorageService, STORAGE_PREFIX }; diff --git a/js/services/theme.js b/js/services/theme.js new file mode 100644 index 0000000..5a02dcd --- /dev/null +++ b/js/services/theme.js @@ -0,0 +1,156 @@ +/** + * @fileoverview Theme Service for Rantii + * @author retoor + * @description Manages application themes and visual appearance + * @keywords theme, dark mode, light mode, appearance, styling + */ + +const THEMES = { + dark: { + name: 'Dark', + class: 'theme-dark' + }, + light: { + name: 'Light', + class: 'theme-light' + }, + black: { + name: 'Black', + class: 'theme-black' + }, + white: { + name: 'White', + class: 'theme-white' + }, + ocean: { + name: 'Ocean', + class: 'theme-ocean' + }, + forest: { + name: 'Forest', + class: 'theme-forest' + }, + sunset: { + name: 'Sunset', + class: 'theme-sunset' + } +}; + +class ThemeService { + constructor(storageService) { + this.storage = storageService; + this.currentTheme = 'dark'; + this.onThemeChange = null; + } + + init() { + const savedTheme = this.storage.getTheme(); + if (savedTheme && THEMES[savedTheme]) { + this.applyTheme(savedTheme); + } else { + this.applyTheme(this.detectPreferredTheme()); + } + this.listenToSystemPreference(); + } + + detectPreferredTheme() { + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return 'dark'; + } + return 'light'; + } + + listenToSystemPreference() { + if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + if (!this.storage.has('theme')) { + this.applyTheme(e.matches ? 'dark' : 'light'); + } + }); + } + } + + applyTheme(themeName) { + const theme = THEMES[themeName]; + if (!theme) { + return false; + } + + Object.values(THEMES).forEach(t => { + document.documentElement.classList.remove(t.class); + document.body.classList.remove(t.class); + }); + + document.documentElement.classList.add(theme.class); + document.body.classList.add(theme.class); + this.currentTheme = themeName; + this.storage.setTheme(themeName); + + const metaThemeColor = document.querySelector('meta[name="theme-color"]'); + if (metaThemeColor) { + const colors = { + dark: '#1a1a2e', + light: '#f5f5f7', + black: '#000000', + white: '#ffffff', + ocean: '#0a1929', + forest: '#0d1f0d', + sunset: '#1f1410' + }; + metaThemeColor.setAttribute('content', colors[themeName] || '#1a1a2e'); + } + + if (this.onThemeChange) { + this.onThemeChange(themeName, theme); + } + + window.dispatchEvent(new CustomEvent('rantii:theme-change', { + detail: { theme: themeName, themeData: theme } + })); + + return true; + } + + setTheme(themeName) { + return this.applyTheme(themeName); + } + + getTheme() { + return this.currentTheme; + } + + getThemeData() { + return THEMES[this.currentTheme]; + } + + getAvailableThemes() { + return Object.entries(THEMES).map(([key, value]) => ({ + id: key, + name: value.name + })); + } + + toggle() { + const currentIndex = Object.keys(THEMES).indexOf(this.currentTheme); + const nextIndex = (currentIndex + 1) % Object.keys(THEMES).length; + const nextTheme = Object.keys(THEMES)[nextIndex]; + return this.applyTheme(nextTheme); + } + + toggleDarkLight() { + if (this.currentTheme === 'dark' || this.currentTheme === 'black') { + return this.applyTheme('light'); + } + return this.applyTheme('dark'); + } + + setThemeChangeCallback(callback) { + this.onThemeChange = callback; + } + + isDark() { + return ['dark', 'black', 'ocean', 'forest', 'sunset'].includes(this.currentTheme); + } +} + +export { ThemeService, THEMES }; diff --git a/js/utils/date.js b/js/utils/date.js new file mode 100644 index 0000000..044e225 --- /dev/null +++ b/js/utils/date.js @@ -0,0 +1,90 @@ +/** + * @fileoverview Date Formatting Utilities for Rantii + * @author retoor + * @description Date and time formatting functions + * @keywords date, time, format, timestamp, relative + */ + +function formatTimestamp(timestamp) { + const date = new Date(timestamp * 1000); + return date.toLocaleString(); +} + +function formatDate(timestamp) { + const date = new Date(timestamp * 1000); + return date.toLocaleDateString(); +} + +function formatTime(timestamp) { + const date = new Date(timestamp * 1000); + return date.toLocaleTimeString(); +} + +function formatRelativeTime(timestamp) { + const now = Date.now(); + const time = timestamp * 1000; + const diff = now - time; + + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + const weeks = Math.floor(days / 7); + const months = Math.floor(days / 30); + const years = Math.floor(days / 365); + + if (seconds < 60) { + return 'just now'; + } + if (minutes < 60) { + return `${minutes}m ago`; + } + if (hours < 24) { + return `${hours}h ago`; + } + if (days < 7) { + return `${days}d ago`; + } + if (weeks < 4) { + return `${weeks}w ago`; + } + if (months < 12) { + return `${months}mo ago`; + } + return `${years}y ago`; +} + +function formatFullDate(timestamp) { + const date = new Date(timestamp * 1000); + const options = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }; + return date.toLocaleDateString(undefined, options); +} + +function isToday(timestamp) { + const date = new Date(timestamp * 1000); + const today = new Date(); + return date.toDateString() === today.toDateString(); +} + +function isThisWeek(timestamp) { + const date = new Date(timestamp * 1000); + const now = new Date(); + const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + return date >= weekAgo; +} + +export { + formatTimestamp, + formatDate, + formatTime, + formatRelativeTime, + formatFullDate, + isToday, + isThisWeek +}; diff --git a/js/utils/markdown.js b/js/utils/markdown.js new file mode 100644 index 0000000..79839ea --- /dev/null +++ b/js/utils/markdown.js @@ -0,0 +1,148 @@ +/** + * @fileoverview Markdown Rendering Wrapper for Rantii + * @author retoor + * @description Markdown parsing and rendering with syntax highlighting + * @keywords markdown, parsing, syntax, highlight, render + */ + +class MarkdownRenderer { + constructor() { + this.marked = null; + this.hljs = null; + this.initialized = false; + } + + async init() { + if (this.initialized) { + return true; + } + + try { + if (window.marked) { + this.marked = window.marked; + } + if (window.hljs) { + this.hljs = window.hljs; + } + + if (this.marked && this.hljs) { + this.marked.setOptions({ + highlight: (code, lang) => { + if (lang && this.hljs.getLanguage(lang)) { + try { + return this.hljs.highlight(code, { language: lang }).value; + } catch (e) { + return code; + } + } + return this.hljs.highlightAuto(code).value; + }, + breaks: true, + gfm: true + }); + } else if (this.marked) { + this.marked.setOptions({ + breaks: true, + gfm: true + }); + } + + this.initialized = true; + return true; + } catch (e) { + return false; + } + } + + render(text) { + if (!text) { + return ''; + } + + if (this.marked) { + try { + return this.marked.parse(text); + } catch (e) { + return this.escapeHtml(text); + } + } + + return this.simpleRender(text); + } + + simpleRender(text) { + let result = this.escapeHtml(text); + + result = result.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { + const highlighted = this.highlightCode(code.trim(), lang); + return `
${highlighted}
`; + }); + + result = result.replace(/`([^`]+)`/g, '$1'); + + result = result.replace(/\*\*([^*]+)\*\*/g, '$1'); + result = result.replace(/\*([^*]+)\*/g, '$1'); + + result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + result = result.replace(/\n/g, '
'); + + return result; + } + + highlightCode(code, lang) { + if (this.hljs && lang && this.hljs.getLanguage(lang)) { + try { + return this.hljs.highlight(code, { language: lang }).value; + } catch (e) { + return this.escapeHtml(code); + } + } + if (this.hljs) { + try { + return this.hljs.highlightAuto(code).value; + } catch (e) { + return this.escapeHtml(code); + } + } + return this.escapeHtml(code); + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + renderInline(text) { + if (!text) { + return ''; + } + + let result = this.escapeHtml(text); + result = result.replace(/`([^`]+)`/g, '$1'); + result = result.replace(/\*\*([^*]+)\*\*/g, '$1'); + result = result.replace(/\*([^*]+)\*/g, '$1'); + + return result; + } + + stripMarkdown(text) { + if (!text) { + return ''; + } + + let result = text; + result = result.replace(/```[\s\S]*?```/g, ''); + result = result.replace(/`([^`]+)`/g, '$1'); + result = result.replace(/\*\*([^*]+)\*\*/g, '$1'); + result = result.replace(/\*([^*]+)\*/g, '$1'); + result = result.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); + + return result; + } +} + +const markdownRenderer = new MarkdownRenderer(); + +export { MarkdownRenderer, markdownRenderer }; diff --git a/js/utils/template.js b/js/utils/template.js new file mode 100644 index 0000000..ba649b7 --- /dev/null +++ b/js/utils/template.js @@ -0,0 +1,154 @@ +/** + * @fileoverview Template Utilities for Rantii + * @author retoor + * @description HTML template creation and manipulation helpers + * @keywords template, html, dom, element, create + */ + +function createElement(tag, attributes = {}, children = []) { + const element = document.createElement(tag); + + Object.entries(attributes).forEach(([key, value]) => { + if (key === 'className') { + element.className = value; + } else if (key === 'dataset') { + Object.entries(value).forEach(([dataKey, dataValue]) => { + element.dataset[dataKey] = dataValue; + }); + } else if (key.startsWith('on') && typeof value === 'function') { + const eventName = key.substring(2).toLowerCase(); + element.addEventListener(eventName, value); + } else if (key === 'style' && typeof value === 'object') { + Object.assign(element.style, value); + } else { + element.setAttribute(key, value); + } + }); + + children.forEach(child => { + if (typeof child === 'string') { + element.appendChild(document.createTextNode(child)); + } else if (child instanceof Node) { + element.appendChild(child); + } + }); + + return element; +} + +function html(strings, ...values) { + const template = document.createElement('template'); + template.innerHTML = strings.reduce((result, string, i) => { + const value = values[i - 1]; + if (value instanceof Node) { + return result + '' + string; + } + if (Array.isArray(value)) { + return result + value.map(v => v instanceof Node ? '' : String(v)).join('') + string; + } + return result + (value !== undefined ? String(value) : '') + string; + }); + return template.content.cloneNode(true); +} + +function createFragment(htmlString) { + const template = document.createElement('template'); + template.innerHTML = htmlString.trim(); + return template.content.cloneNode(true); +} + +function clearElement(element) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } +} + +function replaceContent(element, newContent) { + clearElement(element); + if (typeof newContent === 'string') { + element.innerHTML = newContent; + } else if (newContent instanceof Node) { + element.appendChild(newContent); + } +} + +function insertBefore(newElement, referenceElement) { + referenceElement.parentNode.insertBefore(newElement, referenceElement); +} + +function insertAfter(newElement, referenceElement) { + referenceElement.parentNode.insertBefore(newElement, referenceElement.nextSibling); +} + +function removeElement(element) { + if (element && element.parentNode) { + element.parentNode.removeChild(element); + } +} + +function toggleClass(element, className, force) { + if (force !== undefined) { + element.classList.toggle(className, force); + } else { + element.classList.toggle(className); + } +} + +function addClass(element, ...classNames) { + element.classList.add(...classNames); +} + +function removeClass(element, ...classNames) { + element.classList.remove(...classNames); +} + +function hasClass(element, className) { + return element.classList.contains(className); +} + +function setAttributes(element, attributes) { + Object.entries(attributes).forEach(([key, value]) => { + if (value === null || value === undefined) { + element.removeAttribute(key); + } else { + element.setAttribute(key, value); + } + }); +} + +function getDataAttributes(element) { + return { ...element.dataset }; +} + +function show(element) { + element.style.display = ''; + element.removeAttribute('hidden'); +} + +function hide(element) { + element.style.display = 'none'; +} + +function isVisible(element) { + return element.offsetParent !== null; +} + +export { + createElement, + html, + createFragment, + clearElement, + replaceContent, + insertBefore, + insertAfter, + removeElement, + toggleClass, + addClass, + removeClass, + hasClass, + setAttributes, + getDataAttributes, + show, + hide, + isVisible +}; diff --git a/js/utils/url.js b/js/utils/url.js new file mode 100644 index 0000000..e0d8bf9 --- /dev/null +++ b/js/utils/url.js @@ -0,0 +1,119 @@ +/** + * @fileoverview URL Utilities for Rantii + * @author retoor + * @description URL parsing and detection utilities + * @keywords url, link, youtube, image, detection + */ + +const URL_REGEX = /https?:\/\/[^\s<]+[^<.,:;"')\]\s]/gi; +const YOUTUBE_REGEX = /(?:youtube\.com\/(?:watch\?v=|embed\/|v\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/i; +const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp']; + +function extractUrls(text) { + const matches = text.match(URL_REGEX); + return matches || []; +} + +function isYoutubeUrl(url) { + return YOUTUBE_REGEX.test(url); +} + +function getYoutubeVideoId(url) { + const match = url.match(YOUTUBE_REGEX); + return match ? match[1] : null; +} + +function getYoutubeThumbnail(videoId) { + return `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`; +} + +function getYoutubeEmbedUrl(videoId) { + return `https://www.youtube-nocookie.com/embed/${videoId}`; +} + +function isImageUrl(url) { + const lower = url.toLowerCase(); + return IMAGE_EXTENSIONS.some(ext => lower.includes(ext)); +} + +function isGifUrl(url) { + return url.toLowerCase().includes('.gif'); +} + +function extractImageUrls(text) { + return extractUrls(text).filter(isImageUrl); +} + +function extractYoutubeUrls(text) { + return extractUrls(text).filter(isYoutubeUrl); +} + +function extractNonMediaUrls(text) { + return extractUrls(text).filter(url => !isImageUrl(url) && !isYoutubeUrl(url)); +} + +function sanitizeUrl(url) { + try { + const parsed = new URL(url); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return null; + } + return parsed.href; + } catch { + return null; + } +} + +function getDomain(url) { + try { + const parsed = new URL(url); + return parsed.hostname; + } catch { + return null; + } +} + +function makeAbsoluteUrl(url, base) { + try { + return new URL(url, base).href; + } catch { + return url; + } +} + +function isDevrantImageUrl(url) { + return url.includes('devrant.com') || url.includes('devrant.io'); +} + +function buildDevrantImageUrl(imagePath) { + if (imagePath.startsWith('http')) { + return imagePath; + } + return `https://img.devrant.com/${imagePath}`; +} + +function buildAvatarUrl(avatar) { + if (!avatar || !avatar.i) { + return null; + } + return `https://avatars.devrant.com/${avatar.i}`; +} + +export { + extractUrls, + isYoutubeUrl, + getYoutubeVideoId, + getYoutubeThumbnail, + getYoutubeEmbedUrl, + isImageUrl, + isGifUrl, + extractImageUrls, + extractYoutubeUrls, + extractNonMediaUrls, + sanitizeUrl, + getDomain, + makeAbsoluteUrl, + isDevrantImageUrl, + buildDevrantImageUrl, + buildAvatarUrl +}; diff --git a/lib/highlight.css b/lib/highlight.css new file mode 100644 index 0000000..03b6da8 --- /dev/null +++ b/lib/highlight.css @@ -0,0 +1,10 @@ +pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! + Theme: GitHub Dark + Description: Dark theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-dark + Current colors taken from GitHub's CSS +*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c} \ No newline at end of file diff --git a/lib/highlight.min.js b/lib/highlight.min.js new file mode 100644 index 0000000..5d699ae --- /dev/null +++ b/lib/highlight.min.js @@ -0,0 +1,1213 @@ +/*! + Highlight.js v11.9.0 (git: f47103d4f1) + (c) 2006-2023 undefined and other contributors + License: BSD-3-Clause + */ +var hljs=function(){"use strict";function e(n){ +return n instanceof Map?n.clear=n.delete=n.set=()=>{ +throw Error("map is read-only")}:n instanceof Set&&(n.add=n.clear=n.delete=()=>{ +throw Error("set is read-only") +}),Object.freeze(n),Object.getOwnPropertyNames(n).forEach((t=>{ +const a=n[t],i=typeof a;"object"!==i&&"function"!==i||Object.isFrozen(a)||e(a) +})),n}class n{constructor(e){ +void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} +ignoreMatch(){this.isMatchIgnored=!0}}function t(e){ +return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") +}function a(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n] +;return n.forEach((e=>{for(const n in e)t[n]=e[n]})),t}const i=e=>!!e.scope +;class r{constructor(e,n){ +this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){ +this.buffer+=t(e)}openNode(e){if(!i(e))return;const n=((e,{prefix:n})=>{ +if(e.startsWith("language:"))return e.replace("language:","language-") +;if(e.includes(".")){const t=e.split(".") +;return[`${n}${t.shift()}`,...t.map(((e,n)=>`${e}${"_".repeat(n+1)}`))].join(" ") +}return`${n}${e}`})(e.scope,{prefix:this.classPrefix});this.span(n)} +closeNode(e){i(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ +this.buffer+=``}}const s=(e={})=>{const n={children:[]} +;return Object.assign(n,e),n};class o{constructor(){ +this.rootNode=s(),this.stack=[this.rootNode]}get top(){ +return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ +this.top.children.push(e)}openNode(e){const n=s({scope:e}) +;this.add(n),this.stack.push(n)}closeNode(){ +if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ +for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} +walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){ +return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n), +n.children.forEach((n=>this._walk(e,n))),e.closeNode(n)),e}static _collapse(e){ +"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ +o._collapse(e)})))}}class l extends o{constructor(e){super(),this.options=e} +addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ +this.closeNode()}__addSublanguage(e,n){const t=e.root +;n&&(t.scope="language:"+n),this.add(t)}toHTML(){ +return new r(this,this.options).value()}finalize(){ +return this.closeAllNodes(),!0}}function c(e){ +return e?"string"==typeof e?e:e.source:null}function d(e){return b("(?=",e,")")} +function g(e){return b("(?:",e,")*")}function u(e){return b("(?:",e,")?")} +function b(...e){return e.map((e=>c(e))).join("")}function m(...e){const n=(e=>{ +const n=e[e.length-1] +;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{} +})(e);return"("+(n.capture?"":"?:")+e.map((e=>c(e))).join("|")+")"} +function p(e){return RegExp(e.toString()+"|").exec("").length-1} +const _=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ +;function h(e,{joinWith:n}){let t=0;return e.map((e=>{t+=1;const n=t +;let a=c(e),i="";for(;a.length>0;){const e=_.exec(a);if(!e){i+=a;break} +i+=a.substring(0,e.index), +a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0], +"("===e[0]&&t++)}return i})).map((e=>`(${e})`)).join(n)} +const f="[a-zA-Z]\\w*",E="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",N="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",v={ +begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'", +illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", +contains:[v]},x=(e,n,t={})=>{const i=a({scope:"comment",begin:e,end:n, +contains:[]},t);i.contains.push({scope:"doctag", +begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", +end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) +;const r=m("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) +;return i.contains.push({begin:b(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i +},M=x("//","$"),S=x("/\\*","\\*/"),A=x("#","$");var C=Object.freeze({ +__proto__:null,APOS_STRING_MODE:O,BACKSLASH_ESCAPE:v,BINARY_NUMBER_MODE:{ +scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:x, +C_BLOCK_COMMENT_MODE:S,C_LINE_COMMENT_MODE:M,C_NUMBER_MODE:{scope:"number", +begin:N,relevance:0},C_NUMBER_RE:N,END_SAME_AS_BEGIN:e=>Object.assign(e,{ +"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{ +n.data._beginMatch!==e[1]&&n.ignoreMatch()}}),HASH_COMMENT_MODE:A,IDENT_RE:f, +MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+E,relevance:0}, +NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y, +PHRASAL_WORDS_MODE:{ +begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, +end:/\/[gimuy]*/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]}, +RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", +SHEBANG:(e={})=>{const n=/^#![ ]*\// +;return e.binary&&(e.begin=b(n,/.*\b/,e.binary,/\b.*/)),a({scope:"meta",begin:n, +end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)}, +TITLE_MODE:{scope:"title",begin:f,relevance:0},UNDERSCORE_IDENT_RE:E, +UNDERSCORE_TITLE_MODE:{scope:"title",begin:E,relevance:0}});function T(e,n){ +"."===e.input[e.index-1]&&n.ignoreMatch()}function R(e,n){ +void 0!==e.className&&(e.scope=e.className,delete e.className)}function D(e,n){ +n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", +e.__beforeBegin=T,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, +void 0===e.relevance&&(e.relevance=0))}function I(e,n){ +Array.isArray(e.illegal)&&(e.illegal=m(...e.illegal))}function L(e,n){ +if(e.match){ +if(e.begin||e.end)throw Error("begin & end are not supported with match") +;e.begin=e.match,delete e.match}}function B(e,n){ +void 0===e.relevance&&(e.relevance=1)}const $=(e,n)=>{if(!e.beforeMatch)return +;if(e.starts)throw Error("beforeMatch cannot be used with starts") +;const t=Object.assign({},e);Object.keys(e).forEach((n=>{delete e[n] +})),e.keywords=t.keywords,e.begin=b(t.beforeMatch,d(t.begin)),e.starts={ +relevance:0,contains:[Object.assign(t,{endsParent:!0})] +},e.relevance=0,delete t.beforeMatch +},z=["of","and","for","in","not","or","if","then","parent","list","value"],F="keyword" +;function U(e,n,t=F){const a=Object.create(null) +;return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach((t=>{ +Object.assign(a,U(e[t],n,t))})),a;function i(e,t){ +n&&(t=t.map((e=>e.toLowerCase()))),t.forEach((n=>{const t=n.split("|") +;a[t[0]]=[e,j(t[0],t[1])]}))}}function j(e,n){ +return n?Number(n):(e=>z.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{ +console.error(e)},H=(e,...n)=>{console.log("WARN: "+e,...n)},q=(e,n)=>{ +P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0) +},G=Error();function Z(e,n,{key:t}){let a=0;const i=e[t],r={},s={} +;for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=p(n[e-1]) +;e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{ +e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, +delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ +_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope +}),(e=>{if(Array.isArray(e.begin)){ +if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), +G +;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), +G;Z(e,e.begin,{key:"beginScope"}),e.begin=h(e.begin,{joinWith:""})}})(e),(e=>{ +if(Array.isArray(e.end)){ +if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), +G +;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), +G;Z(e,e.end,{key:"endScope"}),e.end=h(e.end,{joinWith:""})}})(e)}function Q(e){ +function n(n,t){ +return RegExp(c(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":"")) +}class t{constructor(){ +this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} +addRule(e,n){ +n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]), +this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) +;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(h(e,{joinWith:"|" +}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex +;const n=this.matcherRe.exec(e);if(!n)return null +;const t=n.findIndex(((e,n)=>n>0&&void 0!==e)),a=this.matchIndexes[t] +;return n.splice(0,t),Object.assign(n,a)}}class i{constructor(){ +this.rules=[],this.multiRegexes=[], +this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ +if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t +;return this.rules.slice(e).forEach((([e,t])=>n.addRule(e,t))), +n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){ +return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){ +this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){ +const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex +;let t=n.exec(e) +;if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{ +const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)} +return t&&(this.regexIndex+=t.position+1, +this.regexIndex===this.count&&this.considerAll()),t}} +if(e.compilerExtensions||(e.compilerExtensions=[]), +e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") +;return e.classNameAliases=a(e.classNameAliases||{}),function t(r,s){const o=r +;if(r.isCompiled)return o +;[R,L,W,$].forEach((e=>e(r,s))),e.compilerExtensions.forEach((e=>e(r,s))), +r.__beforeBegin=null,[D,I,B].forEach((e=>e(r,s))),r.isCompiled=!0;let l=null +;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), +l=r.keywords.$pattern, +delete r.keywords.$pattern),l=l||/\w+/,r.keywords&&(r.keywords=U(r.keywords,e.case_insensitive)), +o.keywordPatternRe=n(l,!0), +s&&(r.begin||(r.begin=/\B|\b/),o.beginRe=n(o.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), +r.end&&(o.endRe=n(o.end)), +o.terminatorEnd=c(o.end)||"",r.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(r.end?"|":"")+s.terminatorEnd)), +r.illegal&&(o.illegalRe=n(r.illegal)), +r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((n=>a(e,{ +variants:null},n)))),e.cachedVariants?e.cachedVariants:X(e)?a(e,{ +starts:e.starts?a(e.starts):null +}):Object.isFrozen(e)?a(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{t(e,o) +})),r.starts&&t(r.starts,s),o.matcher=(e=>{const n=new i +;return e.contains.forEach((e=>n.addRule(e.begin,{rule:e,type:"begin" +}))),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end" +}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){ +return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{ +constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}} +const J=t,Y=a,ee=Symbol("nomatch"),ne=t=>{ +const a=Object.create(null),i=Object.create(null),r=[];let s=!0 +;const o="Could not find the language '{}', did you forget to load/include a language module?",c={ +disableAutodetect:!0,name:"Plain text",contains:[]};let p={ +ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, +languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", +cssSelector:"pre code",languages:null,__emitter:l};function _(e){ +return p.noHighlightRe.test(e)}function h(e,n,t){let a="",i="" +;"object"==typeof n?(a=e, +t=n.ignoreIllegals,i=n.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."), +q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), +i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};x("before:highlight",r) +;const s=r.result?r.result:f(r.language,r.code,t) +;return s.code=r.code,x("after:highlight",s),s}function f(e,t,i,r){ +const l=Object.create(null);function c(){if(!x.keywords)return void S.addText(A) +;let e=0;x.keywordPatternRe.lastIndex=0;let n=x.keywordPatternRe.exec(A),t="" +;for(;n;){t+=A.substring(e,n.index) +;const i=w.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,x.keywords[a]);if(r){ +const[e,a]=r +;if(S.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(C+=a),e.startsWith("_"))t+=n[0];else{ +const t=w.classNameAliases[e]||e;g(n[0],t)}}else t+=n[0] +;e=x.keywordPatternRe.lastIndex,n=x.keywordPatternRe.exec(A)}var a +;t+=A.substring(e),S.addText(t)}function d(){null!=x.subLanguage?(()=>{ +if(""===A)return;let e=null;if("string"==typeof x.subLanguage){ +if(!a[x.subLanguage])return void S.addText(A) +;e=f(x.subLanguage,A,!0,M[x.subLanguage]),M[x.subLanguage]=e._top +}else e=E(A,x.subLanguage.length?x.subLanguage:null) +;x.relevance>0&&(C+=e.relevance),S.__addSublanguage(e._emitter,e.language) +})():c(),A=""}function g(e,n){ +""!==e&&(S.startScope(n),S.addText(e),S.endScope())}function u(e,n){let t=1 +;const a=n.length-1;for(;t<=a;){if(!e._emit[t]){t++;continue} +const a=w.classNameAliases[e[t]]||e[t],i=n[t];a?g(i,a):(A=i,c(),A=""),t++}} +function b(e,n){ +return e.scope&&"string"==typeof e.scope&&S.openNode(w.classNameAliases[e.scope]||e.scope), +e.beginScope&&(e.beginScope._wrap?(g(A,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), +A=""):e.beginScope._multi&&(u(e.beginScope,n),A="")),x=Object.create(e,{parent:{ +value:x}}),x}function m(e,t,a){let i=((e,n)=>{const t=e&&e.exec(n) +;return t&&0===t.index})(e.endRe,a);if(i){if(e["on:end"]){const a=new n(e) +;e["on:end"](t,a),a.isMatchIgnored&&(i=!1)}if(i){ +for(;e.endsParent&&e.parent;)e=e.parent;return e}} +if(e.endsWithParent)return m(e.parent,t,a)}function _(e){ +return 0===x.matcher.regexIndex?(A+=e[0],1):(D=!0,0)}function h(e){ +const n=e[0],a=t.substring(e.index),i=m(x,e,a);if(!i)return ee;const r=x +;x.endScope&&x.endScope._wrap?(d(), +g(n,x.endScope._wrap)):x.endScope&&x.endScope._multi?(d(), +u(x.endScope,e)):r.skip?A+=n:(r.returnEnd||r.excludeEnd||(A+=n), +d(),r.excludeEnd&&(A=n));do{ +x.scope&&S.closeNode(),x.skip||x.subLanguage||(C+=x.relevance),x=x.parent +}while(x!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length} +let y={};function N(a,r){const o=r&&r[0];if(A+=a,null==o)return d(),0 +;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===o){ +if(A+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`) +;throw n.languageName=e,n.badRule=y.rule,n}return 1} +if(y=r,"begin"===r.type)return(e=>{ +const t=e[0],a=e.rule,i=new n(a),r=[a.__beforeBegin,a["on:begin"]] +;for(const n of r)if(n&&(n(e,i),i.isMatchIgnored))return _(t) +;return a.skip?A+=t:(a.excludeBegin&&(A+=t), +d(),a.returnBegin||a.excludeBegin||(A=t)),b(a,e),a.returnBegin?0:t.length})(r) +;if("illegal"===r.type&&!i){ +const e=Error('Illegal lexeme "'+o+'" for mode "'+(x.scope||"")+'"') +;throw e.mode=x,e}if("end"===r.type){const e=h(r);if(e!==ee)return e} +if("illegal"===r.type&&""===o)return 1 +;if(R>1e5&&R>3*r.index)throw Error("potential infinite loop, way more iterations than matches") +;return A+=o,o.length}const w=v(e) +;if(!w)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"') +;const O=Q(w);let k="",x=r||O;const M={},S=new p.__emitter(p);(()=>{const e=[] +;for(let n=x;n!==w;n=n.parent)n.scope&&e.unshift(n.scope) +;e.forEach((e=>S.openNode(e)))})();let A="",C=0,T=0,R=0,D=!1;try{ +if(w.__emitTokens)w.__emitTokens(t,S);else{for(x.matcher.considerAll();;){ +R++,D?D=!1:x.matcher.considerAll(),x.matcher.lastIndex=T +;const e=x.matcher.exec(t);if(!e)break;const n=N(t.substring(T,e.index),e) +;T=e.index+n}N(t.substring(T))}return S.finalize(),k=S.toHTML(),{language:e, +value:k,relevance:C,illegal:!1,_emitter:S,_top:x}}catch(n){ +if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t), +illegal:!0,relevance:0,_illegalBy:{message:n.message,index:T, +context:t.slice(T-100,T+100),mode:n.mode,resultSoFar:k},_emitter:S};if(s)return{ +language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:S,_top:x} +;throw n}}function E(e,n){n=n||p.languages||Object.keys(a);const t=(e=>{ +const n={value:J(e),illegal:!1,relevance:0,_top:c,_emitter:new p.__emitter(p)} +;return n._emitter.addText(e),n})(e),i=n.filter(v).filter(k).map((n=>f(n,e,!1))) +;i.unshift(t);const r=i.sort(((e,n)=>{ +if(e.relevance!==n.relevance)return n.relevance-e.relevance +;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1 +;if(v(n.language).supersetOf===e.language)return-1}return 0})),[s,o]=r,l=s +;return l.secondBest=o,l}function y(e){let n=null;const t=(e=>{ +let n=e.className+" ";n+=e.parentNode?e.parentNode.className:"" +;const t=p.languageDetectRe.exec(n);if(t){const n=v(t[1]) +;return n||(H(o.replace("{}",t[1])), +H("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"} +return n.split(/\s+/).find((e=>_(e)||v(e)))})(e);if(_(t))return +;if(x("before:highlightElement",{el:e,language:t +}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) +;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), +console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), +console.warn("The element with unescaped HTML:"), +console.warn(e)),p.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML) +;n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a) +;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,n,t)=>{const a=n&&i[n]||t +;e.classList.add("hljs"),e.classList.add("language-"+a) +})(e,t,r.language),e.result={language:r.language,re:r.relevance, +relevance:r.relevance},r.secondBest&&(e.secondBest={ +language:r.secondBest.language,relevance:r.secondBest.relevance +}),x("after:highlightElement",{el:e,result:r,text:a})}let N=!1;function w(){ +"loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(y):N=!0 +}function v(e){return e=(e||"").toLowerCase(),a[e]||a[i[e]]} +function O(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ +i[e.toLowerCase()]=n}))}function k(e){const n=v(e) +;return n&&!n.disableAutodetect}function x(e,n){const t=e;r.forEach((e=>{ +e[t]&&e[t](n)}))} +"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ +N&&w()}),!1),Object.assign(t,{highlight:h,highlightAuto:E,highlightAll:w, +highlightElement:y, +highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"), +q("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)}, +initHighlighting:()=>{ +w(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, +initHighlightingOnLoad:()=>{ +w(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") +},registerLanguage:(e,n)=>{let i=null;try{i=n(t)}catch(n){ +if(K("Language definition for '{}' could not be registered.".replace("{}",e)), +!s)throw n;K(n),i=c} +i.name||(i.name=e),a[e]=i,i.rawDefinition=n.bind(null,t),i.aliases&&O(i.aliases,{ +languageName:e})},unregisterLanguage:e=>{delete a[e] +;for(const n of Object.keys(i))i[n]===e&&delete i[n]}, +listLanguages:()=>Object.keys(a),getLanguage:v,registerAliases:O, +autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{ +e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{ +e["before:highlightBlock"](Object.assign({block:n.el},n)) +}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{ +e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)}, +removePlugin:e=>{const n=r.indexOf(e);-1!==n&&r.splice(n,1)}}),t.debugMode=()=>{ +s=!1},t.safeMode=()=>{s=!0},t.versionString="11.9.0",t.regex={concat:b, +lookahead:d,either:m,optional:u,anyNumberOfTimes:g} +;for(const n in C)"object"==typeof C[n]&&e(C[n]);return Object.assign(t,C),t +},te=ne({});te.newInstance=()=>ne({});var ae=te;const ie=e=>({IMPORTANT:{ +scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{ +scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/}, +FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/}, +ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} +}),re=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],se=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],oe=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],le=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],ce=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),de=oe.concat(le) +;var ge="[0-9](_*[0-9])*",ue=`\\.(${ge})`,be="[0-9a-fA-F](_*[0-9a-fA-F])*",me={ +className:"number",variants:[{ +begin:`(\\b(${ge})((${ue})|\\.)?|(${ue}))[eE][+-]?(${ge})[fFdD]?\\b`},{ +begin:`\\b(${ge})((${ue})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{ +begin:`(${ue})[fFdD]?\\b`},{begin:`\\b(${ge})[fFdD]\\b`},{ +begin:`\\b0[xX]((${be})\\.?|(${be})?\\.(${be}))[pP][+-]?(${ge})[fFdD]?\\b`},{ +begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${be})[lL]?\\b`},{ +begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], +relevance:0};function pe(e,n,t){return-1===t?"":e.replace(n,(a=>pe(e,n,t-1)))} +const _e="[A-Za-z$_][0-9A-Za-z$_]*",he=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],fe=["true","false","null","undefined","NaN","Infinity"],Ee=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],ye=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Ne=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],we=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],ve=[].concat(Ne,Ee,ye) +;function Oe(e){const n=e.regex,t=_e,a={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const t=e[0].length+e.index,a=e.input[t] +;if("<"===a||","===a)return void n.ignoreMatch();let i +;">"===a&&(((e,{after:n})=>{const t="",M={ +match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,/(async\s*)?/,n.lookahead(x)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[f]} +;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{ +PARAMS_CONTAINS:h,CLASS_REFERENCE:y},illegal:/#(?![$_A-z])/, +contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,m,{match:/\$\d+/},l,y,{ +className:"attr",begin:t+n.lookahead(":"),relevance:0},M,{ +begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[m,e.REGEXP_MODE,{ +className:"function",begin:x,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:i,contains:h}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin, +"on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{ +begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},N,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[f,e.inherit(e.TITLE_MODE,{begin:t, +className:"title.function"})]},{match:/\.\.\./,relevance:0},O,{match:"\\$"+t, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[f]},w,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},E,k,{match:/\$[(.]/}]}} +const ke=e=>b(/\b/,e,/\w$/.test(e)?/\b/:/\B/),xe=["Protocol","Type"].map(ke),Me=["init","self"].map(ke),Se=["Any","Self"],Ae=["actor","any","associatedtype","async","await",/as\?/,/as!/,"as","borrowing","break","case","catch","class","consume","consuming","continue","convenience","copy","default","defer","deinit","didSet","distributed","do","dynamic","each","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","macro","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],Ce=["false","nil","true"],Te=["assignment","associativity","higherThan","left","lowerThan","none","right"],Re=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warning"],De=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Ie=m(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Le=m(Ie,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),Be=b(Ie,Le,"*"),$e=m(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),ze=m($e,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Fe=b($e,ze,"*"),Ue=b(/[A-Z]/,ze,"*"),je=["attached","autoclosure",b(/convention\(/,m("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","freestanding","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",b(/objc\(/,Fe,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","Sendable","testable","UIApplicationMain","unchecked","unknown","usableFromInline","warn_unqualified_access"],Pe=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"] +;var Ke=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={ +begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]} +;Object.assign(t,{className:"variable",variants:[{ +begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={ +className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={ +begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, +end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$?\(\(/, +end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] +},l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 +}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, +contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ +name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, +keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"], +literal:["true","false"], +built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] +},contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{ +match:/\\"/},{className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}, +grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}] +}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ +className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{ +match:/\batomic_[a-z]{3,6}\b/}]},o={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ +className:"number",variants:[{begin:"\\b(0b[01']+)"},{ +begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" +},{ +begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" +}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ +className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ +className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 +},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ +keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"], +type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"], +literal:"true false NULL", +built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" +},b=[c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],m={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:u,contains:b.concat([{begin:/\(/,end:/\)/,keywords:u, +contains:b.concat(["self"]),relevance:0}]),relevance:0},p={ +begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ +begin:g,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})], +relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/, +keywords:u,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/, +end:/\)/,keywords:u,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s] +}]},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u, +disableAutodetect:!0,illegal:"=]/,contains:[{ +beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c, +strings:o,keywords:u}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{ +contains:[{begin:/\\\n/}] +}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="(?!struct)("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ +className:"type",begin:"\\b[a-z\\d_]*_t\\b"},o={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ +className:"number",variants:[{begin:"\\b(0b[01']+)"},{ +begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" +},{ +begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" +}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ +className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ +className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 +},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ +type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"], +keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"], +literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"], +_type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"] +},b={className:"function.dispatch",relevance:0,keywords:{ +_hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"] +}, +begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/)) +},m=[b,c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],p={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u, +contains:m.concat(["self"]),relevance:0}]),relevance:0},_={className:"function", +begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ +begin:g,returnBegin:!0,contains:[d],relevance:0},{begin:/::/,relevance:0},{ +begin:/:/,endsWithParent:!0,contains:[o,l]},{relevance:0,match:/,/},{ +className:"params",begin:/\(/,end:/\)/,keywords:u,relevance:0, +contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,end:/\)/,keywords:u, +relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]}] +},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C++", +aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"",keywords:u,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:u},{ +match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/], +className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={ +keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","scoped","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]), +built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"], +literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{ +begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{ +begin:"\\b(0b[01']+)"},{ +begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{ +begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" +}],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}] +},r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/, +keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/, +end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/ +},e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{ +begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/, +contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]}) +;s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE], +o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{ +illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] +},u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t] +},b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={ +begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"], +keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0, +contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{ +begin:"\x3c!--|--\x3e"},{begin:""}]}] +}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#", +end:"$",keywords:{ +keyword:"if else elif endif define undef warning error line region endregion pragma checksum" +}},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/, +illegal:/[^\s:,]/,contains:[{beginKeywords:"where class" +},t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace", +relevance:0,end:/[{;=]/,illegal:/[^\s:]/, +contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ +beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/, +contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta", +begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{ +className:"string",begin:/"/,end:/"/}]},{ +beginKeywords:"new return throw await else",relevance:0},{className:"function", +begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, +end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{ +beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial", +relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, +contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params", +begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0, +contains:[g,a,e.C_BLOCK_COMMENT_MODE] +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{ +const n=e.regex,t=ie(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{ +name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{ +keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"}, +contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/ +},t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0 +},{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 +},t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ +begin:":("+oe.join("|")+")"},{begin:":(:)?("+le.join("|")+")"}] +},t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b"},{ +begin:/:/,end:/[;}{]/, +contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{ +begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" +},contains:[...a,{className:"string",begin:/[^)]/,endsWithParent:!0, +excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]", +relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ +},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ +$pattern:/[a-z-]+/,keyword:"and or not only",attribute:se.join(" ")},contains:[{ +begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{ +className:"selector-tag",begin:"\\b("+re.join("|")+")\\b"}]}},grmr_diff:e=>{ +const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{ +className:"meta",relevance:10, +match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/) +},{className:"comment",variants:[{ +begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/), +end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{ +className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/, +end:/$/}]}},grmr_go:e=>{const n={ +keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], +type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], +literal:["true","false","iota","nil"], +built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] +};return{name:"Go",aliases:["golang"],keywords:n,illegal:"{const n=e.regex;return{name:"GraphQL",aliases:["gql"], +case_insensitive:!0,disableAutodetect:!1,keywords:{ +keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"], +literal:["true","false","null"]}, +contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{ +scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation", +begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/, +end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{ +scope:"symbol",begin:n.concat(/[_A-Za-z][_0-9A-Za-z]*/,n.lookahead(/\s*:/)), +relevance:0}],illegal:[/[;<']/,/BEGIN/]}},grmr_ini:e=>{const n=e.regex,t={ +className:"number",relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{ +begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{begin:/;/,end:/$/},{begin:/#/, +end:/$/}];const i={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{ +begin:/\$\{(.*?)\}/}]},r={className:"literal", +begin:/\bon|off|true|false|yes|no\b/},s={className:"string", +contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{ +begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}] +},o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0 +},l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{ +name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/, +contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{ +begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)), +className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{ +const n=e.regex,t="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",a=t+pe("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),i={ +keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits"], +literal:["false","true","null"], +type:["char","boolean","long","float","int","byte","short","double"], +built_in:["super","this"]},r={className:"meta",begin:"@"+t,contains:[{ +begin:/\(/,end:/\)/,contains:["self"]}]},s={className:"params",begin:/\(/, +end:/\)/,keywords:i,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} +;return{name:"Java",aliases:["jsp"],keywords:i,illegal:/<\/|#/, +contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, +relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ +begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, +className:"string",contains:[e.BACKSLASH_ESCAPE] +},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ +match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{ +1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ +begin:[n.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=(?!=)/],className:{1:"type", +3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword", +3:"title.class"},contains:[s,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ +beginKeywords:"new throw return else",relevance:0},{ +begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ +2:"title.function"},keywords:i,contains:[{className:"params",begin:/\(/, +end:/\)/,keywords:i,relevance:0, +contains:[r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,me,e.C_BLOCK_COMMENT_MODE] +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},me,r]}},grmr_javascript:Oe, +grmr_json:e=>{const n=["true","false","null"],t={scope:"literal", +beginKeywords:n.join(" ")};return{name:"JSON",keywords:{literal:n},contains:[{ +className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{ +match:/[{}[\],:]/,className:"punctuation",relevance:0 +},e.QUOTE_STRING_MODE,t,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], +illegal:"\\S"}},grmr_kotlin:e=>{const n={ +keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual", +built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing", +literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@" +},a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={ +className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string", +variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'", +illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/, +contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={ +className:"meta", +begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?" +},o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/, +end:/\)/,contains:[e.inherit(r,{className:"string"}),"self"]}] +},l=me,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={ +variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/, +contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g], +{name:"Kotlin",aliases:["kt","kts"],keywords:n, +contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag", +begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword", +begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol", +begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$", +returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{ +begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0, +contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://, +keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/, +endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/, +endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0 +},e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{ +begin:[/class|interface|trait/,/\s+/,e.UNDERSCORE_IDENT_RE],beginScope:{ +3:"title.class"},keywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0, +illegal:"extends implements",contains:[{ +beginKeywords:"public protected internal private constructor" +},e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0, +excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,){\s]|$/, +excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env", +end:"$",illegal:"\n"},l]}},grmr_less:e=>{ +const n=ie(e),t=de,a="[\\w-]+",i="("+a+"|@\\{"+a+"\\})",r=[],s=[],o=e=>({ +className:"string",begin:"~?"+e+".*?"+e}),l=(e,n,t)=>({className:e,begin:n, +relevance:t}),c={$pattern:/[a-z-]+/,keyword:"and or not only", +attribute:se.join(" ")},d={begin:"\\(",end:"\\)",contains:s,keywords:c, +relevance:0} +;s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,o("'"),o('"'),n.CSS_NUMBER_MODE,{ +begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]", +excludeEnd:!0} +},n.HEXCOLOR,d,l("variable","@@?"+a,10),l("variable","@\\{"+a+"\\}"),l("built_in","~?`[^`]*?`"),{ +className:"attribute",begin:a+"\\s*:",end:":",returnBegin:!0,excludeEnd:!0 +},n.IMPORTANT,{beginKeywords:"and not"},n.FUNCTION_DISPATCH);const g=s.concat({ +begin:/\{/,end:/\}/,contains:r}),u={beginKeywords:"when",endsWithParent:!0, +contains:[{beginKeywords:"and not"}].concat(s)},b={begin:i+"\\s*:", +returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/ +},n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b", +end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}] +},m={className:"keyword", +begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b", +starts:{end:"[;{}]",keywords:c,returnEnd:!0,contains:s,relevance:0}},p={ +className:"variable",variants:[{begin:"@"+a+"\\s*:",relevance:15},{begin:"@"+a +}],starts:{end:"[;}]",returnEnd:!0,contains:g}},_={variants:[{ +begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:i,end:/\{/}],returnBegin:!0, +returnEnd:!0,illegal:"[<='$\"]",relevance:0, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,u,l("keyword","all\\b"),l("variable","@\\{"+a+"\\}"),{ +begin:"\\b("+re.join("|")+")\\b",className:"selector-tag" +},n.CSS_NUMBER_MODE,l("selector-tag",i,0),l("selector-id","#"+i),l("selector-class","\\."+i,0),l("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{ +className:"selector-pseudo",begin:":("+oe.join("|")+")"},{ +className:"selector-pseudo",begin:":(:)?("+le.join("|")+")"},{begin:/\(/, +end:/\)/,relevance:0,contains:g},{begin:"!important"},n.FUNCTION_DISPATCH]},h={ +begin:a+":(:)?"+`(${t.join("|")})`,returnBegin:!0,contains:[_]} +;return r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,m,p,h,b,_,u,n.FUNCTION_DISPATCH), +{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:r}}, +grmr_lua:e=>{const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"] +},i=[e.COMMENT("--(?!"+n+")","$"),e.COMMENT("--"+n,t,{contains:[a],relevance:10 +})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE, +literal:"true false nil", +keyword:"and break do else elseif end for goto if in local not or repeat return then until while", +built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" +},contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)", +contains:[e.inherit(e.TITLE_MODE,{ +begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params", +begin:"\\(",endsWithParent:!0,contains:i}].concat(i) +},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string", +begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={ +className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)", +contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%{ +const n={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},t={ +variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{ +begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, +relevance:2},{ +begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), +relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ +begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ +},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, +returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", +excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", +end:"\\]",excludeBegin:!0,excludeEnd:!0}]},a={className:"strong",contains:[], +variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] +},i={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ +begin:/_(?![_\s])/,end:/_/,relevance:0}]},r=e.inherit(a,{contains:[] +}),s=e.inherit(i,{contains:[]});a.contains.push(s),i.contains.push(r) +;let o=[n,t];return[a,i,r,s].forEach((e=>{e.contains=e.contains.concat(o) +})),o=o.concat(a,i),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ +className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{ +begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", +contains:o}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", +end:"\\s+",excludeEnd:!0},a,i,{className:"quote",begin:"^>\\s+",contains:o, +end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ +begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ +begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", +contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ +begin:"^[-\\*]{3,}",end:"$"},t,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ +className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ +className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}},grmr_objectivec:e=>{ +const n=/[a-zA-Z@][a-zA-Z0-9_]*/,t={$pattern:n, +keyword:["@interface","@class","@protocol","@implementation"]};return{ +name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"], +keywords:{"variable.language":["this","super"],$pattern:n, +keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","switch","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"], +literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"], +built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"], +type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"] +},illegal:"/,end:/$/,illegal:"\\n" +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class", +begin:"("+t.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:t, +contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE, +relevance:0}]}},grmr_perl:e=>{const n=e.regex,t=/[dualxmsipngr]{0,12}/,a={ +$pattern:/[\w.]+/, +keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0" +},i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:a},r={begin:/->\{/, +end:/\}/},s={variants:[{begin:/\$\d/},{ +begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])") +},{begin:/[$%@][^\s\w{]/,relevance:0}] +},o=[e.BACKSLASH_ESCAPE,i,s],l=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],c=(e,a,i="\\1")=>{ +const r="\\1"===i?i:n.concat(i,a) +;return n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,r,/(?:\\.|[^\\\/])*?/,i,t) +},d=(e,a,i)=>n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,i,t),g=[s,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{ +endsWithParent:!0}),r,{className:"string",contains:o,variants:[{ +begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[", +end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{ +begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">", +relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'", +contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`", +contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{ +begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number", +begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b", +relevance:0},{ +begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*", +keywords:"split return print reverse grep",relevance:0, +contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{ +begin:c("s|tr|y",n.either(...l,{capture:!0}))},{begin:c("s|tr|y","\\(","\\)")},{ +begin:c("s|tr|y","\\[","\\]")},{begin:c("s|tr|y","\\{","\\}")}],relevance:2},{ +className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{ +begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",n.either(...l,{capture:!0 +}),/\1/)},{begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{ +begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub", +end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{ +begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$", +subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}] +}];return i.contains=g,r.contains=g,{name:"Perl",aliases:["pl","pm"],keywords:a, +contains:g}},grmr_php:e=>{ +const n=e.regex,t=/(?![A-Za-z0-9])(?![$])/,a=n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,t),i=n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,t),r={ +scope:"variable",match:"\\$+"+a},s={scope:"subst",variants:[{begin:/\$\w+/},{ +begin:/\{\$/,end:/\}/}]},o=e.inherit(e.APOS_STRING_MODE,{illegal:null +}),l="[ \t\n]",c={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{ +illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(s)}),o,{ +begin:/<<<[ \t]*(?:(\w+)|"(\w+)")\n/,end:/[ \t]*(\w+)\b/, +contains:e.QUOTE_STRING_MODE.contains.concat(s),"on:begin":(e,n)=>{ +n.data._beginMatch=e[1]||e[2]},"on:end":(e,n)=>{ +n.data._beginMatch!==e[1]&&n.ignoreMatch()}},e.END_SAME_AS_BEGIN({ +begin:/<<<[ \t]*'(\w+)'\n/,end:/[ \t]*(\w+)\b/})]},d={scope:"number",variants:[{ +begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{ +begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{ +begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?" +}],relevance:0 +},g=["false","null","true"],u=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],m={ +keyword:u,literal:(e=>{const n=[];return e.forEach((e=>{ +n.push(e),e.toLowerCase()===e?n.push(e.toUpperCase()):n.push(e.toLowerCase()) +})),n})(g),built_in:b},p=e=>e.map((e=>e.replace(/\|\d+$/,""))),_={variants:[{ +match:[/new/,n.concat(l,"+"),n.concat("(?!",p(b).join("\\b|"),"\\b)"),i],scope:{ +1:"keyword",4:"title.class"}}]},h=n.concat(a,"\\b(?!\\()"),f={variants:[{ +match:[n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant" +}},{match:[/::/,/class/],scope:{2:"variable.language"}},{ +match:[i,n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{1:"title.class", +3:"variable.constant"}},{match:[i,n.concat("::",n.lookahead(/(?!class\b)/))], +scope:{1:"title.class"}},{match:[i,/::/,/class/],scope:{1:"title.class", +3:"variable.language"}}]},E={scope:"attr", +match:n.concat(a,n.lookahead(":"),n.lookahead(/(?!::)/))},y={relevance:0, +begin:/\(/,end:/\)/,keywords:m,contains:[E,r,f,e.C_BLOCK_COMMENT_MODE,c,d,_] +},N={relevance:0, +match:[/\b/,n.concat("(?!fn\\b|function\\b|",p(u).join("\\b|"),"|",p(b).join("\\b|"),"\\b)"),a,n.concat(l,"*"),n.lookahead(/(?=\()/)], +scope:{3:"title.function.invoke"},contains:[y]};y.contains.push(N) +;const w=[E,f,e.C_BLOCK_COMMENT_MODE,c,d,_];return{case_insensitive:!1, +keywords:m,contains:[{begin:n.concat(/#\[\s*/,i),beginScope:"meta",end:/]/, +endScope:"meta",keywords:{literal:g,keyword:["new","array"]},contains:[{ +begin:/\[/,end:/]/,keywords:{literal:g,keyword:["new","array"]}, +contains:["self",...w]},...w,{scope:"meta",match:i}] +},e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{ +scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/, +keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE, +contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{ +begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{ +begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},r,N,f,{ +match:[/const/,/\s/,a],scope:{1:"keyword",3:"variable.constant"}},_,{ +scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/, +excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use" +},e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params", +begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:m, +contains:["self",r,f,e.C_BLOCK_COMMENT_MODE,c,d]}]},{scope:"class",variants:[{ +beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait", +illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{ +beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{ +beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/, +contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{ +beginKeywords:"use",relevance:0,end:";",contains:[{ +match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},c,d]} +},grmr_php_template:e=>({name:"PHP template",subLanguage:"xml",contains:[{ +begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*", +end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0 +},e.inherit(e.APOS_STRING_MODE,{illegal:null,className:null,contains:null, +skip:!0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null,className:null, +contains:null,skip:!0})]}]}),grmr_plaintext:e=>({name:"Plain text", +aliases:["text","txt"],disableAutodetect:!0}),grmr_python:e=>{ +const n=e.regex,t=/[\p{XID_Start}_]\p{XID_Continue}*/u,a=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={ +$pattern:/[A-Za-z]\w+|__\w+__/,keyword:a, +built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"], +literal:["__debug__","Ellipsis","False","None","NotImplemented","True"], +type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"] +},r={className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/, +end:/\}/,keywords:i,illegal:/#/},o={begin:/\{\{/,relevance:0},l={ +className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/, +contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ +begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/, +end:/"""/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([uU]|[rR])'/,end:/'/, +relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{ +begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/, +end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/, +contains:[e.BACKSLASH_ESCAPE,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,o,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] +},c="[0-9](_?[0-9])*",d=`(\\b(${c}))?\\.(${c})|\\b(${c})\\.`,g="\\b|"+a.join("|"),u={ +className:"number",relevance:0,variants:[{ +begin:`(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})`},{begin:`(${d})[jJ]?`},{ +begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})`},{ +begin:`\\b0[bB](_?[01])+[lL]?(?=${g})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${g})` +},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})`},{begin:`\\b(${c})[jJ](?=${g})` +}]},b={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i, +contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={ +className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/, +end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i, +contains:["self",r,u,l,e.HASH_COMMENT_MODE]}]};return s.contains=[l,u,r],{ +name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i, +illegal:/(<\/|\?)|=>/,contains:[r,u,{begin:/\bself\b/},{beginKeywords:"if", +relevance:0},l,b,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,t],scope:{ +1:"keyword",3:"title.function"},contains:[m]},{variants:[{ +match:[/\bclass/,/\s+/,t,/\s*/,/\(\s*/,t,/\s*\)/]},{match:[/\bclass/,/\s+/,t]}], +scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{ +className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[u,m,l]}]}}, +grmr_python_repl:e=>({aliases:["pycon"],contains:[{className:"meta.prompt", +starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{ +begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}),grmr_r:e=>{ +const n=e.regex,t=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,a=n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),i=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,r=n.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/) +;return{name:"R",keywords:{$pattern:t, +keyword:"function if in break next repeat else for while", +literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10", +built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm" +},contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/, +starts:{end:n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)), +endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{ +scope:"variable",variants:[{match:t},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0 +}]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}] +}),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE], +variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"', +relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{ +1:"operator",2:"number"},match:[i,a]},{scope:{1:"operator",2:"number"}, +match:[/%[^%]*%/,a]},{scope:{1:"punctuation",2:"number"},match:[r,a]},{scope:{ +2:"number"},match:[/[^a-zA-Z0-9._]|^/,a]}]},{scope:{3:"operator"}, +match:[t,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:i},{ +match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:r},{begin:"`",end:"`", +contains:[{begin:/\\./}]}]}},grmr_ruby:e=>{ +const n=e.regex,t="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",a=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(a,/(::\w+)*/),r={ +"variable.constant":["__FILE__","__LINE__","__ENCODING__"], +"variable.language":["self","super"], +keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"], +built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"], +literal:["true","false","nil"]},s={className:"doctag",begin:"@[A-Za-z]+"},o={ +begin:"#<",end:">"},l=[e.COMMENT("#","$",{contains:[s] +}),e.COMMENT("^=begin","^=end",{contains:[s],relevance:10 +}),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],c={className:"subst",begin:/#\{/, +end:/\}/,keywords:r},d={className:"string",contains:[e.BACKSLASH_ESCAPE,c], +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{ +begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{ +begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//, +end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{ +begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{ +begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ +begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ +begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ +begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), +contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, +contains:[e.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",u={className:"number", +relevance:0,variants:[{ +begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{ +begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" +},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ +begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ +begin:"\\b0(_?[0-7])+r?i?\\b"}]},b={variants:[{match:/\(\)/},{ +className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0, +keywords:r}]},m=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{ +match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class", +4:"title.class.inherited"},keywords:r},{match:[/(include|extend)\s+/,i],scope:{ +2:"title.class"},keywords:r},{relevance:0,match:[i,/\.new[. (]/],scope:{ +1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},{relevance:0,match:a,scope:"title.class"},{ +match:[/def/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[b]},{ +begin:e.IDENT_RE+"::"},{className:"symbol", +begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", +begin:":(?!\\s)",contains:[d,{begin:t}],relevance:0},u,{className:"variable", +begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ +className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0, +relevance:0,keywords:r},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*", +keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c], +illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{ +begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[", +end:"\\][a-z]*"}]}].concat(o,l),relevance:0}].concat(o,l) +;c.contains=m,b.contains=m;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:m} +},{className:"meta.prompt", +begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", +starts:{end:"$",keywords:r,contains:m}}];return l.unshift(o),{name:"Ruby", +aliases:["rb","gemspec","podspec","thor","irb"],keywords:r,illegal:/\/\*/, +contains:[e.SHEBANG({binary:"ruby"})].concat(p).concat(l).concat(m)}}, +grmr_rust:e=>{const n=e.regex,t={className:"title.function.invoke",relevance:0, +begin:n.concat(/\b/,/(?!let|for|while|if|else|match\b)/,e.IDENT_RE,n.lookahead(/\s*\(/)) +},a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],r=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"] +;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:r, +keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"], +literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:""},t]}}, +grmr_scss:e=>{const n=ie(e),t=le,a=oe,i="@[a-z-]+",r={className:"variable", +begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", +case_insensitive:!0,illegal:"[=/|']", +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,n.CSS_NUMBER_MODE,{ +className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ +className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 +},n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", +begin:"\\b("+re.join("|")+")\\b",relevance:0},{className:"selector-pseudo", +begin:":("+a.join("|")+")"},{className:"selector-pseudo", +begin:":(:)?("+t.join("|")+")"},r,{begin:/\(/,end:/\)/, +contains:[n.CSS_NUMBER_MODE]},n.CSS_VARIABLE,{className:"attribute", +begin:"\\b("+ce.join("|")+")\\b"},{ +begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" +},{begin:/:/,end:/[;}{]/,relevance:0, +contains:[n.BLOCK_COMMENT,r,n.HEXCOLOR,n.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.IMPORTANT,n.FUNCTION_DISPATCH] +},{begin:"@(page|font-face)",keywords:{$pattern:i,keyword:"@page @font-face"}},{ +begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, +keyword:"and or not only",attribute:se.join(" ")},contains:[{begin:i, +className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" +},r,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE] +},n.FUNCTION_DISPATCH]}},grmr_shell:e=>({name:"Shell Session", +aliases:["console","shellsession"],contains:[{className:"meta.prompt", +begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/, +subLanguage:"bash"}}]}),grmr_sql:e=>{ +const n=e.regex,t=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],r=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=r,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!r.includes(e))),c={ +begin:n.concat(/\b/,n.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}} +;return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{ +$pattern:/\b[\w\.]+/,keyword:((e,{exceptions:n,when:t}={})=>{const a=t +;return n=n||[],e.map((e=>e.match(/\|\d+$/)||n.includes(e)?e:a(e)?e+"|0":e)) +})(l,{when:e=>e.length<3}),literal:a,type:i, +built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"] +},contains:[{begin:n.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/, +keyword:l.concat(s),literal:a,type:i}},{className:"type", +begin:n.either("double precision","large object","with timezone","without timezone") +},c,{className:"variable",begin:/@[a-z0-9][a-z0-9_]*/},{className:"string", +variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/, +contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{ +className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/, +relevance:0}]}},grmr_swift:e=>{const n={match:/\s+/,relevance:0 +},t=e.COMMENT("/\\*","\\*/",{contains:["self"]}),a=[e.C_LINE_COMMENT_MODE,t],i={ +match:[/\./,m(...xe,...Me)],className:{2:"keyword"}},r={match:b(/\./,m(...Ae)), +relevance:0},s=Ae.filter((e=>"string"==typeof e)).concat(["_|0"]),o={variants:[{ +className:"keyword", +match:m(...Ae.filter((e=>"string"!=typeof e)).concat(Se).map(ke),...Me)}]},l={ +$pattern:m(/\b\w+/,/#\w+/),keyword:s.concat(Re),literal:Ce},c=[i,r,o],g=[{ +match:b(/\./,m(...De)),relevance:0},{className:"built_in", +match:b(/\b/,m(...De),/(?=\()/)}],u={match:/->/,relevance:0},p=[u,{ +className:"operator",relevance:0,variants:[{match:Be},{match:`\\.(\\.|${Le})+`}] +}],_="([0-9]_*)+",h="([0-9a-fA-F]_*)+",f={className:"number",relevance:0, +variants:[{match:`\\b(${_})(\\.(${_}))?([eE][+-]?(${_}))?\\b`},{ +match:`\\b0x(${h})(\\.(${h}))?([pP][+-]?(${_}))?\\b`},{match:/\b0o([0-7]_*)+\b/ +},{match:/\b0b([01]_*)+\b/}]},E=(e="")=>({className:"subst",variants:[{ +match:b(/\\/,e,/[0\\tnr"']/)},{match:b(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}] +}),y=(e="")=>({className:"subst",match:b(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/) +}),N=(e="")=>({className:"subst",label:"interpol",begin:b(/\\/,e,/\(/),end:/\)/ +}),w=(e="")=>({begin:b(e,/"""/),end:b(/"""/,e),contains:[E(e),y(e),N(e)] +}),v=(e="")=>({begin:b(e,/"/),end:b(/"/,e),contains:[E(e),N(e)]}),O={ +className:"string", +variants:[w(),w("#"),w("##"),w("###"),v(),v("#"),v("##"),v("###")] +},k=[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0, +contains:[e.BACKSLASH_ESCAPE]}],x={begin:/\/[^\s](?=[^/\n]*\/)/,end:/\//, +contains:k},M=e=>{const n=b(e,/\//),t=b(/\//,e);return{begin:n,end:t, +contains:[...k,{scope:"comment",begin:`#(?!.*${t})`,end:/$/}]}},S={ +scope:"regexp",variants:[M("###"),M("##"),M("#"),x]},A={match:b(/`/,Fe,/`/) +},C=[A,{className:"variable",match:/\$\d+/},{className:"variable", +match:`\\$${ze}+`}],T=[{match:/(@|#(un)?)available/,scope:"keyword",starts:{ +contains:[{begin:/\(/,end:/\)/,keywords:Pe,contains:[...p,f,O]}]}},{ +scope:"keyword",match:b(/@/,m(...je))},{scope:"meta",match:b(/@/,Fe)}],R={ +match:d(/\b[A-Z]/),relevance:0,contains:[{className:"type", +match:b(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,ze,"+") +},{className:"type",match:Ue,relevance:0},{match:/[?!]+/,relevance:0},{ +match:/\.\.\./,relevance:0},{match:b(/\s+&\s+/,d(Ue)),relevance:0}]},D={ +begin://,keywords:l,contains:[...a,...c,...T,u,R]};R.contains.push(D) +;const I={begin:/\(/,end:/\)/,relevance:0,keywords:l,contains:["self",{ +match:b(Fe,/\s*:/),keywords:"_|0",relevance:0 +},...a,S,...c,...g,...p,f,O,...C,...T,R]},L={begin://, +keywords:"repeat each",contains:[...a,R]},B={begin:/\(/,end:/\)/,keywords:l, +contains:[{begin:m(d(b(Fe,/\s*:/)),d(b(Fe,/\s+/,Fe,/\s*:/))),end:/:/, +relevance:0,contains:[{className:"keyword",match:/\b_\b/},{className:"params", +match:Fe}]},...a,...c,...p,f,O,...T,R,I],endsParent:!0,illegal:/["']/},$={ +match:[/(func|macro)/,/\s+/,m(A.match,Fe,Be)],className:{1:"keyword", +3:"title.function"},contains:[L,B,n],illegal:[/\[/,/%/]},z={ +match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"}, +contains:[L,B,n],illegal:/\[|%/},F={match:[/operator/,/\s+/,Be],className:{ +1:"keyword",3:"title"}},U={begin:[/precedencegroup/,/\s+/,Ue],className:{ +1:"keyword",3:"title"},contains:[R],keywords:[...Te,...Ce],end:/}/} +;for(const e of O.variants){const n=e.contains.find((e=>"interpol"===e.label)) +;n.keywords=l;const t=[...c,...g,...p,f,O,...C];n.contains=[...t,{begin:/\(/, +end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:l, +contains:[...a,$,z,{beginKeywords:"struct protocol class extension enum actor", +end:"\\{",excludeEnd:!0,keywords:l,contains:[e.inherit(e.TITLE_MODE,{ +className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...c] +},F,U,{beginKeywords:"import",end:/$/,contains:[...a],relevance:0 +},S,...c,...g,...p,f,O,...C,...T,R,I]}},grmr_typescript:e=>{ +const n=Oe(e),t=_e,a=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],i={ +beginKeywords:"namespace",end:/\{/,excludeEnd:!0, +contains:[n.exports.CLASS_REFERENCE]},r={beginKeywords:"interface",end:/\{/, +excludeEnd:!0,keywords:{keyword:"interface extends",built_in:a}, +contains:[n.exports.CLASS_REFERENCE]},s={$pattern:_e, +keyword:he.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), +literal:fe,built_in:ve.concat(a),"variable.language":we},o={className:"meta", +begin:"@"+t},l=(e,n,t)=>{const a=e.contains.findIndex((e=>e.label===n)) +;if(-1===a)throw Error("can not find mode to replace");e.contains.splice(a,1,t)} +;return Object.assign(n.keywords,s), +n.exports.PARAMS_CONTAINS.push(o),n.contains=n.contains.concat([o,i,r]), +l(n,"shebang",e.SHEBANG()),l(n,"use_strict",{className:"meta",relevance:10, +begin:/^\s*['"]use strict['"]/ +}),n.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(n,{ +name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),n},grmr_vbnet:e=>{ +const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,s={ +className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{ +begin:n.concat(/# */,r,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{ +begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,r),/ *#/)}] +},o=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}] +}),l=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]}) +;return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0, +classNameAliases:{label:"symbol"},keywords:{ +keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield", +built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort", +type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort", +literal:"true false nothing"}, +illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{ +className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/, +end:/"/,illegal:/\n/,contains:[{begin:/""/}]},s,{className:"number",relevance:0, +variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/ +},{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{ +begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{ +className:"label",begin:/^\w+:/},o,l,{className:"meta", +begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/, +end:/$/,keywords:{ +keyword:"const disable else elseif enable end externalsource if region then"}, +contains:[l]}]}},grmr_wasm:e=>{e.regex;const n=e.COMMENT(/\(;/,/;\)/) +;return n.contains.push("self"),{name:"WebAssembly",keywords:{$pattern:/[\w.]+/, +keyword:["anyfunc","block","br","br_if","br_table","call","call_indirect","data","drop","elem","else","end","export","func","global.get","global.set","local.get","local.set","local.tee","get_global","get_local","global","if","import","local","loop","memory","memory.grow","memory.size","module","mut","nop","offset","param","result","return","select","set_global","set_local","start","table","tee_local","then","type","unreachable"] +},contains:[e.COMMENT(/;;/,/$/),n,{match:[/(?:offset|align)/,/\s*/,/=/], +className:{1:"keyword",3:"operator"}},{className:"variable",begin:/\$[\w_]+/},{ +match:/(\((?!;)|\))+/,className:"punctuation",relevance:0},{ +begin:[/(?:func|call|call_indirect)/,/\s+/,/\$[^\s)]+/],className:{1:"keyword", +3:"title.function"}},e.QUOTE_STRING_MODE,{match:/(i32|i64|f32|f64)(?!\.)/, +className:"type"},{className:"keyword", +match:/\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/ +},{className:"number",relevance:0, +match:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/ +}]}},grmr_xml:e=>{ +const n=e.regex,t=n.concat(/[\p{L}_]/u,n.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),a={ +className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/, +contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] +},r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{ +className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={ +endsWithParent:!0,illegal:/`]+/}]}]}]};return{ +name:"HTML, XML", +aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], +case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{ +className:"meta",begin://,contains:[i,r,o,s]}]}] +},e.COMMENT(//,{relevance:10}),{begin://, +relevance:10},a,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, +relevance:10,contains:[o]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"style"},contains:[l],starts:{ +end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"script"},contains:[l],starts:{ +end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ +className:"tag",begin:/<>|<\/>/},{className:"tag", +begin:n.concat(//,/>/,/\s/)))), +end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:l}]},{ +className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(t,/>/))),contains:[{ +className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]} +},grmr_yaml:e=>{ +const n="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={ +className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ +},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", +variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(a,{ +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={ +end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},s={begin:/\{/, +end:/\}/,contains:[r],illegal:"\\n",relevance:0},o={begin:"\\[",end:"\\]", +contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{ +begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{ +begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$", +relevance:10},{className:"string", +begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ +begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, +relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type", +begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t +},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", +begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", +relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ +className:"number", +begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" +},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},s,o,a],c=[...l] +;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0, +aliases:["yml"],contains:l}}});const He=ae;for(const e of Object.keys(Ke)){ +const n=e.replace("grmr_","").replace("_","-");He.registerLanguage(n,Ke[e])} +return He}() +;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs); \ No newline at end of file diff --git a/lib/marked.min.js b/lib/marked.min.js new file mode 100644 index 0000000..be3f096 --- /dev/null +++ b/lib/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked v12.0.0 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=x(t[0].replace(/^ *>[ \t]?/gm,""),"\n"),n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=d(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(const e of n)i.header.push({text:e,tokens:this.lexer.inline(e)});for(const e of r)i.rows.push(d(e,i.header.length).map((e=>({text:e,tokens:this.lexer.inline(e)}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=x(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),b(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return b(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const m=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,y=/(?:[*+-]|\d{1,9}[.)])/,$=k(/^(?!bull )((?:.|\n(?!\s*?\n|bull ))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,y).getRegex(),z=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,T=/(?!\s*\])(?:\\.|[^\[\]\\])+/,R=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",T).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,y).getRegex(),A="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",S=/|$))/,I=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",S).replace("tag",A).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),E=k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),Z={blockquote:k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",E).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:R,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:m,html:I,lheading:$,list:_,newline:/^(?: *(?:\n|$))+/,paragraph:E,table:f,text:/^[^\n]+/},q=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),L={...Z,table:q,paragraph:k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",q).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...Z,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",S).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:f,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(z).replace("hr",m).replace("heading"," *#{1,6} *[^\n]").replace("lheading",$).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Q=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,B="\\p{P}\\p{S}",M=k(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,B).getRegex(),O=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,B).getRegex(),C=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,B).getRegex(),D=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,B).getRegex(),j=k(/\\([punct])/,"gu").replace(/punct/g,B).getRegex(),H=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),U=k(S).replace("(?:--\x3e|$)","--\x3e").getRegex(),X=k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",U).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),F=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",F).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),G=k(/^!?\[(label)\]\[(ref)\]/).replace("label",F).replace("ref",T).getRegex(),J=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",T).getRegex(),K={_backpedal:f,anyPunctuation:j,autolink:H,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:f,emStrongLDelim:O,emStrongRDelimAst:C,emStrongRDelimUnd:D,escape:Q,link:N,nolink:J,punctuation:M,reflink:G,reflinkSearch:k("reflink|nolink(?!\\()","g").replace("reflink",G).replace("nolink",J).getRegex(),tag:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class se{options;constructor(t){this.options=t||e.defaults}code(e,t,n){const s=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",s?'
'+(n?e:c(e,!0))+"
\n":"
"+(n?e:c(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e,t){return e}heading(e,t,n){return`${e}\n`}hr(){return"
\n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e,t,n){return`
  • ${e}
  • \n`}checkbox(e){return"'}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return"
    "}del(e){return`${e}`}link(e,t,n){const s=g(e);if(null===s)return n;let r='
    ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=`${n}0&&"paragraph"===n.tokens[0].type?(n.tokens[0].text=e+" "+n.tokens[0].text,n.tokens[0].tokens&&n.tokens[0].tokens.length>0&&"text"===n.tokens[0].tokens[0].type&&(n.tokens[0].tokens[0].text=e+" "+n.tokens[0].tokens[0].text)):n.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parse(n.tokens,i),l+=this.renderer.listitem(o,r,!!s)}n+=this.renderer.list(l,t,s);continue}case"html":{const e=r;n+=this.renderer.html(e.text,e.block);continue}case"paragraph":{const e=r;n+=this.renderer.paragraph(this.parseInline(e.tokens));continue}case"text":{let i=r,l=i.tokens?this.parseInline(i.tokens):i.text;for(;s+1{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new se(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new w(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new le;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];le.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ne.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#t(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#t(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+c(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const ae=new oe;function ce(e,t){return ae.parse(e,t)}ce.options=ce.setOptions=function(e){return ae.setOptions(e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.getDefaults=t,ce.defaults=e.defaults,ce.use=function(...e){return ae.use(...e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.walkTokens=function(e,t){return ae.walkTokens(e,t)},ce.parseInline=ae.parseInline,ce.Parser=ie,ce.parser=ie.parse,ce.Renderer=se,ce.TextRenderer=re,ce.Lexer=ne,ce.lexer=ne.lex,ce.Tokenizer=w,ce.Hooks=le,ce.parse=ce;const he=ce.options,pe=ce.setOptions,ue=ce.use,ke=ce.walkTokens,ge=ce.parseInline,fe=ce,de=ie.parse,xe=ne.lex;e.Hooks=le,e.Lexer=ne,e.Marked=oe,e.Parser=ie,e.Renderer=se,e.TextRenderer=re,e.Tokenizer=w,e.getDefaults=t,e.lexer=xe,e.marked=ce,e.options=he,e.parse=fe,e.parseInline=ge,e.parser=de,e.setOptions=pe,e.use=ue,e.walkTokens=ke})); diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..2893593 --- /dev/null +++ b/manifest.json @@ -0,0 +1,65 @@ +{ + "name": "Rantii - DevRant Client", + "short_name": "Rantii", + "description": "A modern DevRant client for developers", + "start_url": "./index.html", + "display": "standalone", + "background_color": "#1a1a2e", + "theme_color": "#e94560", + "orientation": "portrait-primary", + "scope": "./", + "icons": [ + { + "src": "assets/icons/icon-72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "assets/icons/icon-96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "assets/icons/icon-128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "assets/icons/icon-144.png", + "sizes": "144x144", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "assets/icons/icon-152.png", + "sizes": "152x152", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "assets/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "assets/icons/icon-384.png", + "sizes": "384x384", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "assets/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "categories": ["social", "developer tools"], + "lang": "en", + "dir": "ltr", + "prefer_related_applications": false +} diff --git a/proxy.py b/proxy.py new file mode 100644 index 0000000..8bdd52d --- /dev/null +++ b/proxy.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +@fileoverview CORS proxy server for Rantii +@author retoor +""" + +import os +import asyncio +import mimetypes +import posixpath +from urllib.parse import urlparse, unquote +from aiohttp import web, ClientSession + +API_BASE = 'https://dr.molodetz.nl/api/' +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +PORT = 8101 + +def add_cors_headers(response): + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Allow-Methods'] = 'GET, POST, DELETE, OPTIONS' + response.headers['Access-Control-Allow-Headers'] = 'Content-Type' + return response + +def is_path_safe(path): + normalized = posixpath.normpath(path) + if '..' in normalized: + return False + full_path = os.path.abspath(os.path.join(ROOT_DIR, normalized.lstrip('/'))) + return full_path.startswith(ROOT_DIR) + +async def handle_options(request): + return add_cors_headers(web.Response(status=200)) + +async def proxy_request(request, method, max_retries=10, retry_delay=2): + path = request.path[5:] + parsed = urlparse(path) + clean_path = posixpath.normpath(parsed.path).lstrip('/') + + if '..' in clean_path: + response = web.json_response({'success': False, 'error': 'Invalid path'}, status=400) + return add_cors_headers(response) + + url = API_BASE + clean_path + if request.query_string: + url += '?' + request.query_string + + body = None + if method in ('POST', 'DELETE'): + body = await request.read() + + headers = {} + if 'Content-Type' in request.headers: + headers['Content-Type'] = request.headers['Content-Type'] + + for attempt in range(max_retries): + try: + async with ClientSession() as session: + async with session.request(method, url, data=body, headers=headers) as resp: + data = await resp.read() + response = web.Response( + body=data, + status=resp.status, + content_type=resp.content_type + ) + return add_cors_headers(response) + except Exception: + if attempt < max_retries - 1: + await asyncio.sleep(retry_delay) + + response = web.json_response({'success': False, 'error': 'Connection failed'}, status=503) + return add_cors_headers(response) + +async def handle_api_get(request): + return await proxy_request(request, 'GET') + +async def handle_api_post(request): + return await proxy_request(request, 'POST') + +async def handle_api_delete(request): + return await proxy_request(request, 'DELETE') + +async def handle_static(request): + path = request.path + + if path == '/' or path.startswith('/?'): + path = '/index.html' + + path = unquote(path).lstrip('/') + + if not is_path_safe(path): + return add_cors_headers(web.Response(status=403, text='Forbidden')) + + full_path = os.path.join(ROOT_DIR, path) + + if os.path.isdir(full_path): + full_path = os.path.join(full_path, 'index.html') + + if not os.path.isfile(full_path): + return add_cors_headers(web.Response(status=404, text='Not Found')) + + content_type, _ = mimetypes.guess_type(full_path) + if content_type is None: + content_type = 'application/octet-stream' + + with open(full_path, 'rb') as f: + data = f.read() + + response = web.Response(body=data, content_type=content_type) + return add_cors_headers(response) + +def create_app(): + app = web.Application() + app.router.add_route('OPTIONS', '/{path:.*}', handle_options) + app.router.add_get('/api/{path:.*}', handle_api_get) + app.router.add_post('/api/{path:.*}', handle_api_post) + app.router.add_delete('/api/{path:.*}', handle_api_delete) + app.router.add_get('/{path:.*}', handle_static) + app.router.add_get('/', handle_static) + return app + +if __name__ == '__main__': + print(f'Server running at http://localhost:{PORT}') + print('API proxy at /api/*') + app = create_app() + web.run_app(app, host='localhost', port=PORT, print=None) diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..2f14a3f --- /dev/null +++ b/sw.js @@ -0,0 +1,132 @@ +/** + * @fileoverview Service Worker for Rantii PWA + * @author retoor + * @description Handles caching and offline functionality + * @keywords service worker, pwa, cache, offline + */ + +const CACHE_NAME = 'rantii-v1'; +const STATIC_ASSETS = [ + './', + './index.html', + './manifest.json', + './css/variables.css', + './css/base.css', + './css/themes/dark.css', + './css/themes/light.css', + './css/themes/black.css', + './css/themes/white.css', + './css/themes/ocean.css', + './css/themes/forest.css', + './css/themes/sunset.css', + './css/components/app-header.css', + './css/components/app-nav.css', + './css/components/components.css', + './css/components/rant.css', + './css/components/comment.css', + './css/components/user.css', + './css/components/notification.css', + './css/components/form.css', + './css/components/pages.css', + './js/app.js', + './js/api/client.js', + './js/services/storage.js', + './js/services/auth.js', + './js/services/router.js', + './js/services/theme.js', + './js/utils/date.js', + './js/utils/url.js', + './js/utils/markdown.js', + './js/utils/template.js', + './lib/marked.min.js', + './lib/highlight.min.js', + './lib/highlight.css' +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(STATIC_ASSETS); + }) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + return caches.delete(cacheName); + } + }) + ); + }) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url); + + if (url.origin === 'https://dr.molodetz.nl' || + url.origin === 'https://devrant.com' || + url.origin === 'https://www.devrant.com' || + url.pathname.startsWith('/api/')) { + event.respondWith( + fetch(event.request).catch(() => { + return new Response(JSON.stringify({ success: false, error: 'Offline' }), { + headers: { 'Content-Type': 'application/json' } + }); + }) + ); + return; + } + + if (url.hostname.includes('avatars.devrant.com') || url.hostname.includes('img.devrant.com')) { + event.respondWith( + caches.open('rantii-images').then((cache) => { + return cache.match(event.request).then((response) => { + if (response) { + return response; + } + return fetch(event.request).then((networkResponse) => { + cache.put(event.request, networkResponse.clone()); + return networkResponse; + }).catch(() => { + return new Response('', { status: 404 }); + }); + }); + }) + ); + return; + } + + event.respondWith( + caches.match(event.request).then((response) => { + if (response) { + return response; + } + return fetch(event.request).then((networkResponse) => { + if (networkResponse && networkResponse.status === 200) { + const responseClone = networkResponse.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseClone); + }); + } + return networkResponse; + }).catch(() => { + if (event.request.destination === 'document') { + return caches.match('./index.html'); + } + }); + }) + ); +}); + +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } +});