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}