Compare commits

...

20 commits

Author SHA1 Message Date
822c3e3b66 Better lists offset 2024-08-26 20:50:23 +02:00
860fc785ac Blockquote doc 2024-08-26 12:45:41 +02:00
c23afc7f55 Fix regex 2024-08-26 12:38:43 +02:00
ffd1903a65 Reference styling 2024-08-26 10:59:15 +02:00
ea0a0cf5b1 Spaces between blockquotes paragraphs 2024-08-25 14:46:06 +02:00
87164cc2c9 Nested blockquotes 2024-08-25 14:16:08 +02:00
07970d2745 Fixed blockquote 2024-08-25 13:19:24 +02:00
46d579247b Improved navigation sorting 2024-08-25 12:02:46 +02:00
6eae5cd79b Clippu 2024-08-14 22:50:32 +02:00
fa68b68bf6 Update docs 2024-08-14 15:27:34 +02:00
b3b99c4069 Added bindings & tests 2024-08-14 11:09:42 +02:00
35408f03b1 Tests 2024-08-13 23:17:08 +02:00
2211c44ee0 Refactored processing 2024-08-13 22:46:10 +02:00
d6e6dbd660 Cross-references 2024-08-13 19:18:10 +02:00
d8fd2feefb Blockquotes style 2024-08-12 22:33:25 +02:00
06fd73c9f9 Blockquotes 2024-08-12 11:25:17 +02:00
cd54479618 More bindings 2024-08-09 13:25:59 +02:00
fcc401f203 Update macro 2024-08-08 17:11:32 +02:00
4784921bb8 Automatic rules registration 2024-08-08 14:12:16 +02:00
85fe0425b9 Added auto-registry proc macro 2024-08-08 11:21:58 +02:00
57 changed files with 2845 additions and 758 deletions

21
Cargo.lock generated
View file

