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