Compare commits
10 commits
92bab91020
...
921df96c95
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
921df96c95 | ||
|
|
6244dd4d0f | ||
|
|
5e5ce9541c | ||
|
|
660f892d6c | ||
|
|
ad35f289c7 | ||
|
|
f02d8e419e | ||
|
|
32c9bc5b40 | ||
|
|
89e03c8cb6 | ||
|
|
25c3d008cc | ||
|
|
16e3b04b4e |
29 changed files with 1957 additions and 265 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
|
@ -8,6 +8,18 @@ version = "1.0.95"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
|
|
@ -19,5 +31,7 @@ name = "vis"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
"once_cell",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,3 +6,5 @@ edition = "2021"
|
|||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
unicode-segmentation = "1.1"
|
||||
once_cell = "1.20.2"
|
||||
log = "0.4"
|
||||
40
README.md
40
README.md
|
|
@ -4,12 +4,10 @@
|
|||
|
||||
_It is Dutch for fish_
|
||||
|
||||
### NB I just started, needs a lot of work
|
||||
|
||||
sample vis file:
|
||||
|
||||
```
|
||||
markup {
|
||||
structure {
|
||||
lanes {
|
||||
functions {
|
||||
calc: "Calculation"
|
||||
|
|
@ -32,39 +30,23 @@ markup {
|
|||
interest_engine: "InterestEngine"
|
||||
}
|
||||
}
|
||||
bank-->calc
|
||||
bank_scripts--<>bank_db
|
||||
bank_motor--<>bank_db
|
||||
interest_engine-->calc
|
||||
bank==>calc
|
||||
bank_scripts==<>bank_db
|
||||
bank_motor==<>bank_db
|
||||
interest_engine==>calc
|
||||
}
|
||||
|
||||
styles {
|
||||
lanes(group) {
|
||||
type: textnode
|
||||
orientation: horizontal
|
||||
orientation: vertical
|
||||
shape: rectangle
|
||||
font-family: arial
|
||||
border-width: 1px
|
||||
border-color: gray
|
||||
}
|
||||
functions(group) {
|
||||
background-color: yellow
|
||||
font-family: arial
|
||||
border-radius: 20px
|
||||
border-width: 1px
|
||||
border-color: gray
|
||||
}
|
||||
systems(group) {
|
||||
background-color: lightblue
|
||||
}
|
||||
tag1: "⚭" { // how will this work?
|
||||
right:0px
|
||||
top:0px
|
||||
}
|
||||
tag2: {
|
||||
itchy: scratchy
|
||||
}
|
||||
functions(group) {orientation: horizontal}
|
||||
systems(group) {orientation: horizontal}
|
||||
}
|
||||
```
|
||||
|
||||
Will have to be turned into an architecture diagram... we'll see how it goes!
|
||||
#### Output (work in progress):
|
||||
|
||||

