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