Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Work in progress, everything is broken at the moment.

  • Loading branch information...
commit 0f62e1480d326db24f8fc25e02f5defabb95cab9 1 parent 22db717
@supernovus authored
View
2  META.info
@@ -1,7 +1,7 @@
{
"name" : "Flower",
"version" : "*",
- "description" : "Implementation of TAL and METAL template engines",
+ "description" : "XML Application Languages, including TAL.",
"depends" : [ "Exemel", "DateTime::Utils" ],
"source-url" : "git://github.com/supernovus/flower.git"
}
View
92 README
@@ -1,34 +1,45 @@
-=== Flower -- Petal for Perl 6
-
-NOTE: A massive rewrite is planned for Flower that will vastly change how
- it works, and what it's capable of. When it's done, Flower will be
- a LOT more than what it is now. See doc/TODO.txt for more details.
-
-Petal is the Perl Template Attribute Language (Perl 5), and is derived from the
-TAL, METAL and TALES specifications from Zope (Python).
-
-Flower is an implementation of Petal in Perl 6.
-
-It's not the same as the Perl 5 implementation, and has a different
-feature-set and parsing model.
-
-That said, it tries to implement as much of Petal as possible in a mostly
-compatible form, major differences are listed below.
-
-Status: Flower is currently usable, but there are still plenty of planned
- features. See the doc/TODO.txt for a list of things I'm planning.
-
-= Requirements:
-
- - Exemel, the XML library that powers Flower.
+=== Flower: XML Application Languages ===
+
+Flower is a library for building and using XML Application Languages.
+An XML Application Language can be anything that takes an XML document,
+and parses it using a set of data. It's perfect for creating template engines,
+custom XML syntax, and much more, all using pure XML as its input and output
+formats.
+
+Flower was originally written to create an implementation of the TAL and METAL
+template engines from Zope. Flower's name was originally a play on the Perl 5
+Petal library. Since then, Flower has outgrown its original scope, bringing
+in further ideas from Template::TAL, PHPTAL, and some of my own custom XML
+concepts. The original TAL/METAL/TALES parsers are still included, and can
+be easily used by using Flower::TAL, which is included (see below.)
+
+= Requirements =
+
+ - Rakudo Perl 6
+ http://rakudo.org/
+ NOTE: Unlike most of my libraries, Exemel and Flower only work on the
+ "ng" branch of Rakudo, and not the "nom" branch. Once grammars/regexes are
+ working fully in "nom", I'll make sure both libraries work on "nom" again.
+
+ - Exemel
+ The XML library that powers Flower.
http://github.com/supernovus/exemel/
= Optional libraries:
- - Temporal-Utils, needed if you want to use Flower::Utils::Date
+ - DateTime::Utils
+ Needed if you want to use Flower::TAL::TALES::Date
+ which is included in Flower::TAL's list of plugins.
http://github.com/supernovus/temporal-utils/
-= Major Differences from Petal:
+=== Flower::TAL ===
+
+This is an easily loadable library that extends Flower and automatically
+loads the Flower::TAL::TAL, Flower::TAL::METAL application languages
+by default, and offers the ability to easily load plugins for the
+Flower::TAL::TALES attribute parser (used by Flower::TAL::TAL)
+
+= Major Differences from Petal =
* The default local namespace is 'tal', and to override it, you must
declare http://xml.zope.org/namespaces/tal instead of the Petal namespace.
@@ -38,7 +49,7 @@ Status: Flower is currently usable, but there are still plenty of planned
* There is NO support for anything but well-formed XML.
There is no equivelant to the Petal::Parser::HTB, and no plans for one.
Use well-formed XML, it's just better.
- * Flower supports petal:block elements, as per the PHPTAL project.
+ * Flower supports tal:block elements, as per the PHPTAL project.
* While you can use the 'structure' keyword, it's not really needed.
If you want an unescaped XML structure for a replacement, send an
Exemel object (any class other than Document) and it will be added to
@@ -64,11 +75,11 @@ The above list will be updated as this project is developed, as I'm sure
other changes will be introduced that will be a gotchya for users of Petal,
Zope or PHPTAL.
-= Flower::Utils
+= Flower::TAL Plugins =
-Inspired by Petal::Utils, Flower includes a namespace called Flower::Utils::
-If you load modules in the Petal::Utils:: namespace, it will add additional
-modifiers. The following sets are currently included with Flower:
+Inspired by Petal::Utils, Flower includes a bunch of libraries in the
+Flower::TAL::TALES:: namespace. These are also available using Flower::TAL's
+add-tales() method.
- Text, same as the :text set from Petal::Utils
lc: make the string lowercase.
@@ -90,10 +101,6 @@ modifiers. The following sets are currently included with Flower:
strftime: Displays a date/time string in a specified format.
rfc: A modifier specifically for use with strftime, as the format.
now: A modifier specifically for use with strftime, as the object.
- - Perl, makes a 'perl:' modifier available, which is the counterpart
- to Zope's 'python:' or PHPTAL's 'php:' modifiers.
- There are some extra, more dangerous modifiers included, but they
- are not exported by default, and must be imported manually.
- Debug, similar to :debug set from Petal::Utils.
dump: modifier spits out the .perl representation of the object.
what: modifier spits out the class name of the object.
@@ -103,14 +110,14 @@ In addition, the following sets are planned for inclusion very soon:
- Logic, similar to the :logic set from Petal::Utils
- Hash, same as the :hash set from Petal::Utils
-The syntax for the Flower::Utils methods is based on the modifiers from
+The syntax for the Flower::TAL plugins is based on the modifiers from
Petal::Utils, but extended to use the Flower-specific extensions (the
same method that is used to parse method call parameters in queries.)
As is obvious, the syntax is not always the same, and not all of the
modifiers are the same.
Full documentation for the usage of Flower and the Flower::Utils modifiers
-will be included in the docs/ folder in an upcoming release, until then
+will be included in the doc/ folder in an upcoming release, until then
there are comments in the libraries, and test files in t/ that show the
proper usage.
@@ -118,7 +125,14 @@ The URI set from Petal::Utils is not planned for inclusion,
feel free to write it if you need it.
I'm sure new exciting libraries will be made adding onto these.
-= Author: Timothy Totten
-= License: Artistic License 2.0
- http://www.perlfoundation.org/artistic_license_2_0
+=== Author ===
+
+Timothy Totten
+http://huri.net/
+http://github.com/supernovus/
+
+=== License ===
+
+Artistic License 2.0
+http://www.perlfoundation.org/artistic_license_2_0
View
2  deps.proto
@@ -1,2 +0,0 @@
-exemel
-temporal-utils
View
19 doc/TODO.txt
@@ -1,22 +1,7 @@
Things yet to do (in the order they will likely be done in):
- - Massive rewrite! The original Flower was sort of based
- on Petal from Perl 5. Sort of, but not really.
- Since then, it's diverged quite significantly.
- However, I have also started my own fork of the Perl 5
- Template::TAL library, and really like the module way
- that it is structured. I intend to completely restructure
- Flower to be more modular like Template::TAL. The various
- XML sublangs will be separated out into their own libraries,
- the same way that 'modifiers' are done now.
- The exact implementation details are not quite settled upon yet.
- This will make Flower into an XML extension and manipulation framework
- rather than just an implementation of TAL/METAL/TALES,
- although those will still be the default sub-langs.
-
- - Finish implementing Flower::Utils, as per the README.
- Although they will no longer by in that namespace, as they will be
- using the new framework described above.
+ - Clean up the alpha-isms that are still hanging about.
+ - Finish implementing Flower::TAL::TALES::* plugins, as per the README.
- Implement query caching (it will be optional)
- Implement multiple paths (/test/path1 | /test/path2) support.
- Implement on-error.
View
627 lib/Flower.pm
@@ -1,627 +0,0 @@
-class Flower;
-
-use Exemel;
-use Flower::DefaultModifiers;
-
-has $.template;
-
-has $!tal is rw = 'tal';
-has $!metal is rw = 'metal';
-has $!i18n is rw = 'i18n';
-
-has $!tal-ns = 'http://xml.zope.org/namespaces/tal';
-has $!metal-ns = 'http://xml.zope.org/namespaces/metal';
-has $!i18n-ns = 'http://xml.zope.org/namespaces/i18n';
-
-## a restricted set of tags for the root element.
-has @!root-tal-tags = 'define', 'attributes', 'content';
-
-## the normal set of TAL tags for every other element.
-has @!tal-tags = 'define', 'condition', 'repeat', 'attributes', 'content', 'replace', 'omit-tag';
-
-## the tags for METAL macro processing.
-## -- define-slot and use-slot are handled by use-macro.
-has @!metal-tags = 'define-macro', 'use-macro';
-
-## The cache for METAL macros. Is available for modifiers.
-has %.metal is rw;
-
-## The cache for included XML templates. Is available for modifiers.
-has %.file is rw;
-
-## Override find with a subroutine that can find templates based off
-## of whatever your needs are (multiple roots, extensions, etc.)
-has $!find;
-
-## Modifiers, keys are strings, values are subroutines.
-has %!modifiers;
-
-## Data, is used to store the replacement data. Is available for modifiers.
-has %.data is rw;
-
-## Default stores the elements in order of parsing.
-## Used to get the 'default' value, and other such stuff.
-has @.elements;
-
-## Internal class, used for the 'repeat' object.
-class Flower::Repeat {
- has $.index;
- has $.length;
-
- method number { $.index + 1 }
- method start { $.index == 0 }
- method end { $.index == $.length-1 }
- method odd { $.number % 2 != 0 }
- method even { $.number % 2 == 0 }
-
- method inner { $.index != 0 && $.index != $.length-1 }
-
- ## Flower exclusive methods below here, make lists and tables easier.
- method every ($num) { $.number % $num == 0 }
- method skip ($num) { $.number % $num != 0 }
- method lt ($num) { $.number < $num }
- method gt ($num) { $.number > $num }
- method eq ($num) { $.number == $num }
- method ne ($num) { $.number != $num }
-
- ## Versions of every and skip that also match on start.
- method repeat-every ($num) { $.start || $.every($num) }
- method repeat-skip ($num) { $.start || $.every($num) }
-}
-
-method new (:$find is copy, :$file, :$template is copy) {
- if ( ! $file && ! $template ) { die "a file or template must be specified."; }
- if ! $find {
- $find = sub ($file) {
- if $file.IO ~~ :f { return $file; }
- }
- }
- if $file {
- my $filename;
- if ($file.IO ~~ :f) { $filename = $file; }
- else { $filename = $find($file); }
- $template = Exemel::Document.parse(slurp($filename));
- }
- else {
- if $template ~~ Exemel::Document {
- 1; # we don't need to do anything.
- }
- elsif $template ~~ Exemel::Element {
- $template = Exemel::Document.new(:root($template));
- }
- elsif $template ~~ Str {
- $template = Exemel::Document.parse($template);
- }
- else {
- die "invalid template type passed.";
- }
- }
- my %modifiers = Flower::DefaultModifiers::export();
- self.bless(*, :$template, :$find, :%modifiers);
-}
-
-## A way to spawn another Flower using the same find and modifiers.
-method another (:$file, :$template, :$cache=True) {
- if ( ! $file && ! $template ) { die "a file or template must be specified."; }
- my $new = Flower.new(:$file, :$template, :find($!find));
- $new.add-modifiers(%!modifiers);
- ## Add caches, for extra speedups.
- if ($cache) {
- $new.file = %.file;
- $new.metal = %.metal;
- }
- return $new;
-}
-
-## Loading more XML documents.
-## Now caches results, for easy re-use.
-method load-xml-file ($filename) {
- if %.file.exists($filename) {
- return %.file{$filename};
- }
-
- my $file = $!find($filename);
- if ($file) {
- my $xml = Exemel::Document.parse(slurp($file));
- %.file{$filename} = $xml;
- return $xml;
- }
-}
-
-method !xml-ns ($ns) {
- return $ns.subst(/^xmlns\:/, '');
-}
-
-## Note: Once you have parsed, the template will forever be changed.
-## You can't parse twice, so don't fark it up!
-## You can access the template Exemel::Document object by using the
-## $flower.template attribute.
-method parse (*%data) {
- ## First we need to set the data.
- %.data = %data;
-
- ## Next, let's see if the namespaces have been renamed.
- for $.template.root.attribs.kv -> $key, $val {
- if $val eq $!tal-ns {
- $!tal = self!xml-ns($key);
- }
- elsif $val eq $!metal-ns {
- $!metal = self!xml-ns($key);
- }
- elsif $val eq $!i18n-ns {
- $!i18n = self!xml-ns($key);
- }
- }
-
- ## Okay, now let's parse the elements.
- self!parse-element($.template.root, @!root-tal-tags, False);
- return ~$.template;
-}
-
-## parse-elements, parses TAL and METAL.
-## I18N may be added at some point in the future.
-## also TODO: implement 'on-error'.
-method !parse-elements ($xml is rw, $custom-parser?) {
- ## Due to the strange nature of some rules, we're not using the
- ## 'elements' helper, nor using a nice 'for' loop. Instead we're doing this
- ## by hand. Don't worry, it'll all make sense.
- loop (my $i=0; True; $i++) {
- if $i == $xml.nodes.elems { last; }
- my $element = $xml.nodes[$i];
- if $element !~~ Exemel::Element { next; } # skip non-elements.
- @.elements.unshift: $element; ## Stuff the newest element into place.
- if ($custom-parser) {
- $custom-parser($element, $custom-parser);
- }
- else {
- self!parse-element($element);
- }
- @.elements.shift; ## and remove it again.
- ## Now we clean up removed elements, and insert replacements.
- if ! defined $element {
- $xml.nodes.splice($i--, 1);
- }
- elsif $element ~~ Array {
- $xml.nodes.splice($i--, 1, |@($element));
- }
- else {
- $xml.nodes[$i] = $element; # Ensure the node is updated.
- }
- }
-}
-
-method !parse-element($element is rw, @tal-tags = @!tal-tags, $dometal=True) {
-## First we parse METAL tags, as long as $dometal is true.
- if ($dometal) {
- for @!metal-tags -> $metal {
- my $tag = $!metal~':'~$metal;
- self!parse-tag($element, $tag, $metal);
- }
- }
-## Now we parse TAL tags.
- for @tal-tags -> $tal {
- my $tag = $!tal~':'~$tal;
- self!parse-tag($element, $tag, $tal);
- if $element !~~ Exemel::Element { last; } # skip if we changed type.
- }
-## tal:block borrowed from PHPTAL.
- if ($element ~~ Exemel::Element && $element.name eq $!tal~':block') {
- $element = $element.nodes;
- }
-## Now let's parse any child elements.
- if $element ~~ Exemel::Element {
- self!parse-elements($element);
- }
-}
-
-method !parse-tag ($element is rw, $tag, $ns) {
- my $method = 'parse-'~$ns;
- if $element.attribs.exists($tag) {
- self!"$method"($element, $tag);
- }
-}
-
-method !parse-define ($xml is rw, $tag) {
- my @statements = $xml.attribs{$tag}.split(/\;\s+/);
- for @statements -> $statement {
- my ($attrib, $query) = $statement.split(/\s+/, 2);
- my $val = self.query($query);
- if defined $val { %.data{$attrib} = $val; }
- }
- $xml.unset($tag);
-}
-
-method !parse-condition ($xml is rw, $tag) {
- if self.query($xml.attribs{$tag}, :bool) {
- $xml.unset($tag);
- } else {
- $xml = Nil;
- }
-}
-
-method !parse-content ($xml is rw, $tag) {
- my $node = self.query($xml.attribs{$tag}, :forcexml);
- if defined $node {
- if $node === $xml.nodes {} # special case for 'default'.
- else {
- $xml.nodes.splice;
- $xml.nodes.push: $node;
- }
- }
- $xml.unset: $tag;
-}
-
-method !parse-replace ($xml is rw, $tag) {
- my $text = $xml.attribs{$tag};
- if defined $text {
- $xml = self.query($text, :forcexml);
- }
- else {
- $xml = Nil;
- }
-}
-
-method !parse-attributes ($xml is rw, $tag) {
- my @statements = $xml.attribs{$tag}.split(/\;\s+/);
- for @statements -> $statement {
- my ($attrib, $query) = $statement.split(/\s+/, 2);
- my $val = self.query($query, :noxml);
- if defined $val {
- $xml.set($attrib, $val);
- }
- }
- $xml.unset: $tag;
-}
-
-method !parse-repeat ($xml is rw, $tag) {
- my ($attrib, $query) = $xml.attribs{$tag}.split(/\s+/, 2);
- my $array = self.query($query);
- if (defined $array && $array ~~ Array) {
- if (! %.data.exists('repeat') || %.data<repeat> !~~ Hash) {
- %.data<repeat> = {}; # Initialize the repeat hash.
- }
- $xml.unset($tag);
- my @elements;
- my $count = 0;
- for @($array) -> $item {
- my $newxml = $xml.deep-clone;
- %.data{$attrib} = $item;
- my $repeat = Flower::Repeat.new(:index($count), :length($array.elems));
- %.data<repeat>{$attrib} = $repeat;
- my $wrapper = Exemel::Element.new(:nodes(($newxml)));
- self!parse-elements($wrapper);
- @elements.push: @($wrapper.nodes);
- $count++;
- }
- %.data<repeat>.delete($attrib);
- %.data.delete($attrib);
- $xml = @elements;
- }
- else {
- $xml = Nil;
- }
-}
-
-method !parse-define-macro ($xml is rw, $tag) {
- my $macro = $xml.attribs{$tag};
- $xml.unset: $tag;
- my $section = $xml.deep-clone;
- %!metal{$macro} = $section;
- #say "## Saved macro '$macro': $section";
-}
-
-method !parse-use-macro ($xml is rw, $tag) {
- my $macro = $xml.attribs{$tag};
- my $fillslot = $!metal~':fill-slot';
- my %params = {
- :RECURSE(10),
- $fillslot => True,
- };
- my @slots = $xml.elements(|%params);
- my $found = False;
- if %!metal.exists($macro) {
- $xml = %!metal{$macro}.deep-clone;
- $found = True;
- }
- else {
- my @ns = $macro.split('#', 2);
- my $file = @ns[0];
- my $section = @ns[1];
- my $include = self.load-xml-file($file);
- if ($include) {
- my $defmacro = $!metal~':define-macro';
- my %search = {
- :RECURSE(10),
- $defmacro => $section,
- };
- my @macros = $include.root.elements(|%search);
- if (@macros.elems > 0) {
- $xml = @macros[0].deep-clone;
- $xml.unset: $defmacro;
- %!metal{$macro} = $xml.deep-clone;
- $found = True;
- }
- }
- }
- if ($found) {
- my $parser = -> $element is rw, $me {
- self!parse-use-macro-slots(@slots, $element, $me);
- };
- self!parse-elements($xml, $parser);
- }
- else {
- $xml.unset: $tag;
- for @slots -> $slot {
- $slot.unset: $fillslot;
- }
- }
-}
-
-method !parse-use-macro-slots (@slots, $xml is rw, $parser) {
- my $defslot = $!metal~':define-slot';
- my $fillslot = $!metal~':fill-slot';
- if $xml.attribs.exists($defslot) {
- my $slotid = $xml.attribs{$defslot};
- $xml.unset: $defslot;
- for @slots -> $slot {
- if $slot.attribs{$fillslot} eq $slotid {
- $xml = $slot.deep-clone;
- $xml.unset: $fillslot;
- last;
- }
- }
- }
- ## Now let's parse any child elements.
- if $xml ~~ Exemel::Element {
- self!parse-elements($xml, $parser);
- }
-}
-
-method !parse-omit-tag ($xml is rw, $tag) {
- my $nodes = $xml.nodes;
- my $query = $xml.attribs{$tag};
- if self.query($query, :bool) {
- $xml = $nodes;
- }
- else {
- $xml.unset: $tag;
- }
-}
-
-## Query data.
-method query ($query is copy, :$noxml, :$forcexml, :$bool, :$noescape is copy) {
- if $query eq '' {
- if ($bool) { return True; }
- else { return ''; }
- }
- if $query eq 'nothing' {
- if ($bool) { return False; }
- else { return ''; }
- }
- if $query eq 'default' {
- my $default = @.elements[0].nodes;
- return $default;
- }
- if $query ~~ /^ structure \s+ / {
- $query.=subst(/^ structure \s+ /, '');
- $noescape = True;
- }
- if $query ~~ /^\'(.*?)\'$/ {
- return self.process-query(~$0, :$forcexml, :$noxml, :$noescape);
- } # quoted string, no interpolation.
- if $query ~~ /^<.ident>+\:/ {
- my ($handler, $subquery) = $query.split(/\:\s*/, 2);
- if %!modifiers.exists($handler) {
- ## Modifiers are responsible for subqueries, and calls to process-query.
- return %!modifiers{$handler}(self, $subquery, :$noxml, :$forcexml, :$bool, :$noescape);
- }
- }
- my @paths = $query.split('/');
- my $data = self!lookup(@paths, %.data);
- return self.process-query($data, :$forcexml, :$noxml, :$noescape);
-}
-
-## Enforce processing rules for query().
-method process-query($data is copy, :$forcexml, :$noxml, :$noescape, :$bool) {
- ## First off, let's escape text, unless noescape is set.
- if (!defined $noescape && $data ~~ Str) {
- $data.=subst('&', '&amp;', :g);
- $data.=subst('<', '&lt;', :g);
- $data.=subst('>', '&gt;', :g);
- $data.=subst('"', '&quot;', :g);
- }
- ## Default rule for forcexml converts non-XML objects into Exemel::Text.
- if ($forcexml) {
- if ($data ~~ Array) {
- for @($data) -> $elm is rw {
- if $elm !~~ Exemel { $elm = Exemel::Text.new(:text(~$elm)); }
- }
- return $data;
- }
- elsif ($data !~~ Exemel) {
- return Exemel::Text.new(:text(~$data));
- }
- }
- elsif ($noxml && $data !~~ Str|Numeric) {
- return; ## With noxml set, we only accept Strings or Numbers.
- }
- return $data;
-}
-
-## get-args now supports parameters in the form of {{param name}} for
-## when you have nested queries with spaces in them that shouldn't be treated
-## as strings, like 'a string' does. It also captures ${vars} and does no
-## processing on them unless you are using string processing (see below.)
-## It also supports named parameters in the form of :param(value).
-## If the :query option is set, all found parameters will be looked up using
-## the query() method (with default options.)
-## If :query is set to a Hash, then the keys of the Hash represent positional
-## parameters (the first positional parameter is 0 not 1.)
-## the value represents an action to take, if it is 0, then no querying or
-## parsing is done on the value. If it is 1, then the value is parsed as a
-## string with any ${name} variables queried.
-## If there is a key called .STRING in the query Hash, then parsing as
-## strings becomes default, and keys with a value of 1 parse as normal queries.
-## so :query({0=>0, 3=>0}) would query all parameters except the 1st and 4th.
-## If you specify the :named option, it will always include the %named
-## parameter, even if it's empty.
-method get-args($string, :$query, :$named, *@defaults) {
- my @result =
- $string.comb(/ [ '{{'.*?'}}' | '${'.*?'}' | '$('.*?')' | ':'\w+'('.*?')' | \'.*?\' | \S+ ] /);
- @result>>.=subst(/^'{{'/, '');
- @result>>.=subst(/'}}'$/, '');
- @result>>.=subst(:g, /'$('(.*?)')'/, -> $/ { '${'~$0~'}' });
- my %named;
- ## Our nice for loop has been replaced now that we support named
- ## parameters. Oh well, such is life.
- loop (my $i=0; $i < @result.elems; $i++) {
- my $param = @result[$i];
- if $param ~~ /^ ':' (\w+) '(' (.*?) ')' $/ {
- my $key = ~$0;
- my $val = ~$1;
- if $query { $val = self!parse-rules($query, $key, $val); }
- %named{$key} = $val;
- @result.splice($i, 1);
- if $i < @result.elems {
- $i--;
- }
- }
- else {
- if $query { @result[$i] = self!parse-rules($query, $i, $param); }
- }
- }
-
- my $results = @result.elems - 1;
- my $defs = @defaults.elems;
-
- if $results < $defs {
- @result.push: @defaults[$results..$defs-1];
- }
- ## Named params are always last.
- if ($named || (%named.elems > 0)) {
- @result.push: %named;
- }
- return @result;
-}
-
-method !parse-rules ($rules, $tag, $value) {
- my $stringy = False;
- if $rules ~~ Hash && $rules.exists('.STRING') {
- $stringy = True;
- }
- if $rules ~~ Hash && $rules.exists($tag) {
- if $rules{$tag} {
- if $stringy {
- return self.query($value);
- }
- else {
- return self.parse-string($value);
- }
- }
- else {
- return $value;
- }
- }
- else {
- if $stringy {
- return self.parse-string($value);
- }
- else {
- return self.query($value);
- }
- }
-}
-
-method parse-string ($string) {
- $string.subst(:g, rx/'${' (.*?) '}'/, -> $/ { self.query($0) });
-}
-
-## This handles the lookups for query().
-method !lookup (@paths is copy, $data) {
- my $path = @paths.shift;
- my $found;
- given $data {
- when Hash {
- if $data.exists($path) {
- $found = .{$path};
- }
- }
- when Array {
- if $path < .elems {
- $found = .[$path];
- }
- }
- default {
- my ($command, *@args) = self.get-args(:query({0=>0}), $path);
- if .can($command) {
- $found = ."$command"(|@args);
- }
- else {
- warn "attempt to access an invalid item '$path'.";
- }
- }
- }
- if @paths {
- return self!lookup(@paths, $found);
- }
- return $found;
-}
-
-## Add a single modifier routine.
-## Example:
-## use Flower;
-## use Flower::Utils::Perl;
-## my $flower = Flower.new(:file('template.xml'));
-## $flower.add-modifier('evil', &Flower::Utils::Perl::perl_execute);
-##
-method add-modifier($name, Callable $routine) {
- %!modifiers{$name} = $routine;
-}
-
-## Add a bunch of modifiers, mainly used for plugin libraries.
-## Expects a Hash, where the key is the name of the modifier, and the
-## value is a subroutine reference.
-## Example:
-## use Flower;
-## use Flower::Utils::Logic;
-## use Flower::Utils::List;
-## my %mods = Flower::Utils::Logic::export();
-## %mods<sort> = &Flower::Utils::List::list_sort;
-## my $flower = Flower.new(:file('template.xml'));
-## $flower.add-modifiers(%mods);
-##
-## Now $flower has all Logic modifiers, plus 'sort'.
-##
-method add-modifiers(%modifiers) {
- for %modifiers.kv -> $key, $val {
- self.add-modifier($key, $val);
- }
-}
-
-## The newest method for loading modifiers.
-## Pass it a list of libraries which have export() subs
-## and it will load them dynamically. The export() sub must
-## return a Hash usable by the add-modifiers() method.
-## If the library name doesn't have a :: in it,
-## load-modifiers prepends "Flower::Utils::" to it.
-## Example:
-## use Flower;
-## my $flower = Flower.new(:file('template.xml'));
-## $flower.load-modifiers('Text', 'Date', 'My::Modifiers');
-##
-## Example will load File::Utils::Text, File::Utils::Date and My::Modifiers.
-##
-method load-modifiers(*@modules) {
- for @modules -> $module {
- my $plugin = $module;
- if $plugin !~~ /'::'/ {
- $plugin = "Flower::Utils::$plugin";
- }
- eval("use $plugin");
- if defined $! { die "use failed: $!"; }
- my $modifiers = eval($plugin~'::export()');
- if defined $! { die "export failed: $!"; }
- self.add-modifiers($modifiers);
- }
-}
-
View
191 lib/Flower.pm6
@@ -0,0 +1,191 @@
+class Flower;
+
+use Exemel;
+
+## Override find with a subroutine that can find templates based off
+## of whatever your needs are (multiple roots, extensions, etc.)
+has $.find = sub ($file) {
+ if $file.IO ~~ :f { return $file }
+}
+
+## Data, is used to store the replacement data. Is available for modifiers.
+has %.data is rw;
+
+## Default stores the elements in order of parsing.
+## Used to get the 'default' value, and other such stuff.
+has @.elements;
+
+## The XML application languages we support.
+has @.plugins;
+
+## Add an XML application language plugin.
+method add-plugin ($plugin) {
+ my $object = self!get-plugin($plugin);
+ if $object.defined {
+ @.plugins.push: $object;
+ }
+}
+
+## Add an XML application language plugin, to the beginning of our list.
+method insert-plugin ($plugin) {
+ my $object = self!get-plugin($plugin);
+ if $object.defined {
+ @.plugins.unshift: $object;
+ }
+}
+
+## Return an object instance representing a plugin.
+## Can take an object instance or a type object.
+method !get-plugin ($plugin) {
+ my $object = $plugin;
+ if ! $plugin.defined {
+ $object = $plugin.new(:flower(self));
+ }
+ return $object;
+}
+
+## The main method to parse a template. Expects an Exemel::Document.
+multi method parse (Exemel::Document $template, *%data) {
+ ## First we need to set the data, for later re-use.
+ %.data = %data;
+
+ ## Let's see if the namespaces has been renamed.
+ my %rootattrs;
+ for $template.root.attribs.kv -> $key, $val {
+ %rootattrs{$val} = $key; ## Yeah, we're reversing it.
+ }
+ for @.plugins -> $plugin {
+ if %rootattrs.exists($plugin.ns) {
+ my $tag = %rootattrs{$plugin.ns};
+ $tag ~~ s/^xmlns\://;
+ $plugin.tag = $tag;
+ }
+ }
+
+ ## Okay, now let's parse the elements.
+ self.parse-element($template.root, :safe);
+ return $template;
+}
+
+## Parse a template in Exemel::Element form.
+multi method parse (Exemel::Element $template, *%data) {
+ my $document = Exemel::Document.new(:root($template));
+ return self.parse($document, |%data);
+}
+
+## Parse a template that is passed as XML text.
+multi method parse (Stringy $template, *%data) {
+ my $document = Exemel::Document.parse($template);
+ if ($document) {
+ return self.parse($document, |%data);
+ }
+}
+
+## Parse a template using a filename. The filename is passed to find().
+method parse-file ($filename, *%data) {
+ my $file = $.find($filename);
+ if $file {
+ my $template = Exemel::Document.parse(slurp($file));
+ if $template {
+ return self.parse($template, |%data);
+ }
+ }
+}
+
+## parse-elements: Parse the child elements of an XML node.
+method parse-elements ($xml is rw, $custom-parser?) {
+ ## Due to the strange nature of some rules, we're not using the
+ ## 'elements' helper, nor using a nice 'for' loop. Instead we're doing this
+ ## by hand. Don't worry, it'll all make sense.
+ loop (my $i=0; True; $i++) {
+ if $i == $xml.nodes.elems { last; }
+ my $element = $xml.nodes[$i];
+ if $element !~~ Exemel::Element { next; } # skip non-elements.
+ @.elements.unshift: $element; ## Stuff the newest element into place.
+ if ($custom-parser) {
+ $custom-parser($element, $custom-parser);
+ }
+ else {
+ self.parse-element($element);
+ }
+ @.elements.shift; ## and remove it again.
+ ## Now we clean up removed elements, and insert replacements.
+ if ! defined $element {
+ $xml.nodes.splice($i--, 1);
+ }
+ elsif $element ~~ Array {
+ $xml.nodes.splice($i--, 1, |@($element));
+ }
+ else {
+ $xml.nodes[$i] = $element; # Ensure the node is updated.
+ }
+ }
+}
+
+## parse-element: parse a single element.
+method parse-element($element is rw, :$safe) {
+ ## Let's do this.
+ for @.plugins -> $plugin {
+ ## First attributes.
+ my $defel = False; ## By default we handle XML Attributes, not Elements.
+ if $plugin.options.exists('element') {
+ $defel = $plugin.options<element>;
+ }
+ for $plugin.handlers -> $hand {
+ my $name; ## Name of the attribute or element.
+ my $meth; ## Method to call.
+ my $issafe = False; ## Is it safe? Only used if safe mode is in place.
+ my $isel = $defel; ## Is this an element instead of an attribute?
+ if $hand ~~ Pair {
+ $name = $hand.key;
+ my $rules = $hand.value;
+ if $rules ~~ Hash {
+ if $rules.exists('method') {
+ $meth = $rules<method>;
+ }
+ if $rules.exists('safe') {
+ $issafe = $rules<safe>;
+ }
+ if $rules.exists('element') {
+ $isel = $rules<element>;
+ }
+ }
+ elsif $rules ~~ Str {
+ ## If the pair value is a string, it's the method name.
+ $meth = $rules;
+ }
+ }
+ elsif $hand ~~ Str {
+ ## If the handler is a string, it's the name of the attribute/element.
+ $name = $hand;
+ }
+ if ! $meth {
+ ## If no method has been found by other means, the default is
+ ## parse-$name(). E.g. for a name of 'block', we'd call parse-block().
+ $meth = "parse-$name";
+ }
+ if $safe && !$issafe {
+ next; ## Skip unsafe handlers.
+ }
+ if ! $meth { next; } ## Undefined method, we can't handle that.
+ my $fullname = $plugin.tag ~ ':' ~ $name;
+ if $isel {
+ if $element.name eq $fullname {
+ $plugin."$meth"($element, $fullname);
+ }
+ }
+ else {
+ if $element.attribs.exists($fullname) {
+ $plugin."$meth"($element, $fullname);
+ }
+ }
+ if $element !~~ Exemel::Element { last; } ## skip if we changed type.
+ } ## /for $plugin.handlers
+ } ## /for @.plugins
+
+ ## Okay, now we parse child elements.
+ if $element ~~ Exemel::Element {
+ self.parse-elements($element);
+ }
+}
+
View
30 lib/Flower/DefaultModifiers.pm
@@ -1,30 +0,0 @@
-module Flower::DefaultModifiers;
-
-our sub export() {
- my %modifiers = {
- str => &string,
- string => &string,
- is => &true,
- true => &true,
- false => &false,
- 'not' => &false,
- };
- return %modifiers;
-}
-
-our sub true ($parent, $query, *%opts) {
- my $result = $parent.query($query, :bool);
- return ?$result;
-}
-
-our sub false ($parent, $query, *%opts) {
- my $result = $parent.query($query, :bool);
- if $result { return False; }
- else { return True; }
-}
-
-our sub string ($parent, $query, *%opts) {
- my $string = $parent.parse-string($query);
- return $parent.process-query($string, |%opts);
-}
-
View
116 lib/Flower/TAL/METAL.pm6
@@ -0,0 +1,116 @@
+class Flower::TAL::METAL; ## The METAL XML Application Language.
+
+use Exemel;
+
+has $.flower;
+has $.tag is rw = 'metal';
+has $.ns = 'http://xml.zope.org/namespaces/metal';
+
+## The tags for METAL macro processing.
+## -- define-slot and use-slot are handled by parse_use().
+has @.handlers =
+ 'define-macro' => 'parse-define',
+ 'use-macro' => 'parse-use';
+
+## Needed by the spec, but unused here.
+has %.options;
+
+## The cache for METAL macros.
+has %.metal is rw;
+
+## The cache for included XML templates.
+has %.file is rw;
+
+## Loading more XML documents.
+## Now caches results, for easy re-use.
+method load-xml-file ($filename) {
+ if %.file.exists($filename) {
+ return %.file{$filename};
+ }
+
+ my $file = $.flower.find($filename);
+ if ($file) {
+ my $xml = Exemel::Document.parse(slurp($file));
+ %.file{$filename} = $xml;
+ return $xml;
+ }
+}
+
+## Now the handlers.
+
+method parse-define ($xml is rw, $tag) {
+ my $macro = $xml.attribs{$tag};
+ $xml.unset: $tag;
+ my $section = $xml.deep-clone;
+ %!metal{$macro} = $section;
+ #say "## Saved macro '$macro': $section";
+}
+
+method parse-use ($xml is rw, $tag) {
+ my $macro = $xml.attribs{$tag};
+ my $fillslot = $.tag~':fill-slot';
+ my %params = {
+ :RECURSE(10),
+ $fillslot => True,
+ };
+ my @slots = $xml.elements(|%params);
+ my $found = False;
+ if %!metal.exists($macro) {
+ $xml = %!metal{$macro}.deep-clone;
+ $found = True;
+ }
+ else {
+ my @ns = $macro.split('#', 2);
+ my $file = @ns[0];
+ my $section = @ns[1];
+ my $include = self.load-xml-file($file);
+ if ($include) {
+ my $defmacro = $.tag~':define-macro';
+ my %search = {
+ :RECURSE(10),
+ $defmacro => $section,
+ };
+ my @macros = $include.root.elements(|%search);
+ if (@macros.elems > 0) {
+ $xml = @macros[0].deep-clone;
+ $xml.unset: $defmacro;
+ %!metal{$macro} = $xml.deep-clone;
+ $found = True;
+ }
+ }
+ }
+ if ($found) {
+ my $metal = self;
+ my $parser = -> $element is rw, $me {
+ $metal.use-macro-slots(@slots, $element, $me);
+ };
+ $.flower.parse-elements($xml, $parser);
+ }
+ else {
+ $xml.unset: $tag;
+ for @slots -> $slot {
+ $slot.unset: $fillslot;
+ }
+ }
+}
+
+method use-macro-slots (@slots, $xml is rw, $parser) {
+ my $defslot = $.tag~':define-slot';
+ my $fillslot = $.tag~':fill-slot';
+ if $xml.attribs.exists($defslot) {
+ my $slotid = $xml.attribs{$defslot};
+ $xml.unset: $defslot;
+ for @slots -> $slot {
+ if $slot.attribs{$fillslot} eq $slotid {
+ $xml = $slot.deep-clone;
+ $xml.unset: $fillslot;
+ last;
+ }
+ }
+ }
+ ## Now let's parse any child elements.
+ if $xml ~~ Exemel::Element {
+ $.flower.parse-elements($xml, $parser);
+ }
+}
+
View
27 lib/Flower/TAL/Repeat.pm6
@@ -0,0 +1,27 @@
+class Flower::TAL::Repeat;
+
+## Represents a TAL Repeat object.
+
+has $.index;
+has $.length;
+
+method number { $.index + 1 }
+method start { $.index == 0 }
+method end { $.index == $.length-1 }
+method odd { $.number % 2 != 0 }
+method even { $.number % 2 == 0 }
+
+method inner { $.index != 0 && $.index != $.length-1 }
+
+## Flower exclusive methods below here, make lists and tables easier.
+method every ($num) { $.number % $num == 0 }
+method skip ($num) { $.number % $num != 0 }
+method lt ($num) { $.number < $num }
+method gt ($num) { $.number > $num }
+method eq ($num) { $.number == $num }
+method ne ($num) { $.number != $num }
+
+## Versions of every and skip that also match on start.
+method repeat-every ($num) { $.start || $.every($num) }
+method repeat-skip ($num) { $.start || $.every($num) }
+
View
132 lib/Flower/TAL/TAL.pm6
@@ -0,0 +1,132 @@
+class Flower::TAL::TAL; ## The TAL XML Application Language
+
+use Exemel;
+
+use Flower::TAL::TALES; ## The TALES attribute sub-language.
+use Flower::TAL::Repeat; ## A class representing our repeat object.
+
+has $.flower;
+has $.tag is rw = 'tal';
+has $.ns = 'http://xml.zope.org/namespaces/tal';
+
+## The full set of TAL attributes.
+## Ones marked 'safe' can be used if the :safe rule is passed.
+has @.handlers =
+ 'define' => { :safe },
+ 'condition',
+ 'repeat',
+ 'attributes' => { :method<parse-attrs>, :safe },
+ 'attrs' => { :safe }, ## non-standard extension for lazy people.
+ 'content' => { :safe },
+ 'replace',
+ 'omit-tag' => 'parse-omit',
+ 'block' => { :element }; ## lazy <tal:block> extension from PHPTAL.
+
+## Needed by the spec, but unused here.
+has %.options;
+
+has $.tales;
+
+our submethod BUILD (:$flower) {
+ $!flower = $flower;
+ $!tales = Flower::TAL::TALES.new(:parent(self));
+}
+
+## This is super simple, as a <tal:block> acts the
+## same as a normal element with a tal:omit-tag="" rule.
+method parse_block ($element is rw, $name) {
+ $element = $element.nodes;
+}
+
+method parse-define ($xml is rw, $tag) {
+ my @statements = $xml.attribs{$tag}.split(/\;\s+/);
+ for @statements -> $statement {
+ my ($attrib, $query) = $statement.split(/\s+/, 2);
+ my $val = self.query($query);
+ if defined $val { %.data{$attrib} = $val; }
+ }
+ $xml.unset($tag);
+}
+
+method parse-condition ($xml is rw, $tag) {
+ if $.tales.query($xml.attribs{$tag}, :bool) {
+ $xml.unset($tag);
+ } else {
+ $xml = Nil;
+ }
+}
+
+method parse-content ($xml is rw, $tag) {
+ my $node = $.tales.query($xml.attribs{$tag}, :forcexml);
+ if defined $node {
+ if $node === $xml.nodes {} # special case for 'default'.
+ else {
+ $xml.nodes.splice;
+ $xml.nodes.push: $node;
+ }
+ }
+ $xml.unset: $tag;
+}
+
+method parse-replace ($xml is rw, $tag) {
+ my $text = $xml.attribs{$tag};
+ if defined $text {
+ $xml = $.tales.query($text, :forcexml);
+ }
+ else {
+ $xml = Nil;
+ }
+}
+
+method parse-attrs ($xml is rw, $tag) {
+ my @statements = $xml.attribs{$tag}.split(/\;\s+/);
+ for @statements -> $statement {
+ my ($attrib, $query) = $statement.split(/\s+/, 2);
+ my $val = $.tales.query($query, :noxml);
+ if defined $val {
+ $xml.set($attrib, $val);
+ }
+ }
+ $xml.unset: $tag;
+}
+
+method parse-repeat ($xml is rw, $tag) {
+ my ($attrib, $query) = $xml.attribs{$tag}.split(/\s+/, 2);
+ my $array = $.tales.query($query);
+ if (defined $array && $array ~~ Array) {
+ if (! %.data.exists('repeat') || %.data<repeat> !~~ Hash) {
+ %.data<repeat> = {}; # Initialize the repeat hash.
+ }
+ $xml.unset($tag);
+ my @elements;
+ my $count = 0;
+ for @($array) -> $item {
+ my $newxml = $xml.deep-clone;
+ %.data{$attrib} = $item;
+ my $repeat = Flower::TAL::Repeat.new(:index($count), :length($array.elems));
+ %.data<repeat>{$attrib} = $repeat;
+ my $wrapper = Exemel::Element.new(:nodes(($newxml)));
+ self.parse-elements($wrapper);
+ @elements.push: @($wrapper.nodes);
+ $count++;
+ }
+ %.data<repeat>.delete($attrib);
+ %.data.delete($attrib);
+ $xml = @elements;
+ }
+ else {
+ $xml = Nil;
+ }
+}
+
+method parse-omit ($xml is rw, $tag) {
+ my $nodes = $xml.nodes;
+ my $query = $xml.attribs{$tag};
+ if $.tales.query($query, :bool) {
+ $xml = $nodes;
+ }
+ else {
+ $xml.unset: $tag;
+ }
+}
+
View
234 lib/Flower/TAL/TALES.pm6
@@ -0,0 +1,234 @@
+class Flower::TAL::TALES; ## Parses TALES strings, used by TAL.
+
+use Exemel;
+use Flower::TAL::TALES::Default; ## The default TALES parsers.
+
+has @!plugins; ## Our private list of plugins. Use add-plugin() to add more.
+has $.parent; ## The XML Lang that called us. Probably Flower::TAL::TAL
+has $.flower; ## The top-most Flower object.
+
+our submethod BUILD (:$parent) {
+ $!parent = $parent;
+ $!flower = $parent.flower;
+ my $default = Flower::TAL::TALES::Default.new(:tales(self), :$.flower);
+ @!plugins = $default;
+}
+
+## Add a TALES plugin to our list.
+## Can take an object instance, a Type object, or a string.
+## If you use a string without a :: separator in it, the
+## prefix Flower::TAL::TALES:: is added to it.
+## Note: due to rakudo bugs, using a string plugin currently has
+## the following limitations:
+## - The plugin's $.flower and $.tales attributes must be "rw".
+## - They must not refer to $.flower or $.tales in any BUILD submethods.
+method add-plugin ($plugin) {
+ my $object = $plugin;
+ if $plugin ~~ Str {
+ my $plugname;
+ if $plugin !~~ /'::'/ { ## If there's no namespace, we add one.
+ $plugname = "Flower::TAL::TALES::$plugin";
+ }
+ else {
+ $plugname = $plugin;
+ }
+ if $plugname eq 'Flower::TAL::TALES::Default' { return; }
+### This doesn't work in rakudo yet.
+# require $plugname;
+# $object = ::($plugname).new(:tales(self), :flower($.flower));
+### So we use the evil workaround instead.
+ eval("use $plugname; \$object = {$plugname}.new;"); ## EVIL!
+ $object.tales = self; ## More evil, $.tales should not be rw.
+ $object.flower = $.flower; ## Yet more evil, $.flower should not be rw.
+ }
+ elsif ! $plugin.defined {
+ $object = $plugin.new(:tales(self), :flower($.flower));
+ }
+ ## Add the plugin.
+ if $object.defined {
+ @!plugins.push: $object;
+ }
+}
+
+## Query data.
+method query ($query is copy, :$noxml, :$forcexml, :$bool, :$noescape is copy) {
+ if $query eq '' {
+ if ($bool) { return True; }
+ else { return ''; }
+ }
+ if $query eq 'nothing' {
+ if ($bool) { return False; }
+ else { return ''; }
+ }
+ if $query eq 'default' {
+ my $default = $.flower.elements[0].nodes;
+ return $default;
+ }
+ if $query ~~ /^ structure \s+ / {
+ $query.=subst(/^ structure \s+ /, '');
+ $noescape = True;
+ }
+ if $query ~~ /^\'(.*?)\'$/ {
+ return self.process-query(~$0, :$forcexml, :$noxml, :$noescape);
+ } # quoted string, no interpolation.
+ if $query ~~ /^<.ident>+\:/ {
+ my ($handler, $subquery) = $query.split(/\:\s*/, 2);
+ for @!plugins -> $plugin {
+ if $plugin.handlers.exists($handler) {
+ my $method = $plugin.handlers{$handler};
+ ## Modifiers are responsible for subqueries and process-query calls.
+ return $plugin."$method"($subquery, :$noxml, :$forcexml, :$bool, :$noescape);
+ }
+ }
+ }
+ my @paths = $query.split('/');
+ my $data = self!lookup(@paths, $.flower.data);
+ return self.process-query($data, :$forcexml, :$noxml, :$noescape);
+}
+
+## Enforce processing rules for query().
+method process-query($data is copy, :$forcexml, :$noxml, :$noescape, :$bool) {
+ ## First off, let's escape text, unless noescape is set.
+ if (!defined $noescape && $data ~~ Str) {
+ $data.=subst(/'&' [<!before \w+ ';'>]/, '&amp;', :g);
+ $data.=subst('<', '&lt;', :g);
+ $data.=subst('>', '&gt;', :g);
+ $data.=subst('"', '&quot;', :g);
+ }
+ ## Default rule for forcexml converts non-XML objects into Exemel::Text.
+ if ($forcexml) {
+ if ($data ~~ Array) {
+ for @($data) -> $elm is rw {
+ if $elm !~~ Exemel { $elm = Exemel::Text.new(:text(~$elm)); }
+ }
+ return $data;
+ }
+ elsif ($data !~~ Exemel) {
+ return Exemel::Text.new(:text(~$data));
+ }
+ }
+ elsif ($noxml && $data !~~ Str|Numeric) {
+ return; ## With noxml set, we only accept Strings or Numbers.
+ }
+ return $data;
+}
+
+## get-args now supports parameters in the form of {{param name}} for
+## when you have nested queries with spaces in them that shouldn't be treated
+## as strings, like 'a string' does. It also captures ${vars} and does no
+## processing on them unless you are using string processing (see below.)
+## It also supports named parameters in the form of :param(value).
+## If the :query option is set, all found parameters will be looked up using
+## the query() method (with default options.)
+## If :query is set to a Hash, then the keys of the Hash represent positional
+## parameters (the first positional parameter is 0 not 1.)
+## the value represents an action to take, if it is 0, then no querying or
+## parsing is done on the value. If it is 1, then the value is parsed as a
+## string with any ${name} variables queried.
+## If there is a key called .STRING in the query Hash, then parsing as
+## strings becomes default, and keys with a value of 1 parse as normal queries.
+## so :query({0=>0, 3=>0}) would query all parameters except the 1st and 4th.
+## If you specify the :named option, it will always include the %named
+## parameter, even if it's empty.
+method get-args($string, :$query, :$named, *@defaults) {
+ my @result =
+ $string.comb(/ [ '{{'.*?'}}' | '${'.*?'}' | '$('.*?')' | ':'\w+'('.*?')' | \'.*?\' | \S+ ] /);
+ @result>>.=subst(/^'{{'/, '');
+ @result>>.=subst(/'}}'$/, '');
+ @result>>.=subst(:g, /'$('(.*?)')'/, -> $/ { '${'~$0~'}' });
+ my %named;
+ ## Our nice for loop has been replaced now that we support named
+ ## parameters. Oh well, such is life.
+ loop (my $i=0; $i < @result.elems; $i++) {
+ my $param = @result[$i];
+ if $param ~~ /^ ':' (\w+) '(' (.*?) ')' $/ {
+ my $key = ~$0;
+ my $val = ~$1;
+ if $query { $val = self!parse-rules($query, $key, $val); }
+ %named{$key} = $val;
+ @result.splice($i, 1);
+ if $i < @result.elems {
+ $i--;
+ }
+ }
+ else {
+ if $query { @result[$i] = self!parse-rules($query, $i, $param); }
+ }
+ }
+
+ my $results = @result.elems - 1;
+ my $defs = @defaults.elems;
+
+ if $results < $defs {
+ @result.push: @defaults[$results..$defs-1];
+ }
+ ## Named params are always last.
+ if ($named || (%named.elems > 0)) {
+ @result.push: %named;
+ }
+ return @result;
+}
+
+method !parse-rules ($rules, $tag, $value) {
+ my $stringy = False;
+ if $rules ~~ Hash && $rules.exists('.STRING') {
+ $stringy = True;
+ }
+ if $rules ~~ Hash && $rules.exists($tag) {
+ if $rules{$tag} {
+ if $stringy {
+ return self.query($value);
+ }
+ else {
+ return self.parse-string($value);
+ }
+ }
+ else {
+ return $value;
+ }
+ }
+ else {
+ if $stringy {
+ return self.parse-string($value);
+ }
+ else {
+ return self.query($value);
+ }
+ }
+}
+
+method parse-string ($string) {
+ $string.subst(:g, rx/'${' (.*?) '}'/, -> $/ { self.query($0) });
+}
+
+## This handles the lookups for query().
+method !lookup (@paths is copy, $data) {
+ my $path = @paths.shift;
+ my $found;
+ given $data {
+ when Hash {
+ if $data.exists($path) {
+ $found = .{$path};
+ }
+ }
+ when Array {
+ if $path < .elems {
+ $found = .[$path];
+ }
+ }
+ default {
+ my ($command, *@args) = self.get-args(:query({0=>0}), $path);
+ if .can($command) {
+ $found = ."$command"(|@args);
+ }
+ else {
+ warn "attempt to access an invalid item '$path'.";
+ }
+ }
+ }
+ if @paths {
+ return self!lookup(@paths, $found);
+ }
+ return $found;
+}
+
View
54 lib/Flower/Utils/Date.pm → lib/Flower/TAL/TALES/Date.pm6
@@ -1,18 +1,17 @@
-module Flower::Utils::Date;
+class Flower::TAL::TALES::Date;
use DateTime::Utils;
-our sub export() {
- my %modifiers = {
- 'date' => &date_string,
- 'dateof' => &date_new,
- 'time' => &date_time,
- 'strftime' => &date_format,
- 'rfc' => &date_format_rfc,
- 'now' => &date_format_now,
- };
- return %modifiers;
-}
+has $.flower is rw;
+has $.tales is rw;
+
+has %.handlers =
+ 'date' => 'date_string',
+ 'dateof' => 'date_new',
+ 'time' => 'date_time',
+ 'strftime' => 'date_format',
+ 'rfc' => 'date_format_rfc',
+ 'now' => 'date_format_now';
## dateof: modifier, Creates a DateTime object with the given spec.
## Usage: dateof: year [month] [day] [hour] [minute] [second] :tz(timezone)
@@ -20,9 +19,9 @@ our sub export() {
## '-0800' would represent a timezone that is 8 hours behind UTC.
## '+0430' would represent a timezone that is 4 hours and 30 minutes ahead.
-our sub date_new ($parent, $query, *%opts) {
+method date_new ($query, *%opts) {
my ($year, $month, $day, $hour, $minute, $second, %params) =
- $parent.get-args(
+ $.tales.get-args(
:query({'.STRING'=>1, 'tz' => 1}),
:named, $query, 1, 1, 0, 0, 0
);
@@ -36,24 +35,24 @@ our sub date_new ($parent, $query, *%opts) {
:hour($hour.Int), :minute($minute.Int), :second($second.Int),
:timezone($timezone)
);
- return $parent.process-query($dt, |%opts);
+ return $.tales.process-query($dt, |%opts);
}
}
## date: modifier, Creates a DateTime object based on an ISO datetime stamp.
-our sub date_string ($parent, $query, *%opts) {
- my $dtstring = $parent.query($query);
+method date_string ($query, *%opts) {
+ my $dtstring = $.tales.query($query);
my $dt = DateTime.new(~$dtstring);
- return $parent.process-query($dt, |%opts);
+ return $.tales.process-query($dt, |%opts);
}
## time: modifier, Creates a DateTime object based on an epoch integer/string.
-our sub date_time ($parent, $query, *%opts) {
- my $epoch = $parent.query($query);
+method date_time ($query, *%opts) {
+ my $epoch = $.tales.query($query);
my $dt = DateTime.new($epoch.Int);
- return $parent.process-query($dt, |%opts);
+ return $.tales.process-query($dt, |%opts);
}
## strftime: modifier, formats a DateTime object.
@@ -67,9 +66,9 @@ our sub date_time ($parent, $query, *%opts) {
## or epoch integers, UTC will be used. DateTime objects will
## use their existing timezones.
-our sub date_format ($parent, $query, *%opts) {
+method date_format ($query, *%opts) {
my ($format, $date, $timezone) =
- $parent.get-args(:query, $query, DateTime.now(), Nil);
+ $.tales.get-args(:query, $query, DateTime.now(), Nil);
if defined $format && defined $date {
if defined $timezone {
$timezone = iso-offset($timezone);
@@ -104,19 +103,14 @@ our sub date_format ($parent, $query, *%opts) {
## <div tal:content="strftime: rfc: {{date: 2010 10 10 :tz('-0800')}}"/>
## Will return <div>Sun, 10 Oct 2010 00:00:00 -0800</div>
-our sub date_format_rfc ($parent, $query, *%opts) {
+method date_format_rfc ($query, *%opts) {
return '%a, %d %b %Y %T %z';
}
## now: special modifier extension for strftime.
## Returns a datetime object representing 'right now'.
-## It's in UTC by default, but that's okay, because the only
-## real use for this is in strftime: where you may want to
-## add a timezone, example:
-## <div tal:content="strftime: rfc: now: '-0800'"/>
-## Will return the current time in RFC format, in the -0800 timezone.
-our sub date_format_now ($parent, $query, *%opts) {
+method date_format_now ($query, *%opts) {
return DateTime.now();
}
View
21 lib/Flower/TAL/TALES/Debug.pm6
@@ -0,0 +1,21 @@
+class Flower::TAL::TALES::Debug;
+
+has $.flower is rw;
+has $.tales is rw;
+
+has %.handlers =
+ 'dump' => 'debug_dump',
+ 'what' => 'debug_what';
+
+method debug_dump($query, *%opts) {
+ my $result = $.tales.query($query, :noescape);
+ %opts.delete('noescape');
+ return $.tales.process-query($result.perl, :noescape, |%opts);
+}
+
+method debug_what($query, *%opts) {
+ my $result = $.tales.query($query, :noescape);
+ %opts.delete('noescape');
+ return $.tales.process-query($result.WHAT.perl, :noescape, |%opts);
+}
+
View
29 lib/Flower/TAL/TALES/Default.pm6
@@ -0,0 +1,29 @@
+class Flower::TAL::TALES::Default;
+
+has $.flower;
+has $.tales;
+
+has %.handlers =
+ 'str' => 'parse_string',
+ 'string' => 'parse_string',
+ 'is' => 'parse_true',
+ 'true' => 'parse_true',
+ 'false' => 'parse_false',
+ 'not' => 'parse_false';
+
+method parse_true ($query, *%opts) {
+ my $result = $.tales.query($query, :bool);
+ return ?$result;
+}
+
+method parse_false ($query, *%opts) {
+ my $result = $.tales.query($query, :bool);
+ if $result { return False; }
+ else { return True; }
+}
+
+method parse_string ($query, *%opts) {
+ my $string = $.tales.parse-string($query);
+ return $.tales.process-query($string, |%opts);
+}
+
View
64 lib/Flower/TAL/TALES/List.pm6
@@ -0,0 +1,64 @@
+class Flower::TAL::TALES::List;
+
+has $.flower is rw;
+has $.tales is rw;
+
+my %modifiers =
+ 'group' => 'list_group',
+ 'sort' => 'list_sort',
+ 'reverse' => 'list_reverse',
+ 'limit' => 'list_limit',
+ 'shuffle' => 'list_pick',
+ 'pick' => 'list_pick';
+
+method list_sort ($query, *%opts) {
+ my $array = $.tales.query($query);
+ if $array ~~ Array {
+ my @newarray = $array.sort;
+ return @newarray;
+ }
+}
+
+method list_group ($query, *%opts) {
+ my ($array, $num) = $.tales.get-args(:query({1=>1}), $query, 1);
+ if $array ~~ Array {
+ my @nest = ([]);
+ my $level = 0;
+ loop (my $i=0; $i < $array.elems; $i++) {
+ if $level > @nest.end {
+ @nest.push: [];
+ }
+ @nest[$level].push: $array[$i];
+ if ($i+1) % $num == 0 {
+ $level++;
+ }
+ }
+ return @nest;
+ }
+}
+
+method list_limit ($query, *%opts) {
+ my ($array, $num) = $.tales.get-args(:query({1=>1}), $query, 1);
+ if $array ~~ Array {
+ my $count = $num - 1;
+ my @return = $array[0..$count];
+ return @return;
+ }
+}
+
+method list_pick ($query, *%opts) {
+ my ($array, $num) = $.tales.get-args(:query({1=>1}), $query, *);
+ if $array ~~ Array {
+ my @return = $array.pick($num);
+ return @return;
+ }
+}
+
+method list_reverse ($query, *%opts) {
+ my $array = $.tales.query($query);
+ if $array ~~ Array {
+ my @return = $array.reverse;
+ return @return;
+ }
+}
+
View
67 lib/Flower/TAL/TALES/Text.pm6
@@ -0,0 +1,67 @@
+class Flower::TAL::TALES::Text;
+
+has $.flower is rw;
+has $.tales is rw;
+
+has %.modifiers =
+ 'uppercase' => 'text_uc',
+ 'upper' => 'text_uc',
+ 'uc' => 'text_uc',
+ 'lowercase' => 'text_lc',
+ 'lower' => 'text_lc',
+ 'lc' => 'text_lc',
+ 'ucfirst' => 'text_ucfirst',
+ 'uc_first' => 'text_ucfirst',
+ 'substr' => 'text_substr',
+ 'printf' => 'text_printf',
+ 'sprintf' => 'text_printf';
+
+## Usage: uc: varname
+method text_uc ($query, *%opts) {
+ my $result = $.tales.query($query);
+ return $.tales.process-query($result.uc, |%opts);
+}
+
+## Usage: lc: varname
+method text_lc ($query, *%opts) {
+ my $result = $.tales.query($query);
+ return $.tales.process-query($result.lc, |%opts);
+}
+
+## Usage: ucfirst: varname
+method text_ucfirst ($query, *%opts) {
+ my $result = $.tales.query($query);
+ return $.tales.process-query($result.ucfirst, |%opts);
+}
+
+## Usage: substr: opts string/variable
+## Opts: 1[,2][,3]
+## where 1 is the offset to start at,
+## 2 is the number of characters to keep,
+## and if 3 is true add an ellipsis (...) to the end.
+## E.g.: <div tal:content="substr: 3,5 'theendoftheworld'"/>
+## Returns: <div>endof</div>
+method text_substr ($query, *%opts) {
+ my ($subquery, $start, $chars, $ellipsis) =
+ $.tales.get-args($query, 0, Nil, Nil);
+ my $text = $.tales.query($subquery);
+ if defined $text {
+ my $substr = $text.substr($start, $chars);
+ if $ellipsis {
+ $substr ~= '...';
+ }
+ return $.tales.process-query($substr, |%opts);
+ }
+}
+
+## Usage: printf: format varname/path
+## E.g.: <div tal:content="printf: '$%0.2f' '2.5'"/>
+## Returns: <div>$2.50</div>
+method text_printf ($query, *%opts) {
+ my ($format, $text) = $.tales.get-args(:query, $query, Nil);
+ if defined $text && defined $format {
+ my $formatted = sprintf($format, $text);
+ return $.tales.process-query($formatted, |%opts);
+ }
+}
+
View
22 lib/Flower/Utils/Debug.pm
@@ -1,22 +0,0 @@
-module Flower::Utils::Debug;
-
-our sub export() {
- my %modifiers = {
- dump => &debug_dump,
- what => &debug_what,
- };
- return %modifiers;
-}
-
-our sub debug_dump($parent, $query, *%opts) {
- my $result = $parent.query($query, :noescape);
- %opts.delete('noescape');
- return $parent.process-query($result.perl, :noescape, |%opts);
-}
-
-our sub debug_what($parent, $query, *%opts) {
- my $result = $parent.query($query, :noescape);
- %opts.delete('noescape');
- return $parent.process-query($result.WHAT.perl, :noescape, |%opts);
-}
-
View
65 lib/Flower/Utils/List.pm
@@ -1,65 +0,0 @@
-module Flower::Utils::List;
-
-our sub export() {
- my %modifiers = {
- group => &list_group,
- 'sort' => &list_sort,
- 'reverse' => &list_reverse,
- limit => &list_limit,
- shuffle => &list_pick,
- pick => &list_pick,
- };
- return %modifiers;
-}
-
-our sub list_sort ($parent, $query, *%opts) {
- my $array = $parent.query($query);
- if $array ~~ Array {
- my @newarray = $array.sort;
- return @newarray;
- }
-}
-
-our sub list_group ($parent, $query, *%opts) {
- my ($array, $num) = $parent.get-args(:query({1=>1}), $query, 1);
- if $array ~~ Array {
- my @nest = ([]);
- my $level = 0;
- loop (my $i=0; $i < $array.elems; $i++) {
- if $level > @nest.end {
- @nest.push: [];
- }
- @nest[$level].push: $array[$i];
- if ($i+1) % $num == 0 {
- $level++;
- }
- }
- return @nest;
- }
-}
-
-our sub list_limit ($parent, $query, *%opts) {
- my ($array, $num) = $parent.get-args(:query({1=>1}), $query, 1);
- if $array ~~ Array {
- my $count = $num - 1;
- my @return = $array[0..$count];
- return @return;
- }
-}
-
-our sub list_pick ($parent, $query, *%opts) {
- my ($array, $num) = $parent.get-args(:query({1=>1}), $query, *);
- if $array ~~ Array {
- my @return = $array.pick($num);
- return @return;
- }
-}
-
-our sub list_reverse ($parent, $query, *%opts) {
- my $array = $parent.query($query);
- if $array ~~ Array {
- my @return = $array.reverse;
- return @return;
- }
-}
-
View
61 lib/Flower/Utils/Perl.pm
@@ -1,61 +0,0 @@
-module Flower::Utils::Perl;
-
-## Similar to it's cousins 'python:' and 'php:', the 'perl:' modifier allows
-## you to query the data using the native Perl 6 format instead of the
-## path syntax. Example: <eg tal:replace="perl:my-hash<key>.method('param')" />
-## The 'my-hash' in the example must be a valid key in the Flower data.
-
-## This is a dangerous library, know what you are doing before you enable this.
-## There's only so much that can be filtered out, so be careful.
-
-our sub export() {
- my %modifiers = {
- perl => &perl_query,
- };
- return %modifiers;
-}
-
-my sub clean_perl ($perl is rw) {
- my token callsign { [ '(' | \s+ ] }
- my regex badwords {
- | \.IO.*
- | 'run' <&callsign> .*
- | 'unlink' <&callsign> .*
- | \.?eval .*
- }
- $perl.=subst(/<&badwords>/, '', :g); ## Murder bad things.
-}
-
-## perl: the only modifier included by default, when loading this plugin.
-our sub perl_query ($parent, $query, *%opts) {
- my token keyname { <-['<'|'('|'.'|'{'|'['|'«']>+ }
- my $perl = $query.subst(/^(<&keyname>)/, -> $/ { '$parent.data<'~$0~'>' });
- clean_perl($perl);
- my $result = eval($perl);
- return $parent.process-query($result, |%opts);
-}
-
-## perlx: Look up a value, then run methods on the string it returns.
-## Not exported by default, you must request this modifier.
-our sub perl_lookup ($parent, $query, *%opts) {
- my ($subquery, $perl) = $query.split(/\./, 2);
- clean_perl($perl);
- my $lookup = $parent.query($subquery);
- my $result = eval("'$lookup'."~$perl);
- return $parent.process-query($result, |%opts);
-}
-
-## perlxx: Execute arbitrary Perl 6 code, and return the output.
-## Not exported by default, you must request this modifier.
-our sub perl_execute ($parent, $query, *%opts) {
- my $perl = $query;
- clean_perl($perl);
- my $result = eval($perl);
- if defined $result {
- return $parent.process-query($result, |%opts);
- }
- else {
- return;
- }
-}
-
View
68 lib/Flower/Utils/Text.pm
@@ -1,68 +0,0 @@
-module Flower::Utils::Text;
-
-our sub export() {
- my %modifiers = {
- uppercase => &text_uc,
- upper => &text_uc,
- 'uc' => &text_uc,
- lowercase => &text_lc,
- lower => &text_lc,
- 'lc' => &text_lc,
- 'ucfirst' => &text_ucfirst,
- uc_first => &text_ucfirst,
- 'substr' => &text_substr,
- 'printf' => &text_printf,
- 'sprintf' => &text_printf,
- };
- return %modifiers;
-}
-
-## Usage: uc: varname
-our sub text_uc ($parent, $query, *%opts) {
- my $result = $parent.query($query);
- return $parent.process-query($result.uc, |%opts);
-}
-
-## Usage: lc: varname
-our sub text_lc ($parent, $query, *%opts) {
- my $result = $parent.query($query);
- return $parent.process-query($result.lc, |%opts);
-}
-
-## Usage: ucfirst: varname
-our sub text_ucfirst ($parent, $query, *%opts) {
- my $result = $parent.query($query);
- return $parent.process-query($result.ucfirst, |%opts);
-}
-
-## Usage: substr: opts string/variable
-## Opts: 1[,2][,3]
-## where 1 is the offset to start at,
-## 2 is the number of characters to keep,
-## and if 3 is true add an ellipsis (...) to the end.
-## E.g.: <div tal:content="substr: 3,5 'theendoftheworld'"/>
-## Returns: <div>endof</div>
-our sub text_substr ($parent, $query, *%opts) {
- my ($subquery, $start, $chars, $ellipsis) =
- $parent.get-args($query, 0, Nil, Nil);
- my $text = $parent.query($subquery);
- if defined $text {
- my $substr = $text.substr($start, $chars);
- if $ellipsis {
- $substr ~= '...';
- }
- return $parent.process-query($substr, |%opts);
- }
-}
-
-## Usage: printf: format varname/path
-## E.g.: <div tal:content="printf: '$%0.2f' '2.5'"/>
-## Returns: <div>$2.50</div>
-our sub text_printf ($parent, $query, *%opts) {
- my ($format, $text) = $parent.get-args(:query, $query, Nil);
- if defined $text && defined $format {
- my $formatted = sprintf($format, $text);
- return $parent.process-query($formatted, |%opts);
- }
-}
-
View
35 t/01-basics.t
@@ -3,7 +3,7 @@
BEGIN { @*INC.unshift: './lib' }
use Test;
-use Flower;
+use Flower::TAL;
plan 10;
@@ -11,62 +11,55 @@ sub attrmake (*@opts) { @opts.join(' ') | @opts.reverse.join(' ') }
my $xml = '<?xml version="1.0"?>';
+my $tal = Flower::TAL.new();
+
## test 1
my $template = '<test><item tal:define="test my_test_var" tal:content="test"/></test>';
-my $flower = Flower.new(:template($template));
-is $flower.parse(my_test_var => 'Hello World'), $xml~'<test><item>Hello World</item></test>', 'tal:define and tal:content';
+is ~$tal.parse($template, my_test_var => 'Hello World'), $xml~'<test><item>Hello World</item></test>', 'tal:define and tal:content';
## test 2
$template = '<test><item tal:define="test1 my_test_var1; test2 my_test_var2" tal:content="string:${test1} = ${test2}"/></test>';
-$flower.=another(:template($template));
-is $flower.parse(my_test_var1 => 'Hello', my_test_var2 => 'World'), $xml~'<test><item>Hello = World</item></test>', 'tal:define with multiple definitions.';
+is ~$tal.parse($template, my_test_var1 => 'Hello', my_test_var2 => 'World'), $xml~'<test><item>Hello = World</item></test>', 'tal:define with multiple definitions.';
## test 3
$template = '<test><replaced tal:replace="hello">This will be replaced</replaced></test>';
-$flower.=another(:template($template));
-is $flower.parse(hello => 'Hello World'), $xml~'<test>Hello World</test>', 'tal:replace';
+is ~$tal.parse($template, hello => 'Hello World'), $xml~'<test>Hello World</test>', 'tal:replace';
## test 4
$template = '<test><true tal:condition="hello">This is true</true><false tal:condition="notreal">This is false</false><right tal:condition="true:hello"/><wrong tal:condition="false:hello"/></test>';
-$flower.=another(:template($template));
-is $flower.parse(hello => 'Hello World'), $xml~'<test><true>This is true</true><right/></test>', 'tal:condition';
+is ~$tal.parse($template, hello => 'Hello World'), $xml~'<test><true>This is true</true><right/></test>', 'tal:condition';
## test 5
$template = '<test><div tal:omit-tag="">Good <b tal:omit-tag="hello">Day</b> Mate</div></test>';
#$template = '<test><div tal:omit-tag="string:1">Good Day Mate</div></test>';
-$flower.=another(:template($template));
-is $flower.parse(hello => 'hello world'), $xml~'<test>Good Day Mate</test>', 'tal:omit-tag';
+is ~$tal.parse($template, hello => 'hello world'), $xml~'<test>Good Day Mate</test>', 'tal:omit-tag';
## test 6
$template = '<test><attrib tal:attributes="hello hello; cya goodbye"/></test>';
-$flower.=another(:template($template));
my $attrpos = attrmake 'hello="Hello World"', 'cya="Goodbye Universe"';
-is $flower.parse(hello => 'Hello World', goodbye => 'Goodbye Universe'),
+is ~$tal.parse($template, hello => 'Hello World', goodbye => 'Goodbye Universe'),
$xml~'<test><attrib '~$attrpos~'/></test>', 'tal:attributes';
## test 7
$template = '<test><hello/><goodbye tal:replace=""/></test>';
-$flower.=another(:template($template));
-is $flower.parse(), $xml~'<test><hello/></test>', 'tal:replace when empty.';
+is ~$tal.parse($template), $xml~'<test><hello/></test>', 'tal:replace when empty.';
## test 8
$template = '<test xmlns:petal="http://xml.zope.org/namespaces/tal"><div petal:replace="test"/></test>';
-$flower.=another(:template($template));
-is $flower.parse(test=>'Hello World'), $xml~'<test xmlns:petal="http://xml.zope.org/namespaces/tal">Hello World</test>', 'tal:replace with custom namespace.';
+is ~$tal.parse($template, test=>'Hello World'), $xml~'<test xmlns:petal="http://xml.zope.org/namespaces/tal">Hello World</test>', 'tal:replace with custom namespace.';
## test 9
$template = '<test tal:attributes="id id">Test document</test>';
-$flower.=another(:template($template));
-is $flower.parse(id=>'first'), $xml~'<test id="first">Test document</test>', 'attributes on root document';
+is ~$tal.parse($template, id=>'first'), $xml~'<test id="first">Test document</test>', 'attributes on root document';
## test 10
@@ -78,9 +71,7 @@ my @options = (
$template = '<select><option tal:repeat="option options" tal:attributes="value option/value; selected option/selected" tal:content="option/label"/></select>';
-$flower.=another(:template($template));
-
$attrpos = attrmake 'value="b"', 'selected="selected"';
-is $flower.parse(options => @options), $xml~'<select><option value="a">Option 1</option><option '~$attrpos~'>Option 2</option><option value="c">Option 3</option></select>', 'attributes with undefined value';
+is ~$tal.parse($template, options => @options), $xml~'<select><option value="a">Option 1</option><option '~$attrpos~'>Option 2</option><option value="c">Option 3</option></select>', 'attributes with undefined value';
Please sign in to comment.
Something went wrong with that request. Please try again.