Repository: prettier/prettier (JavaScript)
Date: 2024-10-18 14:52:41+00:00
Task type: Feature

Issue:
Prettier breaks XHTML documents by incorrectly lowercasing DOCTYPE
The change in https://github.com/prettier/prettier/pull/7391 caused prettier to break my XHTML documents by lowercasing the DOCTYPE, resulting in XML well-formedness errors for my use case.

DOCTYPE is case-sensitive in XML contexts. I cannot wrap it with prettier ignore comments as that would produce invalid XML.
Proposal: Add `--doctype-case` option for customizable DOCTYPE casing
<!--

BEFORE SUBMITTING AN ISSUE:

1.  Search for your issue on GitHub: https://github.com/prettier/prettier/issues
    A large number of opened issues are duplicates of existing issues.
    If someone has already opened an issue for what you are experiencing,
    you do not need to open a new issue — please add a 👍 reaction to the
    existing issue instead.

2.  We get a lot of requests for adding options, but Prettier is
    built on the principle of being opinionated about code formatting.
    This means we add options only in the case of strict technical necessity.
    Find out more: https://prettier.io/docs/en/option-philosophy.html

Don't fill the form below manually! Let a program create a report for you:

1. Go to https://prettier.io/playground
2. Paste your code and set options
3. Press the "Report issue" button in the lower right

-->

**Prettier 3.3.3**

**Input:**

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    
</body>
</html>
```

**Output:**

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body></body>
</html>
```

**Expected output:**

### Depends on the --doctype-case option setting:
For `--doctype-case lowercase` (can be default):
```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
</html>
```
For --doctype-case uppercase:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
</html>
```
For `--doctype-case preserve`:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
</html>
```

**Why?**

Proposal to add a new configuration option `--doctype-case` to allow users to choose their preferred **DOCTYPE** casing style.

- Specification compliance: The **HTML5** specification allows for **DOCTYPE** to be written in both lowercase and uppercase (case-insensitive). However, the HTML specification explicitly states: "In other words, `<!DOCTYPE html>`, case-insensitively." This suggests that uppercase is the canonical form, which some developers prefer to follow strictly. https://html.spec.whatwg.org/multipage/syntax.html#the-doctype
- Special nature of DOCTYPE: It's important to note that the **DOCTYPE i**s not an HTML tag, but rather a required **preamble**. Its unique status as a declaration rather than a tag makes it a special case in HTML documents, which some argue warrants special consideration in formatting.
- Flexibility: Different developers and teams have different preferences regarding code style. Adding this option will allow for customization of formatting to individual or team coding standards.
- Backward compatibility: The preserve option will allow maintaining existing style in projects that don't want to introduce changes in DOCTYPE casing.
- Resolving controversy: There are differing opinions in the community about the preferred **DOCTYPE** casing. Adding this option will allow reconciling different preferences without imposing a single style on all users.
- Consistency with other tools: Some tools or environments may prefer a specific **DOCTYPE** casing style. This option will facilitate Prettier's integration with such tools. e.g. the Angular team is facing problems because of this change https://github.com/angular/angular/pull/51052#issuecomment-1974933556

The proposed implementation does not violate **Prettier's** opinionated principle as it concerns a purely stylistic aspect that does not affect the functionality or semantics of HTML code. 



Issue discussion:
We only lowercase doctype when it's HTML5.
Can you share an example that we break it?
I have plenty of `.xhtml` documents that prettier started doing this to recently. Here's an example XHTML document that was affected (src redacted):

```xhtml
<!DOCTYPE html>
<script xmlns="http://www.w3.org/1999/xhtml" src="[...]"/>
```
> We only lowercase doctype when it's HTML5.

