Compare commits

..

No commits in common. "master" and "parser-refactor" have entirely different histories.

180 changed files with 10889 additions and 15991 deletions

1031
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -17,35 +17,26 @@ inherits = "release"
debug = true
[dependencies]
auto-registry = { version = "0.0.1" }
auto-userdata = { path = "crates/auto-userdata" }
ariadne = "0.4.1"
dashmap = "6.0.1"
downcast-rs = "1.2.1"
getopts = "0.2.21"
graphviz-rust = "0.9.0"
lazy_static = "1.5.0"
lsp-server = "0.7.6"
lsp-types = "0.97.0"
mlua = { version = "0.9.9", features = ["lua54", "vendored", "serialize"] }
regex = "1.10.3"
rusqlite = { version = "0.31.0", features = [ "bundled" ] }
rusqlite = "0.31.0"
rust-crypto = "0.2.36"
serde = "1.0.204"
serde_json = "1.0.120"
syntect = "5.2.0"
tokio = { version = "1.38.1", features = [
"macros",
"rt-multi-thread",
"io-std",
] }
tokio = { version = "1.38.1", features = ["macros", "rt-multi-thread", "io-std"]}
tower-lsp = "0.20.0"
unicode-segmentation = "1.11.0"
walkdir = "2.5.0"
runtime-format = "0.1.3"
url = "2.5.4"
pathdiff = "0.2.3"
toml = "0.8.22"
parking_lot = "0.12.4"
anyhow = "1.0.98"
[dev-dependencies]
rand = "0.8.5"

View file

@ -1,15 +1,6 @@
# NML -- Not a markup language!
This branch is for development, check out the [stable](https://github.com/ef3d0c3e/nml/tree/stable) branch.
![Screenshot of NML in NeoVim](./showcase.png)
NML is a language server that comes with a tool for rendering to HTML. The goal of NML is to be a org mode replacement compatible with any editor supporting the language server protocol.
Documentation is available [here](https://ef3d0c3e.github.io/nml/readme/Getting%20Started.html)
## Integration
* **NeoVim** Check my plugin for neovim integration of the language servers extensions: [nml-nvim](https://github.com/ef3d0c3e/nml-nvim/).
Currently a work in progress, expect features and fixes to arrive soon!
# Requirements
@ -29,7 +20,8 @@ you need to install the `dot` program from [Graphviz](https://graphviz.org/).
## Lua kernels
NML statically compiles liblua5.4 to use the lua features.
To execute Lua kernels you need to install `liblua` version 5.4.
Support for a statically linked Lua may be added in the future.
# Compiling
@ -43,14 +35,14 @@ cargo build --release --bin nml
- [x] LaTeX rendering
- [x] Graphviz rendering
- [x] Media
- [x] References
- [x] Navigation
- [x] Cross-Document references
- [x] LSP
- [ ] References
- [ ] Navigation
- [ ] Cross-Document references
- [ ] Complete Lua api
- [ ] Documentation
- [x] Table
- [ ] Table
- [ ] LaTeX output
- [ ] LSP
# License

View file

@ -1,47 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "auto-registry"
version = "0.0.4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

View file

@ -1,12 +0,0 @@
[package]
name = "auto-registry"
version = "0.0.4"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = { version = "1.0"}
quote = "1.0"
syn = { version = "1.0", features = [ "full" ] }

View file

@ -1,353 +0,0 @@
#![feature(proc_macro_span)]
use std::cell::RefCell;
use std::sync::LazyLock;
use std::sync::Mutex;
use std::collections::HashMap;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::Parse;
use syn::parse::ParseStream;
use syn::parse_macro_input;
use syn::ItemStruct;
static REGISTRY: LazyLock<Mutex<HashMap<String, Vec<String>>>> = LazyLock::new(|| Mutex::new(HashMap::default()));
/// Arguments for the [`auto_registry`] proc macro
struct AutoRegistryArgs {
/// The registry name
registry: syn::LitStr,
/// The absolute path to the struct, if not specified the macro will try
/// to automatically infer the full path.
path: Option<syn::LitStr>,
}
/// Parser for [`AutoRegistryArgs`]
impl Parse for AutoRegistryArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut registry = None;
let mut path = None;
loop {
let key: syn::Ident = input.parse()?;
input.parse::<syn::Token![=]>()?;
let value: syn::LitStr = input.parse()?;
match key.to_string().as_str() {
"registry" => registry = Some(value),
"path" => path = Some(value),
_ => {
return Err(syn::Error::new(
key.span(),
format!(
"Unknown attribute `{}`, excepted `registry` or `path`",
key.to_string()
),
))
}
}
if input.is_empty() {
break;
}
input.parse::<syn::Token![,]>()?;
}
if registry.is_none() {
return Err(syn::Error::new(
input.span(),
"Missing required attribute `registry`".to_string(),
));
}
Ok(AutoRegistryArgs {
registry: registry.unwrap(),
path,
})
}
}
/// The proc macro used on a struct to add it to the registry
///
/// # Attributes
/// - registry: (String) Name of the registry to collect the struct into
/// - path: (Optional String) The crate path in which the struct is located
/// If left empty, the path will be try to be automatically-deduced
///
/// # Example
///
/// ```
/// #[auto_registry::auto_registry(registry = "listeners")]
/// struct KeyboardListener { ... }
/// ```
/// This will register `KeyboardListener` to the `listeners` registry.
///
/// # Note
///
/// Due to a lacking implementation of `proc_macro_span` in rust-analyzer,
/// it is highly advised the set the `path` attribute when using this macro.
/// See https://github.com/rust-lang/rust-analyzer/issues/15950
#[proc_macro_attribute]
pub fn auto_registry(attr: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as AutoRegistryArgs);
let input = parse_macro_input!(input as ItemStruct);
let ident = &input.ident;
let path = if let Some(path) = args.path {
let value = path.value();
if value.is_empty() {
value
} else {
format!("{}::{}", value, ident.to_string().as_str())
}
} else {
// Attempt to get the path in a hacky way in case the path wasn't
// specified as an attribute to the macro
let path = match input
.ident
.span()
.unwrap()
.source_file()
.path()
.canonicalize()
{
Ok(path) => path,
Err(e) => {
return syn::Error::new(
input.ident.span(),
format!("Failed to canonicalize path: {}", e),
)
.to_compile_error()
.into();
}
};
let crate_path = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let relative_path = path.strip_prefix(&crate_path).unwrap();
let relative_path_str = relative_path.to_string_lossy();
// Remove the first path component e.g "src/"
let pos = if let Some(pos) = relative_path_str.find("/") {
pos + 1
} else {
0
};
let module_path = relative_path_str
.split_at(pos)
.1
.strip_suffix(".rs")
.unwrap()
.replace("/", "::");
if module_path.is_empty() {
format!("crate::{}", ident.to_string())
} else {
format!("crate::{module_path}::{}", ident.to_string())
}
};
let mut reg = REGISTRY.lock().unwrap();
if let Some(ref mut vec) = reg.get_mut(args.registry.value().as_str()) {
vec.push(path);
} else {
reg.insert(args.registry.value(), vec![path]);
}
quote! {
#input
}
.into()
}
/// Arguments for the [`generate_registry`] proc macro
struct GenerateRegistryArgs {
/// The registry name
registry: syn::LitStr,
/// The collector macro, takes all constructed items and processes them
collector: Option<syn::Expr>,
/// The maper macro, maps types to expressions
mapper: Option<syn::Expr>,
/// The name of the output macro
output: syn::Ident,
}
/// Parser for [`GenerateRegistryArgs`]
impl Parse for GenerateRegistryArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut registry = None;
let mut collector = None;
let mut mapper = None;
let mut output = None;
loop {
let key: syn::Ident = input.parse()?;
input.parse::<syn::Token![=]>()?;
match key.to_string().as_str() {
"registry" => registry = Some(input.parse()?),
"collector" => collector = Some(input.parse()?),
"mapper" => mapper = Some(input.parse()?),
"output" => output = Some(input.parse()?),
_ => {
return Err(syn::Error::new(
key.span(),
format!(
"Unknown attribute `{}`, excepted `registry`, `collector`, `mapper` or `output`",
key.to_string()
),
))
}
}
if input.is_empty() {
break;
}
input.parse::<syn::Token![,]>()?;
}
if registry.is_none() {
return Err(syn::Error::new(
input.span(),
"Missing required attribute `registry`".to_string(),
));
} else if output.is_none() {
return Err(syn::Error::new(
input.span(),
"Missing required attribute `output`".to_string(),
));
} else if collector.is_none() && mapper.is_none() {
return Err(syn::Error::new(
input.span(),
"Macro requires that either `collector` or `mapper` be set".to_string(),
));
}
Ok(GenerateRegistryArgs {
registry: registry.unwrap(),
collector,
mapper,
output: output.unwrap(),
})
}
}
/// The proc macro that generates the function to build the registry
///
/// # Attributes
/// - registry: (String) Name of the registry to generate
/// - collector: (Optional Macro) A macro that will take all the newly constructed
/// objects comma-separated and create the resulting expression
/// - mapper: (Optional Macro) A macro that will map each registered types to
/// an expression. By default `$type::default()` will be called.
/// - output: (Identifier) The generated macro to get access to all registered
/// values. Calling to this macro is what actually generates the values
///
/// Note: Using `mapper` and `collector` will pass the results of calling `mapper`
/// on all types in the registry to `collector`
///
/// # Example
///
/// Basic example
/// ```
/// #[auto_registry::auto_registry(registry = "listeners")]
/// #[derive(Default)]
/// struct KeyboardListener { ... }
///
/// #[auto_registry::auto_registry(registry = "listeners")]
/// #[derive(Default)]
/// struct MouseListener { ... }
///
/// macro_rules! collect_listeners { // Collects to a Vec<Box<dyn Listener>>
/// ( $($construct:expr);+ $(;)? ) => {{ // Macro must accepts `;`-separated arguments
/// vec![$(Box::new($construct) as Box<dyn Listener + Send + Sync>,)+]
/// }};
/// }
///
/// #[auto_registry::generate_registry(registry = "listeners", collector = collect_listeners, output = get_listeners)]
///
/// fn main()
/// {
/// // All listeners will be initialized by calling to `::default()`
/// let listeners = get_listeners!();
/// }
/// ```
///
/// Example using `mapper`
/// ```
/// #[auto_registry::auto_registry(registry = "listeners")]
/// #[derive(Default)]
/// struct KeyboardListener { ... }
///
/// #[auto_registry::auto_registry(registry = "listeners")]
/// #[derive(Default)]
/// struct MouseListener { ... }
///
/// // Some global variable that will hold out registered listeners
/// static LISTENERS: LazyLock<Mutex<Vec<Box<dyn Listener + Send + Sync>>>> = LazyLock::new(|| Mutex::new(Vec::default()));
///
/// macro_rules! register_listener { // Register a single listener
/// ($t:ty) => {{
/// let mut listeners = LISTENERS.lock();
/// listeners
/// .unwrap()
/// .push(Box::new(<$t>::default()) as Box<dyn Listener + Send + Sync>);
/// }};
/// }
///
/// #[auto_registry::generate_registry(registry = "listeners", mapper = register_listener, output = register_all_listeners)]
///
/// fn main()
/// {
/// register_all_listeners!();
/// }
/// ```
#[proc_macro_attribute]
pub fn generate_registry(attr: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as GenerateRegistryArgs);
let reg = REGISTRY.lock().unwrap();
let mut stream = proc_macro2::TokenStream::new();
if let Some(names) = reg.get(args.registry.value().as_str()) {
for name in names {
let struct_name: proc_macro2::TokenStream = name.parse().unwrap();
if let Some(ref mapper) = args.mapper
{
stream.extend(quote::quote_spanned!(proc_macro2::Span::call_site() =>
#mapper!(#struct_name);
));
}
else
{
stream.extend(quote::quote_spanned!(proc_macro2::Span::call_site() =>
#struct_name::default(),
));
}
}
} else {
panic!(
"Unable to find registry item with key=`{}`",
args.registry.value()
);
}
let rest: proc_macro2::TokenStream = input.into();
let output = args.output;
if let Some(collector) = args.collector
{
quote! {
macro_rules! #output {
() => { #collector!(#stream); };
}
#rest
}
}
else
{
quote! {
macro_rules! #output {
() => { #stream };
}
#rest
}
}
.into()
}

View file

