diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2892b12 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: NMD Cargo Build & Test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore index ae88591..0c6a9a8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ Cargo.lock perf.data perf.data.old flamegraph.svg -profile.json \ No newline at end of file +./profile.json \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 53548f1..23790f6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ "./Cargo.toml", "./Cargo.toml" ], - "rust-analyzer.showUnlinkedFileNotification": false + "rust-analyzer.showUnlinkedFileNotification": false, + "cSpell.words": [ + "nuid" + ] } \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..86c4bf1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "nmd" +version = "1.3.0" +authors = ["Nicola Ricciardi"] +edition = "2021" +description = "Official NMD CLI and compiler" +readme = "README.md" +repository = "https://github.com/nricciardi/nmd" +license-file = "LICENSE" +keywords = ["compiler", "nmd", "markdown"] +exclude = [ + "test-resources/*", + "logo/*", + "docs/*" +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = "4.4.18" +env_logger = "0.10.1" +getset = "0.1.2" +log = "0.4.20" +notify = "6.1.1" +once_cell = "1.19.0" +rayon = "1.8.0" +regex = "1.10.2" +reqwest = { version = "0.11.24", features = ["blocking"] } +serde = { version = "1.0.195", features = ["derive"] } +serde_json = "1.0.113" +serde_yaml = "0.9.31" +simple_logger = "4.3.3" +thiserror = "1.0.50" +url = "2.5.0" +tokio = { version = "1", features = ["full"] } +warp = "0.3" +chrono = "0.4.38" +nmd-core = "0.40.0" + + +[profile.profiling] +inherits = "release" +debug = true \ No newline at end of file diff --git a/DEVELOP.md b/DEVELOP.md index 50127c6..da7c180 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -4,69 +4,6 @@ nothing -### Features done - -- [x] Use file name instead of absolute path in dossier configuration -- [x] Other sections in dossier configuration to manage all options -- [x] Local math (no CDN) -- [x] List -- [x] List creation check -- [x] Link with identifier -- [x] Link to chapters -- [x] Quote -- [x] Image caption -- [x] Image URL fix meta-characters -- [x] Multiple image in a single row -- [x] Set image dimensions -- [x] Image compression -- [x] Image in dossier: default path to `assets/images` -- [x] Parse image caption -- [x] Focus block -- [x] Light base page style -- [x] Embedded style -- [x] Embedded chapter style -- [x] Custom style files -- [x] Tables -- [x] Embedded Greek letters -- [x] Fix single list item -- [x] Todo modifier with only `todo` or `TODO` -- [x] Todo modifier with text between `TODO:` and `:TODO` -- [x] Relative header (e.g. `#+` to indicate precedent header level + 1, `#=` to indicate same header level of precedente header) -- [x] Short image modifier (without alt) -- [x] `nmd dossier add` auto-add `.nmd` -- [x] `nmd dossier add` accept more than one file -- [x] Escape -- [x] Metadata -- [x] Reference -- [x] Table of contents without page numbers -- [x] Bibliography -- [x] Compile only modified chapters in watcher mode -- [x] Web server to refresh compiled output - ### Planned Features -- [ ] All modifiers -- [ ] `* words *` -- [x] Use `getset` crate -- [ ] embed_remote_image -- [ ] Possibility to use a different dossier configuration name -- [ ] PDF compile format -- [x] Vintage style (typewriter) -- [x] Dark style -- [ ] Run code -- [ ] Video -- [x] Scientific style -- [ ] Linkify (convert URL-like strings in links) -- [x] Fast draft (prevent to parse time consuming parts) -- [ ] Dynamics addons (e.g. katex iff math is used) - [ ] Watcher mode for single file compilation -- [ ] Split CLI lib from compiler -- [x] Compile only a subset of documents -- [ ] Paper format support (A3, A5, ...) -- [x] MD to NMD converter -- [x] Include all .nmd file in running directory as default option in dossier configuration -- [x] Compile single files -- [ ] Table of contents with page numbers -- [ ] Select position of ToC and Bibliography -- [ ] Cover page -- [ ] VS Code extension (https://dev.to/salesforceeng/how-to-build-a-vs-code-extension-for-markdown-preview-using-remark-processor-1169) diff --git a/README.md b/README.md index 2d19600..481d644 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ ![Logo](logo/logo.png) + +[![License](https://img.shields.io/badge/license-GPL3-green.svg)](LICENSE) +[![Version](https://img.shields.io/badge/version-v1.3.0-blue.svg)](CHANGELOG.md) + # New MarkDown **New way to write in markdown** @@ -57,18 +61,131 @@ Why stick to Markdown when you can be cool using **NMD**? You can find the current NMD's syntax to build nmd documents go to this [link](https://github.com/nricciardi/nmd-core/blob/main/NMD.md). -## NMD command line interface -You can download last release of **NMD command line interface** [here](https://github.com/nricciardi/nmd-cli/releases) or download it through [Cargo](https://github.com/rust-lang/cargo): +## NMD CLI + +### Getting Started + +Do you want **migrate from Markdown to New Markdown** easily and quickly? Read [how to do that](#markdown-to-new-markdown) using CLI! + +### TL;DR ```shell cargo install nmd + +nmd generate dossier -p dossier/input/path -f -w + +nmd dossier -p dossier/input/path add -d new-document.nmd + +nmd build -i input/path +``` + +### Installation + +You can install NMD using Cargo or downloading last release from Github. + +```shell +cargo install nmd +``` + +### Commands + +#### Generate a new dossier + +To **generate a new dossier** you can use the following command: + +```shell +nmd generate dossier -p dossier/input/path +``` + +There are many *flags* that you can use in combination with `generate dossier`. For example, if you want *force* the generation you can use `-f`, or if you want a *welcome page* you can use `-w`. + +```shell +nmd generate dossier -p dossier/input/path -f -w +``` + +The Git support is planned, but not implemented yet. You can only add `.gitkeep` files in assets directories using `-k`. + +`-n` permits to specify dossier name. + +##### Markdown to New Markdown + +You can easily convert a standard Markdown file in a New Markdown dossier using `--from-md ` option. + +##### Add a new document + +To **add a new document** you can use the following command: + +```shell +nmd dossier -p dossier/input/path add -d new-document.nmd +``` + +If the document name doesn't have `nmd` extension, it will be added automatically. + +You can add more than one document at the same time: + +```shell +nmd dossier -p dossier/input/path add -d new-document-1.nmd -d new-document-2.nmd -d new-document-3.nmd +``` + +##### Reset dossier configuration + +```shell +nmd dossier -p dossier/input/path reset [ -p ] +``` + +`-p` reset flag to preserve documents list. + +#### Build + +You can build a dossier or a single file through `build` command. + +The only mandatory option is the input path. It can be a path to a directory (dossier) or a file. + +```shell +nmd build -i input/path +``` + +`compile` command has a lot of options. You could specify the output format using `-f ` (e.g. `html`, which is the default), the output path with `-o ` or the theme using `-t `. The available themes are: + +- `light` +- `dark` +- `vintage` +- `scientific` +- `none` + + +```shell +nmd compile -f html dossier -i dossier/input/path -o artifact/output/path +``` + +Moreover, if you watch dossier files and compile them if something changes, you should use watcher mode (`-w` option). Watcher mode compile dossier if any change is captured. Changes are captured only if a minimum time is elapsed. To set minimum time use `--watcher-time` option. + +`--fast-draft` to create a fast draft of dossier, generally compiler takes less time to generate it. + +`--parallelization` to parallelize work (default is single thread). + +`-s -s ` to compile only a subset of documents in dossier configuration list. + +In the end, if you are writing in NMD and you want a preview, you could compile with `-p` option. `-p` renders a preview in a web server on `127.0.0.1:1234` (`--preview-scraping-interval ` to set client scraping interval in *milliseconds*). + +`--embed-local-image`, `--embed-remote-image`, `--strict-image-src-check` and `--embed-local-image` to manage images parsing. + +You can use `--nuid` to add *NUID*. + +#### Analyze + +You could want analyze a dossier or a document before build it. `analyze` command print on `stdout` the corresponding JSON. + +```shell +nmd analyze -i input/path ``` -## NMD x VS Code +You can use `--nuid` to add *NUID* or `--pretty` to print pretty formatted JSON. -You can use the official [NMD x VS Code](https://github.com/nricciardi/nmd-vscode) extension! +## Develop +Develop [check list](DEVELOP.md) ## Author diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..99754ef --- /dev/null +++ b/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +rustup update + +cargo test --verbose + +cargo build --release --target x86_64-unknown-linux-gnu + +cargo build --release --target x86_64-pc-windows-gnu diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..7b3890f --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,537 @@ +pub mod builder_error; +pub mod builder_configuration; +mod constants; + + +use std::{borrow::Borrow, collections::HashSet, path::PathBuf, sync::Arc, time::Instant}; +use builder_configuration::BuilderConfiguration; +use builder_error::BuilderError; +use nmd_core::{assembler::{html_assembler::{html_assembler_configuration::HtmlAssemblerConfiguration, HtmlAssembler}, Assembler}, compiler::{compilation_configuration::compilation_configuration_overlay::CompilationConfigurationOverLay, Compiler}, constants::{DOSSIER_CONFIGURATION_JSON_FILE_NAME, DOSSIER_CONFIGURATION_YAML_FILE_NAME}, dossier::{dossier_configuration::DossierConfiguration, Document, Dossier}, dumpable::{DumpConfiguration, Dumpable}, loader::{loader_configuration::{LoaderConfiguration, LoaderConfigurationOverLay}, Loader}, output_format::OutputFormat, theme::Theme, utility::{file_utility, nmd_unique_identifier::assign_nuid_to_document_paragraphs}}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use tokio::{sync::RwLock as TokioRwLock, task::JoinSet}; +use crate::preview::{html_preview::PREVIEW_URL, Preview}; +use crate::{preview::html_preview::HtmlPreview, watcher::{NmdWatcher, WatcherError}}; + + + +pub struct Builder { +} + +impl Builder { + + /// Load dossier from `BuilderConfiguration` + pub async fn load_dossier(builder_configuration: &BuilderConfiguration) -> Result { + + log::info!("start to load dossier {:?}", builder_configuration.input_location()); + + let loading_start = Instant::now(); + + let mut loader_configuration = LoaderConfiguration::default(); + loader_configuration.set_input_location(builder_configuration.input_location().clone()); + + let loader_configuration_overlay = LoaderConfigurationOverLay::default(); + + let mut dossier: Dossier; + + if let Some(dstc) = builder_configuration.documents_subset_to_compile() { + + dossier = Loader::load_dossier_from_path_buf_only_documents(builder_configuration.input_location(), &dstc, &builder_configuration.codex(), &loader_configuration, loader_configuration_overlay)?; + + } else { + + dossier = Loader::load_dossier_from_path_buf(builder_configuration.input_location(), &builder_configuration.codex(), &loader_configuration, loader_configuration_overlay)?; + } + + if let Some(with_nuid) = builder_configuration.nuid() { + if with_nuid { + log::info!("assign nuid..."); + dossier.documents_mut().iter_mut().for_each(|d| assign_nuid_to_document_paragraphs(d)); + } + } + + log::info!("dossier loaded in {} ms", loading_start.elapsed().as_millis()); + + Ok(dossier) + } + + pub async fn build_dossier(dossier: &mut Dossier, builder_configuration: &BuilderConfiguration) -> Result<(), BuilderError> { + Self::build_dossier_compiling_subset(dossier, builder_configuration, None).await + } + + pub async fn build_dossier_compiling_subset(dossier: &mut Dossier, builder_configuration: &BuilderConfiguration, subset_documents_to_parse: Option>) -> Result<(), BuilderError> { + + log::info!("start to compile dossier"); + + let compilation_start = Instant::now(); + + let mut compilation_configuration = builder_configuration.generate_compilation_configuration(); + + compilation_configuration.set_list_bullets_configuration(dossier.configuration().style().list_bullets_configuration().clone()); + compilation_configuration.set_strict_list_check(dossier.configuration().compilation().strict_list_check()); + + if compilation_configuration.compress_embed_image() || compilation_configuration.embed_local_image() || compilation_configuration.embed_remote_image() { + + log::warn!("embedding or compressing images is a time consuming task! Consider not using this feature unless strictly necessary"); + } + + log::info!("will use dossier configuration: {:?}", compilation_configuration.input_location()); + log::debug!("will use dossier configuration:\n\n{:#?}\n", dossier.configuration()); + + log::info!("parsing using theme: {}", compilation_configuration.theme()); + log::debug!("parsing configuration:\n{:#?}\n", compilation_configuration); + + if compilation_configuration.fast_draft() { + log::info!("fast draft mode on!") + } + + let mut compilation_configuration_overlay = CompilationConfigurationOverLay::default(); + + if let Some(subset) = subset_documents_to_parse { + + compilation_configuration_overlay.set_compile_only_documents(Some(subset)); + } + + Compiler::compile_dossier(dossier, builder_configuration.format(), &builder_configuration.codex(), &compilation_configuration, compilation_configuration_overlay)?; + + log::info!("dossier compiled in {} ms", compilation_start.elapsed().as_millis()); + + log::info!("assembling..."); + + let assembly_time = Instant::now(); + + let mut artifact = match builder_configuration.format() { + OutputFormat::Html => { + let mut assembler_configuration = HtmlAssemblerConfiguration::from(dossier.configuration().clone()); + + if let Some(t) = builder_configuration.theme().as_ref() { + + assembler_configuration.set_theme(t.clone()); + } + + if let Some(there_is_preview) = builder_configuration.preview() { + if there_is_preview { + if let Some(watch_mode) = builder_configuration.watching() { + if watch_mode { + + assembler_configuration.external_scripts_mut() + .push(include_str!("preview/check_preview_updates.js").to_string()) + + } + } + } + } + + HtmlAssembler::assemble_dossier(&dossier, &assembler_configuration)? + }, + }; + + log::info!("end to assembly (assembly time {} ms)", assembly_time.elapsed().as_millis()); + + let mut output_location = compilation_configuration.output_location().clone(); + + if output_location.is_dir() { + output_location = output_location.join(file_utility::build_output_file_name( + &dossier.name(), + Some(&builder_configuration.format().get_extension()) + )); + } + + let dump_configuration = DumpConfiguration::new( + output_location, + builder_configuration.force_output().unwrap_or(false) + ); + + artifact.dump(&dump_configuration)?; + + Ok(()) + } + + /// Watch filesystem and compile dossier if any changes occur + /// + /// - min_elapsed_time_between_events_in_secs is the minimum time interval between two compilation + pub async fn watch_compile_dossier(mut builder_configuration: BuilderConfiguration, min_elapsed_time_between_events_in_secs: u64, preview: Option>>) -> Result<(), BuilderError> { + + let input_location_abs = Arc::new(builder_configuration.input_location().canonicalize().unwrap()); + + let dossier = Self::load_dossier(&builder_configuration).await?; + + builder_configuration.merge_dossier_configuration(dossier.configuration()); + + let dossier = Arc::new(TokioRwLock::new(dossier)); + + let builder_configuration = Arc::new(TokioRwLock::new(builder_configuration.clone())); + + let mut watcher = tokio::spawn(async move { + + NmdWatcher::new( + min_elapsed_time_between_events_in_secs, + &input_location_abs.clone(), + Box::new({ + + let preview = preview.clone(); + let builder_configuration: Arc> = Arc::clone(&builder_configuration); + let dossier = dossier.clone(); + let input_location_abs = input_location_abs.clone(); + + move || { + + let builder_configuration = Arc::clone(&builder_configuration); + + let preview = preview.clone(); + + let dossier = dossier.clone(); + + Box::pin({ + + let input_location_abs = input_location_abs.clone(); + async move { + + let compilation_result = tokio::spawn(async move { + Self::build_dossier(&mut (*dossier.write().await), &builder_configuration.read().await.clone()).await + }); + + match compilation_result.await { + Ok(_) => { + + log::info!("compilation OK"); + + println!("\n\n"); + log::info!("watch mode ON: modification to the dossier files will cause recompilation"); + log::info!("start watching: {:?}", input_location_abs); + log::info!("press CTRL + C to terminate"); + println!("\n\n"); + + if let Some(preview) = preview { + + tokio::spawn(async move { + preview.write().await.render().await + }).await??; + } + + return Ok(()) + }, + Err(err) => { + log::error!("error during compilation: {:?}", err); + + return Err(WatcherError::ElaborationError(err.to_string())) + } + } + } + }) + } + }), + Box::new({ + + let input_location_abs = input_location_abs.clone(); + + move |event| { + + let input_location_abs = input_location_abs.clone(); + + Box::pin(async move { + + if event.paths.contains(&input_location_abs.join(DOSSIER_CONFIGURATION_YAML_FILE_NAME)) || + event.paths.contains(&input_location_abs.join(DOSSIER_CONFIGURATION_JSON_FILE_NAME)) { + + log::info!("recompilation needed"); + return Ok(true) + } + + Ok(false) + }) + } + }), + Box::new({ + + let builder_configuration = Arc::clone(&builder_configuration); + + let input_location_abs = input_location_abs.clone(); + + move |event| { + + let builder_configuration = Arc::clone(&builder_configuration); + + let input_location_abs = input_location_abs.clone(); + + Box::pin(async move { + + let original_log_max_level = log::max_level(); + + log::set_max_level(log::LevelFilter::Warn); + + let dc = DossierConfiguration::try_from(builder_configuration.read().await.input_location()); + + log::set_max_level(original_log_max_level); + + if let Err(err) = dc { + log::error!("error during dossier configuration loading: {}", err); + + return Ok(false) + } + + let dc = dc.unwrap(); + + let mut relative_paths_to_monitoring = dc.raw_documents_paths().clone(); + relative_paths_to_monitoring.push(String::from("assets/")); + + let relative_paths_to_monitoring = Arc::new(relative_paths_to_monitoring); + + if let Some(_) = event.paths.par_iter().find_any(|path| { + + let path = path.strip_prefix(&*input_location_abs.clone()); + + if let Ok(path) = path { + let matched = relative_paths_to_monitoring.par_iter().find_any(|rptm| { + log::debug!("{:?} contains {:?} -> {}", *rptm, path.to_string_lossy().to_string().as_str(), rptm.contains(path.to_string_lossy().to_string().as_str())); + + rptm.contains(path.to_string_lossy().to_string().as_str()) + }); + + return matched.is_some() + } + + false + }) { + log::info!("recompilation needed"); + + return Ok(true) + + + } else { + log::info!("recompilation not needed"); + + return Ok(false) + } + }) + } + }), + Box::new({ + let builder_configuration = Arc::clone(&builder_configuration); + + // let preview = Arc::clone(&preview); + let preview = preview.clone(); + + move |paths| { + Box::pin({ + + let builder_configuration = Arc::clone(&builder_configuration); + let preview = preview.clone(); + let dossier = dossier.clone(); + + async move { + + let documents_to_parse: Option>; // None => all documents + + // check if nmd.yml or nmd.json is changed => load whole dossier + if paths.iter() + .map(|p| p.file_name()) + .filter(|f| f.is_some()) + .map(|f| f.unwrap().to_string_lossy().to_string()) + .find(|f| f.eq(DOSSIER_CONFIGURATION_YAML_FILE_NAME)) + .is_some() { + + documents_to_parse = None; + + match Self::load_dossier(&*builder_configuration.clone().read().await).await { + Ok(d) => { + + builder_configuration.write().await.merge_dossier_configuration(d.configuration()); + + *dossier.write().await = d; + }, + Err(err) => return Err(WatcherError::ElaborationError(err.to_string())), + } + + } else { // load dossier partially + let codex = Arc::new(builder_configuration.read().await.codex()); + + let mut dtp: HashSet = HashSet::new(); + + let mut document_read_handles = JoinSet::new(); + for path in &paths { + + if dossier.read().await.configuration().raw_documents_paths().par_iter().find_any(|raw_path| { + let document_path = PathBuf::from(raw_path); + + if let Some(document_name) = document_path.file_name() { + + if let Some(file_name) = path.file_name() { + return document_name.eq(file_name); + } + } + + false + }).is_some() { + + let path = path.clone(); + let codex = codex.clone(); + + document_read_handles.spawn(async move { + + let document = Loader::load_document_from_path(&path, &codex, &LoaderConfiguration::default(), LoaderConfigurationOverLay::default()); + + document + }); + } + } + + while let Some(document_read_res) = document_read_handles.join_next().await { + if let Ok(document) = document_read_res? { + + let name = document.name().clone(); + + let res = dossier.write().await.replace_document(&name, document); + + dtp.insert(name); + + res + } + } + + documents_to_parse = Some(dtp); + } + + let build_result = tokio::spawn(async move { + + Self::build_dossier_compiling_subset(&mut *dossier.write().await, builder_configuration.read().await.borrow(), documents_to_parse).await + }); + + let preview = preview.clone(); + + match build_result.await { + Ok(_) => { + + log::info!("compilation OK"); + + if let Some(preview) = preview { + + tokio::spawn(async move { + preview.write().await.update().await + }).await??; + } + + println!("\n\n"); + log::info!("preview is available on {}", PREVIEW_URL); + println!("\n\n"); + + return Ok(()) + }, + Err(err) => { + log::error!("error during compilation: {:?}", err); + + return Err(WatcherError::ElaborationError(err.to_string())) + } + } + } + }) + } + }), + ).await + }).await??; + + let watcher_join_handle = tokio::spawn(async move { + + watcher.start().await + + }); + + watcher_join_handle.await??; + + log::info!("stop watching..."); + + Ok(()) + + } + + + /// Load document + pub async fn load_document(builder_configuration: &BuilderConfiguration) -> Result { + + log::info!("start to load dossier"); + + let build_start = Instant::now(); + + let codex = builder_configuration.codex(); + + let mut document: Document = Loader::load_document_from_path(builder_configuration.input_location(), &codex, &LoaderConfiguration::default(), LoaderConfigurationOverLay::default())?; + + if let Some(with_nuid) = builder_configuration.nuid() { + if with_nuid { + log::info!("assign nuid..."); + assign_nuid_to_document_paragraphs(&mut document); + } + } + + log::info!("document loaded in {} ms", build_start.elapsed().as_millis()); + + Ok(document) + } + + /// Standard file compilation based on `BuilderConfiguration` + /// It loads, compiles and dumps a document + pub async fn build_document(builder_configuration: &BuilderConfiguration) -> Result<(), BuilderError> { + + log::info!("start to build document"); + + let build_start = Instant::now(); + + let mut document = Self::load_document(builder_configuration).await?; + + let compilation_configuration = builder_configuration.generate_compilation_configuration(); + + if compilation_configuration.compress_embed_image() || compilation_configuration.embed_local_image() || compilation_configuration.embed_remote_image() { + + log::warn!("embedding or compressing images is a time consuming task! Consider not using this feature unless strictly necessary"); + } + + log::info!("will use dossier configuration: {:?}", builder_configuration.input_location()); + + log::info!("parsing using theme: {}", compilation_configuration.theme()); + log::debug!("parsing configuration:\n{:#?}\n", compilation_configuration); + + if compilation_configuration.fast_draft() { + log::info!("fast draft mode on!") + } + + Compiler::compile_document(&mut document, builder_configuration.format(), &builder_configuration.codex(), &compilation_configuration, CompilationConfigurationOverLay::default())?; + + log::info!("document compiled in {} ms", build_start.elapsed().as_millis()); + + log::info!("assembling..."); + + let output_location = builder_configuration.output_location().clone(); + + let assembly_time = Instant::now(); + + let mut artifact = match builder_configuration.format() { + OutputFormat::Html => { + + let mut assembler_configuration = HtmlAssemblerConfiguration::default(); + + assembler_configuration.set_theme(builder_configuration.theme().clone().unwrap_or(Theme::default())); + + if let Some(there_is_preview) = builder_configuration.preview() { + if there_is_preview { + assembler_configuration.external_styles_mut().push(include_str!("preview/check_preview_updates.js").to_string()) + } + } + + HtmlAssembler::assemble_document_standalone(&document, &output_location.file_stem().unwrap().to_string_lossy().to_string(), None, None, &assembler_configuration)? + }, + }; + + log::info!("end to assembly (assembly time {} ms)", assembly_time.elapsed().as_millis()); + + let dump_configuration = DumpConfiguration::new(output_location, builder_configuration.force_output().unwrap_or(false)); + + artifact.dump(&dump_configuration)?; + + log::info!("document build in {} ms", build_start.elapsed().as_millis()); + + Ok(()) + } +} + +#[cfg(test)] +mod test { +} \ No newline at end of file diff --git a/src/builder/builder_configuration.rs b/src/builder/builder_configuration.rs new file mode 100644 index 0000000..b66d157 --- /dev/null +++ b/src/builder/builder_configuration.rs @@ -0,0 +1,273 @@ +use std::{collections::HashSet, path::PathBuf}; +use getset::{CopyGetters, Getters, MutGetters, Setters}; +use nmd_core::{bibliography::Bibliography, codex::Codex, compiler::compilation_configuration::{CompilableResourceType, CompilationConfiguration}, dossier::dossier_configuration::DossierConfiguration, output_format::OutputFormat, resource::text_reference::TextReferenceMap, theme::Theme}; + + +/// Struct which contains all information about possible compilation options. It is used to wrap specific user requests for compilation +#[derive(Debug, Getters, CopyGetters, MutGetters, Setters, Clone)] +pub struct BuilderConfiguration { + + #[getset(get = "pub", set = "pub")] + format: OutputFormat, + + #[getset(get = "pub")] + input_location: PathBuf, + + #[getset(get = "pub", set = "pub")] + output_location: PathBuf, + + #[getset(get_copy = "pub", set = "pub")] + force_output: Option, + + #[getset(get = "pub", set = "pub")] + fast_draft: Option, + + #[getset(get = "pub", set = "pub")] + embed_local_image: Option, + + #[getset(get = "pub", set = "pub")] + embed_remote_image: Option, + + #[getset(get = "pub", set = "pub")] + compress_embed_image: Option, + + #[getset(get = "pub", set = "pub")] + strict_image_src_check: Option, + + #[getset(get = "pub", set = "pub")] + parallelization: Option, + + #[getset(get = "pub", set = "pub")] + use_remote_addons: Option, + + #[getset(get = "pub", set = "pub")] + references: Option, + + #[getset(get = "pub", set = "pub")] + documents_subset_to_compile: Option>, + + #[getset(get = "pub", set = "pub")] + bibliography: Option, + + #[getset(get = "pub", set = "pub")] + theme: Option, + + #[getset(get = "pub", set = "pub")] + styles_raw_path: Vec, + + #[getset(get = "pub", set = "pub")] + resource_type: CompilableResourceType, + + #[getset(get_copy = "pub", set = "pub")] + preview: Option, + + #[getset(get_copy = "pub", set = "pub")] + watching: Option, + + #[getset(get_copy = "pub", set = "pub")] + nuid: Option, +} + +impl BuilderConfiguration { + pub fn new(input_location: PathBuf, output_location: PathBuf) -> Self { + + let mut builder_configuration = Self { + output_location, + + ..Default::default() + }; + + builder_configuration.set_input_location(input_location); + + builder_configuration + } + + /// Set input location and auto-set resource type + pub fn set_input_location(&mut self, input_location: PathBuf) { + if input_location.is_dir() { + + self.resource_type = CompilableResourceType::Dossier; + + } else { + + self.resource_type = CompilableResourceType::File; + } + + self.input_location = input_location; + } + + pub fn codex(&self) -> Codex { + + Codex::from(&self.format) + } + + pub fn generate_compilation_configuration(&self) -> CompilationConfiguration { + let mut compilation_configuration = CompilationConfiguration::default(); + + compilation_configuration.set_input_location(self.input_location().clone()); + + compilation_configuration.set_output_location(self.output_location().clone()); + + if let Some(val) = self.embed_local_image() { + + compilation_configuration.set_embed_local_image(*val); + } + + if let Some(val) = self.compress_embed_image() { + + compilation_configuration.set_compress_embed_image(*val); + } + + if let Some(val) = self.embed_remote_image() { + + compilation_configuration.set_embed_remote_image(*val); + } + + if let Some(val) = self.strict_image_src_check() { + + compilation_configuration.set_strict_image_src_check(*val); + } + + if let Some(val) = self.embed_remote_image() { + + compilation_configuration.set_embed_remote_image(*val); + } + + if let Some(val) = self.embed_remote_image() { + + compilation_configuration.set_embed_remote_image(*val); + } + + if let Some(val) = self.parallelization() { + + compilation_configuration.set_parallelization(*val); + } + + if let Some(val) = self.fast_draft() { + + compilation_configuration.set_fast_draft(*val); + } + + if let Some(val) = self.references() { + + compilation_configuration.set_references(val.clone()); + } + + compilation_configuration.set_bibliography(self.bibliography().clone()); + + if let Some(val) = self.theme() { + + compilation_configuration.set_theme(val.clone()); + } + + compilation_configuration.set_resource_type(self.resource_type().clone()); + + compilation_configuration + } +} + +impl BuilderConfiguration { + pub fn merge_dossier_configuration(&mut self, dossier_configuration: &DossierConfiguration) { + + if self.embed_local_image().is_none() { + self.set_embed_local_image(Some(dossier_configuration.compilation().embed_local_image().clone())); + } + + if self.embed_remote_image().is_none() { + self.set_embed_remote_image(Some(dossier_configuration.compilation().embed_remote_image().clone())); + } + + if self.compress_embed_image().is_none() { + self.set_compress_embed_image(Some(dossier_configuration.compilation().compress_embed_image().clone())); + } + + if self.use_remote_addons().is_none() { + self.set_use_remote_addons(Some(dossier_configuration.compilation().use_remote_addons().clone())); + } + + if self.parallelization().is_none() { + self.set_parallelization(Some(dossier_configuration.compilation().parallelization().clone())); + } + + if self.strict_image_src_check().is_none() { + self.set_strict_image_src_check(Some(dossier_configuration.compilation().strict_image_src_check().clone())); + } + + if self.references().is_none() { + self.set_references(Some(dossier_configuration.references().clone())); + } + + if self.bibliography().is_none() { + self.set_bibliography(Some(Bibliography::from(dossier_configuration.bibliography()))); + } + + if self.theme().is_none() { + self.set_theme(Some(dossier_configuration.style().theme().clone())); + } + } + + pub fn fill_with_default(&mut self) { + if self.embed_local_image().is_none() { + self.set_embed_local_image(Some(false)); + } + + if self.embed_remote_image().is_none() { + self.set_embed_remote_image(Some(false)); + } + + if self.compress_embed_image().is_none() { + self.set_compress_embed_image(Some(false)); + } + + if self.use_remote_addons().is_none() { + self.set_use_remote_addons(Some(false)); + } + + if self.parallelization().is_none() { + self.set_parallelization(Some(false)); + } + + if self.strict_image_src_check().is_none() { + self.set_strict_image_src_check(Some(true)); + } + + if self.references().is_none() { + self.set_references(Some(TextReferenceMap::default())); + } + + if self.bibliography().is_none() { + self.set_bibliography(None); + } + + if self.theme().is_none() { + self.set_theme(Some(Theme::default())); + } + } +} + +impl Default for BuilderConfiguration { + fn default() -> Self { + Self { + format: Default::default(), + input_location: PathBuf::from("."), + output_location: PathBuf::from("."), + fast_draft: Some(false), + force_output: Some(false), + embed_local_image: None, + embed_remote_image: None, + compress_embed_image: None, + strict_image_src_check: None, + parallelization: None, + use_remote_addons: None, + references: None, + documents_subset_to_compile: None, + bibliography: None, + theme: None, + styles_raw_path: Vec::new(), + resource_type: CompilableResourceType::default(), + preview: Some(false), + watching: Some(false), + nuid: Some(false), + } + } +} \ No newline at end of file diff --git a/src/builder/builder_error.rs b/src/builder/builder_error.rs new file mode 100644 index 0000000..0aa8190 --- /dev/null +++ b/src/builder/builder_error.rs @@ -0,0 +1,33 @@ +use nmd_core::{assembler::AssemblerError, compiler::compilation_error::CompilationError, dumpable::DumpError, loader::LoadError}; +use thiserror::Error; +use tokio::task::JoinError; + +use crate::{preview::PreviewError, watcher::WatcherError}; + +#[derive(Error, Debug)] +pub enum BuilderError { + + #[error("unknown error")] + Unknown(String), + + #[error(transparent)] + LoadError(#[from] LoadError), + + #[error(transparent)] + CompilationError(#[from] CompilationError), + + #[error(transparent)] + AssemblerError(#[from] AssemblerError), + + #[error(transparent)] + DumpError(#[from] DumpError), + + #[error(transparent)] + PreviewError(#[from] PreviewError), + + #[error(transparent)] + WatcherError(#[from] WatcherError), + + #[error(transparent)] + JoinError(#[from] JoinError), +} \ No newline at end of file diff --git a/src/builder/constants.rs b/src/builder/constants.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..b72543c --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,833 @@ +use std::collections::HashSet; +use std::io::{stdout, Write}; +use std::num::ParseIntError; +use std::ops::Deref; +use std::sync::Arc; +use nmd_core::compiler::compilation_configuration::CompilableResourceType; +use nmd_core::output_format::{OutputFormat, OutputFormatError}; +use nmd_core::resource::ResourceError; +use nmd_core::theme::{Theme, ThemeError}; +use nmd_core::utility::file_utility; +use tokio::sync::RwLock as TokioRwLock; +use std::{path::PathBuf, str::FromStr}; +use clap::{Arg, ArgAction, ArgMatches, Command}; +use tokio::task::{JoinError, JoinHandle}; +use crate::builder::builder_configuration::BuilderConfiguration; +use crate::builder::builder_error::BuilderError; +use crate::builder::Builder; +use crate::constants::{MINIMUM_WATCHER_TIME, VERSION}; +use crate::dossier_manager::{dossier_manager_configuration::DossierManagerConfiguration, DossierManager, DossierManagerError}; +use crate::generator::{generator_configuration::GeneratorConfiguration, Generator}; +use crate::preview::html_preview::HtmlPreview; +use crate::preview::PreviewError; +use crate::preview::Preview; +use log::{LevelFilter, ParseLevelError}; +use thiserror::Error; +use simple_logger::SimpleLogger; + + +#[derive(Error, Debug)] +pub enum NmdCliError { + + #[error("bad command")] + BadCommand, + + #[error("unknown resource")] + UnknownResource, + + #[error(transparent)] + BuilderError(#[from] BuilderError), + + #[error("you must provide only one value of '{0}'")] + MoreThanOneValue(String), + + #[error(transparent)] + OutputFormatError(#[from] OutputFormatError), + + #[error(transparent)] + ThemeError(#[from] ThemeError), + + #[error(transparent)] + VerboseLevelError(#[from] ParseLevelError), + + #[error(transparent)] + ResourceError(#[from] ResourceError), + + #[error("too few arguments: {0} needed")] + TooFewArguments(String), + + #[error(transparent)] + DossierManagerError(#[from] DossierManagerError), + + #[error(transparent)] + PreviewError(#[from] PreviewError), + + #[error(transparent)] + JoinError(#[from] JoinError), + + #[error(transparent)] + ParseIntError(#[from] ParseIntError), + + #[error(transparent)] + JsonError(#[from] serde_json::Error), + + #[error(transparent)] + IoError(#[from] std::io::Error), +} + + +/// NMD CLI. It is used as interface with NDM compiler, NDM generator and others +pub struct NmdCli { + cli: Command +} + +impl NmdCli { + + pub fn new() -> Self { + + let cli: Command = Command::new("nmd") + .about("Official NMD command line interface") + .version(VERSION) + .subcommand_required(true) + .arg_required_else_help(true) + .arg( + Arg::new("verbose") + .short('v') + .long("verbose") + .help("set verbose mode") + .action(ArgAction::Set) + .default_value("info") + ) + .subcommand( + Command::new("build") + .about("Build a NMD file or dossier") + .short_flag('b') + .alias("compile") + .arg( + Arg::new("format") + .short('f') + .long("format") + .help("output format") + .action(ArgAction::Set) + .num_args(1) + .default_value("html") + ) + .arg( + Arg::new("force-output") + .long("force") + .help("force output if destination not exists") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("theme") + .short('t') + .long("theme") + .help("set theme") + .action(ArgAction::Set) + .num_args(1) + ) + .arg( + Arg::new("watch") + .short('w') + .long("watch") + .help("set to compile if files change") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("watcher-time") + .long("watcher-time") + .help("set minimum watcher time interval") + .action(ArgAction::Set) + ) + .arg( + Arg::new("preview") + .short('p') + .long("preview") + .help("show preview") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("preview-scraping-interval") + .long("preview-scraping-interval") + .help("set preview scraping interval") + .required(false) + .action(ArgAction::Set) + ) + .arg( + Arg::new("fast-draft") + .long("fast-draft") + .help("fast draft instead of complete compilation") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("style-file") + .long("style-file") + .help("add style file") + .action(ArgAction::Append) + ) + .arg( + Arg::new("input-path") + .short('i') + .long("input-path") + .help("input path") + .action(ArgAction::Set) + .num_args(1) + .default_value(".") + + ) + .arg( + Arg::new("output-path") + .short('o') + .long("output-path") + .help("output path") + .action(ArgAction::Set) + .num_args(1) + ) + .arg( + Arg::new("documents-subset") + .short('s') + .long("documents-subset") + .help("compile only a documents subset") + .action(ArgAction::Append) + ) + .arg( + Arg::new("parallelization") + .long("parallelization") + .help("set parallelization") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("embed-local-image") + .long("embed-local-image") + .help("set embed local image") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("embed-remote-image") + .long("embed-remote-image") + .help("set embed remote image") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("compress-embed-image") + .long("compress-embed-image") + .help("set compress embed image") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("strict-image-src-check") + .long("strict-image-src-check") + .help("set strict image source check") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("nuid") + .long("nuid") + .help("set nuid") + .action(ArgAction::SetTrue) + ) + ) + .subcommand( + Command::new("generate") + .about("Generate a new NMD resource") + .short_flag('g') + .subcommand_required(true) + .subcommand( + Command::new("dossier") + .about("Generate a new NMD dossier") + .short_flag('d') + .arg( + Arg::new("path") + .short('p') + .long("path") + .help("destination path") + .action(ArgAction::Set) + .num_args(1) + .required(true) + + ) + .arg( + Arg::new("force") + .short('f') + .long("force") + .help("force generation") + .action(ArgAction::SetTrue) + + ) + .arg( + Arg::new("gitkeep") + .short('k') + .long("gitkeep") + .help("add .gitkeep file") + .action(ArgAction::SetTrue) + + ) + .arg( + Arg::new("welcome") + .short('w') + .long("welcome") + .help("add welcome page") + .action(ArgAction::SetTrue) + + ) + .arg( + Arg::new("from-md") + .long("from-md") + .help("generate NMD dossier from Markdown file") + .action(ArgAction::Set) + ) + .arg( + Arg::new("name") + .long("name") + .short('n') + .help("set dossier name") + .action(ArgAction::Set) + ) + + ) + ) + .subcommand( + Command::new("dossier") + .about("Manage NMD dossier") + .subcommand_required(true) + .arg( + Arg::new("dossier-path") + .short('p') + .long("dossier-path") + .help("insert dossier path") + .action(ArgAction::Append) + .default_value(".") + .required(true) + ) + .subcommand( + Command::new("add") + .about("Add resource to a dossier") + .short_flag('a') + .arg( + Arg::new("document-name") + .short('d') + .long("document-name") + .help("insert file name of the document") + .required(true) + .action(ArgAction::Append) + ) + ) + .subcommand( + Command::new("reset") + .about("Reset dossier configuration") + .short_flag('r') + .arg( + Arg::new("no-preserve-documents") + .help("no preserve documents list") + .long("no-preserve-documents") + .required(false) + .action(ArgAction::SetTrue) + ) + ) + ) + .subcommand( + Command::new("analyze") + .about("Analyze NMD dossier or document") + .subcommand_required(false) + .arg( + Arg::new("input-path") + .short('i') + .long("input") + .help("insert input path") + .action(ArgAction::Append) + .default_value(".") + .required(true) + ) + .arg( + Arg::new("nuid") + .long("nuid") + .help("set nuid") + .action(ArgAction::SetTrue) + ) + .arg( + Arg::new("pretty") + .long("pretty") + .help("pretty json") + .action(ArgAction::SetTrue) + ) + ); + Self { + cli + } + } + + pub async fn serve(self) -> Result<(), NmdCliError> { + + let matches = self.cli.get_matches(); + + if let Some(verbose) = matches.get_one::("verbose") { + + let log_level = LevelFilter::from_str(verbose)?; + + Self::set_logger(log_level); + } + + let result: Result<(), NmdCliError> = match matches.subcommand() { + + Some(("build", compile_matches)) => Self::handle_build_command(&compile_matches).await, + + Some(("generate", generate_matches)) => Self::handle_generate_command(&generate_matches).await, + + Some(("dossier", dossier_matches)) => Self::handle_dossier_command(&dossier_matches).await, + + Some(("analyze", analyze_matches)) => Self::handle_analyze_command(&analyze_matches).await, + + _ => { + log::error!("bad command"); + + return Err(NmdCliError::BadCommand) + } + }; + + if let Err(error) = result { + log::error!("{}", error); + return Err(error) + } + + Ok(()) + } + + fn set_logger(log_level: LevelFilter) { + + SimpleLogger::new() + .with_level(log_level) + .init() + .unwrap(); + } + + async fn handle_build_command(matches: &ArgMatches) -> Result<(), NmdCliError> { + + let mut builder_configuration = BuilderConfiguration::default(); + + // FORMAT + if let Some(format) = matches.get_one::("format") { + + let format = OutputFormat::from_str(format)?; + + builder_configuration.set_format(format); + } + + let there_is_preview = matches.get_flag("preview"); + + builder_configuration.set_preview(Some(there_is_preview)); + + if there_is_preview { + + assert!(builder_configuration.format().eq(&OutputFormat::Html)); // there is only HtmlPreview + } + + // INPUT & OUTPUT PATHs + if let Some(input_path) = matches.get_one::("input-path") { + + let input_path = PathBuf::from(input_path); + + builder_configuration.set_input_location(input_path); + } + + if let Some(output_path) = matches.get_one::("output-path") { + + let output_path = PathBuf::from(output_path); + + builder_configuration.set_output_location(output_path); + + } else { + + match builder_configuration.resource_type() { + CompilableResourceType::Dossier => { + + builder_configuration.set_output_location(builder_configuration.input_location().clone()); // could be a dir or a file + + if there_is_preview && builder_configuration.output_location().is_dir() { + + builder_configuration.set_output_location(builder_configuration.output_location().join(file_utility::build_output_file_name( + "nmd-dossier-preview", + Some(&builder_configuration.format().get_extension()) + ))); + } + }, + CompilableResourceType::File => { + + let mut output_path = builder_configuration.input_location().clone(); + + if output_path.is_dir() { + + output_path = output_path.join(file_utility::build_output_file_name( + output_path.file_stem().unwrap().to_string_lossy().to_string().as_str(), + Some(&builder_configuration.format().get_extension()) + )); + + } else { + + output_path = output_path.parent().unwrap().join(file_utility::build_output_file_name( + output_path.file_stem().unwrap().to_string_lossy().to_string().as_str(), + Some(&builder_configuration.format().get_extension()) + )); + } + + builder_configuration.set_output_location(output_path); + }, + CompilableResourceType::Unknown => (), + } + } + + // PREVIEW + let preview: Option>>; + let preview_start_handle: Option>>; + + if there_is_preview { + + let scraping_interval: Option; + + if let Some(interval) = matches.get_one::("preview-scraping-interval") { + + scraping_interval = Some(interval.clone().parse::()?); + + } else { + scraping_interval = None; + } + + let p = HtmlPreview::new(builder_configuration.output_location().clone(), scraping_interval); + + let p = Arc::new(TokioRwLock::new(p)); + + let handle = tokio::spawn({ + + let p = Arc::clone(&p); + + async move { + p.write().await.start().await + } + }); + + preview = Some(p); + preview_start_handle = Some(handle); + + } else { + + preview = None; + preview_start_handle = None; + } + + if let Some(theme) = matches.get_one::("theme") { + + let theme = Theme::from_str(theme)?; + + builder_configuration.set_theme(Some(theme)); + } + + // WATCHER + let watcher_time: u64; + + if let Some(wt) = matches.get_one::("watcher-time") { + + watcher_time = wt.parse::().unwrap(); + + } else { + watcher_time = MINIMUM_WATCHER_TIME; + } + + let watch: bool = matches.get_flag("watch"); + + builder_configuration.set_watching(Some(watch)); + + + // FAST DRAFT, FORCE, STYLEs + let fast_draft: bool = matches.get_flag("fast-draft"); + + builder_configuration.set_fast_draft(Some(fast_draft)); + + builder_configuration.set_force_output(Some(matches.get_flag("force-output"))); + + if let Some(styles) = matches.get_many::("style-file") { + builder_configuration.set_styles_raw_path(styles.map(|s| s.clone()).collect()); + } + + // PARALLELIZATION + if matches.get_flag("parallelization") { + builder_configuration.set_parallelization(Some(true)); + } + + // NUID + if matches.get_flag("nuid") { + builder_configuration.set_nuid(Some(true)); + } + + // IMAGEs + if matches.get_flag("embed-local-image") { + builder_configuration.set_embed_local_image(Some(true)); + } + + if matches.get_flag("embed-remote-image") { + builder_configuration.set_embed_remote_image(Some(true)); + } + + if matches.get_flag("compress-embed-image") { + builder_configuration.set_compress_embed_image(Some(true)); + } + + if matches.get_flag("strict-image-src-check") { + builder_configuration.set_strict_image_src_check(Some(true)); + } + + // DOCUMENT SUBSET (only if dossier) + if let Some(documents_subset) = matches.get_many::("documents-subset") { + + if documents_subset.len() < 1 { + return Err(NmdCliError::MoreThanOneValue("documents-subset".to_string())); + } + + let mut subset: HashSet = HashSet::new(); + for file_name in documents_subset { + subset.insert(file_name.clone()); + } + + builder_configuration.set_documents_subset_to_compile(Some(subset)); + } + + // wait preview startup + if let Some(handle) = preview_start_handle { + handle.await??; + } + + let builder_configuration = Arc::new(TokioRwLock::new(builder_configuration)); + + let build_handle: JoinHandle>; + + match builder_configuration.read().await.resource_type() { + CompilableResourceType::Dossier => { + + if watch { + build_handle = tokio::spawn({ + + let preview = preview.clone(); + let builder_configuration = builder_configuration.clone(); + + async move { + Builder::watch_compile_dossier(builder_configuration.read().await.deref().clone(), watcher_time, preview).await + } + }); + + } else { + + build_handle = tokio::spawn({ + + let builder_configuration = builder_configuration.clone(); + + async move { + + let mut dossier = Builder::load_dossier(builder_configuration.read().await.deref()).await?; + + builder_configuration.write().await.merge_dossier_configuration(dossier.configuration()); + + Builder::build_dossier(&mut dossier, builder_configuration.read().await.deref()).await + } + }); + + if let Some(p) = preview.clone() { + + tokio::spawn(async move { + p.write().await.render().await + }).await??; + } + } + + }, + CompilableResourceType::File => { + + if watch { + + build_handle = tokio::spawn({ + async move { + unimplemented!("watch compile file will be added in a next version") + } + }); + + } else { + + build_handle = tokio::spawn({ + + let builder_configuration = builder_configuration.clone(); + + async move { + + Builder::build_document(builder_configuration.read().await.deref()).await + } + }); + + if let Some(p) = preview.clone() { + + tokio::spawn(async move { + p.write().await.render().await + }).await??; + } + } + }, + + CompilableResourceType::Unknown => return Err(NmdCliError::ResourceError(ResourceError::InvalidResourceVerbose("resource is a dossier nor file".to_string()))), + } + + build_handle.await??; + + if let Some(preview) = preview { + preview.write().await.stop().await?; // need Ctrl + C to terminate + } + + Ok(()) + } + + async fn handle_generate_command(matches: &ArgMatches) -> Result<(), NmdCliError> { + match matches.subcommand() { + Some(("dossier", generate_dossier_matches)) => { + + let mut generator_configuration = GeneratorConfiguration::default(); + + if let Some(name) = generate_dossier_matches.get_one::("name") { + + generator_configuration.set_name(Some(name.clone())); + } + + if let Some(input_path) = generate_dossier_matches.get_one::("path") { + + let input_path = PathBuf::from(input_path); + + generator_configuration.set_path(input_path); + } + + let md_file_path: Option; + if let Some(md_fp) = generate_dossier_matches.get_one::("from-md") { + + md_file_path = Some(PathBuf::from(md_fp)); + + } else { + md_file_path = None; + } + + generator_configuration.set_force_generation(generate_dossier_matches.get_flag("force")); + generator_configuration.set_gitkeep(generate_dossier_matches.get_flag("gitkeep")); + generator_configuration.set_welcome(generate_dossier_matches.get_flag("welcome")); + + if let Some(md_file_path) = md_file_path { + + Generator::generate_dossier_from_markdown_file(&md_file_path, generator_configuration)?; + + } else { + + Generator::generate_dossier(generator_configuration)?; + } + + Ok(()) + }, + _ => unreachable!() + } + } + + async fn handle_dossier_command(matches: &ArgMatches) -> Result<(), NmdCliError> { + + let dossier_path = PathBuf::from(matches.get_one::("dossier-path").unwrap()); + + match matches.subcommand() { + Some(("add", add_dossier_matches)) => { + + if let Some(document_names) = add_dossier_matches.get_many::("document-name") { + + let dossier_manager_configuration = DossierManagerConfiguration::new(dossier_path); + + let dossier_manager = DossierManager::new(dossier_manager_configuration); + + for file_name in document_names { + dossier_manager.add_empty_document(&file_name)?; + } + + return Ok(()) + } + + Err(NmdCliError::TooFewArguments("dossier path".to_string())) + }, + + Some(("reset", reset_dossier_matches)) => { + + let dossier_manager_configuration = DossierManagerConfiguration::new(dossier_path.clone()); + + let dossier_manager = DossierManager::new(dossier_manager_configuration); + + dossier_manager.reset_dossier_configuration(dossier_path, !reset_dossier_matches.get_flag("no-preserve-documents"))?; + + Ok(()) + }, + + _ => unreachable!() + } + } + + async fn handle_analyze_command(matches: &ArgMatches) -> Result<(), NmdCliError> { + let mut builder_configuration = BuilderConfiguration::default(); + + builder_configuration.set_input_location(PathBuf::from(matches.get_one::("input-path").unwrap())); + + if matches.get_flag("nuid") { + builder_configuration.set_nuid(Some(true)); + } + + let json_output: String; + + if matches.get_flag("pretty") { + + match builder_configuration.resource_type() { + CompilableResourceType::Dossier => { + + let dossier = Builder::load_dossier(&builder_configuration).await?; + + json_output = serde_json::to_string_pretty(&dossier)?; + }, + + CompilableResourceType::File => { + let document = Builder::load_document(&builder_configuration).await?; + + json_output = serde_json::to_string_pretty(&document)?; + }, + + + CompilableResourceType::Unknown => { + log::error!("unknown resource"); + + return Err(NmdCliError::UnknownResource) + }, + } + + } else { + + match builder_configuration.resource_type() { + CompilableResourceType::Dossier => { + + let dossier = Builder::load_dossier(&builder_configuration).await?; + + json_output = serde_json::to_string(&dossier)?; + }, + + CompilableResourceType::File => { + + let document = Builder::load_document(&builder_configuration).await?; + + json_output = serde_json::to_string(&document)?; + }, + + + CompilableResourceType::Unknown => { + log::error!("unknown resource"); + + return Err(NmdCliError::UnknownResource) + }, + } + } + + stdout().write_all(json_output.as_bytes())?; + + Ok(()) + } + +} \ No newline at end of file diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..be618f0 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,5 @@ + +/// NMD CLI version +pub const VERSION: &str = "1.3.0"; + +pub const MINIMUM_WATCHER_TIME: u64 = 5; \ No newline at end of file diff --git a/src/dossier_manager.rs b/src/dossier_manager.rs new file mode 100644 index 0000000..66b1672 --- /dev/null +++ b/src/dossier_manager.rs @@ -0,0 +1,95 @@ +pub mod dossier_manager_configuration; + + +use std::{io, path::PathBuf}; +use nmd_core::{constants::{DOSSIER_CONFIGURATION_YAML_FILE_NAME, NMD_EXTENSION}, dossier::dossier_configuration::DossierConfiguration, resource::ResourceError, utility::file_utility}; +use thiserror::Error; +use self::dossier_manager_configuration::DossierManagerConfiguration; + +#[derive(Error, Debug)] +pub enum DossierManagerError { + #[error(transparent)] + ResourceError(#[from] ResourceError), + + #[error(transparent)] + IoError(#[from] io::Error), + + #[error(transparent)] + SerdeYamlError(#[from] serde_yaml::Error) + +} + +/// Dossier manager, it can be used to add documents to a dossier +#[derive(Debug)] +pub struct DossierManager { + configuration: DossierManagerConfiguration +} + +impl DossierManager { + pub fn new(configuration: DossierManagerConfiguration) -> Self { + + log::debug!("new DossierManager using configuration: \n{:#?}", configuration); + + Self { + configuration + } + } + + /// Add document to dossier. Create file if it doesn't exist. + pub fn add_document(&self, filename: &str, content: &str) -> Result<(), DossierManagerError> { + + let mut filename = String::from(filename); + + if filename.ends_with(NMD_EXTENSION) { + for _ in 0..NMD_EXTENSION.len() + 1 { + + filename.remove(filename.len() - 1); + } + } + + let filename = file_utility::build_output_file_name(&filename, Some(NMD_EXTENSION)); + + let mut dossier_configuration = DossierConfiguration::try_from(self.configuration.dossier_path())?; + + let abs_file_path = self.configuration.dossier_path().clone().join(&filename); + let rel_file_path = format!(r"./{}", filename); + + file_utility::create_file_with_content(&abs_file_path, content)?; + + log::info!("created document: '{}'", filename); + + dossier_configuration.append_raw_document_path(rel_file_path); + + dossier_configuration.dump_as_yaml(self.configuration.dossier_path().clone().join(DOSSIER_CONFIGURATION_YAML_FILE_NAME))?; + + Ok(()) + } + + pub fn add_empty_document(&self, filename: &String) -> Result<(), DossierManagerError> { + self.add_document(filename, "") + } + + pub fn reset_dossier_configuration(&self, dossier_path: PathBuf, preserve_documents_list: bool) -> Result<(), DossierManagerError> { + + log::info!("resetting dossier configuration..."); + + let mut dc: DossierConfiguration = DossierConfiguration::default(); + + if preserve_documents_list { + + let ex_dc = DossierConfiguration::try_from(&dossier_path)?; + + dc.set_raw_documents_paths(ex_dc.raw_documents_paths().clone()); + log::info!("documents list will be preserved") + } + + file_utility::create_file_with_content( + &dossier_path.join(DOSSIER_CONFIGURATION_YAML_FILE_NAME), + &serde_yaml::to_string(&dc)? + )?; + + log::info!("reset done"); + + Ok(()) + } +} \ No newline at end of file diff --git a/src/dossier_manager/dossier_manager_configuration.rs b/src/dossier_manager/dossier_manager_configuration.rs new file mode 100644 index 0000000..47a2794 --- /dev/null +++ b/src/dossier_manager/dossier_manager_configuration.rs @@ -0,0 +1,26 @@ +use std::path::PathBuf; + +use getset::{Getters, Setters}; + +#[derive(Debug, Clone, Getters, Setters)] +pub struct DossierManagerConfiguration { + + #[getset(get = "pub", set = "pub")] + dossier_path: PathBuf +} + +impl DossierManagerConfiguration { + pub fn new(dossier_path: PathBuf) -> Self { + Self { + dossier_path + } + } +} + +impl Default for DossierManagerConfiguration { + fn default() -> Self { + Self { + dossier_path: PathBuf::from(".") + } + } +} \ No newline at end of file diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..d9d9a09 --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,185 @@ +pub mod generator_configuration; + + +use std::{collections::HashMap, fs, path::PathBuf}; +use nmd_core::{codex::modifier::constants::NEW_LINE, constants::DOSSIER_CONFIGURATION_YAML_FILE_NAME, dossier::{self, dossier_configuration::DossierConfiguration}, resource::{disk_resource::DiskResource, Resource, ResourceError}, utility::file_utility::{self, read_file_content}}; +use once_cell::sync::Lazy; +use regex::Regex; +use crate::dossier_manager::{dossier_manager_configuration::DossierManagerConfiguration, DossierManager}; + +use self::generator_configuration::GeneratorConfiguration; + + +pub const WELCOME_FILE_NAME: &str = "welcome.nmd"; + +static SEARCH_HEADING_1_REGEX: Lazy = Lazy::new(|| Regex::new(r"(?:^#[^#][ ]*(.+))").unwrap()); + + +pub struct Generator { +} + +impl Generator { + + /// Generate a new dossier based on GeneratorConfiguration + pub fn generate_dossier(configuration: GeneratorConfiguration) -> Result { + + if configuration.path().exists() { + + if !configuration.path().is_dir() { + return Err(ResourceError::InvalidResourceVerbose("not a directory".to_string())) + } + + if let Ok(entries) = fs::read_dir(&configuration.path()) { + if entries.count() != 0 && !configuration.force_generation() { + return Err(ResourceError::InvalidResourceVerbose("directory not empty, try to use force option".to_string())) + } + } else { + return Err(ResourceError::ReadError("check permission".to_string())) + } + + if configuration.force_generation() { + fs::remove_dir_all(configuration.path().clone())?; + log::info!("cleared {}", configuration.path().to_string_lossy()); + + if let Err(err) = fs::create_dir_all(&configuration.path()) { + return Err(ResourceError::ReadError(err.to_string())) + } + + log::info!("created dossier directory"); + } + + } else { + if !configuration.force_generation() { + + log::warn!("consider to use force flag"); + + return Err(ResourceError::ResourceNotFound(format!("{}", configuration.path().to_string_lossy()))) + } + + if let Err(err) = fs::create_dir_all(&configuration.path()) { + return Err(ResourceError::ReadError(err.to_string())) + } + + log::info!("created dossier directory"); + } + + let assets_path = configuration.path().join(dossier::ASSETS_DIR); + + file_utility::create_directory(&assets_path)?; + log::info!("added {}/ directory", dossier::ASSETS_DIR); + + file_utility::create_directory(&assets_path.join(dossier::IMAGES_DIR))?; + log::info!("added {}/{} directory", dossier::ASSETS_DIR, dossier::IMAGES_DIR); + + file_utility::create_directory(&assets_path.join(dossier::DOCUMENTS_DIR))?; + log::info!("added {}/{} directory", dossier::ASSETS_DIR, dossier::DOCUMENTS_DIR); + + file_utility::create_directory(&assets_path.join(dossier::STYLES_DIR))?; + log::info!("added {}/{} directory", dossier::ASSETS_DIR, dossier::STYLES_DIR); + + if configuration.gitkeep() { + + file_utility::create_empty_file(&assets_path.join("images").join(".gitkeep"))?; + file_utility::create_empty_file(&assets_path.join("documents").join(".gitkeep"))?; + file_utility::create_empty_file(&assets_path.join("styles").join(".gitkeep"))?; + + log::info!("added .gitkeep files"); + } + + let mut dossier_configuration: DossierConfiguration; + + if !configuration.evaluate_existing_files() { + + dossier_configuration = DossierConfiguration::default(); + + } else { + + dossier_configuration = DossierConfiguration::default().with_files_in_dir(configuration.path())?; + } + + if let Some(name) = configuration.name() { + dossier_configuration.set_name(name.clone()); + } + + if configuration.welcome() { + + let mut welcome_document = DiskResource::try_from(configuration.path().join(WELCOME_FILE_NAME))?; + + welcome_document.write("Welcome in **NMD**!")?; + + log::info!("added welcome page"); + + let mut path = "./".to_string(); + path.push_str(WELCOME_FILE_NAME); + + dossier_configuration.set_raw_documents_paths(vec![path]); + } + + dossier_configuration.dump_as_yaml(configuration.path().join(DOSSIER_CONFIGURATION_YAML_FILE_NAME))?; + + log::info!("added dossier configuration file: '{}'", DOSSIER_CONFIGURATION_YAML_FILE_NAME); + + Ok(dossier_configuration) + } + + pub fn generate_dossier_from_markdown_file(markdown_source_file_path: &PathBuf, configuration: GeneratorConfiguration) -> Result { + let markdown_file_content = read_file_content(markdown_source_file_path)?; + + let dossier_path = configuration.path().clone(); + let dossier_configuration = Self::generate_dossier(configuration)?; + + let dossier_manager = DossierManager::new(DossierManagerConfiguration::new(dossier_path)); + + let mut current_nmd_file_content = String::new(); + let mut current_nmd_file_name: Option = None; + + let mut document_names: HashMap = HashMap::new(); + + let mut in_code_block = false; + + for line in markdown_file_content.lines() { + + if line.starts_with("```") { + in_code_block = !in_code_block; + } + + if !in_code_block && SEARCH_HEADING_1_REGEX.is_match(line) { + + log::info!("new header 1 found, generating new document..."); + + if let Some(ref file_name) = current_nmd_file_name { + + let mut file_name = String::from(file_name); + + let n = *document_names.get(&file_name).unwrap_or(&0) + 1; + document_names.insert(file_name.clone(), n); + + if let Some(n) = document_names.get(&file_name) { + + let n = *n; + + if n > 1 { + file_name = format!("{}-{}", file_name, n); + } + + } else { + unreachable!(); + } + + dossier_manager.add_document(&file_name, ¤t_nmd_file_content).unwrap(); + current_nmd_file_content.clear(); + } + + current_nmd_file_name = Some(String::from(SEARCH_HEADING_1_REGEX.captures(line).unwrap().get(1).unwrap().as_str())); + + } + + current_nmd_file_content.push_str(&format!("{}{}", line, NEW_LINE)); + + } + + + Ok(dossier_configuration) + } +} + diff --git a/src/generator/generator_configuration.rs b/src/generator/generator_configuration.rs new file mode 100644 index 0000000..c97a27c --- /dev/null +++ b/src/generator/generator_configuration.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; + +use getset::{CopyGetters, Getters, Setters}; + + +#[derive(Debug, Clone, Getters, CopyGetters, Setters)] +pub struct GeneratorConfiguration { + + #[getset(get = "pub", set = "pub")] + name: Option, + + #[getset(get = "pub", set = "pub")] + path: PathBuf, + + #[getset(get_copy = "pub", set = "pub")] + force_generation: bool, + + #[getset(get_copy = "pub", set = "pub")] + welcome: bool, + + #[getset(get_copy = "pub", set = "pub")] + gitkeep: bool, + + #[getset(get_copy = "pub", set = "pub")] + evaluate_existing_files: bool, +} + + +impl GeneratorConfiguration { + pub fn new(name: Option, path: PathBuf, force_generation: bool, welcome: bool, gitkeep: bool, evaluate_existing_files: bool) -> Self { + Self { + name, + path, + force_generation, + welcome, + gitkeep, + evaluate_existing_files, + } + } + +} + +impl Default for GeneratorConfiguration { + fn default() -> Self { + Self { + name: None, + path: Default::default(), + force_generation: false, + welcome: false, + gitkeep: false, + evaluate_existing_files: true + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f242f18 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,20 @@ +pub mod cli; +pub mod preview; +pub mod watcher; +pub mod dossier_manager; +pub mod generator; +pub mod builder; +pub mod constants; + + +use cli::{NmdCli, NmdCliError}; +use tokio; + + +#[tokio::main] +async fn main() -> Result<(), NmdCliError> { + + let cli = NmdCli::new(); + + cli.serve().await +} \ No newline at end of file diff --git a/src/preview.rs b/src/preview.rs new file mode 100644 index 0000000..aeae76f --- /dev/null +++ b/src/preview.rs @@ -0,0 +1,31 @@ +use html_preview::HtmlPreviewError; +use thiserror::Error; +use tokio::task::JoinError; + +pub mod html_preview; + + +#[derive(Error, Debug)] +pub enum PreviewError { + + #[error(transparent)] + IoError(#[from] std::io::Error), + + #[error(transparent)] + JoinError(#[from] JoinError), + + #[error(transparent)] + HtmlPreviewError(#[from] HtmlPreviewError), +} + + +pub trait Preview { + + fn start(&mut self) -> impl std::future::Future> + Send; + + fn render(&mut self) -> impl std::future::Future> + Send; + + fn update(&mut self) -> impl std::future::Future> + Send; + + fn stop(&mut self) -> impl std::future::Future> + Send; +} \ No newline at end of file diff --git a/src/preview/check_preview_updates.js b/src/preview/check_preview_updates.js new file mode 100644 index 0000000..f441241 --- /dev/null +++ b/src/preview/check_preview_updates.js @@ -0,0 +1,65 @@ +const url = 'http://127.0.0.1:1234/preview-state-info'; +const MIN_SCRAPE_INTERVAL = 1000; + + +var scrapeInterval = MIN_SCRAPE_INTERVAL; + +var interval = null; + +function stopScraping() { + if (!!interval) { + clearInterval(interval); + interval = null; + } + +} + +function startScraping() { + + stopScraping(); + interval = setInterval(checkPreviewUpdates, scrapeInterval); +} + +async function checkPreviewUpdates() { + + console.log("checking preview updates..."); + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error('network response was not ok'); + } + + const data = await response.json(); + const lastUpdateTimestamp = !data.last_update_timestamp ? null : new Date(data.last_update_timestamp); + const lastSeenTimestamp = !data.last_seen_timestamp ? null : new Date(data.last_seen_timestamp); + + if (!!data.scrape_interval && data.scrape_interval != scrapeInterval) { + + console.log(`new scrape interval found (before: ${scrapeInterval})`); + + scrapeInterval = Math.max(data.scrape_interval, MIN_SCRAPE_INTERVAL); + + console.log(`new scrape interval: ${scrapeInterval}`); + + stopScraping(); + startScraping(); + } + + console.log("last update timestamp: " + lastUpdateTimestamp); + console.log("last seen timestamp: " + lastSeenTimestamp); + + if (lastUpdateTimestamp !== null && lastUpdateTimestamp >= lastSeenTimestamp) { + + console.log("new preview found!"); + console.log("reloading..."); + + window.location.reload(); + } + } catch (error) { + console.error('error occurs during check preview updates:', error); + } +} + + +startScraping(); \ No newline at end of file diff --git a/src/preview/html_preview.rs b/src/preview/html_preview.rs new file mode 100644 index 0000000..ad937b7 --- /dev/null +++ b/src/preview/html_preview.rs @@ -0,0 +1,190 @@ +use std::{path::PathBuf, sync::RwLock}; +use chrono::{DateTime, Local}; +use getset::{Getters, Setters}; +use once_cell::sync::Lazy; +use serde::Serialize; +use thiserror::Error; +use tokio::{fs::File, io::AsyncReadExt, task::JoinHandle}; +use warp::Filter; + +use super::{Preview, PreviewError}; + +pub const PREVIEW_STATE_INFO_ROUTE: &str = "preview-state-info"; +const DEFAULT_SCRAPE_INTERVAL: u32 = 2000; + + +pub static LAST_UPDATE: Lazy>>> = Lazy::new(|| RwLock::new(None)); +pub static LAST_SEEN: Lazy>>> = Lazy::new(|| RwLock::new(None)); + + +#[derive(Debug, Serialize)] +struct PreviewStateInfo { + last_update_timestamp: Option, + last_seen_timestamp: Option, + scrape_interval: Option, +} + + +#[derive(Error, Debug)] +pub enum HtmlPreviewError { +} + + +pub const PREVIEW_PORT: u16 = 1234; +pub const PREVIEW_URL: &str = "http://127.0.0.1:1234"; // change if PREVIEW_PORT changes + + +#[derive(Debug, Getters, Setters)] +pub struct HtmlPreview { + + #[getset(get = "pub", set = "pub")] + src: PathBuf, + + server_thread_handle: Option>, + + client_preview_scraping_interval: u32, +} + +impl HtmlPreview { + pub fn new(src: PathBuf, client_preview_scraping_interval: Option) -> Self { + Self { + src, + server_thread_handle: None, + client_preview_scraping_interval: client_preview_scraping_interval.unwrap_or(DEFAULT_SCRAPE_INTERVAL) + } + } +} + +impl Preview for HtmlPreview { + + async fn start(&mut self) -> Result<(), PreviewError> { + + let src = self.src.clone(); + + let original_log_max_level = log::max_level(); + + log::set_max_level(log::LevelFilter::Warn); + + let client_preview_scraping_interval = self.client_preview_scraping_interval; + + self.server_thread_handle = Some(tokio::spawn(async move { + + let show_preview = move || { + let src = src.clone(); + + log::info!("serving preview..."); + + serve_preview(src) + }; + + let preview_route_implicite = warp::path::end() + .and_then(show_preview.clone()); + + let preview_route_explicit = warp::path!("preview") + .and_then(show_preview); + + let preview_state_info_route = warp::path(PREVIEW_STATE_INFO_ROUTE) + .map(move || { + + let last_update_timestamp: Option; + let last_seen_timestamp: Option; + + if let Some(l) = *LAST_UPDATE.read().unwrap() { + + last_update_timestamp = Some(l.timestamp()); + + } else { + + last_update_timestamp = None; + } + + if let Some(l) = *LAST_SEEN.read().unwrap() { + + last_seen_timestamp = Some(l.timestamp()); + + } else { + + last_seen_timestamp = None; + } + + let response = PreviewStateInfo { + last_update_timestamp, + last_seen_timestamp, + scrape_interval: Some(client_preview_scraping_interval) + }; + + let now = chrono::offset::Local::now(); + + log::debug!("html preview seen (new last seen: {})", now); + + *LAST_SEEN.write().unwrap() = Some(now); + + warp::reply::json(&response) + }); + + log::info!("html preview will be running on: {}", PREVIEW_URL); + + warp::serve( + preview_route_implicite + .or(preview_route_explicit) + .or(preview_state_info_route) + ) + .run(([127, 0, 0, 1], PREVIEW_PORT)) + .await + })); + + log::set_max_level(original_log_max_level); + + Ok(()) + } + + async fn render(&mut self) -> Result<(), PreviewError> { + + log::info!("html preview rendered"); + + Ok(()) + } + + async fn update(&mut self) -> Result<(), PreviewError> { + + let now = chrono::offset::Local::now(); + + *LAST_UPDATE.write().unwrap() = Some(now); + + log::info!("html preview updated (new last update: {})", now); + + Ok(()) + } + + async fn stop(&mut self) -> Result<(), PreviewError> { + + if let Some(j) = self.server_thread_handle.take() { + j.await?; // need Ctrl + C to terminate + } + + log::info!("html preview stop"); + + Ok(()) + } +} + +async fn serve_preview(file_path: PathBuf) -> Result { + + let mut file = File::open(file_path.clone()).await.map_err(|err| { + + log::error!("error occurs during preview file opening: {} ({:?})", err.to_string(), file_path); + + warp::reject() + })?; + + let mut contents = String::new(); + + file.read_to_string(&mut contents).await.map_err(|err| { + + log::error!("error occurs during preview file reading: {} ({:?})", err.to_string(), file_path); + + warp::reject() + })?; + + Ok(warp::reply::html(contents)) +} \ No newline at end of file diff --git a/src/watcher.rs b/src/watcher.rs new file mode 100644 index 0000000..7463d0b --- /dev/null +++ b/src/watcher.rs @@ -0,0 +1,152 @@ +use std::{collections::HashSet, future::Future, path::PathBuf, pin::Pin, sync::mpsc::RecvError, time::SystemTime}; + +use getset::{Getters, Setters}; +use notify::{Error, Event, RecursiveMode, Watcher}; +use thiserror::Error; +use tokio::{sync::mpsc::Receiver, task::{JoinError, JoinHandle}}; + +use super::preview::PreviewError; + + +#[derive(Error, Debug)] +pub enum WatcherError { + + #[error(transparent)] + WatcherError(#[from] notify::Error), + + #[error(transparent)] + ChannelError(#[from] RecvError), + + #[error(transparent)] + PreviewError(#[from] PreviewError), + + #[error("elaboration error: {0}")] + ElaborationError(String), + + #[error(transparent)] + JoinError(#[from] JoinError), + + #[error("send error")] + SendError, +} + +pub type CheckIfElaborateFn<'a> = Box Pin> + Send>> + Send + Sync + 'a>; +pub type OnStartFn<'a> = Box Pin> + Send>> + Send + Sync + 'a>; +pub type ElaborateFn<'a> = Box) -> Pin> + Send>> + Send + Sync + 'a>; + + +#[derive(Getters, Setters)] +pub struct NmdWatcher<'a> { + + rx: Receiver>, + + on_start_fn: OnStartFn<'a>, + + check_if_elaborate_fn: CheckIfElaborateFn<'a>, + + check_if_elaborate_skipping_timeout_fn: CheckIfElaborateFn<'a>, + + elaborate_fn: ElaborateFn<'a>, + + min_elapsed_time_between_events_in_secs: u64, +} + +impl<'a> NmdWatcher<'a> { + + pub async fn new(min_elapsed_time_between_events_in_secs: u64, input_path: &PathBuf, on_start_fn: OnStartFn<'a>, check_if_elaborate_skipping_timeout_fn: CheckIfElaborateFn<'a>, check_if_elaborate_fn: CheckIfElaborateFn<'a>, elaborate_fn: ElaborateFn<'a>) -> Result { + + let (tx, rx) = tokio::sync::mpsc::channel(4096); + + let input_path = input_path.clone(); + + let _: JoinHandle> = tokio::spawn(async move { + let (notify_tx, notify_rx) = std::sync::mpsc::channel(); + + let mut watcher = notify::recommended_watcher(move |res| { + + notify_tx.send(res).unwrap_or_else(|val| { + log::error!("error occurs during watching: {}", val); + }); + })?; + + watcher.watch(&input_path, RecursiveMode::Recursive)?; + + while let Ok(event) = notify_rx.recv() { + if let Err(_) = tx.send(event).await { + return Err(WatcherError::SendError) + } + } + + Ok(()) + }); + + let s = Self { + min_elapsed_time_between_events_in_secs, + rx, + on_start_fn, + check_if_elaborate_fn, + check_if_elaborate_skipping_timeout_fn, + elaborate_fn + }; + + Ok(s) + } + + pub async fn start(&mut self) -> Result<(), WatcherError> { + + let mut last_event_time = SystemTime::now(); + + let mut paths_change_detection_from_last_elaboration: HashSet = HashSet::new(); + + (self.on_start_fn)().await?; + + loop { + + if let Ok(recv_res) = self.rx.try_recv() { + + match recv_res { + Ok(event) => { + log::debug!("new event from watcher: {:?}", event); + log::debug!("change detected on file(s): {:?}", event.paths); + + event.clone().paths.iter().for_each(|path| { + paths_change_detection_from_last_elaboration.insert(path.clone()); + }); + + if (self.check_if_elaborate_skipping_timeout_fn)(event.clone()).await? { + + (self.elaborate_fn)(paths_change_detection_from_last_elaboration.clone()).await?; + + continue; + } + + let event_time = SystemTime::now(); + + let elapsed_time = event_time.duration_since(last_event_time).unwrap(); + + if elapsed_time.as_secs() < self.min_elapsed_time_between_events_in_secs { + log::info!("change detected, but minimum elapsed time not satisfied ({}/{} s)", elapsed_time.as_secs(), self.min_elapsed_time_between_events_in_secs); + + continue; + } + + if (self.check_if_elaborate_fn)(event).await? { + (self.elaborate_fn)(paths_change_detection_from_last_elaboration.clone()).await?; + + last_event_time = event_time; + + continue; + } + }, + Err(err) => { + log::error!("error: {}", err.to_string()); + }, + } + + } else { + + tokio::task::yield_now().await; + } + } + } +} \ No newline at end of file diff --git a/test-resources/.gitignore b/test-resources/.gitignore new file mode 100644 index 0000000..f782e0e --- /dev/null +++ b/test-resources/.gitignore @@ -0,0 +1,2 @@ +from-md.md +nmd-test-dossier-from-md/* diff --git a/test-resources/nmd-test-dossier-1/.gitignore b/test-resources/nmd-test-dossier-1/.gitignore new file mode 100644 index 0000000..0b84df0 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/.gitignore @@ -0,0 +1 @@ +*.html \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-1/assets/images/jpg.jpg b/test-resources/nmd-test-dossier-1/assets/images/jpg.jpg new file mode 100644 index 0000000..498c749 Binary files /dev/null and b/test-resources/nmd-test-dossier-1/assets/images/jpg.jpg differ diff --git a/test-resources/nmd-test-dossier-1/assets/images/wikipedia-logo.png b/test-resources/nmd-test-dossier-1/assets/images/wikipedia-logo.png new file mode 100644 index 0000000..b46cefb Binary files /dev/null and b/test-resources/nmd-test-dossier-1/assets/images/wikipedia-logo.png differ diff --git a/test-resources/nmd-test-dossier-1/assets/styles/custom.css b/test-resources/nmd-test-dossier-1/assets/styles/custom.css new file mode 100644 index 0000000..cd38089 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/assets/styles/custom.css @@ -0,0 +1,5 @@ + + +.red { + color: red; +} \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-1/d0.nmd b/test-resources/nmd-test-dossier-1/d0.nmd new file mode 100644 index 0000000..212ac12 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d0.nmd @@ -0,0 +1 @@ +[abridged style]{;;Garamond} text [abridged style]{blue;gray} \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-1/d1.nmd b/test-resources/nmd-test-dossier-1/d1.nmd new file mode 100644 index 0000000..a9f187b --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d1.nmd @@ -0,0 +1,191 @@ + +preamble + + +# title 1 + + +paragraph 1 + +TODO + +**bold text**, __bold text__, _italic text_, *italic text*, ~~strikethrough~~, ++underlined++ + +this is a *nested italic, ++underlined, **bold**++ text*. + +emoji: :alien: + +^superscript^ normal text + +~subscript~ normal text + +> Simple quote + +text between quote + +|table heading 1|table heading 2| +|---|---| +|table body 1| table body 2| +|---| +||table footer| +[caption] + +> Single line quote. + +> Simple +> multiline +> quote + +> Simple +> multiline +> quote +> +> with 2 paragraphs + +### block quotes + +> [!NOTE] +> Simple +> multiline +> quote + +> [!WARNING] +> Simple +> multiline +> quote +> +> with 2 paragraphs. + + +==highlight== + +## title 2 + +paragraph 2a. + +--- + + +not line break --- + +paragraph 2b. + +### Title 3 + +paragraph +2c. +multi +line +. + +#### Title 4 + +Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, +molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum +numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium +optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis +obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam +nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, +tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, +quia. Quo neque error repudiandae fuga? Ipsa laudantium molestias eos +sapiente officiis modi at sunt excepturi expedita sint? Sed quibusdam +recusandae alias error harum maxime adipisci amet laborum. Perspiciatis +minima nesciunt dolorem! Officiis iure rerum voluptates a cumque velit + +Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, +molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum +numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium +optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis +obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam +nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, +tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, +quia. Quo neque error repudiandae fuga? Ipsa laudantium molestias eos +sapiente officiis modi at sunt excepturi expedita sint? Sed quibusdam +recusandae alias error harum maxime adipisci amet laborum. Perspiciatis +minima nesciunt dolorem! Officiis iure rerum voluptates a cumque velit + +Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, +molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum +numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium +optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis +obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam +nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, + +@[todo](this is a todo) + +tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, +quia. Quo neque error repudiandae fuga? Ipsa laudantium molestias eos +sapiente officiis modi at sunt excepturi expedita sint? Sed quibusdam +recusandae alias error harum maxime adipisci amet laborum. Perspiciatis +minima nesciunt dolorem! Officiis iure rerum voluptates a cumque velit + +Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, +molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum + +::: tip +You should use NMD!!! +::: + +numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium +optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis +obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam +nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, +tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, +quia. Quo neque error repudiandae fuga? Ipsa laudantium molestias eos +sapiente officiis modi at sunt excepturi expedita sint? Sed quibusdam + +::: warning +Before read the doc! +::: + +sapiente officiis modi at sunt excepturi expedita sint? Sed quibusdam +recusandae alias error harum maxime adipisci amet laborum. Perspiciatis +minima nesciunt dolorem! Officiis iure rerum voluptates a cumque velit + +# title 1 + +::: quote +new +warning + +multiline +::: + +> [!TIP] +> +> new quote + +::: tip +new +warning + +multiline +::: + +::: note +new +warning + +multiline +::: + +::: important +new +warning + +multiline +::: + +::: warning +new +warning + +multiline +::: + +::: caution +new +warning + +multiline +::: \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-1/d10.nmd b/test-resources/nmd-test-dossier-1/d10.nmd new file mode 100644 index 0000000..c4cd1bf --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d10.nmd @@ -0,0 +1,15 @@ +# title +@key value +@key +@style color:red +@class class + +%aphib% + +this is \** a test to show star ** issue **. + +*test +test* + +&ref1& + diff --git a/test-resources/nmd-test-dossier-1/d11.nmd b/test-resources/nmd-test-dossier-1/d11.nmd new file mode 100644 index 0000000..ed51a27 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d11.nmd @@ -0,0 +1,23 @@ + + + +| Syntax3 :| Description |: Test Text 3 :| +| :--- | :----: | ---: | +| Header :| Title 3 | Here's this | +| Paragraph3 | Text :| And more | +|---| +|| Footer | +[Caption]#table-id{{color:red;}} + +| Syntax | Description | Test Text | +| :--- | :----: | ---: | +| Header | Title | Here's this | +| Paragraph | Text | And more | + +| Syntax2 | Description2 | Test Text2 | +| :--- | :----: | ---: | +| Header2 | Title2 | Here's this2 | +| Paragraph 2 | Text 2 | And more 2 | + + + diff --git a/test-resources/nmd-test-dossier-1/d2.nmd b/test-resources/nmd-test-dossier-1/d2.nmd new file mode 100644 index 0000000..064150a --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d2.nmd @@ -0,0 +1,48 @@ +# image + +!!:space-between:[[ +:start:![(wikipedia-logo.png)]#image-7{{width:70%}} +![Wikipedia is **awesome**](./assets/images/wikipedia-logo.png){{width:45%;margin:0;heigh:25vh;}} +]] + + +![(wikipedia-logo.png)]#image-7{{width:70%}} + +![Wikipedia](./assets/images/wikipedia-logo.png){{width:45%;margin:0;}} + + + +![Wikipedia](./assets/images/wikipedia-logo.png) + +![Infer Wikipedia image](wikipedia-logo.png) + +![JPG image](jpg.jpg) + + + +![(./assets/images/wikipedia-logo.png)] + +![(wikipedia-logo.png)] + +![(jpg.jpg)] + + + +![Wikipedia]#image-1(./assets/images/wikipedia-logo.png) + +![Infer Wikipedia image]#image-2(wikipedia-logo.png) + +![JPG image]#image-3(jpg.jpg) + + + +![(./assets/images/wikipedia-logo.png)]#image-4 + +![(wikipedia-logo.png)]#image-5 + +![(jpg.jpg)]#image-6 + + +![external image](https://en.wikipedia.org/static/images/icons/wikipedia.png) + +![(https://en.wikipedia.org/static/images/icons/wikipedia.png)] \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-1/d3.nmd b/test-resources/nmd-test-dossier-1/d3.nmd new file mode 100644 index 0000000..1b564c9 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d3.nmd @@ -0,0 +1,9 @@ +# code chapter + +```python + +print("python") + +``` + +`console.log("javascript")` \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-1/d4.nmd b/test-resources/nmd-test-dossier-1/d4.nmd new file mode 100644 index 0000000..36fc178 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d4.nmd @@ -0,0 +1,9 @@ + +# math + +When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\). + + +$$ +\int_0^{+\infty} \, x^2 \; dx +$$ diff --git a/test-resources/nmd-test-dossier-1/d5.nmd b/test-resources/nmd-test-dossier-1/d5.nmd new file mode 100644 index 0000000..584aa7e --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d5.nmd @@ -0,0 +1,68 @@ +# List + +- list 1 element 1 +- list 1 element 2 + - element 2.1 + - element 2.1.1a + | element 2.1.1b + - element 2.1.2 + - element 2.1.3 + - element 2.2 + -> element 2.3 +* list 1 element 3 ++ element 4 +5. element 5 +6) element 6 +-[] todo1 +-[ ] todo2 +-[x] todo3 +-[X] todo4 + +text between two list + +- list 2 element 1 +- list 2 element 2 + - element 2.1 + - element 2.1.1a + | element 2.1.1b + - element 2.1.2 + - element 2.1.3 + - element 2.2 + -> element 2.3 +* list 2 element 3 ++ element 4 +5. element 5 +6) element 6 +-[] todo1 +-[ ] todo2 +-[x] todo3 +-[X] todo4 + +text between two list + +- element in other list + +text. that must not be a list +text +text + +text +text +text. that must not be a list +text +text + +text +text +text. that must not be a list + +- list +- with +| element on 2 lines + + +- list with 2 items and inner image + +![(https://en.wikipedia.org/static/images/icons/wikipedia.png)] + +- other item \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-1/d6.nmd b/test-resources/nmd-test-dossier-1/d6.nmd new file mode 100644 index 0000000..7307957 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d6.nmd @@ -0,0 +1,45 @@ + +# Style + +[embedded style2]{{color:red; +font-size:2em;}} text [**abridged** style]{red;gray;Arial} text [abridged style]{red;gray;Arial} text [embedded style2]{{color:red; +font-size:2em;}} + +[abridged style]{green;gray;Garamond} + +[abridged style]{;;Garamond} text [abridged style]{blue;gray} + +[abridged style]{blue;gray} text [abridged style]{blue;gray} + +[abridged style]{;gray} text without style [abridged style]{;red} + +[embedded style]{{color:red;font-size:2em;}} text without style [embedded style2]{{color:red; +font-size:2em;}} + +[[embedded style + +multiline]]{{color:red; +font-size:2em;}} + +text + +[[embedded style + +multiline]]{{ + .red +}} + +[[embedded style + +| Syntax3 :| Description |: Test Text 3 :| +| :--- | :----: | ---: | +| Header :| Title 3 | Here's this | +| Paragraph3 | Text :| And more | +|---| +|| Footer | +[Caption]#table-id{{color:red;}} + + +multiline]]{{ + color: aqua; +}} \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-1/d7.nmd b/test-resources/nmd-test-dossier-1/d7.nmd new file mode 100644 index 0000000..04f9280 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d7.nmd @@ -0,0 +1,24 @@ +# bookmark + +@[todo](this is a todo) text @[TODO](this is a todo) + +text between +todo + +TODO: abridged todo + +@[abridged bookmark] text @[abridged bookmark]#the-id + +text +between +todo + +@[bookmark](this is a bookmark) text @[bookmark]#the-id(this is a bookmark) + +# TODO + + +TODO: +this is a multiline +todo +:TODO \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-1/d8.nmd b/test-resources/nmd-test-dossier-1/d8.nmd new file mode 100644 index 0000000..ff96195 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d8.nmd @@ -0,0 +1,14 @@ +# title for link + + + +Link to [title](d8.nmd#title-for-link) + +Link to [title (infer)](#title-for-link) + +Link to other [document chapter (image)](d2.nmd#image) + +Link to [url](https://github.com/nricciardi/nmd) + +Link to [url with #](https://github.com/nricciardi/nmd?tab=readme-ov-file#nmd-syntax) + diff --git a/test-resources/nmd-test-dossier-1/d9.nmd b/test-resources/nmd-test-dossier-1/d9.nmd new file mode 100644 index 0000000..68c74a5 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/d9.nmd @@ -0,0 +1,22 @@ + +#- title inferred as 1 + +# title 1 + +#+ title 2 + +#+ title 3 + +#- title 2 + + +### title 3 + +#= title 3 + +#+ title 4 + +#- title 3 + +#- title 2 + diff --git a/test-resources/nmd-test-dossier-1/nmd copy.yml b/test-resources/nmd-test-dossier-1/nmd copy.yml new file mode 100644 index 0000000..53d9dd2 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/nmd copy.yml @@ -0,0 +1,76 @@ +name: new-dossier +documents: + - "./d1.nmd" + - "./d2.nmd" + - "./d3.nmd" + - "./d4.nmd" + - "./d5.nmd" + - "./d6.nmd" + - "./d7.nmd" + - ./d8.nmd + - ./d9.nmd + - ./d10.nmd + - ./d11.nmd +style: + theme: Light + styles: + - custom.css + list_bullets_configuration: + - from: '|' + to: '‍' + indentation_level: 0 + strict_indentation: false + - from: '-' + to: '•' + indentation_level: 0 + strict_indentation: true + - from: '-' + to: '◦' + indentation_level: 1 + strict_indentation: true + - from: '-' + to: '–' + indentation_level: 2 + strict_indentation: false + - from: '*' + to: '•' + indentation_level: 0 + strict_indentation: false + - from: + + to: '◦' + indentation_level: 0 + strict_indentation: false + - from: -> + to: '▶' + indentation_level: 0 + strict_indentation: false + - from: -- + to: '–' + indentation_level: 0 + strict_indentation: false + - from: -[] + to: ':checkbox:' + indentation_level: 0 + strict_indentation: false + - from: -[ ] + to: ':checkbox:' + indentation_level: 0 + strict_indentation: false + - from: -[x] + to: ':checkbox-checked:' + indentation_level: 0 + strict_indentation: false + - from: -[X] + to: ':checkbox-checked:' + indentation_level: 0 + strict_indentation: false +references: + ref1: my ref1 +compilation: + embed_local_image: true + embed_remote_image: false + compress_embed_image: false + strict_image_src_check: true + parallelization: false + use_remote_addons: false + strict_list_check: false diff --git a/test-resources/nmd-test-dossier-1/nmd.json b/test-resources/nmd-test-dossier-1/nmd.json new file mode 100644 index 0000000..ffaa6a9 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/nmd.json @@ -0,0 +1,19 @@ +{ + "documents": [ + "path/to/d1.nmd", + "path/to/d2.md", + "path/to/d3.nmd" + ], + "styles": [ + "path/to/style1", + "path/to/style2", + "path/to/style3" + ], + "metadata": { + "name": "artifact-name", + "authors": [ + "a1", + "a2" + ] + } +} \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-1/nmd.yml b/test-resources/nmd-test-dossier-1/nmd.yml new file mode 100644 index 0000000..446c034 --- /dev/null +++ b/test-resources/nmd-test-dossier-1/nmd.yml @@ -0,0 +1,89 @@ +name: New Dossier +toc: + title: Table of contents + include_in_output: true + page_numbers: false + plain: false + maximum_heading_level: 4 +documents: +- ./d1.nmd +- ./d2.nmd +- ./d3.nmd +- ./d4.nmd +- ./d5.nmd +- ./d6.nmd +- ./d7.nmd +- ./d8.nmd +- ./d9.nmd +- ./d10.nmd +- ./d11.nmd +style: + theme: Light + styles: + - custom.css + list_bullets_configuration: + - from: '|' + to: '‍' + indentation_level: 0 + strict_indentation: false + - from: '-' + to: '•' + indentation_level: 0 + strict_indentation: true + - from: '-' + to: '◦' + indentation_level: 1 + strict_indentation: true + - from: '-' + to: '–' + indentation_level: 2 + strict_indentation: false + - from: '*' + to: '•' + indentation_level: 0 + strict_indentation: false + - from: + + to: '◦' + indentation_level: 0 + strict_indentation: false + - from: -> + to: '▶' + indentation_level: 0 + strict_indentation: false + - from: -- + to: '–' + indentation_level: 0 + strict_indentation: false + - from: -[] + to: ':checkbox:' + indentation_level: 0 + strict_indentation: false + - from: -[ ] + to: ':checkbox:' + indentation_level: 0 + strict_indentation: false + - from: -[x] + to: ':checkbox-checked:' + indentation_level: 0 + strict_indentation: false + - from: -[X] + to: ':checkbox-checked:' + indentation_level: 0 + strict_indentation: false +references: + ref1: this is the ref1! +bibliography: + title: Bibliography + records: {} + include_in_output: false +compilation: + embed_local_image: false + embed_remote_image: false + compress_embed_image: false + strict_image_src_check: true + parallelization: true + use_remote_addons: false + strict_list_check: false + strict_greek_letters_check: true + strict_cite_check: true + strict_reference_check: true diff --git a/test-resources/nmd-test-dossier-2/.gitignore b/test-resources/nmd-test-dossier-2/.gitignore new file mode 100644 index 0000000..0b84df0 --- /dev/null +++ b/test-resources/nmd-test-dossier-2/.gitignore @@ -0,0 +1 @@ +*.html \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-2/nmd.yml b/test-resources/nmd-test-dossier-2/nmd.yml new file mode 100644 index 0000000..d7606ab --- /dev/null +++ b/test-resources/nmd-test-dossier-2/nmd.yml @@ -0,0 +1,38 @@ +name: New Dossier +documents: [] +style: + theme: Light + addons: [] + list_bullets_configuration: + - from: '|' + to: '‍' + indentation_level: 0 + strict_indentation: false + - from: '-' + to: '•' + indentation_level: 0 + strict_indentation: true + - from: '-' + to: '◦' + indentation_level: 1 + strict_indentation: true + - from: '-' + to: '–' + indentation_level: 2 + strict_indentation: false + - from: '*' + to: '•' + indentation_level: 0 + strict_indentation: false + - from: + + to: '◦' + indentation_level: 0 + strict_indentation: false +metadata: {} +compilation: + embed_local_image: true + embed_remote_image: true + compress_embed_image: true + strict_image_src_check: true + parallelization: true + use_remote_addons: false diff --git a/test-resources/nmd-test-dossier-2/welcome.nmd b/test-resources/nmd-test-dossier-2/welcome.nmd new file mode 100644 index 0000000..9ff71e7 --- /dev/null +++ b/test-resources/nmd-test-dossier-2/welcome.nmd @@ -0,0 +1 @@ +Welcome in NMD! \ No newline at end of file diff --git a/test-resources/nmd-test-dossier-3/nmd.yml b/test-resources/nmd-test-dossier-3/nmd.yml new file mode 100644 index 0000000..d261625 --- /dev/null +++ b/test-resources/nmd-test-dossier-3/nmd.yml @@ -0,0 +1,64 @@ +name: New Dossier +raw_documents_paths: +- ./welcome.nmd +style: + theme: Light + addons: [] + list_bullets_configuration: + - from: '|' + to: '‍' + indentation_level: 0 + strict_indentation: false + - from: '-' + to: '•' + indentation_level: 0 + strict_indentation: true + - from: '-' + to: '◦' + indentation_level: 1 + strict_indentation: true + - from: '-' + to: '–' + indentation_level: 2 + strict_indentation: false + - from: '*' + to: '•' + indentation_level: 0 + strict_indentation: false + - from: + + to: '◦' + indentation_level: 0 + strict_indentation: false + - from: -> + to: '▶' + indentation_level: 0 + strict_indentation: false + - from: -- + to: '–' + indentation_level: 0 + strict_indentation: false + - from: -[] + to: ':checkbox:' + indentation_level: 0 + strict_indentation: false + - from: -[ ] + to: ':checkbox:' + indentation_level: 0 + strict_indentation: false + - from: -[x] + to: ':checkbox-checked:' + indentation_level: 0 + strict_indentation: false + - from: -[X] + to: ':checkbox-checked:' + indentation_level: 0 + strict_indentation: false +metadata: {} +compilation: + embed_local_image: true + embed_remote_image: true + compress_embed_image: true + strict_image_src_check: true + parallelization: true + use_remote_addons: false + strict_list_check: false diff --git a/test-resources/nmd-test-dossier-3/welcome.nmd b/test-resources/nmd-test-dossier-3/welcome.nmd new file mode 100644 index 0000000..24d2bce --- /dev/null +++ b/test-resources/nmd-test-dossier-3/welcome.nmd @@ -0,0 +1 @@ +Welcome in **NMD**! \ No newline at end of file diff --git a/test-resources/wikipedia-logo.png b/test-resources/wikipedia-logo.png new file mode 100644 index 0000000..b46cefb Binary files /dev/null and b/test-resources/wikipedia-logo.png differ