Spec-Zone.ru › Prettier

Плагины

Плагины — это способы добавления новых языков или правил форматирования в Prettier. Собственные реализации всех языков в Prettier выражены с помощью API плагинов. Основной пакет prettier содержит JavaScript и другие ориентированные на веб-языки, встроенные в него. Для дополнительных языков вам потребуется установить плагин.

Использование плагинов

Вы можете загружать плагины с помощью:

  • Командной строки (CLI), через --plugin:

    prettier --write main.foo --plugin=prettier-plugin-foo
    

    Подсказка: Вы можете устанавливать параметры --plugin несколько раз.

  • API (API), через параметры plugins:

    await prettier.format("code", {
      parser: "foo",
      plugins: ["prettier-plugin-foo"],
    });
    
  • Файл конфигурации (Файл конфигурации):

    {
      "plugins": ["prettier-plugin-foo"]
    }
    

Строки, переданные в plugins, в конечном итоге передаются в import() выражение, поэтому вы можете указать имя модуля/пакета, путь или что-то ещё, что принимает import().

Официальные плагины

  • @prettier/plugin-php
  • @prettier/plugin-pug от @Shinigami92
  • @prettier/plugin-ruby
  • @prettier/plugin-xml

Плагины сообщества

  • prettier-plugin-apex от @dangmai
  • prettier-plugin-astro от @withastro contributors
  • prettier-plugin-elm от @giCentre
  • prettier-plugin-erb от @adamzapasnik
  • prettier-plugin-gherkin от @mapado
  • prettier-plugin-glsl от @NaridaL
  • prettier-plugin-go-template от @NiklasPor
  • prettier-plugin-java от @JHipster
  • prettier-plugin-jinja-template от @davidodenwald
  • prettier-plugin-jsonata от @Stedi
  • prettier-plugin-kotlin от @Angry-Potato
  • prettier-plugin-motoko от @dfinity
  • prettier-plugin-nginx от @joedeandev
  • prettier-plugin-prisma от @umidbekk
  • prettier-plugin-properties от @eemeli
  • prettier-plugin-rust от @jinxdash
  • prettier-plugin-sh от @JounQin
  • prettier-plugin-sql от @JounQin
  • prettier-plugin-sql-cst от @nene
  • prettier-plugin-solidity от @mattiaerre
  • prettier-plugin-svelte от @sveltejs
  • prettier-plugin-toml от @bd82

Разработка плагинов

Плагины Prettier — это обычные JavaScript-модули со следующими пятью экспортами или экспортом по умолчанию со следующими свойствами:

  • languages
  • parsers
  • printers
  • options
  • defaultOptions

languages

Languages — это массив определений языков, которые ваш плагин внесёт в Prettier. Он может содержать все поля, указанные в prettier.getSupportInfo().

Он обязательно должен содержать name и parsers.

export const languages = [
  {
    // The language name
    name: "InterpretedDanceScript",
    // Parsers that can parse this language.
    // This can be built-in parsers, or parsers you have contributed via this plugin.
    parsers: ["dance-parse"],
  },
];

parsers

Парсеры преобразуют код в виде строки в AST.

Ключ должен соответствовать имени в массиве parsers из languages. Значение содержит функцию разбора, имя формата AST и две функции извлечения местоположений (locStart и locEnd).

export const parsers = {
  "dance-parse": {
    parse,
    // The name of the AST that the parser produces.
    astFormat: "dance-ast",
    hasPragma,
    locStart,
    locEnd,
    preprocess,
  },
};

Подпись функции parse:

function parse(text: string, options: object): Promise<AST> | AST;

Функции извлечения местоположений (locStart и locEnd) возвращают начальное и конечное местоположения заданного узла AST:

function locStart(node: object): number;

(Необязательно) Функция обнаружения директивы pragma (hasPragma) должна вернуть, содержит ли текст комментарий pragma.

function hasPragma(text: string): boolean;

(Необязательно) Функция preprocess может обработать входной текст перед передачей в функцию parse.

function preprocess(text: string, options: object): string;

printers

Печатальщиках преобразуют AST в промежуточное представление Prettier, также известное как Doc.

