Skip to content
Draft
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
158 changes: 157 additions & 1 deletion config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,80 @@ errors:
- name: nested_tag_column
type: size_t

- name: RenderAmbiguousLocalsError
message:
template: "Did you mean `render partial: '%s', locals: { ... }`? Using `render '%s', locals: { ... }` passes a local variable named `locals` to the partial instead of setting the `locals:` option."
arguments:
- partial
- partial

fields:
- name: partial
type: string

- name: RenderMissingLocalsError
message:
template: "Wrap `%s` in `locals: { ... }` when using `partial:`. Use `render partial: '%s', locals: { %s }` instead. Without `locals:`, these keyword arguments are ignored."
arguments:
- keywords
- partial
- keywords

fields:
- name: partial
type: string

- name: keywords
type: string

- name: RenderNoArgumentsError
message:
template: "No arguments passed to `render`. It needs a partial name, a keyword argument like `partial:`, `template:`, or `layout:`, or a renderable object."
arguments: []

fields: []

- name: RenderConflictingPartialError
message:
template: "Both a positional partial `'%s'` and a keyword `partial: '%s'` were passed to `render`. Use one form or the other, not both."
arguments:
- positional_partial
- keyword_partial

fields:
- name: positional_partial
type: string

- name: keyword_partial
type: string

- name: RenderInvalidAsOptionError
message:
template: "The `as:` value `'%s'` is not a valid Ruby identifier. Use a name that starts with a lowercase letter or underscore, like `as: :item`."
arguments:
- as_value

fields:
- name: as_value
type: string

- name: RenderObjectAndCollectionError
message:
template: "The `object:` and `collection:` options are mutually exclusive. Use one or the other in your `render` call."
arguments: []

fields: []

- name: RenderLayoutWithoutBlockError
message:
template: "Layout rendering needs a block. Use `render layout: '%s' do ... end` so the layout has content to wrap."
arguments:
- layout

fields:
- name: layout
type: string

warnings:
fields: []
types: []
Expand Down Expand Up @@ -499,7 +573,7 @@ nodes:
- ERBUnlessNode

- name: tag_name
type: token
type: borrowed_token

- name: element_source
type: element_source
Expand Down Expand Up @@ -1013,6 +1087,88 @@ nodes:
type: node
kind: ERBEndNode

- name: RubyRenderLocalNode
fields:
- name: name
type: token

- name: value
type: node
kind: RubyLiteralNode

- name: ERBRenderNode
fields:
- name: tag_opening
type: token

- name: content
type: token

- name: tag_closing
type: token

- name: analyzed_ruby
type: analyzed_ruby

- name: prism_node
type: prism_node

- name: partial
type: token

- name: template_path
type: token

- name: layout
type: token

- name: file
type: token

- name: inline_template
type: token

- name: body
type: token

- name: plain
type: token

- name: html
type: token

- name: renderable
type: token

- name: collection
type: token

- name: object
type: token

- name: as_name
type: token

- name: spacer_template
type: token

- name: formats
type: token

- name: variants
type: token

- name: handlers
type: token

- name: content_type
type: token

- name: locals
type: array
kind:
- RubyRenderLocalNode

- name: ERBYieldNode
fields:
- name: tag_opening
Expand Down
4 changes: 4 additions & 0 deletions ext/herb/extension.c
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ static VALUE Herb_parse(int argc, VALUE* argv, VALUE self) {
}
if (!NIL_P(action_view_helpers) && RTEST(action_view_helpers)) { parser_options.action_view_helpers = true; }

VALUE render_nodes = rb_hash_lookup(options, rb_utf8_str_new_cstr("render_nodes"));
if (NIL_P(render_nodes)) { render_nodes = rb_hash_lookup(options, ID2SYM(rb_intern("render_nodes"))); }
if (!NIL_P(render_nodes) && RTEST(render_nodes)) { parser_options.render_nodes = true; }