@ -1,383 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "auto-userdata"
version = "0.0.1"
dependencies = [
"lazy_static",
"mlua",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "bstr"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "cc"
version = "1.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_home"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]]
name = "erased-serde"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
dependencies = [
"serde",
"typeid",
]
[[package]]
name = "errno"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "lua-src"
version = "547.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42"
dependencies = [
"cc",
]
[[package]]
name = "luajit-src"
version = "210.5.12+a4f56a4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671"
dependencies = [
"cc",
"which",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mlua"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d111deb18a9c9bd33e1541309f4742523bfab01d276bfa9a27519f6de9c11dc7"
dependencies = [
"bstr",
"erased-serde",
"mlua-sys",
"num-traits",
"once_cell",
"rustc-hash",
"serde",
"serde-value",
]
[[package]]
name = "mlua-sys"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93"
dependencies = [
"cc",
"cfg-if",
"lua-src",
"luajit-src",
"pkg-config",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "ordered-float"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
dependencies = [
"num-traits",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "which"
version = "7.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
dependencies = [
"either",
"env_home",
"rustix",
"winsafe",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winsafe"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"

View file

@ -1,14 +0,0 @@
[package]
name = "auto-userdata"
version = "0.0.1"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = { version = "1.0"}
mlua = { version = "0.9.9", features = ["lua54", "vendored", "serialize"] }
quote = "1.0"
syn = { version = "1.0", features = [ "full" ] }
lazy_static = "1.5.0"

View file

@ -1,95 +0,0 @@
#![feature(proc_macro_span)]
use std::cell::RefCell;
use std::collections::HashMap;
use lazy_static::lazy_static;
use proc_macro::TokenStream;
use quote::quote;
use std::sync::Mutex;
use syn::parse::Parse;
use syn::parse::ParseStream;
use syn::parse_macro_input;
use syn::DeriveInput;
use syn::Fields;
use syn::ItemStruct;
#[proc_macro_derive(AutoUserData, attributes(lua_ignore, lua_map))]
pub fn derive_lua_user_data(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = input.ident;
let fields = match input.data {
syn::Data::Struct(data) => match data.fields {
Fields::Named(named) => named.named,
_ => {
return syn::Error::new_spanned(
ident,
"AutoUserData only supports named struct fields",
)
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(ident, "Only structs supported")
.to_compile_error()
.into();
}
};
let mut field_getters = Vec::new();
for field in fields {
let name = field.ident.clone().unwrap();
let field_name_str = name.to_string();
let mut skip = false;
let mut map_wrapper = None;
for attr in &field.attrs {
if attr.path.is_ident("lua_ignore") {
skip = true;
break;
}
if attr.path.is_ident("lua_map") {
let meta = attr.parse_meta();
if let Ok(syn::Meta::List(meta_list)) = meta {
if let Some(syn::NestedMeta::Meta(syn::Meta::Path(path))) =
meta_list.nested.first()
{
if let Some(ident) = path.get_ident() {
map_wrapper = Some(ident.clone());
}
}
}
}
}
if skip {
continue;
}
let getter_expr = if let Some(wrapper_ident) = map_wrapper {
quote! { Ok(#wrapper_ident { inner: this.#name.clone() }) }
} else {
quote! { Ok(this.#name.clone()) }
};
field_getters.push(quote! {
fields.add_field_method_get(#field_name_str, |_, this| {
#getter_expr
});
});
}
let expanded = quote! {
impl mlua::UserData for #ident {
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) {
#(#field_getters)*
}
}
};
TokenStream::from(expanded)
}

View file

@ -1,85 +0,0 @@
@import ../template.nml
%<make_doc({"Blocks"}, "Blocks", "Blocks")>%
# Blocks
#+LAYOUT_BEGIN Split
NML supports different kind of blocks. The syntax goes like this:
```Plain Text,
>[!Warning]
>Text...
> * A list
>> [!Quote][author=me]
>>Something I said..
```
#+LAYOUT_NEXT
Which looks something like this:
>[!Warning]
>Text...
> * A list
>> [!Quote][author=me]
>>Something I said..
#+LAYOUT_END
The default blocks types are:
- `Quote` (see &{quotes}[caption=Quotes] for properties)
- `Warning`
- `Note`
- `Todo`
- `Tip`
- `Caution`
Blocks can be comprised of any paragraph element (e.g style, links) as wall as other blocks and lists.
# Nesting blocks
#+LAYOUT_BEGIN Split
>[!Tip]
> You can nest blocks as much as you like
>>[!Quote]
>> Here's a quote inside the tip.
>>>[!Note]
>>>Some information...
>> Back to the quote
>
>>[!Caution]
>>Another block
#+LAYOUT_NEXT
```Markdown, Given by the following
>[!Tip]
> You can nest blocks as much as you like
>>[!Quote]
>> Here's a quote inside the tip.
>>>[!Note]
>>>Some information...
>> Back to the quote
>
>>[!Caution]
>>Another block
```
#+LAYOUT_END
#{quotes} Quotes
Quotes support properties and have a style under style key `style.block.quote`.
**Properties**:
* ``author`` The quote author
* ``cite`` The quote source name
* ``url`` The quote source url (used for accessibility)
**Style**:
* ``author_pos`` Position of the author statement, available options:
*- `None` Hides the author
*- `Before` Displays the author before the quote
*- `After` Displays the author after the quote (default)
* ``format`` An array with 3 format strings to control how the author is displayed:
*-[offset=0] Format for author+cite
*- Format for author only
*- Format for cite only
```JSON, Default Style
{
"author_pos": "After",
"format": ["{author}, {cite}", "{author}", "{cite}"],
}
```

View file

@ -1,95 +0,0 @@
@import ../template.nml
%<make_doc({"Blocks"}, "Code", "Code")>%
# Full blocks of code
NML supports different kind of code blocks. The *full block* modes creates a (optionally titled), numbered code block.
You can also specify the language of the block to get proper highlighting via the [syntect](https://docs.rs/syntect/latest/syntect/) crate.
**Example:**
#+LAYOUT_BEGIN Split
*The following...*
``Markdown
`\``C, Factorial in C
int factorial(int n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
`\``
``
#+LAYOUT_NEXT
*...gives the following*
```C, Factorial in C
int factorial(int n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
```
#+LAYOUT_END
##+* Properties
* ``line_offset``: (number) The number of the first line (defaults: 0)
# Mini blocks
Mini blocks are code blocks that can span on a single line (thus blending within a paragraph).
**Example:**
* ``Plain Text,\``Rust, str.chars().iter().fold(0, |acc, _| acc + 1)\`` `` → ``Rust, str.chars().iter().fold(0, |acc, _| acc + 1)``
* ``Plain Text, \``C++, auto car{ std::make_unique<Car>(...) };\`` `` → ``C++, auto car{ std::make_unique<Car>(...) };``
Mini blocks can span multiple lines, in which case they become similar to full code blocks with the following differences:
- **No title:** Mini blocks cannot have a title at all
- **No line numbers:** Line numbers won't be shown
**Example:**
#+LAYOUT_BEGIN Split
*The following...*
``Markdown
\``Rust
fn real_position(
source: Rc<dyn Source>,
position: usize
) -> (Rc<dyn Source>, usize)
{
if let Some(parent) = source.parent
{
return real_position(parent.clone(), source.apply_offsets(position));
}
return (source.clone(), source.apply_offsets(position));
}
\``
``
#+LAYOUT_NEXT
*...gives the following*
``Rust
fn real_position(
source: Rc<dyn Source>,
position: usize
) -> (Rc<dyn Source>, usize)
{
if let Some(parent) = source.parent
{
return real_position(parent.clone(), source.apply_offsets(position));
}
return (source.clone(), source.apply_offsets(position));
}
``
#+LAYOUT_END
# Code theme
Code theme can be controlled by the variable ``code.theme``. The default value is ``base16-ocean.dark``.
According to [syntect](https://docs.rs/syntect/latest/syntect/highlighting/struct.ThemeSet.html#method.load_defaults)'s
documentation, the following themes are available:
* ``base16-ocean.dark``
* ``base16-eighties.dark``
* ``base16-mocha.dark``
* ``base16-ocean.light``
* ``InspiredGitHub``
* ``Solarized (dark)``
* ``Solarized (light)``

View file

@ -1,78 +0,0 @@
@import ../template.nml
%<make_doc({"Blocks"}, "Lists", "Lists")>%
# Lists
* Here is a simple list
** With nested entries
*- Numbered even!
Lists start after a newline with one or more space or tab, followed by `\*` for unnumbered lists or `-` for numbered lists.
**Example**
#+LAYOUT_BEGIN Split
*The following...*
```Markdown
* A
* B
- A
- B
```
#+LAYOUT_NEXT
*...gives the following*
* A
* B
- A
- B
#+LAYOUT_END
# Nested lists
Lists can contain other lists as nested elements.
**Example**
#+LAYOUT_BEGIN Split
*The following...*
```Markdown
- First
--[offset=11] Nested
-- Numbered list!
- Back to the first list
```
#+LAYOUT_NEXT
*...gives the following*
- First
--[offset=11] Nested
-- Numbered list!
- Back to the first list
#+LAYOUT_END
# Checkboxes
You can add checkboxes to lists. Lists support the following checkboxes:
* Unchecked: with `[ ]` or `[]`.
* Partial: with `[-]`
* Checked: with `[x]` or `[X]`
**Example**
#+LAYOUT_BEGIN Split
*The following...*
```Markdown
* [x] Checked
* [-] Partial
* [] Unchecked
```
#+LAYOUT_NEXT
*...gives the following*
* [x] Checked
* [-] Partial
* [] Unchecked
#+LAYOUT_END
# Properties
Lists currently support these properties:
* ``offset`` (number) The start offset for a numbered list, defaults to 1
* ``bullet`` (currently unused)

View file

@ -1,98 +0,0 @@
@import ../template.nml
%<make_doc({"Blocks"}, "Tables", "Tables")>%
# Tables
NML support for tables is still very limited, though it is possible to build complex layouts like this one:
:TABLE {sample_table} Sample table
| **First column** |:hspan=2: **Second column** |
|:align=center: Centered | B | C |
|:hspan=2: 1 | 2 |
#+LAYOUT_BEGIN[title=Given by the following code] Spoiler
``Plain Text,
:TABLE {sample_table} Sample table
| **First column** |:hspan=2: **Second column** |
|:align=center: Centered | B | C |
|:hspan=2: 1 | 2 |``
#+LAYOUT_END
When a line starts with `|` it is considered as the start of a table. Other `|`'s delimit the cells of a table row.
You can also use `:TABLE {refname} Title` before the first line of the table, to make the table into a referenceable element. Tables declared like this are displayed as `media`.
# Cell Properties
On each cell of a table, you may specify properties for the cell, row, column or table. Properties are specified between `:`'s at the start of a cell. It is not possible to redefine an already present property (e.g setting the table text-alignment twice). Below are the supported properties:
* **Cells**
*- `align` The text-alignment of the cell
*- `hspan` The horizontal span of the cell (1 if unset)
*- `vspan` The vertical span of the cell (1 if unset)
* **Rows** *Cells will inherit properties from their parent row*
*- `align` The text-alignment of the row
*- `rvspan` The vertical span of the row (1 if unset)
* **Columns** *Cells will inherit properties from their parent column*
*- `chspan` The horizontal span of the column (1 if unset)
* **Table** *Each cell will inherit these properties*
*- `align` Text-alignment for the entire table
# Tables to Lua
You can export a table to use it inside a lua snippet.
Using `:TABLE[export_as=table1]` will make the table available to lua as `nml.tables.table1`.
**Example**:
#+LAYOUT_BEGIN Split
Using Lua, you can perform computations on table rows, cells, etc.
:TABLE[export_as=measures]
| Length | Occurences |
| 1.15 | 357 |
| 1.20 | 143 |
| 1.23 | 72 |
@<main
function weighted_average(table, i, j)
local weighted_sum = 0
local total = 0
for rowIndex, row in pairs(table) do
if rowIndex ~= 1 then
weighted_sum = weighted_sum + row[i] * row[j];
total = total + row[j]
end
end
return weighted_sum / total
end
>@
Average = %<" weighted_average(nml.tables.measures, 1, 2)>%
#+LAYOUT_NEXT
Which is given by:
```Plain Text,
:TABLE[export_as=measures]
| Length | Occurences |
| 1.15 | 357 |
| 1.20 | 143 |
| 1.23 | 72 |
@<main
function weighted_average(table, i, j)
local weighted_sum = 0
local total = 0
for rowIndex, row in pairs(table) do
if rowIndex ~= 1 then
weighted_sum = weighted_sum + row[i] * row[j];
total = total + row[j]
end
end
return weighted_sum / total
end
>@
Average = %<" weighted_average(nml.tables.measures, 1, 2)>%
```
#+LAYOUT_END
# Current limitations
Current known limitations for tables, may change in the future:
* Referenceable elements cannot be referenced if defined inside of a table.
* Table layouts are limited and it is not possible to `split` a cell in half if it's parent column has a span of 2.

View file

@ -1,16 +0,0 @@
@import template.nml
@nav.previous = Imports
%<make_doc({}, "Changelog", "Changelog")>%
# beta-0.5
* Added tables into the language
* Added `textDocument/definition` support in nmlls
* Added off-spec LSP extensions: `textDocument/conceal`, `textDocument/style` and `textDocument/codeRange`
* Added text blocks into the language
* Merged quotes with blocks
* Added async processing for certain compilation tasks: LaTeX, Graphviz and Code highlighting
* More precise tokenization for the language server
# beta-0.4
Initial public release

View file

@ -1,16 +0,0 @@
@import template.nml
@nav.previous = Imports
%<make_doc({}, "Comments", "Comments")>%
# Comments
NML supports line comment with the following syntax: ``Plain Text, :: Comment``
Comments will eat any preceding white space.
**Example**
#+LAYOUT_BEGIN Split
``Markdown, **Bold Text** :: This is a comment``
#+LAYOUT_NEXT
**Bold Text** :: This is a comment
#+LAYOUT_END

View file

@ -8,50 +8,28 @@
digraph {
bgcolor=transparent;
graph[fontcolor=darkgray];
node[shape=box,fontcolor=darkgray];
edge[fontcolor=darkgray, color=gray];
filelist [color=orange, label="File List"];
doclist [color=orange, label="Document List"];
node[fontcolor=darkgray];
edge[fontcolor=darkgray, color=gray90];
filelist [shape=box, color=orange, label="File List"];
doclist [shape=box, color=orange, label="Document List"];
iscached [shape=diamond, color=red, label="Cached?"];
parse [color=white, label=Parse];
compile [color=white, label=Compile];
cache [color=orange, label=Cache];
parse [shape=box, color=white, label=Parse];
compile [shape=box,color=white, label=Compile];
cache [shape=box, color=orange, label=Cache];
filelist -> iscached;
iscached -> cache[dir=both,color=lightblue,style=dashed];
iscached -> doclist[label="Yes",color=lightblue,style=dashed];
iscached -> parse[label="No",color=lightblue,style=dashed];
subgraph cluster_0 {
style=dotted;
color=white;
label = "Processing";
labeljust="l";
parse -> compile;
}
iscached -> cache[dir=both];
iscached -> doclist[label="Yes"];
iscached -> parse[label="No"];
parse -> compile;
compile -> cache[label=""];
compile -> doclist[label=""];
buildnav [color=white, label="Build Navigation"];
xref [color=white, label="Resolve Cross-References"];
doclist -> xref;
doclist -> buildnav[label="Cached",color=lightblue,style=dashed];
subgraph cluster_1 {
style=dotted;
color=white;
label = "Post-Processing";
labeljust="l";
xref -> buildnav;
}
xref -> cache[color=lightblue,style=dashed];
output [color=orange, label="Output"];
doclist -> buildnav;
output [color=white, label="Output"];
buildnav -> output;
}
[/graph]
@ -65,15 +43,15 @@ Graphs blocks are delimited by `` [graph]...[/graph]``
# Properties
* ``layout`` The layout engine, defaults to `dot`
see [Graphviz's documentation](https://graphviz.org/docs/layouts/). Allowed values:
*- [`dot`](https://graphviz.org/docs/layouts/dot/)
*- [`neato`](https://graphviz.org/docs/layouts/neato/)
*- [`fdp`](https://graphviz.org/docs/layouts/fdp/)
*- [`sfdp`](https://graphviz.org/docs/layouts/sfdp/)
*- [`circo`](https://graphviz.org/docs/layouts/circo/)
*- [`twopi`](https://graphviz.org/docs/layouts/twopi/)
*- [`osage`](https://graphviz.org/docs/layouts/osage/)
*- [`patchwork`](https://graphviz.org/docs/layouts/patchwork/)
see [Graphviz's documentation](https://graphviz.org/docs/layouts/), allowed values:
*- [`dot`](https://graphviz.org/docs/layouts/dot/)
*- [`neato`](https://graphviz.org/docs/layouts/neato/)
*- [`fdp`](https://graphviz.org/docs/layouts/fdp/)
*- [`sfdp`](https://graphviz.org/docs/layouts/sfdp/)
*- [`circo`](https://graphviz.org/docs/layouts/circo/)
*- [`twopi`](https://graphviz.org/docs/layouts/twopi/)
*- [`osage`](https://graphviz.org/docs/layouts/osage/)
*- [`patchwork`](https://graphviz.org/docs/layouts/patchwork/)
* ``width`` The resulting svg's width property, defaults to `100%`
# Examples
@ -331,14 +309,7 @@ digraph UML_Class_diagram {
``
#+LAYOUT_END
# Graphviz cache
# Graphiz cache
Graphviz graphs that have been rendered to **svg** are stored in the cache database, under table ``cached_dot``.
Unless you modify the graph or it's properties, it won't be rendered again, instead it will be sourced from the database.
# Bindings
* ``Lua, nml.graphviz.push(layout, width, dot)``
** ``layout`` *(string)* the layout engine
** ``width`` *(string)* the width property (empty string for default)
** ``dot`` *(string)* the graphviz code

View file

@ -3,14 +3,7 @@
@LaTeX = $|[kind=inline, caption=LaTeX]\LaTeX|$
#+LAYOUT_BEGIN Centered
*Bring some %LaTeX% unto your document!*
#+LAYOUT_END
# Requirements
In order to use LaTeX processing, you need to have a %LaTeX% distribution installed. We recommend the [TeX Live](https://en.wikipedia.org/wiki/TeX_Live) distribution.
You'll also need to install the [latex2svg](https://github.com/ef3d0c3e/nml/blob/master/third/latex2svg) python script provided with NML. You'll have to follow the installation instructions from the [original latex2svg repository](https://github.com/Moonbase59/latex2svg). If you don't want to add the script to your `\$PATH`, you can set the executable path in the §{tex_env}[caption=LaTeX environment].
# Inline Math
@ -71,7 +64,7 @@ $|\begin{tikzpicture}
\end{tikzpicture}|$
#+LAYOUT_END
#{tex_env} LaTeX environment
# LaTeX environment
You can define multiple %LaTeX% environment, the default being `main`
* ``@tex.env.fontsize`` The fontsize (in pt) specified to `latex2svg` (default: `12`).
@ -101,24 +94,8 @@ To set the environment you wish to use for a particular %LaTeX% element, set the
*- `block` (default for non math mode) display %LaTeX% on it's own line.
* ``caption`` Caption for accessibility, defaults to `none`.
#{tex_cache} LaTeX cache
# LaTeX cache
%LaTeX% elements that have been successfully rendered to **svg** are stored in the cache database, to avoid processing them a second time.
Note that this cache is shared between documents, so you don't need to reprocess them if they share the same environment.
They are stored under the table named ``Plain Text,cached_tex``, if you modify the `env` all elements will be reprocessed which may take a while...
# Bindings
* ``Lua, nml.tex.push_math(kind, tex [, env [, caption]])``
inserts a math mode %LaTeX% element.
** ``kind`` *(string)* the element kind (inline or block)
** ``tex`` *(string)* the %LaTeX% code
** ``env`` *(string)* the %LaTeX% environment (defaults to `main`)
** ``caption`` *(string)* the accessibility caption
* ``Lua, nml.tex.push(kind, tex [, env [, caption]])``
inserts a non-math %LaTeX% element.
** ``kind`` *(string)* the element kind (inline or block)
** ``tex`` *(string)* the %LaTeX% code
** ``env`` *(string)* the %LaTeX% environment (defaults to `main`)
** ``caption`` *(string)* the accessibility caption
They are stored under the table named ``cached_tex``, if you modify the `env` all elements will be reprocessed which may take a while...

View file

@ -1,17 +0,0 @@
@import template.nml
@nav.previous = References
%<make_doc({}, "Imports", "Imports")>%
# Imports
NML lets you import documents into the current document via the following syntax:
``Plain Text, @import <PATH.nml>``
Note that this will import everything from the other document, such as content but also variables and references.
# Scoped imports
If you wish to import a document, while not overwriting current variables and references, use the following:
``Plain Text, @import[as=util] lib.nml``
With this syntax, any variable or reference imported will be prefixed with ``util.``

4
docs/index.nml Normal file
View file

@ -0,0 +1,4 @@
@import template.nml
%<make_doc({}, "Index", "Index")>%
# Welcome to the NML documentation!

View file

@ -1,40 +0,0 @@
@import template.nml
@nav.previous = References
%<make_doc({}, "Raw", "Raw")>%
Raws are elements to be rendered as-is by the compiler.
# Inline raws
Inline raws are meant to be used inside a paragraph and thus, don't break the paragraph.
Here's the syntax for inline raws: ``Plain Text, {?[kind=inline] CONTENT ?}``.
Here, ``CONTENT`` will added directly to the resulting document.
**Example**
#+LAYOUT_BEGIN Split
``Plain Text, {? <a style="color:red"> ?} Some text {? </a> ?}``
#+LAYOUT_NEXT
{? <a style="color:red"> ?} Some text {? </a> ?}
#+LAYOUT_END
Raws are better paired with Lua, see &{#custom_style}[caption=Defining a custom style] for how to use them.
# Block raws
You can have raw elements take a full block to define additional capabilities.
The syntax is similar to inline raws, except that ``kind=block`` is used instead.
**Example**
#+LAYOUT_BEGIN Centered
#+LAYOUT_BEGIN Split
``Plain Text, {?[kind=block] <img src="assets/duck.jpg" style="max-height:100%;max-width:100%;"> ?}``
#+LAYOUT_NEXT
{?[kind=block] <img src="assets/duck.jpg" style="max-height:100%;max-width:100%;"> ?}
#+LAYOUT_END
#+LAYOUT_END
# Properties
* ``kind`` The element kind of the resulting raw, defaults to `inline`, allowed values:
*- ``inline``: Make the raw element inline
*- ``block``: Make the raw element a full block

View file

@ -1,31 +0,0 @@
@import template.nml
@nav.previous = Variables
%<make_doc({}, "References", "References")>%
#{internal_references} Internal references
Internal references allow you to create references to elements defined within the current document.
Reference the the current section: ``&{internal_reference}`` → &{internal_references}
## Media references
![flower](assets/flower.webm)[caption = Flower]
When you reference a medium from the current document, the reference can be hovered to show the referenced medium: &{flower}.
# External references
You can reference elements from other documents by adding the document's name before the reference name (separated by a ``#``).
The document name refers to the output file (as defined by the variable `compiler.output`) excluding the extension.
* ``&{doc#ref}``: Finds reference named `ref` in document named `doc`.
* ``&{#ref}``: Finds reference named `ref` in all documents.
Note that this will fail if there are multiple documents defining reference `ref`.
For instance:
* ``&{LaTeX#tex_env}[caption=LaTeX environment]`` → &{LaTeX#tex_env}[caption=LaTeX environment]
* ``&{#tex_env}[caption=LaTeX environment]`` → &{#tex_env}[caption=LaTeX environment]
# Properties
* ``caption`` The display caption for the reference

View file

@ -41,11 +41,6 @@ You can then create a clickable reference to this section: ``§{refname}`` or ``
§{refname}[caption=Click me!] or §{first}[caption=First section]
``
# Table of Content
Section can be automatically exported to a table of content, such as shown at the top of this document.
To create a table of content, simply add ``#+TABLE_OF_CONTENT`` somewhere in your document and it will be displayed there.
# Section styling
The styling for the section link is controlled by the style key ``style.section``
@ -59,14 +54,3 @@ The styling for the section link is controlled by the style key ``style.section`
"link": ["", "🔗", " "]
}
```
# Bindings
* ``Lua, nml.section.push(title, depth, [, kind [, reference]])``
** ``title`` *(string)* the section display title
** ``depth`` *(number)* the section depth
** ``kind`` *(string)* the section kind
**- `\*` for unnumbered
**- `+` for outside of the table of content
**- `\*+` or `+\*` for both
** ``reference`` *(string)* the section reference name

View file

@ -1,13 +1,14 @@
@import template.nml
@nav.previous = Index
%<make_doc({}, "Getting Started", "Getting Started")>%
#{building_nml} Building NML
# Building NML
You need at least the nightly version of rustc to compile NML.
Instruction for your operating system can be found on [Rust's website](https://forge.rust-lang.org/infra/other-installation-methods.html).
You'll also need liblua 5.4 installed. You can then move the `nml` executable in `target/release/nml` into your `\$PATH`
``cargo build --bin nml`` or for release mode: ``cargo build --release --bin nml`` *(Note: The release build binary is much smaller than the debug build one)*
``cargo build --bin nml`` or for release mode: ``cargo build --release --bin nml``
# Building your first document
@ -15,66 +16,17 @@ You'll also need liblua 5.4 installed. You can then move the `nml` executable in
# Using the cache
NML relies on sqlite to keep a cache of pre-compiled elements that take a long time to process (e.g $|[kind=inline] \LaTeX|$).
NML relies on sqlite to keep a cache of precompiled elements that take a long time to process (e.g $|[kind=inline] \LaTeX|$).
To enable caching, use option `-d` with a path: ``-d cache.db``. You can reuse the same cache for multiple documents and benefit from cached elements.
Note that in directory-processing mode, a cache is required so that only modified ``.nml`` files get reprocessed.
**Elements that will use the cache:**
* All $|[kind=inline] \LaTeX|$ elements
(*NOTE: Upon modification of the $|[kind=inline] \LaTeX|$ environment, they will be reprocessed, see &{#tex_cache}[caption=TeX Cache] for more information*)
* All Graphviz elements
* All code blocks
# Directory-Processing mode
To use directory-processing mode, you need to pass an input directory and an output directory. Directory-processing mode requires that you use a database, so that it knows which documents have already been compiled. If the output directory doesn't exist, it will be automatically created.
Compiling the docs:
``Plain Text
``Plain Text,
nml -i docs -o docs_out -d cache.db
``
If you modify an ``Plain Text,@import``ed file, you will need to use the ``--force-rebuild`` option, as NML currently doesn't track which files are imported by other files.
# Building the Language Server
NML comes with it's own language server, ready to be used in any LSP-compatible text editor, such as NeoVim.
Build it by using the following command: ``cargo build --bin nmlls`` or for release mode: ``cargo build --release --bin nmlls`` *(Note: The release build binary is much smaller than the debug build one)*
You should move the language server somewhere in your ``$PATH``.
##* Integrating the LSP
Below is a list of integration steps the language server in various editors.
###* NeoVim
The first step is to add the `.nml` extension to NeoVim, so it is recognized:
``Lua
vim.filetype.add({
pattern = {
['.*%.nml'] = 'nml',
},
})
``
Then you need to register the language server in NeoVim. I recommend the ``lsp-zero`` plugin for that purpose:
``Lua
{
"VonHeikemen/lsp-zero.nvim",
config = function()
local lsp_zero = require('lsp-zero')
lsp_zero.on_attach(function(client, bufnr)
lsp_zero.default_keymaps({buffer = bufnr})
end)
lsp_zero.new_client({
name = 'nmlls',
cmd = {'<PATH TO BINARY IF NOT IN $PATH/>nmlls'},
filetypes = {'nml'},
})
end,
}
``

View file

@ -60,17 +60,3 @@ If you wish to modify the relative width of the splits: add `style=flex: 0.5` in
####+* Properties
* ``style`` Added css style to the div (defaults to none)
## Spoiler
The spoiler layout creates a collapsed element which can be opened.
#+LAYOUT_BEGIN[title=Spoiler demo] Spoiler
This content is *hidden*.
#+LAYOUT_END
####+* Style
The ``Spoiler`` layout uses the `.spoiler` class, combined with `<details>/<summary>` to create the desired layout.
####+* Properties
* ``title`` The spoiler title

View file

@ -1,8 +1,9 @@
@import ../template.nml
%<make_doc({"Styles"}, "User-Defined", "User-Defined Styles")>%
#{custom_style} Defining a custom style
# Defining a custom style
```Lua
%<[main]
function undercustom_start(color)
nml.raw.push("inline", "<span style=\"border-bottom: 1px dashed " .. color .. "\">")
end
@ -11,8 +12,8 @@ function undercustom_end()
nml.raw.push("inline", "</span>")
end
nml.custom_style.define_toggled("Undercustom Red", "~", function() undercustom_start("red") end, undercustom_end)
nml.custom_style.define_paired("Undercustom Green", "[|", "|]", function() undercustom_start("green") end, undercustom_end)
nml.custom_style.define_toggled("Undercustom Red", "~", "undercustom_start(\"red\")", "undercustom_end()")
nml.custom_style.define_paired("Undercustom Green", "[|", "|]", "undercustom_start(\"Green\")", "undercustom_end()")
>%
```
@ -25,8 +26,8 @@ function undercustom_end()
nml.raw.push("inline", "</span>")
end
nml.custom_style.define_toggled("Undercustom Red", "~", function() undercustom_start("red") end, undercustom_end)
nml.custom_style.define_paired("Undercustom Green", "[|", "|]", function() undercustom_start("green") end, undercustom_end)
nml.custom_style.define_toggled("Undercustom Red", "~", "undercustom_start(\"red\")", "undercustom_end()")
nml.custom_style.define_paired("Undercustom Green", "[|", "|]", "undercustom_start(\"Green\")", "undercustom_end()")
>%
Results in the following:
* ``Plain Text,~Dashed underline~`` → ~Dashed underline~

View file

@ -27,8 +27,6 @@ end
>@
@@style.section = {
"link_pos": "After",
"link": [" ", "🔗 ", " "]
"link_pos": "Before",
"link": ["", "🔗 ", " "]
}
#+TABLE_OF_CONTENT Table of Content

View file

@ -1,44 +0,0 @@
@import template.nml
@nav.previous = Sections
%<make_doc({}, "Variables", "Variables")>%
# Variable definition
In NML you can defines variables and call them later.
Currently, two types of variables are supported:
* **Text variables**: Just simple text
* **Path variables**: Path aware variables, that will display an error if the path doesn't exist or is not accessible
To define a variable use the following syntax:
``Markdown
@var = value
:: Text variable
@'my_file = ./pic.png
:: Path variable
``
Variable names cannot contain `\%` or `=`. However variables values can span across multiple lines:
``Markdown
@var = A\
B
:: var == "AB"
@var = A\\
B
:: var == "A\nB"
``
Using a single `\\`'s will ignore the following newline, using two `\\\\`'s will keep the newline.
# Variable substitution
Once variables have been defined, you can call them to be expanded to their content:
``Markdown
@var = Hello, World!
:: Definition
%var%
:: Substitution
``
Expanded variables will be processed by the parser to display their content, as if you had written the variable's value directly.

128
readme.nml Normal file
View file

@ -0,0 +1,128 @@
@html.page_title = NML -- Readme
@html.title = NML -- The nice markup languge!
@'html.css = style.css
@tex.main.fontsize = 9
@tex.main.preamble = \usepackage{xcolor, amsmath} \\
\definecolor{__color1}{HTML}{d5d5d5} \\
\everymath{\color{__color1}\displaystyle}
@tex.main.block_prepend = \color{__color1}
# Paragraphs
Blank lines (or multiple `\\n`'s) create new paragraphs!
```Plain Text, Example
First paragraph :: first '\n'
:: second '\n'
Second paragraph
```
# Lists
Numbered lists `-`:
- first
- second
- third
Unnumbered lists `\*`:
* A
* B
* C
NML also supports list nesting of multiple kinds:
* first
* second
*- 2.1
*- 2.2
*-* even more nested
* third
# Style
NML supports markdown-based text styling:
* \*\*bold\*\* -> **bold**
* \*italic\* -> *italic*
* \__underline\__ -> __underline__
* \`emphasis\` -> `emphasis`
Some additionally supportd text styling
* \`\`inline code\`\` -> ``inline code``
* \`\`C, int main()\`\` -> ``C, int main()``
# Code
```[line_offset=64] C, Some C code
int main(int argc, char** argv)
{
return 0;
}
```
# Lua kernel
Simple kernel named `example`:
``Lua
@<example
function make_bold(text)
return "**" .. text .. "**"
end
>@
``
@<example
function make_bold(text)
return "**" .. text .. "**"
end
>@
Evaluating `!` from a kernel: `\%<![kernel] eval>\%`
* `\%<[example]! make_bold("Hello, World!")>\%` → %<[example]! make_bold("Hello, World!")>%
# Latex
## Support for inline maths:
* $\sum^{\infty}_{k=1} \frac{1}{k^2} = \frac{\pi^2}{6}$
* $n! = \int_0^\infty t^n e^{-t} \text{ d}t$
# Graphs
NML adds support for *Graphviz* graphs.
[graph][
width=600px,
layout=neato
]
digraph g {
bgcolor="transparent"
fontname="Helvetica,Arial,sans-serif"
node [fontname="Helvetica,Arial,sans-serif"]
edge [fontname="Helvetica,Arial,sans-serif"]
graph [fontsize=30 labelloc="t" label="" splines=true overlap=false rankdir = "LR"];
"state0" [ style = "filled, bold" penwidth = 5 fillcolor = "white" fontname = "Courier New" shape = "Mrecord" label =<<table border="0" cellborder="0" cellpadding="3" bgcolor="white"><tr><td bgcolor="black" align="center" colspan="2"><font color="white">State #0</font></td></tr><tr><td align="left" port="r0">&#40;0&#41; s -&gt; &bull;e $ </td></tr><tr><td align="left" port="r1">&#40;1&#41; e -&gt; &bull;l '=' r </td></tr><tr><td align="left" port="r2">&#40;2&#41; e -&gt; &bull;r </td></tr><tr><td align="left" port="r3">&#40;3&#41; l -&gt; &bull;'*' r </td></tr><tr><td align="left" port="r4">&#40;4&#41; l -&gt; &bull;'n' </td></tr><tr><td align="left" port="r5">&#40;5&#41; r -&gt; &bull;l </td></tr></table>> ];
"state1" [ style = "filled" penwidth = 1 fillcolor = "white" fontname = "Courier New" shape = "Mrecord" label =<<table border="0" cellborder="0" cellpadding="3" bgcolor="white"><tr><td bgcolor="black" align="center" colspan="2"><font color="white">State #1</font></td></tr><tr><td align="left" port="r3">&#40;3&#41; l -&gt; &bull;'*' r </td></tr><tr><td align="left" port="r3">&#40;3&#41; l -&gt; '*' &bull;r </td></tr><tr><td align="left" port="r4">&#40;4&#41; l -&gt; &bull;'n' </td></tr><tr><td align="left" port="r5">&#40;5&#41; r -&gt; &bull;l </td></tr></table>> ];
"state2" [ style = "filled" penwidth = 1 fillcolor = "white" fontname = "Courier New" shape = "Mrecord" label =<<table border="0" cellborder="0" cellpadding="3" bgcolor="white"><tr><td bgcolor="black" align="center" colspan="2"><font color="white">State #2</font></td></tr><tr><td align="left" port="r4">&#40;4&#41; l -&gt; 'n' &bull;</td><td bgcolor="grey" align="right">=$</td></tr></table>> ];
"state3" [ style = "filled" penwidth = 1 fillcolor = "white" fontname = "Courier New" shape = "Mrecord" label =<<table border="0" cellborder="0" cellpadding="3" bgcolor="white"><tr><td bgcolor="black" align="center" colspan="2"><font color="white">State #3</font></td></tr><tr><td align="left" port="r5">&#40;5&#41; r -&gt; l &bull;</td><td bgcolor="grey" align="right">=$</td></tr></table>> ];
"state4" [ style = "filled" penwidth = 1 fillcolor = "white" fontname = "Courier New" shape = "Mrecord" label =<<table border="0" cellborder="0" cellpadding="3" bgcolor="white"><tr><td bgcolor="black" align="center" colspan="2"><font color="white">State #4</font></td></tr><tr><td align="left" port="r3">&#40;3&#41; l -&gt; '*' r &bull;</td><td bgcolor="grey" align="right">=$</td></tr></table>> ];
"state5" [ style = "filled" penwidth = 1 fillcolor = "black" fontname = "Courier New" shape = "Mrecord" label =<<table border="0" cellborder="0" cellpadding="3" bgcolor="black"><tr><td bgcolor="black" align="center" colspan="2"><font color="white">State #5</font></td></tr><tr><td align="left" port="r0"><font color="white">&#40;0&#41; s -&gt; e &bull;$ </font></td></tr></table>> ];
"state6" [ style = "filled" penwidth = 1 fillcolor = "white" fontname = "Courier New" shape = "Mrecord" label =<<table border="0" cellborder="0" cellpadding="3" bgcolor="white"><tr><td bgcolor="black" align="center" colspan="2"><font color="white">State #6</font></td></tr><tr><td align="left" port="r1">&#40;1&#41; e -&gt; l &bull;'=' r </td></tr><tr><td align="left" port="r5">&#40;5&#41; r -&gt; l &bull;</td><td bgcolor="grey" align="right">$</td></tr></table>> ];
"state7" [ style = "filled" penwidth = 1 fillcolor = "white" fontname = "Courier New" shape = "Mrecord" label =<<table border="0" cellborder="0" cellpadding="3" bgcolor="white"><tr><td bgcolor="black" align="center" colspan="2"><font color="white">State #7</font></td></tr><tr><td align="left" port="r1">&#40;1&#41; e -&gt; l '=' &bull;r </td></tr><tr><td align="left" port="r3">&#40;3&#41; l -&gt; &bull;'*' r </td></tr><tr><td align="left" port="r4">&#40;4&#41; l -&gt; &bull;'n' </td></tr><tr><td align="left" port="r5">&#40;5&#41; r -&gt; &bull;l </td></tr></table>> ];
"state8" [ style = "filled" penwidth = 1 fillcolor = "white" fontname = "Courier New" shape = "Mrecord" label =<<table border="0" cellborder="0" cellpadding="3" bgcolor="white"><tr><td bgcolor="black" align="center" colspan="2"><font color="white">State #8</font></td></tr><tr><td align="left" port="r1">&#40;1&#41; e -&gt; l '=' r &bull;</td><td bgcolor="grey" align="right">$</td></tr></table>> ];
"state9" [ style = "filled" penwidth = 1 fillcolor = "white" fontname = "Courier New" shape = "Mrecord" label =<<table border="0" cellborder="0" cellpadding="3" bgcolor="white"><tr><td bgcolor="black" align="center" colspan="2"><font color="white">State #9</font></td></tr><tr><td align="left" port="r2">&#40;2&#41; e -&gt; r &bull;</td><td bgcolor="grey" align="right">$</td></tr></table>> ];
state0 -> state5 [ penwidth = 5 fontsize = 28 fontcolor = "black" label = "e" ];
state0 -> state6 [ penwidth = 5 fontsize = 28 fontcolor = "black" label = "l" ];
state0 -> state9 [ penwidth = 5 fontsize = 28 fontcolor = "black" label = "r" ];
state0 -> state1 [ penwidth = 1 fontsize = 14 fontcolor = "grey28" label = "'*'" ];
state0 -> state2 [ penwidth = 1 fontsize = 14 fontcolor = "grey28" label = "'n'" ];
state1 -> state1 [ penwidth = 1 fontsize = 14 fontcolor = "grey28" label = "'*'" ];
state1 -> state4 [ penwidth = 5 fontsize = 28 fontcolor = "black" label = "r" ];
state1 -> state2 [ penwidth = 1 fontsize = 14 fontcolor = "grey28" label = "'n'" ];
state1 -> state3 [ penwidth = 5 fontsize = 28 fontcolor = "black" label = "l" ];
state6 -> state7 [ penwidth = 1 fontsize = 14 fontcolor = "grey28" label = "'='" ];
state7 -> state8 [ penwidth = 5 fontsize = 28 fontcolor = "black" label = "r" ];
state7 -> state1 [ penwidth = 1 fontsize = 14 fontcolor = "grey28" label = "'*'" ];
state7 -> state2 [ penwidth = 1 fontsize = 14 fontcolor = "grey28" label = "'n'" ];
state7 -> state3 [ penwidth = 5 fontsize = 28 fontcolor = "black" label = "l" ];
}
[/graph]

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
<!DOCTYPE HTML><html><head><meta charset="UTF-8"><title>NML | Basic Styles</title><link rel="stylesheet" href="../style.css"></head><body><div class="layout"><input id="navbar-checkbox" class="toggle" type="checkbox" style="display:none" checked><div id="navbar"><ul><li class="navbar-entry"><a href="Getting Started.html">Getting Started</a></li><li class="navbar-entry"><a href="Sections.html">Sections</a></li><li class="navbar-entry"><a href="Variables.html">Variables</a></li><li class="navbar-entry"><a href="References.html">References</a></li><li class="navbar-entry"><a href="Imports.html">Imports</a></li><li class="navbar-entry"><a href="Raw.html">Raw</a></li><li class="navbar-entry"><a href="Changelog.html">Changelog</a></li><li class="navbar-entry"><a href="Comments.html">Comments</a></li><li><details><summary class="navbar-category">External Tools</summary><ul><li class="navbar-entry"><a href="Graphviz.html">Graphviz</a></li><li class="navbar-entry"><a href="LaTeX.html">LaTeX</a></li></ul></details></li><li><details open><summary class="navbar-category">Styles</summary><ul><li class="navbar-entry-current"><a href="Basic Styles.html">Basic</a></li><li class="navbar-entry"><a href="Basic Layouts.html">Layouts</a></li><li class="navbar-entry"><a href="User-Defined Styles.html">User-Defined</a></li></ul></details></li><li><details><summary class="navbar-category">Blocks</summary><ul><li class="navbar-entry"><a href="Blocks.html">Blocks</a></li><li class="navbar-entry"><a href="Code.html">Code</a></li><li class="navbar-entry"><a href="Lists.html">Lists</a></li><li class="navbar-entry"><a href="Tables.html">Tables</a></li></ul></details></li><li><details><summary class="navbar-category">Lua</summary><ul><li class="navbar-entry"><a href="Lua Basics.html">Lua</a></li></ul></details></li></ul></div><label for="navbar-checkbox" class="navbar-checkbox-label">&#9776;</label><div class="content"><div class="toc"><span>Table of Content</span><ol><li value="1"><a href="#Basic_styles">Basic styles</a></li><ol><li value="1"><a href="#Bold">Bold</a></li><li value="2"><a href="#Italic">Italic</a></li><li value="3"><a href="#Underline">Underline</a></li><li value="4"><a href="#Highlighted">Highlighted</a></li></div><h1 id="Basic_styles">1. Basic styles <a class="section-link" href="#Basic_styles">🔗 </a> </h1><h2 id="Bold">1.1. Bold <a class="section-link" href="#Bold">🔗 </a> </h2><p>Enclose text between two <a class="inline-code"><code><span style="color:#c0c5ce;">**</span></code></a> to render it <b>bold</b>!</p><ul><li><a class="inline-code"><code><span style="color:#c0c5ce;">**Bold text**</span></code></a><b>Bold text</b></li><li><a class="inline-code"><code><span style="color:#c0c5ce;">Bold [**link**](#)</span></code></a> → Bold <a href="#"><b>link</b></a></li></ul><h2 id="Italic">1.2. Italic <a class="section-link" href="#Italic">🔗 </a> </h2><p>Enclose text between two <a class="inline-code"><code><span style="color:#c0c5ce;">*</span></code></a> to render it <i>italic</i>!</p><ul><li><a class="inline-code"><code><span style="color:#c0c5ce;">*Italic text*</span></code></a><i>Italic text</i></li><li><a class="inline-code"><code><span style="color:#c0c5ce;">**Bold + *Italic***</span></code></a><b>Bold + <i>Italic</b></i></li></ul><h2 id="Underline">1.3. Underline <a class="section-link" href="#Underline">🔗 </a> </h2><p>Enclose text between two <a class="inline-code"><code><span style="color:#c0c5ce;">__</span></code></a> to render it <u>underlined</u>!</p><ul><li><a class="inline-code"><code><span style="color:#c0c5ce;">__Underlined text__</span></code></a><u>Underlined text</u></li><li><a class="inline-code"><code><span style="color:#c0c5ce;">__Underline + *Italic*__</span></code></a><u>Underline + <i>Italic</i></u></li></ul><h2 id="Highlighted">1.4. Highlighted <a class="section-link" href="#Highlighted">🔗 </a> </h2><p>Enclose text between two <a class="inline-code"><code><span style="color:#c0c5ce;">`</span></code></a> to render it <em>overlined</em>!</p><ul><li><a class="inline-code"><code><span style="color:#c0c5ce;">`Highlighted text`</span></code></a><em>Highlighted text</em></li><li><a class="inline-code"><code><span style="color:#c0c5ce;">`Highlight + **Bold**`</span></code></a><em>Highlight + <b>Bold</b></em></li></ul><p></p></div></div></body></html>

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
<!DOCTYPE HTML><html><head><meta charset="UTF-8"><title>NML | Changelog</title><link rel="stylesheet" href="../style.css"></head><body><div class="layout"><input id="navbar-checkbox" class="toggle" type="checkbox" style="display:none" checked><div id="navbar"><ul><li class="navbar-entry"><a href="Getting Started.html">Getting Started</a></li><li class="navbar-entry"><a href="Sections.html">Sections</a></li><li class="navbar-entry"><a href="Variables.html">Variables</a></li><li class="navbar-entry"><a href="References.html">References</a></li><li class="navbar-entry"><a href="Imports.html">Imports</a></li><li class="navbar-entry"><a href="Raw.html">Raw</a></li><li class="navbar-entry-current"><a href="Changelog.html">Changelog</a></li><li class="navbar-entry"><a href="Comments.html">Comments</a></li><li><details><summary class="navbar-category">External Tools</summary><ul><li class="navbar-entry"><a href="Graphviz.html">Graphviz</a></li><li class="navbar-entry"><a href="LaTeX.html">LaTeX</a></li></ul></details></li><li><details><summary class="navbar-category">Styles</summary><ul><li class="navbar-entry"><a href="Basic Styles.html">Basic</a></li><li class="navbar-entry"><a href="Basic Layouts.html">Layouts</a></li><li class="navbar-entry"><a href="User-Defined Styles.html">User-Defined</a></li></ul></details></li><li><details><summary class="navbar-category">Blocks</summary><ul><li class="navbar-entry"><a href="Blocks.html">Blocks</a></li><li class="navbar-entry"><a href="Code.html">Code</a></li><li class="navbar-entry"><a href="Lists.html">Lists</a></li><li class="navbar-entry"><a href="Tables.html">Tables</a></li></ul></details></li><li><details><summary class="navbar-category">Lua</summary><ul><li class="navbar-entry"><a href="Lua Basics.html">Lua</a></li></ul></details></li></ul></div><label for="navbar-checkbox" class="navbar-checkbox-label">&#9776;</label><div class="content"><div class="toc"><span>Table of Content</span><ol><li value="1"><a href="#beta-0.5">beta-0.5</a></li><li value="2"><a href="#beta-0.4">beta-0.4</a></li></div><h1 id="beta-0.5">1. beta-0.5 <a class="section-link" href="#beta-0.5">🔗 </a> </h1><ul><li>Added tables into the language</li><li>Added <em>textDocument/definition</em> support in nmll</li><li>Added off-spec LSP extensions: <em>textDocument/conceal</em>, <em>textDocument/style</em> and <em>textDocument/codeRange</em></li><li>Added text blocks into the language</li><li>Merged quotes with blocks</li><li>Added async processing for certain compilation tasks: LaTeX, Graphviz and Code highlighting</li><li>More precise tokenization for the language server</li></ul><h1 id="beta-0.4">2. beta-0.4 <a class="section-link" href="#beta-0.4">🔗 </a> </h1><p>Initial public release</p></div></div></body></html>

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
<!DOCTYPE HTML><html><head><meta charset="UTF-8"><title>NML | Comments</title><link rel="stylesheet" href="../style.css"></head><body><div class="layout"><input id="navbar-checkbox" class="toggle" type="checkbox" style="display:none" checked><div id="navbar"><ul><li class="navbar-entry"><a href="Getting Started.html">Getting Started</a></li><li class="navbar-entry"><a href="Sections.html">Sections</a></li><li class="navbar-entry"><a href="Variables.html">Variables</a></li><li class="navbar-entry"><a href="References.html">References</a></li><li class="navbar-entry"><a href="Imports.html">Imports</a></li><li class="navbar-entry"><a href="Raw.html">Raw</a></li><li class="navbar-entry"><a href="Changelog.html">Changelog</a></li><li class="navbar-entry-current"><a href="Comments.html">Comments</a></li><li><details><summary class="navbar-category">External Tools</summary><ul><li class="navbar-entry"><a href="Graphviz.html">Graphviz</a></li><li class="navbar-entry"><a href="LaTeX.html">LaTeX</a></li></ul></details></li><li><details><summary class="navbar-category">Styles</summary><ul><li class="navbar-entry"><a href="Basic Styles.html">Basic</a></li><li class="navbar-entry"><a href="Basic Layouts.html">Layouts</a></li><li class="navbar-entry"><a href="User-Defined Styles.html">User-Defined</a></li></ul></details></li><li><details><summary class="navbar-category">Blocks</summary><ul><li class="navbar-entry"><a href="Blocks.html">Blocks</a></li><li class="navbar-entry"><a href="Code.html">Code</a></li><li class="navbar-entry"><a href="Lists.html">Lists</a></li><li class="navbar-entry"><a href="Tables.html">Tables</a></li></ul></details></li><li><details><summary class="navbar-category">Lua</summary><ul><li class="navbar-entry"><a href="Lua Basics.html">Lua</a></li></ul></details></li></ul></div><label for="navbar-checkbox" class="navbar-checkbox-label">&#9776;</label><div class="content"><div class="toc"><span>Table of Content</span><ol><li value="1"><a href="#Comments">Comments</a></li></div><h1 id="Comments">1. Comments <a class="section-link" href="#Comments">🔗 </a> </h1><p>NML supports line comment with the following syntax: <a class="inline-code"><code><span style="color:#c0c5ce;">:: Comment</span></code></a></p><p>Comments will eat any preceding white space.</p><p><b>Example</b></p><div class="split-container"><div class="split"><p><a class="inline-code"><code><span style="font-weight:bold;color:#ebcb8b;">**Bold Text**</span><span style="color:#c0c5ce;"> :: This is a comment</span></code></a></p></div><div class="split"><p><b>Bold Text</b></p></div></div><p></p></div></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
<!DOCTYPE HTML><html><head><meta charset="UTF-8"><title>NML | Imports</title><link rel="stylesheet" href="../style.css"></head><body><div class="layout"><input id="navbar-checkbox" class="toggle" type="checkbox" style="display:none" checked><div id="navbar"><ul><li class="navbar-entry"><a href="Getting Started.html">Getting Started</a></li><li class="navbar-entry"><a href="Sections.html">Sections</a></li><li class="navbar-entry"><a href="Variables.html">Variables</a></li><li class="navbar-entry"><a href="References.html">References</a></li><li class="navbar-entry-current"><a href="Imports.html">Imports</a></li><li class="navbar-entry"><a href="Raw.html">Raw</a></li><li class="navbar-entry"><a href="Changelog.html">Changelog</a></li><li class="navbar-entry"><a href="Comments.html">Comments</a></li><li><details><summary class="navbar-category">External Tools</summary><ul><li class="navbar-entry"><a href="Graphviz.html">Graphviz</a></li><li class="navbar-entry"><a href="LaTeX.html">LaTeX</a></li></ul></details></li><li><details><summary class="navbar-category">Styles</summary><ul><li class="navbar-entry"><a href="Basic Styles.html">Basic</a></li><li class="navbar-entry"><a href="Basic Layouts.html">Layouts</a></li><li class="navbar-entry"><a href="User-Defined Styles.html">User-Defined</a></li></ul></details></li><li><details><summary class="navbar-category">Blocks</summary><ul><li class="navbar-entry"><a href="Blocks.html">Blocks</a></li><li class="navbar-entry"><a href="Code.html">Code</a></li><li class="navbar-entry"><a href="Lists.html">Lists</a></li><li class="navbar-entry"><a href="Tables.html">Tables</a></li></ul></details></li><li><details><summary class="navbar-category">Lua</summary><ul><li class="navbar-entry"><a href="Lua Basics.html">Lua</a></li></ul></details></li></ul></div><label for="navbar-checkbox" class="navbar-checkbox-label">&#9776;</label><div class="content"><div class="toc"><span>Table of Content</span><ol><li value="1"><a href="#Imports">Imports</a></li><li value="2"><a href="#Scoped_imports">Scoped imports</a></li></div><h1 id="Imports">1. Imports <a class="section-link" href="#Imports">🔗 </a> </h1><p>NML lets you import documents into the current document via the following syntax:</p><p><a class="inline-code"><code><span style="color:#c0c5ce;">@import &lt;PATH.nml&gt;</span></code></a> Note that this will import everything from the other document, such as content but also variables and references.</p><h1 id="Scoped_imports">2. Scoped imports <a class="section-link" href="#Scoped_imports">🔗 </a> </h1><p>If you wish to import a document, while not overwriting current variables and references, use the following:</p><p><a class="inline-code"><code><span style="color:#c0c5ce;">@import[as=util] lib.nml</span></code></a> With this syntax, any variable or reference imported will be prefixed with <a class="inline-code"><code><span style="color:#c0c5ce;">util.</span></code></a></p></div></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
<!DOCTYPE HTML><html><head><meta charset="UTF-8"><title>NML | Lua Basics</title><link rel="stylesheet" href="../style.css"></head><body><div class="layout"><input id="navbar-checkbox" class="toggle" type="checkbox" style="display:none" checked><div id="navbar"><ul><li class="navbar-entry"><a href="Getting Started.html">Getting Started</a></li><li class="navbar-entry"><a href="Sections.html">Sections</a></li><li class="navbar-entry"><a href="Variables.html">Variables</a></li><li class="navbar-entry"><a href="References.html">References</a></li><li class="navbar-entry"><a href="Imports.html">Imports</a></li><li class="navbar-entry"><a href="Raw.html">Raw</a></li><li class="navbar-entry"><a href="Changelog.html">Changelog</a></li><li class="navbar-entry"><a href="Comments.html">Comments</a></li><li><details><summary class="navbar-category">External Tools</summary><ul><li class="navbar-entry"><a href="Graphviz.html">Graphviz</a></li><li class="navbar-entry"><a href="LaTeX.html">LaTeX</a></li></ul></details></li><li><details><summary class="navbar-category">Styles</summary><ul><li class="navbar-entry"><a href="Basic Styles.html">Basic</a></li><li class="navbar-entry"><a href="Basic Layouts.html">Layouts</a></li><li class="navbar-entry"><a href="User-Defined Styles.html">User-Defined</a></li></ul></details></li><li><details><summary class="navbar-category">Blocks</summary><ul><li class="navbar-entry"><a href="Blocks.html">Blocks</a></li><li class="navbar-entry"><a href="Code.html">Code</a></li><li class="navbar-entry"><a href="Lists.html">Lists</a></li><li class="navbar-entry"><a href="Tables.html">Tables</a></li></ul></details></li><li><details open><summary class="navbar-category">Lua</summary><ul><li class="navbar-entry-current"><a href="Lua Basics.html">Lua</a></li></ul></details></li></ul></div><label for="navbar-checkbox" class="navbar-checkbox-label">&#9776;</label><div class="content"><div class="toc"><span>Table of Content</span><ol><li value="1"><a href="#Running_lua_code">Running lua code</a></li><ol><li value="1"><a href="#Lua_to_text">Lua to text</a></li><li value="2"><a href="#Parse_lua_string">Parse lua string</a></li></div><h1 id="Running_lua_code">1. Running lua code <a class="section-link" href="#Running_lua_code">🔗 </a> </h1><p>Running lua code is done using the following syntax: <a class="inline-code"><code><span style="color:#c0c5ce;">%&lt;</span><span style="color:#96b5b4;">print</span><span style="color:#c0c5ce;">(&quot;</span><span style="color:#a3be8c;">Hello World!</span><span style="color:#c0c5ce;">&quot;)&gt;%</span></code></a></p><h2 id="Lua_to_text">1.1. Lua to text <a class="section-link" href="#Lua_to_text">🔗 </a> </h2><p>To convert the return value of your lua code, append <a class="inline-code"><code><span style="color:#c0c5ce;">&quot;</span></code></a> at the start of your lua expression:</p><ul><li><a class="inline-code"><code><span style="color:#c0c5ce;">%&lt;&quot;</span><span style="color:#a3be8c;">return </span><span style="color:#c0c5ce;">&quot;Hello World&quot;</span><span style="color:#a3be8c;">&gt;%</span></code></a> → Hello World</li><li><a class="inline-code"><code><span style="color:#c0c5ce;">%&lt;&quot; &quot;Hello, &quot;</span><span style="color:#a3be8c;"> .. </span><span style="color:#c0c5ce;">&quot;World&quot;</span><span style="color:#a3be8c;">&gt;%</span></code></a> → Hello, World</li></ul><h2 id="Parse_lua_string">1.2. Parse lua string <a class="section-link" href="#Parse_lua_string">🔗 </a> </h2><p>Additionnaly, you can output lua to be parsed by the document's parser. To do so, append <a class="inline-code"><code><span style="color:#c0c5ce;">!</span></code></a> at the start of your lua expression:</p><ul><li><a class="inline-code"><code><span style="color:#c0c5ce;">%&lt;!&quot;</span><span style="color:#a3be8c;">**</span><span style="color:#c0c5ce;">&quot; .. &quot;</span><span style="color:#a3be8c;">Bold from lua?</span><span style="color:#c0c5ce;">&quot; .. &quot;</span><span style="color:#a3be8c;">**</span><span style="color:#c0c5ce;">&quot;&gt;%</span></code></a><b>Bold from lua?</b></li><li><a class="inline-code"><code><span style="color:#c0c5ce;">%&lt;!&quot;</span><span style="color:#a3be8c;">[</span><span style="color:#c0c5ce;">&quot; .. &quot;</span><span style="color:#a3be8c;">Link from Lua</span><span style="color:#c0c5ce;">&quot; .. &quot;</span><span style="color:#a3be8c;">](#)</span><span style="color:#c0c5ce;">&quot;&gt;%</span></code></a><a href="#">Link from Lua</a></li></ul><p></p></div></div></body></html>

View file

@ -1 +0,0 @@
<!DOCTYPE HTML><html><head><meta charset="UTF-8"><title>NML | Raw</title><link rel="stylesheet" href="../style.css"></head><body><div class="layout"><input id="navbar-checkbox" class="toggle" type="checkbox" style="display:none" checked><div id="navbar"><ul><li class="navbar-entry"><a href="Getting Started.html">Getting Started</a></li><li class="navbar-entry"><a href="Sections.html">Sections</a></li><li class="navbar-entry"><a href="Variables.html">Variables</a></li><li class="navbar-entry"><a href="References.html">References</a></li><li class="navbar-entry"><a href="Imports.html">Imports</a></li><li class="navbar-entry-current"><a href="Raw.html">Raw</a></li><li class="navbar-entry"><a href="Changelog.html">Changelog</a></li><li class="navbar-entry"><a href="Comments.html">Comments</a></li><li><details><summary class="navbar-category">External Tools</summary><ul><li class="navbar-entry"><a href="Graphviz.html">Graphviz</a></li><li class="navbar-entry"><a href="LaTeX.html">LaTeX</a></li></ul></details></li><li><details><summary class="navbar-category">Styles</summary><ul><li class="navbar-entry"><a href="Basic Styles.html">Basic</a></li><li class="navbar-entry"><a href="Basic Layouts.html">Layouts</a></li><li class="navbar-entry"><a href="User-Defined Styles.html">User-Defined</a></li></ul></details></li><li><details><summary class="navbar-category">Blocks</summary><ul><li class="navbar-entry"><a href="Blocks.html">Blocks</a></li><li class="navbar-entry"><a href="Code.html">Code</a></li><li class="navbar-entry"><a href="Lists.html">Lists</a></li><li class="navbar-entry"><a href="Tables.html">Tables</a></li></ul></details></li><li><details><summary class="navbar-category">Lua</summary><ul><li class="navbar-entry"><a href="Lua Basics.html">Lua</a></li></ul></details></li></ul></div><label for="navbar-checkbox" class="navbar-checkbox-label">&#9776;</label><div class="content"><div class="toc"><span>Table of Content</span><ol><li value="1"><a href="#Inline_raws">Inline raws</a></li><li value="2"><a href="#Block_raws">Block raws</a></li><li value="3"><a href="#Properties">Properties</a></li></div><p>Raws are elements to be rendered as-is by the compiler.</p><h1 id="Inline_raws">1. Inline raws <a class="section-link" href="#Inline_raws">🔗 </a> </h1><p>Inline raws are meant to be used inside a paragraph and thus, don't break the paragraph. Here's the syntax for inline raws: <a class="inline-code"><code><span style="color:#c0c5ce;">{?[kind=inline] CONTENT ?}</span></code></a>. Here, <a class="inline-code"><code><span style="color:#c0c5ce;">CONTENT</span></code></a> will added directly to the resulting document.</p><p><b>Example</b></p><div class="split-container"><div class="split"><p><a class="inline-code"><code><span style="color:#c0c5ce;">{? &lt;a style=&quot;color:red&quot;&gt; ?} Some text {? &lt;/a&gt; ?}</span></code></a></p></div><div class="split"><p><a style="color:red"> Some text </a></p></div></div><p>Raws are better paired with Lua, see for how to use them.</p><h1 id="Block_raws">2. Block raws <a class="section-link" href="#Block_raws">🔗 </a> </h1><p>You can have raw elements take a full block to define additional capabilities. The syntax is similar to inline raws, except that <a class="inline-code"><code><span style="color:#c0c5ce;">kind=block</span></code></a> is used instead.</p><p><b>Example</b></p><div class="centered"><div class="split-container"><div class="split"><p><a class="inline-code"><code><span style="color:#c0c5ce;">{?[kind=block] &lt;img src=&quot;assets/duck.jpg&quot; style=&quot;max-height:100%;max-width:100%;&quot;&gt; ?}</span></code></a></p></div><div class="split"><img src="assets/duck.jpg" style="max-height:100%;max-width:100%;"></div></div></div><h1 id="Properties">3. Properties <a class="section-link" href="#Properties">🔗 </a> </h1><ul><li><a class="inline-code"><code><span style="color:#c0c5ce;">kind</span></code></a> The element kind of the resulting raw, defaults to <em>inline</em>, allowed values:</li><ol><li value="1"><a class="inline-code"><code><span style="color:#c0c5ce;">inline</span></code></a>: Make the raw element inline</li><li value="2"><a class="inline-code"><code><span style="color:#c0c5ce;">block</span></code></a>: Make the raw element a full block</li></ol></ul><p></p></div></div></body></html>

View file

@ -1 +0,0 @@
<!DOCTYPE HTML><html><head><meta charset="UTF-8"><title>NML | References</title><link rel="stylesheet" href="../style.css"></head><body><div class="layout"><input id="navbar-checkbox" class="toggle" type="checkbox" style="display:none" checked><div id="navbar"><ul><li class="navbar-entry"><a href="Getting Started.html">Getting Started</a></li><li class="navbar-entry"><a href="Sections.html">Sections</a></li><li class="navbar-entry"><a href="Variables.html">Variables</a></li><li class="navbar-entry-current"><a href="References.html">References</a></li><li class="navbar-entry"><a href="Imports.html">Imports</a></li><li class="navbar-entry"><a href="Raw.html">Raw</a></li><li class="navbar-entry"><a href="Changelog.html">Changelog</a></li><li class="navbar-entry"><a href="Comments.html">Comments</a></li><li><details><summary class="navbar-category">External Tools</summary><ul><li class="navbar-entry"><a href="Graphviz.html">Graphviz</a></li><li class="navbar-entry"><a href="LaTeX.html">LaTeX</a></li></ul></details></li><li><details><summary class="navbar-category">Styles</summary><ul><li class="navbar-entry"><a href="Basic Styles.html">Basic</a></li><li class="navbar-entry"><a href="Basic Layouts.html">Layouts</a></li><li class="navbar-entry"><a href="User-Defined Styles.html">User-Defined</a></li></ul></details></li><li><details><summary class="navbar-category">Blocks</summary><ul><li class="navbar-entry"><a href="Blocks.html">Blocks</a></li><li class="navbar-entry"><a href="Code.html">Code</a></li><li class="navbar-entry"><a href="Lists.html">Lists</a></li><li class="navbar-entry"><a href="Tables.html">Tables</a></li></ul></details></li><li><details><summary class="navbar-category">Lua</summary><ul><li class="navbar-entry"><a href="Lua Basics.html">Lua</a></li></ul></details></li></ul></div><label for="navbar-checkbox" class="navbar-checkbox-label">&#9776;</label><div class="content"><div class="toc"><span>Table of Content</span><ol><li value="1"><a href="#Internal_references">Internal references</a></li><ol><li value="1"><a href="#Media_references">Media references</a></li></ol><li value="2"><a href="#External_references">External references</a></li><li value="3"><a href="#Properties">Properties</a></li></div><h1 id="Internal_references">1. Internal references <a class="section-link" href="#Internal_references">🔗 </a> </h1><p>Internal references allow you to create references to elements defined within the current document.</p><p>Reference the the current section: <a class="inline-code"><code><span style="color:#c0c5ce;">&amp;{internal_reference}</span></code></a><a class="section-reference" href="#Internal_references">(Internal references)</a></p><h2 id="Media_references">1.1. Media references <a class="section-link" href="#Media_references">🔗 </a> </h2><div class="media"><div id="medium-1" class="medium"><video controls><source src="assets/flower.webm"></video><p class="medium-refname">(1) Flower</p></div></div><p>When you reference a medium from the current document, the reference can be hovered to show the referenced medium: <a class="medium-ref" href="#medium-1">(1)<video><source src="assets/flower.webm"></video></a>.</p><h1 id="External_references">1. External references <a class="section-link" href="#External_references">🔗 </a> </h1><p>You can reference elements from other documents by adding the document's name before the reference name (separated by a <a class="inline-code"><code><span style="color:#c0c5ce;">#</span></code></a>). The document name refers to the output file (as defined by the variable <em>compiler.output</em>) excluding the extension.</p><ul><li><a class="inline-code"><code><span style="color:#c0c5ce;">&amp;{doc#ref}</span></code></a>: Finds reference named <em>ref</em> in document named <em>doc</em>.</li><li><a class="inline-code"><code><span style="color:#c0c5ce;">&amp;{#ref}</span></code></a>: Finds reference named <em>ref</em> in all documents. Note that this will fail if there are multiple documents defining reference <em>ref</em>.</li></ul><p>For instance:</p><ul><li><a class="inline-code"><code><span style="color:#c0c5ce;">&amp;{LaTeX#tex_env}[caption=LaTeX environment]</span></code></a></li><li><a class="inline-code"><code><span style="color:#c0c5ce;">&amp;{#tex_env}[caption=LaTeX environment]</span></code></a></li></ul><h1 id="Properties">2. Properties <a class="section-link" href="#Properties">🔗 </a> </h1><ul><li><a class="inline-code"><code><span style="color:#c0c5ce;">caption</span></code></a> The display caption for the reference</li></ul><p></p></div></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

(image error) Size: 180 KiB

Binary file not shown.

Before

(image error) Size: 640 KiB

Binary file not shown.

Binary file not shown.

Before

(image error) Size: 62 KiB

409
src/cache/cache.rs vendored
View file

@ -1,20 +1,6 @@
use std::collections::HashMap;
use std::sync::Arc;
use rusqlite::params;
use rusqlite::types::FromSql;
use rusqlite::Connection;
use rusqlite::OptionalExtension;
use rusqlite::ToSql;
use tokio::sync::Mutex;
use tokio::sync::MutexGuard;
use crate::lsp::reference::LsReference;
use crate::parser::resolver::UnitDependency;
use crate::unit::element::ReferenceableElement;
use crate::unit::translation::TranslationUnit;
use crate::unit::unit::DatabaseUnit;
use crate::unit::unit::Reference;
pub enum CachedError<E> {
SqlErr(rusqlite::Error),
@ -37,7 +23,7 @@ pub trait Cached {
fn key(&self) -> <Self as Cached>::Key;
fn init(con: &MutexGuard<'_, Connection>) -> Result<(), rusqlite::Error> {
fn init(con: &mut Connection) -> Result<(), rusqlite::Error> {
con.execute(<Self as Cached>::sql_table(), ()).map(|_| ())
}
@ -47,12 +33,12 @@ pub trait Cached {
/// # Error
///
/// Will return an error if the database connection(s) fail,
/// or if not cached, an error from the generator `f`
/// or if not cached, an error from the generator [`f`]
///
/// Note that on error, `f` may still have been called
/// Note that on error, [`f`] may still have been called
fn cached<E, F>(
&self,
con: &MutexGuard<'_, Connection>,
con: &mut Connection,
f: F,
) -> Result<<Self as Cached>::Value, CachedError<E>>
where
@ -76,10 +62,10 @@ pub trait Cached {
if let Some(value) = value {
// Found in cache
Ok(value)
return Ok(value);
} else {
// Compute a value
let value = match f(self) {
let value = match f(&self) {
Ok(val) => val,
Err(e) => return Err(CachedError::GenErr(e)),
};
@ -97,386 +83,3 @@ pub trait Cached {
}
}
}
/// Handles caching of [`Cached`] elements
#[derive(Clone)]
pub struct Cache {
con: Arc<Mutex<Connection>>,
}
impl Cache {
pub fn new(db_path: &str) -> Result<Self, String> {
let con = if db_path.is_empty() {
Connection::open_in_memory()
} else {
Connection::open(db_path)
}
.map_err(|err| format!("Unable to open connection to the database: {err}"))?;
Ok(Self {
con: Arc::new(Mutex::new(con)),
})
}
pub async fn get_connection<'s>(&'s self) -> MutexGuard<'s, Connection> {
self.con.lock().await
}
/// Sets up cache tables
pub fn setup_tables(&self) {
let con = tokio::runtime::Runtime::new()
.unwrap()
.block_on(self.get_connection());
// Table containing all compiled units
con.execute(
"CREATE TABLE IF NOT EXISTS units(
input_file TEXT PRIMARY KEY,
mtime INTEGER NOT NULL
);",
(),
)
.unwrap();
// Table containing all units that can be referenced
con.execute(
"CREATE TABLE IF NOT EXISTS referenceable_units(
reference_key TEXT PRIMARY KEY,
input_file TEXT NOT NULL,
output_file TEXT,
FOREIGN KEY(input_file) REFERENCES units(input_file)
);",
(),
)
.unwrap();
// Table containing all referenceable objects
con.execute(
"CREATE TABLE IF NOT EXISTS exported_references(
name TEXT PRIMARY KEY,
unit_ref TEXT NOT NULL,
token_start INTEGER NOT NULL,
token_end INTEGER NOT NULL,
type TEXT NOT NULL,
data TEXT NOT NULL,
link TEXT,
FOREIGN KEY(unit_ref) REFERENCES referenceable_units(reference_key) ON DELETE CASCADE,
UNIQUE(unit_ref, name)
);",
(),
)
.unwrap();
// Table containing unit dependencies
con.execute(
"CREATE TABLE IF NOT EXISTS dependencies(
unit_ref TEXT NOT NULL,
depends_on TEXT NOT NULL,
range_start INTEGER NOT NULL,
range_end INTEGER NOT NULL,
depends_for TEXT NOT NULL,
PRIMARY KEY(depends_for, unit_ref),
FOREIGN KEY(unit_ref) REFERENCES referenceable_units(reference_key) ON DELETE CASCADE
);",
(),
)
.unwrap();
}
/// Loads offloaded units from the database
pub fn load_units<F, E>(&self, mut input_it: F) -> Result<(), E>
where
F: FnMut(DatabaseUnit) -> Result<(), E>,
{
let con = tokio::runtime::Runtime::new()
.unwrap()
.block_on(self.get_connection());
// Load from database
let mut cmd = con.prepare("SELECT * FROM referenceable_units").unwrap();
let unlodaded_iter = cmd
.query_map([], |row| {
Ok((row.get(0).unwrap(), row.get(1).unwrap(), row.get(2).ok()))
})
.unwrap();
// Insert
for unloaded in unlodaded_iter {
let unloaded: (String, String, Option<String>) = unloaded.unwrap();
input_it(DatabaseUnit {
reference_key: unloaded.0.clone(),
input_file: unloaded.1.clone(),
output_file: unloaded.2,
})?;
}
Ok(())
}
/// Export units
pub fn export_units<'a, I>(&self, it: I, time_now: u64)
where
I: Iterator<Item = &'a TranslationUnit>,
{
let con = tokio::runtime::Runtime::new()
.unwrap()
.block_on(self.get_connection());
let mut insert_stmt = con
.prepare(
"INSERT OR REPLACE
INTO units (input_file, mtime)
VALUES (?1, ?2);",
)
.unwrap();
for unit in it {
insert_stmt
.execute(params![unit.input_path(), time_now])
.unwrap();
}
}
/// Gets a unit's mtime
pub fn get_mtime(&self, input_file: &String) -> Option<u64> {
let con = tokio::runtime::Runtime::new()
.unwrap()
.block_on(self.get_connection());
con.query_row(
"SELECT mtime
FROM units
WHERE input_file = (?1)",
[input_file],
|row| Ok(row.get_unwrap::<_, u64>(0)),
)
.ok()
}
/// Export a referenceable unit
pub fn export_ref_unit(&self, unit: &TranslationUnit, input: &String, output: &Option<String>) {
let con = tokio::runtime::Runtime::new()
.unwrap()
.block_on(self.get_connection());
// Find if unit reference key changed
if let Some(previous) =
con.query_row(
"SELECT reference_key
FROM referenceable_units
WHERE input_file = (?1)",
[input], |row| {
Ok(row.get_unwrap::<_, String>(0))
}).optional().unwrap() {
con.execute(
"DELETE
FROM referenceable_units
WHERE reference_key = (?1);",
[previous],
).unwrap();
}
// Delete previous unit-related data
con.execute(
"DELETE
FROM referenceable_units
WHERE reference_key = (?1);",
[unit.reference_key()],
)
.unwrap();
// Insert new unit
con.execute(
"INSERT OR REPLACE
INTO referenceable_units
(reference_key, input_file, output_file)
VALUES
(?1, ?2, ?3)",
(unit.reference_key(), input, output),
)
.unwrap();
}
/// Query reference from the cache
pub fn query_reference(&self, unit: &DatabaseUnit, name: &str) -> Option<Reference> {
let con = tokio::runtime::Runtime::new()
.unwrap()
.block_on(self.get_connection());
con.query_row(
"SELECT name, token_start, token_end, type, link
FROM exported_references
WHERE name = (?1) AND unit_ref = (?2)",
[name, &unit.reference_key],
|row| {
Ok(Reference {
refname: row.get_unwrap(0),
refkey: row.get_unwrap(3),
source_unit: unit.input_file.clone(),
token: row.get_unwrap(1)..row.get_unwrap(2),
link: row.get_unwrap(4),
})
},
)
.ok()
}
/// Gets all exported references in the project
pub async fn get_references(&self) -> Vec<LsReference> {
let con = self.get_connection().await;
let mut stmt = con
.prepare(
"SELECT
name, unit_ref, token_start, token_end, type, ru.input_file
FROM exported_references
LEFT JOIN referenceable_units ru ON unit_ref = ru.reference_key;",
)
.unwrap();
let res = stmt
.query_map((), |row| {
Ok(LsReference {
name: row.get_unwrap::<_, String>(0),
range: row.get_unwrap::<_, usize>(2)..row.get_unwrap::<_, usize>(3),
source_path: row.get_unwrap::<_, String>(5),
source_refkey: row.get_unwrap::<_, String>(1),
reftype: row.get_unwrap::<_, String>(4),
})
})
.unwrap();
let mut result = vec![];
for r in res {
let Ok(reference) = r else { continue };
result.push(reference);
}
result
}
pub fn export_references<'a, I>(&self, reference_key: &String, refs: I) -> Result<(), String>
where
I: Iterator<Item = (&'a String, &'a Arc<dyn ReferenceableElement>)>,
{
let con = tokio::runtime::Runtime::new()
.unwrap()
.block_on(self.get_connection());
let mut stmt = con
.prepare(
"INSERT OR REPLACE
INTO exported_references (name, unit_ref, token_start, token_end, type, data, link)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);",
)
.unwrap();
for (name, reference) in refs {
// FIXME: Proper type-erased serialization for referneceables
let serialized = "TODO";
let range = reference.original_location().range;
stmt.execute(params![
name,
reference_key,
range.start,
range.end,
reference.refcount_key(),
serialized,
reference.get_link().unwrap().clone(),
])
.map_err(|err| {
format!(
"Failed to insert reference ({name}, {serialized}, {0}): {err:#?}",
reference_key
)
})?;
}
Ok(())
}
/// Export dependencies to the cache, returns units that are missing dependencies
pub fn export_dependencies(
&self,
deps: &HashMap<String, HashMap<String, Vec<UnitDependency>>>,
) -> HashMap<String, Vec<UnitDependency>> {
let mut con = tokio::runtime::Runtime::new()
.unwrap()
.block_on(self.get_connection());
let tx = con.transaction().unwrap();
let delete_stmt = tx
.prepare(
"DELETE
FROM dependencies
WHERE unit_ref = (?1);",
)
.unwrap();
let mut export_stmt = tx
.prepare(
"INSERT OR REPLACE
INTO dependencies (unit_ref, depends_on, range_start, range_end, depends_for)
VALUES (?1, ?2, ?3, ?4, ?5);",
)
.unwrap();
// Export new dependencies
for (unit_ref, map) in deps {
//delete_stmt.execute([unit_ref]).unwrap();
for (depends_on, list) in map {
for dep in list {
export_stmt
.execute(params!(
unit_ref,
depends_on,
dep.range.start,
dep.range.end,
dep.depends_for
))
.unwrap();
}
}
}
drop(export_stmt);
drop(delete_stmt);
tx.commit().unwrap();
// Populate missing
let mut update = con
.prepare(
"SELECT DISTINCT ru.input_file, dep.range_start, dep.range_end, dep.depends_for
FROM dependencies AS dep
JOIN referenceable_units AS ru
ON ru.reference_key = dep.unit_ref
WHERE NOT EXISTS (
SELECT 1
FROM exported_references AS ref
WHERE ref.unit_ref = dep.depends_on
AND ref.name = dep.depends_for
);",
)
.unwrap();
let mut missing: HashMap<String, Vec<UnitDependency>> = HashMap::new();
update
.query_map([], |row| {
Ok((
row.get_unwrap::<_, String>(0),
row.get_unwrap::<_, usize>(1),
row.get_unwrap::<_, usize>(2),
row.get_unwrap::<_, String>(3),
))
})
.unwrap()
.into_iter()
.map(|v| {
let Ok((unit_ref, start, end, depends_for)) = v else {
panic!()
};
if let Some(list) = missing.get_mut(&unit_ref) {
list.push(UnitDependency {
depends_for,
range: start..end,
});
} else {
missing.insert(
unit_ref,
vec![UnitDependency {
depends_for,
range: start..end,
}],
);
}
})
.count();
missing
}
}

View file

@ -1,209 +1,295 @@
use std::sync::Arc;
use std::cell::Ref;
use std::cell::RefCell;
use std::cell::RefMut;
use std::collections::HashMap;
use std::rc::Rc;
use parking_lot::RwLock;
use rusqlite::Connection;
use crate::cache::cache::Cache;
use crate::parser::reports::Report;
use crate::unit::element::nested_kind;
use crate::unit::element::ElemKind;
use crate::unit::scope::Scope;
use crate::unit::scope::ScopeAccessor;
use crate::unit::translation::TranslationAccessors;
use crate::unit::translation::TranslationUnit;
use crate::util::settings::ProjectOutput;
use super::output::CompilerOutput;
use super::sanitize::Sanitizer;
use crate::document::document::Document;
use crate::document::document::ElemReference;
use crate::document::variable::Variable;
#[derive(Clone, Copy)]
pub enum Target {
HTML,
#[allow(unused)]
LATEX,
}
impl From<&ProjectOutput> for Target {
fn from(value: &ProjectOutput) -> Self {
match value {
ProjectOutput::Html(_) => Target::HTML,
}
}
}
pub struct Compiler {
target: Target,
cache: Arc<Cache>,
sanitizer: Sanitizer,
cache: Option<RefCell<Connection>>,
reference_count: RefCell<HashMap<String, HashMap<String, usize>>>,
// TODO: External references, i.e resolved later
sections_counter: RefCell<Vec<usize>>,
}
impl Compiler {
pub fn new(target: Target, cache: Arc<Cache>) -> Self {
pub fn new(target: Target, db_path: Option<String>) -> Self {
let cache = match db_path {
None => None,
Some(path) => match Connection::open(path) {
Err(e) => panic!("Cannot connect to database: {e}"),
Ok(con) => Some(con),
},
};
Self {
target: target,
cache,
sanitizer: Sanitizer::new(target),
target,
cache: cache.map(|con| RefCell::new(con)),
reference_count: RefCell::new(HashMap::new()),
sections_counter: RefCell::new(vec![]),
}
}
/// Gets the sanitizer for this compiler
pub fn sanitizer(&self) -> Sanitizer {
self.sanitizer
/// Gets the section counter for a given depth
/// This function modifies the section counter
pub fn section_counter(&self, depth: usize) -> Ref<'_, Vec<usize>>
{
// Increment current counter
if self.sections_counter.borrow().len() == depth {
self.sections_counter.borrow_mut().last_mut()
.map(|id| *id += 1);
return Ref::map(self.sections_counter.borrow(), |b| &*b);
}
// Close
while self.sections_counter.borrow().len() > depth {
self.sections_counter.borrow_mut().pop();
}
// Open
while self.sections_counter.borrow().len() < depth {
self.sections_counter.borrow_mut().push(1);
}
Ref::map(self.sections_counter.borrow(), |b| &*b)
}
/// Sanitizes text using this compiler's [`sanitizer`]
pub fn sanitize<S: AsRef<str>>(&self, str: S) -> String {
self.sanitizer.sanitize(str)
}
/// Sanitizes a format string for a [`Target`]
///
/// # Notes
///
/// This function may process invalid format string, which will be caught later
/// by runtime_format.
pub fn sanitize_format<S: AsRef<str>>(&self, str: S) -> String {
self.sanitizer.sanitize_format(str)
/// Sanitizes text for a [`Target`]
pub fn sanitize<S: AsRef<str>>(target: Target, str: S) -> String {
match target {
Target::HTML => str
.as_ref()
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;"),
_ => todo!("Sanitize not implemented"),
}
}
/// Gets a reference name
pub fn refname<S: AsRef<str>>(&self, str: S) -> String {
self.sanitizer.sanitize(str).replace(' ', "_")
pub fn refname<S: AsRef<str>>(target: Target, str: S) -> String {
Self::sanitize(target, str).replace(' ', "_")
}
/// Gets the output target of this compiler
pub fn target(&self) -> Target {
self.target
}
/// Inserts or get a reference id for the compiled document
///
/// # Parameters
/// - [`reference`] The reference to get or insert
pub fn reference_id<'a>(&self, document: &'a dyn Document, reference: ElemReference) -> usize {
let mut borrow = self.reference_count.borrow_mut();
let reference = document.get_from_reference(&reference).unwrap();
let refkey = reference.refcount_key();
let refname = reference.reference_name().unwrap();
pub fn compile_scope(
&self,
mut output: CompilerOutput,
scope: Arc<RwLock<Scope>>,
) -> CompilerOutput {
let mut reports = vec![];
for (scope, elem) in scope.content_iter(false) {
if nested_kind(elem.clone()) == ElemKind::Inline && !output.in_paragraph(&scope) {
match self.target {
Target::HTML => output.add_content("<p>"),
Target::LATEX => todo!(),
}
output.set_paragraph(&scope, true);
} else if output.in_paragraph(&scope) && nested_kind(elem.clone()) != ElemKind::Inline {
match self.target {
Target::HTML => output.add_content("</p>"),
Target::LATEX => todo!(),
}
output.set_paragraph(&scope, false);
let map = match borrow.get_mut(refkey) {
Some(map) => map,
None => {
borrow.insert(refkey.to_string(), HashMap::new());
borrow.get_mut(refkey).unwrap()
}
};
if let Err(mut reps) = elem.compile(scope, self, &mut output) {
reports.extend(reps.drain(..));
}
if let Some(elem) = map.get(refname) {
*elem
} else {
// Insert new ref
let index = map
.iter()
.fold(0, |max, (_, value)| std::cmp::max(max, *value));
map.insert(refname.clone(), index + 1);
index + 1
}
println!("Output={}", output.content());
output
}
fn header(&self, unit: &TranslationUnit) -> String {
let settings = unit.get_settings();
pub fn target(&self) -> Target { self.target }
match self.target {
pub fn cache(&self) -> Option<RefMut<'_, Connection>> {
self.cache.as_ref().map(RefCell::borrow_mut)
}
pub fn header(&self, document: &dyn Document) -> String {
pub fn get_variable_or_error(
document: &dyn Document,
var_name: &'static str,
) -> Option<Rc<dyn Variable>> {
document
.get_variable(var_name)
.and_then(|var| Some(var))
.or_else(|| {
println!(
"Missing variable `{var_name}` in {}",
document.source().name()
);
None
})
}
let mut result = String::new();
match self.target() {
Target::HTML => {
let ProjectOutput::Html(html) = &settings.output else {
panic!("Invalid project settings")
};
let css = if let Some(css) = &html.css {
format!(
result += "<!DOCTYPE HTML><html><head>";
result += "<meta charset=\"UTF-8\">";
if let Some(page_title) = get_variable_or_error(document, "html.page_title") {
result += format!(
"<title>{}</title>",
Compiler::sanitize(self.target(), page_title.to_string())
)
.as_str();
}
if let Some(css) = document.get_variable("html.css") {
result += format!(
"<link rel=\"stylesheet\" href=\"{}\">",
self.sanitize(css.as_str())
Compiler::sanitize(self.target(), css.to_string())
)
} else {
"".into()
};
let icon = if let Some(icon) = &html.icon {
format!(
"<link rel=\"icon\" href=\"{}\">",
self.sanitize(icon.as_str())
)
} else {
"".into()
};
format!(
"<!DOCTYPE html><html lang=\"{}\"><meta charset=\"utf-8\">{icon}{css}<head></head><body>",
self.sanitize(html.language.as_str())
)
.as_str();
}
result += r#"</head><body><div class="layout">"#;
// TODO: TOC
// TODO: Author, Date, Title, Div
}
_ => todo!(),
Target::LATEX => {}
}
result
}
fn footer(&self, unit: &TranslationUnit) -> String {
let settings = unit.get_settings();
match self.target {
Target::HTML => "</body></html>".into(),
_ => todo!(),
pub fn footer(&self, _document: &dyn Document) -> String {
let mut result = String::new();
match self.target() {
Target::HTML => {
result += "</div></body></html>";
}
Target::LATEX => todo!(""),
}
result
}
/// Compiles a document to it's output
pub fn compile(&self, unit: &TranslationUnit) -> Result<(), Vec<Report>> {
match CompilerOutput::run_with_processor(self.target, &unit.colors(), |output| {
self.compile_scope(output, unit.get_entry_scope().to_owned())
}) {
Ok(output) => {
println!("output={:#?}", output.content);
Ok(())
}
Err(reports) => Err(reports),
}
/*
pub fn compile(&self, document: &dyn Document) -> CompiledDocument {
let borrow = document.content().borrow();
// Header
let header = self.header(document);
// Body
let output = CompilerOutput::run_with_processor(colors, |mut output| {
{
output.add_content(r#"<div class="content">"#);
for elem in borrow.iter() {
if let Err(reports) = elem.compile(self, document, &mut output) {
Report::reports_to_stdout(colors, reports);
};
}
output.add_content(r#"</div>"#);
let mut body = r#"<div class="content">"#.to_string();
for i in 0..borrow.len() {
let elem = &borrow[i];
match elem.compile(self, document) {
Ok(result) => body.push_str(result.as_str()),
Err(err) => println!("Unable to compile element: {err}\n{elem:#?}"),
}
output
});
}
body.push_str("</div>");
// Footer
let footer = self.footer(document);
output.to_compiled(self, document, header, footer)
*/
}
// Variables
let variables = document
.scope()
.borrow_mut()
.variables
.iter()
.map(|(key, var)| (key.clone(), var.to_string()))
.collect::<HashMap<String, String>>();
pub fn get_cache(&self) -> Arc<Cache> {
self.cache.clone()
CompiledDocument {
input: document.source().name().clone(),
mtime: 0,
variables,
header,
body,
footer,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
pub struct CompiledDocument {
/// Input path relative to the input directory
pub input: String,
/// Modification time (i.e seconds since last epoch)
pub mtime: u64,
#[test]
fn sanitize_test() {
let sanitizer = Sanitizer::new(Target::HTML);
// TODO: Also store exported references
// so they can be referenced from elsewhere
// This will also require rebuilding in case some exported references have changed...
/// Variables exported to string, so they can be querried later
pub variables: HashMap<String, String>,
assert_eq!(sanitizer.sanitize("<a>"), "&lt;a&gt;");
assert_eq!(sanitizer.sanitize("&lt;"), "&amp;lt;");
assert_eq!(sanitizer.sanitize("\""), "&quot;");
/// Compiled document's header
pub header: String,
/// Compiled document's body
pub body: String,
/// Compiled document's footer
pub footer: String,
}
assert_eq!(sanitizer.sanitize_format("{<>&\"}"), "{<>&\"}");
assert_eq!(sanitizer.sanitize_format("{{<>}}"), "{{&lt;&gt;}}");
assert_eq!(sanitizer.sanitize_format("{{<"), "{{&lt;");
impl CompiledDocument {
pub fn get_variable(&self, name: &str) -> Option<&String> { self.variables.get(name) }
fn sql_table() -> &'static str {
"CREATE TABLE IF NOT EXISTS compiled_documents (
input TEXT PRIMARY KEY,
mtime INTEGER NOT NULL,
variables TEXT NOT NULL,
header TEXT NOT NULL,
body TEXT NOT NULL,
footer TEXT NOT NULL
);"
}
fn sql_get_query() -> &'static str { "SELECT * FROM compiled_documents WHERE input = (?1)" }
fn sql_insert_query() -> &'static str {
"INSERT OR REPLACE INTO compiled_documents (input, mtime, variables, header, body, footer) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
}
pub fn init_cache(con: &Connection) -> Result<usize, rusqlite::Error> {
con.execute(Self::sql_table(), [])
}
pub fn from_cache(con: &Connection, input: &str) -> Option<Self> {
con.query_row(Self::sql_get_query(), [input], |row| {
Ok(CompiledDocument {
input: input.to_string(),
mtime: row.get_unwrap::<_, u64>(1),
variables: serde_json::from_str(row.get_unwrap::<_, String>(2).as_str()).unwrap(),
header: row.get_unwrap::<_, String>(3),
body: row.get_unwrap::<_, String>(4),
footer: row.get_unwrap::<_, String>(5),
})
})
.ok()
}
/// Inserts [`CompiledDocument`] into cache
pub fn insert_cache(&self, con: &Connection) -> Result<usize, rusqlite::Error> {
con.execute(
Self::sql_insert_query(),
(
&self.input,
&self.mtime,
serde_json::to_string(&self.variables).unwrap(),
&self.header,
&self.body,
&self.footer,
),
)
}
}

View file

@ -1,59 +0,0 @@
use std::collections::HashSet;
use crate::unit::unit::OffloadedUnit;
use crate::unit::unit::Reference;
use super::compiler::Target;
/// Gets a unique link for a string for a given target
pub fn get_unique_link(
target: Target,
used_links: &mut HashSet<String>,
refname: &String,
) -> String {
// Replace illegal characters
let mut transformed = match target {
Target::HTML => refname
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' {
c
} else if c == ' ' {
'-'
} else {
'_'
}
})
.collect::<String>(),
_ => todo!(),
};
// Ensure uniqueness
while used_links.contains(&transformed) {
match target {
Target::HTML => transformed.push('_'),
_ => todo!(),
}
}
used_links.insert(transformed.clone());
transformed
}
/// Translate link from source unit to target unit
pub fn translate_reference(
target: Target,
from: &OffloadedUnit,
to: &OffloadedUnit,
reference: &Reference,
) -> String {
match target {
Target::HTML => {
if from.output_path() == to.output_path() {
format!("#{}", reference.link)
} else {
format!("{}#{}", from.output_path(), reference.link)
}
}
_ => todo!(),
}
}

View file

@ -1,6 +1,2 @@
pub mod compiler;
pub mod links;
pub mod navigation;
pub mod output;
pub mod process;
pub mod sanitize;

View file

@ -1,60 +1,45 @@
/*
use super::compiler::Target;
use super::sanitize::Sanitizer;
use std::collections::HashMap;
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct NavEntry {
title: String,
path: String,
previous: Option<String>,
}
use crate::compiler::compiler::Compiler;
use super::compiler::CompiledDocument;
use super::compiler::Target;
#[derive(Debug, Default)]
pub struct NavEntries {
pub(self) entries: Vec<NavEntry>,
pub(self) children: HashMap<String, NavEntries>,
pub struct NavEntry {
pub(self) entries: Vec<(String, String, Option<String>)>,
pub(self) children: HashMap<String, NavEntry>,
}
impl NavEntries {
impl NavEntry {
// FIXME: Sanitize
pub fn compile(&self, sanitizer: Sanitizer, doc: &RefCell<CompiledDocument>) -> String {
let doc_borrow = doc.borrow();
pub fn compile(&self, target: Target, doc: &CompiledDocument) -> String {
let categories = vec![
doc_borrow
.get_variable("nav.category")
.map_or("", |s| s.as_str()),
doc_borrow
.get_variable("nav.subcategory")
doc.get_variable("nav.category").map_or("", |s| s.as_str()),
doc.get_variable("nav.subcategory")
.map_or("", |s| s.as_str()),
];
let mut result = String::new();
match sanitizer.target() {
match target {
Target::HTML => {
result += r#"<input id="navbar-checkbox" class="toggle" type="checkbox" style="display:none" checked><div id="navbar"><ul>"#;
result += r#"<div class="navbar"><ul>"#;
fn process(
sanitizer: Sanitizer,
doc_path: &String,
target: Target,
categories: &Vec<&str>,
did_match: bool,
result: &mut String,
entry: &NavEntries,
entry: &NavEntry,
depth: usize,
) {
// Orphans = Links
for entry in &entry.entries {
let style = if doc_path == &entry.path {
" class=\"navbar-entry-current\""
} else {
" class=\"navbar-entry\""
}
.to_string();
for (title, path, _) in &entry.entries {
result.push_str(
format!(
r#"<li {style}><a href="{}">{}</a></li>"#,
sanitizer.sanitize(entry.path.as_str()),
sanitizer.sanitize(entry.title.as_str())
r#"<li><a href="{}">{}</a></li>"#,
Compiler::sanitize(target, path),
Compiler::sanitize(target, title)
)
.as_str(),
);
@ -70,39 +55,21 @@ impl NavEntries {
result.push_str("<li>");
result.push_str(
format!(
"<details{}><summary class=\"navbar-category\">{}</summary>",
"<details{}><summary>{}</summary>",
["", " open"][is_match as usize],
sanitizer.sanitize(name)
Compiler::sanitize(target, name)
)
.as_str(),
);
result.push_str("<ul>");
process(
sanitizer,
doc_path,
categories,
is_match,
result,
ent,
depth + 1,
);
process(target, categories, is_match, result, ent, depth + 1);
result.push_str("</ul></details></li>");
}
}
process(
sanitizer,
doc_borrow
.get_variable("compiler.output")
.unwrap_or(&String::new()),
&categories,
true,
&mut result,
self,
0,
);
process(target, &categories, true, &mut result, self, 0);
result += r#"</ul></div><label for="navbar-checkbox" class="navbar-checkbox-label">&#9776;</label>"#;
result += r#"</ul></div>"#;
}
_ => todo!(""),
}
@ -110,37 +77,32 @@ impl NavEntries {
}
fn sort_entry(
entrymap: &HashMap<String, Option<String>>,
left_title: &str,
right_title: &str,
left: &(String, String, Option<String>),
right: &(String, String, Option<String>),
) -> std::cmp::Ordering {
let lp = entrymap.get(left_title).unwrap();
let rp = entrymap.get(right_title).unwrap();
if lp.clone().map(|s| s.as_str() == right_title) == Some(true) {
std::cmp::Ordering::Greater
} else if rp.clone().map(|s| s.as_str() == left_title) == Some(true) {
std::cmp::Ordering::Less
} else if lp.is_some() && rp.is_none() {
std::cmp::Ordering::Greater
} else if rp.is_some() && lp.is_none() {
std::cmp::Ordering::Less
} else if let (Some(pl), Some(pr)) = (lp, rp) {
if pl == pr {
left_title.cmp(right_title)
} else {
Self::sort_entry(entrymap, pl, pr)
match (&left.2, &right.2) {
(Some(_), Some(_)) => left.0.cmp(&right.0),
(Some(lp), None) => {
if &right.0 == lp {
std::cmp::Ordering::Greater
} else {
left.0.cmp(&right.0)
}
}
} else {
left_title.cmp(right_title)
(None, Some(rp)) => {
if &left.0 == rp {
std::cmp::Ordering::Less
} else {
left.0.cmp(&right.0)
}
}
(None, None) => left.0.cmp(&right.0),
}
}
}
pub fn create_navigation(
docs: &Vec<(RefCell<CompiledDocument>, Option<PostProcess>)>,
) -> Result<NavEntries, String> {
let mut nav = NavEntries {
pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<NavEntry, String> {
let mut nav = NavEntry {
entries: vec![],
children: HashMap::new(),
};
@ -148,20 +110,19 @@ pub fn create_navigation(
// All paths (for duplicate checking)
let mut all_paths = HashMap::new();
for (doc, _) in docs {
let doc_borrow = doc.borrow();
let cat = doc_borrow.get_variable("nav.category");
let subcat = doc_borrow.get_variable("nav.subcategory");
let title = doc_borrow
for doc in docs {
let cat = doc.get_variable("nav.category");
let subcat = doc.get_variable("nav.subcategory");
let title = doc
.get_variable("nav.title")
.or(doc_borrow.get_variable("doc.title"));
let previous = doc_borrow.get_variable("nav.previous").cloned();
let path = doc_borrow.get_variable("compiler.output");
.or(doc.get_variable("doc.title"));
let previous = doc.get_variable("nav.previous").map(|s| s.clone());
let path = doc.get_variable("compiler.output");
let (title, path) = match (title, path) {
(Some(title), Some(path)) => (title, path),
_ => {
eprintln!("Skipping navigation generation for `{}`, must have a defined `@nav.title` and `@compiler.output`", doc_borrow.input);
eprintln!("Skipping navigation generation for `{}`, must have a defined `@nav.title` and `@compiler.output`", doc.input);
continue;
}
};
@ -173,7 +134,7 @@ pub fn create_navigation(
None => {
eprintln!(
"Skipping `{}`: No `@nav.category`, but `@nav.subcategory` is set",
doc_borrow.input
doc.input
);
continue;
}
@ -183,7 +144,7 @@ pub fn create_navigation(
Some(cat_ent) => cat_ent,
None => {
// Insert
nav.children.insert(cat.clone(), NavEntries::default());
nav.children.insert(cat.clone(), NavEntry::default());
nav.children.get_mut(cat.as_str()).unwrap()
}
};
@ -192,9 +153,7 @@ pub fn create_navigation(
Some(subcat_ent) => subcat_ent,
None => {
// Insert
cat_ent
.children
.insert(subcat.clone(), NavEntries::default());
cat_ent.children.insert(subcat.clone(), NavEntry::default());
cat_ent.children.get_mut(subcat.as_str()).unwrap()
}
}
@ -203,7 +162,7 @@ pub fn create_navigation(
Some(cat_ent) => cat_ent,
None => {
// Insert
nav.children.insert(cat.clone(), NavEntries::default());
nav.children.insert(cat.clone(), NavEntry::default());
nav.children.get_mut(cat.as_str()).unwrap()
}
}
@ -212,13 +171,13 @@ pub fn create_navigation(
};
// Find duplicates titles in current parent
for entry in &pent.entries {
if &entry.title == title {
for (ent_title, _, _) in &pent.entries {
if ent_title == title {
return Err(format!(
"Conflicting entry title `{title}` for entries with the same parent: ({})",
pent.entries
.iter()
.map(|entry| entry.title.clone())
.map(|(title, _, _)| title.clone())
.collect::<Vec<_>>()
.join(", ")
));
@ -231,24 +190,15 @@ pub fn create_navigation(
}
all_paths.insert(path.clone(), title.clone());
pent.entries.push(NavEntry {
title: title.clone(),
path: path.clone(),
previous,
});
pent.entries.push((title.clone(), path.clone(), previous));
}
// Sort entries
fn sort_entries(nav: &mut NavEntries) {
let entrymap = nav
.entries
.iter()
.map(|ent| (ent.title.clone(), ent.previous.clone()))
.collect::<HashMap<String, Option<String>>>();
fn sort_entries(nav: &mut NavEntry) {
nav.entries
.sort_by(|l, r| NavEntries::sort_entry(&entrymap, l.title.as_str(), r.title.as_str()));
.sort_unstable_by(|l, r| NavEntry::sort_entry(l, r));
for child in nav.children.values_mut() {
for (_, child) in &mut nav.children {
sort_entries(child);
}
}
@ -256,116 +206,33 @@ pub fn create_navigation(
Ok(nav)
}
*/
/*
#[cfg(test)]
mod tests {
use rand::prelude::SliceRandom;
use rand::rngs::OsRng;
use crate::compiler::process::process_from_memory;
use rand::RngCore;
use super::*;
#[test]
fn sort() {
let entries: Vec<NavEntry> = vec![
NavEntry {
title: "Index".into(),
path: "".into(),
previous: None,
},
NavEntry {
title: "AB".into(),
path: "".into(),
previous: Some("Index".into()),
},
NavEntry {
title: "Getting Started".into(),
path: "".into(),
previous: Some("Index".into()),
},
NavEntry {
title: "Sections".into(),
path: "".into(),
previous: Some("Getting Started".into()),
},
NavEntry {
title: "Style".into(),
path: "".into(),
previous: Some("Getting Started".into()),
},
let entries: Vec<(String, String, Option<String>)> = vec![
("Index".into(), "".into(), None),
("AB".into(), "".into(), Some("Index".into())),
("Getting Started".into(), "".into(), Some("Index".into())),
("Sections".into(), "".into(), Some("Getting Started".into())),
("Style".into(), "".into(), Some("Getting Started".into())),
];
let mut shuffled = entries.clone();
for _ in 0..10 {
let mut rng = OsRng {};
shuffled.shuffle(&mut rng);
for i in 0..5 {
let pos = OsRng.next_u64() % entries.len() as u64;
shuffled.swap(i, pos as usize);
}
let entrymap = shuffled
.iter()
.map(|ent| (ent.title.clone(), ent.previous.clone()))
.collect::<HashMap<String, Option<String>>>();
shuffled.sort_by(|l, r| {
NavEntries::sort_entry(&entrymap, l.title.as_str(), r.title.as_str())
});
shuffled.sort_by(|l, r| NavEntry::sort_entry(l, r));
assert_eq!(shuffled, entries);
}
}
#[test]
pub fn batch() {
let result = process_from_memory(
Target::HTML,
vec![
r#"
@html.page_title = 0
@compiler.output = 0.html
@nav.title = C
@nav.category = First
"#
.into(),
r#"
@html.page_title = 1
@compiler.output = 1.html
@nav.title = A
@nav.category = First
"#
.into(),
r#"
@html.page_title = 2
@compiler.output = 2.html
@nav.title = B
@nav.category = First
"#
.into(),
],
)
.unwrap();
let nav = create_navigation(&result).unwrap();
assert_eq!(
nav.children.get("First").unwrap().entries,
vec![
NavEntry {
title: "A".to_string(),
path: "1.html".to_string(),
previous: None
},
NavEntry {
title: "B".to_string(),
path: "2.html".to_string(),
previous: None
},
NavEntry {
title: "C".to_string(),
path: "0.html".to_string(),
previous: None
},
]
);
}
}
*/

View file

@ -1,229 +0,0 @@
use std::cell::OnceCell;
use std::collections::HashSet;
use std::future::Future;
use std::future::IntoFuture;
use std::hash::Hash;
use std::hash::Hasher;
use std::pin::Pin;
use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
use std::time::Instant;
use crate::parser::reports::macros::*;
use crate::parser::reports::*;
use ariadne::Color;
use ariadne::Fmt;
use parking_lot::RwLock;
use tokio::task::JoinHandle;
use crate::make_err;
use crate::parser::reports::Report;
use crate::parser::reports::ReportColors;
use crate::parser::source::Token;
use crate::unit::scope::Scope;
use super::compiler::Target;
#[derive(Debug)]
struct ArcKey<T>(Arc<RwLock<T>>);
impl<T> PartialEq for ArcKey<T> {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0) // Compare Rc pointer addresses
}
}
impl<T> Eq for ArcKey<T> {}
impl<T> Hash for ArcKey<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_usize(Arc::as_ptr(&self.0) as usize); // Hash the pointer address
}
}
/// Async task processed by the output
#[derive(Debug)]
pub struct OutputTask {
/// Task handle
handle: OnceCell<JoinHandle<Result<String, Vec<Report>>>>,
/// Task location in it's original scope
location: Token,
/// Task position in the final output
pos: usize,
/// Task name
name: String,
/// Task timeout in milliseconds
timeout: u128,
/// Task result
result: OnceCell<Result<String, Vec<Report>>>,
}
pub struct CompilerOutput {
/// Compilation target
target: Target,
/// Paragraph state of the output
paragraph: HashSet<ArcKey<Scope>>,
// Holds the content of the resulting document
pub(crate) content: String,
/// Holds the spawned async tasks. After the work function has completed, these tasks will be
/// waited on in order to insert their result in the compiled document
tasks: Vec<OutputTask>,
/// The tasks runtime
runtime: tokio::runtime::Runtime,
}
impl CompilerOutput {
/// Run work function `f` with the task processor running
///
/// The result of async taks will be inserted into the output
pub fn run_with_processor<F>(
target: Target,
colors: &ReportColors,
f: F,
) -> Result<CompilerOutput, Vec<Report>>
where
F: FnOnce(CompilerOutput) -> CompilerOutput,
{
// Create the output & the runtime
let mut output = Self {
target,
paragraph: HashSet::new(),
content: String::default(),
tasks: vec![],
runtime: tokio::runtime::Builder::new_multi_thread()
.worker_threads(8)
.enable_all()
.build()
.unwrap(),
};
// Process the document with caller work-function
output = f(output);
// Wait for all tasks to finish
let mut finished = 0;
let time_start = Instant::now();
while finished != output.tasks.len() {
for task in &mut output.tasks {
if task.result.get().is_some() || !task.handle.get().is_some() {
continue;
}
if task
.handle
.get()
.map_or(false, |handle| handle.is_finished())
{
output
.runtime
.block_on(async {
task.result
.set(task.handle.take().unwrap().into_future().await.unwrap())
})
.unwrap();
task.handle.take();
finished += 1;
continue;
} else if time_start.elapsed().as_millis() < task.timeout {
continue;
}
task.handle.get().unwrap().abort();
task.handle.take();
println!("Aborted task `{}`, timeout exceeded", task.name);
finished += 1;
}
println!(
"[{}/{}] Waiting for tasks... ({}ms)",
finished,
output.tasks.len(),
time_start.elapsed().as_millis()
);
sleep(Duration::from_millis(500));
}
// Check for errors
let mut reports = vec![];
for task in &mut output.tasks {
if task.result.get().is_some_and(Result::is_ok) {
continue;
}
if task.result.get().is_none() {
reports.push(make_err!(
task.location.source(),
"Task processing failed".into(),
span(
task.location.range.clone(),
format!(
"Processing for task `{}` timed out",
(&task.name).fg(Color::Green),
)
)
));
continue;
}
let Some(Err(mut err)) = task.result.take() else {
panic!()
};
reports.extend(err.drain(..));
}
if !reports.is_empty() {
return Err(reports);
}
// Insert tasks results into output & offset references positions
for (pos, content) in output
.tasks
.iter()
.rev()
.map(|task| (task.pos, task.result.get().unwrap().as_ref().unwrap()))
{
output.content.insert_str(pos, content.as_str());
}
Ok(output)
}
/// Appends content to the output
pub fn add_content<S: AsRef<str>>(&mut self, s: S) {
self.content.push_str(s.as_ref());
}
/// Adds an async task to the output. The task's result will be appended at the current output position
///
/// The task is a future that returns it's result in a string, or errors as a Vec of [`Report`]s
pub fn add_task<F>(&mut self, location: Token, name: String, task: Pin<Box<F>>)
where
F: Future<Output = Result<String, Vec<Report>>> + Send + 'static,
{
let handle = self.runtime.spawn(task);
self.tasks.push(OutputTask {
handle: OnceCell::from(handle),
location,
pos: self.content.len(),
name,
timeout: 1500,
result: OnceCell::default(),
});
}
pub fn content(&self) -> &String {
&self.content
}
pub fn in_paragraph(&mut self, scope: &Arc<RwLock<Scope>>) -> bool {
self.paragraph.contains(&ArcKey(scope.to_owned()))
}
pub fn set_paragraph(&mut self, scope: &Arc<RwLock<Scope>>, value: bool) {
if value {
self.paragraph.insert(ArcKey(scope.to_owned()));
} else {
self.paragraph.remove(&ArcKey(scope.to_owned()));
}
}
}

View file

@ -1,296 +0,0 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use ariadne::Color;
use ariadne::Fmt;
use crate::cache::cache::Cache;
use crate::parser::parser::Parser;
use crate::parser::reports::macros::*;
use crate::parser::reports::*;
use crate::parser::resolver::Resolver;
use crate::parser::source::SourceFile;
use crate::unit::scope::ScopeAccessor;
use crate::unit::translation::TranslationAccessors;
use crate::unit::translation::TranslationUnit;
use util::settings::ProjectSettings;
use super::compiler::Compiler;
use super::compiler::Target;
#[derive(Default)]
pub struct ProcessOptions {
pub debug_ast: bool,
}
#[derive(Debug)]
pub enum ProcessError {
GeneralError(String),
InputError(String, String),
LinkError(Vec<Report>),
CompileError(Vec<Report>),
}
pub enum ProcessOutputOptions {
/// Path to the directory
Directory(String),
/// Path to the output file
File(String),
}
/// Message for the queue
pub enum ProcessQueueMessage<'u> {
/// Source file being skipped
Skipped(&'u String),
/// Source file being parsed
Parsing(&'u String),
/// Unit being resolved
Resolving(&'u TranslationUnit),
/// Unit being compiled
Compiling(&'u TranslationUnit),
}
/// Displays message to stdout
pub fn output_message<'u>(message: ProcessQueueMessage<'u>, perc: f64) {
print!("[{: >3.0}%] ", perc * 100.0f64);
match message {
ProcessQueueMessage::Skipped(source) => {
println!("{}", format!("Skipping '{}'", source).fg(Color::Green))
}
ProcessQueueMessage::Parsing(source) => {
println!("{}", format!("Parsing '{}'", source).fg(Color::Green))
}
ProcessQueueMessage::Resolving(unit) => println!(
"{} {}",
format!("Resolving '{}'", unit.input_path()).fg(Color::Green),
format!("[{}]", unit.reference_key()).fg(Color::Blue)
),
ProcessQueueMessage::Compiling(unit) => println!(
"{}",
format!(
"Compiling '{}' -> '{}'",
unit.input_path(),
unit.output_path().unwrap()
)
.fg(Color::Green)
),
}
}
/// Processqueue for inputs
pub struct ProcessQueue {
settings: ProjectSettings,
inputs: Vec<PathBuf>,
outputs: Vec<()>,
cache: Arc<Cache>,
project_path: String,
parser: Arc<Parser>,
compiler: Compiler,
}
impl ProcessQueue {
pub fn new(
target: Target,
project_path: String,
settings: ProjectSettings,
inputs: Vec<PathBuf>,
) -> Self {
let cache = Arc::new(Cache::new(settings.db_path.as_str()).unwrap());
cache.setup_tables();
let parser = Arc::new(Parser::new());
let compiler = Compiler::new(target, cache.clone());
Self {
settings,
inputs,
outputs: vec![],
cache,
project_path,
parser,
compiler,
}
}
pub fn process(
&mut self,
output: ProcessOutputOptions,
options: ProcessOptions,
) -> Result<Vec<()>, ProcessError> {
match &output {
ProcessOutputOptions::Directory(dir) => {}
ProcessOutputOptions::File(file) => {
if self.inputs.len() > 1 {
Err(ProcessError::GeneralError("Single file specified with multiple inputs. Please specify a directory instead".into()))?
}
}
};
let mut processed = vec![];
for (idx, input) in self.inputs.iter().enumerate() {
let input_string =
input
.to_str()
.map(|s| s.to_string())
.ok_or(ProcessError::GeneralError(format!(
"Failed to convert {input:#?} to string"
)))?;
// Compute path
let Some(local_path) = pathdiff::diff_paths(&input_string, &self.project_path) else {
Err(ProcessError::InputError(
format!(
"Failed to compute local path. Base=`{:#?}` Input=`{input_string}`",
self.project_path
),
input_string,
))?
};
let Some(local_path) = local_path.to_str().map(|s| s.to_string()) else {
Err(ProcessError::InputError(
format!("Failed to translate `{local_path:#?}` to a string."),
input_string,
))?
};
// Get mtime
let meta = std::fs::metadata(input).map_err(|err| {
ProcessError::InputError(
input_string.clone(),
format!("Failed to get metadata for `{input_string}`: {err}"),
)
})?;
let mtime = meta
.modified()
.map(|e| e.duration_since(UNIX_EPOCH).unwrap().as_secs())
.map_err(|err| {
ProcessError::InputError(
input_string.clone(),
format!("Unable to query modification time for `{input_string}`: {err}"),
)
})?;
let prev_mtime = self.cache.get_mtime(&local_path.to_string()).unwrap_or(0);
if prev_mtime >= mtime {
output_message(
ProcessQueueMessage::Skipped(&input_string),
(1 + idx) as f64 / self.inputs.len() as f64,
);
continue;
}
output_message(
ProcessQueueMessage::Parsing(&input_string),
(1 + idx) as f64 / self.inputs.len() as f64,
);
// Create unit
let source = Arc::new(SourceFile::new(input_string.clone(), None).unwrap());
let unit =
TranslationUnit::new(local_path.clone(), self.parser.clone(), source, false, true);
let output_file = match &output {
ProcessOutputOptions::Directory(dir) => {
let basename = match local_path.rfind(|c| c == '.') {
Some(pos) => &local_path[0..pos],
None => &local_path,
};
format!("{dir}/{basename}.html")
}
ProcessOutputOptions::File(file) => {
let basename = match local_path.rfind(|c| c == '.') {
Some(pos) => &local_path[0..pos],
None => &local_path,
};
format!("{basename}.html")
}
};
let (reports, unit) = unit.consume(output_file);
Report::reports_to_stdout(unit.colors(), reports);
if options.debug_ast {
println!("{:#?}", unit.get_entry_scope());
}
processed.push(unit);
}
// Insert with time 0
self.cache.export_units(processed.iter(), 0);
// Resolve all references
let colors = ReportColors::with_colors();
let resolver = Resolver::new(&colors, self.cache.clone(), &processed)
.map_err(|err| ProcessError::LinkError(vec![err]))?;
resolver.resolve_links(self.cache.clone(), self.compiler.target());
for (idx, unit) in processed.iter().enumerate() {
output_message(
ProcessQueueMessage::Resolving(unit),
(1 + idx) as f64 / processed.len() as f64,
);
// Output references
unit.export_references(self.cache.clone())
.expect("Failed to export");
}
let dependencies = resolver
.resolve_references(self.cache.clone(), self.compiler.target())
.map_err(|err| ProcessError::LinkError(err))?;
let missing = self.cache.export_dependencies(&dependencies);
if !missing.is_empty() {
let mut reports = vec![];
missing.iter().for_each(|(unit_file, list)| {
for item in list {
let source = Arc::new(
SourceFile::new(format!("{}/{unit_file}", self.project_path), None)
.unwrap(),
);
reports.push(make_err!(
source.clone(),
"Missing references".into(),
span(
item.range.clone(),
format!(
"Reference `{}` no longer exists",
(&item.depends_for).fg(Color::Blue),
)
)
));
}
});
return Err(ProcessError::LinkError(reports));
}
// Apply settings
for unit in &mut processed {
unit.update_settings(self.settings.clone());
}
// Compile all units
let mut reports = vec![];
for (idx, unit) in processed.iter().enumerate() {
output_message(
ProcessQueueMessage::Compiling(unit),
(1 + idx) as f64 / processed.len() as f64,
);
match self.compiler.compile(unit) {
Ok(_) => {
// TODO
}
Err(err) => reports.extend(err),
}
}
if !reports.is_empty() {
return Err(ProcessError::CompileError(reports));
}
let time_now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
self.cache.export_units(processed.iter(), time_now);
Ok(vec![])
}
}

View file

@ -1,68 +0,0 @@
use super::compiler::Target;
/// The sanitizer is an object whose goal is to sanitize text for a given target
#[derive(Clone, Copy)]
pub struct Sanitizer {
target: Target,
}
impl Sanitizer {
pub fn new(target: Target) -> Self {
Self { target }
}
pub fn target(&self) -> Target {
self.target
}
pub fn sanitize<S: AsRef<str>>(&self, str: S) -> String {
match self.target {
Target::HTML => str
.as_ref()
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;"),
_ => todo!("Sanitize not implemented"),
}
}
pub fn sanitize_format<S: AsRef<str>>(&self, str: S) -> String {
match self.target {
Target::HTML => {
let mut out = String::new();
let mut braces = 0;
for c in str.as_ref().chars() {
if c == '{' {
out.push(c);
braces += 1;
continue;
} else if c == '}' {
out.push(c);
if braces != 0 {
braces -= 1;
}
continue;
}
// Inside format args
if braces % 2 == 1 {
out.push(c);
continue;
}
match c {
'&' => out += "&amp;",
'<' => out += "&lt;",
'>' => out += "&gt;",
'"' => out += "&quot;",
_ => out.push(c),
}
}
out
}
_ => todo!("Sanitize not implemented"),
}
}
}

291
src/document/document.rs Normal file
View file

@ -0,0 +1,291 @@
use std::cell::Ref;
use std::cell::RefCell;
use std::cell::RefMut;
use std::collections::hash_map::HashMap;
use std::rc::Rc;
use crate::parser::source::Source;
use super::element::Element;
use super::element::ReferenceableElement;
use super::variable::Variable;
#[derive(Debug, Clone, Copy)]
pub enum ElemReference {
Direct(usize),
// Reference nested inside another element, e.g [`Paragraph`] or [`Media`]
Nested(usize, usize),
}
#[derive(Debug)]
pub struct Scope {
/// List of all referenceable elements in current scope.
/// All elements in this should return a non empty
pub referenceable: HashMap<String, ElemReference>,
pub variables: HashMap<String, Rc<dyn Variable>>,
}
impl Scope {
pub fn new() -> Self {
Self {
referenceable: HashMap::new(),
variables: HashMap::new(),
}
}
pub fn merge(&mut self, other: &mut Scope, merge_as: &String, ref_offset: usize) {
match merge_as.is_empty() {
true => {
// References
self.referenceable.extend(other.referenceable.drain().map(
|(name, idx)| match idx {
ElemReference::Direct(index) => {
(name, ElemReference::Direct(index + ref_offset))
}
ElemReference::Nested(index, sub_index) => {
(name, ElemReference::Nested(index + ref_offset, sub_index))
}
},
));
// Variables
self.variables
.extend(other.variables.drain().map(|(name, var)| (name, var)));
}
false => {
// References
self.referenceable.extend(other.referenceable.drain().map(
|(name, idx)| match idx {
ElemReference::Direct(index) => (
format!("{merge_as}.{name}"),
ElemReference::Direct(index + ref_offset),
),
ElemReference::Nested(index, sub_index) => (
format!("{merge_as}.{name}"),
ElemReference::Nested(index + ref_offset, sub_index),
),
},
));
// Variables
self.variables.extend(
other
.variables
.drain()
.map(|(name, var)| (format!("{merge_as}.{name}"), var)),
);
}
}
}
}
pub trait Document<'a>: core::fmt::Debug {
/// Gets the document [`Source`]
fn source(&self) -> Rc<dyn Source>;
/// Gets the document parent (if it exists)
fn parent(&self) -> Option<&'a dyn Document<'a>>;
/// Gets the document content
/// The content is essentially the AST for the document
fn content(&self) -> &RefCell<Vec<Box<dyn Element>>>;
/// Gets the document [`Scope`]
fn scope(&self) -> &RefCell<Scope>;
/// Pushes a new element into the document's content
fn push(&self, elem: Box<dyn Element>) {
if let Some(refname) = elem
.as_referenceable()
.and_then(|reference| reference.reference_name())
{
self.scope().borrow_mut().referenceable.insert(
refname.clone(),
ElemReference::Direct(self.content().borrow().len()),
);
} else if let Some(container) = self
.content()
.borrow()
.last()
.and_then(|elem| elem.as_container())
{
// This is a hack that works thanks to the fact that at document end, a [`DocumentEnd`] elem is pushed
container
.contained()
.iter()
.enumerate()
.for_each(|(sub_idx, elem)| {
if let Some(refname) = elem
.as_referenceable()
.and_then(|reference| reference.reference_name())
{
self.scope().borrow_mut().referenceable.insert(
refname.clone(),
ElemReference::Nested(self.content().borrow().len() - 1, sub_idx),
);
}
});
}
self.content().borrow_mut().push(elem);
}
fn add_variable(&self, variable: Rc<dyn Variable>) {
self.scope()
.borrow_mut()
.variables
.insert(variable.name().to_string(), variable);
}
fn get_variable(&self, name: &str) -> Option<Rc<dyn Variable>> {
match self.scope().borrow().variables.get(name) {
Some(variable) => {
return Some(variable.clone());
}
// Continue search recursively
None => match self.parent() {
Some(parent) => return parent.get_variable(name),
// Not found
None => return None,
},
}
}
/// Merges [`other`] into [`self`]
///
/// # Parameters
///
/// If [`merge_as`] is None, references and variables from the other document are not merged into self
fn merge(
&self,
content: &RefCell<Vec<Box<dyn Element>>>,
scope: &RefCell<Scope>,
merge_as: Option<&String>,
) {
match merge_as {
Some(merge_as) => self.scope().borrow_mut().merge(
&mut *scope.borrow_mut(),
merge_as,
self.content().borrow().len(),
),
_ => {}
}
// Content
self.content()
.borrow_mut()
.extend((content.borrow_mut()).drain(..).map(|value| value));
}
fn get_reference(&self, refname: &str) -> Option<ElemReference> {
self.scope()
.borrow()
.referenceable
.get(refname)
.and_then(|reference| Some(*reference))
}
fn get_from_reference(
&self,
reference: &ElemReference,
) -> Option<Ref<'_, dyn ReferenceableElement>> {
match reference {
ElemReference::Direct(idx) => Ref::filter_map(self.content().borrow(), |content| {
content.get(*idx).and_then(|elem| elem.as_referenceable())
})
.ok(),
ElemReference::Nested(idx, sub_idx) => {
Ref::filter_map(self.content().borrow(), |content| {
content
.get(*idx)
.and_then(|elem| elem.as_container())
.and_then(|container| container.contained().get(*sub_idx))
.and_then(|elem| elem.as_referenceable())
})
.ok()
}
}
}
}
pub trait DocumentAccessors<'a> {
fn last_element<T: Element>(&self) -> Option<Ref<'_, T>>;
fn last_element_mut<T: Element>(&self) -> Option<RefMut<'_, T>>;
}
impl<'a> DocumentAccessors<'a> for dyn Document<'a> + '_ {
fn last_element<T: Element>(&self) -> Option<Ref<'_, T>> {
Ref::filter_map(self.content().borrow(), |content| {
content.last().and_then(|last| last.downcast_ref::<T>())
})
.ok()
}
fn last_element_mut<T: Element>(&self) -> Option<RefMut<'_, T>> {
RefMut::filter_map(self.content().borrow_mut(), |content| {
content.last_mut().and_then(|last| last.downcast_mut::<T>())
})
.ok()
}
}
#[cfg(test)]
pub mod tests {
#[macro_export]
macro_rules! validate_document {
($container:expr, $idx:expr,) => {};
($container:expr, $idx:expr, $t:ty; $($tail:tt)*) => {{
let elem = &$container[$idx];
assert!(elem.downcast_ref::<$t>().is_some(), "Invalid element at index {}, expected {}, got: {elem:#?}", $idx, stringify!($t));
validate_document!($container, ($idx+1), $($tail)*);
}};
($container:expr, $idx:expr, $t:ty { $($field:ident == $value:expr),* }; $($tail:tt)*) => {{
let elem = &$container[$idx];
assert!(elem.downcast_ref::<$t>().is_some(), "Invalid element at index {}, expected {}, got: {elem:#?}", $idx, stringify!($t));
$(
let val = &elem.downcast_ref::<$t>().unwrap().$field;
assert!(*val == $value, "Invalid field {} for {} at index {}, expected {:#?}, found {:#?}",
stringify!($field),
stringify!($t),
$idx,
$value,
val);
)*
validate_document!($container, ($idx+1), $($tail)*);
}};
($container:expr, $idx:expr, $t:ty { $($ts:tt)* }; $($tail:tt)*) => {{
let elem = &$container[$idx];
assert!(elem.downcast_ref::<$t>().is_some(), "Invalid container element at index {}, expected {}", $idx, stringify!($t));
let contained = elem.as_container().unwrap().contained();
validate_document!(contained, 0, $($ts)*);
validate_document!($container, ($idx+1), $($tail)*);
}};
($container:expr, $idx:expr, $t:ty { $($field:ident == $value:expr),* } { $($ts:tt)* }; $($tail:tt)*) => {{
let elem = &$container[$idx];
assert!(elem.downcast_ref::<$t>().is_some(), "Invalid element at index {}, expected {}, got: {elem:#?}", $idx, stringify!($t));
$(
let val = &elem.downcast_ref::<$t>().unwrap().$field;
assert!(*val == $value, "Invalid field {} for {} at index {}, expected {:#?}, found {:#?}",
stringify!($field),
stringify!($t),
$idx,
$value,
val);
)*
let contained = elem.as_container().unwrap().contained();
validate_document!(contained, 0, $($ts)*);
validate_document!($container, ($idx+1), $($tail)*);
}};
}
}

95
src/document/element.rs Normal file
View file

@ -0,0 +1,95 @@
use std::str::FromStr;
use crate::compiler::compiler::Compiler;
use crate::elements::reference::Reference;
use crate::parser::source::Token;
use downcast_rs::impl_downcast;
use downcast_rs::Downcast;
use super::document::Document;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ElemKind {
/// An invisible element (i.e comment)
Invisible,
/// Special elements don't trigger special formatting events
Special,
/// Inline elements don't break paragraphing
Inline,
/// Block elements are outside of paragraphs
Block,
}
impl FromStr for ElemKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"invisible" => Ok(ElemKind::Invisible),
"special" => Ok(ElemKind::Special),
"inline" => Ok(ElemKind::Inline),
"block" => Ok(ElemKind::Block),
_ => Err(format!("Unknown ElemKind: {s}")),
}
}
}
pub trait Element: Downcast + core::fmt::Debug {
/// Gets the element defined location i.e token without filename
fn location(&self) -> &Token;
fn kind(&self) -> ElemKind;
/// Get the element's name
fn element_name(&self) -> &'static str;
/// Gets the element as a referenceable i.e an element that can be referenced
fn as_referenceable(&self) -> Option<&dyn ReferenceableElement> { None }
/// Gets the element as a container containing other elements
fn as_container(&self) -> Option<&dyn ContainerElement> { None }
/// Compiles element
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String>;
}
impl_downcast!(Element);
pub trait ReferenceableElement: Element {
/// Reference name
fn reference_name(&self) -> Option<&String>;
/// Key for refcounting
fn refcount_key(&self) -> &'static str;
/// Creates the reference element
fn compile_reference(
&self,
compiler: &Compiler,
document: &dyn Document,
reference: &Reference,
refid: usize,
) -> Result<String, String>;
}
pub trait ContainerElement: Element {
/// Gets the contained elements
fn contained(&self) -> &Vec<Box<dyn Element>>;
/// Adds an element to the container
fn push(&mut self, elem: Box<dyn Element>) -> Result<(), String>;
}
#[derive(Debug)]
pub struct DocumentEnd(pub Token);
impl Element for DocumentEnd {
fn location(&self) -> &Token { &self.0 }
fn kind(&self) -> ElemKind { ElemKind::Invisible }
fn element_name(&self) -> &'static str { "Document End" }
fn compile(&self, _compiler: &Compiler, _document: &dyn Document) -> Result<String, String> {
Ok(String::new())
}
}

View file

@ -0,0 +1,41 @@
use std::cell::RefCell;
use std::rc::Rc;
use crate::parser::source::Source;
use super::document::Document;
use super::document::Scope;
use super::element::Element;
#[derive(Debug)]
pub struct LangDocument<'a> {
source: Rc<dyn Source>,
parent: Option<&'a dyn Document<'a>>,
/// Document's parent
// FIXME: Render these fields private
pub content: RefCell<Vec<Box<dyn Element>>>,
pub scope: RefCell<Scope>,
}
impl<'a> LangDocument<'a> {
pub fn new(source: Rc<dyn Source>, parent: Option<&'a dyn Document<'a>>) -> Self {
Self {
source: source,
parent: parent,
content: RefCell::new(Vec::new()),
scope: RefCell::new(Scope::new()),
}
}
}
impl<'a> Document<'a> for LangDocument<'a> {
fn source(&self) -> Rc<dyn Source> { self.source.clone() }
fn parent(&self) -> Option<&'a dyn Document<'a>> {
self.parent.and_then(|p| Some(p as &dyn Document<'a>))
}
fn content(&self) -> &RefCell<Vec<Box<dyn Element>>> { &self.content }
fn scope(&self) -> &RefCell<Scope> { &self.scope }
}

View file

@ -1,6 +1,5 @@
pub mod element;
pub mod document;
pub mod references;
pub mod scope;
pub mod translation;
pub mod unit;
pub mod langdocument;
pub mod element;
pub mod variable;

View file

@ -0,0 +1,73 @@
use super::document::Document;
pub fn validate_refname<'a>(
document: &dyn Document,
name: &'a str,
check_duplicate: bool,
) -> Result<&'a str, String> {
let trimmed = name.trim_start().trim_end();
if trimmed.is_empty() {
return Err("Refname cannot be empty".to_string());
}
for c in trimmed.chars() {
if c.is_ascii_punctuation() && !(c == '.' || c == '_') {
return Err(format!(
"Refname `{trimmed}` cannot contain punctuation codepoint: `{c}`"
));
}
if c.is_whitespace() {
return Err(format!(
"Refname `{trimmed}` cannot contain whitespaces: `{c}`"
));
}
if c.is_control() {
return Err(format!(
"Refname `{trimmed}` cannot contain control codepoint: `{c}`"
));
}
}
if check_duplicate && document.get_reference(trimmed).is_some() {
Err(format!("Refname `{trimmed}` is already in use!"))
} else {
Ok(trimmed)
}
}
#[cfg(test)]
pub mod tests {
use std::rc::Rc;
use super::*;
use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser;
use crate::parser::source::SourceFile;
use crate::ParserState;
#[test]
fn validate_refname_tests() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
"#{ref} Section".to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
assert_eq!(validate_refname(&*doc, " abc ", true), Ok("abc"));
assert_eq!(
validate_refname(&*doc, " Some_reference ", true),
Ok("Some_reference")
);
assert!(validate_refname(&*doc, "", true).is_err());
assert!(validate_refname(&*doc, "\n", true).is_err());
assert!(validate_refname(&*doc, "'", true).is_err());
assert!(validate_refname(&*doc, "]", true).is_err());
// Duplicate
assert!(validate_refname(&*doc, "ref", true).is_err());
}
}

116
src/document/variable.rs Normal file
View file

@ -0,0 +1,116 @@
use super::document::Document;
use crate::elements::text::Text;
use crate::parser::parser::ParserState;
use crate::parser::source::Source;
use crate::parser::source::Token;
use crate::parser::source::VirtualSource;
use std::path::PathBuf;
use std::rc::Rc;
// TODO enforce to_string(from_string(to_string())) == to_string()
pub trait Variable {
fn location(&self) -> &Token;
fn name(&self) -> &str;
/// Parse variable from string, returns an error message on failure
fn from_string(&mut self, str: &str) -> Option<String>;
/// Converts variable to a string
fn to_string(&self) -> String;
fn parse<'a>(&self, state: &ParserState, location: Token, document: &'a dyn Document<'a>);
}
impl core::fmt::Debug for dyn Variable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{{{}}}", self.name(), self.to_string())
}
}
#[derive(Debug)]
pub struct BaseVariable {
location: Token,
name: String,
value: String,
}
impl BaseVariable {
pub fn new(location: Token, name: String, value: String) -> Self {
Self {
location,
name,
value,
}
}
}
impl Variable for BaseVariable {
fn location(&self) -> &Token { &self.location }
fn name(&self) -> &str { self.name.as_str() }
fn from_string(&mut self, str: &str) -> Option<String> {
self.value = str.to_string();
None
}
fn to_string(&self) -> String { self.value.clone() }
fn parse<'a>(&self, state: &ParserState, _location: Token, document: &'a dyn Document<'a>) {
let source = Rc::new(VirtualSource::new(
self.location().clone(),
self.name().to_string(),
self.to_string(),
));
state.with_state(|new_state| {
let _ = new_state.parser.parse_into(new_state, source, document);
});
}
}
#[derive(Debug)]
pub struct PathVariable {
location: Token,
name: String,
path: PathBuf,
}
impl PathVariable {
pub fn new(location: Token, name: String, path: PathBuf) -> Self {
Self {
location,
name,
path,
}
}
}
impl Variable for PathVariable {
fn location(&self) -> &Token { &self.location }
fn name(&self) -> &str { self.name.as_str() }
fn from_string(&mut self, str: &str) -> Option<String> {
self.path = PathBuf::from(std::fs::canonicalize(str).unwrap());
None
}
fn to_string(&self) -> String { self.path.to_str().unwrap().to_string() }
fn parse<'a>(&self, state: &ParserState, location: Token, document: &'a dyn Document) {
let source = Rc::new(VirtualSource::new(
location,
self.name().to_string(),
self.to_string(),
));
state.push(
document,
Box::new(Text::new(
Token::new(0..source.content().len(), source),
self.to_string(),
)),
);
}
}

View file

@ -1,58 +0,0 @@
use tower_lsp::lsp_types::CompletionContext;
use tower_lsp::lsp_types::CompletionItem;
use tower_lsp::lsp_types::CompletionItemKind;
use tower_lsp::lsp_types::InsertTextFormat;
use tower_lsp::lsp_types::MarkupContent;
use crate::lsp::completion::context_triggered;
use crate::lsp::completion::CompletionProvider;
use crate::unit::translation::TranslationUnit;
pub struct AnchorCompletion;
impl CompletionProvider for AnchorCompletion {
fn trigger(&self) -> &'static [&'static str] {
[":"].as_slice()
}
fn unit_items(&self, _unit: &TranslationUnit, _items: &mut Vec<CompletionItem>) {}
fn static_items(&self, context: &Option<CompletionContext>, items: &mut Vec<CompletionItem>) {
// @import
items.push(CompletionItem {
label: ":anchor ".to_string(),
detail: Some("Creates an anchor".into()),
documentation: Some(tower_lsp::lsp_types::Documentation::MarkupContent(
MarkupContent {
kind: tower_lsp::lsp_types::MarkupKind::Markdown,
value: "# Usage
`:anchor NAME:`
Creates an anchor at the location of the `:anchor` command.
Created anchors can then be linked against using internal links.
# Examples
* `:anchor foo:` *creates an anchor named **foo** here*
:anchor bar:
Link to anchor: &{bar}"
.into(),
},
)),
kind: Some(CompletionItemKind::FUNCTION),
insert_text_format: Some(InsertTextFormat::SNIPPET),
insert_text: Some(format!(
"{}anchor ${{1:NAME}}:",
if context_triggered(context, ":") {
""
} else {
":"
}
)),
..CompletionItem::default()
});
}
}

View file

@ -1,98 +0,0 @@
use std::sync::Arc;
use std::sync::OnceLock;
use ariadne::Span;
use parking_lot::RwLock;
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target;
use crate::compiler::output::CompilerOutput;
use crate::parser::reports::Report;
use crate::parser::source::Token;
use crate::unit::element::ContainerElement;
use crate::unit::element::ElemKind;
use crate::unit::element::Element;
use crate::unit::element::LinkableElement;
use crate::unit::element::ReferenceableElement;
use crate::unit::references::InternalReference;
use crate::unit::references::Refname;
use crate::unit::scope::Scope;
#[derive(Debug)]
pub struct Anchor {
pub(crate) location: Token,
pub(crate) refname: Refname,
pub(crate) reference: Arc<InternalReference>,
pub(crate) link: OnceLock<String>,
}
impl Element for Anchor {
fn location(&self) -> &crate::parser::source::Token {
&self.location
}
fn kind(&self) -> ElemKind {
ElemKind::Invisible
}
fn element_name(&self) -> &'static str {
"Anchor"
}
fn compile(
&self,
_scope: Arc<RwLock<Scope>>,
compiler: &Compiler,
output: &mut CompilerOutput,
) -> Result<(), Vec<Report>> {
// Get link
let link = self.get_link().unwrap();
match compiler.target() {
Target::HTML => {
output.add_content(format!("<a id=\"{link}\"></a>",));
}
_ => todo!(""),
}
Ok(())
}
fn provide_hover(&self) -> Option<String> {
Some(format!(
"Anchor
# Properties
* **Location**: {} ({}..{})
* **Name**: {}",
self.location.source().name(),
self.location().range.start(),
self.location().range.end(),
self.refname.to_string()))
}
fn as_referenceable(self: Arc<Self>) -> Option<Arc<dyn ReferenceableElement>> {
Some(self)
}
}
impl ReferenceableElement for Anchor {
fn reference(&self) -> Arc<InternalReference> {
self.reference.clone()
}
fn refcount_key(&self) -> &'static str {
"anchor"
}
fn refid(&self, _compiler: &Compiler, refid: usize) -> String {
refid.to_string()
}
fn get_link(&self) -> Option<&String> {
self.link.get()
}
fn set_link(&self, url: String) {
self.link.set(url).expect("set_url can only be called once");
}
}

View file

@ -1,3 +0,0 @@
pub mod completion;
pub mod elem;
pub mod rule;

View file

@ -1,145 +0,0 @@
use crate::parser::reports::macros::*;
use crate::parser::reports::*;
use crate::parser::rule::RuleTarget;
use crate::parser::source::SourcePosition;
use crate::parser::state::CustomStates;
use crate::parser::state::ParseMode;
use crate::unit::references::InternalReference;
use crate::unit::references::Refname;
use crate::unit::translation::TranslationAccessors;
use crate::unit::translation::TranslationUnit;
use ariadne::Fmt;
use regex::Captures;
use regex::Regex;
use std::sync::Arc;
use std::sync::OnceLock;
use crate::parser::reports::Report;
use crate::parser::rule::RegexRule;
use crate::parser::source::Token;
use super::completion::AnchorCompletion;
use super::elem::Anchor;
#[auto_registry::auto_registry(registry = "rules")]
pub struct AnchorRule {
re: [Regex; 1],
}
impl Default for AnchorRule {
fn default() -> Self {
Self {
re: [Regex::new(r"(:anchor)[^\S\r\n]+([^:\r\n]*)?(:)?").unwrap()],
}
}
}
impl RegexRule for AnchorRule {
fn name(&self) -> &'static str {
"Anchor"
}
fn target(&self) -> RuleTarget {
RuleTarget::Inline
}
fn regexes(&self) -> &[regex::Regex] {
&self.re
}
fn enabled(
&self,
_unit: &TranslationUnit,
_mode: &ParseMode,
_states: &mut CustomStates,
_id: usize,
) -> bool {
true
}
fn on_regex_match<'u>(
&self,
_index: usize,
unit: &mut TranslationUnit,
token: Token,
captures: Captures,
) {
let anchor = captures.get(2).unwrap();
// Missing ':'
if captures.get(3).is_none() {
report_err!(
unit,
token.source(),
"Invalid anchor".into(),
span(
token.end()..anchor.end() + 1,
format!("Missing closing `{}`", ":".fg(unit.colors().info))
),
span_highlight(
token.start()..token.start() + 1,
format!("Opening `{}` here", ":".fg(unit.colors().highlight))
),
note("While attempting to parse anchor".into())
);
return;
}
// Parse to refname
let anchor_refname = match Refname::try_from(anchor.as_str()) {
// Parse error
Err(err) => {
report_err!(
unit,
token.source(),
"Invalid anchor".into(),
span(anchor.range(), err),
note("While attempting to parse anchor".into())
);
return;
}
// Check format
Ok(r) => match r {
Refname::Internal(_) => r,
_ => {
report_err!(
unit,
token.source(),
"Invalid anchor".into(),
span(
anchor.range(),
format!("Use of reserved character: `{}` (external reference), `{}` (bibliography)", '#'.fg(unit.colors().info), '@'.fg(unit.colors().info))
),
note("While attempting to parse anchor".into())
);
return;
}
},
};
unit.with_lsp(|lsp| {
lsp.with_semantics(token.source(), |sems, tokens| {
sems.add(captures.get(1).unwrap().range(), tokens.command);
sems.add(anchor.range(), tokens.anchor_refname);
sems.add(captures.get(3).unwrap().range(), tokens.command);
})
});
let reference = Arc::new(InternalReference::new(
token.source().original_range(token.range.clone()),
anchor_refname.clone(),
));
unit.add_content(Arc::new(Anchor {
location: token.clone(),
refname: anchor_refname.clone(),
reference: reference.clone(),
link: OnceLock::default(),
}));
}
fn completion(
&self,
) -> Option<Box<dyn lsp::completion::CompletionProvider + 'static + Send + Sync>> {
Some(Box::new(AnchorCompletion {}))
}
}

760
src/elements/code.rs Normal file
View file

@ -0,0 +1,760 @@
use std::collections::HashMap;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Once;
use ariadne::Fmt;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use crypto::digest::Digest;
use crypto::sha2::Sha512;
use mlua::Function;
use mlua::Lua;
use regex::Captures;
use regex::Regex;
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
use crate::cache::cache::Cached;
use crate::cache::cache::CachedError;
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target;
use crate::document::document::Document;
use crate::document::element::ElemKind;
use crate::document::element::Element;
use crate::lua::kernel::CTX;
use crate::parser::parser::ParserState;
use crate::parser::rule::RegexRule;
use crate::parser::source::Source;
use crate::parser::source::Token;
use crate::parser::util::Property;
use crate::parser::util::PropertyMapError;
use crate::parser::util::PropertyParser;
use crate::parser::util::{self};
use lazy_static::lazy_static;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CodeKind {
FullBlock,
MiniBlock,
Inline,
}
impl From<&CodeKind> for ElemKind {
fn from(value: &CodeKind) -> Self {
match value {
CodeKind::FullBlock | CodeKind::MiniBlock => ElemKind::Block,
CodeKind::Inline => ElemKind::Inline,
}
}
}
#[derive(Debug)]
struct Code {
location: Token,
block: CodeKind,
language: String,
name: Option<String>,
code: String,
theme: Option<String>,
line_offset: usize,
}
impl Code {
fn new(
location: Token,
block: CodeKind,
language: String,
name: Option<String>,
code: String,
theme: Option<String>,
line_offset: usize,
) -> Self {
Self {
location,
block,
language,
name,
code,
theme,
line_offset,
}
}
pub fn get_syntaxes() -> &'static SyntaxSet {
lazy_static! {
static ref syntax_set: SyntaxSet = SyntaxSet::load_defaults_newlines();
}
&syntax_set
}
fn highlight_html(&self, compiler: &Compiler) -> Result<String, String> {
lazy_static! {
static ref theme_set: ThemeSet = ThemeSet::load_defaults();
}
let syntax = match Code::get_syntaxes().find_syntax_by_name(self.language.as_str()) {
Some(syntax) => syntax,
None => {
return Err(format!(
"Unable to find syntax for language: {}",
self.language
))
}
};
let theme_string = match self.theme.as_ref() {
Some(theme) => theme.as_str(),
None => "base16-ocean.dark",
};
let mut h = HighlightLines::new(syntax, &theme_set.themes[theme_string]);
let mut result = String::new();
if self.block == CodeKind::FullBlock {
result += "<div class=\"code-block\">";
if let Some(name) = &self.name {
result += format!(
"<div class=\"code-block-title\">{}</div>",
Compiler::sanitize(compiler.target(), name.as_str())
)
.as_str();
}
result +=
format!("<div class=\"code-block-content\"><table cellspacing=\"0\">").as_str();
for (line_id, line) in self.code.split(|c| c == '\n').enumerate() {
result += "<tr><td class=\"code-block-gutter\">";
// Line number
result +=
format!("<pre><span>{}</span></pre>", line_id + self.line_offset).as_str();
// Code
result += "</td><td class=\"code-block-line\"><pre>";
match h.highlight_line(line, Code::get_syntaxes()) {
Err(e) => {
return Err(format!(
"Error highlighting line `{line}`: {}",
e.to_string()
))
}
Ok(regions) => {
match syntect::html::styled_line_to_highlighted_html(
&regions[..],
syntect::html::IncludeBackground::No,
) {
Err(e) => {
return Err(format!("Error highlighting code: {}", e.to_string()))
}
Ok(highlighted) => {
result += if highlighted.is_empty() {
"<br>"
} else {
highlighted.as_str()
}
}
}
}
}
result += "</pre></td></tr>";
}
result += "</table></div></div>";
} else if self.block == CodeKind::MiniBlock {
result += "<div class=\"code-block\"><div class=\"code-block-content\"><table cellspacing=\"0\">";
for line in self.code.split(|c| c == '\n') {
result += "<tr><td class=\"code-block-line\"><pre>";
// Code
match h.highlight_line(line, Code::get_syntaxes()) {
Err(e) => {
return Err(format!(
"Error highlighting line `{line}`: {}",
e.to_string()
))
}
Ok(regions) => {
match syntect::html::styled_line_to_highlighted_html(
&regions[..],
syntect::html::IncludeBackground::No,
) {
Err(e) => {
return Err(format!("Error highlighting code: {}", e.to_string()))
}
Ok(highlighted) => {
result += if highlighted.is_empty() {
"<br>"
} else {
highlighted.as_str()
}
}
}
}
}
result += "</pre></td></tr>";
}
result += "</table></div></div>";
} else if self.block == CodeKind::Inline {
result += "<a class=\"inline-code\"><code>";
match h.highlight_line(self.code.as_str(), Code::get_syntaxes()) {
Err(e) => {
return Err(format!(
"Error highlighting line `{}`: {}",
self.code,
e.to_string()
))
}
Ok(regions) => {
match syntect::html::styled_line_to_highlighted_html(
&regions[..],
syntect::html::IncludeBackground::No,
) {
Err(e) => {
return Err(format!("Error highlighting code: {}", e.to_string()))
}
Ok(highlighted) => result += highlighted.as_str(),
}
}
}
result += "</code></a>";
}
Ok(result)
}
}
impl Cached for Code {
type Key = String;
type Value = String;
fn sql_table() -> &'static str {
"CREATE TABLE IF NOT EXISTS cached_code (
digest TEXT PRIMARY KEY,
highlighted BLOB NOT NULL);"
}
fn sql_get_query() -> &'static str { "SELECT highlighted FROM cached_code WHERE digest = (?1)" }
fn sql_insert_query() -> &'static str {
"INSERT INTO cached_code (digest, highlighted) VALUES (?1, ?2)"
}
fn key(&self) -> <Self as Cached>::Key {
let mut hasher = Sha512::new();
hasher.input((self.block as usize).to_be_bytes().as_slice());
hasher.input((self.line_offset as usize).to_be_bytes().as_slice());
self.theme
.as_ref()
.map(|theme| hasher.input(theme.as_bytes()));
self.name.as_ref().map(|name| hasher.input(name.as_bytes()));
hasher.input(self.language.as_bytes());
hasher.input(self.code.as_bytes());
hasher.result_str()
}
}
impl Element for Code {
fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { (&self.block).into() }
fn element_name(&self) -> &'static str { "Code Block" }
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> {
match compiler.target() {
Target::HTML => {
static CACHE_INIT: Once = Once::new();
CACHE_INIT.call_once(|| {
if let Some(mut con) = compiler.cache() {
if let Err(e) = Code::init(&mut con) {
eprintln!("Unable to create cache table: {e}");
}
}
});
if let Some(mut con) = compiler.cache() {
match self.cached(&mut con, |s| s.highlight_html(compiler)) {
Ok(s) => Ok(s),
Err(e) => match e {
CachedError::SqlErr(e) => {
Err(format!("Querying the cache failed: {e}"))
}
CachedError::GenErr(e) => Err(e),
},
}
} else {
self.highlight_html(compiler)
}
}
Target::LATEX => {
todo!("")
}
}
}
}
pub struct CodeRule {
re: [Regex; 2],
properties: PropertyParser,
}
impl CodeRule {
pub fn new() -> Self {
let mut props = HashMap::new();
props.insert(
"line_offset".to_string(),
Property::new(
true,
"Line number offset".to_string(),
Some("1".to_string()),
),
);
Self {
re: [
Regex::new(
r"(?:^|\n)```(?:\[((?:\\.|[^\\\\])*?)\])?(.*?)(?:,(.*))?\n((?:\\(?:.|\n)|[^\\\\])*?)```",
)
.unwrap(),
Regex::new(
r"``(?:\[((?:\\.|[^\\\\])*?)\])?(?:(.*?),)?((?:\\(?:.|\n)|[^\\\\])*?)``",
)
.unwrap(),
],
properties: PropertyParser { properties: props },
}
}
}
impl RegexRule for CodeRule {
fn name(&self) -> &'static str { "Code" }
fn regexes(&self) -> &[regex::Regex] { &self.re }
fn on_regex_match<'a>(
&self,
index: usize,
state: &ParserState,
document: &'a dyn Document,
token: Token,
matches: Captures,
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
let mut reports = vec![];
let properties = match matches.get(1) {
None => match self.properties.default() {
Ok(properties) => properties,
Err(e) => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid code")
.with_label(
Label::new((token.source().clone(), token.range.clone()))
.with_message(format!("Code is missing properties: {e}"))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
},
Some(props) => {
let processed =
util::process_escaped('\\', "]", props.as_str().trim_start().trim_end());
match self.properties.parse(processed.as_str()) {
Err(e) => {
reports.push(
Report::build(ReportKind::Error, token.source(), props.start())
.with_message("Invalid Code Properties")
.with_label(
Label::new((token.source().clone(), props.range()))
.with_message(e)
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
Ok(properties) => properties,
}
}
};
let code_lang = match matches.get(2) {
None => "Plain Text".to_string(),
Some(lang) => {
let code_lang = lang.as_str().trim_start().trim_end().to_string();
if code_lang.is_empty() {
reports.push(
Report::build(ReportKind::Error, token.source(), lang.start())
.with_message("Missing Code Language")
.with_label(
Label::new((token.source().clone(), lang.range()))
.with_message("No language specified")
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
if Code::get_syntaxes()
.find_syntax_by_name(code_lang.as_str())
.is_none()
{
reports.push(
Report::build(ReportKind::Error, token.source(), lang.start())
.with_message("Unknown Code Language")
.with_label(
Label::new((token.source().clone(), lang.range()))
.with_message(format!(
"Language `{}` cannot be found",
code_lang.fg(state.parser.colors().info)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
code_lang
}
};
let mut code_content = if index == 0 {
util::process_escaped('\\', "```", matches.get(4).unwrap().as_str())
} else {
util::process_escaped('\\', "``", matches.get(3).unwrap().as_str())
};
if code_content.bytes().last() == Some('\n' as u8)
// Remove newline
{
code_content.pop();
}
if code_content.is_empty() {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Missing code content")
.with_label(
Label::new((token.source().clone(), token.range.clone()))
.with_message("Code content cannot be empty")
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
let theme = document
.get_variable("code.theme")
.and_then(|var| Some(var.to_string()));
if index == 0
// Block
{
let code_name = matches.get(3).and_then(|name| {
let code_name = name.as_str().trim_end().trim_start().to_string();
(!code_name.is_empty()).then_some(code_name)
});
let line_offset =
match properties.get("line_offset", |prop, value| {
value.parse::<usize>().map_err(|e| (prop, e))
}) {
Ok((_prop, offset)) => offset,
Err(e) => {
match e {
PropertyMapError::ParseError((prop, err)) => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid Code Property")
.with_label(
Label::new((token.source().clone(), token.start()+1..token.end()))
.with_message(format!("Property `line_offset: {}` cannot be converted: {}",
prop.fg(state.parser.colors().info),
err.fg(state.parser.colors().error)))
.with_color(state.parser.colors().warning))
.finish());
return reports;
}
PropertyMapError::NotFoundError(err) => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid Code Property")
.with_label(
Label::new((
token.source().clone(),
token.start() + 1..token.end(),
))
.with_message(format!(
"Property `{}` doesn't exist",
err.fg(state.parser.colors().info)
))
.with_color(state.parser.colors().warning),
)
.finish(),
);
return reports;
}
}
}
};
state.push(
document,
Box::new(Code::new(
token.clone(),
CodeKind::FullBlock,
code_lang,
code_name,
code_content,
theme,
line_offset,
)),
);
} else
// Maybe inline
{
let block = if code_content.contains('\n') {
CodeKind::MiniBlock
} else {
CodeKind::Inline
};
state.push(
document,
Box::new(Code::new(
token.clone(),
block,
code_lang,
None,
code_content,
theme,
1,
)),
);
}
reports
}
fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> {
let mut bindings = vec![];
bindings.push((
"push_inline".to_string(),
lua.create_function(|_, (language, content): (String, String)| {
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
let theme = ctx
.document
.get_variable("code.theme")
.and_then(|var| Some(var.to_string()));
ctx.state.push(
ctx.document,
Box::new(Code {
location: ctx.location.clone(),
block: CodeKind::Inline,
language,
name: None,
code: content,
theme,
line_offset: 1,
}),
);
})
});
Ok(())
})
.unwrap(),
));
bindings.push((
"push_miniblock".to_string(),
lua.create_function(
|_, (language, content, line_offset): (String, String, Option<usize>)| {
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
let theme = ctx
.document
.get_variable("code.theme")
.and_then(|var| Some(var.to_string()));
ctx.state.push(
ctx.document,
Box::new(Code {
location: ctx.location.clone(),
block: CodeKind::MiniBlock,
language,
name: None,
code: content,
theme,
line_offset: line_offset.unwrap_or(1),
}),
);
})
});
Ok(())
},
)
.unwrap(),
));
bindings.push((
"push_block".to_string(),
lua.create_function(
|_,
(language, name, content, line_offset): (
String,
Option<String>,
String,
Option<usize>,
)| {
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
let theme = ctx
.document
.get_variable("code.theme")
.and_then(|var| Some(var.to_string()));
ctx.state.push(
ctx.document,
Box::new(Code {
location: ctx.location.clone(),
block: CodeKind::FullBlock,
language,
name,
code: content,
theme,
line_offset: line_offset.unwrap_or(1),
}),
);
})
});
Ok(())
},
)
.unwrap(),
));
bindings
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser;
use crate::parser::source::SourceFile;
#[test]
fn code_block() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
```[line_offset=32] C, Some Code...
static int INT32_MIN = 0x80000000;
```
%<nml.code.push_block("Lua", "From Lua", "print(\"Hello, World!\")", nil)>%
``Rust,
fn fact(n: usize) -> usize
{
match n
{
0 | 1 => 1,
_ => n * fact(n-1)
}
}
``
%<nml.code.push_miniblock("Bash", "NUM=$(($RANDOM % 10))", 18)>%
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
let borrow = doc.content().borrow();
let found = borrow
.iter()
.filter_map(|e| e.downcast_ref::<Code>())
.collect::<Vec<_>>();
assert_eq!(found[0].block, CodeKind::FullBlock);
assert_eq!(found[0].language, "C");
assert_eq!(found[0].name, Some("Some Code...".to_string()));
assert_eq!(found[0].code, "static int INT32_MIN = 0x80000000;");
assert_eq!(found[0].line_offset, 32);
assert_eq!(found[1].block, CodeKind::FullBlock);
assert_eq!(found[1].language, "Lua");
assert_eq!(found[1].name, Some("From Lua".to_string()));
assert_eq!(found[1].code, "print(\"Hello, World!\")");
assert_eq!(found[1].line_offset, 1);
assert_eq!(found[2].block, CodeKind::MiniBlock);
assert_eq!(found[2].language, "Rust");
assert_eq!(found[2].name, None);
assert_eq!(found[2].code, "fn fact(n: usize) -> usize\n{\n\tmatch n\n\t{\n\t\t0 | 1 => 1,\n\t\t_ => n * fact(n-1)\n\t}\n}");
assert_eq!(found[2].line_offset, 1);
assert_eq!(found[3].block, CodeKind::MiniBlock);
assert_eq!(found[3].language, "Bash");
assert_eq!(found[3].name, None);
assert_eq!(found[3].code, "NUM=$(($RANDOM % 10))");
assert_eq!(found[3].line_offset, 18);
}
#[test]
fn code_inline() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
``C, int fact(int n)``
``Plain Text, Text in a code block!``
%<nml.code.push_inline("C++", "std::vector<std::vector<int>> u;")>%
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
let borrow = doc.content().borrow();
let found = borrow
.first()
.unwrap()
.as_container()
.unwrap()
.contained()
.iter()
.filter_map(|e| e.downcast_ref::<Code>())
.collect::<Vec<_>>();
assert_eq!(found[0].block, CodeKind::Inline);
assert_eq!(found[0].language, "C");
assert_eq!(found[0].name, None);
assert_eq!(found[0].code, "int fact(int n)");
assert_eq!(found[0].line_offset, 1);
assert_eq!(found[1].block, CodeKind::Inline);
assert_eq!(found[1].language, "Plain Text");
assert_eq!(found[1].name, None);
assert_eq!(found[1].code, "Text in a code block!");
assert_eq!(found[1].line_offset, 1);
assert_eq!(found[2].block, CodeKind::Inline);
assert_eq!(found[2].language, "C++");
assert_eq!(found[2].name, None);
assert_eq!(found[2].code, "std::vector<std::vector<int>> u;");
assert_eq!(found[2].line_offset, 1);
}
}

136
src/elements/comment.rs Normal file
View file

@ -0,0 +1,136 @@
use crate::compiler::compiler::Compiler;
use crate::document::document::Document;
use crate::document::element::ElemKind;
use crate::document::element::Element;
use crate::parser::parser::ParserState;
use crate::parser::rule::RegexRule;
use crate::parser::source::Source;
use crate::parser::source::Token;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use regex::Captures;
use regex::Regex;
use std::ops::Range;
use std::rc::Rc;
#[derive(Debug)]
pub struct Comment {
location: Token,
content: String,
}
impl Comment {
pub fn new(location: Token, content: String) -> Self {
Self {
location: location,
content,
}
}
}
impl Element for Comment {
fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Invisible }
fn element_name(&self) -> &'static str { "Comment" }
fn compile(&self, _compiler: &Compiler, _document: &dyn Document) -> Result<String, String> {
Ok("".to_string())
}
}
pub struct CommentRule {
re: [Regex; 1],
}
impl CommentRule {
pub fn new() -> Self {
Self {
re: [Regex::new(r"(?:(?:^|\n)|[^\S\n]+)::(.*)").unwrap()],
}
}
}
impl RegexRule for CommentRule {
fn name(&self) -> &'static str { "Comment" }
fn regexes(&self) -> &[Regex] { &self.re }
fn on_regex_match<'a>(
&self,
_: usize,
state: &ParserState,
document: &'a dyn Document,
token: Token,
matches: Captures,
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
let mut reports = vec![];
let content = match matches.get(1) {
None => panic!("Unknown error"),
Some(comment) => {
let trimmed = comment.as_str().trim_start().trim_end().to_string();
if trimmed.is_empty() {
reports.push(
Report::build(ReportKind::Warning, token.source(), comment.start())
.with_message("Empty comment")
.with_label(
Label::new((token.source(), comment.range()))
.with_message("Comment is empty")
.with_color(state.parser.colors().warning),
)
.finish(),
);
}
trimmed
}
};
state.push(
document,
Box::new(Comment {
location: token.clone(),
content,
}),
);
return reports;
}
}
#[cfg(test)]
mod tests {
use crate::elements::paragraph::Paragraph;
use crate::elements::style::Style;
use crate::elements::text::Text;
use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser;
use crate::parser::source::SourceFile;
use crate::validate_document;
use super::*;
#[test]
fn parser() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
NOT COMMENT: `std::cmp`
:: Commented line
COMMENT ::Test
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Paragraph {
Text; Style; Text; Style;
Comment { content == "Commented line" };
Text; Comment { content == "Test" };
};
);
}
}

572
src/elements/customstyle.rs Normal file
View file

@ -0,0 +1,572 @@
use crate::lua::kernel::Kernel;
use std::any::Any;
use std::cell::Ref;
use std::cell::RefCell;
use std::collections::HashMap;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
use ariadne::Fmt;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use mlua::Error::BadArgument;
use mlua::Function;
use mlua::Lua;
use crate::document::document::Document;
use crate::document::document::DocumentAccessors;
use crate::lua::kernel::KernelContext;
use crate::lua::kernel::CTX;
use crate::parser::customstyle::CustomStyle;
use crate::parser::customstyle::CustomStyleToken;
use crate::parser::parser::ParserState;
use crate::parser::rule::Rule;
use crate::parser::source::Cursor;
use crate::parser::source::Source;
use crate::parser::source::Token;
use crate::parser::state::RuleState;
use crate::parser::state::Scope;
use super::paragraph::Paragraph;
#[derive(Debug)]
struct LuaCustomStyle {
pub(self) name: String,
pub(self) tokens: CustomStyleToken,
pub(self) start: String,
pub(self) end: String,
}
impl CustomStyle for LuaCustomStyle {
fn name(&self) -> &str { self.name.as_str() }
fn tokens(&self) -> &CustomStyleToken { &self.tokens }
fn on_start<'a>(
&self,
location: Token,
state: &ParserState,
document: &'a dyn Document<'a>,
) -> Vec<Report<(Rc<dyn Source>, Range<usize>)>> {
let kernel: Ref<'_, Kernel> =
Ref::map(state.shared.kernels.borrow(), |b| b.get("main").unwrap());
//let kernel = RefMut::map(parser_state.shared.kernels.borrow(), |ker| ker.get("main").unwrap());
let ctx = KernelContext {
location: location.clone(),
state,
document,
};
let mut reports = vec![];
kernel.run_with_context(ctx, |lua| {
let chunk = lua.load(self.start.as_str());
if let Err(err) = chunk.eval::<()>() {
reports.push(
Report::build(ReportKind::Error, location.source(), location.start())
.with_message("Lua execution failed")
.with_label(
Label::new((location.source(), location.range.clone()))
.with_message(err.to_string())
.with_color(state.parser.colors().error),
)
.with_note(format!(
"When trying to start custom style {}",
self.name().fg(state.parser.colors().info)
))
.finish(),
);
}
});
reports
}
fn on_end<'a>(
&self,
location: Token,
state: &ParserState,
document: &'a dyn Document<'a>,
) -> Vec<Report<(Rc<dyn Source>, Range<usize>)>> {
let kernel: Ref<'_, Kernel> =
Ref::map(state.shared.kernels.borrow(), |b| b.get("main").unwrap());
let ctx = KernelContext {
location: location.clone(),
state,
document,
};
let mut reports = vec![];
kernel.run_with_context(ctx, |lua| {
let chunk = lua.load(self.end.as_str());
if let Err(err) = chunk.eval::<()>() {
reports.push(
Report::build(ReportKind::Error, location.source(), location.start())
.with_message("Lua execution failed")
.with_label(
Label::new((location.source(), location.range.clone()))
.with_message(err.to_string())
.with_color(state.parser.colors().error),
)
.with_note(format!(
"When trying to end custom style {}",
self.name().fg(state.parser.colors().info)
))
.finish(),
);
}
});
reports
}
}
struct CustomStyleState {
toggled: HashMap<String, Token>,
}
impl RuleState for CustomStyleState {
fn scope(&self) -> Scope { Scope::PARAGRAPH }
fn on_remove<'a>(
&self,
state: &ParserState,
document: &dyn Document,
) -> Vec<Report<'a, (Rc<dyn Source>, Range<usize>)>> {
let mut reports = vec![];
self.toggled.iter().for_each(|(style, token)| {
let paragraph = document.last_element::<Paragraph>().unwrap();
let paragraph_end = paragraph
.content
.last()
.and_then(|last| {
Some((
last.location().source(),
last.location().end() - 1..last.location().end(),
))
})
.unwrap();
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Unterminated Custom Style")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_order(1)
.with_message(format!(
"Style {} starts here",
style.fg(state.parser.colors().info)
))
.with_color(state.parser.colors().error),
)
.with_label(
Label::new(paragraph_end)
.with_order(1)
.with_message(format!("Paragraph ends here"))
.with_color(state.parser.colors().error),
)
.with_note("Styles cannot span multiple documents (i.e @import)")
.finish(),
);
});
return reports;
}
}
static STATE_NAME: &'static str = "elements.custom_style";
pub struct CustomStyleRule;
impl Rule for CustomStyleRule {
fn name(&self) -> &'static str { "Custom Style" }
fn next_match(&self, state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> {
let content = cursor.source.content();
let mut closest_match = usize::MAX;
let mut matched_style = (None, false);
state
.shared
.custom_styles
.borrow()
.iter()
.for_each(|(_name, style)| match style.tokens() {
CustomStyleToken::Toggle(s) => {
if let Some(pos) = &content[cursor.pos..].find(s) {
if *pos < closest_match {
closest_match = *pos;
matched_style = (Some(style.clone()), false);
}
}
}
CustomStyleToken::Pair(begin, end) => {
if let Some(pos) = &content[cursor.pos..].find(begin) {
if *pos < closest_match {
closest_match = *pos;
matched_style = (Some(style.clone()), false);
}
}
if let Some(pos) = &content[cursor.pos..].find(end) {
if *pos < closest_match {
closest_match = *pos;
matched_style = (Some(style.clone()), true);
}
}
}
});
if closest_match == usize::MAX {
None
} else {
Some((
closest_match + cursor.pos,
Box::new((matched_style.0.unwrap().clone(), matched_style.1)) as Box<dyn Any>,
))
}
}
fn on_match<'a>(
&self,
state: &ParserState,
document: &'a dyn Document<'a>,
cursor: Cursor,
match_data: Box<dyn Any>,
) -> (Cursor, Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>>) {
let (style, end) = match_data
.downcast_ref::<(Rc<dyn CustomStyle>, bool)>()
.unwrap();
let mut rule_state_borrow = state.shared.rule_state.borrow_mut();
let style_state = match rule_state_borrow.get(STATE_NAME) {
Some(rule_state) => rule_state,
// Insert as a new state
None => {
match rule_state_borrow.insert(
STATE_NAME.into(),
Rc::new(RefCell::new(CustomStyleState {
toggled: HashMap::new(),
})),
) {
Err(err) => panic!("{err}"),
Ok(rule_state) => rule_state,
}
}
};
let (close, token) = match style.tokens() {
CustomStyleToken::Toggle(s) => {
let mut borrow = style_state.as_ref().borrow_mut();
let style_state = borrow.downcast_mut::<CustomStyleState>().unwrap();
if style_state.toggled.get(style.name()).is_some() {
// Terminate style
let token = Token::new(cursor.pos..cursor.pos + s.len(), cursor.source.clone());
style_state.toggled.remove(style.name());
(true, token)
} else {
// Start style
let token = Token::new(cursor.pos..cursor.pos + s.len(), cursor.source.clone());
style_state
.toggled
.insert(style.name().into(), token.clone());
(false, token)
}
}
CustomStyleToken::Pair(s_begin, s_end) => {
let mut borrow = style_state.borrow_mut();
let style_state = borrow.downcast_mut::<CustomStyleState>().unwrap();
if *end {
// Terminate style
let token =
Token::new(cursor.pos..cursor.pos + s_end.len(), cursor.source.clone());
if style_state.toggled.get(style.name()).is_none() {
return (
cursor.at(cursor.pos + s_end.len()),
vec![
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid End of Style")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_order(1)
.with_message(format!(
"Cannot end style {} here, is it not started anywhere",
style.name().fg(state.parser.colors().info)
))
.with_color(state.parser.colors().error),
)
.finish(),
],
);
}
style_state.toggled.remove(style.name());
(true, token)
} else {
// Start style
let token = Token::new(
cursor.pos..cursor.pos + s_begin.len(),
cursor.source.clone(),
);
if let Some(start_token) = style_state.toggled.get(style.name()) {
return (
cursor.at(cursor.pos + s_end.len()),
vec![Report::build(
ReportKind::Error,
start_token.source(),
start_token.start(),
)
.with_message("Invalid Start of Style")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_order(1)
.with_message(format!(
"Style cannot {} starts here",
style.name().fg(state.parser.colors().info)
))
.with_color(state.parser.colors().error),
)
.with_label(
Label::new((start_token.source(), start_token.range.clone()))
.with_order(2)
.with_message(format!(
"Style {} starts previously here",
style.name().fg(state.parser.colors().info)
))
.with_color(state.parser.colors().error),
)
.finish()],
);
}
style_state
.toggled
.insert(style.name().into(), token.clone());
(false, token)
}
}
};
let reports = if close {
style.on_end(token.clone(), state, document)
} else {
style.on_start(token.clone(), state, document)
};
(cursor.at(token.end()), unsafe {
std::mem::transmute(reports)
})
}
fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> {
let mut bindings = vec![];
bindings.push((
"define_toggled".into(),
lua.create_function(
|_, (name, token, on_start, on_end): (String, String, String, String)| {
let mut result = Ok(());
let style = LuaCustomStyle {
tokens: CustomStyleToken::Toggle(token),
name: name.clone(),
start: on_start,
end: on_end,
};
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
if let Some(_) =
ctx.state.shared.custom_styles.borrow().get(name.as_str())
{
result = Err(BadArgument {
to: Some("define_toggled".to_string()),
pos: 1,
name: Some("name".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Custom style with name `{name}` already exists"
))),
});
return;
}
ctx.state
.shared
.custom_styles
.borrow_mut()
.insert(Rc::new(style));
ctx.state.reset_match("Custom Style").unwrap();
});
});
result
},
)
.unwrap(),
));
bindings.push((
"define_paired".into(),
lua.create_function(
|_,
(name, token_start, token_end, on_start, on_end): (
String,
String,
String,
String,
String,
)| {
let mut result = Ok(());
if token_start == token_end
{
return Err(BadArgument {
to: Some("define_paired".to_string()),
pos: 3,
name: Some("token_end".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Custom style with name `{name}` cannot be defined: The start token must differ from the end token, use `define_toggled` insteda"
))),
});
}
let style = LuaCustomStyle {
tokens: CustomStyleToken::Pair(token_start, token_end),
name: name.clone(),
start: on_start,
end: on_end,
};
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
if let Some(_) = ctx.state.shared.custom_styles.borrow().get(name.as_str()) {
result = Err(BadArgument {
to: Some("define_paired".to_string()),
pos: 1,
name: Some("name".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Custom style with name `{name}` already exists"
))),
});
return;
}
ctx.state.shared.custom_styles.borrow_mut().insert(Rc::new(style));
ctx.state.reset_match("Custom Style").unwrap();
});
});
result
},
)
.unwrap(),
));
bindings
}
}
#[cfg(test)]
mod tests {
use crate::elements::raw::Raw;
use crate::elements::text::Text;
use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser;
use crate::parser::source::SourceFile;
use crate::validate_document;
use super::*;
#[test]
fn toggle() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
%<[main]
function my_style_start()
nml.raw.push("inline", "start")
end
function my_style_end()
nml.raw.push("inline", "end")
end
function red_style_start()
nml.raw.push("inline", "<a style=\"color:red\">")
end
function red_style_end()
nml.raw.push("inline", "</a>")
end
nml.custom_style.define_toggled("My Style", "|", "my_style_start()", "my_style_end()")
nml.custom_style.define_toggled("My Style2", "°", "red_style_start()", "red_style_end()")
>%
pre |styled| post °Hello°.
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Paragraph {
Text { content == "pre " };
Raw { content == "start" };
Text { content == "styled" };
Raw { content == "end" };
Text { content == " post " };
Raw { content == "<a style=\"color:red\">" };
Text { content == "Hello" };
Raw { content == "</a>" };
Text { content == "." };
};
);
}
#[test]
fn paired() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
%<[main]
function my_style_start()
nml.raw.push("inline", "start")
end
function my_style_end()
nml.raw.push("inline", "end")
end
function red_style_start()
nml.raw.push("inline", "<a style=\"color:red\">")
end
function red_style_end()
nml.raw.push("inline", "</a>")
end
nml.custom_style.define_paired("My Style", "[", "]", "my_style_start()", "my_style_end()")
nml.custom_style.define_paired("My Style2", "(", ")", "red_style_start()", "red_style_end()")
>%
pre [styled] post (Hello).
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Paragraph {
Text { content == "pre " };
Raw { content == "start" };
Text { content == "styled" };
Raw { content == "end" };
Text { content == " post " };
Raw { content == "<a style=\"color:red\">" };
Text { content == "Hello" };
Raw { content == "</a>" };
Text { content == "." };
};
);
}
}

