1use std::borrow::Cow;
8
9use salvo_core::writing::Text;
10use salvo_core::{async_trait, Depot, FlowCtrl, Handler, Request, Response, Router};
11
12const INDEX_TMPL: &str = r#"
13<!DOCTYPE html>
14<html>
15 <head>
16 <title>{{title}}</title>
17 {{keywords}}
18 {{description}}
19 <meta charset="utf-8">
20 <meta name="viewport" content="width=device-width, initial-scale=1">
21 <style>
22 {{style}}
23 </style>
24 </head>
25
26 <body>{{header}}
27 <script id="api-reference" data-url="{{spec_url}}"></script>
28 <script src="{{lib_url}}"></script>
29 </body>
30</html>
31"#;
32
33#[non_exhaustive]
35#[derive(Clone, Debug)]
36pub struct Scalar {
37 pub title: Cow<'static, str>,
39 pub keywords: Option<Cow<'static, str>>,
41 pub description: Option<Cow<'static, str>>,
43 pub style: Option<Cow<'static, str>>,
45 pub header: Option<Cow<'static, str>>,
47 pub lib_url: Cow<'static, str>,
49 pub spec_url: Cow<'static, str>,
51}
52impl Scalar {
53 #[must_use]
65 pub fn new(spec_url: impl Into<Cow<'static, str>>) -> Self {
66 Self {
67 title: "Scalar".into(),
68 keywords: None,
69 description: None,
70 style: Some(Cow::from(DEFAULT_STYLE)),
71 header: None,
72 lib_url: "https://cdn.jsdelivr.net/npm/@scalar/api-reference".into(),
73 spec_url: spec_url.into(),
74 }
75 }
76
77 #[must_use]
79 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
80 self.title = title.into();
81 self
82 }
83
84 #[must_use]
86 pub fn keywords(mut self, keywords: impl Into<Cow<'static, str>>) -> Self {
87 self.keywords = Some(keywords.into());
88 self
89 }
90
91 #[must_use]
93 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
94 self.description = Some(description.into());
95 self
96 }
97
98 #[must_use]
100 pub fn lib_url(mut self, lib_url: impl Into<Cow<'static, str>>) -> Self {
101 self.lib_url = lib_url.into();
102 self
103 }
104
105 pub fn into_router(self, path: impl Into<String>) -> Router {
107 Router::with_path(path.into()).goal(self)
108 }
109}
110#[async_trait]
111impl Handler for Scalar {
112 async fn handle(&self, _req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) {
113 let keywords = self
114 .keywords
115 .as_ref()
116 .map(|s| {
117 format!(
118 "<meta name=\"keywords\" content=\"{}\">",
119 s.split(',').map(|s| s.trim()).collect::<Vec<_>>().join(",")
120 )
121 })
122 .unwrap_or_default();
123 let description = self
124 .description
125 .as_ref()
126 .map(|s| format!("<meta name=\"description\" content=\"{s}\">"))
127 .unwrap_or_default();
128 let style = self
129 .style
130 .as_ref()
131 .map(|s| format!("<style>{s}</style>"))
132 .unwrap_or_default();
133 let html = INDEX_TMPL
134 .replacen("{{lib_url}}", &self.lib_url, 1)
135 .replacen("{{spec_url}}", &self.spec_url, 1)
136 .replacen("{{header}}", self.header.as_deref().unwrap_or_default(), 1)
137 .replacen("{{style}}", &style, 1)
138 .replacen("{{description}}", &description, 1)
139 .replacen("{{keywords}}", &keywords, 1)
140 .replacen("{{title}}", &self.title, 1);
141 res.render(Text::Html(html));
142 }
143}
144
145const DEFAULT_STYLE: &str = r#":root {
146 --theme-font: 'Inter', var(--system-fonts);
147 }
148 /* basic theme */
149 .light-mode {
150 --theme-color-1: #2c3d50;
151 --theme-color-2: #38495c;
152 --theme-color-3: #445569;
153 --theme-color-accent: #3faf7c;
154
155 --theme-background-1: #fff;
156 --theme-background-2: #f6f6f6;
157 --theme-background-3: #e7e7e7;
158 --theme-background-accent: #8ab4f81f;
159
160 --theme-border-color: rgba(0, 0, 0, 0.1);
161 }
162 .dark-mode {
163 --theme-color-1: rgb(150, 167, 183, 1);
164 --theme-color-2: rgba(150, 167, 183, 0.72);
165 --theme-color-3: rgba(150, 167, 183, 0.54);
166 --theme-color-accent: #329066;
167
168 --theme-background-1: #22272e;
169 --theme-background-2: #282c34;
170 --theme-background-3: #343841;
171 --theme-background-accent: #3290661f;
172
173 --theme-border-color: rgba(255, 255, 255, 0.1);
174 }
175 /* Document header */
176 .light-mode .t-doc__header {
177 --header-background-1: var(--theme-background-1);
178 --header-border-color: var(--theme-border-color);
179 --header-color-1: var(--theme-color-1);
180 --header-color-2: var(--theme-color-2);
181 --header-background-toggle: var(--theme-color-3);
182 --header-call-to-action-color: var(--theme-color-accent);
183 }
184
185 .dark-mode .t-doc__header {
186 --header-background-1: var(--theme-background-1);
187 --header-border-color: var(--theme-border-color);
188 --header-color-1: var(--theme-color-1);
189 --header-color-2: var(--theme-color-2);
190 --header-background-toggle: var(--theme-color-3);
191 --header-call-to-action-color: var(--theme-color-accent);
192 }
193 /* Document Sidebar */
194 .light-mode .t-doc__sidebar,
195 .dark-mode .t-doc__sidebar {
196 --sidebar-background-1: var(--theme-background-1);
197 --sidebar-item-hover-color: var(--theme-color-accent);
198 --sidebar-item-hover-background: transparent;
199 --sidebar-item-active-background: transparent;
200 --sidebar-border-color: var(--theme-border-color);
201 --sidebar-color-1: var(--theme-color-1);
202 --sidebar-color-2: var(--theme-color-2);
203 --sidebar-color-active: var(--theme-color-accent);
204 --sidebar-search-background: transparent;
205 --sidebar-search-border-color: var(--theme-border-color);
206 --sidebar-search--color: var(--theme-color-3);
207 }
208 .light-mode .t-doc__sidebar .active_page.sidebar-heading,
209 .dark-mode .t-doc__sidebar .active_page.sidebar-heading {
210 background: transparent !important;
211 box-shadow: inset 3px 0 0 var(--theme-color-accent);
212 }
213
214 /* advanced */
215 .light-mode {
216 --theme-button-1: rgb(49 53 56);
217 --theme-button-1-color: #fff;
218 --theme-button-1-hover: rgb(28 31 33);
219
220 --theme-color-green: #069061;
221 --theme-color-red: #ef0006;
222 --theme-color-yellow: #edbe20;
223 --theme-color-blue: #0082d0;
224 --theme-color-orange: #fb892c;
225 --theme-color-purple: #5203d1;
226
227 --theme-scrollbar-color: rgba(0, 0, 0, 0.18);
228 --theme-scrollbar-color-active: rgba(0, 0, 0, 0.36);
229 }
230 .dark-mode {
231 --theme-button-1: #f6f6f6;
232 --theme-button-1-color: #000;
233 --theme-button-1-hover: #e7e7e7;
234
235 --theme-color-green: #00b648;
236 --theme-color-red: #dc1b19;
237 --theme-color-yellow: #ffc90d;
238 --theme-color-blue: #4eb3ec;
239 --theme-color-orange: #ff8d4d;
240 --theme-color-purple: #b191f9;
241
242 --theme-scrollbar-color: var(--theme-color-accent);
243 --theme-scrollbar-color-active: var(--theme-color-accent);
244 }
245 body {margin: 0; padding: 0;}
246 .dark-mode .show-api-client-button span,
247 .light-mode .show-api-client-button span,
248 .light-mode .show-api-client-button svg,
249 .dark-mode .show-api-client-button svg {
250 color: white;
251 }
252 .t-doc__header .header-item-logo {
253 height: 32px;
254 }
255"#;