diff --git a/.github/ISSUE_TEMPLATE/formatting.md b/.github/ISSUE_TEMPLATE/formatting.md index 7849d0248faa..823cb8b4b772 100644 --- a/.github/ISSUE_TEMPLATE/formatting.md +++ b/.github/ISSUE_TEMPLATE/formatting.md @@ -26,7 +26,7 @@ Don't fill the form below manually! Let a program create a report for you: --> -**Prettier 3.9.6** +**Prettier 3.9.7** [Playground link](https://prettier.io/playground/#.....) ```sh diff --git a/.github/ISSUE_TEMPLATE/integration.md b/.github/ISSUE_TEMPLATE/integration.md index 9d78e6d82895..70501cfcf654 100644 --- a/.github/ISSUE_TEMPLATE/integration.md +++ b/.github/ISSUE_TEMPLATE/integration.md @@ -20,7 +20,7 @@ BEFORE SUBMITTING AN ISSUE: **Environments:** -- Prettier Version: 3.9.6 +- Prettier Version: 3.9.7 - Running Prettier via: - Runtime: - Operating System: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f8737f13220..dea72898b210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,197 @@ +# 3.9.7 + +[diff](https://github.com/prettier/prettier/compare/3.9.6...3.9.7) + +#### Markdown: Prevent indentation drift in list-item code blocks ([#19647](https://github.com/prettier/prettier/pull/19647), [#19990](https://github.com/prettier/prettier/pull/19990) by [@Austin1serb](https://github.com/Austin1serb), [@giaBaoJS](https://github.com/giaBaoJS)) + + +```markdown + +- [x] short first line. + + second paragraph at six spaces that wraps + onto another line here. + + +- [x] short first line. + + second paragraph at six spaces that wraps + onto another line here. + + +- [x] short first line. + + second paragraph at six spaces that wraps + onto another line here. +``` + +#### JavaScript: Fix embedded template literal idempotency ([#19725](https://github.com/prettier/prettier/pull/19725) by [@fisker](https://github.com/fisker)) + + +```jsx +// Input +const foo = html` +
${getText({ + value, + })}
+`; + +// Prettier 3.9.6 (first format) +const foo = html` +
+ ${getText({ + value, + })} +
+`; + +// Prettier 3.9.6 (second format) +const foo = html` +
+ ${getText({ + value, + })} +
+`; + +// Prettier 3.9.7 +const foo = html` +
+ ${getText({ + value, + })} +
+`; +``` + +#### Markdown: Preserve Liquid blocks after Markdown tables ([#19730](https://github.com/prettier/prettier/pull/19730) by [@wanxiankai](https://github.com/wanxiankai), [@seiyab](https://github.com/seiyab)) + + +```markdown + +| Argument | Type | +| -------- | ---- | +{% set default_params = { + "model": ["str"], +} %} + + +| Argument | Type | +| ------------------------- | ---- | +| {% set default_params = { | + + "model": ["str"], + +} %} + + +| Argument | Type | +| -------- | ---- | +{% set default_params = { + "model": ["str"], +} %} +``` + +#### Markdown: Preserve single tildes in Markdown ([#19739](https://github.com/prettier/prettier/pull/19739) by [@lazerg](https://github.com/lazerg)) + +Since 3.9.0, single-tilde spans like `H~2~O` were rewritten to double tildes, turning subscript syntax into strike-through. Only double tildes are treated that way now, matching GitHub. + + +```markdown + +H~2~O + + +H~~2~~O + + +H~2~O +``` + +#### Markdown: Fix Markdown blockquote containing `>` characters ([#19802](https://github.com/prettier/prettier/pull/19802) by [@seiyab](https://github.com/seiyab)) + + +```md + +> **test**\> + + +> **test**>\> + + +> **test**\> +``` + +#### Markdown: Strip blockquote markers from a setext heading's continuation lines ([#19878](https://github.com/prettier/prettier/pull/19878) by [@Kjubikstronk](https://github.com/Kjubikstronk)) + +A setext heading spanning multiple lines inside a blockquote kept the `>` marker of its continuation lines as literal text. + + +```md + +> Multi +> Line +> === + + +> Multi > Line +> === + + +> Multi +> Line +> === +``` + +#### Markdown: Fix HTML block indentation drift in markdown list items ([#19987](https://github.com/prettier/prettier/pull/19987) by [@lazerg](https://github.com/lazerg)) + + +```markdown + +1. Enable images: + +

+ example +

+ + +1. Enable images: + +

+ example +

+ + +1. Enable images: + +

+ example +

