Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M src/main/java/com/keenwrite/dom/DocumentConverter.java
1616
import java.util.LinkedHashMap;
1717
import java.util.Map;
18
import java.util.Set;
1819
1920
import static com.keenwrite.dom.DocumentParser.sDomImplementation;
...
4142
    entry( "fi", "fi" ),
4243
    entry( "fl", "fl" )
44
  );
45
46
  private static final Set<String> CODE_BLOCKS = Set.of(
47
    "pre",
48
    "code",
49
    "kbd",
50
    "script",
51
    "style",
52
    "samp",
53
    "blockcode",
54
    "var",
55
    "tex",
56
    "tt"
4357
  );
4458
4559
  private static final NodeVisitor LIGATURE_VISITOR = new NodeVisitor() {
4660
    @Override
4761
    public void head( final @NotNull Node node, final int depth ) {
4862
      if( node instanceof final TextNode textNode ) {
4963
        final var parent = node.parentNode();
5064
        final var name = parent == null ? "root" : parent.nodeName();
51
        final var codeBlock =
52
          "pre".equalsIgnoreCase( name ) ||
53
          "code".equalsIgnoreCase( name ) ||
54
          "kbd".equalsIgnoreCase( name ) ||
55
          "var".equalsIgnoreCase( name ) ||
56
          "tex".equalsIgnoreCase( name ) ||
57
          "tt".equalsIgnoreCase( name );
5865
59
        if( !codeBlock ) {
66
        if( !CODE_BLOCKS.contains( name.toLowerCase() ) ) {
6067
          // Obtaining the whole text will return newlines, which must be kept
6168
          // to ensure that preformatted text maintains its formatting.
M src/main/java/com/keenwrite/dom/DocumentParser.java
3636
import static javax.xml.xpath.XPathConstants.NODE;
3737
import static javax.xml.xpath.XPathConstants.NODESET;
38
39
/**
40
 * Responsible for initializing an XML parser.
41
 */
42
public class DocumentParser {
43
  private static final String LOAD_EXTERNAL_DTD =
44
    "http://apache.org/xml/features/nonvalidating/load-external-dtd";
45
  private static final String INDENT_AMOUNT =
46
    "{http://xml.apache.org/xslt}indent-amount";
47
  private static final String NAMESPACE = "http://www.w3.org/1999/xhtml";
48
49
  private static final XPath XPATH = XPathFactory.newInstance().newXPath();
50
51
  private static final ByteArrayOutputStream sWriter =
52
    new ByteArrayOutputStream( 65536 );
53
  private static final OutputStreamWriter sOutput =
54
    new OutputStreamWriter( sWriter, UTF_8 );
55
56
  /**
57
   * Caches {@link XPathExpression}s to avoid re-compiling.
58
   */
59
  private static final Map<String, XPathExpression> sXpaths = new HashMap<>();
60
61
  private static final DocumentBuilderFactory sDocumentFactory;
62
  private static DocumentBuilder sDocumentBuilder;
63
  private static Transformer sTransformer;
64
  private static final XPath sXpath = XPathFactory.newInstance().newXPath();
65
66
  public static final DOMImplementation sDomImplementation;
67
68
  static {
69
    sDocumentFactory = DocumentBuilderFactory.newInstance();
70
71
    sDocumentFactory.setValidating( false );
72
    sDocumentFactory.setAttribute( LOAD_EXTERNAL_DTD, false );
73
    sDocumentFactory.setNamespaceAware( true );
74
    sDocumentFactory.setIgnoringComments( true );
75
    sDocumentFactory.setIgnoringElementContentWhitespace( true );
76
77
    DOMImplementation domImplementation;
78
79
    try {
80
      sDocumentBuilder = sDocumentFactory.newDocumentBuilder();
81
      domImplementation = sDocumentBuilder.getDOMImplementation();
82
      sTransformer = TransformerFactory.newInstance().newTransformer();
83
84
      // Ensure Unicode characters (emojis) are encoded correctly.
85
      sTransformer.setOutputProperty( ENCODING, UTF_16.toString() );
86
      sTransformer.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
87
      sTransformer.setOutputProperty( METHOD, "xml" );
88
      sTransformer.setOutputProperty( INDENT, "no" );
89
      sTransformer.setOutputProperty( INDENT_AMOUNT, "2" );
90
    }
91
    catch( final Exception ex ) {
92
      clue( ex );
93
      domImplementation = sDocumentBuilder.getDOMImplementation();
94
    }
95
96
    sDomImplementation = domImplementation;
97
  }
98
99
  public static Document newDocument() {
100
    return sDocumentBuilder.newDocument();
101
  }
102
103
  /**
104
   * Creates a new document object model based on the given XML document
105
   * string. This will return an empty document if the document could not
106
   * be parsed.
107
   *
108
   * @param xml The document text to convert into a DOM.
109
   * @return The DOM that represents the given XML data.
110
   */
111
  public static Document parse( final String xml ) {
112
    assert xml != null;
113
114
    if( !xml.isBlank() ) {
115
      try( final var reader = new StringReader( xml ) ) {
116
        final var input = new InputSource();
117
118
        input.setEncoding( UTF_8.toString() );
119
        input.setCharacterStream( reader );
120
121
        return sDocumentBuilder.parse( input );
122
      }
123
      catch( final Throwable t ) {
124
        clue( t );
125
      }
126
    }
127
128
    return sDocumentBuilder.newDocument();
129
  }
130
131
  /**
132
   * Creates a well-formed XHTML document from a standard HTML document.
133
   *
134
   * @param source   The HTML source document to transform.
135
   * @param metadata The metadata contained within the head element.
136
   * @param locale   The localization information for the lang attribute.
137
   * @return The well-formed XHTML document.
138
   */
139
  public static Document create(
140
    final Document source,
141
    final Map<String, String> metadata,
142
    final Locale locale,
143
    final String pageTitle
144
  ) throws XPathExpressionException {
145
    final var target = createXhtmlDocument();
146
    final var html = target.getDocumentElement();
147
    final var sourceHead = evaluate( "//head", source );
148
    final var head = target.importNode( sourceHead, true );
149
150
    html.setAttribute( "lang", locale.getLanguage() );
151
152
    final var encoding = createEncoding( target, "UTF-8" );
153
    head.appendChild( encoding );
154
155
    for( final var entry : metadata.entrySet() ) {
156
      final var node = createMeta( target, entry );
157
      head.appendChild( node );
158
    }
159
160
    final var titleText = Strings.sanitize( pageTitle );
161
162
    // Empty titles result in <title/>, which some browsers cannot parse.
163
    if( !titleText.isBlank() ) {
164
      final var title = createElement( target, "title", titleText );
165
      head.appendChild( title );
166
    }
167
168
    html.appendChild( head );
169
170
    final var body = createElement( target, "body", null );
171
    final var sourceBody = source.getElementsByTagName( "body" ).item( 0 );
172
    final var children = sourceBody.getChildNodes();
173
    final var count = children.getLength();
174
175
    for( var i = 0; i < count; i++ ) {
176
      body.appendChild( importNode( target, children.item( i ) ) );
177
    }
178
179
    html.appendChild( body );
180
181
    return target;
182
  }
183
184
  public static Node evaluate( final String xpath, final Document doc ) throws XPathExpressionException {
185
    return (Node) XPATH.evaluate( xpath, doc, NODE );
186
  }
187
188
  /**
189
   * Parses the given file contents into a document object model.
190
   *
191
   * @param doc The source XML document to parse.
192
   * @return The file as a document object model.
193
   * @throws IOException  Could not open the document.
194
   * @throws SAXException Could not read the XML file content.
195
   */
196
  public static Document parse( final File doc )
197
    throws IOException, SAXException {
198
    assert doc != null;
199
200
    try( final var in = new FileInputStream( doc ) ) {
201
      return parse( in );
202
    }
203
  }
204
205
  /**
206
   * Parses the given file contents into a document object model. Callers
207
   * must close the stream.
208
   *
209
   * @param doc The source XML document to parse.
210
   * @return The {@link InputStream} converted to a document object model.
211
   * @throws IOException  Could not open the document.
212
   * @throws SAXException Could not read the XML file content.
213
   */
214
  public static Document parse( final InputStream doc )
215
    throws IOException, SAXException {
216
    assert doc != null;
217
218
    return sDocumentBuilder.parse( doc );
219
  }
220
221
  /**
222
   * Allows an operation to be applied for every node in the document that
223
   * matches a given tag name pattern.
224
   *
225
   * @param document Document to traverse.
226
   * @param xpath    Document elements to find via {@link XPath} expression.
227
   * @param consumer The consumer to call for each matching document node.
228
   */
229
  public static void visit(
230
    final Document document,
231
    final CharSequence xpath,
232
    final Consumer<Node> consumer ) {
233
    assert document != null;
234
    assert consumer != null;
235
236
    try {
237
      final var expr = compile( xpath );
238
      final var nodeSet = expr.evaluate( document, NODESET );
239
240
      if( nodeSet instanceof NodeList nodes ) {
241
        for( int i = 0, len = nodes.getLength(); i < len; i++ ) {
242
          consumer.accept( nodes.item( i ) );
243
        }
244
      }
245
    }
246
    catch( final Exception ex ) {
247
      clue( ex );
248
    }
249
  }
250
251
  public static Node createMeta(
252
    final Document document, final Map.Entry<String, String> entry ) {
253
    assert document != null;
254
    assert entry != null;
255
256
    final var node = createElement( document, "meta", null );
257
258
    node.setAttribute( "name", entry.getKey() );
259
    node.setAttribute( "content", entry.getValue() );
260
261
    return node;
262
  }
263
264
  public static Node createEncoding(
265
    final Document document, final String encoding
266
  ) {
267
    assert document != null;
268
    assert encoding != null;
269
270
    final var node = createElement( document, "meta", null );
271
272
    node.setAttribute( "http-equiv", "Content-Type" );
273
    node.setAttribute( "content", "text/html; charset=" + encoding );
274
275
    return node;
276
  }
277
278
  public static Element createElement(
279
    final Document document, final String nodeName, final String nodeValue
280
  ) {
281
    assert document != null;
282
    assert nodeName != null;
283
    assert !nodeName.isBlank();
284
285
    final var node = document.createElement( nodeName );
286
287
    if( nodeValue != null ) {
288
      node.setTextContent( nodeValue );
289
    }
290
291
    return node;
292
  }
293
294
  public static String toString( final Node xhtml ) {
295
    assert xhtml != null;
296
297
    String result = "";
298
299
    try( final var writer = new StringWriter() ) {
300
      final var stream = new StreamResult( writer );
301
302
      transform( xhtml, stream );
303
304
      result = writer.toString();
305
    }
306
    catch( final Exception ex ) {
307
      clue( ex );
308
    }
309
310
    return result;
311
  }
312
313
  public static String transform( final Element root )
314
    throws IOException, TransformerException {
315
    assert root != null;
316
317
    try( final var writer = new StringWriter() ) {
318
      transform( root.getOwnerDocument(), new StreamResult( writer ) );
319
320
      return writer.toString();
321
    }
322
  }
323
324
  /**
325
   * Remove whitespace, comments, and XML/DOCTYPE declarations to make
326
   * processing work with ConTeXt.
327
   *
328
   * @param path The SVG file to process.
329
   * @throws Exception The file could not be processed.
330
   */
331
  public static void sanitize( final Path path ) throws Exception {
332
    assert path != null;
333
334
    // Preprocessing the SVG image is a single-threaded operation, no matter
335
    // how many SVG images are in the document to typeset.
336
    sWriter.reset();
337
338
    final var target = new StreamResult( sOutput );
339
    final var source = sDocumentBuilder.parse( toFile( path ) );
340
341
    transform( source, target );
342
    write( path, sWriter.toByteArray() );
343
  }
344
345
  /**
346
   * Converts a string into an {@link XPathExpression}, which may be used to
347
   * extract elements from a {@link Document} object model.
348
   *
349
   * @param cs The string to convert to an {@link XPathExpression}.
350
   * @return {@code null} if there was an error compiling the xpath.
351
   */
352
  public static XPathExpression compile( final CharSequence cs ) {
353
    assert cs != null;
354
355
    final var xpath = cs.toString();
356
357
    return sXpaths.computeIfAbsent(
358
      xpath, _ -> {
359
        try {
360
          return sXpath.compile( xpath );
361
        }
362
        catch( final XPathExpressionException ex ) {
363
          clue( ex );
364
          return null;
365
        }
366
      }
367
    );
368
  }
369
370
  /**
371
   * Merges a source document into a target document. This avoids adding an
372
   * empty XML namespace attribute to elements.
373
   *
374
   * @param target The document to envelop the source document.
375
   * @param source The source document to embed.
376
   * @return The target document with the source document included.
377
   */
378
  private static Node importNode( final Document target, final Node source ) {
379
    assert target != null;
380
    assert source != null;
381
382
    Node result;
383
    final var nodeType = source.getNodeType();
384
385
    if( nodeType == Node.ELEMENT_NODE ) {
386
      final var element = createElement( target, source.getNodeName(), null );
387
      final var attrs = source.getAttributes();
388
389
      if( attrs != null ) {
390
        final var attrLength = attrs.getLength();
391
392
        for( var i = 0; i < attrLength; i++ ) {
393
          final var attr = attrs.item( i );
394
          element.setAttribute( attr.getNodeName(), attr.getNodeValue() );
395
        }
396
      }
397
398
      final var children = source.getChildNodes();
399
      final var childLength = children.getLength();
400
401
      for( var i = 0; i < childLength; i++ ) {
402
        element.appendChild( importNode( target, children.item( i ) ) );
403
      }
404
405
      result = element;
406
    }
407
    else if( nodeType == Node.TEXT_NODE ) {
38
import static org.w3c.dom.Node.*;
39
40
/**
41
 * Responsible for initializing an XML parser.
42
 */
43
public class DocumentParser {
44
  private static final String LOAD_EXTERNAL_DTD =
45
    "http://apache.org/xml/features/nonvalidating/load-external-dtd";
46
  private static final String INDENT_AMOUNT =
47
    "{http://xml.apache.org/xslt}indent-amount";
48
  private static final String NAMESPACE = "http://www.w3.org/1999/xhtml";
49
50
  private static final XPath XPATH = XPathFactory.newInstance().newXPath();
51
52
  private static final ByteArrayOutputStream sWriter =
53
    new ByteArrayOutputStream( 65536 );
54
  private static final OutputStreamWriter sOutput =
55
    new OutputStreamWriter( sWriter, UTF_8 );
56
57
  /**
58
   * Caches {@link XPathExpression}s to avoid re-compiling.
59
   */
60
  private static final Map<String, XPathExpression> sXpaths = new HashMap<>();
61
62
  private static final DocumentBuilderFactory sDocumentFactory;
63
  private static DocumentBuilder sDocumentBuilder;
64
  private static Transformer sTransformer;
65
  private static final XPath sXpath = XPathFactory.newInstance().newXPath();
66
67
  public static final DOMImplementation sDomImplementation;
68
69
  static {
70
    sDocumentFactory = DocumentBuilderFactory.newInstance();
71
72
    sDocumentFactory.setValidating( false );
73
    sDocumentFactory.setAttribute( LOAD_EXTERNAL_DTD, false );
74
    sDocumentFactory.setNamespaceAware( true );
75
    sDocumentFactory.setIgnoringComments( true );
76
    sDocumentFactory.setIgnoringElementContentWhitespace( true );
77
78
    DOMImplementation domImplementation;
79
80
    try {
81
      sDocumentBuilder = sDocumentFactory.newDocumentBuilder();
82
      domImplementation = sDocumentBuilder.getDOMImplementation();
83
      sTransformer = TransformerFactory.newInstance().newTransformer();
84
85
      // Ensure Unicode characters (emojis) are encoded correctly.
86
      sTransformer.setOutputProperty( ENCODING, UTF_16.toString() );
87
      sTransformer.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
88
      sTransformer.setOutputProperty( METHOD, "xml" );
89
      sTransformer.setOutputProperty( INDENT, "no" );
90
      sTransformer.setOutputProperty( INDENT_AMOUNT, "2" );
91
    }
92
    catch( final Exception ex ) {
93
      clue( ex );
94
      domImplementation = sDocumentBuilder.getDOMImplementation();
95
    }
96
97
    sDomImplementation = domImplementation;
98
  }
99
100
  public static Document newDocument() {
101
    return sDocumentBuilder.newDocument();
102
  }
103
104
  /**
105
   * Creates a new document object model based on the given XML document
106
   * string. This will return an empty document if the document could not
107
   * be parsed.
108
   *
109
   * @param xml The document text to convert into a DOM.
110
   * @return The DOM that represents the given XML data.
111
   */
112
  public static Document parse( final String xml ) {
113
    assert xml != null;
114
115
    if( !xml.isBlank() ) {
116
      try( final var reader = new StringReader( xml ) ) {
117
        final var input = new InputSource();
118
119
        input.setEncoding( UTF_8.toString() );
120
        input.setCharacterStream( reader );
121
122
        return sDocumentBuilder.parse( input );
123
      }
124
      catch( final Throwable t ) {
125
        clue( t );
126
      }
127
    }
128
129
    return sDocumentBuilder.newDocument();
130
  }
131
132
  /**
133
   * Creates a well-formed XHTML document from a standard HTML document.
134
   *
135
   * @param source   The HTML source document to transform.
136
   * @param metadata The metadata contained within the head element.
137
   * @param locale   The localization information for the lang attribute.
138
   * @return The well-formed XHTML document.
139
   */
140
  public static Document create(
141
    final Document source,
142
    final Map<String, String> metadata,
143
    final Locale locale,
144
    final String pageTitle
145
  ) throws XPathExpressionException {
146
    final var target = createXhtmlDocument();
147
    final var html = target.getDocumentElement();
148
    final var hSource = evaluate( "//head", source );
149
    final var head = createElement( target, "head", null );
150
    final var hChildren = hSource.getChildNodes();
151
    final var hCount = hChildren.getLength();
152
153
    for( var i = 0; i < hCount; i++ ) {
154
      final var imported = target.importNode( hChildren.item( i ), true );
155
156
      if( imported.getNodeType() == ELEMENT_NODE ) {
157
        final var nodeName = imported.getNodeName();
158
        final var node = target.renameNode( imported, NAMESPACE, nodeName );
159
160
        head.appendChild( node );
161
      }
162
    }
163
164
    html.setAttribute( "lang", locale.getLanguage() );
165
166
    final var encoding = createEncoding( target, "UTF-8" );
167
    head.appendChild( encoding );
168
169
    for( final var entry : metadata.entrySet() ) {
170
      final var node = createMeta( target, entry );
171
172
      head.appendChild( node );
173
    }
174
175
    final var titleText = Strings.sanitize( pageTitle );
176
177
    // Empty titles result in <title/>, which some browsers cannot parse.
178
    if( !titleText.isBlank() ) {
179
      final var title = createElement( target, "title", titleText );
180
      head.appendChild( title );
181
    }
182
183
    html.appendChild( head );
184
185
    final var body = createElement( target, "body", null );
186
    final var bSource = source.getElementsByTagName( "body" ).item( 0 );
187
    final var bChildren = bSource.getChildNodes();
188
    final var bCount = bChildren.getLength();
189
190
    for( var i = 0; i < bCount; i++ ) {
191
      body.appendChild( importNode( target, bChildren.item( i ) ) );
192
    }
193
194
    html.appendChild( body );
195
196
    return target;
197
  }
198
199
  public static Node evaluate( final String xpath, final Document doc ) throws XPathExpressionException {
200
    return (Node) XPATH.evaluate( xpath, doc, NODE );
201
  }
202
203
  /**
204
   * Parses the given file contents into a document object model.
205
   *
206
   * @param doc The source XML document to parse.
207
   * @return The file as a document object model.
208
   * @throws IOException  Could not open the document.
209
   * @throws SAXException Could not read the XML file content.
210
   */
211
  public static Document parse( final File doc )
212
    throws IOException, SAXException {
213
    assert doc != null;
214
215
    try( final var in = new FileInputStream( doc ) ) {
216
      return parse( in );
217
    }
218
  }
219
220
  /**
221
   * Parses the given file contents into a document object model. Callers
222
   * must close the stream.
223
   *
224
   * @param doc The source XML document to parse.
225
   * @return The {@link InputStream} converted to a document object model.
226
   * @throws IOException  Could not open the document.
227
   * @throws SAXException Could not read the XML file content.
228
   */
229
  public static Document parse( final InputStream doc )
230
    throws IOException, SAXException {
231
    assert doc != null;
232
233
    return sDocumentBuilder.parse( doc );
234
  }
235
236
  /**
237
   * Allows an operation to be applied for every node in the document that
238
   * matches a given tag name pattern.
239
   *
240
   * @param document Document to traverse.
241
   * @param xpath    Document elements to find via {@link XPath} expression.
242
   * @param consumer The consumer to call for each matching document node.
243
   */
244
  public static void visit(
245
    final Document document,
246
    final CharSequence xpath,
247
    final Consumer<Node> consumer ) {
248
    assert document != null;
249
    assert consumer != null;
250
251
    try {
252
      final var expr = compile( xpath );
253
      final var nodeSet = expr.evaluate( document, NODESET );
254
255
      if( nodeSet instanceof NodeList nodes ) {
256
        for( int i = 0, len = nodes.getLength(); i < len; i++ ) {
257
          consumer.accept( nodes.item( i ) );
258
        }
259
      }
260
    }
261
    catch( final Exception ex ) {
262
      clue( ex );
263
    }
264
  }
265
266
  public static Node createMeta(
267
    final Document document, final Map.Entry<String, String> entry ) {
268
    assert document != null;
269
    assert entry != null;
270
271
    final var node = createElement( document, "meta", null );
272
273
    node.setAttribute( "name", entry.getKey() );
274
    node.setAttribute( "content", entry.getValue() );
275
276
    return node;
277
  }
278
279
  public static Node createEncoding(
280
    final Document document, final String encoding
281
  ) {
282
    assert document != null;
283
    assert encoding != null;
284
285
    final var node = createElement( document, "meta", null );
286
287
    node.setAttribute( "http-equiv", "Content-Type" );
288
    node.setAttribute( "content", "text/html; charset=" + encoding );
289
290
    return node;
291
  }
292
293
  public static Element createElement(
294
    final Document document, final String nodeName, final String nodeValue
295
  ) {
296
    assert document != null;
297
    assert nodeName != null;
298
    assert !nodeName.isBlank();
299
300
    // Recreate elements in the target document with namespace.
301
    final var node = document.createElementNS( NAMESPACE, nodeName );
302
303
    if( nodeValue != null ) {
304
      node.setTextContent( nodeValue );
305
    }
306
307
    return node;
308
  }
309
310
  public static String toString( final Node xhtml ) {
311
    assert xhtml != null;
312
313
    String result = "";
314
315
    try( final var writer = new StringWriter() ) {
316
      final var stream = new StreamResult( writer );
317
318
      transform( xhtml, stream );
319
320
      result = writer.toString();
321
    }
322
    catch( final Exception ex ) {
323
      clue( ex );
324
    }
325
326
    return result;
327
  }
328
329
  public static String transform( final Element root )
330
    throws IOException, TransformerException {
331
    assert root != null;
332
333
    try( final var writer = new StringWriter() ) {
334
      transform( root.getOwnerDocument(), new StreamResult( writer ) );
335
336
      return writer.toString();
337
    }
338
  }
339
340
  /**
341
   * Remove whitespace, comments, and XML/DOCTYPE declarations to make
342
   * processing work with ConTeXt.
343
   *
344
   * @param path The SVG file to process.
345
   * @throws Exception The file could not be processed.
346
   */
347
  public static void sanitize( final Path path ) throws Exception {
348
    assert path != null;
349
350
    // Preprocessing the SVG image is a single-threaded operation, no matter
351
    // how many SVG images are in the document to typeset.
352
    sWriter.reset();
353
354
    final var target = new StreamResult( sOutput );
355
    final var source = sDocumentBuilder.parse( toFile( path ) );
356
357
    transform( source, target );
358
    write( path, sWriter.toByteArray() );
359
  }
360
361
  /**
362
   * Converts a string into an {@link XPathExpression}, which may be used to
363
   * extract elements from a {@link Document} object model.
364
   *
365
   * @param cs The string to convert to an {@link XPathExpression}.
366
   * @return {@code null} if there was an error compiling the xpath.
367
   */
368
  public static XPathExpression compile( final CharSequence cs ) {
369
    assert cs != null;
370
371
    final var xpath = cs.toString();
372
373
    return sXpaths.computeIfAbsent(
374
      xpath, _ -> {
375
        try {
376
          return sXpath.compile( xpath );
377
        }
378
        catch( final XPathExpressionException ex ) {
379
          clue( ex );
380
          return null;
381
        }
382
      }
383
    );
384
  }
385
386
  /**
387
   * Merges a source document into a target document. This avoids adding an
388
   * empty XML namespace attribute to elements.
389
   *
390
   * @param target The document to envelop the source document.
391
   * @param source The source document to embed.
392
   * @return The target document with the source document included.
393
   */
394
  private static Node importNode( final Document target, final Node source ) {
395
    assert target != null;
396
    assert source != null;
397
398
    Node result;
399
    final var nodeType = source.getNodeType();
400
401
    if( nodeType == ELEMENT_NODE ) {
402
      final var element = createElement( target, source.getNodeName(), null );
403
      final var attrs = source.getAttributes();
404
405
      if( attrs != null ) {
406
        final var attrLength = attrs.getLength();
407
408
        for( var i = 0; i < attrLength; i++ ) {
409
          final var attr = attrs.item( i );
410
          element.setAttribute( attr.getNodeName(), attr.getNodeValue() );
411
        }
412
      }
413
414
      final var children = source.getChildNodes();
415
      final var childLength = children.getLength();
416
417
      for( var i = 0; i < childLength; i++ ) {
418
        element.appendChild( importNode( target, children.item( i ) ) );
419
      }
420
421
      result = element;
422
    }
423
    else if( nodeType == TEXT_NODE ) {
408424
      result = target.createTextNode( source.getNodeValue() );
409425
    }