use std::collections::{HashMap, HashSet};
use comrak::nodes::AstNode;
#[derive(Debug, Clone)]
#[allow(
clippy::struct_excessive_bools,
reason = "Config struct with related boolean flags"
)]
pub struct MarkdownOptions {
pub gfm: bool,
pub nixpkgs: bool,
pub highlight_code: bool,
pub highlight_theme: Option<String>,
pub manpage_urls_path: Option<String>,
pub auto_link_options: bool,
pub valid_options: Option<HashSet<String>>,
pub tab_style: TabStyle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TabStyle {
None,
Warn,
Normalize,
}
impl MarkdownOptions {
#[must_use]
pub const fn with_all_features() -> Self {
Self {
gfm: cfg!(feature = "gfm"),
nixpkgs: cfg!(feature = "nixpkgs"),
highlight_code: cfg!(any(feature = "syntastica", feature = "syntect")),
highlight_theme: None,
manpage_urls_path: None,
auto_link_options: true,
valid_options: None,
tab_style: TabStyle::None,
}
}
#[must_use]
pub const fn with_features(
gfm: bool,
nixpkgs: bool,
highlight_code: bool,
) -> Self {
Self {
gfm,
nixpkgs,
highlight_code,
highlight_theme: None,
manpage_urls_path: None,
auto_link_options: true,
valid_options: None,
tab_style: TabStyle::None,
}
}
}
impl Default for MarkdownOptions {
fn default() -> Self {
Self {
gfm: cfg!(feature = "gfm"),
nixpkgs: cfg!(feature = "nixpkgs"),
highlight_code: cfg!(feature = "syntastica"),
manpage_urls_path: None,
highlight_theme: None,
auto_link_options: true,
valid_options: None,
tab_style: TabStyle::None,
}
}
}
#[derive(Clone)]
pub struct MarkdownProcessor {
pub(crate) options: MarkdownOptions,
pub(crate) manpage_urls: Option<HashMap<String, String>>,
pub(crate) syntax_manager: Option<crate::syntax::SyntaxManager>,
pub(crate) base_dir: std::path::PathBuf,
}
pub trait AstTransformer {
fn transform<'a>(&self, node: &'a AstNode<'a>);
}
pub struct PromptTransformer;
impl AstTransformer for PromptTransformer {
fn transform<'a>(&self, node: &'a AstNode<'a>) {
use std::sync::LazyLock;
use comrak::nodes::NodeValue;
use regex::Regex;
static COMMAND_PROMPT_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^\s*\$\s+(.+)$").unwrap_or_else(|e| {
log::error!(
"Failed to compile COMMAND_PROMPT_RE regex: {e}\n Falling back to \
never matching regex."
);
crate::utils::never_matching_regex().unwrap_or_else(|_| {
#[allow(
clippy::expect_used,
reason = "This pattern is guaranteed to be valid"
)]
Regex::new(r"[^\s\S]")
.expect("regex pattern [^\\s\\S] should always compile")
})
})
});
static REPL_PROMPT_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^nix-repl>\s*(.*)$").unwrap_or_else(|e| {
log::error!(
"Failed to compile REPL_PROMPT_RE regex: {e}\n Falling back to \
never matching regex."
);
crate::utils::never_matching_regex().unwrap_or_else(|_| {
#[allow(
clippy::expect_used,
reason = "This pattern is guaranteed to be valid"
)]
Regex::new(r"[^\s\S]")
.expect("regex pattern [^\\s\\S] should always compile")
})
})
});
for child in node.children() {
{
let mut data = child.data.borrow_mut();
if let NodeValue::Code(ref code) = data.value {
let literal = code.literal.trim();
if let Some(caps) = COMMAND_PROMPT_RE.captures(literal) {
if !literal.starts_with("\\$") && !literal.starts_with("$$") {
let command = caps[1].trim();
let html = format!(
"<code class=\"terminal\"><span class=\"prompt\">$</span> \
{command}</code>"
);
data.value = NodeValue::HtmlInline(html);
}
} else if let Some(caps) = REPL_PROMPT_RE.captures(literal) {
if !literal.starts_with("nix-repl>>") {
let expression = caps[1].trim();
let html = format!(
"<code class=\"nix-repl\"><span \
class=\"prompt\">nix-repl></span> {expression}</code>"
);
data.value = NodeValue::HtmlInline(html);
}
}
}
}
self.transform(child);
}
}
}
#[derive(Debug, Clone)]
pub struct MarkdownOptionsBuilder {
options: MarkdownOptions,
}
impl MarkdownOptionsBuilder {
#[must_use]
pub fn new() -> Self {
Self {
options: MarkdownOptions::default(),
}
}
#[must_use]
pub const fn gfm(mut self, enabled: bool) -> Self {
self.options.gfm = enabled;
self
}
#[must_use]
pub const fn nixpkgs(mut self, enabled: bool) -> Self {
self.options.nixpkgs = enabled;
self
}
#[must_use]
pub const fn highlight_code(mut self, enabled: bool) -> Self {
self.options.highlight_code = enabled;
self
}
#[must_use]
pub fn highlight_theme<S: Into<String>>(mut self, theme: Option<S>) -> Self {
self.options.highlight_theme = theme.map(Into::into);
self
}
#[must_use]
pub fn manpage_urls_path<S: Into<String>>(mut self, path: Option<S>) -> Self {
self.options.manpage_urls_path = path.map(Into::into);
self
}
#[must_use]
pub const fn auto_link_options(mut self, enabled: bool) -> Self {
self.options.auto_link_options = enabled;
self
}
#[must_use]
pub fn valid_options(mut self, options: Option<HashSet<String>>) -> Self {
self.options.valid_options = options;
self
}
#[must_use]
pub const fn tab_style(mut self, style: TabStyle) -> Self {
self.options.tab_style = style;
self
}
#[must_use]
pub fn build(self) -> MarkdownOptions {
self.options
}
#[must_use]
pub fn from_external_config<T>(_config: &T) -> Self {
Self::new()
}
}
impl Default for MarkdownOptionsBuilder {
fn default() -> Self {
Self::new()
}
}