VALUE prism_nodes = rb_hash_lookup(options, rb_utf8_str_new_cstr("prism_nodes"));
if (NIL_P(prism_nodes)) { prism_nodes = rb_hash_lookup(options, ID2SYM(rb_intern("prism_nodes"))); }
if (!NIL_P(prism_nodes) && RTEST(prism_nodes)) { parser_options.prism_nodes = true; }
Expand Down
1 change: 1 addition & 0 deletions ext/herb/extension_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ VALUE create_parse_result(AST_DOCUMENT_NODE_T* root, VALUE source, const parser_
rb_hash_aset(kwargs, ID2SYM(rb_intern("track_whitespace")), options->track_whitespace ? Qtrue : Qfalse);
rb_hash_aset(kwargs, ID2SYM(rb_intern("analyze")), options->analyze ? Qtrue : Qfalse);
rb_hash_aset(kwargs, ID2SYM(rb_intern("action_view_helpers")), options->action_view_helpers ? Qtrue : Qfalse);
rb_hash_aset(kwargs, ID2SYM(rb_intern("render_nodes")), options->render_nodes ? Qtrue : Qfalse);
rb_hash_aset(kwargs, ID2SYM(rb_intern("prism_nodes")), options->prism_nodes ? Qtrue : Qfalse);
rb_hash_aset(kwargs, ID2SYM(rb_intern("prism_nodes_deep")), options->prism_nodes_deep ? Qtrue : Qfalse);
rb_hash_aset(kwargs, ID2SYM(rb_intern("prism_program")), options->prism_program ? Qtrue : Qfalse);
Expand Down
8 changes: 8 additions & 0 deletions java/herb_jni.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ Java_org_herb_Herb_parse(JNIEnv* env, jclass clazz, jstring source, jobject opti
parser_options.action_view_helpers = (actionViewHelpers == JNI_TRUE);
}

jmethodID getRenderNodes =
(*env)->GetMethodID(env, optionsClass, "isRenderNodes", "()Z");

if (getRenderNodes != NULL) {
jboolean renderNodes = (*env)->CallBooleanMethod(env, options, getRenderNodes);
parser_options.render_nodes = (renderNodes == JNI_TRUE);
}

jmethodID getPrismNodes =
(*env)->GetMethodID(env, optionsClass, "isPrismNodes", "()Z");

Expand Down
10 changes: 10 additions & 0 deletions java/org/herb/ParserOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public class ParserOptions {
private boolean analyze = true;
private boolean strict = true;
private boolean actionViewHelpers = false;
private boolean renderNodes = false;
private boolean prismNodes = false;
private boolean prismNodesDeep = false;
private boolean prismProgram = false;
Expand Down Expand Up @@ -47,6 +48,15 @@ public boolean isActionViewHelpers() {
return actionViewHelpers;
}

public ParserOptions renderNodes(boolean value) {
this.renderNodes = value;
return this;
}

public boolean isRenderNodes() {
return renderNodes;
}

public ParserOptions prismNodes(boolean value) {
this.prismNodes = value;
return this;
Expand Down
6 changes: 6 additions & 0 deletions javascript/packages/core/src/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export interface ParseOptions {
analyze?: boolean
strict?: boolean
action_view_helpers?: boolean
render_nodes?: boolean
prism_nodes?: boolean
prism_nodes_deep?: boolean
prism_program?: boolean
Expand All @@ -15,6 +16,7 @@ export const DEFAULT_PARSER_OPTIONS: SerializedParserOptions = {
analyze: true,
strict: true,
action_view_helpers: false,
render_nodes: false,
prism_nodes: false,
prism_nodes_deep: false,
prism_program: false,
Expand All @@ -36,6 +38,9 @@ export class ParserOptions {
/** Whether ActionView tag helper transformation was enabled during parsing. */
readonly action_view_helpers: boolean

/** Whether ActionView render call detection was enabled during parsing. */
readonly render_nodes: boolean

/** Whether Prism node serialization was enabled during parsing. */
readonly prism_nodes: boolean

Expand All @@ -54,6 +59,7 @@ export class ParserOptions {
this.track_whitespace = options.track_whitespace ?? DEFAULT_PARSER_OPTIONS.track_whitespace
this.analyze = options.analyze ?? DEFAULT_PARSER_OPTIONS.analyze
this.action_view_helpers = options.action_view_helpers ?? DEFAULT_PARSER_OPTIONS.action_view_helpers
this.render_nodes = options.render_nodes ?? DEFAULT_PARSER_OPTIONS.render_nodes
this.prism_nodes = options.prism_nodes ?? DEFAULT_PARSER_OPTIONS.prism_nodes
this.prism_nodes_deep = options.prism_nodes_deep ?? DEFAULT_PARSER_OPTIONS.prism_nodes_deep
this.prism_program = options.prism_program ?? DEFAULT_PARSER_OPTIONS.prism_program
Expand Down
10 changes: 10 additions & 0 deletions javascript/packages/formatter/src/format-printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ import {
ERBUnlessNode,
ERBYieldNode,
ERBInNode,
ERBRenderNode,
RubyRenderLocalNode,
ERBOpenTagNode,
HTMLVirtualCloseTagNode,
XMLDeclarationNode,
Expand Down Expand Up @@ -1019,6 +1021,14 @@ export class FormatPrinter extends Printer implements TextFlowDelegate, Attribut
this.printERBNode(node)
}

visitERBRenderNode(node: ERBRenderNode) {
this.printERBNode(node)
}

visitRubyRenderLocalNode(_node: RubyRenderLocalNode) {
// extracted metadata, nothing to print
}

visitERBYieldNode(node: ERBYieldNode) {
this.trackBoundary(node, () => {
this.printERBNode(node)
Expand Down
56 changes: 56 additions & 0 deletions javascript/packages/formatter/test/erb/render-nodes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, test, beforeAll } from "vitest"
import { Herb } from "@herb-tools/node-wasm"
import { Formatter } from "../../src"
import { createExpectFormattedToMatch } from "../helpers.js"

import dedent from "dedent"

let formatter: Formatter
let expectFormattedToMatch: ReturnType<typeof createExpectFormattedToMatch>

describe("@herb-tools/formatter - Render Nodes", () => {
beforeAll(async () => {
await Herb.load()

formatter = new Formatter(Herb, {
indentWidth: 2,
maxLineLength: 80,
}, {
render_nodes: true,
})

expectFormattedToMatch = createExpectFormattedToMatch(formatter)
})

test("render partial string preserves source", () => {
expectFormattedToMatch(`<%= render "card" %>`)
})

test("render with keyword partial preserves source", () => {
expectFormattedToMatch(`<%= render partial: "card" %>`)
})

test("render with locals preserves source", () => {
expectFormattedToMatch(`<%= render partial: "card", locals: { title: @title } %>`)
})

test("render with implicit locals preserves source", () => {
expectFormattedToMatch(`<%= render "card", title: @title, body: "Hello" %>`)
})

test("render with collection preserves source", () => {
expectFormattedToMatch(`<%= render partial: "product", collection: @products %>`)
})

test("render object preserves source", () => {
expectFormattedToMatch(`<%= render @product %>`)
})

test("render inside HTML element", () => {
expectFormattedToMatch(dedent`
<div>
<%= render "card" %>
</div>
`)
})
})
4 changes: 4 additions & 0 deletions javascript/packages/linter/test/parse-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ describe("ParseCache", () => {
prism_nodes: false,
prism_nodes_deep: false,
prism_program: false,
render_nodes: false,
strict: true,
action_view_helpers: false,
})
Expand All @@ -86,6 +87,7 @@ describe("ParseCache", () => {
prism_nodes: false,
prism_nodes_deep: false,
prism_program: false,
render_nodes: false,
strict: false,
action_view_helpers: false,
})
Expand All @@ -101,6 +103,7 @@ describe("ParseCache", () => {
prism_nodes: false,
prism_nodes_deep: false,
prism_program: false,
render_nodes: false,
strict: true,
action_view_helpers: false,
})
Expand All @@ -116,6 +119,7 @@ describe("ParseCache", () => {
prism_nodes: false,
prism_nodes_deep: false,
prism_program: false,
render_nodes: false,
strict: false,
action_view_helpers: false,
})
Expand Down
1 change: 1 addition & 0 deletions javascript/packages/node/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"./extension/libherb/analyze/missing_end.c",
"./extension/libherb/analyze/parse_errors.c",
"./extension/libherb/analyze/prism_annotate.c",
"./extension/libherb/analyze/render_nodes.c",
"./extension/libherb/analyze/transform.c",
"./extension/libherb/analyze/action_view/attribute_extraction_helpers.c",
"./extension/libherb/analyze/action_view/content_tag.c",
Expand Down
Loading
Loading