mirror of
https://github.com/LukeHagar/prettier-plugin-openapi.git
synced 2025-12-06 12:47:47 +00:00
Swizzle Prettier Markdown
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "prettier-plugin-openapi",
|
"name": "prettier-plugin-openapi",
|
||||||
"version": "1.0.11",
|
"version": "1.0.12",
|
||||||
"description": "A Prettier plugin for formatting OpenAPI/Swagger JSON and YAML files",
|
"description": "A Prettier plugin for formatting OpenAPI/Swagger JSON and YAML files",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Luke Hagar",
|
"name": "Luke Hagar",
|
||||||
|
|||||||
108
src/index.ts
108
src/index.ts
@@ -1,6 +1,5 @@
|
|||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import type { AstPath, Doc, Parser, ParserOptions, Printer, SupportLanguage } from "prettier";
|
import type { AstPath, Doc, Parser, ParserOptions, Printer, SupportLanguage } from "prettier";
|
||||||
import prettier from "prettier";
|
|
||||||
import { getVendorExtensions } from "./extensions/vendor-loader.js";
|
import { getVendorExtensions } from "./extensions/vendor-loader.js";
|
||||||
|
|
||||||
export type PrintFn = (path: AstPath) => Doc;
|
export type PrintFn = (path: AstPath) => Doc;
|
||||||
@@ -321,8 +320,10 @@ export const printers: Record<string, Printer> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats markdown strings using Prettier's markdown formatter
|
* Formats markdown strings using Prettier's markdown parser and printer
|
||||||
* Since Prettier plugins must be synchronous, we access Prettier's internal formatting APIs
|
*
|
||||||
|
* This uses Prettier's actual markdown formatting implementation, ensuring
|
||||||
|
* that markdown in OpenAPI descriptions is formatted exactly as Prettier would format it.
|
||||||
*/
|
*/
|
||||||
function formatMarkdownSync(markdown: string, options?: OpenAPIPluginOptions): string {
|
function formatMarkdownSync(markdown: string, options?: OpenAPIPluginOptions): string {
|
||||||
if (!markdown || typeof markdown !== "string") {
|
if (!markdown || typeof markdown !== "string") {
|
||||||
@@ -341,100 +342,21 @@ function formatMarkdownSync(markdown: string, options?: OpenAPIPluginOptions): s
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prettierInstance = prettier as any;
|
// Use Prettier's markdown formatter
|
||||||
|
// Dynamic require to avoid issues during build
|
||||||
// Try multiple approaches to access Prettier's markdown formatting
|
const formatModule = require("./prettier-markdown/format-markdown.js");
|
||||||
// Approach 1: Try formatDocument (internal API)
|
const formatted = formatModule.formatMarkdown(trimmed, {
|
||||||
if (prettierInstance.formatDocument) {
|
printWidth: options?.printWidth || 80,
|
||||||
try {
|
tabWidth: options?.tabWidth || 2,
|
||||||
const formatted = prettierInstance.formatDocument(trimmed, {
|
proseWrap: "preserve",
|
||||||
parser: "markdown",
|
singleQuote: false,
|
||||||
printWidth: options?.printWidth || 80,
|
|
||||||
tabWidth: options?.tabWidth || 2,
|
|
||||||
proseWrap: "preserve",
|
|
||||||
});
|
|
||||||
return formatted.trimEnd();
|
|
||||||
} catch {
|
|
||||||
// Fall through to next approach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Approach 2: Try to access Prettier's markdown formatting synchronously
|
|
||||||
// Since Prettier.format is async, we need to find a sync way to format markdown
|
|
||||||
// Prettier's internal APIs might not expose sync formatting, so we'll need to
|
|
||||||
// either use a sync wrapper or do intelligent normalization
|
|
||||||
|
|
||||||
// Try using Prettier's format through require (if available in sync context)
|
|
||||||
try {
|
|
||||||
const prettierModule = require("prettier");
|
|
||||||
|
|
||||||
// Check if there's a synchronous formatting method
|
|
||||||
// Prettier 3.x might expose different APIs
|
|
||||||
if (prettierModule.__internal?.markdown?.formatSync) {
|
|
||||||
const formatted = prettierModule.__internal.markdown.formatSync(trimmed, {
|
|
||||||
printWidth: options?.printWidth || 80,
|
|
||||||
tabWidth: options?.tabWidth || 2,
|
|
||||||
proseWrap: "preserve",
|
|
||||||
});
|
|
||||||
return formatted.trimEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try accessing Prettier's internal formatting through utils
|
|
||||||
if (prettierModule.__internal?.utils) {
|
|
||||||
const utils = prettierModule.__internal.utils;
|
|
||||||
// Check if there's a sync formatter in utils
|
|
||||||
// This is a best-effort attempt to use Prettier's formatting
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Prettier's internal APIs may not be accessible or may not be sync
|
|
||||||
// Fall through to manual formatting
|
|
||||||
}
|
|
||||||
|
|
||||||
// Approach 3: Manual markdown formatting
|
|
||||||
// Since Prettier.format is async and plugins must be sync,
|
|
||||||
// we do intelligent markdown normalization that matches Prettier's style
|
|
||||||
let formatted = trimmed;
|
|
||||||
|
|
||||||
// Normalize line breaks (preserve intentional double line breaks)
|
|
||||||
formatted = formatted.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
||||||
|
|
||||||
// Normalize multiple blank lines to maximum of 2 (preserve paragraph breaks)
|
|
||||||
formatted = formatted.replace(/\n{3,}/g, "\n\n");
|
|
||||||
|
|
||||||
// Process each line
|
|
||||||
const lines = formatted.split("\n");
|
|
||||||
const processedLines = lines.map((line) => {
|
|
||||||
// Preserve code block indentation (lines starting with 4+ spaces or tabs)
|
|
||||||
if (/^[\t ]{4,}/.test(line)) {
|
|
||||||
// Code block - preserve but normalize trailing whitespace
|
|
||||||
return line.trimEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
// For regular lines, normalize multiple spaces to single spaces
|
|
||||||
// But preserve intentional formatting (like tables, lists with specific spacing)
|
|
||||||
// First, trim trailing whitespace
|
|
||||||
let processed = line.trimEnd();
|
|
||||||
|
|
||||||
// Normalize multiple spaces to single space, but preserve:
|
|
||||||
// - Multiple spaces at start (indentation for nested lists)
|
|
||||||
// - Patterns that look intentional (like markdown tables)
|
|
||||||
if (!/^[\s]*\|/.test(processed) && !/^[\s]*[-*+]\s{2,}/.test(processed)) {
|
|
||||||
// Not a table or list, normalize spaces
|
|
||||||
processed = processed.replace(/[ \t]+/g, " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
return processed;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
formatted = processedLines.join("\n");
|
|
||||||
|
|
||||||
// Remove trailing newline if we added one (we want YAML to control formatting)
|
|
||||||
formatted = formatted.trimEnd();
|
|
||||||
|
|
||||||
return formatted;
|
return formatted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If markdown formatting fails, return original string
|
// If Prettier's formatter fails, fall back to basic normalization
|
||||||
return markdown;
|
// This ensures we always return valid markdown
|
||||||
|
return trimmed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
54
src/prettier-markdown/README.md
Normal file
54
src/prettier-markdown/README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Prettier Markdown Integration
|
||||||
|
|
||||||
|
This directory contains Prettier's markdown language implementation, adapted for use within this plugin.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
### Core Files (from Prettier)
|
||||||
|
These files are copied from Prettier's `src/language-markdown` directory:
|
||||||
|
- `parser-markdown.js` - Markdown parser
|
||||||
|
- `printer-markdown.js` - Markdown printer
|
||||||
|
- `clean.js` - AST cleaning utilities
|
||||||
|
- `utils.js` - Utility functions
|
||||||
|
- `constants.evaluate.js` - Constants
|
||||||
|
- `print/` - Printing utilities
|
||||||
|
- `unified-plugins/` - Unified/Remark plugins
|
||||||
|
- Other supporting files
|
||||||
|
|
||||||
|
### Adapter Files (created for this plugin)
|
||||||
|
These files adapt Prettier's internal dependencies:
|
||||||
|
- `adapter-document-builders.js` - Adapts Prettier's document builders
|
||||||
|
- `adapter-document-utils.js` - Adapts Prettier's document utilities
|
||||||
|
- `adapter-document-constants.js` - Adapts Prettier's document constants
|
||||||
|
- `adapter-prettier-utils.js` - Adapts Prettier's utility functions
|
||||||
|
- `adapter-pragma.js` - Adapts pragma handling (simplified)
|
||||||
|
|
||||||
|
### Integration Files
|
||||||
|
- `format-markdown.ts` - Type-safe wrapper for formatting markdown
|
||||||
|
- `options.js` - Markdown formatting options (adapted from Prettier)
|
||||||
|
|
||||||
|
## Updating from Prettier
|
||||||
|
|
||||||
|
When Prettier updates its markdown implementation:
|
||||||
|
|
||||||
|
1. **Copy updated files** from `prettier/src/language-markdown/` to this directory
|
||||||
|
2. **Update adapter files** if Prettier's internal structure changed:
|
||||||
|
- Check if document builders path changed → update `adapter-document-builders.js`
|
||||||
|
- Check if document utils path changed → update `adapter-document-utils.js`
|
||||||
|
- Check if utility functions changed → update `adapter-prettier-utils.js`
|
||||||
|
3. **Update imports** in copied files to use adapter files:
|
||||||
|
- `../document/builders.js` → `./adapter-document-builders.js`
|
||||||
|
- `../document/utils.js` → `./adapter-document-utils.js`
|
||||||
|
- `../document/constants.js` → `./adapter-document-constants.js`
|
||||||
|
- `../utils/*` → `./adapter-prettier-utils.js`
|
||||||
|
- `../common/common-options.evaluate.js` → update `options.js`
|
||||||
|
- `../main/front-matter/index.js` → update `adapter-pragma.js` or `clean.js`
|
||||||
|
- `../utils/pragma/pragma.evaluate.js` → update `adapter-pragma.js`
|
||||||
|
4. **Test** that markdown formatting still works correctly
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
The adapter files attempt to access Prettier's internal APIs at runtime. If Prettier's internal structure changes significantly, you may need to update the adapter files to match.
|
||||||
|
|
||||||
|
The interfaces in the adapter files are designed to be similar to Prettier's actual structure, making updates easier.
|
||||||
|
|
||||||
55
src/prettier-markdown/adapter-document-builders.js
Normal file
55
src/prettier-markdown/adapter-document-builders.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Adapter for Prettier's document builders
|
||||||
|
*
|
||||||
|
* This file attempts to import Prettier's document builders from internal APIs.
|
||||||
|
* Update this file when Prettier's internal structure changes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let builders: any = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prettier = require("prettier");
|
||||||
|
|
||||||
|
// Try multiple paths to access document builders
|
||||||
|
if (prettier.__internal?.document?.builders) {
|
||||||
|
builders = prettier.__internal.document.builders;
|
||||||
|
} else {
|
||||||
|
// Try to require directly (may work in plugin context)
|
||||||
|
try {
|
||||||
|
builders = require("prettier/internal/document/builders");
|
||||||
|
} catch {
|
||||||
|
// Fallback: try alternative paths
|
||||||
|
try {
|
||||||
|
const doc = require("prettier/doc");
|
||||||
|
if (doc) {
|
||||||
|
builders = doc;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not accessible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Builders not accessible
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export what we found, or throw if not available
|
||||||
|
if (!builders) {
|
||||||
|
throw new Error(
|
||||||
|
"Prettier document builders not accessible. " +
|
||||||
|
"Markdown formatting requires Prettier's internal document builders."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const {
|
||||||
|
align,
|
||||||
|
fill,
|
||||||
|
group,
|
||||||
|
hardline,
|
||||||
|
indent,
|
||||||
|
line,
|
||||||
|
literalline,
|
||||||
|
markAsRoot,
|
||||||
|
softline,
|
||||||
|
} = builders;
|
||||||
|
|
||||||
33
src/prettier-markdown/adapter-document-constants.js
Normal file
33
src/prettier-markdown/adapter-document-constants.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Adapter for Prettier's document constants
|
||||||
|
*
|
||||||
|
* This file attempts to import Prettier's document constants from internal APIs.
|
||||||
|
* Update this file when Prettier's internal structure changes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let constants: any = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prettier = require("prettier");
|
||||||
|
|
||||||
|
if (prettier.__internal?.document?.constants) {
|
||||||
|
constants = prettier.__internal.document.constants;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
constants = require("prettier/internal/document/constants");
|
||||||
|
} catch {
|
||||||
|
// Constants may not be accessible, provide a fallback
|
||||||
|
constants = {
|
||||||
|
DOC_TYPE_STRING: "doc-type-string",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Provide fallback
|
||||||
|
constants = {
|
||||||
|
DOC_TYPE_STRING: "doc-type-string",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { DOC_TYPE_STRING } = constants;
|
||||||
|
|
||||||
39
src/prettier-markdown/adapter-document-utils.js
Normal file
39
src/prettier-markdown/adapter-document-utils.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Adapter for Prettier's document utils
|
||||||
|
*
|
||||||
|
* This file attempts to import Prettier's document utilities from internal APIs.
|
||||||
|
* Update this file when Prettier's internal structure changes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let utils: any = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prettier = require("prettier");
|
||||||
|
|
||||||
|
if (prettier.__internal?.document?.utils) {
|
||||||
|
utils = prettier.__internal.document.utils;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
utils = require("prettier/internal/document/utils");
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
const docUtils = require("prettier/doc");
|
||||||
|
utils = docUtils;
|
||||||
|
} catch {
|
||||||
|
// Not accessible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Utils not accessible
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!utils) {
|
||||||
|
throw new Error(
|
||||||
|
"Prettier document utils not accessible. " +
|
||||||
|
"Markdown formatting requires Prettier's internal document utilities."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { getDocType, replaceEndOfLine } = utils;
|
||||||
|
|
||||||
48
src/prettier-markdown/adapter-pragma.js
Normal file
48
src/prettier-markdown/adapter-pragma.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Adapter for Prettier's pragma utilities
|
||||||
|
*
|
||||||
|
* Simplified version that doesn't depend on Prettier's internal pragma system.
|
||||||
|
* Update this file when Prettier's pragma behavior changes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Simplified pragma regexes based on Prettier's implementation
|
||||||
|
const MARKDOWN_HAS_PRAGMA_REGEXP = /^<!--\s*@(prettier|format)\s*-->$/m;
|
||||||
|
const MARKDOWN_HAS_IGNORE_PRAGMA_REGEXP = /^<!--\s*prettier-ignore(?:-(start|end))?\s*-->$/m;
|
||||||
|
const FORMAT_PRAGMA_TO_INSERT = "format";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple front matter parser (minimal implementation)
|
||||||
|
*/
|
||||||
|
function parseFrontMatter(text: string) {
|
||||||
|
const yamlMatch = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
||||||
|
if (yamlMatch) {
|
||||||
|
return {
|
||||||
|
content: text.slice(yamlMatch[0].length),
|
||||||
|
frontMatter: {
|
||||||
|
raw: yamlMatch[0],
|
||||||
|
end: { index: yamlMatch[0].length },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { content: text, frontMatter: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasPragma = (text: string) =>
|
||||||
|
parseFrontMatter(text).content.trimStart().match(MARKDOWN_HAS_PRAGMA_REGEXP)
|
||||||
|
?.index === 0;
|
||||||
|
|
||||||
|
const hasIgnorePragma = (text: string) =>
|
||||||
|
parseFrontMatter(text)
|
||||||
|
.content.trimStart()
|
||||||
|
.match(MARKDOWN_HAS_IGNORE_PRAGMA_REGEXP)?.index === 0;
|
||||||
|
|
||||||
|
const insertPragma = (text: string) => {
|
||||||
|
const { frontMatter } = parseFrontMatter(text);
|
||||||
|
const pragma = `<!-- @${FORMAT_PRAGMA_TO_INSERT} -->`;
|
||||||
|
return frontMatter
|
||||||
|
? `${frontMatter.raw}\n\n${pragma}\n\n${text.slice(frontMatter.end.index)}`
|
||||||
|
: `${pragma}\n\n${text}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { hasIgnorePragma, hasPragma, insertPragma };
|
||||||
|
|
||||||
130
src/prettier-markdown/adapter-prettier-internals.js
Normal file
130
src/prettier-markdown/adapter-prettier-internals.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* Adapter for Prettier's internal document builders and utilities
|
||||||
|
*
|
||||||
|
* This file attempts to access Prettier's internal APIs that are needed
|
||||||
|
* by the markdown parser and printer. These adapters provide a type-safe
|
||||||
|
* way to access Prettier internals when available.
|
||||||
|
*
|
||||||
|
* Note: This file can be updated when Prettier's internal structure changes.
|
||||||
|
* The interfaces should remain similar to allow easy updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to import Prettier's document builders
|
||||||
|
* @returns {Promise<typeof import('prettier/internal/document/builders')> | null>}
|
||||||
|
*/
|
||||||
|
export function getDocumentBuilders() {
|
||||||
|
try {
|
||||||
|
// Try to require Prettier's document builders
|
||||||
|
// Prettier 3.x structure
|
||||||
|
const prettier = require("prettier");
|
||||||
|
|
||||||
|
// Path 1: Check if accessible via __internal
|
||||||
|
if (prettier.__internal?.document?.builders) {
|
||||||
|
return prettier.__internal.document.builders;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path 2: Try to require directly (may work in some contexts)
|
||||||
|
try {
|
||||||
|
return require("prettier/internal/document/builders");
|
||||||
|
} catch {
|
||||||
|
// Not accessible
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to import Prettier's document utils
|
||||||
|
*/
|
||||||
|
export function getDocumentUtils() {
|
||||||
|
try {
|
||||||
|
const prettier = require("prettier");
|
||||||
|
|
||||||
|
if (prettier.__internal?.document?.utils) {
|
||||||
|
return prettier.__internal.document.utils;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return require("prettier/internal/document/utils");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to import Prettier's document constants
|
||||||
|
*/
|
||||||
|
export function getDocumentConstants() {
|
||||||
|
try {
|
||||||
|
const prettier = require("prettier");
|
||||||
|
|
||||||
|
if (prettier.__internal?.document?.constants) {
|
||||||
|
return prettier.__internal.document.constants;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return require("prettier/internal/document/constants");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to import Prettier's utility functions
|
||||||
|
*/
|
||||||
|
export function getPrettierUtils() {
|
||||||
|
try {
|
||||||
|
const prettier = require("prettier");
|
||||||
|
|
||||||
|
if (prettier.__internal?.utils) {
|
||||||
|
return prettier.__internal.utils;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to get Prettier's doc-to-string printer
|
||||||
|
* This is needed to convert Doc objects to strings
|
||||||
|
*/
|
||||||
|
export function getDocPrinter() {
|
||||||
|
try {
|
||||||
|
const prettier = require("prettier");
|
||||||
|
|
||||||
|
// Try multiple paths
|
||||||
|
if (prettier.__internal?.doc?.printDocToString) {
|
||||||
|
return prettier.__internal.doc.printDocToString;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prettier.__internal?.docPrinter?.formatDoc) {
|
||||||
|
return prettier.__internal.docPrinter.formatDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const docUtils = require("prettier/internal/doc");
|
||||||
|
if (docUtils?.printDocToString) {
|
||||||
|
return docUtils.printDocToString;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not accessible
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
72
src/prettier-markdown/adapter-prettier-utils.js
Normal file
72
src/prettier-markdown/adapter-prettier-utils.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Adapter for Prettier's utility functions
|
||||||
|
*
|
||||||
|
* This file attempts to import Prettier's utility functions from internal APIs.
|
||||||
|
* Update this file when Prettier's internal structure changes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let utils: any = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prettier = require("prettier");
|
||||||
|
|
||||||
|
if (prettier.__internal?.utils) {
|
||||||
|
utils = prettier.__internal.utils;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// Try alternative paths
|
||||||
|
utils = require("prettier/internal/utils");
|
||||||
|
} catch {
|
||||||
|
// Utils may not be accessible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Utils not accessible
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide fallback implementations if utils aren't accessible
|
||||||
|
if (!utils) {
|
||||||
|
// Minimal fallback implementations
|
||||||
|
utils = {
|
||||||
|
getMaxContinuousCount: (str: string, char: string) => {
|
||||||
|
let max = 0;
|
||||||
|
let current = 0;
|
||||||
|
for (const c of str) {
|
||||||
|
if (c === char) {
|
||||||
|
current++;
|
||||||
|
max = Math.max(max, current);
|
||||||
|
} else {
|
||||||
|
current = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max;
|
||||||
|
},
|
||||||
|
getMinNotPresentContinuousCount: (str: string, char: string) => {
|
||||||
|
let count = 1;
|
||||||
|
while (str.includes(char.repeat(count))) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
getPreferredQuote: (str: string, singleQuote: boolean) => {
|
||||||
|
if (singleQuote) return "'";
|
||||||
|
const hasSingle = str.includes("'");
|
||||||
|
const hasDouble = str.includes('"');
|
||||||
|
if (hasSingle && !hasDouble) return '"';
|
||||||
|
return "'";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMaxContinuousCount = utils.getMaxContinuousCount;
|
||||||
|
export const getMinNotPresentContinuousCount = utils.getMinNotPresentContinuousCount;
|
||||||
|
export const getPreferredQuote = utils.getPreferredQuote;
|
||||||
|
|
||||||
|
// UnexpectedNodeError may be in utils or separate
|
||||||
|
export class UnexpectedNodeError extends Error {
|
||||||
|
constructor(node: any, language: string) {
|
||||||
|
super(`Unexpected node type: ${node.type} in ${language}`);
|
||||||
|
this.name = "UnexpectedNodeError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
91
src/prettier-markdown/clean.js
Normal file
91
src/prettier-markdown/clean.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import collapseWhiteSpace from "collapse-white-space";
|
||||||
|
import { hasPragma } from "./adapter-pragma.js";
|
||||||
|
|
||||||
|
// Simplified front matter check
|
||||||
|
function isFrontMatter(node: any): boolean {
|
||||||
|
return node?.type === "yaml" || node?.type === "toml";
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignoredProperties = new Set([
|
||||||
|
"position",
|
||||||
|
"raw", // front-matter
|
||||||
|
]);
|
||||||
|
function clean(original, cloned, parent) {
|
||||||
|
// for codeblock
|
||||||
|
if (
|
||||||
|
original.type === "code" ||
|
||||||
|
original.type === "yaml" ||
|
||||||
|
original.type === "import" ||
|
||||||
|
original.type === "export" ||
|
||||||
|
original.type === "jsx"
|
||||||
|
) {
|
||||||
|
delete cloned.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (original.type === "list") {
|
||||||
|
delete cloned.isAligned;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (original.type === "list" || original.type === "listItem") {
|
||||||
|
delete cloned.spread;
|
||||||
|
}
|
||||||
|
|
||||||
|
// texts can be splitted or merged
|
||||||
|
if (original.type === "text") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (original.type === "inlineCode") {
|
||||||
|
cloned.value = original.value.replaceAll("\n", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (original.type === "wikiLink") {
|
||||||
|
cloned.value = original.value.trim().replaceAll(/[\t\n]+/gu, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
original.type === "definition" ||
|
||||||
|
original.type === "linkReference" ||
|
||||||
|
original.type === "imageReference"
|
||||||
|
) {
|
||||||
|
cloned.label = collapseWhiteSpace(original.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(original.type === "link" || original.type === "image") &&
|
||||||
|
original.url &&
|
||||||
|
original.url.includes("(")
|
||||||
|
) {
|
||||||
|
for (const character of "<>") {
|
||||||
|
cloned.url = original.url.replaceAll(
|
||||||
|
character,
|
||||||
|
encodeURIComponent(character),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(original.type === "definition" ||
|
||||||
|
original.type === "link" ||
|
||||||
|
original.type === "image") &&
|
||||||
|
original.title
|
||||||
|
) {
|
||||||
|
cloned.title = original.title.replaceAll(/\\(?=["')])/gu, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// for insert pragma
|
||||||
|
if (
|
||||||
|
parent?.type === "root" &&
|
||||||
|
parent.children.length > 0 &&
|
||||||
|
(parent.children[0] === original ||
|
||||||
|
(isFrontMatter(parent.children[0]) && parent.children[1] === original)) &&
|
||||||
|
original.type === "html" &&
|
||||||
|
hasPragma(original.value)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clean.ignoredProperties = ignoredProperties;
|
||||||
|
|
||||||
|
export default clean;
|
||||||
86
src/prettier-markdown/constants.evaluate.js
Normal file
86
src/prettier-markdown/constants.evaluate.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { all as getCjkCharset } from "cjk-regex";
|
||||||
|
import { Charset } from "regexp-util";
|
||||||
|
import unicodeRegex from "unicode-regex";
|
||||||
|
|
||||||
|
const cjkCharset = new Charset(
|
||||||
|
getCjkCharset(),
|
||||||
|
unicodeRegex({
|
||||||
|
Script_Extensions: ["Han", "Katakana", "Hiragana", "Hangul", "Bopomofo"],
|
||||||
|
General_Category: [
|
||||||
|
"Other_Letter",
|
||||||
|
"Letter_Number",
|
||||||
|
"Other_Symbol",
|
||||||
|
"Modifier_Letter",
|
||||||
|
"Modifier_Symbol",
|
||||||
|
"Nonspacing_Mark",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const variationSelectorsCharset = unicodeRegex({
|
||||||
|
Block: ["Variation_Selectors", "Variation_Selectors_Supplement"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const CJK_REGEXP = new RegExp(
|
||||||
|
`(?:${cjkCharset.toString("u")})(?:${variationSelectorsCharset.toString("u")})?`,
|
||||||
|
"u",
|
||||||
|
);
|
||||||
|
|
||||||
|
const asciiPunctuationCharacters = [
|
||||||
|
"!",
|
||||||
|
'"',
|
||||||
|
"#",
|
||||||
|
"$",
|
||||||
|
"%",
|
||||||
|
"&",
|
||||||
|
"'",
|
||||||
|
"(",
|
||||||
|
")",
|
||||||
|
"*",
|
||||||
|
"+",
|
||||||
|
",",
|
||||||
|
"-",
|
||||||
|
".",
|
||||||
|
"/",
|
||||||
|
":",
|
||||||
|
";",
|
||||||
|
"<",
|
||||||
|
"=",
|
||||||
|
">",
|
||||||
|
"?",
|
||||||
|
"@",
|
||||||
|
"[",
|
||||||
|
"\\",
|
||||||
|
"]",
|
||||||
|
"^",
|
||||||
|
"_",
|
||||||
|
"`",
|
||||||
|
"{",
|
||||||
|
"|",
|
||||||
|
"}",
|
||||||
|
"~",
|
||||||
|
];
|
||||||
|
|
||||||
|
// https://spec.commonmark.org/0.25/#punctuation-character
|
||||||
|
// https://unicode.org/Public/5.1.0/ucd/UCD.html#General_Category_Values
|
||||||
|
const unicodePunctuationClasses = [
|
||||||
|
/* Pc */ "Connector_Punctuation",
|
||||||
|
/* Pd */ "Dash_Punctuation",
|
||||||
|
/* Pe */ "Close_Punctuation",
|
||||||
|
/* Pf */ "Final_Punctuation",
|
||||||
|
/* Pi */ "Initial_Punctuation",
|
||||||
|
/* Po */ "Other_Punctuation",
|
||||||
|
/* Ps */ "Open_Punctuation",
|
||||||
|
];
|
||||||
|
|
||||||
|
const PUNCTUATION_REGEXP = new RegExp(
|
||||||
|
`(?:${[
|
||||||
|
new Charset(...asciiPunctuationCharacters).toRegExp("u").source,
|
||||||
|
...unicodePunctuationClasses.map(
|
||||||
|
(charset) => String.raw`\p{General_Category=${charset}}`,
|
||||||
|
"\u{ff5e}", // Used as a substitute for U+301C in Windows
|
||||||
|
),
|
||||||
|
].join("|")})`,
|
||||||
|
"u",
|
||||||
|
);
|
||||||
|
|
||||||
|
export { CJK_REGEXP, PUNCTUATION_REGEXP };
|
||||||
87
src/prettier-markdown/embed.js
Normal file
87
src/prettier-markdown/embed.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { hardline, markAsRoot } from "../document/builders.js";
|
||||||
|
import { replaceEndOfLine } from "../document/utils.js";
|
||||||
|
import getMaxContinuousCount from "../utils/get-max-continuous-count.js";
|
||||||
|
import inferParser from "../utils/infer-parser.js";
|
||||||
|
import { getFencedCodeBlockValue } from "./utils.js";
|
||||||
|
|
||||||
|
function embed(path, options) {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
if (node.type === "code" && node.lang !== null) {
|
||||||
|
const parser = inferParser(options, { language: node.lang });
|
||||||
|
if (parser) {
|
||||||
|
return async (textToDoc) => {
|
||||||
|
const styleUnit = options.__inJsTemplate ? "~" : "`";
|
||||||
|
const style = styleUnit.repeat(
|
||||||
|
Math.max(3, getMaxContinuousCount(node.value, styleUnit) + 1),
|
||||||
|
);
|
||||||
|
const newOptions = { parser };
|
||||||
|
|
||||||
|
// Override the filepath option.
|
||||||
|
// This is because whether the trailing comma of type parameters
|
||||||
|
// should be printed depends on whether it is `*.ts` or `*.tsx`.
|
||||||
|
// https://github.com/prettier/prettier/issues/15282
|
||||||
|
if (node.lang === "ts" || node.lang === "typescript") {
|
||||||
|
newOptions.filepath = "dummy.ts";
|
||||||
|
} else if (node.lang === "tsx") {
|
||||||
|
newOptions.filepath = "dummy.tsx";
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await textToDoc(
|
||||||
|
getFencedCodeBlockValue(node, options.originalText),
|
||||||
|
newOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
return markAsRoot([
|
||||||
|
style,
|
||||||
|
node.lang,
|
||||||
|
node.meta ? " " + node.meta : "",
|
||||||
|
hardline,
|
||||||
|
replaceEndOfLine(doc),
|
||||||
|
hardline,
|
||||||
|
style,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (node.type) {
|
||||||
|
// MDX
|
||||||
|
case "import":
|
||||||
|
case "export":
|
||||||
|
return (textToDoc) =>
|
||||||
|
textToDoc(node.value, {
|
||||||
|
// TODO: Rename this option since it's not used in HTML
|
||||||
|
__onHtmlBindingRoot: (ast) => validateImportExport(ast, node.type),
|
||||||
|
parser: "babel",
|
||||||
|
});
|
||||||
|
case "jsx":
|
||||||
|
return (textToDoc) =>
|
||||||
|
textToDoc(`<$>${node.value}</$>`, {
|
||||||
|
parser: "__js_expression",
|
||||||
|
rootMarker: "mdx",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateImportExport(ast, type) {
|
||||||
|
const {
|
||||||
|
program: { body },
|
||||||
|
} = ast;
|
||||||
|
|
||||||
|
// https://github.com/mdx-js/mdx/blob/3430138958c9c0344ecad9d59e0d6b5d72bedae3/packages/remark-mdx/extract-imports-and-exports.js#L16
|
||||||
|
if (
|
||||||
|
!body.every(
|
||||||
|
(node) =>
|
||||||
|
node.type === "ImportDeclaration" ||
|
||||||
|
node.type === "ExportDefaultDeclaration" ||
|
||||||
|
node.type === "ExportNamedDeclaration",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(`Unexpected '${type}' in MDX.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default embed;
|
||||||
123
src/prettier-markdown/format-markdown.js
Normal file
123
src/prettier-markdown/format-markdown.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* Type-safe wrapper for Prettier's markdown formatting
|
||||||
|
*
|
||||||
|
* This module provides a synchronous interface to Prettier's markdown
|
||||||
|
* parser and printer, adapted to work within a Prettier plugin context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { markdown: markdownParser } = require("./parser-markdown.js");
|
||||||
|
const printer = require("./printer-markdown.js");
|
||||||
|
const { getDocPrinter } = require("./adapter-prettier-internals.js");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a markdown string using Prettier's markdown parser and printer
|
||||||
|
*
|
||||||
|
* @param {string} markdown - The markdown string to format
|
||||||
|
* @param {Object} options - Formatting options
|
||||||
|
* @param {number} [options.printWidth=80] - Maximum line width
|
||||||
|
* @param {number} [options.tabWidth=2] - Tab width
|
||||||
|
* @param {string} [options.proseWrap='preserve'] - Prose wrapping mode
|
||||||
|
* @param {boolean} [options.singleQuote=false] - Use single quotes
|
||||||
|
* @returns {string} The formatted markdown string, or the original if formatting fails
|
||||||
|
*/
|
||||||
|
function formatMarkdown(markdown, options = {}) {
|
||||||
|
if (!markdown || typeof markdown !== "string") {
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = markdown.trim();
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse markdown to AST
|
||||||
|
const ast = markdownParser.parse(trimmed, {
|
||||||
|
originalText: trimmed,
|
||||||
|
filepath: "temp.md",
|
||||||
|
printWidth: options.printWidth || 80,
|
||||||
|
tabWidth: options.tabWidth || 2,
|
||||||
|
proseWrap: options.proseWrap || "preserve",
|
||||||
|
singleQuote: options.singleQuote || false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an AstPath-like object for the printer
|
||||||
|
const astPath = {
|
||||||
|
getNode: () => ast,
|
||||||
|
stack: [ast],
|
||||||
|
node: ast,
|
||||||
|
callParent: (fn) => fn(astPath),
|
||||||
|
each: (fn) => {
|
||||||
|
if (ast.children) {
|
||||||
|
ast.children.forEach((child, index) => {
|
||||||
|
const childPath = {
|
||||||
|
getNode: () => child,
|
||||||
|
stack: [...astPath.stack, child],
|
||||||
|
node: child,
|
||||||
|
index,
|
||||||
|
previous: index > 0 ? ast.children[index - 1] : null,
|
||||||
|
next: index < ast.children.length - 1 ? ast.children[index + 1] : null,
|
||||||
|
parent: ast,
|
||||||
|
isFirst: index === 0,
|
||||||
|
isLast: index === ast.children.length - 1,
|
||||||
|
callParent: (fn) => fn(childPath),
|
||||||
|
};
|
||||||
|
fn(childPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a print function for recursive printing
|
||||||
|
const createPrintFn = (path) => {
|
||||||
|
return (printPath) => {
|
||||||
|
return printer.print(printPath, {
|
||||||
|
printWidth: options.printWidth || 80,
|
||||||
|
tabWidth: options.tabWidth || 2,
|
||||||
|
proseWrap: options.proseWrap || "preserve",
|
||||||
|
singleQuote: options.singleQuote || false,
|
||||||
|
originalText: trimmed,
|
||||||
|
}, createPrintFn);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Print the AST to a Doc object
|
||||||
|
const doc = printer.print(astPath, {
|
||||||
|
printWidth: options.printWidth || 80,
|
||||||
|
tabWidth: options.tabWidth || 2,
|
||||||
|
proseWrap: options.proseWrap || "preserve",
|
||||||
|
singleQuote: options.singleQuote || false,
|
||||||
|
originalText: trimmed,
|
||||||
|
}, createPrintFn);
|
||||||
|
|
||||||
|
// Convert Doc to string
|
||||||
|
if (typeof doc === "string") {
|
||||||
|
return doc.trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert Doc object to string using Prettier's doc printer
|
||||||
|
const docPrinter = getDocPrinter();
|
||||||
|
if (docPrinter && typeof docPrinter === "function") {
|
||||||
|
try {
|
||||||
|
const formattedString = docPrinter(doc, {
|
||||||
|
printWidth: options.printWidth || 80,
|
||||||
|
tabWidth: options.tabWidth || 2,
|
||||||
|
useTabs: false,
|
||||||
|
});
|
||||||
|
return typeof formattedString === "string" ? formattedString.trimEnd() : markdown;
|
||||||
|
} catch {
|
||||||
|
// Doc printing failed
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't convert Doc to string, return original
|
||||||
|
return markdown;
|
||||||
|
} catch (error) {
|
||||||
|
// Parsing or printing failed, return original
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { formatMarkdown };
|
||||||
|
|
||||||
113
src/prettier-markdown/format-markdown.ts
Normal file
113
src/prettier-markdown/format-markdown.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Type-safe wrapper for Prettier's markdown formatting
|
||||||
|
*
|
||||||
|
* This module provides a synchronous interface to Prettier's markdown
|
||||||
|
* parser and printer, adapted to work within a Prettier plugin context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ParserOptions } from "prettier";
|
||||||
|
import {
|
||||||
|
getDocPrinter,
|
||||||
|
getDocumentBuilders,
|
||||||
|
getDocumentConstants,
|
||||||
|
getDocumentUtils,
|
||||||
|
// @ts-expect-error - No declaration file
|
||||||
|
} from "./adapter-prettier-internals.js";
|
||||||
|
// @ts-expect-error - No declaration file
|
||||||
|
import { markdown as markdownParser } from "./parser-markdown.js";
|
||||||
|
// @ts-expect-error - No declaration file
|
||||||
|
import printer from "./printer-markdown.js";
|
||||||
|
|
||||||
|
interface MarkdownFormatOptions {
|
||||||
|
printWidth?: number;
|
||||||
|
tabWidth?: number;
|
||||||
|
proseWrap?: "always" | "never" | "preserve";
|
||||||
|
singleQuote?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a markdown string using Prettier's markdown parser and printer
|
||||||
|
*
|
||||||
|
* @param markdown - The markdown string to format
|
||||||
|
* @param options - Formatting options
|
||||||
|
* @returns The formatted markdown string, or the original if formatting fails
|
||||||
|
*/
|
||||||
|
export function formatMarkdown(markdown: string, options: MarkdownFormatOptions = {}): string {
|
||||||
|
if (!markdown || typeof markdown !== "string") {
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = markdown.trim();
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse markdown to AST
|
||||||
|
const ast = markdownParser.parse(trimmed, {
|
||||||
|
originalText: trimmed,
|
||||||
|
filepath: "temp.md",
|
||||||
|
} as ParserOptions);
|
||||||
|
|
||||||
|
// Create an AstPath-like object for the printer
|
||||||
|
const astPath = {
|
||||||
|
getNode: () => ast,
|
||||||
|
stack: [ast],
|
||||||
|
callParent: (fn: (path: any) => any) => fn(astPath),
|
||||||
|
each: (fn: (path: any) => void) => {
|
||||||
|
if (ast.children) {
|
||||||
|
ast.children.forEach((child: any, index: number) => {
|
||||||
|
const childPath = {
|
||||||
|
getNode: () => child,
|
||||||
|
stack: [...astPath.stack, child],
|
||||||
|
index,
|
||||||
|
previous: index > 0 ? ast.children[index - 1] : null,
|
||||||
|
next: index < ast.children.length - 1 ? ast.children[index + 1] : null,
|
||||||
|
parent: ast,
|
||||||
|
isFirst: index === 0,
|
||||||
|
isLast: index === ast.children.length - 1,
|
||||||
|
};
|
||||||
|
fn(childPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a print function for recursive printing
|
||||||
|
const createPrintFn = (path: any): any => {
|
||||||
|
return (printPath: any) => {
|
||||||
|
return printer.print(printPath, options as ParserOptions, createPrintFn);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Print the AST to a Doc object
|
||||||
|
const doc = printer.print(astPath, options as ParserOptions, createPrintFn);
|
||||||
|
|
||||||
|
// Convert Doc to string
|
||||||
|
if (typeof doc === "string") {
|
||||||
|
return doc.trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert Doc object to string using Prettier's doc printer
|
||||||
|
const docPrinter = getDocPrinter();
|
||||||
|
if (docPrinter && typeof docPrinter === "function") {
|
||||||
|
try {
|
||||||
|
const formattedString = docPrinter(doc, {
|
||||||
|
printWidth: options.printWidth || 80,
|
||||||
|
tabWidth: options.tabWidth || 2,
|
||||||
|
useTabs: false,
|
||||||
|
});
|
||||||
|
return typeof formattedString === "string" ? formattedString.trimEnd() : markdown;
|
||||||
|
} catch {
|
||||||
|
// Doc printing failed
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't convert Doc to string, return original
|
||||||
|
return markdown;
|
||||||
|
} catch (error) {
|
||||||
|
// Parsing or printing failed, return original
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/prettier-markdown/get-visitor-keys.js
Normal file
6
src/prettier-markdown/get-visitor-keys.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import createGetVisitorKeys from "../utils/create-get-visitor-keys.js";
|
||||||
|
import visitorKeys from "./visitor-keys.js";
|
||||||
|
|
||||||
|
const getVisitorKeys = createGetVisitorKeys(visitorKeys);
|
||||||
|
|
||||||
|
export default getVisitorKeys;
|
||||||
8
src/prettier-markdown/index.js
Normal file
8
src/prettier-markdown/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import printer from "./printer-markdown.js";
|
||||||
|
|
||||||
|
export const printers = {
|
||||||
|
mdast: printer,
|
||||||
|
};
|
||||||
|
export { default as languages } from "./languages.evaluate.js";
|
||||||
|
export { default as options } from "./options.js";
|
||||||
|
export * as parsers from "./parser-markdown.js";
|
||||||
20
src/prettier-markdown/languages.evaluate.js
Normal file
20
src/prettier-markdown/languages.evaluate.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import * as linguistLanguages from "linguist-languages";
|
||||||
|
import createLanguage from "../utils/create-language.js";
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
createLanguage(linguistLanguages.Markdown, (data) => ({
|
||||||
|
parsers: ["markdown"],
|
||||||
|
vscodeLanguageIds: ["markdown"],
|
||||||
|
filenames: [...data.filenames, "README"],
|
||||||
|
extensions: data.extensions.filter((extension) => extension !== ".mdx"),
|
||||||
|
})),
|
||||||
|
createLanguage(linguistLanguages.Markdown, () => ({
|
||||||
|
name: "MDX",
|
||||||
|
parsers: ["mdx"],
|
||||||
|
vscodeLanguageIds: ["mdx"],
|
||||||
|
filenames: [],
|
||||||
|
extensions: [".mdx"],
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
export default languages;
|
||||||
4
src/prettier-markdown/loc.js
Normal file
4
src/prettier-markdown/loc.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const locStart = (node) => node.position.start.offset;
|
||||||
|
const locEnd = (node) => node.position.end.offset;
|
||||||
|
|
||||||
|
export { locEnd, locStart };
|
||||||
83
src/prettier-markdown/mdx.js
Normal file
83
src/prettier-markdown/mdx.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* modified from https://github.com/mdx-js/mdx/blob/c91b00c673bcf3e7c28b861fd692b69016026c45/packages/remark-mdx/index.js
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017-2018 Compositor and Zeit, Inc.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const IMPORT_REGEX = /^import\s/u;
|
||||||
|
const EXPORT_REGEX = /^export\s/u;
|
||||||
|
const BLOCKS_REGEX = String.raw`[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*|`;
|
||||||
|
const COMMENT_REGEX = /<!---->|<!---?[^>-](?:-?[^-])*-->/u;
|
||||||
|
const ES_COMMENT_REGEX = /^\{\s*\/\*(.*)\*\/\s*\}/u;
|
||||||
|
const EMPTY_NEWLINE = "\n\n";
|
||||||
|
|
||||||
|
const isImport = (text) => IMPORT_REGEX.test(text);
|
||||||
|
const isExport = (text) => EXPORT_REGEX.test(text);
|
||||||
|
const isImportOrExport = (text) => isImport(text) || isExport(text);
|
||||||
|
|
||||||
|
const tokenizeEsSyntax = (eat, value) => {
|
||||||
|
const index = value.indexOf(EMPTY_NEWLINE);
|
||||||
|
const subvalue = index === -1 ? value : value.slice(0, index);
|
||||||
|
|
||||||
|
if (isImportOrExport(subvalue)) {
|
||||||
|
return eat(subvalue)({
|
||||||
|
type: isExport(subvalue) ? "export" : "import",
|
||||||
|
value: subvalue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tokenizeEsSyntax.notInBlock = true;
|
||||||
|
|
||||||
|
tokenizeEsSyntax.locator = (value /* , fromIndex*/) =>
|
||||||
|
isImportOrExport(value) ? -1 : 1;
|
||||||
|
|
||||||
|
const tokenizeEsComment = (eat, value) => {
|
||||||
|
const match = ES_COMMENT_REGEX.exec(value);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return eat(match[0])({
|
||||||
|
type: "esComment",
|
||||||
|
value: match[1].trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tokenizeEsComment.locator = (value, fromIndex) => value.indexOf("{", fromIndex);
|
||||||
|
|
||||||
|
/** @import {Plugin, Settings} from "unified" */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Plugin<[], Settings>}
|
||||||
|
*/
|
||||||
|
const esSyntax = function () {
|
||||||
|
const { Parser } = this;
|
||||||
|
const { blockTokenizers, blockMethods, inlineTokenizers, inlineMethods } =
|
||||||
|
Parser.prototype;
|
||||||
|
|
||||||
|
blockTokenizers.esSyntax = tokenizeEsSyntax;
|
||||||
|
inlineTokenizers.esComment = tokenizeEsComment;
|
||||||
|
|
||||||
|
blockMethods.splice(blockMethods.indexOf("paragraph"), 0, "esSyntax");
|
||||||
|
inlineMethods.splice(inlineMethods.indexOf("text"), 0, "esComment");
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BLOCKS_REGEX, COMMENT_REGEX, esSyntax };
|
||||||
20
src/prettier-markdown/options.js
Normal file
20
src/prettier-markdown/options.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Options for markdown formatting
|
||||||
|
// These match Prettier's default markdown options
|
||||||
|
// Update this file if Prettier's markdown options change
|
||||||
|
const options = {
|
||||||
|
proseWrap: {
|
||||||
|
type: "choice",
|
||||||
|
default: "preserve",
|
||||||
|
choices: [
|
||||||
|
{ value: "always", description: "Wrap prose if it exceeds the print width" },
|
||||||
|
{ value: "never", description: "Don't wrap prose" },
|
||||||
|
{ value: "preserve", description: "Preserve the original wrapping" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
singleQuote: {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default options;
|
||||||
57
src/prettier-markdown/parser-markdown.js
Normal file
57
src/prettier-markdown/parser-markdown.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import footnotes from "remark-footnotes";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import remarkParse from "remark-parse";
|
||||||
|
import unified from "unified";
|
||||||
|
import { locEnd, locStart } from "./loc.js";
|
||||||
|
import { BLOCKS_REGEX, esSyntax } from "./mdx.js";
|
||||||
|
import { hasIgnorePragma, hasPragma } from "./pragma.js";
|
||||||
|
import frontMatter from "./unified-plugins/front-matter.js";
|
||||||
|
import htmlToJsx from "./unified-plugins/html-to-jsx.js";
|
||||||
|
import liquid from "./unified-plugins/liquid.js";
|
||||||
|
import wikiLink from "./unified-plugins/wiki-link.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* based on [MDAST](https://github.com/syntax-tree/mdast) with following modifications:
|
||||||
|
*
|
||||||
|
* 1. restore unescaped character (Text)
|
||||||
|
* 2. merge continuous Texts
|
||||||
|
* 3. replace whitespaces in InlineCode#value with one whitespace
|
||||||
|
* reference: http://spec.commonmark.org/0.25/#example-605
|
||||||
|
* 4. split Text into Sentence
|
||||||
|
*
|
||||||
|
* interface Word { value: string }
|
||||||
|
* interface Whitespace { value: string }
|
||||||
|
* interface Sentence { children: Array<Word | Whitespace> }
|
||||||
|
* interface InlineCode { children: Array<Sentence> }
|
||||||
|
*/
|
||||||
|
function createParse({ isMDX }) {
|
||||||
|
return (text) => {
|
||||||
|
const processor = unified()
|
||||||
|
.use(remarkParse, {
|
||||||
|
commonmark: true,
|
||||||
|
...(isMDX && { blocks: [BLOCKS_REGEX] }),
|
||||||
|
})
|
||||||
|
.use(footnotes)
|
||||||
|
.use(frontMatter)
|
||||||
|
.use(remarkMath)
|
||||||
|
.use(isMDX ? esSyntax : noop)
|
||||||
|
.use(liquid)
|
||||||
|
.use(isMDX ? htmlToJsx : noop)
|
||||||
|
.use(wikiLink);
|
||||||
|
return processor.run(processor.parse(text));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
|
const baseParser = {
|
||||||
|
astFormat: "mdast",
|
||||||
|
hasPragma,
|
||||||
|
hasIgnorePragma,
|
||||||
|
locStart,
|
||||||
|
locEnd,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markdown = { ...baseParser, parse: createParse({ isMDX: false }) };
|
||||||
|
export const mdx = { ...baseParser, parse: createParse({ isMDX: true }) };
|
||||||
|
export { markdown as remark };
|
||||||
25
src/prettier-markdown/pragma.js
Normal file
25
src/prettier-markdown/pragma.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { parseFrontMatter } from "../main/front-matter/index.js";
|
||||||
|
import {
|
||||||
|
FORMAT_PRAGMA_TO_INSERT,
|
||||||
|
MARKDOWN_HAS_IGNORE_PRAGMA_REGEXP,
|
||||||
|
MARKDOWN_HAS_PRAGMA_REGEXP,
|
||||||
|
} from "../utils/pragma/pragma.evaluate.js";
|
||||||
|
|
||||||
|
const hasPragma = (text) =>
|
||||||
|
parseFrontMatter(text).content.trimStart().match(MARKDOWN_HAS_PRAGMA_REGEXP)
|
||||||
|
?.index === 0;
|
||||||
|
|
||||||
|
const hasIgnorePragma = (text) =>
|
||||||
|
parseFrontMatter(text)
|
||||||
|
.content.trimStart()
|
||||||
|
.match(MARKDOWN_HAS_IGNORE_PRAGMA_REGEXP)?.index === 0;
|
||||||
|
|
||||||
|
const insertPragma = (text) => {
|
||||||
|
const { frontMatter } = parseFrontMatter(text);
|
||||||
|
const pragma = `<!-- @${FORMAT_PRAGMA_TO_INSERT} -->`;
|
||||||
|
return frontMatter
|
||||||
|
? `${frontMatter.raw}\n\n${pragma}\n\n${text.slice(frontMatter.end.index)}`
|
||||||
|
: `${pragma}\n\n${text}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { hasIgnorePragma, hasPragma, insertPragma };
|
||||||
55
src/prettier-markdown/print-paragraph.js
Normal file
55
src/prettier-markdown/print-paragraph.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { fill } from "../document/builders.js";
|
||||||
|
import { DOC_TYPE_ARRAY, DOC_TYPE_FILL } from "../document/constants.js";
|
||||||
|
import { getDocType } from "../document/utils.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @import AstPath from "../common/ast-path.js"
|
||||||
|
* @import {Doc} from "../document/builders.js"
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AstPath} path
|
||||||
|
* @param {*} options
|
||||||
|
* @param {*} print
|
||||||
|
* @returns {Doc}
|
||||||
|
*/
|
||||||
|
function printParagraph(path, options, print) {
|
||||||
|
const parts = path.map(print, "children");
|
||||||
|
return flattenFill(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Doc[]} docs
|
||||||
|
* @returns {Doc}
|
||||||
|
*/
|
||||||
|
function flattenFill(docs) {
|
||||||
|
/*
|
||||||
|
* We assume parts always meet following conditions:
|
||||||
|
* - parts.length is odd
|
||||||
|
* - odd elements are line-like doc that comes from odd element off inner fill
|
||||||
|
*/
|
||||||
|
/** @type {Doc[]} */
|
||||||
|
const parts = [""];
|
||||||
|
|
||||||
|
(function rec(/** @type {*} */ docArray) {
|
||||||
|
for (const doc of docArray) {
|
||||||
|
const docType = getDocType(doc);
|
||||||
|
if (docType === DOC_TYPE_ARRAY) {
|
||||||
|
rec(doc);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let head = doc;
|
||||||
|
let rest = [];
|
||||||
|
if (docType === DOC_TYPE_FILL) {
|
||||||
|
[head, ...rest] = doc.parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.push([parts.pop(), head], ...rest);
|
||||||
|
}
|
||||||
|
})(docs);
|
||||||
|
|
||||||
|
return fill(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { printParagraph };
|
||||||
256
src/prettier-markdown/print-preprocess.js
Normal file
256
src/prettier-markdown/print-preprocess.js
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import htmlWhitespaceUtils from "../utils/html-whitespace-utils.js";
|
||||||
|
import { getOrderedListItemInfo, mapAst, splitText } from "./utils.js";
|
||||||
|
|
||||||
|
// 0x0 ~ 0x10ffff
|
||||||
|
const isSingleCharRegex = /^\\?.$/su;
|
||||||
|
const isNewLineBlockquoteRegex = /^\n *>[ >]*$/u;
|
||||||
|
|
||||||
|
function preprocess(ast, options) {
|
||||||
|
ast = restoreUnescapedCharacter(ast, options);
|
||||||
|
ast = mergeContinuousTexts(ast);
|
||||||
|
ast = transformIndentedCodeblockAndMarkItsParentList(ast, options);
|
||||||
|
ast = markAlignedList(ast, options);
|
||||||
|
ast = splitTextIntoSentences(ast);
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreUnescapedCharacter(ast, options) {
|
||||||
|
return mapAst(ast, (node) => {
|
||||||
|
if (node.type !== "text") {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value } = node;
|
||||||
|
|
||||||
|
if (
|
||||||
|
value === "*" ||
|
||||||
|
value === "_" || // handle these cases in printer
|
||||||
|
!isSingleCharRegex.test(value) ||
|
||||||
|
node.position.end.offset - node.position.start.offset === value.length
|
||||||
|
) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = options.originalText.slice(
|
||||||
|
node.position.start.offset,
|
||||||
|
node.position.end.offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNewLineBlockquoteRegex.test(text)) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...node, value: text };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeChildren(ast, shouldMerge, mergeNode) {
|
||||||
|
return mapAst(ast, (node) => {
|
||||||
|
if (!node.children) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = [];
|
||||||
|
let lastChild;
|
||||||
|
let changed;
|
||||||
|
for (let child of node.children) {
|
||||||
|
if (lastChild && shouldMerge(lastChild, child)) {
|
||||||
|
child = mergeNode(lastChild, child);
|
||||||
|
// Replace the previous node
|
||||||
|
children.splice(-1, 1, child);
|
||||||
|
changed ||= true;
|
||||||
|
} else {
|
||||||
|
children.push(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastChild = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed ? { ...node, children } : node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeContinuousTexts(ast) {
|
||||||
|
return mergeChildren(
|
||||||
|
ast,
|
||||||
|
(prevNode, node) => prevNode.type === "text" && node.type === "text",
|
||||||
|
(prevNode, node) => ({
|
||||||
|
type: "text",
|
||||||
|
value: prevNode.value + node.value,
|
||||||
|
position: {
|
||||||
|
start: prevNode.position.start,
|
||||||
|
end: node.position.end,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitTextIntoSentences(ast) {
|
||||||
|
return mapAst(ast, (node, index, [parentNode]) => {
|
||||||
|
if (node.type !== "text") {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { value } = node;
|
||||||
|
|
||||||
|
if (parentNode.type === "paragraph") {
|
||||||
|
// CommonMark doesn't remove trailing/leading \f, but it should be
|
||||||
|
// removed in the HTML rendering process
|
||||||
|
if (index === 0) {
|
||||||
|
value = htmlWhitespaceUtils.trimStart(value);
|
||||||
|
}
|
||||||
|
if (index === parentNode.children.length - 1) {
|
||||||
|
value = htmlWhitespaceUtils.trimEnd(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "sentence",
|
||||||
|
position: node.position,
|
||||||
|
children: splitText(value),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformIndentedCodeblockAndMarkItsParentList(ast, options) {
|
||||||
|
return mapAst(ast, (node, index, parentStack) => {
|
||||||
|
if (node.type === "code") {
|
||||||
|
// the first char may point to `\n`, e.g. `\n\t\tbar`, just ignore it
|
||||||
|
const isIndented = /^\n?(?: {4,}|\t)/u.test(
|
||||||
|
options.originalText.slice(
|
||||||
|
node.position.start.offset,
|
||||||
|
node.position.end.offset,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
node.isIndented = isIndented;
|
||||||
|
|
||||||
|
if (isIndented) {
|
||||||
|
for (let i = 0; i < parentStack.length; i++) {
|
||||||
|
const parent = parentStack[i];
|
||||||
|
|
||||||
|
// no need to check checked items
|
||||||
|
if (parent.hasIndentedCodeblock) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.type === "list") {
|
||||||
|
parent.hasIndentedCodeblock = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function markAlignedList(ast, options) {
|
||||||
|
return mapAst(ast, (node, index, parentStack) => {
|
||||||
|
if (node.type === "list" && node.children.length > 0) {
|
||||||
|
// if one of its parents is not aligned, it's not possible to be aligned in sub-lists
|
||||||
|
for (let i = 0; i < parentStack.length; i++) {
|
||||||
|
const parent = parentStack[i];
|
||||||
|
if (parent.type === "list" && !parent.isAligned) {
|
||||||
|
node.isAligned = false;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.isAligned = isAligned(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getListItemStart(listItem) {
|
||||||
|
return listItem.children.length === 0
|
||||||
|
? -1
|
||||||
|
: listItem.children[0].position.start.column - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAligned(list) {
|
||||||
|
if (!list.ordered) {
|
||||||
|
/**
|
||||||
|
* - 123
|
||||||
|
* - 123
|
||||||
|
*/
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstItem, secondItem] = list.children;
|
||||||
|
|
||||||
|
const firstInfo = getOrderedListItemInfo(firstItem, options);
|
||||||
|
|
||||||
|
if (firstInfo.leadingSpaces.length > 1) {
|
||||||
|
/**
|
||||||
|
* 1. 123
|
||||||
|
*
|
||||||
|
* 1. 123
|
||||||
|
* 1. 123
|
||||||
|
*/
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstStart = getListItemStart(firstItem);
|
||||||
|
|
||||||
|
if (firstStart === -1) {
|
||||||
|
/**
|
||||||
|
* 1.
|
||||||
|
*
|
||||||
|
* 1.
|
||||||
|
* 1.
|
||||||
|
*/
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.children.length === 1) {
|
||||||
|
/**
|
||||||
|
* aligned:
|
||||||
|
*
|
||||||
|
* 11. 123
|
||||||
|
*
|
||||||
|
* not aligned:
|
||||||
|
*
|
||||||
|
* 1. 123
|
||||||
|
*/
|
||||||
|
return firstStart % options.tabWidth === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondStart = getListItemStart(secondItem);
|
||||||
|
|
||||||
|
if (firstStart !== secondStart) {
|
||||||
|
/**
|
||||||
|
* 11. 123
|
||||||
|
* 1. 123
|
||||||
|
*
|
||||||
|
* 1. 123
|
||||||
|
* 11. 123
|
||||||
|
*/
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstStart % options.tabWidth === 0) {
|
||||||
|
/**
|
||||||
|
* 11. 123
|
||||||
|
* 12. 123
|
||||||
|
*/
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* aligned:
|
||||||
|
*
|
||||||
|
* 11. 123
|
||||||
|
* 1. 123
|
||||||
|
*
|
||||||
|
* not aligned:
|
||||||
|
*
|
||||||
|
* 1. 123
|
||||||
|
* 2. 123
|
||||||
|
*/
|
||||||
|
const secondInfo = getOrderedListItemInfo(secondItem, options);
|
||||||
|
return secondInfo.leadingSpaces.length > 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default preprocess;
|
||||||
37
src/prettier-markdown/print-sentence.js
Normal file
37
src/prettier-markdown/print-sentence.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @import AstPath from "../common/ast-path.js"
|
||||||
|
* @import {Doc} from "../document/builders.js"
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { fill } from "../document/builders.js";
|
||||||
|
import { DOC_TYPE_STRING } from "../document/constants.js";
|
||||||
|
import { getDocType } from "../document/utils.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AstPath} path
|
||||||
|
* @param {*} print
|
||||||
|
* @returns {Doc}
|
||||||
|
*/
|
||||||
|
function printSentence(path, print) {
|
||||||
|
/** @type {Doc[]} */
|
||||||
|
const parts = [""];
|
||||||
|
|
||||||
|
path.each(() => {
|
||||||
|
const { node } = path;
|
||||||
|
const doc = print();
|
||||||
|
switch (node.type) {
|
||||||
|
case "whitespace":
|
||||||
|
if (getDocType(doc) !== DOC_TYPE_STRING) {
|
||||||
|
parts.push(doc, "");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// fallthrough
|
||||||
|
default:
|
||||||
|
parts.push([parts.pop(), doc]);
|
||||||
|
}
|
||||||
|
}, "children");
|
||||||
|
|
||||||
|
return fill(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { printSentence };
|
||||||
266
src/prettier-markdown/print-whitespace.js
Normal file
266
src/prettier-markdown/print-whitespace.js
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
import { hardline, line, softline } from "../document/builders.js";
|
||||||
|
import {
|
||||||
|
KIND_CJ_LETTER,
|
||||||
|
KIND_CJK_PUNCTUATION,
|
||||||
|
KIND_K_LETTER,
|
||||||
|
KIND_NON_CJK,
|
||||||
|
} from "./utils.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @import {WordNode, WhitespaceValue, WordKind} from "./utils.js"
|
||||||
|
* @import AstPath from "../common/ast-path.js"
|
||||||
|
* @typedef {"always" | "never" | "preserve"} ProseWrap
|
||||||
|
* @typedef {{ next?: WordNode | null, previous?: WordNode | null }}
|
||||||
|
* AdjacentNodes Nodes adjacent to a `whitespace` node. Are always of type
|
||||||
|
* `word`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SINGLE_LINE_NODE_TYPES = new Set([
|
||||||
|
"heading",
|
||||||
|
"tableCell",
|
||||||
|
"link",
|
||||||
|
"wikiLink",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A line break between a character from this set and CJ can be converted to a
|
||||||
|
* space. Includes only ASCII punctuation marks for now.
|
||||||
|
*/
|
||||||
|
const lineBreakBetweenTheseAndCJConvertsToSpace = new Set(
|
||||||
|
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~",
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the preferred style of spacing between Chinese or Japanese and non-CJK
|
||||||
|
* characters in the parent `sentence` node.
|
||||||
|
*
|
||||||
|
* @param {AstPath} path
|
||||||
|
* @returns {boolean} `true` if Space tends to be inserted between CJ and
|
||||||
|
* non-CJK, `false` otherwise.
|
||||||
|
*/
|
||||||
|
function isInSentenceWithCJSpaces({ parent: sentenceNode }) {
|
||||||
|
if (sentenceNode.usesCJSpaces === undefined) {
|
||||||
|
const stats = { " ": 0, "": 0 };
|
||||||
|
const { children } = sentenceNode;
|
||||||
|
|
||||||
|
for (let i = 1; i < children.length - 1; ++i) {
|
||||||
|
const node = children[i];
|
||||||
|
if (
|
||||||
|
node.type === "whitespace" &&
|
||||||
|
(node.value === " " || node.value === "")
|
||||||
|
) {
|
||||||
|
const previousKind = children[i - 1].kind;
|
||||||
|
const nextKind = children[i + 1].kind;
|
||||||
|
if (
|
||||||
|
(previousKind === KIND_CJ_LETTER && nextKind === KIND_NON_CJK) ||
|
||||||
|
(previousKind === KIND_NON_CJK && nextKind === KIND_CJ_LETTER)
|
||||||
|
) {
|
||||||
|
++stats[node.value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject a property to cache the result.
|
||||||
|
sentenceNode.usesCJSpaces = stats[" "] > stats[""];
|
||||||
|
}
|
||||||
|
|
||||||
|
return sentenceNode.usesCJSpaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the given `"\n"` node can be converted to a space.
|
||||||
|
*
|
||||||
|
* For example, if you would like to squash English text
|
||||||
|
*
|
||||||
|
* "You might want\nto use Prettier."
|
||||||
|
*
|
||||||
|
* into a single line, you would replace `"\n"` with `" "`:
|
||||||
|
*
|
||||||
|
* "You might want to use Prettier."
|
||||||
|
*
|
||||||
|
* However, Chinese and Japanese don't use U+0020 Space to divide words, so line
|
||||||
|
* breaks shouldn't be replaced with spaces for those languages.
|
||||||
|
*
|
||||||
|
* PRs are welcome to support line breaking rules for other languages.
|
||||||
|
*
|
||||||
|
* @param {AstPath} path
|
||||||
|
* @param {boolean} isLink
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function lineBreakCanBeConvertedToSpace(path, isLink) {
|
||||||
|
if (isLink) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {AdjacentNodes} */
|
||||||
|
const { previous, next } = path;
|
||||||
|
|
||||||
|
// e.g. " \nletter"
|
||||||
|
if (!previous || !next) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousKind = previous.kind;
|
||||||
|
const nextKind = next.kind;
|
||||||
|
|
||||||
|
if (
|
||||||
|
// "\n" between non-CJK or Korean characters always can be converted to a
|
||||||
|
// space. Korean Hangul simulates Latin words. See
|
||||||
|
// https://github.com/prettier/prettier/issues/6516
|
||||||
|
(isNonCJKOrKoreanLetter(previousKind) &&
|
||||||
|
isNonCJKOrKoreanLetter(nextKind)) ||
|
||||||
|
// Han & Hangul: same way preferred
|
||||||
|
(previousKind === KIND_K_LETTER && nextKind === KIND_CJ_LETTER) ||
|
||||||
|
(nextKind === KIND_K_LETTER && previousKind === KIND_CJ_LETTER)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not convert \n to a space:
|
||||||
|
if (
|
||||||
|
// around CJK punctuation
|
||||||
|
previousKind === KIND_CJK_PUNCTUATION ||
|
||||||
|
nextKind === KIND_CJK_PUNCTUATION ||
|
||||||
|
// between CJ
|
||||||
|
(previousKind === KIND_CJ_LETTER && nextKind === KIND_CJ_LETTER)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rest of this function deals only with line breaks between CJ and
|
||||||
|
// non-CJK characters.
|
||||||
|
|
||||||
|
// Convert a line break between CJ and certain non-letter characters (e.g.
|
||||||
|
// ASCII punctuation) to a space.
|
||||||
|
//
|
||||||
|
// E.g. :::\n句子句子句子\n::: → ::: 句子句子句子 :::
|
||||||
|
//
|
||||||
|
// Note: line breaks like "(\n句子句子\n)" or "句子\n." are suppressed in
|
||||||
|
// `isBreakable(...)`.
|
||||||
|
if (
|
||||||
|
lineBreakBetweenTheseAndCJConvertsToSpace.has(next.value[0]) ||
|
||||||
|
lineBreakBetweenTheseAndCJConvertsToSpace.has(previous.value.at(-1))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converting a line break between CJ and non-ASCII punctuation to a space is
|
||||||
|
// undesired in many cases. PRs are welcome to fine-tune this logic.
|
||||||
|
//
|
||||||
|
// Examples where \n must not be converted to a space:
|
||||||
|
//
|
||||||
|
// 1. "〜" (U+301C, belongs to Pd) in
|
||||||
|
//
|
||||||
|
// "ア〜\nエの中から1つ選べ。"
|
||||||
|
//
|
||||||
|
// 2. "…" (U+2026, belongs to Po) in
|
||||||
|
//
|
||||||
|
// "これはひどい……\nなんと汚いコミットログなんだ……"
|
||||||
|
if (previous.hasTrailingPunctuation || next.hasLeadingPunctuation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the sentence uses the style with spaces between CJ and non-CJK, "\n" can
|
||||||
|
// be converted to a space.
|
||||||
|
return isInSentenceWithCJSpaces(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {WordKind | undefined} kind
|
||||||
|
* @returns {boolean} `true` if `kind` is Korean letter or non-CJK
|
||||||
|
*/
|
||||||
|
function isNonCJKOrKoreanLetter(kind) {
|
||||||
|
return kind === KIND_NON_CJK || kind === KIND_K_LETTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether whitespace can be printed as a line break.
|
||||||
|
*
|
||||||
|
* @param {AstPath} path
|
||||||
|
* @param {WhitespaceValue} value
|
||||||
|
* @param {ProseWrap} proseWrap
|
||||||
|
* @param {boolean} isLink
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isBreakable(path, value, proseWrap, isLink) {
|
||||||
|
if (
|
||||||
|
proseWrap !== "always" ||
|
||||||
|
path.hasAncestor((node) => SINGLE_LINE_NODE_TYPES.has(node.type))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLink) {
|
||||||
|
return value !== "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {AdjacentNodes} */
|
||||||
|
const { previous, next } = path;
|
||||||
|
|
||||||
|
// [1]: We will make a breaking change to the rule to convert spaces between
|
||||||
|
// a Chinese or Japanese character and another character in the future.
|
||||||
|
// Such a space must have been always interchangeable with a line break.
|
||||||
|
// https://wpt.fyi/results/css/css-text/line-breaking?label=master&label=experimental&aligned&q=segment-break-transformation-rules-
|
||||||
|
// [2]: we should not break lines even between Chinese/Japanese characters because Chrome & Safari replaces "\n" between such characters with " " now.
|
||||||
|
// [3]: Hangul (Korean) must simulate Latin words; see https://github.com/prettier/prettier/issues/6516
|
||||||
|
// [printable][""][Hangul] & vice versa => Don't break
|
||||||
|
// [printable][\n][Hangul] will be interchangeable to [printable][" "][Hangul] in the future
|
||||||
|
// (will be compatible with Firefox's behavior)
|
||||||
|
|
||||||
|
if (!previous || !next) {
|
||||||
|
// empty side is Latin ASCII symbol (e.g. *, [, ], or `)
|
||||||
|
// value is " " or "\n" (not "")
|
||||||
|
// [1] & [2]? No, it's the only exception because " " & "\n" have been always interchangeable only here
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
// [1] & [2] & [3]
|
||||||
|
// At least either of previous or next is non-Latin (=CJK)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
// See the same product terms as the following in lineBreakCanBeConvertedToSpace
|
||||||
|
// The behavior is consistent between browsers and Prettier in that line breaks between Korean and Chinese/Japanese letters are equivalent to spaces.
|
||||||
|
// Currently, [CJK punctuation][\n][Hangul] is interchangeable to [CJK punctuation][""][Hangul],
|
||||||
|
// but this is not compatible with Firefox's behavior.
|
||||||
|
// Will be changed to [CJK punctuation][" "][Hangul] in the future
|
||||||
|
(previous.kind === KIND_K_LETTER && next.kind === KIND_CJ_LETTER) ||
|
||||||
|
(next.kind === KIND_K_LETTER && previous.kind === KIND_CJ_LETTER)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [1] & [2]
|
||||||
|
if (previous.isCJ || next.isCJ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AstPath} path
|
||||||
|
* @param {WhitespaceValue} value
|
||||||
|
* @param {ProseWrap} proseWrap
|
||||||
|
* @param {boolean} [isLink] Special mode of (un)wrapping that preserves the
|
||||||
|
* normalized form of link labels. https://spec.commonmark.org/0.30/#matches
|
||||||
|
*/
|
||||||
|
function printWhitespace(path, value, proseWrap, isLink) {
|
||||||
|
if (proseWrap === "preserve" && value === "\n") {
|
||||||
|
return hardline;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canBeSpace =
|
||||||
|
value === " " ||
|
||||||
|
(value === "\n" && lineBreakCanBeConvertedToSpace(path, isLink));
|
||||||
|
|
||||||
|
if (isBreakable(path, value, proseWrap, isLink)) {
|
||||||
|
return canBeSpace ? line : softline;
|
||||||
|
}
|
||||||
|
|
||||||
|
return canBeSpace ? " " : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export { printWhitespace };
|
||||||
81
src/prettier-markdown/print/table.js
Normal file
81
src/prettier-markdown/print/table.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
breakParent,
|
||||||
|
group,
|
||||||
|
hardlineWithoutBreakParent,
|
||||||
|
ifBreak,
|
||||||
|
join,
|
||||||
|
} from "../../document/builders.js";
|
||||||
|
import { printDocToString } from "../../document/printer.js";
|
||||||
|
import getStringWidth from "../../utils/get-string-width.js";
|
||||||
|
|
||||||
|
function printTable(path, options, print) {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
const columnMaxWidths = [];
|
||||||
|
// { [rowIndex: number]: { [columnIndex: number]: {text: string, width: number} } }
|
||||||
|
const contents = path.map(
|
||||||
|
() =>
|
||||||
|
path.map(({ index: columnIndex }) => {
|
||||||
|
const text = printDocToString(print(), options).formatted;
|
||||||
|
const width = getStringWidth(text);
|
||||||
|
columnMaxWidths[columnIndex] = Math.max(
|
||||||
|
columnMaxWidths[columnIndex] ?? 3, // minimum width = 3 (---, :--, :-:, --:)
|
||||||
|
width,
|
||||||
|
);
|
||||||
|
return { text, width };
|
||||||
|
}, "children"),
|
||||||
|
"children",
|
||||||
|
);
|
||||||
|
|
||||||
|
const alignedTable = printTableContents(/* isCompact */ false);
|
||||||
|
if (options.proseWrap !== "never") {
|
||||||
|
return [breakParent, alignedTable];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only if the --prose-wrap never is set and it exceeds the print width.
|
||||||
|
const compactTable = printTableContents(/* isCompact */ true);
|
||||||
|
return [breakParent, group(ifBreak(compactTable, alignedTable))];
|
||||||
|
|
||||||
|
function printTableContents(isCompact) {
|
||||||
|
return join(
|
||||||
|
hardlineWithoutBreakParent,
|
||||||
|
[
|
||||||
|
printRow(contents[0], isCompact),
|
||||||
|
printAlign(isCompact),
|
||||||
|
...contents
|
||||||
|
.slice(1)
|
||||||
|
.map((rowContents) => printRow(rowContents, isCompact)),
|
||||||
|
].map((columns) => `| ${columns.join(" | ")} |`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printAlign(isCompact) {
|
||||||
|
return columnMaxWidths.map((width, index) => {
|
||||||
|
const align = node.align[index];
|
||||||
|
const first = align === "center" || align === "left" ? ":" : "-";
|
||||||
|
const last = align === "center" || align === "right" ? ":" : "-";
|
||||||
|
const middle = isCompact ? "-" : "-".repeat(width - 2);
|
||||||
|
return `${first}${middle}${last}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function printRow(columns, isCompact) {
|
||||||
|
return columns.map(({ text, width }, columnIndex) => {
|
||||||
|
if (isCompact) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
const spaces = columnMaxWidths[columnIndex] - width;
|
||||||
|
const align = node.align[columnIndex];
|
||||||
|
let before = 0;
|
||||||
|
if (align === "right") {
|
||||||
|
before = spaces;
|
||||||
|
} else if (align === "center") {
|
||||||
|
before = Math.floor(spaces / 2);
|
||||||
|
}
|
||||||
|
const after = spaces - before;
|
||||||
|
return `${" ".repeat(before)}${text}${" ".repeat(after)}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { printTable };
|
||||||
810
src/prettier-markdown/printer-markdown.js
Normal file
810
src/prettier-markdown/printer-markdown.js
Normal file
@@ -0,0 +1,810 @@
|
|||||||
|
import collapseWhiteSpace from "collapse-white-space";
|
||||||
|
import escapeStringRegexp from "escape-string-regexp";
|
||||||
|
import {
|
||||||
|
align,
|
||||||
|
fill,
|
||||||
|
group,
|
||||||
|
hardline,
|
||||||
|
indent,
|
||||||
|
line,
|
||||||
|
literalline,
|
||||||
|
markAsRoot,
|
||||||
|
softline,
|
||||||
|
} from "./adapter-document-builders.js";
|
||||||
|
import { DOC_TYPE_STRING } from "./adapter-document-constants.js";
|
||||||
|
import { getDocType, replaceEndOfLine } from "./adapter-document-utils.js";
|
||||||
|
import {
|
||||||
|
getMaxContinuousCount,
|
||||||
|
getMinNotPresentContinuousCount,
|
||||||
|
getPreferredQuote,
|
||||||
|
UnexpectedNodeError,
|
||||||
|
} from "./adapter-prettier-utils.js";
|
||||||
|
import clean from "./clean.js";
|
||||||
|
import { PUNCTUATION_REGEXP } from "./constants.evaluate.js";
|
||||||
|
import embed from "./embed.js";
|
||||||
|
import getVisitorKeys from "./get-visitor-keys.js";
|
||||||
|
import { locEnd, locStart } from "./loc.js";
|
||||||
|
import { insertPragma } from "./pragma.js";
|
||||||
|
import { printTable } from "./print/table.js";
|
||||||
|
import { printParagraph } from "./print-paragraph.js";
|
||||||
|
import preprocess from "./print-preprocess.js";
|
||||||
|
import { printSentence } from "./print-sentence.js";
|
||||||
|
import { printWhitespace } from "./print-whitespace.js";
|
||||||
|
import {
|
||||||
|
getFencedCodeBlockValue,
|
||||||
|
hasGitDiffFriendlyOrderedList,
|
||||||
|
INLINE_NODE_TYPES,
|
||||||
|
INLINE_NODE_WRAPPER_TYPES,
|
||||||
|
isAutolink,
|
||||||
|
splitText,
|
||||||
|
} from "./utils.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @import {Doc} from "../document/builders.js"
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SIBLING_NODE_TYPES = new Set(["listItem", "definition"]);
|
||||||
|
|
||||||
|
function prevOrNextWord(path) {
|
||||||
|
const { previous, next } = path;
|
||||||
|
const hasPrevOrNextWord =
|
||||||
|
(previous?.type === "sentence" &&
|
||||||
|
previous.children.at(-1)?.type === "word" &&
|
||||||
|
!previous.children.at(-1).hasTrailingPunctuation) ||
|
||||||
|
(next?.type === "sentence" &&
|
||||||
|
next.children[0]?.type === "word" &&
|
||||||
|
!next.children[0].hasLeadingPunctuation);
|
||||||
|
return hasPrevOrNextWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
function genericPrint(path, options, print) {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
if (shouldRemainTheSameContent(path)) {
|
||||||
|
/*
|
||||||
|
* We assume parts always meet following conditions:
|
||||||
|
* - parts.length is odd
|
||||||
|
* - odd (0-indexed) elements are line-like doc
|
||||||
|
*/
|
||||||
|
/** @type {Doc[]} */
|
||||||
|
const parts = [""];
|
||||||
|
const textsNodes = splitText(
|
||||||
|
options.originalText.slice(
|
||||||
|
node.position.start.offset,
|
||||||
|
node.position.end.offset,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (const node of textsNodes) {
|
||||||
|
if (node.type === "word") {
|
||||||
|
parts.push([parts.pop(), node.value]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const doc = printWhitespace(path, node.value, options.proseWrap, true);
|
||||||
|
if (getDocType(doc) === DOC_TYPE_STRING) {
|
||||||
|
parts.push([parts.pop(), doc]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// In this path, doc is line. To meet the condition, we need additional element "".
|
||||||
|
parts.push(doc, "");
|
||||||
|
}
|
||||||
|
return fill(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (node.type) {
|
||||||
|
case "root":
|
||||||
|
/* c8 ignore next 3 */
|
||||||
|
if (node.children.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return [printRoot(path, options, print), hardline];
|
||||||
|
case "paragraph":
|
||||||
|
return printParagraph(path, options, print);
|
||||||
|
case "sentence":
|
||||||
|
return printSentence(path, print);
|
||||||
|
case "word": {
|
||||||
|
let escapedValue = node.value
|
||||||
|
.replaceAll("*", String.raw`\*`) // escape all `*`
|
||||||
|
.replaceAll(
|
||||||
|
new RegExp(
|
||||||
|
[
|
||||||
|
`(^|${PUNCTUATION_REGEXP.source})(_+)`,
|
||||||
|
`(_+)(${PUNCTUATION_REGEXP.source}|$)`,
|
||||||
|
].join("|"),
|
||||||
|
"gu",
|
||||||
|
),
|
||||||
|
(_, text1, underscore1, underscore2, text2) =>
|
||||||
|
(underscore1
|
||||||
|
? `${text1}${underscore1}`
|
||||||
|
: `${underscore2}${text2}`
|
||||||
|
).replaceAll("_", String.raw`\_`),
|
||||||
|
); // escape all `_` except concating with non-punctuation, e.g. `1_2_3` is not considered emphasis
|
||||||
|
|
||||||
|
const isFirstSentence = (node, name, index) =>
|
||||||
|
node.type === "sentence" && index === 0;
|
||||||
|
const isLastChildAutolink = (node, name, index) =>
|
||||||
|
isAutolink(node.children[index - 1]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
escapedValue !== node.value &&
|
||||||
|
(path.match(undefined, isFirstSentence, isLastChildAutolink) ||
|
||||||
|
path.match(
|
||||||
|
undefined,
|
||||||
|
isFirstSentence,
|
||||||
|
(node, name, index) => node.type === "emphasis" && index === 0,
|
||||||
|
isLastChildAutolink,
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
// backslash is parsed as part of autolinks, so we need to remove it
|
||||||
|
escapedValue = escapedValue.replace(/^(\\?[*_])+/u, (prefix) =>
|
||||||
|
prefix.replaceAll("\\", ""),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return escapedValue;
|
||||||
|
}
|
||||||
|
case "whitespace": {
|
||||||
|
const { next } = path;
|
||||||
|
|
||||||
|
const proseWrap =
|
||||||
|
// leading char that may cause different syntax
|
||||||
|
next && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/u.test(next.value)
|
||||||
|
? "never"
|
||||||
|
: options.proseWrap;
|
||||||
|
|
||||||
|
return printWhitespace(path, node.value, proseWrap);
|
||||||
|
}
|
||||||
|
case "emphasis": {
|
||||||
|
let style;
|
||||||
|
if (isAutolink(node.children[0])) {
|
||||||
|
style = options.originalText[node.position.start.offset];
|
||||||
|
} else {
|
||||||
|
const hasPrevOrNextWord = prevOrNextWord(path); // `1*2*3` is considered emphasis but `1_2_3` is not
|
||||||
|
const inStrongAndHasPrevOrNextWord = // `1***2***3` is considered strong emphasis but `1**_2_**3` is not
|
||||||
|
path.callParent(
|
||||||
|
({ node }) => node.type === "strong" && prevOrNextWord(path),
|
||||||
|
);
|
||||||
|
style =
|
||||||
|
hasPrevOrNextWord ||
|
||||||
|
inStrongAndHasPrevOrNextWord ||
|
||||||
|
path.hasAncestor((node) => node.type === "emphasis")
|
||||||
|
? "*"
|
||||||
|
: "_";
|
||||||
|
}
|
||||||
|
return [style, printChildren(path, options, print), style];
|
||||||
|
}
|
||||||
|
case "strong":
|
||||||
|
return ["**", printChildren(path, options, print), "**"];
|
||||||
|
case "delete":
|
||||||
|
return ["~~", printChildren(path, options, print), "~~"];
|
||||||
|
case "inlineCode": {
|
||||||
|
const code =
|
||||||
|
options.proseWrap === "preserve"
|
||||||
|
? node.value
|
||||||
|
: node.value.replaceAll("\n", " ");
|
||||||
|
const backtickCount = getMinNotPresentContinuousCount(code, "`");
|
||||||
|
const backtickString = "`".repeat(backtickCount);
|
||||||
|
const padding =
|
||||||
|
code.startsWith("`") ||
|
||||||
|
code.endsWith("`") ||
|
||||||
|
(/^[\n ]/u.test(code) && /[\n ]$/u.test(code) && /[^\n ]/u.test(code))
|
||||||
|
? " "
|
||||||
|
: "";
|
||||||
|
return [backtickString, padding, code, padding, backtickString];
|
||||||
|
}
|
||||||
|
case "wikiLink": {
|
||||||
|
let contents = "";
|
||||||
|
if (options.proseWrap === "preserve") {
|
||||||
|
contents = node.value;
|
||||||
|
} else {
|
||||||
|
contents = node.value.replaceAll(/[\t\n]+/gu, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ["[[", contents, "]]"];
|
||||||
|
}
|
||||||
|
case "link":
|
||||||
|
switch (options.originalText[node.position.start.offset]) {
|
||||||
|
case "<": {
|
||||||
|
const mailto = "mailto:";
|
||||||
|
const url =
|
||||||
|
// <hello@example.com> is parsed as { url: "mailto:hello@example.com" }
|
||||||
|
node.url.startsWith(mailto) &&
|
||||||
|
options.originalText.slice(
|
||||||
|
node.position.start.offset + 1,
|
||||||
|
node.position.start.offset + 1 + mailto.length,
|
||||||
|
) !== mailto
|
||||||
|
? node.url.slice(mailto.length)
|
||||||
|
: node.url;
|
||||||
|
return ["<", url, ">"];
|
||||||
|
}
|
||||||
|
case "[":
|
||||||
|
return [
|
||||||
|
"[",
|
||||||
|
printChildren(path, options, print),
|
||||||
|
"](",
|
||||||
|
printUrl(node.url, ")"),
|
||||||
|
printTitle(node.title, options),
|
||||||
|
")",
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return options.originalText.slice(
|
||||||
|
node.position.start.offset,
|
||||||
|
node.position.end.offset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "image":
|
||||||
|
return [
|
||||||
|
""),
|
||||||
|
printTitle(node.title, options),
|
||||||
|
")",
|
||||||
|
];
|
||||||
|
case "blockquote":
|
||||||
|
return ["> ", align("> ", printChildren(path, options, print))];
|
||||||
|
case "heading":
|
||||||
|
return [
|
||||||
|
"#".repeat(node.depth) + " ",
|
||||||
|
printChildren(path, options, print),
|
||||||
|
];
|
||||||
|
case "code": {
|
||||||
|
if (node.isIndented) {
|
||||||
|
// indented code block
|
||||||
|
const alignment = " ".repeat(4);
|
||||||
|
return align(alignment, [
|
||||||
|
alignment,
|
||||||
|
replaceEndOfLine(node.value, hardline),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fenced code block
|
||||||
|
const styleUnit = options.__inJsTemplate ? "~" : "`";
|
||||||
|
const style = styleUnit.repeat(
|
||||||
|
Math.max(3, getMaxContinuousCount(node.value, styleUnit) + 1),
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
style,
|
||||||
|
node.lang || "",
|
||||||
|
node.meta ? " " + node.meta : "",
|
||||||
|
hardline,
|
||||||
|
replaceEndOfLine(
|
||||||
|
getFencedCodeBlockValue(node, options.originalText),
|
||||||
|
hardline,
|
||||||
|
),
|
||||||
|
hardline,
|
||||||
|
style,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case "html": {
|
||||||
|
const { parent, isLast } = path;
|
||||||
|
const value =
|
||||||
|
parent.type === "root" && isLast ? node.value.trimEnd() : node.value;
|
||||||
|
const isHtmlComment = /^<!--.*-->$/su.test(value);
|
||||||
|
|
||||||
|
return replaceEndOfLine(
|
||||||
|
value,
|
||||||
|
// @ts-expect-error
|
||||||
|
isHtmlComment ? hardline : markAsRoot(literalline),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "list": {
|
||||||
|
const nthSiblingIndex = getNthListSiblingIndex(node, path.parent);
|
||||||
|
|
||||||
|
const isGitDiffFriendlyOrderedList = hasGitDiffFriendlyOrderedList(
|
||||||
|
node,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
return printChildren(path, options, print, {
|
||||||
|
processor() {
|
||||||
|
const prefix = getPrefix();
|
||||||
|
const { node: childNode } = path;
|
||||||
|
|
||||||
|
if (
|
||||||
|
childNode.children.length === 2 &&
|
||||||
|
childNode.children[1].type === "html" &&
|
||||||
|
childNode.children[0].position.start.column !==
|
||||||
|
childNode.children[1].position.start.column
|
||||||
|
) {
|
||||||
|
return [prefix, printListItem(path, options, print, prefix)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
prefix,
|
||||||
|
align(
|
||||||
|
" ".repeat(prefix.length),
|
||||||
|
printListItem(path, options, print, prefix),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
function getPrefix() {
|
||||||
|
const rawPrefix = node.ordered
|
||||||
|
? (path.isFirst
|
||||||
|
? node.start
|
||||||
|
: isGitDiffFriendlyOrderedList
|
||||||
|
? 1
|
||||||
|
: node.start + path.index) +
|
||||||
|
(nthSiblingIndex % 2 === 0 ? ". " : ") ")
|
||||||
|
: nthSiblingIndex % 2 === 0
|
||||||
|
? "- "
|
||||||
|
: "* ";
|
||||||
|
|
||||||
|
return (node.isAligned ||
|
||||||
|
/* workaround for https://github.com/remarkjs/remark/issues/315 */ node.hasIndentedCodeblock) &&
|
||||||
|
node.ordered
|
||||||
|
? alignListPrefix(rawPrefix, options)
|
||||||
|
: rawPrefix;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case "thematicBreak": {
|
||||||
|
const { ancestors } = path;
|
||||||
|
const counter = ancestors.findIndex((node) => node.type === "list");
|
||||||
|
if (counter === -1) {
|
||||||
|
return "---";
|
||||||
|
}
|
||||||
|
const nthSiblingIndex = getNthListSiblingIndex(
|
||||||
|
ancestors[counter],
|
||||||
|
ancestors[counter + 1],
|
||||||
|
);
|
||||||
|
return nthSiblingIndex % 2 === 0 ? "***" : "---";
|
||||||
|
}
|
||||||
|
case "linkReference":
|
||||||
|
return [
|
||||||
|
"[",
|
||||||
|
printChildren(path, options, print),
|
||||||
|
"]",
|
||||||
|
node.referenceType === "full"
|
||||||
|
? printLinkReference(node)
|
||||||
|
: node.referenceType === "collapsed"
|
||||||
|
? "[]"
|
||||||
|
: "",
|
||||||
|
];
|
||||||
|
case "imageReference":
|
||||||
|
switch (node.referenceType) {
|
||||||
|
case "full":
|
||||||
|
return ["![", node.alt || "", "]", printLinkReference(node)];
|
||||||
|
default:
|
||||||
|
return [
|
||||||
|
"![",
|
||||||
|
node.alt,
|
||||||
|
"]",
|
||||||
|
node.referenceType === "collapsed" ? "[]" : "",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case "definition": {
|
||||||
|
const lineOrSpace = options.proseWrap === "always" ? line : " ";
|
||||||
|
return group([
|
||||||
|
printLinkReference(node),
|
||||||
|
":",
|
||||||
|
indent([
|
||||||
|
lineOrSpace,
|
||||||
|
printUrl(node.url),
|
||||||
|
node.title === null
|
||||||
|
? ""
|
||||||
|
: [lineOrSpace, printTitle(node.title, options, false)],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// `footnote` requires `.use(footnotes, {inlineNotes: true})`, we are not using this option
|
||||||
|
// https://github.com/remarkjs/remark-footnotes#optionsinlinenotes
|
||||||
|
/* c8 ignore next 2 */
|
||||||
|
case "footnote":
|
||||||
|
return ["[^", printChildren(path, options, print), "]"];
|
||||||
|
case "footnoteReference":
|
||||||
|
return printFootnoteReference(node);
|
||||||
|
case "footnoteDefinition": {
|
||||||
|
const shouldInlineFootnote =
|
||||||
|
node.children.length === 1 &&
|
||||||
|
node.children[0].type === "paragraph" &&
|
||||||
|
(options.proseWrap === "never" ||
|
||||||
|
(options.proseWrap === "preserve" &&
|
||||||
|
node.children[0].position.start.line ===
|
||||||
|
node.children[0].position.end.line));
|
||||||
|
return [
|
||||||
|
printFootnoteReference(node),
|
||||||
|
": ",
|
||||||
|
shouldInlineFootnote
|
||||||
|
? printChildren(path, options, print)
|
||||||
|
: group([
|
||||||
|
align(
|
||||||
|
" ".repeat(4),
|
||||||
|
printChildren(path, options, print, {
|
||||||
|
processor: ({ isFirst }) =>
|
||||||
|
isFirst ? group([softline, print()]) : print(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case "table":
|
||||||
|
return printTable(path, options, print);
|
||||||
|
case "tableCell":
|
||||||
|
return printChildren(path, options, print);
|
||||||
|
case "break":
|
||||||
|
return /\s/u.test(options.originalText[node.position.start.offset])
|
||||||
|
? [" ", markAsRoot(literalline)]
|
||||||
|
: ["\\", hardline];
|
||||||
|
case "liquidNode":
|
||||||
|
return replaceEndOfLine(node.value, hardline);
|
||||||
|
// MDX
|
||||||
|
// fallback to the original text if multiparser failed
|
||||||
|
// or `embeddedLanguageFormatting: "off"`
|
||||||
|
case "import":
|
||||||
|
case "export":
|
||||||
|
case "jsx":
|
||||||
|
return node.value.trimEnd();
|
||||||
|
case "esComment":
|
||||||
|
return ["{/* ", node.value, " */}"];
|
||||||
|
case "math":
|
||||||
|
return [
|
||||||
|
"$$",
|
||||||
|
hardline,
|
||||||
|
node.value ? [replaceEndOfLine(node.value, hardline), hardline] : "",
|
||||||
|
"$$",
|
||||||
|
];
|
||||||
|
case "inlineMath":
|
||||||
|
// remark-math trims content but we don't want to remove whitespaces
|
||||||
|
// since it's very possible that it's recognized as math accidentally
|
||||||
|
return options.originalText.slice(locStart(node), locEnd(node));
|
||||||
|
|
||||||
|
case "frontMatter": // Handled in core
|
||||||
|
case "tableRow": // handled in "table"
|
||||||
|
case "listItem": // handled in "list"
|
||||||
|
case "text": // handled in other types
|
||||||
|
default:
|
||||||
|
/* c8 ignore next */
|
||||||
|
throw new UnexpectedNodeError(node, "Markdown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printListItem(path, options, print, listPrefix) {
|
||||||
|
const { node } = path;
|
||||||
|
const prefix = node.checked === null ? "" : node.checked ? "[x] " : "[ ] ";
|
||||||
|
return [
|
||||||
|
prefix,
|
||||||
|
printChildren(path, options, print, {
|
||||||
|
processor({ node, isFirst }) {
|
||||||
|
if (isFirst && node.type !== "list") {
|
||||||
|
return align(" ".repeat(prefix.length), print());
|
||||||
|
}
|
||||||
|
|
||||||
|
const alignment = " ".repeat(
|
||||||
|
clamp(options.tabWidth - listPrefix.length, 0, 3), // 4+ will cause indented code block
|
||||||
|
);
|
||||||
|
return [alignment, align(alignment, print())];
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignListPrefix(prefix, options) {
|
||||||
|
const additionalSpaces = getAdditionalSpaces();
|
||||||
|
return (
|
||||||
|
prefix +
|
||||||
|
" ".repeat(
|
||||||
|
additionalSpaces >= 4 ? 0 : additionalSpaces, // 4+ will cause indented code block
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
function getAdditionalSpaces() {
|
||||||
|
const restSpaces = prefix.length % options.tabWidth;
|
||||||
|
return restSpaces === 0 ? 0 : options.tabWidth - restSpaces;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNthListSiblingIndex(node, parentNode) {
|
||||||
|
return getNthSiblingIndex(
|
||||||
|
node,
|
||||||
|
parentNode,
|
||||||
|
(siblingNode) => siblingNode.ordered === node.ordered,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNthSiblingIndex(node, parentNode, condition) {
|
||||||
|
let index = -1;
|
||||||
|
|
||||||
|
for (const childNode of parentNode.children) {
|
||||||
|
if (childNode.type === node.type && condition(childNode)) {
|
||||||
|
index++;
|
||||||
|
} else {
|
||||||
|
index = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (childNode === node) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printRoot(path, options, print) {
|
||||||
|
/** @typedef {{ index: number, offset: number }} IgnorePosition */
|
||||||
|
/** @type {Array<{start: IgnorePosition, end: IgnorePosition}>} */
|
||||||
|
const ignoreRanges = [];
|
||||||
|
|
||||||
|
/** @type {IgnorePosition | null} */
|
||||||
|
let ignoreStart = null;
|
||||||
|
|
||||||
|
const { children } = path.node;
|
||||||
|
for (const [index, childNode] of children.entries()) {
|
||||||
|
switch (isPrettierIgnore(childNode)) {
|
||||||
|
case "start":
|
||||||
|
if (ignoreStart === null) {
|
||||||
|
ignoreStart = { index, offset: childNode.position.end.offset };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "end":
|
||||||
|
if (ignoreStart !== null) {
|
||||||
|
ignoreRanges.push({
|
||||||
|
start: ignoreStart,
|
||||||
|
end: { index, offset: childNode.position.start.offset },
|
||||||
|
});
|
||||||
|
ignoreStart = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return printChildren(path, options, print, {
|
||||||
|
processor({ index }) {
|
||||||
|
if (ignoreRanges.length > 0) {
|
||||||
|
const ignoreRange = ignoreRanges[0];
|
||||||
|
|
||||||
|
if (index === ignoreRange.start.index) {
|
||||||
|
return [
|
||||||
|
printIgnoreComment(children[ignoreRange.start.index]),
|
||||||
|
options.originalText.slice(
|
||||||
|
ignoreRange.start.offset,
|
||||||
|
ignoreRange.end.offset,
|
||||||
|
),
|
||||||
|
printIgnoreComment(children[ignoreRange.end.index]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreRange.start.index < index && index < ignoreRange.end.index) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === ignoreRange.end.index) {
|
||||||
|
ignoreRanges.shift();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return print();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function printChildren(path, options, print, events = {}) {
|
||||||
|
const { processor = print } = events;
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
path.each(() => {
|
||||||
|
const result = processor(path);
|
||||||
|
if (result !== false) {
|
||||||
|
if (parts.length > 0 && shouldPrePrintHardline(path)) {
|
||||||
|
parts.push(hardline);
|
||||||
|
|
||||||
|
if (
|
||||||
|
shouldPrePrintDoubleHardline(path, options) ||
|
||||||
|
shouldPrePrintTripleHardline(path)
|
||||||
|
) {
|
||||||
|
parts.push(hardline);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldPrePrintTripleHardline(path)) {
|
||||||
|
parts.push(hardline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.push(result);
|
||||||
|
}
|
||||||
|
}, "children");
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printIgnoreComment(node) {
|
||||||
|
if (node.type === "html") {
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
node.type === "paragraph" &&
|
||||||
|
Array.isArray(node.children) &&
|
||||||
|
node.children.length === 1 &&
|
||||||
|
node.children[0].type === "esComment"
|
||||||
|
) {
|
||||||
|
return ["{/* ", node.children[0].value, " */}"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return {false | 'next' | 'start' | 'end'} */
|
||||||
|
function isPrettierIgnore(node) {
|
||||||
|
let match;
|
||||||
|
|
||||||
|
if (node.type === "html") {
|
||||||
|
match = node.value.match(
|
||||||
|
/^<!--\s*prettier-ignore(?:-(start|end))?\s*-->$/u,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let comment;
|
||||||
|
|
||||||
|
if (node.type === "esComment") {
|
||||||
|
comment = node;
|
||||||
|
} else if (
|
||||||
|
node.type === "paragraph" &&
|
||||||
|
node.children.length === 1 &&
|
||||||
|
node.children[0].type === "esComment"
|
||||||
|
) {
|
||||||
|
comment = node.children[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment) {
|
||||||
|
match = comment.value.match(/^prettier-ignore(?:-(start|end))?$/u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ? match[1] || "next" : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPrePrintHardline({ node, parent }) {
|
||||||
|
const isInlineNode = INLINE_NODE_TYPES.has(node.type);
|
||||||
|
|
||||||
|
const isInlineHTML =
|
||||||
|
node.type === "html" && INLINE_NODE_WRAPPER_TYPES.has(parent.type);
|
||||||
|
|
||||||
|
return !isInlineNode && !isInlineHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLooseListItem(node, options) {
|
||||||
|
return (
|
||||||
|
node.type === "listItem" &&
|
||||||
|
(node.spread ||
|
||||||
|
// Check if `listItem` ends with `\n`
|
||||||
|
// since it can't be empty, so we only need check the last character
|
||||||
|
options.originalText.charAt(node.position.end.offset - 1) === "\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPrePrintDoubleHardline({ node, previous, parent }, options) {
|
||||||
|
if (
|
||||||
|
isLooseListItem(previous, options) ||
|
||||||
|
(node.type === "list" &&
|
||||||
|
parent.type === "listItem" &&
|
||||||
|
previous.type === "code")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSequence = previous.type === node.type;
|
||||||
|
const isSiblingNode = isSequence && SIBLING_NODE_TYPES.has(node.type);
|
||||||
|
const isInTightListItem =
|
||||||
|
parent.type === "listItem" &&
|
||||||
|
(node.type === "list" || !isLooseListItem(parent, options));
|
||||||
|
const isPrevNodePrettierIgnore = isPrettierIgnore(previous) === "next";
|
||||||
|
const isBlockHtmlWithoutBlankLineBetweenPrevHtml =
|
||||||
|
node.type === "html" &&
|
||||||
|
previous.type === "html" &&
|
||||||
|
previous.position.end.line + 1 === node.position.start.line;
|
||||||
|
const isHtmlDirectAfterListItem =
|
||||||
|
node.type === "html" &&
|
||||||
|
parent.type === "listItem" &&
|
||||||
|
previous.type === "paragraph" &&
|
||||||
|
previous.position.end.line + 1 === node.position.start.line;
|
||||||
|
|
||||||
|
return !(
|
||||||
|
isSiblingNode ||
|
||||||
|
isInTightListItem ||
|
||||||
|
isPrevNodePrettierIgnore ||
|
||||||
|
isBlockHtmlWithoutBlankLineBetweenPrevHtml ||
|
||||||
|
isHtmlDirectAfterListItem
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPrePrintTripleHardline({ node, previous }) {
|
||||||
|
const isPrevNodeList = previous.type === "list";
|
||||||
|
const isIndentedCode = node.type === "code" && node.isIndented;
|
||||||
|
|
||||||
|
return isPrevNodeList && isIndentedCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRemainTheSameContent(path) {
|
||||||
|
const node = path.findAncestor(
|
||||||
|
(node) => node.type === "linkReference" || node.type === "imageReference",
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
node && (node.type !== "linkReference" || node.referenceType !== "full")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodeUrl = (url, characters) => {
|
||||||
|
for (const character of characters) {
|
||||||
|
url = url.replaceAll(character, encodeURIComponent(character));
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} url
|
||||||
|
* @param {string[] | string} [dangerousCharOrChars]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function printUrl(url, dangerousCharOrChars = []) {
|
||||||
|
const dangerousChars = [
|
||||||
|
" ",
|
||||||
|
...(Array.isArray(dangerousCharOrChars)
|
||||||
|
? dangerousCharOrChars
|
||||||
|
: [dangerousCharOrChars]),
|
||||||
|
];
|
||||||
|
|
||||||
|
return new RegExp(
|
||||||
|
dangerousChars.map((x) => escapeStringRegexp(x)).join("|"),
|
||||||
|
"u",
|
||||||
|
).test(url)
|
||||||
|
? `<${encodeUrl(url, "<>")}>`
|
||||||
|
: url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printTitle(title, options, printSpace = true) {
|
||||||
|
if (!title) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (printSpace) {
|
||||||
|
return " " + printTitle(title, options, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// title is escaped after `remark-parse` v7
|
||||||
|
title = title.replaceAll(/\\(?=["')])/gu, "");
|
||||||
|
|
||||||
|
if (title.includes('"') && title.includes("'") && !title.includes(")")) {
|
||||||
|
return `(${title})`; // avoid escaped quotes
|
||||||
|
}
|
||||||
|
const quote = getPreferredQuote(title, options.singleQuote);
|
||||||
|
title = title.replaceAll("\\", "\\\\");
|
||||||
|
title = title.replaceAll(quote, `\\${quote}`);
|
||||||
|
return `${quote}${title}${quote}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value, min, max) {
|
||||||
|
return Math.max(min, Math.min(value, max));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPrettierIgnore(path) {
|
||||||
|
return path.index > 0 && isPrettierIgnore(path.previous) === "next";
|
||||||
|
}
|
||||||
|
|
||||||
|
// `remark-parse` lowercase the `label` as `identifier`, we don't want do that
|
||||||
|
// https://github.com/remarkjs/remark/blob/daddcb463af2d5b2115496c395d0571c0ff87d15/packages/remark-parse/lib/tokenize/reference.js
|
||||||
|
function printLinkReference(node) {
|
||||||
|
return `[${collapseWhiteSpace(node.label)}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printFootnoteReference(node) {
|
||||||
|
return `[^${node.label}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const printer = {
|
||||||
|
features: {
|
||||||
|
experimental_frontMatterSupport: {
|
||||||
|
massageAstNode: true,
|
||||||
|
embed: true,
|
||||||
|
print: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preprocess,
|
||||||
|
print: genericPrint,
|
||||||
|
embed,
|
||||||
|
massageAstNode: clean,
|
||||||
|
hasPrettierIgnore,
|
||||||
|
insertPragma,
|
||||||
|
getVisitorKeys,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default printer;
|
||||||
23
src/prettier-markdown/unified-plugins/front-matter.js
Normal file
23
src/prettier-markdown/unified-plugins/front-matter.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { parseFrontMatter } from "../../main/front-matter/index.js";
|
||||||
|
|
||||||
|
/** @import {Plugin, Settings} from "unified" */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Plugin<[], Settings>}
|
||||||
|
*/
|
||||||
|
const frontMatter = function () {
|
||||||
|
const proto = this.Parser.prototype;
|
||||||
|
proto.blockMethods = ["frontMatter", ...proto.blockMethods];
|
||||||
|
proto.blockTokenizers.frontMatter = tokenizer;
|
||||||
|
|
||||||
|
function tokenizer(eat, value) {
|
||||||
|
const { frontMatter } = parseFrontMatter(value);
|
||||||
|
|
||||||
|
if (frontMatter) {
|
||||||
|
return eat(frontMatter.raw)({ ...frontMatter, type: "frontMatter" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokenizer.onlyAtStart = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default frontMatter;
|
||||||
19
src/prettier-markdown/unified-plugins/html-to-jsx.js
Normal file
19
src/prettier-markdown/unified-plugins/html-to-jsx.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { COMMENT_REGEX } from "../mdx.js";
|
||||||
|
import { INLINE_NODE_WRAPPER_TYPES, mapAst } from "../utils.js";
|
||||||
|
|
||||||
|
function htmlToJsx() {
|
||||||
|
return (ast) =>
|
||||||
|
mapAst(ast, (node, _index, [parent]) => {
|
||||||
|
if (
|
||||||
|
node.type !== "html" ||
|
||||||
|
// Keep HTML-style comments (legacy MDX)
|
||||||
|
COMMENT_REGEX.test(node.value) ||
|
||||||
|
INLINE_NODE_WRAPPER_TYPES.has(parent.type)
|
||||||
|
) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
return { ...node, type: "jsx" };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default htmlToJsx;
|
||||||
27
src/prettier-markdown/unified-plugins/liquid.js
Normal file
27
src/prettier-markdown/unified-plugins/liquid.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/** @import {Plugin, Settings} from "unified" */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Plugin<[], Settings>}
|
||||||
|
*/
|
||||||
|
const liquid = function () {
|
||||||
|
const proto = this.Parser.prototype;
|
||||||
|
const methods = proto.inlineMethods;
|
||||||
|
methods.splice(methods.indexOf("text"), 0, "liquid");
|
||||||
|
proto.inlineTokenizers.liquid = tokenizer;
|
||||||
|
|
||||||
|
function tokenizer(eat, value) {
|
||||||
|
const match = value.match(/^(\{%.*?%\}|\{\{.*?\}\})/su);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return eat(match[0])({
|
||||||
|
type: "liquidNode",
|
||||||
|
value: match[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokenizer.locator = function (value, fromIndex) {
|
||||||
|
return value.indexOf("{", fromIndex);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default liquid;
|
||||||
32
src/prettier-markdown/unified-plugins/wiki-link.js
Normal file
32
src/prettier-markdown/unified-plugins/wiki-link.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/** @import {Plugin, Settings} from "unified" */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Plugin<[], Settings>}
|
||||||
|
*/
|
||||||
|
const wikiLink = function () {
|
||||||
|
const entityType = "wikiLink";
|
||||||
|
const wikiLinkRegex = /^\[\[(?<linkContents>.+?)\]\]/su;
|
||||||
|
const proto = this.Parser.prototype;
|
||||||
|
const methods = proto.inlineMethods;
|
||||||
|
methods.splice(methods.indexOf("link"), 0, entityType);
|
||||||
|
proto.inlineTokenizers.wikiLink = tokenizer;
|
||||||
|
|
||||||
|
function tokenizer(eat, value) {
|
||||||
|
const match = wikiLinkRegex.exec(value);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const linkContents = match.groups.linkContents.trim();
|
||||||
|
|
||||||
|
return eat(match[0])({
|
||||||
|
type: entityType,
|
||||||
|
value: linkContents,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenizer.locator = function (value, fromIndex) {
|
||||||
|
return value.indexOf("[", fromIndex);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default wikiLink;
|
||||||
268
src/prettier-markdown/utils.js
Normal file
268
src/prettier-markdown/utils.js
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
import * as assert from "#universal/assert";
|
||||||
|
import { CJK_REGEXP, PUNCTUATION_REGEXP } from "./constants.evaluate.js";
|
||||||
|
import { locEnd, locStart } from "./loc.js";
|
||||||
|
|
||||||
|
const INLINE_NODE_TYPES = new Set([
|
||||||
|
"liquidNode",
|
||||||
|
"inlineCode",
|
||||||
|
"emphasis",
|
||||||
|
"esComment",
|
||||||
|
"strong",
|
||||||
|
"delete",
|
||||||
|
"wikiLink",
|
||||||
|
"link",
|
||||||
|
"linkReference",
|
||||||
|
"image",
|
||||||
|
"imageReference",
|
||||||
|
"footnote",
|
||||||
|
"footnoteReference",
|
||||||
|
"sentence",
|
||||||
|
"whitespace",
|
||||||
|
"word",
|
||||||
|
"break",
|
||||||
|
"inlineMath",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const INLINE_NODE_WRAPPER_TYPES = new Set([
|
||||||
|
...INLINE_NODE_TYPES,
|
||||||
|
"tableCell",
|
||||||
|
"paragraph",
|
||||||
|
"heading",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const KIND_NON_CJK = "non-cjk";
|
||||||
|
const KIND_CJ_LETTER = "cj-letter";
|
||||||
|
const KIND_K_LETTER = "k-letter";
|
||||||
|
const KIND_CJK_PUNCTUATION = "cjk-punctuation";
|
||||||
|
|
||||||
|
const K_REGEXP = /\p{Script_Extensions=Hangul}/u;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {" " | "\n" | ""} WhitespaceValue
|
||||||
|
* @typedef { KIND_NON_CJK | KIND_CJ_LETTER | KIND_K_LETTER | KIND_CJK_PUNCTUATION } WordKind
|
||||||
|
* @typedef {{
|
||||||
|
* type: "whitespace",
|
||||||
|
* value: WhitespaceValue,
|
||||||
|
* kind?: never
|
||||||
|
* }} WhitespaceNode
|
||||||
|
* @typedef {{
|
||||||
|
* type: "word",
|
||||||
|
* value: string,
|
||||||
|
* kind: WordKind,
|
||||||
|
* isCJ: boolean,
|
||||||
|
* hasLeadingPunctuation: boolean,
|
||||||
|
* hasTrailingPunctuation: boolean,
|
||||||
|
* }} WordNode
|
||||||
|
* Node for a single CJK character or a sequence of non-CJK characters
|
||||||
|
* @typedef {WhitespaceNode | WordNode} TextNode
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* split text into whitespaces and words
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
function splitText(text) {
|
||||||
|
/** @type {Array<TextNode>} */
|
||||||
|
const nodes = [];
|
||||||
|
|
||||||
|
const tokens = text.split(/([\t\n ]+)/u);
|
||||||
|
for (const [index, token] of tokens.entries()) {
|
||||||
|
// whitespace
|
||||||
|
if (index % 2 === 1) {
|
||||||
|
nodes.push({
|
||||||
|
type: "whitespace",
|
||||||
|
value: /\n/u.test(token) ? "\n" : " ",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// word separated by whitespace
|
||||||
|
|
||||||
|
if ((index === 0 || index === tokens.length - 1) && token === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerTokens = token.split(new RegExp(`(${CJK_REGEXP.source})`, "u"));
|
||||||
|
for (const [innerIndex, innerToken] of innerTokens.entries()) {
|
||||||
|
if (
|
||||||
|
(innerIndex === 0 || innerIndex === innerTokens.length - 1) &&
|
||||||
|
innerToken === ""
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-CJK word
|
||||||
|
if (innerIndex % 2 === 0) {
|
||||||
|
if (innerToken !== "") {
|
||||||
|
appendNode({
|
||||||
|
type: "word",
|
||||||
|
value: innerToken,
|
||||||
|
kind: KIND_NON_CJK,
|
||||||
|
isCJ: false,
|
||||||
|
hasLeadingPunctuation: PUNCTUATION_REGEXP.test(innerToken[0]),
|
||||||
|
hasTrailingPunctuation: PUNCTUATION_REGEXP.test(innerToken.at(-1)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CJK character
|
||||||
|
|
||||||
|
// punctuation for CJ(K)
|
||||||
|
// Korean doesn't use them in horizontal writing usually
|
||||||
|
if (PUNCTUATION_REGEXP.test(innerToken)) {
|
||||||
|
appendNode({
|
||||||
|
type: "word",
|
||||||
|
value: innerToken,
|
||||||
|
kind: KIND_CJK_PUNCTUATION,
|
||||||
|
isCJ: true,
|
||||||
|
hasLeadingPunctuation: true,
|
||||||
|
hasTrailingPunctuation: true,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Korean uses space to divide words, but Chinese & Japanese do not
|
||||||
|
// This is why Korean should be treated like non-CJK
|
||||||
|
if (K_REGEXP.test(innerToken)) {
|
||||||
|
appendNode({
|
||||||
|
type: "word",
|
||||||
|
value: innerToken,
|
||||||
|
kind: KIND_K_LETTER,
|
||||||
|
isCJ: false,
|
||||||
|
hasLeadingPunctuation: false,
|
||||||
|
hasTrailingPunctuation: false,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendNode({
|
||||||
|
type: "word",
|
||||||
|
value: innerToken,
|
||||||
|
kind: KIND_CJ_LETTER,
|
||||||
|
isCJ: true,
|
||||||
|
hasLeadingPunctuation: false,
|
||||||
|
hasTrailingPunctuation: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for `canBeConvertedToSpace` in ./print-whitespace.js etc.
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
for (let i = 1; i < nodes.length; i++) {
|
||||||
|
assert.ok(
|
||||||
|
!(nodes[i - 1].type === "whitespace" && nodes[i].type === "whitespace"),
|
||||||
|
"splitText should not create consecutive whitespace nodes",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
|
||||||
|
function appendNode(node) {
|
||||||
|
const lastNode = nodes.at(-1);
|
||||||
|
if (
|
||||||
|
lastNode?.type === "word" &&
|
||||||
|
!isBetween(KIND_NON_CJK, KIND_CJK_PUNCTUATION) &&
|
||||||
|
// disallow leading/trailing full-width whitespace
|
||||||
|
![lastNode.value, node.value].some((value) => /\u3000/u.test(value))
|
||||||
|
) {
|
||||||
|
nodes.push({ type: "whitespace", value: "" });
|
||||||
|
}
|
||||||
|
nodes.push(node);
|
||||||
|
|
||||||
|
function isBetween(kind1, kind2) {
|
||||||
|
return (
|
||||||
|
(lastNode.kind === kind1 && node.kind === kind2) ||
|
||||||
|
(lastNode.kind === kind2 && node.kind === kind1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrderedListItemInfo(orderListItem, options) {
|
||||||
|
const text = options.originalText.slice(
|
||||||
|
orderListItem.position.start.offset,
|
||||||
|
orderListItem.position.end.offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { numberText, leadingSpaces } = text.match(
|
||||||
|
/^\s*(?<numberText>\d+)(\.|\))(?<leadingSpaces>\s*)/u,
|
||||||
|
).groups;
|
||||||
|
|
||||||
|
return { number: Number(numberText), leadingSpaces };
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasGitDiffFriendlyOrderedList(node, options) {
|
||||||
|
if (!node.ordered || node.children.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondNumber = getOrderedListItemInfo(node.children[1], options).number;
|
||||||
|
|
||||||
|
if (secondNumber !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstNumber = getOrderedListItemInfo(node.children[0], options).number;
|
||||||
|
|
||||||
|
if (firstNumber !== 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
node.children.length > 2 &&
|
||||||
|
getOrderedListItemInfo(node.children[2], options).number === 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The final new line should not include in value
|
||||||
|
// https://github.com/remarkjs/remark/issues/512
|
||||||
|
function getFencedCodeBlockValue(node, originalText) {
|
||||||
|
const { value } = node;
|
||||||
|
if (
|
||||||
|
node.position.end.offset === originalText.length &&
|
||||||
|
value.endsWith("\n") &&
|
||||||
|
// Code block has no end mark
|
||||||
|
originalText.endsWith("\n")
|
||||||
|
) {
|
||||||
|
return value.slice(0, -1);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapAst(ast, handler) {
|
||||||
|
return (function preorder(node, index, parentStack) {
|
||||||
|
const newNode = { ...handler(node, index, parentStack) };
|
||||||
|
if (newNode.children) {
|
||||||
|
newNode.children = newNode.children.map((child, index) =>
|
||||||
|
preorder(child, index, [newNode, ...parentStack]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNode;
|
||||||
|
})(ast, null, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAutolink(node) {
|
||||||
|
if (node?.type !== "link" || node.children.length !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const [child] = node.children;
|
||||||
|
return locStart(node) === locStart(child) && locEnd(node) === locEnd(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getFencedCodeBlockValue,
|
||||||
|
getOrderedListItemInfo,
|
||||||
|
hasGitDiffFriendlyOrderedList,
|
||||||
|
INLINE_NODE_TYPES,
|
||||||
|
INLINE_NODE_WRAPPER_TYPES,
|
||||||
|
isAutolink,
|
||||||
|
KIND_CJ_LETTER,
|
||||||
|
KIND_CJK_PUNCTUATION,
|
||||||
|
KIND_K_LETTER,
|
||||||
|
KIND_NON_CJK,
|
||||||
|
mapAst,
|
||||||
|
splitText,
|
||||||
|
};
|
||||||
41
src/prettier-markdown/visitor-keys.js
Normal file
41
src/prettier-markdown/visitor-keys.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const visitorKeys = {
|
||||||
|
root: ["children"],
|
||||||
|
paragraph: ["children"],
|
||||||
|
sentence: ["children"],
|
||||||
|
word: [],
|
||||||
|
whitespace: [],
|
||||||
|
emphasis: ["children"],
|
||||||
|
strong: ["children"],
|
||||||
|
delete: ["children"],
|
||||||
|
inlineCode: [],
|
||||||
|
wikiLink: [],
|
||||||
|
link: ["children"],
|
||||||
|
image: [],
|
||||||
|
blockquote: ["children"],
|
||||||
|
heading: ["children"],
|
||||||
|
code: [],
|
||||||
|
html: [],
|
||||||
|
list: ["children"],
|
||||||
|
thematicBreak: [],
|
||||||
|
linkReference: ["children"],
|
||||||
|
imageReference: [],
|
||||||
|
definition: [],
|
||||||
|
footnote: ["children"],
|
||||||
|
footnoteReference: [],
|
||||||
|
footnoteDefinition: ["children"],
|
||||||
|
table: ["children"],
|
||||||
|
tableCell: ["children"],
|
||||||
|
break: [],
|
||||||
|
liquidNode: [],
|
||||||
|
import: [],
|
||||||
|
export: [],
|
||||||
|
esComment: [],
|
||||||
|
jsx: [],
|
||||||
|
math: [],
|
||||||
|
inlineMath: [],
|
||||||
|
tableRow: ["children"],
|
||||||
|
listItem: ["children"],
|
||||||
|
text: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default visitorKeys;
|
||||||
File diff suppressed because it is too large
Load Diff
117
test/extensions-api.test.ts
Normal file
117
test/extensions-api.test.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import {
|
||||||
|
createContextExtensions,
|
||||||
|
isValidExtensionKey,
|
||||||
|
createPositionHelpers,
|
||||||
|
} from '../src/extensions/index.js';
|
||||||
|
|
||||||
|
describe('Extension API Tests', () => {
|
||||||
|
describe('isValidExtensionKey', () => {
|
||||||
|
it('should return true for valid extension keys starting with x-', () => {
|
||||||
|
expect(isValidExtensionKey('x-test')).toBeTrue();
|
||||||
|
expect(isValidExtensionKey('x-custom-field')).toBeTrue();
|
||||||
|
expect(isValidExtensionKey('x-speakeasy-sdk-name')).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for keys not starting with x-', () => {
|
||||||
|
expect(isValidExtensionKey('test')).toBeFalse();
|
||||||
|
expect(isValidExtensionKey('custom-field')).toBeFalse();
|
||||||
|
expect(isValidExtensionKey('description')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge cases', () => {
|
||||||
|
expect(isValidExtensionKey('')).toBeFalse();
|
||||||
|
expect(isValidExtensionKey('x')).toBeFalse();
|
||||||
|
expect(isValidExtensionKey('x-')).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createContextExtensions', () => {
|
||||||
|
it('should create context extensions for info context', () => {
|
||||||
|
const extensions = createContextExtensions('info', (before, after) => ({
|
||||||
|
'x-custom-before-title': before('title'),
|
||||||
|
'x-custom-after-title': after('title'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(extensions).toBeDefined();
|
||||||
|
expect(extensions.info).toBeDefined();
|
||||||
|
expect(typeof extensions.info).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create context extensions for operation context', () => {
|
||||||
|
const extensions = createContextExtensions('operation', (before, after) => ({
|
||||||
|
'x-custom-before-summary': before('summary'),
|
||||||
|
'x-custom-after-summary': after('summary'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(extensions).toBeDefined();
|
||||||
|
expect(extensions.operation).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create context extensions for schema context', () => {
|
||||||
|
const extensions = createContextExtensions('schema', (before, after) => ({
|
||||||
|
'x-custom-before-type': before('type'),
|
||||||
|
'x-custom-after-type': after('type'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(extensions).toBeDefined();
|
||||||
|
expect(extensions.schema).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createPositionHelpers', () => {
|
||||||
|
it('should create position helpers for info context', () => {
|
||||||
|
const helpers = createPositionHelpers('info');
|
||||||
|
|
||||||
|
expect(helpers).toBeDefined();
|
||||||
|
expect(helpers.before).toBeDefined();
|
||||||
|
expect(helpers.after).toBeDefined();
|
||||||
|
expect(helpers.getAvailableKeys).toBeDefined();
|
||||||
|
expect(helpers.isValidKey).toBeDefined();
|
||||||
|
|
||||||
|
expect(typeof helpers.before).toBe('function');
|
||||||
|
expect(typeof helpers.after).toBe('function');
|
||||||
|
expect(typeof helpers.getAvailableKeys).toBe('function');
|
||||||
|
expect(typeof helpers.isValidKey).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return available keys for info context', () => {
|
||||||
|
const helpers = createPositionHelpers('info');
|
||||||
|
const keys = helpers.getAvailableKeys();
|
||||||
|
|
||||||
|
expect(Array.isArray(keys)).toBeTrue();
|
||||||
|
expect(keys.length).toBeGreaterThan(0);
|
||||||
|
expect(keys).toContain('title');
|
||||||
|
expect(keys).toContain('version');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate keys for info context', () => {
|
||||||
|
const helpers = createPositionHelpers('info');
|
||||||
|
|
||||||
|
expect(helpers.isValidKey('title')).toBeTrue();
|
||||||
|
expect(helpers.isValidKey('version')).toBeTrue();
|
||||||
|
expect(helpers.isValidKey('invalid-key')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create position helpers for operation context', () => {
|
||||||
|
const helpers = createPositionHelpers('operation');
|
||||||
|
|
||||||
|
expect(helpers).toBeDefined();
|
||||||
|
const keys = helpers.getAvailableKeys();
|
||||||
|
expect(keys.length).toBeGreaterThan(0);
|
||||||
|
expect(keys).toContain('summary');
|
||||||
|
expect(keys).toContain('operationId');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create position helpers for schema context', () => {
|
||||||
|
const helpers = createPositionHelpers('schema');
|
||||||
|
|
||||||
|
expect(helpers).toBeDefined();
|
||||||
|
const keys = helpers.getAvailableKeys();
|
||||||
|
expect(keys.length).toBeGreaterThan(0);
|
||||||
|
expect(keys).toContain('type');
|
||||||
|
expect(keys).toContain('properties');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user