@ -59,6 +59,16 @@ dependencies = [
"syn 2.0.72", "syn 2.0.72",
] ]
[[package]]
name = "auto-registry"
version = "0.0.4"
dependencies = [
"lazy_static",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "auto_impl" name = "auto_impl"
version = "1.2.0" version = "1.2.0"
@ -733,6 +743,7 @@ name = "nml"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ariadne", "ariadne",
"auto-registry",
"dashmap 6.0.1", "dashmap 6.0.1",
"downcast-rs", "downcast-rs",
"getopts", "getopts",
@ -743,6 +754,7 @@ dependencies = [
"mlua", "mlua",
"rand 0.8.5", "rand 0.8.5",
"regex", "regex",
"runtime-format",
"rusqlite", "rusqlite",
"rust-crypto", "rust-crypto",
"serde", "serde",
@ -1094,6 +1106,15 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "runtime-format"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09958d5b38bca768ede7928c767c89a08ba568144a7b61992aecae79b03c8c94"
dependencies = [
"tinyvec",
]
[[package]] [[package]]
name = "rusqlite" name = "rusqlite"
version = "0.31.0" version = "0.31.0"

View file

@ -17,6 +17,7 @@ inherits = "release"
debug = true debug = true
[dependencies] [dependencies]
auto-registry = { path = "crates/auto-registry" }
ariadne = "0.4.1" ariadne = "0.4.1"
dashmap = "6.0.1" dashmap = "6.0.1"
downcast-rs = "1.2.1" downcast-rs = "1.2.1"
@ -32,11 +33,16 @@ rust-crypto = "0.2.36"
serde = "1.0.204" serde = "1.0.204"
serde_json = "1.0.120" serde_json = "1.0.120"
syntect = "5.2.0" 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" tower-lsp = "0.20.0"
unicode-segmentation = "1.11.0" unicode-segmentation = "1.11.0"
walkdir = "2.5.0" walkdir = "2.5.0"
runtime-format = "0.1.3"
[dev-dependencies] [dev-dependencies]
rand = "0.8.5" rand = "0.8.5"

View file

@ -35,9 +35,9 @@ cargo build --release --bin nml
- [x] LaTeX rendering - [x] LaTeX rendering
- [x] Graphviz rendering - [x] Graphviz rendering
- [x] Media - [x] Media
- [ ] References - [x] References
- [ ] Navigation - [x] Navigation
- [ ] Cross-Document references - [x] Cross-Document references
- [ ] Complete Lua api - [ ] Complete Lua api
- [ ] Documentation - [ ] Documentation
- [ ] Table - [ ] Table

54
crates/auto-registry/Cargo.lock generated Normal file
View file

@ -0,0 +1,54 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "auto-registry"
version = "0.0.4"
dependencies = [
"lazy_static",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[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

@ -0,0 +1,13 @@
[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" ] }
lazy_static = "1.5.0"

View file

@ -0,0 +1,296 @@
#![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::ItemStruct;
lazy_static! {
/// The registry, each key corresponds to an identifier that needs to be
/// valid in the context of the [`genegenerate_registry`] macro.
static ref REGISTRY: Mutex<RefCell<HashMap<String, Vec<String>>>> =
Mutex::new(RefCell::new(HashMap::new()));
}
/// 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
///
/// # 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 reg_mtx = REGISTRY.lock().unwrap();
let mut reg_borrow = reg_mtx.borrow_mut();
if let Some(ref mut vec) = reg_borrow.get_mut(args.registry.value().as_str()) {
vec.push(path);
} else {
reg_borrow.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 target, i.e the generated function name
target: syn::Ident,
/// The maker macro, takes all constructed items and processes them
maker: syn::Expr,
/// The return type for the function
return_type: syn::Type,
}
/// Parser for [`GenerateRegistryArgs`]
impl Parse for GenerateRegistryArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut registry = None;
let mut target = None;
let mut maker = None;
let mut return_type = None;
loop {
let key: syn::Ident = input.parse()?;
input.parse::<syn::Token![=]>()?;
match key.to_string().as_str() {
"registry" => registry = Some(input.parse()?),
"target" => target = Some(input.parse()?),
"maker" => maker = Some(input.parse()?),
"return_type" => return_type = Some(input.parse()?),
_ => {
return Err(syn::Error::new(
key.span(),
format!(
"Unknown attribute `{}`, excepted `registry` or `target`",
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 target.is_none() {
return Err(syn::Error::new(
input.span(),
"Missing required attribute `target`".to_string(),
));
} else if maker.is_none() {
return Err(syn::Error::new(
input.span(),
"Missing required attribute `maker`".to_string(),
));
} else if return_type.is_none() {
return Err(syn::Error::new(
input.span(),
"Missing required attribute `return_type`".to_string(),
));
}
Ok(GenerateRegistryArgs {
registry: registry.unwrap(),
target: target.unwrap(),
maker: maker.unwrap(),
return_type: return_type.unwrap(),
})
}
}
/// The proc macro that generates the function to build the registry
///
/// # Attributes
/// - registry: (String) Name of the registry to generate
/// - target: (Identifier) Name of the resulting function
/// - maker: (Macro) A macro that will take all the newly constructed objects
/// comma-separated and create the resulting expression
/// - return_type: (Type) The return type of the generated function.
/// Must match the type of the macro invocation
///
/// # Example
/// ```
/// macro_rules! create_listeners {
/// ( $($construct:expr),+ $(,)? ) => {{
/// vec![$(Box::new($construct) as Box<dyn Listener>,)+]
/// }};
/// }
/// #[generate_registry(
/// registry = "listeners",
/// target = build_listeners,
/// return_type = Vec<Box<dyn Listener>>,
/// maker = create_listeners)]
///
/// fn main()
/// {
/// let all_listeners : Vec<Box<dyn Listener>> = build_listeners();
/// }
/// ```
#[proc_macro_attribute]
pub fn generate_registry(attr: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as GenerateRegistryArgs);
let reg_mtx = REGISTRY.lock().unwrap();
let mut stream = proc_macro2::TokenStream::new();
if let Some(names) = reg_mtx.borrow().get(args.registry.value().as_str()) {
for name in names {
let struct_name: proc_macro2::TokenStream = name.parse().unwrap();
stream.extend(quote::quote_spanned!(proc_macro2::Span::call_site() =>
#struct_name::new(),
));
}
} else {
panic!(
"Unable to find registry item with key=`{}`",
args.registry.value()
);
}
let function = args.target;
let return_type = args.return_type;
let maker = args.maker;
let rest: proc_macro2::TokenStream = input.into();
quote! {
fn #function() -> #return_type {
#maker!(
#stream
)
}
#rest
}
.into()
}

View file

@ -0,0 +1,59 @@
@import ../template.nml
@nav.previous = Blockquote
%<make_doc({"Blocks"}, "Blockquotes", "Blockquotes")>%
# Blockquotes
>[author=Lennart Poettering, cite=SystemD github issue 5998, url=https://github.com/systemd/systemd/pull/5998]
>>IMO, you shouldn't see the assignment of a CVE as a negative thing. The bug exists whether or not a CVE is assigned. The assignment of a CVE allows for people to consider what this issue means for them.
>
>Well, that makes no sense. You don't assign CVEs to every single random bugfix we do, do you? So why this one? I understand your currency is CVEs, but this just makes CVEs useless. And hardly anymore useful than a git history...
>
>I mean, I am fine with security bureaucracy if it actually helps anyone, but you just create noise where there shouldn't be any. And that way you just piss off the upstreams whose cooperation you actually should be interested in. Your at least made sure that my own interest in helping your efforts goes to zero...
# Nesting blockquotes
> Quotes can be nested
>> Here's a subquote
>>>[author=With author, cite=With cite]
>>> Here's another subquote
>> Back to the subquote
>
>> Another subquote
> This issue is getting a bit too heated, locking right now
```Markdown, Given by the following
> Nest quotes can be nested
>> Here's a subquote
>>>[author=With author, cite=With cite]
>>> Here's another subquote
>> Back to the subquote
>
>> Another subquote
> This issue is getting a bit too heated, locking right now
```
# Properties
Properties must be specified on the first `>` of the quote, inside brackets.
* ``author`` The quote author
* ``cite`` The quote source name
* ``url`` The quote source url (used for accessibility)
# Blockquotes styling
The blockquotes styling controls how the author, cite and url are rendered. This is controlled by style key ``style.blockquote``.
* ``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 onl
*- Format for cite only
```JSON, Default Style
{
"author_pos": "After",
"format": ["{author}, {cite}", "{author}", "{cite}"],
}
```

View file

@ -8,28 +8,50 @@
digraph { digraph {
bgcolor=transparent; bgcolor=transparent;
graph[fontcolor=darkgray]; graph[fontcolor=darkgray];
node[fontcolor=darkgray]; node[shape=box,fontcolor=darkgray];
edge[fontcolor=darkgray, color=gray90]; edge[fontcolor=darkgray, color=gray];
filelist [color=orange, label="File List"];
doclist [color=orange, label="Document List"];
filelist [shape=box, color=orange, label="File List"];
doclist [shape=box, color=orange, label="Document List"];
iscached [shape=diamond, color=red, label="Cached?"]; iscached [shape=diamond, color=red, label="Cached?"];
parse [shape=box, color=white, label=Parse]; parse [color=white, label=Parse];
compile [shape=box,color=white, label=Compile]; compile [color=white, label=Compile];
cache [shape=box, color=orange, label=Cache]; cache [color=orange, label=Cache];
filelist -> iscached; filelist -> iscached;
iscached -> cache[dir=both]; iscached -> cache[dir=both,color=lightblue,style=dashed];
iscached -> doclist[label="Yes"]; 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 -> parse[label="No"];
parse -> compile;
compile -> cache[label=""];
compile -> doclist[label=""]; compile -> doclist[label=""];
buildnav [color=white, label="Build Navigation"]; buildnav [color=white, label="Build Navigation"];
doclist -> buildnav; xref [color=white, label="Resolve Cross-References"];
output [color=white, label="Output"];
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"];
buildnav -> output; buildnav -> output;
} }
[/graph] [/graph]
@ -43,7 +65,7 @@ Graphs blocks are delimited by `` [graph]...[/graph]``
# Properties # Properties
* ``layout`` The layout engine, defaults to `dot` * ``layout`` The layout engine, defaults to `dot`
see [Graphviz's documentation](https://graphviz.org/docs/layouts/), allowed values: see [Graphviz's documentation](https://graphviz.org/docs/layouts/). Allowed values:
*- [`dot`](https://graphviz.org/docs/layouts/dot/) *- [`dot`](https://graphviz.org/docs/layouts/dot/)
*- [`neato`](https://graphviz.org/docs/layouts/neato/) *- [`neato`](https://graphviz.org/docs/layouts/neato/)
*- [`fdp`](https://graphviz.org/docs/layouts/fdp/) *- [`fdp`](https://graphviz.org/docs/layouts/fdp/)
@ -313,3 +335,10 @@ digraph UML_Class_diagram {
Graphviz graphs that have been rendered to **svg** are stored in the cache database, under table ``cached_dot``. 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. Unless you modify the graph or it's properties, it won't be rendered again, instead it will be sourced from the database.
# Bindigs
* ``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,7 +3,14 @@
@LaTeX = $|[kind=inline, caption=LaTeX]\LaTeX|$ @LaTeX = $|[kind=inline, caption=LaTeX]\LaTeX|$
#+LAYOUT_BEGIN Centered
*Bring some %LaTeX% unto your document!* *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 # Inline Math
@ -64,7 +71,7 @@ $|\begin{tikzpicture}
\end{tikzpicture}|$ \end{tikzpicture}|$
#+LAYOUT_END #+LAYOUT_END
# LaTeX environment #{tex_env} LaTeX environment
You can define multiple %LaTeX% environment, the default being `main` You can define multiple %LaTeX% environment, the default being `main`
* ``@tex.env.fontsize`` The fontsize (in pt) specified to `latex2svg` (default: `12`). * ``@tex.env.fontsize`` The fontsize (in pt) specified to `latex2svg` (default: `12`).
@ -98,4 +105,20 @@ To set the environment you wish to use for a particular %LaTeX% element, set the
%LaTeX% elements that have been successfully rendered to **svg** are stored in the cache database, to avoid processing them a second time. %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. 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 ``cached_tex``, if you modify the `env` all elements will be reprocessed which may take a while... 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

31
docs/references.nml Normal file
View file

@ -0,0 +1,31 @@
@import template.nml
@nav.previous = Sections
%<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

@ -54,3 +54,14 @@ The styling for the section link is controlled by the style key ``style.section`
"link": ["", "🔗", " "] "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

8
src/cache/cache.rs vendored
View file

@ -23,7 +23,7 @@ pub trait Cached {
fn key(&self) -> <Self as Cached>::Key; fn key(&self) -> <Self as Cached>::Key;
fn init(con: &mut Connection) -> Result<(), rusqlite::Error> { fn init(con: &Connection) -> Result<(), rusqlite::Error> {
con.execute(<Self as Cached>::sql_table(), ()).map(|_| ()) con.execute(<Self as Cached>::sql_table(), ()).map(|_| ())
} }
@ -38,7 +38,7 @@ pub trait Cached {
/// Note that on error, [`f`] may still have been called /// Note that on error, [`f`] may still have been called
fn cached<E, F>( fn cached<E, F>(
&self, &self,
con: &mut Connection, con: &Connection,
f: F, f: F,
) -> Result<<Self as Cached>::Value, CachedError<E>> ) -> Result<<Self as Cached>::Value, CachedError<E>>
where where
@ -62,10 +62,10 @@ pub trait Cached {
if let Some(value) = value { if let Some(value) = value {
// Found in cache // Found in cache
return Ok(value); Ok(value)
} else { } else {
// Compute a value // Compute a value
let value = match f(&self) { let value = match f(self) {
Ok(val) => val, Ok(val) => val,
Err(e) => return Err(CachedError::GenErr(e)), Err(e) => return Err(CachedError::GenErr(e)),
}; };

View file

@ -1,56 +1,54 @@
use std::cell::Ref; use std::cell::Ref;
use std::cell::RefCell; use std::cell::RefCell;
use std::cell::RefMut;
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use rusqlite::Connection; use rusqlite::Connection;
use crate::document::document::CrossReference;
use crate::document::document::Document; use crate::document::document::Document;
use crate::document::document::ElemReference; use crate::document::document::ElemReference;
use crate::document::variable::Variable; use crate::document::variable::Variable;
use super::postprocess::PostProcess;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum Target { pub enum Target {
HTML, HTML,
#[allow(unused)]
LATEX, LATEX,
} }
pub struct Compiler { pub struct Compiler<'a> {
target: Target, target: Target,
cache: Option<RefCell<Connection>>, cache: Option<&'a Connection>,
reference_count: RefCell<HashMap<String, HashMap<String, usize>>>, reference_count: RefCell<HashMap<String, HashMap<String, usize>>>,
// TODO: External references, i.e resolved later
sections_counter: RefCell<Vec<usize>>, sections_counter: RefCell<Vec<usize>>,
unresolved_references: RefCell<Vec<(usize, CrossReference)>>,
} }
impl Compiler { impl<'a> Compiler<'a> {
pub fn new(target: Target, db_path: Option<String>) -> Self { pub fn new(target: Target, con: Option<&'a Connection>) -> 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 { Self {
target, target,
cache: cache.map(|con| RefCell::new(con)), cache: con,
reference_count: RefCell::new(HashMap::new()), reference_count: RefCell::new(HashMap::new()),
sections_counter: RefCell::new(vec![]), sections_counter: RefCell::new(vec![]),
unresolved_references: RefCell::new(vec![]),
} }
} }
/// Gets the section counter for a given depth /// Gets the section counter for a given depth
/// This function modifies the section counter /// This function modifies the section counter
pub fn section_counter(&self, depth: usize) -> Ref<'_, Vec<usize>> pub fn section_counter(&self, depth: usize) -> Ref<'_, Vec<usize>> {
{
// Increment current counter // Increment current counter
if self.sections_counter.borrow().len() == depth { if self.sections_counter.borrow().len() == depth {
self.sections_counter.borrow_mut().last_mut() self.sections_counter
.borrow_mut()
.last_mut()
.map(|id| *id += 1); .map(|id| *id += 1);
return Ref::map(self.sections_counter.borrow(), |b| &*b); return Ref::map(self.sections_counter.borrow(), |b| b);
} }
// Close // Close
@ -63,7 +61,7 @@ impl Compiler {
self.sections_counter.borrow_mut().push(1); self.sections_counter.borrow_mut().push(1);
} }
Ref::map(self.sections_counter.borrow(), |b| &*b) Ref::map(self.sections_counter.borrow(), |b| b)
} }
/// Sanitizes text for a [`Target`] /// Sanitizes text for a [`Target`]
@ -79,6 +77,51 @@ impl Compiler {
} }
} }
/// 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>>(target: Target, str: S) -> String {
match 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"),
}
}
/// Gets a reference name /// Gets a reference name
pub fn refname<S: AsRef<str>>(target: Target, str: S) -> String { pub fn refname<S: AsRef<str>>(target: Target, str: S) -> String {
Self::sanitize(target, str).replace(' ', "_") Self::sanitize(target, str).replace(' ', "_")
@ -88,7 +131,7 @@ impl Compiler {
/// ///
/// # Parameters /// # Parameters
/// - [`reference`] The reference to get or insert /// - [`reference`] The reference to get or insert
pub fn reference_id<'a>(&self, document: &'a dyn Document, reference: ElemReference) -> usize { pub fn reference_id(&self, document: &dyn Document, reference: ElemReference) -> usize {
let mut borrow = self.reference_count.borrow_mut(); let mut borrow = self.reference_count.borrow_mut();
let reference = document.get_from_reference(&reference).unwrap(); let reference = document.get_from_reference(&reference).unwrap();
let refkey = reference.refcount_key(); let refkey = reference.refcount_key();
@ -114,10 +157,18 @@ impl Compiler {
} }
} }
/// Inserts a new crossreference
pub fn insert_crossreference(&self, pos: usize, reference: CrossReference) {
self.unresolved_references
.borrow_mut()
.push((pos, reference));
}
pub fn target(&self) -> Target { self.target } pub fn target(&self) -> Target { self.target }
pub fn cache(&self) -> Option<RefMut<'_, Connection>> { pub fn cache(&self) -> Option<&'a Connection> {
self.cache.as_ref().map(RefCell::borrow_mut) self.cache
//self.cache.as_ref().map(RefCell::borrow_mut)
} }
pub fn header(&self, document: &dyn Document) -> String { pub fn header(&self, document: &dyn Document) -> String {
@ -125,16 +176,13 @@ impl Compiler {
document: &dyn Document, document: &dyn Document,
var_name: &'static str, var_name: &'static str,
) -> Option<Rc<dyn Variable>> { ) -> Option<Rc<dyn Variable>> {
document document.get_variable(var_name).or_else(|| {
.get_variable(var_name) println!(
.and_then(|var| Some(var)) "Missing variable `{var_name}` in {}",
.or_else(|| { document.source().name()
println!( );
"Missing variable `{var_name}` in {}", None
document.source().name() })
);
None
})
} }
let mut result = String::new(); let mut result = String::new();
@ -178,7 +226,7 @@ impl Compiler {
result result
} }
pub fn compile(&self, document: &dyn Document) -> CompiledDocument { pub fn compile(&self, document: &dyn Document) -> (CompiledDocument, PostProcess) {
let borrow = document.content().borrow(); let borrow = document.content().borrow();
// Header // Header
@ -189,7 +237,7 @@ impl Compiler {
for i in 0..borrow.len() { for i in 0..borrow.len() {
let elem = &borrow[i]; let elem = &borrow[i];
match elem.compile(self, document) { match elem.compile(self, document, body.len()) {
Ok(result) => body.push_str(result.as_str()), Ok(result) => body.push_str(result.as_str()),
Err(err) => println!("Unable to compile element: {err}\n{elem:#?}"), Err(err) => println!("Unable to compile element: {err}\n{elem:#?}"),
} }
@ -208,14 +256,35 @@ impl Compiler {
.map(|(key, var)| (key.clone(), var.to_string())) .map(|(key, var)| (key.clone(), var.to_string()))
.collect::<HashMap<String, String>>(); .collect::<HashMap<String, String>>();
CompiledDocument { // References
let references = document
.scope()
.borrow_mut()
.referenceable
.iter()
.map(|(key, reference)| {
let elem = document.get_from_reference(reference).unwrap();
let refid = self.reference_id(document, *reference);
(key.clone(), elem.refid(self, refid))
})
.collect::<HashMap<String, String>>();
let postprocess = PostProcess {
resolve_references: self.unresolved_references.replace(vec![]),
};
let cdoc = CompiledDocument {
input: document.source().name().clone(), input: document.source().name().clone(),
mtime: 0, mtime: 0,
variables, variables,
references,
header, header,
body, body,
footer, footer,
} };
(cdoc, postprocess)
} }
} }
@ -226,12 +295,14 @@ pub struct CompiledDocument {
/// Modification time (i.e seconds since last epoch) /// Modification time (i.e seconds since last epoch)
pub mtime: u64, pub mtime: u64,
// TODO: Also store exported references /// All the variables defined in the document
// so they can be referenced from elsewhere /// with values mapped by [`Variable::to_string()`]
// 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>, pub variables: HashMap<String, String>,
/// All the referenceable elements in the document
/// with values mapped by [`ReferenceableElement::refid()`]
pub references: HashMap<String, String>,
/// Compiled document's header /// Compiled document's header
pub header: String, pub header: String,
/// Compiled document's body /// Compiled document's body
@ -244,10 +315,11 @@ impl CompiledDocument {
pub fn get_variable(&self, name: &str) -> Option<&String> { self.variables.get(name) } pub fn get_variable(&self, name: &str) -> Option<&String> { self.variables.get(name) }
fn sql_table() -> &'static str { fn sql_table() -> &'static str {
"CREATE TABLE IF NOT EXISTS compiled_documents ( "CREATE TABLE IF NOT EXISTS compiled_documents(
input TEXT PRIMARY KEY, input TEXT PRIMARY KEY,
mtime INTEGER NOT NULL, mtime INTEGER NOT NULL,
variables TEXT NOT NULL, variables TEXT NOT NULL,
internal_references TEXT NOT NULL,
header TEXT NOT NULL, header TEXT NOT NULL,
body TEXT NOT NULL, body TEXT NOT NULL,
footer TEXT NOT NULL footer TEXT NOT NULL
@ -257,7 +329,7 @@ impl CompiledDocument {
fn sql_get_query() -> &'static str { "SELECT * FROM compiled_documents WHERE input = (?1)" } fn sql_get_query() -> &'static str { "SELECT * FROM compiled_documents WHERE input = (?1)" }
fn sql_insert_query() -> &'static str { 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)" "INSERT OR REPLACE INTO compiled_documents (input, mtime, variables, internal_references, header, body, footer) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
} }
pub fn init_cache(con: &Connection) -> Result<usize, rusqlite::Error> { pub fn init_cache(con: &Connection) -> Result<usize, rusqlite::Error> {
@ -270,15 +342,16 @@ impl CompiledDocument {
input: input.to_string(), input: input.to_string(),
mtime: row.get_unwrap::<_, u64>(1), mtime: row.get_unwrap::<_, u64>(1),
variables: serde_json::from_str(row.get_unwrap::<_, String>(2).as_str()).unwrap(), variables: serde_json::from_str(row.get_unwrap::<_, String>(2).as_str()).unwrap(),
header: row.get_unwrap::<_, String>(3), references: serde_json::from_str(row.get_unwrap::<_, String>(3).as_str()).unwrap(),
body: row.get_unwrap::<_, String>(4), header: row.get_unwrap::<_, String>(4),
footer: row.get_unwrap::<_, String>(5), body: row.get_unwrap::<_, String>(5),
footer: row.get_unwrap::<_, String>(6),
}) })
}) })
.ok() .ok()
} }
/// Inserts [`CompiledDocument`] into cache /// Interts [`CompiledDocument`] into cache
pub fn insert_cache(&self, con: &Connection) -> Result<usize, rusqlite::Error> { pub fn insert_cache(&self, con: &Connection) -> Result<usize, rusqlite::Error> {
con.execute( con.execute(
Self::sql_insert_query(), Self::sql_insert_query(),
@ -286,6 +359,7 @@ impl CompiledDocument {
&self.input, &self.input,
&self.mtime, &self.mtime,
serde_json::to_string(&self.variables).unwrap(), serde_json::to_string(&self.variables).unwrap(),
serde_json::to_string(&self.references).unwrap(),
&self.header, &self.header,
&self.body, &self.body,
&self.footer, &self.footer,
@ -293,3 +367,19 @@ impl CompiledDocument {
) )
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_test() {
assert_eq!(Compiler::sanitize(Target::HTML, "<a>"), "&lt;a&gt;");
assert_eq!(Compiler::sanitize(Target::HTML, "&lt;"), "&amp;lt;");
assert_eq!(Compiler::sanitize(Target::HTML, "\""), "&quot;");
assert_eq!(Compiler::sanitize_format(Target::HTML, "{<>&\"}"), "{<>&\"}");
assert_eq!(Compiler::sanitize_format(Target::HTML, "{{<>}}"), "{{&lt;&gt;}}");
assert_eq!(Compiler::sanitize_format(Target::HTML, "{{<"), "{{&lt;");
}
}

View file

@ -1,2 +1,4 @@
pub mod compiler; pub mod compiler;
pub mod navigation; pub mod navigation;
pub mod process;
pub mod postprocess;

View file

@ -1,22 +1,35 @@
use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use crate::compiler::compiler::Compiler; use crate::compiler::compiler::Compiler;
use super::compiler::CompiledDocument; use super::compiler::CompiledDocument;
use super::compiler::Target; use super::compiler::Target;
use super::postprocess::PostProcess;
#[derive(Debug, Default)] #[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct NavEntry { pub struct NavEntry {
pub(self) entries: Vec<(String, String, Option<String>)>, title: String,
pub(self) children: HashMap<String, NavEntry>, path: String,
previous: Option<String>,
} }
impl NavEntry { #[derive(Debug, Default)]
pub struct NavEntries {
pub(self) entries: Vec<NavEntry>,
pub(self) children: HashMap<String, NavEntries>,
}
impl NavEntries {
// FIXME: Sanitize // FIXME: Sanitize
pub fn compile(&self, target: Target, doc: &CompiledDocument) -> String { pub fn compile(&self, target: Target, doc: &RefCell<CompiledDocument>) -> String {
let doc_borrow = doc.borrow();
let categories = vec![ let categories = vec![
doc.get_variable("nav.category").map_or("", |s| s.as_str()), doc_borrow
doc.get_variable("nav.subcategory") .get_variable("nav.category")
.map_or("", |s| s.as_str()),
doc_borrow
.get_variable("nav.subcategory")
.map_or("", |s| s.as_str()), .map_or("", |s| s.as_str()),
]; ];
@ -30,16 +43,16 @@ impl NavEntry {
categories: &Vec<&str>, categories: &Vec<&str>,
did_match: bool, did_match: bool,
result: &mut String, result: &mut String,
entry: &NavEntry, entry: &NavEntries,
depth: usize, depth: usize,
) { ) {
// Orphans = Links // Orphans = Links
for (title, path, _) in &entry.entries { for entry in &entry.entries {
result.push_str( result.push_str(
format!( format!(
r#"<li><a href="{}">{}</a></li>"#, r#"<li><a href="{}">{}</a></li>"#,
Compiler::sanitize(target, path), Compiler::sanitize(target, entry.path.as_str()),
Compiler::sanitize(target, title) Compiler::sanitize(target, entry.title.as_str())
) )
.as_str(), .as_str(),
); );
@ -77,32 +90,48 @@ impl NavEntry {
} }
fn sort_entry( fn sort_entry(
left: &(String, String, Option<String>), entrymap: &HashMap<String, Option<String>>,
right: &(String, String, Option<String>), left_title: &str,
right_title: &str,
) -> std::cmp::Ordering { ) -> std::cmp::Ordering {
match (&left.2, &right.2) { let left_previous = entrymap.get(left_title).unwrap();
(Some(_), Some(_)) => left.0.cmp(&right.0), let right_previous = entrymap.get(right_title).unwrap();
match (left_previous, right_previous) {
(Some(lp), Some(rp)) => {
if lp.as_str() == right_title {
std::cmp::Ordering::Greater
} else if rp.as_str() == left_title {
std::cmp::Ordering::Less
} else if rp.as_str() == lp.as_str() {
left_title.cmp(right_title)
} else {
Self::sort_entry(entrymap, lp.as_str(), rp.as_str())
}
}
(Some(lp), None) => { (Some(lp), None) => {
if &right.0 == lp { if right_title == lp.as_str() {
std::cmp::Ordering::Greater std::cmp::Ordering::Greater
} else { } else {
left.0.cmp(&right.0) left_title.cmp(right_title)
} }
} }
(None, Some(rp)) => { (None, Some(rp)) => {
if &left.0 == rp { if left_title == rp.as_str() {
std::cmp::Ordering::Less std::cmp::Ordering::Less
} else { } else {
left.0.cmp(&right.0) left_title.cmp(right_title)
} }
} }
(None, None) => left.0.cmp(&right.0), (None, None) => left_title.cmp(right_title),
} }
} }
} }
pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<NavEntry, String> { pub fn create_navigation(
let mut nav = NavEntry { docs: &Vec<(RefCell<CompiledDocument>, Option<PostProcess>)>,
) -> Result<NavEntries, String> {
let mut nav = NavEntries {
entries: vec![], entries: vec![],
children: HashMap::new(), children: HashMap::new(),
}; };
@ -110,19 +139,20 @@ pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<NavEntry, Strin
// All paths (for duplicate checking) // All paths (for duplicate checking)
let mut all_paths = HashMap::new(); let mut all_paths = HashMap::new();
for doc in docs { for (doc, _) in docs {
let cat = doc.get_variable("nav.category"); let doc_borrow = doc.borrow();
let subcat = doc.get_variable("nav.subcategory"); let cat = doc_borrow.get_variable("nav.category");
let title = doc let subcat = doc_borrow.get_variable("nav.subcategory");
let title = doc_borrow
.get_variable("nav.title") .get_variable("nav.title")
.or(doc.get_variable("doc.title")); .or(doc_borrow.get_variable("doc.title"));
let previous = doc.get_variable("nav.previous").map(|s| s.clone()); let previous = doc_borrow.get_variable("nav.previous").cloned();
let path = doc.get_variable("compiler.output"); let path = doc_borrow.get_variable("compiler.output");
let (title, path) = match (title, path) { let (title, path) = match (title, path) {
(Some(title), Some(path)) => (title, path), (Some(title), Some(path)) => (title, path),
_ => { _ => {
eprintln!("Skipping navigation generation for `{}`, must have a defined `@nav.title` and `@compiler.output`", doc.input); eprintln!("Skipping navigation generation for `{}`, must have a defined `@nav.title` and `@compiler.output`", doc_borrow.input);
continue; continue;
} }
}; };
@ -134,7 +164,7 @@ pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<NavEntry, Strin
None => { None => {
eprintln!( eprintln!(
"Skipping `{}`: No `@nav.category`, but `@nav.subcategory` is set", "Skipping `{}`: No `@nav.category`, but `@nav.subcategory` is set",
doc.input doc_borrow.input
); );
continue; continue;
} }
@ -144,7 +174,7 @@ pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<NavEntry, Strin
Some(cat_ent) => cat_ent, Some(cat_ent) => cat_ent,
None => { None => {
// Insert // Insert
nav.children.insert(cat.clone(), NavEntry::default()); nav.children.insert(cat.clone(), NavEntries::default());
nav.children.get_mut(cat.as_str()).unwrap() nav.children.get_mut(cat.as_str()).unwrap()
} }
}; };
@ -153,7 +183,9 @@ pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<NavEntry, Strin
Some(subcat_ent) => subcat_ent, Some(subcat_ent) => subcat_ent,
None => { None => {
// Insert // Insert
cat_ent.children.insert(subcat.clone(), NavEntry::default()); cat_ent
.children
.insert(subcat.clone(), NavEntries::default());
cat_ent.children.get_mut(subcat.as_str()).unwrap() cat_ent.children.get_mut(subcat.as_str()).unwrap()
} }
} }
@ -162,7 +194,7 @@ pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<NavEntry, Strin
Some(cat_ent) => cat_ent, Some(cat_ent) => cat_ent,
None => { None => {
// Insert // Insert
nav.children.insert(cat.clone(), NavEntry::default()); nav.children.insert(cat.clone(), NavEntries::default());
nav.children.get_mut(cat.as_str()).unwrap() nav.children.get_mut(cat.as_str()).unwrap()
} }
} }
@ -171,13 +203,13 @@ pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<NavEntry, Strin
}; };
// Find duplicates titles in current parent // Find duplicates titles in current parent
for (ent_title, _, _) in &pent.entries { for entry in &pent.entries {
if ent_title == title { if &entry.title == title {
return Err(format!( return Err(format!(
"Conflicting entry title `{title}` for entries with the same parent: ({})", "Conflicting entry title `{title}` for entries with the same parent: ({})",
pent.entries pent.entries
.iter() .iter()
.map(|(title, _, _)| title.clone()) .map(|entry| entry.title.clone())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ") .join(", ")
)); ));
@ -190,13 +222,22 @@ pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<NavEntry, Strin
} }
all_paths.insert(path.clone(), title.clone()); all_paths.insert(path.clone(), title.clone());
pent.entries.push((title.clone(), path.clone(), previous)); pent.entries.push(NavEntry {
title: title.clone(),
path: path.clone(),
previous,
});
} }
// Sort entries // Sort entries
fn sort_entries(nav: &mut NavEntry) { fn sort_entries(nav: &mut NavEntries) {
let entrymap = nav
.entries
.iter()
.map(|ent| (ent.title.clone(), ent.previous.clone()))
.collect::<HashMap<String, Option<String>>>();
nav.entries nav.entries
.sort_unstable_by(|l, r| NavEntry::sort_entry(l, r)); .sort_by(|l, r| NavEntries::sort_entry(&entrymap, l.title.as_str(), r.title.as_str()));
for (_, child) in &mut nav.children { for (_, child) in &mut nav.children {
sort_entries(child); sort_entries(child);
@ -209,30 +250,110 @@ pub fn create_navigation(docs: &Vec<CompiledDocument>) -> Result<NavEntry, Strin
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use rand::prelude::SliceRandom;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use rand::RngCore;
use crate::compiler::process::process_from_memory;
use super::*; use super::*;
#[test] #[test]
fn sort() { fn sort() {
let entries: Vec<(String, String, Option<String>)> = vec![ let entries: Vec<NavEntry> = vec![
("Index".into(), "".into(), None), NavEntry {
("AB".into(), "".into(), Some("Index".into())), title: "Index".into(),
("Getting Started".into(), "".into(), Some("Index".into())), path: "".into(),
("Sections".into(), "".into(), Some("Getting Started".into())), previous: None,
("Style".into(), "".into(), Some("Getting Started".into())), },
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 mut shuffled = entries.clone(); let mut shuffled = entries.clone();
for _ in 0..10 { for _ in 0..10 {
for i in 0..5 { let mut rng = OsRng {};
let pos = OsRng.next_u64() % entries.len() as u64; shuffled.shuffle(&mut rng);
shuffled.swap(i, pos as usize);
}
shuffled.sort_by(|l, r| NavEntry::sort_entry(l, r)); 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())
});
assert_eq!(shuffled, entries); 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

@ -0,0 +1,81 @@
use std::cell::RefCell;
use crate::document::document::CrossReference;
use super::compiler::CompiledDocument;
use super::compiler::Target;
/// Represents the list of tasks that have to run after the document has been compiled and the
/// compiled document list has been built. Every task is stored with a raw byte position in the
/// compiled document's body. The position represents the original position and thus should be
/// offset accordingly to other post-processing tasks.
pub struct PostProcess {
/// List of references to resolve i.e insert the resolved refname at a certain byte position
/// in the document's body
pub resolve_references: Vec<(usize, CrossReference)>,
}
impl PostProcess {
/// Applies postprocessing to a [`CompiledDocument`]
pub fn apply(
&self,
_target: Target,
list: &Vec<(RefCell<CompiledDocument>, Option<PostProcess>)>,
doc: &RefCell<CompiledDocument>,
) -> Result<String, String> {
let mut content = doc.borrow().body.clone();
let mut offset = 0;
for (pos, cross_ref) in &self.resolve_references {
// Cross-references
let mut found_ref: Option<(String, &RefCell<CompiledDocument>)> = None;
match cross_ref {
CrossReference::Unspecific(name) => {
for (doc, _) in list {
if let Some(found) = doc.borrow().references.get(name) {
// Check for duplicates
if let Some((_, previous_doc)) = &found_ref {
return Err(format!("Cannot use an unspecific reference for reference named: `{name}`. Found in document `{}` but also in `{}`. Specify the source of the reference to resolve the conflict.", previous_doc.borrow().input, doc.borrow().input));
}
found_ref = Some((found.clone(), doc));
}
}
}
CrossReference::Specific(doc_name, name) => {
let ref_doc = list.iter().find(|(doc, _)| {
let doc_borrow = doc.borrow();
if let Some(outname) = doc_borrow.variables.get("compiler.output") {
// Strip extension
let split_at = outname.rfind('.').unwrap_or(outname.len());
return doc_name == outname.split_at(split_at).0;
}
false
});
if ref_doc.is_none() {
return Err(format!(
"Cannot find document `{doc_name}` for reference `{name}` in `{}`",
doc.borrow().input
));
}
if let Some(found) = ref_doc.unwrap().0.borrow().references.get(name) {
found_ref = Some((found.clone(), &ref_doc.unwrap().0));
}
}
}
if let Some((found_ref, found_doc)) = &found_ref {
let found_borrow = found_doc.borrow();
let found_path = found_borrow.get_variable("compiler.output").ok_or("Unable to get the output. Aborting postprocessing.".to_string())?;
let insert_content = format!("{found_path}#{found_ref}");
content.insert_str(pos + offset, insert_content.as_str());
offset += insert_content.len();
} else {
return Err(format!("Cannot find reference `{cross_ref}` from document `{}`. Aborting postprocessing.", doc.borrow().input));
}
}
Ok(content)
}
}

196
src/compiler/process.rs Normal file
View file

@ -0,0 +1,196 @@
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use std::time::UNIX_EPOCH;
use rusqlite::Connection;
use crate::document::document::Document;
use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser;
use crate::parser::parser::ParserState;
use crate::parser::source::Source;
use crate::parser::source::SourceFile;
use super::compiler::CompiledDocument;
use super::compiler::Compiler;
use super::compiler::Target;
use super::postprocess::PostProcess;
/// Parses a source file into a document
fn parse(
parser: &LangParser,
source: Rc<dyn Source>,
debug_opts: &Vec<String>,
) -> Result<Box<dyn Document<'static>>, String> {
// Parse
//let source = SourceFile::new(input.to_string(), None).unwrap();
let (doc, _) = parser.parse(ParserState::new(parser, None), source.clone(), None);
if debug_opts.contains(&"ast".to_string()) {
println!("-- BEGIN AST DEBUGGING --");
doc.content()
.borrow()
.iter()
.for_each(|elem| println!("{elem:#?}"));
println!("-- END AST DEBUGGING --");
}
if debug_opts.contains(&"ref".to_string()) {
println!("-- BEGIN REFERENCES DEBUGGING --");
let sc = doc.scope().borrow();
sc.referenceable.iter().for_each(|(name, reference)| {
println!(" - {name}: `{:#?}`", doc.get_from_reference(reference));
});
println!("-- END REFERENCES DEBUGGING --");
}
if debug_opts.contains(&"var".to_string()) {
println!("-- BEGIN VARIABLES DEBUGGING --");
let sc = doc.scope().borrow();
sc.variables.iter().for_each(|(_name, var)| {
println!(" - `{:#?}`", var);
});
println!("-- END VARIABLES DEBUGGING --");
}
if parser.has_error() {
return Err("Parsing failed due to errors while parsing".to_string());
}
Ok(doc)
}
/// Takes a list of paths and processes it into a list of compiled documents
pub fn process(
target: Target,
files: Vec<PathBuf>,
db_path: &Option<String>,
force_rebuild: bool,
debug_opts: &Vec<String>,
) -> Result<Vec<(RefCell<CompiledDocument>, Option<PostProcess>)>, String> {
let mut compiled = vec![];
let current_dir = std::env::current_dir()
.map_err(|err| format!("Unable to get the current working directory: {err}"))?;
let con = db_path
.as_ref()
.map_or(Connection::open_in_memory(), Connection::open)
.map_err(|err| format!("Unable to open connection to the database: {err}"))?;
CompiledDocument::init_cache(&con)
.map_err(|err| format!("Failed to initialize cached document table: {err}"))?;
let parser = LangParser::default();
for file in files {
let meta = std::fs::metadata(&file)
.map_err(|err| format!("Failed to get metadata for `{file:#?}`: {err}"))?;
let modified = meta
.modified()
.map_err(|err| format!("Unable to query modification time for `{file:#?}`: {err}"))?;
// Move to file's directory
let file_parent_path = file
.parent()
.ok_or(format!("Failed to get parent path for `{file:#?}`"))?;
std::env::set_current_dir(file_parent_path)
.map_err(|err| format!("Failed to move to path `{file_parent_path:#?}`: {err}"))?;
let parse_and_compile = || -> Result<(CompiledDocument, Option<PostProcess>), String> {
// Parse
let source = SourceFile::new(file.to_str().unwrap().to_string(), None).unwrap();
println!("Parsing {}...", source.name());
let doc = parse(&parser, Rc::new(source), debug_opts)?;
// Compile
let compiler = Compiler::new(target, Some(&con));
let (mut compiled, postprocess) = compiler.compile(&*doc);
compiled.mtime = modified.duration_since(UNIX_EPOCH).unwrap().as_secs();
Ok((compiled, Some(postprocess)))
};
let (cdoc, post) = if force_rebuild {
parse_and_compile()?
} else {
match CompiledDocument::from_cache(&con, file.to_str().unwrap()) {
Some(compiled) => {
if compiled.mtime < modified.duration_since(UNIX_EPOCH).unwrap().as_secs() {
parse_and_compile()?
} else {
(compiled, None)
}
}
None => parse_and_compile()?,
}
};
compiled.push((RefCell::new(cdoc), post));
}
for (doc, postprocess) in &compiled {
if postprocess.is_none() {
continue;
}
// Post processing
let body = postprocess
.as_ref()
.unwrap()
.apply(target, &compiled, doc)?;
doc.borrow_mut().body = body;
// Insert into cache
doc.borrow().insert_cache(&con).map_err(|err| {
format!(
"Failed to insert compiled document from `{}` into cache: {err}",
doc.borrow().input
)
})?;
}
std::env::set_current_dir(current_dir)
.map_err(|err| format!("Failed to set current directory: {err}"))?;
Ok(compiled)
}
/// Processes sources from in-memory strings
/// This function is indented for testing
#[cfg(test)]
pub fn process_from_memory(target: Target, sources: Vec<String>) -> Result<Vec<(RefCell<CompiledDocument>, Option<PostProcess>)>, String> {
let mut compiled = vec![];
let parser = LangParser::default();
for (idx, content) in sources.iter().enumerate() {
let parse_and_compile = || -> Result<(CompiledDocument, Option<PostProcess>), String> {
// Parse
let source = SourceFile::with_content(format!("{idx}"), content.clone(), None);
let doc = parse(&parser, Rc::new(source), &vec![])?;
// Compile
let compiler = Compiler::new(target, None);
let (compiled, postprocess) = compiler.compile(&*doc);
Ok((compiled, Some(postprocess)))
};
let (cdoc, post) = parse_and_compile()?;
compiled.push((RefCell::new(cdoc), post));
}
for (doc, postprocess) in &compiled {
if postprocess.is_none() {
continue;
}
// Post processing
let body = postprocess
.as_ref()
.unwrap()
.apply(target, &compiled, doc)?;
doc.borrow_mut().body = body;
}
Ok(compiled)
}

View file

@ -4,13 +4,17 @@ use std::cell::RefMut;
use std::collections::hash_map::HashMap; use std::collections::hash_map::HashMap;
use std::rc::Rc; use std::rc::Rc;
use serde::Deserialize;
use serde::Serialize;
use crate::parser::source::Source; use crate::parser::source::Source;
use super::element::Element; use super::element::Element;
use super::element::ReferenceableElement; use super::element::ReferenceableElement;
use super::variable::Variable; use super::variable::Variable;
#[derive(Debug, Clone, Copy)] /// For references inside the current document
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ElemReference { pub enum ElemReference {
Direct(usize), Direct(usize),
@ -18,10 +22,30 @@ pub enum ElemReference {
Nested(usize, usize), Nested(usize, usize),
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CrossReference {
/// When the referenced document is unspecified
Unspecific(String),
/// When the referenced document is specified
Specific(String, String),
}
impl core::fmt::Display for CrossReference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self
{
CrossReference::Unspecific(name) => write!(f, "#{name}"),
CrossReference::Specific(doc_name, name) => write!(f, "{doc_name}#{name}"),
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Scope { pub struct Scope {
/// List of all referenceable elements in current scope. /// List of all referenceable elements in current scope.
/// All elements in this should return a non empty /// All elements in this should return a non empty element
/// when [`Element::as_referenceable`] is called
pub referenceable: HashMap<String, ElemReference>, pub referenceable: HashMap<String, ElemReference>,
pub variables: HashMap<String, Rc<dyn Variable>>, pub variables: HashMap<String, Rc<dyn Variable>>,
} }
@ -51,7 +75,7 @@ impl Scope {
// Variables // Variables
self.variables self.variables
.extend(other.variables.drain().map(|(name, var)| (name, var))); .extend(other.variables.drain());
} }
false => { false => {
// References // References
@ -141,15 +165,15 @@ pub trait Document<'a>: core::fmt::Debug {
fn get_variable(&self, name: &str) -> Option<Rc<dyn Variable>> { fn get_variable(&self, name: &str) -> Option<Rc<dyn Variable>> {
match self.scope().borrow().variables.get(name) { match self.scope().borrow().variables.get(name) {
Some(variable) => { Some(variable) => {
return Some(variable.clone()); Some(variable.clone())
} }
// Continue search recursively // Continue search recursively
None => match self.parent() { None => match self.parent() {
Some(parent) => return parent.get_variable(name), Some(parent) => parent.get_variable(name),
// Not found // Not found
None => return None, None => None,
}, },
} }
} }
@ -165,27 +189,23 @@ pub trait Document<'a>: core::fmt::Debug {
scope: &RefCell<Scope>, scope: &RefCell<Scope>,
merge_as: Option<&String>, merge_as: Option<&String>,
) { ) {
match merge_as { if let Some(merge_as) = merge_as { self.scope().borrow_mut().merge(
Some(merge_as) => self.scope().borrow_mut().merge( &mut scope.borrow_mut(),
&mut *scope.borrow_mut(), merge_as,
merge_as, self.content().borrow().len(),
self.content().borrow().len(), ) }
),
_ => {}
}
// Content // Content
self.content() self.content()
.borrow_mut() .borrow_mut()
.extend((content.borrow_mut()).drain(..).map(|value| value)); .extend((content.borrow_mut()).drain(..));
} }
fn get_reference(&self, refname: &str) -> Option<ElemReference> { fn get_reference(&self, refname: &str) -> Option<ElemReference> {
self.scope() self.scope()
.borrow() .borrow()
.referenceable .referenceable
.get(refname) .get(refname).copied()
.and_then(|reference| Some(*reference))
} }
fn get_from_reference( fn get_from_reference(

View file

@ -1,7 +1,7 @@
use std::str::FromStr; use std::str::FromStr;
use crate::compiler::compiler::Compiler; use crate::compiler::compiler::Compiler;
use crate::elements::reference::Reference; use crate::elements::reference::InternalReference;
use crate::parser::source::Token; use crate::parser::source::Token;
use downcast_rs::impl_downcast; use downcast_rs::impl_downcast;
use downcast_rs::Downcast; use downcast_rs::Downcast;
@ -50,7 +50,7 @@ pub trait Element: Downcast + core::fmt::Debug {
fn as_container(&self) -> Option<&dyn ContainerElement> { None } fn as_container(&self) -> Option<&dyn ContainerElement> { None }
/// Compiles element /// Compiles element
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String>; fn compile(&self, compiler: &Compiler, document: &dyn Document, cursor: usize) -> Result<String, String>;
} }
impl_downcast!(Element); impl_downcast!(Element);
@ -66,9 +66,13 @@ pub trait ReferenceableElement: Element {
&self, &self,
compiler: &Compiler, compiler: &Compiler,
document: &dyn Document, document: &dyn Document,
reference: &Reference, reference: &InternalReference,
refid: usize, refid: usize,
) -> Result<String, String>; ) -> Result<String, String>;
/// Gets the refid for a compiler. The refid is some key that can be used from an external
/// document to reference this element.
fn refid(&self, compiler: &Compiler, refid: usize) -> String;
} }
pub trait ContainerElement: Element { pub trait ContainerElement: Element {
@ -89,7 +93,7 @@ impl Element for DocumentEnd {
fn element_name(&self) -> &'static str { "Document End" } fn element_name(&self) -> &'static str { "Document End" }
fn compile(&self, _compiler: &Compiler, _document: &dyn Document) -> Result<String, String> { fn compile(&self, _compiler: &Compiler, _document: &dyn Document, _cursor: usize) -> Result<String, String> {
Ok(String::new()) Ok(String::new())
} }
} }

View file

@ -20,8 +20,8 @@ pub struct LangDocument<'a> {
impl<'a> LangDocument<'a> { impl<'a> LangDocument<'a> {
pub fn new(source: Rc<dyn Source>, parent: Option<&'a dyn Document<'a>>) -> Self { pub fn new(source: Rc<dyn Source>, parent: Option<&'a dyn Document<'a>>) -> Self {
Self { Self {
source: source, source,
parent: parent, parent,
content: RefCell::new(Vec::new()), content: RefCell::new(Vec::new()),
scope: RefCell::new(Scope::new()), scope: RefCell::new(Scope::new()),
} }
@ -32,7 +32,7 @@ impl<'a> Document<'a> for LangDocument<'a> {
fn source(&self) -> Rc<dyn Source> { self.source.clone() } fn source(&self) -> Rc<dyn Source> { self.source.clone() }
fn parent(&self) -> Option<&'a dyn Document<'a>> { fn parent(&self) -> Option<&'a dyn Document<'a>> {
self.parent.and_then(|p| Some(p as &dyn Document<'a>)) self.parent.map(|p| p as &dyn Document<'a>)
} }
fn content(&self) -> &RefCell<Vec<Box<dyn Element>>> { &self.content } fn content(&self) -> &RefCell<Vec<Box<dyn Element>>> { &self.content }

View file

@ -45,7 +45,7 @@ pub mod tests {
use crate::parser::langparser::LangParser; use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser; use crate::parser::parser::Parser;
use crate::parser::source::SourceFile; use crate::parser::source::SourceFile;
use crate::ParserState; use crate::parser::parser::ParserState;
#[test] #[test]
fn validate_refname_tests() { fn validate_refname_tests() {

View file

@ -92,13 +92,13 @@ impl Variable for PathVariable {
fn name(&self) -> &str { self.name.as_str() } fn name(&self) -> &str { self.name.as_str() }
fn from_string(&mut self, str: &str) -> Option<String> { fn from_string(&mut self, str: &str) -> Option<String> {
self.path = PathBuf::from(std::fs::canonicalize(str).unwrap()); self.path = std::fs::canonicalize(str).unwrap();
None None
} }
fn to_string(&self) -> String { self.path.to_str().unwrap().to_string() } fn to_string(&self) -> String { self.path.to_str().unwrap().to_string() }
fn parse<'a>(&self, state: &ParserState, location: Token, document: &'a dyn Document) { fn parse(&self, state: &ParserState, location: Token, document: &dyn Document) {
let source = Rc::new(VirtualSource::new( let source = Rc::new(VirtualSource::new(
location, location,
self.name().to_string(), self.name().to_string(),

522
src/elements/blockquote.rs Normal file
View file

@ -0,0 +1,522 @@
use core::fmt;
use std::any::Any;
use std::collections::HashMap;
use std::ops::Range;
use std::rc::Rc;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use blockquote_style::AuthorPos::After;
use blockquote_style::AuthorPos::Before;
use blockquote_style::BlockquoteStyle;
use regex::Match;
use regex::Regex;
use runtime_format::FormatArgs;
use runtime_format::FormatError;
use runtime_format::FormatKey;
use runtime_format::FormatKeyError;
use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target;
use crate::compiler::compiler::Target::HTML;
use crate::document::document::Document;
use crate::document::element::ContainerElement;
use crate::document::element::DocumentEnd;
use crate::document::element::ElemKind;
use crate::document::element::Element;
use crate::elements::paragraph::Paragraph;
use crate::elements::text::Text;
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::style::StyleHolder;
use crate::parser::util::process_escaped;
use crate::parser::util::Property;
use crate::parser::util::PropertyParser;
#[derive(Debug)]
pub struct Blockquote {
pub(self) location: Token,
pub(self) content: Vec<Box<dyn Element>>,
pub(self) author: Option<String>,
pub(self) cite: Option<String>,
pub(self) url: Option<String>,
/// Style of the blockquote
pub(self) style: Rc<blockquote_style::BlockquoteStyle>,
}
struct FmtPair<'a>(Target, &'a Blockquote);
impl FormatKey for FmtPair<'_> {
fn fmt(&self, key: &str, f: &mut fmt::Formatter<'_>) -> Result<(), FormatKeyError> {
match key {
"author" => write!(
f,
"{}",
Compiler::sanitize(self.0, self.1.author.as_ref().unwrap_or(&"".into()))
)
.map_err(FormatKeyError::Fmt),
"cite" => write!(
f,
"{}",
Compiler::sanitize(self.0, self.1.cite.as_ref().unwrap_or(&"".into()))
)
.map_err(FormatKeyError::Fmt),
_ => Err(FormatKeyError::UnknownKey),
}
}
}
impl Element for Blockquote {
fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Block }
fn element_name(&self) -> &'static str { "Blockquote" }
fn compile(
&self,
compiler: &Compiler,
document: &dyn Document,
cursor: usize,
) -> Result<String, String> {
match compiler.target() {
HTML => {
let mut result = r#"<div class="blockquote-content">"#.to_string();
let format_author = || -> Result<String, String> {
let mut result = String::new();
if self.cite.is_some() || self.author.is_some() {
result += r#"<p class="blockquote-author">"#;
let fmt_pair = FmtPair(compiler.target(), self);
let format_string = match (self.author.is_some(), self.cite.is_some()) {
(true, true) => {
Compiler::sanitize_format(fmt_pair.0, self.style.format[0].as_str())
}
(true, false) => {
Compiler::sanitize_format(fmt_pair.0, self.style.format[1].as_str())
}
(false, false) => {
Compiler::sanitize_format(fmt_pair.0, self.style.format[2].as_str())
}
_ => panic!(""),
};
let args = FormatArgs::new(format_string.as_str(), &fmt_pair);
args.status().map_err(|err| {
format!("Failed to format Blockquote style `{format_string}`: {err}")
})?;
result += args.to_string().as_str();
result += "</p>";
}
Ok(result)
};
if let Some(url) = &self.url {
result += format!(r#"<blockquote cite="{}">"#, Compiler::sanitize(HTML, url))
.as_str();
} else {
result += "<blockquote>";
}
if self.style.author_pos == Before {
result += format_author()?.as_str();
}
let mut in_paragraph = false;
for elem in &self.content {
if elem.downcast_ref::<DocumentEnd>().is_some() {
} else if elem.downcast_ref::<Blockquote>().is_some() {
if in_paragraph {
result += "</p>";
in_paragraph = false;
}
result += elem
.compile(compiler, document, cursor + result.len())?
.as_str();
} else {
if !in_paragraph {
result += "<p>";
in_paragraph = true;
}
result += elem
.compile(compiler, document, cursor + result.len())?
.as_str();
}
}
if in_paragraph {
result += "</p>";
}
result += "</blockquote>";
if self.style.author_pos == After {
result += format_author().map_err(|err| err.to_string())?.as_str();
}
result += "</div>";
Ok(result)
}
_ => todo!(""),
}
}
fn as_container(&self) -> Option<&dyn ContainerElement> { Some(self) }
}
impl ContainerElement for Blockquote {
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 blockquote".to_string());
}
self.content.push(elem);
Ok(())
}
}
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::blockquote")]
pub struct BlockquoteRule {
start_re: Regex,
continue_re: Regex,
properties: PropertyParser,
}
impl BlockquoteRule {
pub fn new() -> Self {
let mut props = HashMap::new();
props.insert(
"author".to_string(),
Property::new(false, "Quote author".to_string(), None),
);
props.insert(
"cite".to_string(),
Property::new(false, "Quote source".to_string(), None),
);
props.insert(
"url".to_string(),
Property::new(false, "Quote source url".to_string(), None),
);
Self {
start_re: Regex::new(r"(?:^|\n)>(?:\[((?:\\.|[^\\\\])*?)\])?\s*?(.*)").unwrap(),
continue_re: Regex::new(r"(?:^|\n)>\s*?(.*)").unwrap(),
properties: PropertyParser { properties: props },
}
}
fn parse_properties(
&self,
m: Match,
) -> Result<(Option<String>, Option<String>, Option<String>), String> {
let processed = process_escaped('\\', "]", m.as_str());
let pm = self.properties.parse(processed.as_str())?;
let author = pm
.get("author", |_, s| -> Result<String, ()> { Ok(s.to_string()) })
.map(|(_, s)| s)
.ok();
let cite = pm
.get("cite", |_, s| -> Result<String, ()> { Ok(s.to_string()) })
.map(|(_, s)| s)
.ok();
let url = pm
.get("url", |_, s| -> Result<String, ()> { Ok(s.to_string()) })
.map(|(_, s)| s)
.ok();
Ok((author, cite, url))
}
}
impl Rule for BlockquoteRule {
fn name(&self) -> &'static str { "Blockquote" }
fn previous(&self) -> Option<&'static str> { Some("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(|m| (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 content = cursor.source.content();
let mut end_cursor = cursor.clone();
if let Some(captures) = self.start_re.captures_at(content, end_cursor.pos) {
if captures.get(0).unwrap().start() != end_cursor.pos {
return (end_cursor, reports);
}
// Advance cursor
end_cursor = end_cursor.at(captures.get(0).unwrap().end());
// Properties
let mut author = None;
let mut cite = None;
let mut url = None;
if let Some(properties) = captures.get(1) {
match self.parse_properties(properties) {
Err(err) => {
reports.push(
Report::build(
ReportKind::Warning,
cursor.source.clone(),
properties.start(),
)
.with_message("Invalid Blockquote Properties")
.with_label(
Label::new((cursor.source.clone(), properties.range()))
.with_message(err)
.with_color(state.parser.colors().warning),
)
.finish(),
);
return (end_cursor, reports);
}
Ok(props) => (author, cite, url) = props,
}
}
// Content
let entry_start = captures.get(0).unwrap().start();
let mut entry_content = captures.get(2).unwrap().as_str().to_string();
while let Some(captures) = self.continue_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());
let trimmed = captures.get(1).unwrap().as_str().trim_start().trim_end();
entry_content += "\n";
entry_content += trimmed;
}
// 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(),
"Blockquote Entry".to_string(),
entry_content,
));
// Parse content
let parsed_doc = state.with_state(|new_state| {
new_state
.parser
.parse(new_state, entry_src, Some(document))
.0
});
// Extract paragraph and nested blockquotes
let mut parsed_content: Vec<Box<dyn Element>> = vec![];
for mut elem in parsed_doc.content().borrow_mut().drain(..) {
if let Some(paragraph) = elem.downcast_mut::<Paragraph>() {
if let Some(last) = parsed_content.last() {
if last.kind() == ElemKind::Inline {
parsed_content.push(Box::new(Text {
location: Token::new(
last.location().end()..last.location().end(),
last.location().source(),
),
content: " ".to_string(),
}) as Box<dyn Element>);
}
}
parsed_content.extend(std::mem::take(&mut paragraph.content));
} else if elem.downcast_ref::<Blockquote>().is_some() {
parsed_content.push(elem);
} else {
reports.push(
Report::build(ReportKind::Error, token.source(), token.range.start)
.with_message("Unable to Parse Blockquote Entry")
.with_label(
Label::new((token.source(), token.range.clone()))
.with_message("Blockquotes may only contain paragraphs and other blockquotes")
.with_color(state.parser.colors().error),
)
.finish(),
);
return (end_cursor, reports);
}
}
// Get style
let style = state
.shared
.styles
.borrow()
.current(blockquote_style::STYLE_KEY)
.downcast_rc::<BlockquoteStyle>()
.unwrap();
state.push(
document,
Box::new(Blockquote {
location: Token::new(entry_start..end_cursor.pos, end_cursor.source.clone()),
content: parsed_content,
author,
cite,
url,
style,
}),
);
}
(end_cursor, reports)
}
fn register_styles(&self, holder: &mut StyleHolder) {
holder.set_current(Rc::new(BlockquoteStyle::default()));
}
}
mod blockquote_style {
use serde::Deserialize;
use serde::Serialize;
use crate::impl_elementstyle;
pub static STYLE_KEY: &str = "style.blockquote";
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub enum AuthorPos {
Before,
After,
None,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BlockquoteStyle {
pub author_pos: AuthorPos,
pub format: [String; 3],
}
impl Default for BlockquoteStyle {
fn default() -> Self {
Self {
author_pos: AuthorPos::After,
format: [
"{author}, {cite}".into(),
"{author}".into(),
"{cite}".into(),
],
}
}
}
impl_elementstyle!(BlockquoteStyle, STYLE_KEY);
}
#[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]
pub fn parser() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
BEFORE
>[author=A, cite=B, url=C] Some entry
> contin**ued here
> **
AFTER
> Another
>
> quote
>>[author=B] Nested
>>> More nested
END
"#
.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 == "BEFORE" }; };
Blockquote {
author == Some("A".to_string()),
cite == Some("B".to_string()),
url == Some("C".to_string())
} {
Text { content == "Some entry contin" };
Style;
Text { content == "ued here" };
Style;
};
Paragraph { Text{ content == "AFTER" }; };
Blockquote {
Text { content == "Another" };
Text { content == " " };
Text { content == "quote" };
Blockquote { author == Some("B".to_string()) } {
Text { content == "Nested" };
Blockquote {
Text { content == "More nested" };
};
};
};
Paragraph { Text{ content == "END" }; };
);
}
#[test]
pub fn style() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
@@style.blockquote = {
"author_pos": "Before",
"format": ["{cite} by {author}", "Author: {author}", "From: {cite}"]
}
PRE
>[author=A, cite=B, url=C] Some entry
> contin**ued here
> **
AFTER
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (_, state) = parser.parse(ParserState::new(&parser, None), source, None);
let style = state
.shared
.styles
.borrow()
.current(blockquote_style::STYLE_KEY)
.downcast_rc::<BlockquoteStyle>()
.unwrap();
assert_eq!(style.author_pos, Before);
assert_eq!(
style.format,
[
"{cite} by {author}".to_string(),
"Author: {author}".to_string(),
"From: {cite}".to_string()
]
);
}
}

View file

@ -123,8 +123,8 @@ impl Code {
} }
result += result +=
format!("<div class=\"code-block-content\"><table cellspacing=\"0\">").as_str(); "<div class=\"code-block-content\"><table cellspacing=\"0\">".to_string().as_str();
for (line_id, line) in self.code.split(|c| c == '\n').enumerate() { for (line_id, line) in self.code.split('\n').enumerate() {
result += "<tr><td class=\"code-block-gutter\">"; result += "<tr><td class=\"code-block-gutter\">";
// Line number // Line number
@ -137,7 +137,7 @@ impl Code {
Err(e) => { Err(e) => {
return Err(format!( return Err(format!(
"Error highlighting line `{line}`: {}", "Error highlighting line `{line}`: {}",
e.to_string() e
)) ))
} }
Ok(regions) => { Ok(regions) => {
@ -146,7 +146,7 @@ impl Code {
syntect::html::IncludeBackground::No, syntect::html::IncludeBackground::No,
) { ) {
Err(e) => { Err(e) => {
return Err(format!("Error highlighting code: {}", e.to_string())) return Err(format!("Error highlighting code: {}", e))
} }
Ok(highlighted) => { Ok(highlighted) => {
result += if highlighted.is_empty() { result += if highlighted.is_empty() {
@ -165,14 +165,14 @@ impl Code {
} else if self.block == CodeKind::MiniBlock { } else if self.block == CodeKind::MiniBlock {
result += "<div class=\"code-block\"><div class=\"code-block-content\"><table cellspacing=\"0\">"; result += "<div class=\"code-block\"><div class=\"code-block-content\"><table cellspacing=\"0\">";
for line in self.code.split(|c| c == '\n') { for line in self.code.split('\n') {
result += "<tr><td class=\"code-block-line\"><pre>"; result += "<tr><td class=\"code-block-line\"><pre>";
// Code // Code
match h.highlight_line(line, Code::get_syntaxes()) { match h.highlight_line(line, Code::get_syntaxes()) {
Err(e) => { Err(e) => {
return Err(format!( return Err(format!(
"Error highlighting line `{line}`: {}", "Error highlighting line `{line}`: {}",
e.to_string() e
)) ))
} }
Ok(regions) => { Ok(regions) => {
@ -181,7 +181,7 @@ impl Code {
syntect::html::IncludeBackground::No, syntect::html::IncludeBackground::No,
) { ) {
Err(e) => { Err(e) => {
return Err(format!("Error highlighting code: {}", e.to_string())) return Err(format!("Error highlighting code: {}", e))
} }
Ok(highlighted) => { Ok(highlighted) => {
result += if highlighted.is_empty() { result += if highlighted.is_empty() {
@ -203,7 +203,7 @@ impl Code {
return Err(format!( return Err(format!(
"Error highlighting line `{}`: {}", "Error highlighting line `{}`: {}",
self.code, self.code,
e.to_string() e
)) ))
} }
Ok(regions) => { Ok(regions) => {
@ -212,7 +212,7 @@ impl Code {
syntect::html::IncludeBackground::No, syntect::html::IncludeBackground::No,
) { ) {
Err(e) => { Err(e) => {
return Err(format!("Error highlighting code: {}", e.to_string())) return Err(format!("Error highlighting code: {}", e))
} }
Ok(highlighted) => result += highlighted.as_str(), Ok(highlighted) => result += highlighted.as_str(),
} }
@ -244,11 +244,10 @@ impl Cached for Code {
fn key(&self) -> <Self as Cached>::Key { fn key(&self) -> <Self as Cached>::Key {
let mut hasher = Sha512::new(); let mut hasher = Sha512::new();
hasher.input((self.block as usize).to_be_bytes().as_slice()); hasher.input((self.block as usize).to_be_bytes().as_slice());
hasher.input((self.line_offset as usize).to_be_bytes().as_slice()); hasher.input(self.line_offset.to_be_bytes().as_slice());
self.theme if let Some(theme) = self.theme
.as_ref() .as_ref() { hasher.input(theme.as_bytes()) }
.map(|theme| hasher.input(theme.as_bytes())); if let Some(name) = self.name.as_ref() { hasher.input(name.as_bytes()) }
self.name.as_ref().map(|name| hasher.input(name.as_bytes()));
hasher.input(self.language.as_bytes()); hasher.input(self.language.as_bytes());
hasher.input(self.code.as_bytes()); hasher.input(self.code.as_bytes());
@ -263,20 +262,20 @@ impl Element for Code {
fn element_name(&self) -> &'static str { "Code Block" } fn element_name(&self) -> &'static str { "Code Block" }
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, _document: &dyn Document, _cursor: usize) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
static CACHE_INIT: Once = Once::new(); static CACHE_INIT: Once = Once::new();
CACHE_INIT.call_once(|| { CACHE_INIT.call_once(|| {
if let Some(mut con) = compiler.cache() { if let Some(con) = compiler.cache() {
if let Err(e) = Code::init(&mut con) { if let Err(e) = Code::init(con) {
eprintln!("Unable to create cache table: {e}"); eprintln!("Unable to create cache table: {e}");
} }
} }
}); });
if let Some(mut con) = compiler.cache() { if let Some(con) = compiler.cache() {
match self.cached(&mut con, |s| s.highlight_html(compiler)) { match self.cached(con, |s| s.highlight_html(compiler)) {
Ok(s) => Ok(s), Ok(s) => Ok(s),
Err(e) => match e { Err(e) => match e {
CachedError::SqlErr(e) => { CachedError::SqlErr(e) => {
@ -296,6 +295,7 @@ impl Element for Code {
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::code")]
pub struct CodeRule { pub struct CodeRule {
re: [Regex; 2], re: [Regex; 2],
properties: PropertyParser, properties: PropertyParser,
@ -330,14 +330,15 @@ impl CodeRule {
impl RegexRule for CodeRule { impl RegexRule for CodeRule {
fn name(&self) -> &'static str { "Code" } fn name(&self) -> &'static str { "Code" }
fn previous(&self) -> Option<&'static str> { Some("Blockquote") }
fn regexes(&self) -> &[regex::Regex] { &self.re } fn regexes(&self) -> &[regex::Regex] { &self.re }
fn on_regex_match<'a>( fn on_regex_match(
&self, &self,
index: usize, index: usize,
state: &ParserState, state: &ParserState,
document: &'a dyn Document, document: &dyn Document,
token: Token, token: Token,
matches: Captures, matches: Captures,
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> { ) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
@ -430,7 +431,7 @@ impl RegexRule for CodeRule {
} else { } else {
util::process_escaped('\\', "``", matches.get(3).unwrap().as_str()) util::process_escaped('\\', "``", matches.get(3).unwrap().as_str())
}; };
if code_content.bytes().last() == Some('\n' as u8) if code_content.bytes().last() == Some(b'\n')
// Remove newline // Remove newline
{ {
code_content.pop(); code_content.pop();
@ -451,8 +452,7 @@ impl RegexRule for CodeRule {
} }
let theme = document let theme = document
.get_variable("code.theme") .get_variable("code.theme").map(|var| var.to_string());
.and_then(|var| Some(var.to_string()));
if index == 0 if index == 0
// Block // Block
@ -551,8 +551,7 @@ impl RegexRule for CodeRule {
ctx.as_ref().map(|ctx| { ctx.as_ref().map(|ctx| {
let theme = ctx let theme = ctx
.document .document
.get_variable("code.theme") .get_variable("code.theme").map(|var| var.to_string());
.and_then(|var| Some(var.to_string()));
ctx.state.push( ctx.state.push(
ctx.document, ctx.document,
@ -582,8 +581,7 @@ impl RegexRule for CodeRule {
ctx.as_ref().map(|ctx| { ctx.as_ref().map(|ctx| {
let theme = ctx let theme = ctx
.document .document
.get_variable("code.theme") .get_variable("code.theme").map(|var| var.to_string());
.and_then(|var| Some(var.to_string()));
ctx.state.push( ctx.state.push(
ctx.document, ctx.document,
@ -620,8 +618,7 @@ impl RegexRule for CodeRule {
ctx.as_ref().map(|ctx| { ctx.as_ref().map(|ctx| {
let theme = ctx let theme = ctx
.document .document
.get_variable("code.theme") .get_variable("code.theme").map(|var| var.to_string());
.and_then(|var| Some(var.to_string()));
ctx.state.push( ctx.state.push(
ctx.document, ctx.document,

View file

@ -16,28 +16,21 @@ use std::rc::Rc;
#[derive(Debug)] #[derive(Debug)]
pub struct Comment { pub struct Comment {
location: Token, pub location: Token,
content: String, #[allow(unused)]
} pub content: String,
impl Comment {
pub fn new(location: Token, content: String) -> Self {
Self {
location: location,
content,
}
}
} }
impl Element for Comment { impl Element for Comment {
fn location(&self) -> &Token { &self.location } fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Invisible } fn kind(&self) -> ElemKind { ElemKind::Invisible }
fn element_name(&self) -> &'static str { "Comment" } fn element_name(&self) -> &'static str { "Comment" }
fn compile(&self, _compiler: &Compiler, _document: &dyn Document) -> Result<String, String> { fn compile(&self, _compiler: &Compiler, _document: &dyn Document, _cursor: usize) -> Result<String, String> {
Ok("".to_string()) Ok("".to_string())
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::comment")]
pub struct CommentRule { pub struct CommentRule {
re: [Regex; 1], re: [Regex; 1],
} }
@ -53,13 +46,15 @@ impl CommentRule {
impl RegexRule for CommentRule { impl RegexRule for CommentRule {
fn name(&self) -> &'static str { "Comment" } fn name(&self) -> &'static str { "Comment" }
fn previous(&self) -> Option<&'static str> { None }
fn regexes(&self) -> &[Regex] { &self.re } fn regexes(&self) -> &[Regex] { &self.re }
fn on_regex_match<'a>( fn on_regex_match(
&self, &self,
_: usize, _: usize,
state: &ParserState, state: &ParserState,
document: &'a dyn Document, document: &dyn Document,
token: Token, token: Token,
matches: Captures, matches: Captures,
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> { ) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
@ -94,7 +89,7 @@ impl RegexRule for CommentRule {
}), }),
); );
return reports; reports
} }
} }

View file

@ -140,13 +140,10 @@ impl RuleState for CustomStyleState {
let paragraph = document.last_element::<Paragraph>().unwrap(); let paragraph = document.last_element::<Paragraph>().unwrap();
let paragraph_end = paragraph let paragraph_end = paragraph
.content .content
.last() .last().map(|last| (
.and_then(|last| {
Some((
last.location().source(), last.location().source(),
last.location().end() - 1..last.location().end(), last.location().end() - 1..last.location().end(),
)) ))
})
.unwrap(); .unwrap();
reports.push( reports.push(
@ -164,7 +161,7 @@ impl RuleState for CustomStyleState {
.with_label( .with_label(
Label::new(paragraph_end) Label::new(paragraph_end)
.with_order(1) .with_order(1)
.with_message(format!("Paragraph ends here")) .with_message("Paragraph ends here".to_string())
.with_color(state.parser.colors().error), .with_color(state.parser.colors().error),
) )
.with_note("Styles cannot span multiple documents (i.e @import)") .with_note("Styles cannot span multiple documents (i.e @import)")
@ -172,16 +169,22 @@ impl RuleState for CustomStyleState {
); );
}); });
return reports; reports
} }
} }
static STATE_NAME: &'static str = "elements.custom_style"; static STATE_NAME: &str = "elements.custom_style";
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::customstyle")]
pub struct CustomStyleRule; pub struct CustomStyleRule;
impl CustomStyleRule {
pub fn new() -> Self { Self{} }
}
impl Rule for CustomStyleRule { impl Rule for CustomStyleRule {
fn name(&self) -> &'static str { "Custom Style" } fn name(&self) -> &'static str { "Custom Style" }
fn previous(&self) -> Option<&'static str> { Some("Style") }
fn next_match(&self, state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> { fn next_match(&self, state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> {
let content = cursor.source.content(); let content = cursor.source.content();

View file

@ -21,6 +21,7 @@ use crate::parser::rule::Rule;
use crate::parser::source::Cursor; use crate::parser::source::Cursor;
use crate::parser::source::Source; use crate::parser::source::Source;
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::elemstyle")]
pub struct ElemStyleRule { pub struct ElemStyleRule {
start_re: Regex, start_re: Regex,
} }
@ -58,13 +59,11 @@ impl ElemStyleRule {
impl Rule for ElemStyleRule { impl Rule for ElemStyleRule {
fn name(&self) -> &'static str { "Element Style" } fn name(&self) -> &'static str { "Element Style" }
fn previous(&self) -> Option<&'static str> { Some("Script") }
fn next_match(&self, _state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> { fn next_match(&self, _state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> {
self.start_re self.start_re
.find_at(cursor.source.content(), cursor.pos) .find_at(cursor.source.content(), cursor.pos).map(|m| (m.start(), Box::new([false; 0]) as Box<dyn Any>))
.map_or(None, |m| {
Some((m.start(), Box::new([false; 0]) as Box<dyn Any>))
})
} }
fn on_match<'a>( fn on_match<'a>(
@ -91,7 +90,7 @@ impl Rule for ElemStyleRule {
.with_message("Empty Style Key") .with_message("Empty Style Key")
.with_label( .with_label(
Label::new((cursor.source.clone(), key.range())) Label::new((cursor.source.clone(), key.range()))
.with_message(format!("Expected a non-empty style key",)) .with_message("Expected a non-empty style key".to_string())
.with_color(state.parser.colors().error), .with_color(state.parser.colors().error),
) )
.finish(), .finish(),
@ -133,9 +132,7 @@ impl Rule for ElemStyleRule {
.with_message("Invalid Style Value") .with_message("Invalid Style Value")
.with_label( .with_label(
Label::new((cursor.source.clone(), matches.get(0).unwrap().range())) Label::new((cursor.source.clone(), matches.get(0).unwrap().range()))
.with_message(format!( .with_message("Unable to parse json string after style key".to_string())
"Unable to parse json string after style key",
))
.with_color(state.parser.colors().error), .with_color(state.parser.colors().error),
) )
.finish(), .finish(),

View file

@ -1,8 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use std::sync::Once; use std::sync::Once;
use crate::lua::kernel::CTX;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::util::Property; use crate::parser::util::Property;
use crate::parser::util::PropertyMapError; use crate::parser::util::PropertyMapError;
@ -16,6 +18,9 @@ use crypto::sha2::Sha512;
use graphviz_rust::cmd::Format; use graphviz_rust::cmd::Format;
use graphviz_rust::cmd::Layout; use graphviz_rust::cmd::Layout;
use graphviz_rust::exec_dot; use graphviz_rust::exec_dot;
use mlua::Error::BadArgument;
use mlua::Function;
use mlua::Lua;
use regex::Captures; use regex::Captures;
use regex::Regex; use regex::Regex;
@ -111,21 +116,26 @@ impl Element for Graphviz {
fn element_name(&self) -> &'static str { "Graphviz" } fn element_name(&self) -> &'static str { "Graphviz" }
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> { fn compile(
&self,
compiler: &Compiler,
_document: &dyn Document,
_cursor: usize,
) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
static CACHE_INIT: Once = Once::new(); static CACHE_INIT: Once = Once::new();
CACHE_INIT.call_once(|| { CACHE_INIT.call_once(|| {
if let Some(mut con) = compiler.cache() { if let Some(con) = compiler.cache() {
if let Err(e) = Graphviz::init(&mut con) { if let Err(e) = Graphviz::init(con) {
eprintln!("Unable to create cache table: {e}"); eprintln!("Unable to create cache table: {e}");
} }
} }
}); });
// TODO: Format svg in a div // TODO: Format svg in a div
if let Some(mut con) = compiler.cache() { if let Some(con) = compiler.cache() {
match self.cached(&mut con, |s| s.dot_to_svg()) { match self.cached(con, |s| s.dot_to_svg()) {
Ok(s) => Ok(s), Ok(s) => Ok(s),
Err(e) => match e { Err(e) => match e {
CachedError::SqlErr(e) => { CachedError::SqlErr(e) => {
@ -146,6 +156,7 @@ impl Element for Graphviz {
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::graphviz")]
pub struct GraphRule { pub struct GraphRule {
re: [Regex; 1], re: [Regex; 1],
properties: PropertyParser, properties: PropertyParser,
@ -177,7 +188,8 @@ impl GraphRule {
} }
impl RegexRule for GraphRule { impl RegexRule for GraphRule {
fn name(&self) -> &'static str { "Graph" } fn name(&self) -> &'static str { "Graphviz" }
fn previous(&self) -> Option<&'static str> { Some("Tex") }
fn regexes(&self) -> &[regex::Regex] { &self.re } fn regexes(&self) -> &[regex::Regex] { &self.re }
@ -358,4 +370,102 @@ impl RegexRule for GraphRule {
reports 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(|_, (layout, width, dot): (String, String, String)| {
let mut result = Ok(());
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
let layout = match layout_from_str(layout.as_str()) {
Err(err) => {
result = Err(BadArgument {
to: Some("push".to_string()),
pos: 1,
name: Some("layout".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Unable to get layout type: {err}"
))),
});
return;
}
Ok(layout) => layout,
};
ctx.state.push(
ctx.document,
Box::new(Graphviz {
location: ctx.location.clone(),
dot,
layout,
width,
}),
);
})
});
result
})
.unwrap(),
));
bindings
}
}
#[cfg(test)]
mod tests {
use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser;
use crate::parser::source::SourceFile;
use crate::validate_document;
use super::*;
#[test]
pub fn parse() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
[graph][width=200px, layout=neato]
Some graph...
[/graph]
[graph]
Another graph
[/graph]
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Graphviz { width == "200px", dot == "Some graph..." };
Graphviz { dot == "Another graph" };
);
}
#[test]
pub fn lua() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
%<nml.graphviz.push("neato", "200px", "Some graph...")>%
%<nml.graphviz.push("dot", "", "Another graph")>%
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Graphviz { width == "200px", dot == "Some graph..." };
Graphviz { dot == "Another graph" };
);
}
} }

View file

@ -17,6 +17,7 @@ use std::rc::Rc;
use super::paragraph::Paragraph; use super::paragraph::Paragraph;
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::import")]
pub struct ImportRule { pub struct ImportRule {
re: [Regex; 1], re: [Regex; 1],
} }
@ -40,6 +41,7 @@ impl ImportRule {
impl RegexRule for ImportRule { impl RegexRule for ImportRule {
fn name(&self) -> &'static str { "Import" } fn name(&self) -> &'static str { "Import" }
fn previous(&self) -> Option<&'static str> { Some("Paragraph") }
fn regexes(&self) -> &[Regex] { &self.re } fn regexes(&self) -> &[Regex] { &self.re }
@ -177,6 +179,6 @@ impl RegexRule for ImportRule {
); );
} }
return result; result
} }
} }

View file

@ -232,7 +232,7 @@ impl Element for Layout {
fn location(&self) -> &Token { &self.location } fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Block } fn kind(&self) -> ElemKind { ElemKind::Block }
fn element_name(&self) -> &'static str { "Layout" } fn element_name(&self) -> &'static str { "Layout" }
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, document: &dyn Document, _cursor: usize) -> Result<String, String> {
self.layout self.layout
.compile(self.token, self.id, &self.properties, compiler, document) .compile(self.token, self.id, &self.properties, compiler, document)
} }
@ -280,10 +280,11 @@ impl RuleState for LayoutState {
); );
} }
return reports; reports
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::layout")]
pub struct LayoutRule { pub struct LayoutRule {
re: [Regex; 3], re: [Regex; 3],
} }
@ -374,10 +375,11 @@ impl LayoutRule {
} }
} }
static STATE_NAME: &'static str = "elements.layout"; static STATE_NAME: &str = "elements.layout";
impl RegexRule for LayoutRule { impl RegexRule for LayoutRule {
fn name(&self) -> &'static str { "Layout" } fn name(&self) -> &'static str { "Layout" }
fn previous(&self) -> Option<&'static str> { Some("Media") }
fn regexes(&self) -> &[regex::Regex] { &self.re } fn regexes(&self) -> &[regex::Regex] { &self.re }
@ -647,7 +649,7 @@ impl RegexRule for LayoutRule {
}), }),
); );
return reports; reports
} }
// TODO: Add method to create new layouts // TODO: Add method to create new layouts
@ -744,7 +746,7 @@ impl RegexRule for LayoutRule {
to: Some("push".to_string()), to: Some("push".to_string()),
pos: 1, pos: 1,
name: Some("token".to_string()), name: Some("token".to_string()),
cause: Arc::new(mlua::Error::external(format!("Unable set next layout: No active layout found"))), cause: Arc::new(mlua::Error::external("Unable set next layout: No active layout found".to_string())),
}); });
return; return;
} }
@ -792,7 +794,7 @@ impl RegexRule for LayoutRule {
to: Some("push".to_string()), to: Some("push".to_string()),
pos: 1, pos: 1,
name: Some("token".to_string()), name: Some("token".to_string()),
cause: Arc::new(mlua::Error::external(format!("Unable set layout end: No active layout found"))), cause: Arc::new(mlua::Error::external("Unable set layout end: No active layout found".to_string())),
}); });
return; return;
} }

View file

@ -37,7 +37,7 @@ impl Element for Link {
fn location(&self) -> &Token { &self.location } fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Inline } fn kind(&self) -> ElemKind { ElemKind::Inline }
fn element_name(&self) -> &'static str { "Link" } fn element_name(&self) -> &'static str { "Link" }
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, document: &dyn Document, cursor: usize) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
let mut result = format!( let mut result = format!(
@ -46,7 +46,7 @@ impl Element for Link {
); );
for elem in &self.display { for elem in &self.display {
result += elem.compile(compiler, document)?.as_str(); result += elem.compile(compiler, document, cursor+result.len())?.as_str();
} }
result += "</a>"; result += "</a>";
@ -71,6 +71,7 @@ impl ContainerElement for Link {
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::link")]
pub struct LinkRule { pub struct LinkRule {
re: [Regex; 1], re: [Regex; 1],
} }
@ -85,6 +86,7 @@ impl LinkRule {
impl RegexRule for LinkRule { impl RegexRule for LinkRule {
fn name(&self) -> &'static str { "Link" } fn name(&self) -> &'static str { "Link" }
fn previous(&self) -> Option<&'static str> { Some("Link") }
fn regexes(&self) -> &[Regex] { &self.re } fn regexes(&self) -> &[Regex] { &self.re }
@ -151,7 +153,7 @@ impl RegexRule for LinkRule {
); );
return reports; return reports;
} }
Ok(mut paragraph) => std::mem::replace(&mut paragraph.content, vec![]), Ok(mut paragraph) => std::mem::take(&mut paragraph.content),
} }
} }
_ => panic!("Empty link name"), _ => panic!("Empty link name"),
@ -174,7 +176,7 @@ impl RegexRule for LinkRule {
} }
let text_content = util::process_text(document, url.as_str()); let text_content = util::process_text(document, url.as_str());
if text_content.as_str().is_empty() { if text_content.is_empty() {
reports.push( reports.push(
Report::build(ReportKind::Error, token.source(), url.start()) Report::build(ReportKind::Error, token.source(), url.start())
.with_message("Empty link url") .with_message("Empty link url")
@ -205,7 +207,7 @@ impl RegexRule for LinkRule {
}), }),
); );
return reports; reports
} }
fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> { fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> {
@ -236,7 +238,7 @@ impl RegexRule for LinkRule {
return; return;
} }
Ok(mut paragraph) => { Ok(mut paragraph) => {
std::mem::replace(&mut paragraph.content, vec![]) std::mem::take(&mut paragraph.content)
} }
}; };

View file

@ -48,7 +48,7 @@ impl Element for ListMarker {
fn element_name(&self) -> &'static str { "List Marker" } fn element_name(&self) -> &'static str { "List Marker" }
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, _document: &dyn Document, _cursor: usize) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => match (self.kind, self.numbered) { Target::HTML => match (self.kind, self.numbered) {
(MarkerKind::Close, true) => Ok("</ol>".to_string()), (MarkerKind::Close, true) => Ok("</ol>".to_string()),
@ -76,12 +76,21 @@ impl Element for ListEntry {
fn element_name(&self) -> &'static str { "List Entry" } fn element_name(&self) -> &'static str { "List Entry" }
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, document: &dyn Document, cursor: usize) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
let mut result = "<li>".to_string(); let mut result = String::new();
if let Some((numbered, number)) = self.numbering.last()
{
if *numbered {
result += format!("<li value=\"{number}\">").as_str();
}
else {
result += "<li>";
}
}
for elem in &self.content { for elem in &self.content {
result += elem.compile(compiler, document)?.as_str(); result += elem.compile(compiler, document, cursor+result.len())?.as_str();
} }
result += "</li>"; result += "</li>";
Ok(result) Ok(result)
@ -106,6 +115,7 @@ impl ContainerElement for ListEntry {
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::list")]
pub struct ListRule { pub struct ListRule {
start_re: Regex, start_re: Regex,
continue_re: Regex, continue_re: Regex,
@ -197,14 +207,13 @@ impl ListRule {
fn parse_depth(depth: &str, document: &dyn Document, offset: usize) -> Vec<(bool, usize)> { fn parse_depth(depth: &str, document: &dyn Document, offset: usize) -> Vec<(bool, usize)> {
let mut parsed = vec![]; let mut parsed = vec![];
// FIXME: Previous iteration used to recursively retrieve the list indent
let prev_entry = document let prev_entry = document
.last_element::<ListEntry>() .last_element::<ListEntry>()
.and_then(|entry| Ref::filter_map(entry, |e| Some(&e.numbering)).ok()); .and_then(|entry| Ref::filter_map(entry, |e| Some(&e.numbering)).ok());
let mut continue_match = true; let mut continue_match = true;
depth.chars().enumerate().for_each(|(idx, c)| { depth.chars().enumerate().for_each(|(idx, c)| {
let number = if offset == 0 { let number = if offset == usize::MAX {
prev_entry prev_entry
.as_ref() .as_ref()
.and_then(|v| { .and_then(|v| {
@ -243,19 +252,17 @@ impl ListRule {
} }
}); });
return parsed; parsed
} }
} }
impl Rule for ListRule { impl Rule for ListRule {
fn name(&self) -> &'static str { "List" } fn name(&self) -> &'static str { "List" }
fn previous(&self) -> Option<&'static str> { Some("Raw") }
fn next_match(&self, _state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> { fn next_match(&self, _state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> {
self.start_re self.start_re
.find_at(cursor.source.content(), cursor.pos) .find_at(cursor.source.content(), cursor.pos).map(|m| (m.start(), Box::new([false; 0]) as Box<dyn Any>))
.map_or(None, |m| {
Some((m.start(), Box::new([false; 0]) as Box<dyn Any>))
})
} }
fn on_match<'a>( fn on_match<'a>(
@ -313,7 +320,7 @@ impl Rule for ListRule {
let depth = ListRule::parse_depth( let depth = ListRule::parse_depth(
captures.get(1).unwrap().as_str(), captures.get(1).unwrap().as_str(),
document, document,
offset.unwrap_or(0), offset.unwrap_or(usize::MAX),
); );
// Content // Content
@ -327,7 +334,7 @@ impl Rule for ListRule {
.get(2) .get(2)
.unwrap() .unwrap()
.as_str() .as_str()
.find(|c| c == '*' || c == '-') .find(['*', '-'])
== Some(0) == Some(0)
{ {
break; break;
@ -391,7 +398,7 @@ impl Rule for ListRule {
); );
break; break;
} }
Ok(mut paragraph) => std::mem::replace(&mut paragraph.content, vec![]), Ok(mut paragraph) => std::mem::take(&mut paragraph.content),
}; };
if let Some(previous_depth) = document if let Some(previous_depth) = document

View file

@ -35,7 +35,7 @@ use crate::parser::util::PropertyMapError;
use crate::parser::util::PropertyParser; use crate::parser::util::PropertyParser;
use super::paragraph::Paragraph; use super::paragraph::Paragraph;
use super::reference::Reference; use super::reference::InternalReference;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum MediaType { pub enum MediaType {
@ -72,17 +72,14 @@ impl Element for Media {
fn as_container(&self) -> Option<&dyn ContainerElement> { Some(self) } fn as_container(&self) -> Option<&dyn ContainerElement> { Some(self) }
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, document: &dyn Document, cursor: usize) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
let mut result = String::new(); let mut result = String::new();
result.push_str("<div class=\"media\">"); result.push_str("<div class=\"media\">");
for medium in &self.media { for medium in &self.media {
match medium.compile(compiler, document) { result += medium.compile(compiler, document, cursor+result.len())?.as_str();
Ok(r) => result.push_str(r.as_str()),
Err(e) => return Err(e),
}
} }
result.push_str("</div>"); result.push_str("</div>");
@ -135,21 +132,23 @@ impl Element for Medium {
fn as_referenceable(&self) -> Option<&dyn ReferenceableElement> { Some(self) } fn as_referenceable(&self) -> Option<&dyn ReferenceableElement> { Some(self) }
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, document: &dyn Document, cursor: usize) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
let mut result = String::new(); let mut result = String::new();
// Reference
let elemref = document.get_reference(self.reference.as_str()).unwrap();
let refcount = compiler.reference_id(document, elemref);
let width = self let width = self
.width .width
.as_ref() .as_ref()
.map_or(String::new(), |w| format!(r#" style="width:{w};""#)); .map_or(String::new(), |w| format!(r#" style="width:{w};""#));
result.push_str(format!(r#"<div class="medium"{width}>"#).as_str()); result.push_str(format!(r#"<div id="{}" class="medium"{width}>"#, self.refid(compiler, refcount)).as_str());
result += match self.media_type { result += match self.media_type {
MediaType::IMAGE => format!(r#"<a href="{0}"><img src="{0}"></a>"#, self.uri), MediaType::IMAGE => format!(r#"<a href="{0}"><img src="{0}"></a>"#, self.uri),
MediaType::VIDEO => format!( MediaType::VIDEO => format!(r#"<video controls{width}><source src="{0}"></video>"#, self.uri
r#"<video controls{width}><source src="{0}"></video>"#,
self.uri
), ),
MediaType::AUDIO => { MediaType::AUDIO => {
format!(r#"<audio controls src="{0}"{width}></audio>"#, self.uri) format!(r#"<audio controls src="{0}"{width}></audio>"#, self.uri)
@ -159,26 +158,17 @@ impl Element for Medium {
let caption = self let caption = self
.caption .caption
.as_ref() .as_ref().map(|cap| format!(
.and_then(|cap| {
Some(format!(
" {}", " {}",
Compiler::sanitize(compiler.target(), cap.as_str()) Compiler::sanitize(compiler.target(), cap.as_str())
)) ))
}) .unwrap_or_default();
.unwrap_or(String::new());
// Reference
let elemref = document.get_reference(self.reference.as_str()).unwrap();
let refcount = compiler.reference_id(document, elemref);
result.push_str( result.push_str(
format!(r#"<p class="medium-refname">({refcount}){caption}</p>"#).as_str(), format!(r#"<p class="medium-refname">({refcount}){caption}</p>"#).as_str(),
); );
if let Some(paragraph) = self.description.as_ref() { if let Some(paragraph) = self.description.as_ref() {
match paragraph.compile(compiler, document) { result += paragraph.compile(compiler, document, cursor+result.len())?.as_str();
Ok(res) => result.push_str(res.as_str()),
Err(err) => return Err(err),
}
} }
result.push_str("</div>"); result.push_str("</div>");
@ -198,7 +188,7 @@ impl ReferenceableElement for Medium {
&self, &self,
compiler: &Compiler, compiler: &Compiler,
_document: &dyn Document, _document: &dyn Document,
reference: &Reference, reference: &InternalReference,
refid: usize, refid: usize,
) -> Result<String, String> { ) -> Result<String, String> {
match compiler.target() { match compiler.target() {
@ -210,7 +200,11 @@ impl ReferenceableElement for Medium {
// TODO Handle other kind of media // TODO Handle other kind of media
match self.media_type { match self.media_type {
MediaType::IMAGE => Ok(format!( MediaType::IMAGE => Ok(format!(
r#"<a class="medium-ref">{caption}<img src="{}"></a>"#, "<a class=\"medium-ref\" href=\"#medium-{refid}\">{caption}<img src=\"{}\"></a>",
self.uri
)),
MediaType::VIDEO => Ok(format!(
"<a class=\"medium-ref\" href=\"#medium-{refid}\">{caption}<video><source src=\"{0}\"></video></a>",
self.uri self.uri
)), )),
_ => todo!(""), _ => todo!(""),
@ -219,8 +213,13 @@ impl ReferenceableElement for Medium {
_ => todo!(""), _ => todo!(""),
} }
} }
fn refid(&self, _compiler: &Compiler, refid: usize) -> String {
format!("medium-{refid}")
}
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::media")]
pub struct MediaRule { pub struct MediaRule {
re: [Regex; 1], re: [Regex; 1],
properties: PropertyParser, properties: PropertyParser,
@ -325,6 +324,7 @@ impl MediaRule {
impl RegexRule for MediaRule { impl RegexRule for MediaRule {
fn name(&self) -> &'static str { "Media" } fn name(&self) -> &'static str { "Media" }
fn previous(&self) -> Option<&'static str> { Some("Graphviz") }
fn regexes(&self) -> &[regex::Regex] { &self.re } fn regexes(&self) -> &[regex::Regex] { &self.re }
@ -433,15 +433,13 @@ impl RegexRule for MediaRule {
.get("width", |_, value| -> Result<String, ()> { .get("width", |_, value| -> Result<String, ()> {
Ok(value.clone()) Ok(value.clone())
}) })
.ok() .ok().map(|(_, s)| s);
.and_then(|(_, s)| Some(s));
let caption = properties let caption = properties
.get("caption", |_, value| -> Result<String, ()> { .get("caption", |_, value| -> Result<String, ()> {
Ok(value.clone()) Ok(value.clone())
}) })
.ok() .ok().map(|(_, value)| value);
.and_then(|(_, value)| Some(value));
let description = match matches.get(4) { let description = match matches.get(4) {
Some(content) => { Some(content) => {

View file

@ -9,7 +9,6 @@ pub mod media;
pub mod paragraph; pub mod paragraph;
pub mod raw; pub mod raw;
pub mod reference; pub mod reference;
pub mod registrar;
pub mod script; pub mod script;
pub mod section; pub mod section;
pub mod style; pub mod style;
@ -18,3 +17,4 @@ pub mod text;
pub mod variable; pub mod variable;
pub mod elemstyle; pub mod elemstyle;
pub mod customstyle; pub mod customstyle;
pub mod blockquote;

View file

@ -48,7 +48,7 @@ impl Element for Paragraph {
fn element_name(&self) -> &'static str { "Paragraph" } fn element_name(&self) -> &'static str { "Paragraph" }
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, document: &dyn Document, cursor: usize) -> Result<String, String> {
if self.content.is_empty() { if self.content.is_empty() {
return Ok(String::new()); return Ok(String::new());
} }
@ -63,7 +63,7 @@ impl Element for Paragraph {
result.push_str("<p>"); result.push_str("<p>");
for elems in &self.content { for elems in &self.content {
result += elems.compile(compiler, document)?.as_str(); result += elems.compile(compiler, document, cursor+result.len())?.as_str();
} }
result.push_str("</p>"); result.push_str("</p>");
@ -91,6 +91,7 @@ impl ContainerElement for Paragraph {
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::paragraph")]
pub struct ParagraphRule { pub struct ParagraphRule {
re: Regex, re: Regex,
} }
@ -104,12 +105,12 @@ impl ParagraphRule {
} }
impl Rule for ParagraphRule { impl Rule for ParagraphRule {
fn name(&self) -> &'static str { "Paragraphing" } fn name(&self) -> &'static str { "Paragraph" }
fn previous(&self) -> Option<&'static str> { Some("Comment") }
fn next_match(&self, _state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> { fn next_match(&self, _state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> {
self.re self.re
.find_at(cursor.source.content(), cursor.pos) .find_at(cursor.source.content(), cursor.pos).map(|m| (m.start(), Box::new([false; 0]) as Box<dyn Any>))
.and_then(|m| Some((m.start(), Box::new([false; 0]) as Box<dyn Any>)))
} }
fn on_match( fn on_match(

View file

@ -39,11 +39,12 @@ impl Element for Raw {
fn element_name(&self) -> &'static str { "Raw" } fn element_name(&self) -> &'static str { "Raw" }
fn compile(&self, _compiler: &Compiler, _document: &dyn Document) -> Result<String, String> { fn compile(&self, _compiler: &Compiler, _document: &dyn Document, _cursor: usize) -> Result<String, String> {
Ok(self.content.clone()) Ok(self.content.clone())
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::raw")]
pub struct RawRule { pub struct RawRule {
re: [Regex; 1], re: [Regex; 1],
properties: PropertyParser, properties: PropertyParser,
@ -72,6 +73,7 @@ impl RawRule {
impl RegexRule for RawRule { impl RegexRule for RawRule {
fn name(&self) -> &'static str { "Raw" } fn name(&self) -> &'static str { "Raw" }
fn previous(&self) -> Option<&'static str> { Some("Variable Substitution") }
fn regexes(&self) -> &[regex::Regex] { &self.re } fn regexes(&self) -> &[regex::Regex] { &self.re }

View file

@ -2,16 +2,20 @@ use std::collections::HashMap;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
use ariadne::Fmt;
use ariadne::Label; use ariadne::Label;
use ariadne::Report; use ariadne::Report;
use ariadne::ReportKind; use ariadne::ReportKind;
use reference_style::ExternalReferenceStyle;
use regex::Captures; use regex::Captures;
use regex::Match; use regex::Match;
use regex::Regex; use regex::Regex;
use runtime_format::FormatArgs;
use runtime_format::FormatKey;
use runtime_format::FormatKeyError;
use crate::compiler::compiler::Compiler; use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target; use crate::compiler::compiler::Target;
use crate::document::document::CrossReference;
use crate::document::document::Document; use crate::document::document::Document;
use crate::document::element::ElemKind; use crate::document::element::ElemKind;
use crate::document::element::Element; use crate::document::element::Element;
@ -21,33 +25,44 @@ use crate::parser::parser::ReportColors;
use crate::parser::rule::RegexRule; use crate::parser::rule::RegexRule;
use crate::parser::source::Source; use crate::parser::source::Source;
use crate::parser::source::Token; use crate::parser::source::Token;
use crate::parser::style::StyleHolder;
use crate::parser::util; use crate::parser::util;
use crate::parser::util::Property; use crate::parser::util::Property;
use crate::parser::util::PropertyMap; use crate::parser::util::PropertyMap;
use crate::parser::util::PropertyParser; use crate::parser::util::PropertyParser;
#[derive(Debug)] #[derive(Debug)]
pub struct Reference { pub struct InternalReference {
pub(self) location: Token, pub(self) location: Token,
pub(self) refname: String, pub(self) refname: String,
pub(self) caption: Option<String>, pub(self) caption: Option<String>,
} }
impl Reference { impl InternalReference {
pub fn caption(&self) -> Option<&String> { self.caption.as_ref() } pub fn caption(&self) -> Option<&String> { self.caption.as_ref() }
} }
impl Element for Reference { impl Element for InternalReference {
fn location(&self) -> &Token { &self.location } fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Inline } fn kind(&self) -> ElemKind { ElemKind::Inline }
fn element_name(&self) -> &'static str { "Reference" } fn element_name(&self) -> &'static str { "Reference" }
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> { fn compile(
&self,
compiler: &Compiler,
document: &dyn Document,
_cursor: usize,
) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
let elemref = document.get_reference(self.refname.as_str()).unwrap(); let elemref = document
.get_reference(self.refname.as_str())
.ok_or(format!(
"Unable to find reference `{}` in current document",
self.refname
))?;
let elem = document.get_from_reference(&elemref).unwrap(); let elem = document.get_from_reference(&elemref).unwrap();
elem.compile_reference( elem.compile_reference(
@ -62,6 +77,89 @@ impl Element for Reference {
} }
} }
#[derive(Debug)]
pub struct ExternalReference {
pub(self) location: Token,
pub(self) reference: CrossReference,
pub(self) caption: Option<String>,
pub(self) style: Rc<reference_style::ExternalReferenceStyle>,
}
struct FmtPair<'a>(Target, &'a ExternalReference);
impl FormatKey for FmtPair<'_> {
fn fmt(&self, key: &str, f: &mut std::fmt::Formatter<'_>) -> Result<(), FormatKeyError> {
match &self.1.reference {
CrossReference::Unspecific(refname) => match key {
"refname" => write!(f, "{}", Compiler::sanitize(self.0, refname))
.map_err(FormatKeyError::Fmt),
_ => Err(FormatKeyError::UnknownKey),
},
CrossReference::Specific(refdoc, refname) => match key {
"refdoc" => {
write!(f, "{}", Compiler::sanitize(self.0, refdoc)).map_err(FormatKeyError::Fmt)
}
"refname" => write!(f, "{}", Compiler::sanitize(self.0, refname))
.map_err(FormatKeyError::Fmt),
_ => Err(FormatKeyError::UnknownKey),
},
}
}
}
impl Element for ExternalReference {
fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Inline }
fn element_name(&self) -> &'static str { "Reference" }
fn compile(
&self,
compiler: &Compiler,
_document: &dyn Document,
cursor: usize,
) -> Result<String, String> {
match compiler.target() {
Target::HTML => {
let mut result = "<a href=\"".to_string();
// Link position
let crossreference_pos = cursor + result.len();
if let Some(caption) = &self.caption {
result +=
format!("\">{}</a>", Compiler::sanitize(Target::HTML, caption)).as_str();
} else {
// Use style
let fmt_pair = FmtPair(compiler.target(), self);
let format_string = match &self.reference {
CrossReference::Unspecific(_) => Compiler::sanitize_format(
fmt_pair.0,
self.style.format_unspecific.as_str(),
),
CrossReference::Specific(_, _) => Compiler::sanitize_format(
fmt_pair.0,
self.style.format_specific.as_str(),
),
};
let args = FormatArgs::new(format_string.as_str(), &fmt_pair);
args.status().map_err(|err| {
format!("Failed to format ExternalReference style `{format_string}`: {err}")
})?;
result += format!("\">{}</a>", args.to_string()).as_str();
}
// Add crossreference
compiler.insert_crossreference(crossreference_pos, self.reference.clone());
Ok(result)
}
_ => todo!(""),
}
}
}
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::reference")]
pub struct ReferenceRule { pub struct ReferenceRule {
re: [Regex; 1], re: [Regex; 1],
properties: PropertyParser, properties: PropertyParser,
@ -127,6 +225,7 @@ impl ReferenceRule {
impl RegexRule for ReferenceRule { impl RegexRule for ReferenceRule {
fn name(&self) -> &'static str { "Reference" } fn name(&self) -> &'static str { "Reference" }
fn previous(&self) -> Option<&'static str> { Some("Text") }
fn regexes(&self) -> &[regex::Regex] { &self.re } fn regexes(&self) -> &[regex::Regex] { &self.re }
@ -140,41 +239,50 @@ impl RegexRule for ReferenceRule {
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> { ) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
let mut reports = vec![]; let mut reports = vec![];
let refname = match ( let (refdoc, refname) = if let Some(refname_match) = matches.get(1) {
matches.get(1).unwrap(), if let Some(sep) = refname_match.as_str().find('#')
validate_refname(document, matches.get(1).unwrap().as_str(), false), // External reference
) { {
(m, Ok(refname)) => { let refdoc = refname_match.as_str().split_at(sep).0;
if document.get_reference(refname).is_none() { match validate_refname(document, refname_match.as_str().split_at(sep + 1).1, false)
reports.push( {
Report::build(ReportKind::Error, token.source(), m.start()) Err(err) => {
.with_message("Uknown Reference Refname") reports.push(
.with_label( Report::build(ReportKind::Error, token.source(), refname_match.start())
Label::new((token.source().clone(), m.range())).with_message( .with_message("Invalid Reference Refname")
format!( .with_label(
"Could not find element with reference: `{}`", Label::new((token.source().clone(), refname_match.range()))
refname.fg(state.parser.colors().info) .with_message(err),
), )
), .finish(),
) );
.finish(), return reports;
); }
return reports; Ok(refname) => (Some(refdoc.to_string()), refname.to_string()),
}
} else
// Internal reference
{
match validate_refname(document, refname_match.as_str(), false) {
Err(err) => {
reports.push(
Report::build(ReportKind::Error, token.source(), refname_match.start())
.with_message("Invalid Reference Refname")
.with_label(
Label::new((token.source().clone(), refname_match.range()))
.with_message(err),
)
.finish(),
);
return reports;
}
Ok(refname) => (None, refname.to_string()),
} }
refname.to_string()
}
(m, Err(err)) => {
reports.push(
Report::build(ReportKind::Error, token.source(), m.start())
.with_message("Invalid Reference Refname")
.with_label(
Label::new((token.source().clone(), m.range())).with_message(err),
)
.finish(),
);
return reports;
} }
} else {
panic!("Unknown error")
}; };
// Properties // Properties
let properties = match self.parse_properties(state.parser.colors(), &token, &matches.get(3)) let properties = match self.parse_properties(state.parser.colors(), &token, &matches.get(3))
{ {
@ -190,17 +298,193 @@ impl RegexRule for ReferenceRule {
Ok(value.clone()) Ok(value.clone())
}) })
.ok() .ok()
.and_then(|(_, s)| Some(s)); .map(|(_, s)| s);
state.push( if let Some(refdoc) = refdoc {
document, // Get style
Box::new(Reference { let style = state
location: token, .shared
refname, .styles
caption, .borrow()
}), .current(reference_style::STYLE_KEY)
); .downcast_rc::<reference_style::ExternalReferenceStyle>()
.unwrap();
// §{#refname}
if refdoc.is_empty() {
state.push(
document,
Box::new(ExternalReference {
location: token,
reference: CrossReference::Unspecific(refname),
caption,
style,
}),
);
// §{docname#refname}
} else {
state.push(
document,
Box::new(ExternalReference {
location: token,
reference: CrossReference::Specific(refdoc, refname),
caption,
style,
}),
);
}
} else {
state.push(
document,
Box::new(InternalReference {
location: token,
refname,
caption,
}),
);
}
reports reports
} }
fn register_styles(&self, holder: &mut StyleHolder) {
holder.set_current(Rc::new(ExternalReferenceStyle::default()));
}
}
mod reference_style {
use serde::Deserialize;
use serde::Serialize;
use crate::impl_elementstyle;
pub static STYLE_KEY: &str = "style.external_reference";
#[derive(Debug, Serialize, Deserialize)]
pub struct ExternalReferenceStyle {
pub format_unspecific: String,
pub format_specific: String,
}
impl Default for ExternalReferenceStyle {
fn default() -> Self {
Self {
format_unspecific: "(#{refname})".into(),
format_specific: "({refdoc}#{refname})".into(),
}
}
}
impl_elementstyle!(ExternalReferenceStyle, STYLE_KEY);
}
#[cfg(test)]
mod tests {
use crate::compiler::process::process_from_memory;
use crate::elements::paragraph::Paragraph;
use crate::elements::section::Section;
use crate::parser::langparser::LangParser;
use crate::parser::parser::Parser;
use crate::parser::source::SourceFile;
use crate::validate_document;
use super::*;
#[test]
pub fn parse_internal() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
#{ref} Referenceable section
§{ref}[caption=Section]
§{ref}[caption=Another]
§{ref2}[caption=Before]
#{ref2} Another section
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Section;
Paragraph {
InternalReference { refname == "ref", caption == Some("Section".to_string()) };
InternalReference { refname == "ref", caption == Some("Another".to_string()) };
InternalReference { refname == "ref2", caption == Some("Before".to_string()) };
};
Paragraph;
Section;
);
}
#[test]
pub fn parse_external() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
§{DocA#ref}[caption=Section]
§{DocB#ref}
§{#ref}[caption='ref' from any document]
"#
.to_string(),
None,
));
let parser = LangParser::default();
let (doc, _) = parser.parse(ParserState::new(&parser, None), source, None);
validate_document!(doc.content().borrow(), 0,
Paragraph {
ExternalReference { reference == CrossReference::Specific("DocA".into(), "ref".into()), caption == Some("Section".to_string()) };
ExternalReference { reference == CrossReference::Specific("DocB".into(), "ref".into()), caption == None::<String> };
ExternalReference { reference == CrossReference::Unspecific("ref".into()), caption == Some("'ref' from any document".to_string()) };
};
);
}
#[test]
pub fn test_external() {
let result = process_from_memory(
Target::HTML,
vec![
r#"
@html.page_title = 0
@compiler.output = a.html
#{ref} Referenceable section
"#
.into(),
r#"
@html.page_title = 1
@compiler.output = b.html
§{#ref}
§{a#ref}
#{ref2} Another Referenceable section
"#
.into(),
r#"
@html.page_title = 2
@@style.external_reference = {
"format_unspecific": "[UNSPECIFIC {refname}]",
"format_specific": "[SPECIFIC {refdoc}:{refname}]"
}
§{#ref}[caption=from 0]
§{#ref}
§{#ref2}[caption=from 1]
§{b#ref2}
"#
.into(),
],
)
.unwrap();
assert!(result[1].0.borrow().body.starts_with("<div class=\"content\"><p><a href=\"a.html#Referenceable_section\">(#ref)</a><a href=\"a.html#Referenceable_section\">(a#ref)</a></p>"));
assert!(result[2].0.borrow().body.starts_with("<div class=\"content\"><p><a href=\"a.html#Referenceable_section\">from 0</a><a href=\"a.html#Referenceable_section\">[UNSPECIFIC ref]</a><a href=\"b.html#Another_Referenceable_section\">from 1</a><a href=\"b.html#Another_Referenceable_section\">[SPECIFIC b:ref2]</a></p>"));
}
} }

View file

@ -1,46 +0,0 @@
use crate::parser::parser::Parser;
use super::code::CodeRule;
use super::comment::CommentRule;
use super::elemstyle::ElemStyleRule;
use super::graphviz::GraphRule;
use super::import::ImportRule;
use super::layout::LayoutRule;
use super::link::LinkRule;
use super::list::ListRule;
use super::media::MediaRule;
use super::paragraph::ParagraphRule;
use super::raw::RawRule;
use super::script::ScriptRule;
use super::section::SectionRule;
use super::style::StyleRule;
use super::customstyle::CustomStyleRule;
use super::tex::TexRule;
use super::text::TextRule;
use super::variable::VariableRule;
use super::variable::VariableSubstitutionRule;
use super::reference::ReferenceRule;
pub fn register<P: Parser>(parser: &mut P) {
parser.add_rule(Box::new(CommentRule::new()), None).unwrap();
parser.add_rule(Box::new(ParagraphRule::new()), None).unwrap();
parser.add_rule(Box::new(ImportRule::new()), None).unwrap();
parser.add_rule(Box::new(ScriptRule::new()), None).unwrap();
parser.add_rule(Box::new(ElemStyleRule::new()), None).unwrap();
parser.add_rule(Box::new(VariableRule::new()), None).unwrap();
parser.add_rule(Box::new(VariableSubstitutionRule::new()), None).unwrap();
parser.add_rule(Box::new(RawRule::new()), None).unwrap();
parser.add_rule(Box::new(ListRule::new()), None).unwrap();
parser.add_rule(Box::new(CodeRule::new()), None).unwrap();
parser.add_rule(Box::new(TexRule::new()), None).unwrap();
parser.add_rule(Box::new(GraphRule::new()), None).unwrap();
parser.add_rule(Box::new(MediaRule::new()), None).unwrap();
parser.add_rule(Box::new(LayoutRule::new()), None).unwrap();
parser.add_rule(Box::new(StyleRule::new()), None).unwrap();
parser.add_rule(Box::new(CustomStyleRule{}), None).unwrap();
parser.add_rule(Box::new(SectionRule::new()), None).unwrap();
parser.add_rule(Box::new(LinkRule::new()), None).unwrap();
parser.add_rule(Box::new(TextRule::default()), None).unwrap();
parser.add_rule(Box::new(ReferenceRule::new()), None).unwrap();
}

View file

@ -20,6 +20,7 @@ use std::rc::Rc;
use super::text::Text; use super::text::Text;
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::script")]
pub struct ScriptRule { pub struct ScriptRule {
re: [Regex; 2], re: [Regex; 2],
eval_kinds: [(&'static str, &'static str); 3], eval_kinds: [(&'static str, &'static str); 3],
@ -77,6 +78,7 @@ impl ScriptRule {
impl RegexRule for ScriptRule { impl RegexRule for ScriptRule {
fn name(&self) -> &'static str { "Script" } fn name(&self) -> &'static str { "Script" }
fn previous(&self) -> Option<&'static str> { Some("Import") }
fn regexes(&self) -> &[regex::Regex] { &self.re } fn regexes(&self) -> &[regex::Regex] { &self.re }
@ -170,7 +172,7 @@ impl RegexRule for ScriptRule {
Label::new((source.clone(), 0..source.content().len())) Label::new((source.clone(), 0..source.content().len()))
.with_message(format!( .with_message(format!(
"Kernel execution failed:\n{}", "Kernel execution failed:\n{}",
e.to_string() e
)) ))
.with_color(state.parser.colors().error), .with_color(state.parser.colors().error),
) )
@ -213,7 +215,7 @@ impl RegexRule for ScriptRule {
Label::new((source.clone(), 0..source.content().len())) Label::new((source.clone(), 0..source.content().len()))
.with_message(format!( .with_message(format!(
"Kernel evaluation failed:\n{}", "Kernel evaluation failed:\n{}",
e.to_string() e
)) ))
.with_color(state.parser.colors().error), .with_color(state.parser.colors().error),
) )
@ -261,7 +263,7 @@ impl RegexRule for ScriptRule {
Label::new((source.clone(), 0..source.content().len())) Label::new((source.clone(), 0..source.content().len()))
.with_message(format!( .with_message(format!(
"Kernel evaluation failed:\n{}", "Kernel evaluation failed:\n{}",
e.to_string() e
)) ))
.with_color(state.parser.colors().error), .with_color(state.parser.colors().error),
) )

View file

@ -24,6 +24,8 @@ use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use super::reference::InternalReference;
#[derive(Debug)] #[derive(Debug)]
pub struct Section { pub struct Section {
pub(self) location: Token, pub(self) location: Token,
@ -43,7 +45,7 @@ impl Element for Section {
fn location(&self) -> &Token { &self.location } fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Block } fn kind(&self) -> ElemKind { ElemKind::Block }
fn element_name(&self) -> &'static str { "Section" } fn element_name(&self) -> &'static str { "Section" }
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, _document: &dyn Document, _cursor: usize) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
// Section numbering // Section numbering
@ -112,7 +114,7 @@ impl ReferenceableElement for Section {
&self, &self,
compiler: &Compiler, compiler: &Compiler,
_document: &dyn Document, _document: &dyn Document,
reference: &super::reference::Reference, reference: &InternalReference,
_refid: usize, _refid: usize,
) -> Result<String, String> { ) -> Result<String, String> {
match compiler.target() { match compiler.target() {
@ -133,8 +135,13 @@ impl ReferenceableElement for Section {
_ => todo!(""), _ => todo!(""),
} }
} }
fn refid(&self, compiler: &Compiler, _refid: usize) -> String {
Compiler::refname(compiler.target(), self.title.as_str())
}
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::section")]
pub struct SectionRule { pub struct SectionRule {
re: [Regex; 1], re: [Regex; 1],
} }
@ -155,6 +162,7 @@ pub mod section_kind {
impl RegexRule for SectionRule { impl RegexRule for SectionRule {
fn name(&self) -> &'static str { "Section" } fn name(&self) -> &'static str { "Section" }
fn previous(&self) -> Option<&'static str> { Some("Custom Style") }
fn regexes(&self) -> &[Regex] { &self.re } fn regexes(&self) -> &[Regex] { &self.re }
@ -214,7 +222,7 @@ impl RegexRule for SectionRule {
.with_message(format!("`{}` previously defined here", .with_message(format!("`{}` previously defined here",
refname.as_str().fg(state.parser.colors().highlight))) refname.as_str().fg(state.parser.colors().highlight)))
.with_color(state.parser.colors().warning)) .with_color(state.parser.colors().warning))
.with_note(format!("Previous reference was overwritten")) .with_note("Previous reference was overwritten".to_string())
.finish()); .finish());
} }
Some(refname.as_str().to_string()) Some(refname.as_str().to_string())
@ -239,7 +247,7 @@ impl RegexRule for SectionRule {
"+".fg(state.parser.colors().info), "+".fg(state.parser.colors().info),
kind.as_str().fg(state.parser.colors().highlight))) kind.as_str().fg(state.parser.colors().highlight)))
.with_color(state.parser.colors().error)) .with_color(state.parser.colors().error))
.with_help(format!("Leave empty for a numbered listed section")) .with_help("Leave empty for a numbered listed section".to_string())
.finish()); .finish());
return result; return result;
} }
@ -313,7 +321,7 @@ impl RegexRule for SectionRule {
}), }),
); );
return result; result
} }
fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> { fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> {
@ -322,8 +330,8 @@ impl RegexRule for SectionRule {
bindings.push(( bindings.push((
"push".to_string(), "push".to_string(),
lua.create_function( lua.create_function(
|_, (title, depth, kind, reference): (String, usize, String, Option<String>)| { |_, (title, depth, kind, reference): (String, usize, Option<String>, Option<String>)| {
let kind = match kind.as_str() { let kind = match kind.as_deref().unwrap_or("") {
"*+" | "+*" => section_kind::NO_NUMBER | section_kind::NO_TOC, "*+" | "+*" => section_kind::NO_NUMBER | section_kind::NO_TOC,
"*" => section_kind::NO_NUMBER, "*" => section_kind::NO_NUMBER,
"+" => section_kind::NO_TOC, "+" => section_kind::NO_TOC,
@ -333,9 +341,7 @@ impl RegexRule for SectionRule {
to: Some("push".to_string()), to: Some("push".to_string()),
pos: 3, pos: 3,
name: Some("kind".to_string()), name: Some("kind".to_string()),
cause: Arc::new(mlua::Error::external(format!( cause: Arc::new(mlua::Error::external("Unknown section kind specified".to_string())),
"Unknown section kind specified"
))),
}) })
} }
}; };
@ -386,7 +392,7 @@ mod section_style {
use crate::impl_elementstyle; use crate::impl_elementstyle;
pub static STYLE_KEY: &'static str = "style.section"; pub static STYLE_KEY: &str = "style.section";
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub enum SectionLinkPos { pub enum SectionLinkPos {

View file

@ -4,6 +4,7 @@ use crate::document::document::Document;
use crate::document::document::DocumentAccessors; use crate::document::document::DocumentAccessors;
use crate::document::element::ElemKind; use crate::document::element::ElemKind;
use crate::document::element::Element; use crate::document::element::Element;
use crate::lua::kernel::CTX;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::rule::RegexRule; use crate::parser::rule::RegexRule;
use crate::parser::source::Source; use crate::parser::source::Source;
@ -14,11 +15,13 @@ use ariadne::Fmt;
use ariadne::Label; use ariadne::Label;
use ariadne::Report; use ariadne::Report;
use ariadne::ReportKind; use ariadne::ReportKind;
use mlua::Function;
use regex::Captures; use regex::Captures;
use regex::Regex; use regex::Regex;
use std::cell::RefCell; use std::cell::RefCell;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use super::paragraph::Paragraph; use super::paragraph::Paragraph;
@ -42,8 +45,8 @@ impl Style {
impl Element for Style { impl Element for Style {
fn location(&self) -> &Token { &self.location } fn location(&self) -> &Token { &self.location }
fn kind(&self) -> ElemKind { ElemKind::Inline } fn kind(&self) -> ElemKind { ElemKind::Inline }
fn element_name(&self) -> &'static str { "Section" } fn element_name(&self) -> &'static str { "Style" }
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, _document: &dyn Document, _cursor: usize) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
Ok([ Ok([
@ -96,13 +99,10 @@ impl RuleState for StyleState {
let paragraph = document.last_element::<Paragraph>().unwrap(); let paragraph = document.last_element::<Paragraph>().unwrap();
let paragraph_end = paragraph let paragraph_end = paragraph
.content .content
.last() .last().map(|last| (
.and_then(|last| {
Some((
last.location().source(), last.location().source(),
last.location().end() - 1..last.location().end(), last.location().end() - 1..last.location().end(),
)) ))
})
.unwrap(); .unwrap();
reports.push( reports.push(
@ -120,7 +120,7 @@ impl RuleState for StyleState {
.with_label( .with_label(
Label::new(paragraph_end) Label::new(paragraph_end)
.with_order(1) .with_order(1)
.with_message(format!("Paragraph ends here")) .with_message("Paragraph ends here".to_string())
.with_color(state.parser.colors().error), .with_color(state.parser.colors().error),
) )
.with_note("Styles cannot span multiple documents (i.e @import)") .with_note("Styles cannot span multiple documents (i.e @import)")
@ -128,10 +128,11 @@ impl RuleState for StyleState {
); );
}); });
return reports; reports
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::style")]
pub struct StyleRule { pub struct StyleRule {
re: [Regex; 4], re: [Regex; 4],
} }
@ -153,10 +154,11 @@ impl StyleRule {
} }
} }
static STATE_NAME: &'static str = "elements.style"; static STATE_NAME: &str = "elements.style";
impl RegexRule for StyleRule { impl RegexRule for StyleRule {
fn name(&self) -> &'static str { "Style" } fn name(&self) -> &'static str { "Style" }
fn previous(&self) -> Option<&'static str> { Some("Layout") }
fn regexes(&self) -> &[regex::Regex] { &self.re } fn regexes(&self) -> &[regex::Regex] { &self.re }
@ -194,14 +196,80 @@ impl RegexRule for StyleRule {
Box::new(Style::new( Box::new(Style::new(
token.clone(), token.clone(),
index, index,
!style_state.toggled[index].is_some(), style_state.toggled[index].is_none(),
)), )),
); );
} else { } else {
panic!("Invalid state at `{STATE_NAME}`"); panic!("Invalid state at `{STATE_NAME}`");
} }
return vec![]; vec![]
}
fn register_bindings<'lua>(&self, lua: &'lua mlua::Lua) -> Vec<(String, Function<'lua>)> {
let mut bindings = vec![];
bindings.push((
"toggle".to_string(),
lua.create_function(|_, style: String| {
let kind = match style.as_str() {
"bold" | "Bold" => 0,
"italic" | "Italic" => 1,
"underline" | "Underline" => 2,
"emphasis" | "Emphasis" => 3,
_ => {
return Err(mlua::Error::BadArgument {
to: Some("toggle".to_string()),
pos: 1,
name: Some("style".to_string()),
cause: Arc::new(mlua::Error::external("Unknown style specified".to_string())),
})
}
};
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
let query = ctx.state.shared.rule_state.borrow().get(STATE_NAME);
let style_state = match query {
Some(state) => state,
None => {
// Insert as a new state
match ctx.state.shared.rule_state.borrow_mut().insert(
STATE_NAME.into(),
Rc::new(RefCell::new(StyleState::new())),
) {
Err(_) => panic!("Unknown error"),
Ok(state) => state,
}
}
};
if let Some(style_state) =
style_state.borrow_mut().downcast_mut::<StyleState>()
{
style_state.toggled[kind] = style_state.toggled[kind]
.clone()
.map_or(Some(ctx.location.clone()), |_| None);
ctx.state.push(
ctx.document,
Box::new(Style::new(
ctx.location.clone(),
kind,
style_state.toggled[kind].is_none(),
)),
);
} else {
panic!("Invalid state at `{STATE_NAME}`");
};
})
});
Ok(())
})
.unwrap(),
));
bindings
} }
} }
@ -255,4 +323,45 @@ __`UNDERLINE+EM`__
}; };
); );
} }
#[test]
fn lua() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
r#"
Some %<nml.style.toggle("italic")>%style
terminated here%<nml.style.toggle("Italic")>%
%<nml.style.toggle("Bold")>%NOLD + %<nml.style.toggle("italic")>%italic%<nml.style.toggle("bold") nml.style.toggle("italic")>%
%<nml.style.toggle("Underline") nml.style.toggle("Emphasis")>%UNDERLINE+EM%<nml.style.toggle("emphasis")>%%<nml.style.toggle("underline")>%
"#
.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 { kind == 1, close == false };
Text;
Style { kind == 1, close == true };
};
Paragraph {
Style { kind == 0, close == false }; // **
Text;
Style { kind == 1, close == false }; // *
Text;
Style { kind == 0, close == true }; // **
Style { kind == 1, close == true }; // *
Style { kind == 2, close == false }; // __
Style { kind == 3, close == false }; // `
Text;
Style { kind == 3, close == true }; // `
Style { kind == 2, close == true }; // __
};
);
}
} }

View file

@ -6,6 +6,7 @@ use std::process::Command;
use std::process::Stdio; use std::process::Stdio;
use std::rc::Rc; use std::rc::Rc;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc;
use std::sync::Once; use std::sync::Once;
use ariadne::Fmt; use ariadne::Fmt;
@ -14,6 +15,8 @@ use ariadne::Report;
use ariadne::ReportKind; use ariadne::ReportKind;
use crypto::digest::Digest; use crypto::digest::Digest;
use crypto::sha2::Sha512; use crypto::sha2::Sha512;
use mlua::Function;
use mlua::Lua;
use regex::Captures; use regex::Captures;
use regex::Match; use regex::Match;
use regex::Regex; use regex::Regex;
@ -25,6 +28,7 @@ use crate::compiler::compiler::Target;
use crate::document::document::Document; use crate::document::document::Document;
use crate::document::element::ElemKind; use crate::document::element::ElemKind;
use crate::document::element::Element; use crate::document::element::Element;
use crate::lua::kernel::CTX;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::parser::ReportColors; use crate::parser::parser::ReportColors;
use crate::parser::rule::RegexRule; use crate::parser::rule::RegexRule;
@ -110,10 +114,7 @@ impl FormattedTex {
} }
let mut result = String::new(); let mut result = String::new();
match process.stdout.unwrap().read_to_string(&mut result) { if let Err(e) = process.stdout.unwrap().read_to_string(&mut result) { panic!("Unable to read `latex2svg` stdout: {}", e) }
Err(e) => panic!("Unable to read `latex2svg` stdout: {}", e),
Ok(_) => {}
}
println!("Done!"); println!("Done!");
Ok(result) Ok(result)
@ -149,13 +150,18 @@ impl Element for Tex {
fn element_name(&self) -> &'static str { "LaTeX" } fn element_name(&self) -> &'static str { "LaTeX" }
fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result<String, String> { fn compile(
&self,
compiler: &Compiler,
document: &dyn Document,
_cursor: usize,
) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
static CACHE_INIT: Once = Once::new(); static CACHE_INIT: Once = Once::new();
CACHE_INIT.call_once(|| { CACHE_INIT.call_once(|| {
if let Some(mut con) = compiler.cache() { if let Some(con) = compiler.cache() {
if let Err(e) = FormattedTex::init(&mut con) { if let Err(e) = FormattedTex::init(con) {
eprintln!("Unable to create cache table: {e}"); eprintln!("Unable to create cache table: {e}");
} }
} }
@ -185,8 +191,8 @@ impl Element for Tex {
Tex::format_latex(&fontsize, &preamble, &format!("{prepend}{}", self.tex)) Tex::format_latex(&fontsize, &preamble, &format!("{prepend}{}", self.tex))
}; };
let result = if let Some(mut con) = compiler.cache() { let result = if let Some(con) = compiler.cache() {
match latex.cached(&mut con, |s| s.latex_to_svg(&exec, &fontsize)) { match latex.cached(con, |s| s.latex_to_svg(&exec, &fontsize)) {
Ok(s) => Ok(s), Ok(s) => Ok(s),
Err(e) => match e { Err(e) => match e {
CachedError::SqlErr(e) => { CachedError::SqlErr(e) => {
@ -219,6 +225,7 @@ impl Element for Tex {
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::tex")]
pub struct TexRule { pub struct TexRule {
re: [Regex; 2], re: [Regex; 2],
properties: PropertyParser, properties: PropertyParser,
@ -296,6 +303,7 @@ impl TexRule {
impl RegexRule for TexRule { impl RegexRule for TexRule {
fn name(&self) -> &'static str { "Tex" } fn name(&self) -> &'static str { "Tex" }
fn previous(&self) -> Option<&'static str> { Some("Code") }
fn regexes(&self) -> &[regex::Regex] { &self.re } fn regexes(&self) -> &[regex::Regex] { &self.re }
@ -399,16 +407,14 @@ impl RegexRule for TexRule {
.get("caption", |_, value| -> Result<String, ()> { .get("caption", |_, value| -> Result<String, ()> {
Ok(value.clone()) Ok(value.clone())
}) })
.ok() .ok().map(|(_, value)| value);
.and_then(|(_, value)| Some(value));
// Environ // Environ
let tex_env = properties let tex_env = properties
.get("env", |_, value| -> Result<String, ()> { .get("env", |_, value| -> Result<String, ()> {
Ok(value.clone()) Ok(value.clone())
}) })
.ok() .ok().map(|(_, value)| value)
.and_then(|(_, value)| Some(value))
.unwrap(); .unwrap();
state.push( state.push(
@ -425,6 +431,95 @@ impl RegexRule for TexRule {
reports reports
} }
fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> {
let mut bindings = vec![];
bindings.push((
"push_math".to_string(),
lua.create_function(
|_, (kind, tex, env, caption): (String, String, Option<String>, Option<String>)| {
let mut result = Ok(());
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
let kind = match TexKind::from_str(kind.as_str()) {
Ok(kind) => kind,
Err(err) => {
result = Err(mlua::Error::BadArgument {
to: Some("push".to_string()),
pos: 2,
name: Some("kind".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Unable to get tex kind: {err}"
))),
});
return;
}
};
ctx.state.push(
ctx.document,
Box::new(Tex {
location: ctx.location.clone(),
mathmode: true,
kind,
env: env.unwrap_or("main".to_string()),
tex,
caption,
}),
);
})
});
result
},
)
.unwrap(),
));
bindings.push((
"push".to_string(),
lua.create_function(
|_, (kind, tex, env, caption): (String, String, Option<String>, Option<String>)| {
let mut result = Ok(());
CTX.with_borrow(|ctx| {
ctx.as_ref().map(|ctx| {
let kind = match TexKind::from_str(kind.as_str()) {
Ok(kind) => kind,
Err(err) => {
result = Err(mlua::Error::BadArgument {
to: Some("push".to_string()),
pos: 2,
name: Some("kind".to_string()),
cause: Arc::new(mlua::Error::external(format!(
"Unable to get tex kind: {err}"
))),
});
return;
}
};
ctx.state.push(
ctx.document,
Box::new(Tex {
location: ctx.location.clone(),
mathmode: false,
kind,
env: env.unwrap_or("main".to_string()),
tex,
caption,
}),
);
})
});
result
},
)
.unwrap(),
));
bindings
}
} }
#[cfg(test)] #[cfg(test)]
@ -445,6 +540,9 @@ mod tests {
$[kind=block, caption=Some\, text\\] 1+1=2 $ $[kind=block, caption=Some\, text\\] 1+1=2 $
$|[env=another] Non Math \LaTeX |$ $|[env=another] Non Math \LaTeX |$
$[kind=block,env=another] e^{i\pi}=-1$ $[kind=block,env=another] e^{i\pi}=-1$
%<nml.tex.push_math("block", "1+1=2", nil, "Some, text\\")>%
%<nml.tex.push("block", "Non Math \\LaTeX", "another", nil)>%
%<nml.tex.push_math("block", "e^{i\\pi}=-1", "another", nil)>%
"# "#
.to_string(), .to_string(),
None, None,
@ -456,6 +554,9 @@ $[kind=block,env=another] e^{i\pi}=-1$
Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\".to_string()) }; Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\".to_string()) };
Tex { mathmode == false, tex == "Non Math \\LaTeX", env == "another" }; Tex { mathmode == false, tex == "Non Math \\LaTeX", env == "another" };
Tex { mathmode == true, tex == "e^{i\\pi}=-1", env == "another" }; Tex { mathmode == true, tex == "e^{i\\pi}=-1", env == "another" };
Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\".to_string()) };
Tex { mathmode == false, tex == "Non Math \\LaTeX", env == "another" };
Tex { mathmode == true, tex == "e^{i\\pi}=-1", env == "another" };
); );
} }
@ -467,6 +568,9 @@ $[kind=block,env=another] e^{i\pi}=-1$
$[ caption=Some\, text\\] 1+1=2 $ $[ caption=Some\, text\\] 1+1=2 $
$|[env=another, kind=inline , caption = Enclosed \]. ] Non Math \LaTeX|$ $|[env=another, kind=inline , caption = Enclosed \]. ] Non Math \LaTeX|$
$[env=another] e^{i\pi}=-1$ $[env=another] e^{i\pi}=-1$
%<nml.tex.push_math("inline", "1+1=2", "main", "Some, text\\")>%
%<nml.tex.push("inline", "Non Math \\LaTeX", "another", "Enclosed ].")>%
%<nml.tex.push_math("inline", "e^{i\\pi}=-1", "another", nil)>%
"# "#
.to_string(), .to_string(),
None, None,
@ -477,7 +581,10 @@ $[env=another] e^{i\pi}=-1$
validate_document!(doc.content().borrow(), 0, validate_document!(doc.content().borrow(), 0,
Paragraph { Paragraph {
Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\".to_string()) }; Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\".to_string()) };
Tex { mathmode == false, tex == "Non Math \\LaTeX", env == "another" }; Tex { mathmode == false, tex == "Non Math \\LaTeX", env == "another", caption == Some("Enclosed ].".to_string()) };
Tex { mathmode == true, tex == "e^{i\\pi}=-1", env == "another" };
Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\".to_string()) };
Tex { mathmode == false, tex == "Non Math \\LaTeX", env == "another", caption == Some("Enclosed ].".to_string()) };
Tex { mathmode == true, tex == "e^{i\\pi}=-1", env == "another" }; Tex { mathmode == true, tex == "e^{i\\pi}=-1", env == "another" };
}; };
); );

View file

@ -26,8 +26,8 @@ pub struct Text {
impl Text { impl Text {
pub fn new(location: Token, content: String) -> Text { pub fn new(location: Token, content: String) -> Text {
Text { Text {
location: location, location,
content: content, content,
} }
} }
} }
@ -37,18 +37,25 @@ impl Element for Text {
fn kind(&self) -> ElemKind { ElemKind::Inline } fn kind(&self) -> ElemKind { ElemKind::Inline }
fn element_name(&self) -> &'static str { "Text" } fn element_name(&self) -> &'static str { "Text" }
fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> { fn compile(&self, compiler: &Compiler, _document: &dyn Document, _cursor: usize) -> Result<String, String> {
Ok(Compiler::sanitize(compiler.target(), self.content.as_str())) Ok(Compiler::sanitize(compiler.target(), self.content.as_str()))
} }
} }
#[derive(Default)] #[auto_registry::auto_registry(registry = "rules", path = "crate::elements::text")]
pub struct TextRule; pub struct TextRule;
impl TextRule {
pub fn new() -> Self { Self {} }
}
impl Rule for TextRule { impl Rule for TextRule {
fn name(&self) -> &'static str { "Text" } fn name(&self) -> &'static str { "Text" }
fn previous(&self) -> Option<&'static str> { Some("Link") }
fn next_match(&self, _state: &ParserState, _cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> { None } fn next_match(&self, _state: &ParserState, _cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> {
None
}
fn on_match( fn on_match(
&self, &self,

View file

@ -37,6 +37,7 @@ impl FromStr for VariableKind {
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::variable")]
pub struct VariableRule { pub struct VariableRule {
re: [Regex; 1], re: [Regex; 1],
kinds: Vec<(String, String)>, kinds: Vec<(String, String)>,
@ -66,7 +67,7 @@ impl VariableRule {
Ok(path) => Ok(Rc::new(PathVariable::new(location, name, path))), Ok(path) => Ok(Rc::new(PathVariable::new(location, name, path))),
Err(e) => Err(format!("Unable to canonicalize path `{}`: {}", Err(e) => Err(format!("Unable to canonicalize path `{}`: {}",
value.fg(colors.highlight), value.fg(colors.highlight),
e.to_string())) e))
} }
} }
_ => panic!("Unhandled variable kind"), _ => panic!("Unhandled variable kind"),
@ -82,7 +83,7 @@ impl VariableRule {
if name.contains("%") { if name.contains("%") {
return Err(format!("Name cannot contain '{}'", "%".fg(colors.info))); return Err(format!("Name cannot contain '{}'", "%".fg(colors.info)));
} }
return Ok(name); Ok(name)
} }
pub fn validate_value(original_value: &str) -> Result<String, String> { pub fn validate_value(original_value: &str) -> Result<String, String> {
@ -117,14 +118,15 @@ impl VariableRule {
impl RegexRule for VariableRule { impl RegexRule for VariableRule {
fn name(&self) -> &'static str { "Variable" } fn name(&self) -> &'static str { "Variable" }
fn previous(&self) -> Option<&'static str> { Some("Element Style") }
fn regexes(&self) -> &[Regex] { &self.re } fn regexes(&self) -> &[Regex] { &self.re }
fn on_regex_match<'a>( fn on_regex_match(
&self, &self,
_: usize, _: usize,
state: &ParserState, state: &ParserState,
document: &'a dyn Document, document: &dyn Document,
token: Token, token: Token,
matches: regex::Captures, matches: regex::Captures,
) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> { ) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
@ -178,7 +180,7 @@ impl RegexRule for VariableRule {
}; };
let var_name = match matches.get(2) { let var_name = match matches.get(2) {
Some(name) => match VariableRule::validate_name(&state.parser.colors(), name.as_str()) { Some(name) => match VariableRule::validate_name(state.parser.colors(), name.as_str()) {
Ok(var_name) => var_name, Ok(var_name) => var_name,
Err(msg) => { Err(msg) => {
result.push( result.push(
@ -226,7 +228,7 @@ impl RegexRule for VariableRule {
}; };
match self.make_variable( match self.make_variable(
&state.parser.colors(), state.parser.colors(),
token.clone(), token.clone(),
var_kind, var_kind,
var_name.to_string(), var_name.to_string(),
@ -254,7 +256,7 @@ impl RegexRule for VariableRule {
} }
} }
return result; result
} }
fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> { fn register_bindings<'lua>(&self, lua: &'lua Lua) -> Vec<(String, Function<'lua>)> {
@ -295,6 +297,7 @@ impl RegexRule for VariableRule {
} }
} }
#[auto_registry::auto_registry(registry = "rules", path = "crate::elements::variable")]
pub struct VariableSubstitutionRule { pub struct VariableSubstitutionRule {
re: [Regex; 1], re: [Regex; 1],
} }
@ -309,6 +312,7 @@ impl VariableSubstitutionRule {
impl RegexRule for VariableSubstitutionRule { impl RegexRule for VariableSubstitutionRule {
fn name(&self) -> &'static str { "Variable Substitution" } fn name(&self) -> &'static str { "Variable Substitution" }
fn previous(&self) -> Option<&'static str> { Some("Variable") }
fn regexes(&self) -> &[regex::Regex] { &self.re } fn regexes(&self) -> &[regex::Regex] { &self.re }
@ -331,7 +335,7 @@ impl RegexRule for VariableSubstitutionRule {
.with_message("Empty variable name") .with_message("Empty variable name")
.with_label( .with_label(
Label::new((token.source(), matches.get(0).unwrap().range())) Label::new((token.source(), matches.get(0).unwrap().range()))
.with_message(format!("Missing variable name for substitution")) .with_message("Missing variable name for substitution".to_string())
.with_color(state.parser.colors().error), .with_color(state.parser.colors().error),
) )
.finish(), .finish(),
@ -346,7 +350,7 @@ impl RegexRule for VariableSubstitutionRule {
.with_message("Invalid variable name") .with_message("Invalid variable name")
.with_label( .with_label(
Label::new((token.source(), name.range())) Label::new((token.source(), name.range()))
.with_message(format!("Variable names contains leading spaces")) .with_message("Variable names contains leading spaces".to_string())
.with_color(state.parser.colors().error), .with_color(state.parser.colors().error),
) )
.with_help("Remove leading spaces") .with_help("Remove leading spaces")
@ -362,9 +366,7 @@ impl RegexRule for VariableSubstitutionRule {
.with_message("Invalid variable name") .with_message("Invalid variable name")
.with_label( .with_label(
Label::new((token.source(), name.range())) Label::new((token.source(), name.range()))
.with_message(format!( .with_message("Variable names contains trailing spaces".to_string())
"Variable names contains trailing spaces"
))
.with_color(state.parser.colors().error), .with_color(state.parser.colors().error),
) )
.with_help("Remove trailing spaces") .with_help("Remove trailing spaces")
@ -374,23 +376,20 @@ impl RegexRule for VariableSubstitutionRule {
return result; return result;
} }
// Invalid name // Invalid name
match VariableRule::validate_name(&state.parser.colors(), name.as_str()) { if let Err(msg) = VariableRule::validate_name(state.parser.colors(), name.as_str()) {
Err(msg) => { result.push(
result.push( Report::build(ReportKind::Error, token.source(), name.start())
Report::build(ReportKind::Error, token.source(), name.start()) .with_message("Invalid variable name")
.with_message("Invalid variable name") .with_label(
.with_label( Label::new((token.source(), name.range()))
Label::new((token.source(), name.range())) .with_message(msg)
.with_message(msg) .with_color(state.parser.colors().error),
.with_color(state.parser.colors().error), )
) .finish(),
.finish(), );
);
return result; return result;
} }
_ => {}
}
// Get variable // Get variable
match document.get_variable(name.as_str()) { match document.get_variable(name.as_str()) {
@ -418,6 +417,6 @@ impl RegexRule for VariableSubstitutionRule {
variable.parse(state, token, document); variable.parse(state, token, document);
return result; result
} }
} }

View file

@ -94,85 +94,3 @@ impl From<&LineCursor> for Cursor
} }
} }
} }
#[derive(Debug)]
pub struct LsParser
{
rules: Vec<Box<dyn Rule>>,
colors: ReportColors,
// Parser state
pub state: RefCell<StateHolder>,
pub kernels: RefCell<HashMap<String, Kernel>>,
}
impl Parser for LsParser
{
fn colors(&self) -> &ReportColors { &self.colors }
fn rules(&self) -> &Vec<Box<dyn Rule>> { &self.rules }
fn rules_mut(&mut self) -> &mut Vec<Box<dyn Rule>> { &mut self.rules }
fn state(&self) -> Ref<'_, StateHolder> { self.state.borrow() }
fn state_mut(&self) -> std::cell::RefMut<'_, StateHolder> { self.state.borrow_mut() }
fn has_error(&self) -> bool { true }
fn push<'a>(&self, doc: &dyn Document, elem: Box<dyn Element>) {
todo!()
}
fn parse<'a>(&self, source: Rc<dyn Source>, parent: Option<&'a dyn Document<'a>>) -> Box<dyn Document<'a>+'a> {
todo!()
}
fn parse_into<'a>(&self, source: Rc<dyn Source>, document: &'a dyn Document<'a>) {
todo!()
}
}
impl KernelHolder for LsParser
{
fn get_kernel(&self, name: &str)
-> Option<RefMut<'_, Kernel>> {
RefMut::filter_map(self.kernels.borrow_mut(),
|map| map.get_mut(name)).ok()
}
fn insert_kernel(&self, name: String, kernel: Kernel)
-> RefMut<'_, Kernel> {
//TODO do not get
self.kernels.borrow_mut()
.insert(name.clone(), kernel);
self.get_kernel(name.as_str()).unwrap()
}
}
impl StyleHolder for LsParser {
fn element_styles(&self) -> Ref<'_, HashMap<String, Rc<dyn ElementStyle>>> {
todo!()
}
fn element_styles_mut(&self) -> RefMut<'_, HashMap<String, Rc<dyn ElementStyle>>> {
todo!()
}
}
impl LayoutHolder for LsParser {
fn layouts(&self) -> Ref<'_, HashMap<String, Rc<dyn LayoutType>>> {
todo!()
}
fn layouts_mut(&self) -> RefMut<'_, HashMap<String, Rc<dyn LayoutType>>> {
todo!()
}
}
impl CustomStyleHolder for LsParser {
fn custom_styles(&self) -> Ref<'_, HashMap<String, Rc<dyn CustomStyle>>> {
todo!()
}
fn custom_styles_mut(&self) -> RefMut<'_, HashMap<String, Rc<dyn CustomStyle>>> {
todo!()
}
}

View file

@ -15,7 +15,7 @@ pub struct KernelContext<'a, 'b, 'c> {
} }
thread_local! { thread_local! {
pub static CTX: RefCell<Option<KernelContext<'static, 'static, 'static>>> = RefCell::new(None); pub static CTX: RefCell<Option<KernelContext<'static, 'static, 'static>>> = const { RefCell::new(None) };
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -8,24 +8,13 @@ mod parser;
use std::env; use std::env;
use std::io::BufWriter; use std::io::BufWriter;
use std::io::Write; use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode; use std::process::ExitCode;
use std::rc::Rc;
use std::time::UNIX_EPOCH;
use compiler::compiler::CompiledDocument;
use compiler::compiler::Compiler;
use compiler::compiler::Target; use compiler::compiler::Target;
use compiler::navigation::create_navigation; use compiler::navigation::create_navigation;
use document::document::Document;
use getopts::Options; use getopts::Options;
use parser::langparser::LangParser;
use parser::parser::Parser;
use parser::parser::ParserState;
use rusqlite::Connection;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::parser::source::SourceFile;
extern crate getopts; extern crate getopts;
fn print_usage(program: &str, opts: Options) { fn print_usage(program: &str, opts: Options) {
@ -47,126 +36,6 @@ NML version: 0.4\n"
); );
} }
fn parse(
parser: &LangParser,
input: &str,
debug_opts: &Vec<String>,
) -> Result<Box<dyn Document<'static>>, String> {
println!("Parsing {input}...");
// Parse
let source = SourceFile::new(input.to_string(), None).unwrap();
let (doc, _) = parser.parse(ParserState::new(parser, None), Rc::new(source), None);
if debug_opts.contains(&"ast".to_string()) {
println!("-- BEGIN AST DEBUGGING --");
doc.content()
.borrow()
.iter()
.for_each(|elem| println!("{elem:#?}"));
println!("-- END AST DEBUGGING --");
}
if debug_opts.contains(&"ref".to_string()) {
println!("-- BEGIN REFERENCES DEBUGGING --");
let sc = doc.scope().borrow();
sc.referenceable.iter().for_each(|(name, reference)| {
println!(" - {name}: `{:#?}`", doc.get_from_reference(reference));
});
println!("-- END REFERENCES DEBUGGING --");
}
if debug_opts.contains(&"var".to_string()) {
println!("-- BEGIN VARIABLES DEBUGGING --");
let sc = doc.scope().borrow();
sc.variables.iter().for_each(|(_name, var)| {
println!(" - `{:#?}`", var);
});
println!("-- END VARIABLES DEBUGGING --");
}
if parser.has_error() {
return Err("Parsing failed aborted due to errors while parsing".to_string());
}
Ok(doc)
}
fn process(
target: Target,
files: Vec<PathBuf>,
db_path: &Option<String>,
force_rebuild: bool,
debug_opts: &Vec<String>,
) -> Result<Vec<CompiledDocument>, String> {
let mut compiled = vec![];
let current_dir = std::env::current_dir()
.map_err(|err| format!("Unable to get the current working directory: {err}"))?;
let con = db_path
.as_ref()
.map_or(Connection::open_in_memory(), |path| Connection::open(path))
.map_err(|err| format!("Unable to open connection to the database: {err}"))?;
CompiledDocument::init_cache(&con)
.map_err(|err| format!("Failed to initialize cached document table: {err}"))?;
let parser = LangParser::default();
for file in files {
let meta = std::fs::metadata(&file)
.map_err(|err| format!("Failed to get metadata for `{file:#?}`: {err}"))?;
let modified = meta
.modified()
.map_err(|err| format!("Unable to query modification time for `{file:#?}`: {err}"))?;
// Move to file's directory
let file_parent_path = file
.parent()
.ok_or(format!("Failed to get parent path for `{file:#?}`"))?;
std::env::set_current_dir(file_parent_path)
.map_err(|err| format!("Failed to move to path `{file_parent_path:#?}`: {err}"))?;
let parse_and_compile = || -> Result<CompiledDocument, String> {
// Parse
let doc = parse(&parser, file.to_str().unwrap(), debug_opts)?;
// Compile
let compiler = Compiler::new(target, db_path.clone());
let mut compiled = compiler.compile(&*doc);
// Insert into cache
compiled.mtime = modified.duration_since(UNIX_EPOCH).unwrap().as_secs();
compiled.insert_cache(&con).map_err(|err| {
format!("Failed to insert compiled document from `{file:#?}` into cache: {err}")
})?;
Ok(compiled)
};
let cdoc = if force_rebuild {
parse_and_compile()?
} else {
match CompiledDocument::from_cache(&con, file.to_str().unwrap()) {
Some(compiled) => {
if compiled.mtime < modified.duration_since(UNIX_EPOCH).unwrap().as_secs() {
parse_and_compile()?
} else {
compiled
}
}
None => parse_and_compile()?,
}
};
compiled.push(cdoc);
}
std::env::set_current_dir(current_dir)
.map_err(|err| format!("Failed to set current directory: {err}"))?;
Ok(compiled)
}
fn main() -> ExitCode { fn main() -> ExitCode {
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
let program = args[0].clone(); let program = args[0].clone();
@ -344,19 +213,21 @@ fn main() -> ExitCode {
} }
// Parse, compile using the cache // Parse, compile using the cache
let compiled = match process(Target::HTML, files, &db_path, force_rebuild, &debug_opts) { let processed =
Ok(compiled) => compiled, match compiler::process::process(Target::HTML, files, &db_path, force_rebuild, &debug_opts)
Err(e) => { {
eprintln!("{e}"); Ok(processed) => processed,
return ExitCode::FAILURE; Err(e) => {
} eprintln!("{e}");
}; return ExitCode::FAILURE;
}
};
if input_meta.is_dir() if input_meta.is_dir()
// Batch mode // Batch mode
{ {
// Build navigation // Build navigation
let navigation = match create_navigation(&compiled) { let navigation = match create_navigation(&processed) {
Ok(nav) => nav, Ok(nav) => nav,
Err(e) => { Err(e) => {
eprintln!("{e}"); eprintln!("{e}");
@ -365,38 +236,54 @@ fn main() -> ExitCode {
}; };
// Output // Output
for doc in compiled { for (doc, _) in &processed {
let out_path = match doc let out_path = match doc
.borrow()
.get_variable("compiler.output") .get_variable("compiler.output")
.or(input_meta.is_file().then_some(&output)) .or(input_meta.is_file().then_some(&output))
{ {
Some(path) => path.clone(), Some(path) => path.clone(),
None => { None => {
eprintln!("Unable to get output file for `{}`", doc.input); eprintln!("Unable to get output file for `{}`", doc.borrow().input);
continue; continue;
} }
}; };
let nav = navigation.compile(Target::HTML, &doc); let nav = navigation.compile(Target::HTML, doc);
let file = std::fs::File::create(output.clone() + "/" + out_path.as_str()).unwrap(); let file = std::fs::File::create(output.clone() + "/" + out_path.as_str()).unwrap();
let mut writer = BufWriter::new(file); let mut writer = BufWriter::new(file);
write!(writer, "{}{}{}{}", doc.header, nav, doc.body, doc.footer).unwrap(); write!(
writer,
"{}{}{}{}",
doc.borrow().header,
nav,
doc.borrow().body,
doc.borrow().footer
)
.unwrap();
writer.flush().unwrap(); writer.flush().unwrap();
} }
} else } else
// Single file // Single file
{ {
for doc in compiled { for (doc, _) in &processed {
let file = std::fs::File::create(output.clone()).unwrap(); let file = std::fs::File::create(output.clone()).unwrap();
let mut writer = BufWriter::new(file); let mut writer = BufWriter::new(file);
write!(writer, "{}{}{}", doc.header, doc.body, doc.footer).unwrap(); write!(
writer,
"{}{}{}",
doc.borrow().header,
doc.borrow().body,
doc.borrow().footer
)
.unwrap();
writer.flush().unwrap(); writer.flush().unwrap();
} }
} }
return ExitCode::SUCCESS; ExitCode::SUCCESS
} }

View file

@ -45,8 +45,7 @@ pub struct CustomStyleHolder {
impl CustomStyleHolder { impl CustomStyleHolder {
pub fn get(&self, style_name: &str) -> Option<Rc<dyn CustomStyle>> { pub fn get(&self, style_name: &str) -> Option<Rc<dyn CustomStyle>> {
self.custom_styles self.custom_styles
.get(style_name) .get(style_name).cloned()
.map(|style| style.clone())
} }
pub fn insert(&mut self, style: Rc<dyn CustomStyle>) { pub fn insert(&mut self, style: Rc<dyn CustomStyle>) {

View file

@ -4,7 +4,6 @@ use std::rc::Rc;
use crate::document::document::Document; use crate::document::document::Document;
use crate::document::element::DocumentEnd; use crate::document::element::DocumentEnd;
use crate::document::langdocument::LangDocument; use crate::document::langdocument::LangDocument;
use crate::elements::registrar::register;
use crate::elements::text::Text; use crate::elements::text::Text;
use super::parser::Parser; use super::parser::Parser;
@ -35,8 +34,10 @@ impl LangParser {
}; };
// Register rules // Register rules
// TODO: use https://docs.rs/inventory/latest/inventory/ for rule in super::rule::get_rule_registry()
register(&mut s); {
s.add_rule(rule).unwrap();
}
s s
} }
@ -119,7 +120,7 @@ impl Parser for LangParser {
))), ))),
); );
return (Box::new(doc), state); (Box::new(doc), state)
} }
fn parse_into<'p, 'a, 'doc>( fn parse_into<'p, 'a, 'doc>(
@ -163,7 +164,7 @@ impl Parser for LangParser {
} }
} }
return state; state
// State // State
//self.handle_reports(source.clone(), //self.handle_reports(source.clone(),
// self.state_mut().on_scope_end(&self, &document, super::state::Scope::DOCUMENT)); // self.state_mut().on_scope_end(&self, &document, super::state::Scope::DOCUMENT));

View file

@ -36,7 +36,7 @@ pub struct LayoutHolder {
impl LayoutHolder { impl LayoutHolder {
pub fn get(&self, layout_name: &str) -> Option<Rc<dyn LayoutType>> { pub fn get(&self, layout_name: &str) -> Option<Rc<dyn LayoutType>> {
self.layouts.get(layout_name).map(|layout| layout.clone()) self.layouts.get(layout_name).cloned()
} }
pub fn insert(&mut self, layout: Rc<dyn LayoutType>) { pub fn insert(&mut self, layout: Rc<dyn LayoutType>) {

View file

@ -19,7 +19,6 @@ use crate::document::document::DocumentAccessors;
use crate::document::element::ContainerElement; use crate::document::element::ContainerElement;
use crate::document::element::ElemKind; use crate::document::element::ElemKind;
use crate::document::element::Element; use crate::document::element::Element;
use crate::elements::customstyle::CustomStyleRule;
use crate::elements::paragraph::Paragraph; use crate::elements::paragraph::Paragraph;
use crate::lua::kernel::Kernel; use crate::lua::kernel::Kernel;
use crate::lua::kernel::KernelHolder; use crate::lua::kernel::KernelHolder;
@ -90,8 +89,8 @@ impl SharedState {
// Default styles & layouts // Default styles & layouts
parser.rules().iter().for_each(|rule| { parser.rules().iter().for_each(|rule| {
rule.register_styles(&mut *s.styles.borrow_mut()); rule.register_styles(&mut s.styles.borrow_mut());
rule.register_layouts(&mut *s.layouts.borrow_mut()); rule.register_layouts(&mut s.layouts.borrow_mut());
}); });
s s
@ -186,7 +185,6 @@ impl<'a, 'b> ParserState<'a, 'b> {
.for_each(|(rule, (matched_at, match_data))| { .for_each(|(rule, (matched_at, match_data))| {
// Don't upate if not stepped over yet // Don't upate if not stepped over yet
if *matched_at > cursor.pos { if *matched_at > cursor.pos {
// TODO: maybe we should expose matches() so it becomes possible to dynamically register a new rule
return; return;
} }
@ -200,7 +198,7 @@ impl<'a, 'b> ParserState<'a, 'b> {
let mut escaped = false; let mut escaped = false;
'inner: loop { 'inner: loop {
let g = graphemes.next_back(); let g = graphemes.next_back();
if !g.is_some() || g.unwrap() != "\\" { if g.is_none() || g.unwrap() != "\\" {
break 'inner; break 'inner;
} }
@ -238,10 +236,10 @@ impl<'a, 'b> ParserState<'a, 'b> {
return (cursor.at(content.len()), None); return (cursor.at(content.len()), None);
} }
return ( (
cursor.at(next_pos), cursor.at(next_pos),
Some((winner, matches_borrow[winner].1.take().unwrap())), Some((winner, matches_borrow[winner].1.take().unwrap())),
); )
} }
/// Add an [`Element`] to the [`Document`] /// Add an [`Element`] to the [`Document`]
@ -264,7 +262,7 @@ impl<'a, 'b> ParserState<'a, 'b> {
if doc.last_element::<Paragraph>().is_some_and(|_| true) { if doc.last_element::<Paragraph>().is_some_and(|_| true) {
self.parser self.parser
.handle_reports(self.shared.rule_state.borrow_mut().on_scope_end( .handle_reports(self.shared.rule_state.borrow_mut().on_scope_end(
&self, self,
doc, doc,
super::state::Scope::PARAGRAPH, super::state::Scope::PARAGRAPH,
)); ));
@ -369,11 +367,11 @@ pub trait Parser {
/// # Warning /// # Warning
/// ///
/// This method must not be called if a [`ParserState`] for this parser exists. /// This method must not be called if a [`ParserState`] for this parser exists.
fn add_rule(&mut self, rule: Box<dyn Rule>, after: Option<&'static str>) -> Result<(), String> { fn add_rule(&mut self, rule: Box<dyn Rule>) -> Result<(), String> {
if let Some(_) = self if self
.rules() .rules()
.iter() .iter()
.find(|other_rule| other_rule.name() == rule.name()) .any(|other_rule| other_rule.name() == rule.name())
{ {
return Err(format!( return Err(format!(
"Attempted to introduce duplicate rule: `{}`", "Attempted to introduce duplicate rule: `{}`",
@ -381,23 +379,7 @@ pub trait Parser {
)); ));
} }
// Try to insert after self.rules_mut().push(rule);
if let Some(after) = after {
let index = self
.rules()
.iter()
.enumerate()
.find(|(_, rule)| rule.name() == after)
.map(|(idx, _)| idx);
if let Some(index) = index {
self.rules_mut().insert(index, rule);
} else {
return Err(format!("Unable to find rule `{after}` to insert after"));
}
} else {
self.rules_mut().push(rule);
}
Ok(()) Ok(())
} }
@ -409,14 +391,11 @@ pub trait Parser {
let mut sources: HashSet<Rc<dyn Source>> = HashSet::new(); let mut sources: HashSet<Rc<dyn Source>> = HashSet::new();
fn recurse_source(sources: &mut HashSet<Rc<dyn Source>>, source: Rc<dyn Source>) { fn recurse_source(sources: &mut HashSet<Rc<dyn Source>>, source: Rc<dyn Source>) {
sources.insert(source.clone()); sources.insert(source.clone());
match source.location() { if let Some(parent) = source.location() {
Some(parent) => { let parent_source = parent.source();
let parent_source = parent.source(); if sources.get(&parent_source).is_none() {
if sources.get(&parent_source).is_none() { recurse_source(sources, parent_source);
recurse_source(sources, parent_source);
}
} }
None => {}
} }
} }
@ -441,10 +420,8 @@ pub trait Parser {
if let Some(_s) = source.downcast_ref::<VirtualSource>() { if let Some(_s) = source.downcast_ref::<VirtualSource>() {
let start = location.start() let start = location.start()
+ (location.source().content().as_bytes()[location.start()] + if location.source().content().as_bytes()[location.start()]
== '\n' as u8) == b'\n' { 1 } else { 0 };
.then_some(1)
.unwrap_or(0);
report.labels.push( report.labels.push(
Label::new((location.source(), start..location.end())) Label::new((location.source(), start..location.end()))
.with_message("In evaluation of") .with_message("In evaluation of")

View file

@ -12,12 +12,66 @@ use mlua::Function;
use mlua::Lua; use mlua::Lua;
use std::any::Any; use std::any::Any;
use std::collections::HashMap;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
macro_rules! create_registry {
( $($construct:expr),+ $(,)? ) => {{
let mut map = HashMap::new();
$(
let boxed = Box::new($construct) as Box<dyn Rule>;
map.insert(boxed.name(), boxed);
)+
map
}};
}
/// Gets the list of all rules exported with the [`auto_registry`] proc macro.
/// Rules are sorted according to topological order using the [`Rule::previous`] method.
#[auto_registry::generate_registry(registry = "rules", target = make_rules, return_type = HashMap<&'static str, Box<dyn Rule>>, maker = create_registry)]
pub fn get_rule_registry() -> Vec<Box<dyn Rule>> {
fn cmp(
map: &HashMap<&'static str, Box<dyn Rule>>,
lname: &'static str,
rname: &'static str,
) -> std::cmp::Ordering {
let l = map.get(lname).unwrap();
let r = map.get(rname).unwrap();
if l.previous() == Some(r.name()) {
std::cmp::Ordering::Greater
} else if r.previous() == Some(l.name()) {
std::cmp::Ordering::Less
} else if l.previous().is_some() && r.previous().is_none() {
std::cmp::Ordering::Greater
} else if r.previous().is_some() && l.previous().is_none() {
std::cmp::Ordering::Less
} else if let (Some(pl), Some(pr)) = (l.previous(), r.previous()) {
cmp(map, pl, pr)
} else {
std::cmp::Ordering::Equal
}
}
let mut map = make_rules();
let mut sorted_keys = map.keys().copied().collect::<Vec<_>>();
sorted_keys.sort_by(|l, r| cmp(&map, l, r));
let mut owned = Vec::with_capacity(sorted_keys.len());
for key in sorted_keys {
let rule = map.remove(key).unwrap();
owned.push(rule);
}
owned
}
pub trait Rule: Downcast { pub trait Rule: Downcast {
/// Returns rule's name /// The rule name
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
/// The name of the rule that should come before this one
fn previous(&self) -> Option<&'static str>;
/// Finds the next match starting from [`cursor`] /// Finds the next match starting from [`cursor`]
fn next_match(&self, state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)>; fn next_match(&self, state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)>;
/// Callback when rule matches /// Callback when rule matches
@ -47,8 +101,12 @@ impl core::fmt::Debug for dyn Rule {
} }
pub trait RegexRule { pub trait RegexRule {
/// The rule name
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
/// The name of the rule that should come before this one
fn previous(&self) -> Option<&'static str>;
/// Returns the rule's regexes /// Returns the rule's regexes
fn regexes(&self) -> &[regex::Regex]; fn regexes(&self) -> &[regex::Regex];
@ -69,6 +127,7 @@ pub trait RegexRule {
impl<T: RegexRule + 'static> Rule for T { impl<T: RegexRule + 'static> Rule for T {
fn name(&self) -> &'static str { RegexRule::name(self) } fn name(&self) -> &'static str { RegexRule::name(self) }
fn previous(&self) -> Option<&'static str> { RegexRule::previous(self) }
/// Finds the next match starting from [`cursor`] /// Finds the next match starting from [`cursor`]
fn next_match(&self, _state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> { fn next_match(&self, _state: &ParserState, cursor: &Cursor) -> Option<(usize, Box<dyn Any>)> {
@ -77,18 +136,18 @@ impl<T: RegexRule + 'static> Rule for T {
self.regexes().iter().enumerate().for_each(|(id, re)| { self.regexes().iter().enumerate().for_each(|(id, re)| {
if let Some(m) = re.find_at(content.as_str(), cursor.pos) { if let Some(m) = re.find_at(content.as_str(), cursor.pos) {
found = found found = found
.and_then(|(f_pos, f_id)| { .map(|(f_pos, f_id)| {
if f_pos > m.start() { if f_pos > m.start() {
Some((m.start(), id)) (m.start(), id)
} else { } else {
Some((f_pos, f_id)) (f_pos, f_id)
} }
}) })
.or(Some((m.start(), id))); .or(Some((m.start(), id)));
} }
}); });
return found.map(|(pos, id)| (pos, Box::new(id) as Box<dyn Any>)); found.map(|(pos, id)| (pos, Box::new(id) as Box<dyn Any>))
} }
fn on_match<'a>( fn on_match<'a>(
@ -120,3 +179,42 @@ impl<T: RegexRule + 'static> Rule for T {
fn register_layouts(&self, holder: &mut LayoutHolder) { self.register_layouts(holder); } fn register_layouts(&self, holder: &mut LayoutHolder) { self.register_layouts(holder); }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry() {
let rules = get_rule_registry();
let names: Vec<&'static str> = rules.iter().map(|rule| rule.name()).collect();
assert_eq!(
names,
vec![
"Comment",
"Paragraph",
"Import",
"Script",
"Element Style",
"Variable",
"Variable Substitution",
"Raw",
"List",
"Blockquote",
"Code",
"Tex",
"Graphviz",
"Media",
"Layout",
"Style",
"Custom Style",
"Section",
"Link",
"Text",
"Reference",
]
);
}
}

View file

@ -24,7 +24,6 @@ impl core::fmt::Display for dyn Source {
} }
impl core::fmt::Debug for dyn Source { impl core::fmt::Debug for dyn Source {
// TODO
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Source{{{}}}", self.name()) write!(f, "Source{{{}}}", self.name())
} }
@ -55,10 +54,10 @@ impl SourceFile {
pub fn new(path: String, location: Option<Token>) -> Result<Self, String> { pub fn new(path: String, location: Option<Token>) -> Result<Self, String> {
match fs::read_to_string(&path) { match fs::read_to_string(&path) {
Err(_) => { Err(_) => {
return Err(String::from(format!( Err(format!(
"Unable to read file content: `{}`", "Unable to read file content: `{}`",
path path
))) ))
} }
Ok(content) => Ok(Self { Ok(content) => Ok(Self {
location, location,
@ -70,9 +69,9 @@ impl SourceFile {
pub fn with_content(path: String, content: String, location: Option<Token>) -> Self { pub fn with_content(path: String, content: String, location: Option<Token>) -> Self {
Self { Self {
location: location, location,
path: path, path,
content: content, content,
} }
} }
} }
@ -162,7 +161,7 @@ impl Token {
} }
pub fn source(&self) -> Rc<dyn Source> { pub fn source(&self) -> Rc<dyn Source> {
return self.source.clone(); self.source.clone()
} }
/// Construct Token from a range /// Construct Token from a range
@ -176,10 +175,10 @@ impl Token {
} }
pub fn start(&self) -> usize { pub fn start(&self) -> usize {
return self.range.start; self.range.start
} }
pub fn end(&self) -> usize { pub fn end(&self) -> usize {
return self.range.end; self.range.end
} }
} }

View file

@ -64,7 +64,7 @@ impl RuleStateHolder {
} }
pub fn get(&self, state_name: &str) -> Option<Rc<RefCell<dyn RuleState>>> { pub fn get(&self, state_name: &str) -> Option<Rc<RefCell<dyn RuleState>>> {
self.states.get(state_name).map(|state| state.clone()) self.states.get(state_name).cloned()
} }
pub fn on_scope_end( pub fn on_scope_end(
@ -88,6 +88,6 @@ impl RuleStateHolder {
} }
}); });
return reports; reports
} }
} }

View file

@ -38,7 +38,7 @@ impl StyleHolder {
/// NOTE: Will panic if a style is not defined for a given element /// NOTE: Will panic if a style is not defined for a given element
/// If you need to process user input, use [`is_registered`] /// If you need to process user input, use [`is_registered`]
pub fn current(&self, style_key: &str) -> Rc<dyn ElementStyle> { pub fn current(&self, style_key: &str) -> Rc<dyn ElementStyle> {
self.styles.get(style_key).map(|rc| rc.clone()).unwrap() self.styles.get(style_key).cloned().unwrap()
} }
/// Sets the [`style`] /// Sets the [`style`]
@ -50,17 +50,17 @@ impl StyleHolder {
#[macro_export] #[macro_export]
macro_rules! impl_elementstyle { macro_rules! impl_elementstyle {
($t:ty, $key:expr) => { ($t:ty, $key:expr) => {
impl crate::parser::style::ElementStyle for $t { impl $crate::parser::style::ElementStyle for $t {
fn key(&self) -> &'static str { $key } fn key(&self) -> &'static str { $key }
fn from_json( fn from_json(
&self, &self,
json: &str, json: &str,
) -> Result<std::rc::Rc<dyn crate::parser::style::ElementStyle>, String> { ) -> Result<std::rc::Rc<dyn $crate::parser::style::ElementStyle>, String> {
serde_json::from_str::<$t>(json) serde_json::from_str::<$t>(json)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
.map(|obj| { .map(|obj| {
std::rc::Rc::new(obj) as std::rc::Rc<dyn crate::parser::style::ElementStyle> std::rc::Rc::new(obj) as std::rc::Rc<dyn $crate::parser::style::ElementStyle>
}) })
} }
@ -68,9 +68,9 @@ macro_rules! impl_elementstyle {
&self, &self,
lua: &mlua::Lua, lua: &mlua::Lua,
value: mlua::Value, value: mlua::Value,
) -> Result<std::rc::Rc<dyn crate::parser::style::ElementStyle>, mlua::Error> { ) -> Result<std::rc::Rc<dyn $crate::parser::style::ElementStyle>, mlua::Error> {
mlua::LuaSerdeExt::from_value::<$t>(lua, value).map(|obj| { mlua::LuaSerdeExt::from_value::<$t>(lua, value).map(|obj| {
std::rc::Rc::new(obj) as std::rc::Rc<dyn crate::parser::style::ElementStyle> std::rc::Rc::new(obj) as std::rc::Rc<dyn $crate::parser::style::ElementStyle>
}) })
} }
} }

View file

@ -36,7 +36,7 @@ pub fn process_text(document: &dyn Document, content: &str) -> String {
.last_element::<Paragraph>() .last_element::<Paragraph>()
.and_then(|par| { .and_then(|par| {
par.find_back(|e| e.kind() != ElemKind::Invisible) par.find_back(|e| e.kind() != ElemKind::Invisible)
.and_then(|e| Some(e.kind() == ElemKind::Inline)) .map(|e| e.kind() == ElemKind::Inline)
}) })
.unwrap_or(false) .unwrap_or(false)
{ {
@ -79,12 +79,12 @@ pub fn process_text(document: &dyn Document, content: &str) -> String {
} }
} }
return (out + g, Some(g)); (out + g, Some(g))
}) })
.0 .0
.to_string(); .to_string();
return processed; processed
} }
/// Processed a string and escapes a single token out of it /// Processed a string and escapes a single token out of it
@ -111,7 +111,7 @@ pub fn process_escaped<S: AsRef<str>>(escape: char, token: &'static str, content
escaped += 1; escaped += 1;
} else if escaped % 2 == 1 && token_it.peek().map_or(false, |p| *p == c) { } else if escaped % 2 == 1 && token_it.peek().map_or(false, |p| *p == c) {
let _ = token_it.next(); let _ = token_it.next();
if token_it.peek() == None { if token_it.peek().is_none() {
(0..(escaped / 2)).for_each(|_| processed.push(escape)); (0..(escaped / 2)).for_each(|_| processed.push(escape));
escaped = 0; escaped = 0;
token_it = token.chars().peekable(); token_it = token.chars().peekable();
@ -333,9 +333,8 @@ impl PropertyParser {
escaped = 0; escaped = 0;
in_name = true; in_name = true;
if let Err(e) = try_insert(&name, &value) { try_insert(&name, &value)?;
return Err(e);
}
name.clear(); name.clear();
value.clear(); value.clear();
} else { } else {
@ -361,9 +360,7 @@ impl PropertyParser {
return Err("Expected non empty property list.".to_string()); return Err("Expected non empty property list.".to_string());
} }
if let Err(e) = try_insert(&name, &value) { try_insert(&name, &value)?;
return Err(e);
}
if let Err(e) = self.properties.iter().try_for_each(|(key, prop)| { if let Err(e) = self.properties.iter().try_for_each(|(key, prop)| {
if !properties.properties.contains_key(key) { if !properties.properties.contains_key(key) {
@ -423,7 +420,10 @@ mod tests {
(&doc as &dyn Document) (&doc as &dyn Document)
.last_element_mut::<Paragraph>() .last_element_mut::<Paragraph>()
.unwrap() .unwrap()
.push(Box::new(Comment::new(tok.clone(), "COMMENT".to_string()))) .push(Box::new(Comment {
location: tok.clone(),
content: "COMMENT".into(),
}))
.unwrap(); .unwrap();
assert_eq!(process_text(&doc, "\na"), "a"); assert_eq!(process_text(&doc, "\na"), "a");

View file

@ -137,6 +137,11 @@ a.inline-code {
content: ""; content: "";
} }
/* Sections */
a.section-link {
text-decoration: none;
}
/* Code blocks */ /* Code blocks */
div.code-block-title { div.code-block-title {
background-color: #20202a; background-color: #20202a;
@ -196,7 +201,7 @@ div.code-block-content .code-block-line {
margin-right: .5em; margin-right: .5em;
} }
.medium img { .medium img, video, audio {
max-width: 100%; max-width: 100%;
} }
@ -222,6 +227,7 @@ a.medium-ref {
font-weight: bold; font-weight: bold;
color: #d367c1; color: #d367c1;
text-decoration: none;
} }
a.medium-ref:hover { a.medium-ref:hover {
@ -233,11 +239,50 @@ a.medium-ref img {
margin: 1.3em 0 0 0; margin: 1.3em 0 0 0;
} }
a.medium-ref video {
display: none;
margin: 1.3em 0 0 0;
}
a:hover.medium-ref img { a:hover.medium-ref img {
max-width: 50%; max-width: 25%;
margin: auto; left: 37.5%;
display: inline-block; display: inline-block;
position: absolute; position: absolute;
box-shadow: 0px 0px 6px 2px rgba(0, 0, 0, 0.75); box-shadow: 0px 0px 6px 2px rgba(0, 0, 0, 0.75);
} }
a:hover.medium-ref video {
max-width: 25%;
left: 37.5%;
display: inline-block;
position: absolute;
box-shadow: 0px 0px 6px 2px rgba(0, 0, 0, 0.75);
}
/* Blockquote */
blockquote {
margin-left: 0.2em;
padding-left: 0.6em;
border-left: 4px solid #0ff08b;
}
blockquote p::before {
content: '\201C';
}
blockquote p::after {
content: '\201D';
}
.blockquote-author:before {
content: '—';
}
.blockquote-author {
margin-left: 0.2em;
}