Compare commits

..

10 commits

Author SHA1 Message Date
Shautvast
921df96c95 added sample output 2025-01-26 17:55:44 +01:00
Shautvast
6244dd4d0f added sample output 2025-01-26 17:54:53 +01:00
Shautvast
5e5ce9541c added vertical layout 2025-01-26 17:49:53 +01:00
Shautvast
660f892d6c test parsing styles and wiring them up with the structure elements 2025-01-26 12:36:27 +01:00
Shautvast
ad35f289c7 back references for visnodes 2025-01-25 13:35:08 +01:00
Shautvast
f02d8e419e lot of cleaning and improving 2025-01-22 18:09:15 +01:00
Shautvast
32c9bc5b40 e2e rendering 0.1 kinda works 2025-01-22 17:25:32 +01:00
Shautvast
89e03c8cb6 parsed styles and struggling with svg 2025-01-19 21:34:55 +01:00
Shautvast
25c3d008cc parsed styles and struggling with svg 2025-01-19 21:34:48 +01:00
Shautvast
16e3b04b4e moved parsing into parse module. 2025-01-07 16:09:24 +01:00
29 changed files with 1957 additions and 265 deletions

14
Cargo.lock generated
View file

@ -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",
]

View file

@ -6,3 +6,5 @@ edition = "2021"
[dependencies]
anyhow = "1.0"
unicode-segmentation = "1.1"
once_cell = "1.20.2"
log = "0.4"

View file

@ -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):
![sample_bank](examples/bank_architecture/bank.svg)

View 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

View file

@ -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

Binary file not shown.

View file

@ -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);
}
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)]
#[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
}

View file

@ -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
View 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
View 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");
}
}
}

View file

@ -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 == '_'
}
}

View file

@ -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,

View file

@ -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
View 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
View 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
View 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");
}
}

View 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
View 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
)
}
}

View 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()
)
}
}

View 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
}
}

View 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()
)
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
)
}
}