The HTML5 doctype is not exclusive to the `text/html` content type. You should only apply this lowercasing to `text/html`, `*.html`, `*.htm`, etc. files (i.e. not `application/xhtml+xml`, `*.xhtml`, `*.xml`, etc. files)
@eligrey Could you share a specification text that indicates that XML doctype declarations are case-sensitive?
Before I can find a spec reference for you, I emplore you to create an .xhtml file locally and test this for yourself in any web browser.
[Here's the relevant standard](https://www.w3.org/TR/xml/#NT-doctypedecl:~:text=Physical%20Structures.-,Document%20Type%20Definition,%27%3C!DOCTYPE%27,-S%20Name%20\() which shows that XML requires an all-capital DOCTYPE.
In XML serializations (i.e. XHTML) the DOCTYPE is not required, but if you use it, DOCTYPE should be uppercase.

If the xml contains a lowercase doctype, the browser cannot parse it, but the uppercase one will not.

<img width="597" alt="image" src="https://github.com/prettier/prettier/assets/6134547/691ee9e1-459f-480a-8d2d-0e722df82e7c">

This change to prettier boggles my mind... !DOCTYPE has always been the standard. At least allow a configuration option to decide how one would want it to be rather than this blanket change
@sosukesuzuki any updates here?
I can't understand why they must modify it to lowercase. It's so stupid! Uppercase has become the default standard over the years.
https://github.com/prettier/prettier/issues/15096

this topic is coming back, but I think such a change is needed
Thank you for writing a detailed proposal. I understand the motivation behind it. However, I do not agree with this suggestion. I believe that Prettier should avoid adding more options unless absolutely necessary.

If formatting the DOCTYPE declaration in all uppercase causes an actual problem (e.g., rendering issues in certain browsers), then we can consider it. However, if it’s just a matter of user preference, I don't think we should add such an option.

As the Angular team has done, just run Prettier with the `--write` option.
@sosukesuzuki thank you for answer.
Could you please provide a specific reference or link to where the Angular team used the `--write` option to address this `DOCTYPE` casing issue? I'd like to see the exact context where this approach was implemented or discussed.

TBH, `<!doctype html>` looks very weird 🤔.
There are use cases where an HTML document can also be parsed by XML tools and a lowercase DOCTYPE is invalid in XML.

Also, uppercase DOCTYPE, although not required in HTML, has been the convention for decades. It was not broken; Why did Prettier "fix" it? (and break some rare use cases in the process)
In [dprint](https://dprint.dev/) with [markup_fmt](https://github.com/g-plane/markup_fmt) plugin, it has an option call [`doctypeKeywordCase`](https://markup-fmt.netlify.app/config/doctype-keyword-case.html) which behaves like this. We can consider dprint and give it a try if we need to customize it.
Let's leave `DOCTYPE` as it is? 

Solution patch:
diff --git a/changelog_unreleased/html/16765.md b/changelog_unreleased/html/16765.md
new file mode 100644
index 000000000000..f4232830b3ab
--- /dev/null
+++ b/changelog_unreleased/html/16765.md
@@ -0,0 +1,18 @@
+#### Keep doctype in non-html files unchanged (#16765 by @fisker)
+
+In Prettier v3, [we print HTML5 doctype in lowercase](https://prettier.io/blog/2023/07/05/3.0.0#print-html5-doctype-in-lowercase-7391httpsgithubcomprettierprettierpull7391-by-fiskerhttpsgithubcomfisker), it's safe for HTML files, however users may use the `html` parser to format other files eg: XHTML files, lowercase the `doctype` will break XHTML documents.
+
+Since Prettier main, we'll only lowercase [HTML5 doctype (`<!doctype html>`)](https://developer.mozilla.org/en-US/docs/MDN/Writing_guidelines/Writing_style_guide/Code_style_guide/HTML#doctype) when the file extension is `.html` or `.htm`, otherwise they will be untouched.
+
+<!-- prettier-ignore -->
+```html
+<!-- Input -->
+<!-- foo.xhtml -->
+<!DOCTYPE html>
+
+<!-- Prettier stable -->
+<!doctype html>
+
+<!-- Prettier main -->
+<!DOCTYPE html>
+```
diff --git a/src/language-html/print/tag.js b/src/language-html/print/tag.js
index 7399320c1396..b91f181d4f56 100644
--- a/src/language-html/print/tag.js
+++ b/src/language-html/print/tag.js
@@ -62,7 +62,7 @@ function printClosingTagSuffix(node, options) {
   return needsToBorrowParentClosingTagStartMarker(node)
     ? printClosingTagStartMarker(node.parent, options)
     : needsToBorrowNextOpeningTagStartMarker(node)
-      ? printOpeningTagStartMarker(node.next)
+      ? printOpeningTagStartMarker(node.next, options)
       : "";
 }
 
@@ -325,7 +325,10 @@ function printOpeningTag(path, options, print) {
 function printOpeningTagStart(node, options) {
   return node.prev && needsToBorrowNextOpeningTagStartMarker(node.prev)
     ? ""
-    : [printOpeningTagPrefix(node, options), printOpeningTagStartMarker(node)];
+    : [
+        printOpeningTagPrefix(node, options),
+        printOpeningTagStartMarker(node, options),
+      ];
 }
 
 function printOpeningTagPrefix(node, options) {
@@ -336,7 +339,8 @@ function printOpeningTagPrefix(node, options) {
       : "";
 }
 
-function printOpeningTagStartMarker(node) {
+const HTML5_DOCTYPE_START_MARKER = "<!doctype";
+function printOpeningTagStartMarker(node, options) {
   switch (node.type) {
     case "ieConditionalComment":
     case "ieConditionalStartComment":
@@ -345,8 +349,20 @@ function printOpeningTagStartMarker(node) {
       return "<!--<!";
     case "interpolation":
       return "{{";
-    case "docType":
-      return node.value === "html" ? "<!doctype" : "<!DOCTYPE";
+    case "docType": {
+      // Only lowercase HTML5 doctype in `.html` and `.htm` files
+      if (node.value === "html") {
+        const filepath = options.filepath ?? "";
+        if (/\.html?$/u.test(filepath)) {
+          return HTML5_DOCTYPE_START_MARKER;
+        }
+      }
+
+      const original = options.originalText.slice(locStart(node), locEnd(node));
+
+      return original.slice(0, HTML5_DOCTYPE_START_MARKER.length);
+    }
+
     case "angularIcuExpression":
       return "{";
     case "element":


Test patch:
diff --git a/tests/format/filename-matters/html-doctype/__snapshots__/format.test.js.snap b/tests/format/filename-matters/html-doctype/__snapshots__/format.test.js.snap
new file mode 100644
index 000000000000..ab6d595f1c03
--- /dev/null
+++ b/tests/format/filename-matters/html-doctype/__snapshots__/format.test.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`snippet: #0 format 1`] = `
+====================================options=====================================
+parsers: ["html"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<!DoCtYpE html>
+=====================================output=====================================
+<!DoCtYpE html>
+
+================================================================================
+`;
+
+exports[`xhtml-doctype.xhtml format 1`] = `
+====================================options=====================================
+parsers: ["html"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<!DOCTYPE html>
+<script xmlns="http://www.w3.org/1999/xhtml" src="[...]"/>
+
+=====================================output=====================================
+<!DOCTYPE html>
+<script xmlns="http://www.w3.org/1999/xhtml" src="[...]" />
+
+================================================================================
+`;
diff --git a/tests/format/filename-matters/html-doctype/format.test.js b/tests/format/filename-matters/html-doctype/format.test.js
new file mode 100644
index 000000000000..9e686bc0198b
--- /dev/null
+++ b/tests/format/filename-matters/html-doctype/format.test.js
@@ -0,0 +1,10 @@
+runFormatTest(
+  {
+    snippets: [
+      // Unknown filename
+      "<!DoCtYpE html>",
+    ],
+    importMeta: import.meta,
+  },
+  ["html"],
+);
diff --git a/tests/format/filename-matters/html-doctype/xhtml-doctype.xhtml b/tests/format/filename-matters/html-doctype/xhtml-doctype.xhtml
new file mode 100644
index 000000000000..b839df4808a6
--- /dev/null
+++ b/tests/format/filename-matters/html-doctype/xhtml-doctype.xhtml
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<script xmlns="http://www.w3.org/1999/xhtml" src="[...]"/>