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;