227
src/elements/elemstyle.rs Normal file
View file

@ -0,0 +1,227 @@
use crate::parser::style::ElementStyle;
use std::any::Any;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
use ariadne::Fmt;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use mlua::Error::BadArgument;
use mlua::Function;
use mlua::Lua;
use mlua::Value;
use regex::Regex;
use crate::document::document::Document;
use crate::lua::kernel::CTX;
use crate::parser::parser::ParserState;
use crate::parser::rule::Rule;
use crate::parser::source::Cursor;
use crate::parser::source::Source;
pub struct ElemStyleRule {
start_re: Regex,
}
impl ElemStyleRule {
pub fn new() -> Self {
Self {
start_re: Regex::new(r"(?:^|\n)@@(.*?)=\s*\{").unwrap(),
}
}
/// Finds the json substring inside aother string
pub fn json_substring(str: &str) -> Option<&str> {
let mut in_string = false;
let mut brace_depth = 0;
let mut escaped = false;
for (pos, c) in str.char_indices() {
match c {
'{' if !in_string => brace_depth += 1,
'}' if !in_string => brace_depth -= 1,
'\\' if in_string => escaped = !escaped,
'"' if !escaped => in_string = !in_string,
_ => escaped = false,
}
if brace_depth == 0 {
return Some(&str[..=pos]);
}
}
None
}
}
impl Rule for ElemStyleRule {
fn name(&self) -> &'static str { "Element Style" }
fn next_match(&self, _state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> {
self.start_re
.find_at(cursor.source.content(), cursor.pos)
.map_or(None, |m| {
Some((m.start(), Box::new([false; 0]) as Box<dyn Any>))
})
}
fn on_match<'a>(
&self,
state: &ParserState,
_document: &'a (dyn Document<'a> + 'a),
cursor: Cursor,
_match_data: Box<dyn Any>,
) -> (Cursor, Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>>) {
let mut reports = vec![];
let matches = self
.start_re
.captures_at(cursor.source.content(), cursor.pos)
.unwrap();
let mut cursor = cursor.at(matches.get(0).unwrap().end() - 1);
let style: Rc<dyn ElementStyle> = if let Some(key) = matches.get(1) {
let trimmed = key.as_str().trim_start().trim_end();
// Check if empty
if trimmed.is_empty() {
reports.push(
Report::build(ReportKind::Error, cursor.source.clone(), key.start())
.with_message("Empty Style Key")
.with_label(
Label::new((cursor.source.clone(), key.range()))
.with_message(format!("Expected a non-empty style key",))
.with_color(state.parser.colors().error),
)
.finish(),
);
return (cursor, reports);
}
// Check if key exists
if !state.shared.styles.borrow().is_registered(trimmed) {
reports.push(
Report::build(ReportKind::Error, cursor.source.clone(), key.start())
.with_message("Unknown Style Key")
.with_label(
Label::new((cursor.source.clone(), key.range()))
.with_message(format!(
"Could not find a style with key: {}",
trimmed.fg(state.parser.colors().info)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return (cursor, reports);
}
state.shared.styles.borrow().current(trimmed)
} else {
panic!("Unknown error")
};
// Get value
let new_style = match ElemStyleRule::json_substring(
&cursor.source.clone().content().as_str()[cursor.pos..],
) {
None => {
reports.push(
Report::build(ReportKind::Error, cursor.source.clone(), cursor.pos)
.with_message("Invalid Style Value")
.with_label(
Label::new((cursor.source.clone(), matches.get(0).unwrap().range()))
.with_message(format!(
"Unable to parse json string after style key",
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return (cursor, reports);
}
Some(json) => {
cursor = cursor.at(cursor.pos + json.len());
// Attempt to deserialize
match style.from_json(json) {
Err(err) => {
reports.push(
Report::build(ReportKind::Error, cursor.source.clone(), cursor.pos)
.with_message("Invalid Style Value")
.with_label(
Label::new((
cursor.source.clone(),
cursor.pos..cursor.pos + json.len(),
))
.with_message(format!(
"Failed to serialize `{}` into style with key `{}`: {err}",
json.fg(state.parser.colors().highlight),
style.key().fg(state.parser.colors().info)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return (cursor, reports);
}
Ok(style) => style,
}
}
};
state.shared.styles.borrow_mut().set_current(new_style);
(cursor, reports)
}
fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> {
let mut bindings = vec![];
bindings.push((
"set".to_string(),
lua.create_function(|lua, (style_key, new_style): (String, Value)| {
let mut result = Ok(());
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
if !ctx
.state
.shared
.styles
.borrow()
.is_registered(style_key.as_str())
{
result = Err(BadArgument {
to: Some("set".to_string()),
pos: 1,
name: Some("style_key".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Unable to find style with key: {style_key}"
))),
});
return;
}
let style = ctx.state.shared.styles.borrow().current(style_key.as_str());
let new_style = match style.from_lua(lua, new_style) {
Err(err) => {
result = Err(err);
return;
}
Ok(new_style) => new_style,
};
ctx.state.shared.styles.borrow_mut().set_current(new_style);
})
});
result
})
.unwrap(),
));
bindings
}
}

361
src/elements/graphviz.rs Normal file
View file

@ -0,0 +1,361 @@
use std::collections::HashMap;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Once;
use crate::parser::parser::ParserState;
use crate::parser::util::Property;
use crate::parser::util::PropertyMapError;
use crate::parser::util::PropertyParser;
use ariadne::Fmt;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use crypto::digest::Digest;
use crypto::sha2::Sha512;
use graphviz_rust::cmd::Format;
use graphviz_rust::cmd::Layout;
use graphviz_rust::exec_dot;
use regex::Captures;
use regex::Regex;
use crate::cache::cache::Cached;
use crate::cache::cache::CachedError;
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target;
use crate::document::document::Document;
use crate::document::element::ElemKind;
use crate::document::element::Element;
use crate::parser::rule::RegexRule;
use crate::parser::source::Source;
use crate::parser::source::Token;
use crate::parser::util;
#[derive(Debug)]
struct Graphviz {
pub location: Token,
pub dot: String,
pub layout: Layout,
pub width: String,
}
fn layout_from_str(value: &str) -> Result<Layout, String> {
match value {
"dot" => Ok(Layout::Dot),
"neato" => Ok(Layout::Neato),
"fdp" => Ok(Layout::Fdp),
"sfdp" => Ok(Layout::Sfdp),
"circo" => Ok(Layout::Circo),
"twopi" => Ok(Layout::Twopi),
"osage" => Ok(Layout::Asage), // typo in graphviz_rust ?
"patchwork" => Ok(Layout::Patchwork),
_ => Err(format!("Unknown layout: {value}")),
}
}
impl Graphviz {
/// Renders dot to svg
fn dot_to_svg(&self) -> Result<String, String> {
print!("Rendering Graphviz `{}`... ", self.dot);
let svg = match exec_dot(
self.dot.clone(),
vec![self.layout.into(), Format::Svg.into()],
) {
Ok(svg) => {
let out = String::from_utf8_lossy(svg.as_slice());
let svg_start = out.find("<svg").unwrap(); // Remove svg header
let split_at = out.split_at(svg_start).1.find('\n').unwrap();
let mut result = format!("<svg width=\"{}\"", self.width);
result.push_str(out.split_at(svg_start + split_at).1);
result
}
Err(e) => return Err(format!("Unable to execute dot: {e}")),
};
println!("Done!");
Ok(svg)
}
}
impl Cached for Graphviz {
type Key = String;
type Value = String;
fn sql_table() -> &'static str {
"CREATE TABLE IF NOT EXISTS cached_dot (
digest TEXT PRIMARY KEY,
svg BLOB NOT NULL);"
}
fn sql_get_query() -> &'static str { "SELECT svg FROM cached_dot WHERE digest = (?1)" }
fn sql_insert_query() -> &'static str { "INSERT INTO cached_dot (digest, svg) VALUES (?1, ?2)" }
fn key(&self) -> <Self as Cached>::Key {
let mut hasher = Sha512::new();
hasher.input((self.layout as usize).to_be_bytes().as_slice());
hasher.input(self.width.as_bytes());
hasher.input(self.dot.as_bytes());
hasher.result_str()
}
}
impl Element for Graphviz {
fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Block }
fn element_name(&self) -> &'static str { "Graphviz" }
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> {
match compiler.target() {
Target::HTML => {
static CACHE_INIT: Once = Once::new();
CACHE_INIT.call_once(|| {
if let Some(mut con) = compiler.cache() {
if let Err(e) = Graphviz::init(&mut con) {
eprintln!("Unable to create cache table: {e}");
}
}
});
// TODO: Format svg in a div
if let Some(mut con) = compiler.cache() {
match self.cached(&mut con, |s| s.dot_to_svg()) {
Ok(s) => Ok(s),
Err(e) => match e {
CachedError::SqlErr(e) => {
Err(format!("Querying the cache failed: {e}"))
}
CachedError::GenErr(e) => Err(e),
},
}
} else {
match self.dot_to_svg() {
Ok(svg) => Ok(svg),
Err(e) => Err(e),
}
}
}
_ => todo!("Unimplemented"),
}
}
}
pub struct GraphRule {
re: [Regex; 1],
properties: PropertyParser,
}
impl GraphRule {
pub fn new() -> Self {
let mut props = HashMap::new();
props.insert(
"layout".to_string(),
Property::new(
true,
"Graphviz layout engine see <https://graphviz.org/docs/layouts/>".to_string(),
Some("dot".to_string()),
),
);
props.insert(
"width".to_string(),
Property::new(true, "SVG width".to_string(), Some("100%".to_string())),
);
Self {
re: [Regex::new(
r"\[graph\](?:\[((?:\\.|[^\[\]\\])*?)\])?(?:((?:\\.|[^\\\\])*?)\[/graph\])?",
)
.unwrap()],
properties: PropertyParser { properties: props },
}
}
}
impl RegexRule for GraphRule {
fn name(&self) -> &'static str { "Graph" }
fn regexes(&self) -> &[regex::Regex] { &self.re }
fn on_regex_match(
&self,
_: usize,
state: &ParserState,
document: &dyn Document,
token: Token,
matches: Captures,
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
let mut reports = vec![];
let graph_content = match matches.get(2) {
// Unterminated `[graph]`
None => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Unterminated Graph Code")
.with_label(
Label::new((token.source().clone(), token.range.clone()))
.with_message(format!(
"Missing terminating `{}` after first `{}`",
"[/graph]".fg(state.parser.colors().info),
"[graph]".fg(state.parser.colors().info)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
Some(content) => {
let processed = util::process_escaped(
'\\',
"[/graph]",
content.as_str().trim_start().trim_end(),
);
if processed.is_empty() {
reports.push(
Report::build(ReportKind::Error, token.source(), content.start())
.with_message("Empty Graph Code")
.with_label(
Label::new((token.source().clone(), content.range()))
.with_message("Graph code is empty")
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
processed
}
};
// Properties
let properties = match matches.get(1) {
None => match self.properties.default() {
Ok(properties) => properties,
Err(e) => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid Graph")
.with_label(
Label::new((token.source().clone(), token.range.clone()))
.with_message(format!("Graph is missing property: {e}"))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
},
Some(props) => {
let processed =
util::process_escaped('\\', "]", props.as_str().trim_start().trim_end());
match self.properties.parse(processed.as_str()) {
Err(e) => {
reports.push(
Report::build(ReportKind::Error, token.source(), props.start())
.with_message("Invalid Graph Properties")
.with_label(
Label::new((token.source().clone(), props.range()))
.with_message(e)
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
Ok(properties) => properties,
}
}
};
// Property "layout"
let graph_layout = match properties.get("layout", |prop, value| {
layout_from_str(value.as_str()).map_err(|e| (prop, e))
}) {
Ok((_prop, kind)) => kind,
Err(e) => match e {
PropertyMapError::ParseError((prop, err)) => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid Graph Property")
.with_label(
Label::new((token.source().clone(), token.range.clone()))
.with_message(format!(
"Property `layout: {}` cannot be converted: {}",
prop.fg(state.parser.colors().info),
err.fg(state.parser.colors().error)
))
.with_color(state.parser.colors().warning),
)
.finish(),
);
return reports;
}
PropertyMapError::NotFoundError(err) => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid Graph Property")
.with_label(
Label::new((
token.source().clone(),
token.start() + 1..token.end(),
))
.with_message(err)
.with_color(state.parser.colors().warning),
)
.finish(),
);
return reports;
}
},
};
// FIXME: You can escape html, make sure we escape single "
// Property "width"
let graph_width = match properties.get("width", |_, value| -> Result<String, ()> {
Ok(value.clone())
}) {
Ok((_, kind)) => kind,
Err(e) => match e {
PropertyMapError::NotFoundError(err) => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid Graph Property")
.with_label(
Label::new((
token.source().clone(),
token.start() + 1..token.end(),
))
.with_message(format!(
"Property `{}` is missing",
err.fg(state.parser.colors().info)
))
.with_color(state.parser.colors().warning),
)
.finish(),
);
return reports;
}
_ => panic!("Unknown error"),
},
};
state.push(
document,
Box::new(Graphviz {
location: token,
dot: graph_content,
layout: graph_layout,
width: graph_width,
}),
);
reports
}
}

182
src/elements/import.rs Normal file
View file

@ -0,0 +1,182 @@
use crate::document::document::Document;
use crate::document::document::DocumentAccessors;
use crate::parser::parser::ParserState;
use crate::parser::parser::ReportColors;
use crate::parser::rule::RegexRule;
use crate::parser::source::Source;
use crate::parser::source::SourceFile;
use crate::parser::source::Token;
use ariadne::Fmt;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use regex::Captures;
use regex::Regex;
use std::ops::Range;
use std::rc::Rc;
use super::paragraph::Paragraph;
pub struct ImportRule {
re: [Regex; 1],
}
impl ImportRule {
pub fn new() -> Self {
Self {
re: [Regex::new(r"(?:^|\n)@import(?:\[(.*)\])?[^\S\r\n]+(.*)").unwrap()],
}
}
pub fn validate_name(_colors: &ReportColors, name: &str) -> Result<String, String> {
Ok(name.to_string())
}
pub fn validate_as(_colors: &ReportColors, as_name: &str) -> Result<String, String> {
// TODO: Use variable name validation rules
Ok(as_name.to_string())
}
}
impl RegexRule for ImportRule {
fn name(&self) -> &'static str { "Import" }
fn regexes(&self) -> &[Regex] { &self.re }
fn on_regex_match<'a>(
&self,
_: usize,
state: &ParserState,
document: &'a dyn Document<'a>,
token: Token,
matches: Captures,
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
let mut result = vec![];
// Path
let import_file = match matches.get(2) {
Some(name) => match ImportRule::validate_name(state.parser.colors(), name.as_str()) {
Err(msg) => {
result.push(
Report::build(ReportKind::Error, token.source(), name.start())
.with_message("Invalid name for import")
.with_label(
Label::new((token.source(), name.range()))
.with_message(format!(
"Import name `{}` is invalid. {msg}",
name.as_str().fg(state.parser.colors().highlight)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return result;
}
Ok(filename) => {
let meta = match std::fs::metadata(filename.as_str()) {
Err(_) => {
result.push(
Report::build(ReportKind::Error, token.source(), name.start())
.with_message("Invalid import path")
.with_label(
Label::new((token.source(), name.range()))
.with_message(format!(
"Unable to access file `{}`",
filename.fg(state.parser.colors().highlight)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return result;
}
Ok(meta) => meta,
};
if !meta.is_file() {
result.push(
Report::build(ReportKind::Error, token.source(), name.start())
.with_message("Invalid import path")
.with_label(
Label::new((token.source(), name.range()))
.with_message(format!(
"Path `{}` is not a file!",
filename.fg(state.parser.colors().highlight)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return result;
}
filename
}
},
_ => panic!("Invalid name for import"),
};
// [Optional] import as
let import_as = match matches.get(1) {
Some(as_name) => match ImportRule::validate_as(state.parser.colors(), as_name.as_str())
{
Ok(as_name) => as_name,
Err(msg) => {
result.push(
Report::build(ReportKind::Error, token.source(), as_name.start())
.with_message("Invalid name for import as")
.with_label(
Label::new((token.source(), as_name.range()))
.with_message(format!(
"Canot import `{import_file}` as `{}`. {msg}",
as_name.as_str().fg(state.parser.colors().highlight)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return result;
}
},
_ => "".to_string(),
};
let import = match SourceFile::new(import_file, Some(token.clone())) {
Ok(import) => Rc::new(import),
Err(path) => {
result.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Unable to read file content")
.with_label(
Label::new((token.source(), token.range))
.with_message(format!("Failed to read content from path `{path}`"))
.with_color(state.parser.colors().error),
)
.finish(),
);
return result;
}
};
state.with_state(|new_state| {
let (import_doc, _) = new_state.parser.parse(new_state, import, Some(document));
document.merge(import_doc.content(), import_doc.scope(), Some(&import_as));
});
// Close paragraph
// TODO2: Check if this is safe to remove
if document.last_element::<Paragraph>().is_none() {
state.push(
document,
Box::new(Paragraph {
location: Token::new(token.end()..token.end(), token.source()),
content: Vec::new(),
}),
);
}
return result;
}
}

View file

@ -1,76 +0,0 @@
use tower_lsp::lsp_types::CompletionContext;
use tower_lsp::lsp_types::CompletionItem;
use tower_lsp::lsp_types::CompletionItemKind;
use tower_lsp::lsp_types::InsertTextFormat;
use tower_lsp::lsp_types::MarkupContent;
use crate::lsp::completion::context_triggered;
use crate::lsp::completion::CompletionProvider;
use crate::unit::translation::TranslationUnit;
pub struct ImportCompletion;
impl CompletionProvider for ImportCompletion {
fn trigger(&self) -> &'static [&'static str] {
["@"].as_slice()
}
fn unit_items(&self, _unit: &TranslationUnit, _items: &mut Vec<CompletionItem>) {}
fn static_items(&self, context: &Option<CompletionContext>, items: &mut Vec<CompletionItem>) {
// @import
items.push(CompletionItem {
label: "@import ".to_string(),
detail: Some("Import a file".into()),
documentation: Some(tower_lsp::lsp_types::Documentation::MarkupContent(
MarkupContent {
kind: tower_lsp::lsp_types::MarkupKind::Markdown,
value: "# Usage
`@import FILE`
Import another file's content. The content of the imported file is added to the
current file at the location of the import call.
When importing a file, all references will be exported to the importing file.
Variables set using `:set` will not be exported. If you want variables to be
available in subsequent imports, you need to use `:export` when defining variables.
# Examples
* `@import \"source.nml\"` *imports file `source.nml` into the current file*
[current.nml]():
Content
@import \"footer.nml\"
[footer.nml]():
Footer
Will result in:
[current.nml]():
Content
Footer
# See also
* `:export` *export a variable*"
.into(),
},
)),
kind: Some(CompletionItemKind::FUNCTION),
insert_text_format: Some(InsertTextFormat::SNIPPET),
insert_text: Some(format!(
"{}import \"${{1:FILE}}\"",
if context_triggered(context, "@") {
""
} else {
"@"
}
)),
..CompletionItem::default()
});
}
}

View file

@ -1,72 +0,0 @@
use std::sync::Arc;
use ariadne::Span;
use parking_lot::RwLock;
use crate::compiler::compiler::Compiler;
use crate::compiler::output::CompilerOutput;
use crate::parser::reports::Report;
use crate::parser::source::Token;
use crate::unit::element::ContainerElement;
use crate::unit::element::ElemKind;
use crate::unit::element::Element;
use crate::unit::element::LinkableElement;
use crate::unit::element::ReferenceableElement;
use crate::unit::scope::Scope;
use crate::unit::scope::ScopeAccessor;
#[derive(Debug)]
pub struct Import {
pub(crate) location: Token,
pub(crate) content: Vec<Arc<RwLock<Scope>>>,
}
impl Element for Import {
fn location(&self) -> &Token {
&self.location
}
fn kind(&self) -> ElemKind {
ElemKind::Compound
}
fn element_name(&self) -> &'static str {
"Import"
}
fn compile(
&self,
scope: Arc<RwLock<Scope>>,
compiler: &Compiler,
output: &mut CompilerOutput,
) -> Result<(), Vec<Report>> {
for scope in self.content.iter().cloned() {
for (scope, elem) in scope.content_iter(false) {
elem.compile(scope, compiler, output)?
}
}
Ok(())
}
fn provide_hover(&self) -> Option<String> {
Some(format!(
"Import
# Properties
* **Location**: [{}] ({}..{})",
self.location.source().name(),
self.location().range.start(),
self.location().range.end(),
))
}
fn as_container(self: Arc<Self>) -> Option<Arc<dyn ContainerElement>> {
Some(self)
}
}
impl ContainerElement for Import {
fn contained(&self) -> &[Arc<RwLock<Scope>>] {
self.content.as_slice()
}
}

View file

@ -1,3 +0,0 @@
pub mod completion;
pub mod elem;
pub mod rule;

View file

@ -1,197 +0,0 @@
use std::env::current_dir;
use std::sync::Arc;
use ariadne::Fmt;
use parser::rule::RegexRule;
use parser::source::Token;
use regex::Captures;
use regex::Regex;
use crate::parser::reports::macros::*;
use crate::parser::reports::*;
use crate::parser::rule::RuleTarget;
use crate::parser::source::SourceFile;
use crate::parser::state::CustomStates;
use crate::parser::state::ParseMode;
use crate::parser::util;
use crate::unit::scope::ScopeAccessor;
use crate::unit::translation::TranslationAccessors;
use crate::unit::translation::TranslationUnit;
use super::completion::ImportCompletion;
use super::elem::Import;
#[auto_registry::auto_registry(registry = "rules")]
pub struct ImportRule {
re: [Regex; 1],
}
impl Default for ImportRule {
fn default() -> Self {
Self {
re: [Regex::new(r#"(?:^|\n)(@import)\s+(")?((?:[^"\\]|\\.)*)(")?([^\n]*)"#).unwrap()],
}
}
}
impl RegexRule for ImportRule {
fn name(&self) -> &'static str {
"Import"
}
fn target(&self) -> RuleTarget {
RuleTarget::Command
}
fn regexes(&self) -> &[Regex] {
&self.re
}
fn enabled(
&self,
_unit: &TranslationUnit,
mode: &ParseMode,
_states: &mut CustomStates,
_id: usize,
) -> bool {
!mode.paragraph_only
}
fn on_regex_match<'u>(
&self,
_index: usize,
unit: &mut TranslationUnit,
token: Token,
captures: Captures,
) {
let path = captures.get(3).unwrap();
// Missing starting '"'
if captures.get(2).is_none() {
report_err!(
unit,
token.source(),
"Invalid import".into(),
span(
path.start()..path.start(),
format!(
"Missing `{}` delimiter for import",
"\"".fg(unit.colors().info)
)
),
);
return;
}
// Missing ending '"'
if captures.get(4).is_none() {
report_err!(
unit,
token.source(),
"Invalid import".into(),
span(
path.end() - 1..path.end() - 1,
format!(
"Missing `{}` delimiter for import",
"\"".fg(unit.colors().info)
)
),
);
return;
}
// Leftover
if !captures.get(5).unwrap().as_str().trim_start().is_empty() {
report_err!(
unit,
token.source(),
"Invalid import".into(),
span(
captures.get(5).unwrap().range(),
format!("Unexpected content here")
),
);
return;
}
unit.with_lsp(|lsp| {
lsp.with_semantics(token.source(), |sems, tokens| {
sems.add(captures.get(1).unwrap().range(), tokens.import);
sems.add(
captures.get(2).unwrap().start()..captures.get(5).unwrap().end(),
tokens.import_path,
);
})
});
// Build path
let path_content = util::escape_text('\\', "\"", path.as_str(), false);
let path_buf = match std::fs::canonicalize(&path_content) {
Ok(path) => path,
Err(err) => {
report_err!(
unit,
token.source(),
"Invalid import".into(),
span(
path.range(),
format!(
"Failed to canonicalize `{}`: {err}",
path_content.fg(unit.colors().highlight)
)
),
note(format!(
"Current working directory: {}",
current_dir()
.unwrap()
.to_string_lossy()
.fg(unit.colors().info)
))
);
return;
}
};
// Parse imported
let source = match SourceFile::new(
path_buf.to_str().expect("Invalid path").to_string(),
Some(token.clone()),
) {
Ok(source) => source,
Err(err) => {
report_err!(
unit,
token.source(),
"Invalid import".into(),
span(path.range(), format!("{err}"))
);
return;
}
};
let content = unit.with_child(
Arc::new(source),
ParseMode::default(),
true,
|unit, scope| {
unit.with_lsp(|lsp| {
lsp.add_definition(token.clone(), &Token::new(0..0, scope.read().source()))
});
unit.parser.clone().parse(unit);
scope
},
);
unit.get_scope().add_import(content.clone());
unit.add_content(Arc::new(Import {
location: token,
content: vec![content],
}));
}
fn completion(
&self,
) -> Option<Box<dyn lsp::completion::CompletionProvider + 'static + Send + Sync>> {
Some(Box::new(ImportCompletion {}))
}
}

View file

@ -1,169 +0,0 @@
use std::sync::Arc;
use ariadne::Span;
use tower_lsp::lsp_types::CompletionContext;
use tower_lsp::lsp_types::CompletionItem;
use tower_lsp::lsp_types::CompletionItemKind;
use tower_lsp::lsp_types::InsertTextFormat;
use tower_lsp::lsp_types::MarkupContent;
use tower_lsp::lsp_types::MarkupKind::Markdown;
use crate::lsp::completion::context_triggered;
use crate::lsp::completion::CompletionProvider;
use crate::lsp::data::LangServerData;
use crate::lsp::reference::LsReference;
use crate::unit::element::ReferenceableElement;
use crate::unit::scope::ScopeAccessor;
use crate::unit::translation::TranslationUnit;
pub struct ReferenceCompletion;
impl ReferenceCompletion {
pub(crate) fn get_documentation(reference: &LsReference, label: &String) -> String {
format!(
"Reference `{}`
Full name: [{label}]()
# Definition
* **Defined in**: [{}]() ({}..{})
* **Type**: `{}`",
reference.name,
reference.source_path,
reference.range.start(),
reference.range.end(),
reference.reftype
)
}
pub(crate) fn export_internal_ref(
unit: &TranslationUnit,
lsp: &mut LangServerData,
elem: Arc<dyn ReferenceableElement>,
) {
let Some(referenceable) = elem.as_referenceable() else {
return;
};
let iref = referenceable.reference();
let label = iref.name().to_string();
lsp.external_refs.insert(
label.clone(),
LsReference {
name: label,
range: iref.location().range.clone(),
source_path: iref.location().source().name().to_owned(),
source_refkey: unit.reference_key().to_owned(),
reftype: referenceable.refcount_key().to_owned(),
},
);
}
}
impl CompletionProvider for ReferenceCompletion {
fn trigger(&self) -> &'static [&'static str] {
["&"].as_slice()
}
fn unit_items(&self, unit: &TranslationUnit, items: &mut Vec<CompletionItem>) {
let start = format!("{}#", unit.reference_key());
unit.with_lsp(|mut lsp| {
unit.get_entry_scope()
.content_iter(true)
.filter_map(|(_, elem)| elem.as_referenceable())
.for_each(|referenceable| {
Self::export_internal_ref(unit, &mut lsp, referenceable);
});
lsp.external_refs.iter().for_each(|(label, reference)| {
if label.starts_with(&start) {
return;
}
items.push(CompletionItem {
label: label.clone(),
detail: Some(format!("Reference {label}")),
documentation: Some(tower_lsp::lsp_types::Documentation::MarkupContent(
MarkupContent {
kind: Markdown,
value: Self::get_documentation(reference, label),
},
)),
kind: Some(CompletionItemKind::REFERENCE),
..CompletionItem::default()
});
});
});
}
fn static_items(&self, context: &Option<CompletionContext>, items: &mut Vec<CompletionItem>) {
// &{ref}
items.push(CompletionItem {
label: "&{ref}".to_string(),
detail: Some("Link to reference".into()),
documentation: Some(tower_lsp::lsp_types::Documentation::MarkupContent(
MarkupContent {
kind: tower_lsp::lsp_types::MarkupKind::Markdown,
value: "# Usage
`&{REF}` Link to reference `REF`
`&{REF}[DISP]` Link to reference `REF` displayed using `DISP`
Create a link to a reference.
# Examples
* `&{foo}` *Will display a link to reference **foo***
* `&{bar}[click me]` *Will display `click me` that will link to reference **bar***
* `&{source#baz}` *Will display a link to reference **baz** declared in unit **source***"
.into(),
},
)),
kind: Some(CompletionItemKind::SNIPPET),
insert_text_format: Some(InsertTextFormat::SNIPPET),
insert_text: Some(format!(
"{}{{${{1:REFNAME}}}}",
if context_triggered(context, "&") {
""
} else {
"&"
}
)),
..CompletionItem::default()
});
// &{ref}[disp]
items.push(CompletionItem {
label: "&{ref}[disp]".to_string(),
detail: Some("Link to reference with display".into()),
documentation: Some(tower_lsp::lsp_types::Documentation::MarkupContent(
MarkupContent {
kind: tower_lsp::lsp_types::MarkupKind::Markdown,
value: "# Usage
`&{REF}` Link to reference `REF`
`&{REF}[DISP]` Link to reference `REF` displayed using `DISP`
Create a link to a reference.
# Examples
* `&{foo}` *Will display a link to reference **foo***
* `&{bar}[click me]` *Will display `click me` that will link to reference **bar***
* `&{source#baz}` *Will display a link to reference **baz** declared in unit **source***"
.into(),
},
)),
kind: Some(CompletionItemKind::SNIPPET),
insert_text_format: Some(InsertTextFormat::SNIPPET),
insert_text: Some(format!(
"{}{{${{1:REFNAME}}}}[${{2:DISPLAY}}]",
if context_triggered(context, "&") {
""
} else {
"&"
}
)),
..CompletionItem::default()
});
}
}

View file

@ -1,93 +0,0 @@
use std::sync::Arc;
use std::sync::OnceLock;
use parking_lot::RwLock;
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target;
use crate::compiler::output::CompilerOutput;
use crate::parser::reports::Report;
use crate::parser::source::Token;
use crate::unit::element::ContainerElement;
use crate::unit::element::ElemKind;
use crate::unit::element::Element;
use crate::unit::element::LinkableElement;
use crate::unit::element::ReferenceableElement;
use crate::unit::references::Refname;
use crate::unit::scope::Scope;
use crate::unit::scope::ScopeAccessor;
use crate::unit::unit::Reference;
#[derive(Debug)]
pub struct InternalLink {
pub(crate) location: Token,
pub(crate) refname: Refname,
pub(crate) display: Vec<Arc<RwLock<Scope>>>,
pub(crate) reference: OnceLock<(String, Reference)>,
}
impl Element for InternalLink {
fn location(&self) -> &Token {
&self.location
}
fn kind(&self) -> ElemKind {
ElemKind::Compound
}
fn element_name(&self) -> &'static str {
"Internal Link"
}
fn compile(
&self,
_scope: Arc<RwLock<Scope>>,
compiler: &Compiler,
output: &mut CompilerOutput,
) -> Result<(), Vec<Report>> {
match compiler.target() {
Target::HTML => {
output.add_content(format!("<a href=\"{}\">", self.reference.get().unwrap().0));
let display = &self.display[0];
for (scope, elem) in display.content_iter(false) {
elem.compile(scope, compiler, output)?;
}
output.add_content("</a>");
}
_ => todo!(""),
}
Ok(())
}
fn as_referenceable(self: Arc<Self>) -> Option<Arc<dyn ReferenceableElement>> {
None
}
fn as_linkable(self: Arc<Self>) -> Option<Arc<dyn LinkableElement>> {
Some(self)
}
fn as_container(self: Arc<Self>) -> Option<Arc<dyn ContainerElement>> {
Some(self)
}
}
impl ContainerElement for InternalLink {
fn contained(&self) -> &[Arc<RwLock<Scope>>] {
&self.display
}
}
impl LinkableElement for InternalLink {
fn wants_refname(&self) -> &Refname {
&self.refname
}
fn wants_link(&self) -> bool {
self.reference.get().is_none()
}
fn set_link(&self, reference: Reference, link: String) {
self.reference.set((link, reference)).unwrap();
}
}

View file

@ -1,3 +0,0 @@
pub mod completion;
pub mod elem;
pub mod rule;

View file

@ -1,250 +0,0 @@
use crate::elements::text::elem::Text;
use crate::parser::reports::macros::*;
use crate::parser::reports::*;
use crate::parser::rule::RuleTarget;
use crate::parser::state::CustomStates;
use crate::parser::state::ParseMode;
use crate::parser::util::escape_source;
use crate::parser::util::parse_paragraph;
use crate::unit::references::Refname;
use crate::unit::scope::ScopeAccessor;
use crate::unit::translation::TranslationAccessors;
use crate::unit::translation::TranslationUnit;
use ariadne::Fmt;
use ariadne::Span;
use regex::Captures;
use regex::Regex;
use std::sync::Arc;
use std::sync::OnceLock;
use crate::parser::reports::Report;
use crate::parser::rule::RegexRule;
use crate::parser::source::Token;
use super::completion::ReferenceCompletion;
use super::elem::InternalLink;
#[auto_registry::auto_registry(registry = "rules")]
pub struct InternalLinkRule {
re: [Regex; 1],
}
impl Default for InternalLinkRule {
fn default() -> Self {
Self {
re: [Regex::new(r"&\{([^\}\r\n]*)(\})?(?:(\[)((?:[^]\\]|\\.)*)(\])?)?").unwrap()],
}
}
}
impl RegexRule for InternalLinkRule {
fn name(&self) -> &'static str {
"Inernal Link"
}
fn target(&self) -> RuleTarget {
RuleTarget::Inline
}
fn regexes(&self) -> &[regex::Regex] {
&self.re
}
fn enabled(
&self,
_unit: &TranslationUnit,
_mode: &ParseMode,
_states: &mut CustomStates,
_id: usize,
) -> bool {
true
}
fn on_regex_match<'u>(
&self,
_index: usize,
unit: &mut TranslationUnit,
token: Token,
captures: Captures,
) {
let link = captures.get(1).unwrap();
// Missing '}'
if captures.get(2).is_none() {
report_err!(
unit,
token.source(),
"Invalid internal link".into(),
span(
link.end()..link.end() + 1,
format!("Missing closing `{}`", "}".fg(unit.colors().info))
),
span_highlight(
link.start() - 1..link.start(),
format!("Opening `{}` here", "{".fg(unit.colors().highlight))
),
note("While attempting to parse internal link path".into())
);
return;
}
let link_content = link.as_str().trim_start().trim_end();
// Empty link
if link_content.is_empty() {
report_err!(
unit,
token.source(),
"Invalid internal link".into(),
span(link.range(), format!("Expected path, found empty string"))
);
return;
}
// Link to refname
let link_refname = match Refname::try_from(link_content) {
Ok(refname) => {
if let Refname::Internal(name) = &refname {
if let Some(reference) = unit.get_reference(name) {
unit.with_lsp(|mut lsp| {
ReferenceCompletion::export_internal_ref(unit, &mut lsp, reference);
});
}
}
refname
}
Err(err) => {
report_err!(
unit,
token.source(),
"Invalid internal link".into(),
span(link.range(), err),
note("While attempting to parse internal link path".into())
);
return;
}
};
unit.with_lsp(|lsp| {
lsp.with_semantics(token.source(), |sems, tokens| {
let range = captures.get(0).unwrap().range();
sems.add(
range.start()..range.start() + 2,
tokens.internal_link_ref_sep,
);
sems.add(link.range(), tokens.internal_link_ref);
sems.add(link.end()..link.end() + 1, tokens.internal_link_ref_sep);
})
});
// Custom display, if '[' present
let display = if captures.get(3).is_some() {
let display = captures.get(4).unwrap();
// Missing ']'
if captures.get(5).is_none() {
report_err!(
unit,
token.source(),
"Invalid internal link".into(),
span(
display.end() - 1..display.end(),
format!("Missing closing `{}`", "]".fg(unit.colors().info))
),
span_highlight(
display.start() - 1..display.start(),
format!("Opening `{}` here", "[".fg(unit.colors().highlight))
),
note("While attempting to parse internal link display".into())
);
return;
}
unit.with_lsp(|lsp| {
lsp.with_semantics(token.source(), |sems, tokens| {
sems.add(
display.start() - 1..display.start(),
tokens.internal_link_display_sep,
);
sems.add_to_queue(
display.end()..display.end() + 1,
tokens.internal_link_display_sep,
);
})
});
let display_source = escape_source(
token.source(),
display.range(),
"Internal Link Display".into(),
'\\',
"]",
);
match parse_paragraph(unit, display_source) {
Err(err) => {
report_err!(
unit,
token.source(),
"Invalid internal link display".into(),
span(
display.range(),
format!("Failed to parse internal link display:\n{err}")
)
);
return;
}
Ok(paragraph) => paragraph,
}
}
// Default display
else {
let display_source = token.to_source(format!(
"Internal link display for `{}`",
link_refname.to_string()
));
// Add content to scope
unit.with_child(
display_source.clone(),
ParseMode::default(),
true,
|_unit, scope| {
scope.add_content(Arc::new(Text {
location: display_source.into(),
content: link_refname.to_string(),
}));
scope
},
)
};
unit.with_lsp(|lsp| {
let label = link_refname.to_string();
let Some(reference) = lsp.external_refs.get(&label) else {
return;
};
let Some(ref_source) = lsp.get_source(reference.source_path.as_str()) else {
return;
};
let source = Token::new(link.range(), token.source());
lsp.add_definition(
source.clone(),
&Token::new(reference.range.clone(), ref_source),
);
lsp.add_hover(
source,
ReferenceCompletion::get_documentation(reference, &label),
);
});
unit.add_content(Arc::new(InternalLink {
location: token.clone(),
refname: link_refname,
display: vec![display],
reference: OnceLock::new(),
}));
}
fn completion(
&self,
) -> Option<Box<dyn lsp::completion::CompletionProvider + 'static + Send + Sync>> {
Some(Box::new(ReferenceCompletion {}))
}
}

View file

@ -1,115 +0,0 @@
use tower_lsp::lsp_types::CompletionContext;
use tower_lsp::lsp_types::CompletionItem;
use tower_lsp::lsp_types::CompletionItemKind;
use tower_lsp::lsp_types::InsertTextFormat;
use tower_lsp::lsp_types::MarkupContent;
use crate::lsp::completion::context_triggered;
use crate::lsp::completion::CompletionProvider;
use crate::unit::translation::TranslationUnit;
pub struct LatexCompletion;
impl CompletionProvider for LatexCompletion {
fn trigger(&self) -> &'static [&'static str] {
["$"].as_slice()
}
fn unit_items(&self, _unit: &TranslationUnit, _items: &mut Vec<CompletionItem>) {}
fn static_items(&self, context: &Option<CompletionContext>, items: &mut Vec<CompletionItem>) {
// $LaTeX$
items.push(CompletionItem {
label: "$LaTeX$".to_string(),
detail: Some("Mathmode LaTeX".into()),
documentation: Some(tower_lsp::lsp_types::Documentation::MarkupContent(
MarkupContent {
kind: tower_lsp::lsp_types::MarkupKind::Markdown,
value: "# Usage
`$LaTeX$` *render LaTeX content in mathmode, displays inline*
`$[kind=block]LaTeX$` *render LaTeX content in mathmode, displays as a block*
# Examples
* `$1+1=2$` *display **1+1=2** as an inline element*
* `$[kind=block]\\LaTeX$` *display **LaTeX** as a block element*
* `$[env=custom]\\sum_{k=0}^n \\frac{1}{k^2}$` *render using LaTeX environment **custom***
# Properties
* `env` LaTeX environment to use (defaults to **main**)
* `kind` Display kind of the LaTeX element (defaults to **inline**)
- **inline** Element displays inline
- **block** Element displays as a block
* `caption` Alternate text display for the LaTeX element (defaults to none)
# See also
* `$|LaTeX|$` *normal mode LaTeX*
"
.into(),
},
)),
kind: Some(CompletionItemKind::SNIPPET),
insert_text_format: Some(InsertTextFormat::SNIPPET),
insert_text: Some(format!(
"{}${{1:LATEX}}$",
if context_triggered(context, "$") {
""
} else {
"$"
}
)),
..CompletionItem::default()
});
// $|LaTeX|$
items.push(CompletionItem {
label: "$|LaTeX|$".to_string(),
detail: Some("Normal LaTeX".into()),
documentation: Some(tower_lsp::lsp_types::Documentation::MarkupContent(
MarkupContent {
kind: tower_lsp::lsp_types::MarkupKind::Markdown,
value: "# Usage
`$|LaTeX|$` *render LaTeX content, displays as a block*
`$|[kind=inline]|LaTeX|$` *render LaTeX content, displays inline*
# Examples
* `$|\\textit{italic}|$` *display **italic** as a block*
* `$|[kind=inline]\\LaTeX|$` *display **LaTeX** inline*
* `$|[env=custom]\\textbb{Bold}|$` *render using LaTeX environment **custom***
# Properties
* `env` LaTeX environment to use (defaults to **main**)
* `kind` Display kind of the LaTeX element (defaults to **block**)
- **inline** Element displays inline
- **block** Element displays as a block
* `caption` Alternate text display for the LaTeX element (defaults to none)
# See also
* `$LaTeX$` *mathmode LaTeX*
"
.into(),
},
)),
kind: Some(CompletionItemKind::SNIPPET),
insert_text_format: Some(InsertTextFormat::SNIPPET),
insert_text: Some(format!(
"{}|${{1:LATEX}}|$",
if context_triggered(context, "$") {
""
} else {
"$"
}
)),
..CompletionItem::default()
});
}
}

View file

@ -1,271 +0,0 @@
use std::fmt::write;
use std::fmt::Display;
use std::io::Read;
use std::io::Write;
use std::process::Command;
use std::process::Stdio;
use std::str::FromStr;
use std::sync::Arc;
use std::sync::Once;
use ariadne::Span;
use crypto::digest::Digest;
use crypto::sha2::Sha512;
use parking_lot::RwLock;
use crate::parser::reports::macros::*;
use crate::parser::reports::*;
use crate::cache::cache::Cached;
use crate::cache::cache::CachedError;
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target::HTML;
use crate::compiler::output::CompilerOutput;
use crate::parser::reports::Report;
use crate::parser::source::Token;
use crate::unit::element::ContainerElement;
use crate::unit::element::ElemKind;
use crate::unit::element::Element;
use crate::unit::element::LinkableElement;
use crate::unit::element::ReferenceableElement;
use crate::unit::scope::Scope;
use crate::unit::scope::ScopeAccessor;
use crate::unit::variable::VariableName;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TexKind {
Block,
Inline,
}
impl From<TexKind> for ElemKind {
fn from(value: TexKind) -> Self {
match value {
TexKind::Block => ElemKind::Block,
TexKind::Inline => ElemKind::Inline,
}
}
}
impl FromStr for TexKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"inline" => Ok(TexKind::Inline),
"block" => Ok(TexKind::Block),
_ => Err(format!("Unknown kind: {s}")),
}
}
}
impl Display for TexKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TexKind::Block => write!(f, "Block"),
TexKind::Inline => write!(f, "Inline"),
}
}
}
#[derive(Debug)]
pub struct Latex {
pub(crate) location: Token,
pub(crate) mathmode: bool,
pub(crate) kind: TexKind,
pub(crate) env: String,
pub(crate) tex: String,
pub(crate) caption: Option<String>,
}
struct FormattedTex(String);
impl Cached for FormattedTex {
type Key = String;
type Value = String;
fn sql_table() -> &'static str {
"CREATE TABLE IF NOT EXISTS cached_latex (
digest TEXT PRIMARY KEY,
svg BLOB NOT NULL);"
}
fn sql_get_query() -> &'static str {
"SELECT svg FROM cached_latex WHERE digest = (?1)"
}
fn sql_insert_query() -> &'static str {
"INSERT INTO cached_latex (digest, svg) VALUES (?1, ?2)"
}
fn key(&self) -> <Self as Cached>::Key {
let mut hasher = Sha512::new();
hasher.input(self.0.as_bytes());
hasher.result_str()
}
}
fn format_latex(fontsize: &str, preamble: &str, tex: &str) -> FormattedTex {
FormattedTex(format!(
r"\documentclass[preview]{{standalone}}
{preamble}
\begin{{document}}
\begin{{preview}}
{tex}
\end{{preview}}
\end{{document}}"
))
}
fn latex_to_svg(tex: &FormattedTex, exec: &String, fontsize: &String) -> Result<String, String> {
let process = match Command::new(exec)
.arg("--fontsize")
.arg(fontsize)
.stdout(Stdio::piped())
.stdin(Stdio::piped())
.spawn()
{
Err(e) => return Err(format!("Could not spawn `{exec}`: {}", e)),
Ok(process) => process,
};
if let Err(e) = process.stdin.unwrap().write_all(tex.0.as_bytes()) {
panic!("Unable to write to `latex2svg`'s stdin: {}", e);
}
let mut result = String::new();
if let Err(e) = process.stdout.unwrap().read_to_string(&mut result) {
panic!("Unable to read `latex2svg` stdout: {}", e)
}
Ok(result)
}
impl Element for Latex {
fn location(&self) -> &Token {
&self.location
}
fn kind(&self) -> ElemKind {
self.kind.into()
}
fn element_name(&self) -> &'static str {
"Latex"
}
fn compile<'e>(
&'e self,
scope: Arc<RwLock<Scope>>,
compiler: &'e Compiler,
output: &mut CompilerOutput,
) -> Result<(), Vec<Report>> {
match compiler.target() {
HTML => {
static CACHE_INIT: Once = Once::new();
CACHE_INIT.call_once(|| {
let cache = compiler.get_cache();
let con = tokio::runtime::Runtime::new()
.unwrap()
.block_on(cache.get_connection());
if let Err(e) = FormattedTex::init(&con) {
eprintln!("Unable to create cache table: {e}");
}
});
let exec = scope
.get_variable(&VariableName(format!("latex.{}.exec", self.env)))
.map_or("latex2svg".to_string(), |(var, _)| var.to_string());
let fontsize = scope
.get_variable(&VariableName(format!("latex.{}.fontsize", self.env)))
.map_or("12".to_string(), |(var, _)| var.to_string());
let preamble = scope
.get_variable(&VariableName(format!("latex.{}.preamble", self.env)))
.map_or("".to_string(), |(var, _)| var.to_string());
let prepend = if self.mathmode {
"".to_string()
} else {
scope
.get_variable(&VariableName(format!("tex.{}.block_prepend", self.env)))
.map_or("".to_string(), |(var, _)| var.to_string() + "\n")
};
let latex = if self.mathmode {
format_latex(&fontsize, &preamble, &format!("${{{}}}$", self.tex))
} else {
format_latex(&fontsize, &preamble, &format!("{prepend}{}", self.tex))
};
let sanitizer = compiler.sanitizer();
let location = self.location().clone();
let caption = self.caption.clone();
let cache = compiler.get_cache();
let fut = async move {
let con = cache.get_connection().await;
let mut result = match latex.cached(&con, |s| latex_to_svg(s, &exec, &fontsize))
{
Ok(s) => s,
Err(CachedError::SqlErr(e)) => {
return Err(compile_err!(
location,
"Failed to process LaTeX element".to_string(),
format!("Querying the cache failed: {e}")
))
}
Err(CachedError::GenErr(e)) => {
return Err(compile_err!(
location,
"Failed to process LaTeX element".to_string(),
e
))
}
};
// Caption
if let (Some(caption), Some(start)) = (&caption, result.find('>')) {
result.insert_str(
start + 1,
format!("<title>{}</title>", sanitizer.sanitize(caption)).as_str(),
);
}
Ok(result)
};
output.add_task(self.location.clone(), "Latex".into(), Box::pin(fut));
}
_ => todo!(),
}
Ok(())
}
fn provide_hover(&self) -> Option<String> {
Some(format!(
"LaTeX
# Properties
* **Location**: [{}] ({}..{})
* **Kind**: {}
* **Mathmode**: {}
* **Environment**: {}
* **Caption**: {}",
self.location.source().name(),
self.location().range.start(),
self.location().range.end(),
self.kind,
self.mathmode,
self.env,
self.caption.as_ref().unwrap_or(&"*<none>*".to_string())
))
}
fn as_referenceable(self: Arc<Self>) -> Option<Arc<dyn ReferenceableElement>> {
None
}
fn as_linkable(self: Arc<Self>) -> Option<Arc<dyn LinkableElement>> {
None
}
fn as_container(self: Arc<Self>) -> Option<Arc<dyn ContainerElement>> {
None
}
}

