diff --git a/docs/docs/bindings/java/reference.md b/docs/docs/bindings/java/reference.md index 3cc841892..4abcdda4c 100644 --- a/docs/docs/bindings/java/reference.md +++ b/docs/docs/bindings/java/reference.md @@ -14,6 +14,7 @@ The `Herb` class provides the following static methods: * `Herb.parse(source)` * `Herb.parse(source, options)` * `Herb.extractRuby(source)` +* `Herb.extractRuby(source, options)` * `Herb.extractHTML(source)` * `Herb.version()` * `Herb.herbVersion()` @@ -154,10 +155,104 @@ String source = "

Hello <%= user.name %>

"; String ruby = Herb.extractRuby(source); System.out.println(ruby); -// Output: " user.name " +// Output: " user.name ; " ``` ::: +### `Herb.extractRuby(String source, ExtractRubyOptions options)` + +Extract Ruby with custom options. + +#### Default behavior + +By default, the output is position-preserving with semicolons: + +:::code-group +```java +import org.herb.Herb; + +String source = "<% x = 1 %> <% y = 2 %>"; +String ruby = Herb.extractRuby(source); + +System.out.println(ruby); +// Output: " x = 1 ; y = 2 ;" +``` +::: + +#### Without semicolons + +:::code-group +```java +import org.herb.Herb; +import org.herb.ExtractRubyOptions; + +String source = "<% x = 1 %> <% y = 2 %>"; +ExtractRubyOptions options = ExtractRubyOptions.create().semicolons(false); +String ruby = Herb.extractRuby(source, options); + +System.out.println(ruby); +// Output: " x = 1 y = 2 " +``` +::: + +#### Including ERB comments + +:::code-group +```java +import org.herb.Herb; +import org.herb.ExtractRubyOptions; + +String source = "<%# comment %>\n<% code %>"; +ExtractRubyOptions options = ExtractRubyOptions.create().comments(true); +String ruby = Herb.extractRuby(source, options); + +System.out.println(ruby); +// Output: " # comment \n code ;" +``` +::: + +#### Without position preservation + +Use `preservePositions(false)` for readable output where each ERB tag is placed on its own line: + +:::code-group +```java +import org.herb.Herb; +import org.herb.ExtractRubyOptions; + +String source = "<%# comment %><%= something %>"; +ExtractRubyOptions options = ExtractRubyOptions.create().preservePositions(false).comments(true); +String ruby = Herb.extractRuby(source, options); + +System.out.println(ruby); +// Output: "# comment \n something " +``` +::: + +### `ExtractRubyOptions` + +The `ExtractRubyOptions` class provides fluent configuration: + +```java +public class ExtractRubyOptions { + public ExtractRubyOptions semicolons(boolean value); + public ExtractRubyOptions comments(boolean value); + public ExtractRubyOptions preservePositions(boolean value); + + public static ExtractRubyOptions create(); +} +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `semicolons` | `true` | Add ` ;` at the end of each ERB tag to separate statements | +| `comments` | `false` | Include ERB comments (`<%# %>`) in the output | +| `preservePositions` | `true` | Maintain character positions by padding with whitespace | + +> [!TIP] +> Use `preservePositions(false)` when you need readable Ruby output. +> Use `preservePositions(true)` (default) when you need accurate error position mapping. + ### `Herb.extractHTML(String source)` The `extractHTML` method extracts only the HTML parts of an HTML document with embedded Ruby. diff --git a/docs/docs/bindings/javascript/reference.md b/docs/docs/bindings/javascript/reference.md index cbf5361d4..80b5d558f 100644 --- a/docs/docs/bindings/javascript/reference.md +++ b/docs/docs/bindings/javascript/reference.md @@ -42,7 +42,7 @@ Learn more on [how to install and load the NPM packages](/bindings/javascript/#i - **`Herb.lexFile(path: string): LexResult`** - **`Herb.parse(source: string): ParseResult`** - **`Herb.parseFile(path: string): ParseResult`** -- **`Herb.extractRuby(source: string): string`** +- **`Herb.extractRuby(source: string, options?: ExtractRubyOptions): string`** - **`Herb.extractHTML(source: string): string`** - **`Herb.version: string`** @@ -134,7 +134,7 @@ console.log(result) Herb allows you to extract either Ruby or HTML from mixed content. -### `Herb.extractRuby(source)` +### `Herb.extractRuby(source, options?)` The `Herb.extractRuby` method allows you to extract only the Ruby parts of an HTML document with embedded Ruby. @@ -148,10 +148,66 @@ const source = "

Hello <%= user.name %>

" const ruby = Herb.extractRuby(source) console.log(ruby); -// Outputs: " user.name " +// Outputs: " user.name ; " ``` ::: +#### Options + +```typescript +interface ExtractRubyOptions { + semicolons?: boolean // default: true + comments?: boolean // default: false + preserve_positions?: boolean // default: true +} +``` + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `semicolons` | `boolean` | `true` | Add ` ;` at the end of each ERB tag to separate statements | +| `comments` | `boolean` | `false` | Include ERB comments (`<%# %>`) in the output | +| `preserve_positions` | `boolean` | `true` | Maintain character positions by padding with whitespace | + +#### Examples + +**Default behavior** (position-preserving with semicolons): + +```js +const source = "<% x = 1 %> <% y = 2 %>" + +Herb.extractRuby(source) +// => " x = 1 ; y = 2 ;" +``` + +**Without semicolons:** + +```js +Herb.extractRuby(source, { semicolons: false }) +// => " x = 1 y = 2 " +``` + +**Including ERB comments:** + +```js +const source = "<%# comment %>\n<% code %>" + +Herb.extractRuby(source, { comments: true }) +// => " # comment \n code ;" +``` + +**Without position preservation** (readable output, each tag on its own line): + +```js +const source = "<%# comment %><%= something %>" + +Herb.extractRuby(source, { preserve_positions: false, comments: true }) +// => "# comment \n something " +``` + +> [!TIP] +> Use `preserve_positions: false` when you need readable Ruby output. +> Use `preserve_positions: true` (default) when you need accurate error position mapping. + ### `Herb.extractHTML(source)` The `Herb.extractHTML` method allows you to extract only the HTML parts of an HTML document with embedded Ruby. diff --git a/docs/docs/bindings/ruby/reference.md b/docs/docs/bindings/ruby/reference.md index 59b062018..b6546f0b9 100644 --- a/docs/docs/bindings/ruby/reference.md +++ b/docs/docs/bindings/ruby/reference.md @@ -118,7 +118,7 @@ Herb.parse_file("./index.html.erb").value ## Extracting Code -### `Herb.extract_ruby(source)` +### `Herb.extract_ruby(source, **options)` The `Herb.extract_ruby` method allows you to extract only the Ruby parts of an HTML document with embedded Ruby. @@ -127,10 +127,58 @@ The `Herb.extract_ruby` method allows you to extract only the Ruby parts of an H source = %(

