use crate::config_value::CfgVal;
use crate::error::LibCfgError;
#[cfg(feature = "renderer")]
use crate::highlight::get_highlighting_css;
#[cfg(feature = "lang-detection")]
use crate::lingua::IsoCode639_1;
use crate::markup_language::InputConverter;
use crate::markup_language::MarkupLanguage;
use parking_lot::RwLock;
use sanitize_filename_reader_friendly::TRIM_LINE_CHARS;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Write;
use std::str::FromStr;
use std::sync::LazyLock;
#[cfg(feature = "renderer")]
use syntect::highlighting::ThemeSet;
use toml::Value;
pub const LIB_CONFIG_DEFAULT_TOML: &str = include_str!("config_default.toml");
pub const FILENAME_LEN_MAX: usize =
255
- 2
- 5
- 6;
pub const FILENAME_ROOT_PATH_MARKER: &str = "tpnote.toml";
pub const FILENAME_COPY_COUNTER_MAX: usize = 400;
pub(crate) const FILENAME_EXTENSION_SEPARATOR_DOT: char = '.';
pub(crate) const FILENAME_DOTFILE_MARKER: char = '.';
pub const TMPL_VAR_PATH: &str = "path";
pub const TMPL_VAR_DIR_PATH: &str = "dir_path";
pub const TMPL_VAR_ROOT_PATH: &str = "root_path";
pub const TMPL_VAR_HEADER: &str = "header";
pub const TMPL_VAR_BODY: &str = "body";
pub const TMPL_VAR_HTML_CLIPBOARD: &str = "html_clipboard";
pub const TMPL_VAR_TXT_CLIPBOARD: &str = "txt_clipboard";
pub const TMPL_VAR_STDIN: &str = "stdin";
pub const TMPL_VAR_CURRENT_SCHEME: &str = "current_scheme";
pub const TMPL_VAR_EXTENSION_DEFAULT: &str = "extension_default";
pub const TMPL_VAR_SCHEME_SYNC_DEFAULT: &str = "scheme_sync_default";
pub const TMPL_VAR_USERNAME: &str = "username";
pub const TMPL_VAR_LANG: &str = "lang";
pub const TMPL_VAR_FORCE_LANG: &str = "force_lang";
pub const TMPL_VAR_DOC: &str = "doc";
pub const TMPL_VAR_DOC_FILE_DATE: &str = "doc_file_date";
pub const TMPL_VAR_FM_: &str = "fm_";
pub const TMPL_VAR_FM_ALL: &str = "fm";
pub const TMPL_VAR_FM_SCHEME: &str = "fm_scheme";
pub const TMPL_VAR_FM_FILE_EXT: &str = "fm_file_ext";
pub const TMPL_VAR_FM_SORT_TAG: &str = "fm_sort_tag";
pub const TMPL_VAR_FM_NO_FILENAME_SYNC: &str = "fm_no_filename_sync";
pub const TMPL_VAR_FM_FILENAME_SYNC: &str = "fm_filename_sync";
pub const TMPL_HTML_VAR_VIEWER_DOC_JS: &str = "viewer_doc_js";
pub const TMPL_HTML_VAR_EXPORTER_DOC_CSS: &str = "exporter_doc_css";
pub const TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS: &str = "exporter_highlighting_css";
pub const TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH: &str = "viewer_doc_css_path";
pub const TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH_VALUE: &str = "/viewer_doc.css";
pub const TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH: &str = "viewer_highlighting_css_path";
pub const TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH_VALUE: &str = "/viewer_highlighting.css";
#[cfg(feature = "viewer")]
pub const TMPL_HTML_VAR_DOC_ERROR: &str = "doc_error";
#[cfg(feature = "viewer")]
pub const TMPL_HTML_VAR_DOC_TEXT: &str = "doc_text";
pub static LIB_CFG: LazyLock<RwLock<LibCfg>> = LazyLock::new(|| RwLock::new(LibCfg::default()));
pub const LIB_CFG_RAW_FIELD_NAMES: [&str; 4] =
["scheme_sync_default", "base_scheme", "scheme", "tmpl_html"];
#[derive(Debug, Serialize, Deserialize)]
#[serde(try_from = "LibCfgIntermediate")]
pub struct LibCfg {
pub scheme_sync_default: String,
pub scheme: Vec<Scheme>,
pub tmpl_html: TmplHtml,
}
#[derive(Debug, Serialize, Deserialize)]
struct LibCfgIntermediate {
pub scheme_sync_default: String,
pub base_scheme: Value,
#[serde(flatten)]
pub scheme: HashMap<String, Value>,
pub tmpl_html: TmplHtml,
}
impl LibCfg {
pub fn scheme_idx(&self, name: &str) -> Result<usize, LibCfgError> {
self.scheme
.iter()
.enumerate()
.find(|&(_, scheme)| scheme.name == name)
.map_or_else(
|| {
Err(LibCfgError::SchemeNotFound {
scheme_name: name.to_string(),
schemes: {
let mut errstr =
self.scheme.iter().fold(String::new(), |mut output, s| {
let _ = write!(output, "{}, ", s.name);
output
});
errstr.truncate(errstr.len().saturating_sub(2));
errstr
},
})
},
|(i, _)| Ok(i),
)
}
pub fn assert_validity(&self) -> Result<(), LibCfgError> {
for scheme in &self.scheme {
if scheme
.filename
.sort_tag
.extra_chars
.contains(scheme.filename.sort_tag.extra_separator)
|| (scheme.filename.sort_tag.extra_separator == FILENAME_DOTFILE_MARKER)
|| scheme.filename.sort_tag.extra_separator.is_ascii_digit()
|| scheme
.filename
.sort_tag
.extra_separator
.is_ascii_lowercase()
{
return Err(LibCfgError::SortTagExtraSeparator {
scheme_name: scheme.name.to_string(),
dot_file_marker: FILENAME_DOTFILE_MARKER,
sort_tag_extra_chars: scheme
.filename
.sort_tag
.extra_chars
.escape_default()
.to_string(),
extra_separator: scheme
.filename
.sort_tag
.extra_separator
.escape_default()
.to_string(),
});
}
if !scheme.filename.sort_tag.separator.chars().all(|c| {
c.is_ascii_digit()
|| c.is_ascii_lowercase()
|| scheme.filename.sort_tag.extra_chars.contains(c)
}) || scheme
.filename
.sort_tag
.separator
.starts_with(FILENAME_DOTFILE_MARKER)
{
return Err(LibCfgError::SortTagSeparator {
scheme_name: scheme.name.to_string(),
dot_file_marker: FILENAME_DOTFILE_MARKER,
chars: scheme
.filename
.sort_tag
.extra_chars
.escape_default()
.to_string(),
separator: scheme
.filename
.sort_tag
.separator
.escape_default()
.to_string(),
});
}
if !TRIM_LINE_CHARS.contains(&scheme.filename.copy_counter.extra_separator) {
return Err(LibCfgError::CopyCounterExtraSeparator {
scheme_name: scheme.name.to_string(),
chars: TRIM_LINE_CHARS.escape_default().to_string(),
extra_separator: scheme
.filename
.copy_counter
.extra_separator
.escape_default()
.to_string(),
});
}
if !scheme
.filename
.extensions
.iter()
.any(|ext| ext.0 == scheme.filename.extension_default)
{
return Err(LibCfgError::ExtensionDefault {
scheme_name: scheme.name.to_string(),
extension_default: scheme.filename.extension_default.to_owned(),
extensions: {
let mut list = scheme.filename.extensions.iter().fold(
String::new(),
|mut output, (k, _v1, _v2)| {
let _ = write!(output, "{k}, ");
output
},
);
list.truncate(list.len().saturating_sub(2));
list
},
});
}
if let Mode::Error(e) = &scheme.tmpl.filter.get_lang.mode {
return Err(e.clone());
}
let dist = scheme.tmpl.filter.get_lang.relative_distance_min;
if !(0.0..=0.99).contains(&dist) {
return Err(LibCfgError::MinimumRelativeDistanceInvalid {
scheme_name: scheme.name.to_string(),
dist,
});
}
}
#[cfg(feature = "renderer")]
{
let hl_theme_set = ThemeSet::load_defaults();
let hl_theme_name = &self.tmpl_html.viewer_highlighting_theme;
if !hl_theme_name.is_empty() && !hl_theme_set.themes.contains_key(hl_theme_name) {
return Err(LibCfgError::HighlightingThemeName {
var: "viewer_highlighting_theme".to_string(),
value: hl_theme_name.to_owned(),
available: hl_theme_set.themes.into_keys().fold(
String::new(),
|mut output, k| {
let _ = write!(output, "{k}, ");
output
},
),
});
};
let hl_theme_name = &self.tmpl_html.exporter_highlighting_theme;
if !hl_theme_name.is_empty() && !hl_theme_set.themes.contains_key(hl_theme_name) {
return Err(LibCfgError::HighlightingThemeName {
var: "exporter_highlighting_theme".to_string(),
value: hl_theme_name.to_owned(),
available: hl_theme_set.themes.into_keys().fold(
String::new(),
|mut output, k| {
let _ = write!(output, "{k}, ");
output
},
),
});
};
}
Ok(())
}
}
impl Default for LibCfg {
fn default() -> Self {
toml::from_str(LIB_CONFIG_DEFAULT_TOML)
.expect("Error parsing LIB_CONFIG_DEFAULT_TOML into LibCfg")
}
}
impl TryFrom<LibCfgIntermediate> for LibCfg {
type Error = LibCfgError;
fn try_from(lib_cfg_raw: LibCfgIntermediate) -> Result<Self, Self::Error> {
let mut raw = lib_cfg_raw;
let mut schemes: Vec<Scheme> = vec![];
if let Some(toml::Value::Array(lib_cfg_scheme)) = raw
.scheme
.drain()
.filter(|(k, _)| k == "scheme")
.map(|(_, v)| v)
.next()
{
schemes = lib_cfg_scheme
.into_iter()
.map(|v| CfgVal::merge_toml_values(raw.base_scheme.clone(), v, 0))
.map(|v| v.try_into().map_err(|e| e.into()))
.collect::<Result<Vec<Scheme>, LibCfgError>>()?;
}
let raw = raw;
let mut tmpl_html = raw.tmpl_html;
#[cfg(feature = "renderer")]
let css = if !tmpl_html.viewer_highlighting_css.is_empty() {
tmpl_html.viewer_highlighting_css
} else {
get_highlighting_css(&tmpl_html.viewer_highlighting_theme)
};
#[cfg(not(feature = "renderer"))]
let css = String::new();
tmpl_html.viewer_highlighting_css = css;
#[cfg(feature = "renderer")]
let css = if !tmpl_html.exporter_highlighting_css.is_empty() {
tmpl_html.exporter_highlighting_css
} else {
get_highlighting_css(&tmpl_html.exporter_highlighting_theme)
};
#[cfg(not(feature = "renderer"))]
let css = String::new();
tmpl_html.exporter_highlighting_css = css;
let res = LibCfg {
scheme_sync_default: raw.scheme_sync_default,
scheme: schemes,
tmpl_html,
};
res.assert_validity()?;
Ok(res)
}
}
impl TryFrom<CfgVal> for LibCfg {
type Error = LibCfgError;
fn try_from(cfg_val: CfgVal) -> Result<Self, Self::Error> {
let value: toml::Value = cfg_val.into();
Ok(value.try_into()?)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Scheme {
pub name: String,
pub filename: Filename,
pub tmpl: Tmpl,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Filename {
pub sort_tag: SortTag,
pub copy_counter: CopyCounter,
pub extension_default: String,
pub extensions: Vec<(String, InputConverter, MarkupLanguage)>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SortTag {
pub extra_chars: String,
pub separator: String,
pub extra_separator: char,
pub letters_in_succession_max: u8,
pub sequential: Sequential,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Sequential {
pub digits_in_succession_max: u8,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CopyCounter {
pub extra_separator: String,
pub opening_brackets: String,
pub closing_brackets: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Tmpl {
pub fm_var: FmVar,
pub filter: Filter,
pub from_dir_content: String,
pub from_dir_filename: String,
pub from_text_file_content: String,
pub from_text_file_filename: String,
pub annotate_file_content: String,
pub annotate_file_filename: String,
pub sync_filename: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FmVar {
pub localization: Vec<(String, String)>,
pub assertions: Vec<(String, Vec<Assertion>)>,
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Filter {
pub get_lang: GetLang,
pub map_lang: Vec<Vec<String>>,
pub to_yaml_tab: u64,
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(try_from = "GetLangIntermediate")]
pub struct GetLang {
pub mode: Mode,
#[cfg(feature = "lang-detection")]
pub language_candidates: Vec<IsoCode639_1>,
#[cfg(not(feature = "lang-detection"))]
pub language_candidates: Vec<String>,
pub relative_distance_min: f64,
pub consecutive_words_min: usize,
pub words_total_percentage_min: usize,
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
struct GetLangIntermediate {
pub mode: Mode,
pub language_candidates: Vec<String>,
pub relative_distance_min: f64,
pub consecutive_words_min: usize,
pub words_total_percentage_min: usize,
}
impl TryFrom<GetLangIntermediate> for GetLang {
type Error = LibCfgError;
fn try_from(value: GetLangIntermediate) -> Result<Self, Self::Error> {
let GetLangIntermediate {
mode,
language_candidates,
relative_distance_min,
consecutive_words_min,
words_total_percentage_min,
} = value;
#[cfg(feature = "lang-detection")]
let language_candidates: Vec<IsoCode639_1> = language_candidates
.iter()
.map(|l| {
IsoCode639_1::from_str(l.trim())
.map_err(|_| {
let mut all_langs = lingua::Language::all()
.iter()
.map(|l| {
let mut s = l.iso_code_639_1().to_string();
s.push_str(", ");
s
})
.collect::<Vec<String>>();
all_langs.sort();
let mut all_langs = all_langs.into_iter().collect::<String>();
all_langs.truncate(all_langs.len() - ", ".len());
LibCfgError::ParseLanguageCode {
language_code: l.into(),
all_langs,
}
})
})
.collect::<Result<Vec<IsoCode639_1>, LibCfgError>>()?;
Ok(GetLang {
mode,
language_candidates,
relative_distance_min,
consecutive_words_min,
words_total_percentage_min,
})
}
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum Mode {
Disabled,
Monolingual,
#[default]
Multilingual,
#[serde(skip)]
Error(LibCfgError),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TmplHtml {
pub viewer: String,
pub viewer_error: String,
pub viewer_doc_css: String,
pub viewer_highlighting_theme: String,
pub viewer_highlighting_css: String,
pub exporter: String,
pub exporter_doc_css: String,
pub exporter_highlighting_theme: String,
pub exporter_highlighting_css: String,
}
#[derive(Debug, Hash, Clone, Eq, PartialEq, Deserialize, Serialize, Copy, Default)]
pub enum LocalLinkKind {
Off,
Short,
#[default]
Long,
}
impl FromStr for LocalLinkKind {
type Err = LibCfgError;
fn from_str(level: &str) -> Result<LocalLinkKind, Self::Err> {
match &*level.to_ascii_lowercase() {
"off" => Ok(LocalLinkKind::Off),
"short" => Ok(LocalLinkKind::Short),
"long" => Ok(LocalLinkKind::Long),
_ => Err(LibCfgError::ParseLocalLinkKind {}),
}
}
}
#[derive(Default, Debug, Hash, Clone, Eq, PartialEq, Deserialize, Serialize, Copy)]
pub enum Assertion {
IsDefined,
IsNotEmptyString,
IsString,
IsNumber,
IsBool,
IsNotCompound,
IsValidSortTag,
IsConfiguredScheme,
IsTpnoteExtension,
#[default]
NoOperation,
}