View file

@ -1,3 +0,0 @@
pub mod completion;
pub mod elem;
pub mod rule;

View file

@ -1,249 +0,0 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use ariadne::Fmt;
use regex::Captures;
use regex::Regex;
use crate::parser::property::Property;
use crate::parser::property::PropertyParser;
use crate::parser::rule::RegexRule;
use crate::parser::rule::RuleTarget;
use crate::parser::source::Token;
use crate::parser::state::CustomStates;
use crate::parser::state::ParseMode;
use crate::parser::util::escape_source;
use crate::parser::util::escape_text;
use crate::report_err;
use crate::unit::translation::TranslationAccessors;
use crate::unit::translation::TranslationUnit;
use crate::parser::reports::macros::*;
use crate::parser::reports::*;
use super::completion::LatexCompletion;
use super::elem::Latex;
use super::elem::TexKind;
#[auto_registry::auto_registry(registry = "rules")]
pub struct LatexRule {
re: [Regex; 2],
properties: PropertyParser,
}
impl Default for LatexRule {
fn default() -> Self {
let mut props = HashMap::new();
props.insert(
"env".to_string(),
Property::new("Tex environment".to_string(), Some("main".to_string())),
);
props.insert(
"kind".to_string(),
Property::new("Element display kind".to_string(), None),
);
props.insert(
"caption".to_string(),
Property::new("Latex caption".to_string(), None),
);
Self {
re: [
Regex::new(r"\$\|(?:\[((?:\\.|[^\\\\])*?)\])?(?:((?:\\.|[^\\\\])*?)\|\$)?")
.unwrap(),
Regex::new(r"\$(?:\[((?:\\.|[^\\\\])*?)\])?(?:((?:\\.|[^\\\\])*?)\$)?").unwrap(),
],
properties: PropertyParser { properties: props },
}
}
}
impl RegexRule for LatexRule {
fn name(&self) -> &'static str {
"Latex"
}
fn target(&self) -> RuleTarget {
RuleTarget::Inline
}
fn regexes(&self) -> &[regex::Regex] {
&self.re
}
fn enabled(
&self,
_unit: &TranslationUnit,
_mode: &ParseMode,
_states: &mut CustomStates,
_index: usize,
) -> bool {
true
}
fn on_regex_match<'u>(
&self,
index: usize,
unit: &mut TranslationUnit,
token: Token,
captures: Captures,
) {
let tex_content = match captures.get(2) {
// Unterminated `$`
None => {
report_err!(
unit,
token.source(),
"Unterminated Tex Code".into(),
span(
token.range.clone(),
format!(
"Missing terminating `{}` after first `{}`",
["|$", "$"][index].fg(unit.colors().info),
["$|", "$"][index].fg(unit.colors().info)
)
)
);
return;
}
Some(content) => {
let processed = escape_text(
'\\',
["|$", "$"][index],
content.as_str().trim_start().trim_end(),
true,
);
if processed.is_empty() {
report_err!(
unit,
token.source(),
"Empty Tex Code".into(),
span(content.range(), "Tex code is empty".into())
);
}
processed
}
};
// Properties
let prop_source = escape_source(
token.source(),
captures.get(1).map_or(0..0, |m| m.range()),
"Tex Properties".into(),
'\\',
"]",
);
let Some(mut properties) = self.properties.parse(
"Raw Code",
unit,
Token::new(0..prop_source.content().len(), prop_source),
) else {
return;
};
let (Some(tex_kind), Some(tex_caption), Some(tex_env)) = (
properties.get_or(
unit,
"kind",
if index == 1 {
TexKind::Inline
} else {
TexKind::Block
},
|_, value| TexKind::from_str(value.value.as_str()),
),
properties.get_opt(unit, "caption", |_, value| {
Result::<_, String>::Ok(value.value.clone())
}),
properties.get(unit, "env", |_, value| {
Result::<_, String>::Ok(value.value.clone())
}),
) else {
return;
};
// Code ranges
/*
if let Some(coderanges) = CodeRange::from_source(token.source(), &state.shared.lsp) {
if index == 0 && tex_content.contains('\n') {
let range = captures
.get(2)
.map(|m| {
if token.source().content().as_bytes()[m.start()] == b'\n' {
m.start() + 1..m.end()
} else {
m.range()
}
})
.unwrap();
coderanges.add(range, "Latex".into());
}
}
state.push(
document,
Box::new(Tex {
mathmode: index == 1,
location: token.clone(),
kind: tex_kind,
env: tex_env,
tex: tex_content,
caption,
}),
);
// Semantics
if let Some((sems, tokens)) = Semantics::from_source(token.source(), &state.shared.lsp) {
let range = token.range;
sems.add(
range.start..range.start + if index == 0 { 2 } else { 1 },
tokens.tex_sep,
);
if let Some(props) = captures.get(1).map(|m| m.range()) {
sems.add(props.start - 1..props.start, tokens.tex_props_sep);
sems.add(props.end..props.end + 1, tokens.tex_props_sep);
}
sems.add(captures.get(2).unwrap().range(), tokens.tex_content);
sems.add(
range.end - if index == 0 { 2 } else { 1 }..range.end,
tokens.tex_sep,
);
}
*/
unit.with_lsp(|lsp| {
lsp.with_semantics(token.source(), |sems, tokens| {
let range = &token.range;
sems.add(
range.start..range.start + if index == 0 { 2 } else { 1 },
tokens.tex_sep,
);
if let Some(props) = captures.get(1).map(|m| m.range()) {
sems.add(props.start - 1..props.start, tokens.tex_prop_sep);
sems.add(props.end..props.end + 1, tokens.tex_prop_sep);
}
sems.add(captures.get(2).unwrap().range(), tokens.tex_content);
sems.add(
range.end - if index == 0 { 2 } else { 1 }..range.end,
tokens.tex_sep,
);
})
});
unit.add_content(Arc::new(Latex {
location: token,
mathmode: index == 1,
kind: tex_kind,
env: tex_env,
tex: tex_content,
caption: tex_caption,
}));
}
fn completion(
&self,
) -> Option<Box<dyn lsp::completion::CompletionProvider + 'static + Send + Sync>> {
Some(Box::new(LatexCompletion {}))
}
}

