diff --git a/.changeset/use-marko-parse.md b/.changeset/use-marko-parse.md new file mode 100644 index 0000000..97f5dea --- /dev/null +++ b/.changeset/use-marko-parse.md @@ -0,0 +1,5 @@ +--- +"prettier-plugin-marko": minor +--- + +Use the new shared `@marko/parse` package for parsing instead of a vendored syntax tree builder. diff --git a/package.json b/package.json index ef789f4..3b0336b 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "test:update": "vitest run --update" }, "dependencies": { - "htmljs-parser": "^5.12.1" + "@marko/parse": "link:../language-server/packages/parse" }, "devDependencies": { "@changesets/changelog-github": "^0.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a69a0a9..a7c4842 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - htmljs-parser: - specifier: ^5.12.1 - version: 5.12.1 + '@marko/parse': + specifier: link:../language-server/packages/parse + version: link:../language-server/packages/parse devDependencies: '@changesets/changelog-github': specifier: ^0.7.0 diff --git a/src/index.ts b/src/index.ts index 5ed0fd8..b7423cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,12 @@ +import { + CommentType, + type Node, + NodeType, + parse as parseMarko, + type Parsed, + type Range, + TagType, +} from "@marko/parse"; import { type AstPath, type Doc, @@ -10,15 +19,6 @@ import { type SupportOptions, } from "prettier"; -import { - CommentType, - type Node, - NodeType, - parse as parseMarko, - type Parsed, - type Range, - TagType, -} from "./parser"; import { getFormattedBody } from "./utils/get-formatted-body"; import { getParserFromExt, @@ -152,6 +152,11 @@ export const parsers: Record> = { parse(text, opts) { const { program } = (opts._markoParsed = parseMarko(text, opts.filepath)); + // Comments are printed as part of the body; prettier core registers + // anything on the root's `comments` and would throw in its + // ensureAllCommentsPrinted check since we print them ourselves. + program.comments = undefined; + if (opts.markoSyntax === "auto") { opts.markoSyntax = "html"; @@ -303,12 +308,13 @@ const embedHandlers: EmbedHandlers = { [NodeType.Static]: async (toDoc, _print, path, opts) => { const { node } = path; + const target = node.target || "static"; const code = opts - ._markoParsed!.code.slice(node.start + node.target.length + 1, node.end) + ._markoParsed!.code.slice(node.start + target.length + 1, node.end) .replace(/^\s*\{([\s\S]*)\}\s*$/, "$1") .trim(); return code - ? [`${node.target} `, toValidStatement(await toDoc(code, stmtParse))] + ? [`${target} `, toValidStatement(await toDoc(code, stmtParse))] : []; }, diff --git a/src/parser.ts b/src/parser.ts deleted file mode 100644 index 95e0f98..0000000 --- a/src/parser.ts +++ /dev/null @@ -1,835 +0,0 @@ -import { createParser, type Range, type Ranges, TagType } from "htmljs-parser"; - -const styleBlockReg = /((?:\.[^\s\\/:*?"<>|({]+)*)\s*\{/y; - -export type Repeated = [T, ...T[]] | [...T[], T] | [T, ...T[], T]; -export type Repeatable = undefined | Repeated; -export const UNFINISHED = Number.MAX_SAFE_INTEGER; - -export { - getLines, - getLocation, - getPosition, - type Location, - type Position, - type Range, - type Ranges, - TagType, -} from "htmljs-parser"; - -export type Parsed = ReturnType; -export enum NodeType { - Program, - Tag, - OpenTagName, - ShorthandId, - ShorthandClassName, - TagTypeArgs, - TagTypeParams, - TagVar, - TagArgs, - TagParams, - AttrNamed, - AttrName, - AttrArgs, - AttrValue, - AttrMethod, - AttrSpread, - AttrTag, - Text, - CDATA, - Doctype, - Declaration, - Comment, - Placeholder, - Scriptlet, - Import, - Export, - Class, - Style, - Static, -} - -export enum CommentType { - line, - block, - html, -} - -export namespace Node { - export type AnyNode = - | Program - | Tag - | OpenTagName - | ShorthandId - | ShorthandClassName - | TagTypeArgs - | TagTypeParams - | TagVar - | TagArgs - | TagParams - | AttrNamed - | AttrName - | AttrArgs - | AttrValue - | AttrMethod - | AttrSpread - | AttrTag - | Text - | CDATA - | Doctype - | Declaration - | Comment - | Placeholder - | Scriptlet - | Import - | Export - | Class - | Style - | Static; - export type ParentNode = Program | Tag | AttrTag; - export type StaticNode = Import | Export | Class | Style | Static; - export type ParentTag = Tag | AttrTag; - export type AttrNode = AttrNamed | AttrSpread; - export type ControlFlowTag = Tag & { - nameText: "if" | "else" | "else-if" | "for" | "while"; - bodyType: typeof TagType.html; - }; - export type ChildNode = - | Tag - | AttrTag - | Text - | Doctype - | Declaration - | CDATA - | Placeholder - | Scriptlet - | Comment; - - export interface Program extends Range { - type: NodeType.Program; - parent: undefined; - body: (ChildNode | StaticNode)[]; - } - - export interface Tag extends Range { - type: NodeType.Tag; - parent: ParentNode; - owner: undefined; - concise: boolean; - selfClosed: boolean; - hasAttrTags: boolean; - open: Range; - close: Range | undefined; - nameText: string | undefined; - bodyType: Exclude; - name: OpenTagName; - var: TagVar | undefined; - args: TagArgs | undefined; - params: TagParams | undefined; - shorthandId: ShorthandId | undefined; - shorthandClassNames: Repeatable; - typeArgs: TagTypeArgs | undefined; - typeParams: TagTypeParams | undefined; - attrs: Repeatable; - body: Repeatable; - } - - export interface AttrTag extends Range { - type: NodeType.AttrTag; - parent: ParentTag; - owner: Tag | undefined; - concise: boolean; - selfClosed: boolean; - hasAttrTags: boolean; - open: Range; - close: Range | undefined; - nameText: string; - bodyType: typeof TagType.html; - name: OpenTagName; - var: TagVar | undefined; - args: TagArgs | undefined; - params: TagParams | undefined; - shorthandId: ShorthandId | undefined; - shorthandClassNames: Repeatable; - typeArgs: TagTypeArgs | undefined; - typeParams: TagTypeParams | undefined; - attrs: Repeatable; - body: Repeatable; - } - - export interface OpenTagName extends Ranges.Template { - type: NodeType.OpenTagName; - parent: ParentTag; - } - - export interface ShorthandId extends Ranges.Template { - type: NodeType.ShorthandId; - parent: ParentTag; - } - - export interface ShorthandClassName extends Ranges.Template { - type: NodeType.ShorthandClassName; - parent: ParentTag; - } - - export interface TagTypeArgs extends Ranges.Value { - type: NodeType.TagTypeArgs; - parent: ParentTag; - } - - export interface TagTypeParams extends Ranges.Value { - type: NodeType.TagTypeParams; - parent: ParentTag; - } - - export interface TagVar extends Ranges.Value { - type: NodeType.TagVar; - parent: ParentTag; - } - - export interface TagArgs extends Ranges.Value { - type: NodeType.TagArgs; - parent: ParentTag; - } - - export interface TagParams extends Ranges.Value { - type: NodeType.TagParams; - parent: ParentTag; - } - - export interface Text extends Range { - type: NodeType.Text; - parent: ParentNode; - } - - export interface CDATA extends Ranges.Value { - type: NodeType.CDATA; - parent: ParentNode; - } - - export interface Doctype extends Ranges.Value { - type: NodeType.Doctype; - parent: ParentNode; - } - - export interface Declaration extends Ranges.Value { - type: NodeType.Declaration; - parent: ParentNode; - } - - export interface Comment extends Ranges.Value { - type: NodeType.Comment; - parent: ParentNode; - commentType: CommentType; - } - - export interface Placeholder extends Ranges.Value { - type: NodeType.Placeholder; - parent: ParentNode; - escape: boolean; - } - - export interface Scriptlet extends Ranges.Value { - type: NodeType.Scriptlet; - parent: ParentNode; - block: boolean; - } - - export interface AttrNamed extends Range { - type: NodeType.AttrNamed; - parent: ParentTag; - name: AttrName; - args: undefined | AttrArgs; - value: undefined | AttrValue | AttrMethod; - } - - export interface AttrName extends Range { - type: NodeType.AttrName; - parent: AttrNamed; - } - - export interface AttrArgs extends Ranges.Value { - type: NodeType.AttrArgs; - parent: AttrNamed; - } - - export interface AttrValue extends Range { - type: NodeType.AttrValue; - parent: AttrNamed; - value: Range; - bound: boolean; - } - - export interface AttrMethod extends Range { - type: NodeType.AttrMethod; - parent: AttrNamed; - typeParams: undefined | Ranges.Value; - params: Range; - body: Range; - } - - export interface AttrSpread extends Ranges.Value { - type: NodeType.AttrSpread; - parent: ParentTag; - } - - export interface Import extends Range { - type: NodeType.Import; - parent: ParentNode; - } - - export interface Export extends Range { - type: NodeType.Export; - parent: ParentNode; - } - - export interface Class extends Range { - type: NodeType.Class; - parent: ParentNode; - } - - export interface Style extends Range { - type: NodeType.Style; - parent: ParentNode; - ext: string | undefined; - value: Range; - } - - export interface Static extends Range { - type: NodeType.Static; - parent: ParentNode; - target: "client" | "server" | "static"; - } -} - -export function parse(code: string, filename = "index.marko") { - const builder = new Builder(code); - const parser = createParser(builder); - const { program } = builder; - - parser.parse(code); - return { - read: parser.read, - locationAt: parser.locationAt, - positionAt: parser.positionAt, - filename, - program, - code, - }; -} - -class Builder { - public program: Node.Program; - #code: string; - #openTagStart: Range | undefined; - #parentNode: Node.ParentNode; - #staticNode: Node.StaticNode | undefined; - #attrNode: Node.AttrNamed | undefined; - - constructor(code: string) { - this.#code = code; - this.program = this.#parentNode = { - type: NodeType.Program, - parent: undefined, - body: [], - start: 0, - end: code.length, - }; - } - - onText(range: Range) { - pushBody(this.#parentNode, { - type: NodeType.Text, - parent: this.#parentNode, - start: range.start, - end: range.end, - }); - } - onCDATA(range: Ranges.Value) { - pushBody(this.#parentNode, { - type: NodeType.CDATA, - parent: this.#parentNode, - value: range.value, - start: range.start, - end: range.end, - }); - } - onDoctype(range: Ranges.Value) { - pushBody(this.#parentNode, { - type: NodeType.Doctype, - parent: this.#parentNode, - value: range.value, - start: range.start, - end: range.end, - }); - } - onDeclaration(range: Ranges.Value) { - pushBody(this.#parentNode, { - type: NodeType.Declaration, - parent: this.#parentNode, - value: range.value, - start: range.start, - end: range.end, - }); - } - onComment(range: Ranges.Value) { - let commentType: CommentType = CommentType.html; - switch (this.#code.charCodeAt(range.start + 1)) { - case 47: // / - commentType = CommentType.line; - break; - case 42: // * - commentType = CommentType.block; - break; - } - pushBody(this.#parentNode, { - type: NodeType.Comment, - parent: this.#parentNode, - commentType, - value: range.value, - start: range.start, - end: range.end, - }); - } - onPlaceholder(range: Ranges.Placeholder) { - pushBody(this.#parentNode, { - type: NodeType.Placeholder, - parent: this.#parentNode, - value: range.value, - escape: range.escape, - start: range.start, - end: range.end, - }); - } - onScriptlet(range: Ranges.Scriptlet) { - pushBody(this.#parentNode, { - type: NodeType.Scriptlet, - parent: this.#parentNode, - value: range.value, - block: range.block, - start: range.start, - end: range.end, - }); - } - onOpenTagStart(range: Range) { - this.#openTagStart = range; - } - onOpenTagName(range: Ranges.Template) { - let concise = true; - let start = range.start; - let type = NodeType.Tag; - let bodyType: TagType = TagType.html; - let nameText: string | undefined = undefined; - - if (this.#openTagStart) { - concise = false; - start = this.#openTagStart.start; - this.#openTagStart = undefined; - } - - if (!range.expressions.length) { - switch ((nameText = this.#code.slice(range.start, range.end) || "div")) { - // All statement types will early return. - case "style": { - styleBlockReg.lastIndex = range.end; - const styleBlockMatch = styleBlockReg.exec(this.#code); - - if (styleBlockMatch) { - const [{ length }, ext] = styleBlockMatch; - this.program.body.push( - (this.#staticNode = { - type: NodeType.Style, - parent: this.program, - ext: ext || undefined, - value: { - start: range.end + length, - end: UNFINISHED, - }, - start: range.start, - end: UNFINISHED, - }), - ); - return TagType.statement; - } else { - bodyType = TagType.text; - break; - } - } - case "class": - this.program.body.push( - (this.#staticNode = { - type: NodeType.Class, - parent: this.program, - start: range.start, - end: UNFINISHED, - }), - ); - - return TagType.statement; - case "export": - this.program.body.push( - (this.#staticNode = { - type: NodeType.Export, - parent: this.program, - start: range.start, - end: UNFINISHED, - }), - ); - - return TagType.statement; - case "import": - this.program.body.push( - (this.#staticNode = { - type: NodeType.Import, - parent: this.program, - start: range.start, - end: UNFINISHED, - }), - ); - - return TagType.statement; - case "server": - case "client": - case "static": - this.program.body.push( - (this.#staticNode = { - type: NodeType.Static, - parent: this.program, - target: nameText, - start: range.start, - end: UNFINISHED, - }), - ); - - return TagType.statement; - - // The following are all still tags, - // but with a different body type. - case "area": - case "base": - case "br": - case "col": - case "embed": - case "hr": - case "img": - case "input": - case "link": - case "meta": - case "param": - case "source": - case "track": - case "wbr": - case "const": - case "debug": - case "id": - case "let": - case "lifecycle": - case "log": - case "return": - bodyType = TagType.void; - break; - case "html-comment": - case "html-script": - case "html-style": - case "script": - case "textarea": - bodyType = TagType.text; - break; - default: - if (nameText[0] === "@") { - type = NodeType.AttrTag; - } - break; - } - } - - const parent = this.#parentNode as Node.ParentNode; - const end = UNFINISHED; - const name: Node.OpenTagName = { - type: NodeType.OpenTagName, - parent: undefined as unknown as Node.Tag, - quasis: range.quasis, - expressions: range.expressions, - start: range.start, - end: range.end, - }; - const tag = - (this.#parentNode = - name.parent = - { - type, - parent, - owner: undefined, - concise, - selfClosed: false, - hasAttrTags: false, - open: { start, end }, - nameText, - name, - var: undefined, - args: undefined, - params: undefined, - shorthandId: undefined, - shorthandClassNames: undefined, - typeArgs: undefined, - typeParams: undefined, - attrs: undefined, - bodyType, - body: undefined, - close: undefined, - start, - end, - } as Node.ParentTag); - - if (tag.type === NodeType.AttrTag) { - let parentTag = parent; - let nameText = tag.nameText.slice(1); - - while (parentTag.type === NodeType.Tag && isControlFlowTag(parentTag)) { - parentTag.hasAttrTags = true; - parentTag = parentTag.parent; - } - - switch (parentTag.type) { - case NodeType.AttrTag: - tag.owner = parentTag.owner; - parentTag.hasAttrTags = true; - nameText = `${parentTag.nameText}:${nameText}`; - break; - case NodeType.Tag: - tag.owner = parentTag; - parentTag.hasAttrTags = true; - nameText = `${parentTag.nameText || "*"}:${nameText}`; - break; - } - - // This name includes the full ancestry of the attribute tag and can be used in `TaglibLookup.getTag`. - tag.nameText = nameText; - } - - pushBody(parent, tag); - this.#openTagStart = undefined; - return bodyType; - } - onTagShorthandId(range: Ranges.Template) { - const parent = this.#parentNode as Node.ParentTag; - parent.shorthandId = { - type: NodeType.ShorthandId, - parent, - quasis: range.quasis, - expressions: range.expressions, - start: range.start, - end: range.end, - }; - } - onTagShorthandClass(range: Ranges.Template) { - const parent = this.#parentNode as Node.ParentTag; - const shorthandClassName: Node.ShorthandClassName = { - type: NodeType.ShorthandClassName, - parent, - quasis: range.quasis, - expressions: range.expressions, - start: range.start, - end: range.end, - }; - - if (parent.shorthandClassNames) { - parent.shorthandClassNames.push(shorthandClassName); - } else { - parent.shorthandClassNames = [shorthandClassName]; - } - } - onTagTypeArgs(range: Ranges.Value) { - const parent = this.#parentNode as Node.ParentTag; - parent.typeArgs = { - type: NodeType.TagTypeArgs, - parent, - value: range.value, - start: range.start, - end: range.end, - }; - } - onTagTypeParams(range: Ranges.Value) { - const parent = this.#parentNode as Node.ParentTag; - parent.typeParams = { - type: NodeType.TagTypeParams, - parent, - value: range.value, - start: range.start, - end: range.end, - }; - } - onTagVar(range: Ranges.Value) { - const parent = this.#parentNode as Node.ParentTag; - parent.var = { - type: NodeType.TagVar, - parent, - value: range.value, - start: range.start, - end: range.end, - }; - } - onTagParams(range: Ranges.Value) { - const parent = this.#parentNode as Node.ParentTag; - parent.params = { - type: NodeType.TagParams, - parent, - value: range.value, - start: range.start, - end: range.end, - }; - } - onTagArgs(range: Ranges.Value) { - const parent = this.#parentNode as Node.ParentTag; - parent.args = { - type: NodeType.TagArgs, - parent, - value: range.value, - start: range.start, - end: range.end, - }; - } - onAttrName(range: Ranges.Value) { - const parent = this.#parentNode as Node.ParentTag; - const name: Node.AttrName = { - type: NodeType.AttrName, - parent: undefined as unknown as Node.AttrNamed, - start: range.start, - end: range.end, - }; - - pushAttr( - parent, - (this.#attrNode = name.parent = - { - type: NodeType.AttrNamed, - parent, - name, - value: undefined, - args: undefined, - start: range.start, - end: range.end, - }), - ); - } - onAttrArgs(range: Ranges.Value) { - const parent = this.#attrNode!; - parent.args = { - type: NodeType.AttrArgs, - parent, - value: range.value, - start: range.start, - end: range.end, - }; - parent.end = range.end; - } - onAttrValue(range: Ranges.AttrValue) { - const parent = this.#attrNode!; - parent.value = { - type: NodeType.AttrValue, - parent, - value: range.value, - bound: range.bound, - start: range.start, - end: range.end, - }; - parent.end = range.end; - } - onAttrMethod(range: Ranges.AttrMethod) { - const parent = this.#attrNode!; - parent.value = { - type: NodeType.AttrMethod, - parent, - typeParams: range.typeParams, - params: range.params, - body: range.body, - start: range.start, - end: range.end, - }; - parent.end = range.end; - } - onAttrSpread(range: Ranges.Value) { - const parent = this.#parentNode as Node.ParentTag; - pushAttr(parent, { - type: NodeType.AttrSpread, - parent, - value: range.value, - start: range.start, - end: range.end, - }); - } - onOpenTagEnd(range: Ranges.OpenTagEnd) { - if (this.#staticNode) { - if (this.#staticNode.type === NodeType.Style) { - this.#staticNode.value.end = range.end - 1; - } - - this.#staticNode.end = range.end; - this.#staticNode = undefined; - } else { - this.#attrNode = undefined; - - const tag = this.#parentNode as Node.ParentTag; - tag.open.end = range.end; - - if (range.selfClosed || tag.bodyType === TagType.void) { - this.#parentNode = tag.parent; - tag.end = range.end; - tag.selfClosed = range.selfClosed; - } - } - } - onCloseTagStart(range: Range) { - (this.#parentNode as Node.ParentTag).close = { - start: range.start, - end: UNFINISHED, - }; - } - onCloseTagEnd(range: Range) { - const parent = this.#parentNode as Node.ParentTag; - if (hasCloseTag(parent)) parent.close.end = range.end; - parent.end = range.end; - this.#parentNode = parent.parent; - } -} - -function pushBody(parent: Node.ParentNode, node: Node.ChildNode) { - if (parent.body) { - parent.body.push(node); - } else { - parent.body = [node]; - } -} - -function pushAttr(parent: Node.ParentTag, node: Node.AttrNode) { - if (parent.attrs) { - parent.attrs.push(node); - } else { - parent.attrs = [node]; - } -} - -function hasCloseTag( - parent: Node.AnyNode, -): parent is Node.ParentTag & { close: Range } { - return (parent as Node.ParentTag).close !== undefined; -} - -/** - * Used to check if a node should be ignored as the parent of an attribute tag. - * When control flow is the parent of an attribute tag, we add the attribute tag to - * the closest non control flow ancestor attrs instead. - */ -export function isControlFlowTag(node: Node.Tag): node is Node.ControlFlowTag { - switch (node.nameText) { - case "if": - case "else": - case "else-if": - case "for": - case "while": - return true; - default: - return false; - } -} diff --git a/src/utils/get-formatted-body.ts b/src/utils/get-formatted-body.ts index 2b4acad..6ad5839 100644 --- a/src/utils/get-formatted-body.ts +++ b/src/utils/get-formatted-body.ts @@ -1,6 +1,6 @@ +import { type Node, NodeType } from "@marko/parse"; import { type AstPath, type Doc, doc, type Options } from "prettier"; -import { type Node, NodeType } from "../parser"; import { read } from "./read"; const placeholderReg = /MARKO_(\d+)_/g; const { mapDoc } = doc.utils; diff --git a/src/utils/get-parser-name.ts b/src/utils/get-parser-name.ts index 56da18f..25ca3d0 100644 --- a/src/utils/get-parser-name.ts +++ b/src/utils/get-parser-name.ts @@ -1,6 +1,6 @@ +import { type Node, NodeType } from "@marko/parse"; import type { Options } from "prettier"; -import { type Node, NodeType } from "../parser"; import { read } from "./read"; export function getParserFromExt(ext: string) { diff --git a/src/utils/read.ts b/src/utils/read.ts index 835cc2b..9f0ad49 100644 --- a/src/utils/read.ts +++ b/src/utils/read.ts @@ -1,6 +1,5 @@ +import type { Range } from "@marko/parse"; import type { Options } from "prettier"; - -import type { Range } from "../parser"; export function read(range: Range, opts: Options) { return opts._markoParsed!.read(range); } diff --git a/src/utils/to-valid-doc.ts b/src/utils/to-valid-doc.ts index 98c8ce7..9143814 100644 --- a/src/utils/to-valid-doc.ts +++ b/src/utils/to-valid-doc.ts @@ -3,7 +3,7 @@ import { isValidScriptlet, isValidStatement, Validity, -} from "htmljs-parser"; +} from "@marko/parse"; import { type Doc, doc as d } from "prettier"; import printDoc from "./print-doc";