+``` + +#### Angular: Support `@boundary` error boundaries ([#20014](https://github.com/prettier/prettier/pull/20014) by [@Yahiro025](https://github.com/Yahiro025)) + + +```html + +@boundary { + +} +@error (let err) { +

{{err.message}}

+} + + +SyntaxError: Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.) (6:1) + + +@boundary { + +} @error (let err) { +

{{ err.message }}

+} +``` + # 3.9.6 [diff](https://github.com/prettier/prettier/compare/3.9.5...3.9.6) diff --git a/changelog_unreleased/angular/20014.md b/changelog_unreleased/angular/20014.md new file mode 100644 index 000000000000..3bfb0ef09579 --- /dev/null +++ b/changelog_unreleased/angular/20014.md @@ -0,0 +1,22 @@ +#### Support `@boundary` error boundaries (#20014 by @Yahiro025) + + +```html + +@boundary { + +} +@error (let err) { +

{{err.message}}

+} + + +SyntaxError: Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.) (6:1) + + +@boundary { + +} @error (let err) { +

{{ err.message }}

+} +``` diff --git a/changelog_unreleased/javascript/19629.md b/changelog_unreleased/javascript/19629.md deleted file mode 100644 index 1717459ce6f1..000000000000 --- a/changelog_unreleased/javascript/19629.md +++ /dev/null @@ -1,26 +0,0 @@ -#### Added a new official plugin `@prettier/plugin-yuku` (#19628, #19629 by @fisker) - -`@prettier/plugin-yuku` is powered by [Yuku](https://yuku.fyi/) (A high-performance JavaScript/TypeScript compiler toolchain written in Zig). - -This plugin includes two new parsers: `yuku` (JavaScript syntax) and `yuku-ts` (TypeScript syntax). - -**To use this plugin:** - -1. Install the plugin: - - ```bash - yarn add --dev prettier @prettier/plugin-yuku - ``` - -2. Add it to your `.prettierrc`: - - ```yaml - plugins: - - "@prettier/plugin-yuku" - ``` - -Due to package size limitations, this plugin is not bundled with the main prettier package and must be installed separately. - -For more information, check [the package homepage](https://github.com/prettier/prettier/tree/main/packages/plugin-yuku). - -Big thanks to [@arshad-yaseen](https://github.com/arshad-yaseen) for his excellent work. diff --git a/changelog_unreleased/javascript/19725.md b/changelog_unreleased/javascript/19725.md new file mode 100644 index 000000000000..088f037de697 --- /dev/null +++ b/changelog_unreleased/javascript/19725.md @@ -0,0 +1,38 @@ +#### Fix embedded template literal idempotency (#19725 by @fisker) + + +```jsx +// Input +const foo = html` +
${getText({ + value, + })}
+`; + +// Prettier stable (first format) +const foo = html` +
+ ${getText({ + value, + })} +
+`; + +// Prettier stable (second format) +const foo = html` +
+ ${getText({ + value, + })} +
+`; + +// Prettier main +const foo = html` +
+ ${getText({ + value, + })} +
+`; +``` diff --git a/changelog_unreleased/markdown/19647.md b/changelog_unreleased/markdown/19647.md new file mode 100644 index 000000000000..813a730d5e8c --- /dev/null +++ b/changelog_unreleased/markdown/19647.md @@ -0,0 +1,22 @@ +#### Prevent indentation drift in list-item code blocks (#19647, #19990 by @Austin1serb, @giaBaoJS) + + +```markdown + +- [x] short first line. + + second paragraph at six spaces that wraps + onto another line here. + + +- [x] short first line. + + second paragraph at six spaces that wraps + onto another line here. + + +- [x] short first line. + + second paragraph at six spaces that wraps + onto another line here. +``` diff --git a/changelog_unreleased/markdown/19730.md b/changelog_unreleased/markdown/19730.md new file mode 100644 index 000000000000..82aa68636831 --- /dev/null +++ b/changelog_unreleased/markdown/19730.md @@ -0,0 +1,27 @@ +#### Preserve Liquid blocks after Markdown tables (#19730 by @wanxiankai, @seiyab) + + +```markdown + +| Argument | Type | +| -------- | ---- | +{% set default_params = { + "model": ["str"], +} %} + + +| Argument | Type | +| ------------------------- | ---- | +| {% set default_params = { | + + "model": ["str"], + +} %} + + +| Argument | Type | +| -------- | ---- | +{% set default_params = { + "model": ["str"], +} %} +``` diff --git a/changelog_unreleased/markdown/19739.md b/changelog_unreleased/markdown/19739.md new file mode 100644 index 000000000000..a4ed6a4963a3 --- /dev/null +++ b/changelog_unreleased/markdown/19739.md @@ -0,0 +1,15 @@ +#### Preserve single tildes in Markdown (#19739 by @lazerg) + +Since 3.9.0, single-tilde spans like `H~2~O` were rewritten to double tildes, turning subscript syntax into strike-through. Only double tildes are treated that way now, matching GitHub. + + +```markdown + +H~2~O + + +H~~2~~O + + +H~2~O +``` diff --git a/changelog_unreleased/markdown/19802.md b/changelog_unreleased/markdown/19802.md new file mode 100644 index 000000000000..b3e670c80fe7 --- /dev/null +++ b/changelog_unreleased/markdown/19802.md @@ -0,0 +1,13 @@ +#### Fix Markdown blockquote containing `>` characters (#19802 by @seiyab) + + +```md + +> **test**\> + + +> **test**>\> + + +> **test**\> +``` diff --git a/changelog_unreleased/markdown/19878.md b/changelog_unreleased/markdown/19878.md new file mode 100644 index 000000000000..61af67a38550 --- /dev/null +++ b/changelog_unreleased/markdown/19878.md @@ -0,0 +1,20 @@ +#### Strip blockquote markers from a setext heading's continuation lines (#19878 by @Kjubikstronk) + +A setext heading spanning multiple lines inside a blockquote kept the `>` marker of its continuation lines as literal text. + + +```md + +> Multi +> Line +> === + + +> Multi > Line +> === + + +> Multi +> Line +> === +``` diff --git a/changelog_unreleased/markdown/19987.md b/changelog_unreleased/markdown/19987.md new file mode 100644 index 000000000000..49fcd70eaf36 --- /dev/null +++ b/changelog_unreleased/markdown/19987.md @@ -0,0 +1,25 @@ +#### Fix HTML block indentation drift in markdown list items (#19987 by @lazerg) + + +```markdown + +1. Enable images: + +

+ example +

+ + +1. Enable images: + +

+ example +

+ + +1. Enable images: + +

+ example +

