Friday, January 10, 2014

Day 992: Search and Replace Dart Transformer to Hide from Polymer


OK I lied. Yesterday, I breezed past a little Dart problem in an effort to finish off some work. Today I try to address that oversight.

As I briefly mentioned yesterday, I am able to read Polymer configuration that is stored <link> imports inside the definition of a Dart Polymer:
<script src="../../packages/browser/dart.js"></script>
<script src="../../packages/browser/interop.js"></script>
<polymer-element name="hello-you">
  <link rel="import" href="hello-you.json">
  <template>
    <!-- ... -->
  </template>
  <script type="application/dart" src="hello_you.dart"></script>
</polymer-element>
It takes a little effort, but I can grab hello-you.json from the Polymer, reading the data into the Polymer's configuration data. I have to work through the declaration property and run the result through a transformer, but it is doable.

What I cannot quite do is <link> import data when I use the Polymer:
<hello-you>
  <link rel="import" href="hello-you.json">
  <p>And good-bye…</p>
</hello-you>
My problem is something of a running theme with Polymer.dart—the deployment transformer includes <link> imports inline, which results in actually seeing the JSON data embedded diring in the web page:



I can work around this inside the Polymer definition with a deployment build transformer that searched for raw JSON, wrapping it in a display: none styled tag. The actual <link> tag remains in the Polymer, allowing me to read it as configuration.

My problem with configuration in a Polymer is twofold. First, it is a little harder to find the JSON to wrap it inside display: none tags. Second, even if I could do so, the <link> tag is stripped out so that I could not read it even if I wanted to do so. I end up with this in the main web page:
{
  "hello": "Yo"
}
<div class="container">
  <hello-you>
    
    <p>And good-bye…</p>
  </hello-you>
</div>
I think the best thing would be to pre-transform the page so that, by the time the Polymer transformer sees it, it is no longer a <link>. The pre-transformer could take the Polymer:
<hello-you>
  <link rel="import" href="hello-you.json">
  <p>And good-bye…</p>
</hello-you>
And transform it to:
<hello-you>
  <!-- link rel="import" href="hello-you.json" -->
  <p>And good-bye…</p>
</hello-you>
Then the code could pass through the Polymer transformer, which can do its usual thing—except that it will skip the commented <link> tag. Finally, a post-transformer could restore the original <link>:
<hello-you>
  <link rel="import" href="hello-you.json">
  <p>And good-bye…</p>
</hello-you>
I have gotten pretty good at writing search and replace Dart Pub transformers. So good, in fact, that I think it is time to write a pub package. If I call the package replace_transformer, then my application could have a pubspec.yaml that looks like:
name: config_example
dependencies:
  polymer: any
  replace_transformer:
    path: /home/chris/repos/replace_transformer
dev_dependencies:
  unittest: any
transformers:
- replace_transformer:
    file: web/index.html
    match: '<link rel="import" href="hello-you.json">'
    replace: '<!-- link rel="import" href="hello-you.json" -->'
- polymer:
    entry_points: web/index.html
- replace_transformer:
    file: web/index.html
    match: '<!-- link rel="import" href="hello-you.json" -->'
    replace: '<link rel="import" href="hello-you.json">'
In the dependencies section of pubspec.yaml, I mark my new replace_transformer package as a dependency. Once I am ready to release this package to pub.dartlang.org, I will remove the local path. For now, the local path allows me to build my transformer and try it right away. Brilliant!

There are two ways to name a Dart transformer and I have forgotten one of them. The one I do remember is to create a file named transformer.dart. So in replace_transformer, I create lib/transformer:
library replace.transformer;
import 'dart:async';
import 'package:barback/barback.dart';

class ReplaceTransformer extends Transformer {
  String file;
  Pattern match;
  String replace;

  ReplaceTransformer(this.file, this.match, this.replace);

  Future isPrimary(Asset input) { /* ... */ }

  Future apply(Transform transform) { /* ... */ }
}
Nothing out of the ordinary there. I declare a library. I import packages—async for access to Future and barback from pub itself for the Transformer class as well as the ability to read the pubspec.yaml configuration. I declare the three instance variables that will be assigned from pubspec.yaml and define a constructor that assigns those instance variables. Lastly, I define the two methods that need to be implemented by a subclass of Transformer. The isPrimary() method indicates if a particular asset needs to be directly transformed (it will have to return true when the asset matches file). And the apply() method actually performs the transformation.

Pub expects its tranformers to define an asPlugin named constructor which access barback settings:
class ReplaceTransformer extends Transformer {
  String file;
  Pattern match;
  String replace;

  ReplaceTransformer(this.file, this.match, this.replace);

  ReplaceTransformer.fromList(List a): this(a[0], a[1], a[2]);

  ReplaceTransformer.asPlugin(BarbackSettings settings)
    : this.fromList(_parseSettings(settings));
  // ...
}
I redirect this constructor to the fromList() named constructor that takes a list and redirects to the primary constructor with the three expected values. The _parsedSettings() is easy enough:
List<String> _parseSettings(BarbackSettings settings) {
  var args = settings.configuration;
  return [
    args['file'],
    args['match'],
    args['replace']
  ];
}
With that, I need to teach my ReplaceTransfomer when to apply transforms:
  Future<bool> isPrimary(Asset input) {
    if (file == input.id.path) return new Future.value(true);
    return new Future.value(false);
  }
And I need to replace all instances of match with replace when the transform is applied:
  Future apply(Transform transform) {
    var input = transform.primaryInput;
    return transform.
      readInputAsString(input.id).
      then((html){
        var fixed = html.replaceAllMapped(
          match,
          (m) => replace
        );

        transform.addOutput(new Asset.fromString(input.id, fixed));
      });
  }
And that actually does the trick. Now when I load my Polymer, the <link> tag makes it through Polymer's transformer unscathed:



Nice! I am still a little uncertain how to approach this stuff in Patterns in Polymer, especially since the JavaScript version of Polymer does not really have the concept of asset packaging. But, for now, I am glad to feel like I have a workable, if a bit verbose solution in Dart.

Day #992

2 comments:

  1. Hey I heard about Polymer JS Vulcanize, which is, by the looks of things, doing a similar job to pub build. Just curious if you've heard of it and if you going to have to use a similar strategy in Polymer JS, to what you're doing with Dart Transformers here, as a result of what Vulcanizer does? http://www.polymer-project.org/tooling-strategy.html#vulcanize-build-tool

    ReplyDelete
    Replies
    1. D'oh! You're right, of course. I'll check it out tonight -- before the #pairwithme session :)

      Thanks for the reminder!

      Delete