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(¨_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 template.render_to_write(self.current_html.to_landing_html_schema(&self.config), out)?;
123
124 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 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 #[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 }