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