Hello <%= user.name %>

) Herb.extract_ruby(source) -# => " user.name " +# => " user.name ; " ``` ::: +#### Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `semicolons` | `Boolean` | `true` | Add ` ;` at the end of each ERB tag to separate statements | +| `comments` | `Boolean` | `false` | Include ERB comments (`<%# %>`) in the output | +| `preserve_positions` | `Boolean` | `true` | Maintain character positions by padding with whitespace | + +#### Examples + +**Default behavior** (position-preserving with semicolons): + +```ruby +source = "<% x = 1 %> <% y = 2 %>" + +Herb.extract_ruby(source) +# => " x = 1 ; y = 2 ;" +``` + +**Without semicolons:** + +```ruby +Herb.extract_ruby(source, semicolons: false) +# => " x = 1 y = 2 " +``` + +**Including ERB comments:** + +```ruby +source = "<%# comment %>\n<% code %>" + +Herb.extract_ruby(source, comments: true) +# => " # comment \n code ;" +``` + +**Without position preservation** (readable output, each tag on its own line): + +```ruby +source = "<%# comment %><%= something %>" + +Herb.extract_ruby(source, preserve_positions: false, comments: true) +# => "# comment \n something " +``` + +> [!TIP] +> Use `preserve_positions: false` when you need readable Ruby output. +> Use `preserve_positions: true` (default) when you need accurate error position mapping. + ### `Herb.extract_html(source)` The `Herb.extract_html` method allows you to extract only the HTML parts of an HTML document with embedded Ruby. diff --git a/docs/docs/bindings/rust/reference.md b/docs/docs/bindings/rust/reference.md index 94d2c4c37..0bccfa2ab 100644 --- a/docs/docs/bindings/rust/reference.md +++ b/docs/docs/bindings/rust/reference.md @@ -12,7 +12,9 @@ The `herb` crate exposes functions for lexing, parsing, and extracting Ruby and * `herb::lex(source)` * `herb::parse(source)` +* `herb::parse_with_options(source, options)` * `herb::extract_ruby(source)` +* `herb::extract_ruby_with_options(source, options)` * `herb::extract_html(source)` * `herb::version()` * `herb::herb_version()` @@ -196,10 +198,127 @@ match extract_ruby(source) { Ok(ruby) => println!("{}", ruby), Err(e) => eprintln!("Error: {}", e), } -// Output: " user.name " +// Output: " user.name ; " ``` ::: +### `herb::extract_ruby_with_options(source: &str, options: &ExtractRubyOptions) -> Result` + +Extract Ruby with custom options. + +#### Default behavior + +By default, the output is position-preserving with semicolons: + +:::code-group +```rust +use herb::extract_ruby; + +let source = "<% x = 1 %> <% y = 2 %>"; + +match extract_ruby(source) { + Ok(ruby) => println!("{:?}", ruby), + Err(e) => eprintln!("Error: {}", e), +} +// Output: " x = 1 ; y = 2 ;" +``` +::: + +#### Without semicolons + +:::code-group +```rust +use herb::{extract_ruby_with_options, ExtractRubyOptions}; + +let source = "<% x = 1 %> <% y = 2 %>"; +let options = ExtractRubyOptions { + semicolons: false, + ..Default::default() +}; + +match extract_ruby_with_options(source, &options) { + Ok(ruby) => println!("{:?}", ruby), + Err(e) => eprintln!("Error: {}", e), +} +// Output: " x = 1 y = 2 " +``` +::: + +#### Including ERB comments + +:::code-group +```rust +use herb::{extract_ruby_with_options, ExtractRubyOptions}; + +let source = "<%# comment %>\n<% code %>"; +let options = ExtractRubyOptions { + comments: true, + ..Default::default() +}; + +match extract_ruby_with_options(source, &options) { + Ok(ruby) => println!("{:?}", ruby), + Err(e) => eprintln!("Error: {}", e), +} +// Output: " # comment \n code ;" +``` +::: + +#### Without position preservation + +Use `preserve_positions: false` for readable output where each ERB tag is placed on its own line: + +:::code-group +```rust +use herb::{extract_ruby_with_options, ExtractRubyOptions}; + +let source = "<%# comment %><%= something %>"; +let options = ExtractRubyOptions { + preserve_positions: false, + comments: true, + ..Default::default() +}; + +match extract_ruby_with_options(source, &options) { + Ok(ruby) => println!("{:?}", ruby), + Err(e) => eprintln!("Error: {}", e), +} +// Output: "# comment \n something " +``` +::: + +### `ExtractRubyOptions` + +The `ExtractRubyOptions` struct provides configuration for Ruby extraction: + +```rust +pub struct ExtractRubyOptions { + pub semicolons: bool, + pub comments: bool, + pub preserve_positions: bool, +} + +impl Default for ExtractRubyOptions { + fn default() -> Self { + Self { + semicolons: true, + comments: false, + preserve_positions: true, + } + } +} +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `semicolons` | `true` | Add `;` at the end of each ERB tag to separate statements | +| `comments` | `false` | Include ERB comments (`<%# %>`) in the output | +| `preserve_positions` | `true` | Maintain character positions by padding with whitespace | + +> [!TIP] +> Use `preserve_positions: false` when you need readable Ruby output. +> Use `preserve_positions: true` (default) when you need accurate error position mapping. + ### `herb::extract_html(source: &str) -> Result` The `extract_html` function extracts only the HTML parts of an HTML document with embedded Ruby. diff --git a/ext/herb/extension.c b/ext/herb/extension.c index d7c4f6761..94f8b1bd6 100644 --- a/ext/herb/extension.c +++ b/ext/herb/extension.c @@ -80,13 +80,34 @@ static VALUE Herb_parse_file(VALUE self, VALUE path) { return result; } -static VALUE Herb_extract_ruby(VALUE self, VALUE source) { +static VALUE Herb_extract_ruby(int argc, VALUE* argv, VALUE self) { + VALUE source, options; + rb_scan_args(argc, argv, "1:", &source, &options); + char* string = (char*) check_string(source); hb_buffer_T output; if (!hb_buffer_init(&output, strlen(string))) { return Qnil; } - herb_extract_ruby_to_buffer(string, &output); + herb_extract_ruby_options_T extract_options = HERB_EXTRACT_RUBY_DEFAULT_OPTIONS; + + if (!NIL_P(options)) { + VALUE semicolons_value = rb_hash_lookup(options, rb_utf8_str_new_cstr("semicolons")); + if (NIL_P(semicolons_value)) { semicolons_value = rb_hash_lookup(options, ID2SYM(rb_intern("semicolons"))); } + if (!NIL_P(semicolons_value)) { extract_options.semicolons = RTEST(semicolons_value); } + + VALUE comments_value = rb_hash_lookup(options, rb_utf8_str_new_cstr("comments")); + if (NIL_P(comments_value)) { comments_value = rb_hash_lookup(options, ID2SYM(rb_intern("comments"))); } + if (!NIL_P(comments_value)) { extract_options.comments = RTEST(comments_value); } + + VALUE preserve_positions_value = rb_hash_lookup(options, rb_utf8_str_new_cstr("preserve_positions")); + if (NIL_P(preserve_positions_value)) { + preserve_positions_value = rb_hash_lookup(options, ID2SYM(rb_intern("preserve_positions"))); + } + if (!NIL_P(preserve_positions_value)) { extract_options.preserve_positions = RTEST(preserve_positions_value); } + } + + herb_extract_ruby_to_buffer_with_options(string, &output, &extract_options); VALUE result = rb_utf8_str_new_cstr(output.value); free(output.value); @@ -131,7 +152,7 @@ __attribute__((__visibility__("default"))) void Init_herb(void) { rb_define_singleton_method(mHerb, "lex", Herb_lex, 1); rb_define_singleton_method(mHerb, "parse_file", Herb_parse_file, 1); rb_define_singleton_method(mHerb, "lex_file", Herb_lex_file, 1); - rb_define_singleton_method(mHerb, "extract_ruby", Herb_extract_ruby, 1); + rb_define_singleton_method(mHerb, "extract_ruby", Herb_extract_ruby, -1); rb_define_singleton_method(mHerb, "extract_html", Herb_extract_html, 1); rb_define_singleton_method(mHerb, "version", Herb_version, 0); } diff --git a/java/herb_jni.c b/java/herb_jni.c index 0ecb3b766..8164093f5 100644 --- a/java/herb_jni.c +++ b/java/herb_jni.c @@ -1,6 +1,7 @@ #include "herb_jni.h" #include "extension_helpers.h" +#include "../../src/include/extract.h" #include "../../src/include/herb.h" #include "../../src/include/util/hb_buffer.h" @@ -77,7 +78,7 @@ Java_org_herb_Herb_lex(JNIEnv* env, jclass clazz, jstring source) { } JNIEXPORT jstring JNICALL -Java_org_herb_Herb_extractRuby(JNIEnv* env, jclass clazz, jstring source) { +Java_org_herb_Herb_extractRuby(JNIEnv* env, jclass clazz, jstring source, jobject options) { const char* src = (*env)->GetStringUTFChars(env, source, 0); hb_buffer_T output; @@ -88,7 +89,31 @@ Java_org_herb_Herb_extractRuby(JNIEnv* env, jclass clazz, jstring source) { return NULL; } - herb_extract_ruby_to_buffer(src, &output); + herb_extract_ruby_options_T extract_options = HERB_EXTRACT_RUBY_DEFAULT_OPTIONS; + + if (options != NULL) { + jclass optionsClass = (*env)->GetObjectClass(env, options); + + jmethodID getSemicolons = (*env)->GetMethodID(env, optionsClass, "isSemicolons", "()Z"); + if (getSemicolons != NULL) { + jboolean semicolons = (*env)->CallBooleanMethod(env, options, getSemicolons); + extract_options.semicolons = (semicolons == JNI_TRUE); + } + + jmethodID getComments = (*env)->GetMethodID(env, optionsClass, "isComments", "()Z"); + if (getComments != NULL) { + jboolean comments = (*env)->CallBooleanMethod(env, options, getComments); + extract_options.comments = (comments == JNI_TRUE); + } + + jmethodID getPreservePositions = (*env)->GetMethodID(env, optionsClass, "isPreservePositions", "()Z"); + if (getPreservePositions != NULL) { + jboolean preservePositions = (*env)->CallBooleanMethod(env, options, getPreservePositions); + extract_options.preserve_positions = (preservePositions == JNI_TRUE); + } + } + + herb_extract_ruby_to_buffer_with_options(src, &output, &extract_options); jstring result = (*env)->NewStringUTF(env, output.value); diff --git a/java/herb_jni.h b/java/herb_jni.h index 8895209cc..8dbee1ff0 100644 --- a/java/herb_jni.h +++ b/java/herb_jni.h @@ -11,7 +11,7 @@ JNIEXPORT jstring JNICALL Java_org_herb_Herb_herbVersion(JNIEnv*, jclass); JNIEXPORT jstring JNICALL Java_org_herb_Herb_prismVersion(JNIEnv*, jclass); JNIEXPORT jobject JNICALL Java_org_herb_Herb_parse(JNIEnv*, jclass, jstring, jobject); JNIEXPORT jobject JNICALL Java_org_herb_Herb_lex(JNIEnv*, jclass, jstring); -JNIEXPORT jstring JNICALL Java_org_herb_Herb_extractRuby(JNIEnv*, jclass, jstring); +JNIEXPORT jstring JNICALL Java_org_herb_Herb_extractRuby(JNIEnv*, jclass, jstring, jobject); JNIEXPORT jstring JNICALL Java_org_herb_Herb_extractHTML(JNIEnv*, jclass, jstring); #ifdef __cplusplus diff --git a/java/org/herb/ExtractRubyOptions.java b/java/org/herb/ExtractRubyOptions.java new file mode 100644 index 000000000..966ed4bbb --- /dev/null +++ b/java/org/herb/ExtractRubyOptions.java @@ -0,0 +1,40 @@ +package org.herb; + +public class ExtractRubyOptions { + private boolean semicolons = true; + private boolean comments = false; + private boolean preservePositions = true; + + public ExtractRubyOptions() {} + + public ExtractRubyOptions semicolons(boolean value) { + this.semicolons = value; + return this; + } + + public boolean isSemicolons() { + return semicolons; + } + + public ExtractRubyOptions comments(boolean value) { + this.comments = value; + return this; + } + + public boolean isComments() { + return comments; + } + + public ExtractRubyOptions preservePositions(boolean value) { + this.preservePositions = value; + return this; + } + + public boolean isPreservePositions() { + return preservePositions; + } + + public static ExtractRubyOptions create() { + return new ExtractRubyOptions(); + } +} diff --git a/java/org/herb/Herb.java b/java/org/herb/Herb.java index e1a739449..2f9115924 100644 --- a/java/org/herb/Herb.java +++ b/java/org/herb/Herb.java @@ -18,13 +18,17 @@ public class Herb { public static native String prismVersion(); public static native ParseResult parse(String source, ParserOptions options); public static native LexResult lex(String source); - public static native String extractRuby(String source); + public static native String extractRuby(String source, ExtractRubyOptions options); public static native String extractHTML(String source); public static ParseResult parse(String source) { return parse(source, null); } + public static String extractRuby(String source) { + return extractRuby(source, null); + } + public static String version() { return String.format("herb java v%s, libprism v%s, libherb v%s (Java JNI)", herbVersion(), prismVersion(), herbVersion()); } diff --git a/javascript/packages/core/src/backend.ts b/javascript/packages/core/src/backend.ts index 587280adf..a00ca103d 100644 --- a/javascript/packages/core/src/backend.ts +++ b/javascript/packages/core/src/backend.ts @@ -1,6 +1,7 @@ import type { SerializedParseResult } from "./parse-result.js" import type { SerializedLexResult } from "./lex-result.js" import type { ParserOptions } from "./parser-options.js" +import type { ExtractRubyOptions } from "./extract-ruby-options.js" interface LibHerbBackendFunctions { lex: (source: string) => SerializedLexResult @@ -9,7 +10,7 @@ interface LibHerbBackendFunctions { parse: (source: string, options?: ParserOptions) => SerializedParseResult parseFile: (path: string) => SerializedParseResult - extractRuby: (source: string) => string + extractRuby: (source: string, options?: ExtractRubyOptions) => string extractHTML: (source: string) => string version: () => string diff --git a/javascript/packages/core/src/extract-ruby-options.ts b/javascript/packages/core/src/extract-ruby-options.ts new file mode 100644 index 000000000..224c81d46 --- /dev/null +++ b/javascript/packages/core/src/extract-ruby-options.ts @@ -0,0 +1,11 @@ +export interface ExtractRubyOptions { + semicolons?: boolean + comments?: boolean + preserve_positions?: boolean +} + +export const DEFAULT_EXTRACT_RUBY_OPTIONS: ExtractRubyOptions = { + semicolons: true, + comments: false, + preserve_positions: true, +} diff --git a/javascript/packages/core/src/herb-backend.ts b/javascript/packages/core/src/herb-backend.ts index 700c18301..a6a77c13d 100644 --- a/javascript/packages/core/src/herb-backend.ts +++ b/javascript/packages/core/src/herb-backend.ts @@ -4,9 +4,11 @@ import { ensureString } from "./util.js" import { LexResult } from "./lex-result.js" import { ParseResult } from "./parse-result.js" import { DEFAULT_PARSER_OPTIONS } from "./parser-options.js" +import { DEFAULT_EXTRACT_RUBY_OPTIONS } from "./extract-ruby-options.js" import type { LibHerbBackend, BackendPromise } from "./backend.js" import type { ParserOptions } from "./parser-options.js" +import type { ExtractRubyOptions } from "./extract-ruby-options.js" /** * The main Herb parser interface, providing methods to lex and parse input. @@ -93,13 +95,16 @@ export abstract class HerbBackend { /** * Extracts embedded Ruby code from the given source. * @param source - The source code to extract Ruby from. + * @param options - Optional extraction options. * @returns The extracted Ruby code as a string. * @throws Error if the backend is not loaded. */ - extractRuby(source: string): string { + extractRuby(source: string, options?: ExtractRubyOptions): string { this.ensureBackend() - return this.backend.extractRuby(ensureString(source)) + const mergedOptions = { ...DEFAULT_EXTRACT_RUBY_OPTIONS, ...options } + + return this.backend.extractRuby(ensureString(source), mergedOptions) } /** diff --git a/javascript/packages/core/src/index.ts b/javascript/packages/core/src/index.ts index cd7125815..b8c2d488f 100644 --- a/javascript/packages/core/src/index.ts +++ b/javascript/packages/core/src/index.ts @@ -3,6 +3,7 @@ export * from "./backend.js" export * from "./diagnostic.js" export * from "./didyoumean.js" export * from "./errors.js" +export * from "./extract-ruby-options.js" export * from "./herb-backend.js" export * from "./levenshtein.js" export * from "./lex-result.js" diff --git a/javascript/packages/node-wasm/test/node-wasm.test.ts b/javascript/packages/node-wasm/test/node-wasm.test.ts index ab2ec3250..5732bf6b2 100644 --- a/javascript/packages/node-wasm/test/node-wasm.test.ts +++ b/javascript/packages/node-wasm/test/node-wasm.test.ts @@ -40,6 +40,30 @@ describe("@herb-tools/node-wasm", () => { expect(ruby).toBe(' "Hello World" ; ') }) + test("extractRuby() with semicolons: false", async () => { + const source = "<% x = 1 %> <% y = 2 %>" + const ruby = Herb.extractRuby(source, { semicolons: false }) + expect(ruby).toBe(" x = 1 y = 2 ") + }) + + test("extractRuby() with comments: true", async () => { + const source = "<%# comment %>\n<% code %>" + const ruby = Herb.extractRuby(source, { comments: true }) + expect(ruby).toBe(" # comment \n code ;") + }) + + test("extractRuby() with preserve_positions: false", async () => { + const source = "<% x = 1 %> <% y = 2 %>" + const ruby = Herb.extractRuby(source, { preserve_positions: false }) + expect(ruby).toBe(" x = 1 \n y = 2 ") + }) + + test("extractRuby() with preserve_positions: false and comments: true", async () => { + const source = "<%# comment %><%= something %>" + const ruby = Herb.extractRuby(source, { preserve_positions: false, comments: true }) + expect(ruby).toBe("# comment \n something ") + }) + test("extractHTML() extracts HTML content", async () => { const simpleHtml = '
<%= "Hello World" %>
' const html = Herb.extractHTML(simpleHtml) diff --git a/javascript/packages/node/extension/herb.cpp b/javascript/packages/node/extension/herb.cpp index 207aaa691..65c2a6080 100644 --- a/javascript/packages/node/extension/herb.cpp +++ b/javascript/packages/node/extension/herb.cpp @@ -1,5 +1,6 @@ extern "C" { #include "../extension/libherb/include/ast_nodes.h" +#include "../extension/libherb/include/extract.h" #include "../extension/libherb/include/herb.h" #include "../extension/libherb/include/location.h" #include "../extension/libherb/include/range.h" @@ -153,8 +154,8 @@ napi_value Herb_parse_file(napi_env env, napi_callback_info info) { } napi_value Herb_extract_ruby(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; + size_t argc = 2; + napi_value args[2]; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); if (argc < 1) { @@ -172,7 +173,43 @@ napi_value Herb_extract_ruby(napi_env env, napi_callback_info info) { return nullptr; } - herb_extract_ruby_to_buffer(string, &output); + herb_extract_ruby_options_T extract_options = HERB_EXTRACT_RUBY_DEFAULT_OPTIONS; + + if (argc >= 2) { + napi_valuetype valuetype; + napi_typeof(env, args[1], &valuetype); + + if (valuetype == napi_object) { + napi_value prop; + bool has_prop; + + napi_has_named_property(env, args[1], "semicolons", &has_prop); + if (has_prop) { + napi_get_named_property(env, args[1], "semicolons", &prop); + bool value; + napi_get_value_bool(env, prop, &value); + extract_options.semicolons = value; + } + + napi_has_named_property(env, args[1], "comments", &has_prop); + if (has_prop) { + napi_get_named_property(env, args[1], "comments", &prop); + bool value; + napi_get_value_bool(env, prop, &value); + extract_options.comments = value; + } + + napi_has_named_property(env, args[1], "preserve_positions", &has_prop); + if (has_prop) { + napi_get_named_property(env, args[1], "preserve_positions", &prop); + bool value; + napi_get_value_bool(env, prop, &value); + extract_options.preserve_positions = value; + } + } + } + + herb_extract_ruby_to_buffer_with_options(string, &output, &extract_options); napi_value result; napi_create_string_utf8(env, output.value, NAPI_AUTO_LENGTH, &result); diff --git a/javascript/packages/node/test/node.test.ts b/javascript/packages/node/test/node.test.ts index 496761e34..fe3fdd6dc 100644 --- a/javascript/packages/node/test/node.test.ts +++ b/javascript/packages/node/test/node.test.ts @@ -38,6 +38,30 @@ describe("@herb-tools/node", () => { expect(ruby).toBe(' "Hello World" ; ') }) + test("extractRuby() with semicolons: false", async () => { + const source = "<% x = 1 %> <% y = 2 %>" + const ruby = Herb.extractRuby(source, { semicolons: false }) + expect(ruby).toBe(" x = 1 y = 2 ") + }) + + test("extractRuby() with comments: true", async () => { + const source = "<%# comment %>\n<% code %>" + const ruby = Herb.extractRuby(source, { comments: true }) + expect(ruby).toBe(" # comment \n code ;") + }) + + test("extractRuby() with preserve_positions: false", async () => { + const source = "<% x = 1 %> <% y = 2 %>" + const ruby = Herb.extractRuby(source, { preserve_positions: false }) + expect(ruby).toBe(" x = 1 \n y = 2 ") + }) + + test("extractRuby() with preserve_positions: false and comments: true", async () => { + const source = "<%# comment %><%= something %>" + const ruby = Herb.extractRuby(source, { preserve_positions: false, comments: true }) + expect(ruby).toBe("# comment \n something ") + }) + test("extractHTML() extracts HTML content", async () => { const simpleHtml = '
<%= "Hello World" %>
' const html = Herb.extractHTML(simpleHtml) diff --git a/rust/build.rs b/rust/build.rs index 19f65cc1f..0c4cd0cb0 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -81,13 +81,16 @@ fn main() { .header(include_dir.join("ast_nodes.h").to_str().unwrap()) .header(include_dir.join("errors.h").to_str().unwrap()) .header(include_dir.join("element_source.h").to_str().unwrap()) + .header(include_dir.join("extract.h").to_str().unwrap()) .header(include_dir.join("token_struct.h").to_str().unwrap()) .header(include_dir.join("util/hb_string.h").to_str().unwrap()) .header(include_dir.join("util/hb_array.h").to_str().unwrap()) + .header(include_dir.join("util/hb_buffer.h").to_str().unwrap()) .clang_arg(format!("-I{}", include_dir.display())) .clang_arg(format!("-I{}", prism_include.display())) .allowlist_function("herb_.*") .allowlist_function("hb_array_.*") + .allowlist_function("hb_buffer_.*") .allowlist_function("token_type_to_string") .allowlist_function("ast_node_free") .allowlist_function("element_source_to_string") @@ -98,11 +101,13 @@ fn main() { .allowlist_type("ast_node_type_T") .allowlist_type("error_type_T") .allowlist_type("hb_array_T") + .allowlist_type("hb_buffer_T") .allowlist_type("hb_string_T") .allowlist_type("token_T") .allowlist_type("position_T") .allowlist_type("location_T") .allowlist_type("herb_extract_language_T") + .allowlist_type("herb_extract_ruby_options_T") .allowlist_type("parser_options_T") .allowlist_var("AST_.*") .allowlist_var("ERROR_.*") diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs index 5a2c98e50..191f7a9ba 100644 --- a/rust/src/ffi.rs +++ b/rust/src/ffi.rs @@ -1,4 +1,5 @@ pub use crate::bindings::{ - ast_node_free, element_source_to_string, hb_array_get, hb_array_size, hb_string_T, herb_extract, + ast_node_free, element_source_to_string, hb_array_get, hb_array_size, hb_buffer_init, + hb_buffer_value, hb_string_T, herb_extract, herb_extract_ruby_to_buffer_with_options, herb_free_tokens, herb_lex, herb_parse, herb_prism_version, herb_version, token_type_to_string, }; diff --git a/rust/src/herb.rs b/rust/src/herb.rs index baa09d070..fc4a88860 100644 --- a/rust/src/herb.rs +++ b/rust/src/herb.rs @@ -1,4 +1,4 @@ -use crate::bindings::{hb_array_T, token_T}; +use crate::bindings::{hb_array_T, hb_buffer_T, token_T}; use crate::convert::token_from_c; use crate::{LexResult, ParseResult}; use std::ffi::CString; @@ -18,6 +18,23 @@ impl Default for ParserOptions { } } +#[derive(Debug, Clone)] +pub struct ExtractRubyOptions { + pub semicolons: bool, + pub comments: bool, + pub preserve_positions: bool, +} + +impl Default for ExtractRubyOptions { + fn default() -> Self { + Self { + semicolons: true, + comments: false, + preserve_positions: true, + } + } +} + pub fn lex(source: &str) -> Result { unsafe { let c_source = CString::new(source).map_err(|e| e.to_string())?; @@ -76,21 +93,39 @@ pub fn parse_with_options(source: &str, options: &ParserOptions) -> Result Result { + extract_ruby_with_options(source, &ExtractRubyOptions::default()) +} + +pub fn extract_ruby_with_options( + source: &str, + options: &ExtractRubyOptions, +) -> Result { unsafe { let c_source = CString::new(source).map_err(|e| e.to_string())?; - let result = crate::ffi::herb_extract( - c_source.as_ptr(), - crate::bindings::HERB_EXTRACT_LANGUAGE_RUBY, - ); - if result.is_null() { - return Ok(String::new()); + let mut output: hb_buffer_T = std::mem::zeroed(); + let init_result = crate::ffi::hb_buffer_init(&mut output, source.len()); + + if !init_result { + return Err("Failed to initialize buffer".to_string()); } - let c_str = std::ffi::CStr::from_ptr(result); + let c_options = crate::bindings::herb_extract_ruby_options_T { + semicolons: options.semicolons, + comments: options.comments, + preserve_positions: options.preserve_positions, + }; + + crate::ffi::herb_extract_ruby_to_buffer_with_options( + c_source.as_ptr(), + &mut output, + &c_options, + ); + + let c_str = std::ffi::CStr::from_ptr(crate::ffi::hb_buffer_value(&output)); let rust_str = c_str.to_string_lossy().into_owned(); - libc::free(result as *mut std::ffi::c_void); + libc::free(output.value as *mut std::ffi::c_void); Ok(rust_str) } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index b6d044d6d..1f5e2e225 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -15,8 +15,8 @@ pub mod union_types; pub use errors::{AnyError, ErrorNode, ErrorType}; pub use herb::{ - extract_html, extract_ruby, herb_version, lex, parse, parse_with_options, prism_version, version, - ParserOptions, + extract_html, extract_ruby, extract_ruby_with_options, herb_version, lex, parse, + parse_with_options, prism_version, version, ExtractRubyOptions, ParserOptions, }; pub use lex_result::LexResult; pub use location::Location; diff --git a/rust/tests/extract_ruby_options_test.rs b/rust/tests/extract_ruby_options_test.rs new file mode 100644 index 000000000..b4463e9e9 --- /dev/null +++ b/rust/tests/extract_ruby_options_test.rs @@ -0,0 +1,53 @@ +use herb::{extract_ruby, extract_ruby_with_options, ExtractRubyOptions}; + +#[test] +fn test_extract_ruby_default() { + let source = "<% x = 1 %> <% y = 2 %>"; + let result = extract_ruby(source).unwrap(); + assert_eq!(result, " x = 1 ; y = 2 ;"); +} + +#[test] +fn test_extract_ruby_without_semicolons() { + let source = "<% x = 1 %> <% y = 2 %>"; + let options = ExtractRubyOptions { + semicolons: false, + ..Default::default() + }; + let result = extract_ruby_with_options(source, &options).unwrap(); + assert_eq!(result, " x = 1 y = 2 "); +} + +#[test] +fn test_extract_ruby_with_comments() { + let source = "<%# comment %>\n<% code %>"; + let options = ExtractRubyOptions { + comments: true, + ..Default::default() + }; + let result = extract_ruby_with_options(source, &options).unwrap(); + assert_eq!(result, " # comment \n code ;"); +} + +#[test] +fn test_extract_ruby_without_preserve_positions() { + let source = "<% x = 1 %> <% y = 2 %>"; + let options = ExtractRubyOptions { + preserve_positions: false, + ..Default::default() + }; + let result = extract_ruby_with_options(source, &options).unwrap(); + assert_eq!(result, " x = 1 \n y = 2 "); +} + +#[test] +fn test_extract_ruby_without_preserve_positions_with_comments() { + let source = "<%# comment %><%= something %>"; + let options = ExtractRubyOptions { + preserve_positions: false, + comments: true, + ..Default::default() + }; + let result = extract_ruby_with_options(source, &options).unwrap(); + assert_eq!(result, "# comment \n something "); +} diff --git a/sig/herb_c_extension.rbs b/sig/herb_c_extension.rbs index 1f285ca5b..fed3b0471 100644 --- a/sig/herb_c_extension.rbs +++ b/sig/herb_c_extension.rbs @@ -4,4 +4,6 @@ module Herb def self.parse: (String input, ?track_whitespace: bool) -> ParseResult def self.lex: (String input) -> LexResult + def self.extract_ruby: (String source, ?semicolons: bool, ?comments: bool, ?preserve_positions: bool) -> String + def self.extract_html: (String source) -> String end diff --git a/src/extract.c b/src/extract.c index 0b322017d..3032e0a56 100644 --- a/src/extract.c +++ b/src/extract.c @@ -9,10 +9,22 @@ #include #include -void herb_extract_ruby_to_buffer(const char* source, hb_buffer_T* output) { +const herb_extract_ruby_options_T HERB_EXTRACT_RUBY_DEFAULT_OPTIONS = { .semicolons = true, + .comments = false, + .preserve_positions = true }; + +void herb_extract_ruby_to_buffer_with_options( + const char* source, + hb_buffer_T* output, + const herb_extract_ruby_options_T* options +) { + herb_extract_ruby_options_T extract_options = options ? *options : HERB_EXTRACT_RUBY_DEFAULT_OPTIONS; + hb_array_T* tokens = herb_lex(source); bool skip_erb_content = false; bool is_comment_tag = false; + bool is_erb_comment_tag = false; + bool need_newline = false; for (size_t i = 0; i < hb_array_size(tokens); i++) { const token_T* token = hb_array_get(tokens, i); @@ -20,23 +32,48 @@ void herb_extract_ruby_to_buffer(const char* source, hb_buffer_T* output) { switch (token->type) { case TOKEN_NEWLINE: { hb_buffer_append(output, token->value); + need_newline = false; break; } case TOKEN_ERB_START: { - if (string_equals(token->value, "<%#")) { - skip_erb_content = true; - is_comment_tag = true; + is_erb_comment_tag = string_equals(token->value, "<%#"); + + if (is_erb_comment_tag) { + if (extract_options.comments) { + skip_erb_content = false; + is_comment_tag = false; + + if (extract_options.preserve_positions) { + hb_buffer_append_whitespace(output, 2); + hb_buffer_append_char(output, '#'); + } else { + if (need_newline) { hb_buffer_append_char(output, '\n'); } + hb_buffer_append_char(output, '#'); + need_newline = true; + } + } else { + skip_erb_content = true; + is_comment_tag = true; + if (extract_options.preserve_positions) { hb_buffer_append_whitespace(output, range_length(token->range)); } + } } else if (string_equals(token->value, "<%%") || string_equals(token->value, "<%%=") || string_equals(token->value, "<%graphql")) { skip_erb_content = true; is_comment_tag = false; + if (extract_options.preserve_positions) { hb_buffer_append_whitespace(output, range_length(token->range)); } } else { skip_erb_content = false; is_comment_tag = false; + + if (extract_options.preserve_positions) { + hb_buffer_append_whitespace(output, range_length(token->range)); + } else if (need_newline) { + hb_buffer_append_char(output, '\n'); + need_newline = false; + } } - hb_buffer_append_whitespace(output, range_length(token->range)); break; } @@ -44,7 +81,7 @@ void herb_extract_ruby_to_buffer(const char* source, hb_buffer_T* output) { if (skip_erb_content == false) { bool is_inline_comment = false; - if (!is_comment_tag && token->value != NULL) { + if (!extract_options.comments && !is_comment_tag && token->value != NULL) { const char* content = token->value; while (*content == ' ' || *content == '\t') { @@ -58,12 +95,13 @@ void herb_extract_ruby_to_buffer(const char* source, hb_buffer_T* output) { } if (is_inline_comment) { - hb_buffer_append_whitespace(output, range_length(token->range)); + if (extract_options.preserve_positions) { hb_buffer_append_whitespace(output, range_length(token->range)); } } else { hb_buffer_append(output, token->value); + if (!extract_options.preserve_positions) { need_newline = true; } } } else { - hb_buffer_append_whitespace(output, range_length(token->range)); + if (extract_options.preserve_positions) { hb_buffer_append_whitespace(output, range_length(token->range)); } } break; @@ -71,22 +109,30 @@ void herb_extract_ruby_to_buffer(const char* source, hb_buffer_T* output) { case TOKEN_ERB_END: { bool was_comment = is_comment_tag; + bool was_erb_comment = is_erb_comment_tag; skip_erb_content = false; is_comment_tag = false; + is_erb_comment_tag = false; - if (was_comment) { - hb_buffer_append_whitespace(output, range_length(token->range)); - break; + if (extract_options.preserve_positions) { + if (was_comment) { + hb_buffer_append_whitespace(output, range_length(token->range)); + } else if (was_erb_comment && extract_options.comments) { + hb_buffer_append_whitespace(output, range_length(token->range)); + } else if (extract_options.semicolons) { + hb_buffer_append_char(output, ' '); + hb_buffer_append_char(output, ';'); + hb_buffer_append_whitespace(output, range_length(token->range) - 2); + } else { + hb_buffer_append_whitespace(output, range_length(token->range)); + } } - hb_buffer_append_char(output, ' '); - hb_buffer_append_char(output, ';'); - hb_buffer_append_whitespace(output, range_length(token->range) - 2); break; } default: { - hb_buffer_append_whitespace(output, range_length(token->range)); + if (extract_options.preserve_positions) { hb_buffer_append_whitespace(output, range_length(token->range)); } } } } @@ -94,6 +140,10 @@ void herb_extract_ruby_to_buffer(const char* source, hb_buffer_T* output) { herb_free_tokens(&tokens); } +void herb_extract_ruby_to_buffer(const char* source, hb_buffer_T* output) { + herb_extract_ruby_to_buffer_with_options(source, output, NULL); +} + void herb_extract_html_to_buffer(const char* source, hb_buffer_T* output) { hb_array_T* tokens = herb_lex(source); diff --git a/src/include/extract.h b/src/include/extract.h index 36f0b5028..0adf4e6d2 100644 --- a/src/include/extract.h +++ b/src/include/extract.h @@ -3,11 +3,26 @@ #include "util/hb_buffer.h" +#include + typedef enum { HERB_EXTRACT_LANGUAGE_RUBY, HERB_EXTRACT_LANGUAGE_HTML, } herb_extract_language_T; +typedef struct { + bool semicolons; + bool comments; + bool preserve_positions; +} herb_extract_ruby_options_T; + +extern const herb_extract_ruby_options_T HERB_EXTRACT_RUBY_DEFAULT_OPTIONS; + +void herb_extract_ruby_to_buffer_with_options( + const char* source, + hb_buffer_T* output, + const herb_extract_ruby_options_T* options +); void herb_extract_ruby_to_buffer(const char* source, hb_buffer_T* output); void herb_extract_html_to_buffer(const char* source, hb_buffer_T* output); diff --git a/test/c/test_extract.c b/test/c/test_extract.c index 0d268f01b..061a6ba17 100644 --- a/test/c/test_extract.c +++ b/test/c/test_extract.c @@ -1,9 +1,10 @@ #include "include/test.h" -#include "../../src/include/herb.h" #include "../../src/include/extract.h" #include "../../src/include/util/hb_buffer.h" +#include + TEST(extract_ruby_single_erb_with_semicolons) char* source = "<% if %>\n<% end %>"; char* result = herb_extract_ruby_with_semicolons(source); @@ -145,6 +146,84 @@ TEST(extract_ruby_inline_comment_complex) free(result); END +TEST(extract_ruby_with_options_semicolons_false) + char* source = "<% x = 1 %> <% y = 2 %>"; + + hb_buffer_T output; + hb_buffer_init(&output, strlen(source)); + + herb_extract_ruby_options_T options = HERB_EXTRACT_RUBY_DEFAULT_OPTIONS; + options.semicolons = false; + + herb_extract_ruby_to_buffer_with_options(source, &output, &options); + + ck_assert_str_eq(output.value, " x = 1 y = 2 "); + + free(output.value); +END + +TEST(extract_ruby_with_options_comments_true) + char* source = "<%# comment %>\n<% code %>"; + + hb_buffer_T output; + hb_buffer_init(&output, strlen(source)); + + herb_extract_ruby_options_T options = HERB_EXTRACT_RUBY_DEFAULT_OPTIONS; + options.comments = true; + + herb_extract_ruby_to_buffer_with_options(source, &output, &options); + + ck_assert_str_eq(output.value, " # comment \n code ;"); + + free(output.value); +END + +TEST(extract_ruby_with_options_preserve_positions_false) + char* source = "<% x = 1 %> <% y = 2 %>"; + + hb_buffer_T output; + hb_buffer_init(&output, strlen(source)); + + herb_extract_ruby_options_T options = HERB_EXTRACT_RUBY_DEFAULT_OPTIONS; + options.preserve_positions = false; + + herb_extract_ruby_to_buffer_with_options(source, &output, &options); + + ck_assert_str_eq(output.value, " x = 1 \n y = 2 "); + + free(output.value); +END + +TEST(extract_ruby_with_options_preserve_positions_false_and_comments_true) + char* source = "<%# comment %><%= something %>"; + + hb_buffer_T output; + hb_buffer_init(&output, strlen(source)); + + herb_extract_ruby_options_T options = HERB_EXTRACT_RUBY_DEFAULT_OPTIONS; + options.preserve_positions = false; + options.comments = true; + + herb_extract_ruby_to_buffer_with_options(source, &output, &options); + + ck_assert_str_eq(output.value, "# comment \n something "); + + free(output.value); +END + +TEST(extract_ruby_with_options_default) + char* source = "<% x = 1 %> <% y = 2 %>"; + + hb_buffer_T output; + hb_buffer_init(&output, strlen(source)); + + herb_extract_ruby_to_buffer_with_options(source, &output, &HERB_EXTRACT_RUBY_DEFAULT_OPTIONS); + + ck_assert_str_eq(output.value, " x = 1 ; y = 2 ;"); + + free(output.value); +END + TCase *extract_tests(void) { TCase *extract = tcase_create("Extract"); @@ -163,6 +242,11 @@ TCase *extract_tests(void) { tcase_add_test(extract, extract_ruby_inline_comment_multiline); tcase_add_test(extract, extract_ruby_inline_comment_between_code); tcase_add_test(extract, extract_ruby_inline_comment_complex); + tcase_add_test(extract, extract_ruby_with_options_semicolons_false); + tcase_add_test(extract, extract_ruby_with_options_comments_true); + tcase_add_test(extract, extract_ruby_with_options_preserve_positions_false); + tcase_add_test(extract, extract_ruby_with_options_preserve_positions_false_and_comments_true); + tcase_add_test(extract, extract_ruby_with_options_default); return extract; } diff --git a/test/extractor/extract_ruby_semicolons_test.rb b/test/extractor/extract_ruby_semicolons_test.rb new file mode 100644 index 000000000..d4cfc6d33 --- /dev/null +++ b/test/extractor/extract_ruby_semicolons_test.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module Extractor + class ExtractRubySemicolonsTest < Minitest::Spec + test "extract_ruby_single_erb_with_semicolon" do + source = "<% if %>\n<% end %>" + result = Herb.extract_ruby(source, semicolons: true) + + expected = " if ;\n end ;" + assert_equal expected, result + end + + test "extract_ruby_multiple_erb_same_line_with_semicolon" do + source = "<% x = 1 %> <% y = 2 %>" + result = Herb.extract_ruby(source, semicolons: true) + + assert_equal " x = 1 ; y = 2 ;", result + end + + test "extract_ruby_three_erb_same_line_with_semicolons" do + source = "<% a = 1 %> <% b = 2 %> <% c = 3 %>" + result = Herb.extract_ruby(source, semicolons: true) + + assert_equal " a = 1 ; b = 2 ; c = 3 ;", result + end + + test "extract_ruby_different_lines_with_semicolons" do + source = "<% x = 1 %>\n<% y = 2 %>" + result = Herb.extract_ruby(source, semicolons: true) + + expected = " x = 1 ;\n y = 2 ;" + assert_equal expected, result + end + + test "extract_ruby_mixed_lines" do + source = "<% a = 1 %> <% b = 2 %>\n<% c = 3 %>" + result = Herb.extract_ruby(source, semicolons: true) + + expected = " a = 1 ; b = 2 ;\n c = 3 ;" + assert_equal expected, result + end + + test "extract_ruby_output_tags_same_line" do + source = "<%= x %> <%= y %>" + result = Herb.extract_ruby(source, semicolons: true) + + assert_equal " x ; y ;", result + end + + test "extract_ruby_empty_erb_same_line" do + source = "<% %> <% %>" + result = Herb.extract_ruby(source, semicolons: true) + + assert_equal " ; ;", result + end + + test "extract_ruby_comments_skipped" do + source = "<%# comment %> <% code %>" + result = Herb.extract_ruby(source, semicolons: true) + + assert_equal " code ;", result + end + + test "extract_ruby_issue_135_if_without_condition" do + source = "<% if %>\n<% end %>" + result = Herb.extract_ruby(source, semicolons: true) + + expected = " if ;\n end ;" + assert_equal expected, result + end + + test "extract_ruby_inline_comment_same_line" do + source = "<% if true %><% # Comment here %><% end %>" + result = Herb.extract_ruby(source, semicolons: true) + + assert_equal " if true ; end ;", result + end + + test "extract_ruby_inline_comment_with_newline" do + source = "<% if true %><% # Comment here %>\n<% end %>" + result = Herb.extract_ruby(source, semicolons: true) + + expected = " if true ; \n end ;" + assert_equal expected, result + end + + test "extract_ruby_inline_comment_with_spaces" do + source = "<% # Comment %> <% code %>" + result = Herb.extract_ruby(source, semicolons: true) + + assert_equal " code ;", result + end + + test "extract_ruby_inline_comment_multiline" do + source = "<% # Comment\nmore %> <% code %>" + result = Herb.extract_ruby(source, semicolons: true) + + expected = " # Comment\nmore ; code ;" + assert_equal expected, result + end + + test "extract_ruby_inline_comment_between_code" do + source = "<% if true %><% # Comment here %><%= hello %><% end %>" + result = Herb.extract_ruby(source, semicolons: true) + + assert_equal " if true ; hello ; end ;", result + end + + test "extract_ruby_inline_comment_complex" do + source = "<% # Comment here %><% if true %><% # Comment here %><%= hello %><% end %>" + result = Herb.extract_ruby(source, semicolons: true) + + assert_equal " if true ; hello ; end ;", result + end + + test "extract_ruby_without_semicolons" do + source = "<% x = 1 %> <% y = 2 %>" + result = Herb.extract_ruby(source, semicolons: false) + + assert_equal " x = 1 y = 2 ", result + end + + test "extract_ruby_without_semicolons_multiline" do + source = "<% if %>\n<% end %>" + result = Herb.extract_ruby(source, semicolons: false) + + expected = " if \n end " + assert_equal expected, result + end + + test "extract_ruby_with_comments_included" do + source = "<%# comment %>" + result = Herb.extract_ruby(source, comments: true) + + assert_equal " # comment ", result + end + + test "extract_ruby_with_comments_and_code_same_line" do + source = "<%# comment %> <% code %>" + result = Herb.extract_ruby(source, comments: true) + + assert_equal " # comment code ;", result + end + + test "extract_ruby_with_comments_on_separate_lines" do + source = "<%# comment %>\n<% code %>" + result = Herb.extract_ruby(source, comments: true) + + assert_equal " # comment \n code ;", result + end + + test "extract_ruby_without_semicolons_with_comments" do + source = "<%# comment %> <% code %>" + result = Herb.extract_ruby(source, semicolons: false, comments: true) + + assert_equal " # comment code ", result + end + + test "extract_ruby_without_preserve_positions_single_tag" do + source = "<% code %>" + result = Herb.extract_ruby(source, preserve_positions: false) + + assert_equal " code ", result + end + + test "extract_ruby_without_preserve_positions_multiple_tags_same_line" do + source = "<% x = 1 %> <% y = 2 %>" + result = Herb.extract_ruby(source, preserve_positions: false) + + assert_equal " x = 1 \n y = 2 ", result + end + + test "extract_ruby_without_preserve_positions_with_html" do + source = "
<% code %>
" + result = Herb.extract_ruby(source, preserve_positions: false) + + assert_equal " code ", result + end + + test "extract_ruby_without_preserve_positions_multiline" do + source = "<% if true %>\n <% x = 1 %>\n<% end %>" + result = Herb.extract_ruby(source, preserve_positions: false) + + assert_equal " if true \n x = 1 \n end ", result + end + + test "extract_ruby_without_preserve_positions_with_comments" do + source = "<%# comment %><%= something_else %>" + result = Herb.extract_ruby(source, preserve_positions: false, comments: true) + + assert_equal "# comment \n something_else ", result + end + + test "extract_ruby_without_preserve_positions_comments_dont_affect_code" do + source = "<%# this is a comment %><% code %>" + result = Herb.extract_ruby(source, preserve_positions: false, comments: true) + + assert_equal "# this is a comment \n code ", result + end + + test "extract_ruby_without_preserve_positions_skips_comments_by_default" do + source = "<%# comment %><% code %>" + result = Herb.extract_ruby(source, preserve_positions: false) + + assert_equal " code ", result + end + end +end diff --git a/wasm/herb-wasm.cpp b/wasm/herb-wasm.cpp index 261083e6c..d778fef17 100644 --- a/wasm/herb-wasm.cpp +++ b/wasm/herb-wasm.cpp @@ -61,11 +61,27 @@ val Herb_parse(const std::string& source, val options) { return result; } -std::string Herb_extract_ruby(const std::string& source) { +std::string Herb_extract_ruby(const std::string& source, val options) { hb_buffer_T output; hb_buffer_init(&output, source.length()); - herb_extract_ruby_to_buffer(source.c_str(), &output); + herb_extract_ruby_options_T extract_options = HERB_EXTRACT_RUBY_DEFAULT_OPTIONS; + + if (!options.isUndefined() && !options.isNull() && options.typeOf().as() == "object") { + if (options.hasOwnProperty("semicolons")) { + extract_options.semicolons = options["semicolons"].as(); + } + + if (options.hasOwnProperty("comments")) { + extract_options.comments = options["comments"].as(); + } + + if (options.hasOwnProperty("preserve_positions")) { + extract_options.preserve_positions = options["preserve_positions"].as(); + } + } + + herb_extract_ruby_to_buffer_with_options(source.c_str(), &output, &extract_options); std::string result(hb_buffer_value(&output)); free(output.value); return result;