summaryrefslogtreecommitdiff
path: root/lib/Tree/Transform/XSLTish.pm
blob: e4899922e6b7c92af1af4c045897c541235f0964 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
package Tree::Transform::XSLTish; 
use strict;
use warnings;
use Sub::Exporter;
use Params::Validate ':all';
use Tree::Transform::XSLTish::Utils;
use Tree::Transform::XSLTish::Transformer;
use Carp::Clan qw(^Tree::Transform::XSLTish);
use 5.006;
 
our $VERSION='0.2';
 
my @DEFAULT_EXPORTS=('tree_rule',
                     'default_rules',
                     'new_transformer' => {-as => 'new'},
                 );
 
Sub::Exporter::setup_exporter({
    exports => [qw(tree_rule default_rules new_transformer engine_class engine_factory)],
    groups => {
        default => \@DEFAULT_EXPORTS,
        engine => [@DEFAULT_EXPORTSqw(engine_class engine_factory)],
    }
});
 
sub default_rules {
    my $store=Tree::Transform::XSLTish::Utils::_rules_store(scalar caller);
 
    push @{$store->{by_match}},
        {match=> '/',priority=>0,action=>sub { $_[0]->apply_rules } },
        {match=> '*',priority=>0,action=>sub { $_[0]->apply_rules } },
            ;
    return;
}
 
sub tree_rule {
    my (%args)=validate(@_, {
        match => { type => SCALAR, optional => 1 },
        action => { type => CODEREF },
        name => { type => SCALAR, optional => 1},
        priority => { type => SCALAR, default => 1 },
    });
 
    # TODO at least one of 'name' and 'match' must be specified 
    # TODO default priority based on match 
 
    my $store=Tree::Transform::XSLTish::Utils::_rules_store(scalar caller);
 
    if ($args{match}) {
        push @{$store->{by_match}},\%args;
    }
    if ($args{name}) {
        if (exists $store->{by_name}{$args{name}}) {
            carp "Duplicate rule named $args{name}, ignoring";
            return;
        }
        $store->{by_name}{$args{name}}=\%args;
    }
 
    return;
}
 
sub engine_class {
    my ($classname)=@_;
 
    Tree::Transform::XSLTish::Utils::_set_engine_factory(
        scalar caller,
        sub{$classname->new()},
    );
 
    return;
}
 
sub engine_factory(&) {
    my ($new_factory)=@_;
 
    Tree::Transform::XSLTish::Utils::_set_engine_factory(
        scalar caller,
        $new_factory,
    );
 
    return;
}
 
sub new_transformer {
    my $rules_package=shift;
 
    return Tree::Transform::XSLTish::Transformer->new(rules_package=>$rules_package,@_);
}
 
1;
__END__
 
=head1 NAME
 
Tree::Transform::XSLTish - transform tree data, like XSLT but in Perl
 
=head1 SYNOPSIS
 
  package MyTransform;
  use Tree::Transform::XSLTish;
 
  default_rules;
 
  tree_rule match => 'node[@id=5]', action => sub {
    return $_[0]->it->data();
  };
 
  package main;
  use My::Tree;
 
  my $tree= My::Tree->new();
  # build something inside the tree
 
  my ($node5_data)=MyTransform->new->transform($tree);
 
Transforming an HTML document:
 
 package HtmlTransform;
 use Tree::Transform::XSLTish;
 use strict;
 use warnings;
 
 engine_class 'XML::XPathEngine';
 
 default_rules;
 
 tree_rule match => 'img[@alt="pick"]', action => sub {
     return $_[0]->it->findvalue('@src');
 };
 
 package main;
 use HTML::TreeBuilder::XPath;
 
 my $tree=HTML::TreeBuilder::XPath->new();
 $tree->parse_file('mypage.html');
 
 my $trans=HtmlTransform->new();
 my ($image_srce)=$trans->transform($tree);
 
=head1 DESCRIPTION
 
This module allows you to transform tree with Perl subroutines, just
like XSLT does for XML documents.
 
It tries to model as closely as reasonable the semantic of XSLT.
 
=head1 REQUIREMENTS
 
By default, this module uses L<Tree::XPathEngine> as its XPath engine,
but you can use any other similar module, provided it implements the
method C<findnodes> with the same signature and
meaning. L<XML::XPathEngine> is a good candidate, or you could use
L<XML::LibXML::XPathContext>.
 
