Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ nodes:
- HTMLCloseTagNode
- HTMLOmittedCloseTagNode
- HTMLVirtualCloseTagNode
- ERBEndNode

- name: is_void
type: boolean
Expand Down
19 changes: 18 additions & 1 deletion javascript/packages/core/src/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
HTMLAttributeNode,
HTMLAttributeNameNode,
HTMLAttributeValueNode,
HTMLCommentNode
HTMLCommentNode,
WhitespaceNode
} from "./nodes.js"

import {
Expand All @@ -38,6 +39,9 @@ import {
} from "./node-type-guards.js"

import { Location } from "./location.js"
import { Range } from "./range.js"
import { Token } from "./token.js"

import type { Position } from "./position.js"

export type ERBOutputNode = ERBNode & {
Expand Down Expand Up @@ -856,3 +860,16 @@ export function createLiteral(content: string): LiteralNode {
errors: [],
})
}

export function createSyntheticToken(value: string, type = "TOKEN_SYNTHETIC"): Token {
return new Token(value, Range.zero, Location.zero, type)
}

export function createWhitespaceNode(): WhitespaceNode {
return new WhitespaceNode({
type: "AST_WHITESPACE_NODE",
location: Location.zero,
errors: [],
value: createSyntheticToken(" "),
})
}
24 changes: 10 additions & 14 deletions javascript/packages/language-server/src/comment_ast_utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ParserService } from "./parser_service"
import { LineContextCollector } from "./line_context_collector"

import { HTMLCommentNode, Token, Location, Range } from "@herb-tools/core"
import { HTMLCommentNode, Location, createSyntheticToken } from "@herb-tools/core"
import { IdentityPrinter } from "@herb-tools/printer"

import { isERBCommentNode, isLiteralNode, createLiteral } from "@herb-tools/core"
Expand All @@ -25,19 +25,15 @@ interface LineSegment {
node?: ERBContentNode
}

export function createSyntheticToken(type: string, value: string): Token {
return new Token(value, Range.zero, Location.zero, type)
}

export function commentERBNode(node: ERBContentNode): void {
const mutable = asMutable(node)

if (mutable.tag_opening) {
const currentValue = mutable.tag_opening.value

mutable.tag_opening = createSyntheticToken(
mutable.tag_opening.type,
currentValue.substring(0, 2) + "#" + currentValue.substring(2)
currentValue.substring(0, 2) + "#" + currentValue.substring(2),
mutable.tag_opening.type
)
}
}
Expand All @@ -56,10 +52,10 @@ export function uncommentERBNode(node: ERBContentNode): void {
contentValue.startsWith(" = ") ||
contentValue.startsWith(" - ")
) {
mutable.tag_opening = createSyntheticToken(mutable.tag_opening.type, "<%")
mutable.content = createSyntheticToken(mutable.content!.type, contentValue.substring(1))
mutable.tag_opening = createSyntheticToken("<%", mutable.tag_opening.type)
mutable.content = createSyntheticToken(contentValue.substring(1), mutable.content!.type)
} else {
mutable.tag_opening = createSyntheticToken(mutable.tag_opening.type, "<%")
mutable.tag_opening = createSyntheticToken("<%", mutable.tag_opening.type)
}
}
}
Expand Down Expand Up @@ -158,9 +154,9 @@ export function commentLineContent(content: string, erbNodes: ERBContentNode[],
type: "AST_HTML_COMMENT_NODE",
location: Location.zero,
errors: [],
comment_start: createSyntheticToken("TOKEN_HTML_COMMENT_START", "<!--"),
comment_start: createSyntheticToken("<!--", "TOKEN_HTML_COMMENT_START"),
children: [createLiteral(" "), ...(children.slice() as Node[]), createLiteral(" ")],
comment_end: createSyntheticToken("TOKEN_HTML_COMMENT_END", "-->"),
comment_end: createSyntheticToken("-->", "TOKEN_HTML_COMMENT_END"),
})

children.length = 0
Expand All @@ -173,9 +169,9 @@ export function commentLineContent(content: string, erbNodes: ERBContentNode[],
type: "AST_HTML_COMMENT_NODE",
location: Location.zero,
errors: [],
comment_start: createSyntheticToken("TOKEN_HTML_COMMENT_START", "<!--"),
comment_start: createSyntheticToken("<!--", "TOKEN_HTML_COMMENT_START"),
children: [createLiteral(" "), ...(children.slice() as Node[]), createLiteral(" ")],
comment_end: createSyntheticToken("TOKEN_HTML_COMMENT_END", "-->"),
comment_end: createSyntheticToken("-->", "TOKEN_HTML_COMMENT_END"),
})

children.length = 0
Expand Down
7 changes: 2 additions & 5 deletions javascript/packages/linter/src/rules/html-no-space-in-tag.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Token, Location, WhitespaceNode } from "@herb-tools/core"
import { Token, WhitespaceNode, createWhitespaceNode } from "@herb-tools/core"
import { ParserRule, BaseAutofixContext } from "../types.js"

