use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use serde::Deserialize;
use crate::MdtError;
use crate::MdtResult;
pub const DEFAULT_MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
#[derive(Debug, Deserialize)]
pub struct MdtConfig {
#[serde(default)]
pub data: HashMap<String, PathBuf>,
#[serde(default)]
pub exclude: ExcludeConfig,
#[serde(default)]
pub include: IncludeConfig,
#[serde(default)]
pub templates: TemplatesConfig,
#[serde(default = "default_max_file_size")]
pub max_file_size: u64,
#[serde(default)]
pub padding: Option<PaddingConfig>,
#[serde(default)]
pub disable_gitignore: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
#[allow(variant_size_differences)]
pub enum PaddingValue {
Bool(bool),
Lines(u32),
}
impl PaddingValue {
pub fn line_count(&self) -> Option<u32> {
match self {
Self::Bool(false) => None,
Self::Bool(true) => Some(1),
Self::Lines(n) => Some(*n),
}
}
}
impl Default for PaddingValue {
fn default() -> Self {
Self::Lines(1)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct PaddingConfig {
#[serde(default)]
pub before: PaddingValue,
#[serde(default)]
pub after: PaddingValue,
}
fn default_max_file_size() -> u64 {
DEFAULT_MAX_FILE_SIZE
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum CodeBlockFilter {
Bool(bool),
InfoStrings(Vec<String>),
InfoString(String),
}
impl Default for CodeBlockFilter {
fn default() -> Self {
Self::Bool(false)
}
}
impl CodeBlockFilter {
pub fn is_enabled(&self) -> bool {
match self {
Self::Bool(b) => *b,
Self::InfoString(_) => true,
Self::InfoStrings(v) => !v.is_empty(),
}
}
pub fn should_skip(&self, info_string: &str) -> bool {
match self {
Self::Bool(b) => *b,
Self::InfoString(s) => info_string.contains(s.as_str()),
Self::InfoStrings(v) => v.iter().any(|s| info_string.contains(s.as_str())),
}
}
}
#[derive(Debug, Default, Deserialize)]
pub struct ExcludeConfig {
#[serde(default)]
pub patterns: Vec<String>,
#[serde(default)]
pub markdown_codeblocks: CodeBlockFilter,
#[serde(default)]
pub blocks: Vec<String>,
}
#[derive(Debug, Default, Deserialize)]
pub struct IncludeConfig {
#[serde(default)]
pub patterns: Vec<String>,
}
#[derive(Debug, Default, Deserialize)]
pub struct TemplatesConfig {
#[serde(default)]
pub paths: Vec<PathBuf>,
}
impl MdtConfig {
pub fn load(root: &Path) -> MdtResult<Option<MdtConfig>> {
let config_path = root.join("mdt.toml");
if !config_path.exists() {
return Ok(None);
}
let content = std::fs::read_to_string(&config_path)?;
let config: MdtConfig =
toml::from_str(&content).map_err(|e| MdtError::ConfigParse(e.to_string()))?;
Ok(Some(config))
}
pub fn load_data(&self, root: &Path) -> MdtResult<HashMap<String, serde_json::Value>> {
let mut data = HashMap::new();
for (namespace, rel_path) in &self.data {
let abs_path = root.join(rel_path);
let content = std::fs::read_to_string(&abs_path).map_err(|e| {
MdtError::DataFile {
path: rel_path.display().to_string(),
reason: e.to_string(),
}
})?;
let ext = abs_path.extension().and_then(|e| e.to_str()).unwrap_or("");
let value = parse_data_file(&content, ext, &rel_path.display().to_string())?;
data.insert(namespace.clone(), value);
}
Ok(data)
}
}
fn parse_data_file(
content: &str,
extension: &str,
path_display: &str,
) -> MdtResult<serde_json::Value> {
match extension {
"json" => {
serde_json::from_str(content).map_err(|e| {
MdtError::DataFile {
path: path_display.to_string(),
reason: e.to_string(),
}
})
}
"toml" => {
let toml_value: toml::Value = toml::from_str(content).map_err(|e| {
MdtError::DataFile {
path: path_display.to_string(),
reason: e.to_string(),
}
})?;
toml_to_json(toml_value, path_display)
}
"yaml" | "yml" => {
serde_yaml_ng::from_str(content).map_err(|e| {
MdtError::DataFile {
path: path_display.to_string(),
reason: e.to_string(),
}
})
}
"kdl" => {
let doc: kdl::KdlDocument = content.parse().map_err(|e: kdl::KdlError| {
MdtError::DataFile {
path: path_display.to_string(),
reason: e.to_string(),
}
})?;
kdl_document_to_value(&doc, path_display)
}
other => Err(MdtError::UnsupportedDataFormat(other.to_string())),
}
}
fn toml_to_json(value: toml::Value, path_display: &str) -> MdtResult<serde_json::Value> {
let json = match value {
toml::Value::String(s) => serde_json::Value::String(s),
toml::Value::Integer(i) => {
serde_json::Value::Number(serde_json::Number::from_f64(i as f64).ok_or_else(|| {
MdtError::UnconvertibleFloat {
path: path_display.to_string(),
value: i.to_string(),
}
})?)
}
toml::Value::Float(f) => {
serde_json::Value::Number(serde_json::Number::from_f64(f).ok_or_else(|| {
MdtError::UnconvertibleFloat {
path: path_display.to_string(),
value: f.to_string(),
}
})?)
}
toml::Value::Boolean(b) => serde_json::Value::Bool(b),
toml::Value::Datetime(dt) => serde_json::Value::String(dt.to_string()),
toml::Value::Array(arr) => {
let items: MdtResult<Vec<serde_json::Value>> = arr
.into_iter()
.map(|v| toml_to_json(v, path_display))
.collect();
serde_json::Value::Array(items?)
}
toml::Value::Table(table) => {
let mut map = serde_json::Map::new();
for (k, v) in table {
map.insert(k, toml_to_json(v, path_display)?);
}
serde_json::Value::Object(map)
}
};
Ok(json)
}
fn kdl_document_to_value(
doc: &kdl::KdlDocument,
path_display: &str,
) -> MdtResult<serde_json::Value> {
let mut map = serde_json::Map::new();
for node in doc.nodes() {
let name = node.name().to_string();
let value = kdl_node_to_value(node, path_display)?;
map.insert(name, value);
}
Ok(serde_json::Value::Object(map))
}
fn kdl_node_to_value(node: &kdl::KdlNode, path_display: &str) -> MdtResult<serde_json::Value> {
if let Some(children) = node.children() {
return kdl_document_to_value(children, path_display);
}
let entries: Vec<&kdl::KdlEntry> = node.entries().iter().collect();
if entries.is_empty() {
return Ok(serde_json::Value::Null);
}
if entries.len() == 1 && entries[0].name().is_none() {
return kdl_entry_value_to_json(entries[0].value(), path_display);
}
let all_named = entries.iter().all(|e| e.name().is_some());
if all_named {
let mut map = serde_json::Map::new();
for entry in &entries {
if let Some(name) = entry.name() {
map.insert(
name.to_string(),
kdl_entry_value_to_json(entry.value(), path_display)?,
);
}
}
return Ok(serde_json::Value::Object(map));
}
let values: MdtResult<Vec<serde_json::Value>> = entries
.iter()
.map(|e| kdl_entry_value_to_json(e.value(), path_display))
.collect();
Ok(serde_json::Value::Array(values?))
}
fn kdl_entry_value_to_json(
value: &kdl::KdlValue,
path_display: &str,
) -> MdtResult<serde_json::Value> {
match value {
kdl::KdlValue::String(s) => Ok(serde_json::Value::String(s.clone())),
kdl::KdlValue::Integer(i) => {
Ok(serde_json::Value::Number(
serde_json::Number::from_f64(*i as f64).ok_or_else(|| {
MdtError::UnconvertibleFloat {
path: path_display.to_string(),
value: i.to_string(),
}
})?,
))
}
kdl::KdlValue::Float(f) => {
Ok(serde_json::Value::Number(
serde_json::Number::from_f64(*f).ok_or_else(|| {
MdtError::UnconvertibleFloat {
path: path_display.to_string(),
value: f.to_string(),
}
})?,
))
}
kdl::KdlValue::Bool(b) => Ok(serde_json::Value::Bool(*b)),
kdl::KdlValue::Null => Ok(serde_json::Value::Null),
}
}