senny / sablon

Ruby Document Template Processor based on docx templates and Mail Merge fields.
MIT License
443 stars 126 forks source link

A way to skip merge fields not in context #108

Closed sashman closed 6 years ago

sashman commented 6 years ago

I have a layout file which contains some merge fields that I'd like to fill in later. But I do want to fill in the body merge field for example. My context would look like:

{
body: "My body"
}

But in the docx file, I have additional merge field I'd like not to be replaced and stay as merge fields for the next templating stage. What would be the easiest way to achieve this? Maybe somehow skipping merge fields which do not have a matching key in the context hash?

sashman commented 6 years ago

In mail_merge.rb I've messed about with parse_fields by adding the env as a parameter, then I was able to do the following:

Add an extra check:

# line 144
fields << field if field && field.valid? && field_in_env?(field, env)

and check the env if the context contains the key:

      def field_in_env?(field, env)
        env.context.key?(field.expression.gsub(/^=/, ''))
      end

For the above I've had to change document.rb line 73 to:

operations = build_operations(@parser.parse_fields(xml_node, env))

But I'm not sure where else @parser.parse_fields might be called...

stadelmanma commented 6 years ago

The main catch with your approach is that the code allows for chained access using context keys such as article.title which might evaluate out to a proper value but searching explicitly for that key will throw a false negative (further complicating things is that title could be a key in a nested hash or a method call on the article object).

You could design a recursive version using the same logic in the LookupOrMethodCall class (see lib/sablon/operations.rb), and if you only care about hash lookups tweak the code accordingly.


As for a different solution, a quick test shows the following change to the Sablon::Statement::Insertion class is sufficient to preserve any expressions that evaluate to a "falsey" value. However, this will preserve both missing keys and keys that are set to a "falsey" value alike which might not be what you want.

    class Insertion < Struct.new(:expr, :field)
      def evaluate(env)
        if content = expr.evaluate(env.context)
          field.replace(Sablon::Content.wrap(content), env)
        else
          #field.remove
        end
      end
    end

That being said, I think your best bet might be defining a kind of "optional" field expression and registering it with the document processor. See lib/sablon/processor/document/field_handlers.rb and lib/sablon/processor/document.rb for examples on how the "builtin" handlers are done.

For example you could create an "OptionalInsertion" statement class and field handler using the expression ?=keyname instead of =keyname. Although that would require modifying your templates which is never fun.

One key caveat is that field handlers are not applied in any particular order and the first one that "handles" the given field wins. This does pose an issue where a more specific handler might never get invoked because a more general handler gets checked first. It was something I overlooked until now and probably should fix.

sashman commented 6 years ago

Thanks for the swift reply @stadelmanma! You're awesome! Currently, this is a one-off feature I require so will probably leave it as a local change to the gem, especially as this has no quick idiomatic fix. But thanks a lot for the help!

stadelmanma commented 6 years ago

Closing this as resolved.