Skip to content

Dan doc link fix #1281

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pgml-dashboard/src/api/cms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ This is the end of the markdown
#[sqlx::test]
async fn render_blogs_test() {
let client = Client::tracked(rocket().await).await.unwrap();
let blog: Collection = Collection::new("Blog", true);
let blog: Collection = Collection::new("Blog", true, HashMap::new());

for path in blog.index {
let req = client.get(path.clone().href);
Expand All @@ -579,7 +579,7 @@ This is the end of the markdown
#[sqlx::test]
async fn render_guides_test() {
let client = Client::tracked(rocket().await).await.unwrap();
let docs: Collection = Collection::new("Docs", true);
let docs: Collection = Collection::new("Docs", true, HashMap::new());

for path in docs.index {
let req = client.get(path.clone().href);
Expand Down
61 changes: 54 additions & 7 deletions pgml-dashboard/src/templates/docs.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use convert_case;
use lazy_static::lazy_static;
use sailfish::TemplateOnce;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

use crate::utils::markdown::SearchResult;

Expand All @@ -11,6 +15,26 @@ pub struct Search {
pub results: Vec<SearchResult>,
}

lazy_static! {
static ref CMS_IDENTIFIER: CmsIdentifier = CmsIdentifier::new();
}

// Prevent css collisions in cms header ids.
pub struct CmsIdentifier {
pub id: String,
}

impl CmsIdentifier {
pub fn new() -> CmsIdentifier {
let mut s = DefaultHasher::new();
"cms header".hash(&mut s);

CmsIdentifier {
id: s.finish().to_string(),
}
}
}

/// Table of contents link.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TocLink {
Expand All @@ -25,9 +49,23 @@ impl TocLink {
/// # Arguments
///
/// * `title` - The title of the link.
/// * `counter` - The number of times that header is in the document
///
pub fn new(title: &str, counter: usize) -> TocLink {
let id = format!("header-{}", counter);
let conv = convert_case::Converter::new().to_case(convert_case::Case::Kebab);
let id = conv.convert(title.to_string());

// gitbook style id's
let id = format!(
"{}{}-{}",
id,
if counter > 0 {
format!("-{counter}")
} else {
String::new()
},
CMS_IDENTIFIER.id
);

TocLink {
title: title.to_string(),
Expand All @@ -43,11 +81,20 @@ impl TocLink {
self.level = level;
self
}
}

/// Table of contents template.
#[derive(TemplateOnce)]
#[template(path = "components/toc.html")]
pub struct Toc {
pub links: Vec<TocLink>,
/// Converts gitbook link fragment to toc header
pub fn from_fragment(link: String) -> TocLink {
match link.is_empty() {
true => TocLink {
title: String::new(),
id: String::new(),
level: 0,
},
_ => TocLink {
title: link.clone(),
id: format!("#{}-{}", link.clone(), CMS_IDENTIFIER.id),
level: 0,
},
}
}
}
112 changes: 55 additions & 57 deletions pgml-dashboard/src/utils/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ use crate::{templates::docs::TocLink, utils::config};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
use std::sync::Arc;

use anyhow::Result;
use comrak::{
Expand All @@ -15,25 +12,27 @@ use comrak::{
nodes::{Ast, AstNode, NodeValue},
parse_document, Arena, ComrakExtensionOptions, ComrakOptions, ComrakRenderOptions,
};
use convert_case;
use itertools::Itertools;
use regex::Regex;
use tantivy::collector::TopDocs;
use tantivy::query::{QueryParser, RegexQuery};
use tantivy::schema::*;
use tantivy::tokenizer::{LowerCaser, NgramTokenizer, TextAnalyzer};
use tantivy::{Index, IndexReader, SnippetGenerator};
use url::Url;

use std::sync::Mutex;

use std::fmt;

pub struct MarkdownHeadings {
counter: Arc<AtomicUsize>,
header_map: Arc<Mutex<HashMap<String, usize>>>,
}

impl Default for MarkdownHeadings {
fn default() -> Self {
Self {
counter: Arc::new(AtomicUsize::new(0)),
header_map: Arc::new(Mutex::new(HashMap::new())),
}
}
}
Expand All @@ -44,31 +43,42 @@ impl MarkdownHeadings {
}
}

/// Sets the document headers
///
/// uses toclink to ensure header id matches what the TOC expects
///
impl HeadingAdapter for MarkdownHeadings {
fn enter(&self, meta: &HeadingMeta) -> String {
// let id = meta.content.to_case(convert_case::Case::Kebab);
let id = self.counter.fetch_add(1, Ordering::SeqCst);
let id = format!("header-{}", id);
let conv = convert_case::Converter::new().to_case(convert_case::Case::Kebab);
let id = conv.convert(meta.content.to_string());

let index = match self.header_map.lock().unwrap().get(&id) {
Some(value) => value + 1,
_ => 0,
};
self.header_map.lock().unwrap().insert(id.clone(), index);

let id = TocLink::new(&id, index).id;

match meta.level {
1 => format!(r#"<h1 class="h1 mb-5" id="{id}">"#),
2 => format!(r#"<h2 class="h2 mb-4 mt-5" id="{id}">"#),
3 => format!(r#"<h3 class="h3 mb-4 mt-5" id="{id}">"#),
4 => format!(r#"<h4 class="h5 mb-3 mt-3" id="{id}">"#),
5 => format!(r#"<h5 class="h6 mb-2 mt-4" id="{id}">"#),
6 => format!(r#"<h6 class="h6 mb-1 mt-1" id="{id}">"#),
1 => format!(r##"<h1 class="h1 mb-5" id="{id}"><a href="#{id}">"##),
2 => format!(r##"<h2 class="h2 mb-4 mt-5" id="{id}"><a href="#{id}">"##),
3 => format!(r##"<h3 class="h3 mb-4 mt-5" id="{id}"><a href="#{id}">"##),
4 => format!(r##"<h4 class="h5 mb-3 mt-3" id="{id}"><a href="#{id}">"##),
5 => format!(r##"<h5 class="h6 mb-2 mt-4" id="{id}"><a href="#{id}">"##),
6 => format!(r##"<h6 class="h6 mb-1 mt-1" id="{id}"><a href="#{id}">"##),
_ => unreachable!(),
}
}

fn exit(&self, meta: &HeadingMeta) -> String {
match meta.level {
1 => r#"</h1>"#,
2 => r#"</h2>"#,
3 => r#"</h3>"#,
4 => r#"</h4>"#,
5 => r#"</h5>"#,
6 => r#"</h6>"#,
1 => r#"</a></h1>"#,
2 => r#"</a></h2>"#,
3 => r#"</a></h3>"#,
4 => r#"</a></h4>"#,
5 => r#"</a></h5>"#,
6 => r#"</a></h6>"#,
_ => unreachable!(),
}
.into()
Expand Down Expand Up @@ -335,38 +345,6 @@ where
Ok(())
}

pub fn nest_relative_links(node: &mut markdown::mdast::Node, path: &PathBuf) {
let _ = iter_mut_all(node, &mut |node| {
if let markdown::mdast::Node::Link(ref mut link) = node {
match Url::parse(&link.url) {
Ok(url) => {
if !url.has_host() {
let mut url_path = url.path().to_string();
let url_path_path = Path::new(&url_path);
match url_path_path.extension() {
Some(ext) => {
if ext.to_str() == Some(".md") {
let base = url_path_path.with_extension("");
url_path = base.into_os_string().into_string().unwrap();
}
}
_ => {
warn!("not markdown path: {:?}", path)
}
}
link.url = path.join(url_path).into_os_string().into_string().unwrap();
}
}
Err(e) => {
warn!("could not parse url in markdown: {}", e)
}
}
}

Ok(())
});
}

/// Get the title of the article.
///
/// # Arguments
Expand Down Expand Up @@ -462,11 +440,10 @@ pub fn wrap_tables<'a>(root: &'a AstNode<'a>, arena: &'a Arena<AstNode<'a>>) ->
///
pub fn get_toc<'a>(root: &'a AstNode<'a>) -> anyhow::Result<Vec<TocLink>> {
let mut links = Vec::new();
let mut header_counter = 0;
let mut header_count: HashMap<String, usize> = HashMap::new();

iter_nodes(root, &mut |node| {
if let NodeValue::Heading(header) = &node.data.borrow().value {
header_counter += 1;
if header.level != 1 {
let sibling = match node.first_child() {
Some(child) => child,
Expand All @@ -476,7 +453,14 @@ pub fn get_toc<'a>(root: &'a AstNode<'a>) -> anyhow::Result<Vec<TocLink>> {
}
};
if let NodeValue::Text(text) = &sibling.data.borrow().value {
links.push(TocLink::new(text, header_counter - 1).level(header.level));
let index = match header_count.get(text) {
Some(index) => index + 1,
_ => 0,
};

header_count.insert(text.clone(), index);

links.push(TocLink::new(text, index).level(header.level));
return Ok(false);
}
}
Expand Down Expand Up @@ -753,11 +737,25 @@ pub fn mkdocs<'a>(root: &'a AstNode<'a>, arena: &'a Arena<AstNode<'a>>) -> anyho
let path = Path::new(link.url.as_str());

if path.is_relative() {
let fragment = match link.url.find("#") {
Some(index) => link.url[index + 1..link.url.len()].to_string(),
_ => "".to_string(),
};

for _ in 0..fragment.len() + 1 {
link.url.pop();
}

if link.url.ends_with(".md") {
for _ in 0..".md".len() {
link.url.pop();
}
}

let header_id = TocLink::from_fragment(fragment).id;
for c in header_id.chars() {
link.url.push(c)
}
}

Ok(true)
Expand Down
16 changes: 16 additions & 0 deletions pgml-dashboard/static/css/scss/pages/_docs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -206,5 +206,21 @@
display: contents !important;
}
}

h1, h2, h3, h4, h5, h6 {
scroll-margin-top: 108px;

&:hover {
&:after {
content: '#';
margin-left: 0.2em;
position: absolute;
}
}

a {
color: inherit !important;
}
}
}

12 changes: 12 additions & 0 deletions pgml-dashboard/static/js/docs-toc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,16 @@ export default class extends Controller {
threshold: [1],
})
}

setUrlFragment(e) {
let href = e.target.attributes.href.nodeValue;
if (href) {
if (href.startsWith("#")) {
let hash = href.slice(1);
if (window.location.hash != hash) {
window.location.hash = hash
}
}
}
}
}
18 changes: 0 additions & 18 deletions pgml-dashboard/templates/components/toc.html

This file was deleted.

2 changes: 1 addition & 1 deletion pgml-dashboard/templates/layout/nav/toc.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ <h6 class="mb-2 pb-2 d-none d-xxl-block">Table of Contents</h6>
<div id="toc-nav" class="d-xxl-flex pt-2 flex-column collapse border-top" aria-orientation="vertical" data-controller="docs-toc">
<% for link in toc_links.iter() { %>
<div style="padding-left: <%= link.level as f32 * 0.7 - 1.4 %>rem;">
<a class="nav-link px-0 text-break" href="#<%= link.id %>" role="button" data-action="docs-toc#scrollSpyAppend" >
<a class="nav-link px-0 text-break" href="#<%= link.id %>" role="button" data-action="click->docs-toc#setUrlFragment">
<%= link.title %>
</a>
</div>
Expand Down