Ключ должен соответствовать astFormat, которое создаёт парсер. Значение содержит объект с функцией print. Все остальные свойства (embed, preprocess, и т.д.) необязательны.

export const printers = {
  "dance-ast": {
    print,
    embed,
    preprocess,
    getVisitorKeys,
    insertPragma,
    canAttachComment,
    isBlockComment,
    printComment,
    getCommentChildNodes,
    handleComments: {
      ownLine,
      endOfLine,
      remaining,
    },
  },
};

Процесс печати

Prettier использует промежуточное представление, называемое Doc, которое Prettier затем преобразует в строку (на основе таких параметров, как printWidth). Задача печатальщика — принять AST, сгенерированный parsers[<parser name>].parse, и вернуть Doc. Doc строится с помощью команд-строителей:

import { doc } from "prettier";

const { join, line, ifBreak, group } = doc.builders;

Процесс печати состоит из следующих шагов:

  1. Предварительная обработка AST (необязательно). Смотрите preprocess.
  2. Прикрепление комментариев (необязательно). Смотрите Обработка комментариев в печатальщике.
  3. Обработка вложенных языков (необязательно). Метод embed, если определён, вызывается для каждого узла в глубину. Хотя по соображениям производительности сама рекурсия синхронная, embed может возвращать асинхронные функции, которые могут вызывать другие парсеры и печатальщики для построения документов для вложенных синтаксисов, таких как CSS-в-JS. Эти возвращённые функции помещаются в очередь и выполняются последовательно перед следующим шагом.
  4. Рекурсивный вывод. Doc рекурсивно строится из AST. Начиная с корневого узла:
    • Если из шага 3 существует документ вложенного языка, связанный с текущим узлом, этот документ используется.
    • В противном случае вызывается метод print(path, options, print): Doc. Он создаёт документ для текущего узла, часто печатая дочерние узлы с помощью обратного вызова print.

print

Большая часть работы печатальщика плагина выполняется в его функции print с подписью:

function print(
  // Path to the AST node to print
  path: AstPath,
  options: object,
  // Recursively print a child node
  print: (selector?: string | number | Array<string | number> | AstPath) => Doc,
): Doc;

Функция print получает следующие параметры:

  • path: Объект, который можно использовать для доступа к узлам в AST. Это структура данных, похожая на стек, которая сохраняет текущее состояние рекурсии. Она называется «путь», потому что представляет собой путь к текущему узлу от корня AST. Текущий узел возвращается функцией path.node.
  • options: Постоянный объект, который содержит глобальные параметры и который плагин может изменить для хранения контекстуальных данных.
  • print: Обратный вызов для вывода дочерних узлов. Эта функция содержит основную логику вывода, которая состоит из шагов, реализация которых предоставляется плагинами. В частности, она вызывает функцию печатальщика print и передаёт себе в качестве аргумента. Таким образом, обе функции print — основная и плагина — вызывают друг друга, рекурсивно опускаясь по AST.

Вот упрощённый пример, чтобы понять, как выглядит типичная реализация функции print:

import { doc } from "prettier";

const { group, indent, join, line, softline } = doc.builders;

function print(path, options, print) {
  const node = path.node;

  switch (node.type) {
    case "list":
      return group([
        "(",
        indent([softline, join(line, path.map(print, "elements"))]),
        softline,
        ")",
      ]);

    case "pair":
      return group([
        "(",
        indent([softline, print("left"), line, ". ", print("right")]),
        softline,
        ")",
      ]);

    case "symbol":
      return node.name;
  }

  throw new Error(`Unknown node type: ${node.type}`);
}

Посмотрите на печатальщик prettier-python для примеров того, что возможно.

(необязательно) embed

Печатальщик может иметь метод embed для печати одного языка внутри другого. Примерами являются вывод CSS-в-JS или помеченных блоков кода в Markdown. Подпись:

function embed(
  // Path to the current AST node
  path: AstPath,
  // Current options
  options: Options,
):
  | ((
      // Parses and prints the passed text using a different parser.
      // You should set `options.parser` to specify which parser to use.
      textToDoc: (text: string, options: Options) => Promise<Doc>,
      // Prints the current node or its descendant node with the current printer
      print: (
        selector?: string | number | Array<string | number> | AstPath,
      ) => Doc,
      // The following two arguments are passed for convenience.
      // They're the same `path` and `options` that are passed to `embed`.
      path: AstPath,
      options: Options,
    ) => Promise<Doc | undefined> | Doc | undefined)
  | Doc
  | undefined;

Метод embed похож на метод print тем, что он сопоставляет узлы AST с документами, но в отличие от print, он имеет возможность выполнять асинхронную работу, возвращая асинхронную функцию. Первый параметр этой функции, асинхронная функция textToDoc, может быть использован для рендеринга документа с помощью другого плагина.

Если функция, возвращённая из embed, возвращает документ или промис, который разрешается в документ, этот документ будет использован при печати, и метод print не будет вызван для этого узла. Также возможно и, в редких случаях, может быть удобно вернуть документ синхронно непосредственно из embed, однако textToDoc и обратный вызов print недоступны в этом случае. Верните функцию, чтобы получить их.

END_OF_DOCUMENT_MARKER

Если embed возвращает undefined, или если функция, которую оно вернуло, возвращает undefined или промис, который разрешается в undefined, узел будет напечатан обычным способом с помощью метода print. То же самое произойдёт, если возвращаемая функция выбросит ошибку или вернёт промис, который отклоняется (например, если произошла ошибка парсинга). Установите переменную окружения PRETTIER_DEBUG в ненулевое значение, если вы хотите, чтобы Prettier повторно выбросил эти ошибки.

Например, плагин, у которого есть узлы с встроенным JavaScript, может иметь следующий метод embed:

function embed(path, options) {
  const node = path.node;
  if (node.type === "javascript") {
    return async (textToDoc) => {
      return [
        "<script>",
        hardline,
        await textToDoc(node.javaScriptCode, { parser: "babel" }),
        hardline,
        "</script>",
      ];
    };
  }
}

Если опция --embedded-language-formatting установлена в off, этап встраивания полностью пропускается, embed не вызывается, и все узлы печатаются с помощью метода print.

(дополнительный) preprocess

Метод preprocess может обработать AST из парсера перед передачей его в метод print.

function preprocess(ast: AST, options: Options): AST | Promise<AST>;

(дополнительный) getVisitorKeys

Эта свойство может быть полезно, если плагин использует прикрепление комментариев или встроенные языки. Эти функции обходятся по AST, перебирая все собственные перечисляемые свойства каждого узла, начиная с корня. Если AST содержит циклы, такой обход приводит к бесконечному циклу. Кроме того, узлы могут содержать объекты, не являющиеся узлами (например, данные о местоположении), перебор которых является пустой тратой ресурсов. Для решения этих проблем принтер может определить функцию, возвращающую имена свойств, которые должны быть пройдены.

Её сигнатура:

function getVisitorKeys(node, nonTraversableKeys: Set<string>): string[];

По умолчанию getVisitorKeys:

function getVisitorKeys(node, nonTraversableKeys) {
  return Object.keys(node).filter((key) => !nonTraversableKeys.has(key));
}

Второй аргумент nonTraversableKeys — это набор общих ключей и ключей, используемых Prettier внутри.

Если у вас есть полный список ключей посетителя:

const visitorKeys = {
  Program: ["body"],
  Identifier: [],
  // ...
};

function getVisitorKeys(node /* , nonTraversableKeys*/) {
  // Return `[]` for unknown node to prevent Prettier fallback to use `Object.keys()`
  return visitorKeys[node.type] ?? [];
}

Если вам нужно только исключить небольшой набор ключей:

const ignoredKeys = new Set(["prev", "next", "range"]);

function getVisitorKeys(node, nonTraversableKeys) {
  return Object.keys(node).filter(
    (key) => !nonTraversableKeys.has(key) && !ignoredKeys.has(key),
  );
}

(дополнительный) insertPragma

Плагин может реализовать способ вставки комментария pragma в результирующий код, когда используется опция --insert-pragma, в функции insertPragma. Её сигнатура:

function insertPragma(text: string): string;

Обработка комментариев в принтере

