liwe 0.15.0

IWE core library
Documentation
use std::{collections::HashMap, fmt::Display, ops::Range, path::Path, sync::Arc};

use percent_encoding::percent_decode_str;
use relative_path::RelativePath;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use config::RefsPath;

pub mod config;
pub mod document;
pub mod ids;
pub mod inline;
pub mod key_index;
pub mod node;
pub mod node_iter;
pub mod node_pointer;
pub mod projector;
pub mod reference;
pub mod tree;
pub mod tree_iter;
pub mod writer;

pub use document::{
    Attributes, BlockQuote, BulletList, Code, CodeBlock, Div, Document, DocumentBlock,
    DocumentBlocks, DocumentInline, DocumentInlines, Emph, Format, Header, HorizontalRule, Image,
    LineBlock, LineBreak, Link, LinkType, Math, MathType, OrderedList, Para, Plain, RawBlock,
    RawInline, SmallCaps, SoftBreak, Space, Strikeout, Strong, Subscript, Superscript, Target,
    Underline,
};
pub use inline::{inlines_to_markdown, to_graph_inlines, to_plain_text, Inline, Inlines};
pub use node::{ColumnAlignment, Node, Reference, ReferenceType};
pub use node_iter::NodeIter;
pub use node_pointer::NodePointer;
pub use projector::Projector;
pub use tree::Tree;
pub use tree_iter::TreeIter;
pub use writer::{blocks_to_markdown, Block};

use writer::frontmatter_to_yaml;

pub type Frontmatter = serde_yaml::Mapping;

pub fn frontmatter_from_str(raw: &str) -> Option<Frontmatter> {
    if raw.trim().is_empty() {
        return Some(Frontmatter::new());
    }
    match serde_yaml::from_str::<serde_yaml::Value>(raw.trim()).ok()? {
        serde_yaml::Value::Mapping(mapping) => Some(mapping),
        _ => None,
    }
}

pub fn frontmatter_to_string(frontmatter: &Frontmatter) -> String {
    if frontmatter.is_empty() {
        return String::new();
    }
    frontmatter_to_yaml(frontmatter)
        .trim_start_matches("---\n")
        .trim_end()
        .to_string()
}

pub type Markdown = String;

pub type MaybeKey = Option<Key>;

pub type Content = String;
pub type State = HashMap<String, String>;

pub type NodeId = i64;
pub type MaybeNodeId = Option<NodeId>;

pub type LineId = i64;
pub type MaybeLineId = Option<LineId>;

pub type StrId = usize;
pub type MaybeStrId = Option<StrId>;

pub type LineNumber = usize;
pub type LineRange = Range<LineNumber>;

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Key {
    text: Arc<str>,
}

impl Display for Key {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl Key {
    pub fn as_str(&self) -> &str {
        &self.text
    }

    pub fn parent(&self) -> String {
        RelativePath::new(self.as_str())
            .parent()
            .map(|p| p.to_string())
            .unwrap_or_default()
    }

    pub fn source(&self) -> String {
        RelativePath::new(self.as_str())
            .file_name()
            .unwrap_or("")
            .to_string()
    }

    pub fn path(&self) -> Option<String> {
        RelativePath::new(self.as_str())
            .parent()
            .map(|p| p.to_string())
    }

    pub fn combine(parent: &str, id: &str) -> Key {
        let path = RelativePath::new(parent).join(id).to_string();
        Key {
            text: Arc::from(path),
        }
    }

    pub fn name(name: &str) -> Self {
        Key {
            text: Arc::from(strip_doc_extension(name)),
        }
    }

    pub fn from_stripped(key: &str) -> Self {
        Key {
            text: Arc::from(key),
        }
    }

    pub fn from_rel_link_url(url: &str, relative_to: &str) -> Self {
        let decoded = percent_decode_str(url).decode_utf8_lossy().into_owned();
        let without_fragment = match decoded.split_once('#') {
            Some((path, _)) => path,
            None => decoded.as_str(),
        };
        let key = strip_doc_extension(without_fragment);
        let base = if key.starts_with('/') {
            ""
        } else {
            relative_to
        };
        let path = RelativePath::new(base).join_normalized(key).to_string();
        Key {
            text: Arc::from(path),
        }
    }

    pub fn to_rel_link_url(&self, relative_to: &str) -> String {
        RelativePath::new(relative_to)
            .relative(self.as_str())
            .to_string()
    }

    pub fn to_library_url(&self) -> String {
        self.as_str().to_string()
    }

    pub fn link_url(&self, relative_to: &str, refs_path: RefsPath) -> String {
        match refs_path {
            RefsPath::Relative => self.to_rel_link_url(relative_to),
            RefsPath::Absolute => format!("/{}", self.to_library_url()),
        }
    }

    pub fn last_url_segment(&self) -> String {
        self.as_str().to_string()
    }

    pub fn to_path(&self, format: config::Format) -> String {
        format!("{}.{}", self.as_str(), format.extension())
    }

    pub fn from_path(path: &Path) -> Option<Key> {
        let name = path.file_name()?.to_string_lossy().to_string();
        Some(Key::name(&name))
    }
}

impl Default for Key {
    fn default() -> Self {
        Key::name("")
    }
}

impl Serialize for Key {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de> Deserialize<'de> for Key {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Ok(Key::name(&s))
    }
}

pub fn strip_doc_extension(name: &str) -> &str {
    name.strip_suffix(".md")
        .or_else(|| name.strip_suffix(".dj"))
        .unwrap_or(name)
}

impl From<&str> for Key {
    fn from(value: &str) -> Self {
        Key::name(value)
    }
}

impl From<String> for Key {
    fn from(value: String) -> Self {
        Key::name(&value)
    }
}

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default, Hash)]
pub struct Position {
    pub line: usize,
    pub character: usize,
}

impl From<(usize, usize)> for Position {
    fn from(value: (usize, usize)) -> Self {
        Position {
            line: value.0,
            character: value.1,
        }
    }
}

pub type InlineRange = Range<Position>;

pub type NodesMap = Vec<(NodeId, LineRange)>;
pub type DocumentNodesMap = (Key, NodesMap);

pub type Lang = String;
pub type LibraryUrl = String;

pub type Level = u8;
pub type Title = String;

pub trait InlinesContext: Copy {
    fn get_ref_title(&self, key: &Key) -> Option<String>;
    fn wiki_display(&self, key: &Key, original_url: &str) -> String;
    fn normalize_ref_text(&self) -> bool;
}

pub fn is_ref_url(url: &str) -> bool {
    !(url.to_lowercase().starts_with("http://")
        || url.to_lowercase().starts_with("https://")
        || url.to_lowercase().starts_with("mailto:"))
}

pub fn normalize_url(url: &str, extension: &str) -> String {
    if !is_ref_url(url) {
        return url.to_string();
    }
    match url.split_once('#') {
        Some((path, fragment)) => format!(
            "{}#{}",
            path.strip_suffix(extension).unwrap_or(path),
            fragment
        ),
        None => url.strip_suffix(extension).unwrap_or(url).to_string(),
    }
}