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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
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]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
|
|
@ -19,5 +31,7 @@ name = "vis"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,5 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
unicode-segmentation = "1.1"
|
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_
|
_It is Dutch for fish_
|
||||||
|
|
||||||
### NB I just started, needs a lot of work
|
|
||||||
|
|
||||||
sample vis file:
|
sample vis file:
|
||||||
|
|
||||||
```
|
```
|
||||||
markup {
|
structure {
|
||||||
lanes {
|
lanes {
|
||||||
functions {
|
functions {
|
||||||
calc: "Calculation"
|
calc: "Calculation"
|
||||||
|
|
@ -32,39 +30,23 @@ markup {
|
||||||
interest_engine: "InterestEngine"
|
interest_engine: "InterestEngine"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bank-->calc
|
bank==>calc
|
||||||
bank_scripts--<>bank_db
|
bank_scripts==<>bank_db
|
||||||
bank_motor--<>bank_db
|
bank_motor==<>bank_db
|
||||||
interest_engine-->calc
|
interest_engine==>calc
|
||||||
}
|
}
|
||||||
|
|
||||||
styles {
|
styles {
|
||||||
lanes(group) {
|
lanes(group) {
|
||||||
type: textnode
|
type: textnode
|
||||||
orientation: horizontal
|
orientation: vertical
|
||||||
shape: rectangle
|
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"
|
interest_engine: "InterestEngine"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bank-->calc
|
bank==>calc
|
||||||
bank_scripts--<>bank_db
|
bank_scripts==<>bank_db
|
||||||
bank_motor--<>bank_db
|
bank_motor==<>bank_db
|
||||||
interest_engine-->calc
|
interest_engine==>calc
|
||||||
}
|
}
|
||||||
|
|
||||||
styles {
|
styles {
|
||||||
lanes(group) {
|
lanes(group) {
|
||||||
type: textnode
|
type: textnode
|
||||||
orientation: horizontal
|
orientation: vertical
|
||||||
shape: rectangle
|
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::collections::HashMap;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use tokens::TokenType;
|
pub const BODY: &str = "structure";
|
||||||
|
pub const DEFAULT_ORIENTATION: &str = "horizontal";
|
||||||
pub mod parser;
|
pub const ORIENTATION: &str = "orientation";
|
||||||
mod scanner;
|
pub const HORIZONTAL: &str = "horizontal";
|
||||||
mod tokens;
|
pub const VERTICAL: &str = "vertical";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Vis {
|
pub struct Vis {
|
||||||
pub structure: Vec<Element>,
|
pub structure: Vec<VisNode>,
|
||||||
pub styles: Vec<StyleNode>,
|
pub styles: Vec<StyleNode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl Vis {
|
||||||
pub enum Element {
|
pub fn new(structure: VisNode, styles: Vec<StyleNode>) -> Self {
|
||||||
Node(String, Option<String>, Vec<Element>),
|
let mut vis = Self {
|
||||||
Edge(String, String, TokenType, Option<String>),
|
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 struct StyleNode {
|
||||||
pub id_ref: String,
|
pub id_ref: String,
|
||||||
pub containertype: ContainerType,
|
pub containertype: ContainerType,
|
||||||
pub attributes: HashMap<String, String>,
|
pub attributes: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl StyleNode {
|
||||||
pub enum ContainerType {
|
fn new(id_ref: impl Into<String>, containertype: ContainerType) -> Self {
|
||||||
Node,
|
Self {
|
||||||
Group,
|
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 anyhow::anyhow;
|
||||||
use std::env::args;
|
use std::env::args;
|
||||||
use std::fs;
|
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<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let args: Vec<String> = args().collect();
|
let args: Vec<String> = args().collect();
|
||||||
if args.len() != 2 {
|
if args.len() != 2 {
|
||||||
return Err(anyhow!("Usage: vis vis-file"));
|
eprintln!("Usage: vis vis-file");
|
||||||
|
exit(-64);
|
||||||
} else {
|
} else {
|
||||||
let vis_file = read_file(&args[1])?;
|
let vis_file = read_file(&args[1])?;
|
||||||
let vis = vis::parser::parse_vis(vis_file.as_str())?;
|
let vis = vis::parse::parse_vis(vis_file.as_str())?;
|
||||||
println!("{:?}", vis);
|
// 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(())
|
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 unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use crate::tokens::{
|
use crate::parse::tokens::{
|
||||||
Token,
|
Token,
|
||||||
TokenType::{self, *},
|
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>> {
|
pub fn scan(vis: &str) -> anyhow::Result<Vec<Token>> {
|
||||||
let mut scanner = Scanner::new(vis);
|
let mut scanner = Scanner::new(vis);
|
||||||
scanner.scan();
|
scanner.scan();
|
||||||
|
|
@ -48,8 +58,8 @@ impl<'a> Scanner<'a> {
|
||||||
"," => self.add_token(Comma),
|
"," => self.add_token(Comma),
|
||||||
"." => self.add_token(Dot),
|
"." => self.add_token(Dot),
|
||||||
|
|
||||||
"-" => {
|
"=" => {
|
||||||
if self.match_token("-") {
|
if self.match_token("=") {
|
||||||
if self.match_token(">") {
|
if self.match_token(">") {
|
||||||
self.add_token(ArrowRight);
|
self.add_token(ArrowRight);
|
||||||
} else if self.match_token("<") {
|
} else if self.match_token("<") {
|
||||||
|
|
@ -122,14 +132,11 @@ impl<'a> Scanner<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn identifier(&mut self) {
|
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();
|
self.advance();
|
||||||
}
|
}
|
||||||
let text = self.chars[self.start_pos..self.current_pos].concat();
|
let text = self.chars[self.start_pos..self.current_pos].concat();
|
||||||
let tokentype = KEYWORDS
|
let tokentype = KEYWORDS.get(text.as_str()).cloned().unwrap_or(Identifier);
|
||||||
.get(text.as_str())
|
|
||||||
.map(|d| *d)
|
|
||||||
.unwrap_or(Identifier);
|
|
||||||
|
|
||||||
self.add_token(tokentype);
|
self.add_token(tokentype);
|
||||||
}
|
}
|
||||||
|
|
@ -175,7 +182,7 @@ impl<'a> Scanner<'a> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.current_pos += 1;
|
self.current_pos += 1;
|
||||||
return true;
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn advance(&mut self) -> &str {
|
fn advance(&mut self) -> &str {
|
||||||
|
|
@ -192,18 +199,14 @@ impl<'a> Scanner<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_digit(char: &str) -> bool {
|
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 {
|
fn is_alpha(char: &str) -> bool {
|
||||||
if char.len() == 0 {
|
if char.is_empty() {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
let char = char.chars().next().unwrap();
|
let char = char.chars().next().unwrap();
|
||||||
if char.is_alphabetic() || char == '_' {
|
char.is_alphabetic() || char == '_'
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Token {
|
pub struct Token {
|
||||||
pub tokentype: TokenType,
|
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