+``` diff --git a/changelog_unreleased/typescript/19621.md b/changelog_unreleased/typescript/19621.md deleted file mode 100644 index 91228377781b..000000000000 --- a/changelog_unreleased/typescript/19621.md +++ /dev/null @@ -1,19 +0,0 @@ -#### Preserve quotes for methods named `new` (#19621 by @kovsu) - - -```tsx -// Input -interface Container { - "new"(id: string): number; -} - -// Prettier stable -interface Container { - new(id: string): number; -} - -// Prettier main -interface Container { - "new"(id: string): number; -} -``` diff --git a/changelog_unreleased/typescript/19624.md b/changelog_unreleased/typescript/19624.md deleted file mode 100644 index 0155f29588d1..000000000000 --- a/changelog_unreleased/typescript/19624.md +++ /dev/null @@ -1,13 +0,0 @@ -#### Support `import defer` (#19624, #19675 by @fisker) - - -```tsx -// Input -import defer * as foo from "foo"; - -// Prettier stable -import * as foo from "foo"; - -// Prettier main -import defer * as foo from "foo"; -``` diff --git a/docs/browser.md b/docs/browser.md index 873cb8ed82ed..9b03bd1fa9f2 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -97,7 +97,7 @@ This syntax doesn’t necessarily work in the browser, but it can be used when b ```js import * as prettier from "https://unpkg.com/prettier@%PRETTIER_VERSION%/standalone.mjs"; -import * as prettierPluginGraphql from "https://unpkg.com/prettier@%PRETTIER_VERSION%1/plugins/graphql.mjs"; +import * as prettierPluginGraphql from "https://unpkg.com/prettier@%PRETTIER_VERSION%/plugins/graphql.mjs"; const formatted = await prettier.format("type Query { hello: String }", { parser: "graphql", diff --git a/package.json b/package.json index b330ab4d2dbc..50962f05438b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prettier", - "version": "3.9.6", + "version": "3.9.7", "description": "Prettier is an opinionated code formatter", "bin": "./bin/prettier.cjs", "repository": "prettier/prettier", @@ -53,12 +53,12 @@ "@prettier/html-event-attributes": "0.0.2", "@prettier/html-tags": "2.2.0", "@prettier/parse-srcset": "3.1.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", "acorn": "8.17.0", "acorn-jsx": "5.3.2", "angular-estree-parser": "15.5.0", - "angular-html-parser": "10.11.0", + "angular-html-parser": "10.12.0", "camelcase": "9.0.0", "ci-info": "4.4.0", "cjk-regex": "3.4.0", @@ -139,8 +139,8 @@ "@eslint/js": "10.0.1", "@stylistic/eslint-plugin": "5.10.0", "@types/jsesc": "3.0.3", - "@typescript-eslint/eslint-plugin": "8.65.0", - "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", "@yuku-parser/wasm": "0.7.0", "base64-arraybuffer-es6": "3.1.0", "browserslist": "4.28.6", @@ -184,7 +184,7 @@ "node-style-text": "2.1.2", "npm-run-all2": "9.0.2", "open-editor": "6.0.0", - "prettier": "3.9.5", + "prettier": "3.9.6", "pretty-bytes": "7.1.0", "pretty-ms": "9.3.0", "puppeteer": "25.3.0", diff --git a/packages/plugin-hermes/package.json b/packages/plugin-hermes/package.json index 7632ac503393..cf1a0297c826 100644 --- a/packages/plugin-hermes/package.json +++ b/packages/plugin-hermes/package.json @@ -1,6 +1,6 @@ { "name": "@prettier/plugin-hermes", - "version": "0.2.1", + "version": "0.2.2", "description": "Prettier Hermes plugin.", "type": "module", "exports": { diff --git a/packages/plugin-oxc/package.json b/packages/plugin-oxc/package.json index 66130941d999..4defb218b6ab 100644 --- a/packages/plugin-oxc/package.json +++ b/packages/plugin-oxc/package.json @@ -1,6 +1,6 @@ { "name": "@prettier/plugin-oxc", - "version": "0.2.1", + "version": "0.2.2", "description": "Prettier Oxc plugin.", "type": "module", "exports": { diff --git a/packages/plugin-yuku/package.json b/packages/plugin-yuku/package.json index e91966f27c90..a7f4c0109668 100644 --- a/packages/plugin-yuku/package.json +++ b/packages/plugin-yuku/package.json @@ -1,6 +1,6 @@ { "name": "@prettier/plugin-yuku", - "version": "0.0.0", + "version": "0.0.1", "description": "Prettier yuku plugin.", "type": "module", "exports": { diff --git a/scripts/build/transform/index.js b/scripts/build/transform/index.js index c44ec24c2e7e..82afe7369d85 100644 --- a/scripts/build/transform/index.js +++ b/scripts/build/transform/index.js @@ -28,7 +28,10 @@ const packageTransforms = new Map([ transforms["method-at"], ["@glimmer/syntax", "angular-estree-parser", "espree"], ], - [transforms["object-has-own"], ["@babel/parser", "meriyah", "graphql"]], + [ + transforms["object-has-own"], + ["@babel/parser", "graphql", "meriyah", "angular-html-parser"], + ], [transforms["string-raw"], ["camelcase", "@angular/compiler", "js-yaml"]], [transforms["method-is-well-formed"], ["meriyah"]], /* spell-checker: enable */ diff --git a/src/language-html/print/angular-control-flow-block-settings.evaluate.js b/src/language-html/print/angular-control-flow-block-settings.evaluate.js index bc9b33c232f0..48243b99a0ec 100644 --- a/src/language-html/print/angular-control-flow-block-settings.evaluate.js +++ b/src/language-html/print/angular-control-flow-block-settings.evaluate.js @@ -15,6 +15,8 @@ const ANGULAR_CONTROL_FLOW_BLOCK_SETTINGS = new Map( ["placeholder", ["placeholder", "error", "loading"]], ["error", ["placeholder", "error", "loading"]], ["loading", ["placeholder", "error", "loading"]], + + ["boundary", ["error"]], ] .filter(([, followingBlockNames]) => followingBlockNames.length > 0) .map(([name, followingBlockNames]) => [name, new Set(followingBlockNames)]), diff --git a/src/language-js/embed/css.js b/src/language-js/embed/css.js index f3171e7603e4..baaa60b51036 100644 --- a/src/language-js/embed/css.js +++ b/src/language-js/embed/css.js @@ -7,7 +7,7 @@ import { softline, } from "../../document/index.js"; import isNonEmptyArray from "../../utilities/is-non-empty-array.js"; -import { printTemplateExpressions } from "../print/template-literal.js"; +import { printEmbeddedTemplateExpressions } from "../print/template-literal.js"; import { isNodeMatches } from "../utilities/is-node-matches.js"; import { isAngularComponentStyles } from "./utilities.js"; @@ -26,7 +26,7 @@ async function printEmbedCss(textToDoc, print, path, options) { text += raw; } const quasisDoc = await textToDoc(text, { parser: "scss" }); - const expressionDocs = printTemplateExpressions(path, options, print); + const expressionDocs = printEmbeddedTemplateExpressions(path, options, print); const newDoc = replacePlaceholders(quasisDoc, expressionDocs); /* c8 ignore next 3 */ if (!newDoc) { diff --git a/src/language-js/embed/graphql.js b/src/language-js/embed/graphql.js index 4d98a5ab4bb5..02421933fc60 100644 --- a/src/language-js/embed/graphql.js +++ b/src/language-js/embed/graphql.js @@ -1,7 +1,7 @@ import { hardline, indent, join } from "../../document/index.js"; import { escapeTemplateCharacters, - printTemplateExpressions, + printEmbeddedTemplateExpressions, } from "../print/template-literal.js"; import { hasLanguageComment } from "./utilities.js"; @@ -10,7 +10,7 @@ async function printEmbedGraphQL(textToDoc, print, path, options) { const numQuasis = node.quasis.length; - const expressionDocs = printTemplateExpressions(path, options, print); + const expressionDocs = printEmbeddedTemplateExpressions(path, options, print); const parts = []; for (let i = 0; i < numQuasis; i++) { diff --git a/src/language-js/embed/html.js b/src/language-js/embed/html.js index e2ac60357e11..ce4f6bb8d602 100644 --- a/src/language-js/embed/html.js +++ b/src/language-js/embed/html.js @@ -7,7 +7,7 @@ import { mapDoc, } from "../../document/index.js"; import { - printTemplateExpressions, + printEmbeddedTemplateExpressions, uncookTemplateElementValue, } from "../print/template-literal.js"; import { hasLanguageComment, isAngularComponentTemplate } from "./utilities.js"; @@ -30,7 +30,7 @@ async function printEmbedHtmlLike(parser, textToDoc, print, path, options) { ) .join(""); - const expressionDocs = printTemplateExpressions(path, options, print); + const expressionDocs = printEmbeddedTemplateExpressions(path, options, print); const placeholderRegex = new RegExp( composePlaceholder(String.raw`(\d+)`), diff --git a/src/language-js/print/template-literal.js b/src/language-js/print/template-literal.js index dbb668b1ce20..e84e235ed702 100644 --- a/src/language-js/print/template-literal.js +++ b/src/language-js/print/template-literal.js @@ -203,7 +203,12 @@ function getTemplateLiteralExpressionIndent(path, options) { - `TemplateLiteral` - `TSTemplateLiteralType` (TypeScript) */ -function printTemplateExpression(path, options, print) { +function printTemplateExpression( + path, + options, + print, + shouldPreserveIndentation = true, +) { const { node, index } = path; let expressionDoc = print(); @@ -249,6 +254,11 @@ function printTemplateExpression(path, options, print) { expressionDoc = [indent([softline, expressionDoc]), softline]; } + // There is no way to know how the quasis will be printed + if (!shouldPreserveIndentation) { + return group(["${", expressionDoc, lineSuffixBoundary, "}"]); + } + // For a template literal of the following form: // `someQuery { // ${call({ @@ -283,6 +293,19 @@ function printTemplateExpressions(path, options, print) { ); } +function printEmbeddedTemplateExpressions(path, options, print) { + return path.map( + () => + printTemplateExpression( + path, + options, + print, + /* shouldPreserveIndentation */ false, + ), + "expressions", + ); +} + function escapeTemplateCharacters(doc, raw) { return mapDoc(doc, (currentDoc) => { if (typeof currentDoc === "string") { @@ -331,8 +354,8 @@ function isJestEachTemplateLiteral({ node, parent }) { export { escapeTemplateCharacters, + printEmbeddedTemplateExpressions, printTaggedTemplateExpression, - printTemplateExpressions, printTemplateLiteral, uncookTemplateElementValue, }; diff --git a/src/language-markdown/parse/micromark/micromark-extension-liquid.js b/src/language-markdown/parse/micromark/micromark-extension-liquid.js index cac4a27a4d0b..45b011a28d08 100644 --- a/src/language-markdown/parse/micromark/micromark-extension-liquid.js +++ b/src/language-markdown/parse/micromark/micromark-extension-liquid.js @@ -1,4 +1,4 @@ -import { markdownLineEnding } from "micromark-util-character"; +import { markdownLineEnding, markdownSpace } from "micromark-util-character"; import { codes, types } from "micromark-util-symbol"; /** @@ -7,6 +7,7 @@ import { codes, types } from "micromark-util-symbol"; * @typedef {import('mdast-util-from-markdown').CompileContext} CompileContext * @typedef {import('mdast-util-from-markdown').Handle} Handle * @typedef {import('micromark-util-types').State} State + * @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext */ const nodeType = "liquidNode"; @@ -46,15 +47,37 @@ function liquidFromMarkdown() { */ function liquidSyntax() { return { + flow: { + [codes.leftCurlyBrace]: { + name: "liquidFlow", + tokenize: tokenizeFlow, + }, + }, text: { [codes.leftCurlyBrace]: { - name: "liquid", - tokenize, + name: "liquidText", + tokenize: tokenizeText, }, }, }; - function tokenize(effects, ok, nok) { + /** @this {TokenizeContext} */ + function tokenizeFlow(effects, ok, nok) { + return tokenize.call(this, effects, ok, nok, "flow"); + } + + /** @this {TokenizeContext} */ + function tokenizeText(effects, ok, nok) { + return tokenize.call(this, effects, ok, nok, "text"); + } + + /** + * @this {TokenizeContext} + * @param mode {"text" | "flow"} + */ + function tokenize(effects, ok, nok, mode) { + const isFlow = mode === "flow"; + const { interrupt, now, parser } = this; /** @type {typeof codes.rightCurlyBrace | typeof codes.percentSign} */ let closingCode; @@ -74,7 +97,7 @@ function liquidSyntax() { ? codes.percentSign : codes.rightCurlyBrace; effects.consume(code); - return inside; + return isFlow && interrupt ? ok : inside; default: return nok(code); } @@ -95,6 +118,9 @@ function liquidSyntax() { effects.enter(types.lineEnding); effects.consume(code); effects.exit(types.lineEnding); + if (isFlow) { + return afterLineEnding; + } effects.enter(types.data); return inside; } @@ -103,17 +129,63 @@ function liquidSyntax() { } } + /** @type {State} */ + function afterLineEnding(code) { + if (parser.lazy[now().line]) { + return nok(code); + } + + if (markdownLineEnding(code)) { + effects.enter(types.lineEnding); + effects.consume(code); + effects.exit(types.lineEnding); + return afterLineEnding; + } + + effects.enter(types.data); + return inside(code); + } + /** @type {State} */ function mayClose(code) { if (code === codes.rightCurlyBrace) { effects.consume(code); effects.exit(types.data); effects.exit(nodeType); - return ok; + return isFlow ? afterClose : ok; } return inside; } + + /** @type {State} */ + function afterClose(code) { + if (markdownSpace(code)) { + effects.enter(types.whitespace); + effects.consume(code); + return afterWhitespace; + } + + return after(code); + } + + /** @type {State} */ + function afterWhitespace(code) { + if (markdownSpace(code)) { + effects.consume(code); + return afterWhitespace; + } + + effects.exit(types.whitespace); + return after(code); + } + + /** @type {State} */ + function after(code) { + return code === codes.eof || markdownLineEnding(code) + ? ok(code) + : nok(code); + } } } diff --git a/src/language-markdown/parse/parse-markdown.js b/src/language-markdown/parse/parse-markdown.js index 8c07cc832ec3..ba7f76a2b3c2 100644 --- a/src/language-markdown/parse/parse-markdown.js +++ b/src/language-markdown/parse/parse-markdown.js @@ -16,7 +16,7 @@ let markdownParseOptions; function getMarkdownParseOptions() { return (markdownParseOptions ??= { extensions: [ - gfmSyntax(), + gfmSyntax({ singleTilde: false }), mathSyntax(), wikiLinkSyntax({ // We don't need support alias, use a fake string to bypass diff --git a/src/language-markdown/print/children.js b/src/language-markdown/print/children.js index be78f2e95450..9b313551d753 100644 --- a/src/language-markdown/print/children.js +++ b/src/language-markdown/print/children.js @@ -42,7 +42,11 @@ function printChildren(path, options, print, events = {}) { } function shouldPrePrintHardline({ node, parent }) { - const isInlineNode = INLINE_NODE_TYPES.has(node.type); + const isInlineNode = + INLINE_NODE_TYPES.has(node.type) && + !( + node.type === "liquidNode" && !INLINE_NODE_WRAPPER_TYPES.has(parent.type) + ); const isInlineHTML = node.type === "html" && INLINE_NODE_WRAPPER_TYPES.has(parent.type); @@ -119,6 +123,9 @@ function shouldPrePrintDoubleHardline(path, options) { parent.type === "listItem" && previous.type === "paragraph" && previous.position.end.line + 1 === node.position.start.line; + const isBlockLiquidWithoutBlankLine = + (node.type === "liquidNode" || previous.type === "liquidNode") && + previous.position.end.line + 1 === node.position.start.line; return !( isSiblingNode || @@ -126,7 +133,8 @@ function shouldPrePrintDoubleHardline(path, options) { isPrevNodePrettierIgnore || isBlockHtmlWithoutBlankLineBetweenPrevHtml || isBlockHtmlWithoutBlankLineBetweenPrevParagraph || - isHtmlDirectAfterListItem + isHtmlDirectAfterListItem || + isBlockLiquidWithoutBlankLine ); } diff --git a/src/language-markdown/print/list.js b/src/language-markdown/print/list.js index 911325b6f349..755d76769a95 100644 --- a/src/language-markdown/print/list.js +++ b/src/language-markdown/print/list.js @@ -99,13 +99,14 @@ function printListItem(path, options, print, listPrefix) { prefix, printChildren(path, options, print, { processor({ node, isFirst }) { - if ( - (isFirst && node.type !== "list") || - (node.type === "code" && node.isIndented) - ) { + if ((isFirst && node.type !== "list") || node.type === "html") { return align(" ".repeat(prefix.length), print()); } + if (node.type === "code" && node.isIndented) { + return print(); + } + const alignment = " ".repeat( clamp(options.tabWidth - listPrefix.length, 0, 3), // 4+ will cause indented code block ); @@ -211,6 +212,10 @@ function printListItemLegacy(path, options, print, listPrefix) { return align(" ".repeat(prefix.length), print()); } + if (node.type === "code" && node.isIndented) { + return print(); + } + const alignment = " ".repeat( clamp(options.tabWidth - listPrefix.length, 0, 3), // 4+ will cause indented code block ); diff --git a/src/language-markdown/print/preprocess.js b/src/language-markdown/print/preprocess.js index 84b203131eb7..bffce2cf3b6d 100644 --- a/src/language-markdown/print/preprocess.js +++ b/src/language-markdown/print/preprocess.js @@ -1,5 +1,6 @@ import htmlWhitespace from "../../utilities/html-whitespace.js"; import { getOrderedListItemInfo, mapAst, splitText } from "../utilities.js"; +import { getBlockquoteRawText } from "../utilities/get-blockquote-raw-text.js"; // 0x0 ~ 0x10ffff const isSingleCharRegex = /^\\?.$/su; @@ -182,8 +183,11 @@ function splitTextIntoSentences(ast) { // Using `node.value`, we don't need to care about markers like `\n > ` in `blockquote`s. let text = node.raw; + // A setext heading's text can span multiple raw source lines too, e.g. + // `> Title\n> continued\n> ===`. const paragraphIndex = parentStack.findIndex( - (ancestor) => ancestor?.type === "paragraph", + (ancestor) => + ancestor?.type === "paragraph" || ancestor?.type === "heading", ); const paragraphNode = @@ -248,19 +252,6 @@ function splitTextIntoSentences(ast) { } } -function getBlockquoteRawText(text, node) { - const angleBracketsRegex = /^([ \t]*>[ \t]*)*/; - const rawLines = text.split("\n"); - const valueLines = node.value.split("\n"); - const resultLines = rawLines.map((rawLine, index) => { - const valueLine = valueLines[index] ?? ""; - const leadingTextAngleBrackets = - valueLine.match(angleBracketsRegex)[0] ?? ""; - return rawLine.replace(angleBracketsRegex, leadingTextAngleBrackets); - }); - return resultLines.join("\n"); -} - function transformIndentedCodeblock(ast, options) { return mapAst(ast, (node) => { if (node.type !== "code") { diff --git a/src/language-markdown/utilities/get-blockquote-raw-text.js b/src/language-markdown/utilities/get-blockquote-raw-text.js new file mode 100644 index 000000000000..8c9279bb31f9 --- /dev/null +++ b/src/language-markdown/utilities/get-blockquote-raw-text.js @@ -0,0 +1,67 @@ +/** + * @param line {string} + * @returns {number} + */ +function countValueLeadingGreaterThan(line) { + let count = 0; + + for (const character of line) { + if (character === ">") { + count++; + } else if (character !== " " && character !== "\t") { + break; + } + } + + return count; +} + +/** + * @param line {string} + * @param greaterThanToKeep {number} + * @returns {string} + */ +function removeBlockquoteMarkers(line, greaterThanToKeep) { + const rawBlockquoteRegexp = + /[ \t]*(?:>|\\>|>|>|�*62;|&#[xX]0*3[eE];)[ \t]*/y; + /** @type {Array} */ + const greaterThanIndexes = []; + let { lastIndex } = rawBlockquoteRegexp; + + let match; + while ((match = rawBlockquoteRegexp.exec(line))) { + greaterThanIndexes.push(match.index); + lastIndex = rawBlockquoteRegexp.lastIndex; + } + + if (greaterThanToKeep === 0) { + return line.slice(lastIndex); + } + const firstGreaterThanToKeep = greaterThanIndexes.at(-greaterThanToKeep); + /* c8 ignore next 3 */ + if (firstGreaterThanToKeep === null) { + return line; + } + return line.slice(firstGreaterThanToKeep); +} + +const newLineRegexp = /(? { + const valueLine = valueLines[valueIndex++]; + const decodedNewlines = rawLine.matchAll(newLineRegexp); + valueIndex += [...decodedNewlines].length; + + if (index === 0) { + return rawLine; + } + const valueMarkerCount = countValueLeadingGreaterThan(valueLine); + return removeBlockquoteMarkers(rawLine, valueMarkerCount); + }); + return resultLines.join("\n"); +} + +export { getBlockquoteRawText }; diff --git a/tests/config/format-test/failed-format-tests.js b/tests/config/format-test/failed-format-tests.js index 625c6a413b10..54f332b64a7f 100644 --- a/tests/config/format-test/failed-format-tests.js +++ b/tests/config/format-test/failed-format-tests.js @@ -25,8 +25,6 @@ const unstableTests = new Map( "typescript/satisfies-operators/comments-unstable.ts", "jsx/comments/in-attributes.js", "typescript/import-type/long-module-name/long-module-name4.ts", - // Unstable due to lack of indent information - "js/multiparser-comments/comment-inside.js", [ "typescript/method-chain/object/issue-17239.ts", (options) => options.objectWrap !== "collapse", diff --git a/tests/config/format-test/verify-fixtures.js b/tests/config/format-test/verify-fixtures.js index facc6c63d9d2..48afb6ea6e53 100644 --- a/tests/config/format-test/verify-fixtures.js +++ b/tests/config/format-test/verify-fixtures.js @@ -138,7 +138,7 @@ const categoryParsers = new Map([ ["lwc", { parsers: ["lwc"], verifyParsers: [], extensions: [".html"] }], [ "markdown", - { parsers: ["markdown"], verifyParsers: [], extensions: [".md"] }, + { parsers: ["markdown"], verifyParsers: ["mdx"], extensions: [".md"] }, ], ["mdx", { parsers: ["mdx"], verifyParsers: [], extensions: [".mdx"] }], [ diff --git a/tests/format/angular/control-flow/__snapshots__/format.test.js.snap b/tests/format/angular/control-flow/__snapshots__/format.test.js.snap index 8cf72183e5f7..f5f199f6eefb 100644 --- a/tests/format/angular/control-flow/__snapshots__/format.test.js.snap +++ b/tests/format/angular/control-flow/__snapshots__/format.test.js.snap @@ -52,6 +52,258 @@ parsers: ["angular"] ================================================================================ `; +exports[`boundary.html format 1`] = ` +====================================options===================================== +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +@boundary { + +} @error { +

