shapes_converter/shex_to_html/
shex2html.rs

1use std::ffi::OsStr;
2use std::fs::OpenOptions;
3use std::io::{BufWriter, Write};
4
5use crate::{find_annotation, object_value2string, ShEx2HtmlError, ShEx2Uml, UmlGenerationMode};
6use minijinja::Template;
7use minijinja::{path_loader, Environment};
8use prefixmap::{IriRef, PrefixMap, PrefixMapError};
9use shex_ast::{Annotation, Schema, Shape, ShapeExpr, ShapeExprLabel, TripleExpr};
10
11use super::{
12    Cardinality, HtmlSchema, HtmlShape, Name, NodeId, ShEx2HtmlConfig, ShapeTemplateEntry,
13    ValueConstraint,
14};
15
16pub struct ShEx2Html {
17    config: ShEx2HtmlConfig,
18    current_html: HtmlSchema,
19    current_uml_converter: ShEx2Uml,
20}
21
22impl ShEx2Html {
23    pub fn new(config: ShEx2HtmlConfig) -> ShEx2Html {
24        let uml_config = config.shex2uml_config();
25        ShEx2Html {
26            config,
27            current_html: HtmlSchema::new(),
28            current_uml_converter: ShEx2Uml::new(&uml_config),
29        }
30    }
31
32    pub fn current_html(&self) -> &HtmlSchema {
33        &self.current_html
34    }
35
36    pub fn convert(&mut self, shex: &Schema) -> Result<(), ShEx2HtmlError> {
37        let prefixmap = shex
38            .prefixmap()
39            .unwrap_or_default()
40            .without_rich_qualifying();
41        let parent = self.create_name_for_schema(shex);
42        if self.config.embed_svg_schema || self.config.embed_svg_shape {
43            let _ = self.current_uml_converter.convert(shex);
44        }
45        if let Some(shapes) = shex.shapes() {
46            for shape_decl in shapes {
47                let mut name = self.shape_label2name(&shape_decl.id, &prefixmap)?;
48                let (node_id, _found) = self.current_html.get_node_adding_label(&name.name());
49                let component = self.shape_expr2htmlshape(
50                    &mut name,
51                    &shape_decl.shape_expr,
52                    &prefixmap,
53                    &node_id,
54                    &parent,
55                )?;
56                self.current_html.add_component(node_id, component)?;
57            }
58        }
59
60        if self.config.embed_svg_shape {
61            for shape in self.current_html.shapes_mut() {
62                let str = create_svg_shape(&self.current_uml_converter, &shape.name().name())?;
63                shape.set_svg_shape(str.as_str());
64            }
65        }
66
67        if self.config.embed_svg_schema {
68            let str = self.create_svg_schema()?;
69            self.current_html.set_svg_schema(str.as_str())
70        }
71        Ok(())
72    }
73
74    pub fn create_svg_schema(&self) -> Result<String, ShEx2HtmlError> {
75        let mut str_writer = BufWriter::new(Vec::new());
76        self.current_uml_converter.as_image(
77            str_writer.by_ref(),
78            crate::ImageFormat::SVG,
79            &UmlGenerationMode::all(),
80        )?;
81        let str = String::from_utf8(str_writer.into_inner()?)?;
82        Ok(str)
83    }
84
85    pub fn create_svg_shape(&self, name: &str) -> Result<String, ShEx2HtmlError> {
86        let mut str_writer = BufWriter::new(Vec::new());
87        self.current_uml_converter.as_image(
88            str_writer.by_ref(),
89            crate::ImageFormat::SVG,
90            &UmlGenerationMode::neighs(name),
91        )?;
92        let str = String::from_utf8(str_writer.into_inner()?)?;
93        Ok(str)
94    }
95
96    pub fn create_name_for_schema(&self, _shex: &Schema) -> Name {
97        let path_name = &self.config.landing_page();
98        let os_str = path_name.file_stem().unwrap_or_else(|| OsStr::new("kiko"));
99        let str = os_str
100            .to_os_string()
101            .into_string()
102            .unwrap_or_else(|_| panic!("Creating name for schema. Should be a string {os_str:?}"));
103        Name::new(&str, None, self.config.landing_page())
104    }
105
106    pub fn export_schema(&self) -> Result<(), ShEx2HtmlError> {
107        let environment = create_env();
108        let landing_page = self.config.landing_page();
109        let template = environment.get_template(self.config.landing_page_name.as_str())?;
110        let landing_page_name = self.config.landing_page_name();
111        let out = OpenOptions::new()
112            .write(true)
113            .truncate(true)
114            .create(true)
115            .open(landing_page)
116            .map_err(|e| ShEx2HtmlError::ErrorCreatingLandingPage {
117                name: landing_page_name,
118                error: e,
119            })?;
120
121        /* Generate landing page */
122        template.render_to_write(self.current_html.to_landing_html_schema(&self.config), out)?;
123
124        /* Generate a page for each shape */
125        let shape_template = environment.get_template(self.config.shape_template_name.as_str())?;
126        for shape in self.current_html.shapes() {
127            generate_shape_page(shape, &shape_template, &self.config)?;
128        }
129        Ok(())
130    }
131
132    fn shape_label2name(
133        &mut self,
134        label: &ShapeExprLabel,
135        prefixmap: &PrefixMap,
136    ) -> Result<Name, ShEx2HtmlError> {
137        match label {
138            ShapeExprLabel::IriRef { value } => iri_ref2name(value, &self.config, &None, prefixmap),
139            ShapeExprLabel::BNode { value: _ } => todo!(),
140            ShapeExprLabel::Start => todo!(),
141        }
142    }
143
144    fn shape_expr2htmlshape(
145        &mut self,
146        name: &mut Name,
147        shape_expr: &ShapeExpr,
148        prefixmap: &PrefixMap,
149        current_node_id: &NodeId,
150        parent: &Name,
151    ) -> Result<HtmlShape, ShEx2HtmlError> {
152        match shape_expr {
153            ShapeExpr::Shape(shape) => {
154                self.shape2htmlshape(name, shape, prefixmap, current_node_id, parent)
155            }
156            _ => Err(ShEx2HtmlError::NotImplemented {
157                msg: format!("Complex shape expressions are not implemented yet for conversion to HTML: {shape_expr:?}"),
158            }),
159        }
160    }
161
162    fn shape2htmlshape(
163        &mut self,
164        name: &mut Name,
165        shape: &Shape,
166        prefixmap: &PrefixMap,
167        current_node_id: &NodeId,
168        parent: &Name,
169    ) -> Result<HtmlShape, ShEx2HtmlError> {
170        if let Some(label) = get_label(&shape.annotations, prefixmap, &self.config)? {
171            name.add_label(label.as_str())
172        }
173        let mut html_shape = HtmlShape::new(name.clone(), parent.clone());
174        if let Some(extends) = &shape.extends {
175            for e in extends.iter() {
176                let extended_name = self.shape_label2name(e, prefixmap)?;
177                let (extended_node, found) = self
178                    .current_html
179                    .get_node_adding_label(&extended_name.name());
180                html_shape.add_extends(&extended_name);
181                if !found {
182                    self.current_html.add_component(
183                        extended_node,
184                        HtmlShape::new(extended_name, parent.clone()),
185                    )?;
186                }
187            }
188        }
189        if let Some(te) = &shape.expression {
190            match &te.te {
191                TripleExpr::EachOf {
192                    id: _,
193                    expressions,
194                    min: _,
195                    max: _,
196                    sem_acts: _,
197                    annotations: _,
198                } => {
199                    for e in expressions {
200                        match &e.te {
201                            TripleExpr::TripleConstraint {
202                                id: _,
203                                negated: _,
204                                inverse: _,
205                                predicate,
206                                value_expr,
207                                min,
208                                max,
209                                sem_acts: _,
210                                annotations,
211                            } => {
212                                let pred_name =
213                                    mk_name(predicate, annotations, &self.config, prefixmap)?;
214                                let card = mk_card(min, max)?;
215                                let value_constraint = if let Some(se) = value_expr {
216                                    self.value_expr2value_constraint(
217                                        se,
218                                        prefixmap,
219                                        current_node_id,
220                                        &pred_name,
221                                        &card,
222                                        parent,
223                                    )?
224                                } else {
225                                    ValueConstraint::default()
226                                };
227                                match value_constraint {
228                                    ValueConstraint::None => {}
229                                    _ => {
230                                        let entry = ShapeTemplateEntry::new(
231                                            pred_name,
232                                            value_constraint,
233                                            card,
234                                        );
235                                        html_shape.add_entry(entry)
236                                    }
237                                }
238                            }
239                            _ => todo!(),
240                        }
241                    }
242                    Ok(())
243                }
244                TripleExpr::OneOf {
245                    id: _,
246                    expressions: _,
247                    min: _,
248                    max: _,
249                    sem_acts: _,
250                    annotations: _,
251                } => todo!(),
252                TripleExpr::TripleConstraint {
253                    id: _,
254                    negated: _,
255                    inverse: _,
256                    predicate,
257                    value_expr,
258                    min,
259                    max,
260                    sem_acts: _,
261                    annotations,
262                } => {
263                    let pred_name = mk_name(predicate, annotations, &self.config, prefixmap)?;
264                    let card = mk_card(min, max)?;
265                    let value_constraint = if let Some(se) = value_expr {
266                        self.value_expr2value_constraint(
267                            se,
268                            prefixmap,
269                            current_node_id,
270                            &pred_name,
271                            &card,
272                            parent,
273                        )?
274                    } else {
275                        ValueConstraint::default()
276                    };
277                    match value_constraint {
278                        ValueConstraint::None => {}
279                        _ => {
280                            let entry = ShapeTemplateEntry::new(pred_name, value_constraint, card);
281                            html_shape.add_entry(entry);
282                        }
283                    };
284                    Ok(())
285                }
286                TripleExpr::TripleExprRef(tref) => Err(ShEx2HtmlError::not_implemented(
287                    format!("Triple expr reference {tref:?} not implemented yet").as_str(),
288                )),
289            }?;
290            Ok(html_shape)
291        } else {
292            Ok(html_shape)
293        }
294    }
295
296    fn value_expr2value_constraint(
297        &mut self,
298        value_expr: &ShapeExpr,
299        prefixmap: &PrefixMap,
300        _current_node_id: &NodeId,
301        _current_predicate: &Name,
302        _current_card: &Cardinality,
303        parent: &Name,
304    ) -> Result<ValueConstraint, ShEx2HtmlError> {
305        match value_expr {
306            ShapeExpr::ShapeOr { shape_exprs } => Err(ShEx2HtmlError::not_implemented(
307                format!("Shape ORs {shape_exprs:?} not implemented yet").as_str(),
308            )),
309            ShapeExpr::ShapeAnd { shape_exprs } => Err(ShEx2HtmlError::not_implemented(
310                format!("Shape ANDs {shape_exprs:?} not implemented yet").as_str(),
311            )),
312            ShapeExpr::ShapeNot { shape_expr } => Err(ShEx2HtmlError::not_implemented(
313                format!("Shape NOTs {shape_expr:?} not implemented yet").as_str(),
314            )),
315            ShapeExpr::NodeConstraint(nc) => {
316                if let Some(datatype) = nc.datatype() {
317                    let name = iri_ref2name(&datatype, &self.config, &None, prefixmap)?;
318                    Ok(ValueConstraint::datatype(name))
319                } else {
320                    Err(ShEx2HtmlError::not_implemented(
321                        format!("Some node constraints like {nc:?} are not implemented yet")
322                            .as_str(),
323                    ))
324                }
325            }
326            ShapeExpr::Shape(shape) => Err(ShEx2HtmlError::not_implemented(
327                format!("Ref to shape {shape:?} not implemented yet").as_str(),
328            )),
329            ShapeExpr::External => Err(ShEx2HtmlError::not_implemented(
330                "Ref to External not implemented yet",
331            )),
332            ShapeExpr::Ref(r) => match &r {
333                ShapeExprLabel::IriRef { value } => {
334                    let ref_name = iri_ref2name(value, &self.config, &None, prefixmap)?;
335                    let (node, found) = self
336                        .current_html
337                        .get_node_adding_label(ref_name.name().as_str());
338                    if !found {
339                        self.current_html
340                            .add_component(node, HtmlShape::new(ref_name.clone(), parent.clone()))?
341                    }
342                    Ok(ValueConstraint::Ref(ref_name))
343                }
344                ShapeExprLabel::BNode { value } => Err(ShEx2HtmlError::not_implemented(
345                    format!("Ref to bnode: {value}").as_str(),
346                )),
347                ShapeExprLabel::Start => Err(ShEx2HtmlError::not_implemented("Ref to Start")),
348            },
349        }
350    }
351}
352
353fn iri_ref2name(
354    iri_ref: &IriRef,
355    config: &ShEx2HtmlConfig,
356    maybe_label: &Option<String>,
357    prefixmap: &PrefixMap,
358) -> Result<Name, ShEx2HtmlError> {
359    let mut name = match iri_ref {
360        IriRef::Iri(iri) => Name::new(
361            prefixmap.qualify(iri).as_str(),
362            Some(iri.as_str()),
363            config.target_folder().as_path(),
364        ),
365        IriRef::Prefixed { prefix, local } => {
366            let iri = prefixmap.resolve_prefix_local(prefix, local)?;
367            Name::new(
368                format!("{prefix}:{local}").as_str(),
369                Some(iri.as_str()),
370                config.target_folder().as_path(),
371            )
372        }
373    };
374    if let Some(label) = maybe_label {
375        name.add_label(label)
376    };
377    Ok(name)
378}
379
380pub fn create_env() -> Environment<'static> {
381    let mut env = Environment::new();
382    env.set_loader(path_loader("shapes_converter/default_templates"));
383    env
384}
385
386fn mk_card(min: &Option<i32>, max: &Option<i32>) -> Result<Cardinality, ShEx2HtmlError> {
387    let min = if let Some(n) = min { *n } else { 1 };
388    let max = if let Some(n) = max { *n } else { 1 };
389    match (min, max) {
390        (1, 1) => Ok(Cardinality::OneOne),
391        (0, -1) => Ok(Cardinality::Star),
392        (0, 1) => Ok(Cardinality::Optional),
393        (1, -1) => Ok(Cardinality::Plus),
394        (m, n) if m >= 0 && n > m => Ok(Cardinality::Fixed(m)),
395        (m, n) if m >= 0 && n > m => Ok(Cardinality::Range(m, n)),
396        _ => Err(ShEx2HtmlError::WrongCardinality { min, max }),
397    }
398}
399
400fn generate_shape_page(
401    shape: &HtmlShape,
402    template: &Template,
403    _config: &ShEx2HtmlConfig,
404) -> Result<(), ShEx2HtmlError> {
405    let name = shape.name();
406    if let Some((path, _local_name)) = name.get_path_localname() {
407        let file_name = path.as_path().display().to_string();
408        let out_shape = OpenOptions::new()
409            .write(true)
410            .truncate(true)
411            .create(true)
412            .open(path)
413            .map_err(|e| ShEx2HtmlError::ErrorCreatingShapesFile {
414                name: file_name,
415                error: e,
416            })?;
417        let _state = template.render_to_write(shape, out_shape)?;
418        Ok(())
419    } else {
420        // It doesn't generate local page because name doesn't have a local ref
421        Ok(())
422    }
423}
424
425fn mk_name(
426    iri: &IriRef,
427    annotations: &Option<Vec<Annotation>>,
428    config: &ShEx2HtmlConfig,
429    prefixmap: &PrefixMap,
430) -> Result<Name, ShEx2HtmlError> {
431    let label = get_label(annotations, prefixmap, config)?;
432    let name = iri_ref2name(iri, config, &label, prefixmap)?;
433    Ok(name)
434}
435
436fn get_label(
437    annotations: &Option<Vec<Annotation>>,
438    prefixmap: &PrefixMap,
439    config: &ShEx2HtmlConfig,
440) -> Result<Option<String>, PrefixMapError> {
441    for label in config.annotation_label.iter() {
442        if let Some(value) = find_annotation(annotations, label, prefixmap)? {
443            return Ok(Some(object_value2string(&value)));
444        }
445    }
446    Ok(None)
447}
448
449pub fn create_svg_shape(converter: &ShEx2Uml, name: &str) -> Result<String, ShEx2HtmlError> {
450    let mut str_writer = BufWriter::new(Vec::new());
451    converter.as_image(
452        str_writer.by_ref(),
453        crate::ImageFormat::SVG,
454        &UmlGenerationMode::neighs(name),
455    )?;
456    let str = String::from_utf8(str_writer.into_inner()?)?;
457    Ok(str)
458}
459
460#[cfg(test)]
461mod tests {
462    // use super::*;
463    // use shex_compact::ShExParser;
464
465    #[test]
466    fn test_minininja() {
467        use minijinja::{context, Environment};
468
469        let mut env = Environment::new();
470        env.add_template("hello", "Hello {{ name }}!").unwrap();
471        let tmpl = env.get_template("hello").unwrap();
472        assert_eq!(
473            tmpl.render(context!(name => "John")).unwrap(),
474            "Hello John!".to_string()
475        )
476    }
477
478    /*    #[test]
479        fn test_simple() {
480            let shex_str = "\
481    prefix : <http://example.org/>
482    prefix xsd: <http://www.w3.org/2001/XMLSchema#>
483
484    :Person {
485      :name xsd:string ;
486      :knows @:Person  ;
487      :works_for @:Course * ;
488    }
489
490    :Course {
491      :name xsd:string
492    }";
493            let mut expected_uml = Uml::new();
494            expected_uml.add_label(Name::new(":Person", Some("http://example.org/Person")));
495            expected_uml.add_label(Name::new(":Course", Some("http://example.org/Course")));
496            let shex = ShExParser::parse(shex_str, None).unwrap();
497            let converter = ShEx2Uml::new(ShEx2UmlConfig::default());
498            let converted_uml = converter.convert(&shex).unwrap();
499            assert_eq!(converted_uml, expected_uml);
500        } */
501}