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