Something went wrong!

+} + +@boundary { + +} @error { +

Error occurred: {{ $error.message }}

+} + +@boundary { + +} @error { +

Loading failed.

+ +} + +@boundary { + +} @error { + +} + +@boundary { + +} @error (let err; retry = $reset; when isNetworkError(err)) { + + +} @error { + +} + +@boundary { + +} @error (let err; reset = $reset; when isRenderError(err)) { +

Network issue. Check your connection.

+ +} @error { +

An unexpected error occurred: {{ $error.message }}

+} + +@boundary { + +} @error (let err = $error; let r = $reset) { +

Failed to load chart: {{err.message}}

+ +} + +@boundary { + Outer Main + @boundary { + Inner Main + } @error { + Inner Fallback + } +} @error { + Outer Fallback +} + +@defer { + @boundary { + + } @error { + Render error + } +} @error { + Load error +} + +=====================================output===================================== +@boundary { + +} @error { +

Something went wrong!

+} + +@boundary { + +} @error { +

Error occurred: {{ $error.message }}

+} + +@boundary { + +} @error { +

Loading failed.

+ +} + +@boundary { + +} @error { + +} + +@boundary { + +} @error (let err; retry = $reset; when isNetworkError(err)) { + + +} @error { + +} + +@boundary { + +} @error (let err; reset = $reset; when isRenderError(err)) { +

Network issue. Check your connection.

+ +} @error { +

An unexpected error occurred: {{ $error.message }}

+} + +@boundary { + +} @error (let err = $error; let r = $reset) { +

Failed to load chart: {{ err.message }}

+ +} + +@boundary { + Outer Main + @boundary { + Inner Main + } @error { + Inner Fallback + } +} @error { + Outer Fallback +} + +@defer { + @boundary { + + } @error { + Render error + } +} @error { + Load error +} + +================================================================================ +`; + +exports[`boundary-error-parameters.html format 1`] = ` +====================================options===================================== +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +@boundary { + +} @error (let err; retry + += $reset; when +isNetworkError(err)) { + + +} @error { + +} + +@boundary { + +} @error (let +err; reset += +$reset; when +isRenderError(err)) { +

Network issue. Check your connection.

+ +} @error { +

An unexpected error occurred: {{$error.message}}

+} + +@boundary { + +} @error (let err=$error; let r=$reset) { +

Failed to load chart: {{err.message}}

+ +} + +@boundary { + Main +} @error (let err; when err.message === '404') { + Not Found +} + +@boundary { + Main +} @error (err = $error, retry += +$reset) { + +} + +=====================================output===================================== +@boundary { + +} @error (let err; retry + += $reset; when +isNetworkError(err)) { + + +} @error { + +} + +@boundary { + +} @error (let +err; reset += +$reset; when +isRenderError(err)) { +

Network issue. Check your connection.

+ +} @error { +

An unexpected error occurred: {{ $error.message }}

+} + +@boundary { + +} @error (let err=$error; let r=$reset) { +

Failed to load chart: {{ err.message }}

+ +} + +@boundary { + Main +} @error (let err; when err.message === '404') { + Not Found +} + +@boundary { + Main +} @error (err = $error, retry += +$reset) { + +} + +================================================================================ +`; + exports[`chid.html format 1`] = ` ====================================options===================================== parsers: ["angular"] diff --git a/tests/format/angular/control-flow/boundary-error-parameters.html b/tests/format/angular/control-flow/boundary-error-parameters.html new file mode 100644 index 000000000000..5bcec9af436c --- /dev/null +++ b/tests/format/angular/control-flow/boundary-error-parameters.html @@ -0,0 +1,45 @@ +@boundary { + +} @error (let err; retry + += $reset; when +isNetworkError(err)) { + + +} @error { + +} + +@boundary { + +} @error (let +err; reset += +$reset; when +isRenderError(err)) { +

Network issue. Check your connection.

+ +} @error { +

An unexpected error occurred: {{$error.message}}

+} + +@boundary { + +} @error (let err=$error; let r=$reset) { +

Failed to load chart: {{err.message}}

+ +} + +@boundary { + Main +} @error (let err; when err.message === '404') { + Not Found +} + +@boundary { + Main +} @error (err = $error, retry += +$reset) { + +} diff --git a/tests/format/angular/control-flow/boundary.html b/tests/format/angular/control-flow/boundary.html new file mode 100644 index 000000000000..16d0ea579f3b --- /dev/null +++ b/tests/format/angular/control-flow/boundary.html @@ -0,0 +1,70 @@ +@boundary { + +} @error { +

Something went wrong!

+} + +@boundary { + +} @error { +

Error occurred: {{ $error.message }}

+} + +@boundary { + +} @error { +

Loading failed.

+ +} + +@boundary { + +} @error { + +} + +@boundary { + +} @error (let err; retry = $reset; when isNetworkError(err)) { + + +} @error { + +} + +@boundary { + +} @error (let err; reset = $reset; when isRenderError(err)) { +

Network issue. Check your connection.

+ +} @error { +

An unexpected error occurred: {{ $error.message }}

+} + +@boundary { + +} @error (let err = $error; let r = $reset) { +

Failed to load chart: {{err.message}}

+ +} + +@boundary { + Outer Main + @boundary { + Inner Main + } @error { + Inner Fallback + } +} @error { + Outer Fallback +} + +@defer { + @boundary { + + } @error { + Render error + } +} @error { + Load error +} diff --git a/tests/format/js/dynamic-import/__snapshots__/format.test.js.snap b/tests/format/js/dynamic-import/__snapshots__/format.test.js.snap index 05d949c40731..62b2d68fc1c7 100644 --- a/tests/format/js/dynamic-import/__snapshots__/format.test.js.snap +++ b/tests/format/js/dynamic-import/__snapshots__/format.test.js.snap @@ -93,6 +93,16 @@ exports[`import-phase.js format[meriyah] 1`] = ` Cause: [1:7-1:13]: The only valid meta property for import is 'import.meta'" `; +exports[`import-phase.js format[typescript] 1`] = ` +"'source' is not a valid meta-property for keyword 'import'. (1:1) +> 1 | import.source(\`data:text/javascript, + | ^^^^^^^^^^^^^ + 2 | console.log("RUN"); + 3 | \`) + 4 | +Cause: 'source' is not a valid meta-property for keyword 'import'." +`; + exports[`template-literal.js format 1`] = ` ====================================options===================================== parsers: ["babel", "flow", "typescript"] diff --git a/tests/format/js/dynamic-import/format.test.js b/tests/format/js/dynamic-import/format.test.js index 3c75120caad2..137e1854f72d 100644 --- a/tests/format/js/dynamic-import/format.test.js +++ b/tests/format/js/dynamic-import/format.test.js @@ -5,5 +5,6 @@ runFormatTest(import.meta, ["babel", "flow", "typescript"], { acorn: ["import-phase.js"], espree: ["import-phase.js"], meriyah: ["import-phase.js"], + typescript: ["import-phase.js"], }, }); diff --git a/tests/format/js/embeded/indention/19518.js b/tests/format/js/embeded/indention/19518.js new file mode 100644 index 000000000000..0f3a45acb325 --- /dev/null +++ b/tests/format/js/embeded/indention/19518.js @@ -0,0 +1,36 @@ +const t = html` +
    + ${items.map( + (entry) => html` +
  1. + ${entry.children + ? html` +
      + ${entry.children.map( + (child) => html`
    1. ${child.title}
    2. `, + )} +
    + ` + : entry.title} +
  2. + `, + )} +
