@@ -19,12 +19,94 @@ use crate::{
19
19
utils:: config,
20
20
} ;
21
21
22
+ use serde:: { Deserialize , Serialize } ;
23
+
22
24
lazy_static ! {
23
25
static refBLOG : Collection =Collection :: new( "Blog" , true ) ;
24
26
static refCAREERS : Collection =Collection :: new( "Careers" , true ) ;
25
27
static refDOCS : Collection =Collection :: new( "Docs" , false ) ;
26
28
}
27
29
30
+ #[ derive( Debug , Serialize , Deserialize ) ]
31
+ pub struct Document {
32
+ /// The absolute path on disk
33
+ pub path : PathBuf ,
34
+ pub description : Option < String > ,
35
+ pub image : Option < String > ,
36
+ pub title : String ,
37
+ pub toc_links : Vec < TocLink > ,
38
+ pub html : String ,
39
+ }
40
+
41
+ impl Document {
42
+ pub async fn from_path ( path : & PathBuf ) -> anyhow:: Result < Document > {
43
+ let contents = tokio:: fs:: read_to_string ( & path) . await ?;
44
+
45
+ let parts = contents. split ( "---" ) . collect :: < Vec < & str > > ( ) ;
46
+
47
+ let ( description, contents) =if parts. len ( ) >1 {
48
+ match YamlLoader :: load_from_str ( parts[ 1 ] ) {
49
+ Ok ( meta) =>{
50
+ if meta. len ( ) ==0 || meta[ 0 ] . as_hash ( ) . is_none ( ) {
51
+ ( None , contents)
52
+ } else {
53
+ let description: Option < String > =match meta[ 0 ] [ "description" ] . is_badvalue ( )
54
+ {
55
+ true =>None ,
56
+ false =>Some ( meta[ 0 ] [ "description" ] . as_str ( ) . unwrap ( ) . to_string ( ) ) ,
57
+ } ;
58
+ ( description, parts[ 2 ..] . join ( "---" ) . to_string ( ) )
59
+ }
60
+ }
61
+ Err ( _) =>( None , contents) ,
62
+ }
63
+ } else {
64
+ ( None , contents)
65
+ } ;
66
+
67
+ // Parse Markdown
68
+ let arena =Arena :: new ( ) ;
69
+ let spaced_contents =crate :: utils:: markdown:: gitbook_preprocess ( & contents) ;
70
+ let root =parse_document ( & arena, & spaced_contents, & crate :: utils:: markdown:: options ( ) ) ;
71
+
72
+ // Title of the document is the first (and typically only) <h1>
73
+ let title =crate :: utils:: markdown:: get_title ( root) . unwrap ( ) ;
74
+ let toc_links =crate :: utils:: markdown:: get_toc ( root) . unwrap ( ) ;
75
+ let image =crate :: utils:: markdown:: get_image ( root) ;
76
+ crate :: utils:: markdown:: wrap_tables ( root, & arena) . unwrap ( ) ;
77
+
78
+ // MkDocs, gitbook syntax support, e.g. tabs, notes, alerts, etc.
79
+ crate :: utils:: markdown:: mkdocs ( root, & arena) . unwrap ( ) ;
80
+
81
+ // Style headings like we like them
82
+ let mut plugins =ComrakPlugins :: default ( ) ;
83
+ let headings =crate :: utils:: markdown:: MarkdownHeadings :: new ( ) ;
84
+ plugins. render . heading_adapter =Some ( & headings) ;
85
+ plugins. render . codefence_syntax_highlighter =
86
+ Some ( & crate :: utils:: markdown:: SyntaxHighlighter { } ) ;
87
+
88
+ let mut html =vec ! [ ] ;
89
+ format_html_with_plugins (
90
+ root,
91
+ & crate :: utils:: markdown:: options ( ) ,
92
+ & mut html,
93
+ & plugins,
94
+ )
95
+ . unwrap ( ) ;
96
+ let html =String :: from_utf8 ( html) . unwrap ( ) ;
97
+
98
+ let document =Document {
99
+ path : path. to_owned ( ) ,
100
+ description,
101
+ image,
102
+ title,
103
+ toc_links,
104
+ html,
105
+ } ;
106
+ Ok ( document)
107
+ }
108
+ }
109
+
28
110
/// A Gitbook collection of documents
29
111
#[ derive( Default ) ]
30
112
struct Collection {
@@ -62,6 +144,7 @@ impl Collection {
62
144
63
145
pub async fn get_asset ( & self , path : & str ) ->Option < NamedFile > {
64
146
info ! ( "get_asset: {} {path}" , self . name) ;
147
+
65
148
NamedFile :: open ( self . asset_dir . join ( path) ) . await . ok ( )
66
149
}
67
150
@@ -79,7 +162,7 @@ impl Collection {
79
162
80
163
let path =self . root_dir . join ( format ! ( "{}.md" , path. to_string_lossy( ) ) ) ;
81
164
82
- self . render ( & path, cluster, self ) . await
165
+ self . render ( & path, cluster) . await
83
166
}
84
167
85
168
/// Create an index of the Collection based on the SUMMARY.md from Gitbook.
@@ -173,109 +256,35 @@ impl Collection {
173
256
Ok ( links)
174
257
}
175
258
176
- async fn render < ' a > (
177
- & self ,
178
- path : & ' a PathBuf ,
179
- cluster : & Cluster ,
180
- collection : & Collection ,
181
- ) ->Result < ResponseOk , Status > {
182
- // Read to string0
183
- let contents =match tokio:: fs:: read_to_string ( & path) . await {
184
- Ok ( contents) =>{
185
- info ! ( "loading markdown file: '{:?}" , path) ;
186
- contents
187
- }
188
- Err ( err) =>{
189
- warn ! ( "Error parsing markdown file: '{:?}' {:?}" , path, err) ;
190
- return Err ( Status :: NotFound ) ;
191
- }
192
- } ;
193
- let parts = contents. split ( "---" ) . collect :: < Vec < & str > > ( ) ;
194
- let ( description, contents) =if parts. len ( ) >1 {
195
- match YamlLoader :: load_from_str ( parts[ 1 ] ) {
196
- Ok ( meta) =>{
197
- if !meta. is_empty ( ) {
198
- let meta = meta[ 0 ] . clone ( ) ;
199
- if meta. as_hash ( ) . is_none ( ) {
200
- ( None , contents. to_string ( ) )
201
- } else {
202
- let description: Option < String > =match meta[ "description" ]
203
- . is_badvalue ( )
204
- {
205
- true =>None ,
206
- false =>Some ( meta[ "description" ] . as_str ( ) . unwrap ( ) . to_string ( ) ) ,
207
- } ;
208
-
209
- ( description, parts[ 2 ..] . join ( "---" ) . to_string ( ) )
210
- }
211
- } else {
212
- ( None , contents. to_string ( ) )
213
- }
214
- }
215
- Err ( _) =>( None , contents. to_string ( ) ) ,
216
- }
217
- } else {
218
- ( None , contents. to_string ( ) )
219
- } ;
220
-
221
- // Parse Markdown
222
- let arena =Arena :: new ( ) ;
223
- let root =parse_document ( & arena, & contents, & crate :: utils:: markdown:: options ( ) ) ;
224
-
225
- // Title of the document is the first (and typically only) <h1>
226
- let title =crate :: utils:: markdown:: get_title ( root) . unwrap ( ) ;
227
- let toc_links =crate :: utils:: markdown:: get_toc ( root) . unwrap ( ) ;
228
- let image =crate :: utils:: markdown:: get_image ( root) ;
229
- crate :: utils:: markdown:: wrap_tables ( root, & arena) . unwrap ( ) ;
230
-
231
- // MkDocs syntax support, e.g. tabs, notes, alerts, etc.
232
- crate :: utils:: markdown:: mkdocs ( root, & arena) . unwrap ( ) ;
233
-
234
- // Style headings like we like them
235
- let mut plugins =ComrakPlugins :: default ( ) ;
236
- let headings =crate :: utils:: markdown:: MarkdownHeadings :: new ( ) ;
237
- plugins. render . heading_adapter =Some ( & headings) ;
238
- plugins. render . codefence_syntax_highlighter =
239
- Some ( & crate :: utils:: markdown:: SyntaxHighlighter { } ) ;
240
-
241
- // Render
242
- let mut html =vec ! [ ] ;
243
- format_html_with_plugins (
244
- root,
245
- & crate :: utils:: markdown:: options ( ) ,
246
- & mut html,
247
- & plugins,
248
- )
249
- . unwrap ( ) ;
250
- let html =String :: from_utf8 ( html) . unwrap ( ) ;
251
-
252
- // Handle navigation
253
- // TODO organize this functionality in the collection to cleanup
254
- let index: Vec < IndexLink > =self
255
- . index
259
+ // Sets specified index as currently viewed.
260
+ fn open_index ( & self , path : PathBuf ) ->Vec < IndexLink > {
261
+ self . index
256
262
. clone ( )
257
263
. iter_mut ( )
258
264
. map ( |nav_link|{
259
265
let mut nav_link = nav_link. clone ( ) ;
260
- nav_link. should_open ( path) ;
266
+ nav_link. should_open ( & path) ;
261
267
nav_link
262
268
} )
263
- . collect ( ) ;
269
+ . collect ( )
270
+ }
271
+
272
+ // renders document in layout
273
+ async fn render < ' a > ( & self , path : & ' a PathBuf , cluster : & Cluster ) ->Result < ResponseOk , Status > {
274
+ let doc =Document :: from_path ( & path) . await . unwrap ( ) ;
275
+ let index =self . open_index ( doc. path ) ;
264
276
265
277
let user =if cluster. context . user . is_anonymous ( ) {
266
278
None
267
279
} else {
268
280
Some ( cluster. context . user . clone ( ) )
269
281
} ;
270
282
271
- let mut layout =crate :: templates:: Layout :: new ( & title, Some ( cluster) ) ;
272
- if let Some ( image) = image{
273
- // translate relative url into absolute for head social sharing
274
- let parts = image. split ( ".gitbook/assets/" ) . collect :: < Vec < & str > > ( ) ;
275
- let image_path = collection. url_root . join ( ".gitbook/assets" ) . join ( parts[ 1 ] ) ;
276
- layout. image ( config:: asset_url ( image_path. to_string_lossy ( ) ) . as_ref ( ) ) ;
283
+ let mut layout =crate :: templates:: Layout :: new ( & doc. title , Some ( cluster) ) ;
284
+ if let Some ( image) = doc. image {
285
+ layout. image ( & config:: asset_url ( image. into ( ) ) ) ;
277
286
}
278
- if let Some ( description) =& description{
287
+ if let Some ( description) =& doc . description {
279
288
layout. description ( description) ;
280
289
}
281
290
if let Some ( user) =& user{
@@ -285,11 +294,11 @@ impl Collection {
285
294
let layout = layout
286
295
. nav_title ( & self . name )
287
296
. nav_links ( & index)
288
- . toc_links ( & toc_links)
297
+ . toc_links ( & doc . toc_links )
289
298
. footer ( cluster. context . marketing_footer . to_string ( ) ) ;
290
299
291
300
Ok ( ResponseOk (
292
- layout. render ( crate :: templates:: Article { content : html} ) ,
301
+ layout. render ( crate :: templates:: Article { content : doc . html } ) ,
293
302
) )
294
303
}
295
304
}
@@ -365,6 +374,10 @@ pub fn routes() -> Vec<Route> {
365
374
mod test{
366
375
use super :: * ;
367
376
use crate :: utils:: markdown:: { options, MarkdownHeadings , SyntaxHighlighter } ;
377
+ use regex:: Regex ;
378
+ use rocket:: http:: { ContentType , Cookie , Status } ;
379
+ use rocket:: local:: asynchronous:: Client ;
380
+ use rocket:: { Build , Rocket } ;
368
381
369
382
#[ test]
370
383
fn test_syntax_highlighting ( ) {
@@ -452,4 +465,73 @@ This is the end of the markdown
452
465
!html. contains( r#"<div class="overflow-auto w-100">"# ) || !html. contains( r#"</div>"# )
453
466
) ;
454
467
}
468
+
469
+ async fn rocket ( ) ->Rocket < Build > {
470
+ dotenv:: dotenv ( ) . ok ( ) ;
471
+ rocket:: build ( )
472
+ . manage ( crate :: utils:: markdown:: SearchIndex :: open ( ) . unwrap ( ) )
473
+ . mount ( "/" , crate :: api:: cms:: routes ( ) )
474
+ }
475
+
476
+ fn gitbook_test ( html : String ) ->Option < String > {
477
+ // all gitbook expresions should be removed, this catches {% %} nonsupported expressions.
478
+ let re =Regex :: new ( r"[{][%][^{]*[%][}]" ) . unwrap ( ) ;
479
+ let rsp = re. find ( & html) ;
480
+ if rsp. is_some ( ) {
481
+ return Some ( rsp. unwrap ( ) . as_str ( ) . to_string ( ) ) ;
482
+ }
483
+
484
+ // gitbook TeX block not supported yet
485
+ let re =Regex :: new ( r"(\$\$).*(\$\$)" ) . unwrap ( ) ;
486
+ let rsp = re. find ( & html) ;
487
+ if rsp. is_some ( ) {
488
+ return Some ( rsp. unwrap ( ) . as_str ( ) . to_string ( ) ) ;
489
+ }
490
+
491
+ None
492
+ }
493
+
494
+ // Ensure blogs render and there are no unparsed gitbook components.
495
+ #[ sqlx:: test]
496
+ async fn render_blogs_test ( ) {
497
+ let client =Client :: tracked ( rocket ( ) . await ) . await . unwrap ( ) ;
498
+ let blog: Collection =Collection :: new ( "Blog" , true ) ;
499
+
500
+ for pathin blog. index {
501
+ let req = client. get ( path. clone ( ) . href ) ;
502
+ let rsp = req. dispatch ( ) . await ;
503
+ let body = rsp. into_string ( ) . await . unwrap ( ) ;
504
+
505
+ let test =gitbook_test ( body) ;
506
+
507
+ assert ! (
508
+ test. is_none( ) ,
509
+ "bad html parse in {:?}. This feature is not supported {:?}" ,
510
+ path. href,
511
+ test. unwrap( )
512
+ )
513
+ }
514
+ }
515
+
516
+ // Ensure Docs render and ther are no unparsed gitbook compnents.
517
+ #[ sqlx:: test]
518
+ async fn render_guides_test ( ) {
519
+ let client =Client :: tracked ( rocket ( ) . await ) . await . unwrap ( ) ;
520
+ let docs: Collection =Collection :: new ( "Docs" , true ) ;
521
+
522
+ for pathin docs. index {
523
+ let req = client. get ( path. clone ( ) . href ) ;
524
+ let rsp = req. dispatch ( ) . await ;
525
+ let body = rsp. into_string ( ) . await . unwrap ( ) ;
526
+
527
+ let test =gitbook_test ( body) ;
528
+
529
+ assert ! (
530
+ test. is_none( ) ,
531
+ "bad html parse in {:?}. This feature is not supported {:?}" ,
532
+ path. href,
533
+ test. unwrap( )
534
+ )
535
+ }
536
+ }
455
537
}