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