Комментарии часто не являются частью AST языка и представляют проблему для красивых принтеров. Плагин Prettier может либо печатать комментарии самостоятельно в своей функции print , либо полагаться на алгоритм обработки комментариев Prettier.

По умолчанию, если AST имеет свойство верхнего уровня comments, Prettier предполагает, что comments хранит массив узлов комментариев. Затем Prettier будет использовать предоставленные функции parsers[<plugin>].locStart/locEnd для поиска узла AST, к которому относится каждый комментарий. Комментарии затем прикрепляются к этим узлам, изменяя AST в процессе, и свойство comments удаляется из корня AST. Функции *Comment используются для настройки алгоритма Prettier. После того, как комментарии прикреплены к AST, Prettier автоматически вызовет функцию printComment(path, options): Doc и вставит возвращённый документ в (надеемся) правильное место.

(дополнительный) getCommentChildNodes

По умолчанию Prettier рекурсивно ищет все свойства объектов (за исключением нескольких предопределённых) каждого узла. Эту функцию можно предоставить для переопределения этого поведения. Её сигнатура:

function getCommentChildNodes(
  // The node whose children should be returned.
  node: AST,
  // Current options
  options: object,
): AST[] | undefined;

Возвращает [], если у узла нет дочерних элементов, или undefined, чтобы вернуться к поведению по умолчанию.

(дополнительный) printComment

Вызывается всякий раз, когда необходимо напечатать узел комментария. Её сигнатура:

function printComment(
  // Path to the current comment node
  commentPath: AstPath,
  // Current options
  options: object,
): Doc;

(дополнительный) canAttachComment

function canAttachComment(node: AST): boolean;

Эта функция используется для определения возможности прикрепления комментария к конкретному узлу AST. По умолчанию все свойства AST обрабатываются в поисках узлов, к которым можно прикрепить комментарии. Эта функция используется для предотвращения прикрепления комментариев к конкретному узлу. Типичная реализация выглядит так:

function canAttachComment(node) {
  return node.type && node.type !== "comment";
}

(дополнительный) isBlockComment

function isBlockComment(node: AST): boolean;

Возвращает, является ли узел AST блочным комментарием.

(дополнительный) handleComments

Объект handleComments содержит три необязательные функции, каждая с сигнатурой

(
  // The AST node corresponding to the comment
  comment: AST,
  // The full source code text
  text: string,
  // The global options object
  options: object,
  // The AST
  ast: AST,
  // Whether this comment is the last comment
  isLastComment: boolean,
) => boolean;

Эти функции используются для переопределения алгоритма прикрепления комментариев по умолчанию Prettier. ownLine/endOfLine/remaining ожидается, что они либо вручную прикрепят комментарий к узлу и вернут true, либо вернут false и позволят Prettier прикрепить комментарий.

Основываясь на тексте, окружающем узел комментария, Prettier отправляет:

  • ownLine если перед комментарием есть только пробелы, а за ним — новая строка,
  • endOfLine если за комментарием следует новая строка, но перед ним есть некоторые не-пробелы,
  • remaining во всех остальных случаях.

Во время отправки Prettier аннотирует каждый узел комментария AST (т. е. создаёт новые свойства) по крайней мере с одним из enclosingNode, precedingNode, или followingNode. Это можно использовать для помощи в процессе принятия решения плагином (конечно, весь AST и исходный текст также передаются для принятия более сложных решений).

Вручную прикрепление комментария

Функции util.addTrailingComment/addLeadingComment/addDanglingComment могут использоваться для ручного прикрепления комментария к узлу AST. Пример функции ownLine , которая гарантирует, что комментарий не следует за узлом «знаков препинания» (представлена для демонстрационных целей), может выглядеть следующим образом:

import { util } from "prettier";

function ownLine(comment, text, options, ast, isLastComment) {
  const { precedingNode } = comment;
  if (precedingNode && precedingNode.type === "punctuation") {
    util.addTrailingComment(precedingNode, comment);
    return true;
  }
  return false;
}

Ожидается, что узлы с комментариями будут иметь свойство comments, содержащее массив комментариев. Каждый комментарий должен иметь следующие свойства: leading, trailing, printed.

