Repository: prettier/plugin-xml
Pull request #119 (2020-08-31 15:07:58)

Support embedded parsers


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6986676..2a11caf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
 
 ## [Unreleased]
 
+### Added
+
+- Allow embedded parsers to handle content if element tags contain only text content and the tag name matches the name of an existing parser. For example:
+
+```xml
+<style type="text/css">
+.box {
+  height: 100px;
+  width: 100px;
+}
+</style>
+```
+
 ## [0.11.0] - 2020-08-14
 
 ### Changed
diff --git a/src/embed.js b/src/embed.js
new file mode 100644
index 0000000..5ff5bb8
--- /dev/null
+++ b/src/embed.js
@@ -0,0 +1,159 @@
+const { builders, utils } = require("prettier/doc");
+const {
+  concat,
+  dedentToRoot,
+  group,
+  hardline,
+  indent,
+  join,
+  line,
+  literalline,
+  softline
+} = builders;
+const { mapDoc, stripTrailingHardline } = utils;
+
+// Replace the string content newlines within a doc tree with literallines so
+// that all of the indentation lines up appropriately
+const replaceNewlines = (doc) =>
+  mapDoc(doc, (currentDoc) =>
+    typeof currentDoc === "string" && currentDoc.includes("\n")
+      ? concat(
+          currentDoc
+            .split(/(\n)/g)
+            .map((v, i) => (i % 2 === 0 ? v : literalline))
+        )
+      : currentDoc
+  );
+
+// Get the start and end element tags from the current node on the tree
+const getElementTags = (path, print) => {
+  const node = path.getValue();
+  const {
+    OPEN,
+    Name,
+    attribute,
+    START_CLOSE,
+    SLASH_OPEN,
+    END_NAME,
+    END
+  } = node.children;
+
+  const parts = [OPEN[0].image, Name[0].image];
+
+  if (attribute) {
+    parts.push(
+      indent(
+        concat([line, join(line, path.map(print, "children", "attribute"))])
+      )
+    );
+  }
+
+  return {
+    openTag: group(concat(parts.concat(softline, START_CLOSE[0].image))),
+    closeTag: group(
+      concat([SLASH_OPEN[0].image, END_NAME[0].image, END[0].image])
+    )
+  };
+};
+
+// Get the name of the parser that is represented by the given element node,
+// return null if a matching parser cannot be found
+const getParser = (node, opts) => {
+  const { Name, attribute } = node.children;
+  const parser = Name[0].image.toLowerCase();
+
+  // We don't want to deal with some weird recursive parser situation, so we
+  // need to explicitly call out the XML parser here and just return null
+  if (parser === "xml") {
+    return null;
+  }
+
+  // If this is a style tag with a text/css type, then we can skip straight to
+  // saying that this needs a CSS parser
+  if (
+    parser === "style" &&
+    attribute &&
+    attribute.some(
+      (attr) =>
+        attr.children.Name[0].image === "type" &&
+        attr.children.STRING[0].image === '"text/css"'
+    )
+  ) {
+    return "css";
+  }
+
+  // If there is a plugin that has a parser that matches the name of this
+  // element, then we're going to assume that's correct for embedding and go
+  // ahead and switch to that parser
+  if (
+    opts.plugins.some(
+      (plugin) =>
+        plugin.parsers &&
+        Object.prototype.hasOwnProperty.call(plugin.parsers, parser)
+    )
+  ) {
+    return parser;
+  }
+
+  return null;
+};
+
+// Get the source string that will be passed into the embedded parser from the
+// content of the inside of the element node
+const getSource = (content) =>
+  content.chardata
+    .map((node) => {
+      const { SEA_WS, TEXT } = node.children;
+      const [{ image }] = SEA_WS || TEXT;
+
+      return {
+        offset: node.location.startOffset,
+        printed: image
+      };
+    })
+    .sort(({ offset }) => offset)
+    .map(({ printed }) => printed)
+    .join("");
+
+const embed = (path, print, textToDoc, opts) => {
+  const node = path.getValue();
+
+  // If the node isn't an element node, then skip
+  if (node.name !== "element") {
+    return null;
+  }
+
+  // If the name of the node does not correspond to the name of a parser that
+  // prettier knows about, then skip
+  const parser = getParser(node, opts);
+  if (!parser) {
+    return null;
+  }
+
+  // If the node does not actually contain content, or it contains any content
+  // that is not just plain text, then skip
+  const content = node.children.content[0].children;
+  if (Object.keys(content).length !== 1 || !content.chardata) {
+    return null;
+  }
+
+  // Get the open and close tags of this element, then return the properly
+  // formatted content enclosed within them
+  const { openTag, closeTag } = getElementTags(path, print);
+
+  return group(
+    concat([
+      openTag,
+      literalline,
+      dedentToRoot(
+        replaceNewlines(
+          stripTrailingHardline(textToDoc(getSource(content), { parser }))
+        )
+      ),
+      hardline,
+      closeTag
+    ])
+  );
+};
+
+module.exports = embed;
diff --git a/src/plugin.js b/src/plugin.js
index 6a6a5ae..c57484f 100644
--- a/src/plugin.js
+++ b/src/plugin.js
@@ -1,3 +1,4 @@
+const embed = require("./embed");
 const languages = require("./languages");
 const parser = require("./parser");
 const printer = require("./printer");
@@ -9,6 +10,7 @@ const plugin = {
   },
   printers: {
     xml: {
+      embed,
       print: printer
     }
   },


Test patch:
diff --git a/test/__snapshots__/format.test.js.snap b/test/__snapshots__/format.test.js.snap
index cb393c7..a44aa78 100644
--- a/test/__snapshots__/format.test.js.snap
+++ b/test/__snapshots__/format.test.js.snap
@@ -24,10 +24,16 @@ exports[`defaults 1`] = `
   <?pagebreak?>
   <style />
   <style type=\\"text/css\\">
-    circle          { stroke-opacity: 0.7; }
-    .special circle { stroke: green; }
-    use             { stroke: purple;
-                      fill: orange; }
+circle {
+  stroke-opacity: 0.7;
+}
+.special circle {
+  stroke: green;
+}
+use {
+  stroke: purple;
+  fill: orange;
+}
   </style>
   <?pagebreak?>
     <g class=\\"special\\" style=\\"fill: blue\\">
@@ -78,10 +84,16 @@ exports[`xmlWhitespaceSensitivity => ignore 1`] = `
   <?pagebreak?>
   <style />
   <style type=\\"text/css\\">
-    circle          { stroke-opacity: 0.7; }
-    .special circle { stroke: green; }
-    use             { stroke: purple;
-                      fill: orange; }
+circle {
+  stroke-opacity: 0.7;
+}
+.special circle {
+  stroke: green;
+}
+use {
+  stroke: purple;
+  fill: orange;
+}
   </style>
   <?pagebreak?>
   <g class=\\"special\\" style=\\"fill: blue\\">