+`; + +export function foo() { + return html` +
+
${JSON.stringify({
+                  a: 1,
+                  b: 2,
+                })}
+
+ `; +} + +const a = html` + ${{ + c: y, + }} +`; diff --git a/tests/format/js/embeded/indention/__snapshots__/format.test.js.snap b/tests/format/js/embeded/indention/__snapshots__/format.test.js.snap new file mode 100644 index 000000000000..4ff6e4d96ba0 --- /dev/null +++ b/tests/format/js/embeded/indention/__snapshots__/format.test.js.snap @@ -0,0 +1,236 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`19518.js format 1`] = ` +====================================options===================================== +parsers: ["babel", "flow", "typescript"] + printWidth: 80 (default) | +=====================================input====================================== +const t = html\` +
    + \${items.map( + (entry) => html\` +
  1. + \${entry.children + ? html\` +
      + \${entry.children.map( + (child) => html\`
    1. \${child.title}
    2. \`, + )} +
    + \` + : entry.title} +
  2. + \`, + )} +
+\`; + +export function foo() { + return html\` +
+
\${JSON.stringify({
+                  a: 1,
+                  b: 2,
+                })}
+
+ \`; +} + +const a = html\` + \${{ + c: y, + }} +\`; + +=====================================output===================================== +const t = html\` +
    + \${items.map( + (entry) => html\` +
  1. + \${ + entry.children + ? html\` +
      + \${entry.children.map( + (child) => html\`
    1. \${child.title}
    2. \`, + )} +
    + \` + : entry.title + } +
  2. + \`, + )} +
+\`; + +export function foo() { + return html\` +
+
+\${JSON.stringify({
+          a: 1,
+          b: 2,
+        })}
+
+ \`; +} + +const a = html\` + \${{ + c: y, + }} +\`; + +================================================================================ +`; + +exports[`indention.js format 1`] = ` +====================================options===================================== +parsers: ["babel", "flow", "typescript"] + printWidth: 80 (default) | +=====================================input====================================== +_ = \` + line + \${ + a + // comment + + b} + line +\`; + +_ = String.raw\` + line + \${ + a + // comment + + b} + line +\`; + +_ = css\` + a{ + color: + \${ + a + // comment + + b} + ; + } +\`; + +_ = html\` +
+ \${ + a + // comment + + b} +
+\`; + +_ = gql\` + + \${ + a + // comment + + b} + +\`; + +=====================================output===================================== +_ = \` + line + \${ + a + + // comment + b + } + line +\`; + +_ = String.raw\` + line + \${ + a + + // comment + b + } + line +\`; + +_ = css\` + a { + color: \${ + a + + // comment + b + }; + } +\`; + +_ = html\` +
+ \${ + a + + // comment + b + } +
+\`; + +_ = gql\` + \${ + a + + // comment + b + } +\`; + +================================================================================ +`; + +exports[`indention-2.js format 1`] = ` +====================================options===================================== +parsers: ["babel", "flow", "typescript"] + printWidth: 80 (default) | +=====================================input====================================== +_ = html\` +
+ \${ + a + // + b + } +
+
+ \${ + a + // + b + } +
+\` + +=====================================output===================================== +_ = html\` +
+ \${ + a + // + b + } +
+
+
+
+
+ \${ + a + // + b + } +
+
+
+
+\`; + +================================================================================ +`; diff --git a/tests/format/js/embeded/indention/format.test.js b/tests/format/js/embeded/indention/format.test.js new file mode 100644 index 000000000000..67c251cced19 --- /dev/null +++ b/tests/format/js/embeded/indention/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ["babel", "flow", "typescript"]); diff --git a/tests/format/js/embeded/indention/indention-2.js b/tests/format/js/embeded/indention/indention-2.js new file mode 100644 index 000000000000..587876202e4b --- /dev/null +++ b/tests/format/js/embeded/indention/indention-2.js @@ -0,0 +1,14 @@ +_ = html` +
+ ${ + a + // + b + } +
+
+ ${ + a + // + b + } +
+` diff --git a/tests/format/js/embeded/indention/indention.js b/tests/format/js/embeded/indention/indention.js new file mode 100644 index 000000000000..39c45a0c6a1c --- /dev/null +++ b/tests/format/js/embeded/indention/indention.js @@ -0,0 +1,46 @@ +_ = ` + line + ${ + a + // comment + + b} + line +`; + +_ = String.raw` + line + ${ + a + // comment + + b} + line +`; + +_ = css` + a{ + color: + ${ + a + // comment + + b} + ; + } +`; + +_ = html` +
+ ${ + a + // comment + + b} +
+`; + +_ = gql` + + ${ + a + // comment + + b} + +`; diff --git a/tests/format/js/multiparser-comments/__snapshots__/format.test.js.snap b/tests/format/js/multiparser-comments/__snapshots__/format.test.js.snap index 9eeb77132da5..212d9cbc64af 100644 --- a/tests/format/js/multiparser-comments/__snapshots__/format.test.js.snap +++ b/tests/format/js/multiparser-comments/__snapshots__/format.test.js.snap @@ -84,9 +84,9 @@ html\`\${ }\`; html\` \${ - foo - /* comment */ -} + foo + /* comment */ + } \`; graphql\` @@ -97,9 +97,9 @@ graphql\` \`; graphql\` \${ - foo - /* comment */ -} + foo + /* comment */ + } \`; css\` @@ -110,9 +110,9 @@ css\` \`; css\` \${ - foo - /* comment */ -} + foo + /* comment */ + } \`; markdown\`\${ diff --git a/tests/format/jsx/template/__snapshots__/format.test.js.snap b/tests/format/jsx/template/__snapshots__/format.test.js.snap index c9ca791785ca..deead232333a 100644 --- a/tests/format/jsx/template/__snapshots__/format.test.js.snap +++ b/tests/format/jsx/template/__snapshots__/format.test.js.snap @@ -63,8 +63,8 @@ parsers: ["babel", "typescript", "flow"] ;