В приведённом выше примере используется util.addTrailingComment, что автоматически устанавливает comment.leading/trailing/printed в соответствующие значения и добавляет комментарий в массив comments узла AST.

Флаг командной строки --debug-print-comments может помочь в отладке проблем с прикреплением комментариев. Он выводит подробный список комментариев, который включает информацию о том, как каждый комментарий был классифицирован (ownLine/endOfLine/remaining, leading/trailing/dangling) и к какому узлу он был прикреплён. Для встроенных языков Prettier эта информация также доступна на Playground (флажок «показать комментарии» в разделе «Отладка»).

options

options — это объект, содержащий настраиваемые параметры, которые поддерживает ваш плагин.

Пример:

export default {
  // ... plugin implementation
  options: {
    openingBraceNewLine: {
      type: "boolean",
      category: "Global",
      default: true,
      description: "Move open brace for code blocks onto new line.",
    },
  },
};

defaultOptions

Если вашему плагину требуются другие значения по умолчанию для некоторых основных опций Prettier, вы можете указать их в defaultOptions:

export default {
  // ... plugin implementation
  defaultOptions: {
    tabWidth: 4,
  },
};

Функции для работы с данными

Модуль util из ядра Prettier считается частным API и не предназначен для использования плагинами. Вместо этого модуль util-shared предоставляет следующий ограниченный набор функций для работы с данными для плагинов:

type Quote = '"' | "'";
type SkipOptions = { backwards?: boolean };

function getMaxContinuousCount(text: string, searchString: string): number;

function getStringWidth(text: string): number;

function getAlignmentSize(
  text: string,
  tabWidth: number,
  startIndex?: number,
): number;

function getIndentSize(value: string, tabWidth: number): number;

function skip(
  characters: string | RegExp,
): (
  text: string,
  startIndex: number | false,
  options?: SkipOptions,
) => number | false;

function skipWhitespace(
  text: string,
  startIndex: number | false,
  options?: SkipOptions,
): number | false;

function skipSpaces(
  text: string,
  startIndex: number | false,
  options?: SkipOptions,
): number | false;

function skipToLineEnd(
  text: string,
  startIndex: number | false,
  options?: SkipOptions,
): number | false;

function skipEverythingButNewLine(
  text: string,
  startIndex: number | false,
  options?: SkipOptions,
): number | false;

function skipInlineComment(
  text: string,
  startIndex: number | false,
): number | false;

function skipTrailingComment(
  text: string,
  startIndex: number | false,
): number | false;

function skipNewline(
  text: string,
  startIndex: number | false,
  options?: SkipOptions,
): number | false;

function hasNewline(
  text: string,
  startIndex: number,
  options?: SkipOptions,
): boolean;

function hasNewlineInRange(
  text: string,
  startIndex: number,
  startIndex: number,
): boolean;

function hasSpaces(
  text: string,
  startIndex: number,
  options?: SkipOptions,
): boolean;

function getPreferredQuote(
  text: string,
  preferredQuoteOrPreferSingleQuote: Quote | boolean,
): Quote;

function makeString(
  rawText: string,
  enclosingQuote: Quote,
  unescapeUnnecessaryEscapes?: boolean,
): string;

function getNextNonSpaceNonCommentCharacter(
  text: string,
  startIndex: number,
): string;

function getNextNonSpaceNonCommentCharacterIndex(
  text: string,
  startIndex: number,
): number | false;

function isNextLineEmpty(text: string, startIndex: number): boolean;

function isPreviousLineEmpty(text: string, startIndex: number): boolean;

Учебные пособия

  • Как написать плагин для Prettier: Научит вас как написать очень простой плагин Prettier для TOML.

Тестирование плагинов

Поскольку плагины могут быть разрешены с использованием относительных путей, при работе с ними вы можете сделать следующее:

import * as prettier from "prettier";
const code = "(add 1 2)";
await prettier.format(code, {
  parser: "lisp",
  plugins: ["."],
});

Это позволит разрешить плагин относительно текущей рабочей директории.

© James Long and contributors
https://prettier.io/docs/en/plugins

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API