Skip to content

Commit

Permalink
Add support for styling within Markdown component (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow authored Oct 8, 2021
1 parent 20a298e commit 695fc07
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/cool-donuts-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'prettier-plugin-astro': minor
---

Add formatting for <Markdown> components
44 changes: 33 additions & 11 deletions src/printer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const {
builders: { breakParent, dedent, fill, group, hardline, indent, join, line, literalline, softline },
utils: { removeLines },
utils: { removeLines, stripTrailingHardline },
} = require('prettier/doc');
const { SassFormatter } = require('sass-formatter');

Expand All @@ -13,6 +13,7 @@ const {
flatten,
forceIntoExpression,
formattableAttributes,
getMarkdownName,
getText,
getUnencodedText,
indent: manualIndent,
Expand Down Expand Up @@ -136,7 +137,6 @@ function print(path, opts, print) {
const hasInlineComponent = node.children.filter((x) => x.type === 'InlineComponent').length > 0;
if (text.indexOf('{') === -1 && !hasInlineComponent) {
node.__isRawHTML = true;
node.content = text;
return path.map(print, 'children');
}

Expand Down Expand Up @@ -230,16 +230,31 @@ function print(path, opts, print) {
const firstChild = children[0];
const lastChild = children[children.length - 1];

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) {
body =
isInlineElement(path, opts, node) && node.children.length && isTextNodeStartingWithWhitespace(node.children[0]) && !isPreTagContent(path)
? () => line
: () => (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)) {
body = () => printRaw(node, opts.originalText);
} else if (isInlineElement(path, opts, node) && !isPreTagContent(path)) {
Expand All @@ -256,12 +271,12 @@ function print(path, opts, print) {
return group([...openingTag, isEmpty ? group(huggedContent) : group(indent(huggedContent)), omitSoftlineBeforeClosingTag ? '' : softline, '>']);
}

// 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 {
let didSetEndSeparator = false;

Expand Down Expand Up @@ -398,10 +413,21 @@ function expressionParser(text, parsers, opts) {

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

const node = path.getValue();

if (!node) return null;

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) {
try {
const embeddedopts = {
Expand Down Expand Up @@ -479,10 +505,6 @@ function embed(path, print, textToDoc, opts) {
}
}

if (node.__isRawHTML) {
return textToDoc(node.content, { ...opts, parser: 'html' });
}

return '';
}

Expand Down
31 changes: 29 additions & 2 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ function attachCommentsHTML(node) {
});
}

/* dedent string & return tabSize (the last part is what we need) */
/** dedent string & return tabSize (the last part is what we need) */
function dedent(input) {
let minTabSize = Infinity;
let result = input;
Expand Down Expand Up @@ -590,11 +590,37 @@ function dedent(input) {
};
}

/* re-indent string by chars */
/** 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,
Expand All @@ -603,6 +629,7 @@ module.exports = {
flatten,
forceIntoExpression,
formattableAttributes,
getMarkdownName,
getText,
getUnencodedText,
indent,
Expand Down
13 changes: 8 additions & 5 deletions test/astro-prettier.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,13 @@ test('can format an Astro file with embedded JSX expressions', Prettier, 'embedd

test('can format an Astro file with a `<!DOCTYPE html>` + embedded JSX expressions', Prettier, 'doctype-with-embedded-expr');

// note(drew): this should be fixed in new Parser. And as this is an HTML4 / deprecated / extreme edge case, probably fine to ignore?
test.failing('can format an Astro file with `<!DOCTYPE>` with extraneous attributes', Prettier, 'doctype-with-extra-attributes');

test('can format an Astro file with a JSX expression in an attribute', Prettier, 'attribute-with-embedded-expr');

test('does not alter html comments', PrettierUnaltered, 'html-comment');

test.todo("properly follow prettier' advice on formatting comments");

test('can format an Astro file with a JSX expression and an HTML Comment', Prettier, 'expr-and-html-comment');

test('can format an Astro file containing an Astro file embedded in a codeblock', PrettierMarkdown, 'embedded-in-markdown');
Expand All @@ -113,8 +112,6 @@ test('converts valid shorthand variables into shorthand', Prettier, 'converts-to

test.failing('an Astro file with an invalidly unclosed tag is still formatted', Prettier, 'unclosed-tag');

test.todo('test whether invalid files provide helpful support messages / still try to be parsed by prettier?');

test('can format an Astro file with components that are the uppercase version of html elements', Prettier, 'preserve-tag-case');

test('Autocloses open tags.', Prettier, 'autocloses-open-tags');
Expand All @@ -126,4 +123,10 @@ test('Can format an Astro file with a HTML style prettier ignore comment: https:

test('Can format an Astro file with a JS style prettier ignore comment: https://prettier.io/docs/en/ignore.html', Prettier, 'prettier-ignore-js');

test.failing(`Can format an Astro file with a codespan inside <Markdown/>`, Prettier, 'with-codespans');
test(`Can format an Astro file with a codespan inside <Markdown/>`, Prettier, 'with-codespans');

// note(drew): this _may_ be covered under the 'prettier-ignore-html' test. But if any bugs arise, let’s add more tests!
test.todo("properly follow prettier' advice on formatting comments");

// note(drew): I think this is a function of Astro’s parser, not Prettier. We’ll have to handle helpful error messages there!
test.todo('test whether invalid files provide helpful support messages / still try to be parsed by prettier?');
6 changes: 3 additions & 3 deletions test/fixtures/in/with-codespans.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
let hello = 'hello';
let space = ' ';
let helloWorld = `${hello+space}world`;
import Markdown from "astro/components";
import MarkdownComponent from "astro/components/Markdown.astro";
---
<div>{`${helloWorld+space}too many template strings`}</div>
<Markdown>
<MarkdownComponent>
`hello i am a codespan`

```js
Expand All @@ -16,4 +16,4 @@ let story = "hello i am a codefence";
story
.length
```
</Markdown>
</MarkdownComponent>
7 changes: 3 additions & 4 deletions test/fixtures/out/with-codespans.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@
let hello = "hello";
let space = " ";
let helloWorld = `${hello + space}world`;
import Markdown from "astro/components";
import MarkdownComponent from "astro/components/Markdown.astro";
---

<div>{`${helloWorld + space}too many template strings`}</div>
<Markdown>
<MarkdownComponent>
`hello i am a codespan`

```js
let story = "hello i am a codefence";

story.length;
```
</Markdown>
</MarkdownComponent>

0 comments on commit 695fc07

Please sign in to comment.