prettier-plugin-mdc 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,24 +2,68 @@
2
2
 
3
3
  [![NPM version](https://img.shields.io/npm/v/prettier-plugin-mdc?color=a1b858&label=)](https://www.npmjs.com/package/prettier-plugin-mdc)
4
4
 
5
+ A [Prettier](https://prettier.io/) plugin for formatting [MDC (Markdown Components)](https://content.nuxt.com/docs/files/markdown#mdc-syntax) syntax used in [Nuxt Content](https://content.nuxt.com/).
6
+
5
7
  ## 💎 Features
6
8
 
9
+ - Preserve YAML front matter in components
10
+ - Support for nested components
11
+ - Compatible with GFM (GitHub Flavored Markdown) and math syntax
12
+
7
13
  ## 📦 Installation
8
14
 
9
15
  ```bash
10
- $ npm install prettier-plugin-mdc
11
- $ yarn add prettier-plugin-mdc
12
- $ pnpm add prettier-plugin-mdc
16
+ npm install -D prettier-plugin-mdc
17
+ # or
18
+ yarn add -D prettier-plugin-mdc
19
+ # or
20
+ pnpm add -D prettier-plugin-mdc
13
21
  ```
14
22
 
15
23
  ## 🚀 Usage
16
24
 
17
- ```ts
25
+ Add the plugin to your Prettier configuration:
18
26
 
27
+ ```json
28
+ // .prettierrc
29
+ {
30
+ "plugins": ["prettier-plugin-mdc"],
31
+ "overrides": [
32
+ {
33
+ "files": ["*.md"],
34
+ "options": {
35
+ "parser": "mdc"
36
+ }
37
+ }
38
+ ]
39
+ }
19
40
  ```
20
41
 
21
- ## 📝 License
42
+ Or in `prettier.config.js`:
22
43
 
23
- [MIT](./LICENSE). Made with ❤️ by [Ray](https://github.com/so1ve)
44
+ ```js
45
+ export default {
46
+ plugins: ["prettier-plugin-mdc"],
47
+ overrides: [
48
+ {
49
+ files: ["*.md"],
50
+ options: {
51
+ parser: "mdc",
52
+ },
53
+ },
54
+ ],
55
+ };
56
+ ```
57
+
58
+ Then format your `.mdc` or `.md` files:
24
59
 
25
- [link](https://nuxtjs.org)
60
+ ```bash
61
+ prettier --write "**/*.mdc"
62
+ prettier --write "**/*.md"
63
+ ```
64
+
65
+ For MDC Syntax Reference, please check [remark-mdc](https://github.com/nuxt-content/remark-mdc).
66
+
67
+ ## License
68
+
69
+ [MIT](./LICENSE). Made with ❤️ by [Ray](https://github.com/so1ve)
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { Parser, Printer } from "prettier";
1
+ import { Parser, Printer, SupportLanguage } from "prettier";
2
2
  import { Node } from "unist";
3
3
 
4
4
  //#region src/constants.d.ts
@@ -10,4 +10,7 @@ declare const parsers: Record<typeof AST_FORMAT, Parser<Node>>;
10
10
  //#region src/printers.d.ts
11
11
  declare const printers: Record<typeof AST_FORMAT, Printer<Node>>;
12
12
  //#endregion
13
- export { parsers, printers };
13
+ //#region src/index.d.ts
14
+ declare const languages: Partial<SupportLanguage>[];
15
+ //#endregion
16
+ export { languages, parsers, printers };
package/dist/index.mjs CHANGED
@@ -12,17 +12,6 @@ import { createSyncFn } from "synckit";
12
12
  //#region src/constants.ts
13
13
  const AST_FORMAT = "mdc";
14
14
 
15
- //#endregion
16
- //#region src/parsers.ts
17
- const parsers = { [AST_FORMAT]: {
18
- ...markdown.parsers.markdown,
19
- astFormat: AST_FORMAT,
20
- parse: async (text) => {
21
- const processor = unified().use(remarkParse, { commonmark: true }).use(remarkMath).use(remarkGfm).use(remarkMdc);
22
- return await processor.run(processor.parse(text));
23
- }
24
- } };
25
-
26
15
  //#endregion
27
16
  //#region src/is.ts
28
17
  const isTextComponentNode = (node) => node.type === "textComponent";
@@ -52,6 +41,39 @@ function linkNeedsCustomPrinting(node) {
52
41
  return false;
53
42
  }
54
43
 
44
+ //#endregion
45
+ //#region src/validate.ts
46
+ function validateYamlBlocks(ast, text) {
47
+ function visit(node) {
48
+ if (isContainerComponentNode(node)) {
49
+ const pos = node.position;
50
+ if (pos) {
51
+ const startLine = pos.start.line;
52
+ const lines = text.split("\n");
53
+ if (startLine < lines.length) {
54
+ if (lines[startLine]?.trim() === "---" && !node.rawData) throw new Error(`Invalid YAML block in component "${node.name}" at line ${startLine + 1}: YAML block is
55
+ not properly closed`);
56
+ }
57
+ }
58
+ if (node.children) for (const child of node.children) visit(child);
59
+ } else if ("children" in node && Array.isArray(node.children)) for (const child of node.children) visit(child);
60
+ }
61
+ visit(ast);
62
+ }
63
+
64
+ //#endregion
65
+ //#region src/parsers.ts
66
+ const parsers = { [AST_FORMAT]: {
67
+ ...markdown.parsers.markdown,
68
+ astFormat: AST_FORMAT,
69
+ parse: async (text) => {
70
+ const processor = unified().use(remarkParse, { commonmark: true }).use(remarkMath).use(remarkGfm).use(remarkMdc);
71
+ const ast = await processor.run(processor.parse(text));
72
+ validateYamlBlocks(ast, text);
73
+ return ast;
74
+ }
75
+ } };
76
+
55
77
  //#endregion
56
78
  //#region src/visitor-keys.ts
57
79
  const visitorKeys = {
@@ -71,8 +93,23 @@ const extendedInlineNodes = [
71
93
  "inlineCode",
72
94
  "emphasis"
73
95
  ];
74
- const hasInlineAttribute = (node) => extendedInlineNodes.includes(node.type) && "attributes" in node;
96
+ const extendedInlineNodesHaveAttributes = (node) => extendedInlineNodes.includes(node.type) && "attributes" in node;
75
97
  const escapeQuotes = (value, quote) => value.replace(new RegExp(quote, "g"), `\\${quote}`);
98
+ /**
99
+ * Quote a string value using Prettier's quote selection logic:
100
+ *
101
+ * - Use preferred quote if value doesn't contain it
102
+ * - Switch to alternative quote if value contains preferred but not alternative
103
+ * - Use preferred quote with escaping if value contains both
104
+ */
105
+ function quoteString(value, options) {
106
+ const preferredQuote = options.singleQuote ? "'" : "\"";
107
+ const alternativeQuote = options.singleQuote ? "\"" : "'";
108
+ const hasPreferred = value.includes(preferredQuote);
109
+ const hasAlternative = value.includes(alternativeQuote);
110
+ const quote = hasPreferred && !hasAlternative ? alternativeQuote : preferredQuote;
111
+ return `${quote}${escapeQuotes(value.replace(/\\/g, "\\\\"), quote)}${quote}`;
112
+ }
76
113
 
77
114
  //#endregion
78
115
  //#region src/yaml.ts
@@ -92,16 +129,15 @@ const formatYaml = (text, options) => {
92
129
  const { hardline, join } = doc.builders;
93
130
  const mapChildren = (path, print) => path.map(print, "children");
94
131
  function serializeValue(value, options) {
95
- const quote = options.singleQuote ? "'" : "\"";
96
- if (typeof value === "string") return `${quote}${escapeQuotes(value.replace(/\\/g, "\\\\"), quote)}${quote}`;
132
+ if (typeof value === "string") return quoteString(value, options);
97
133
  if (typeof value === "number" || typeof value === "boolean") return String(value);
98
- return `${quote}${escapeQuotes(JSON.stringify(value), quote)}${quote}`;
134
+ const preferredQuote = options.singleQuote ? "'" : "\"";
135
+ return `${preferredQuote}${escapeQuotes(JSON.stringify(value), preferredQuote)}${preferredQuote}`;
99
136
  }
100
- function printAttributes(node, options) {
101
- const attrs = node.attributes;
102
- if (!attrs || Object.keys(attrs).length === 0) return "";
137
+ function printAttributes({ attributes }, options) {
138
+ if (!attributes || Object.keys(attributes).length === 0) return "";
103
139
  const parts = [];
104
- for (const [key, value] of Object.entries(attrs)) if (key === "id") parts.push(`#${value}`);
140
+ for (const [key, value] of Object.entries(attributes)) if (key === "id") parts.push(`#${value}`);
105
141
  else if (key === "class") {
106
142
  const classes = String(value).split(/\s+/).filter(Boolean);
107
143
  for (const cls of classes) parts.push(`.${cls}`);
@@ -115,8 +151,7 @@ function printAttributes(node, options) {
115
151
  function printBinding(node, options) {
116
152
  const value = node.attributes?.value ?? "";
117
153
  const defaultValue = node.attributes?.defaultValue;
118
- const quote = options.singleQuote ? "'" : "\"";
119
- if (defaultValue !== void 0 && defaultValue !== "undefined") return [`{{ ${value} || ${quote}${escapeQuotes(String(defaultValue), quote)}${quote} }}`];
154
+ if (defaultValue !== void 0 && defaultValue !== "undefined") return [`{{ ${value} || ${quoteString(String(defaultValue), options)} }}`];
120
155
  return [`{{ ${value} }}`];
121
156
  }
122
157
  /**
@@ -134,6 +169,7 @@ function isShorthandSpan(node) {
134
169
  }
135
170
  return nodeLength <= 3;
136
171
  }
172
+ const EMPTY_PROPS_RE = /\{\s*\}\s*$/;
137
173
  /**
138
174
  * Print inline text component: :name[content]{attrs} Special cases:
139
175
  *
@@ -165,6 +201,10 @@ function printTextComponent(path, print, options) {
165
201
  const parts = [`:${node.name}`];
166
202
  if (node.children && node.children.length > 0) parts.push("[", ...printChildrenWithEscapedBrackets(), "]");
167
203
  if (attrStr) parts.push(attrStr);
204
+ else if (node.position) {
205
+ const text = options.originalText.slice(node.position.start.offset, node.position.end.offset);
206
+ if (EMPTY_PROPS_RE.test(text)) parts.push("{}");
207
+ }
168
208
  return parts;
169
209
  }
170
210
  /**
@@ -180,7 +220,7 @@ function getContainerDepth(path) {
180
220
  */
181
221
  function printRawData(rawData, options) {
182
222
  if (!rawData) return [];
183
- let content = rawData.slice(1, -3).trimEnd();
223
+ let content = rawData.trimEnd().slice(1, -3).trimEnd();
184
224
  if (!content) return [];
185
225
  content = formatYaml(content, options);
186
226
  return [
@@ -203,8 +243,15 @@ function printContainerComponent(path, print, options) {
203
243
  const attrStr = printAttributes(node, options);
204
244
  if (attrStr) parts.push(attrStr);
205
245
  parts.push(hardline);
206
- parts.push(...printRawData(node.rawData, options));
246
+ const rawDataDoc = printRawData(node.rawData, options);
247
+ parts.push(...rawDataDoc);
207
248
  if (node.children && node.children.length > 0) {
249
+ if (rawDataDoc.length > 0 && node.rawData) {
250
+ const componentStartLine = node.position?.start.line ?? 0;
251
+ const rawDataNewlines = (node.rawData.match(/\n/g) ?? []).length;
252
+ const rawDataEndLine = componentStartLine + 1 + rawDataNewlines;
253
+ if ((node.children[0].position?.start.line ?? 0) > rawDataEndLine + 1) parts.push(hardline);
254
+ }
208
255
  const childDocs = mapChildren(path, print);
209
256
  parts.push(join(hardline, childDocs));
210
257
  parts.push(hardline);
@@ -257,6 +304,8 @@ function printLink(path, print, options) {
257
304
  parts.push(" ", quote, node.title, quote);
258
305
  }
259
306
  parts.push(")");
307
+ const attrStr = printAttributes(node, options);
308
+ if (attrStr) parts.push(attrStr);
260
309
  return parts;
261
310
  }
262
311
 
@@ -271,8 +320,8 @@ const printers = { [AST_FORMAT]: {
271
320
  },
272
321
  print(path, options, print, args) {
273
322
  const { node } = path;
274
- if (hasInlineAttribute(node)) return [mdastPrinter.print(path, options, print, args), printAttributes(node, options)];
275
323
  if (isLinkNode(node) && linkNeedsCustomPrinting(node)) return printLink(path, print, options);
324
+ if (extendedInlineNodesHaveAttributes(node)) return [mdastPrinter.print(path, options, print, args), printAttributes(node, options)];
276
325
  if (isTextComponentNode(node)) return printTextComponent(path, print, options);
277
326
  else if (isContainerComponentNode(node)) return printContainerComponent(path, print, options);
278
327
  else if (isComponentContainerSectionNode(node)) return printComponentContainerSection(path, print);
@@ -281,4 +330,13 @@ const printers = { [AST_FORMAT]: {
281
330
  } };
282
331
 
283
332
  //#endregion
284
- export { parsers, printers };
333
+ //#region src/index.ts
334
+ const languages = [{
335
+ name: "mdc",
336
+ parsers: [AST_FORMAT],
337
+ extensions: [".mdc"],
338
+ vscodeLanguageIds: ["mdc"]
339
+ }];
340
+
341
+ //#endregion
342
+ export { languages, parsers, printers };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prettier-plugin-mdc",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "author": "Ray <i@mk1.io> (@so1ve)",
5
5
  "type": "module",
6
6
  "description": "Prettier plugin for MDC syntax",