001package org.hl7.fhir.r5.renderers.utils;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.Collections;
006import java.util.Comparator;
007import java.util.List;
008
009import org.apache.commons.lang3.StringUtils;
010import org.hl7.fhir.exceptions.DefinitionException;
011import org.hl7.fhir.exceptions.FHIRFormatError;
012import org.hl7.fhir.r5.model.CanonicalResource;
013import org.hl7.fhir.r5.model.CanonicalType;
014import org.hl7.fhir.r5.model.CodeableConcept;
015import org.hl7.fhir.r5.model.Coding;
016import org.hl7.fhir.r5.model.DataType;
017import org.hl7.fhir.r5.model.ElementDefinition;
018import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
019import org.hl7.fhir.r5.model.Enumerations.BindingStrength;
020import org.hl7.fhir.r5.model.Extension;
021import org.hl7.fhir.r5.model.Quantity;
022import org.hl7.fhir.r5.model.Resource;
023import org.hl7.fhir.r5.model.StructureDefinition;
024import org.hl7.fhir.r5.model.ValueSet;
025import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
026import org.hl7.fhir.r5.renderers.DataRenderer;
027import org.hl7.fhir.r5.renderers.Renderer.RenderingStatus;
028import org.hl7.fhir.r5.renderers.utils.ElementTable.HintDrivenGroupingEngine;
029import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome;
030import org.hl7.fhir.r5.utils.ToolingExtensions;
031import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
032import org.hl7.fhir.utilities.Utilities;
033import org.hl7.fhir.utilities.json.model.JsonArray;
034import org.hl7.fhir.utilities.json.model.JsonObject;
035import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
036import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell;
037import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row;
038import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel;
039import org.hl7.fhir.utilities.xhtml.NodeType;
040import org.hl7.fhir.utilities.xhtml.XhtmlNode;
041
042
043@MarkedToMoveToAdjunctPackage
044public class ElementTable {
045
046  public static class ElementTableGrouping {
047
048    private long key;
049    private String name;
050    private int priority;
051
052    public ElementTableGrouping(long key, String name, int priority) {
053      this.key = key;
054      this.name = name;
055      this.priority = priority;
056    }
057
058    public String getName() {
059      return name;
060    }
061
062    public int getPriority() {
063      return priority;
064    }
065
066    public long getKey() {
067      return key;
068    }
069
070  }
071  public enum ElementTableGroupingState {
072    UNKNOWN, DEFINES_GROUP, IN_GROUP
073  }
074  public static abstract class ElementTableGroupingEngine {
075
076    public abstract ElementTableGroupingState groupState(ElementDefinition ed);
077    public abstract ElementTableGrouping getGroup(ElementDefinition ed);
078    
079
080    protected boolean nameMatches(String name, List<String> strings) {
081      for (String s : strings) {
082        if (nameMatches(name, s)) {
083          return true;
084        }
085      }
086      return false;
087    }
088    
089    public boolean nameMatches(String name, String test) {
090      if (test.equals(name)) {
091        return true;
092      }
093      if (test.endsWith("[x]") && name.startsWith(test.substring(0, test.length()-3))) {
094        return true;
095      }
096      return false;
097    }    
098  }
099
100  public static class JsonDrivenGroupingEngine extends ElementTableGroupingEngine {
101
102    private JsonArray groups;
103    
104    public JsonDrivenGroupingEngine(JsonArray groups) {
105      super();
106      this.groups = groups;
107    }
108    
109   public ElementTableGroupingState groupState(ElementDefinition ed) {
110     String name = ed.getName();
111      for (JsonObject o : groups.asJsonObjects()  ) {
112        if (nameMatches(name, o.getStrings("elements"))) {
113          return ElementTableGroupingState.IN_GROUP;
114        }
115      }
116      for (JsonObject o : groups.asJsonObjects()  ) {
117        if (o.asBoolean("all")) {
118          return ElementTableGroupingState.IN_GROUP;
119        }
120      }
121      return ElementTableGroupingState.UNKNOWN;
122    }
123    
124    public ElementTableGrouping getGroup(ElementDefinition ed) {
125      String name = ed.getName();
126      int c = 0;
127      for (JsonObject o : groups.asJsonObjects()  ) {
128        c++;
129        if (nameMatches(name, o.getStrings("elements"))) {
130          return new ElementTableGrouping(c, o.asString("name"), groups.size() - c);
131        }
132      }
133      c = 0;
134      for (JsonObject o : groups.asJsonObjects()  ) {
135        c++;
136        if (o.asBoolean("all")) {
137          return new ElementTableGrouping(c, o.asString("name"), groups.size() - c);
138        }
139      }
140      return null;
141    }
142  }
143
144  public static class HintDrivenGroupingEngine extends ElementTableGroupingEngine {
145
146    private List<ElementDefinition> list;
147    
148    public HintDrivenGroupingEngine(List<ElementDefinition> list) {
149      super();
150      this.list = list;
151    }
152    
153    public ElementTableGroupingState groupState(ElementDefinition ed) {
154      if (ed.hasExtension(ToolingExtensions.EXT_PROFILE_VIEW_HINT)) {
155        List<Extension> exl = ed.getExtensionsByUrl(ToolingExtensions.EXT_PROFILE_VIEW_HINT);
156        for (Extension ex : exl) {
157          if ("element-view-group".equals(ex.getExtensionString("name")) ) {
158            return ElementTableGroupingState.DEFINES_GROUP;
159          }
160        }
161      }
162      return ElementTableGroupingState.UNKNOWN;
163    }
164    
165    public ElementTableGrouping getGroup(ElementDefinition ed) {
166      if (ed.hasExtension(ToolingExtensions.EXT_PROFILE_VIEW_HINT)) {
167        String n = null;
168        int order = 0;
169        List<Extension> exl = ed.getExtensionsByUrl(ToolingExtensions.EXT_PROFILE_VIEW_HINT);
170        for (Extension ex : exl) {
171          if ("element-view-group".equals(ex.getExtensionString("name")) ) {
172            n = ex.getExtensionString("value");
173          }
174          if ("element-view-order".equals(ex.getExtensionString("name")) ) {
175            order = ToolingExtensions.readIntegerExtension(ex, "value", 0);
176          }
177        }
178        if (n != null) {
179          return new ElementTableGrouping(ed.getName().hashCode(), n, order);
180        }
181      }
182      return null;
183    }
184  }
185
186  
187  public static enum TableElementDefinitionType {
188    DEFINITION, COMMENT, REQUIREMENTS;
189  } 
190  
191  public static class TableElementDefinition {
192    private TableElementDefinitionType type;
193    private String markdown;
194    public TableElementDefinition(TableElementDefinitionType type, String markdown) {
195      super();
196      this.type = type;
197      this.markdown = markdown;
198    }
199    public TableElementDefinitionType getType() {
200      return type;
201    }
202    public String getMarkdown() {
203      return markdown;
204    }
205    
206  }
207  
208  public enum TableElementConstraintType {
209    CHOICE, PROFILE, TARGET, BINDING, RANGE, FIXED, PATTERN, MAXLENGTH, CARDINALITY;
210  }
211  
212  public static class TableElementConstraint {
213    private TableElementConstraintType type;
214    private DataType value;
215    private DataType value2;
216    private String path;
217    private BindingStrength strength;
218    private String valueSet;
219    private List<TypeRefComponent> types;
220    private List<CanonicalType> list;
221
222    public static TableElementConstraint makeValue(TableElementConstraintType type, String path, DataType value) {
223      TableElementConstraint self = new TableElementConstraint();
224      self.type = type;
225      self.path = path;
226      self.value = value;
227      return self;
228    }
229
230    public static TableElementConstraint makeValueVS(TableElementConstraintType type, String path, DataType value, BindingStrength strength, String valueSet) {
231      TableElementConstraint self = new TableElementConstraint();
232      self.type = type;
233      self.path = path;
234      self.value = value;
235      self.strength = strength;
236      self.valueSet = valueSet;
237      return self;
238    }
239
240    public static TableElementConstraint makeRange(TableElementConstraintType type, String path, DataType value, DataType value2) {
241      TableElementConstraint self = new TableElementConstraint();
242      self.type = type;
243      self.path = path;
244      self.value = value;
245      self.value2 = value2;
246      return self;
247    }
248
249    public static TableElementConstraint makeBinding(TableElementConstraintType type, String path, BindingStrength strength, String valueSet) {
250      TableElementConstraint self = new TableElementConstraint();
251      self.type = type;
252      self.path = path;
253      self.strength = strength;
254      self.valueSet = valueSet;
255      return self;
256    }
257
258    public static TableElementConstraint makeTypes(TableElementConstraintType type, String path, List<TypeRefComponent> types) {
259      TableElementConstraint self = new TableElementConstraint();
260      self.type = type;
261      self.path = path;
262      self.types = types;
263      return self;
264    }
265
266    public static TableElementConstraint makeList(TableElementConstraintType type, String path, List<CanonicalType> list) {
267      TableElementConstraint self = new TableElementConstraint();
268      self.type = type;
269      self.path = path;
270      self.list = list;
271      return self;
272    }
273
274//    private BindingStrength strength;
275//    private ValueSet vs;
276//    private List<String> profiles;
277//    private List<String> targetProfiles;
278//    private DataType fixed;
279//    private DataType pattern;
280//    private DataType minValue;
281//    private DataType maxValue;
282  }
283  
284
285  public class ConstraintsSorter implements Comparator<TableElementConstraint> {
286
287    @Override
288    public int compare(TableElementConstraint o1, TableElementConstraint o2) {
289      int r = StringUtils.compare(o1.path, o2.path);
290      return r == 0 ? o1.type.compareTo(o2.type) : r;
291    }
292  }
293  
294  public static class TableElementInvariant {
295    private String level;
296    private String human;
297    private String fhirPath;
298    private String other;
299    private String otherFormat;
300  }
301  
302  public static class TableElement {
303    // column 1
304    private String path;
305    private String name;
306    private String min;
307    private String max;
308    private String typeName;
309    private String typeIcon;
310    private String typeLink;
311    private String typeHint;
312    
313    // column 2
314    private List<TableElementDefinition> definitions = new ArrayList<>();
315    
316    // column 3
317    private List<TableElementConstraint> constraints = new ArrayList<>();
318    
319    private List<TableElementInvariant> invariants = new ArrayList<>();
320    
321    private List<TableElement> childElements = new ArrayList<ElementTable.TableElement>();
322
323    public TableElement(String path, String name, String min, String max) {
324      super();
325      this.path = path;
326      this.name = name;
327      this.min = min;
328      this.max = max;
329    }
330
331    public String getPath() {
332      return path;
333    }
334
335    public String getName() {
336      return name;
337    }
338
339    public String getMin() {
340      return min;
341    }
342
343    public String getMax() {
344      return max;
345    }
346    public String getTypeName() {
347      return typeName;
348    }
349    public String getTypeIcon() {
350      return typeIcon;
351    }
352    public String getTypeLink() {
353      return typeLink;
354    }
355    public String getTypeHint() {
356      return typeHint;
357    }
358
359    public List<TableElementDefinition> getDefinitions() {
360      return definitions;
361    }
362
363    public List<TableElementConstraint> getConstraints() {
364      return constraints;
365    }
366
367    public List<TableElementInvariant> getInvariants() {
368      return invariants;
369    }
370
371    public List<TableElement> getChildElements() {
372      return childElements;
373    }
374
375    public TableElement setPath(String path) {
376      this.path = path;
377      return this;
378    }
379
380    public TableElement setName(String name) {
381      this.name = name;
382      return this;
383    }
384
385    public TableElement setMin(String min) {
386      this.min = min;
387      return this;
388    }
389
390    public TableElement setMax(String max) {
391      this.max = max;
392      return this;
393    }
394    
395    public void setType(String name, String link, String hint, String icon) {
396      this.typeName = name;
397      this.typeIcon = icon;
398      this.typeLink = link;
399      this.typeHint = hint;
400    }
401    
402  }
403  
404  public static class TableGroup {
405    private String name;
406    private String documentation;
407    private boolean buildIfEmpty;
408    private String emptyNote;
409    private List<TableElement> elements = new ArrayList<ElementTable.TableElement>();
410    private int priorty;
411    private int counter;
412    private ElementTableGrouping definition;
413    
414    public TableGroup(int counter, ElementTableGrouping definition) {
415      super();
416      this.counter = counter;
417      this.definition = definition;
418      name = definition.getName();
419      priorty = definition.getPriority();
420      buildIfEmpty = false;            
421    }
422
423    public String getName() {
424      return name;
425    }
426
427    public String getDocumentation() {
428      return documentation;
429    }
430
431    public boolean isBuildIfEmpty() {
432      return buildIfEmpty;
433    }
434
435    public String getEmptyNote() {
436      return emptyNote;
437    }
438
439    public List<TableElement> getElements() {
440      return elements;
441    }
442
443    public int getPriorty() {
444      return priorty;
445    }
446
447    public int getCounter() {
448      return counter;
449    }
450
451    public ElementTableGrouping getDefinition() {
452      return definition;
453    }
454    
455    
456  }
457  
458  public static class TableGroupSorter implements Comparator<TableGroup> {
459
460    @Override
461    public int compare(TableGroup o1, TableGroup o2) {
462      if (o1.priorty == o2.priorty) {
463        return Integer.compare(o1.counter, o2.counter);        
464      } else {
465        return Integer.compare(o2.priorty, o1.priorty); // priorty sorts backwards
466      }
467    }
468  }
469  
470  private RenderingContext context;
471  private List<TableGroup> groups;
472  private DataRenderer dr;
473  private boolean replaceCardinality;
474  
475  public ElementTable(RenderingContext context, List<TableGroup> groups, DataRenderer dr, boolean replaceCardinality) {
476    this.context = context;
477    this.groups = groups;
478    this.dr = dr;
479    this.replaceCardinality = replaceCardinality;
480  }
481
482  public void build(HierarchicalTableGenerator gen, TableModel table) throws FHIRFormatError, DefinitionException, IOException {
483    Collections.sort(groups, new TableGroupSorter());
484    table.setBorder(true);
485    table.setShowHeadings(false);
486
487    
488    for (TableGroup grp : groups) {
489      if (grp.getElements().size() > 0 || grp.buildIfEmpty) {
490        renderGroup(gen, table, grp);
491      }
492    }
493    
494  }
495
496  private void renderGroup(HierarchicalTableGenerator gen, TableModel table, TableGroup grp) throws FHIRFormatError, DefinitionException, IOException {
497    Row row = gen.new Row();
498    table.getRows().add(row);
499    Cell cell = gen.new Cell(null, null, grp.getName(), null, null);
500    row.getCells().add(cell);
501    cell.span(3);
502    row.setColor("#dfdfdf");
503    cell.addStyle("vertical-align: middle");
504    cell.addStyle("font-weight: bold");
505    cell.addStyle("font-size: 14px");
506    cell.addStyle("padding-top: 10px");
507    cell.addStyle("padding-bottom: 10px");
508
509    boolean first = true;
510    for (TableElement e : grp.elements) {
511      renderElement(gen, row.getSubRows(), e, first);
512      first = false;
513    }
514  }
515
516  private void renderElement(HierarchicalTableGenerator gen, List<Row> rows, TableElement e, boolean first) throws FHIRFormatError, DefinitionException, IOException {
517    Row row = gen.new Row();
518    rows.add(row);
519    if (!first) {
520      row.setTopLine("silver");
521    }
522    renderElementIdentity(gen, row, e);
523    renderElementDefinition(gen, row, e);
524    renderElementConstraints(gen, row, e);
525
526    
527//    if (e.invariants.size() > 0) {
528//      tr.style("border-bottom: none");
529//      tr = table.tr();
530//      tr.style("border-top: none");
531//      tr.style("border-left: black 1px solid");
532//      tr.style("border-right: black 1px solid");  
533//      renderElementInvariants(tr.td().colspan(3), e);
534//    } 
535//    tr.style("border-bottom: silver 1px solid");
536    
537    for (TableElement child : e.getChildElements()) {
538      renderElement(gen, row.getSubRows(), child, false);
539    }
540  }
541
542  public void renderElementIdentity(HierarchicalTableGenerator gen, Row row, TableElement e) {
543    Cell cell = gen.new Cell();
544    cell.addCellStyle("min-width: 220px");
545    row.getCells().add(cell);
546    cell.setInnerTable(true);
547    cell.addText(e.getName()).addStyle("font-weight: bold");
548    cell.addPiece(gen.new Piece("br"));
549    if (!replaceCardinality) {
550      cell.addText("Cardinality: "+e.min+".."+e.max);  
551    } else if ("1".equals(e.min) && "1".equals(e.max)) {
552      cell.addText("Required");
553    } else if ("0".equals(e.min) && "*".equals(e.max)) {
554      cell.addText("Optional, Repeating");
555    } else if ("0".equals(e.min) && "1".equals(e.max)) {
556      cell.addText("Optional");
557    } else if ("1".equals(e.min) && "*".equals(e.max)) {
558      cell.addText("Repeating");
559    } else {
560      cell.addText("Cardinality: "+e.min+".."+e.max);      
561    }
562    cell.addPiece(gen.new Piece("br"));
563    cell.addImg(e.getTypeIcon(), e.getTypeHint(), e.getTypeLink());
564    cell.addPiece(gen.new Piece(e.getTypeLink(), " "+e.getTypeName(), e.getTypeHint()));      
565  }
566
567  public void renderElementConstraints(HierarchicalTableGenerator gen, Row row, TableElement e) throws FHIRFormatError, DefinitionException, IOException {
568    Cell cell = gen.new Cell();
569    cell.addCellStyle("min-width: 300px");
570    row.getCells().add(cell);
571    XhtmlNode div = new XhtmlNode(NodeType.Element, "div");
572    
573    Collections.sort(e.getConstraints(), new ConstraintsSorter());
574    boolean first = true;
575    for (TableElementConstraint c : e.getConstraints()) {
576      if (first) first= false; else div.br();
577      switch (c.type) {
578      case BINDING:
579        renderBindingConstraint(div, c);
580        break;
581      case CHOICE:
582        renderChoiceConstraint(div, c);
583        break;
584      case FIXED:
585        renderValueConstraint(div, c);
586        break;
587      case MAXLENGTH:
588        renderValueConstraint(div, c);
589        break;
590      case PATTERN:
591        renderValueConstraint(div, c);
592        break;
593      case PROFILE:
594        renderListConstraint(div, c);
595        break;
596      case RANGE:
597        renderRangeConstraint(div, c);
598        break;
599      case TARGET:
600        renderListConstraint(div, c);
601        break;
602      case CARDINALITY:
603        renderCardinalityConstraint(div, c);
604      break;
605      default:
606        break;
607      
608      }
609    }
610    cell.addXhtml(div);
611  }
612
613  private void renderBindingConstraint(XhtmlNode x, TableElementConstraint c) {
614    String name = c.path == null ? "value" : c.path;
615    x.code().tx(name);
616    renderBinding(x, c, " is bound to "); 
617  }
618
619  private void renderBinding(XhtmlNode x, TableElementConstraint c, String phrase) {
620    ValueSet vs = context.getContext().findTxResource(ValueSet.class, c.valueSet);
621    if (vs == null) {
622      x.tx(phrase+"an unknown valueset ");
623      x.code().tx(c.valueSet);      
624    } else {
625      x.tx(phrase);
626      x.ah(vs.getWebPath()).tx(vs.present());
627      try {      
628        ValueSetExpansionOutcome exp = context.getContext().expandVS(vs, true, false);
629        if (!exp.isOk()) {
630          x.span().attribute("title", exp.getError()).tx(" (??)");                  
631        } else if (exp.getValueset().getExpansion().getContains().size() == 1000) {
632          x.tx(" (>1000 codes)");                            
633        } else if (exp.getValueset().getExpansion().getContains().size() > 6) {
634          x.tx(" ("+exp.getValueset().getExpansion().getContains().size()+" codes)");                            
635        } else {
636          x.tx(".  Codes:");
637          XhtmlNode ul = x.ul();
638          for (ValueSetExpansionContainsComponent cc : exp.getValueset().getExpansion().getContains()) {
639
640            String url = cc.hasSystem() && cc.hasCode() ? dr.getLinkForCode(cc.getSystem(), cc.getVersion(), cc.getCode()) : null; 
641            var li = ul.li();
642            li.ahOrNot(url).tx(dr.displayCodeSource(cc.getSystem(), cc.getVersion())+": "+cc.getCode());
643            if (cc.hasDisplay()) {
644              li.tx(" \""+cc.getDisplay()+"\"");
645            } 
646          }
647        }
648      } catch (Exception e) {
649        x.span().attribute("title", e.getMessage()).tx(" (??)");        
650      }
651    }
652  }
653
654  private void renderChoiceConstraint(XhtmlNode x, TableElementConstraint c) {
655    String name = c.path == null ? "value" : c.path;
656    x.code().tx(name);
657    x.tx(" is a choice of:");
658    var ul = x.ul();
659    for (TypeRefComponent tr : c.types) {
660      if (tr.hasProfile()) {
661        for (CanonicalType ct : tr.getProfile()) {
662          StructureDefinition sd = context.getContext().fetchTypeDefinition(ct.primitiveValue());
663          if (sd == null || !sd.hasWebPath()) {
664            ul.li().ah(ct.primitiveValue()).tx(ct.primitiveValue());
665          } else {
666            ul.li().ah(sd.getWebPath()).tx(sd.present());
667          }
668        }
669      } else if (tr.hasTarget()) {
670        StructureDefinition sd = context.getContext().fetchTypeDefinition(tr.getWorkingCode());
671        var li = ul.li();
672        li.ah(sd.getWebPath()).tx(sd.present());
673        li.tx(" pointing to ");
674        renderTypeList(x, tr.getTargetProfile());        
675        
676      } else {
677        StructureDefinition sd = context.getContext().fetchTypeDefinition(tr.getWorkingCode());
678        if (sd == null || !sd.hasWebPath()) {
679          ul.li().code().tx(tr.getWorkingCode());
680        } else {
681          ul.li().ah(sd.getWebPath()).tx(sd.present());
682        }
683      }
684    }
685    
686    
687  }
688
689  private void renderValueConstraint(XhtmlNode x, TableElementConstraint c) throws FHIRFormatError, DefinitionException, IOException {
690    String name = c.path == null ? "value" : c.path;
691    x.code().tx(name);
692    switch (c.type) {
693    case FIXED:
694      x.tx(" is fixed to ");
695      break;
696    case MAXLENGTH:
697      x.tx(" is limited  in length to ");
698
699      break;
700    case PATTERN:
701      if (c.value.isPrimitive()) {
702        x.tx(" is fixed to ");
703      } else {
704        x.tx(" must match ");
705      }
706      break;
707    default:
708      break;
709    }
710    renderValue(x, c.value);
711    if (c.strength != null && c.valueSet != null) {
712      renderBinding(x, c, " from ");
713    }
714  }
715
716  public void renderValue(XhtmlNode x, DataType v) throws IOException {
717    if (v.isPrimitive()) {
718      String s = v.primitiveValue();
719      if (Utilities.isAbsoluteUrl(s)) {
720        Resource res = context.getContext().fetchResource(Resource.class, s);
721        if (res != null && res.hasWebPath()) {
722          x.ah(res.getWebPath()).tx(res instanceof CanonicalResource ? ((CanonicalResource) res).present() : s);                    
723        } else if (Utilities.isAbsoluteUrlLinkable(s)) {
724          x.ah(s).code().tx(s);          
725        } else {
726          x.code().tx(s);
727        }
728      } else {
729        x.code().tx(s);
730      }
731    } else if (v instanceof Quantity) {
732      genQuantity(x, (Quantity) v);        
733    } else if (v instanceof Coding) {
734      genCoding(x, (Coding) v);
735    } else if (v instanceof CodeableConcept) {
736      genCodeableConcept(x, (CodeableConcept) v);
737    } else {
738      dr.renderBase(new RenderingStatus(), x, v);
739    }
740  }
741
742  private void genCodeableConcept(XhtmlNode div, CodeableConcept cc) {
743    boolean first = true;
744    for (Coding c : cc.getCoding()) {
745      if (first) first = false; else div.tx(",");
746      genCoding(div, c);
747    }
748    if (cc.hasText()) {
749      div.code().tx(" \""+cc.getText()+"\"");      
750    }
751    
752  }
753
754  public void genQuantity(XhtmlNode div, Quantity q) {
755    String url = q.hasSystem() && q.hasUnit() ? dr.getLinkForCode(q.getSystem(), null, q.getCode()) : null; 
756    var code = div.code();
757    if (q.hasComparator()) {
758      code.tx(q.getComparator().toCode());
759    }
760    code.tx(q.getValueElement().asStringValue());
761    code.ahOrNot(url).tx(q.getUnit());
762  }
763
764  public void genCoding(XhtmlNode div, Coding c) {
765    String url = c.hasSystem() && c.hasCode() ? dr.getLinkForCode(c.getSystem(), c.getVersion(), c.getCode()) : null; 
766    var code = div.code();
767    code.ahOrNot(url).tx(dr.displayCodeSource(c.getSystem(), c.getVersion())+": "+c.getCode());
768    if (c.hasDisplay()) {
769      code.tx(" \""+c.getDisplay()+"\"");
770    } else {
771      String s = dr.lookupCode(c.getSystem(), c.getVersion(), c.getCode());
772      if (s != null) {
773        div.span().style("opacity: 0.5").tx("(\""+s+"\")");
774      }
775    }
776  }
777
778  private void renderRangeConstraint(XhtmlNode x, TableElementConstraint c) throws IOException {
779    String name = c.path == null ? "value" : c.path;
780    if (c.value != null && c.value2 != null) {
781      x.tx(name + " between ");
782      renderValue(x, c.value);
783      x.tx(" and " );
784      renderValue(x, c.value2);
785    } else if (c.value != null) {
786      x.tx(name + " more than ");
787      renderValue(x, c.value);
788    } else  {
789      x.tx(name + " less than ");
790      renderValue(x, c.value2);
791    }
792  }
793
794  private void renderCardinalityConstraint(XhtmlNode x, TableElementConstraint c) throws IOException {
795    String name = c.path == null ? "value" : c.path;
796    x.code().tx(name);
797    String min = c.value.primitiveValue();
798    String max = c.value2.primitiveValue(); 
799    if (!replaceCardinality) {
800      x.tx("has cardinality: "+min+".."+max);  
801    } else if ("1".equals(min) && "1".equals(max)) {
802      x.tx("is required");
803    } else if ("0".equals(min) && "*".equals(max)) {
804      x.tx("is Optional and repeats");
805    } else if ("0".equals(min) && "1".equals(max)) {
806      x.tx("is Optional");
807    } else if ("1".equals(min) && "*".equals(max)) {
808      x.tx("repeats");
809    } else {
810      x.tx("has cardinality: "+min+".."+max);      
811    }
812  }
813
814  private void renderListConstraint(XhtmlNode x, TableElementConstraint c) {
815    String name = c.path == null ? "value" : c.path;
816
817    x.code().tx(name);
818    switch (c.type) {
819    case PROFILE:
820      x.tx(" must be ");
821      break;
822    case TARGET:
823      x.tx(" must point to ");
824      break;
825    default:
826      break;
827    }
828    renderTypeList(x, c.list);
829  }
830
831  private void renderTypeList(XhtmlNode x, List<CanonicalType> list) {
832
833    if (list.size() == 1) {
834      x.tx("a ");
835    } else {
836      x.tx("one of ");
837    }
838    boolean first = true;
839    for (int i = 0; i < list.size(); i++) {
840      if (first) {
841        first = false;
842      } else if (i == list.size() - 1) {
843        x.tx(" or " );
844      } else {
845        x.tx(", ");
846      }
847      String s = list.get(i).primitiveValue();
848      Resource res = context.getContext().fetchResource(Resource.class, s);
849      if (res != null && res.hasWebPath()) {
850        x.ah(res.getWebPath()).tx(res instanceof CanonicalResource ? ((CanonicalResource) res).present() : s);
851      } else {
852        x.ah(s).tx(s);
853      }
854    }    
855  }
856
857  public void renderElementDefinition(HierarchicalTableGenerator gen, Row row, TableElement e) {
858    Cell cell = gen.new Cell();    row.getCells().add(cell);
859    for (TableElementDefinition d : e.definitions) {
860      if (d.getType() == TableElementDefinitionType.DEFINITION) {
861        cell.addMarkdown(d.getMarkdown());
862      } else if (d.getType() == TableElementDefinitionType.COMMENT) {
863        cell.addMarkdown("Comment: "+d.getMarkdown(), "font-style: italic");
864      }
865    }
866  }
867
868  private void renderElementInvariants(XhtmlNode td, TableElement e) {
869    XhtmlNode ul = td.ul();
870    for (TableElementInvariant t : e.invariants) {
871      var li = ul.li();
872      li.tx(t.level+": "+t.human);
873      li.tx(" ");
874      li.code().tx(t.other != null ? t.other : t.fhirPath);
875    }
876  
877  }
878
879}