|
||||
89
examples/bank_architecture/bank.svg
Normal file
89
examples/bank_architecture/bank.svg
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="400" viewBox="0 0 980 255">
|
||||
<style>svg {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.node {
|
||||
border: 1px solid black;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
font-size: 10px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
</style>
|
||||
<rect id="calc" x="60" y="60" width="61" height="15" stroke="white" fill="none"/>
|
||||
<text x="90" y="60" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="90" dy="10">Calculation</tspan>
|
||||
</text>
|
||||
<rect id="acc_interest_calc" x="136" y="60" width="145" height="15" stroke="white" fill="none"/>
|
||||
<text x="208" y="60" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="208" dy="10">Account interest Calculation</tspan>
|
||||
</text>
|
||||
<rect id="interest_rates" x="296" y="60" width="76" height="15" stroke="white" fill="none"/>
|
||||
<text x="334" y="60" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="334" dy="10">Interest Rates</tspan>
|
||||
</text>
|
||||
<rect id="config" x="387" y="60" width="71" height="15" stroke="white" fill="none"/>
|
||||
<text x="422" y="60" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="422" dy="10">Configuration</tspan>
|
||||
</text>
|
||||
<rect id="nob_execution" x="488" y="75" width="71" height="15" stroke="white" fill="none"/>
|
||||
<text x="523" y="75" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="523" dy="10">NoB Execution</tspan>
|
||||
</text>
|
||||
<rect id="coll_reinst_inst" x="574" y="75" width="204" height="15" stroke="white" fill="none"/>
|
||||
<text x="676" y="75" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="676" dy="10">Collection of Reinstatement instructions</tspan>
|
||||
</text>
|
||||
<rect id="nob" x="473" y="60" width="320" height="45" stroke="white" fill="none"/>
|
||||
<text x="633" y="60" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="633" dy="10">NoB</tspan>
|
||||
</text>
|
||||
<rect id="reporting" x="808" y="60" width="52" height="15" stroke="white" fill="none"/>
|
||||
<text x="834" y="60" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="834" dy="10">Reporting</tspan>
|
||||
</text>
|
||||
<rect id="functions" x="45" y="45" width="830" height="75" stroke="white" fill="none"/>
|
||||
<text x="460" y="45" fill="white" text-anchor="middle" stroke-width="1" class="node"></text>
|
||||
<rect id="bank_motor" x="75" y="165" width="57" height="15" stroke="white" fill="none"/>
|
||||
<text x="103" y="165" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="103" dy="10">Bank Motor</tspan>
|
||||
</text>
|
||||
<rect id="bank_scripts" x="147" y="165" width="66" height="15" stroke="white" fill="none"/>
|
||||
<text x="180" y="165" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="180" dy="10">Bank Scripts</tspan>
|
||||
</text>
|
||||
<rect id="bank_client" x="228" y="165" width="61" height="15" stroke="white" fill="none"/>
|
||||
<text x="258" y="165" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="258" dy="10">Bank Client</tspan>
|
||||
</text>
|
||||
<rect id="bank_db" x="304" y="165" width="42" height="15" stroke="white" fill="none"/>
|
||||
<text x="325" y="165" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="325" dy="10">Bank DB</tspan>
|
||||
</text>
|
||||
<rect id="bank" x="60" y="150" width="301" height="45" stroke="white" fill="none"/>
|
||||
<text x="210" y="150" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="210" dy="10">Bank</tspan>
|
||||
</text>
|
||||
<rect id="interest_engine" x="376" y="150" width="76" height="15" stroke="white" fill="none"/>
|
||||
<text x="414" y="150" fill="white" text-anchor="middle" stroke-width="1" class="node">
|
||||
<tspan x="414" dy="10">InterestEngine</tspan>
|
||||
</text>
|
||||
<rect id="systems" x="45" y="135" width="422" height="75" stroke="white" fill="none"/>
|
||||
<text x="256" y="45" fill="white" text-anchor="middle" stroke-width="1" class="node"></text>
|
||||
<rect id="lanes" x="30" y="30" width="860" height="195" stroke="white" fill="none"/>
|
||||
<text x="460" y="30" fill="white" text-anchor="middle" stroke-width="1" class="node"></text>
|
||||
<rect id="bank" x="905" y="30" width="0" height="0" stroke="white" fill="none"/>
|
||||
<text x="905" y="30" fill="white" text-anchor="middle" stroke-width="1" class="node"></text>
|
||||
<rect id="bank_scripts" x="920" y="30" width="0" height="0" stroke="white" fill="none"/>
|
||||
<text x="920" y="30" fill="white" text-anchor="middle" stroke-width="1" class="node"></text>
|
||||
<rect id="bank_motor" x="935" y="30" width="0" height="0" stroke="white" fill="none"/>
|
||||
<text x="935" y="30" fill="white" text-anchor="middle" stroke-width="1" class="node"></text>
|
||||
<rect id="interest_engine" x="950" y="30" width="0" height="0" stroke="white" fill="none"/>
|
||||
<text x="950" y="30" fill="white" text-anchor="middle" stroke-width="1" class="node"></text>
|
||||
<rect id="structure" x="15" y="15" width="950" height="225" stroke="white" fill="none"/>
|
||||
<text x="490" y="15" fill="white" text-anchor="middle" stroke-width="1" class="node"></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
|
|
@ -21,36 +21,18 @@ structure {
|
|||
interest_engine: "InterestEngine"
|
||||
}
|
||||
}
|
||||
bank-->calc
|
||||
bank_scripts--<>bank_db
|
||||
bank_motor--<>bank_db
|
||||
interest_engine-->calc
|
||||
bank==>calc
|
||||
bank_scripts==<>bank_db
|
||||
bank_motor==<>bank_db
|
||||
interest_engine==>calc
|
||||
}
|
||||
|
||||
styles {
|
||||
lanes(group) {
|
||||
type: textnode
|
||||
orientation: horizontal
|
||||
orientation: vertical
|
||||
shape: rectangle
|
||||
font-family: arial
|
||||
border-width: 1px
|
||||
border-color: gray
|
||||
}
|
||||
functions(group) {
|
||||
background-color: yellow
|
||||
font-family: arial
|
||||
border-radius: 20px
|
||||
border-width: 1px
|
||||
border-color: gray
|
||||
}
|
||||
systems(group) {
|
||||
background-color: lightblue
|
||||
}
|
||||
tag1: "⚭" { // how will this work?
|
||||
right:0px
|
||||
top:0px
|
||||
}
|
||||
tag2: {
|
||||
itchy: scratchy
|
||||
}
|
||||
functions(group) {orientation: horizontal}
|
||||
systems(group) {orientation: horizontal}
|
||||
}
|
||||
|
|
|
|||
BIN
src/.DS_Store
vendored
Normal file
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
218
src/lib.rs
218
src/lib.rs
|
|
@ -1,32 +1,220 @@
|
|||
pub mod parse;
|
||||
pub mod render;
|
||||
|
||||
use crate::parse::tokens::TokenType;
|
||||
use log::debug;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
|
||||
use tokens::TokenType;
|
||||
|
||||
pub mod parser;
|
||||
mod scanner;
|
||||
mod tokens;
|
||||
pub const BODY: &str = "structure";
|
||||
pub const DEFAULT_ORIENTATION: &str = "horizontal";
|
||||
pub const ORIENTATION: &str = "orientation";
|
||||
pub const HORIZONTAL: &str = "horizontal";
|
||||
pub const VERTICAL: &str = "vertical";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Vis {
|
||||
pub structure: Vec<Element>,
|
||||
pub structure: Vec<VisNode>,
|
||||
pub styles: Vec<StyleNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Element {
|
||||
Node(String, Option<String>, Vec<Element>),
|
||||
Edge(String, String, TokenType, Option<String>),
|
||||
impl Vis {
|
||||
pub fn new(structure: VisNode, styles: Vec<StyleNode>) -> Self {
|
||||
let mut vis = Self {
|
||||
structure: vec![structure],
|
||||
styles,
|
||||
};
|
||||
|
||||
// set defaults
|
||||
let bodystyle = vis.get_style_node_mut(BODY);
|
||||
if let Some(bodystyle) = bodystyle {
|
||||
bodystyle.set_attribute_if_absent(ORIENTATION, DEFAULT_ORIENTATION);
|
||||
} else {
|
||||
let mut bodystyle = StyleNode::new(BODY, ContainerType::Group);
|
||||
bodystyle.set_attribute(ORIENTATION, DEFAULT_ORIENTATION);
|
||||
vis.styles.push(bodystyle);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
vis
|
||||
}
|
||||
|
||||
pub fn get_node(&self, id: impl Into<String>) -> Option<&VisNode> {
|
||||
Self::get_node_recurse(id, &self.structure)
|
||||
}
|
||||
|
||||
fn get_node_recurse(id: impl Into<String>, elements: &Vec<VisNode>) -> Option<&VisNode> {
|
||||
let id = id.into();
|
||||
for element in elements {
|
||||
if let Some(e) = Self::get_node_recurse(id.clone(), &element.children) {
|
||||
return Some(e);
|
||||
} else if element.id == id {
|
||||
return Some(element);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_style_node_mut(&mut self, id_ref: &str) -> Option<&mut StyleNode> {
|
||||
self.styles.iter_mut().find(|s| s.id_ref == id_ref)
|
||||
}
|
||||
|
||||
pub fn get_style_value(&self, idref: impl Into<String>, key: &str) -> Option<String> {
|
||||
self.get_styles(idref).get(key).map(String::to_string)
|
||||
}
|
||||
|
||||
pub fn get_styles(&self, id: impl Into<String>) -> HashMap<String, String> {
|
||||
// println!("get_styles {:?}", id);
|
||||
let mut styles = HashMap::new();
|
||||
self.get_styles_recurse(id, &mut styles);
|
||||
debug!("found {:?}", styles);
|
||||
styles
|
||||
}
|
||||
|
||||
fn get_styles_recurse(&self, id: impl Into<String>, styles: &mut HashMap<String, String>) {
|
||||
let id = id.into();
|
||||
debug!("get style for {}", id);
|
||||
let node = self.get_node(id);
|
||||
if let Some(node) = node {
|
||||
// println!("node {:?}", node);
|
||||
let style = self.styles.iter().find(|s| s.id_ref == node.id);
|
||||
if let Some(style) = style {
|
||||
style.attributes.iter().for_each(|(k, v)| {
|
||||
if !styles.contains_key(k) {
|
||||
styles.insert(k.clone(), v.clone());
|
||||
};
|
||||
});
|
||||
}
|
||||
if let Some(parent) = &node.parent {
|
||||
self.get_styles_recurse(parent, styles);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum NodeType {
|
||||
Node,
|
||||
Edge,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VisNode {
|
||||
pub node_type: NodeType,
|
||||
pub id: String,
|
||||
targetid: Option<String>,
|
||||
pub label: Option<String>,
|
||||
pub children: Vec<VisNode>,
|
||||
pub parent: Option<String>,
|
||||
edgetype: Option<TokenType>,
|
||||
}
|
||||
|
||||
impl VisNode {
|
||||
pub fn new_node(
|
||||
id: impl Into<String>,
|
||||
label: Option<impl Into<String>>,
|
||||
children: Vec<VisNode>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
targetid: None,
|
||||
label: label.map(|s| s.into()),
|
||||
children,
|
||||
parent: None,
|
||||
node_type: NodeType::Node,
|
||||
edgetype: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_edge(
|
||||
sourceid: impl Into<String>,
|
||||
targetid: impl Into<String>,
|
||||
edgetype: TokenType,
|
||||
label: Option<impl Into<String>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: sourceid.into(),
|
||||
targetid: Some(targetid.into()),
|
||||
edgetype: Some(edgetype),
|
||||
label: label.map(|l| l.into()),
|
||||
children: vec![],
|
||||
parent: None,
|
||||
node_type: NodeType::Edge,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for VisNode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.node_type {
|
||||
NodeType::Node => {
|
||||
let mut string = String::new();
|
||||
string.push_str(&format!(
|
||||
"Node {{{}: {}",
|
||||
self.id,
|
||||
self.label.as_ref().unwrap_or(&"".to_string())
|
||||
));
|
||||
for child in &self.children {
|
||||
string.push_str(&format!(" {}", child));
|
||||
}
|
||||
string.push('}');
|
||||
write!(f, "{}", string)
|
||||
}
|
||||
NodeType::Edge => {
|
||||
write!(f, "Edge {{")?;
|
||||
write!(
|
||||
f,
|
||||
"{} {} {:?}",
|
||||
self.id,
|
||||
self.targetid.as_ref().unwrap(),
|
||||
self.edgetype
|
||||
)?;
|
||||
if let Some(label) = &self.label {
|
||||
write!(f, " {}", label)?;
|
||||
}
|
||||
write!(f, "}}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StyleNode {
|
||||
pub id_ref: String,
|
||||
pub containertype: ContainerType,
|
||||
pub attributes: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ContainerType {
|
||||
Node,
|
||||
Group,
|
||||
impl StyleNode {
|
||||
fn new(id_ref: impl Into<String>, containertype: ContainerType) -> Self {
|
||||
Self {
|
||||
id_ref: id_ref.into(),
|
||||
containertype,
|
||||
attributes: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_attribute(&self, key: &str) -> bool {
|
||||
self.attributes.contains_key(key)
|
||||
}
|
||||
pub fn get_attribute(&self, key: &str) -> Option<&String> {
|
||||
self.attributes.get(key)
|
||||
}
|
||||
pub fn get_attribute_string(&self, key: &str) -> Option<String> {
|
||||
self.attributes.get(key).map(|s| s.to_owned())
|
||||
}
|
||||
pub fn set_attribute(&mut self, key: impl Into<String>, value: impl Into<String>) {
|
||||
self.attributes.insert(key.into(), value.into());
|
||||
}
|
||||
|
||||
pub fn set_attribute_if_absent(&mut self, key: impl Into<String>, value: impl Into<String>) {
|
||||
let key = key.into();
|
||||
if !self.has_attribute(&key) {
|
||||
self.set_attribute(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ContainerType {
|
||||
NonGroup, // needs thinking about
|
||||
Group, // the idea is that group nodes don't have style, they just bequeath it to their children
|
||||
}
|
||||
|
|
|
|||
15
src/main.rs
15
src/main.rs
|
|
@ -1,15 +1,24 @@
|
|||
use anyhow::anyhow;
|
||||
use std::env::args;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::process::exit;
|
||||
use vis::render::svg_renderer::SvgRender;
|
||||
use vis::render::Renderer;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args: Vec<String> = args().collect();
|
||||
if args.len() != 2 {
|
||||
return Err(anyhow!("Usage: vis vis-file"));
|
||||
eprintln!("Usage: vis vis-file");
|
||||
exit(-64);
|
||||
} else {
|
||||
let vis_file = read_file(&args[1])?;
|
||||
let vis = vis::parser::parse_vis(vis_file.as_str())?;
|
||||
println!("{:?}", vis);
|
||||
let vis = vis::parse::parse_vis(vis_file.as_str())?;
|
||||
// println!("{:?}", vis);
|
||||
let svg_bytes = SvgRender {}.render(vis)?;
|
||||
let mut file = File::create("bank.svg").expect("Unable to create file");
|
||||
file.write_all(&svg_bytes).expect("Unable to write data");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
5
src/parse/mod.rs
Normal file
5
src/parse/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod scanner;
|
||||
pub mod tokens;
|
||||
pub mod parser;
|
||||
|
||||
pub use parser::parse_vis;
|
||||
325
src/parse/parser.rs
Normal file
325
src/parse/parser.rs
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
use crate::{
|
||||
parse::tokens::{
|
||||
Token,
|
||||
TokenType::{self, *},
|
||||
},
|
||||
ContainerType, StyleNode, Vis, VisNode,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use log::debug;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn parse_vis(contents: &str) -> anyhow::Result<Vis> {
|
||||
let tokens = crate::parse::scanner::scan(contents)?;
|
||||
debug!("{:?}", tokens);
|
||||
let mut parser = Parser::new(tokens);
|
||||
|
||||
let structure = parser.structure()?;
|
||||
let styles = parser.styles()?;
|
||||
debug!("parsed styles{:?}", styles);
|
||||
let mut vis = Vis::new(
|
||||
VisNode::new_node("structure", None::<String>, structure),
|
||||
styles,
|
||||
);
|
||||
|
||||
// add bottom up references
|
||||
vis.structure.iter_mut().for_each(|node| {
|
||||
let c = node.clone();
|
||||
node.children.iter_mut().for_each(|child| {
|
||||
child.parent.replace(c.id.to_owned());
|
||||
});
|
||||
});
|
||||
Ok(vis)
|
||||
}
|
||||
|
||||
struct Parser {
|
||||
tokens: Vec<Token>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new(tokens: Vec<Token>) -> Self {
|
||||
Self { tokens, current: 0 }
|
||||
}
|
||||
|
||||
fn structure(&mut self) -> anyhow::Result<Vec<VisNode>> {
|
||||
if self.match_token(Structure) {
|
||||
self.elements()
|
||||
} else {
|
||||
println!("No structure found");
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
fn elements(&mut self) -> anyhow::Result<Vec<VisNode>> {
|
||||
debug!("nodes {:?}", self.peek());
|
||||
self.consume(LeftBrace, "Expected '{'")?;
|
||||
let mut nodes = vec![];
|
||||
while !self.match_token(RightBrace) {
|
||||
nodes.push(self.element()?);
|
||||
}
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
fn element(&mut self) -> anyhow::Result<VisNode> {
|
||||
let id = self.id()?;
|
||||
let current = self.peek().clone();
|
||||
if self.match_tokens(vec![
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
DiamondArrowRight,
|
||||
DiamondArrowLeft,
|
||||
]) {
|
||||
self.edge(id, current)
|
||||
} else {
|
||||
let title = self.title()?;
|
||||
let children = if self.check(&LeftBrace) {
|
||||
self.elements()?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(VisNode::new_node(id, title, children))
|
||||
}
|
||||
}
|
||||
|
||||
fn edge(&mut self, from_id: String, arrow: Token) -> anyhow::Result<VisNode> {
|
||||
let to_id = self.id()?;
|
||||
let title = self.title()?;
|
||||
Ok(VisNode::new_edge(from_id, to_id, arrow.tokentype, title))
|
||||
}
|
||||
|
||||
fn title(&mut self) -> anyhow::Result<Option<String>> {
|
||||
if self.check(&Colon) {
|
||||
self.advance();
|
||||
Ok(Some(self.string()?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&mut self) -> anyhow::Result<String> {
|
||||
self.text()
|
||||
}
|
||||
|
||||
fn string(&mut self) -> anyhow::Result<String> {
|
||||
let text = self.peek().clone();
|
||||
let text = strip_surrounding(text.lexeme);
|
||||
self.consume(Str, "Expected quoted string")?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
fn text(&mut self) -> anyhow::Result<String> {
|
||||
let text = self.peek().clone();
|
||||
self.consume(Identifier, "Expected text")?;
|
||||
Ok(text.lexeme.to_owned())
|
||||
}
|
||||
|
||||
fn styles(&mut self) -> anyhow::Result<Vec<StyleNode>> {
|
||||
debug!("styles {:?}", self.peek());
|
||||
if self.match_token(Styles) {
|
||||
self.consume(LeftBrace, "Expected '{'")?;
|
||||
let mut styles = vec![];
|
||||
while !self.check(&RightBrace) {
|
||||
styles.push(self.style()?);
|
||||
}
|
||||
self.consume(RightBrace, "Expected '}'")?;
|
||||
Ok(styles)
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
fn style(&mut self) -> anyhow::Result<StyleNode> {
|
||||
debug!("style {:?}", self.peek());
|
||||
if self.check(&Identifier) || self.check(&Structure) {
|
||||
let idref = if self.check(&Structure) {
|
||||
// only structure element can also be referenced
|
||||
"structure".to_string()
|
||||
} else {
|
||||
self.peek().lexeme.to_owned()
|
||||
};
|
||||
debug!("idref {:?}", idref);
|
||||
self.advance();
|
||||
let containertype = self.containertype()?;
|
||||
if self.peek().tokentype == Colon {
|
||||
// optional
|
||||
self.advance();
|
||||
}
|
||||
self.consume(LeftBrace, "Expected '{'")?;
|
||||
debug!("attributes {:?}", self.peek());
|
||||
let attributes = self.style_elements()?;
|
||||
self.consume(RightBrace, "Expected '}'")?;
|
||||
|
||||
Ok(StyleNode {
|
||||
id_ref: idref,
|
||||
containertype,
|
||||
attributes,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("Expected identifier"))?
|
||||
}
|
||||
}
|
||||
|
||||
fn style_elements(&mut self) -> anyhow::Result<HashMap<String, String>> {
|
||||
debug!("read attributes {:?}", self.peek());
|
||||
let mut elements = HashMap::new();
|
||||
let mut key = self.peek().clone();
|
||||
while key.tokentype != RightBrace {
|
||||
self.advance();
|
||||
self.consume(Colon, "Expected ':'")?;
|
||||
let value = self.advance().clone();
|
||||
elements.insert(key.lexeme.to_owned(), value.lexeme);
|
||||
key = self.peek().clone();
|
||||
}
|
||||
Ok(elements)
|
||||
}
|
||||
|
||||
fn containertype(&mut self) -> anyhow::Result<ContainerType> {
|
||||
Ok(if self.check(&LeftParen) {
|
||||
self.advance();
|
||||
if self.match_token(Group) {
|
||||
self.consume(RightParen, "Expected ')'")?;
|
||||
ContainerType::Group
|
||||
} else {
|
||||
self.consume(RightParen, "Expected ')'")?;
|
||||
ContainerType::NonGroup
|
||||
}
|
||||
} else {
|
||||
ContainerType::NonGroup
|
||||
})
|
||||
}
|
||||
|
||||
fn consume(&mut self, tokentype: TokenType, expect: &str) -> anyhow::Result<&Token> {
|
||||
let current = self.peek();
|
||||
if self.check(&tokentype) {
|
||||
Ok(self.advance())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Error: {} but was '{}' on line {}",
|
||||
expect,
|
||||
self.peek().lexeme,
|
||||
current.line
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn match_tokens(&mut self, tokentypes: Vec<TokenType>) -> bool {
|
||||
for tokentype in tokentypes.iter() {
|
||||
if self.check(tokentype) {
|
||||
self.advance();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn match_token(&mut self, tokentype: TokenType) -> bool {
|
||||
if self.check(&tokentype) {
|
||||
self.advance();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn check(&self, tokentype: &TokenType) -> bool {
|
||||
if self.is_at_end() {
|
||||
false
|
||||
} else {
|
||||
&self.peek().tokentype == tokentype
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> &Token {
|
||||
if !self.is_at_end() {
|
||||
self.current += 1;
|
||||
}
|
||||
self.previous()
|
||||
}
|
||||
|
||||
fn previous(&self) -> &Token {
|
||||
&self.tokens[self.current - 1]
|
||||
}
|
||||
|
||||
fn is_at_end(&self) -> bool {
|
||||
self.peek().tokentype == TokenType::Eof
|
||||
}
|
||||
|
||||
fn peek(&self) -> &Token {
|
||||
&self.tokens[self.current]
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_surrounding(s: String) -> String {
|
||||
s[1..s.len() - 1].to_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::NodeType::Node;
|
||||
|
||||
#[test]
|
||||
fn test_parse() {
|
||||
let vis_source = r#"
|
||||
structure {
|
||||
top: "top-node" {
|
||||
child: "child-node" {
|
||||
}
|
||||
}
|
||||
}
|
||||
styles {
|
||||
structure {
|
||||
adam: eve
|
||||
orientation: horizontal;
|
||||
}
|
||||
top(group) {
|
||||
orientation: vertical;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let vis = crate::parse::parser::parse_vis(vis_source).ok();
|
||||
|
||||
assert!(vis.is_some(), "Parsed structure should not be None");
|
||||
|
||||
if let Some(vis) = vis {
|
||||
assert_eq!(
|
||||
vis.structure[0].children.len(),
|
||||
1,
|
||||
"The structure should contain one top-level element"
|
||||
);
|
||||
|
||||
if vis.structure[0].children[0].node_type == Node {
|
||||
let top = &vis.structure[0].children[0];
|
||||
assert_eq!(top.id, "top", "The ID of the first node should be 'top'");
|
||||
assert_eq!(
|
||||
top.label.as_ref().unwrap(),
|
||||
&"top-node".to_owned(),
|
||||
"The title of the first node should be 'top-node'"
|
||||
);
|
||||
assert_eq!(top.children.len(), 1);
|
||||
let child = &top.children[0];
|
||||
if child.node_type == Node {
|
||||
assert_eq!(
|
||||
child.id, "child",
|
||||
"The ID of the second node should be 'child'"
|
||||
);
|
||||
assert_eq!(
|
||||
child.label.as_ref().unwrap(),
|
||||
&"child-node".to_owned(),
|
||||
"The title of the second node should be 'child-node'"
|
||||
);
|
||||
assert_eq!(child.children.len(), 0);
|
||||
}
|
||||
} else {
|
||||
panic!("The top-level element should be a Node");
|
||||
}
|
||||
assert_eq!(vis.styles.len(), 2);
|
||||
let styles = vis.get_styles("top");
|
||||
assert_eq!(styles.len(), 2);
|
||||
assert_eq!(styles["orientation"], "vertical"); // overrides parent style
|
||||
} else {
|
||||
panic!("Parsed structure was None");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,21 @@
|
|||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::tokens::{
|
||||
use crate::parse::tokens::{
|
||||
Token,
|
||||
TokenType::{self, *},
|
||||
KEYWORDS,
|
||||
};
|
||||
|
||||
static KEYWORDS: Lazy<HashMap<&'static str, TokenType>> = Lazy::new(|| {
|
||||
HashMap::from([
|
||||
("structure", Structure),
|
||||
("styles", Styles),
|
||||
("group", Group),
|
||||
("px", Px),
|
||||
])
|
||||
});
|
||||
|
||||
pub fn scan(vis: &str) -> anyhow::Result<Vec<Token>> {
|
||||
let mut scanner = Scanner::new(vis);
|
||||
scanner.scan();
|
||||
|
|
@ -48,8 +58,8 @@ impl<'a> Scanner<'a> {
|
|||
"," => self.add_token(Comma),
|
||||
"." => self.add_token(Dot),
|
||||
|
||||
"-" => {
|
||||
if self.match_token("-") {
|
||||
"=" => {
|
||||
if self.match_token("=") {
|
||||
if self.match_token(">") {
|
||||
self.add_token(ArrowRight);
|
||||
} else if self.match_token("<") {
|
||||
|
|
@ -122,14 +132,11 @@ impl<'a> Scanner<'a> {
|
|||
}
|
||||
|
||||
fn identifier(&mut self) {
|
||||
while is_alpha(self.peek()) || is_digit(self.peek()) {
|
||||
while is_alpha(self.peek()) || is_digit(self.peek()) || self.peek() == "-" {
|
||||
self.advance();
|
||||
}
|
||||
let text = self.chars[self.start_pos..self.current_pos].concat();
|
||||
let tokentype = KEYWORDS
|
||||
.get(text.as_str())
|
||||
.map(|d| *d)
|
||||
.unwrap_or(Identifier);
|
||||
let tokentype = KEYWORDS.get(text.as_str()).cloned().unwrap_or(Identifier);
|
||||
|
||||
self.add_token(tokentype);
|
||||
}
|
||||
|
|
@ -175,7 +182,7 @@ impl<'a> Scanner<'a> {
|
|||
return false;
|
||||
}
|
||||
self.current_pos += 1;
|
||||
return true;
|
||||
true
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> &str {
|
||||
|
|
@ -192,18 +199,14 @@ impl<'a> Scanner<'a> {
|
|||
}
|
||||
|
||||
fn is_digit(char: &str) -> bool {
|
||||
char.len() > 0 && char.chars().next().unwrap().is_ascii_digit()
|
||||
!char.is_empty() && char.chars().next().unwrap().is_ascii_digit()
|
||||
}
|
||||
|
||||
fn is_alpha(char: &str) -> bool {
|
||||
if char.len() == 0 {
|
||||
if char.is_empty() {
|
||||
false
|
||||
} else {
|
||||
let char = char.chars().next().unwrap();
|
||||
if char.is_alphabetic() || char == '_' {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
char.is_alphabetic() || char == '_'
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,3 @@
|
|||
use std::cell::LazyCell;
|
||||
use std::collections::HashMap;
|
||||
use TokenType::*;
|
||||
|
||||
pub const KEYWORDS: LazyCell<HashMap<&str, TokenType>> = LazyCell::new(|| {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("structure", Structure);
|
||||
m.insert("styles", Styles);
|
||||
m.insert("group", Group);
|
||||
m.insert("px", Px);
|
||||
m
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Token {
|
||||
pub tokentype: TokenType,
|
||||
163
src/parser.rs
163
src/parser.rs
|
|
@ -1,163 +0,0 @@
|
|||
use crate::{
|
||||
tokens::{
|
||||
Token,
|
||||
TokenType::{self, *},
|
||||
},
|
||||
Element, StyleNode, Vis,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
|
||||
pub fn parse_vis(contents: &str) -> anyhow::Result<Vis> {
|
||||
let tokens = crate::scanner::scan(contents)?;
|
||||
// println!("{:?}", tokens);
|
||||
let mut parser = Parser::new(tokens);
|
||||
|
||||
Ok(Vis {
|
||||
structure: parser.structure()?,
|
||||
styles: parser.styles()?,
|
||||
})
|
||||
}
|
||||
|
||||
struct Parser {
|
||||
tokens: Vec<Token>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new(tokens: Vec<Token>) -> Self {
|
||||
Self { tokens, current: 0 }
|
||||
}
|
||||
|
||||
fn structure(&mut self) -> anyhow::Result<Vec<Element>> {
|
||||
if self.match_token(Structure) {
|
||||
self.elements()
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
fn elements(&mut self) -> anyhow::Result<Vec<Element>> {
|
||||
// println!("nodes {:?}", self.peek());
|
||||
self.consume(LeftBrace, "Expected '{'")?;
|
||||
let mut nodes = vec![];
|
||||
while !self.match_token(RightBrace) {
|
||||
nodes.push(self.element()?);
|
||||
}
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
fn element(&mut self) -> anyhow::Result<Element> {
|
||||
// println!("node {:?}", self.peek());
|
||||
let id = self.id()?;
|
||||
// println!("id {}", id);
|
||||
let current = self.peek().clone();
|
||||
if self.match_tokens(vec![
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
DiamondArrowRight,
|
||||
DiamondArrowLeft,
|
||||
]) {
|
||||
self.edge(id, current)
|
||||
} else {
|
||||
let title = self.title()?;
|
||||
// println!("title {:?}", title);
|
||||
let children = if self.check(&LeftBrace) {
|
||||
self.elements()?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(Element::Node(id, title, children))
|
||||
}
|
||||
}
|
||||
|
||||
fn edge(&mut self, from_id: String, arrow: Token) -> anyhow::Result<Element> {
|
||||
let to_id = self.id()?;
|
||||
let title = self.title()?;
|
||||
Ok(Element::Edge(from_id, to_id, arrow.tokentype, title))
|
||||
}
|
||||
|
||||
fn title(&mut self) -> anyhow::Result<Option<String>> {
|
||||
if self.check(&Colon) {
|
||||
self.advance();
|
||||
Ok(Some(self.string()?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&mut self) -> anyhow::Result<String> {
|
||||
self.text()
|
||||
}
|
||||
|
||||
fn string(&mut self) -> anyhow::Result<String> {
|
||||
let text = self.peek().clone();
|
||||
self.consume(Str, "Expected quoted string")?;
|
||||
Ok(text.lexeme.to_owned())
|
||||
}
|
||||
|
||||
fn text(&mut self) -> anyhow::Result<String> {
|
||||
let text = self.peek().clone();
|
||||
self.consume(Identifier, "Expected text")?;
|
||||
Ok(text.lexeme.to_owned())
|
||||
}
|
||||
|
||||
fn styles(&mut self) -> anyhow::Result<Vec<StyleNode>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn consume(&mut self, tokentype: TokenType, expect: &str) -> anyhow::Result<&Token> {
|
||||
let current = self.peek();
|
||||
if self.check(&tokentype) {
|
||||
Ok(self.advance())
|
||||
} else {
|
||||
Err(anyhow!("Error: {} on line {}", expect, current.line))
|
||||
}
|
||||
}
|
||||
|
||||
fn match_tokens(&mut self, tokentypes: Vec<TokenType>) -> bool {
|
||||
for tokentype in tokentypes.iter() {
|
||||
if self.check(tokentype) {
|
||||
self.advance();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn match_token(&mut self, tokentype: TokenType) -> bool {
|
||||
if self.check(&tokentype) {
|
||||
self.advance();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn check(&self, tokentype: &TokenType) -> bool {
|
||||
if self.is_at_end() {
|
||||
false
|
||||
} else {
|
||||
&self.peek().tokentype == tokentype
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> &Token {
|
||||
if !self.is_at_end() {
|
||||
self.current += 1;
|
||||
}
|
||||
self.previous()
|
||||
}
|
||||
|
||||
fn previous(&self) -> &Token {
|
||||
&self.tokens[self.current - 1]
|
||||
}
|
||||
|
||||
fn is_at_end(&self) -> bool {
|
||||
self.peek().tokentype == TokenType::Eof
|
||||
}
|
||||
|
||||
fn peek(&self) -> &Token {
|
||||
&self.tokens[self.current]
|
||||
}
|
||||
}
|
||||
9
src/render/mod.rs
Normal file
9
src/render/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use crate::Vis;
|
||||
|
||||
pub mod svg_renderer;
|
||||
pub mod svglib;
|
||||
|
||||
/// trait for turning the object model into a byte representation
|
||||
pub trait Renderer {
|
||||
fn render(&self, vis: Vis) -> anyhow::Result<Vec<u8>>;
|
||||
}
|
||||
14
src/render/svg_node.css
Normal file
14
src/render/svg_node.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
svg {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.node {
|
||||
border: 1px solid black;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
font-size: 10px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
163
src/render/svg_renderer.rs
Normal file
163
src/render/svg_renderer.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
use crate::render::svglib::rect::rect;
|
||||
use crate::render::svglib::svg::{svg, Svg};
|
||||
use crate::render::svglib::text::text;
|
||||
use crate::render::Renderer;
|
||||
use crate::{StyleNode, Vis, VisNode, BODY, HORIZONTAL, ORIENTATION};
|
||||
use log::debug;
|
||||
|
||||
pub struct SvgRender;
|
||||
|
||||
impl Renderer for SvgRender {
|
||||
fn render(&self, vis: Vis) -> anyhow::Result<Vec<u8>> {
|
||||
let mut svg = svg();
|
||||
|
||||
svg.width(100);
|
||||
svg.height(100);
|
||||
let style = include_str!("svg_node.css");
|
||||
svg.style(style);
|
||||
let (width, height) =
|
||||
render_elements(&vis, &mut svg, BODY, &vis.structure, &vis.styles, 0, 0);
|
||||
svg.viewbox(format!("0 0 {} {}", width, height));
|
||||
Ok(svg.to_string().into_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
fn render_elements(
|
||||
vis: &Vis,
|
||||
svg: &mut Svg,
|
||||
parent: &str,
|
||||
elements: &[VisNode],
|
||||
_styles: &Vec<StyleNode>,
|
||||
start_x: u32,
|
||||
start_y: u32,
|
||||
) -> (u32, u32) {
|
||||
let padding = 15;
|
||||
|
||||
let mut total_width = 0;
|
||||
let mut total_height = 0;
|
||||
let mut x = start_x;
|
||||
let mut y = start_y;
|
||||
|
||||
for element in elements {
|
||||
let (width, height) = if !element.children.is_empty() {
|
||||
render_elements(
|
||||
vis,
|
||||
svg,
|
||||
element.id.as_str(),
|
||||
&element.children,
|
||||
_styles,
|
||||
x + padding,
|
||||
y + padding,
|
||||
)
|
||||
} else if let Some(label) = element.label.as_ref() {
|
||||
let label_width = label.lines().map(|l| l.len()).max().unwrap_or(0) as u32;
|
||||
let label_height = label.lines().count() as u32;
|
||||
((label_width as f32 * 4.9 + 8.0) as u32, label_height * 15)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
|
||||
debug!(
|
||||
"{}:{:?}",
|
||||
&element.id,
|
||||
vis.get_style_value(&element.id, ORIENTATION)
|
||||
);
|
||||
if is_horizontal_orientation(vis, parent) {
|
||||
total_width += width + padding;
|
||||
total_height = u32::max(total_height, height + padding);
|
||||
} else {
|
||||
total_width = u32::max(total_width, width + padding);
|
||||
total_height += height + padding;
|
||||
}
|
||||
|
||||
svg.add(
|
||||
rect()
|
||||
.id(element.id.clone())
|
||||
.x(x + padding)
|
||||
.y(y + padding)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.stroke("white")
|
||||
.fill("none"),
|
||||
);
|
||||
|
||||
svg.add(
|
||||
text()
|
||||
.x(x + padding + width / 2)
|
||||
.y(start_y + padding)
|
||||
.fill("white")
|
||||
.text(element.label.as_ref().unwrap_or(&"".to_string()))
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("stroke-width", "1")
|
||||
.class("node"),
|
||||
);
|
||||
if is_horizontal_orientation(vis, parent) {
|
||||
x += width + padding;
|
||||
} else {
|
||||
y += height + padding;
|
||||
}
|
||||
}
|
||||
|
||||
(total_width + padding, total_height + padding)
|
||||
}
|
||||
|
||||
fn is_horizontal_orientation(vis: &Vis, id: impl Into<String>) -> bool {
|
||||
let hor = vis.get_style_value(id, ORIENTATION);
|
||||
let hor = hor.as_deref();
|
||||
hor == Some(HORIZONTAL) || hor == None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{ContainerType, StyleNode, Vis, VisNode};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn test_render_vis_with_grid_layout() {
|
||||
// Create a mock StyleNode
|
||||
let style_node = StyleNode {
|
||||
id_ref: "node_1".to_string(),
|
||||
containertype: ContainerType::NonGroup,
|
||||
attributes: [("fill".to_string(), "white".to_string())]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
};
|
||||
|
||||
// Create mock Elements
|
||||
let element1 = VisNode::new_node(
|
||||
"node1",
|
||||
Some("Node 1\nlonger"),
|
||||
vec![], // No child elements
|
||||
);
|
||||
let element2 = VisNode::new_node(
|
||||
"node2",
|
||||
Some("Node 2"),
|
||||
vec![], // No child elements
|
||||
);
|
||||
|
||||
let element3 = VisNode::new_node(
|
||||
"node3",
|
||||
Some("Node 3\nis longer\nthan you are to me"),
|
||||
vec![], // No child elements
|
||||
);
|
||||
let root = VisNode::new_node(
|
||||
"structure",
|
||||
Some("root"),
|
||||
vec![element1, element2, element3],
|
||||
);
|
||||
|
||||
// Create Vis structure
|
||||
let vis = Vis {
|
||||
styles: vec![style_node],
|
||||
structure: vec![root],
|
||||
};
|
||||
|
||||
let svg_output = SvgRender {}.render(vis).unwrap();
|
||||
|
||||
let mut file = File::create("output_multiple_nodes.svg").expect("Unable to create file");
|
||||
file.write_all(&svg_output).expect("Unable to write data");
|
||||
}
|
||||
}
|
||||
79
src/render/svglib/circle.rs
Normal file
79
src/render/svglib/circle.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
use crate::render::svglib::{att, att_to_string, Att, SvgElement, ElementType, Shape, Value};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub fn circle() -> Circle {
|
||||
Circle::default()
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Circle(Vec<Att>);
|
||||
|
||||
impl Circle {
|
||||
pub fn id<V: Into<Value>>(mut self, id: V) -> Self {
|
||||
self.0.push(att("id", id));
|
||||
self
|
||||
}
|
||||
pub fn cx<V: Into<Value>>(mut self, cx: V) -> Self {
|
||||
self.0.push(att("cx", cx));
|
||||
self
|
||||
}
|
||||
pub fn cy<V: Into<Value>>(mut self, cy: V) -> Self {
|
||||
self.0.push(att("cy", cy));
|
||||
self
|
||||
}
|
||||
pub fn r<V: Into<Value>>(mut self, r: V) -> Self {
|
||||
self.0.push(att("r", r));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape for Circle {
|
||||
fn fill<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.0.push(att("fill", value));
|
||||
self
|
||||
}
|
||||
|
||||
fn stroke<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.0.push(att("stroke", value));
|
||||
self
|
||||
}
|
||||
|
||||
fn transform<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.0.push(att("transform", value));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Circle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"<circle{} />"#,
|
||||
self.0.iter().map(att_to_string).collect::<String>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SvgElement for Circle {
|
||||
fn get_type(&self) -> ElementType {
|
||||
ElementType::Circle
|
||||
}
|
||||
|
||||
fn atts(&self) -> &[Att] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_circle() {
|
||||
let circle = circle().cx("1em").cy(0).r("10").id("c");
|
||||
assert_eq!(
|
||||
r#"<circle cx="1em" cy="0" r="10" id="c" />"#,
|
||||
circle.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
44
src/render/svglib/div.rs
Normal file
44
src/render/svglib/div.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use crate::render::svglib::{att, att_to_string, Att, Value};
|
||||
use std::fmt;
|
||||
pub fn div() -> Div {
|
||||
Div::default()
|
||||
}
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Div {
|
||||
atts: Vec<Att>,
|
||||
child: String,
|
||||
}
|
||||
|
||||
impl Div {
|
||||
pub fn id<V: Into<Value>>(&mut self, id: V) {
|
||||
self.atts.push(att("id", id));
|
||||
}
|
||||
|
||||
pub fn class<V>(mut self, class: V) -> Self
|
||||
where
|
||||
V: Into<Value>,
|
||||
{
|
||||
self.atts.push(att("class", class));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn inner_html<V: Into<String>>(mut self, html: V) -> Self {
|
||||
self.child = html.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn atts(&self) -> &[Att] {
|
||||
&self.atts
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Div {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"<div xmlns="http://www.w3.org/1999/xhtml"{}>{}</div>"#,
|
||||
self.atts.iter().map(att_to_string).collect::<String>(),
|
||||
self.child
|
||||
)
|
||||
}
|
||||
}
|
||||
82
src/render/svglib/ellipse.rs
Normal file
82
src/render/svglib/ellipse.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
use crate::render::svglib::{att, att_to_string, Att, ElementType, Shape, SvgElement, Value};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub fn ellipse() -> Ellipse {
|
||||
Ellipse(vec![])
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Ellipse(Vec<Att>);
|
||||
|
||||
impl Ellipse {
|
||||
pub fn id<V: Into<Value>>(&mut self, id: V) {
|
||||
self.0.push(att("id", id));
|
||||
}
|
||||
pub fn cx<V: Into<Value>>(mut self, cx: V) -> Self {
|
||||
self.0.push(att("cx", cx));
|
||||
self
|
||||
}
|
||||
pub fn cy<V: Into<Value>>(mut self, cy: V) -> Self {
|
||||
self.0.push(att("cy", cy));
|
||||
self
|
||||
}
|
||||
pub fn rx<V: Into<Value>>(mut self, rx: V) -> Self {
|
||||
self.0.push(att("rx", rx));
|
||||
self
|
||||
}
|
||||
pub fn ry<V: Into<Value>>(mut self, ry: V) -> Self {
|
||||
self.0.push(att("ry", ry));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape for Ellipse {
|
||||
fn fill<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.0.push(att("fill", value));
|
||||
self
|
||||
}
|
||||
|
||||
fn stroke<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.0.push(att("stroke", value));
|
||||
self
|
||||
}
|
||||
|
||||
fn transform<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.0.push(att("transform", value));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Ellipse {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"<ellipse{} />"#,
|
||||
self.0.iter().map(att_to_string).collect::<String>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SvgElement for Ellipse {
|
||||
fn get_type(&self) -> ElementType {
|
||||
ElementType::Ellipse
|
||||
}
|
||||
|
||||
fn atts(&self) -> &[Att] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ellipse() {
|
||||
let ellipse = ellipse().cx(0).cy(0).rx(10).ry(15);
|
||||
assert_eq!(
|
||||
r#"<ellipse cx="0" cy="0" rx="10" ry="15" />"#,
|
||||
ellipse.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
73
src/render/svglib/foreign_object.rs
Normal file
73
src/render/svglib/foreign_object.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
use crate::render::svglib::div::Div;
|
||||
use crate::render::svglib::{att, att_to_string, Att, ElementType, SvgElement, Value};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub fn foreign_object() -> ForeignObject {
|
||||
ForeignObject::default()
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct ForeignObject {
|
||||
child: Option<Div>, //for now
|
||||
atts: Vec<Att>,
|
||||
}
|
||||
|
||||
impl ForeignObject {
|
||||
pub fn id<V>(&mut self, id: V)
|
||||
where
|
||||
V: Into<Value>,
|
||||
{
|
||||
self.atts.push(att("id", id));
|
||||
}
|
||||
|
||||
pub fn x<V: Into<Value>>(mut self, x: V) -> Self {
|
||||
self.atts.push(att("x", x));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn y<V: Into<Value>>(mut self, y: V) -> Self {
|
||||
self.atts.push(att("y", y));
|
||||
self
|
||||
}
|
||||
pub fn width<V: Into<Value>>(mut self, width: V) -> Self {
|
||||
self.atts.push(att("width", width));
|
||||
self
|
||||
}
|
||||
pub fn height<V: Into<Value>>(mut self, height: V) -> Self {
|
||||
self.atts.push(att("height", height));
|
||||
self
|
||||
}
|
||||
pub fn class<V: Into<Value>>(mut self, class: V) -> Self {
|
||||
self.atts.push(att("class", class));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_child(mut self, child: Div) -> Self {
|
||||
self.child = Some(child);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ForeignObject {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"<foreignObject{}>{}</foreignObject>"#,
|
||||
self.atts.iter().map(att_to_string).collect::<String>(),
|
||||
self.child
|
||||
.as_ref()
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or("".to_string())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SvgElement for ForeignObject {
|
||||
fn get_type(&self) -> ElementType {
|
||||
ElementType::ForeignObject
|
||||
}
|
||||
|
||||
fn atts(&self) -> &[Att] {
|
||||
&self.atts
|
||||
}
|
||||
}
|
||||
67
src/render/svglib/group.rs
Normal file
67
src/render/svglib/group.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use crate::render::svglib::{att, att_to_string, Att, ElementType, SvgElement, Value};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub fn group() -> Group {
|
||||
Group::default()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Group {
|
||||
children: Vec<Box<dyn SvgElement>>,
|
||||
atts: Vec<Att>,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
pub fn add(&mut self, child: impl SvgElement + 'static) {
|
||||
self.children.push(Box::new(child));
|
||||
}
|
||||
|
||||
pub fn id<V: Into<Value>>(&mut self, id: V) {
|
||||
self.atts.push(att("id", id));
|
||||
}
|
||||
|
||||
pub fn transform<V: Into<Value>>(&mut self, value: V) {
|
||||
self.atts.push(att("transform", value));
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Group {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"<g{}>",
|
||||
self.atts.iter().map(att_to_string).collect::<String>()
|
||||
)?;
|
||||
for e in &self.children {
|
||||
write!(f, "{}", e.to_string().as_str())?;
|
||||
}
|
||||
write!(f, "</g>")
|
||||
}
|
||||
}
|
||||
|
||||
impl SvgElement for Group {
|
||||
fn get_type(&self) -> ElementType {
|
||||
ElementType::Group(Vec::new())
|
||||
}
|
||||
|
||||
fn atts(&self) -> &[Att] {
|
||||
&self.atts
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::render::svglib::rect::rect;
|
||||
|
||||
#[test]
|
||||
fn test_group() {
|
||||
let mut g = group();
|
||||
g.id("testgroup");
|
||||
g.add(rect().x(0).y(0).width(10).height(10));
|
||||
assert_eq!(
|
||||
r#"<g id="testgroup"><rect x="0" y="0" width="10" height="10" /></g>"#,
|
||||
g.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
80
src/render/svglib/image.rs
Normal file
80
src/render/svglib/image.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use crate::render::svglib::{att, att_to_string, Att, ElementType, SvgElement, Value};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub fn image() -> Image {
|
||||
Image::default()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Image {
|
||||
atts: Vec<Att>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn id<V: Into<Value>>(&mut self, id: V) {
|
||||
self.atts.push(att("id", id));
|
||||
}
|
||||
|
||||
pub fn transform<V: Into<Value>>(&mut self, value: V) {
|
||||
self.atts.push(att("transform", value));
|
||||
}
|
||||
pub fn x<V: Into<Value>>(mut self, x: V) -> Self {
|
||||
self.atts.push(att("x", x));
|
||||
self
|
||||
}
|
||||
pub fn y<V: Into<Value>>(mut self, y: V) -> Self {
|
||||
self.atts.push(att("y", y));
|
||||
self
|
||||
}
|
||||
pub fn width<V: Into<Value>>(mut self, width: V) -> Self {
|
||||
self.atts.push(att("width", width));
|
||||
self
|
||||
}
|
||||
pub fn height<V: Into<Value>>(mut self, height: V) -> Self {
|
||||
self.atts.push(att("height", height));
|
||||
self
|
||||
}
|
||||
pub fn href<V: Into<Value>>(mut self, href: V) -> Self {
|
||||
self.atts.push(att("href", href));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Image {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"<image{} />"#,
|
||||
self.atts.iter().map(att_to_string).collect::<String>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SvgElement for Image {
|
||||
fn get_type(&self) -> ElementType {
|
||||
ElementType::Image
|
||||
}
|
||||
|
||||
fn atts(&self) -> &[Att] {
|
||||
&self.atts
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_image() {
|
||||
let rect = image()
|
||||
.href("http://acme.com/coyote.jpg")
|
||||
.x(0)
|
||||
.y(0)
|
||||
.width(10)
|
||||
.height(10);
|
||||
assert_eq!(
|
||||
r#"<image href="http://acme.com/coyote.jpg" x="0" y="0" width="10" height="10" />"#,
|
||||
rect.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
82
src/render/svglib/line.rs
Normal file
82
src/render/svglib/line.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
use crate::render::svglib::{att, att_to_string, Att, SvgElement, ElementType, Shape, Value};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub fn line() -> Line {
|
||||
Line(vec![])
|
||||
}
|
||||
|
||||
pub struct Line(Vec<Att>);
|
||||
|
||||
impl Line {
|
||||
pub fn id<V: Into<Value>>(mut self, id: V) -> Self {
|
||||
self.0.push(att("id", id));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn x1<V: Into<Value>>(mut self, x: V) -> Self {
|
||||
self.0.push(att("x1", x));
|
||||
self
|
||||
}
|
||||
pub fn y1<V: Into<Value>>(mut self, y: V) -> Self {
|
||||
self.0.push(att("y1", y));
|
||||
self
|
||||
}
|
||||
pub fn x2<V: Into<Value>>(mut self, x2: V) -> Self {
|
||||
self.0.push(att("x2", x2));
|
||||
self
|
||||
}
|
||||
pub fn y2<V: Into<Value>>(mut self, y2: V) -> Self {
|
||||
self.0.push(att("y2", y2));
|
||||
self
|
||||
}
|
||||
pub fn attr<V: Into<Value>>(mut self, key: &str, value: V) -> Self {
|
||||
self.0.push(att(key, value));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Line {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#" <line{} />"#,
|
||||
self.0.iter().map(att_to_string).collect::<String>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SvgElement for Line {
|
||||
fn get_type(&self) -> ElementType {
|
||||
ElementType::Line
|
||||
}
|
||||
|
||||
fn atts(&self) -> &[Att] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape for Line {
|
||||
fn fill<V>(mut self, value: V) -> Self
|
||||
where
|
||||
V: Into<Value>,
|
||||
{
|
||||
self.0.push(att("fill", value));
|
||||
self
|
||||
}
|
||||
|
||||
fn stroke<V>(mut self, value: V) -> Self
|
||||
where
|
||||
V: Into<Value>,
|
||||
{
|
||||
self.0.push(att("stroke", value));
|
||||
self
|
||||
}
|
||||
|
||||
fn transform<V>(mut self, value: V) -> Self
|
||||
where
|
||||
V: Into<Value>,
|
||||
{
|
||||
self.0.push(att("transform", value));
|
||||
self
|
||||
}
|
||||
}
|
||||
34
src/render/svglib/link.rs
Normal file
34
src/render/svglib/link.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use crate::render::svglib::{att, att_to_string, Att, SvgElement, ElementType, Value};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub fn link(href: &str) -> Link {
|
||||
Link(vec![att("href", href)])
|
||||
}
|
||||
|
||||
pub struct Link(Vec<Att>);
|
||||
|
||||
impl Link {
|
||||
pub fn id<V: Into<Value>>(&mut self, id: V) {
|
||||
self.0.push(att("id", id));
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Link {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"<link{} />"#,
|
||||
self.0.iter().map(att_to_string).collect::<String>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SvgElement for Link {
|
||||
fn get_type(&self) -> ElementType {
|
||||
ElementType::Link
|
||||
}
|
||||
|
||||
fn atts(&self) -> &[Att] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
122
src/render/svglib/mod.rs
Normal file
122
src/render/svglib/mod.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
pub mod circle;
|
||||
pub mod div;
|
||||
pub mod ellipse;
|
||||
pub mod foreign_object;
|
||||
pub mod group;
|
||||
pub mod image;
|
||||
pub mod line;
|
||||
pub mod link;
|
||||
pub mod path;
|
||||
pub mod rect;
|
||||
pub mod svg;
|
||||
pub mod text;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Value(String);
|
||||
|
||||
impl From<&str> for Value {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for Value {
|
||||
fn from(s: usize) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Value {
|
||||
fn from(s: u32) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Value {
|
||||
fn from(s: i32) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for Value {
|
||||
fn from(s: f32) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Value {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&String> for Value {
|
||||
fn from(s: &String) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ElementType {
|
||||
Circle,
|
||||
Ellipse,
|
||||
ForeignObject,
|
||||
Group(Vec<ElementType>),
|
||||
Image,
|
||||
Line,
|
||||
Link,
|
||||
Path,
|
||||
Rect,
|
||||
}
|
||||
|
||||
pub trait SvgElement: Display {
|
||||
fn get_type(&self) -> ElementType;
|
||||
fn atts(&self) -> &[Att];
|
||||
}
|
||||
|
||||
pub trait Shape {
|
||||
fn fill<V>(self, value: V) -> Self
|
||||
where
|
||||
V: Into<Value>;
|
||||
|
||||
fn stroke<V>(self, value: V) -> Self
|
||||
where
|
||||
V: Into<Value>;
|
||||
|
||||
fn transform<V>(self, value: V) -> Self
|
||||
where
|
||||
V: Into<Value>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Att {
|
||||
name: String,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
impl Att {
|
||||
pub fn new(name: &str, value: Value) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn att<V>(name: &str, value: V) -> Att
|
||||
where
|
||||
V: Into<Value>,
|
||||
{
|
||||
Att::new(name, value.into())
|
||||
}
|
||||
|
||||
fn att_to_string(att: &Att) -> String {
|
||||
format!(r#" {}="{}""#, att.name, att.value)
|
||||
}
|
||||
120
src/render/svglib/path.rs
Normal file
120
src/render/svglib/path.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
use crate::render::svglib::{att, att_to_string, Att, ElementType, Shape, SvgElement, Value};
|
||||
use std::fmt;
|
||||
|
||||
pub fn path(d: &str) -> Path {
|
||||
Path::new(d)
|
||||
}
|
||||
|
||||
pub struct Path {
|
||||
d: String,
|
||||
atts: Vec<Att>,
|
||||
}
|
||||
|
||||
impl fmt::Display for Path {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"<path d="{}"{} />"#,
|
||||
self.d,
|
||||
self.atts.iter().map(att_to_string).collect::<String>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SvgElement for Path {
|
||||
fn get_type(&self) -> ElementType {
|
||||
ElementType::Path
|
||||
}
|
||||
|
||||
fn atts(&self) -> &[Att] {
|
||||
&self.atts
|
||||
}
|
||||
}
|
||||
|
||||
impl Path {
|
||||
pub fn new(d: &str) -> Self {
|
||||
Self {
|
||||
d: d.to_string(),
|
||||
atts: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id<V: Into<Value>>(&mut self, id: V) {
|
||||
self.atts.push(att("id", id));
|
||||
}
|
||||
|
||||
pub fn m(&mut self, x: usize, y: usize) {
|
||||
self.d.push_str(&format!(" m{} {}", x, y));
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn M(&mut self, x: usize, y: usize) {
|
||||
self.d.push_str(&format!(" M{} {}", x, y));
|
||||
}
|
||||
|
||||
pub fn z(&mut self) {
|
||||
self.d.push_str(" z");
|
||||
}
|
||||
|
||||
pub fn l(&mut self, x: usize, y: usize) {
|
||||
self.d.push_str(&format!(" l{} {}", x, y));
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn L(&mut self, x: usize, y: usize) {
|
||||
self.d.push_str(&format!(" L{} {}", x, y));
|
||||
}
|
||||
|
||||
pub fn h(&mut self, x: usize) {
|
||||
self.d.push_str(&format!(" h{}", x));
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn H(&mut self, x: usize) {
|
||||
self.d.push_str(&format!(" H{}", x));
|
||||
}
|
||||
|
||||
pub fn v(&mut self, x: usize) {
|
||||
self.d.push_str(&format!(" v{}", x));
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn V(&mut self, x: usize) {
|
||||
self.d.push_str(&format!(" V{}", x));
|
||||
}
|
||||
|
||||
pub fn c(&mut self, x1: usize, y1: usize, x2: usize, y2: usize) {
|
||||
self.d.push_str(&format!(" c{} {} {} {}", x1, y1, x2, y2));
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn C(&mut self, x1: usize, y1: usize, x2: usize, y2: usize) {
|
||||
self.d.push_str(&format!(" C{} {} {} {}", x1, y1, x2, y2));
|
||||
}
|
||||
|
||||
pub fn s(&mut self, x1: usize, y1: usize, x2: usize, y2: usize) {
|
||||
self.d.push_str(&format!(" s{} {} {} {}", x1, y1, x2, y2));
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn S(&mut self, x1: usize, y1: usize, x2: usize, y2: usize) {
|
||||
self.d.push_str(&format!(" S{} {} {} {}", x1, y1, x2, y2));
|
||||
}
|
||||
}
|
||||
|
||||
impl Shape for Path {
|
||||
fn fill<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.atts.push(att("fill", value));
|
||||
self
|
||||
}
|
||||
|
||||
fn stroke<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.atts.push(att("stroke", value));
|
||||
self
|
||||
}
|
||||
|
||||
fn transform<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.atts.push(att("transform", value));
|
||||
self
|
||||
}
|
||||
}
|
||||
95
src/render/svglib/rect.rs
Normal file
95
src/render/svglib/rect.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
use crate::render::svglib::{att, att_to_string, Att, ElementType, SvgElement, Value};
|
||||
|
||||
pub fn rect() -> Rect {
|
||||
Rect::default()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Rect {
|
||||
atts: Vec<Att>,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn id<V: Into<Value>>(mut self, id: V) -> Self {
|
||||
self.atts.push(att("id", id));
|
||||
self
|
||||
}
|
||||
pub fn rounded() -> Self {
|
||||
Self { atts: vec![] }
|
||||
}
|
||||
|
||||
pub fn x<V: Into<Value>>(mut self, x: V) -> Self {
|
||||
self.atts.push(att("x", x));
|
||||
self
|
||||
}
|
||||
pub fn y<V: Into<Value>>(mut self, y: V) -> Self {
|
||||
self.atts.push(att("y", y));
|
||||
self
|
||||
}
|
||||
pub fn width<V: Into<Value>>(mut self, width: V) -> Self {
|
||||
self.atts.push(att("width", width));
|
||||
self
|
||||
}
|
||||
pub fn height<V: Into<Value>>(mut self, height: V) -> Self {
|
||||
self.atts.push(att("height", height));
|
||||
self
|
||||
}
|
||||
pub fn class<V: Into<Value>>(mut self, class: V) -> Self {
|
||||
self.atts.push(att("class", class));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fill<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.atts.push(att("fill", value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn stroke<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.atts.push(att("stroke", value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn transform<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.atts.push(att("transform", value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn attr<V: Into<Value>>(mut self, key: &str, value: V) -> Self {
|
||||
self.atts.push(att(key, value));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Rect {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"<rect{} />"#,
|
||||
self.atts.iter().map(att_to_string).collect::<String>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SvgElement for Rect {
|
||||
fn get_type(&self) -> ElementType {
|
||||
ElementType::Rect
|
||||
}
|
||||
|
||||
fn atts(&self) -> &[Att] {
|
||||
&self.atts
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_rect() {
|
||||
let rect = rect().x(0).y(0).width(10).height(10);
|
||||
assert_eq!(
|
||||
r#"<rect x="0" y="0" width="10" height="10" />"#,
|
||||
rect.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
90
src/render/svglib/svg.rs
Normal file
90
src/render/svglib/svg.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use crate::render::svglib::{att, att_to_string, Att, SvgElement, Value};
|
||||
use std::fmt;
|
||||
|
||||
pub fn svg() -> Svg {
|
||||
Svg::default()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Svg {
|
||||
style: Option<String>,
|
||||
elements: Vec<Box<dyn SvgElement>>,
|
||||
atts: Vec<Att>,
|
||||
}
|
||||
|
||||
impl Svg {
|
||||
pub fn style(&mut self, style: &str) {
|
||||
self.style = Some(style.to_string());
|
||||
}
|
||||
|
||||
pub fn add(&mut self, child: impl SvgElement + 'static) {
|
||||
self.elements.push(Box::new(child));
|
||||
}
|
||||
|
||||
pub fn width<V: Into<Value>>(&mut self, width: V) {
|
||||
self.atts.push(att("width", width.into().to_string()));
|
||||
}
|
||||
|
||||
pub fn height<V: Into<Value>>(&mut self, height: V) {
|
||||
self.atts.push(att("height", height.into().to_string()));
|
||||
}
|
||||
|
||||
pub fn viewbox<V: Into<Value>>(&mut self, viewbox: V) {
|
||||
self.atts.push(att("viewBox", viewbox));
|
||||
}
|
||||
|
||||
pub fn preserveaspectratio<V: Into<Value>>(&mut self, preserveaspectratio: V) {
|
||||
self.atts
|
||||
.push(att("preserveAspectRatio", preserveaspectratio));
|
||||
}
|
||||
|
||||
pub fn transform<V: Into<Value>>(&mut self, transform: V) {
|
||||
self.atts.push(att("transform", transform));
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Svg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg"{}>{}"#,
|
||||
self.atts.iter().map(att_to_string).collect::<String>(),
|
||||
self.style
|
||||
.as_ref()
|
||||
.map(|s| format!("<style>{}</style>", s))
|
||||
.unwrap_or("".to_string())
|
||||
)?;
|
||||
|
||||
for e in &self.elements {
|
||||
write!(f, "{}", e.to_string().as_str())?;
|
||||
}
|
||||
write!(f, "</svg>")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::render::svglib::rect::rect;
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let mut svg = svg();
|
||||
svg.style(".id { background-color: red; }");
|
||||
assert_eq!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg"><style>.id { background-color: red; }</style></svg>"#,
|
||||
svg.to_string()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_rect() {
|
||||
let mut svg = svg();
|
||||
svg.preserveaspectratio("none");
|
||||
svg.add(rect().x(0).y(0).width(10).height(10));
|
||||
assert_eq!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none"><rect x="0" y="0" width="10" height="10" /></svg>"#,
|
||||
svg.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
115
src/render/svglib/text.rs
Normal file
115
src/render/svglib/text.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
use crate::render::svglib::{att, att_to_string, Att, ElementType, SvgElement, Value};
|
||||
use std::fmt::{Display, Write};
|
||||
|
||||
pub fn text() -> Text {
|
||||
Text::default()
|
||||
}
|
||||
|
||||
pub struct Text {
|
||||
atts: Vec<Att>,
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl Default for Text {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
atts: vec![],
|
||||
text: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn text<V: Into<String>>(mut self, text: V) -> Self {
|
||||
self.text = text.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rounded() -> Self {
|
||||
Self {
|
||||
atts: vec![],
|
||||
text: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn x<V: Into<Value>>(mut self, x: V) -> Self {
|
||||
self.atts.push(att("x", x));
|
||||
self
|
||||
}
|
||||
pub fn y<V: Into<Value>>(mut self, y: V) -> Self {
|
||||
self.atts.push(att("y", y));
|
||||
self
|
||||
}
|
||||
pub fn width<V: Into<Value>>(mut self, width: V) -> Self {
|
||||
self.atts.push(att("width", width));
|
||||
self
|
||||
}
|
||||
pub fn height<V: Into<Value>>(mut self, height: V) -> Self {
|
||||
self.atts.push(att("height", height));
|
||||
self
|
||||
}
|
||||
pub fn class<V: Into<Value>>(mut self, class: V) -> Self {
|
||||
self.atts.push(att("class", class));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fill<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.atts.push(att("fill", value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn stroke<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.atts.push(att("stroke", value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn transform<V: Into<Value>>(mut self, value: V) -> Self {
|
||||
self.atts.push(att("transform", value));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn attr<V: Into<Value>>(mut self, key: &str, value: V) -> Self {
|
||||
self.atts.push(att(key, value));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Text {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
r#"<text{}>{}</text>"#,
|
||||
self.atts.iter().map(att_to_string).collect::<String>(),
|
||||
self.text.lines().fold(String::new(), |mut output, l| {
|
||||
let x: Vec<&Att> = self.atts.iter().filter(|a| a.name == "x").collect();
|
||||
let x: String = x[0].value.to_string();
|
||||
let _ = write!(output, "<tspan x=\"{}\" dy=\"10\">{}</tspan>", x, l);
|
||||
output
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SvgElement for Text {
|
||||
fn get_type(&self) -> ElementType {
|
||||
ElementType::Rect
|
||||
}
|
||||
|
||||
fn atts(&self) -> &[Att] {
|
||||
&self.atts
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_rect() {
|
||||
let text = text().x(0).y(0).width(10).height(10);
|
||||
assert_eq!(
|
||||
r#"<text x="0" y="0" width="10" height="10"></text>"#,
|
||||
text.to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue