Sunday, December 29, 2013

Dart Transformers for Polymer Cleanup


All right, I really like this approach to loading configuration data in Polymer applications:
<link rel="import" href="hello-you-en.json">
<link rel="import" href="hello-you-fr.json">
<link rel="src" href="hello-you-es.json">
<polymer-element name="hello-you">
  <!-- Polymer definition here -->
</polymer-element>
The Polymer polyfill code makes sure that this JSON data is imported just like Polymer code that might otherwise go there. In JavaScript, I can then work through the Polymer's element property to access the definition and these data files:
        var list = this.
          element.
          parentElement.
          querySelectorAll('link[rel=import]').
          array().
          filter(function(el) {
            return el.href.match(/hello-you-\w+.json/)
          });
The nice thing about this approach is that the imports are guaranteed to be loaded before the Polymer code executes. There is no need for futures, promises or callbacks. It is ready when the Polymer is ready. Nice!

Only it does not quite work in Polymer.dart, the Dart version of the library. For one thing there is no element property. There is a declaration property which does nearly the same thing, but does not include access to elements outside of the <polymer-element> tag (I am pretty sure that is a bug). But, what does work is moving the <link> tags inside the <polymer-element> definition:
<polymer-element name="hello-you">
  <link rel="import" href="hello-you-en.json">
  <link rel="import" href="hello-you-fr.json">
  <link rel="import" href="hello-you-es.json">
  <!-- Polymer definition here... -->
</polymer-element>
I can then query the import data * through the declaration property that refused me access to these <link> imports when they resided outside the <polymer-element>:
@CustomTag('hello-you')
class HelloYou extends PolymerElement {
  // ...
  ready() {
    super.ready();

    var re = new RegExp(r'hello-you-(\w+).json');
    var list = declaration.
      parent.
      querySelectorAll('link[rel=import]').
      where((el)=> el.href.contains(re));
    // ...
  }
}
This is not ideal, but I can live with this. At least until I can convince the fine Polymer.dart folks that the declaration property (if they keep it) should include extra-<polymer-element> access.

What I cannot live with is what the Polymer “transformer” does to these <link> imports of JSON data. The transformer, which transforms the page into something closely resembling a thing that can be deployed (but that can also include debug code for local development), winds up including the JSON data directly in the page:



There are any number of things that I could do to get around this, but since, this residue is created by Polymer's transformer, writing my own transformer seems like a fine way of dealing with this. But how the heck do you write a transformer in Dart?

To answer that question, I dig through the Polymer code a bit. The transformer in there is named transformer.dart. I am unsure if the name is significant, but, assuming that it is, I create a lib/transformer.dart code file in my application. I eventually trace the Polymer code back to the Pub barback package, which defines the Transformer baseclass. So my lib/transformer.dart code starts as:
library i18n_example.transformer;

import 'package:barback/barback.dart';

class ScrubJsonImports extends Transformer {
  ScrubJsonImports.asPlugin(BarbackSettings settings);

  Future apply(Transform transform) {
    print('here');
  }
}
The asPlugin() named constructor turns out to be necessary to get pub serve to start. To try this skeleton transformer out, I add it to my application's pubspec.yaml:
name: i18n_example
dependencies:
  polymer: any
  polymer_elements: any
  intl: any
dev_dependencies:
  unittest: any
transformers:
- polymer:
    entry_points: web/index.html
- i18n_example:
    entry_points: web/index.html
I add it after the Polymer transformer because I want to fix Polymer's transformer output (it is also possible to run transformers in parallel).

It is when running my transformer with pub serve that I find that I need the asPlugin() named constructor. Without it, I get:
$ pub serve
No transformers that accept configuration were defined in package:i18n_example/transformer.dart or package:i18n_example/i18n_example.dart,
required by i18n_example.
Also, it does seem that the name transformer.dart is significant.

I copy the configuration for the Polymer transformer in pubspec.yaml, so I also copy some of the code to parse it:
class ScrubJsonImports extends Transformer {
  List entryPoints;

  ScrubJsonImports(this.entryPoints);

  ScrubJsonImports.asPlugin(BarbackSettings settings)
    : this(_parseSettings(settings));

  Future<bool> isPrimary(Asset input) {
    if (entryPoints.contains(input.id.path)) return new Future.value(true);
    return new Future.value(false);
  }
}

List<String> _parseSettings(BarbackSettings settings) {
  var args = settings.configuration;
  return _readEntrypoints(args['entry_points']);
}

List<String> _readEntrypoints(value) {
  if (value == null) return null;
  return (value is List) ? value : [value];
}
Most of that pulls the settings from pubspec.yaml and creates a list of entryPoints—places in the application that need to be transformed—to be used as an instance variable. The isPrimary() method uses this list to decide if any of the assets that it sees (and will see all assets in my application) need to be transformed.

At this point, I only need define a way for my transformer to apply its fix to Polymer's work. Given a transform with a primary input (the web/index.html entry point), I needs to read the input as a string, fix the string, and add the fixed string to the transform's output:
class ScrubJsonImports extends Transformer {
  // ...
  Future apply(Transform transform) {
    var input = transform.primaryInput;

    return transform.
      readInputAsString(input.id).
      then((html){
        var fixed = html.replaceAllMapped(
          new RegExp(r'>\s*(\{[\s\S]+\})\s*<polymer-element', multiLine: true),
          (m) => '><div style="display:none">${m[1]}</div><polymer-element'
        );

        transform.addOutput(new Asset.fromString(input.id, fixed));
      });
  }
}
To keep from double-processing assets, Pub transformers assign each a unique id attribute. This lets a transformer look up the current state of an asset and create a new state for the asset.

The above regular expression looks through a Polymer transformer output such as:
...
  <body>{
  "hello": "Hello",
  "done": "Done",
  "how_many": "How many?",
  "instructions": "Introduce yourself for an amazing personalized experience!"
}
{
  "hello": "Bonjour",
  "done": "Fin",
  "how_many": "Combien?",
  "instructions": "Présentez-vous une expérience personnalisée incroyable!"
}
{
  "hello": "¡Hola!",
  "done": "Hecho",
  "how_many": "¿Cuántos?",
  "instructions": "Preséntese para una experiencia personalizada increíble!"
}<polymer-element name="hello-you">
...
And will wrap the nake JSON from my <link> imports inside a hidden <div>:
...
  <body><div style="display:none">{
  "hello": "Hello",
  "done": "Done",
  "how_many": "How many?",
  "instructions": "Introduce yourself for an amazing personalized experience!"
}
{
  "hello": "Bonjour",
  "done": "Fin",
  "how_many": "Combien?",
  "instructions": "Présentez-vous une expérience personnalisée incroyable!"
}
{
  "hello": "¡Hola!",
  "done": "Hecho",
  "how_many": "¿Cuántos?",
  "instructions": "Preséntese para una experiencia personalizada increíble!"
}</div><polymer-element name="hello-you">
...
And, with that, I have my Polymer working as desired. I have imported localization JSON files into the Polymer and scrubbed Polymer's transformer resulting in a working, localized Polymer:



The long term solution here is to file some bug reports and work with the developers to get all of this working for everybody. Admittedly, much of tonight's effort was simply because I was curious how Dart transformers worked. Mission accomplished on that account, but it is still gratifying to have my approach to internationalization working in both JavaScript and Dart. Even if I had to muck with a simple transformer to make that happen.

* This does not work in the dart2js compiled code—the import property of the <link> elements is null. To me the inconsistency is a bug and either the import property here should be the same as in the Dart VM or, better, the <link> elements should be accessible outside the <polymer-element> tag. Either way, I am not too concerned about this. I will file bugs and help get this sorted out.


Day #980

No comments:

Post a Comment