978
src/elements/layout.rs Normal file
View file

@ -0,0 +1,978 @@
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target;
use crate::document::document::Document;
use crate::document::element::ElemKind;
use crate::document::element::Element;
use crate::lua::kernel::CTX;
use crate::parser::layout::LayoutHolder;
use crate::parser::layout::LayoutType;
use crate::parser::parser::ParserState;
use crate::parser::parser::ReportColors;
use crate::parser::rule::RegexRule;
use crate::parser::source::Source;
use crate::parser::source::Token;
use crate::parser::state::RuleState;
use crate::parser::state::Scope;
use crate::parser::util::process_escaped;
use ariadne::Fmt;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use mlua::Error::BadArgument;
use mlua::Function;
use mlua::Lua;
use regex::Captures;
use regex::Match;
use regex::Regex;
use regex::RegexBuilder;
use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::ops::Range;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LayoutToken {
Begin,
Next,
End,
}
impl FromStr for LayoutToken {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Begin" | "begin" => Ok(LayoutToken::Begin),
"Next" | "next" => Ok(LayoutToken::Next),
"End" | "end" => Ok(LayoutToken::End),
_ => Err(format!("Unable to find LayoutToken with name: {s}")),
}
}
}
mod default_layouts {
use crate::parser::layout::LayoutType;
use crate::parser::util::Property;
use crate::parser::util::PropertyParser;
use super::*;
#[derive(Debug)]
pub struct Centered(PropertyParser);
impl Default for Centered {
fn default() -> Self {
let mut properties = HashMap::new();
properties.insert(
"style".to_string(),
Property::new(
true,
"Additional style for the split".to_string(),
Some("".to_string()),
),
);
Self(PropertyParser { properties })
}
}
impl LayoutType for Centered {
fn name(&self) -> &'static str { "Centered" }
fn expects(&self) -> Range<usize> { 1..1 }
fn parse_properties(&self, properties: &str) -> Result<Option<Box<dyn Any>>, String> {
let props = if properties.is_empty() {
self.0.default()
} else {
self.0.parse(properties)
}
.map_err(|err| {
format!(
"Failed to parse properties for layout {}: {err}",
self.name()
)
})?;
let style = props
.get("style", |_, value| -> Result<String, ()> {
Ok(value.clone())
})
.map_err(|err| format!("Failed to parse style: {err:#?}"))
.map(|(_, value)| value)?;
Ok(Some(Box::new(style)))
}
fn compile(
&self,
token: LayoutToken,
_id: usize,
properties: &Option<Box<dyn Any>>,
compiler: &Compiler,
_document: &dyn Document,
) -> Result<String, String> {
match compiler.target() {
Target::HTML => {
let style = match properties
.as_ref()
.unwrap()
.downcast_ref::<String>()
.unwrap()
.as_str()
{
"" => "".to_string(),
str => format!(r#" style={}"#, Compiler::sanitize(compiler.target(), str)),
};
match token {
LayoutToken::Begin => Ok(format!(r#"<div class="centered"{style}>"#)),
LayoutToken::Next => panic!(),
LayoutToken::End => Ok(r#"</div>"#.to_string()),
}
}
_ => todo!(""),
}
}
}
#[derive(Debug)]
pub struct Split(PropertyParser);
impl Default for Split {
fn default() -> Self {
let mut properties = HashMap::new();
properties.insert(
"style".to_string(),
Property::new(
true,
"Additional style for the split".to_string(),
Some("".to_string()),
),
);
Self(PropertyParser { properties })
}
}
impl LayoutType for Split {
fn name(&self) -> &'static str { "Split" }
fn expects(&self) -> Range<usize> { 2..usize::MAX }
fn parse_properties(&self, properties: &str) -> Result<Option<Box<dyn Any>>, String> {
let props = if properties.is_empty() {
self.0.default()
} else {
self.0.parse(properties)
}
.map_err(|err| {
format!(
"Failed to parse properties for layout {}: {err}",
self.name()
)
})?;
let style = props
.get("style", |_, value| -> Result<String, ()> {
Ok(value.clone())
})
.map_err(|err| format!("Failed to parse style: {err:#?}"))
.map(|(_, value)| value)?;
Ok(Some(Box::new(style)))
}
fn compile(
&self,
token: LayoutToken,
_id: usize,
properties: &Option<Box<dyn Any>>,
compiler: &Compiler,
_document: &dyn Document,
) -> Result<String, String> {
match compiler.target() {
Target::HTML => {
let style = match properties
.as_ref()
.unwrap()
.downcast_ref::<String>()
.unwrap()
.as_str()
{
"" => "".to_string(),
str => format!(r#" style={}"#, Compiler::sanitize(compiler.target(), str)),
};
match token {
LayoutToken::Begin => Ok(format!(
r#"<div class="split-container"><div class="split"{style}>"#
)),
LayoutToken::Next => Ok(format!(r#"</div><div class="split"{style}>"#)),
LayoutToken::End => Ok(r#"</div></div>"#.to_string()),
}
}
_ => todo!(""),
}
}
}
}
#[derive(Debug)]
struct Layout {
pub(self) location: Token,
pub(self) layout: Rc<dyn LayoutType>,
pub(self) id: usize,
pub(self) token: LayoutToken,
pub(self) properties: Option<Box<dyn Any>>,
}
impl Element for Layout {
fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Block }
fn element_name(&self) -> &'static str { "Layout" }
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> {
self.layout
.compile(self.token, self.id, &self.properties, compiler, document)
}
}
struct LayoutState {
/// The layout stack
pub(self) stack: Vec<(Vec<Token>, Rc<dyn LayoutType>)>,
}
impl RuleState for LayoutState {
fn scope(&self) -> Scope { Scope::DOCUMENT }
fn on_remove<'a>(
&self,
state: &ParserState,
document: &dyn Document,
) -> Vec<Report<'a, (Rc<dyn Source>, Range<usize>)>> {
let mut reports = vec![];
let doc_borrow = document.content().borrow();
let at = doc_borrow.last().unwrap().location();
for (tokens, layout_type) in &self.stack {
let start = tokens.first().unwrap();
reports.push(
Report::build(ReportKind::Error, start.source(), start.start())
.with_message("Unterminated Layout")
.with_label(
Label::new((start.source(), start.range.start + 1..start.range.end))
.with_order(1)
.with_message(format!(
"Layout {} stars here",
layout_type.name().fg(state.parser.colors().info)
))
.with_color(state.parser.colors().error),
)
.with_label(
Label::new((at.source(), at.range.clone()))
.with_order(2)
.with_message("Document ends here".to_string())
.with_color(state.parser.colors().error),
)
.finish(),
);
}
return reports;
}
}
pub struct LayoutRule {
re: [Regex; 3],
}
impl LayoutRule {
pub fn new() -> Self {
Self {
re: [
RegexBuilder::new(
r"(?:^|\n)(?:[^\S\n]*)#\+LAYOUT_BEGIN(?:\[((?:\\.|[^\\\\])*?)\])?(.*)",
)
.multi_line(true)
.build()
.unwrap(),
RegexBuilder::new(
r"(?:^|\n)(?:[^\S\n]*)#\+LAYOUT_NEXT(?:\[((?:\\.|[^\\\\])*?)\])?$",
)
.multi_line(true)
.build()
.unwrap(),
RegexBuilder::new(
r"(?:^|\n)(?:[^\S\n]*)#\+LAYOUT_END(?:\[((?:\\.|[^\\\\])*?)\])?$",
)
.multi_line(true)
.build()
.unwrap(),
],
}
}
pub fn initialize_state(state: &ParserState) -> Rc<RefCell<dyn RuleState>> {
let mut rule_state_borrow = state.shared.rule_state.borrow_mut();
match rule_state_borrow.get(STATE_NAME) {
Some(state) => state,
None => {
// Insert as a new state
match rule_state_borrow.insert(
STATE_NAME.into(),
Rc::new(RefCell::new(LayoutState { stack: vec![] })),
) {
Err(err) => panic!("{err}"),
Ok(state) => state,
}
}
}
}
pub fn parse_properties<'a>(
colors: &ReportColors,
token: &Token,
layout_type: Rc<dyn LayoutType>,
properties: Option<Match>,
) -> Result<Option<Box<dyn Any>>, Report<'a, (Rc<dyn Source>, Range<usize>)>> {
match properties {
None => match layout_type.parse_properties("") {
Ok(props) => Ok(props),
Err(err) => Err(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Unable to parse layout properties")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_message(err)
.with_color(colors.error),
)
.finish(),
),
},
Some(props) => {
let trimmed = props.as_str().trim_start().trim_end();
let content = process_escaped('\\', "]", trimmed);
match layout_type.parse_properties(content.as_str()) {
Ok(props) => Ok(props),
Err(err) => {
Err(
Report::build(ReportKind::Error, token.source(), props.start())
.with_message("Unable to parse layout properties")
.with_label(
Label::new((token.source(), props.range()))
.with_message(err)
.with_color(colors.error),
)
.finish(),
)
}
}
}
}
}
}
static STATE_NAME: &'static str = "elements.layout";
impl RegexRule for LayoutRule {
fn name(&self) -> &'static str { "Layout" }
fn regexes(&self) -> &[regex::Regex] { &self.re }
fn on_regex_match(
&self,
index: usize,
state: &ParserState,
document: &dyn Document,
token: Token,
matches: Captures,
) -> Vec<Report<(Rc<dyn Source>, Range<usize>)>> {
let mut reports = vec![];
let rule_state = LayoutRule::initialize_state(state);
if index == 0
// BEGIN_LAYOUT
{
match matches.get(2) {
None => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Missing Layout Name")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_message(format!(
"Missing layout name after `{}`",
"#+BEGIN_LAYOUT".fg(state.parser.colors().highlight)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
Some(name) => {
let trimmed = name.as_str().trim_start().trim_end();
if name.as_str().is_empty() || trimmed.is_empty()
// Empty name
{
reports.push(
Report::build(ReportKind::Error, token.source(), name.start())
.with_message("Empty Layout Name")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_message(format!(
"Empty layout name after `{}`",
"#+BEGIN_LAYOUT".fg(state.parser.colors().highlight)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
} else if !name.as_str().chars().next().unwrap().is_whitespace()
// Missing space
{
reports.push(
Report::build(ReportKind::Error, token.source(), name.start())
.with_message("Invalid Layout Name")
.with_label(
Label::new((token.source(), name.range()))
.with_message(format!(
"Missing a space before layout name `{}`",
name.as_str().fg(state.parser.colors().highlight)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
// Get layout
let layout_type = match state.shared.layouts.borrow().get(trimmed) {
None => {
reports.push(
Report::build(ReportKind::Error, token.source(), name.start())
.with_message("Unknown Layout")
.with_label(
Label::new((token.source(), name.range()))
.with_message(format!(
"Cannot find layout `{}`",
trimmed.fg(state.parser.colors().highlight)
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
Some(layout_type) => layout_type,
};
// Parse properties
let properties = match LayoutRule::parse_properties(
state.parser.colors(),
&token,
layout_type.clone(),
matches.get(1),
) {
Ok(props) => props,
Err(rep) => {
reports.push(rep);
return reports;
}
};
state.push(
document,
Box::new(Layout {
location: token.clone(),
layout: layout_type.clone(),
id: 0,
token: LayoutToken::Begin,
properties,
}),
);
rule_state
.as_ref()
.borrow_mut()
.downcast_mut::<LayoutState>()
.map_or_else(
|| panic!("Invalid state at: `{STATE_NAME}`"),
|s| s.stack.push((vec![token.clone()], layout_type.clone())),
);
}
};
return reports;
}
let (id, token_type, layout_type, properties) = if index == 1
// LAYOUT_NEXT
{
let mut rule_state_borrow = rule_state.as_ref().borrow_mut();
let layout_state = rule_state_borrow.downcast_mut::<LayoutState>().unwrap();
let (tokens, layout_type) = match layout_state.stack.last_mut() {
None => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid #+LAYOUT_NEXT")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_message("No active layout found".to_string())
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
Some(last) => last,
};
if layout_type.expects().end < tokens.len()
// Too many blocks
{
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Unexpected #+LAYOUT_NEXT")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_message(format!(
"Layout expects a maximum of {} blocks, currently at {}",
layout_type.expects().end.fg(state.parser.colors().info),
tokens.len().fg(state.parser.colors().info),
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
// Parse properties
let properties = match LayoutRule::parse_properties(
state.parser.colors(),
&token,
layout_type.clone(),
matches.get(1),
) {
Ok(props) => props,
Err(rep) => {
reports.push(rep);
return reports;
}
};
tokens.push(token.clone());
(
tokens.len() - 1,
LayoutToken::Next,
layout_type.clone(),
properties,
)
} else {
// LAYOUT_END
let mut rule_state_borrow = rule_state.as_ref().borrow_mut();
let layout_state = rule_state_borrow.downcast_mut::<LayoutState>().unwrap();
let (tokens, layout_type) = match layout_state.stack.last_mut() {
None => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid #+LAYOUT_END")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_message("No active layout found".to_string())
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
Some(last) => last,
};
if layout_type.expects().start > tokens.len()
// Not enough blocks
{
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Unexpected #+LAYOUT_END")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_message(format!(
"Layout expects a minimum of {} blocks, currently at {}",
layout_type.expects().start.fg(state.parser.colors().info),
tokens.len().fg(state.parser.colors().info),
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
// Parse properties
let properties = match LayoutRule::parse_properties(
state.parser.colors(),
&token,
layout_type.clone(),
matches.get(1),
) {
Ok(props) => props,
Err(rep) => {
reports.push(rep);
return reports;
}
};
let layout_type = layout_type.clone();
let id = tokens.len();
layout_state.stack.pop();
(id, LayoutToken::End, layout_type, properties)
};
state.push(
document,
Box::new(Layout {
location: token,
layout: layout_type,
id,
token: token_type,
properties,
}),
);
return reports;
}
// TODO: Add method to create new layouts
fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> {
let mut bindings = vec![];
bindings.push((
"push".to_string(),
lua.create_function(
|_, (token, layout, properties): (String, String, String)| {
let mut result = Ok(());
// Parse token
let layout_token = match LayoutToken::from_str(token.as_str())
{
Err(err) => {
return Err(BadArgument {
to: Some("push".to_string()),
pos: 1,
name: Some("token".to_string()),
cause: Arc::new(mlua::Error::external(err))
});
},
Ok(token) => token,
};
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
// Make sure the rule state has been initialized
let rule_state = LayoutRule::initialize_state(ctx.state);
// Get layout
//
let layout_type = match ctx.state.shared.layouts.borrow().get(layout.as_str())
{
None => {
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 2,
name: Some("layout".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Cannot find layout with name `{layout}`"
))),
});
return;
},
Some(layout) => layout,
};
// Parse properties
let layout_properties = match layout_type.parse_properties(properties.as_str()) {
Err(err) => {
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 3,
name: Some("properties".to_string()),
cause: Arc::new(mlua::Error::external(err)),
});
return;
},
Ok(properties) => properties,
};
let id = match layout_token {
LayoutToken::Begin => {
ctx.state.push(
ctx.document,
Box::new(Layout {
location: ctx.location.clone(),
layout: layout_type.clone(),
id: 0,
token: LayoutToken::Begin,
properties: layout_properties,
}),
);
rule_state
.as_ref()
.borrow_mut()
.downcast_mut::<LayoutState>()
.map_or_else(
|| panic!("Invalid state at: `{STATE_NAME}`"),
|s| s.stack.push((vec![ctx.location.clone()], layout_type.clone())),
);
return;
},
LayoutToken::Next => {
let mut state_borrow = rule_state.as_ref().borrow_mut();
let layout_state = state_borrow.downcast_mut::<LayoutState>().unwrap();
let (tokens, current_layout_type) = match layout_state.stack.last_mut() {
None => {
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 1,
name: Some("token".to_string()),
cause: Arc::new(mlua::Error::external(format!("Unable set next layout: No active layout found"))),
});
return;
}
Some(last) => last,
};
if !Rc::ptr_eq(&layout_type, current_layout_type) {
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 2,
name: Some("layout".to_string()),
cause: Arc::new(mlua::Error::external(format!("Invalid layout next, current layout is {} vs {}",
current_layout_type.name(),
layout_type.name())))
});
return;
}
if layout_type.expects().end < tokens.len()
// Too many blocks
{
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 1,
name: Some("token".to_string()),
cause: Arc::new(mlua::Error::external(format!("Unable set layout next: layout {} expect at most {} blocks, currently at {} blocks",
layout_type.name(),
layout_type.expects().end,
tokens.len()
))),
});
return;
}
tokens.push(ctx.location.clone());
tokens.len() - 1
},
LayoutToken::End => {
let mut state_borrow = rule_state.as_ref().borrow_mut();
let layout_state = state_borrow.downcast_mut::<LayoutState>().unwrap();
let (tokens, current_layout_type) = match layout_state.stack.last_mut() {
None => {
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 1,
name: Some("token".to_string()),
cause: Arc::new(mlua::Error::external(format!("Unable set layout end: No active layout found"))),
});
return;
}
Some(last) => last,
};
if !Rc::ptr_eq(&layout_type, current_layout_type) {
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 2,
name: Some("layout".to_string()),
cause: Arc::new(mlua::Error::external(format!("Invalid layout end, current layout is {} vs {}",
current_layout_type.name(),
layout_type.name())))
});
return;
}
if layout_type.expects().start > tokens.len()
// Not enough blocks
{
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 1,
name: Some("token".to_string()),
cause: Arc::new(mlua::Error::external(format!("Unable set next layout: layout {} expect at least {} blocks, currently at {} blocks",
layout_type.name(),
layout_type.expects().start,
tokens.len()
))),
});
return;
}
let id = tokens.len();
layout_state.stack.pop();
id
}
};
ctx.state.push(
ctx.document,
Box::new(Layout {
location: ctx.location.clone(),
layout: layout_type.clone(),
id,
token: layout_token,
properties: layout_properties,
}),
);
})
});
result
},
)
.unwrap(),
));
bindings
}
fn register_layouts(&self, holder: &mut LayoutHolder) {
holder.insert(Rc::new(default_layouts::Centered::default()));
holder.insert(Rc::new(default_layouts::Split::default()));
}
}
#[cfg(test)]
mod tests {
use crate::elements::paragraph::Paragraph;
use crate::elements::text::Text;
use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser;
use crate::parser::source::SourceFile;
use crate::validate_document;
use super::*;
#[test]
fn parser() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
#+LAYOUT_BEGIN[style=A] Split
A
#+LAYOUT_BEGIN[style=B] Centered
B
#+LAYOUT_END
#+LAYOUT_NEXT[style=C]
C
#+LAYOUT_BEGIN[style=D] Split
D
#+LAYOUT_NEXT[style=E]
E
#+LAYOUT_END
#+LAYOUT_END
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Layout { token == LayoutToken::Begin, id == 0 };
Paragraph {
Text { content == "A" };
};
Layout { token == LayoutToken::Begin, id == 0 };
Paragraph {
Text { content == "B" };
};
Layout { token == LayoutToken::End, id == 1 };
Layout { token == LayoutToken::Next, id == 1 };
Paragraph {
Text { content == "C" };
};
Layout { token == LayoutToken::Begin, id == 0 };
Paragraph {
Text { content == "D" };
};
Layout { token == LayoutToken::Next, id == 1 };
Paragraph {
Text { content == "E" };
};
Layout { token == LayoutToken::End, id == 2 };
Layout { token == LayoutToken::End, id == 2 };
);
}
#[test]
fn lua() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
%<nml.layout.push("begin", "Split", "style=A")>%
A
%<nml.layout.push("Begin", "Centered", "style=B")>%
B
%<nml.layout.push("end", "Centered", "")>%
%<nml.layout.push("next", "Split", "style=C")>%
C
%<nml.layout.push("Begin", "Split", "style=D")>%
D
%<nml.layout.push("Next", "Split", "style=E")>%
E
%<nml.layout.push("End", "Split", "")>%
%<nml.layout.push("End", "Split", "")>%
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Layout { token == LayoutToken::Begin, id == 0 };
Paragraph {
Text { content == "A" };
};
Layout { token == LayoutToken::Begin, id == 0 };
Paragraph {
Text { content == "B" };
};
Layout { token == LayoutToken::End, id == 1 };
Layout { token == LayoutToken::Next, id == 1 };
Paragraph {
Text { content == "C" };
};
Layout { token == LayoutToken::Begin, id == 0 };
Paragraph {
Text { content == "D" };
};
Layout { token == LayoutToken::Next, id == 1 };
Paragraph {
Text { content == "E" };
};
Layout { token == LayoutToken::End, id == 2 };
Layout { token == LayoutToken::End, id == 2 };
);
}
}