import { findParent, BaseRuleVisitor } from "./rule-utils.js"
Expand Down Expand Up @@ -168,10 +168,7 @@ export class HTMLNoSpaceInTagRule extends ParserRule<HTMLNoSpaceInTagAutofixCont
if (!node) return null

if (isHTMLOpenTagNode(node)) {
const token = Token.from({ type: "TOKEN_WHITESPACE", value: " ", range: [0, 0], location: Location.zero })
const whitespace = new WhitespaceNode({ type: "AST_WHITESPACE_NODE", value: token, location: Location.zero, errors: [] })

node.children.push(whitespace)
node.children.push(createWhitespaceNode())

return result
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Visitor, Location, HTMLOpenTagNode, HTMLCloseTagNode, HTMLElementNode, HTMLAttributeValueNode, WhitespaceNode, ERBContentNode } from "@herb-tools/core"
import { isHTMLAttributeNode, isERBOpenTagNode, isRubyLiteralNode, isRubyHTMLAttributesSplatNode, createSyntheticToken } from "@herb-tools/core"

import { ASTRewriter } from "../ast-rewriter.js"
import { asMutable } from "../mutable.js"

import type { RewriteContext } from "../context.js"
import type { Node } from "@herb-tools/core"

function createWhitespaceNode(): WhitespaceNode {
return new WhitespaceNode({
type: "AST_WHITESPACE_NODE",
location: Location.zero,
errors: [],
value: createSyntheticToken(" "),
})
}

class ActionViewTagHelperToHTMLVisitor extends Visitor {
visitHTMLElementNode(node: HTMLElementNode): void {
if (!node.element_source) {
this.visitChildNodes(node)
return
}

const openTag = node.open_tag

if (!isERBOpenTagNode(openTag)) {
this.visitChildNodes(node)
return
}

const tagName = openTag.tag_name

if (!tagName) {
this.visitChildNodes(node)
return
}

const htmlChildren: Node[] = []

for (const child of openTag.children) {
if (isRubyHTMLAttributesSplatNode(child)) {
htmlChildren.push(createWhitespaceNode())

htmlChildren.push(new ERBContentNode({
type: "AST_ERB_CONTENT_NODE",
location: Location.zero,
errors: [],
tag_opening: createSyntheticToken("<%="),
content: createSyntheticToken(` ${child.content} `),
tag_closing: createSyntheticToken("%>"),
parsed: false,
valid: true,
}))

continue
}

htmlChildren.push(createWhitespaceNode())

if (isHTMLAttributeNode(child)) {
if (child.equals && child.equals.value !== "=") {
asMutable(child).equals = createSyntheticToken("=")
}

if (child.value) {
this.transformAttributeValue(child.value)
}

htmlChildren.push(child)
}
}

const htmlOpenTag = new HTMLOpenTagNode({
type: "AST_HTML_OPEN_TAG_NODE",
location: openTag.location,
errors: [],
tag_opening: createSyntheticToken("<"),
tag_name: createSyntheticToken(tagName.value),
tag_closing: createSyntheticToken(node.is_void ? " />" : ">"),
children: htmlChildren,
is_void: node.is_void,
})

asMutable(node).open_tag = htmlOpenTag

if (node.is_void) {
asMutable(node).close_tag = null
} else if (node.close_tag) {
const htmlCloseTag = new HTMLCloseTagNode({
type: "AST_HTML_CLOSE_TAG_NODE",
location: node.close_tag.location,
errors: [],
tag_opening: createSyntheticToken("</"),
tag_name: createSyntheticToken(tagName.value),
children: [],
tag_closing: createSyntheticToken(">"),
})

asMutable(node).close_tag = htmlCloseTag
}

asMutable(node).element_source = "HTML"

if (node.body) {
for (const child of node.body) {
this.visit(child)
}
}
}

private transformAttributeValue(value: HTMLAttributeValueNode): void {
const mutableValue = asMutable(value)
const hasRubyLiteral = value.children.some(child => isRubyLiteralNode(child))

if (hasRubyLiteral) {
const newChildren: Node[] = value.children.map(child => {
if (isRubyLiteralNode(child)) {
return new ERBContentNode({
type: "AST_ERB_CONTENT_NODE",
location: child.location,
errors: [],
tag_opening: createSyntheticToken("<%="),
content: createSyntheticToken(` ${child.content} `),
tag_closing: createSyntheticToken("%>"),
parsed: false,
valid: true,
})
}

return child
})

mutableValue.children = newChildren

if (!value.quoted) {
mutableValue.quoted = true
mutableValue.open_quote = createSyntheticToken('"')
mutableValue.close_quote = createSyntheticToken('"')
}
}
}
}

export class ActionViewTagHelperToHTMLRewriter extends ASTRewriter {
get name(): string {
return "action-view-tag-helper-to-html"
}

get description(): string {
return "Converts ActionView tag helpers (tag.*, content_tag, link_to) to raw HTML elements"
}

rewrite<T extends Node>(node: T, _context: RewriteContext): T {
const visitor = new ActionViewTagHelperToHTMLVisitor()

visitor.visit(node)

return node
}
}
Loading
Loading