Socket
Socket
Sign inDemoInstall

prettier-plugin-astro

Package Overview
Dependencies
Maintainers
1
Versions
46
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

prettier-plugin-astro - npm Package Compare versions

Comparing version 0.0.8 to 0.0.9

13

CHANGELOG.md
# prettier-plugin-astro
## 0.0.9
### Patch Changes
- 8820423: Fix test macro 'PrettierMarkdown'
- a30ddcd: Bump @astrojs/parser from 0.15.0 to 0.20.2
- 695fc07: Add formatting for <Markdown> components
- 1bf9f7c: Support arbitrary attributes in style tags
- 395b3bd: Add basic support for indented sass
- 672afef: Add new line at the end of the file
- 20a298e: Add support for .sass formatting
- 915a6e2: Add support for prettier-ignore comments
## 0.0.8

@@ -4,0 +17,0 @@

19

package.json
{
"name": "prettier-plugin-astro",
"version": "0.0.8",
"version": "0.0.9",
"description": "A Prettier Plugin for formatting Astro files",
"main": "src/index.js",
"files": [
"src/**"
],
"type": "commonjs",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0",
"npm": ">=6.14.0"
},
"homepage": "https://github.com/snowpackjs/prettier-plugin-astro/",

@@ -30,13 +37,15 @@ "issues": {

"dependencies": {
"@astrojs/parser": "^0.15.0",
"prettier": "^2.4.1"
"@astrojs/parser": "^0.20.2",
"prettier": "^2.4.1",
"sass-formatter": "^0.7.2"
},
"devDependencies": {
"@changesets/cli": "^2.16.0",
"@types/prettier": "^2.2.1",
"@types/prettier": "^2.4.1",
"ava": "^4.0.0-alpha.2",
"eslint": "^7.29.0",
"eslint": "^8.0.0",
"eslint-plugin-ava": "^13.0.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier-doc": "^1.0.1"
}
}

@@ -25,2 +25,3 @@ # Beta Prettier Plugin for [Astro](https://github.com/snowpackjs/astro) -- 🚧 Caution! May break your project 🚧

1. Run `yarn changeset` to add your changes to the changelog on version bump.
Most changes to the plugin should be `patch` changes while we're before `1.0.0`.

@@ -27,0 +28,0 @@ ## Resources for contributing

