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
254/// Unprocessed configuration data, deserialized from the configuration file.
255/// This defines the structure of the configuration file.
256/// Its default values are stored in serialized form in
257/// `LIB_CONFIG_DEFAULT_TOML`.
258#[derive(Debug, Serialize, Deserialize)]
259struct LibCfgRaw {
260    /// The fallback scheme for the `sync_filename` template choice, if the
261    /// `scheme` header variable is empty or is not defined.
262    pub scheme_sync_default: String,
263    /// This is the base scheme, from which all instantiated schemes inherit.
264    pub base_scheme: Value,
265    /// This flatten into a `scheme=Vec<Scheme>` in which the `Scheme`
266    /// definitions are not complete. Only after merging it into a copy of
267    /// `base_scheme` we can parse it into a `Scheme` structs. The result is not
268    /// kept here, it is stored into `LibCfg` struct instead.
269    #[serde(flatten)]
270    pub scheme: HashMap<String, Value>,
271    /// Configuration of HTML templates.
272    pub tmpl_html: TmplHtml,
273}
274
275/// An array of field names after deserialization.
276pub const LIB_CFG_RAW_FIELD_NAMES: [&str; 4] =
277    ["scheme_sync_default", "base_scheme", "scheme", "tmpl_html"];
278
279impl TryFrom<CfgVal> for LibCfgRaw {
280    type Error = LibCfgError;
281
282    fn try_from(cfg_val: CfgVal) -> Result<Self, Self::Error> {
283        let value: toml::Value = cfg_val.into();
284        Ok(value.try_into()?)
285    }
286}
287
288/// Configuration data, deserialized from the configuration file.
289#[derive(Debug, Serialize, Deserialize, Clone)]
290pub struct Scheme {
291    pub name: String,
292    /// Configuration of filename parsing.
293    pub filename: Filename,
294    /// Configuration of content and filename templates.
295    pub tmpl: Tmpl,
296}
297
298/// Configuration of filename parsing, deserialized from the
299/// configuration file.
300#[derive(Debug, Serialize, Deserialize, Clone)]
301pub struct Filename {
302    pub sort_tag: SortTag,
303    pub copy_counter: CopyCounter,
304    pub extension_default: String,
305    pub extensions: Vec<(String, InputConverter, MarkupLanguage)>,
306}
307
308/// Configuration for sort-tag.
309#[derive(Debug, Serialize, Deserialize, Clone)]
310pub struct SortTag {
311    pub extra_chars: String,
312    pub separator: String,
313    pub extra_separator: char,
314    pub letters_in_succession_max: u8,
315    pub sequential: Sequential,
316}
317
318/// Requirements for chronological sort tags.
319#[derive(Debug, Serialize, Deserialize, Clone)]
320pub struct Sequential {
321    pub digits_in_succession_max: u8,
322}
323
324/// Configuration for copy-counter.
325#[derive(Debug, Serialize, Deserialize, Clone)]
326pub struct CopyCounter {
327    pub extra_separator: String,
328    pub opening_brackets: String,
329    pub closing_brackets: String,
330}
331
332/// Filename templates and content templates, deserialized from the
333/// configuration file.
334#[derive(Debug, Serialize, Deserialize, Clone)]
335pub struct Tmpl {
336    pub fm_var: FmVar,
337    pub filter: Filter,
338    pub from_dir_content: String,
339    pub from_dir_filename: String,
340    pub from_clipboard_yaml_content: String,
341    pub from_clipboard_yaml_filename: String,
342    pub from_clipboard_content: String,
343    pub from_clipboard_filename: String,
344    pub from_text_file_content: String,
345    pub from_text_file_filename: String,
346    pub annotate_file_content: String,
347    pub annotate_file_filename: String,
348    pub sync_filename: String,
349}
350
351/// Configuration describing how to localize and check front matter variables.
352#[derive(Debug, Serialize, Deserialize, Clone)]
353pub struct FmVar {
354    pub localization: Vec<(String, String)>,
355    pub assertions: Vec<(String, Vec<Assertion>)>,
356}
357
358/// Configuration related to various Tera template filters.
359#[derive(Debug, Serialize, Deserialize, Clone)]
360pub struct Filter {
361    pub get_lang: Vec<String>,
362    pub map_lang: Vec<Vec<String>>,
363    pub to_yaml_tab: u64,
364}
365
366/// Configuration for the HTML exporter feature, deserialized from the
367/// configuration file.
368#[derive(Debug, Serialize, Deserialize, Clone)]
369pub struct TmplHtml {
370    pub viewer: String,
371    pub viewer_error: String,
372    pub viewer_doc_css: String,
373    pub viewer_highlighting_theme: String,
374    pub viewer_highlighting_css: String,
375    pub exporter: String,
376    pub exporter_doc_css: String,
377    pub exporter_highlighting_theme: String,
378    pub exporter_highlighting_css: String,
379}
380
381/// Processed configuration data.
382///
383/// Its structure is different form the input form defined in `LibCfgRaw` (see
384/// example in `LIB_CONFIG_DEFAULT_TOML`).
385/// For conversion use:
386///
387/// ```rust
388/// use tpnote_lib::config::LIB_CONFIG_DEFAULT_TOML;
389/// use tpnote_lib::config::LibCfg;
390/// use tpnote_lib::config::CfgVal;
391/// use std::str::FromStr;
392///
393/// let cfg_val = CfgVal::from_str(LIB_CONFIG_DEFAULT_TOML).unwrap();
394///
395/// // Run test.
396/// let lib_cfg = LibCfg::try_from(cfg_val).unwrap();
397///
398/// // Check.
399/// assert_eq!(lib_cfg.scheme_sync_default, "default")
400/// ```
401#[derive(Debug, Serialize, Deserialize)]
402pub struct LibCfg {
403    /// The fallback scheme for the `sync_filename` template choice, if the
404    /// `scheme` header variable is empty or is not defined.
405    pub scheme_sync_default: String,
406    /// Configuration of `Scheme`.
407    pub scheme: Vec<Scheme>,
408    /// Configuration of HTML templates.
409    pub tmpl_html: TmplHtml,
410}
411
412impl LibCfg {
413    /// Returns the index of a named scheme. If no scheme with that name can be
414    /// be found, return `LibCfgError::SchemeNotFound`.
415    pub fn scheme_idx(&self, name: &str) -> Result<usize, LibCfgError> {
416        self.scheme
417            .iter()
418            .enumerate()
419            .find(|&(_, scheme)| scheme.name == name)
420            .map_or_else(
421                || {
422                    Err(LibCfgError::SchemeNotFound {
423                        scheme_name: name.to_string(),
424                        schemes: {
425                            //Already imported: `use std::fmt::Write;`
426                            let mut errstr =
427                                self.scheme.iter().fold(String::new(), |mut output, s| {
428                                    let _ = write!(output, "{}, ", s.name);
429                                    output
430                                });
431                            errstr.truncate(errstr.len().saturating_sub(2));
432                            errstr
433                        },
434                    })
435                },
436                |(i, _)| Ok(i),
437            )
438    }
439    /// Perform some semantic consistency checks.
440    /// * `sort_tag.extra_separator` must NOT be in `sort_tag.extra_chars`.
441    /// * `sort_tag.extra_separator` must NOT be in `0..9`.
442    /// * `sort_tag.extra_separator` must NOT be in `a..z`.
443    /// * `sort_tag.extra_separator` must NOT be in `sort_tag.extra_chars`.
444    /// * `sort_tag.extra_separator` must NOT `FILENAME_DOTFILE_MARKER`.
445    /// * `copy_counter.extra_separator` must be one of
446    ///   `sanitize_filename_reader_friendly::TRIM_LINE_CHARS`.
447    /// * All characters of `sort_tag.separator` must be in `sort_tag.extra_chars`.
448    /// * `sort_tag.separator` must start with NOT `FILENAME_DOTFILE_MARKER`.
449    pub fn assert_validity(&self) -> Result<(), LibCfgError> {
450        for scheme in &self.scheme {
451            // Check for obvious configuration errors.
452            // * `sort_tag.extra_separator` must NOT be in `sort_tag.extra_chars`.
453            // * `sort_tag.extra_separator` must NOT `FILENAME_DOTFILE_MARKER`.
454            if scheme
455                .filename
456                .sort_tag
457                .extra_chars
458                .contains(scheme.filename.sort_tag.extra_separator)
459                || (scheme.filename.sort_tag.extra_separator == FILENAME_DOTFILE_MARKER)
460                || scheme.filename.sort_tag.extra_separator.is_ascii_digit()
461                || scheme
462                    .filename
463                    .sort_tag
464                    .extra_separator
465                    .is_ascii_lowercase()
466            {
467                return Err(LibCfgError::SortTagExtraSeparator {
468                    scheme_name: scheme.name.to_string(),
469                    dot_file_marker: FILENAME_DOTFILE_MARKER,
470                    sort_tag_extra_chars: scheme
471                        .filename
472                        .sort_tag
473                        .extra_chars
474                        .escape_default()
475                        .to_string(),
476                    extra_separator: scheme
477                        .filename
478                        .sort_tag
479                        .extra_separator
480                        .escape_default()
481                        .to_string(),
482                });
483            }
484
485            // Check for obvious configuration errors.
486            // * All characters of `sort_tag.separator` must be in `sort_tag.extra_chars`.
487            // * `sort_tag.separator` must NOT start with `FILENAME_DOTFILE_MARKER`.
488            // * `sort_tag.separator` must NOT contain ASCII `0..9` or `a..z`.
489            if !scheme.filename.sort_tag.separator.chars().all(|c| {
490                c.is_ascii_digit()
491                    || c.is_ascii_lowercase()
492                    || scheme.filename.sort_tag.extra_chars.contains(c)
493            }) || scheme
494                .filename
495                .sort_tag
496                .separator
497                .starts_with(FILENAME_DOTFILE_MARKER)
498            {
499                return Err(LibCfgError::SortTagSeparator {
500                    scheme_name: scheme.name.to_string(),
501                    dot_file_marker: FILENAME_DOTFILE_MARKER,
502                    chars: scheme
503                        .filename
504                        .sort_tag
505                        .extra_chars
506                        .escape_default()
507                        .to_string(),
508                    separator: scheme
509                        .filename
510                        .sort_tag
511                        .separator
512                        .escape_default()
513                        .to_string(),
514                });
515            }
516
517            // Check for obvious configuration errors.
518            // * `copy_counter.extra_separator` must one of
519            //   `sanitize_filename_reader_friendly::TRIM_LINE_CHARS`.
520            if !TRIM_LINE_CHARS.contains(&scheme.filename.copy_counter.extra_separator) {
521                return Err(LibCfgError::CopyCounterExtraSeparator {
522                    scheme_name: scheme.name.to_string(),
523                    chars: TRIM_LINE_CHARS.escape_default().to_string(),
524                    extra_separator: scheme
525                        .filename
526                        .copy_counter
527                        .extra_separator
528                        .escape_default()
529                        .to_string(),
530                });
531            }
532
533            // Assert that `filename.extension_default` is listed in
534            // `filename.extensions[..].0`.
535            if !scheme
536                .filename
537                .extensions
538                .iter()
539                .any(|ext| ext.0 == scheme.filename.extension_default)
540            {
541                return Err(LibCfgError::ExtensionDefault {
542                    scheme_name: scheme.name.to_string(),
543                    extension_default: scheme.filename.extension_default.to_owned(),
544                    extensions: {
545                        let mut list = scheme.filename.extensions.iter().fold(
546                            String::new(),
547                            |mut output, (k, _v1, _v2)| {
548                                let _ = write!(output, "{k}, ");
549                                output
550                            },
551                        );
552                        list.truncate(list.len().saturating_sub(2));
553                        list
554                    },
555                });
556            }
557        }
558
559        // Highlighting config is valid?
560        // Validate `tmpl_html.viewer_highlighting_theme` and
561        // `tmpl_html.exporter_highlighting_theme`.
562        #[cfg(feature = "renderer")]
563        {
564            let hl_theme_set = ThemeSet::load_defaults();
565            let hl_theme_name = &self.tmpl_html.viewer_highlighting_theme;
566            if !hl_theme_name.is_empty() && !hl_theme_set.themes.contains_key(hl_theme_name) {
567                return Err(LibCfgError::HighlightingThemeName {
568                    var: "viewer_highlighting_theme".to_string(),
569                    value: hl_theme_name.to_owned(),
570                    available: hl_theme_set.themes.into_keys().fold(
571                        String::new(),
572                        |mut output, k| {
573                            let _ = write!(output, "{k}, ");
574                            output
575                        },
576                    ),
577                });
578            };
579            let hl_theme_name = &self.tmpl_html.exporter_highlighting_theme;
580            if !hl_theme_name.is_empty() && !hl_theme_set.themes.contains_key(hl_theme_name) {
581                return Err(LibCfgError::HighlightingThemeName {
582                    var: "exporter_highlighting_theme".to_string(),
583                    value: hl_theme_name.to_owned(),
584                    available: hl_theme_set.themes.into_keys().fold(
585                        String::new(),
586                        |mut output, k| {
587                            let _ = write!(output, "{k}, ");
588                            output
589                        },
590                    ),
591                });
592            };
593        }
594
595        Ok(())
596    }
597}
598
599/// Reads the file `./config_default.toml` (`LIB_CONFIG_DEFAULT_TOML`) into
600/// `LibCfg`. Panics if this is not possible.
601impl Default for LibCfg {
602    fn default() -> Self {
603        let raw: LibCfgRaw = toml::from_str(LIB_CONFIG_DEFAULT_TOML)
604            .expect("Syntax error in  LIB_CONFIG_DEFAULT_TOML");
605        raw.try_into()
606            .expect("Error parsing LIB_CONFIG_DEFAULT_TOML into LibCfg")
607    }
608}
609
610impl TryFrom<LibCfgRaw> for LibCfg {
611    type Error = LibCfgError;
612
613    /// Constructor expecting a `LibCfgRaw` struct as input.
614    /// The variables `LibCfgRaw.scheme`,
615    /// `LibCfgRaw.html_tmpl.viewer_highlighting_css` and
616    /// `LibCfgRaw.html_tmpl.exporter_highlighting_css` are processed before
617    /// storing in `Self`:
618    /// 1. The entries in `LibCfgRaw.scheme` are merged into copies of
619    ///    `LibCfgRaw.base_scheme` and the results are stored in `LibCfg.scheme`
620    /// 2. If `LibCfgRaw.html_tmpl.viewer_highlighting_css` is empty,
621    ///    a css is calculated from `tmpl.viewer_highlighting_theme`
622    ///    and stored in `LibCfg.html_tmpl.viewer_highlighting_css`.
623    /// 3.  Do the same for `LibCfgRaw.html_tmpl.exporter_highlighting_css`.
624    fn try_from(lib_cfg_raw: LibCfgRaw) -> Result<Self, Self::Error> {
625        let mut raw = lib_cfg_raw;
626        // Now we merge all `scheme` into a copy of `base_scheme` and
627        // parse the result into a `Vec<Scheme>`.
628        //
629        // Here we keep the result after merging and parsing.
630        let mut schemes: Vec<Scheme> = vec![];
631        // Get `theme`s in `config` as toml array. Clears the map as it is not
632        // needed any more.
633        if let Some(toml::Value::Array(lib_cfg_scheme)) = raw
634            .scheme
635            .drain()
636            // Silently ignore all potential toml variables other than `scheme`.
637            .filter(|(k, _)| k == "scheme")
638            .map(|(_, v)| v)
639            .next()
640        {
641            // Merge all `s` into a `base_scheme`, parse the result into a `Scheme`
642            // and collect a `Vector`. `merge_depth=0` means we never append
643            // to left hand arrays, we always overwrite them.
644            schemes = lib_cfg_scheme
645                .into_iter()
646                .map(|v| CfgVal::merge_toml_values(raw.base_scheme.clone(), v, 0))
647                .map(|v| v.try_into().map_err(|e| e.into()))
648                .collect::<Result<Vec<Scheme>, LibCfgError>>()?;
649        }
650        let raw = raw; // Freeze.
651
652        let mut tmpl_html = raw.tmpl_html;
653        // Now calculate `LibCfgRaw.tmpl_html.viewer_highlighting_css`:
654        #[cfg(feature = "renderer")]
655        let css = if !tmpl_html.viewer_highlighting_css.is_empty() {
656            tmpl_html.viewer_highlighting_css
657        } else {
658            get_highlighting_css(&tmpl_html.viewer_highlighting_theme)
659        };
660        #[cfg(not(feature = "renderer"))]
661        let css = String::new();
662
663        tmpl_html.viewer_highlighting_css = css;
664
665        // Calculate `LibCfgRaw.tmpl_html.exporter_highlighting_css`:
666        #[cfg(feature = "renderer")]
667        let css = if !tmpl_html.exporter_highlighting_css.is_empty() {
668            tmpl_html.exporter_highlighting_css
669        } else {
670            get_highlighting_css(&tmpl_html.exporter_highlighting_theme)
671        };
672        #[cfg(not(feature = "renderer"))]
673        let css = String::new();
674
675        tmpl_html.exporter_highlighting_css = css;
676
677        // Store the result:
678        let res = LibCfg {
679            // Copy the parts of `config` into `LIB_CFG`.
680            scheme_sync_default: raw.scheme_sync_default,
681            scheme: schemes,
682            tmpl_html,
683        };
684        // Perform some additional semantic checks.
685        res.assert_validity()?;
686        Ok(res)
687    }
688}
689
690/// This constructor accepts as input the newtype `CfgVal` containing
691/// a `toml::map::Map<String, Value>`. Each `String` is the name of a top
692/// level configuration variable.
693/// The inner Map is expected to be a data structure that can be copied into
694/// the internal temporary variable `LibCfgRaw`. This internal varable
695/// is then processed and the result is stored in a `LibCfg` struct. For details
696/// see the `impl TryFrom<LibCfgRaw> for LibCfg`. The processing occurs as
697/// follows:
698///
699/// 1. Merge each incomplete `CfgVal(key="scheme")` into
700///    `CfgVal(key="base_scheme")` and
701///    store the resulting `scheme` struct in `LibCfg.scheme`.
702/// 2. If `CfgVal(key="html_tmpl.viewer_highlighting_css")` is empty, generate
703///    the value from `CfgVal(key="tmpl.viewer_highlighting_theme")`.
704/// 3. Do the same for `CfgVal(key="html_tmpl.exporter_highlighting_css")`.
705impl TryFrom<CfgVal> for LibCfg {
706    type Error = LibCfgError;
707
708    fn try_from(cfg_val: CfgVal) -> Result<Self, Self::Error> {
709        let c = LibCfgRaw::try_from(cfg_val)?;
710        LibCfg::try_from(c)
711    }
712}
713
714/// Defines the way the HTML exporter rewrites local links.
715/// The command line option `--export-link-rewriting` expects this enum.
716/// Consult the manpage for details.
717#[derive(Debug, Hash, Clone, Eq, PartialEq, Deserialize, Serialize, Copy, Default)]
718pub enum LocalLinkKind {
719    /// Do not rewrite links.
720    Off,
721    /// Rewrite relative local links. Base: location of `.tpnote.toml`
722    Short,
723    /// Rewrite all local links. Base: "/"
724    #[default]
725    Long,
726}
727
728impl FromStr for LocalLinkKind {
729    type Err = LibCfgError;
730    fn from_str(level: &str) -> Result<LocalLinkKind, Self::Err> {
731        match &*level.to_ascii_lowercase() {
732            "off" => Ok(LocalLinkKind::Off),
733            "short" => Ok(LocalLinkKind::Short),
734            "long" => Ok(LocalLinkKind::Long),
735            _ => Err(LibCfgError::ParseLocalLinkKind {}),
736        }
737    }
738}
739
740/// Describes a set of tests, that assert template variable `tera:Value`
741/// properties.
742#[derive(Default, Debug, Hash, Clone, Eq, PartialEq, Deserialize, Serialize, Copy)]
743pub enum Assertion {
744    /// `IsDefined`: Assert that the variable is defined in the template.
745    IsDefined,
746    /// `IsNotEmptyString`: In addition to `IsString`, the condition asserts,
747    /// that the string -or all substrings-) are not empty.
748    IsNotEmptyString,
749    /// `IsString`: Assert, that if the variable is defined, its type -or all
750    /// subtypes- are `Value::String`.
751    IsString,
752    /// `IsNumber`: Assert, that if the variable is defined, its type -or all
753    /// subtypes- are `Value::Number`.
754    IsNumber,
755    /// `IsBool`: Assert, that if the variable is defined, its type -or all
756    /// subtypes- are `Value::Bool`.
757    IsBool,
758    /// `IsNotCompound`: Assert, that if the variable is defined, its type is
759    /// not `Value::Array` or `Value::Object`.
760    IsNotCompound,
761    /// `IsValidSortTag`: Assert, that if the variable is defined, the value's
762    /// string representation contains solely characters of the
763    /// `filename.sort_tag.extra_chars` set, digits or lowercase letters.
764    /// The number of lowercase letters in a row is limited by
765    /// `tpnote_lib::config::FILENAME_SORT_TAG_LETTERS_IN_SUCCESSION_MAX`.
766    IsValidSortTag,
767    /// `IsConfiguredScheme`: Assert, that -if the variable is defined- the
768    /// string equals to one of the `scheme.name` in the configuration file.
769    IsConfiguredScheme,
770    /// `IsTpnoteExtension`: Assert, that if the variable is defined,
771    /// the values string representation is registered in one of the
772    /// `filename.extension_*` configuration file variables.
773    IsTpnoteExtension,
774    /// `NoOperation` (default): A test that is always satisfied. For internal
775    ///  use only.
776    #[default]
777    NoOperation,
778}
779
780/// A newtype holding configuration data.
781#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
782pub struct CfgVal(toml::map::Map<String, Value>);
783
784/// This API deals with configuration values.
785///
786impl CfgVal {
787    /// Constructor returning an empty map.
788    pub fn new() -> Self {
789        Self::default()
790    }
791
792    /// Append key, value pairs from other to `self`.
793    ///
794    /// ```rust
795    /// use tpnote_lib::config::CfgVal;
796    /// use std::str::FromStr;
797    ///
798    /// let toml1 = "\
799    /// [arg_default]
800    /// scheme = 'zettel'
801    /// ";
802    ///
803    /// let toml2 = "\
804    /// [base_scheme]
805    /// name = 'some name'
806    /// ";
807    ///
808    /// let mut cfg1 = CfgVal::from_str(toml1).unwrap();
809    /// let cfg2 = CfgVal::from_str(toml2).unwrap();
810    ///
811    /// let expected = CfgVal::from_str("\
812    /// [arg_default]
813    /// scheme = 'zettel'
814    /// [base_scheme]
815    /// name = 'some name'
816    /// ").unwrap();
817    ///
818    /// // Run test
819    /// cfg1.extend(cfg2);
820    ///
821    /// assert_eq!(cfg1, expected);
822    ///
823    #[inline]
824    pub fn extend(&mut self, other: Self) {
825        self.0.extend(other.0);
826    }
827
828    #[inline]
829    pub fn insert(&mut self, key: String, val: Value) {
830        self.0.insert(key, val); //
831    }
832
833    #[inline]
834    /// Merges configuration values from `other` into `self`
835    /// and returns the result. The top level element is a set of key and value
836    /// pairs (map). If one of its values is a `Value::Array`, then the
837    /// corresponding array from `other` is appended.
838    /// Otherwise the corresponding `other` value replaces the `self` value.
839    /// Deeper nested `Value::Array`s are never appended but always replaced
840    /// (`CONFIG_FILE_MERGE_PEPTH=2`).
841    /// Append key, value pairs from other to `self`.
842    ///
843    /// ```rust
844    /// use tpnote_lib::config::CfgVal;
845    /// use std::str::FromStr;
846    ///
847    /// let toml1 = "\
848    /// version = '1.0.0'
849    /// [[scheme]]
850    /// name = 'default'
851    /// ";
852    /// let toml2 = "\
853    /// version = '2.0.0'
854    /// [[scheme]]
855    /// name = 'zettel'
856    /// ";
857    ///
858    /// let mut cfg1 = CfgVal::from_str(toml1).unwrap();
859    /// let cfg2 = CfgVal::from_str(toml2).unwrap();
860    ///
861    /// let expected = CfgVal::from_str("\
862    /// version = '2.0.0'
863    /// [[scheme]]
864    /// name = 'default'
865    /// [[scheme]]
866    /// name = 'zettel'
867    /// ").unwrap();
868    ///
869    /// // Run test
870    /// let res = cfg1.merge(cfg2);
871    ///
872    /// assert_eq!(res, expected);
873    ///
874    pub fn merge(self, other: Self) -> Self {
875        let left = Value::Table(self.0);
876        let right = Value::Table(other.0);
877        let res = Self::merge_toml_values(left, right, CONFIG_FILE_MERGE_DEPTH);
878        // Invariant: when left and right are `Value::Table`, then `res`
879        // must be a `Value::Table` also.
880        if let Value::Table(map) = res {
881            Self(map)
882        } else {
883            unreachable!()
884        }
885    }
886
887    /// Merges configuration values from the right-hand side into the
888    /// left-hand side and returns the result. The top level element is usually
889    /// a `toml::Value::Table`. The table is a set of key and value pairs.
890    /// The values here can be compound data types, i.e. `Value::Table` or
891    /// `Value::Array`.
892    /// `merge_depth` controls whether a top-level array in the TOML document
893    /// is appended to instead of overridden. This is useful for TOML documents
894    /// that have a top-level arrays (`merge_depth=2`) like `[[scheme]]` in
895    /// `tpnote.toml`. For top level arrays, one usually wants to append the
896    /// right-hand array to the left-hand array instead of just replacing the
897    /// left-hand array with the right-hand array. If you set `merge_depth=0`,
898    /// all arrays whatever level they have, are always overridden by the
899    /// right-hand side.
900    fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: isize) -> toml::Value {
901        use toml::Value;
902
903        fn get_name(v: &Value) -> Option<&str> {
904            v.get("name").and_then(Value::as_str)
905        }
906
907        match (left, right) {
908            (Value::Array(mut left_items), Value::Array(right_items)) => {
909                // The top-level arrays should be merged but nested arrays
910                // should act as overrides. For the `tpnote.toml` config,
911                // this means that you can specify a sub-set of schemes in
912                // an overriding `tpnote.toml` but that nested arrays like
913                // `scheme.tmpl.fm_var_localization` are replaced instead
914                // of merged.
915                if merge_depth > 0 {
916                    left_items.reserve(right_items.len());
917                    for rvalue in right_items {
918                        let lvalue = get_name(&rvalue)
919                            .and_then(|rname| {
920                                left_items.iter().position(|v| get_name(v) == Some(rname))
921                            })
922                            .map(|lpos| left_items.remove(lpos));
923                        let mvalue = match lvalue {
924                            Some(lvalue) => {
925                                Self::merge_toml_values(lvalue, rvalue, merge_depth - 1)
926                            }
927                            None => rvalue,
928                        };
929                        left_items.push(mvalue);
930                    }
931                    Value::Array(left_items)
932                } else {
933                    Value::Array(right_items)
934                }
935            }
936            (Value::Table(mut left_map), Value::Table(right_map)) => {
937                if merge_depth > -10 {
938                    for (rname, rvalue) in right_map {
939                        match left_map.remove(&rname) {
940                            Some(lvalue) => {
941                                let merged_value =
942                                    Self::merge_toml_values(lvalue, rvalue, merge_depth - 1);
943                                left_map.insert(rname, merged_value);
944                            }
945                            None => {
946                                left_map.insert(rname, rvalue);
947                            }
948                        }
949                    }
950                    Value::Table(left_map)
951                } else {
952                    Value::Table(right_map)
953                }
954            }
955            (_, value) => value,
956        }
957    }
958
959    /// Convert to `toml::Value`.
960    ///
961    /// ```rust
962    /// use tpnote_lib::config::CfgVal;
963    /// use std::str::FromStr;
964    ///
965    /// let toml1 = "\
966    /// version = 1
967    /// [[scheme]]
968    /// name = 'default'
969    /// ";
970    ///
971    /// let cfg1 = CfgVal::from_str(toml1).unwrap();
972    ///
973    /// let expected: toml::Value = toml::from_str(toml1).unwrap();
974    ///
975    /// // Run test
976    /// let res = cfg1.to_value();
977    ///
978    /// assert_eq!(res, expected);
979    ///
980    pub fn to_value(self) -> toml::Value {
981        Value::Table(self.0)
982    }
983}
984
985impl FromStr for CfgVal {
986    type Err = LibCfgError;
987
988    /// Constructor taking a text to deserialize.
989    /// Throws an error if the deserialized root element is not a
990    /// `Value::Table`.
991    fn from_str(s: &str) -> Result<Self, Self::Err> {
992        let v = toml::from_str(s)?;
993        if let Value::Table(map) = v {
994            Ok(Self(map))
995        } else {
996            Err(LibCfgError::CfgValInputIsNotTable)
997        }
998    }
999}
1000
1001impl From<CfgVal> for toml::Value {
1002    fn from(cfg_val: CfgVal) -> Self {
1003        cfg_val.to_value()
1004    }
1005}