prettier-plugin-svelte
Advanced tools
Comparing version 1.4.2 to 2.0.0
# prettier-plugin-svelte changelog | ||
## 2.0.0 | ||
This release comes with a rewrite of the HTML formatting. The output is now much more in line with how standard Prettier formats HTML. This is also why `svelteBracketNewLine` now defaults to `true`. Another notable default change is the sort order: `svelte:options` is now part of the sort order and the default changed to `options-scripts-markup-styles`, which is in line with how the majority of users like to order the code blocks. | ||
The complete list of changes: | ||
* Rework the tag breaking logic with the goal to be more in line with how Prettier formats standard HTML. This includes respecting the user's decision to have child tags in separate lines even if they don't exceed the maximum line width ([#143](https://github.com/sveltejs/prettier-plugin-svelte/issues/143), [#117](https://github.com/sveltejs/prettier-plugin-svelte/issues/117)). This is a breaking change because tags are broken up differently now than before. | ||
* `<svelte:options>` is now part of `svelteSortOrder`. Default sort order is now `options-scripts-markup-styles`. This is a breaking change. ([#73](https://github.com/sveltejs/prettier-plugin-svelte/issues/73)) | ||
* `svelteBracketNewLine` defaults to `true` now to be more in line with how Prettier formats standard HTML. This is a breaking change | ||
* Fix formatting of fenced Svelte code blocks inside Markdown ([#129](https://github.com/sveltejs/prettier-plugin-svelte/issues/129)) | ||
* Everything that is not explicitly a block element is now treated as an inline element, including components. This is a breaking change ([#159](https://github.com/sveltejs/prettier-plugin-svelte/issues/159)) | ||
* Single quotes are no longer forced except inside quoted attributes/events/etc. This is a breaking change ([#94](https://github.com/sveltejs/prettier-plugin-svelte/issues/94)) | ||
* If the content inside a `{tag}` is too long, break it up if possible (excluding `{#if}`/`{#await}`/etc. blocks). This is a breaking change ([#170](https://github.com/sveltejs/prettier-plugin-svelte/issues/170)) | ||
* If the content of a `<script>`/`<style>` tag is completely empty (no whitespace), don't put the closing tag on a new line ([#87](https://github.com/sveltejs/prettier-plugin-svelte/issues/87)) | ||
## 1.4.2 | ||
@@ -4,0 +19,0 @@ |
{ | ||
"name": "prettier-plugin-svelte", | ||
"version": "1.4.2", | ||
"version": "2.0.0", | ||
"description": "Svelte plugin for prettier", | ||
@@ -32,13 +32,13 @@ "main": "plugin.js", | ||
"@types/node": "^10.12.18", | ||
"@types/prettier": "^2.0.2", | ||
"ava": "1.2.0", | ||
"prettier": "^2.1.0", | ||
"rollup": "1.1.2", | ||
"rollup-plugin-commonjs": "9.2.0", | ||
"rollup-plugin-node-resolve": "4.0.0", | ||
"rollup-plugin-typescript": "1.0.0", | ||
"@types/prettier": "^2.1.6", | ||
"ava": "3.15.0", | ||
"prettier": "^2.2.1", | ||
"rollup": "2.36.0", | ||
"@rollup/plugin-commonjs": "14.0.0", | ||
"@rollup/plugin-node-resolve": "11.0.1", | ||
"rollup-plugin-typescript": "1.0.1", | ||
"svelte": "^3.30.0", | ||
"ts-node": "^7.0.1", | ||
"tslib": "^1.9.3", | ||
"typescript": "3.2.4" | ||
"ts-node": "^9.1.1", | ||
"tslib": "^2.0.3", | ||
"typescript": "4.1.3" | ||
}, | ||
@@ -45,0 +45,0 @@ "peerDependencies": { |
1291
plugin.js
@@ -24,55 +24,37 @@ 'use strict'; | ||
]; | ||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements#Elements | ||
const inlineElements = [ | ||
'a', | ||
'abbr', | ||
'audio', | ||
'b', | ||
'bdi', | ||
'bdo', | ||
'br', | ||
'button', | ||
'canvas', | ||
'cite', | ||
'code', | ||
'data', | ||
'datalist', | ||
'del', | ||
'dfn', | ||
'em', | ||
'embed', | ||
'i', | ||
'iframe', | ||
'img', | ||
'input', | ||
'ins', | ||
'kbd', | ||
'label', | ||
'map', | ||
'mark', | ||
'meter', | ||
'noscript', | ||
'object', | ||
'output', | ||
'picture', | ||
'progress', | ||
'q', | ||
'ruby', | ||
's', | ||
'samp', | ||
'select', | ||
'slot', | ||
'small', | ||
'span', | ||
'strong', | ||
'sub', | ||
'sup', | ||
'svg', | ||
'template', | ||
'textarea', | ||
'time', | ||
'u', | ||
'var', | ||
'video', | ||
'wbr' | ||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements#Elements | ||
const blockElements = [ | ||
'address', | ||
'article', | ||
'aside', | ||
'blockquote', | ||
'details', | ||
'dialog', | ||
'dd', | ||
'div', | ||
'dl', | ||
'dt', | ||
'fieldset', | ||
'figcaption', | ||
'figure', | ||
'footer', | ||
'form', | ||
'h1', | ||
'h2', | ||
'h3', | ||
'h4', | ||
'h5', | ||
'h6', | ||
'header', | ||
'hgroup', | ||
'hr', | ||
'li', | ||
'main', | ||
'nav', | ||
'ol', | ||
'p', | ||
'pre', | ||
'section', | ||
'table', | ||
'ul', | ||
]; | ||
@@ -88,18 +70,2 @@ /** | ||
/** | ||
* Determines whether or not given node | ||
* is the root of the Svelte AST. | ||
*/ | ||
function isASTNode(n) { | ||
return n && n.__isRoot; | ||
} | ||
function isPreTagContent(path) { | ||
const stack = path.stack; | ||
return stack.some((node) => (node.type === 'Element' && node.name.toLowerCase() === 'pre') || | ||
(node.type === 'Attribute' && !formattableAttributes.includes(node.name))); | ||
} | ||
function flatten(arrays) { | ||
return [].concat.apply([], arrays); | ||
} | ||
function extractAttributes(html) { | ||
@@ -151,17 +117,68 @@ const extractAttributesRegex = /<[a-z]+\s*(.*?)>/i; | ||
const snippedTagContentAttribute = '✂prettier:content✂'; | ||
function snipTagContent(tagName, source, placeholder = '') { | ||
const regex = new RegExp(`[\\s\n]*<${tagName}([^]*?)>([^]*?)<\/${tagName}>[\\s\n]*`, 'gi'); | ||
return source.replace(regex, (_, attributes, content) => { | ||
const encodedContent = Buffer.from(content).toString('base64'); | ||
return `<${tagName}${attributes} ${snippedTagContentAttribute}="${encodedContent}">${placeholder}</${tagName}>`; | ||
}); | ||
} | ||
function hasSnippedContent(text) { | ||
return text.includes(snippedTagContentAttribute); | ||
} | ||
function unsnipContent(text) { | ||
const regex = /(<\w+.*?)\s*✂prettier:content✂="(.*?)">.*?(?=<\/)/gi; | ||
return text.replace(regex, (_, start, encodedContent) => { | ||
const content = Buffer.from(encodedContent, 'base64').toString('utf8'); | ||
return `${start}>${content}`; | ||
}); | ||
} | ||
function makeChoice(choice) { | ||
return { value: choice, description: choice }; | ||
} | ||
const options = { | ||
svelteSortOrder: { | ||
since: '0.6.0', | ||
category: 'Svelte', | ||
type: 'choice', | ||
default: 'scripts-styles-markup', | ||
description: 'Sort order for scripts, styles, and markup', | ||
default: 'options-scripts-markup-styles', | ||
description: 'Sort order for scripts, markup, and styles', | ||
choices: [ | ||
{ value: 'scripts-styles-markup' }, | ||
{ value: 'scripts-markup-styles' }, | ||
{ value: 'markup-styles-scripts' }, | ||
{ value: 'markup-scripts-styles' }, | ||
{ value: 'styles-markup-scripts' }, | ||
{ value: 'styles-scripts-markup' }, | ||
makeChoice('options-scripts-markup-styles'), | ||
makeChoice('options-scripts-styles-markup'), | ||
makeChoice('options-markup-styles-scripts'), | ||
makeChoice('options-markup-scripts-styles'), | ||
makeChoice('options-styles-markup-scripts'), | ||
makeChoice('options-styles-scripts-markup'), | ||
makeChoice('scripts-options-markup-styles'), | ||
makeChoice('scripts-options-styles-markup'), | ||
makeChoice('markup-options-styles-scripts'), | ||
makeChoice('markup-options-scripts-styles'), | ||
makeChoice('styles-options-markup-scripts'), | ||
makeChoice('styles-options-scripts-markup'), | ||
makeChoice('scripts-markup-options-styles'), | ||
makeChoice('scripts-styles-options-markup'), | ||
makeChoice('markup-styles-options-scripts'), | ||
makeChoice('markup-scripts-options-styles'), | ||
makeChoice('styles-markup-options-scripts'), | ||
makeChoice('styles-scripts-options-markup'), | ||
makeChoice('scripts-markup-styles-options'), | ||
makeChoice('scripts-styles-markup-options'), | ||
makeChoice('markup-styles-scripts-options'), | ||
makeChoice('markup-scripts-styles-options'), | ||
makeChoice('styles-markup-scripts-options'), | ||
makeChoice('styles-scripts-markup-options'), | ||
// Deprecated, keep in 2.x for backwards-compatibility. svelte:options will be moved to the top | ||
makeChoice('scripts-markup-styles'), | ||
makeChoice('scripts-styles-markup'), | ||
makeChoice('markup-styles-scripts'), | ||
makeChoice('markup-scripts-styles'), | ||
makeChoice('styles-markup-scripts'), | ||
makeChoice('styles-scripts-markup'), | ||
], | ||
}, | ||
svelteStrictMode: { | ||
since: '0.7.0', | ||
category: 'Svelte', | ||
type: 'boolean', | ||
@@ -172,7 +189,11 @@ default: false, | ||
svelteBracketNewLine: { | ||
since: '0.6.0', | ||
category: 'Svelte', | ||
type: 'boolean', | ||
default: false, | ||
default: true, | ||
description: 'Put the `>` of a multiline element on a new line', | ||
}, | ||
svelteAllowShorthand: { | ||
since: '1.0.0', | ||
category: 'Svelte', | ||
type: 'boolean', | ||
@@ -183,2 +204,4 @@ default: true, | ||
svelteIndentScriptAndStyle: { | ||
since: '1.2.0', | ||
category: 'Svelte', | ||
type: 'boolean', | ||
@@ -191,75 +214,134 @@ default: true, | ||
function parseSortOrder(sortOrder) { | ||
return sortOrder.split(sortOrderSeparator); | ||
const order = sortOrder.split(sortOrderSeparator); | ||
// For backwards compatibility: Add options to beginning if not present | ||
if (!order.includes('options')) { | ||
console.warn('svelteSortOrder is missing option `options`. This will be an error in prettier-plugin-svelte version 3.'); | ||
order.unshift('options'); | ||
} | ||
return order; | ||
} | ||
const snippedTagContentAttribute = '✂prettier:content✂'; | ||
function snipTagContent(tagName, source, placeholder = '') { | ||
const regex = new RegExp(`[\\s\n]*<${tagName}([^]*?)>([^]*?)<\/${tagName}>[\\s\n]*`, 'gi'); | ||
return source.replace(regex, (_, attributes, content) => { | ||
const encodedContent = Buffer.from(content).toString('base64'); | ||
return `<${tagName}${attributes} ${snippedTagContentAttribute}="${encodedContent}">${placeholder}</${tagName}>`; | ||
}); | ||
/** | ||
* Determines whether or not given node | ||
* is the root of the Svelte AST. | ||
*/ | ||
function isASTNode(n) { | ||
return n && n.__isRoot; | ||
} | ||
function hasSnippedContent(text) { | ||
return text.includes(snippedTagContentAttribute); | ||
function isPreTagContent(path) { | ||
const stack = path.stack; | ||
return stack.some((node) => (node.type === 'Element' && node.name.toLowerCase() === 'pre') || | ||
(node.type === 'Attribute' && !formattableAttributes.includes(node.name))); | ||
} | ||
function unsnipContent(text) { | ||
const regex = /(<\w+.*?)\s*✂prettier:content✂="(.*?)">.*?(?=<\/)/gi; | ||
return text.replace(regex, (_, start, encodedContent) => { | ||
const content = Buffer.from(encodedContent, 'base64').toString('utf8'); | ||
return `${start}>${content}`; | ||
}); | ||
function flatten(arrays) { | ||
return [].concat.apply([], arrays); | ||
} | ||
function findLastIndex(isMatch, items) { | ||
for (let i = items.length - 1; i >= 0; i--) { | ||
if (isMatch(items[i], i)) { | ||
return i; | ||
} | ||
} | ||
return -1; | ||
} | ||
const unsupportedLanguages = ['coffee', 'coffeescript', 'pug', 'styl', 'stylus', 'sass']; | ||
function isInlineElement(node) { | ||
return node.type === 'Element' && inlineElements.includes(node.name); | ||
function isLine(docToCheck) { | ||
return (docToCheck === prettier.doc.builders.hardline || | ||
(typeof docToCheck === 'object' && docToCheck.type === 'line') || | ||
(typeof docToCheck === 'object' && | ||
docToCheck.type === 'concat' && | ||
docToCheck.parts.every(isLine))); | ||
} | ||
function isWhitespaceChar(ch) { | ||
return ' \t\n\r'.indexOf(ch) >= 0; | ||
/** | ||
* Check if the doc is empty, i.e. consists of nothing more than empty strings (possibly nested). | ||
*/ | ||
function isEmptyDoc(doc) { | ||
if (typeof doc === 'string') { | ||
return doc.length === 0; | ||
} | ||
if (doc.type === 'line') { | ||
return !doc.keepIfLonely; | ||
} | ||
const { contents } = doc; | ||
if (contents) { | ||
return isEmptyDoc(contents); | ||
} | ||
const { parts } = doc; | ||
if (parts) { | ||
return isEmptyGroup(parts); | ||
} | ||
return false; | ||
} | ||
function canBreakAfter(node) { | ||
switch (node.type) { | ||
case 'Text': | ||
return isWhitespaceChar(node.raw[node.raw.length - 1]); | ||
case 'Element': | ||
return !isInlineElement(node); | ||
case 'IfBlock': | ||
case 'EachBlock': | ||
case 'MustacheTag': | ||
return false; | ||
default: | ||
return true; | ||
function isEmptyGroup(group) { | ||
return !group.find((doc) => !isEmptyDoc(doc)); | ||
} | ||
/** | ||
* Trims both leading and trailing nodes matching `isWhitespace` independent of nesting level | ||
* (though all trimmed adjacent nodes need to be a the same level). Modifies the `docs` array. | ||
*/ | ||
function trim(docs, isWhitespace) { | ||
trimLeft(docs, isWhitespace); | ||
trimRight(docs, isWhitespace); | ||
return docs; | ||
} | ||
/** | ||
* Trims the leading nodes matching `isWhitespace` independent of nesting level (though all nodes need to be a the same level) | ||
* and returnes the removed nodes. | ||
*/ | ||
function trimLeft(group, isWhitespace) { | ||
let firstNonWhitespace = group.findIndex((doc) => !isWhitespace(doc)); | ||
if (firstNonWhitespace < 0 && group.length) { | ||
firstNonWhitespace = group.length; | ||
} | ||
if (firstNonWhitespace > 0) { | ||
return group.splice(0, firstNonWhitespace); | ||
} | ||
else { | ||
const parts = getParts(group[0]); | ||
if (parts) { | ||
return trimLeft(parts, isWhitespace); | ||
} | ||
} | ||
} | ||
function canBreakBefore(node) { | ||
switch (node.type) { | ||
case 'Text': | ||
return isWhitespaceChar(node.raw[0]); | ||
case 'Element': | ||
return !isInlineElement(node); | ||
case 'IfBlock': | ||
case 'EachBlock': | ||
case 'MustacheTag': | ||
return false; | ||
default: | ||
return true; | ||
/** | ||
* Trims the trailing nodes matching `isWhitespace` independent of nesting level (though all nodes need to be a the same level) | ||
* and returnes the removed nodes. | ||
*/ | ||
function trimRight(group, isWhitespace) { | ||
let lastNonWhitespace = group.length ? findLastIndex((doc) => !isWhitespace(doc), group) : 0; | ||
if (lastNonWhitespace < group.length - 1) { | ||
return group.splice(lastNonWhitespace + 1); | ||
} | ||
else { | ||
const parts = getParts(group[group.length - 1]); | ||
if (parts) { | ||
return trimRight(parts, isWhitespace); | ||
} | ||
} | ||
} | ||
function isInlineNode(node) { | ||
switch (node.type) { | ||
case 'Text': | ||
const text = getUnencodedText(node); | ||
const isAllWhitespace = text.trim() === ''; | ||
return !isAllWhitespace || text === ''; | ||
case 'MustacheTag': | ||
case 'EachBlock': | ||
case 'IfBlock': | ||
return true; | ||
case 'Element': | ||
return isInlineElement(node); | ||
default: | ||
return false; | ||
function getParts(doc) { | ||
if (typeof doc === 'object' && (doc.type === 'fill' || doc.type === 'concat')) { | ||
return doc.parts; | ||
} | ||
} | ||
const unsupportedLanguages = ['coffee', 'coffeescript', 'pug', 'styl', 'stylus', 'sass']; | ||
function isInlineElement(path, node) { | ||
return node && node.type === 'Element' && !isBlockElement(node) && !isPreTagContent(path); | ||
} | ||
function isBlockElement(node) { | ||
return node && node.type === 'Element' && blockElements.includes(node.name); | ||
} | ||
function isSvelteBlock(node) { | ||
return [ | ||
'IfBlock', | ||
'AwaitBlock', | ||
'CatchBlock', | ||
'EachBlock', | ||
'ElseBlock', | ||
'KeyBlock', | ||
'PendingBlock', | ||
'ThenBlock', | ||
].includes(node.type); | ||
} | ||
function isNodeWithChildren(node) { | ||
@@ -291,3 +373,3 @@ return node.children; | ||
} | ||
function isEmptyNode(node) { | ||
function isEmptyTextNode(node) { | ||
return node.type === 'Text' && getUnencodedText(node).trim() === ''; | ||
@@ -365,94 +447,170 @@ } | ||
} | ||
function isLine(doc) { | ||
return typeof doc === 'object' && doc.type === 'line'; | ||
function isTextNodeStartingWithLinebreak(node, nrLines = 1) { | ||
return node.type === 'Text' && startsWithLinebreak(getUnencodedText(node), nrLines); | ||
} | ||
function isLineDiscardedIfLonely(doc) { | ||
return isLine(doc) && !doc.keepIfLonely; | ||
function startsWithLinebreak(text, nrLines = 1) { | ||
return new RegExp(`^([\\t\\f\\r ]*\\n){${nrLines}}`).test(text); | ||
} | ||
function isTextNodeEndingWithLinebreak(node, nrLines = 1) { | ||
return node.type === 'Text' && endsWithLinebreak(getUnencodedText(node), nrLines); | ||
} | ||
function endsWithLinebreak(text, nrLines = 1) { | ||
return new RegExp(`(\\n[\\t\\f\\r ]*){${nrLines}}$`).test(text); | ||
} | ||
function isTextNodeStartingWithWhitespace(node) { | ||
return node.type === 'Text' && /^\s/.test(getUnencodedText(node)); | ||
} | ||
function isTextNodeEndingWithWhitespace(node) { | ||
return node.type === 'Text' && /\s$/.test(getUnencodedText(node)); | ||
} | ||
function trimTextNodeRight(node) { | ||
node.raw = node.raw && node.raw.trimRight(); | ||
node.data = node.data && node.data.trimRight(); | ||
} | ||
function trimTextNodeLeft(node) { | ||
node.raw = node.raw && node.raw.trimLeft(); | ||
node.data = node.data && node.data.trimLeft(); | ||
} | ||
/** | ||
* Check if the doc is empty, i.e. consists of nothing more than empty strings (possibly nested). | ||
* Remove all leading whitespace up until the first non-empty text node, | ||
* and all trailing whitepsace from the last non-empty text node onwards. | ||
*/ | ||
function isEmptyDoc(doc) { | ||
if (typeof doc === 'string') { | ||
return doc.length === 0; | ||
function trimChildren(children, path) { | ||
let firstNonEmptyNode = children.findIndex((n) => !isEmptyTextNode(n) && !doesEmbedStartAt(n.end, path)); | ||
firstNonEmptyNode = firstNonEmptyNode === -1 ? children.length - 1 : firstNonEmptyNode; | ||
let lastNonEmptyNode = findLastIndex((n, idx) => { | ||
// Last node is ok to end and the start of an embeded region, | ||
// if it's not a comment (which should stick to the region) | ||
return (!isEmptyTextNode(n) && | ||
((idx === children.length - 1 && n.type !== 'Comment') || | ||
!doesEmbedStartAt(n.end, path))); | ||
}, children); | ||
lastNonEmptyNode = lastNonEmptyNode === -1 ? 0 : lastNonEmptyNode; | ||
for (let i = 0; i <= firstNonEmptyNode; i++) { | ||
const n = children[i]; | ||
if (n.type === 'Text') { | ||
trimTextNodeLeft(n); | ||
} | ||
} | ||
if (doc.type === 'line') { | ||
return !doc.keepIfLonely; | ||
for (let i = children.length - 1; i >= lastNonEmptyNode; i--) { | ||
const n = children[i]; | ||
if (n.type === 'Text') { | ||
trimTextNodeRight(n); | ||
} | ||
} | ||
const { contents } = doc; | ||
if (contents) { | ||
return isEmptyDoc(contents); | ||
} | ||
/** | ||
* Check if given node's starg tag should hug its first child. This is the case for inline elements when there's | ||
* no whitespace between the `>` and the first child. | ||
*/ | ||
function shouldHugStart(node, isSupportedLanguage) { | ||
if (!isSupportedLanguage) { | ||
return true; | ||
} | ||
const { parts } = doc; | ||
if (parts) { | ||
return isEmptyGroup(parts); | ||
if (isBlockElement(node)) { | ||
return false; | ||
} | ||
return false; | ||
if (!isNodeWithChildren(node)) { | ||
return false; | ||
} | ||
const children = node.children; | ||
if (children.length === 0) { | ||
return true; | ||
} | ||
const firstChild = children[0]; | ||
return !isTextNodeStartingWithWhitespace(firstChild); | ||
} | ||
function isEmptyGroup(group) { | ||
return !group.find(doc => !isEmptyDoc(doc)); | ||
} | ||
/** | ||
* Trims both leading and trailing nodes matching `isWhitespace` independent of nesting level | ||
* (though all trimmed adjacent nodes need to be a the same level). Modifies the `docs` array. | ||
* Check if given node's end tag should hug its last child. This is the case for inline elements when there's | ||
* no whitespace between the last child and the `</`. | ||
*/ | ||
function trim(docs, isWhitespace) { | ||
trimLeft(docs, isWhitespace); | ||
trimRight(docs, isWhitespace); | ||
return docs; | ||
function shouldHugEnd(node, isSupportedLanguage) { | ||
if (!isSupportedLanguage) { | ||
return true; | ||
} | ||
if (isBlockElement(node)) { | ||
return false; | ||
} | ||
if (!isNodeWithChildren(node)) { | ||
return false; | ||
} | ||
const children = node.children; | ||
if (children.length === 0) { | ||
return true; | ||
} | ||
const lastChild = children[children.length - 1]; | ||
return !isTextNodeEndingWithWhitespace(lastChild); | ||
} | ||
/** | ||
* Trims the leading nodes matching `isWhitespace` independent of nesting level (though all nodes need to be a the same level) | ||
* and returnes the removed nodes. | ||
* Check for a svelte block if there's whitespace at the start and if it's a space or a line. | ||
*/ | ||
function trimLeft(group, isWhitespace) { | ||
let firstNonWhitespace = group.findIndex((doc) => !isWhitespace(doc)); | ||
if (firstNonWhitespace < 0 && group.length) { | ||
firstNonWhitespace = group.length; | ||
function checkWhitespaceAtStartOfSvelteBlock(node, options) { | ||
if (!isSvelteBlock(node) || !isNodeWithChildren(node)) { | ||
return 'none'; | ||
} | ||
if (firstNonWhitespace > 0) { | ||
return group.splice(0, firstNonWhitespace); | ||
const children = node.children; | ||
if (children.length === 0) { | ||
return 'none'; | ||
} | ||
else { | ||
const parts = getParts(group[0]); | ||
if (parts) { | ||
return trimLeft(parts, isWhitespace); | ||
const firstChild = children[0]; | ||
if (isTextNodeStartingWithLinebreak(firstChild)) { | ||
return 'line'; | ||
} | ||
else if (isTextNodeStartingWithWhitespace(firstChild)) { | ||
return 'space'; | ||
} | ||
// This extra check is necessary because the Svelte AST might swallow whitespace between | ||
// the block's starting end and its first child. | ||
const parentOpeningEnd = options.originalText.lastIndexOf('}', firstChild.start); | ||
if (parentOpeningEnd > 0 && firstChild.start > parentOpeningEnd + 1) { | ||
const textBetween = options.originalText.substring(parentOpeningEnd + 1, firstChild.start); | ||
if (textBetween.trim() === '') { | ||
return startsWithLinebreak(textBetween) ? 'line' : 'space'; | ||
} | ||
} | ||
return 'none'; | ||
} | ||
/** | ||
* Trims the trailing nodes matching `isWhitespace` independent of nesting level (though all nodes need to be a the same level) | ||
* and returnes the removed nodes. | ||
* Check for a svelte block if there's whitespace at the end and if it's a space or a line. | ||
*/ | ||
function trimRight(group, isWhitespace) { | ||
let lastNonWhitespace = group.length ? findLastIndex((doc) => !isWhitespace(doc), group) : 0; | ||
if (lastNonWhitespace < group.length - 1) { | ||
return group.splice(lastNonWhitespace + 1); | ||
function checkWhitespaceAtEndOfSvelteBlock(node, options) { | ||
if (!isSvelteBlock(node) || !isNodeWithChildren(node)) { | ||
return 'none'; | ||
} | ||
else { | ||
const parts = getParts(group[group.length - 1]); | ||
if (parts) { | ||
return trimRight(parts, isWhitespace); | ||
} | ||
const children = node.children; | ||
if (children.length === 0) { | ||
return 'none'; | ||
} | ||
} | ||
function getParts(doc) { | ||
if (typeof doc === 'object' && (doc.type === 'fill' || doc.type === 'concat')) { | ||
return doc.parts; | ||
const lastChild = children[children.length - 1]; | ||
if (isTextNodeEndingWithLinebreak(lastChild)) { | ||
return 'line'; | ||
} | ||
} | ||
function findLastIndex(isMatch, items) { | ||
for (let i = items.length - 1; i >= 0; i--) { | ||
if (isMatch(items[i])) { | ||
return i; | ||
else if (isTextNodeEndingWithWhitespace(lastChild)) { | ||
return 'space'; | ||
} | ||
// This extra check is necessary because the Svelte AST might swallow whitespace between | ||
// the last child and the block's ending start. | ||
const parentClosingStart = options.originalText.indexOf('{', lastChild.end); | ||
if (parentClosingStart > 0 && lastChild.end < parentClosingStart) { | ||
const textBetween = options.originalText.substring(lastChild.end, parentClosingStart); | ||
if (textBetween.trim() === '') { | ||
return endsWithLinebreak(textBetween) ? 'line' : 'space'; | ||
} | ||
} | ||
return -1; | ||
return 'none'; | ||
} | ||
function isInsideQuotedAttribute(path, options) { | ||
const stack = path.stack; | ||
return stack.some((node) => node.type === 'Attribute' && | ||
(!isLoneMustacheTag(node.value) || options.svelteStrictMode)); | ||
} | ||
const { concat, join, line, group, indent, dedent, softline, hardline, fill, breakParent, literalline, } = prettier.doc.builders; | ||
let ignoreNext = false; | ||
const keepIfLonelyLine = Object.assign({}, line, { keepIfLonely: true, hard: true }); | ||
function print(path, options$$1, print) { | ||
let svelteOptionsDoc; | ||
let svelteOptionsComment; | ||
function groupConcat(contents) { | ||
return group(concat(contents)); | ||
} | ||
function print(path, options, print) { | ||
const n = path.getValue(); | ||
@@ -463,40 +621,15 @@ if (!n) { | ||
if (isASTNode(n)) { | ||
const parts = []; | ||
const addParts = { | ||
scripts() { | ||
if (n.module) { | ||
n.module.type = 'Script'; | ||
n.module.attributes = extractAttributes(getText(n.module, options$$1)); | ||
parts.push(path.call(print, 'module')); | ||
} | ||
if (n.instance) { | ||
n.instance.type = 'Script'; | ||
n.instance.attributes = extractAttributes(getText(n.instance, options$$1)); | ||
parts.push(path.call(print, 'instance')); | ||
} | ||
}, | ||
styles() { | ||
if (n.css) { | ||
n.css.type = 'Style'; | ||
n.css.content.type = 'StyleProgram'; | ||
parts.push(path.call(print, 'css')); | ||
} | ||
}, | ||
markup() { | ||
const htmlDoc = path.call(print, 'html'); | ||
if (htmlDoc) { | ||
parts.push(htmlDoc); | ||
} | ||
}, | ||
}; | ||
parseSortOrder(options$$1.svelteSortOrder).forEach((p) => addParts[p]()); | ||
ignoreNext = false; | ||
return group(join(hardline, parts)); | ||
return printTopLevelParts(n, options, path, print); | ||
} | ||
const [open, close] = options$$1.svelteStrictMode ? ['"{', '}"'] : ['{', '}']; | ||
const [open, close] = options.svelteStrictMode ? ['"{', '}"'] : ['{', '}']; | ||
const printJsExpression = () => [ | ||
open, | ||
printJS(path, print, options.svelteStrictMode, false, 'expression'), | ||
close, | ||
]; | ||
const node = n; | ||
if (ignoreNext && (node.type !== 'Text' || !isEmptyNode(node))) { | ||
if (ignoreNext && (node.type !== 'Text' || !isEmptyTextNode(node))) { | ||
ignoreNext = false; | ||
return concat(flatten(options$$1.originalText | ||
.slice(options$$1.locStart(node), options$$1.locEnd(node)) | ||
return concat(flatten(options.originalText | ||
.slice(options.locStart(node), options.locEnd(node)) | ||
.split('\n') | ||
@@ -508,24 +641,36 @@ .map((o, i) => (i == 0 ? [o] : [literalline, o])))); | ||
const children = node.children; | ||
if (children.length === 0 || children.every(isEmptyNode)) { | ||
if (children.length === 0 || children.every(isEmptyTextNode)) { | ||
return ''; | ||
} | ||
if (!isPreTagContent(path)) { | ||
return concat([...trim(printChildren(path, print), isLine), hardline]); | ||
trimChildren(node.children, path); | ||
const output = trim([printChildren(path, print)], (n) => isLine(n) || | ||
(typeof n === 'string' && n.trim() === '') || | ||
// Because printChildren may append this at the end and | ||
// may hide other lines before it | ||
n === breakParent); | ||
if (output.every((doc) => isEmptyDoc(doc))) { | ||
return ''; | ||
} | ||
return groupConcat([...output, hardline]); | ||
} | ||
else { | ||
return concat(printChildren(path, print)); | ||
return groupConcat(path.map(print, 'children')); | ||
} | ||
case 'Text': | ||
if (!isPreTagContent(path)) { | ||
if (isEmptyNode(node)) { | ||
return Object.assign({}, line, { | ||
/** | ||
* A text node is considered lonely if it is in a group without other inline | ||
* elements, such as the line breaks between otherwise consecutive HTML tags. | ||
* Text nodes that are both empty and lonely are discarded unless they have at | ||
* least one empty line (i.e. at least two linebreak sequences). This is to | ||
* allow for flexible grouping of HTML tags in a particular indentation level, | ||
* and is similar to how vanilla HTML is handled in Prettier core. | ||
*/ | ||
keepIfLonely: /\n\r?\s*\n\r?/.test(getUnencodedText(node)) }); | ||
if (isEmptyTextNode(node)) { | ||
const hasWhiteSpace = getUnencodedText(node).trim().length < getUnencodedText(node).length; | ||
const hasOneOrMoreNewlines = /\n/.test(getUnencodedText(node)); | ||
const hasTwoOrMoreNewlines = /\n\r?\s*\n\r?/.test(getUnencodedText(node)); | ||
if (hasTwoOrMoreNewlines) { | ||
return concat([hardline, hardline]); | ||
} | ||
if (hasOneOrMoreNewlines) { | ||
return hardline; | ||
} | ||
if (hasWhiteSpace) { | ||
return line; | ||
} | ||
return ''; | ||
} | ||
@@ -538,3 +683,3 @@ /** | ||
*/ | ||
return fill(splitTextToDocs(getUnencodedText(node))); | ||
return fill(splitTextToDocs(node)); | ||
} | ||
@@ -551,5 +696,5 @@ else { | ||
const isSupportedLanguage = !(node.name === 'template' && !isNodeSupportedLanguage(node)); | ||
const isEmpty = node.children.every((child) => isEmptyNode(child)); | ||
const isEmpty = node.children.every((child) => isEmptyTextNode(child)); | ||
const isSelfClosingTag = isEmpty && | ||
(!options$$1.svelteStrictMode || | ||
(!options.svelteStrictMode || | ||
node.type !== 'Element' || | ||
@@ -559,46 +704,131 @@ selfClosingTags.indexOf(node.name) !== -1); | ||
const attributes = path.map((childPath) => childPath.call(print), 'attributes'); | ||
const possibleThisBinding = node.type === 'InlineComponent' && node.expression | ||
? concat([line, 'this=', ...printJsExpression()]) | ||
: ''; | ||
if (isSelfClosingTag) { | ||
return groupConcat([ | ||
'<', | ||
node.name, | ||
indent(groupConcat([ | ||
possibleThisBinding, | ||
...attributes, | ||
options.svelteBracketNewLine ? dedent(line) : '', | ||
])), | ||
...[options.svelteBracketNewLine ? '' : ' ', `/>`], | ||
]); | ||
} | ||
const children = node.children; | ||
const firstChild = children[0]; | ||
const lastChild = children[children.length - 1]; | ||
// Is a function which is invoked later because printChildren will manipulate child nodes | ||
// which would wrongfully change the other checks about hugging etc done beforehand | ||
let body; | ||
let hugContent = false; | ||
const hugStart = shouldHugStart(node, isSupportedLanguage); | ||
const hugEnd = shouldHugEnd(node, isSupportedLanguage); | ||
if (isEmpty) { | ||
body = ''; | ||
body = | ||
isInlineElement(path, node) && | ||
node.children.length && | ||
isTextNodeStartingWithWhitespace(node.children[0]) && | ||
!isPreTagContent(path) | ||
? () => line | ||
: () => (options.svelteBracketNewLine ? '' : softline); | ||
} | ||
else if (isPreTagContent(path)) { | ||
body = () => printRaw(node, options.originalText); | ||
} | ||
else if (!isSupportedLanguage) { | ||
body = printRaw(node, options$$1.originalText); | ||
body = () => printRaw(node, options.originalText); | ||
hugContent = true; | ||
} | ||
else if (isInlineElement(node) || isPreTagContent(path)) { | ||
body = printIndentedPreservingWhitespace(path, print); | ||
else if (isInlineElement(path, node) && !isPreTagContent(path)) { | ||
body = () => printChildren(path, print); | ||
hugContent = true; | ||
} | ||
else { | ||
body = printIndentedWithNewlines(path, print); | ||
body = () => printChildren(path, print); | ||
} | ||
return group(concat([ | ||
const openingTag = [ | ||
'<', | ||
node.name, | ||
indent(group(concat([ | ||
node.type === 'InlineComponent' && node.expression | ||
? concat([ | ||
line, | ||
'this=', | ||
open, | ||
printJS(path, print, 'expression'), | ||
close, | ||
]) | ||
: '', | ||
indent(groupConcat([ | ||
possibleThisBinding, | ||
...attributes, | ||
options$$1.svelteBracketNewLine | ||
? dedent(isSelfClosingTag ? line : softline) | ||
: '', | ||
]))), | ||
...(isSelfClosingTag | ||
? [options$$1.svelteBracketNewLine ? '' : ' ', `/>`] | ||
: ['>', body, `</${node.name}>`]), | ||
])); | ||
hugContent | ||
? '' | ||
: options.svelteBracketNewLine && !isPreTagContent(path) | ||
? dedent(softline) | ||
: '', | ||
])), | ||
]; | ||
if (hugStart && hugEnd) { | ||
return groupConcat([ | ||
...openingTag, | ||
group(indent(concat([softline, groupConcat(['>', body(), `</${node.name}`])]))), | ||
isEmpty && options.svelteBracketNewLine ? '' : softline, | ||
'>', | ||
]); | ||
} | ||
if (hugStart) { | ||
return groupConcat([ | ||
...openingTag, | ||
group(indent(concat([softline, groupConcat(['>', body()])]))), | ||
softline, | ||
`</${node.name}>`, | ||
]); | ||
} | ||
if (hugEnd) { | ||
return groupConcat([ | ||
...openingTag, | ||
'>', | ||
group(indent(concat([softline, groupConcat([body(), `</${node.name}`])]))), | ||
softline, | ||
'>', | ||
]); | ||
} | ||
if (isEmpty) { | ||
return groupConcat([...openingTag, '>', body(), `</${node.name}>`]); | ||
} | ||
// No hugging of content means it's either a block element and/or there's whitespace at the start/end | ||
let separatorStart = softline; | ||
let separatorEnd = softline; | ||
if (isPreTagContent(path)) { | ||
separatorStart = ''; | ||
separatorEnd = ''; | ||
} | ||
else { | ||
if (firstChild && firstChild.type === 'Text') { | ||
if (isTextNodeStartingWithLinebreak(firstChild) && firstChild !== lastChild) { | ||
separatorStart = hardline; | ||
separatorEnd = hardline; | ||
} | ||
else if (isInlineElement(path, node)) { | ||
separatorStart = line; | ||
} | ||
trimTextNodeLeft(firstChild); | ||
} | ||
if (lastChild && lastChild.type === 'Text') { | ||
if (isInlineElement(path, node)) { | ||
separatorEnd = line; | ||
} | ||
trimTextNodeRight(lastChild); | ||
} | ||
} | ||
return groupConcat([ | ||
...openingTag, | ||
'>', | ||
groupConcat([indent(concat([separatorStart, body()])), separatorEnd]), | ||
`</${node.name}>`, | ||
]); | ||
} | ||
case 'Options': | ||
throw new Error('Options tags should have been handled by prepareChildren'); | ||
case 'Body': | ||
return group(concat([ | ||
return groupConcat([ | ||
'<', | ||
node.name, | ||
indent(group(concat(path.map((childPath) => childPath.call(print), 'attributes')))), | ||
indent(groupConcat(path.map((childPath) => childPath.call(print), 'attributes'))), | ||
' />', | ||
])); | ||
]); | ||
case 'Identifier': | ||
@@ -611,6 +841,6 @@ return node.name; | ||
if (isOrCanBeConvertedToShorthand(node)) { | ||
if (options$$1.svelteStrictMode) { | ||
if (options.svelteStrictMode) { | ||
return concat([line, node.name, '="{', node.name, '}"']); | ||
} | ||
else if (options$$1.svelteAllowShorthand) { | ||
else if (options.svelteAllowShorthand) { | ||
return concat([line, '{', node.name, '}']); | ||
@@ -626,3 +856,3 @@ } | ||
} | ||
const quotes = !isLoneMustacheTag(node.value) || options$$1.svelteStrictMode; | ||
const quotes = !isLoneMustacheTag(node.value) || options.svelteStrictMode; | ||
const attrNodeValue = printAttributeNodeValue(path, print, quotes, node); | ||
@@ -638,9 +868,13 @@ if (quotes) { | ||
case 'MustacheTag': | ||
return concat(['{', printJS(path, print, 'expression'), '}']); | ||
return concat([ | ||
'{', | ||
printJS(path, print, isInsideQuotedAttribute(path, options), false, 'expression'), | ||
'}', | ||
]); | ||
case 'IfBlock': { | ||
const def = [ | ||
'{#if ', | ||
printJS(path, print, 'expression'), | ||
printSvelteBlockJS(path, print, 'expression'), | ||
'}', | ||
printIndentedWithNewlines(path, print), | ||
printSvelteBlockChildren(path, print, options), | ||
]; | ||
@@ -651,3 +885,3 @@ if (node.else) { | ||
def.push('{/if}'); | ||
return concat([group(concat(def)), breakParent]); | ||
return concat([groupConcat(def), breakParent]); | ||
} | ||
@@ -663,5 +897,5 @@ case 'ElseBlock': { | ||
'{:else if ', | ||
path.map((ifPath) => printJS(path, print, 'expression'), 'children')[0], | ||
path.map((ifPath) => printSvelteBlockJS(ifPath, print, 'expression'), 'children')[0], | ||
'}', | ||
path.map((ifPath) => printIndentedWithNewlines(ifPath, print), 'children')[0], | ||
path.map((ifPath) => printSvelteBlockChildren(ifPath, print, options), 'children')[0], | ||
]; | ||
@@ -671,5 +905,5 @@ if (ifNode.else) { | ||
} | ||
return group(concat(def)); | ||
return concat(def); | ||
} | ||
return group(concat(['{:else}', printIndentedWithNewlines(path, print)])); | ||
return concat(['{:else}', printSvelteBlockChildren(path, print, options)]); | ||
} | ||
@@ -679,5 +913,5 @@ case 'EachBlock': { | ||
'{#each ', | ||
printJS(path, print, 'expression'), | ||
printSvelteBlockJS(path, print, 'expression'), | ||
' as ', | ||
printJS(path, print, 'context'), | ||
printSvelteBlockJS(path, print, 'context'), | ||
]; | ||
@@ -688,5 +922,5 @@ if (node.index) { | ||
if (node.key) { | ||
def.push(' (', printJS(path, print, 'key'), ')'); | ||
def.push(' (', printSvelteBlockJS(path, print, 'key'), ')'); | ||
} | ||
def.push('}', printIndentedWithNewlines(path, print)); | ||
def.push('}', printSvelteBlockChildren(path, print, options)); | ||
if (node.else) { | ||
@@ -696,32 +930,32 @@ def.push(path.call(print, 'else')); | ||
def.push('{/each}'); | ||
return concat([group(concat(def)), breakParent]); | ||
return concat([groupConcat(def), breakParent]); | ||
} | ||
case 'AwaitBlock': { | ||
const hasPendingBlock = node.pending.children.some((n) => !isEmptyNode(n)); | ||
const hasThenBlock = node.then.children.some((n) => !isEmptyNode(n)); | ||
const hasCatchBlock = node.catch.children.some((n) => !isEmptyNode(n)); | ||
const hasPendingBlock = node.pending.children.some((n) => !isEmptyTextNode(n)); | ||
const hasThenBlock = node.then.children.some((n) => !isEmptyTextNode(n)); | ||
const hasCatchBlock = node.catch.children.some((n) => !isEmptyTextNode(n)); | ||
let block = []; | ||
if (!hasPendingBlock && hasThenBlock) { | ||
block.push(group(concat([ | ||
block.push(groupConcat([ | ||
'{#await ', | ||
printJS(path, print, 'expression'), | ||
printSvelteBlockJS(path, print, 'expression'), | ||
' then', | ||
expandNode(node.value), | ||
'}', | ||
])), indent(path.call(print, 'then'))); | ||
]), path.call(print, 'then')); | ||
} | ||
else { | ||
block.push(group(concat(['{#await ', printJS(path, print, 'expression'), '}']))); | ||
block.push(groupConcat(['{#await ', printSvelteBlockJS(path, print, 'expression'), '}'])); | ||
if (hasPendingBlock) { | ||
block.push(indent(path.call(print, 'pending'))); | ||
block.push(path.call(print, 'pending')); | ||
} | ||
if (hasThenBlock) { | ||
block.push(group(concat(['{:then', expandNode(node.value), '}'])), indent(path.call(print, 'then'))); | ||
block.push(groupConcat(['{:then', expandNode(node.value), '}']), path.call(print, 'then')); | ||
} | ||
} | ||
if (hasCatchBlock) { | ||
block.push(group(concat(['{:catch', expandNode(node.error), '}'])), indent(path.call(print, 'catch'))); | ||
block.push(groupConcat(['{:catch', expandNode(node.error), '}']), path.call(print, 'catch')); | ||
} | ||
block.push('{/await}'); | ||
return group(concat(block)); | ||
return groupConcat(block); | ||
} | ||
@@ -731,8 +965,8 @@ case 'KeyBlock': { | ||
'{#key ', | ||
printJS(path, print, 'expression'), | ||
printSvelteBlockJS(path, print, 'expression'), | ||
'}', | ||
printIndentedWithNewlines(path, print), | ||
printSvelteBlockChildren(path, print, options), | ||
]; | ||
def.push('{/key}'); | ||
return concat([group(concat(def)), breakParent]); | ||
return concat([groupConcat(def), breakParent]); | ||
} | ||
@@ -742,7 +976,3 @@ case 'ThenBlock': | ||
case 'CatchBlock': | ||
return concat([ | ||
softline, | ||
...trim(printChildren(path, print), isLine), | ||
dedent(softline), | ||
]); | ||
return printSvelteBlockChildren(path, print, options); | ||
case 'EventHandler': | ||
@@ -756,5 +986,3 @@ return concat([ | ||
: '', | ||
node.expression | ||
? concat(['=', open, printJS(path, print, 'expression'), close]) | ||
: '', | ||
node.expression ? concat(['=', ...printJsExpression()]) : '', | ||
]); | ||
@@ -768,3 +996,3 @@ case 'Binding': | ||
? '' | ||
: concat(['=', open, printJS(path, print, 'expression'), close]), | ||
: concat(['=', ...printJsExpression()]), | ||
]); | ||
@@ -778,3 +1006,3 @@ case 'Class': | ||
? '' | ||
: concat(['=', open, printJS(path, print, 'expression'), close]), | ||
: concat(['=', ...printJsExpression()]), | ||
]); | ||
@@ -790,3 +1018,3 @@ case 'Let': | ||
? '' | ||
: concat(['=', open, printJS(path, print, 'expression'), close]), | ||
: concat(['=', ...printJsExpression()]), | ||
]); | ||
@@ -817,7 +1045,3 @@ case 'DebugTag': | ||
} | ||
let text = node.data; | ||
if (hasSnippedContent(text)) { | ||
text = unsnipContent(text); | ||
} | ||
return group(concat(['<!--', text, '-->'])); | ||
return printComment(node); | ||
} | ||
@@ -834,5 +1058,3 @@ case 'Transition': | ||
: '', | ||
node.expression | ||
? concat(['=', open, printJS(path, print, 'expression'), close]) | ||
: '', | ||
node.expression ? concat(['=', ...printJsExpression()]) : '', | ||
]); | ||
@@ -844,5 +1066,3 @@ case 'Action': | ||
node.name, | ||
node.expression | ||
? concat(['=', open, printJS(path, print, 'expression'), close]) | ||
: '', | ||
node.expression ? concat(['=', ...printJsExpression()]) : '', | ||
]); | ||
@@ -854,10 +1074,8 @@ case 'Animation': | ||
node.name, | ||
node.expression | ||
? concat(['=', open, printJS(path, print, 'expression'), close]) | ||
: '', | ||
node.expression ? concat(['=', ...printJsExpression()]) : '', | ||
]); | ||
case 'RawMustacheTag': | ||
return concat(['{@html ', printJS(path, print, 'expression'), '}']); | ||
return concat(['{@html ', printJS(path, print, false, false, 'expression'), '}']); | ||
case 'Spread': | ||
return concat([line, '{...', printJS(path, print, 'expression'), '}']); | ||
return concat([line, '{...', printJS(path, print, false, false, 'expression'), '}']); | ||
} | ||
@@ -867,2 +1085,41 @@ console.error(JSON.stringify(node, null, 4)); | ||
} | ||
function printTopLevelParts(n, options, path, print) { | ||
const parts = { | ||
options: [], | ||
scripts: [], | ||
markup: [], | ||
styles: [], | ||
}; | ||
// scripts | ||
if (n.module) { | ||
n.module.type = 'Script'; | ||
n.module.attributes = extractAttributes(getText(n.module, options)); | ||
parts.scripts.push(path.call(print, 'module')); | ||
} | ||
if (n.instance) { | ||
n.instance.type = 'Script'; | ||
n.instance.attributes = extractAttributes(getText(n.instance, options)); | ||
parts.scripts.push(path.call(print, 'instance')); | ||
} | ||
// styles | ||
if (n.css) { | ||
n.css.type = 'Style'; | ||
n.css.content.type = 'StyleProgram'; | ||
parts.styles.push(path.call(print, 'css')); | ||
} | ||
// markup | ||
const htmlDoc = path.call(print, 'html'); | ||
if (htmlDoc) { | ||
parts.markup.push(htmlDoc); | ||
} | ||
if (svelteOptionsDoc) { | ||
parts.options.push(svelteOptionsDoc); | ||
} | ||
const docs = flatten(parseSortOrder(options.svelteSortOrder).map((p) => parts[p])); | ||
// Need to reset these because they are global and could affect the next formatting run | ||
ignoreNext = false; | ||
svelteOptionsDoc = undefined; | ||
svelteOptionsComment = undefined; | ||
return groupConcat([join(hardline, docs)]); | ||
} | ||
function printAttributeNodeValue(path, print, quotes, node) { | ||
@@ -874,148 +1131,250 @@ const valueDocs = path.map((childPath) => childPath.call(print), 'value'); | ||
else { | ||
return indent(group(concat(trim(valueDocs, isLine)))); | ||
return indent(groupConcat(trim(valueDocs, isLine))); | ||
} | ||
} | ||
function printSvelteBlockChildren(path, print, options) { | ||
const node = path.getValue(); | ||
const children = node.children; | ||
if (!children || children.length === 0) { | ||
return ''; | ||
} | ||
const whitespaceAtStartOfBlock = checkWhitespaceAtStartOfSvelteBlock(node, options); | ||
const whitespaceAtEndOfBlock = checkWhitespaceAtEndOfSvelteBlock(node, options); | ||
const startline = whitespaceAtStartOfBlock === 'none' | ||
? '' | ||
: whitespaceAtEndOfBlock === 'line' || whitespaceAtStartOfBlock === 'line' | ||
? hardline | ||
: line; | ||
const endline = whitespaceAtEndOfBlock === 'none' | ||
? '' | ||
: whitespaceAtEndOfBlock === 'line' || whitespaceAtStartOfBlock === 'line' | ||
? hardline | ||
: line; | ||
const firstChild = children[0]; | ||
const lastChild = children[children.length - 1]; | ||
if (isTextNodeStartingWithWhitespace(firstChild)) { | ||
trimTextNodeLeft(firstChild); | ||
} | ||
if (isTextNodeEndingWithWhitespace(lastChild)) { | ||
trimTextNodeRight(lastChild); | ||
} | ||
return concat([indent(concat([startline, group(printChildren(path, print))])), endline]); | ||
} | ||
function printChildren(path, print) { | ||
let childDocs = []; | ||
let currentGroup = []; | ||
// the index of the last child doc we could add a linebreak after | ||
let lastBreakIndex = -1; | ||
const isPreformat = isPreTagContent(path); | ||
if (isPreTagContent(path)) { | ||
return concat(path.map(print, 'children')); | ||
} | ||
const childNodes = prepareChildren(path.getValue().children, path, print); | ||
// modifiy original array because it's accessed later through map(print, 'children', idx) | ||
path.getValue().children = childNodes; | ||
if (childNodes.length === 0) { | ||
return ''; | ||
} | ||
const childDocs = []; | ||
let handleWhitespaceOfPrevTextNode = false; | ||
for (let i = 0; i < childNodes.length; i++) { | ||
const childNode = childNodes[i]; | ||
if (childNode.type === 'Text') { | ||
handleTextChild(i, childNode); | ||
} | ||
else if (isBlockElement(childNode)) { | ||
handleBlockChild(i); | ||
} | ||
else if (isInlineElement(path, childNode)) { | ||
handleInlineChild(i); | ||
} | ||
else { | ||
childDocs.push(printChild(i)); | ||
handleWhitespaceOfPrevTextNode = false; | ||
} | ||
} | ||
// If there's at least one block element and more than one node, break content | ||
const forceBreakContent = childNodes.length > 1 && childNodes.some((child) => isBlockElement(child)); | ||
if (forceBreakContent) { | ||
childDocs.push(breakParent); | ||
} | ||
return concat(childDocs); | ||
function printChild(idx) { | ||
return path.call(print, 'children', idx); | ||
} | ||
/** | ||
* Call when reaching a point where a linebreak is possible. Will | ||
* put all `childDocs` since the last possible linebreak position | ||
* into a `concat` to avoid them breaking. | ||
* Print inline child. Hug whitespace of previous text child if there was one. | ||
*/ | ||
function linebreakPossible() { | ||
if (lastBreakIndex >= 0 && lastBreakIndex < childDocs.length - 1) { | ||
childDocs = childDocs | ||
.slice(0, lastBreakIndex) | ||
.concat(concat(childDocs.slice(lastBreakIndex))); | ||
function handleInlineChild(idx) { | ||
if (handleWhitespaceOfPrevTextNode) { | ||
childDocs.push(groupConcat([line, printChild(idx)])); | ||
} | ||
lastBreakIndex = -1; | ||
else { | ||
childDocs.push(printChild(idx)); | ||
} | ||
handleWhitespaceOfPrevTextNode = false; | ||
} | ||
/** | ||
* Add a document to the output. | ||
* @param childDoc undefined means do not add anything but allow for the possibility of a linebreak here. | ||
* @param fromNode the Node the doc was generated from. undefined if childDoc is undefined. | ||
* Print block element. Add softlines around it if needed | ||
* so it breaks into a separate line if children are broken up. | ||
* Don't add lines at the start/end if it's the first/last child because this | ||
* kind of whitespace handling is done in the parent already. | ||
*/ | ||
function outputChildDoc(childDoc, fromNode) { | ||
if (!isPreformat) { | ||
if (!childDoc || !fromNode || canBreakBefore(fromNode)) { | ||
linebreakPossible(); | ||
const lastChild = childDocs[childDocs.length - 1]; | ||
// separate children by softlines, but not if the children are already lines. | ||
// one exception: allow for a line break before "keepIfLonely" lines because they represent an empty line | ||
if (childDoc != null && | ||
!isLineDiscardedIfLonely(childDoc) && | ||
lastChild != null && | ||
!isLine(lastChild)) { | ||
childDocs.push(softline); | ||
} | ||
} | ||
if (lastBreakIndex < 0 && childDoc && fromNode && !canBreakAfter(fromNode)) { | ||
lastBreakIndex = childDocs.length; | ||
} | ||
function handleBlockChild(idx) { | ||
const prevChild = childNodes[idx - 1]; | ||
if (prevChild && | ||
!isBlockElement(prevChild) && | ||
(prevChild.type !== 'Text' || | ||
handleWhitespaceOfPrevTextNode || | ||
!isTextNodeEndingWithWhitespace(prevChild))) { | ||
childDocs.push(softline); | ||
} | ||
if (childDoc) { | ||
childDocs.push(childDoc); | ||
childDocs.push(printChild(idx)); | ||
const nextChild = childNodes[idx + 1]; | ||
if (nextChild && | ||
(nextChild.type !== 'Text' || | ||
// Only handle text which starts with a whitespace and has text afterwards, | ||
// or is empty but followed by an inline element. The latter is done | ||
// so that if the children break, the inline element afterwards is in a seperate line. | ||
((!isEmptyTextNode(nextChild) || | ||
(childNodes[idx + 2] && isInlineElement(path, childNodes[idx + 2]))) && | ||
!isTextNodeStartingWithLinebreak(nextChild)))) { | ||
childDocs.push(softline); | ||
} | ||
handleWhitespaceOfPrevTextNode = false; | ||
} | ||
function lastChildDocProduced() { | ||
// line breaks are ok after last child | ||
outputChildDoc(); | ||
} | ||
/** | ||
* Sequences of inline nodes (currently, `TextNode`s and `MustacheTag`s) are collected into | ||
* groups and printed as a single `Fill` doc so that linebreaks as a result of sibling block | ||
* nodes (currently, all HTML elements) don't cause those inline sequences to break | ||
* prematurely. This is particularly important for whitespace sensitivity, as it is often | ||
* desired to have text directly wrapping a mustache tag without additional whitespace. | ||
* Print text child. First/last child white space handling | ||
* is done in parent already. By defintion of the Svelte AST, | ||
* a text node always is inbetween other tags. Add hardlines | ||
* if the users wants to have them inbetween. | ||
* If the text is trimmed right, toggle flag telling | ||
* subsequent (inline)block element to alter its printing logic | ||
* to check if they need to hug or print lines themselves. | ||
*/ | ||
function flush() { | ||
for (let { doc, node } of currentGroup) { | ||
for (const childDoc of extractOutermostNewlines(doc)) { | ||
outputChildDoc(childDoc, node); | ||
function handleTextChild(idx, childNode) { | ||
handleWhitespaceOfPrevTextNode = false; | ||
if (idx === 0 || idx === childNodes.length - 1) { | ||
childDocs.push(printChild(idx)); | ||
return; | ||
} | ||
const prevNode = childNodes[idx - 1]; | ||
const nextNode = childNodes[idx + 1]; | ||
if (isTextNodeStartingWithWhitespace(childNode) && | ||
// If node is empty, go straight through to checking the right end | ||
!isEmptyTextNode(childNode)) { | ||
if (isInlineElement(path, prevNode) && !isTextNodeStartingWithLinebreak(childNode)) { | ||
trimTextNodeLeft(childNode); | ||
const lastChildDoc = childDocs.pop(); | ||
childDocs.push(groupConcat([lastChildDoc, line])); | ||
} | ||
if (isBlockElement(prevNode) && !isTextNodeStartingWithLinebreak(childNode)) { | ||
trimTextNodeLeft(childNode); | ||
} | ||
} | ||
currentGroup = []; | ||
} | ||
path.each((childPath) => { | ||
const childNode = childPath.getValue(); | ||
const childDoc = childPath.call(print); | ||
if (isInlineNode(childNode)) { | ||
currentGroup.push({ doc: childDoc, node: childNode }); | ||
} | ||
else { | ||
flush(); | ||
if (childDoc !== '') { | ||
outputChildDoc(isLine(childDoc) ? childDoc : concat([breakParent, childDoc]), childNode); | ||
if (isTextNodeEndingWithWhitespace(childNode)) { | ||
if (isInlineElement(path, nextNode) && !isTextNodeEndingWithLinebreak(childNode)) { | ||
handleWhitespaceOfPrevTextNode = !prevNode || !isBlockElement(prevNode); | ||
trimTextNodeRight(childNode); | ||
} | ||
if (isBlockElement(nextNode) && !isTextNodeEndingWithLinebreak(childNode, 2)) { | ||
handleWhitespaceOfPrevTextNode = !prevNode || !isBlockElement(prevNode); | ||
trimTextNodeRight(childNode); | ||
} | ||
} | ||
}, 'children'); | ||
flush(); | ||
lastChildDocProduced(); | ||
return childDocs; | ||
childDocs.push(printChild(idx)); | ||
} | ||
} | ||
/** | ||
* Print the nodes in `path` indented and with leading and trailing newlines. | ||
* `svelte:options` is part of the html part but needs to be snipped out and handled | ||
* seperately to reorder it as configured. The comment above it should be moved with it. | ||
* Do that here. | ||
*/ | ||
function printIndentedWithNewlines(path, print) { | ||
return indent(concat([softline, ...trim(printChildren(path, print), isLine), dedent(softline)])); | ||
function prepareChildren(children, path, print) { | ||
const childrenWithoutOptions = []; | ||
for (let idx = 0; idx < children.length; idx++) { | ||
const currentChild = children[idx]; | ||
if (currentChild.type === 'Text' && getUnencodedText(currentChild) === '') { | ||
continue; | ||
} | ||
if (isCommentFollowedByOptions(currentChild, idx)) { | ||
svelteOptionsComment = printComment(currentChild); | ||
const nextChild = children[idx + 1]; | ||
idx += nextChild && isEmptyTextNode(nextChild) ? 1 : 0; | ||
continue; | ||
} | ||
if (currentChild.type === 'Options') { | ||
printSvelteOptions(currentChild, idx, path, print); | ||
continue; | ||
} | ||
childrenWithoutOptions.push(currentChild); | ||
} | ||
const mergedChildrenWithoutOptions = []; | ||
for (let idx = 0; idx < childrenWithoutOptions.length; idx++) { | ||
const currentChild = childrenWithoutOptions[idx]; | ||
const nextChild = childrenWithoutOptions[idx + 1]; | ||
if (currentChild.type === 'Text' && nextChild && nextChild.type === 'Text') { | ||
// A tag was snipped out (f.e. svelte:options). Join text | ||
currentChild.raw += nextChild.raw; | ||
currentChild.data += nextChild.data; | ||
idx++; | ||
} | ||
mergedChildrenWithoutOptions.push(currentChild); | ||
} | ||
return mergedChildrenWithoutOptions; | ||
function printSvelteOptions(node, idx, path, print) { | ||
svelteOptionsDoc = groupConcat([ | ||
groupConcat([ | ||
'<', | ||
node.name, | ||
indent(groupConcat(path.map(print, 'children', idx, 'attributes'))), | ||
' />', | ||
]), | ||
hardline, | ||
]); | ||
if (svelteOptionsComment) { | ||
svelteOptionsDoc = groupConcat([svelteOptionsComment, hardline, svelteOptionsDoc]); | ||
} | ||
} | ||
function isCommentFollowedByOptions(node, idx) { | ||
if (node.type !== 'Comment') { | ||
return false; | ||
} | ||
const nextChild = children[idx + 1]; | ||
if (nextChild) { | ||
if (isEmptyTextNode(nextChild)) { | ||
const afterNext = children[idx + 2]; | ||
return afterNext && afterNext.type === 'Options'; | ||
} | ||
return nextChild.type === 'Options'; | ||
} | ||
return false; | ||
} | ||
} | ||
/** | ||
* Print the nodes in `path` indented but without adding any leading or trailing newlines. | ||
*/ | ||
function printIndentedPreservingWhitespace(path, print) { | ||
return indent(concat(dedentFinalNewline(printChildren(path, print)))); | ||
} | ||
/** | ||
* Split the text into words separated by whitespace. Replace the whitespaces by lines, | ||
* collapsing multiple whitespaces into a single line. | ||
* | ||
* If the text starts or ends with multiple newlines, those newlines should be "keepIfLonely" | ||
* since we want double newlines in the output. | ||
* If the text starts or ends with multiple newlines, two of those should be kept. | ||
*/ | ||
function splitTextToDocs(text) { | ||
function splitTextToDocs(node) { | ||
const text = getUnencodedText(node); | ||
let docs = text.split(/[\t\n\f\r ]+/); | ||
docs = join(line, docs).parts.filter((s) => s !== ''); | ||
// if the text starts with two newlines, the first doc is already a newline. make it "keepIfLonely" | ||
if (text.match(/^([\t\f\r ]*\n){2}/)) { | ||
docs[0] = keepIfLonelyLine; | ||
if (startsWithLinebreak(text)) { | ||
docs[0] = hardline; | ||
} | ||
// if the text ends with two newlines, the last doc is already a newline. make it "keepIfLonely" | ||
if (text.match(/(\n[\t\f\r ]*){2}$/)) { | ||
docs[docs.length - 1] = keepIfLonelyLine; | ||
if (startsWithLinebreak(text, 2)) { | ||
docs = [hardline, ...docs]; | ||
} | ||
return docs; | ||
} | ||
/** | ||
* If there is a trailing newline, pull it out and put it inside a `dedent`. This is used | ||
* when we want to preserve whitespace, but still indent the newline if there is one | ||
* (e.g. for `<b>1\n</b>` the `</b>` will be on its own line; for `<b>1</b>` it can't | ||
* because it would introduce new whitespace) | ||
*/ | ||
function dedentFinalNewline(docs) { | ||
const trimmedRight = trimRight(docs, isLine); | ||
if (trimmedRight) { | ||
return [...docs, dedent(trimmedRight[trimmedRight.length - 1])]; | ||
if (endsWithLinebreak(text)) { | ||
docs[docs.length - 1] = hardline; | ||
} | ||
else { | ||
return docs; | ||
if (endsWithLinebreak(text, 2)) { | ||
docs = [...docs, hardline]; | ||
} | ||
return docs; | ||
} | ||
/** | ||
* Pull out any nested leading or trailing lines and put them at the top level. | ||
*/ | ||
function extractOutermostNewlines(doc) { | ||
const leadingLines = trimLeft([doc], isLine) || []; | ||
const trailingLines = trimRight([doc], isLine) || []; | ||
return [...leadingLines, ...(!isEmptyDoc(doc) ? [doc] : []), ...trailingLines]; | ||
function printSvelteBlockJS(path, print, name) { | ||
return printJS(path, print, false, true, name); | ||
} | ||
function printJS(path, print, name) { | ||
if (!name) { | ||
path.getValue().isJS = true; | ||
return path.call(print); | ||
} | ||
function printJS(path, print, forceSingleQuote, forceSingleLine, name) { | ||
path.getValue()[name].isJS = true; | ||
path.getValue()[name].forceSingleQuote = forceSingleQuote; | ||
path.getValue()[name].forceSingleLine = forceSingleLine; | ||
return path.call(print, name); | ||
@@ -1058,2 +1417,9 @@ } | ||
} | ||
function printComment(node) { | ||
let text = node.data; | ||
if (hasSnippedContent(text)) { | ||
text = unsnipContent(text); | ||
} | ||
return groupConcat(['<!--', text, '-->']); | ||
} | ||
@@ -1065,6 +1431,10 @@ const { builders: { concat: concat$1, hardline: hardline$1, group: group$1, indent: indent$1, literalline: literalline$1 }, utils: { removeLines }, } = prettier.doc; | ||
try { | ||
return removeLines(textToDoc(forceIntoExpression(getText(node, options)), { | ||
const embeddedOptions = { | ||
parser: expressionParser, | ||
singleQuote: true, | ||
})); | ||
}; | ||
if (node.forceSingleQuote) { | ||
embeddedOptions.singleQuote = true; | ||
} | ||
const docs = textToDoc(forceIntoExpression(getText(node, options)), embeddedOptions); | ||
return node.forceSingleLine ? removeLines(docs) : docs; | ||
} | ||
@@ -1101,3 +1471,3 @@ catch (e) { | ||
const ast = parsers.babel(text, parsers, options); | ||
return Object.assign({}, ast, { program: ast.program.body[0].expression }); | ||
return Object.assign(Object.assign({}, ast), { program: ast.program.body[0].expression }); | ||
} | ||
@@ -1181,3 +1551,5 @@ function skipBlank(docs) { | ||
? formatBodyContent(content) | ||
: hardline$1 | ||
: content === '' | ||
? '' | ||
: hardline$1 | ||
: preformattedBody(content); | ||
@@ -1220,7 +1592,7 @@ const attributes = concat$1(path.map((childPath) => childPath.getNode().name !== snippedTagContentAttribute | ||
try { | ||
return Object.assign({}, require(`svelte/compiler`).parse(text), { __isRoot: true }); | ||
return Object.assign(Object.assign({}, require(`svelte/compiler`).parse(text)), { __isRoot: true }); | ||
} | ||
catch (err) { | ||
if (err.start != null && err.end != null) { | ||
// Prettier expects error objects to have loc.start and loc.end fields. | ||
// Prettier expects error objects to have loc.start and loc.end fields. | ||
// Svelte uses start and end directly on the error. | ||
@@ -1235,6 +1607,13 @@ err.loc = { | ||
}, | ||
preprocess: (text) => { | ||
preprocess: (text, options) => { | ||
text = snipTagContent('style', text); | ||
text = snipTagContent('script', text, '{}'); | ||
return text.trim(); | ||
text = text.trim(); | ||
// Prettier sets the preprocessed text as the originalText in case | ||
// the Svelte formatter is called directly. In case it's called | ||
// as an embedded parser (for example when there's a Svelte code block | ||
// inside markdown), the originalText is not updated after preprocessing. | ||
// Therefore we do it ourselves here. | ||
options.originalText = text; | ||
return text; | ||
}, | ||
@@ -1254,5 +1633,5 @@ locStart, | ||
exports.languages = languages; | ||
exports.options = options; | ||
exports.parsers = parsers; | ||
exports.printers = printers; | ||
exports.options = options; | ||
//# sourceMappingURL=plugin.js.map |
@@ -24,4 +24,4 @@ # Prettier for Svelte 3 components | ||
- **`svelteSortOrder`** | ||
- Default: `scripts-styles-markup` | ||
- Sort order for scripts, styles, and markup. | ||
- Default: `options-scripts-markup-styles` | ||
- Sort order for svelte:options, scripts, markup, and styles. | ||
@@ -37,3 +37,3 @@ - **`svelteStrictMode`** | ||
- **`svelteBracketNewLine`** | ||
- Default: `false` | ||
- Default: `true` | ||
- Put the `>` of a multiline element on a new line (svelte equivalent of [jsxBracketSameLine](https://prettier.io/docs/en/options.html#jsx-brackets) rule) | ||
@@ -64,5 +64,5 @@ | ||
{ | ||
"svelteSortOrder" : "styles-scripts-markup", | ||
"svelteSortOrder" : "options-styles-scripts-markup", | ||
"svelteStrictMode": true, | ||
"svelteBracketNewLine": true, | ||
"svelteBracketNewLine": false, | ||
"svelteAllowShorthand": false, | ||
@@ -92,3 +92,3 @@ "svelteIndentScriptAndStyle": false | ||
**`svelte-sort-order`** Sort order for scripts, styles, and markup. Defaults to `scripts-styles-markup`. | ||
**`svelte-sort-order`** Sort order for svelte:options, scripts, styles, and markup. Defaults to `options-scripts-styles-markup`. | ||
@@ -95,0 +95,0 @@ ``` |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
190016
1582