Project

General

Profile

Actions

Feature #19015

open

Language extension by a heredoc

Added by ko1 (Koichi Sasada) 8 days ago. Updated 2 days ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:109985]

Description

This propose new heredoc extension with <<!LANG like

doc = <<!LANG
  # description written in lang LANG
  foo bar
LANG

and it is translated to:

doc = heredoc_extension_LANG(heredoc_text, binding)

Example

require 'erb'

def heredoc_extension_erb str, b
  ERB.new(str).run(b)
end

name = 'ko1'

html = <<!erb
<div>Hello <%= name %></div>
erb

puts html #=> <div>Hello ko1</div>

Background / considerations

  • Sometimes we write Ruby syntax string with <<RUBY and this proposal inspired by it.
  • it is similar to shebang (#!LANG in shell)
  • Elixir's custom sigil translates ~u(...) translates to sigil_u(...). This is why it translated to heredoc_extension_LANG(...) private method call.
  • JavaScript has JSX but I don't think it is fit to the Ruby language.
  • Heredoc is Ruby's chaos part and already confusing a lot. Additional chaos doesn't matter.
  • <<!foo is valid syntax but now I don't think it is not used. gem codesearch doesn't find the usage.
  • Sorry I couldn't wait 1st/Apr.

Implementation

I attached the experimental implementation which only supports erb (because I couldn't find how to get delimiter to determine a method name :p).


Files

heredoc_extension.patch (2.7 KB) heredoc_extension.patch ko1 (Koichi Sasada), 09/22/2022 03:23 AM
Actions #1

Updated by ko1 (Koichi Sasada) 8 days ago

  • File deleted (tmp.YubJ1EHi0O-xyzzy)

Updated by retro (Josef Šimánek) 7 days ago

Would you mind to add also what's the output to the description? If I understand it well, following will be printed.

<div>Hello ko1</div>

Any plans already what to do when method is not implemented?

Updated by zverok (Victor Shepelev) 7 days ago

I am not sure how serious this is (considering the "Apr 1" notice), but I have somewhat adjacent thought:

In many modern code editors, highlighting of several different languages in the same file is supported. Namely, SublimeText (I am not sure about the others, but I suppose the idea is not unique) understands this:

query = <<~SQL
  SELECT * FROM users WHERE status='active
SQL

DB.execute(query)

...and highlights the code inside a heredoc as SQL.

I am thinking that maybe some way of preserving the "tag" it was surrounded (in String metadata?.. Or, making HEREDOC-produced object a different class, a descendant of String with extra functionality) would be generically useful. It will allow implementing the @ko1 (Koichi Sasada) 's example just like this:

require 'erb'

def heredoc_extension_erb str, b
  ERB.new(str).run(b)
end

name = 'ko1'

html = <<~erb
<div>Hello <%= name %></div>
erb

puts execute_heredoc(html, binding)

# where...
def execute_heredoc(str, binding)
  case str.__tag__
  when 'erb'
    ERB.new(str).run(binding)
  # ....
  end
end

The idea can even be expanded to provide additional metadata (currently invalid syntax, so no compatibility would be broken):

html = <<~erb(trim_mode="%>")
<div>Hello <%= name %></div>
erb

WDYT?

Actions #6

Updated by ko1 (Koichi Sasada) 7 days ago

  • Description updated (diff)

Updated by yugui (Yuki Sonoda) 7 days ago

one bikeshedding..

Usually syntax-suger in Ruby are mapped into methods whose names are very consistent with the syntax. e.g. `, []=, or foo=. Therefore, it is more consistent to map the new syntax into <<!LANG method.
It seems possible to make the lexer distinguish such a notation from << symbol.

Updated by ko1 (Koichi Sasada) 7 days ago

This is a tiny example of SQL.
Note that I'm newbe of SQL (I goooooooogled the syntax/api and the example is never tested).

module SQLite3HeredocExtension
  def compile str
    vs = []
    compiled = str.gsub(/\?([a-z_][a-zA-Z\d]+)/){|m|
      vs << $1
      ??
    }
    [vs, compiled]
  end

  # syntax
  # * SQL statement
  # * but ?var is replaced with the Ruby's variable

  def heredoc_exntesion_SQL str, b
    vs, compiled_str = compile str
    stmt = @db.prepare compiled_str
    @db.execute stmt, vs.map{|v| b.local_variable_get(v).to_s}
  end
end

require 'sqlite3'

@db = SQLite3::Database.new "test.db"
include SQLite3HeredocExtension

results = <<!SQL
  select person.name from people where person.age > ?lowest_age
SQL

## hmm, "SQL" seems to return a SQL statement...?

# AR
results = People.select(:name).where("age >= ?", lowest_age)

# AR/execute
results = ActiveRecord::Base.connection.execute("select person.name from perople where person.age > ?", lowest_age)

Updated by estum (Anton (estum)) 7 days ago

Wow, I am not the only such geek %)
My solution of the similar goal is 7 yo and it'll go to school soon.

$ git log lib/osascript.rb
commit 1f39d1d42b499d1424af1fa5a109ecd6ab219563 (HEAD -> master)
Author: Anton
Date:   Thu Jun 11 08:47:12 2015 +0300
# @example Simple
#   Osascript.new(<<~SCPT.freeze).()
#     activate application "Finder"
#   SCPT
#
# @example JSC with args
#   # The script takes 2 arguments: directory path & image path
#   # to set a folder icon to the given directory.
#   script = Osascript.new(<<-JS.freeze, lang: 'JavaScript')
#     ObjC.import("Cocoa");
#     function run(input) {
#       var target_path = input[0].toString();
#       var source_image = $.NSImage.alloc.initWithContentsOfFile(input[1].toString());
#       var result = $.NSWorkspace.sharedWorkspace.setIconForFileOptions(source_image, target_path, 0);
#       return target_path;
#     }
#   JS
#   script.(target_dir, folder_icon)
class Osascript
  attr_accessor :script

  def initialize(script = nil, lang: "AppleScript")
    @script = block_given? ? yield : script
    @lang = lang
  end

  def call(*other)
    handle_errors do
      cmd = ["/usr/bin/env", "osascript", *params(*other)]

      IO.popen cmd, "r+", 2 => %i(child out) do |io|
        io.write script
        io.close_write
        io.readlines
      end
    end
  end

  def params(*args)
    ["-l", @lang].tap { |e| e.concat(args.unshift(?-)) unless args.empty? }
  end

  ERROR_PATTERN = /(?<=execution error: )(.+?)(?=$)/
  USER_CANCELLED_PATTERN = /user canceled/i
  NL = "\n"

  private

  def handle_errors
    yield().each_with_object([]) do |line, buf|
      line.match(ERROR_PATTERN) { |m| raise error_for(m[0]), m[0], caller(4) }
      buf << line.strip
    end.join(NL)
  end

  def error_for(msg)
    USER_CANCELLED_PATTERN.match?(msg) ? UserCanceled : ExecutionError
  end

  class ExecutionError < RuntimeError
    CAPTURE_MSG_AND_CODE = /(.+?) \((-?\d+?)\)$/

    attr_reader :code

    def initialize(msg)
      msg.match(CAPTURE_MSG_AND_CODE) { |m| msg, @code, * = m.captures }
      super(msg)
    end
  end

  UserCanceled = Class.new(ExecutionError)
end

I've wrote it when I've known that cool syntax hook at the first time — an ability to pass only the opening heredoc word in closed parenthesis on single line and you can ducktype it infinitely.

Oh, and I just called in mind one more thing about heredoc: there is some tricky heredoc syntax in core source file forwardable.rb which brakes my brain when I try to understand it:

    if _valid_method?(method)
      loc, = caller_locations(2,1)
      pre = "_ ="
      mesg = "#{Module === obj ? obj : obj.class}\##{ali} at #{loc.path}:#{loc.lineno} forwarding to private method "
      method_call = "#{<<-"begin;"}\n#{<<-"end;".chomp}"
        begin;
          unless defined? _.#{method}
            ::Kernel.warn #{mesg.dump}"\#{_.class}"'##{method}', uplevel: 1
            _#{method_call}
          else
            _.#{method}(*args, &block)
          end
        end;
    end

    _compile_method("#{<<-"begin;"}\n#{<<-"end;"}", __FILE__, __LINE__+1)
    begin;
      proc do
        def #{ali}(*args, &block)
          #{pre}
          begin
            #{accessor}
          end#{method_call}
        end
      end
    end;

Pretty cryptic, isn't it?

Updated by duerst (Martin Dürst) 4 days ago

This proposal sounds interesting, but the naming looks like behind-the-scenes metaprogramming; it may be better to use a more flexible approach that doesn't fix the function name.

Updated by Eregon (Benoit Daloze) 2 days ago

This seems nice.
Would it also remove leading indentation like <<~HEREDOC?

Actions

Also available in: Atom PDF