Skip to content

Commit a50d276

Browse files
Dan gitbook fix 1 (#1247)
1 parent f6eda7a commit a50d276

File tree

4 files changed

+272
-119
lines changed

4 files changed

+272
-119
lines changed

pgml-dashboard/src/api/cms.rs

Lines changed: 174 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,94 @@ use crate::{
1919
utils::config,
2020
};
2121

22+
use serde::{Deserialize, Serialize};
23+
2224
lazy_static! {
2325
static ref BLOG: Collection = Collection::new("Blog", true);
2426
static ref CAREERS: Collection = Collection::new("Careers", true);
2527
static ref DOCS: Collection = Collection::new("Docs", false);
2628
}
2729

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+
28110
/// A Gitbook collection of documents
29111
#[derive(Default)]
30112
struct Collection {
@@ -62,6 +144,7 @@ impl Collection {
62144

63145
pub async fn get_asset(&self, path: &str) -> Option<NamedFile> {
64146
info!("get_asset: {} {path}", self.name);
147+
65148
NamedFile::open(self.asset_dir.join(path)).await.ok()
66149
}
67150

@@ -79,7 +162,7 @@ impl Collection {
79162

80163
let path = self.root_dir.join(format!("{}.md", path.to_string_lossy()));
81164

82-
self.render(&path, cluster, self).await
165+
self.render(&path, cluster).await
83166
}
84167

85168
/// Create an index of the Collection based on the SUMMARY.md from Gitbook.
@@ -173,109 +256,35 @@ impl Collection {
173256
Ok(links)
174257
}
175258

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
256262
.clone()
257263
.iter_mut()
258264
.map(|nav_link| {
259265
let mut nav_link = nav_link.clone();
260-
nav_link.should_open(path);
266+
nav_link.should_open(&path);
261267
nav_link
262268
})
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);
264276

265277
let user = if cluster.context.user.is_anonymous() {
266278
None
267279
} else {
268280
Some(cluster.context.user.clone())
269281
};
270282

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()));
277286
}
278-
if let Some(description) = &description {
287+
if let Some(description) = &doc.description {
279288
layout.description(description);
280289
}
281290
if let Some(user) = &user {
@@ -285,11 +294,11 @@ impl Collection {
285294
let layout = layout
286295
.nav_title(&self.name)
287296
.nav_links(&index)
288-
.toc_links(&toc_links)
297+
.toc_links(&doc.toc_links)
289298
.footer(cluster.context.marketing_footer.to_string());
290299

291300
Ok(ResponseOk(
292-
layout.render(crate::templates::Article { content: html }),
301+
layout.render(crate::templates::Article { content: doc.html }),
293302
))
294303
}
295304
}
@@ -365,6 +374,10 @@ pub fn routes() -> Vec<Route> {
365374
mod test {
366375
use super::*;
367376
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};
368381

369382
#[test]
370383
fn test_syntax_highlighting() {
@@ -452,4 +465,73 @@ This is the end of the markdown
452465
!html.contains(r#"<div class="overflow-auto w-100">"#) || !html.contains(r#"</div>"#)
453466
);
454467
}
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 path in 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 path in 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+
}
455537
}

0 commit comments

Comments
 (0)