tpnote_lib/
config.rs

1//! Set configuration defaults by reading the internal default
2//! configuration file `LIB_CONFIG_DEFAULT_TOML`. After processing, the
3//! configuration data is exposed via the variable `LIB_CFG` behind a
4//! mutex. This makes it possible to modify all configuration defaults
5//! (including templates) at runtime.
6//!
7//! ```rust
8//! use tpnote_lib::config::LIB_CFG;
9//!
10//! let mut lib_cfg = LIB_CFG.write();
11//! let i = lib_cfg.scheme_idx("default").unwrap();
12//! (*lib_cfg).scheme[i].filename.copy_counter.extra_separator = '@'.to_string();
13//! ```
14//!
15//! Contract to be uphold by the user of this API:
16//! seeing that `LIB_CFG` is mutable at runtime, it must be sourced before the
17//! start of Tp-Note. All modification of `LIB_CFG` is terminated before
18//! accessing the high-level API in the `workflow` module of this crate.
19
20use crate::config_value::CfgVal;
21use crate::error::LibCfgError;
22#[cfg(feature = "renderer")]
23use crate::highlight::get_highlighting_css;
24#[cfg(feature = "lang-detection")]
25use crate::lingua::IsoCode639_1;
26use crate::markup_language::InputConverter;
27use crate::markup_language::MarkupLanguage;
28use parking_lot::RwLock;
29use sanitize_filename_reader_friendly::TRIM_LINE_CHARS;
30use serde::{Deserialize, Serialize};
31use std::collections::HashMap;
32use std::fmt::Write;
33use std::str::FromStr;
34use std::sync::LazyLock;
35#[cfg(feature = "renderer")]
36use syntect::highlighting::ThemeSet;
37use toml::Value;
38
39/// Default library configuration as TOML.
40pub const LIB_CONFIG_DEFAULT_TOML: &str = include_str!("config_default.toml");
41
42/// Maximum length of a note's filename in bytes. If a filename template produces
43/// a longer string, it will be truncated.
44pub const FILENAME_LEN_MAX: usize =
45    // Most filesystem's limit.
46    255
47    // Additional separator.
48    - 2
49    // Additional copy counter.
50    - 5
51    // Extra spare bytes, in case the user's copy counter is longer.
52    - 6;
53
54/// The appearance of a file with this filename marks the position of
55/// `TMPL_VAR_ROOT_PATH`.
56pub const FILENAME_ROOT_PATH_MARKER: &str = "tpnote.toml";
57
58/// When a filename is taken already, Tp-Note adds a copy
59/// counter number in the range of `0..COPY_COUNTER_MAX`
60/// at the end.
61pub const FILENAME_COPY_COUNTER_MAX: usize = 400;
62
63/// A filename extension, if present, is separated by a dot.
64pub(crate) const FILENAME_EXTENSION_SEPARATOR_DOT: char = '.';
65
66/// A dotfile starts with a dot.
67pub(crate) const FILENAME_DOTFILE_MARKER: char = '.';
68
69/// The template variable contains the fully qualified path of the `<path>`
70/// command line argument. If `<path>` points to a file, the variable contains
71/// the file path. If it points to a directory, it contains the directory path,
72/// or - if no `path` is given - the current working directory.
73pub const TMPL_VAR_PATH: &str = "path";
74
75/// Contains the fully qualified directory path of the `<path>` command line
76/// argument.
77/// If `<path>` points to a file, the last component (the filename) is omitted.
78/// If it points to a directory, the content of this variable is identical to
79/// `TMPL_VAR_PATH`,
80pub const TMPL_VAR_DIR_PATH: &str = "dir_path";
81
82/// The root directory of the current note. This is the first directory,
83/// that upwards from `TMPL_VAR_DIR_PATH` contains a file named
84/// `FILENAME_ROOT_PATH_MARKER`. The root directory is used by Tp-Note's viewer
85/// as base directory
86pub const TMPL_VAR_ROOT_PATH: &str = "root_path";
87
88/// Names the header of some `Content`.
89pub const TMPL_VAR_HEADER: &str = "header";
90
91/// Names the body of some `Content`.
92pub const TMPL_VAR_BODY: &str = "body";
93
94/// The name of the HTML clipboard to refer to in templates.
95/// Note: as current HTML clipboard provider never send YAML headers (yet),
96/// `html_clipboard.header` is always empty.
97pub const TMPL_VAR_HTML_CLIPBOARD: &str = "html_clipboard";
98
99/// The name of the plaintext clipboard to refer to in templates.
100pub const TMPL_VAR_TXT_CLIPBOARD: &str = "txt_clipboard";
101
102/// The name of the standard input stream to refer to in templates.
103pub const TMPL_VAR_STDIN: &str = "stdin";
104
105/// Contains the name of the selected scheme.
106pub const TMPL_VAR_CURRENT_SCHEME: &str = "current_scheme";
107
108/// Contains the default file extension for new note files as defined in the
109/// configuration file.
110pub const TMPL_VAR_EXTENSION_DEFAULT: &str = "extension_default";
111
112/// Contains the name of the default scheme when no `scheme:` field is
113/// present in the note's YAML header.
114/// This value defined in the configuration file under the same name and
115/// copied from there.
116pub const TMPL_VAR_SCHEME_SYNC_DEFAULT: &str = "scheme_sync_default";
117
118/// Contains the content of the first non empty environment variable
119/// `LOGNAME`, `USERNAME` or `USER`.
120pub const TMPL_VAR_USERNAME: &str = "username";
121
122/// Contains the user's language tag as defined in
123/// [RFC 5646](http://www.rfc-editor.org/rfc/rfc5646.txt).
124/// Not to be confused with the Unix `LANG` environment variable from which
125/// this value is derived under Linux/MacOS.
126/// Under Windows, the user's language tag is queried through the Win-API.
127/// If defined, the environment variable `TPNOTE_LANG` overwrites this value
128/// (all operating systems).
129pub const TMPL_VAR_LANG: &str = "lang";
130
131/// A copy of the command line option `--force_lang`. The empty value
132/// means "disable language forcing".
133pub const TMPL_VAR_FORCE_LANG: &str = "force_lang";
134
135/// Contains the body of the file the command line option `<path>`
136/// points to. Only available in the `tmpl.from_text_file_content`,
137/// `tmpl.sync_filename` and HTML templates.
138pub const TMPL_VAR_DOC: &str = "doc";
139
140/// Contains the date of the file the command line option `<path>` points to.
141/// The date is represented as an integer the way `std::time::SystemTime`
142/// resolves to on the platform. Only available in the
143/// `tmpl.from_text_file_content`, `tmpl.sync_filename` and HTML templates.
144/// Note: this variable might not be defined with some filesystems or on some
145/// platforms.  
146pub const TMPL_VAR_DOC_FILE_DATE: &str = "doc_file_date";
147
148/// Prefix prepended to front matter field names when a template variable
149/// is generated with the same name.
150pub const TMPL_VAR_FM_: &str = "fm_";
151
152/// Contains a Hash Map with all front matter fields. Lists are flattened
153/// into strings. These variables are only available in the
154/// `tmpl.from_text_file_content`, `tmpl.sync_filename` and HTML templates.
155pub const TMPL_VAR_FM_ALL: &str = "fm";
156
157/// If present, this header variable can switch the `settings.current_theme`
158/// before the filename template is processed.
159pub const TMPL_VAR_FM_SCHEME: &str = "fm_scheme";
160
161/// By default, the template `tmpl.sync_filename` defines the function of this
162/// variable as follows:
163/// Contains the value of the front matter field `file_ext` and determines the
164/// markup language used to render the document. When the field is missing the
165/// markup language is derived from the note's filename extension.
166///
167/// This is a dynamically generated variable originating from the front matter
168/// of the current note. As all front matter variables, its value is copied as
169/// it is without modification. Here, the only special treatment is, when
170/// analyzing the front matter, it is verified, that the value of this variable
171/// is registered in one of the `filename.extensions_*` variables.
172pub const TMPL_VAR_FM_FILE_EXT: &str = "fm_file_ext";
173
174/// By default, the template `tmpl.sync_filename` defines the function of this
175/// variable as follows:
176/// If this variable is defined, the _sort tag_ of the filename is replaced with
177/// the value of this variable next time the filename is synchronized. If not
178/// defined, the sort tag of the filename is never changed.
179///
180/// This is a dynamically generated variable originating from the front matter
181/// of the current note. As all front matter variables, its value is copied as
182/// it is without modification. Here, the only special treatment is, when
183/// analyzing the front matter, it is verified, that all the characters of the
184/// value of this variable are listed in `filename.sort_tag.extra_chars`.
185pub const TMPL_VAR_FM_SORT_TAG: &str = "fm_sort_tag";
186
187/// Contains the value of the front matter field `no_filename_sync`. When set
188/// to `no_filename_sync:` or `no_filename_sync: true`, the filename
189/// synchronization mechanism is disabled for this note file. Depreciated
190/// in favor of `TMPL_VAR_FM_FILENAME_SYNC`.
191pub const TMPL_VAR_FM_NO_FILENAME_SYNC: &str = "fm_no_filename_sync";
192
193/// Contains the value of the front matter field `filename_sync`. When set to
194/// `filename_sync: false`, the filename synchronization mechanism is
195/// disabled for this note file. Default value is `true`.
196pub const TMPL_VAR_FM_FILENAME_SYNC: &str = "fm_filename_sync";
197
198/// HTML template variable containing the automatically generated JavaScript
199/// code to be included in the HTML rendition.
200pub const TMPL_HTML_VAR_VIEWER_DOC_JS: &str = "viewer_doc_js";
201
202/// HTML template variable name. The value contains Tp-Note's CSS code
203/// to be included in the HTML rendition produced by the exporter.
204pub const TMPL_HTML_VAR_EXPORTER_DOC_CSS: &str = "exporter_doc_css";
205
206/// HTML template variable name. The value contains the highlighting CSS code
207/// to be included in the HTML rendition produced by the exporter.
208pub const TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS: &str = "exporter_highlighting_css";
209
210/// HTML template variable name. The value contains the path, for which the
211/// viewer delivers Tp-Note's CSS code. Note, the viewer delivers the same CSS
212/// code which is stored as value for `TMPL_HTML_VAR_VIEWER_DOC_CSS`.
213pub const TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH: &str = "viewer_doc_css_path";
214
215/// The constant URL for which Tp-Note's internal web server delivers the CSS
216/// style sheet. In HTML templates, this constant can be accessed as value of
217/// the `TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH` variable.
218pub const TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH_VALUE: &str = "/viewer_doc.css";
219
220/// HTML template variable name. The value contains the path, for which the
221/// viewer delivers Tp-Note's highlighting CSS code.
222pub const TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH: &str = "viewer_highlighting_css_path";
223
224/// The constant URL for which Tp-Note's internal web server delivers the CSS
225/// style sheet. In HTML templates, this constant can be accessed as value of
226/// the `TMPL_HTML_VAR_NOTE_CSS_PATH` variable.
227pub const TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH_VALUE: &str = "/viewer_highlighting.css";
228
229/// HTML template variable used in the error page containing the error message
230/// explaining why this page could not be rendered.
231#[cfg(feature = "viewer")]
232pub const TMPL_HTML_VAR_DOC_ERROR: &str = "doc_error";
233
234/// HTML template variable used in the error page containing a verbatim
235/// HTML rendition with hyperlinks of the erroneous note file.
236#[cfg(feature = "viewer")]
237pub const TMPL_HTML_VAR_DOC_TEXT: &str = "doc_text";
238
239/// Global variable containing the filename and template related configuration
240/// data. This can be changed by the consumer of this library. Once the
241/// initialization done, this should remain static.
242/// For session configuration see: `settings::SETTINGS`.
243pub static LIB_CFG: LazyLock<RwLock<LibCfg>> = LazyLock::new(|| RwLock::new(LibCfg::default()));
244
245/// An array of field names after deserialization.
246pub const LIB_CFG_RAW_FIELD_NAMES: [&str; 4] =
247    ["scheme_sync_default", "base_scheme", "scheme", "tmpl_html"];
248
249/// Processed configuration data.
250///
251/// Its structure is different form the input form defined in `LibCfgRaw` (see
252/// example in `LIB_CONFIG_DEFAULT_TOML`).
253/// For conversion use:
254///
255/// ```rust
256/// use tpnote_lib::config::LIB_CONFIG_DEFAULT_TOML;
257/// use tpnote_lib::config::LibCfg;
258/// use tpnote_lib::config_value::CfgVal;
259/// use std::str::FromStr;
260///
261/// let cfg_val = CfgVal::from_str(LIB_CONFIG_DEFAULT_TOML).unwrap();
262///
263/// // Run test.
264/// let lib_cfg = LibCfg::try_from(cfg_val).unwrap();
265///
266/// // Check.
267/// assert_eq!(lib_cfg.scheme_sync_default, "default")
268/// ```
269#[derive(Debug, Serialize, Deserialize)]
270#[serde(try_from = "LibCfgIntermediate")]
271pub struct LibCfg {
272    /// The fallback scheme for the `sync_filename` template choice, if the
273    /// `scheme` header variable is empty or is not defined.
274    pub scheme_sync_default: String,
275    /// Configuration of `Scheme`.
276    pub scheme: Vec<Scheme>,
277    /// Configuration of HTML templates.
278    pub tmpl_html: TmplHtml,
279}
280
281/// Unprocessed configuration data, deserialized from the configuration file.
282/// This is an intermediate representation of `LibCfg`.
283/// This defines the structure of the configuration file.
284/// Its default values are stored in serialized form in
285/// `LIB_CONFIG_DEFAULT_TOML`.
286#[derive(Debug, Serialize, Deserialize)]
287struct LibCfgIntermediate {
288    /// The fallback scheme for the `sync_filename` template choice, if the
289    /// `scheme` header variable is empty or is not defined.
290    pub scheme_sync_default: String,
291    /// This is the base scheme, from which all instantiated schemes inherit.
292    pub base_scheme: Value,
293    /// This flatten into a `scheme=Vec<Scheme>` in which the `Scheme`
294    /// definitions are not complete. Only after merging it into a copy of
295    /// `base_scheme` we can parse it into a `Scheme` structs. The result is not
296    /// kept here, it is stored into `LibCfg` struct instead.
297    #[serde(flatten)]
298    pub scheme: HashMap<String, Value>,
299    /// Configuration of HTML templates.
300    pub tmpl_html: TmplHtml,
301}
302
303impl LibCfg {
304    /// Returns the index of a named scheme. If no scheme with that name can be
305    /// found, return `LibCfgError::SchemeNotFound`.
306    pub fn scheme_idx(&self, name: &str) -> Result<usize, LibCfgError> {
307        self.scheme
308            .iter()
309            .enumerate()
310            .find(|&(_, scheme)| scheme.name == name)
311            .map_or_else(
312                || {
313                    Err(LibCfgError::SchemeNotFound {
314                        scheme_name: name.to_string(),
315                        schemes: {
316                            //Already imported: `use std::fmt::Write;`
317                            let mut errstr =
318                                self.scheme.iter().fold(String::new(), |mut output, s| {
319                                    let _ = write!(output, "{}, ", s.name);
320                                    output
321                                });
322                            errstr.truncate(errstr.len().saturating_sub(2));
323                            errstr
324                        },
325                    })
326                },
327                |(i, _)| Ok(i),
328            )
329    }
330    /// Perform some semantic consistency checks.
331    /// * `sort_tag.extra_separator` must NOT be in `sort_tag.extra_chars`.
332    /// * `sort_tag.extra_separator` must NOT be in `0..9`.
333    /// * `sort_tag.extra_separator` must NOT be in `a..z`.
334    /// * `sort_tag.extra_separator` must NOT be in `sort_tag.extra_chars`.
335    /// * `sort_tag.extra_separator` must NOT `FILENAME_DOTFILE_MARKER`.
336    /// * `copy_counter.extra_separator` must be one of
337    ///   `sanitize_filename_reader_friendly::TRIM_LINE_CHARS`.
338    /// * All characters of `sort_tag.separator` must be in `sort_tag.extra_chars`.
339    /// * `sort_tag.separator` must start with NOT `FILENAME_DOTFILE_MARKER`.
340    pub fn assert_validity(&self) -> Result<(), LibCfgError> {
341        for scheme in &self.scheme {
342            // Check for obvious configuration errors.
343            // * `sort_tag.extra_separator` must NOT be in `sort_tag.extra_chars`.
344            // * `sort_tag.extra_separator` must NOT `FILENAME_DOTFILE_MARKER`.
345            if scheme
346                .filename
347                .sort_tag
348                .extra_chars
349                .contains(scheme.filename.sort_tag.extra_separator)
350                || (scheme.filename.sort_tag.extra_separator == FILENAME_DOTFILE_MARKER)
351                || scheme.filename.sort_tag.extra_separator.is_ascii_digit()
352                || scheme
353                    .filename
354                    .sort_tag
355                    .extra_separator
356                    .is_ascii_lowercase()
357            {
358                return Err(LibCfgError::SortTagExtraSeparator {
359                    scheme_name: scheme.name.to_string(),
360                    dot_file_marker: FILENAME_DOTFILE_MARKER,
361                    sort_tag_extra_chars: scheme
362                        .filename
363                        .sort_tag
364                        .extra_chars
365                        .escape_default()
366                        .to_string(),
367                    extra_separator: scheme
368                        .filename
369                        .sort_tag
370                        .extra_separator
371                        .escape_default()
372                        .to_string(),
373                });
374            }
375
376            // Check for obvious configuration errors.
377            // * All characters of `sort_tag.separator` must be in `sort_tag.extra_chars`.
378            // * `sort_tag.separator` must NOT start with `FILENAME_DOTFILE_MARKER`.
379            // * `sort_tag.separator` must NOT contain ASCII `0..9` or `a..z`.
380            if !scheme.filename.sort_tag.separator.chars().all(|c| {
381                c.is_ascii_digit()
382                    || c.is_ascii_lowercase()
383                    || scheme.filename.sort_tag.extra_chars.contains(c)
384            }) || scheme
385                .filename
386                .sort_tag
387                .separator
388                .starts_with(FILENAME_DOTFILE_MARKER)
389            {
390                return Err(LibCfgError::SortTagSeparator {
391                    scheme_name: scheme.name.to_string(),
392                    dot_file_marker: FILENAME_DOTFILE_MARKER,
393                    chars: scheme
394                        .filename
395                        .sort_tag
396                        .extra_chars
397                        .escape_default()
398                        .to_string(),
399                    separator: scheme
400                        .filename
401                        .sort_tag
402                        .separator
403                        .escape_default()
404                        .to_string(),
405                });
406            }
407
408            // Check for obvious configuration errors.
409            // * `copy_counter.extra_separator` must one of
410            //   `sanitize_filename_reader_friendly::TRIM_LINE_CHARS`.
411            if !TRIM_LINE_CHARS.contains(&scheme.filename.copy_counter.extra_separator) {
412                return Err(LibCfgError::CopyCounterExtraSeparator {
413                    scheme_name: scheme.name.to_string(),
414                    chars: TRIM_LINE_CHARS.escape_default().to_string(),
415                    extra_separator: scheme
416                        .filename
417                        .copy_counter
418                        .extra_separator
419                        .escape_default()
420                        .to_string(),
421                });
422            }
423
424            // Assert that `filename.extension_default` is listed in
425            // `filename.extensions[..].0`.
426            if !scheme
427                .filename
428                .extensions
429                .iter()
430                .any(|ext| ext.0 == scheme.filename.extension_default)
431            {
432                return Err(LibCfgError::ExtensionDefault {
433                    scheme_name: scheme.name.to_string(),
434                    extension_default: scheme.filename.extension_default.to_owned(),
435                    extensions: {
436                        let mut list = scheme.filename.extensions.iter().fold(
437                            String::new(),
438                            |mut output, (k, _v1, _v2)| {
439                                let _ = write!(output, "{k}, ");
440                                output
441                            },
442                        );
443                        list.truncate(list.len().saturating_sub(2));
444                        list
445                    },
446                });
447            }
448
449            if let Mode::Error(e) = &scheme.tmpl.filter.get_lang.mode {
450                return Err(e.clone());
451            }
452
453            // Assert that `filter.get_lang.relative_distance_min` is
454            // between `0.0` and `0.99`.
455            let dist = scheme.tmpl.filter.get_lang.relative_distance_min;
456            if !(0.0..=0.99).contains(&dist) {
457                return Err(LibCfgError::MinimumRelativeDistanceInvalid {
458                    scheme_name: scheme.name.to_string(),
459                    dist,
460                });
461            }
462        }
463
464        // Highlighting config is valid?
465        // Validate `tmpl_html.viewer_highlighting_theme` and
466        // `tmpl_html.exporter_highlighting_theme`.
467        #[cfg(feature = "renderer")]
468        {
469            let hl_theme_set = ThemeSet::load_defaults();
470            let hl_theme_name = &self.tmpl_html.viewer_highlighting_theme;
471            if !hl_theme_name.is_empty() && !hl_theme_set.themes.contains_key(hl_theme_name) {
472                return Err(LibCfgError::HighlightingThemeName {
473                    var: "viewer_highlighting_theme".to_string(),
474                    value: hl_theme_name.to_owned(),
475                    available: hl_theme_set.themes.into_keys().fold(
476                        String::new(),
477                        |mut output, k| {
478                            let _ = write!(output, "{k}, ");
479                            output
480                        },
481                    ),
482                });
483            };
484            let hl_theme_name = &self.tmpl_html.exporter_highlighting_theme;
485            if !hl_theme_name.is_empty() && !hl_theme_set.themes.contains_key(hl_theme_name) {
486                return Err(LibCfgError::HighlightingThemeName {
487                    var: "exporter_highlighting_theme".to_string(),
488                    value: hl_theme_name.to_owned(),
489                    available: hl_theme_set.themes.into_keys().fold(
490                        String::new(),
491                        |mut output, k| {
492                            let _ = write!(output, "{k}, ");
493                            output
494                        },
495                    ),
496                });
497            };
498        }
499
500        Ok(())
501    }
502}
503
504/// Reads the file `./config_default.toml` (`LIB_CONFIG_DEFAULT_TOML`) into
505/// `LibCfg`. Panics if this is not possible.
506impl Default for LibCfg {
507    fn default() -> Self {
508        toml::from_str(LIB_CONFIG_DEFAULT_TOML)
509            .expect("Error parsing LIB_CONFIG_DEFAULT_TOML into LibCfg")
510    }
511}
512
513impl TryFrom<LibCfgIntermediate> for LibCfg {
514    type Error = LibCfgError;
515
516    /// Constructor expecting a `LibCfgRaw` struct as input.
517    /// The variables `LibCfgRaw.scheme`,
518    /// `LibCfgRaw.html_tmpl.viewer_highlighting_css` and
519    /// `LibCfgRaw.html_tmpl.exporter_highlighting_css` are processed before
520    /// storing in `Self`:
521    /// 1. The entries in `LibCfgRaw.scheme` are merged into copies of
522    ///    `LibCfgRaw.base_scheme` and the results are stored in `LibCfg.scheme`
523    /// 2. If `LibCfgRaw.html_tmpl.viewer_highlighting_css` is empty,
524    ///    a css is calculated from `tmpl.viewer_highlighting_theme`
525    ///    and stored in `LibCfg.html_tmpl.viewer_highlighting_css`.
526    /// 3.  Do the same for `LibCfgRaw.html_tmpl.exporter_highlighting_css`.
527    fn try_from(lib_cfg_raw: LibCfgIntermediate) -> Result<Self, Self::Error> {
528        let mut raw = lib_cfg_raw;
529        // Now we merge all `scheme` into a copy of `base_scheme` and
530        // parse the result into a `Vec<Scheme>`.
531        //
532        // Here we keep the result after merging and parsing.
533        let mut schemes: Vec<Scheme> = vec![];
534        // Get `theme`s in `config` as toml array. Clears the map as it is not
535        // needed any more.
536        if let Some(toml::Value::Array(lib_cfg_scheme)) = raw
537            .scheme
538            .drain()
539            // Silently ignore all potential toml variables other than `scheme`.
540            .filter(|(k, _)| k == "scheme")
541            .map(|(_, v)| v)
542            .next()
543        {
544            // Merge all `s` into a `base_scheme`, parse the result into a `Scheme`
545            // and collect a `Vector`. `merge_depth=0` means we never append
546            // to left-hand arrays, we always overwrite them.
547            schemes = lib_cfg_scheme
548                .into_iter()
549                .map(|v| CfgVal::merge_toml_values(raw.base_scheme.clone(), v, 0))
550                .map(|v| v.try_into().map_err(|e| e.into()))
551                .collect::<Result<Vec<Scheme>, LibCfgError>>()?;
552        }
553        let raw = raw; // Freeze.
554
555        let mut tmpl_html = raw.tmpl_html;
556        // Now calculate `LibCfgRaw.tmpl_html.viewer_highlighting_css`:
557        #[cfg(feature = "renderer")]
558        let css = if !tmpl_html.viewer_highlighting_css.is_empty() {
559            tmpl_html.viewer_highlighting_css
560        } else {
561            get_highlighting_css(&tmpl_html.viewer_highlighting_theme)
562        };
563        #[cfg(not(feature = "renderer"))]
564        let css = String::new();
565
566        tmpl_html.viewer_highlighting_css = css;
567
568        // Calculate `LibCfgRaw.tmpl_html.exporter_highlighting_css`:
569        #[cfg(feature = "renderer")]
570        let css = if !tmpl_html.exporter_highlighting_css.is_empty() {
571            tmpl_html.exporter_highlighting_css
572        } else {
573            get_highlighting_css(&tmpl_html.exporter_highlighting_theme)
574        };
575        #[cfg(not(feature = "renderer"))]
576        let css = String::new();
577
578        tmpl_html.exporter_highlighting_css = css;
579
580        // Store the result:
581        let res = LibCfg {
582            // Copy the parts of `config` into `LIB_CFG`.
583            scheme_sync_default: raw.scheme_sync_default,
584            scheme: schemes,
585            tmpl_html,
586        };
587        // Perform some additional semantic checks.
588        res.assert_validity()?;
589        Ok(res)
590    }
591}
592
593/// This constructor accepts as input the newtype `CfgVal` containing
594/// a `toml::map::Map<String, Value>`. Each `String` is the name of a top
595/// level configuration variable.
596/// The inner Map is expected to be a data structure that can be copied into
597/// the internal temporary variable `LibCfgRaw`. This internal variable
598/// is then processed and the result is stored in a `LibCfg` struct. For details
599/// see the `impl TryFrom<LibCfgRaw> for LibCfg`. The processing occurs as
600/// follows:
601///
602/// 1. Merge each incomplete `CfgVal(key="scheme")` into
603///    `CfgVal(key="base_scheme")` and
604///    store the resulting `scheme` struct in `LibCfg.scheme`.
605/// 2. If `CfgVal(key="html_tmpl.viewer_highlighting_css")` is empty, generate
606///    the value from `CfgVal(key="tmpl.viewer_highlighting_theme")`.
607/// 3. Do the same for `CfgVal(key="html_tmpl.exporter_highlighting_css")`.
608impl TryFrom<CfgVal> for LibCfg {
609    type Error = LibCfgError;
610
611    fn try_from(cfg_val: CfgVal) -> Result<Self, Self::Error> {
612        let value: toml::Value = cfg_val.into();
613        Ok(value.try_into()?)
614    }
615}
616
617/// Configuration data, deserialized from the configuration file.
618#[derive(Debug, Serialize, Deserialize, Clone)]
619pub struct Scheme {
620    pub name: String,
621    /// Configuration of filename parsing.
622    pub filename: Filename,
623    /// Configuration of content and filename templates.
624    pub tmpl: Tmpl,
625}
626
627/// Configuration of filename parsing, deserialized from the
628/// configuration file.
629#[derive(Debug, Serialize, Deserialize, Clone)]
630pub struct Filename {
631    pub sort_tag: SortTag,
632    pub copy_counter: CopyCounter,
633    pub extension_default: String,
634    pub extensions: Vec<(String, InputConverter, MarkupLanguage)>,
635}
636
637/// Configuration for sort-tag.
638#[derive(Debug, Serialize, Deserialize, Clone)]
639pub struct SortTag {
640    pub extra_chars: String,
641    pub separator: String,
642    pub extra_separator: char,
643    pub letters_in_succession_max: u8,
644    pub sequential: Sequential,
645}
646
647/// Requirements for chronological sort tags.
648#[derive(Debug, Serialize, Deserialize, Clone)]
649pub struct Sequential {
650    pub digits_in_succession_max: u8,
651}
652
653/// Configuration for copy-counter.
654#[derive(Debug, Serialize, Deserialize, Clone)]
655pub struct CopyCounter {
656    pub extra_separator: String,
657    pub opening_brackets: String,
658    pub closing_brackets: String,
659}
660
661/// Filename templates and content templates, deserialized from the
662/// configuration file.
663#[derive(Debug, Serialize, Deserialize, Clone)]
664pub struct Tmpl {
665    pub fm_var: FmVar,
666    pub filter: Filter,
667    pub from_dir_content: String,
668    pub from_dir_filename: String,
669    pub from_text_file_content: String,
670    pub from_text_file_filename: String,
671    pub annotate_file_content: String,
672    pub annotate_file_filename: String,
673    pub sync_filename: String,
674}
675
676/// Configuration describing how to localize and check front matter variables.
677#[derive(Debug, Serialize, Deserialize, Clone)]
678pub struct FmVar {
679    pub localization: Vec<(String, String)>,
680    pub assertions: Vec<(String, Vec<Assertion>)>,
681}
682
683/// Configuration related to various Tera template filters.
684#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
685pub struct Filter {
686    pub get_lang: GetLang,
687    pub map_lang: Vec<Vec<String>>,
688    pub to_yaml_tab: u64,
689}
690
691/// Configuration related to various Tera template filters.
692#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
693#[serde(try_from = "GetLangIntermediate")]
694pub struct GetLang {
695    pub mode: Mode,
696    #[cfg(feature = "lang-detection")]
697    pub language_candidates: Vec<IsoCode639_1>,
698    #[cfg(not(feature = "lang-detection"))]
699    pub language_candidates: Vec<String>,
700    pub relative_distance_min: f64,
701    pub consecutive_words_min: usize,
702    pub words_total_percentage_min: usize,
703}
704
705/// Configuration related to various Tera template filters.
706#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
707struct GetLangIntermediate {
708    pub mode: Mode,
709    pub language_candidates: Vec<String>,
710    pub relative_distance_min: f64,
711    pub consecutive_words_min: usize,
712    pub words_total_percentage_min: usize,
713}
714
715impl TryFrom<GetLangIntermediate> for GetLang {
716    type Error = LibCfgError; // Use String as error type just for simplicity
717
718    fn try_from(value: GetLangIntermediate) -> Result<Self, Self::Error> {
719        let GetLangIntermediate {
720            mode,
721            language_candidates,
722            relative_distance_min,
723            consecutive_words_min,
724            words_total_percentage_min,
725        } = value;
726
727        #[cfg(feature = "lang-detection")]
728        let language_candidates: Vec<IsoCode639_1> = language_candidates
729            .iter()
730            // No `to_uppercase()` required, this is done automatically by
731            // `IsoCode639_1::from_str`.
732            .map(|l| {
733                IsoCode639_1::from_str(l.trim())
734                    // Emit proper error message.
735                    .map_err(|_| {
736                        // The error path.
737                        // Produce list of all available languages.
738                        let mut all_langs = lingua::Language::all()
739                            .iter()
740                            .map(|l| {
741                                let mut s = l.iso_code_639_1().to_string();
742                                s.push_str(", ");
743                                s
744                            })
745                            .collect::<Vec<String>>();
746                        all_langs.sort();
747                        let mut all_langs = all_langs.into_iter().collect::<String>();
748                        all_langs.truncate(all_langs.len() - ", ".len());
749                        // Insert data into error object.
750                        LibCfgError::ParseLanguageCode {
751                            language_code: l.into(),
752                            all_langs,
753                        }
754                    })
755            })
756            .collect::<Result<Vec<IsoCode639_1>, LibCfgError>>()?;
757
758        Ok(GetLang {
759            mode,
760            language_candidates,
761            relative_distance_min,
762            consecutive_words_min,
763            words_total_percentage_min,
764        })
765    }
766}
767
768#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)]
769pub enum Mode {
770    /// The `get_lang` filter is disabled. No language guessing occurs.
771    Disabled,
772    /// The algorithm of the `get_lang` filter assumes, that the input is
773    /// monolingual. Only one language is searched and reported.
774    Monolingual,
775    /// The algorithm of the `get_lang` filter assumes, that the input is
776    /// monolingual. If present in the input, more than one language can be
777    /// reported.
778    #[default]
779    Multilingual,
780    /// Variant to represent the error state of an invalid `GetLang` object.
781    #[serde(skip)]
782    Error(LibCfgError),
783}
784
785/// Configuration for the HTML exporter feature, deserialized from the
786/// configuration file.
787#[derive(Debug, Serialize, Deserialize, Clone)]
788pub struct TmplHtml {
789    pub viewer: String,
790    pub viewer_error: String,
791    pub viewer_doc_css: String,
792    pub viewer_highlighting_theme: String,
793    pub viewer_highlighting_css: String,
794    pub exporter: String,
795    pub exporter_doc_css: String,
796    pub exporter_highlighting_theme: String,
797    pub exporter_highlighting_css: String,
798}
799
800/// Defines the way the HTML exporter rewrites local links.
801/// The command line option `--export-link-rewriting` expects this enum.
802/// Consult the manpage for details.
803#[derive(Debug, Hash, Clone, Eq, PartialEq, Deserialize, Serialize, Copy, Default)]
804pub enum LocalLinkKind {
805    /// Do not rewrite links.
806    Off,
807    /// Rewrite relative local links. Base: location of `.tpnote.toml`
808    Short,
809    /// Rewrite all local links. Base: "/"
810    #[default]
811    Long,
812}
813
814impl FromStr for LocalLinkKind {
815    type Err = LibCfgError;
816    fn from_str(level: &str) -> Result<LocalLinkKind, Self::Err> {
817        match &*level.to_ascii_lowercase() {
818            "off" => Ok(LocalLinkKind::Off),
819            "short" => Ok(LocalLinkKind::Short),
820            "long" => Ok(LocalLinkKind::Long),
821            _ => Err(LibCfgError::ParseLocalLinkKind {}),
822        }
823    }
824}
825
826/// Describes a set of tests, that assert template variable `tera:Value`
827/// properties.
828#[derive(Default, Debug, Hash, Clone, Eq, PartialEq, Deserialize, Serialize, Copy)]
829pub enum Assertion {
830    /// `IsDefined`: Assert that the variable is defined in the template.
831    IsDefined,
832    /// `IsNotEmptyString`: In addition to `IsString`, the condition asserts,
833    /// that the string -or all substrings-) are not empty.
834    IsNotEmptyString,
835    /// `IsString`: Assert, that if the variable is defined, its type -or all
836    /// subtypes- are `Value::String`.
837    IsString,
838    /// `IsNumber`: Assert, that if the variable is defined, its type -or all
839    /// subtypes- are `Value::Number`.
840    IsNumber,
841    /// `IsBool`: Assert, that if the variable is defined, its type -or all
842    /// subtypes- are `Value::Bool`.
843    IsBool,
844    /// `IsNotCompound`: Assert, that if the variable is defined, its type is
845    /// not `Value::Array` or `Value::Object`.
846    IsNotCompound,
847    /// `IsValidSortTag`: Assert, that if the variable is defined, the value's
848    /// string representation contains solely characters of the
849    /// `filename.sort_tag.extra_chars` set, digits, or lowercase letters.
850    /// The number of lowercase letters in a row is limited by
851    /// `tpnote_lib::config::FILENAME_SORT_TAG_LETTERS_IN_SUCCESSION_MAX`.
852    IsValidSortTag,
853    /// `IsConfiguredScheme`: Assert, that -if the variable is defined- the
854    /// string equals to one of the `scheme.name` in the configuration file.
855    IsConfiguredScheme,
856    /// `IsTpnoteExtension`: Assert, that if the variable is defined,
857    /// the values string representation is registered in one of the
858    /// `filename.extension_*` configuration file variables.
859    IsTpnoteExtension,
860    /// `NoOperation` (default): A test that is always satisfied. For internal
861    ///  use only.
862    #[default]
863    NoOperation,
864}