const {
builders: { join, fill, line, literalline, hardline, softline, group, conditionalGroup, breakParent, indent, dedent },
utils: { removeLines },
builders: { breakParent, dedent, fill, group, hardline, indent, join, line, literalline, softline },
utils: { removeLines, stripTrailingHardline },
} = require('prettier/doc');
const { SassFormatter } = require('sass-formatter');
const { parseSortOrder } = require('./options');
const {
attachCommentsHTML,
canOmitSoftlineBeforeClosingTag,
dedent: manualDedent,
endsWithLinebreak,
forceIntoExpression,
formattableAttributes,
getMarkdownName,
getText,
getUnencodedText,
indent: manualIndent,
isASTNode,
isEmptyDoc,
isEmptyTextNode,
isPreTagContent,
isInlineElement,
isEmptyDoc,
isTextNodeStartingWithWhitespace,
isTextNodeStartingWithLinebreak,
isTextNodeEndingWithWhitespace,
trimTextNodeLeft,
trimTextNodeRight,
isLine,
isLoneMustacheTag,
isAttributeShorthand,
isNodeWithChildren,
isOrCanBeConvertedToShorthand,
isPreTagContent,
isTextNodeEndingWithWhitespace,
isTextNodeStartingWithLinebreak,
isTextNodeStartingWithWhitespace,
printRaw,
replaceEndOfLineWith,
selfClosingTags,
formattableAttributes,
getUnencodedText,
replaceEndOfLineWith,
forceIntoExpression,
shouldHugEnd,
shouldHugStart,
shouldHugEnd,
canOmitSoftlineBeforeClosingTag,
startsWithLinebreak,
endsWithLinebreak,
printRaw,
trim,
isLine,
trimChildren,
flatten,
getText,
trimTextNodeLeft,
trimTextNodeRight,
} = require('./utils');
const supportedStyleLangValues = ['css', 'scss'];
/**

@@ -48,26 +50,38 @@ *

*/
const printTopLevelParts = (node, path, opts, print) => {
const parts = {
frontmatter: [],
markup: [],
styles: [],
};
function printTopLevelParts(node, path, opts, print) {
let docs = [];
const normalize = (doc) => [stripTrailingHardline(doc), hardline];
// frontmatter always comes first
if (node.module) {
parts.frontmatter.push(path.call(print, 'module'));
const subDoc = normalize(path.call(print, 'module'));
docs.push(subDoc);
}
if (node.css) {
parts.styles.push(path.call(print, 'css'));
// markup and styles follow, whichever the user prefers (default: markup, styles)
for (const section of parseSortOrder(opts.astroSortOrder)) {
switch (section) {
case 'markup': {
const subDoc = path.call(print, 'html');
if (!isEmptyDoc(subDoc)) docs.push(normalize(subDoc));
break;
}
case 'styles': {
const subDoc = path.call(print, 'css');
if (!isEmptyDoc(subDoc)) docs.push(normalize(subDoc));
break;
}
}
}
if (node.html) {
parts.markup.push(path.call(print, 'html'));
}
// remove trailing softline, if any
const lastDoc = docs[docs.length - 1];
const lastItem = lastDoc[lastDoc.length - 1];
if (lastItem.type === 'line') docs[docs.length - 1].pop();
const docs = flatten([parts.frontmatter, ...parseSortOrder(opts.astroSortOrder).map((p) => parts[p])]).filter((doc) => '' !== doc);
return group([join(hardline, docs)]);
};
return join(softline, docs);
}
const printAttributeNodeValue = (path, print, quotes, node) => {
function printAttributeNodeValue(path, print, quotes, node) {
const valueDocs = path.map((childPath) => childPath.call(print), 'value');

@@ -80,3 +94,3 @@

}
};
}