View file

@ -1,63 +0,0 @@
use std::sync::Arc;
use parking_lot::RwLock;
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target::HTML;
use crate::compiler::output::CompilerOutput;
use crate::parser::reports::Report;
use crate::parser::source::Token;
use crate::unit::element::ContainerElement;
use crate::unit::element::ElemKind;
use crate::unit::element::Element;
use crate::unit::element::LinkableElement;
use crate::unit::element::ReferenceableElement;
use crate::unit::scope::Scope;
#[derive(Debug)]
pub struct LineBreak {
pub(crate) location: Token,
pub(crate) length: usize,
}
impl Element for LineBreak {
fn location(&self) -> &Token {
&self.location
}
fn kind(&self) -> ElemKind {
ElemKind::Invisible
}
fn element_name(&self) -> &'static str {
"Break"
}
fn compile<'e>(
&'e self,
scope: Arc<RwLock<Scope>>,
compiler: &'e Compiler,
output: &mut CompilerOutput,
) -> Result<(), Vec<Report>> {
match compiler.target() {
HTML => {
if output.in_paragraph(&scope) {
output.add_content("</p>");
output.set_paragraph(&scope, false);
}
}
_ => todo!("Unimplemented compiler"),
}
Ok(())
}
fn as_referenceable(self: Arc<Self>) -> Option<Arc<dyn ReferenceableElement>> {
None
}
fn as_linkable(self: Arc<Self>) -> Option<Arc<dyn LinkableElement>> {
None
}
fn as_container(self: Arc<Self>) -> Option<Arc<dyn ContainerElement>> {
None
}
}

