Skip to main content

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