The tree that you intend to manipulate must be implemented by classes
that are compatible with the XPath engine; for example,
L<Tree::DAG_Node::XPath> if you use L<Tree::XPathEngine>, or
L<HTML::TreeBuilder::XPath> if you use L<XML::XPathEngine>.
 
=head1 EXPORTS
 
=head2 C<tree_rule>
 
  tree_rule match => '//node_name',
            priority => 1,
            action => sub { ... };
 
This is the basic fuction to declare a transformation rule; it's
equivalent to the C<template> element is XSLT. It takes its parameters
as a hash:
 
=over 4
 
=item C<match>
 
this is equivalent to the C<match> attribute of C<template>: it
specifies the pattern for the nodes to which this rule applies.
 
From the L<XSLT spec|http://www.w3.org/TR/xslt.html#NT-Pattern>:
 
I<A pattern is defined to match a node if and only if there is a
possible context such that when the pattern is evaluated as an
expression with that context, the node is a member of the resulting
node-set. When a node is being matched, the possible contexts have a
context node that is the node being matched or any ancestor of that
node, and a context node list containing just the context node.>
 
=item C<name>
 
this is equivalent of the C<name> attribute of C<template>: it allows
calling rules by name (see
L<call_rule|Tree::Transform::XSLTish::Transformer/call_rule>)
 
=item C<priority>
 
this is equivalent of the C<priority> attribute of C<template>;
currently the "default priority" as specified in the
L<spec|http://www.w3.org/TR/xslt.html#conflict> is not implemented
 
=item C<action>
 
this code-ref will be called (in list context) when the rule is to be
applied; it can return whatever you want:
L<call_rule|Tree::Transform::XSLTish::Transformer/call_rule> will
return the result unchanged,
L<apply_rules|Tree::Transform::XSLTish::Transformer/apply_rules> will
return the list of all results of all the applied rules
 
=back
 
The C<action> code-ref will be called (by
L<apply_rules|Tree::Transform::XSLTish::Transformer/apply_rules> or
L<call_rule|Tree::Transform::XSLTish::Transformer/call_rule>) with a
L<Tree::Transform::XSLTish::Transformer> object as its only parameter.
 
=head2 C<default_rules>
 
This function will declare two rules that mimic the implicit rules of
XSLT. It's equivalent to:
 
 tree_rule match => '/', priority => 0, action => sub {$_[0]->apply_rules};
 tree_rule match => '*', priority => 0, action => sub {$_[0]->apply_rules};
 
=head2 C<engine_class>
 
  engine_class 'XML::LibXML::XPathContext';
 
This function declares that the
L<Tree::Transform::XSLTish::Transformer> object returned by L</new>
should use this class to build its XPath engine.
 
This function is not exported by default: you have to use the module as:
 
 use Tree::Transform::XSLTish ':engine';
 
=head2 C<engine_factory>
 
  engine_factory { My::XPath::Engine->new(params=>$whatever) };
 
This function declares that the
L<Tree::Transform::XSLTish::Transformer> object returned by L</new>
should call the passed code-ref to get its engine.
 
C<engine_class $classname> is equivalent to C<engine_factory {
$classname->new }>.
 
This function is not exported by default: you have to use the module as:
 
 use Tree::Transform::XSLTish ':engine';
 
=head2 C<new>
 
Returns a L<Tree::Transform::XSLTish::Transformer> for the rules
declared in this package.
 
=head1 INHERITANCE
 
L<Stylesheet import|http://www.w3.org/TR/xslt.html#import> is implented
with the usual Perl inheritance scheme. It should even work with
L<Class::C3>, since we use L<Class::MOP>'s C<class_precedence_list> to
get the list of inherited packages.
 
Engine factories are inherited, too, so you can extend a rules package
without re-specifying the engine (you can, of course, override this
and specify another one).
 
=head1 IMPORTING
 
This module uses L<Sub::Exporter>, see that module's documentation for
things like renaming the imports.
 
=head1 KNOWN BUGS & ISSUES
 
=over 4
 
=item *
 
It's I<slow>. Right now each rule application is linear in the number
of defined rules I<times> the depth of the node being
transformed. There are several ways to optimize this for most common
cases (patches welcome), but I prefer to "make it correct, before
making it fast"
 
=item *
 
Some sugaring with L<Devel::Declare> could make everything look better
 
=back
 
=head1 AUTHOR
 
Gianni Ceccarelli <dakkar@thenautilus.net>
 
=cut