View file

@ -1,2 +0,0 @@
pub mod elem;
pub mod rule;

View file

@ -1,71 +0,0 @@
use std::sync::Arc;
use regex::Captures;
use regex::Regex;
use crate::parser::rule::RegexRule;
use crate::parser::rule::RuleTarget;
use crate::parser::source::Token;
use crate::parser::state::CustomStates;
use crate::parser::state::ParseMode;
use crate::unit::translation::TranslationAccessors;
use crate::unit::translation::TranslationUnit;
use super::elem::LineBreak;
#[auto_registry::auto_registry(registry = "rules")]
pub struct BreakRule {
re: [Regex; 1],
}
impl Default for BreakRule {
fn default() -> Self {
Self {
re: [Regex::new(r"(\n[^\S\r\n]*)+$").unwrap()],
}
}
}
impl RegexRule for BreakRule {
fn name(&self) -> &'static str {
"Break"
}
fn target(&self) -> RuleTarget {
RuleTarget::Meta
}
fn regexes(&self) -> &[regex::Regex] {
&self.re
}
fn enabled(
&self,
_unit: &TranslationUnit,
mode: &ParseMode,
_states: &mut CustomStates,
_index: usize,
) -> bool {
return !mode.paragraph_only;
}
fn on_regex_match<'u>(
&self,
_index: usize,
unit: &mut TranslationUnit,
token: Token,
captures: Captures,
) {
let length = captures
.get(1)
.unwrap()
.as_str()
.chars()
.fold(0usize, |count, c| count + (c == '\n') as usize);
unit.add_content(Arc::new(LineBreak {
location: token.clone(),
length,
}))
}
}

332
src/elements/link.rs Normal file
View file