@@ -90,6 +104,14 @@ function printJS(path, print, name, { forceSingleQuote, forceSingleLine }) {

/** @type {import('prettier').Printer['printComment']} */
function printComment(commentPath) {
// note(drew): this isn’t doing anything currently, but Prettier requires it anyway
return commentPath;
}
/** @type {import('prettier').Printer['print']} */
const print = (path, opts, print) => {
function print(path, opts, print) {
const node = path.getValue();
const isMarkdownSubDoc = opts.parentParser === 'markdown'; // is this a code block within .md?
// 1. handle special node types
switch (true) {

@@ -106,2 +128,8 @@ case !node:

// 2. attach comments shallowly to children, if any (https://prettier.io/docs/en/plugins.html#manually-attaching-a-comment)
if (!isPreTagContent(path) && !isMarkdownSubDoc) {
attachCommentsHTML(node);
}
// 3. handle printing
switch (node.type) {

@@ -113,2 +141,10 @@ case 'Fragment': {

}
if (!isNodeWithChildren(node) || node.children.every(isEmptyTextNode)) return '';
// If this is the start of a markdown code block, remove arbitrary beginning whitespace
if (isMarkdownSubDoc) {
if (isEmptyTextNode(node.children[0])) node.children.shift();
}
// If we don't see any JSX expressions, this is just embedded HTML

@@ -119,11 +155,5 @@ // and we can skip a bunch of work. Hooray!

node.__isRawHTML = true;
node.content = text;
return path.call(print);
return path.map(print, 'children');
}
const children = node.children;
if (children.length === 0 || children.every(isEmptyTextNode)) {
return '';
}
if (!isPreTagContent(path)) {

@@ -148,29 +178,6 @@ trimChildren(node.children, path);

}
case 'Text':
if (!isPreTagContent(path)) {
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 [hardline, hardline];
}
if (hasOneOrMoreNewlines) {
return hardline;
}
if (hasWhiteSpace) {
return line;
}
return '';
}
case 'Text': {
const rawText = getUnencodedText(node);
/**
* For non-empty text nodes each sequence of non-whitespace characters (effectively,
* each "word") is joined by a single `line`, which will be rendered as a single space
* until this node's current line is out of room, at which `fill` will break at the
* most convenient instance of `line`.
*/
return fill(splitTextToDocs(node));
} else {
const rawText = getUnencodedText(node);
if (isPreTagContent(path)) {
if (path.getParentNode().type === 'Attribute') {

@@ -183,2 +190,28 @@ // Direct child of attribute value -> add literallines at end of lines

}
if (isEmptyTextNode(node)) {
const hasWhiteSpace = rawText.trim().length < getUnencodedText(node).length;
const hasOneOrMoreNewlines = /\n/.test(getUnencodedText(node));
const hasTwoOrMoreNewlines = /\n\r?\s*\n\r?/.test(getUnencodedText(node));
if (hasTwoOrMoreNewlines) {
return [hardline, hardline];
}
if (hasOneOrMoreNewlines) {
return hardline;
}
if (hasWhiteSpace) {
return line;
}
return '';
}
/**
* For non-empty text nodes each sequence of non-whitespace characters (effectively,
* each "word") is joined by a single `line`, which will be rendered as a single space
* until this node's current line is out of room, at which `fill` will break at the
* most convenient instance of `line`.
*/
return fill(splitTextToDocs(node));
}
case 'Element':

@@ -193,3 +226,3 @@ case 'InlineComponent':

const isSelfClosingTag = isEmpty && (node.type !== 'Element' || selfClosingTags.indexOf(node.name) !== -1);
const attributes = path.map((childPath) => childPath.call(print), 'attributes');
const attributes = path.map(print, 'attributes');

@@ -201,3 +234,3 @@ if (isSelfClosingTag) {

if (node.name.toLowerCase() === '!doctype') {
attributesWithLowercaseHTML = attributes.map((attribute) => {
const attributesWithLowercaseHTML = attributes.map((attribute) => {
if (attribute[0].type === 'line' && attribute[1].toLowerCase() === 'html') {

@@ -220,7 +253,13 @@ attribute[1] = attribute[1].toLowerCase();

const hugStart = shouldHugStart(node, opts);
const hugEnd = shouldHugEnd(node, opts);
// No hugging of content means it's either a block element and/or there's whitespace at the start/end
let noHugSeparatorStart = softline;
let noHugSeparatorEnd = softline;
let hugStart = shouldHugStart(node, opts);
let hugEnd = shouldHugEnd(node, opts);
let body;
const isMarkdownComponent =
node.type === 'InlineComponent' && opts.__astro && opts.__astro.markdownName && opts.__astro.markdownName.has(node.name) && isNodeWithChildren(node);
if (isEmpty) {

@@ -231,2 +270,11 @@ body =

: () => (opts.jsxBracketNewLine ? '' : softline);
} else if (isMarkdownComponent) {
// collapse children into raw Markdown text
const text = node.children.map(getUnencodedText).join('').trim();
node.children = [{ start: firstChild.start, end: lastChild.end - 2, type: 'Text', data: text, raw: text, __isRawMarkdown: true }];
body = () => path.map(print, 'children');
// set hugEnd
hugStart = false;
hugEnd = false;
} else if (isPreTagContent(path)) {

@@ -248,8 +296,8 @@ body = () => printRaw(node, opts.originalText);

// No hugging of content means it's either a block element and/or there's whitespace at the start/end
let noHugSeparatorStart = softline;
let noHugSeparatorEnd = softline;
if (isPreTagContent(path)) {
noHugSeparatorStart = '';
noHugSeparatorEnd = '';
} else if (isMarkdownComponent) {
noHugSeparatorStart = softline;
noHugSeparatorEnd = softline;
} else {

@@ -338,9 +386,12 @@ let didSetEndSeparator = false;

case 'Comment':
return [`<!--`, getUnencodedText(node), `-->`];
return ['<!--', getUnencodedText(node), '-->'];
case 'CodeSpan':
return getUnencodedText(node);
case 'CodeFence':
case 'CodeFence': {
console.debug(node);
return getUnencodedText(node);
// We should use `node.metadata` to select a parser to embed with... something like return [node.metadata, hardline textToDoc(node.getMetadataLanguage()), hardline, `\`\`\``];
// const lang = node.metadata.slice(3);
return [node.metadata, hardline, /** somehow call textToDoc(lang), */ node.data, hardline, '```', hardline];
// We should use `node.metadata` to select a parser to embed with... something like return [node.metadata, hardline textToDoc(node.getMetadataLanguage()), hardline, `\`\`\``];
}
default: {

@@ -350,3 +401,3 @@ throw new Error(`Unhandled node type "${node.type}"!`);

}
};
}

@@ -389,3 +440,5 @@ /**

/** @type {import('prettier').Printer['embed']} */
const embed = (path, print, textToDoc, opts) => {
function embed(path, print, textToDoc, opts) {
if (!opts.__astro) opts.__astro = {};
const node = path.getValue();

@@ -395,2 +448,11 @@

if (node.__isRawMarkdown) {
const docs = textToDoc(getUnencodedText(node), { ...opts, parser: 'markdown' });
return stripTrailingHardline(docs);
}
if (node.type === 'Script' && !opts.__astro.markdownName) {
opts.__astro.markdownName = getMarkdownName(node.content);
}
if (node.isJS) {

@@ -420,3 +482,3 @@ try {

if (parent.type === 'Element' && parent.name === 'script') {
const [formatttedScript, _] = textToDoc(node.data, { ...opts, parser: 'typescript' });
const [formatttedScript, ,] = textToDoc(node.data, { ...opts, parser: 'typescript' });
return group(formatttedScript);

@@ -427,41 +489,68 @@ }

if (node.type === 'Style') {
let styleLang = '';
let parserLang = '';
const supportedStyleLangValues = ['css', 'scss', 'sass'];
let parserLang = 'css';
if ('attributes' in node) {
const langAttribute = node.attributes.filter((x) => x.name === 'lang');
if (langAttribute.length === 0) styleLang = 'css';
else {
styleLang = langAttribute[0].value[0].raw.toLowerCase();
if (langAttribute.length) {
const styleLang = langAttribute[0].value[0].raw.toLowerCase();
if (supportedStyleLangValues.includes(styleLang)) parserLang = styleLang;
}
}
if (styleLang in supportedStyleLangValues) parserLang = styleLang;
// TODO(obnoxiousnerd): Provide error handling in case of unrecognized
// styles language.
else parserLang = 'css';
// the css parser appends an extra indented hardline, which we want outside of the `indent()`,
// so we remove the last element of the array
const [formatttedStyles, _] = textToDoc(node.content.styles, { ...opts, parser: parserLang });
return group([styleLang !== 'css' ? `<style lang="${styleLang}">` : '<style>', indent([hardline, formatttedStyles]), hardline, '</style>', hardline]);
}
switch (parserLang) {
case 'css':
case 'scss': {
// the css parser appends an extra indented hardline, which we want outside of the `indent()`,
// so we remove the last element of the array
const [formattedStyles, ,] = textToDoc(node.content.styles, { ...opts, parser: parserLang });
if (node.__isRawHTML) {
return textToDoc(node.content, { ...opts, parser: 'html' });
// print
const attributes = path.map(print, 'attributes');
const openingTag = group(['<style', indent(group(attributes)), softline, '>']);
return [openingTag, indent([hardline, formattedStyles]), hardline, '</style>'];
}
case 'sass': {
const sassOptions = {
tabSize: opts.tabWidth,
insertSpaces: !opts.useTabs,
lineEnding: opts.endOfLine.toUpperCase(),
};
// dedent the .sass, otherwise SassFormatter gets indentation wrong
const { result: raw, tabSize } = manualDedent(node.content.styles);
// format + re-indent
let formattedSass = SassFormatter.Format(raw, sassOptions).trim();
const indentChar = new Array(Math.max(tabSize, 2) + 1).join(opts.useTabs ? '\t' : ' ');
formattedSass = manualIndent(formattedSass, indentChar);
// print
formattedSass = join(hardline, formattedSass.split('\n'));
const attributes = path.map(print, 'attributes');
const openingTag = group(['<style', indent(group(attributes)), softline, '>']);
return [openingTag, hardline, formattedSass, hardline, '</style>'];
}
}
}
return null;
};
}
/** @type {import('prettier').Printer['hasPrettierIgnore']} */
const hasPrettierIgnore = (path) => {
function hasPrettierIgnore(path) {
const node = path.getNode();
const isSimpleIgnore = (comment) =>
comment.value.includes('prettier-ignore') && !comment.value.includes('prettier-ignore-start') && !comment.value.includes('prettier-ignore-end');
return node && node.comments && node.comments.length > 0 && node.comments.some(isSimpleIgnore);
};
if (!node || !Array.isArray(node.comments)) return false;
const hasIgnore = node.comments.some(
(comment) => comment.data.includes('prettier-ignore') && !comment.data.includes('prettier-ignore-start') && !comment.data.includes('prettier-ignore-end')
);
return hasIgnore;
}
/** @type {import('prettier').Printer} */
const printer = {
print,
printComment,
embed,

@@ -468,0 +557,0 @@ hasPrettierIgnore,

@@ -1,2 +0,2 @@

const { doc } = require('prettier');
const { doc, util } = require('prettier');

@@ -70,5 +70,4 @@ // @see http://xahlee.info/js/html5_non-closing_tag.html

const isPreTagContent = (path) => {
const stack = path.stack;
return stack.some((node) => (node.type === 'Element' && node.name.toLowerCase() === 'pre') || (node.type === 'Attribute' && !formattableAttributes.includes(node.name)));
if (!path || !path.stack || !Array.isArray(path.stack)) return false;
return path.stack.some((node) => (node.type === 'Element' && node.name.toLowerCase() === 'pre') || (node.type === 'Attribute' && !formattableAttributes.includes(node.name)));
};

@@ -175,3 +174,3 @@

function isNodeWithChildren(node) {
return !!node.children;
return node && Array.isArray(node.children);
}

@@ -399,3 +398,3 @@

for (var prop in x) {
if (y.hasOwnProperty(prop)) {
if (Object.prototype.hasOwnProperty.call(y, prop)) {
if (!deepEqual(x[prop], y[prop])) return false;

@@ -529,34 +528,139 @@ } else {

const isObjEmpty = (obj) => {
for (let i in obj) return false;
return true;
};
/** Shallowly attach comments to children */
function attachCommentsHTML(node) {
if (!isNodeWithChildren(node) || !node.children.some(({ type }) => type === 'Comment')) return;
const nodesToRemove = [];
// note: the .length - 1 is because we don’t need to read the last node
for (let n = 0; n < node.children.length - 1; n++) {
if (!node.children[n]) continue;
// attach comment to the next non-whitespace node
if (node.children[n].type === 'Comment') {
let next = n + 1;
while (isEmptyTextNode(node.children[next])) {
nodesToRemove.push(next); // if arbitrary whitespace between comment and node, remove
next++; // skip to the next non-whitespace node
}
util.addLeadingComment(node.children[next], node.children[n]);
}
}
// remove arbitrary whitespace nodes
nodesToRemove.reverse(); // start at back so we aren’t changing indices
nodesToRemove.forEach((index) => {
node.children.splice(index, 1);
});
}
/** dedent string & return tabSize (the last part is what we need) */
function dedent(input) {
let minTabSize = Infinity;
let result = input;
// 1. normalize
result = result.replace(/\r\n/g, '\n');
// 2. count tabSize
let char = '';
for (const line of result.split('\n')) {
if (!line) continue;
// if any line begins with a non-whitespace char, minTabSize is 0
if (line[0] && /^[^\s]/.test(line[0])) {
minTabSize = 0;
break;
}
const match = line.match(/^(\s+)\S+/); // \S ensures we don’t count lines of pure whitespace
if (match) {
if (match[1] && !char) char = match[1][0];
if (match[1].length < minTabSize) minTabSize = match[1].length;
}
}
// 3. reformat string
if (minTabSize > 0 && Number.isFinite(minTabSize)) {
result = result.replace(new RegExp(`^${new Array(minTabSize + 1).join(char)}`, 'gm'), '');
}
return {
tabSize: minTabSize === Infinity ? 0 : minTabSize,
char,
result,
};
}
/** re-indent string by chars */
function indent(input, char = ' ') {
return input.replace(/^(.)/gm, `${char}$1`);
}
/** scan code for Markdown name(s) */
function getMarkdownName(script) {
// default import: could be named anything
let defaultMatch;
while ((defaultMatch = /import\s+([^\s]+)\s+from\s+['|"|`]astro\/components\/Markdown\.astro/g.exec(script))) {
if (defaultMatch[1]) return new Set([defaultMatch[1].trim()]);
}
// named component: must have "Markdown" in specifier, but can be renamed via "as"
let namedMatch;
while ((namedMatch = /import\s+\{\s*([^}]+)\}\s+from\s+['|"|`]astro\/components/g.exec(script))) {
if (namedMatch[1] && !namedMatch[1].includes('Markdown')) continue;
// if "Markdown" was imported, find out whether or not it was renamed
const rawImports = namedMatch[1].trim().replace(/^\{/, '').replace(/\}$/, '').trim();
let importName = 'Markdown';
for (const spec of rawImports.split(',')) {
const [original, renamed] = spec.split(' as ').map((s) => s.trim());
if (original !== 'Markdown') continue;
importName = renamed || original;
break;
}
return new Set([importName]);
}
return new Set(['Markdown']);
}
module.exports = {
attachCommentsHTML,
canOmitSoftlineBeforeClosingTag,
dedent,
endsWithLinebreak,
flatten,
forceIntoExpression,
formattableAttributes,
getMarkdownName,
getText,
getUnencodedText,
indent,
isASTNode,
isAttributeShorthand,
isBlockElement,
isEmptyDoc,
isEmptyTextNode,
isPreTagContent,
isInlineElement,
isLine,
isLoneMustacheTag,
isAttributeShorthand,
isNodeWithChildren,
isObjEmpty,
isOrCanBeConvertedToShorthand,
isPreTagContent,
isTextNodeEndingWithLinebreak,
isTextNodeEndingWithWhitespace,
isTextNodeStartingWithLinebreak,
isTextNodeStartingWithWhitespace,
printRaw,
replaceEndOfLineWith,
getUnencodedText,
selfClosingTags,
formattableAttributes,
forceIntoExpression,
isInlineElement,
isBlockElement,
isTextNodeStartingWithWhitespace,
isTextNodeEndingWithWhitespace,
isTextNodeStartingWithLinebreak,
shouldHugEnd,
shouldHugStart,
startsWithLinebreak,
endsWithLinebreak,
isTextNodeEndingWithLinebreak,
canOmitSoftlineBeforeClosingTag,
shouldHugStart,
shouldHugEnd,
trim,
trimChildren,
trimTextNodeLeft,
trimTextNodeRight,
trimChildren,
flatten,
printRaw,
getText,
trim,
isLine,
isEmptyDoc,
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc