From a0c28f33266aa3365fe3e336c3f9f8ea57fe7910 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 5 Dec 2022 13:41:31 +1100 Subject: [PATCH] Round trip XHTML faithfully wrt empty elements --- .../fhir/r5/test/utils/TestPackageLoader.java | 9 +++ .../fhir/utilities/xhtml/XhtmlComposer.java | 68 +++++++++++-------- .../hl7/fhir/utilities/xhtml/XhtmlNode.java | 24 +++++-- .../hl7/fhir/utilities/xhtml/XhtmlParser.java | 4 ++ .../fhir/utilities/tests/XhtmlNodeTest.java | 23 +++++++ 5 files changed, 92 insertions(+), 36 deletions(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/test/utils/TestPackageLoader.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/test/utils/TestPackageLoader.java index 130dce1db..448e4bb70 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/test/utils/TestPackageLoader.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/test/utils/TestPackageLoader.java @@ -2,12 +2,15 @@ package org.hl7.fhir.r5.test.utils; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r5.context.IWorkerContext.IContextResourceLoader; import org.hl7.fhir.r5.formats.JsonParser; import org.hl7.fhir.r5.formats.XmlParser; import org.hl7.fhir.r5.model.Bundle; +import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.utilities.npm.NpmPackage; @@ -43,4 +46,10 @@ public class TestPackageLoader implements IContextResourceLoader { public IContextResourceLoader getNewLoader(NpmPackage npm) { return this; } + + @Override + public List getCodeSystems() { + return new ArrayList<>(); + } + } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlComposer.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlComposer.java index 1ff09d550..53ae3b671 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlComposer.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlComposer.java @@ -90,40 +90,42 @@ public class XhtmlComposer { private void composeDoc(XhtmlDocument doc) throws IOException { // headers.... // dst.append("" + (pretty ? "\r\n" : "")); - for (XhtmlNode c : doc.getChildNodes()) + for (XhtmlNode c : doc.getChildNodes()) { writeNode(" ", c, false); + } // dst.append("" + (pretty ? "\r\n" : "")); } private void writeNode(String indent, XhtmlNode node, boolean noPrettyOverride) throws IOException { - if (node.getNodeType() == NodeType.Comment) + if (node.getNodeType() == NodeType.Comment) { writeComment(indent, node, noPrettyOverride); - else if (node.getNodeType() == NodeType.DocType) + } else if (node.getNodeType() == NodeType.DocType) { writeDocType(node); - else if (node.getNodeType() == NodeType.Instruction) + } else if (node.getNodeType() == NodeType.Instruction) { writeInstruction(node); - else if (node.getNodeType() == NodeType.Element) + } else if (node.getNodeType() == NodeType.Element) { writeElement(indent, node, noPrettyOverride); - else if (node.getNodeType() == NodeType.Document) + } else if (node.getNodeType() == NodeType.Document) { writeDocument(indent, node); - else if (node.getNodeType() == NodeType.Text) + } else if (node.getNodeType() == NodeType.Text) { writeText(node); - else if (node.getNodeType() == null) + } else if (node.getNodeType() == null) { throw new IOException("Null node type"); - else + } else { throw new IOException("Unknown node type: "+node.getNodeType().toString()); + } } private void writeText(XhtmlNode node) throws IOException { for (char c : node.getContent().toCharArray()) { - if (c == '&') + if (c == '&') { dst.append("&"); - else if (c == '<') + } else if (c == '<') { dst.append("<"); - else if (c == '>') + } else if (c == '>') { dst.append(">"); - else if (xml) { + } else if (xml) { if (c == '"') dst.append("""); else @@ -189,26 +191,34 @@ public class XhtmlComposer { indent = ""; // html self closing tags: http://xahlee.info/js/html5_non-closing_tag.html - if (node.getChildNodes().size() == 0 && (xml || Utilities.existsInList(node.getName(), "area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"))) + boolean concise = node.getChildNodes().size() == 0; + if (node.hasEmptyExpanded() && node.getEmptyExpanded()) { + concise = false; + } + if (!xml && Utilities.existsInList(node.getName(), "area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr")) { + concise = false; + } + + if (concise) dst.append(indent + "<" + node.getName() + attributes(node) + "/>" + (pretty && !noPrettyOverride ? "\r\n" : "")); else { - boolean act = node.allChildrenAreText(); - if (act || !pretty || noPrettyOverride) - dst.append(indent + "<" + node.getName() + attributes(node)+">"); - else - dst.append(indent + "<" + node.getName() + attributes(node) + ">\r\n"); - if (node.getName() == "head" && node.getElement("meta") == null) - dst.append(indent + " " + (pretty && !noPrettyOverride ? "\r\n" : "")); + boolean act = node.allChildrenAreText(); + if (act || !pretty || noPrettyOverride) + dst.append(indent + "<" + node.getName() + attributes(node)+">"); + else + dst.append(indent + "<" + node.getName() + attributes(node) + ">\r\n"); + if (node.getName() == "head" && node.getElement("meta") == null) + dst.append(indent + " " + (pretty && !noPrettyOverride ? "\r\n" : "")); - for (XhtmlNode c : node.getChildNodes()) - writeNode(indent + " ", c, noPrettyOverride || node.isNoPretty()); - if (act) - dst.append("" + (pretty && !noPrettyOverride ? "\r\n" : "")); - else if (node.getChildNodes().get(node.getChildNodes().size() - 1).getNodeType() == NodeType.Text) - dst.append((pretty && !noPrettyOverride ? "\r\n"+ indent : "") + "" + (pretty && !noPrettyOverride ? "\r\n" : "")); - else - dst.append(indent + "" + (pretty && !noPrettyOverride ? "\r\n" : "")); + for (XhtmlNode c : node.getChildNodes()) + writeNode(indent + " ", c, noPrettyOverride || node.isNoPretty()); + if (act) + dst.append("" + (pretty && !noPrettyOverride ? "\r\n" : "")); + else if (node.getChildNodes().get(node.getChildNodes().size() - 1).getNodeType() == NodeType.Text) + dst.append((pretty && !noPrettyOverride ? "\r\n"+ indent : "") + "" + (pretty && !noPrettyOverride ? "\r\n" : "")); + else + dst.append(indent + "" + (pretty && !noPrettyOverride ? "\r\n" : "")); } } diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java index fb7d8f11f..0869f1fb3 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java @@ -1,5 +1,7 @@ package org.hl7.fhir.utilities.xhtml; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + /* Copyright (c) 2011+, HL7, Inc. All rights reserved. @@ -38,21 +40,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; import org.hl7.fhir.instance.model.api.IBaseXhtml; import org.hl7.fhir.utilities.MarkDownProcessor; -import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.MarkDownProcessor.Dialect; -import org.hl7.fhir.utilities.i18n.I18nConstants; -import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; +import org.hl7.fhir.utilities.Utilities; -import ca.uhn.fhir.model.api.annotation.ChildOrder; import ca.uhn.fhir.model.primitive.XhtmlDt; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - @ca.uhn.fhir.model.api.annotation.DatatypeDef(name="xhtml") public class XhtmlNode implements IBaseXhtml { private static final long serialVersionUID = -4362547161441436492L; @@ -93,6 +89,7 @@ public class XhtmlNode implements IBaseXhtml { private boolean inPara; private boolean inLink; private boolean seperated; + private Boolean emptyExpanded; public XhtmlNode() { super(); @@ -416,6 +413,19 @@ public class XhtmlNode implements IBaseXhtml { } + public Boolean getEmptyExpanded() { + return emptyExpanded; + } + + public boolean hasEmptyExpanded() { + return emptyExpanded != null; + } + + public void setEmptyExpanded(Boolean emptyExpanded) { + this.emptyExpanded = emptyExpanded; + } + + @Override public String getValueAsString() { if (isEmpty()) { diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlParser.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlParser.java index ea228c9b8..814754e8a 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlParser.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlParser.java @@ -506,10 +506,12 @@ public class XhtmlParser { if (peekChar() != '>') throw new FHIRFormatError("unexpected non-end of element "+n+" "+descLoc()); readChar(); + root.setEmptyExpanded(false); } else { unwindPoint = null; List p = new ArrayList<>(); parseElementInner(root, p, nsm, true); + root.setEmptyExpanded(true); } return result; } @@ -671,7 +673,9 @@ public class XhtmlParser { if (peekChar() != '>') throw new FHIRFormatError("unexpected non-end of element "+name+" "+descLoc()); readChar(); + node.setEmptyExpanded(false); } else { + node.setEmptyExpanded(true); parseElementInner(node, newParents, namespaceMap, "script".equals(name.getName())); } } diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/tests/XhtmlNodeTest.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/tests/XhtmlNodeTest.java index d585e2f9f..08c655edb 100644 --- a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/tests/XhtmlNodeTest.java +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/tests/XhtmlNodeTest.java @@ -6,6 +6,8 @@ import java.io.ObjectOutputStream; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.utilities.TextFile; +import org.hl7.fhir.utilities.xhtml.XhtmlComposer; import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.hl7.fhir.utilities.xhtml.XhtmlParser; import org.junit.jupiter.api.Assertions; @@ -137,4 +139,25 @@ public class XhtmlNodeTest { Assertions.assertEquals("http://www.w3.org/1999/xlink", x.getChildNodes().get(0).getChildNodes().get(1).getAttributes().get("xmlns:xlink")); } + + @Test + public void testParseSvgElements() throws FHIRFormatError, IOException { + String src = BaseTestingUtilities.loadTestResource("xhtml", "xhtml-empty-elements.xml"); + XhtmlNode x = new XhtmlParser().parse(src, "xml"); + + + String xml = new XhtmlComposer(false, false).compose(x); + Assertions.assertEquals(src.trim(), xml.trim()); + } + + + @Test + public void testParseSvgF() throws FHIRFormatError, IOException { + String src = TextFile.fileToString("/Users/grahamegrieve/work/r5/source/fhir-exchanges.svg.html"); + XhtmlNode x = new XhtmlParser().parse(src, "svg"); + String xml = new XhtmlComposer(false, true).compose(x); + TextFile.stringToFile(xml, "/Users/grahamegrieve/work/r5/source/fhir-exchanges.svg.html"); + } + + } \ No newline at end of file