@ -0,0 +1,332 @@
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target;
use crate::document::document::Document;
use crate::document::element::ContainerElement;
use crate::document::element::ElemKind;
use crate::document::element::Element;
use crate::lua::kernel::CTX;
use crate::parser::parser::ParserState;
use crate::parser::rule::RegexRule;
use crate::parser::source::Source;
use crate::parser::source::Token;
use crate::parser::source::VirtualSource;
use crate::parser::util;
use ariadne::Fmt;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use mlua::Error::BadArgument;
use mlua::Function;
use mlua::Lua;
use regex::Captures;
use regex::Regex;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
#[derive(Debug)]
pub struct Link {
pub location: Token,
/// Display content of link
pub display: Vec<Box<dyn Element>>,
/// Url of link
pub url: String,
}
impl Element for Link {
fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Inline }
fn element_name(&self) -> &'static str { "Link" }
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> {
match compiler.target() {
Target::HTML => {
let mut result = format!(
"<a href=\"{}\">",
Compiler::sanitize(compiler.target(), self.url.as_str())
);
for elem in &self.display {
result += elem.compile(compiler, document)?.as_str();
}
result += "</a>";
Ok(result)
}
_ => todo!(""),
}
}
fn as_container(&self) -> Option<&dyn ContainerElement> { Some(self) }
}
impl ContainerElement for Link {
fn contained(&self) -> &Vec<Box<dyn Element>> { &self.display }
fn push(&mut self, elem: Box<dyn Element>) -> Result<(), String> {
if elem.downcast_ref::<Link>().is_some() {
return Err("Tried to push a link inside of a link".to_string());
}
self.display.push(elem);
Ok(())
}
}
pub struct LinkRule {
re: [Regex; 1],
}
impl LinkRule {
pub fn new() -> Self {
Self {
re: [Regex::new(r"\[((?:\\.|[^\\\\])*?)\]\(((?:\\.|[^\\\\])*?)\)").unwrap()],
}
}
}
impl RegexRule for LinkRule {
fn name(&self) -> &'static str { "Link" }
fn regexes(&self) -> &[Regex] { &self.re }
fn on_regex_match<'a>(
&self,
_: usize,
state: &ParserState,
document: &'a (dyn Document<'a> + 'a),
token: Token,
matches: Captures,
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
let mut reports = vec![];
let link_display = match matches.get(1) {
Some(display) => {
if display.as_str().is_empty() {
reports.push(
Report::build(ReportKind::Error, token.source(), display.start())
.with_message("Empty link name")
.with_label(
Label::new((token.source().clone(), display.range()))
.with_message("Link name is empty")
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
let processed = util::process_escaped('\\', "]", display.as_str());
if processed.is_empty() {
reports.push(
Report::build(ReportKind::Error, token.source(), display.start())
.with_message("Empty link name")
.with_label(
Label::new((token.source(), display.range()))
.with_message(format!(
"Link name is empty. Once processed, `{}` yields `{}`",
display.as_str().fg(state.parser.colors().highlight),
processed.fg(state.parser.colors().highlight),
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
let source = Rc::new(VirtualSource::new(
Token::new(display.range(), token.source()),
"Link Display".to_string(),
processed,
));
match util::parse_paragraph(state, source, document) {
Err(err) => {
reports.push(
Report::build(ReportKind::Error, token.source(), display.start())
.with_message("Failed to parse link display")
.with_label(
Label::new((token.source(), display.range()))
.with_message(err.to_string())
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
Ok(mut paragraph) => std::mem::replace(&mut paragraph.content, vec![]),
}
}
_ => panic!("Empty link name"),
};
let link_url = match matches.get(2) {
Some(url) => {
if url.as_str().is_empty() {
reports.push(
Report::build(ReportKind::Error, token.source(), url.start())
.with_message("Empty link url")
.with_label(
Label::new((token.source(), url.range()))
.with_message("Link url is empty")
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
let text_content = util::process_text(document, url.as_str());
if text_content.as_str().is_empty() {
reports.push(
Report::build(ReportKind::Error, token.source(), url.start())
.with_message("Empty link url")
.with_label(
Label::new((token.source(), url.range()))
.with_message(format!(
"Link url is empty. Once processed, `{}` yields `{}`",
url.as_str().fg(state.parser.colors().highlight),
text_content.as_str().fg(state.parser.colors().highlight),
))
.with_color(state.parser.colors().error),
)
.finish(),
);
return reports;
}
text_content
}
_ => panic!("Empty link url"),
};
state.push(
document,
Box::new(Link {
location: token,
display: link_display,
url: link_url,
}),
);
return reports;
}
fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> {
let mut bindings = vec![];
bindings.push((
"push".to_string(),
lua.create_function(|_, (display, url): (String, String)| {
let mut result = Ok(());
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
let source = Rc::new(VirtualSource::new(
ctx.location.clone(),
"Link Display".to_string(),
display,
));
let display_content =
match util::parse_paragraph(ctx.state, source, ctx.document) {
Err(err) => {
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 1,
name: Some("display".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Failed to parse link display: {err}"
))),
});
return;
}
Ok(mut paragraph) => {
std::mem::replace(&mut paragraph.content, vec![])
}
};
ctx.state.push(
ctx.document,
Box::new(Link {
location: ctx.location.clone(),
display: display_content,
url,
}),
);
})
});
result
})
.unwrap(),
));
bindings
}
}
#[cfg(test)]
mod tests {
use crate::elements::paragraph::Paragraph;
use crate::elements::style::Style;
use crate::elements::text::Text;
use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser;
use crate::parser::source::SourceFile;
use crate::validate_document;
use super::*;
#[test]
fn parser() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
Some [link](url).
[**BOLD link**](another url)
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Paragraph {
Text { content == "Some " };
Link { url == "url" } { Text { content == "link" }; };
Text { content == "." };
Link { url == "another url" } {
Style;
Text { content == "BOLD link" };
Style;
};
};
);
}
#[test]
fn lua() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
Some %<nml.link.push("link", "url")>%.
%<
nml.link.push("**BOLD link**", "another url")
>%
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Paragraph {
Text { content == "Some " };
Link { url == "url" } { Text { content == "link" }; };
Text { content == "." };
Link { url == "another url" } {
Style;
Text { content == "BOLD link" };
Style;
};
};
);
}
}

View file

@ -1,105 +0,0 @@
use std::sync::Arc;
use ariadne::Span;
use parking_lot::RwLock;
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target;
use crate::compiler::output::CompilerOutput;
use crate::parser::reports::Report;
use crate::parser::source::Token;
use crate::unit::element::ContainerElement;
use crate::unit::element::ElemKind;
use crate::unit::element::Element;
use crate::unit::scope::Scope;
use crate::unit::scope::ScopeAccessor;
#[derive(Debug)]
pub struct Link {
pub(crate) location: Token,
/// Link display content
pub(crate) display: Vec<Arc<RwLock<Scope>>>,
/// Url of link
pub(crate) url: url::Url,
}
impl Element for Link {
fn location(&self) -> &Token {
&self.location
}
fn kind(&self) -> ElemKind {
ElemKind::Inline
}
fn element_name(&self) -> &'static str {
"Link"
}
fn compile<'e>(
&'e self,
_scope: Arc<RwLock<Scope>>,
compiler: &'e Compiler,
output: &mut CompilerOutput,
) -> Result<(), Vec<Report>> {
match compiler.target() {
Target::HTML => {
output.add_content(format!(
"<a href=\"{}\">",
compiler.sanitize(self.url.as_str())
));
let display = &self.display[0];
for (scope, elem) in display.content_iter(false) {
elem.compile(scope, compiler, output)?;
}
output.add_content("</a>");
}
_ => todo!(""),
}
Ok(())
}
fn provide_hover(&self) -> Option<String> {
Some(format!("Link
# Properties
* **Location**: [{0}] ({1}..{2})
* **Url**: [{3}]({3})",
self.location.source().name(),
self.location().range.start(),
self.location().range.end(),
self.url.to_string()))
}
fn as_container(self: Arc<Self>) -> Option<Arc<dyn ContainerElement>> {
Some(self)
}
}
impl ContainerElement for Link {
fn contained(&self) -> &[Arc<RwLock<Scope>>] {
self.display.as_slice()
}
fn nested_kind(&self) -> ElemKind {
if self.kind() != ElemKind::Compound {
return self.kind();
}
for contained in self.contained() {
for it in contained.content_iter(true) {
match it.1.kind() {
ElemKind::Block => return ElemKind::Block,
ElemKind::Compound => {
if let Some(container) = it.1.as_container() {
if container.nested_kind() == ElemKind::Block {
return ElemKind::Block;
}
}
}
_ => {}
}
}
}
ElemKind::Inline
}
}

View file

@ -1,2 +0,0 @@
pub mod elem;
pub mod rule;

View file

@ -1,212 +0,0 @@
use std::sync::Arc;
use crate::parser::reports::macros::*;
use crate::parser::reports::*;
use crate::parser::rule::RuleTarget;
use crate::parser::state::CustomStates;
use crate::parser::state::ParseMode;
use crate::unit::translation::TranslationAccessors;
use crate::unit::translation::TranslationUnit;
use ariadne::Fmt;
use parser::rule::RegexRule;
use parser::source::Token;
use parser::util;
use parser::util::escape_source;
use parser::util::parse_paragraph;
use regex::Regex;
use url::Url;
use super::elem::Link;
#[auto_registry::auto_registry(registry = "rules")]
pub struct LinkRule {
re: [Regex; 1],
}
impl Default for LinkRule {
fn default() -> Self {
Self {
re: [Regex::new(r"\[((?:\\.|[^\\\\])*?)\]\(((?:\\.|[^\\\\])*?)\)").unwrap()],
}
}
}
impl RegexRule for LinkRule {
fn name(&self) -> &'static str {
"Link"
}
fn target(&self) -> RuleTarget {
RuleTarget::Meta
}
fn regexes(&self) -> &[Regex] {
&self.re
}
fn enabled(
&self,
_unit: &TranslationUnit,
_mode: &ParseMode,
_states: &mut CustomStates,
_id: usize,
) -> bool {
true
}
fn on_regex_match<'u>(
&self,
_: usize,
unit: &mut TranslationUnit,
token: Token,
matches: regex::Captures,
) {
// Parse display
let link_display = match matches.get(1) {
Some(display) => {
if display.as_str().is_empty() {
report_err!(
unit,
token.source(),
"Empty Link Display".into(),
span(display.range(), "Link display is empty".into())
);
return;
}
let display_source = escape_source(
token.source(),
display.range(),
"Link Display".into(),
'\\',
"]",
);
if display_source.content().is_empty() {
report_err!(
unit,
token.source(),
"Empty Link Display".into(),
span(
display.range(),
format!(
"Link name is empty. Once processed, `{}` yields `{}`",
display.as_str().fg(unit.colors().highlight),
display_source.fg(unit.colors().highlight),
)
)
);
return;
}
unit.with_lsp(|lsp| {
lsp.with_semantics(token.source(), |sems, tokens| {
sems.add(
display.range().start - 1..display.range().start,
tokens.link_display_sep,
);
});
});
match parse_paragraph(unit, display_source) {
Err(err) => {
report_err!(
unit,
token.source(),
"Invalid Link Display".into(),
span(
display.range(),
format!("Failed to parse link display:\n{err}")
)
);
return;
}
Ok(paragraph) => paragraph,
}
}
_ => panic!("Empty link name"),
};
// Parse url
let url_text = matches.get(2).unwrap();
let url = match Url::parse(util::transform_text(url_text.as_str()).as_str()) {
Ok(url) => url,
Err(err) => {
report_err!(
unit,
token.source(),
"Invalid Link URL".into(),
span(url_text.range(), err.to_string())
);
return;
}
};
// Add element
unit.add_content(Arc::new(Link {
location: token.clone(),
display: vec![link_display],
url,
}));
// Add semantics
unit.with_lsp(|lsp| {
lsp.with_semantics(token.source(), |sems, tokens| {
sems.add(
matches.get(1).unwrap().end()..matches.get(1).unwrap().end() + 1,
tokens.link_display_sep,
);
let url = matches.get(2).unwrap().range();
sems.add(url.start - 1..url.start, tokens.link_url_sep);
sems.add(url.clone(), tokens.link_url);
sems.add(url.end..url.end + 1, tokens.link_url_sep);
})
});
}
/*
fn register_bindings(&self, kernel: &Kernel, table: mlua::Table) {
kernel.create_function(
table,
"push",
|mut ctx, _, (display, url): (String, String)| {
// Parse display
let source = Arc::new(VirtualSource::new(
ctx.location.clone(),
":LUA:Link Display".to_string(),
display,
));
let display_content = match parse_paragraph(ctx.unit, source) {
Err(err) => {
return Err(BadArgument {
to: Some("push".to_string()),
pos: 1,
name: Some("display".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Failed to parse link display: {err}"
))),
});
}
Ok(scope) => scope,
};
// Parse url
let url = url::Url::parse(&url).map_err(|err| BadArgument {
to: Some("push".to_string()),
pos: 2,
name: Some("url".to_string()),
cause: Arc::new(mlua::Error::external(format!("Failed to parse url: {err}"))),
})?;
// Add element
let location = ctx.location.clone();
ctx.unit.add_content(Rc::new(Link {
location,
display: vec![display_content],
url,
}));
Ok(())
},
);
}
*/
}

508
src/elements/list.rs Normal file
View file

@ -0,0 +1,508 @@
use std::any::Any;
use std::cell::Ref;
use std::collections::HashMap;
use std::ops::Range;
use std::rc::Rc;
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target;
use crate::document::document::Document;
use crate::document::document::DocumentAccessors;
use crate::document::element::ContainerElement;
use crate::document::element::ElemKind;
use crate::document::element::Element;
use crate::parser::parser::ParserState;
use crate::parser::rule::Rule;
use crate::parser::source::Cursor;
use crate::parser::source::Source;
use crate::parser::source::Token;
use crate::parser::source::VirtualSource;
use crate::parser::util;
use crate::parser::util::process_escaped;
use crate::parser::util::Property;
use crate::parser::util::PropertyMapError;
use crate::parser::util::PropertyParser;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use regex::Match;
use regex::Regex;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum MarkerKind {
Open,
Close,
}
#[derive(Debug)]
pub struct ListMarker {
pub(self) location: Token,
pub(self) numbered: bool,
pub(self) kind: MarkerKind,
}
impl Element for ListMarker {
fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Block }
fn element_name(&self) -> &'static str { "List Marker" }
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> {
match compiler.target() {
Target::HTML => match (self.kind, self.numbered) {
(MarkerKind::Close, true) => Ok("</ol>".to_string()),
(MarkerKind::Close, false) => Ok("</ul>".to_string()),
(MarkerKind::Open, true) => Ok("<ol>".to_string()),
(MarkerKind::Open, false) => Ok("<ul>".to_string()),
},
_ => todo!(),
}
}
}
#[derive(Debug)]
pub struct ListEntry {
pub(self) location: Token,
pub(self) numbering: Vec<(bool, usize)>,
pub(self) content: Vec<Box<dyn Element>>,
pub(self) bullet: Option<String>,
}
impl Element for ListEntry {
fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Block }
fn element_name(&self) -> &'static str { "List Entry" }
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> {
match compiler.target() {
Target::HTML => {
let mut result = "<li>".to_string();
for elem in &self.content {
result += elem.compile(compiler, document)?.as_str();
}
result += "</li>";
Ok(result)
}
_ => todo!(),
}
}
fn as_container(&self) -> Option<&dyn ContainerElement> { Some(self) }
}
impl ContainerElement for ListEntry {
fn contained(&self) -> &Vec<Box<dyn Element>> { &self.content }
fn push(&mut self, elem: Box<dyn Element>) -> Result<(), String> {
if elem.kind() == ElemKind::Block {
return Err("Cannot add block element inside a list".to_string());
}
self.content.push(elem);
Ok(())
}
}
pub struct ListRule {
start_re: Regex,
continue_re: Regex,
properties: PropertyParser,
}
impl ListRule {
pub fn new() -> Self {
let mut props = HashMap::new();
props.insert(
"offset".to_string(),
Property::new(false, "Entry numbering offset".to_string(), None),
);
props.insert(
"bullet".to_string(),
Property::new(false, "Entry bullet".to_string(), None),
);
Self {
start_re: Regex::new(r"(?:^|\n)(?:[^\S\r\n]+)([*-]+)(?:\[((?:\\.|[^\\\\])*?)\])?(.*)")
.unwrap(),
continue_re: Regex::new(r"(?:^|\n)([^\S\r\n]+)([^\s].*)").unwrap(),
properties: PropertyParser { properties: props },
}
}
fn push_markers(
token: &Token,
state: &ParserState,
document: &dyn Document,
current: &Vec<(bool, usize)>,
target: &Vec<(bool, usize)>,
) {
let mut start_pos = 0;
for i in 0..std::cmp::min(target.len(), current.len()) {
if current[i].0 != target[i].0 {
break;
}
start_pos += 1;
}
// Close
for i in start_pos..current.len() {
state.push(
document,
Box::new(ListMarker {
location: token.clone(),
kind: MarkerKind::Close,
numbered: current[current.len() - 1 - (i - start_pos)].0,
}),
);
}
// Open
for i in start_pos..target.len() {
state.push(
document,
Box::new(ListMarker {
location: token.clone(),
kind: MarkerKind::Open,
numbered: target[i].0,
}),
);
}
}
fn parse_properties(&self, m: Match) -> Result<(Option<usize>, Option<String>), String> {
let processed = process_escaped('\\', "]", m.as_str());
let pm = self.properties.parse(processed.as_str())?;
let offset = match pm.get("offset", |_, s| s.parse::<usize>()) {
Ok((_, val)) => Some(val),
Err(err) => match err {
PropertyMapError::ParseError(err) => {
return Err(format!("Failed to parse `offset`: {err}"))
}
PropertyMapError::NotFoundError(_) => None,
},
};
let bullet = pm
.get("bullet", |_, s| -> Result<String, ()> { Ok(s.to_string()) })
.map(|(_, s)| s)
.ok();
Ok((offset, bullet))
}
fn parse_depth(depth: &str, document: &dyn Document, offset: usize) -> Vec<(bool, usize)> {
let mut parsed = vec![];
// FIXME: Previous iteration used to recursively retrieve the list indent
let prev_entry = document
.last_element::<ListEntry>()
.and_then(|entry| Ref::filter_map(entry, |e| Some(&e.numbering)).ok());
let mut continue_match = true;
depth.chars().enumerate().for_each(|(idx, c)| {
let number = if offset == 0 {
prev_entry
.as_ref()
.and_then(|v| {
if !continue_match {
return None;
}
let numbered = c == '-';
match v.get(idx) {
None => None,
Some((prev_numbered, prev_idx)) => {
if *prev_numbered != numbered {
continue_match = false;
None
}
// New depth
else if idx + 1 == v.len() {
Some(prev_idx + 1)
}
// Increase from previous
else {
Some(*prev_idx)
} // Do nothing
}
}
})
.unwrap_or(1)
} else {
offset
};
match c {
'*' => parsed.push((false, number)),
'-' => parsed.push((true, number)),
_ => panic!("Unimplemented"),
}
});
return parsed;
}
}
impl Rule for ListRule {
fn name(&self) -> &'static str { "List" }
fn next_match(&self, _state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> {
self.start_re
.find_at(cursor.source.content(), cursor.pos)
.map_or(None, |m| {
Some((m.start(), Box::new([false; 0]) as Box<dyn Any>))
})
}
fn on_match<'a>(
&self,
state: &ParserState,
document: &'a dyn Document<'a>,
cursor: Cursor,
_match_data: Box<dyn Any>,
) -> (Cursor, Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>>) {
let mut reports = vec![];
let content = cursor.source.content();
let mut end_cursor = cursor.clone();
loop {
if let Some(captures) = self.start_re.captures_at(content, end_cursor.pos) {
if captures.get(0).unwrap().start() != end_cursor.pos {
break;
}
// Advance cursor
end_cursor = end_cursor.at(captures.get(0).unwrap().end());
// Properties
let mut offset = None;
let mut bullet = None;
if let Some(properties) = captures.get(2) {
match self.parse_properties(properties) {
Err(err) => {
reports.push(
Report::build(
ReportKind::Warning,
cursor.source.clone(),
properties.start(),
)
.with_message("Invalid List Entry Properties")
.with_label(
Label::new((cursor.source.clone(), properties.range()))
.with_message(err)
.with_color(state.parser.colors().warning),
)
.finish(),
);
break;
}
Ok(props) => (offset, bullet) = props,
}
}
// Get bullet from previous entry if it exists
if bullet.is_none() {
bullet = document
.last_element::<ListEntry>()
.and_then(|prev| prev.bullet.clone())
}
// Depth
let depth = ListRule::parse_depth(
captures.get(1).unwrap().as_str(),
document,
offset.unwrap_or(0),
);
// Content
let entry_start = captures.get(0).unwrap().start();
let mut entry_content = captures.get(3).unwrap().as_str().to_string();
let mut spacing: Option<(Range<usize>, &str)> = None;
while let Some(captures) = self.continue_re.captures_at(content, end_cursor.pos) {
// Break if next element is another entry
if captures.get(0).unwrap().start() != end_cursor.pos
|| captures
.get(2)
.unwrap()
.as_str()
.find(|c| c == '*' || c == '-')
== Some(0)
{
break;
}
// Advance cursor
end_cursor = end_cursor.at(captures.get(0).unwrap().end());
// Spacing
let current_spacing = captures.get(1).unwrap().as_str();
if let Some(spacing) = &spacing {
if spacing.1 != current_spacing {
reports.push(
Report::build(
ReportKind::Warning,
cursor.source.clone(),
captures.get(1).unwrap().start(),
)
.with_message("Invalid list entry spacing")
.with_label(
Label::new((
cursor.source.clone(),
captures.get(1).unwrap().range(),
))
.with_message("Spacing for list entries do not match")
.with_color(state.parser.colors().warning),
)
.with_label(
Label::new((cursor.source.clone(), spacing.0.clone()))
.with_message("Previous spacing")
.with_color(state.parser.colors().warning),
)
.finish(),
);
}
} else {
spacing = Some((captures.get(1).unwrap().range(), current_spacing));
}
entry_content += " ";
entry_content += captures.get(2).unwrap().as_str();
}
// Parse entry content
let token = Token::new(entry_start..end_cursor.pos, end_cursor.source.clone());
let entry_src = Rc::new(VirtualSource::new(
token.clone(),
"List Entry".to_string(),
entry_content,
));
let parsed_content = match util::parse_paragraph(state, entry_src, document) {
Err(err) => {
reports.push(
Report::build(ReportKind::Warning, token.source(), token.range.start)
.with_message("Unable to Parse List Entry")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_message(err)
.with_color(state.parser.colors().warning),
)
.finish(),
);
break;
}
Ok(mut paragraph) => std::mem::replace(&mut paragraph.content, vec![]),
};
if let Some(previous_depth) = document
.last_element::<ListEntry>()
.map(|ent| ent.numbering.clone())
{
ListRule::push_markers(&token, state, document, &previous_depth, &depth);
} else {
ListRule::push_markers(&token, state, document, &vec![], &depth);
}
state.push(
document,
Box::new(ListEntry {
location: Token::new(
entry_start..end_cursor.pos,
end_cursor.source.clone(),
),
numbering: depth,
content: parsed_content,
bullet,
}),
);
} else {
break;
}
}
// Close all lists
let current = document
.last_element::<ListEntry>()
.map(|ent| ent.numbering.clone())
.unwrap();
let token = Token::new(end_cursor.pos..end_cursor.pos, end_cursor.source.clone());
ListRule::push_markers(&token, state, document, &current, &Vec::new());
(end_cursor, reports)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::elements::paragraph::Paragraph;
use crate::elements::text::Text;
use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser;
use crate::parser::source::SourceFile;
use crate::validate_document;
#[test]
fn parser() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
* 1
*[offset=7] 2
continued
* 3
* New list
*-[bullet=(*)] A
*- B
* Back
*-* More nested
"#
.to_string(),
None,
));
let parser = LangParser::default();
let state = ParserState::new(&parser, None);
let (doc, _) = parser.parse(state, source, None);
validate_document!(doc.content().borrow(), 0,
ListMarker { numbered == false, kind == MarkerKind::Open };
ListEntry { numbering == vec![(false, 1)] } {
Text { content == "1" };
};
ListEntry { numbering == vec![(false, 7)] } {
Text { content == "2 continued" };
};
ListEntry { numbering == vec![(false, 8)] } {
Text { content == "3" };
};
ListMarker { numbered == false, kind == MarkerKind::Close };
Paragraph;
ListMarker { numbered == false, kind == MarkerKind::Open };
ListEntry { numbering == vec![(false, 1)] } {
Text { content == "New list" };
};
ListMarker { numbered == true, kind == MarkerKind::Open };
ListEntry { numbering == vec![(false, 2), (true, 1)], bullet == Some("(*)".to_string()) } {
Text { content == "A" };
};
ListEntry { numbering == vec![(false, 2), (true, 2)], bullet == Some("(*)".to_string()) } {
Text { content == "B" };
};
ListMarker { numbered == true, kind == MarkerKind::Close };
ListEntry { numbering == vec![(false, 2)] } {
Text { content == "Back" };
};
ListMarker { numbered == true, kind == MarkerKind::Open };
ListMarker { numbered == false, kind == MarkerKind::Open };
ListEntry { numbering == vec![(false, 3), (true, 1), (false, 1)] } {
Text { content == "More nested" };
};
ListMarker { numbered == false, kind == MarkerKind::Close };
ListMarker { numbered == true, kind == MarkerKind::Close };
ListMarker { numbered == false, kind == MarkerKind::Close };
);
}
}

View file

@ -1,100 +0,0 @@
use tower_lsp::lsp_types::CompletionContext;
use tower_lsp::lsp_types::CompletionItem;
use tower_lsp::lsp_types::CompletionItemKind;
use tower_lsp::lsp_types::InsertTextFormat;
use tower_lsp::lsp_types::MarkupContent;
use crate::lsp::completion::context_triggered;
use crate::lsp::completion::CompletionProvider;
use crate::unit::translation::TranslationUnit;
pub struct ListCompletion;
impl CompletionProvider for ListCompletion {
fn trigger(&self) -> &'static [&'static str] {
["*", "-"].as_slice()
}
fn unit_items(&self, _unit: &TranslationUnit, _items: &mut Vec<CompletionItem>) {}
fn static_items(&self, context: &Option<CompletionContext>, items: &mut Vec<CompletionItem>) {
// *
items.push(CompletionItem {
label: "*".to_string(),
detail: Some("Unnumbered List".into()),
documentation: Some(tower_lsp::lsp_types::Documentation::MarkupContent(
MarkupContent {
kind: tower_lsp::lsp_types::MarkupKind::Markdown,
value: "# Usage
List block: ```
* First entry
* Second entry
*- Nested entry```
TODO List: ```
* [X] Done
* [-] In progress
* [ ] TODO```
# Properties
* `offset` Numbering offset (**defaults to `1`**)
# See also
* `-` *numbered list*
"
.into(),
},
)),
kind: Some(CompletionItemKind::SNIPPET),
insert_text_format: Some(InsertTextFormat::SNIPPET),
insert_text: Some(format!(
"* ${{1:CONTENT}}"
)),
..CompletionItem::default()
});
// -
items.push(CompletionItem {
label: "-".to_string(),
detail: Some("Numbered List".into()),
documentation: Some(tower_lsp::lsp_types::Documentation::MarkupContent(
MarkupContent {
kind: tower_lsp::lsp_types::MarkupKind::Markdown,
value: "# Usage
List block: ```
- First entry
- Second entry
-* Nested entry
-[offset=32] Last entry```
TODO List: ```
- [X] Done
- [-] In progress
- [ ] TODO```
# Properties
* `offset` Numbering offset (**defaults to `1`**)
# See also
* `*` *unnumbered list*
"
.into(),
},
)),
kind: Some(CompletionItemKind::SNIPPET),
insert_text_format: Some(InsertTextFormat::SNIPPET),
insert_text: Some(format!(
"- ${{1:CONTENT}}"
)),
..CompletionItem::default()
});
}
}

View file

@ -1,169 +0,0 @@
use std::sync::Arc;
use parking_lot::RwLock;
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target;
use crate::compiler::output::CompilerOutput;
use crate::parser::reports::Report;
use crate::parser::source::Token;
use crate::unit::element::ContainerElement;
use crate::unit::element::ElemKind;
use crate::unit::element::Element;
use crate::unit::scope::Scope;
use crate::unit::scope::ScopeAccessor;
#[derive(Debug)]
#[repr(u8)]
pub enum CheckboxState {
Checked,
Unchecked,
Partial,
}
#[derive(Debug)]
pub enum BulletMarker {
Bullet,
Checkbox(CheckboxState),
}
#[derive(Debug)]
pub struct ListMarker {
pub(crate) numbered: bool,
pub(crate) offset: usize,
}
#[derive(Debug)]
pub struct ListEntry {
#[allow(unused)]
pub(crate) location: Token,
pub(crate) bullet: BulletMarker,
pub(crate) content: Arc<RwLock<Scope>>,
pub(crate) marker: Vec<ListMarker>,
}
#[derive(Debug)]
pub struct List {
pub(crate) location: Token,
pub(crate) contained: Vec<Arc<RwLock<Scope>>>,
pub(crate) entries: Vec<ListEntry>,
}
impl List {
pub fn add_entry(&mut self, entry: ListEntry) {
self.contained.push(entry.content.clone());
self.entries.push(entry);
}
}
impl Element for List {
fn location(&self) -> &Token {
&self.location
}
fn kind(&self) -> crate::unit::element::ElemKind {
ElemKind::Block
}
fn element_name(&self) -> &'static str {
"List"
}
fn compile(
&self,
_scope: Arc<RwLock<Scope>>,
compiler: &Compiler,
output: &mut CompilerOutput,
) -> Result<(), Vec<Report>> {
let mut stack = vec![];
let match_stack = |stack: &mut Vec<(bool, usize)>,
target: &Vec<ListMarker>,
output: &mut CompilerOutput| {
// Find first diff index
let mut m = 0;
for t in target {
if stack.len() <= m || stack[m].0 != t.numbered {
break;
}
m += 1;
}
// Apply offset
if m == stack.len() && m != 0 {
stack[m - 1].1 += target[m - 1].offset;
return true;
}
// Close
for e in stack[m..].iter().rev() {
match compiler.target() {
Target::HTML => output.add_content(["</ul>", "</ol>"][e.0 as usize]),
_ => todo!(),
}
}
// Open
for e in target[m..].iter() {
stack.push((e.numbered, e.offset));
match compiler.target() {
Target::HTML => output.add_content(["<ul>", "<ol>"][e.numbered as usize]),
_ => todo!(),
}
}
false
};
for entry in &self.entries {
let has_offset = match_stack(&mut stack, &entry.marker, output);
match compiler.target() {
Target::HTML => {
if has_offset {
output.add_content(format!(r#"<li value="{}">"#, stack.last().unwrap().1));
}
output.add_content("<li>");
match &entry.bullet {
BulletMarker::Checkbox(state) => match state {
CheckboxState::Unchecked => {
output.add_content(
r#"<input type="checkbox" class="checkbox-unchecked" onclick="return false;">"#,
);
}
CheckboxState::Partial => {
output.add_content(
r#"<input type="checkbox" class="checkbox-partial" onclick="return false;">"#,
);
}
CheckboxState::Checked => {
output.add_content(
r#"<input type="checkbox" class="checkbox-checked" onclick="return false;" checked>"#,
);
}
},
_ => {}
}
}
_ => todo!(),
}
for (scope, elem) in entry.content.content_iter(false) {
elem.compile(scope, compiler, output)?;
}
match compiler.target() {
Target::HTML => output.add_content("</li>"),
_ => todo!(),
}
}
match_stack(&mut stack, &vec![], output);
Ok(())
}
fn as_container(self: Arc<Self>) -> Option<Arc<dyn ContainerElement>> {
Some(self)
}
}
impl ContainerElement for List {
fn contained(&self) -> &[Arc<RwLock<Scope>>] {
self.contained.as_slice()
}
}

View file

@ -1,5 +0,0 @@
pub mod completion;
pub mod rule;
pub mod elem;
#[cfg(test)]
pub mod tests;

View file

@ -1,339 +0,0 @@
use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use ariadne::Fmt;
use regex::Regex;
use crate::parser::property::Property;
use crate::parser::property::PropertyParser;
use crate::parser::reports::macros::*;
use crate::parser::reports::*;
use crate::parser::rule::Rule;
use crate::parser::rule::RuleTarget;
use crate::parser::source::Cursor;
use crate::parser::source::Token;
use crate::parser::source::VirtualSource;
use crate::parser::state::CustomStates;
use crate::parser::state::ParseMode;
use crate::parser::util::escape_source;
use crate::parser::util::parse_paragraph;
use crate::unit::translation::TranslationAccessors;
use crate::unit::translation::TranslationUnit;
use super::completion::ListCompletion;
use super::elem::BulletMarker;
use super::elem::CheckboxState;
use super::elem::List;
use super::elem::ListEntry;
use super::elem::ListMarker;
#[auto_registry::auto_registry(registry = "rules")]
pub struct ListRule {
start_re: Regex,
continue_re: Regex,
properties: PropertyParser,
}
impl Default for ListRule {
fn default() -> Self {
let mut props = HashMap::new();
props.insert(
"offset".to_string(),
Property::new("Entry numbering offset".to_string(), None),
);
Self {
start_re: Regex::new(r"(?:^|\n)(?:[^\S\r\n]+)([*-]+)(?:\[((?:\\.|[^\\\\])*?)\])?(?:[^\S\r\n]{0,1}\[((?:\\.|[^\\\\])*?)\])?(?:[^\S\r\n]+)(.*)")
.unwrap(),
continue_re: Regex::new(r"(?:^|\n)([^\S\r\n].*)").unwrap(),
properties: PropertyParser { properties: props },
}
}
}
impl Rule for ListRule {
fn name(&self) -> &'static str {
"List"
}
fn target(&self) -> RuleTarget {
RuleTarget::Block
}
fn next_match(
&self,
_unit: &TranslationUnit,
mode: &ParseMode,
_states: &mut CustomStates,
cursor: &Cursor,
) -> Option<(usize, Box<dyn Any + Send + Sync>)> {
if mode.paragraph_only {
return None;
}
self.start_re
.find_at(cursor.source().content(), cursor.pos())
.map(|m| {
(
m.start(),
Box::new([false; 0]) as Box<dyn Any + Send + Sync>,
)
})
}
fn on_match<'u>(
&self,
unit: &mut TranslationUnit,
cursor: &Cursor,
_match_data: Box<dyn Any + Send + Sync>,
) -> Cursor {
let source = cursor.source();
let content = source.content();
let mut end_cursor = cursor.clone();
let mut list = List {
location: Token::new(0..0, cursor.source()),
contained: vec![],
entries: vec![],
};
let parse_depth =
|depth: &str, offset: usize, previous: &Vec<ListMarker>| -> Vec<ListMarker> {
// Build vec
let mut parsed = depth
.chars()
.map(|c| match c {
'*' => ListMarker {
numbered: false,
offset: 0,
},
'-' => ListMarker {
numbered: true,
offset: 0,
},
_ => panic!(),
})
.collect::<Vec<_>>();
// Appply offsets
let mut matched = true;
for i in 0..parsed.len() {
if matched
&& previous
.get(i)
.is_some_and(|prev| prev.numbered == parsed[i].numbered)
{
if i + 1 == parsed.len() {
parsed[i].offset = 1;
}
} else {
matched = false;
parsed[i].offset = 1;
}
}
parsed.last_mut().map(|last| last.offset = offset);
parsed
};
let mut parse_entry = || -> bool {
let Some(captures) = self.start_re.captures_at(content, end_cursor.pos()) else {
return false;
};
if captures.get(0).unwrap().start() != end_cursor.pos() {
return false;
}
end_cursor = end_cursor.at(captures.get(0).unwrap().end());
// Semantic
unit.with_lsp(|lsp| lsp.with_semantics(end_cursor.source(), |sems, tokens| {
sems.add(captures.get(1).unwrap().range(), tokens.list_bullet);
if let Some(props) = captures.get(2).map(|m| m.range()) {
sems.add(props.start - 1..props.start, tokens.list_prop_sep);
sems.add_to_queue(props.end..props.end + 1, tokens.list_prop_sep);
}
if let Some(props) = captures.get(3).map(|m| m.start()-1..m.end()+1) {
sems.add_to_queue(props, tokens.list_bullet_type);
}
}));
// Properties
let prop_source = escape_source(
end_cursor.source().clone(),
captures.get(2).map_or(0..0, |m| m.range()),
"List Properties".into(),
'\\',
"]",
);
let mut properties = match self.properties.parse(
"List",
unit,
Token::new(0..prop_source.content().len(), prop_source),
) {
Some(props) => props,
None => return false,
};
let offset =
match properties.get_opt(unit, "offset", |_, value| value.value.parse::<usize>()) {
Some(offset) => offset,
_ => return false,
};
// Depth
let depth = {
let empty = vec![];
let previous = list.entries.last().map(|ent| &ent.marker).unwrap_or(&empty);
parse_depth(
captures.get(1).unwrap().as_str(),
offset.unwrap_or(1),
previous,
)
};
// Parse bullet
let bullet = if let Some((bullet_range, bullet_content)) =
captures.get(3).map(|m| (m.range(), m.as_str()))
{
let data = match bullet_content {
"" | " " => BulletMarker::Checkbox(CheckboxState::Unchecked),
"-" => BulletMarker::Checkbox(CheckboxState::Partial),
"x" | "X" => BulletMarker::Checkbox(CheckboxState::Checked),
_ => {
report_err!(
unit,
end_cursor.source().clone(),
"Unknown list bullet type".into(),
span(
bullet_range,
format!(
"Unknown bullet type: `{}`",
bullet_content.fg(unit.colors().highlight),
)
)
);
return false;
}
};
// Add conceal
/*
if let Some(conceals) =
Conceals::from_source(cursor.source.clone(), &state.shared.lsp)
{
match data {
CustomListData::Checkbox(checkbox_state) => conceals.add(
custom_data.start - 1..custom_data.end + 1,
ConcealTarget::Token {
token: "checkbox".into(),
params: json!({
"state": checkbox_state,
}),
},
),
}
}
*/
data
} else {
BulletMarker::Bullet
};
/*
if let Some(conceals) =
Conceals::from_source(cursor.source.clone(), &state.shared.lsp)
{
let mut i = captures.get(1).unwrap().start();
for (depth, (numbered, _)) in depth.iter().enumerate() {
conceals.add(
i..i + 1,
lsp::conceal::ConcealTarget::Token {
token: "bullet".into(),
params: json!({
"depth": depth,
"numbered": *numbered,
}),
},
);
i += 1;
}
}
// Hints
if let Some(hints) = Hints::from_source(cursor.source.clone(), &state.shared.lsp) {
let mut label = String::new();
for (_, id) in &depth {
if !label.is_empty() {
label.push('.');
}
label.push_str(id.to_string().as_str());
}
hints.add(captures.get(1).unwrap().end(), label);
}
*/
// Content
let entry_start = captures.get(4).unwrap().start();
let mut entry_content = captures.get(4).unwrap().as_str().to_string();
while let Some(captures) = self.continue_re.captures_at(content, end_cursor.pos()) {
// Break if next element is another entry
if captures.get(0).unwrap().start() != end_cursor.pos()
|| captures
.get(1)
.unwrap()
.as_str()
.find(['*', '-'])
.map(|delim| {
captures.get(1).unwrap().as_str()[0..delim]
.chars()
.all(|c| c.is_whitespace())
}) == Some(true)
{
break;
}
// Advance cursor
end_cursor = end_cursor.at(captures.get(0).unwrap().end());
entry_content += "\n";
entry_content += captures.get(1).unwrap().as_str();
}
// Parse entry content
let token = Token::new(entry_start..end_cursor.pos(), end_cursor.source().clone());
let entry_src = Arc::new(VirtualSource::new(
token.clone(),
"List Entry".to_string(),
entry_content,
));
let parsed_content = match parse_paragraph(unit, entry_src) {
Err(err) => {
report_warn!(
unit,
token.source(),
"Unable to parse List Entry".into(),
span(token.range.clone(), err.into())
);
// Return an empty paragraph
return false;
}
Ok(paragraph) => paragraph,
};
list.add_entry(ListEntry {
location: Token::new(entry_start..end_cursor.pos(), end_cursor.source()),
bullet,
content: parsed_content,
marker: depth,
});
true
};
while parse_entry() {}
list.location.range = cursor.pos()..end_cursor.pos();
unit.add_content(Arc::new(list));
end_cursor
}
fn completion(&self) -> Option<Box<dyn lsp::completion::CompletionProvider + 'static + Send + Sync>> {
Some(Box::new(ListCompletion {}))
}
}

View file

@ -1,44 +0,0 @@
use std::sync::Arc;
use crate::elements::list::elem::List;
use crate::elements::text::elem::Text;
use crate::elements::variable::elem::VariableDefinition;
use crate::parser::parser::Parser;
use crate::parser::source::SourceFile;
use crate::unit::translation::TranslationUnit;
use crate::unit::variable::{VariableName, VariableVisibility};
use crate::validate_ast;
#[test]
fn parser() {
let source = Arc::new(SourceFile::with_content(
"".to_string(),
r#"
* first
* second
multi
line
- third
* new list
"#.to_string(),
None,
));
let parser = Parser::new();
let unit = TranslationUnit::new("".into(), Arc::new(parser), source, false, false);
let (reports, unit) = unit.consume("".into());
eprintln!("{:#?}", unit.get_entry_scope());
assert!(reports.is_empty());
validate_ast!(unit.get_entry_scope(), 0,
List [
{ Text { content == "first" }; }
{ Text { content == "second \tmulti line" }; }
{ Text { content == "third" }; }
];
List [
{ Text { content == "new list" }; }
];
);
}

Some files were not shown because too many files have changed in this diff Show more