summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoremkael <emkael@tlen.pl>2016-10-31 21:58:33 +0100
committeremkael <emkael@tlen.pl>2016-10-31 21:59:22 +0100
commitd216b3147bc3f37cf2337acab5767c6a4f74aa2e (patch)
tree6090989e5071db101a1112131e2b075a02dccbc4
parentb23bfbb17d1d5f6852a1690f246a84c2d38ae969 (diff)
* PHPTAL library
-rw-r--r--lib/phptal/COPYING504
-rw-r--r--lib/phptal/PHPTAL.php1226
-rw-r--r--lib/phptal/PHPTAL/ConfigurationException.php24
-rw-r--r--lib/phptal/PHPTAL/Context.php563
-rw-r--r--lib/phptal/PHPTAL/DefaultKeyword.php39
-rw-r--r--lib/phptal/PHPTAL/Dom/Attr.php196
-rw-r--r--lib/phptal/PHPTAL/Dom/CDATASection.php49
-rw-r--r--lib/phptal/PHPTAL/Dom/Comment.php28
-rw-r--r--lib/phptal/PHPTAL/Dom/Defs.php246
-rw-r--r--lib/phptal/PHPTAL/Dom/DocumentBuilder.php63
-rw-r--r--lib/phptal/PHPTAL/Dom/DocumentType.php33
-rw-r--r--lib/phptal/PHPTAL/Dom/Element.php521
-rw-r--r--lib/phptal/PHPTAL/Dom/Node.php105
-rw-r--r--lib/phptal/PHPTAL/Dom/PHPTALDocumentBuilder.php167
-rw-r--r--lib/phptal/PHPTAL/Dom/ProcessingInstruction.php34
-rw-r--r--lib/phptal/PHPTAL/Dom/SaxXmlParser.php480
-rw-r--r--lib/phptal/PHPTAL/Dom/Text.php31
-rw-r--r--lib/phptal/PHPTAL/Dom/XmlDeclaration.php29
-rw-r--r--lib/phptal/PHPTAL/Dom/XmlnsState.php95
-rw-r--r--lib/phptal/PHPTAL/Exception.php23
-rw-r--r--lib/phptal/PHPTAL/ExceptionHandler.php81
-rw-r--r--lib/phptal/PHPTAL/FileSource.php51
-rw-r--r--lib/phptal/PHPTAL/FileSourceResolver.php46
-rw-r--r--lib/phptal/PHPTAL/Filter.php32
-rw-r--r--lib/phptal/PHPTAL/GetTextTranslator.php183
-rw-r--r--lib/phptal/PHPTAL/IOException.php25
-rw-r--r--lib/phptal/PHPTAL/InvalidVariableNameException.php25
-rw-r--r--lib/phptal/PHPTAL/Keywords.php26
-rw-r--r--lib/phptal/PHPTAL/MacroMissingException.php24
-rw-r--r--lib/phptal/PHPTAL/Namespace.php70
-rw-r--r--lib/phptal/PHPTAL/Namespace/Builtin.php38
-rw-r--r--lib/phptal/PHPTAL/Namespace/I18N.php32
-rw-r--r--lib/phptal/PHPTAL/Namespace/METAL.php31
-rw-r--r--lib/phptal/PHPTAL/Namespace/PHPTAL.php31
-rw-r--r--lib/phptal/PHPTAL/Namespace/TAL.php36
-rw-r--r--lib/phptal/PHPTAL/NamespaceAttribute.php99
-rw-r--r--lib/phptal/PHPTAL/NamespaceAttributeContent.php23
-rw-r--r--lib/phptal/PHPTAL/NamespaceAttributeReplace.php23
-rw-r--r--lib/phptal/PHPTAL/NamespaceAttributeSurround.php23
-rw-r--r--lib/phptal/PHPTAL/NothingKeyword.php39
-rw-r--r--lib/phptal/PHPTAL/ParserException.php24
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute.php98
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/I18N/Attributes.php118
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/I18N/Data.php36
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/I18N/Domain.php50
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/I18N/Name.php47
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/I18N/Source.php48
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/I18N/Target.php43
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/I18N/Translate.php130
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/METAL/DefineMacro.php67
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/METAL/DefineSlot.php70
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/METAL/FillSlot.php148
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/METAL/UseMacro.php135
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Cache.php97
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Debug.php34
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Id.php53
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Tales.php45
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/TAL/Attributes.php213
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/TAL/Comment.php30
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/TAL/Condition.php93
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/TAL/Content.php95
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/TAL/Define.php193
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/TAL/OmitTag.php70
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/TAL/OnError.php73
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/TAL/Repeat.php99
-rw-r--r--lib/phptal/PHPTAL/Php/Attribute/TAL/Replace.php117
-rw-r--r--lib/phptal/PHPTAL/Php/CodeWriter.php511
-rw-r--r--lib/phptal/PHPTAL/Php/State.php254
-rw-r--r--lib/phptal/PHPTAL/Php/TalesChainExecutor.php96
-rw-r--r--lib/phptal/PHPTAL/Php/TalesChainReader.php25
-rw-r--r--lib/phptal/PHPTAL/Php/TalesInternal.php503
-rw-r--r--lib/phptal/PHPTAL/Php/Transformer.php418
-rw-r--r--lib/phptal/PHPTAL/PreFilter.php132
-rw-r--r--lib/phptal/PHPTAL/PreFilter/Compress.php282
-rw-r--r--lib/phptal/PHPTAL/PreFilter/Normalize.php108
-rw-r--r--lib/phptal/PHPTAL/PreFilter/StripComments.php34
-rw-r--r--lib/phptal/PHPTAL/RepeatController.php323
-rw-r--r--lib/phptal/PHPTAL/RepeatControllerGroups.php199
-rw-r--r--lib/phptal/PHPTAL/Source.php52
-rw-r--r--lib/phptal/PHPTAL/SourceResolver.php25
-rw-r--r--lib/phptal/PHPTAL/StringSource.php51
-rw-r--r--lib/phptal/PHPTAL/Tales.php58
-rw-r--r--lib/phptal/PHPTAL/TalesRegistry.php185
-rw-r--r--lib/phptal/PHPTAL/TemplateException.php160
-rw-r--r--lib/phptal/PHPTAL/Tokenizer.php69
-rw-r--r--lib/phptal/PHPTAL/TranslationService.php62
-rw-r--r--lib/phptal/PHPTAL/Trigger.php29
-rw-r--r--lib/phptal/PHPTAL/UnknownModifierException.php35
-rw-r--r--lib/phptal/PHPTAL/VariableNotFoundException.php24
89 files changed, 11255 insertions, 0 deletions
diff --git a/lib/phptal/COPYING b/lib/phptal/COPYING
new file mode 100644
index 0000000..d3a38e5
--- /dev/null
+++ b/lib/phptal/COPYING
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/lib/phptal/PHPTAL.php b/lib/phptal/PHPTAL.php
new file mode 100644
index 0000000..1e3a846
--- /dev/null
+++ b/lib/phptal/PHPTAL.php
@@ -0,0 +1,1226 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+define('PHPTAL_VERSION', '1_3_0');
+
+PHPTAL::autoloadRegister();
+
+/**
+ * PHPTAL template entry point.
+ *
+ * <code>
+ * <?php
+ * require_once 'PHPTAL.php';
+ * try {
+ * $tpl = new PHPTAL('mytemplate.html');
+ * $tpl->title = 'Welcome here';
+ * $tpl->result = range(1, 100);
+ * ...
+ * echo $tpl->execute();
+ * }
+ * catch (Exception $e) {
+ * echo $e;
+ * }
+ * ?>
+ * </code>
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @link http://phptal.org/
+ */
+class PHPTAL
+{
+ //{{{
+ /**
+ * constants for output mode
+ * @see setOutputMode()
+ */
+ const XHTML = 11;
+ const XML = 22;
+ const HTML5 = 55;
+
+ /**
+ * @see getPreFilters()
+ */
+ protected $prefilters = array();
+
+ /**
+ * Prefilters have been redesigned. Old property is no longer used.
+ *
+ * @deprecated
+ */
+ private $_prefilter = 'REMOVED: DO NOT USE';
+ protected $_postfilter = null;
+
+ /**
+ * list of template source repositories given to file source resolver
+ */
+ protected $_repositories = array();
+
+ /**
+ * template path (path that has been set, not necessarily loaded)
+ */
+ protected $_path = null;
+
+ /**
+ * template source resolvers (classes that search for templates by name)
+ */
+ protected $resolvers = array();
+
+ /**
+ * template source (only set when not working with file)
+ */
+ protected $_source = null;
+
+ /**
+ * destination of PHP intermediate file
+ */
+ protected $_codeFile = null;
+
+ /**
+ * php function generated for the template
+ */
+ protected $_functionName = null;
+
+ /**
+ * set to true when template is ready for execution
+ */
+ protected $_prepared = false;
+
+ /**
+ * associative array of phptal:id => PHPTAL_Trigger
+ */
+ protected $_triggers = array();
+
+ /**
+ * i18n translator
+ */
+ protected $_translator = null;
+
+ /**
+ * global execution context
+ */
+ protected $_globalContext = null;
+
+ /**
+ * current execution context
+ */
+ protected $_context = null;
+
+ /**
+ * list of on-error caught exceptions
+ */
+ protected $_errors = array();
+
+ /**
+ * encoding used throughout
+ */
+ protected $_encoding = 'UTF-8';
+
+ /**
+ * type of syntax used in generated templates
+ */
+ protected $_outputMode = PHPTAL::XHTML;
+ /**
+ * should all comments be stripped
+ */
+
+ // configuration properties
+
+ /**
+ * don't use code cache
+ */
+ protected $_forceReparse = null;
+
+ /**
+ * directory where code cache is
+ */
+ private $_phpCodeDestination;
+ private $_phpCodeExtension = 'php';
+
+ /**
+ * number of days
+ */
+ private $_cacheLifetime = 30;
+
+ /**
+ * 1/x
+ */
+ private $_cachePurgeFrequency = 30;
+
+ /**
+ * speeds up calls to external templates
+ */
+ private $externalMacroTemplatesCache = array();
+
+ //}}}
+
+ /**
+ * PHPTAL Constructor.
+ *
+ * @param string $path Template file path.
+ */
+ public function __construct($path=false)
+ {
+ $this->_path = $path;
+ $this->_globalContext = new stdClass();
+ $this->_context = new PHPTAL_Context();
+ $this->_context->setGlobal($this->_globalContext);
+
+ if (function_exists('sys_get_temp_dir')) {
+ $this->setPhpCodeDestination(sys_get_temp_dir());
+ } elseif (substr(PHP_OS, 0, 3) == 'WIN') {
+ if (file_exists('c:\\WINNT\\Temp\\')) {
+ $this->setPhpCodeDestination('c:\\WINNT\\Temp\\');
+ } else {
+ $this->setPhpCodeDestination('c:\\WINDOWS\\Temp\\');
+ }
+ } else {
+ $this->setPhpCodeDestination('/tmp/');
+ }
+ }
+
+ /**
+ * create
+ * returns a new PHPTAL object
+ *
+ * @param string $path Template file path.
+ *
+ * @return PHPTAL
+ */
+ public static function create($path=false)
+ {
+ return new PHPTAL($path);
+ }
+
+ /**
+ * Clone template state and context.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ $this->_context = $this->_context->pushContext();
+ }
+
+ /**
+ * Set template from file path.
+ *
+ * @param string $path filesystem path,
+ * or any path that will be accepted by source resolver
+ *
+ * @return $this
+ */
+ public function setTemplate($path)
+ {
+ $this->_prepared = false;
+ $this->_functionName = null;
+ $this->_codeFile = null;
+ $this->_path = $path;
+ $this->_source = null;
+ $this->_context->_docType = null;
+ $this->_context->_xmlDeclaration = null;
+ return $this;
+ }
+
+ /**
+ * Set template from source.
+ *
+ * Should be used only with temporary template sources.
+ * Use setTemplate() or addSourceResolver() whenever possible.
+ *
+ * @param string $src The phptal template source.
+ * @param string $path Fake and 'unique' template path.
+ *
+ * @return $this
+ */
+ public function setSource($src, $path = null)
+ {
+ $this->_prepared = false;
+ $this->_functionName = null;
+ $this->_codeFile = null;
+ $this->_source = new PHPTAL_StringSource($src, $path);
+ $this->_path = $this->_source->getRealPath();
+ $this->_context->_docType = null;
+ $this->_context->_xmlDeclaration = null;
+ return $this;
+ }
+
+ /**
+ * Specify where to look for templates.
+ *
+ * @param mixed $rep string or Array of repositories
+ *
+ * @return $this
+ */
+ public function setTemplateRepository($rep)
+ {
+ if (is_array($rep)) {
+ $this->_repositories = $rep;
+ } else {
+ $this->_repositories[] = $rep;
+ }
+ return $this;
+ }
+
+ /**
+ * Get template repositories.
+ *
+ * @return array
+ */
+ public function getTemplateRepositories()
+ {
+ return $this->_repositories;
+ }
+
+ /**
+ * Clears the template repositories.
+ *
+ * @return $this
+ */
+ public function clearTemplateRepositories()
+ {
+ $this->_repositories = array();
+ return $this;
+ }
+
+ /**
+ * Specify how to look for templates.
+ *
+ * @param PHPTAL_SourceResolver $resolver instance of resolver
+ *
+ * @return $this
+ */
+ public function addSourceResolver(PHPTAL_SourceResolver $resolver)
+ {
+ $this->resolvers[] = $resolver;
+ return $this;
+ }
+
+ /**
+ * Ignore XML/XHTML comments on parsing.
+ * Comments starting with <!--! are always stripped.
+ *
+ * @param bool $bool if true all comments are stripped during parse
+ *
+ * @return $this
+ */
+ public function stripComments($bool)
+ {
+ $this->resetPrepared();
+
+ if ($bool) {
+ $this->prefilters['_phptal_strip_comments_'] = new PHPTAL_PreFilter_StripComments();
+ } else {
+ unset($this->prefilters['_phptal_strip_comments_']);
+ }
+ return $this;
+ }
+
+ /**
+ * Set output mode
+ * XHTML output mode will force elements like <link/>, <meta/> and <img/>, etc.
+ * to be empty and threats attributes like selected, checked to be
+ * boolean attributes.
+ *
+ * XML output mode outputs XML without such modifications
+ * and is neccessary to generate RSS feeds properly.
+ *
+ * @param int $mode (PHPTAL::XML, PHPTAL::XHTML or PHPTAL::HTML5).
+ *
+ * @return $this
+ */
+ public function setOutputMode($mode)
+ {
+ $this->resetPrepared();
+
+ if ($mode != PHPTAL::XHTML && $mode != PHPTAL::XML && $mode != PHPTAL::HTML5) {
+ throw new PHPTAL_ConfigurationException('Unsupported output mode '.$mode);
+ }
+ $this->_outputMode = $mode;
+ return $this;
+ }
+
+ /**
+ * Get output mode
+ * @see setOutputMode()
+ *
+ * @return output mode constant
+ */
+ public function getOutputMode()
+ {
+ return $this->_outputMode;
+ }
+
+ /**
+ * Set input and ouput encoding. Encoding is case-insensitive.
+ *
+ * @param string $enc example: 'UTF-8'
+ *
+ * @return $this
+ */
+ public function setEncoding($enc)
+ {
+ $enc = strtoupper($enc);
+ if ($enc != $this->_encoding) {
+ $this->_encoding = $enc;
+ if ($this->_translator) $this->_translator->setEncoding($enc);
+
+ $this->resetPrepared();
+ }
+ return $this;
+ }
+
+ /**
+ * Get input and ouput encoding.
+ *
+ * @param string $enc example: 'UTF-8'
+ *
+ * @return $this
+ */
+ public function getEncoding()
+ {
+ return $this->_encoding;
+ }
+
+ /**
+ * Set the storage location for intermediate PHP files.
+ * The path cannot contain characters that would be interpreted by glob() (e.g. *[]?)
+ *
+ * @param string $path Intermediate file path.
+ *
+ * @return $this
+ */
+ public function setPhpCodeDestination($path)
+ {
+ $this->_phpCodeDestination = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+ $this->resetPrepared();
+ return $this;
+ }
+
+ /**
+ * Get the storage location for intermediate PHP files.
+ *
+ * @return string
+ */
+ public function getPhpCodeDestination()
+ {
+ return $this->_phpCodeDestination;
+ }
+
+ /**
+ * Set the file extension for intermediate PHP files.
+ *
+ * @param string $extension The file extension.
+ *
+ * @return $this
+ */
+ public function setPhpCodeExtension($extension)
+ {
+ $this->_phpCodeExtension = $extension;
+ $this->resetPrepared();
+ return $this;
+ }
+
+ /**
+ * Get the file extension for intermediate PHP files.
+ */
+ public function getPhpCodeExtension()
+ {
+ return $this->_phpCodeExtension;
+ }
+
+ /**
+ * Flags whether to ignore intermediate php files and to
+ * reparse templates every time (if set to true).
+ *
+ * DON'T USE IN PRODUCTION - this makes PHPTAL many times slower.
+ *
+ * @param bool $bool Forced reparse state.
+ *
+ * @return $this
+ */
+ public function setForceReparse($bool)
+ {
+ $this->_forceReparse = (bool) $bool;
+ return $this;
+ }
+
+ /**
+ * Get the value of the force reparse state.
+ */
+ public function getForceReparse()
+ {
+ return $this->_forceReparse;
+ }
+
+ /**
+ * Set I18N translator.
+ *
+ * This sets encoding used by the translator, so be sure to use encoding-dependent
+ * features of the translator (e.g. addDomain) _after_ calling setTranslator.
+ *
+ * @param PHPTAL_TranslationService $t instance
+ *
+ * @return $this
+ */
+ public function setTranslator(PHPTAL_TranslationService $t)
+ {
+ $this->_translator = $t;
+ $t->setEncoding($this->getEncoding());
+ return $this;
+ }
+
+
+ /**
+ * Please use addPreFilter instead.
+ *
+ * This method and use of PHPTAL_Filter for prefilters are deprecated.
+ *
+ * @see PHPTAL::addPreFilter()
+ * @deprecated
+ */
+ final public function setPreFilter(PHPTAL_Filter $filter)
+ {
+ $this->resetPrepared();
+ $this->prefilters['_phptal_old_filter_'] = $filter;
+ }
+
+ /**
+ * Add new prefilter to filter chain.
+ * Prefilters are called only once template is compiled.
+ *
+ * PreFilters must inherit PHPTAL_PreFilter class.
+ * (in future this method will allow string with filter name instead of object)
+ *
+ * @param mixed $filter PHPTAL_PreFilter object or name of prefilter to add
+ *
+ * @return PHPTAL
+ */
+ final public function addPreFilter($filter)
+ {
+ $this->resetPrepared();
+
+ if (!$filter instanceof PHPTAL_PreFilter) {
+ throw new PHPTAL_ConfigurationException("addPreFilter expects PHPTAL_PreFilter object");
+ }
+
+ $this->prefilters[] = $filter;
+ return $this;
+ }
+
+ /**
+ * Array with all prefilter objects *or strings* that are names of prefilter classes.
+ * (the latter is not implemented in 1.2.1)
+ *
+ * Array keys may be non-numeric!
+ *
+ * @return array
+ */
+ protected function getPreFilters()
+ {
+ return $this->prefilters;
+ }
+
+ /**
+ * Returns string that is unique for every different configuration of prefilters.
+ * Result of prefilters may be cached until this string changes.
+ *
+ * You can override this function.
+ *
+ * @return string
+ */
+ private function getPreFiltersCacheId()
+ {
+ $cacheid = '';
+ foreach($this->getPreFilters() as $key => $prefilter) {
+ if ($prefilter instanceof PHPTAL_PreFilter) {
+ $cacheid .= $key.$prefilter->getCacheId();
+ } elseif ($prefilter instanceof PHPTAL_Filter) {
+ $cacheid .= $key.get_class($prefilter);
+ } else {
+ $cacheid .= $key.$prefilter;
+ }
+ }
+ return $cacheid;
+ }
+
+ /**
+ * Instantiate prefilters
+ *
+ * @return array of PHPTAL_[Pre]Filter objects
+ */
+ private function getPreFilterInstances()
+ {
+ $prefilters = $this->getPreFilters();
+
+ foreach($prefilters as $prefilter) {
+ if ($prefilter instanceof PHPTAL_PreFilter) {
+ $prefilter->setPHPTAL($this);
+ }
+ }
+ return $prefilters;
+ }
+
+ /**
+ * Set template post filter.
+ * It will be called every time after template generates output.
+ *
+ * See PHPTAL_PostFilter class.
+ *
+ * @param PHPTAL_Filter $filter filter instance
+ */
+ public function setPostFilter(PHPTAL_Filter $filter)
+ {
+ $this->_postfilter = $filter;
+ return $this;
+ }
+
+ /**
+ * Register a trigger for specified phptal:id.
+ * @param string $id phptal:id to look for
+ */
+ public function addTrigger($id, PHPTAL_Trigger $trigger)
+ {
+ $this->_triggers[$id] = $trigger;
+ return $this;
+ }
+
+ /**
+ * Returns trigger for specified phptal:id.
+ *
+ * @param string $id phptal:id
+ *
+ * @return PHPTAL_Trigger or NULL
+ */
+ public function getTrigger($id)
+ {
+ if (array_key_exists($id, $this->_triggers)) {
+ return $this->_triggers[$id];
+ }
+ return null;
+ }
+
+ /**
+ * Set a context variable.
+ * Use it by setting properties on PHPTAL object.
+ *
+ * @param string $varname
+ * @param mixed $value
+ *
+ * @return void
+ */
+ public function __set($varname, $value)
+ {
+ $this->_context->__set($varname, $value);
+ }
+
+ /**
+ * Set a context variable.
+ *
+ * @see PHPTAL::__set()
+ * @param string $varname name of the variable
+ * @param mixed $value value of the variable
+ *
+ * @return $this
+ */
+ public function set($varname, $value)
+ {
+ $this->_context->__set($varname, $value);
+ return $this;
+ }
+
+ /**
+ * Execute the template code and return generated markup.
+ *
+ * @return string
+ */
+ public function execute()
+ {
+ try
+ {
+ if (!$this->_prepared) {
+ // includes generated template PHP code
+ $this->prepare();
+ }
+ $this->_context->echoDeclarations(false);
+
+ $templateFunction = $this->getFunctionName();
+
+ try {
+ ob_start();
+ $templateFunction($this, $this->_context);
+ $res = ob_get_clean();
+ }
+ catch (Exception $e)
+ {
+ ob_end_clean();
+ throw $e;
+ }
+
+ // unshift doctype
+ if ($this->_context->_docType) {
+ $res = $this->_context->_docType . $res;
+ }
+
+ // unshift xml declaration
+ if ($this->_context->_xmlDeclaration) {
+ $res = $this->_context->_xmlDeclaration . "\n" . $res;
+ }
+
+ if ($this->_postfilter) {
+ return $this->_postfilter->filter($res);
+ }
+ }
+ catch (Exception $e)
+ {
+ PHPTAL_ExceptionHandler::handleException($e, $this->getEncoding());
+ }
+
+ return $res;
+ }
+
+ /**
+ * Execute and echo template without buffering of the output.
+ * This function does not allow postfilters nor DOCTYPE/XML declaration.
+ *
+ * @return NULL
+ */
+ public function echoExecute()
+ {
+ try {
+ if (!$this->_prepared) {
+ // includes generated template PHP code
+ $this->prepare();
+ }
+
+ if ($this->_postfilter) {
+ throw new PHPTAL_ConfigurationException("echoExecute() does not support postfilters");
+ }
+
+ $this->_context->echoDeclarations(true);
+
+ $templateFunction = $this->getFunctionName();
+ $templateFunction($this, $this->_context);
+ }
+ catch (Exception $e)
+ {
+ PHPTAL_ExceptionHandler::handleException($e, $this->getEncoding());
+ }
+ }
+
+ /**
+ * Execute a template macro.
+ * Should be used only from within generated template code!
+ *
+ * @param string $path Template macro path
+ */
+ public function executeMacro($path)
+ {
+ $this->_executeMacroOfTemplate($path, $this);
+ }
+
+ /**
+ * This is PHPTAL's internal function that handles
+ * execution of macros from templates.
+ *
+ * $this is caller's context (the file where execution had originally started)
+ *
+ * @param PHPTAL $local_tpl is PHPTAL instance of the file in which macro is defined
+ * (it will be different from $this if it's external macro call)
+ * @access private
+ */
+ final public function _executeMacroOfTemplate($path, PHPTAL $local_tpl)
+ {
+ // extract macro source file from macro name, if macro path does not
+ // contain filename, then the macro is assumed to be local
+
+ if (preg_match('/^(.*?)\/([a-z0-9_-]*)$/i', $path, $m)) {
+ list(, $file, $macroName) = $m;
+
+ if (isset($this->externalMacroTemplatesCache[$file])) {
+ $tpl = $this->externalMacroTemplatesCache[$file];
+ } else {
+ $tpl = clone $this;
+ array_unshift($tpl->_repositories, dirname($this->_source->getRealPath()));
+ $tpl->setTemplate($file);
+ $tpl->prepare();
+
+ // keep it small (typically only 1 or 2 external files are used)
+ if (count($this->externalMacroTemplatesCache) > 10) {
+ $this->externalMacroTemplatesCache = array();
+ }
+ $this->externalMacroTemplatesCache[$file] = $tpl;
+ }
+
+ $fun = $tpl->getFunctionName() . '_' . strtr($macroName, "-", "_");
+ if (!function_exists($fun)) {
+ throw new PHPTAL_MacroMissingException("Macro '$macroName' is not defined in $file", $this->_source->getRealPath());
+ }
+
+ $fun($tpl, $this);
+
+ } else {
+ // call local macro
+ $fun = $local_tpl->getFunctionName() . '_' . strtr($path, "-", "_");
+ if (!function_exists($fun)) {
+ throw new PHPTAL_MacroMissingException("Macro '$path' is not defined", $local_tpl->_source->getRealPath());
+ }
+ $fun( $local_tpl, $this);
+ }
+ }
+
+ /**
+ * ensure that getCodePath will return up-to-date path
+ */
+ private function setCodeFile()
+ {
+ $this->findTemplate();
+ $this->_codeFile = $this->getPhpCodeDestination() . $this->getFunctionName() . '.' . $this->getPhpCodeExtension();
+ }
+
+ protected function resetPrepared()
+ {
+ $this->_prepared = false;
+ $this->_functionName = null;
+ $this->_codeFile = null;
+ }
+
+ /**
+ * Prepare template without executing it.
+ */
+ public function prepare()
+ {
+ // clear just in case settings changed and cache is out of date
+ $this->externalMacroTemplatesCache = array();
+
+ // find the template source file and update function name
+ $this->setCodeFile();
+
+ if (!function_exists($this->getFunctionName())) {
+ // parse template if php generated code does not exists or template
+ // source file modified since last generation or force reparse is set
+ if ($this->getForceReparse() || !file_exists($this->getCodePath())) {
+
+ // i'm not sure where that belongs, but not in normal path of execution
+ // because some sites have _a lot_ of files in temp
+ if ($this->getCachePurgeFrequency() && mt_rand()%$this->getCachePurgeFrequency() == 0) {
+ $this->cleanUpGarbage();
+ }
+
+ $result = $this->parse();
+
+ if (!file_put_contents($this->getCodePath(), $result)) {
+ throw new PHPTAL_IOException('Unable to open '.$this->getCodePath().' for writing');
+ }
+
+ // the awesome thing about eval() is that parse errors don't stop PHP.
+ // when PHP dies during eval, fatal error is printed and
+ // can be captured with output buffering
+ ob_start();
+ try {
+ eval("?>\n".$result);
+ }
+ catch(Exception $e) {
+ ob_end_clean();
+ throw $e;
+ }
+
+ if (!function_exists($this->getFunctionName())) {
+ $msg = str_replace('eval()\'d code', $this->getCodePath(), ob_get_clean());
+
+ // greedy .* ensures last match
+ if (preg_match('/.*on line (\d+)$/m', $msg, $m)) $line=$m[1]; else $line=0;
+ throw new PHPTAL_TemplateException(trim($msg), $this->getCodePath(), $line);
+ }
+ ob_end_clean();
+
+ } else {
+ // eval trick is used only on first run,
+ // just in case it causes any problems with opcode accelerators
+ require $this->getCodePath();
+ }
+ }
+
+ $this->_prepared = true;
+ return $this;
+ }
+
+ /**
+ * get how long compiled templates and phptal:cache files are kept, in days
+ */
+ public function getCacheLifetime()
+ {
+ return $this->_cacheLifetime;
+ }
+
+ /**
+ * set how long compiled templates and phptal:cache files are kept
+ *
+ * @param $days number of days
+ */
+ public function setCacheLifetime($days)
+ {
+ $this->_cacheLifetime = max(0.5, $days);
+ return $this;
+ }
+
+ /**
+ * PHPTAL will scan cache and remove old files on every nth compile
+ * Set to 0 to disable cleanups
+ */
+ public function setCachePurgeFrequency($n)
+ {
+ $this->_cachePurgeFrequency = (int)$n;
+ return $this;
+ }
+
+ /**
+ * how likely cache cleaning can happen
+ * @see self::setCachePurgeFrequency()
+ */
+ public function getCachePurgeFrequency()
+ {
+ return $this->_cachePurgeFrequency;
+ }
+
+
+ /**
+ * Removes all compiled templates from cache that
+ * are older than getCacheLifetime() days
+ */
+ public function cleanUpGarbage()
+ {
+ $cacheFilesExpire = time() - $this->getCacheLifetime() * 3600 * 24;
+
+ // relies on templates sorting order being related to their modification dates
+ $upperLimit = $this->getPhpCodeDestination() . $this->getFunctionNamePrefix($cacheFilesExpire) . '_';
+ $lowerLimit = $this->getPhpCodeDestination() . $this->getFunctionNamePrefix(0);
+
+ // second * gets phptal:cache
+ $cacheFiles = glob($this->getPhpCodeDestination() . 'tpl_????????_*.' . $this->getPhpCodeExtension() . '*');
+
+ if ($cacheFiles) {
+ foreach ($cacheFiles as $index => $file) {
+
+ // comparison here skips filenames that are certainly too new
+ if (strcmp($file, $upperLimit) <= 0 || substr($file, 0, strlen($lowerLimit)) === $lowerLimit) {
+ $time = filemtime($file);
+ if ($time && $time < $cacheFilesExpire) {
+ @unlink($file);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes content cached with phptal:cache for currently set template
+ * Must be called after setSource/setTemplate.
+ */
+ public function cleanUpCache()
+ {
+ $filename = $this->getCodePath();
+ $cacheFiles = glob($filename . '?*');
+ if ($cacheFiles) {
+ foreach ($cacheFiles as $file) {
+ if (substr($file, 0, strlen($filename)) !== $filename) continue; // safety net
+ @unlink($file);
+ }
+ }
+ $this->_prepared = false;
+ }
+
+ /**
+ * Returns the path of the intermediate PHP code file.
+ *
+ * The returned file may be used to cleanup (unlink) temporary files
+ * generated by temporary templates or more simply for debug.
+ *
+ * @return string
+ */
+ public function getCodePath()
+ {
+ if (!$this->_codeFile) $this->setCodeFile();
+ return $this->_codeFile;
+ }
+
+ /**
+ * Returns the generated template function name.
+ * @return string
+ */
+ public function getFunctionName()
+ {
+ // function name is used as base for caching, so it must be unique for
+ // every combination of settings that changes code in compiled template
+
+ if (!$this->_functionName) {
+
+ // just to make tempalte name recognizable
+ $basename = preg_replace('/\.[a-z]{3,5}$/', '', basename($this->_source->getRealPath()));
+ $basename = substr(trim(preg_replace('/[^a-zA-Z0-9]+/', '_', $basename), "_"), 0, 20);
+
+ $hash = md5(PHPTAL_VERSION . PHP_VERSION
+ . $this->_source->getRealPath()
+ . $this->getEncoding()
+ . $this->getPrefiltersCacheId()
+ . $this->getOutputMode(),
+ true
+ );
+
+ // uses base64 rather than hex to make filename shorter.
+ // there is loss of some bits due to name constraints and case-insensivity,
+ // but that's still over 110 bits in addition to basename and timestamp.
+ $hash = strtr(rtrim(base64_encode($hash),"="),"+/=","_A_");
+
+ $this->_functionName = $this->getFunctionNamePrefix($this->_source->getLastModifiedTime()) .
+ $basename . '__' . $hash;
+ }
+ return $this->_functionName;
+ }
+
+ /**
+ * Returns prefix used for function name.
+ * Function name is also base name for the template.
+ *
+ * @param int $timestamp unix timestamp with template modification date
+ *
+ * @return string
+ */
+ private function getFunctionNamePrefix($timestamp)
+ {
+ // tpl_ prefix and last modified time must not be changed,
+ // because cache cleanup relies on that
+ return 'tpl_' . sprintf("%08x", $timestamp) .'_';
+ }
+
+ /**
+ * Returns template translator.
+ * @return PHPTAL_TranslationService
+ */
+ public function getTranslator()
+ {
+ return $this->_translator;
+ }
+
+ /**
+ * Returns array of exceptions caught by tal:on-error attribute.
+ *
+ * @return array<Exception>
+ */
+ public function getErrors()
+ {
+ return $this->_errors;
+ }
+
+ /**
+ * Public for phptal templates, private for user.
+ *
+ * @return void
+ * @access private
+ */
+ public function addError(Exception $error)
+ {
+ $this->_errors[] = $error;
+ }
+
+ /**
+ * Returns current context object.
+ * Use only in Triggers.
+ *
+ * @return PHPTAL_Context
+ */
+ public function getContext()
+ {
+ return $this->_context;
+ }
+
+ /**
+ * only for use in generated template code
+ *
+ * @access private
+ */
+ public function getGlobalContext()
+ {
+ return $this->_globalContext;
+ }
+
+ /**
+ * only for use in generated template code
+ *
+ * @access private
+ */
+ final public function pushContext()
+ {
+ $this->_context = $this->_context->pushContext();
+ return $this->_context;
+ }
+
+ /**
+ * only for use in generated template code
+ *
+ * @access private
+ */
+ final public function popContext()
+ {
+ $this->_context = $this->_context->popContext();
+ return $this->_context;
+ }
+
+ /**
+ * Parse currently set template, prefilter and generate PHP code.
+ *
+ * @return string (compiled PHP code)
+ */
+ protected function parse()
+ {
+ $data = $this->_source->getData();
+
+ $prefilters = $this->getPreFilterInstances();
+ foreach($prefilters as $prefilter) {
+ $data = $prefilter->filter($data);
+ }
+
+ $realpath = $this->_source->getRealPath();
+ $parser = new PHPTAL_Dom_SaxXmlParser($this->_encoding);
+
+ $builder = new PHPTAL_Dom_PHPTALDocumentBuilder();
+ $tree = $parser->parseString($builder, $data, $realpath)->getResult();
+
+ foreach($prefilters as $prefilter) {
+ if ($prefilter instanceof PHPTAL_PreFilter) {
+ if ($prefilter->filterDOM($tree) !== NULL) {
+ throw new PHPTAL_ConfigurationException("Don't return value from filterDOM()");
+ }
+ }
+ }
+
+ $state = new PHPTAL_Php_State($this);
+
+ $codewriter = new PHPTAL_Php_CodeWriter($state);
+ $codewriter->doTemplateFile($this->getFunctionName(), $tree);
+
+ return $codewriter->getResult();
+ }
+
+ /**
+ * Search template source location.
+ * @return void
+ */
+ protected function findTemplate()
+ {
+ if ($this->_path == false) {
+ throw new PHPTAL_ConfigurationException('No template file specified');
+ }
+
+ // template source already defined
+ if ($this->_source) {
+ return;
+ }
+
+ if (!$this->resolvers && !$this->_repositories) {
+ $this->_source = new PHPTAL_FileSource($this->_path);
+ } else {
+ foreach ($this->resolvers as $resolver) {
+ $source = $resolver->resolve($this->_path);
+ if ($source) {
+ $this->_source = $source;
+ return;
+ }
+ }
+
+ $resolver = new PHPTAL_FileSourceResolver($this->_repositories);
+ $this->_source = $resolver->resolve($this->_path);
+ }
+
+ if (!$this->_source) {
+ throw new PHPTAL_IOException('Unable to locate template file '.$this->_path);
+ }
+ }
+
+ /**
+ * Removed
+ *
+ * @deprecated
+ * @return void
+ */
+ final public static function setIncludePath()
+ {
+ }
+
+ /**
+ * Restore include path to state before PHPTAL modified it.
+ *
+ * @deprecated
+ * @return void
+ */
+ final public static function restoreIncludePath()
+ {
+ }
+
+ /**
+ * Suitable for callbacks from SPL autoload
+ *
+ * @param string $class class name to load
+ *
+ * @return void
+ */
+ final public static function autoload($class)
+ {
+ if (version_compare(PHP_VERSION, '5.3', '>=') && __NAMESPACE__) {
+ $class = str_replace(__NAMESPACE__, 'PHPTAL', $class);
+ $class = strtr($class, '\\', '_');
+ }
+
+ if (substr($class, 0, 7) !== 'PHPTAL_') return;
+
+ $path = dirname(__FILE__) . strtr("_".$class, "_", DIRECTORY_SEPARATOR) . '.php';
+
+ require $path;
+ }
+
+ /**
+ * Sets up PHPTAL's autoloader.
+ *
+ * If you have to use your own autoloader to load PHPTAL files,
+ * use spl_autoload_unregister(array('PHPTAL','autoload'));
+ *
+ * @return void
+ */
+ final public static function autoloadRegister()
+ {
+ // spl_autoload_register disables oldschool autoload
+ // even if it was added using spl_autoload_register!
+ // this is intended to preserve old autoloader
+
+ $uses_autoload = function_exists('__autoload')
+ && (!($tmp = spl_autoload_functions()) || ($tmp[0] === '__autoload'));
+
+ // Prepending PHPTAL's autoloader helps if there are other autoloaders
+ // that throw/die when file is not found. Only >5.3 though.
+ if (version_compare(PHP_VERSION, '5.3', '>=')) {
+ spl_autoload_register(array(__CLASS__,'autoload'), false, true);
+ } else {
+ spl_autoload_register(array(__CLASS__,'autoload'));
+ }
+
+ if ($uses_autoload) {
+ spl_autoload_register('__autoload');
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/ConfigurationException.php b/lib/phptal/PHPTAL/ConfigurationException.php
new file mode 100644
index 0000000..28e760a
--- /dev/null
+++ b/lib/phptal/PHPTAL/ConfigurationException.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * You're probably not using PHPTAL class properly
+ *
+ * @package PHPTAL
+ * @subpackage Exception
+ */
+class PHPTAL_ConfigurationException extends PHPTAL_Exception
+{
+}
diff --git a/lib/phptal/PHPTAL/Context.php b/lib/phptal/PHPTAL/Context.php
new file mode 100644
index 0000000..470d521
--- /dev/null
+++ b/lib/phptal/PHPTAL/Context.php
@@ -0,0 +1,563 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * This class handles template execution context.
+ * Holds template variables and carries state/scope across macro executions.
+ *
+ */
+class PHPTAL_Context
+{
+ public $repeat;
+ public $_xmlDeclaration;
+ public $_docType;
+ private $_nothrow;
+ private $_slots = array();
+ private $_slotsStack = array();
+ private $_parentContext = null;
+ private $_globalContext = null;
+ private $_echoDeclarations = false;
+
+ public function __construct()
+ {
+ $this->repeat = new stdClass();
+ }
+
+ public function __clone()
+ {
+ $this->repeat = clone $this->repeat;
+ }
+
+ /**
+ * will switch to this context when popContext() is called
+ *
+ * @return void
+ */
+ public function setParent(PHPTAL_Context $parent)
+ {
+ $this->_parentContext = $parent;
+ }
+
+ /**
+ * set stdClass object which has property of every global variable
+ * It can use __isset() and __get() [none of them or both]
+ *
+ * @return void
+ */
+ public function setGlobal(stdClass $globalContext)
+ {
+ $this->_globalContext = $globalContext;
+ }
+
+ /**
+ * save current execution context
+ *
+ * @return Context (new)
+ */
+ public function pushContext()
+ {
+ $res = clone $this;
+ $res->setParent($this);
+ return $res;
+ }
+
+ /**
+ * get previously saved execution context
+ *
+ * @return Context (old)
+ */
+ public function popContext()
+ {
+ return $this->_parentContext;
+ }
+
+ /**
+ * @param bool $tf true if DOCTYPE and XML declaration should be echoed immediately, false if buffered
+ */
+ public function echoDeclarations($tf)
+ {
+ $this->_echoDeclarations = $tf;
+ }
+
+ /**
+ * Set output document type if not already set.
+ *
+ * This method ensure PHPTAL uses the first DOCTYPE encountered (main
+ * template or any macro template source containing a DOCTYPE.
+ *
+ * @param bool $called_from_macro will do nothing if _echoDeclarations is also set
+ *
+ * @return void
+ */
+ public function setDocType($doctype,$called_from_macro)
+ {
+ // FIXME: this is temporary workaround for problem of DOCTYPE disappearing in cloned PHPTAL object (because clone keeps _parentContext)
+ if (!$this->_docType) {
+ $this->_docType = $doctype;
+ }
+
+ if ($this->_parentContext) {
+ $this->_parentContext->setDocType($doctype, $called_from_macro);
+ } else if ($this->_echoDeclarations) {
+ if (!$called_from_macro) {
+ echo $doctype;
+ } else {
+ throw new PHPTAL_ConfigurationException("Executed macro in file with DOCTYPE when using echoExecute(). This is not supported yet. Remove DOCTYPE or use PHPTAL->execute().");
+ }
+ }
+ else if (!$this->_docType) {
+ $this->_docType = $doctype;
+ }
+ }
+
+ /**
+ * Set output document xml declaration.
+ *
+ * This method ensure PHPTAL uses the first xml declaration encountered
+ * (main template or any macro template source containing an xml
+ * declaration)
+ *
+ * @param bool $called_from_macro will do nothing if _echoDeclarations is also set
+ *
+ * @return void
+ */
+ public function setXmlDeclaration($xmldec, $called_from_macro)
+ {
+ // FIXME
+ if (!$this->_xmlDeclaration) {
+ $this->_xmlDeclaration = $xmldec;
+ }
+
+ if ($this->_parentContext) {
+ $this->_parentContext->setXmlDeclaration($xmldec, $called_from_macro);
+ } else if ($this->_echoDeclarations) {
+ if (!$called_from_macro) {
+ echo $xmldec."\n";
+ } else {
+ throw new PHPTAL_ConfigurationException("Executed macro in file with XML declaration when using echoExecute(). This is not supported yet. Remove XML declaration or use PHPTAL->execute().");
+ }
+ } else if (!$this->_xmlDeclaration) {
+ $this->_xmlDeclaration = $xmldec;
+ }
+ }
+
+ /**
+ * Activate or deactivate exception throwing during unknown path
+ * resolution.
+ *
+ * @return void
+ */
+ public function noThrow($bool)
+ {
+ $this->_nothrow = $bool;
+ }
+
+ /**
+ * Returns true if specified slot is filled.
+ *
+ * @return bool
+ */
+ public function hasSlot($key)
+ {
+ return isset($this->_slots[$key]) || ($this->_parentContext && $this->_parentContext->hasSlot($key));
+ }
+
+ /**
+ * Returns the content of specified filled slot.
+ *
+ * Use echoSlot() whenever you just want to output the slot
+ *
+ * @return string
+ */
+ public function getSlot($key)
+ {
+ if (isset($this->_slots[$key])) {
+ if (is_string($this->_slots[$key])) {
+ return $this->_slots[$key];
+ }
+ ob_start();
+ call_user_func($this->_slots[$key][0], $this->_slots[$key][1], $this->_slots[$key][2]);
+ return ob_get_clean();
+ } else if ($this->_parentContext) {
+ return $this->_parentContext->getSlot($key);
+ }
+ }
+
+ /**
+ * Immediately echoes content of specified filled slot.
+ *
+ * Equivalent of echo $this->getSlot();
+ *
+ * @return string
+ */
+ public function echoSlot($key)
+ {
+ if (isset($this->_slots[$key])) {
+ if (is_string($this->_slots[$key])) {
+ echo $this->_slots[$key];
+ } else {
+ call_user_func($this->_slots[$key][0], $this->_slots[$key][1], $this->_slots[$key][2]);
+ }
+ } else if ($this->_parentContext) {
+ return $this->_parentContext->echoSlot($key);
+ }
+ }
+
+ /**
+ * Fill a macro slot.
+ *
+ * @return void
+ */
+ public function fillSlot($key, $content)
+ {
+ $this->_slots[$key] = $content;
+ if ($this->_parentContext) {
+ // Works around bug with tal:define popping context after fillslot
+ $this->_parentContext->_slots[$key] = $content;
+ }
+ }
+
+ public function fillSlotCallback($key, $callback, $_thistpl, $tpl)
+ {
+ assert('is_callable($callback)');
+ $this->_slots[$key] = array($callback, $_thistpl, $tpl);
+ if ($this->_parentContext) {
+ // Works around bug with tal:define popping context after fillslot
+ $this->_parentContext->_slots[$key] = array($callback, $_thistpl, $tpl);
+ }
+ }
+
+ /**
+ * Push current filled slots on stack.
+ *
+ * @return void
+ */
+ public function pushSlots()
+ {
+ $this->_slotsStack[] = $this->_slots;
+ $this->_slots = array();
+ }
+
+ /**
+ * Restore filled slots stack.
+ *
+ * @return void
+ */
+ public function popSlots()
+ {
+ $this->_slots = array_pop($this->_slotsStack);
+ }
+
+ /**
+ * Context setter.
+ *
+ * @return void
+ */
+ public function __set($varname, $value)
+ {
+ if (preg_match('/^_|\s/', $varname)) {
+ throw new PHPTAL_InvalidVariableNameException('Template variable error \''.$varname.'\' must not begin with underscore or contain spaces');
+ }
+ $this->$varname = $value;
+ }
+
+ /**
+ * @return bool
+ */
+ public function __isset($varname)
+ {
+ // it doesn't need to check isset($this->$varname), because PHP does that _before_ calling __isset()
+ return isset($this->_globalContext->$varname) || defined($varname);
+ }
+
+ /**
+ * Context getter.
+ * If variable doesn't exist, it will throw an exception, unless noThrow(true) has been called
+ *
+ * @return mixed
+ */
+ public function __get($varname)
+ {
+ // PHP checks public properties first, there's no need to support them here
+
+ // must use isset() to allow custom global contexts with __isset()/__get()
+ if (isset($this->_globalContext->$varname)) {
+ return $this->_globalContext->$varname;
+ }
+
+ if (defined($varname)) {
+ return constant($varname);
+ }
+
+ if ($this->_nothrow) {
+ return null;
+ }
+
+ throw new PHPTAL_VariableNotFoundException("Unable to find variable '$varname' in current scope");
+ }
+
+ /**
+ * helper method for PHPTAL_Context::path()
+ *
+ * @access private
+ */
+ private static function pathError($base, $path, $current, $basename)
+ {
+ if ($current !== $path) {
+ $pathinfo = " (in path '.../$path')";
+ } else $pathinfo = '';
+
+ if (!empty($basename)) {
+ $basename = "'" . $basename . "' ";
+ }
+
+ if (is_array($base)) {
+ throw new PHPTAL_VariableNotFoundException("Array {$basename}doesn't have key named '$current'$pathinfo");
+ }
+ if (is_object($base)) {
+ throw new PHPTAL_VariableNotFoundException(ucfirst(get_class($base))." object {$basename}doesn't have method/property named '$current'$pathinfo");
+ }
+ throw new PHPTAL_VariableNotFoundException(trim("Attempt to read property '$current'$pathinfo from ".gettype($base)." value {$basename}"));
+ }
+
+ /**
+ * Resolve TALES path starting from the first path element.
+ * The TALES path : object/method1/10/method2
+ * will call : $ctx->path($ctx->object, 'method1/10/method2')
+ *
+ * This function is very important for PHPTAL performance.
+ *
+ * This function will become non-static in the future
+ *
+ * @param mixed $base first element of the path ($ctx)
+ * @param string $path rest of the path
+ * @param bool $nothrow is used by phptal_exists(). Prevents this function from
+ * throwing an exception when a part of the path cannot be resolved, null is
+ * returned instead.
+ *
+ * @access private
+ * @return mixed
+ */
+ public static function path($base, $path, $nothrow=false)
+ {
+ if ($base === null) {
+ if ($nothrow) return null;
+ PHPTAL_Context::pathError($base, $path, $path, $path);
+ }
+
+ $chunks = explode('/', $path);
+ $current = null;
+
+ for ($i = 0; $i < count($chunks); $i++) {
+ $prev = $current;
+ $current = $chunks[$i];
+
+ // object handling
+ if (is_object($base)) {
+ $base = phptal_unravel_closure($base);
+
+ // look for method. Both method_exists and is_callable are required because of __call() and protected methods
+ if (method_exists($base, $current) && is_callable(array($base, $current))) {
+ $base = $base->$current();
+ continue;
+ }
+
+ // look for property
+ if (property_exists($base, $current)) {
+ $base = $base->$current;
+ continue;
+ }
+
+ if ($base instanceof ArrayAccess && $base->offsetExists($current)) {
+ $base = $base->offsetGet($current);
+ continue;
+ }
+
+ if (($current === 'length' || $current === 'size') && $base instanceof Countable) {
+ $base = count($base);
+ continue;
+ }
+
+ // look for isset (priority over __get)
+ if (method_exists($base, '__isset')) {
+ if ($base->__isset($current)) {
+ $base = $base->$current;
+ continue;
+ }
+ }
+ // ask __get and discard if it returns null
+ elseif (method_exists($base, '__get')) {
+ $tmp = $base->$current;
+ if (null !== $tmp) {
+ $base = $tmp;
+ continue;
+ }
+ }
+
+ // magic method call
+ if (method_exists($base, '__call')) {
+ try
+ {
+ $base = $base->__call($current, array());
+ continue;
+ }
+ catch(BadMethodCallException $e) {}
+ }
+
+ if ($nothrow) {
+ return null;
+ }
+
+ PHPTAL_Context::pathError($base, $path, $current, $prev);
+ }
+
+ // array handling
+ if (is_array($base)) {
+ // key or index
+ if (array_key_exists((string)$current, $base)) {
+ $base = $base[$current];
+ continue;
+ }
+
+ // virtual methods provided by phptal
+ if ($current == 'length' || $current == 'size') {
+ $base = count($base);
+ continue;
+ }
+
+ if ($nothrow)
+ return null;
+
+ PHPTAL_Context::pathError($base, $path, $current, $prev);
+ }
+
+ // string handling
+ if (is_string($base)) {
+ // virtual methods provided by phptal
+ if ($current == 'length' || $current == 'size') {
+ $base = strlen($base);
+ continue;
+ }
+
+ // access char at index
+ if (is_numeric($current)) {
+ $base = $base[$current];
+ continue;
+ }
+ }
+
+ // if this point is reached, then the part cannot be resolved
+
+ if ($nothrow)
+ return null;
+
+ PHPTAL_Context::pathError($base, $path, $current, $prev);
+ }
+
+ return $base;
+ }
+}
+
+/**
+ * @see PHPTAL_Context::path()
+ * @deprecated
+ */
+function phptal_path($base, $path, $nothrow=false)
+{
+ return PHPTAL_Context::path($base, $path, $nothrow);
+}
+
+/**
+ * helper function for chained expressions
+ *
+ * @param mixed $var value to check
+ * @return bool
+ * @access private
+ */
+function phptal_isempty($var)
+{
+ return $var === null || $var === false || $var === ''
+ || ((is_array($var) || $var instanceof Countable) && count($var)===0);
+}
+
+/**
+ * helper function for conditional expressions
+ *
+ * @param mixed $var value to check
+ * @return bool
+ * @access private
+ */
+function phptal_true($var)
+{
+ $var = phptal_unravel_closure($var);
+ return $var && (!$var instanceof Countable || count($var));
+}
+
+/**
+ * convert to string and html-escape given value (of any type)
+ *
+ * @access private
+ */
+function phptal_escape($var, $encoding)
+{
+ if (is_string($var)) {
+ return htmlspecialchars($var, ENT_QUOTES, $encoding);
+ }
+ return htmlspecialchars(phptal_tostring($var), ENT_QUOTES, $encoding);
+}
+
+/**
+ * convert anything to string
+ *
+ * @access private
+ */
+function phptal_tostring($var)
+{
+ if (is_string($var)) {
+ return $var;
+ } elseif (is_bool($var)) {
+ return (int)$var;
+ } elseif (is_array($var)) {
+ return implode(', ', array_map('phptal_tostring', $var));
+ } elseif ($var instanceof SimpleXMLElement) {
+
+ /* There is no sane way to tell apart element and attribute nodes
+ in SimpleXML, so here's a guess that if something has no attributes
+ or children, and doesn't output <, then it's an attribute */
+
+ $xml = $var->asXML();
+ if ($xml[0] === '<' || $var->attributes() || $var->children()) {
+ return $xml;
+ }
+ }
+ return (string)phptal_unravel_closure($var);
+}
+
+/**
+ * unravel the provided expression if it is a closure
+ *
+ * This will call the base expression and its result
+ * as long as it is a Closure. Once the base (non-Closure)
+ * value is found it is returned.
+ *
+ * This function has no effect on non-Closure expressions
+ */
+function phptal_unravel_closure($var)
+{
+ while ($var instanceof Closure) {
+ $var = $var();
+ }
+ return $var;
+}
diff --git a/lib/phptal/PHPTAL/DefaultKeyword.php b/lib/phptal/PHPTAL/DefaultKeyword.php
new file mode 100644
index 0000000..28d11e2
--- /dev/null
+++ b/lib/phptal/PHPTAL/DefaultKeyword.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Andrew Crites <explosion-pills@aysites.com>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Representation of the template 'default' keyword
+ *
+ * @package PHPTAL
+ * @subpackage Keywords
+ */
+class PHPTAL_DefaultKeyword implements Countable
+{
+ public function __toString()
+ {
+ return "''";
+ }
+
+ public function count()
+ {
+ return 1;
+ }
+
+ public function jsonSerialize()
+ {
+ return new stdClass;
+ }
+}
+?>
diff --git a/lib/phptal/PHPTAL/Dom/Attr.php b/lib/phptal/PHPTAL/Dom/Attr.php
new file mode 100644
index 0000000..64ffe03
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Attr.php
@@ -0,0 +1,196 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * node that represents element's attribute
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_Attr
+{
+ private $value_escaped, $qualified_name, $namespace_uri, $encoding;
+ /**
+ * attribute's value can be overriden with a variable
+ */
+ private $phpVariable;
+ const HIDDEN = -1;
+ const NOT_REPLACED = 0;
+ const VALUE_REPLACED = 1;
+ const FULLY_REPLACED = 2;
+ private $replacedState = 0;
+
+ /**
+ * @param string $qualified_name attribute name with prefix
+ * @param string $namespace_uri full namespace URI or empty string
+ * @param string $value_escaped value with HTML-escaping
+ * @param string $encoding character encoding used by the value
+ */
+ function __construct($qualified_name, $namespace_uri, $value_escaped, $encoding)
+ {
+ $this->value_escaped = $value_escaped;
+ $this->qualified_name = $qualified_name;
+ $this->namespace_uri = $namespace_uri;
+ $this->encoding = $encoding;
+ }
+
+ /**
+ * get character encoding used by this attribute.
+ */
+ public function getEncoding()
+ {
+ return $this->encoding;
+ }
+
+ /**
+ * get full namespace URI. "" for default namespace.
+ */
+ function getNamespaceURI()
+ {
+ return $this->namespace_uri;
+ }
+
+ /**
+ * get attribute name including namespace prefix, if any
+ */
+ function getQualifiedName()
+ {
+ return $this->qualified_name;
+ }
+
+ /**
+ * get "foo" of "ns:foo" attribute name
+ */
+ function getLocalName()
+ {
+ $n = explode(':', $this->qualified_name, 2);
+ return end($n);
+ }
+
+ /**
+ * Returns true if this attribute is ns declaration (xmlns="...")
+ *
+ * @return bool
+ */
+ function isNamespaceDeclaration()
+ {
+ return preg_match('/^xmlns(?:$|:)/', $this->qualified_name);
+ }
+
+
+ /**
+ * get value as plain text
+ *
+ * @return string
+ */
+ function getValue()
+ {
+ return html_entity_decode($this->value_escaped, ENT_QUOTES, $this->encoding);
+ }
+
+ /**
+ * set plain text as value
+ */
+ function setValue($val)
+ {
+ $this->value_escaped = htmlspecialchars($val, ENT_QUOTES, $this->encoding);
+ }
+
+ /**
+ * Depends on replaced state.
+ * If value is not replaced, it will return it with HTML escapes.
+ *
+ * @see getReplacedState()
+ * @see overwriteValueWithVariable()
+ */
+ function getValueEscaped()
+ {
+ return $this->value_escaped;
+ }
+
+ /**
+ * Set value of the attribute to this exact string.
+ * String must be HTML-escaped and use attribute's encoding.
+ *
+ * @param string $value_escaped new content
+ */
+ function setValueEscaped($value_escaped)
+ {
+ $this->replacedState = self::NOT_REPLACED;
+ $this->value_escaped = $value_escaped;
+ }
+
+ /**
+ * set PHP code as value of this attribute. Code is expected to echo the value.
+ */
+ private function setPHPCode($code)
+ {
+ $this->value_escaped = '<?php '.$code." ?>\n";
+ }
+
+ /**
+ * hide this attribute. It won't be generated.
+ */
+ function hide()
+ {
+ $this->replacedState = self::HIDDEN;
+ }
+
+ /**
+ * generate value of this attribute from variable
+ */
+ function overwriteValueWithVariable($phpVariable)
+ {
+ $this->replacedState = self::VALUE_REPLACED;
+ $this->phpVariable = $phpVariable;
+ $this->setPHPCode('echo '.$phpVariable);
+ }
+
+ /**
+ * generate complete syntax of this attribute using variable
+ */
+ function overwriteFullWithVariable($phpVariable)
+ {
+ $this->replacedState = self::FULLY_REPLACED;
+ $this->phpVariable = $phpVariable;
+ $this->setPHPCode('echo '.$phpVariable);
+ }
+
+ /**
+ * use any PHP code to generate this attribute's value
+ */
+ function overwriteValueWithCode($code)
+ {
+ $this->replacedState = self::VALUE_REPLACED;
+ $this->phpVariable = null;
+ $this->setPHPCode($code);
+ }
+
+ /**
+ * if value was overwritten with variable, get its name
+ */
+ function getOverwrittenVariableName()
+ {
+ return $this->phpVariable;
+ }
+
+ /**
+ * whether getValueEscaped() returns real value or PHP code
+ */
+ function getReplacedState()
+ {
+ return $this->replacedState;
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/CDATASection.php b/lib/phptal/PHPTAL/Dom/CDATASection.php
new file mode 100644
index 0000000..838429b
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/CDATASection.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Outputs <![CDATA[ ]]> blocks, sometimes converts them to text
+ * @todo this might be moved to CDATA processing in Element
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_CDATASection extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $mode = $codewriter->getOutputMode();
+ $value = $this->getValueEscaped();
+ $inCDATAelement = PHPTAL_Dom_Defs::getInstance()->isCDATAElementInHTML($this->parentNode->getNamespaceURI(), $this->parentNode->getLocalName());
+
+ // in HTML5 must limit it to <script> and <style>
+ if ($mode === PHPTAL::HTML5 && $inCDATAelement) {
+ $codewriter->pushHTML($codewriter->interpolateCDATA(str_replace('</', '<\/', $value)));
+ } elseif (($mode === PHPTAL::XHTML && $inCDATAelement) // safe for text/html
+ || ($mode === PHPTAL::XML && preg_match('/[<>&]/', $value)) // non-useless in XML
+ || ($mode !== PHPTAL::HTML5 && preg_match('/<\?|\${structure/', $value))) // hacks with structure (in X[HT]ML) may need it
+ {
+ // in text/html "</" is dangerous and the only sensible way to escape is ECMAScript string escapes.
+ if ($mode === PHPTAL::XHTML) $value = str_replace('</', '<\/', $value);
+
+ $codewriter->pushHTML($codewriter->interpolateCDATA('<![CDATA['.$value.']]>'));
+ } else {
+ $codewriter->pushHTML($codewriter->interpolateHTML(
+ htmlspecialchars($value, ENT_QUOTES, $codewriter->getEncoding())
+ ));
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/Comment.php b/lib/phptal/PHPTAL/Dom/Comment.php
new file mode 100644
index 0000000..4a3ba3c
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Comment.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_Comment extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if (!preg_match('/^\s*!/', $this->getValueEscaped())) {
+ $codewriter->pushHTML('<!--'.$this->getValueEscaped().'-->');
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/Defs.php b/lib/phptal/PHPTAL/Dom/Defs.php
new file mode 100644
index 0000000..4d12ed6
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Defs.php
@@ -0,0 +1,246 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * PHPTAL constants.
+ *
+ * This is a pseudo singleton class, a user may decide to provide
+ * his own singleton instance which will then be used by PHPTAL.
+ *
+ * This behaviour is mainly useful to remove builtin namespaces
+ * and provide custom ones.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Dom_Defs
+{
+ /**
+ * this is a singleton
+ */
+ public static function getInstance()
+ {
+ if (!self::$_instance) {
+ self::$_instance = new PHPTAL_Dom_Defs();
+ }
+ return self::$_instance;
+ }
+
+ protected function __construct()
+ {
+ $this->registerNamespace(new PHPTAL_Namespace_TAL());
+ $this->registerNamespace(new PHPTAL_Namespace_METAL());
+ $this->registerNamespace(new PHPTAL_Namespace_I18N());
+ $this->registerNamespace(new PHPTAL_Namespace_PHPTAL());
+ }
+
+ /**
+ * true if it's empty in XHTML (e.g. <img/>)
+ * it will assume elements with no namespace may be XHTML too.
+ *
+ * @param string $tagName local name of the tag
+ *
+ * @return bool
+ */
+ public function isEmptyTagNS($namespace_uri, $local_name)
+ {
+ return ($namespace_uri === 'http://www.w3.org/1999/xhtml' || $namespace_uri === '')
+ && in_array(strtolower($local_name), self::$XHTML_EMPTY_TAGS);
+ }
+
+ /**
+ * gives namespace URI for given registered (built-in) prefix
+ */
+ public function prefixToNamespaceURI($prefix)
+ {
+ return isset($this->prefix_to_uri[$prefix]) ? $this->prefix_to_uri[$prefix] : false;
+ }
+
+ /**
+ * gives typical prefix for given (built-in) namespace
+ */
+ public function namespaceURIToPrefix($uri)
+ {
+ return array_search($uri, $this->prefix_to_uri, true);
+ }
+
+ /**
+ * array prefix => uri for prefixes that don't have to be declared in PHPTAL
+ * @return array
+ */
+ public function getPredefinedPrefixes()
+ {
+ return $this->prefix_to_uri;
+ }
+
+ /**
+ * Returns true if the attribute is an xhtml boolean attribute.
+ *
+ * @param string $att local name
+ *
+ * @return bool
+ */
+ public function isBooleanAttribute($att)
+ {
+ return in_array($att, self::$XHTML_BOOLEAN_ATTRIBUTES);
+ }
+
+ /**
+ * true if elements content is parsed as CDATA in text/html
+ * and also accepts /* * / as comments.
+ */
+ public function isCDATAElementInHTML($namespace_uri, $local_name)
+ {
+ return ($local_name === 'script' || $local_name === 'style')
+ && ($namespace_uri === 'http://www.w3.org/1999/xhtml' || $namespace_uri === '');
+ }
+
+ /**
+ * Returns true if the attribute is a valid phptal attribute
+ *
+ * Examples of valid attributes: tal:content, metal:use-slot
+ * Examples of invalid attributes: tal:unknown, metal:content
+ *
+ * @return bool
+ */
+ public function isValidAttributeNS($namespace_uri, $local_name)
+ {
+ if (!$this->isHandledNamespace($namespace_uri)) return false;
+
+ $attrs = $this->namespaces_by_uri[$namespace_uri]->getAttributes();
+ return isset($attrs[$local_name]);
+ }
+
+ /**
+ * is URI registered (built-in) namespace
+ */
+ public function isHandledNamespace($namespace_uri)
+ {
+ return isset($this->namespaces_by_uri[$namespace_uri]);
+ }
+
+ /**
+ * Returns true if the attribute is a phptal handled xml namespace
+ * declaration.
+ *
+ * Examples of handled xmlns: xmlns:tal, xmlns:metal
+ *
+ * @return bool
+ */
+ public function isHandledXmlNs($qname, $value)
+ {
+ return substr(strtolower($qname), 0, 6) == 'xmlns:' && $this->isHandledNamespace($value);
+ }
+
+ /**
+ * return objects that holds information about given TAL attribute
+ */
+ public function getNamespaceAttribute($namespace_uri, $local_name)
+ {
+ $attrs = $this->namespaces_by_uri[$namespace_uri]->getAttributes();
+ return $attrs[$local_name];
+ }
+
+ /**
+ * Register a PHPTAL_Namespace and its attribute into PHPTAL.
+ */
+ public function registerNamespace(PHPTAL_Namespace $ns)
+ {
+ $this->namespaces_by_uri[$ns->getNamespaceURI()] = $ns;
+ $this->prefix_to_uri[$ns->getPrefix()] = $ns->getNamespaceURI();
+ $prefix = strtolower($ns->getPrefix());
+ foreach ($ns->getAttributes() as $name => $attribute) {
+ $key = $prefix.':'.strtolower($name);
+ $this->_dictionary[$key] = $attribute;
+ }
+ }
+
+ private static $_instance = null;
+ private $_dictionary = array();
+ /**
+ * list of PHPTAL_Namespace objects
+ */
+ private $namespaces_by_uri = array();
+ private $prefix_to_uri = array(
+ 'xml'=>'http://www.w3.org/XML/1998/namespace',
+ 'xmlns'=>'http://www.w3.org/2000/xmlns/',
+ );
+
+ /**
+ * This array contains XHTML tags that must be echoed in a &lt;tag/&gt; form
+ * instead of the &lt;tag&gt;&lt;/tag&gt; form.
+ *
+ * In fact, some browsers does not support the later form so PHPTAL
+ * ensure these tags are correctly echoed.
+ */
+ private static $XHTML_EMPTY_TAGS = array(
+ 'area',
+ 'base',
+ 'basefont',
+ 'br',
+ 'col',
+ 'command',
+ 'embed',
+ 'frame',
+ 'hr',
+ 'img',
+ 'input',
+ 'isindex',
+ 'keygen',
+ 'link',
+ 'meta',
+ 'param',
+ 'wbr',
+ 'source',
+ 'track',
+ );
+
+ /**
+ * This array contains XHTML boolean attributes, their value is self
+ * contained (ie: they are present or not).
+ */
+ private static $XHTML_BOOLEAN_ATTRIBUTES = array(
+ 'autoplay',
+ 'async',
+ 'autofocus',
+ 'checked',
+ 'compact',
+ 'controls',
+ 'declare',
+ 'default',
+ 'defer',
+ 'disabled',
+ 'formnovalidate',
+ 'hidden',
+ 'ismap',
+ 'itemscope',
+ 'loop',
+ 'multiple',
+ 'noresize',
+ 'noshade',
+ 'novalidate',
+ 'nowrap',
+ 'open',
+ 'pubdate',
+ 'readonly',
+ 'required',
+ 'reversed',
+ 'scoped',
+ 'seamless',
+ 'selected',
+ );
+}
diff --git a/lib/phptal/PHPTAL/Dom/DocumentBuilder.php b/lib/phptal/PHPTAL/Dom/DocumentBuilder.php
new file mode 100644
index 0000000..c08587f
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/DocumentBuilder.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * DOM Builder
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+abstract class PHPTAL_Dom_DocumentBuilder
+{
+ protected $_stack; /* array<PHPTAL_Dom_Node> */
+ protected $_current; /* PHPTAL_Dom_Node */
+
+ protected $file, $line;
+
+ public function __construct()
+ {
+ $this->_stack = array();
+ }
+
+ abstract public function getResult();
+
+ abstract public function onDocumentStart();
+
+ abstract public function onDocumentEnd();
+
+ abstract public function onDocType($doctype);
+
+ abstract public function onXmlDecl($decl);
+
+ abstract public function onComment($data);
+
+ abstract public function onCDATASection($data);
+
+ abstract public function onProcessingInstruction($data);
+
+ abstract public function onElementStart($element_qname, array $attributes);
+
+ abstract public function onElementData($data);
+
+ abstract public function onElementClose($qname);
+
+ public function setSource($file, $line)
+ {
+ $this->file = $file; $this->line = $line;
+ }
+
+ abstract public function setEncoding($encoding);
+}
+
diff --git a/lib/phptal/PHPTAL/Dom/DocumentType.php b/lib/phptal/PHPTAL/Dom/DocumentType.php
new file mode 100644
index 0000000..38b49e4
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/DocumentType.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Document doctype representation.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_DocumentType extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($codewriter->getOutputMode() === PHPTAL::HTML5) {
+ $codewriter->setDocType('<!DOCTYPE html>');
+ } else {
+ $codewriter->setDocType($this->getValueEscaped());
+ }
+ $codewriter->doDoctype();
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/Element.php b/lib/phptal/PHPTAL/Dom/Element.php
new file mode 100644
index 0000000..574c830
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Element.php
@@ -0,0 +1,521 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Document Tag representation.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_Element extends PHPTAL_Dom_Node
+{
+ protected $qualifiedName, $namespace_uri;
+ private $attribute_nodes = array();
+ protected $replaceAttributes = array();
+ protected $contentAttributes = array();
+ protected $surroundAttributes = array();
+ public $headFootDisabled = false;
+ public $headPrintCondition = false;
+ public $footPrintCondition = false;
+ public $hidden = false;
+
+ // W3C DOM interface
+ public $childNodes = array();
+ public $parentNode;
+
+ /**
+ * @param string $qname qualified name of the element, e.g. "tal:block"
+ * @param string $namespace_uri namespace of this element
+ * @param array $attribute_nodes array of PHPTAL_Dom_Attr elements
+ * @param object $xmlns object that represents namespaces/prefixes known in element's context
+ */
+ public function __construct($qname, $namespace_uri, array $attribute_nodes, PHPTAL_Dom_XmlnsState $xmlns)
+ {
+ $this->qualifiedName = $qname;
+ $this->attribute_nodes = $attribute_nodes;
+ $this->namespace_uri = $namespace_uri;
+ $this->xmlns = $xmlns;
+
+ // implements inheritance of element's namespace to tal attributes (<metal: use-macro>)
+ foreach ($attribute_nodes as $index => $attr) {
+ // it'll work only when qname == localname, which is good
+ if ($this->xmlns->isValidAttributeNS($namespace_uri, $attr->getQualifiedName())) {
+ $this->attribute_nodes[$index] = new PHPTAL_Dom_Attr($attr->getQualifiedName(), $namespace_uri, $attr->getValueEscaped(), $attr->getEncoding());
+ }
+ }
+
+ if ($this->xmlns->isHandledNamespace($this->namespace_uri)) {
+ $this->headFootDisabled = true;
+ }
+
+ $talAttributes = $this->separateAttributes();
+ $this->orderTalAttributes($talAttributes);
+ }
+
+ /**
+ * returns object that represents namespaces known in element's context
+ */
+ public function getXmlnsState()
+ {
+ return $this->xmlns;
+ }
+
+ /**
+ * Replace <script> foo &gt; bar </script>
+ * with <script>/*<![CDATA[* / foo > bar /*]]>* /</script>
+ * This avoids gotcha in text/html.
+ *
+ * Note that PHPTAL_Dom_CDATASection::generate() does reverse operation, if needed!
+ *
+ * @return void
+ */
+ private function replaceTextWithCDATA()
+ {
+ $isCDATAelement = PHPTAL_Dom_Defs::getInstance()->isCDATAElementInHTML($this->getNamespaceURI(), $this->getLocalName());
+
+ if (!$isCDATAelement) {
+ return;
+ }
+
+ $valueEscaped = ''; // sometimes parser generates split text nodes. "normalisation" is needed.
+ $value = '';
+ foreach ($this->childNodes as $node) {
+ // leave it alone if there is CDATA, comment, or anything else.
+ if (!$node instanceof PHPTAL_Dom_Text) return;
+
+ $value .= $node->getValue();
+ $valueEscaped .= $node->getValueEscaped();
+
+ $encoding = $node->getEncoding(); // encoding of all nodes is the same
+ }
+
+ // only add cdata if there are entities
+ // and there's no ${structure} (because it may rely on cdata syntax)
+ if (false === strpos($valueEscaped, '&') || preg_match('/<\?|\${structure/', $value)) {
+ return;
+ }
+
+ $this->childNodes = array();
+
+ // appendChild sets parent
+ $this->appendChild(new PHPTAL_Dom_Text('/*', $encoding));
+ $this->appendChild(new PHPTAL_Dom_CDATASection('*/'.$value.'/*', $encoding));
+ $this->appendChild(new PHPTAL_Dom_Text('*/', $encoding));
+ }
+
+ public function appendChild(PHPTAL_Dom_Node $child)
+ {
+ if ($child->parentNode) $child->parentNode->removeChild($child);
+ $child->parentNode = $this;
+ $this->childNodes[] = $child;
+ }
+
+ public function removeChild(PHPTAL_Dom_Node $child)
+ {
+ foreach ($this->childNodes as $k => $node) {
+ if ($child === $node) {
+ $child->parentNode = null;
+ array_splice($this->childNodes, $k, 1);
+ return;
+ }
+ }
+ throw new PHPTAL_Exception("Given node is not child of ".$this->getQualifiedName());
+ }
+
+ public function replaceChild(PHPTAL_Dom_Node $newElement, PHPTAL_Dom_Node $oldElement)
+ {
+ foreach ($this->childNodes as $k => $node) {
+ if ($node === $oldElement) {
+ $oldElement->parentNode = NULL;
+
+ if ($newElement->parentNode) $newElement->parentNode->removeChild($child);
+ $newElement->parentNode = $this;
+
+ $this->childNodes[$k] = $newElement;
+ return;
+ }
+ }
+ throw new PHPTAL_Exception("Given node is not child of ".$this->getQualifiedName());
+ }
+
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ try
+ {
+ /// self-modifications
+
+ if ($codewriter->getOutputMode() === PHPTAL::XHTML) {
+ $this->replaceTextWithCDATA();
+ }
+
+ /// code generation
+
+ if ($this->getSourceLine()) {
+ $codewriter->doComment('tag "'.$this->qualifiedName.'" from line '.$this->getSourceLine());
+ }
+
+ $this->generateSurroundHead($codewriter);
+
+ if (count($this->replaceAttributes)) {
+ foreach ($this->replaceAttributes as $att) {
+ $att->before($codewriter);
+ $att->after($codewriter);
+ }
+ } elseif (!$this->hidden) {
+ // a surround tag may decide to hide us (tal:define for example)
+ $this->generateHead($codewriter);
+ $this->generateContent($codewriter);
+ $this->generateFoot($codewriter);
+ }
+
+ $this->generateSurroundFoot($codewriter);
+ }
+ catch(PHPTAL_TemplateException $e) {
+ $e->hintSrcPosition($this->getSourceFile(), $this->getSourceLine());
+ throw $e;
+ }
+ }
+
+ /**
+ * Array with PHPTAL_Dom_Attr objects
+ *
+ * @return array
+ */
+ public function getAttributeNodes()
+ {
+ return $this->attribute_nodes;
+ }
+
+ /**
+ * Replace all attributes
+ *
+ * @param array $nodes array of PHPTAL_Dom_Attr objects
+ */
+ public function setAttributeNodes(array $nodes)
+ {
+ $this->attribute_nodes = $nodes;
+ }
+
+ /** Returns true if the element contains specified PHPTAL attribute. */
+ public function hasAttribute($qname)
+ {
+ foreach($this->attribute_nodes as $attr) if ($attr->getQualifiedName() == $qname) return true;
+ return false;
+ }
+
+ public function hasAttributeNS($ns_uri, $localname)
+ {
+ return null !== $this->getAttributeNodeNS($ns_uri, $localname);
+ }
+
+ public function getAttributeNodeNS($ns_uri, $localname)
+ {
+ foreach ($this->attribute_nodes as $attr) {
+ if ($attr->getNamespaceURI() === $ns_uri && $attr->getLocalName() === $localname) return $attr;
+ }
+ return null;
+ }
+
+ public function removeAttributeNS($ns_uri, $localname)
+ {
+ foreach ($this->attribute_nodes as $k => $attr) {
+ if ($attr->getNamespaceURI() === $ns_uri && $attr->getLocalName() === $localname) {
+ unset($this->attribute_nodes[$k]);
+ return;
+ }
+ }
+ }
+
+ public function getAttributeNode($qname)
+ {
+ foreach($this->attribute_nodes as $attr) if ($attr->getQualifiedName() === $qname) return $attr;
+ return null;
+ }
+
+ /**
+ * If possible, use getAttributeNodeNS and setAttributeNS.
+ *
+ * NB: This method doesn't handle namespaces properly.
+ */
+ public function getOrCreateAttributeNode($qname)
+ {
+ if ($attr = $this->getAttributeNode($qname)) return $attr;
+
+ $attr = new PHPTAL_Dom_Attr($qname, "", null, 'UTF-8'); // FIXME: should find namespace and encoding
+ $this->attribute_nodes[] = $attr;
+ return $attr;
+ }
+
+ /** Returns textual (unescaped) value of specified element attribute. */
+ public function getAttributeNS($namespace_uri, $localname)
+ {
+ if ($n = $this->getAttributeNodeNS($namespace_uri, $localname)) {
+ return $n->getValue();
+ }
+ return '';
+ }
+
+ /**
+ * Set attribute value. Creates new attribute if it doesn't exist yet.
+ *
+ * @param string $namespace_uri full namespace URI. "" for default namespace
+ * @param string $qname prefixed qualified name (e.g. "atom:feed") or local name (e.g. "p")
+ * @param string $value unescaped value
+ *
+ * @return void
+ */
+ public function setAttributeNS($namespace_uri, $qname, $value)
+ {
+ $localname = preg_replace('/^[^:]*:/', '', $qname);
+ if (!($n = $this->getAttributeNodeNS($namespace_uri, $localname))) {
+ $this->attribute_nodes[] = $n = new PHPTAL_Dom_Attr($qname, $namespace_uri, null, 'UTF-8'); // FIXME: find encoding
+ }
+ $n->setValue($value);
+ }
+
+ /**
+ * Returns true if this element or one of its PHPTAL attributes has some
+ * content to print (an empty text node child does not count).
+ *
+ * @return bool
+ */
+ public function hasRealContent()
+ {
+ if (count($this->contentAttributes) > 0) return true;
+
+ foreach ($this->childNodes as $node) {
+ if (!$node instanceof PHPTAL_Dom_Text || $node->getValueEscaped() !== '') return true;
+ }
+ return false;
+ }
+
+ public function hasRealAttributes()
+ {
+ if ($this->hasAttributeNS('http://xml.zope.org/namespaces/tal', 'attributes')) return true;
+ foreach ($this->attribute_nodes as $attr) {
+ if ($attr->getReplacedState() !== PHPTAL_Dom_Attr::HIDDEN) return true;
+ }
+ return false;
+ }
+
+ // ~~~~~ Generation methods may be called by some PHPTAL attributes ~~~~~
+
+ public function generateSurroundHead(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ foreach ($this->surroundAttributes as $att) {
+ $att->before($codewriter);
+ }
+ }
+
+ public function generateHead(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($this->headFootDisabled) return;
+ if ($this->headPrintCondition) {
+ $codewriter->doIf($this->headPrintCondition);
+ }
+
+ $html5mode = ($codewriter->getOutputMode() === PHPTAL::HTML5);
+
+ if ($html5mode) {
+ $codewriter->pushHTML('<'.$this->getLocalName());
+ } else {
+ $codewriter->pushHTML('<'.$this->qualifiedName);
+ }
+
+ $this->generateAttributes($codewriter);
+
+ if (!$html5mode && $this->isEmptyNode($codewriter->getOutputMode())) {
+ $codewriter->pushHTML('/>');
+ } else {
+ $codewriter->pushHTML('>');
+ }
+
+ if ($this->headPrintCondition) {
+ $codewriter->doEnd('if');
+ }
+ }
+
+ public function generateContent(PHPTAL_Php_CodeWriter $codewriter = null, $realContent=false)
+ {
+ if (!$this->isEmptyNode($codewriter->getOutputMode())) {
+ if ($realContent || !count($this->contentAttributes)) {
+ foreach($this->childNodes as $child) {
+ $child->generateCode($codewriter);
+ }
+ }
+ else foreach($this->contentAttributes as $att) {
+ $att->before($codewriter);
+ $att->after($codewriter);
+ }
+ }
+ }
+
+ public function generateFoot(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($this->headFootDisabled)
+ return;
+ if ($this->isEmptyNode($codewriter->getOutputMode()))
+ return;
+
+ if ($this->footPrintCondition) {
+ $codewriter->doIf($this->footPrintCondition);
+ }
+
+ if ($codewriter->getOutputMode() === PHPTAL::HTML5) {
+ $codewriter->pushHTML('</'.$this->getLocalName().'>');
+ } else {
+ $codewriter->pushHTML('</'.$this->getQualifiedName().'>');
+ }
+
+ if ($this->footPrintCondition) {
+ $codewriter->doEnd('if');
+ }
+ }
+
+ public function generateSurroundFoot(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ for ($i = (count($this->surroundAttributes)-1); $i >= 0; $i--) {
+ $this->surroundAttributes[$i]->after($codewriter);
+ }
+ }
+
+ // ~~~~~ Private members ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ private function generateAttributes(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $html5mode = ($codewriter->getOutputMode() === PHPTAL::HTML5);
+
+ foreach ($this->getAttributeNodes() as $attr) {
+
+ // xmlns:foo is not allowed in text/html
+ if ($html5mode && $attr->isNamespaceDeclaration()) {
+ continue;
+ }
+
+ switch ($attr->getReplacedState()) {
+ case PHPTAL_Dom_Attr::NOT_REPLACED:
+ $codewriter->pushHTML(' '.$attr->getQualifiedName());
+ if ($codewriter->getOutputMode() !== PHPTAL::HTML5
+ || !PHPTAL_Dom_Defs::getInstance()->isBooleanAttribute($attr->getQualifiedName())) {
+ $html = $codewriter->interpolateHTML($attr->getValueEscaped());
+ $codewriter->pushHTML('='.$codewriter->quoteAttributeValue($html));
+ }
+ break;
+
+ case PHPTAL_Dom_Attr::HIDDEN:
+ break;
+
+ case PHPTAL_Dom_Attr::FULLY_REPLACED:
+ $codewriter->pushHTML($attr->getValueEscaped());
+ break;
+
+ case PHPTAL_Dom_Attr::VALUE_REPLACED:
+ $codewriter->pushHTML(' '.$attr->getQualifiedName().'="');
+ $codewriter->pushHTML($attr->getValueEscaped());
+ $codewriter->pushHTML('"');
+ break;
+ }
+ }
+ }
+
+ private function isEmptyNode($mode)
+ {
+ return (($mode === PHPTAL::XHTML || $mode === PHPTAL::HTML5) && PHPTAL_Dom_Defs::getInstance()->isEmptyTagNS($this->getNamespaceURI(), $this->getLocalName())) ||
+ ( $mode === PHPTAL::XML && !$this->hasContent());
+ }
+
+ private function hasContent()
+ {
+ return count($this->childNodes) > 0 || count($this->contentAttributes) > 0;
+ }
+
+ private function separateAttributes()
+ {
+ $talAttributes = array();
+ foreach ($this->attribute_nodes as $index => $attr) {
+ // remove handled xml namespaces
+ if (PHPTAL_Dom_Defs::getInstance()->isHandledXmlNs($attr->getQualifiedName(), $attr->getValueEscaped())) {
+ unset($this->attribute_nodes[$index]);
+ } else if ($this->xmlns->isHandledNamespace($attr->getNamespaceURI())) {
+ $talAttributes[$attr->getQualifiedName()] = $attr;
+ $attr->hide();
+ } else if (PHPTAL_Dom_Defs::getInstance()->isBooleanAttribute($attr->getQualifiedName())) {
+ $attr->setValue($attr->getLocalName());
+ }
+ }
+ return $talAttributes;
+ }
+
+ private function orderTalAttributes(array $talAttributes)
+ {
+ $temp = array();
+ foreach ($talAttributes as $key => $domattr) {
+ $nsattr = PHPTAL_Dom_Defs::getInstance()->getNamespaceAttribute($domattr->getNamespaceURI(), $domattr->getLocalName());
+ if (array_key_exists($nsattr->getPriority(), $temp)) {
+ throw new PHPTAL_TemplateException(sprintf("Attribute conflict in < %s > '%s' cannot appear with '%s'",
+ $this->qualifiedName,
+ $key,
+ $temp[$nsattr->getPriority()][0]->getNamespace()->getPrefix() . ':' . $temp[$nsattr->getPriority()][0]->getLocalName()
+ ), $this->getSourceFile(), $this->getSourceLine());
+ }
+ $temp[$nsattr->getPriority()] = array($nsattr, $domattr);
+ }
+ ksort($temp);
+
+ $this->talHandlers = array();
+ foreach ($temp as $prio => $dat) {
+ list($nsattr, $domattr) = $dat;
+ $handler = $nsattr->createAttributeHandler($this, $domattr->getValue());
+ $this->talHandlers[$prio] = $handler;
+
+ if ($nsattr instanceof PHPTAL_NamespaceAttributeSurround)
+ $this->surroundAttributes[] = $handler;
+ else if ($nsattr instanceof PHPTAL_NamespaceAttributeReplace)
+ $this->replaceAttributes[] = $handler;
+ else if ($nsattr instanceof PHPTAL_NamespaceAttributeContent)
+ $this->contentAttributes[] = $handler;
+ else
+ throw new PHPTAL_ParserException("Unknown namespace attribute class ".get_class($nsattr),
+ $this->getSourceFile(), $this->getSourceLine());
+
+ }
+ }
+
+ function getQualifiedName()
+ {
+ return $this->qualifiedName;
+ }
+
+ function getNamespaceURI()
+ {
+ return $this->namespace_uri;
+ }
+
+ function getLocalName()
+ {
+ $n = explode(':', $this->qualifiedName, 2);
+ return end($n);
+ }
+
+ function __toString()
+ {
+ return '<{'.$this->getNamespaceURI().'}:'.$this->getLocalName().'>';
+ }
+
+ function setValueEscaped($e) {
+ throw new PHPTAL_Exception("Not supported");
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/Node.php b/lib/phptal/PHPTAL/Dom/Node.php
new file mode 100644
index 0000000..5858df6
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Node.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Document node abstract class.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+abstract class PHPTAL_Dom_Node
+{
+ public $parentNode;
+
+ private $value_escaped, $source_file, $source_line, $encoding;
+
+ public function __construct($value_escaped, $encoding)
+ {
+ $this->value_escaped = $value_escaped;
+ $this->encoding = $encoding;
+ }
+
+ /**
+ * hint where this node is in source code
+ */
+ public function setSource($file, $line)
+ {
+ $this->source_file = $file;
+ $this->source_line = $line;
+ }
+
+ /**
+ * file from which this node comes from
+ */
+ public function getSourceFile()
+ {
+ return $this->source_file;
+ }
+
+ /**
+ * line on which this node was defined
+ */
+ public function getSourceLine()
+ {
+ return $this->source_line;
+ }
+
+ /**
+ * depends on node type. Value will be escaped according to context that node comes from.
+ */
+ function getValueEscaped()
+ {
+ return $this->value_escaped;
+ }
+
+ /**
+ * Set value of the node (type-dependent) to this exact string.
+ * String must be HTML-escaped and use node's encoding.
+ *
+ * @param string $value_escaped new content
+ */
+ function setValueEscaped($value_escaped)
+ {
+ $this->value_escaped = $value_escaped;
+ }
+
+
+ /**
+ * get value as plain text. Depends on node type.
+ */
+ function getValue()
+ {
+ return html_entity_decode($this->getValueEscaped(), ENT_QUOTES, $this->encoding);
+ }
+
+ /**
+ * encoding used by vaule of this node.
+ */
+ public function getEncoding()
+ {
+ return $this->encoding;
+ }
+
+ /**
+ * use CodeWriter to compile this element to PHP code
+ */
+ public abstract function generateCode(PHPTAL_Php_CodeWriter $gen);
+
+ function __toString()
+ {
+ return " “".$this->getValue()."” ";
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Dom/PHPTALDocumentBuilder.php b/lib/phptal/PHPTAL/Dom/PHPTALDocumentBuilder.php
new file mode 100644
index 0000000..a3157be
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/PHPTALDocumentBuilder.php
@@ -0,0 +1,167 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * DOM Builder
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_PHPTALDocumentBuilder extends PHPTAL_Dom_DocumentBuilder
+{
+ private $_xmlns; /* PHPTAL_Dom_XmlnsState */
+ private $encoding;
+
+ public function __construct()
+ {
+ $this->_xmlns = new PHPTAL_Dom_XmlnsState(array(), '');
+ }
+
+ public function getResult()
+ {
+ return $this->documentElement;
+ }
+
+ protected function getXmlnsState()
+ {
+ return $this->_xmlns;
+ }
+
+ // ~~~~~ XmlParser implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ public function onDocumentStart()
+ {
+ $this->documentElement = new PHPTAL_Dom_Element('documentElement', 'http://xml.zope.org/namespaces/tal', array(), $this->getXmlnsState());
+ $this->documentElement->setSource($this->file, $this->line);
+ $this->_current = $this->documentElement;
+ }
+
+ public function onDocumentEnd()
+ {
+ if (count($this->_stack) > 0) {
+ $left='</'.$this->_current->getQualifiedName().'>';
+ for ($i = count($this->_stack)-1; $i>0; $i--) $left .= '</'.$this->_stack[$i]->getQualifiedName().'>';
+ throw new PHPTAL_ParserException("Not all elements were closed before end of the document. Missing: ".$left,
+ $this->file, $this->line);
+ }
+ }
+
+ public function onDocType($doctype)
+ {
+ $this->pushNode(new PHPTAL_Dom_DocumentType($doctype, $this->encoding));
+ }
+
+ public function onXmlDecl($decl)
+ {
+ if (!$this->encoding) {
+ throw new PHPTAL_Exception("Encoding not set");
+ }
+ $this->pushNode(new PHPTAL_Dom_XmlDeclaration($decl, $this->encoding));
+ }
+
+ public function onComment($data)
+ {
+ $this->pushNode(new PHPTAL_Dom_Comment($data, $this->encoding));
+ }
+
+ public function onCDATASection($data)
+ {
+ $this->pushNode(new PHPTAL_Dom_CDATASection($data, $this->encoding));
+ }
+
+ public function onProcessingInstruction($data)
+ {
+ $this->pushNode(new PHPTAL_Dom_ProcessingInstruction($data, $this->encoding));
+ }
+
+ public function onElementStart($element_qname, array $attributes)
+ {
+ $this->_xmlns = $this->_xmlns->newElement($attributes);
+
+ if (preg_match('/^([^:]+):/', $element_qname, $m)) {
+ $prefix = $m[1];
+ $namespace_uri = $this->_xmlns->prefixToNamespaceURI($prefix);
+ if (false === $namespace_uri) {
+ throw new PHPTAL_ParserException("There is no namespace declared for prefix of element < $element_qname >. You must have xmlns:$prefix declaration in the same document.",
+ $this->file, $this->line);
+ }
+ } else {
+ $namespace_uri = $this->_xmlns->getCurrentDefaultNamespaceURI();
+ }
+
+ $attrnodes = array();
+ foreach ($attributes as $qname=>$value) {
+
+ if (preg_match('/^([^:]+):(.+)$/', $qname, $m)) {
+ list(,$prefix, $local_name) = $m;
+ $attr_namespace_uri = $this->_xmlns->prefixToNamespaceURI($prefix);
+
+ if (false === $attr_namespace_uri) {
+ throw new PHPTAL_ParserException("There is no namespace declared for prefix of attribute $qname of element < $element_qname >. You must have xmlns:$prefix declaration in the same document.",
+ $this->file, $this->line);
+ }
+ } else {
+ $local_name = $qname;
+ $attr_namespace_uri = ''; // default NS. Attributes don't inherit namespace per XMLNS spec
+ }
+
+ if ($this->_xmlns->isHandledNamespace($attr_namespace_uri)
+ && !$this->_xmlns->isValidAttributeNS($attr_namespace_uri, $local_name)) {
+ throw new PHPTAL_ParserException("Attribute '$qname' is in '$attr_namespace_uri' namespace, but is not a supported PHPTAL attribute",
+ $this->file, $this->line);
+ }
+
+ $attrnodes[] = new PHPTAL_Dom_Attr($qname, $attr_namespace_uri, $value, $this->encoding);
+ }
+
+ $node = new PHPTAL_Dom_Element($element_qname, $namespace_uri, $attrnodes, $this->getXmlnsState());
+ $this->pushNode($node);
+ $this->_stack[] = $this->_current;
+ $this->_current = $node;
+ }
+
+ public function onElementData($data)
+ {
+ $this->pushNode(new PHPTAL_Dom_Text($data, $this->encoding));
+ }
+
+ public function onElementClose($qname)
+ {
+ if ($this->_current === $this->documentElement) {
+ throw new PHPTAL_ParserException("Found closing tag for < $qname > where there are no open tags",
+ $this->file, $this->line);
+ }
+ if ($this->_current->getQualifiedName() != $qname) {
+ throw new PHPTAL_ParserException("Tag closure mismatch, expected < /".$this->_current->getQualifiedName()." > (opened in line ".$this->_current->getSourceLine().") but found < /".$qname." >",
+ $this->file, $this->line);
+ }
+ $this->_current = array_pop($this->_stack);
+ if ($this->_current instanceof PHPTAL_Dom_Element) {
+ $this->_xmlns = $this->_current->getXmlnsState(); // restore namespace prefixes info to previous state
+ }
+ }
+
+ private function pushNode(PHPTAL_Dom_Node $node)
+ {
+ $node->setSource($this->file, $this->line);
+ $this->_current->appendChild($node);
+ }
+
+ public function setEncoding($encoding)
+ {
+ $this->encoding = $encoding;
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/ProcessingInstruction.php b/lib/phptal/PHPTAL/Dom/ProcessingInstruction.php
new file mode 100644
index 0000000..552462c
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/ProcessingInstruction.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * processing instructions, including <?php blocks
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_ProcessingInstruction extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if (preg_match('/^<\?(?:php|[=\s])/i', $this->getValueEscaped())) {
+ // block will be executed as PHP
+ $codewriter->pushHTML($this->getValueEscaped());
+ } else {
+ $codewriter->doEchoRaw("'<'");
+ $codewriter->pushHTML(substr($codewriter->interpolateHTML($this->getValueEscaped()), 1));
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/SaxXmlParser.php b/lib/phptal/PHPTAL/Dom/SaxXmlParser.php
new file mode 100644
index 0000000..b59a26d
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/SaxXmlParser.php
@@ -0,0 +1,480 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Simple sax like xml parser for PHPTAL
+ * ("Dom" in the class name comes from name of the directory, not mode of operation)
+ *
+ * At the time this parser was created, standard PHP libraries were not suitable
+ * (could not retrieve doctypes, xml declaration, problems with comments and CDATA).
+ *
+ * There are still some problems: XML parsers don't care about exact format of enties
+ * or CDATA sections (PHPTAL tries to preserve them),
+ * <?php ?> blocks are not allowed in attributes.
+ *
+ * This parser failed to enforce some XML well-formedness constraints,
+ * and there are ill-formed templates "in the wild" because of this.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ * @see PHPTAL_DOM_DocumentBuilder
+ */
+class PHPTAL_Dom_SaxXmlParser
+{
+ private $_file;
+ private $_line;
+ private $_source;
+
+ // available parser states
+ const ST_ROOT = 0;
+ const ST_TEXT = 1;
+ const ST_LT = 2;
+ const ST_TAG_NAME = 3;
+ const ST_TAG_CLOSE = 4;
+ const ST_TAG_SINGLE = 5;
+ const ST_TAG_ATTRIBUTES = 6;
+ const ST_TAG_BETWEEN_ATTRIBUTE = 7;
+ const ST_CDATA = 8;
+ const ST_COMMENT = 9;
+ const ST_DOCTYPE = 10;
+ const ST_XMLDEC = 11;
+ const ST_PREPROC = 12;
+ const ST_ATTR_KEY = 13;
+ const ST_ATTR_EQ = 14;
+ const ST_ATTR_QUOTE = 15;
+ const ST_ATTR_VALUE = 16;
+
+ const BOM_STR = "\xef\xbb\xbf";
+
+
+ static $state_names = array(
+ self::ST_ROOT => 'root node',
+ self::ST_TEXT => 'text',
+ self::ST_LT => 'start of tag',
+ self::ST_TAG_NAME => 'tag name',
+ self::ST_TAG_CLOSE => 'closing tag',
+ self::ST_TAG_SINGLE => 'self-closing tag',
+ self::ST_TAG_ATTRIBUTES => 'tag',
+ self::ST_TAG_BETWEEN_ATTRIBUTE => 'tag attributes',
+ self::ST_CDATA => 'CDATA',
+ self::ST_COMMENT => 'comment',
+ self::ST_DOCTYPE => 'doctype',
+ self::ST_XMLDEC => 'XML declaration',
+ self::ST_PREPROC => 'preprocessor directive',
+ self::ST_ATTR_KEY => 'attribute name',
+ self::ST_ATTR_EQ => 'attribute value',
+ self::ST_ATTR_QUOTE => 'quoted attribute value',
+ self::ST_ATTR_VALUE => 'unquoted attribute value',
+ );
+
+ private $input_encoding;
+ public function __construct($input_encoding)
+ {
+ $this->input_encoding = $input_encoding;
+ $this->_file = "<string>";
+ }
+
+ public function parseFile(PHPTAL_Dom_DocumentBuilder $builder, $src)
+ {
+ if (!file_exists($src)) {
+ throw new PHPTAL_IOException("file $src not found");
+ }
+ return $this->parseString($builder, file_get_contents($src), $src);
+ }
+
+ public function parseString(PHPTAL_Dom_DocumentBuilder $builder, $src, $filename = '<string>')
+ {
+ try
+ {
+ $builder->setEncoding($this->input_encoding);
+ $this->_file = $filename;
+
+ $this->_line = 1;
+ $state = self::ST_ROOT;
+ $mark = 0;
+ $len = strlen($src);
+
+ $quoteStyle = '"';
+ $tagname = "";
+ $attribute = "";
+ $attributes = array();
+
+ $customDoctype = false;
+
+ $builder->setSource($this->_file, $this->_line);
+ $builder->onDocumentStart();
+
+ $i=0;
+ // remove BOM (UTF-8 byte order mark)...
+ if (substr($src, 0, 3) === self::BOM_STR) {
+ $i=3;
+ }
+ for (; $i<$len; $i++) {
+ $c = $src[$i]; // Change to substr($src, $i, 1); if you want to use mb_string.func_overload
+
+ if ($c === "\n") $builder->setSource($this->_file, ++$this->_line);
+
+ switch ($state) {
+ case self::ST_ROOT:
+ if ($c === '<') {
+ $mark = $i; // mark tag start
+ $state = self::ST_LT;
+ } elseif (!self::isWhiteChar($c)) {
+ $this->raiseError("Characters found before beginning of the document! (wrap document in < tal:block > to avoid this error)");
+ }
+ break;
+
+ case self::ST_TEXT:
+ if ($c === '<') {
+ if ($mark != $i) {
+ $builder->onElementData($this->sanitizeEscapedText($this->checkEncoding(substr($src, $mark, $i-$mark))));
+ }
+ $mark = $i;
+ $state = self::ST_LT;
+ }
+ break;
+
+ case self::ST_LT:
+ if ($c === '/') {
+ $mark = $i+1;
+ $state = self::ST_TAG_CLOSE;
+ } elseif ($c === '?' and strtolower(substr($src, $i, 5)) === '?xml ') {
+ $state = self::ST_XMLDEC;
+ } elseif ($c === '?') {
+ $state = self::ST_PREPROC;
+ } elseif ($c === '!' and substr($src, $i, 3) === '!--') {
+ $state = self::ST_COMMENT;
+ } elseif ($c === '!' and substr($src, $i, 8) === '![CDATA[') {
+ $state = self::ST_CDATA;
+ $mark = $i+8; // past opening tag
+ } elseif ($c === '!' and strtoupper(substr($src, $i, 8)) === '!DOCTYPE') {
+ $state = self::ST_DOCTYPE;
+ } elseif (self::isWhiteChar($c)) {
+ $state = self::ST_TEXT;
+ } else {
+ $mark = $i; // mark node name start
+ $attributes = array();
+ $attribute = "";
+ $state = self::ST_TAG_NAME;
+ }
+ break;
+
+ case self::ST_TAG_NAME:
+ if (self::isWhiteChar($c) || $c === '/' || $c === '>') {
+ $tagname = substr($src, $mark, $i-$mark);
+ if (!$this->isValidQName($tagname)) $this->raiseError("Invalid tag name '$tagname'");
+
+ if ($c === '/') {
+ $state = self::ST_TAG_SINGLE;
+ } elseif ($c === '>') {
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ $builder->onElementStart($tagname, $attributes);
+ } else /* isWhiteChar */ {
+ $state = self::ST_TAG_ATTRIBUTES;
+ }
+ }
+ break;
+
+ case self::ST_TAG_CLOSE:
+ if ($c === '>') {
+ $tagname = rtrim(substr($src, $mark, $i-$mark));
+ $builder->onElementClose($tagname);
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_TAG_SINGLE:
+ if ($c !== '>') {
+ $this->raiseError("Expected '/>', but found '/$c' inside tag < $tagname >");
+ }
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ $builder->onElementStart($tagname, $attributes);
+ $builder->onElementClose($tagname);
+ break;
+
+ case self::ST_TAG_BETWEEN_ATTRIBUTE:
+ case self::ST_TAG_ATTRIBUTES:
+ if ($c === '>') {
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ $builder->onElementStart($tagname, $attributes);
+ } elseif ($c === '/') {
+ $state = self::ST_TAG_SINGLE;
+ } elseif (self::isWhiteChar($c)) {
+ $state = self::ST_TAG_ATTRIBUTES;
+ } elseif ($state === self::ST_TAG_ATTRIBUTES && $this->isValidQName($c)) {
+ $mark = $i; // mark attribute key start
+ $state = self::ST_ATTR_KEY;
+ } else $this->raiseError("Unexpected character '$c' between attributes of < $tagname >");
+ break;
+
+ case self::ST_COMMENT:
+ if ($c === '>' && $i > $mark+4 && substr($src, $i-2, 2) === '--') {
+
+ if (preg_match('/^-|--|-$/', substr($src, $mark +4, $i-$mark+1 -7))) {
+ $this->raiseError("Ill-formed comment. XML comments are not allowed to contain '--' or start/end with '-': ".substr($src, $mark+4, $i-$mark+1-7));
+ }
+
+ $builder->onComment($this->checkEncoding(substr($src, $mark+4, $i-$mark+1-7)));
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_CDATA:
+ if ($c === '>' and substr($src, $i-2, 2) === ']]') {
+ $builder->onCDATASection($this->checkEncoding(substr($src, $mark, $i-$mark-2)));
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_XMLDEC:
+ if ($c === '?' && substr($src, $i, 2) === '?>') {
+ $builder->onXmlDecl($this->checkEncoding(substr($src, $mark, $i-$mark+2)));
+ $i++; // skip '>'
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_DOCTYPE:
+ if ($c === '[') {
+ $customDoctype = true;
+ } elseif ($customDoctype && $c === '>' && substr($src, $i-1, 2) === ']>') {
+ $customDoctype = false;
+ $builder->onDocType($this->checkEncoding(substr($src, $mark, $i-$mark+1)));
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ } elseif (!$customDoctype && $c === '>') {
+ $customDoctype = false;
+ $builder->onDocType($this->checkEncoding(substr($src, $mark, $i-$mark+1)));
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_PREPROC:
+ if ($c === '>' and substr($src, $i-1, 1) === '?') {
+ $builder->onProcessingInstruction($this->checkEncoding(substr($src, $mark, $i-$mark+1)));
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_ATTR_KEY:
+ if ($c === '=' || self::isWhiteChar($c)) {
+ $attribute = substr($src, $mark, $i-$mark);
+ if (!$this->isValidQName($attribute)) {
+ $this->raiseError("Invalid attribute name '$attribute' in < $tagname >");
+ }
+ if (isset($attributes[$attribute])) {
+ $this->raiseError("Attribute $attribute in < $tagname > is defined more than once");
+ }
+
+ if ($c === '=') $state = self::ST_ATTR_VALUE;
+ else /* white char */ $state = self::ST_ATTR_EQ;
+ } elseif ($c === '/' || $c==='>') {
+ $attribute = substr($src, $mark, $i-$mark);
+ if (!$this->isValidQName($attribute)) {
+ $this->raiseError("Invalid attribute name '$attribute'");
+ }
+ $this->raiseError("Attribute $attribute does not have value (found end of tag instead of '=')");
+ }
+ break;
+
+ case self::ST_ATTR_EQ:
+ if ($c === '=') {
+ $state = self::ST_ATTR_VALUE;
+ } elseif (!self::isWhiteChar($c)) {
+ $this->raiseError("Attribute $attribute in < $tagname > does not have value (found character '$c' instead of '=')");
+ }
+ break;
+
+ case self::ST_ATTR_VALUE:
+ if (self::isWhiteChar($c)) {
+ } elseif ($c === '"' or $c === '\'') {
+ $quoteStyle = $c;
+ $state = self::ST_ATTR_QUOTE;
+ $mark = $i+1; // mark attribute real value start
+ } else {
+ $this->raiseError("Value of attribute $attribute in < $tagname > is not in quotes (found character '$c' instead of quote)");
+ }
+ break;
+
+ case self::ST_ATTR_QUOTE:
+ if ($c === $quoteStyle) {
+ $attributes[$attribute] = $this->sanitizeEscapedText($this->checkEncoding(substr($src, $mark, $i-$mark)));
+
+ // PHPTAL's code generator assumes input is escaped for double-quoted strings. Single-quoted attributes need to be converted.
+ // FIXME: it should be escaped at later stage.
+ $attributes[$attribute] = str_replace('"',"&quot;", $attributes[$attribute]);
+ $state = self::ST_TAG_BETWEEN_ATTRIBUTE;
+ }
+ break;
+ }
+ }
+
+ if ($state === self::ST_TEXT) // allows text past root node, which is in violation of XML spec
+ {
+ if ($i > $mark) {
+ $text = substr($src, $mark, $i-$mark);
+ if (!ctype_space($text)) $this->raiseError("Characters found after end of the root element (wrap document in < tal:block > to avoid this error)");
+ }
+ } else {
+ if ($state === self::ST_ROOT) {
+ $msg = "Document does not have any tags";
+ } else {
+ $msg = "Finished document in unexpected state: ".self::$state_names[$state]." is not finished";
+ }
+ $this->raiseError($msg);
+ }
+
+ $builder->onDocumentEnd();
+ }
+ catch(PHPTAL_TemplateException $e)
+ {
+ $e->hintSrcPosition($this->_file, $this->_line);
+ throw $e;
+ }
+ return $builder;
+ }
+
+ private function isValidQName($name)
+ {
+ $name = $this->checkEncoding($name);
+ return preg_match('/^([a-z_\x80-\xff]+[a-z0-9._\x80-\xff-]*:)?[a-z_\x80-\xff]+[a-z0-9._\x80-\xff-]*$/i', $name);
+ }
+
+ private function checkEncoding($str)
+ {
+ if ($str === '') return '';
+
+ if ($this->input_encoding === 'UTF-8') {
+
+ // $match expression below somehow triggers quite deep recurrency and stack overflow in preg
+ // to avoid this, check string bit by bit, omitting ASCII fragments.
+ if (strlen($str) > 200) {
+ $chunks = preg_split('/(?>[\x09\x0A\x0D\x20-\x7F]+)/',$str,null,PREG_SPLIT_NO_EMPTY);
+ foreach ($chunks as $chunk) {
+ if (strlen($chunk) < 200) {
+ $this->checkEncoding($chunk);
+ }
+ }
+ return $str;
+ }
+
+ // http://www.w3.org/International/questions/qa-forms-utf-8
+ $match = '[\x09\x0A\x0D\x20-\x7F]' // ASCII
+ . '|[\xC2-\xDF][\x80-\xBF]' // non-overlong 2-byte
+ . '|\xE0[\xA0-\xBF][\x80-\xBF]' // excluding overlongs
+ . '|[\xE1-\xEC\xEE\xEE][\x80-\xBF]{2}' // straight 3-byte (exclude FFFE and FFFF)
+ . '|\xEF[\x80-\xBE][\x80-\xBF]' // straight 3-byte
+ . '|\xEF\xBF[\x80-\xBD]' // straight 3-byte
+ . '|\xED[\x80-\x9F][\x80-\xBF]' // excluding surrogates
+ . '|\xF0[\x90-\xBF][\x80-\xBF]{2}' // planes 1-3
+ . '|[\xF1-\xF3][\x80-\xBF]{3}' // planes 4-15
+ . '|\xF4[\x80-\x8F][\x80-\xBF]{2}'; // plane 16
+
+ if (!preg_match('/^(?:(?>'.$match.'))+$/s',$str)) {
+ $res = preg_split('/((?>'.$match.')+)/s',$str,null,PREG_SPLIT_DELIM_CAPTURE);
+ for($i=0; $i < count($res); $i+=2)
+ {
+ $res[$i] = self::convertBytesToEntities(array(1=>$res[$i]));
+ }
+ $this->raiseError("Invalid UTF-8 bytes: ".implode('', $res));
+ }
+ }
+ if ($this->input_encoding === 'ISO-8859-1') {
+
+ // http://www.w3.org/TR/2006/REC-xml11-20060816/#NT-RestrictedChar
+ $forbid = '/((?>[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F]+))/s';
+
+ if (preg_match($forbid, $str)) {
+ $str = preg_replace_callback($forbid, array('self', 'convertBytesToEntities'), $str);
+ $this->raiseError("Invalid ISO-8859-1 characters: ".$str);
+ }
+ }
+
+ return $str;
+ }
+
+ /**
+ * preg callback
+ * Changes all bytes to hexadecimal XML entities
+ *
+ * @param array $m first array element is used for input
+ *
+ * @return string
+ */
+ private static function convertBytesToEntities(array $m)
+ {
+ $m = $m[1]; $out = '';
+ for($i=0; $i < strlen($m); $i++)
+ {
+ $out .= '&#X'.strtoupper(dechex(ord($m[$i]))).';';
+ }
+ return $out;
+ }
+
+ /**
+ * This is where this parser violates XML and refuses to be an annoying bastard.
+ */
+ private function sanitizeEscapedText($str)
+ {
+ $str = str_replace('&apos;', '&#39;', $str); // PHP's html_entity_decode doesn't seem to support that!
+
+ /* <?php ?> blocks can't reliably work in attributes (due to escaping impossible in XML)
+ so they have to be converted into special TALES expression
+ */
+ $types = ini_get('short_open_tag')?'php|=|':'php';
+ $str = preg_replace_callback("/<\?($types)(.*?)\?>/", array('self', 'convertPHPBlockToTALES'), $str);
+
+ // corrects all non-entities and neutralizes potentially problematic CDATA end marker
+ $str = strtr(preg_replace('/&(?!(?:#x?[a-f0-9]+|[a-z][a-z0-9]*);)/i', '&amp;', $str), array('<'=>'&lt;', ']]>'=>']]&gt;'));
+
+ return $str;
+ }
+
+ private static function convertPHPBlockToTALES($m)
+ {
+ list(, $type, $code) = $m;
+ if ($type === '=') $code = 'echo '.$code;
+ return '${structure phptal-internal-php-block:'.rawurlencode($code).'}';
+ }
+
+ public function getSourceFile()
+ {
+ return $this->_file;
+ }
+
+ public function getLineNumber()
+ {
+ return $this->_line;
+ }
+
+ public static function isWhiteChar($c)
+ {
+ return strpos(" \t\n\r\0", $c) !== false;
+ }
+
+ protected function raiseError($errStr)
+ {
+ throw new PHPTAL_ParserException($errStr, $this->_file, $this->_line);
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/Text.php b/lib/phptal/PHPTAL/Dom/Text.php
new file mode 100644
index 0000000..f8ef2ab
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Text.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Document text data representation.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_Text extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($this->getValueEscaped() !== '') {
+ $codewriter->pushHTML($codewriter->interpolateHTML($this->getValueEscaped()));
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/XmlDeclaration.php b/lib/phptal/PHPTAL/Dom/XmlDeclaration.php
new file mode 100644
index 0000000..e28dfb9
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/XmlDeclaration.php
@@ -0,0 +1,29 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * XML declaration node.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_XmlDeclaration extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->setXmlDeclaration($this->getValueEscaped());
+ $codewriter->doXmlDeclaration();
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/XmlnsState.php b/lib/phptal/PHPTAL/Dom/XmlnsState.php
new file mode 100644
index 0000000..4e9288f
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/XmlnsState.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+/**
+ * Stores XMLNS aliases fluctuation in the xml flow.
+ *
+ * This class is used to bind a PHPTAL namespace to an alias, for example using
+ * xmlns:t="http://xml.zope.org/namespaces/tal" and later use t:repeat instead
+ * of tal:repeat.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Dom_XmlnsState
+{
+ /** Create a new XMLNS state inheriting provided aliases. */
+ public function __construct(array $prefix_to_uri, $current_default)
+ {
+ $this->prefix_to_uri = $prefix_to_uri;
+ $this->current_default = $current_default;
+ }
+
+ public function prefixToNamespaceURI($prefix)
+ {
+ if ($prefix === 'xmlns') return 'http://www.w3.org/2000/xmlns/';
+ if ($prefix === 'xml') return 'http://www.w3.org/XML/1998/namespace';
+
+ // domdefs provides fallback for all known phptal ns
+ if (isset($this->prefix_to_uri[$prefix])) {
+ return $this->prefix_to_uri[$prefix];
+ } else {
+ return PHPTAL_Dom_Defs::getInstance()->prefixToNamespaceURI($prefix);
+ }
+ }
+
+ /** Returns true if $attName is a valid attribute name, false otherwise. */
+ public function isValidAttributeNS($namespace_uri, $local_name)
+ {
+ return PHPTAL_Dom_Defs::getInstance()->isValidAttributeNS($namespace_uri, $local_name);
+ }
+
+ public function isHandledNamespace($namespace_uri)
+ {
+ return PHPTAL_Dom_Defs::getInstance()->isHandledNamespace($namespace_uri);
+ }
+
+ /**
+ * Returns a new XmlnsState inheriting of $this if $nodeAttributes contains
+ * xmlns attributes, returns $this otherwise.
+ *
+ * This method is used by the PHPTAL parser to keep track of xmlns fluctuation for
+ * each encountered node.
+ */
+ public function newElement(array $nodeAttributes)
+ {
+ $prefix_to_uri = $this->prefix_to_uri;
+ $current_default = $this->current_default;
+
+ $changed = false;
+ foreach ($nodeAttributes as $qname => $value) {
+ if (preg_match('/^xmlns:(.+)$/', $qname, $m)) {
+ $changed = true;
+ list(, $prefix) = $m;
+ $prefix_to_uri[$prefix] = $value;
+ }
+
+ if ($qname == 'xmlns') {$changed=true;$current_default = $value;}
+ }
+
+ if ($changed) {
+ return new PHPTAL_Dom_XmlnsState($prefix_to_uri, $current_default);
+ } else {
+ return $this;
+ }
+ }
+
+ function getCurrentDefaultNamespaceURI()
+ {
+ return $this->current_default;
+ }
+
+ private $prefix_to_uri, $current_default;
+}
diff --git a/lib/phptal/PHPTAL/Exception.php b/lib/phptal/PHPTAL/Exception.php
new file mode 100644
index 0000000..6d4f312
--- /dev/null
+++ b/lib/phptal/PHPTAL/Exception.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+/**
+ * @package PHPTAL
+ * @subpackage Exception
+ */
+class PHPTAL_Exception extends Exception
+{
+}
+
+
diff --git a/lib/phptal/PHPTAL/ExceptionHandler.php b/lib/phptal/PHPTAL/ExceptionHandler.php
new file mode 100644
index 0000000..dca7bb7
--- /dev/null
+++ b/lib/phptal/PHPTAL/ExceptionHandler.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id: $
+ * @link http://phptal.org/
+ */
+
+class PHPTAL_ExceptionHandler
+{
+ private $encoding;
+ function __construct($encoding)
+ {
+ $this->encoding = $encoding;
+ }
+
+ /**
+ * PHP's default exception handler allows error pages to be indexed and can reveal too much information,
+ * so if possible PHPTAL sets up its own handler to fix this.
+ *
+ * Doesn't change exception handler if non-default one is set.
+ *
+ * @param Exception e exception to re-throw and display
+ *
+ * @return void
+ * @throws Exception
+ */
+ public static function handleException(Exception $e, $encoding)
+ {
+ // PHPTAL's handler is only useful on fresh HTTP response
+ if (PHP_SAPI !== 'cli' && !headers_sent()) {
+ $old_exception_handler = set_exception_handler(array(new PHPTAL_ExceptionHandler($encoding), '_defaultExceptionHandler'));
+
+ if ($old_exception_handler !== NULL) {
+ restore_exception_handler(); // if there's user's exception handler, let it work
+ }
+ }
+ throw $e; // throws instead of outputting immediately to support user's try/catch
+ }
+
+
+ /**
+ * Generates simple error page. Sets appropriate HTTP status to prevent page being indexed.
+ *
+ * @param Exception e exception to display
+ */
+ public function _defaultExceptionHandler($e)
+ {
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 PHPTAL Exception');
+ header('Content-Type:text/html;charset='.$this->encoding);
+ }
+
+ $line = $e->getFile();
+ if ($e->getLine()) {
+ $line .= ' line '.$e->getLine();
+ }
+
+ if (ini_get('display_errors')) {
+ $title = get_class($e).': '.htmlspecialchars($e->getMessage());
+ $body = "<p><strong>\n".htmlspecialchars($e->getMessage()).'</strong></p>' .
+ '<p>In '.htmlspecialchars($line)."</p><pre>\n".htmlspecialchars($e->getTraceAsString()).'</pre>';
+ } else {
+ $title = "PHPTAL Exception";
+ $body = "<p>This page cannot be displayed.</p><hr/>" .
+ "<p><small>Enable <code>display_errors</code> to see detailed message.</small></p>";
+ }
+
+ echo "<!DOCTYPE html><html xmlns='http://www.w3.org/1999/xhtml'><head><style>body{font-family:sans-serif}</style><title>\n";
+ echo $title.'</title></head><body><h1>PHPTAL Exception</h1>'.$body;
+ error_log($e->getMessage().' in '.$line);
+ echo '</body></html>'.str_repeat(' ', 100)."\n"; // IE won't display error pages < 512b
+ exit(1);
+ }
+}
diff --git a/lib/phptal/PHPTAL/FileSource.php b/lib/phptal/PHPTAL/FileSource.php
new file mode 100644
index 0000000..84d8719
--- /dev/null
+++ b/lib/phptal/PHPTAL/FileSource.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Reads template from the filesystem
+ *
+ * @package PHPTAL
+ */
+class PHPTAL_FileSource implements PHPTAL_Source
+{
+ private $_path;
+
+ public function __construct($path)
+ {
+ $this->_path = realpath($path);
+ if ($this->_path === false) throw new PHPTAL_IOException("Unable to find real path of file '$path' (in ".getcwd().')');
+ }
+
+ public function getRealPath()
+ {
+ return $this->_path;
+ }
+
+ public function getLastModifiedTime()
+ {
+ return filemtime($this->_path);
+ }
+
+ public function getData()
+ {
+ $content = file_get_contents($this->_path);
+
+ // file_get_contents returns "" when loading directory!?
+ if (false === $content || ("" === $content && is_dir($this->_path))) {
+ throw new PHPTAL_IOException("Unable to load file ".$this->_path);
+ }
+ return $content;
+ }
+}
diff --git a/lib/phptal/PHPTAL/FileSourceResolver.php b/lib/phptal/PHPTAL/FileSourceResolver.php
new file mode 100644
index 0000000..3cb001a
--- /dev/null
+++ b/lib/phptal/PHPTAL/FileSourceResolver.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Finds template on disk by looking through repositories first
+ *
+ * @package PHPTAL
+ */
+class PHPTAL_FileSourceResolver implements PHPTAL_SourceResolver
+{
+ public function __construct($repositories)
+ {
+ $this->_repositories = $repositories;
+ }
+
+ public function resolve($path)
+ {
+ foreach ($this->_repositories as $repository) {
+ $file = $repository . DIRECTORY_SEPARATOR . $path;
+ if (file_exists($file)) {
+ return new PHPTAL_FileSource($file);
+ }
+ }
+
+ if (file_exists($path)) {
+ return new PHPTAL_FileSource($path);
+ }
+
+ return null;
+ }
+
+ private $_repositories;
+}
diff --git a/lib/phptal/PHPTAL/Filter.php b/lib/phptal/PHPTAL/Filter.php
new file mode 100644
index 0000000..813c746
--- /dev/null
+++ b/lib/phptal/PHPTAL/Filter.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Objects passed to PHPTAL::setPre/PostFilter() must implement this interface
+ *
+ * @package PHPTAL
+ */
+interface PHPTAL_Filter
+{
+ /**
+ * In prefilter it gets template source file and is expected to return new source.
+ * Prefilters are called only once before template is compiled, so they can be slow.
+ *
+ * In postfilter template output is passed to this method, and final output goes to the browser.
+ * TAL or PHP tags won't be executed. Postfilters should be fast.
+ */
+ public function filter($str);
+}
+
diff --git a/lib/phptal/PHPTAL/GetTextTranslator.php b/lib/phptal/PHPTAL/GetTextTranslator.php
new file mode 100644
index 0000000..108e8f5
--- /dev/null
+++ b/lib/phptal/PHPTAL/GetTextTranslator.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * PHPTAL_TranslationService gettext implementation.
+ *
+ * Because gettext is the most common translation library in use, this
+ * implementation is shipped with the PHPTAL library.
+ *
+ * Please refer to the PHPTAL documentation for usage examples.
+ *
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_GetTextTranslator implements PHPTAL_TranslationService
+{
+ private $_vars = array();
+ private $_currentDomain;
+ private $_encoding = 'UTF-8';
+ private $_canonicalize = false;
+
+ public function __construct()
+ {
+ if (!function_exists('gettext')) throw new PHPTAL_ConfigurationException("Gettext not installed");
+ $this->useDomain("messages"); // PHP bug #21965
+ }
+
+ /**
+ * set encoding that is used by template and is expected from gettext
+ * the default is UTF-8
+ *
+ * @param string $enc encoding name
+ */
+ public function setEncoding($enc)
+ {
+ $this->_encoding = $enc;
+ }
+
+ /**
+ * if true, all non-ASCII characters in keys will be converted to C<xxx> form. This impacts performance.
+ * by default keys will be passed to gettext unmodified.
+ *
+ * This function is only for backwards compatibility
+ *
+ * @param bool $bool enable old behavior
+ */
+ public function setCanonicalize($bool)
+ {
+ $this->_canonicalize = $bool;
+ }
+
+ /**
+ * It expects locale names as arguments.
+ * Choses first one that works.
+ *
+ * setLanguage("en_US.utf8","en_US","en_GB","en")
+ *
+ * @return string - chosen language
+ */
+ public function setLanguage(/*...*/)
+ {
+ $langs = func_get_args();
+
+ $langCode = $this->trySettingLanguages(LC_ALL, $langs);
+ if ($langCode) return $langCode;
+
+ if (defined("LC_MESSAGES")) {
+ $langCode = $this->trySettingLanguages(LC_MESSAGES, $langs);
+ if ($langCode) return $langCode;
+ }
+
+ throw new PHPTAL_ConfigurationException('Language(s) code(s) "'.implode(', ', $langs).'" not supported by your system');
+ }
+
+ private function trySettingLanguages($category, array $langs)
+ {
+ foreach ($langs as $langCode) {
+ putenv("LANG=$langCode");
+ putenv("LC_ALL=$langCode");
+ putenv("LANGUAGE=$langCode");
+ if (setlocale($category, $langCode)) {
+ return $langCode;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds translation domain (usually it's the same as name of .po file [without extension])
+ *
+ * Encoding must be set before calling addDomain!
+ */
+ public function addDomain($domain, $path='./locale/')
+ {
+ bindtextdomain($domain, $path);
+ if ($this->_encoding) {
+ bind_textdomain_codeset($domain, $this->_encoding);
+ }
+ $this->useDomain($domain);
+ }
+
+ /**
+ * Switches to one of the domains previously set via addDomain()
+ *
+ * @param string $domain name of translation domain to be used.
+ *
+ * @return string - old domain
+ */
+ public function useDomain($domain)
+ {
+ $old = $this->_currentDomain;
+ $this->_currentDomain = $domain;
+ textdomain($domain);
+ return $old;
+ }
+
+ /**
+ * used by generated PHP code. Don't use directly.
+ */
+ public function setVar($key, $value)
+ {
+ $this->_vars[$key] = $value;
+ }
+
+ /**
+ * translate given key.
+ *
+ * @param bool $htmlencode if true, output will be HTML-escaped.
+ */
+ public function translate($key, $htmlencode=true)
+ {
+ if ($this->_canonicalize) $key = self::_canonicalizeKey($key);
+
+ $value = gettext($key);
+
+ if ($htmlencode) {
+ $value = htmlspecialchars($value, ENT_QUOTES, $this->_encoding);
+ }
+ while (preg_match('/\${(.*?)\}/sm', $value, $m)) {
+ list($src, $var) = $m;
+ if (!array_key_exists($var, $this->_vars)) {
+ throw new PHPTAL_VariableNotFoundException('Interpolation error. Translation uses ${'.$var.'}, which is not defined in the template (via i18n:name)');
+ }
+ $value = str_replace($src, $this->_vars[$var], $value);
+ }
+ return $value;
+ }
+
+ /**
+ * For backwards compatibility only.
+ */
+ private static function _canonicalizeKey($key_)
+ {
+ $result = "";
+ $key_ = trim($key_);
+ $key_ = str_replace("\n", "", $key_);
+ $key_ = str_replace("\r", "", $key_);
+ for ($i = 0; $i<strlen($key_); $i++) {
+ $c = $key_[$i];
+ $o = ord($c);
+ if ($o < 5 || $o > 127) {
+ $result .= 'C<'.$o.'>';
+ } else {
+ $result .= $c;
+ }
+ }
+ return $result;
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/IOException.php b/lib/phptal/PHPTAL/IOException.php
new file mode 100644
index 0000000..166290d
--- /dev/null
+++ b/lib/phptal/PHPTAL/IOException.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * PHPTAL failed to load template
+ *
+ * @package PHPTAL
+ * @subpackage Exception
+ */
+class PHPTAL_IOException extends PHPTAL_Exception
+{
+}
diff --git a/lib/phptal/PHPTAL/InvalidVariableNameException.php b/lib/phptal/PHPTAL/InvalidVariableNameException.php
new file mode 100644
index 0000000..427e138
--- /dev/null
+++ b/lib/phptal/PHPTAL/InvalidVariableNameException.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Parse error in TALES expression.
+ *
+ * @package PHPTAL
+ * @subpackage Exception
+ */
+class PHPTAL_InvalidVariableNameException extends PHPTAL_Exception
+{
+}
diff --git a/lib/phptal/PHPTAL/Keywords.php b/lib/phptal/PHPTAL/Keywords.php
new file mode 100644
index 0000000..bee7b7a
--- /dev/null
+++ b/lib/phptal/PHPTAL/Keywords.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Andrew Crites <explosion-pills@aysites.com>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Interface for template keywords
+ *
+ * @package PHPTAL
+ * @subpackage Keywords
+ */
+interface PHPTAL_Keywords extends Countable
+{
+ public function __toString();
+}
+?>
diff --git a/lib/phptal/PHPTAL/MacroMissingException.php b/lib/phptal/PHPTAL/MacroMissingException.php
new file mode 100644
index 0000000..0e3a057
--- /dev/null
+++ b/lib/phptal/PHPTAL/MacroMissingException.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Wrong macro name in metal:use-macro
+ *
+ * @package PHPTAL
+ * @subpackage Exception
+ */
+class PHPTAL_MacroMissingException extends PHPTAL_TemplateException
+{
+}
diff --git a/lib/phptal/PHPTAL/Namespace.php b/lib/phptal/PHPTAL/Namespace.php
new file mode 100644
index 0000000..17d5911
--- /dev/null
+++ b/lib/phptal/PHPTAL/Namespace.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * @see PHPTAL_NamespaceAttribute
+ * @package PHPTAL
+ * @subpackage Namespace
+ */
+abstract class PHPTAL_Namespace
+{
+ private $prefix, $namespace_uri;
+ protected $_attributes;
+
+ public function __construct($prefix, $namespace_uri)
+ {
+ if (!$namespace_uri || !$prefix) {
+ throw new PHPTAL_ConfigurationException("Can't create namespace with empty prefix or namespace URI");
+ }
+
+ $this->_attributes = array();
+ $this->prefix = $prefix;
+ $this->namespace_uri = $namespace_uri;
+ }
+
+ public function getPrefix()
+ {
+ return $this->prefix;
+ }
+
+ public function getNamespaceURI()
+ {
+ return $this->namespace_uri;
+ }
+
+ public function hasAttribute($attributeName)
+ {
+ return array_key_exists(strtolower($attributeName), $this->_attributes);
+ }
+
+ public function getAttribute($attributeName)
+ {
+ return $this->_attributes[strtolower($attributeName)];
+ }
+
+ public function addAttribute(PHPTAL_NamespaceAttribute $attribute)
+ {
+ $attribute->setNamespace($this);
+ $this->_attributes[strtolower($attribute->getLocalName())] = $attribute;
+ }
+
+ public function getAttributes()
+ {
+ return $this->_attributes;
+ }
+
+ abstract public function createAttributeHandler(PHPTAL_NamespaceAttribute $att, PHPTAL_Dom_Element $tag, $expression);
+}
diff --git a/lib/phptal/PHPTAL/Namespace/Builtin.php b/lib/phptal/PHPTAL/Namespace/Builtin.php
new file mode 100644
index 0000000..5cec74d
--- /dev/null
+++ b/lib/phptal/PHPTAL/Namespace/Builtin.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * @package PHPTAL
+ * @subpackage Namespace
+ */
+class PHPTAL_Namespace_Builtin extends PHPTAL_Namespace
+{
+ public function createAttributeHandler(PHPTAL_NamespaceAttribute $att, PHPTAL_Dom_Element $tag, $expression)
+ {
+ $name = $att->getLocalName();
+
+ // change define-macro to "define macro" and capitalize words
+ $name = str_replace(' ', '', ucwords(strtr($name, '-', ' ')));
+
+ // case is important when using autoload on case-sensitive filesystems
+ if (version_compare(PHP_VERSION, '5.3', '>=') && __NAMESPACE__) {
+ $class = 'PHPTALNAMESPACE\\Php\\Attribute\\'.strtoupper($this->getPrefix()).'\\'.$name;
+ } else {
+ $class = 'PHPTAL_Php_Attribute_'.strtoupper($this->getPrefix()).'_'.$name;
+ }
+ $result = new $class($tag, $expression);
+ return $result;
+ }
+}
diff --git a/lib/phptal/PHPTAL/Namespace/I18N.php b/lib/phptal/PHPTAL/Namespace/I18N.php
new file mode 100644
index 0000000..81dd8e6
--- /dev/null
+++ b/lib/phptal/PHPTAL/Namespace/I18N.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * @package PHPTAL
+ * @subpackage Namespace
+ */
+class PHPTAL_Namespace_I18N extends PHPTAL_Namespace_Builtin
+{
+ public function __construct()
+ {
+ parent::__construct('i18n', 'http://xml.zope.org/namespaces/i18n');
+ $this->addAttribute(new PHPTAL_NamespaceAttributeContent('translate', 5));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('name', 5));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('attributes', 10));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('domain', 3));
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Namespace/METAL.php b/lib/phptal/PHPTAL/Namespace/METAL.php
new file mode 100644
index 0000000..2773667
--- /dev/null
+++ b/lib/phptal/PHPTAL/Namespace/METAL.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * @package PHPTAL
+ * @subpackage Namespace
+ */
+class PHPTAL_Namespace_METAL extends PHPTAL_Namespace_Builtin
+{
+ public function __construct()
+ {
+ parent::__construct('metal', 'http://xml.zope.org/namespaces/metal');
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('define-macro', 1));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeReplace('use-macro', 9));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('define-slot', 9));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('fill-slot', 9));
+ }
+}
diff --git a/lib/phptal/PHPTAL/Namespace/PHPTAL.php b/lib/phptal/PHPTAL/Namespace/PHPTAL.php
new file mode 100644
index 0000000..4d4270a
--- /dev/null
+++ b/lib/phptal/PHPTAL/Namespace/PHPTAL.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * @package PHPTAL
+ * @subpackage Namespace
+ */
+class PHPTAL_Namespace_PHPTAL extends PHPTAL_Namespace_Builtin
+{
+ public function __construct()
+ {
+ parent::__construct('phptal', 'http://phptal.org/ns/phptal');
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('tales', -1));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('debug', -2));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('id', 7));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('cache', -3));
+ }
+}
diff --git a/lib/phptal/PHPTAL/Namespace/TAL.php b/lib/phptal/PHPTAL/Namespace/TAL.php
new file mode 100644
index 0000000..74cd90a
--- /dev/null
+++ b/lib/phptal/PHPTAL/Namespace/TAL.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * @package PHPTAL
+ * @subpackage Namespace
+ */
+class PHPTAL_Namespace_TAL extends PHPTAL_Namespace_Builtin
+{
+ public function __construct()
+ {
+ parent::__construct('tal', 'http://xml.zope.org/namespaces/tal');
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('define', 4));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('condition', 6));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('repeat', 8));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeContent('content', 11));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeReplace('replace', 9));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('attributes', 9));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('omit-tag', 0));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('comment', 12));
+ $this->addAttribute(new PHPTAL_NamespaceAttributeSurround('on-error', 2));
+ }
+}
diff --git a/lib/phptal/PHPTAL/NamespaceAttribute.php b/lib/phptal/PHPTAL/NamespaceAttribute.php
new file mode 100644
index 0000000..db1fd95
--- /dev/null
+++ b/lib/phptal/PHPTAL/NamespaceAttribute.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Information about TAL attributes (in which order they are executed and how they generate the code)
+ *
+ * From http://dev.zope.org/Wikis/DevSite/Projects/ZPT/TAL%20Specification%201.4
+ *
+ * Order of Operations
+ *
+ * When there is only one TAL statement per element, the order in which
+ * they are executed is simple. Starting with the root element, each
+ * element's statements are executed, then each of its child elements is
+ * visited, in order, to do the same.
+ *
+ * Any combination of statements may appear on the same elements, except
+ * that the content and replace statements may not appear together.
+ *
+ * When an element has multiple statements, they are executed in this
+ * order:
+ *
+ * * define
+ * * condition
+ * * repeat
+ * * content or replace
+ * * attributes
+ * * omit-tag
+ *
+ * Since the on-error statement is only invoked when an error occurs, it
+ * does not appear in the list.
+ *
+ * The reasoning behind this ordering goes like this: You often want to set
+ * up variables for use in other statements, so define comes first. The
+ * very next thing to do is decide whether this element will be included at
+ * all, so condition is next; since the condition may depend on variables
+ * you just set, it comes after define. It is valuable be able to replace
+ * various parts of an element with different values on each iteration of a
+ * repeat, so repeat is next. It makes no sense to replace attributes and
+ * then throw them away, so attributes is last. The remaining statements
+ * clash, because they each replace or edit the statement element.
+ *
+ * If you want to override this ordering, you must do so by enclosing the
+ * element in another element, possibly div or span, and placing some of
+ * the statements on this new element.
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Namespace
+ */
+abstract class PHPTAL_NamespaceAttribute
+{
+ /** Attribute name without the namespace: prefix */
+ private $local_name;
+
+ /** [0 - 1000] */
+ private $_priority;
+
+ /** PHPTAL_Namespace */
+ private $_namespace;
+
+ /**
+ * @param string $name The attribute name
+ * @param int $priority Attribute execution priority
+ */
+ public function __construct($local_name, $priority)
+ {
+ $this->local_name = $local_name;
+ $this->_priority = $priority;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLocalName()
+ {
+ return $this->local_name;
+ }
+
+ public function getPriority() { return $this->_priority; }
+ public function getNamespace() { return $this->_namespace; }
+ public function setNamespace(PHPTAL_Namespace $ns) { $this->_namespace = $ns; }
+
+ public function createAttributeHandler(PHPTAL_Dom_Element $tag, $expression)
+ {
+ return $this->_namespace->createAttributeHandler($this, $tag, $expression);
+ }
+}
diff --git a/lib/phptal/PHPTAL/NamespaceAttributeContent.php b/lib/phptal/PHPTAL/NamespaceAttributeContent.php
new file mode 100644
index 0000000..de33521
--- /dev/null
+++ b/lib/phptal/PHPTAL/NamespaceAttributeContent.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * This type of attribute replaces element's content entirely
+ * @package PHPTAL
+ * @subpackage Namespace
+ */
+class PHPTAL_NamespaceAttributeContent extends PHPTAL_NamespaceAttribute
+{
+}
diff --git a/lib/phptal/PHPTAL/NamespaceAttributeReplace.php b/lib/phptal/PHPTAL/NamespaceAttributeReplace.php
new file mode 100644
index 0000000..defc360
--- /dev/null
+++ b/lib/phptal/PHPTAL/NamespaceAttributeReplace.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * This type of attribute replaces element entirely
+ * @package PHPTAL
+ * @subpackage Namespace
+ */
+class PHPTAL_NamespaceAttributeReplace extends PHPTAL_NamespaceAttribute
+{
+}
diff --git a/lib/phptal/PHPTAL/NamespaceAttributeSurround.php b/lib/phptal/PHPTAL/NamespaceAttributeSurround.php
new file mode 100644
index 0000000..fca87d6
--- /dev/null
+++ b/lib/phptal/PHPTAL/NamespaceAttributeSurround.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * This type of attribute wraps element
+ * @package PHPTAL
+ * @subpackage Namespace
+ */
+class PHPTAL_NamespaceAttributeSurround extends PHPTAL_NamespaceAttribute
+{
+}
diff --git a/lib/phptal/PHPTAL/NothingKeyword.php b/lib/phptal/PHPTAL/NothingKeyword.php
new file mode 100644
index 0000000..22a79b0
--- /dev/null
+++ b/lib/phptal/PHPTAL/NothingKeyword.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Andrew Crites <explosion-pills@aysites.com>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Representation of the template 'nothing' keyword
+ *
+ * @package PHPTAL
+ * @subpackage Keywords
+ */
+class PHPTAL_NothingKeyword implements PHPTAL_Keywords
+{
+ public function __toString()
+ {
+ return 'null';
+ }
+
+ public function count()
+ {
+ return 0;
+ }
+
+ public function jsonSerialize()
+ {
+ return null;
+ }
+}
+?>
diff --git a/lib/phptal/PHPTAL/ParserException.php b/lib/phptal/PHPTAL/ParserException.php
new file mode 100644
index 0000000..225ad54
--- /dev/null
+++ b/lib/phptal/PHPTAL/ParserException.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * XML well-formedness errors and alike.
+ *
+ * @package PHPTAL
+ * @subpackage Exception
+ */
+class PHPTAL_ParserException extends PHPTAL_TemplateException
+{
+}
diff --git a/lib/phptal/PHPTAL/Php/Attribute.php b/lib/phptal/PHPTAL/Php/Attribute.php
new file mode 100644
index 0000000..ceb8a12
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Base class for all PHPTAL attributes.
+ *
+ * Attributes are first ordered by PHPTAL then called depending on their
+ * priority before and after the element printing.
+ *
+ * An attribute must implements start() and end().
+ *
+ * @package PHPTAL
+ * @subpackage Php
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+abstract class PHPTAL_Php_Attribute
+{
+ const ECHO_TEXT = 'text';
+ const ECHO_STRUCTURE = 'structure';
+
+ /** Attribute value specified by the element. */
+ protected $expression;
+
+ /** Element using this attribute (PHPTAL's counterpart of XML node) */
+ protected $phpelement;
+
+ /**
+ * Called before element printing.
+ */
+ abstract function before(PHPTAL_Php_CodeWriter $codewriter);
+
+ /**
+ * Called after element printing.
+ */
+ abstract function after(PHPTAL_Php_CodeWriter $codewriter);
+
+ function __construct(PHPTAL_Dom_Element $phpelement, $expression)
+ {
+ $this->expression = $expression;
+ $this->phpelement = $phpelement;
+ }
+
+ /**
+ * Remove structure|text keyword from expression and stores it for later
+ * doEcho() usage.
+ *
+ * $expression = 'stucture my/path';
+ * $expression = $this->extractEchoType($expression);
+ *
+ * ...
+ *
+ * $this->doEcho($code);
+ */
+ protected function extractEchoType($expression)
+ {
+ $echoType = self::ECHO_TEXT;
+ $expression = trim($expression);
+ if (preg_match('/^(text|structure)\s+(.*?)$/ism', $expression, $m)) {
+ list(, $echoType, $expression) = $m;
+ }
+ $this->_echoType = strtolower($echoType);
+ return trim($expression);
+ }
+
+ protected function doEchoAttribute(PHPTAL_Php_CodeWriter $codewriter, $code)
+ {
+ if ($this->_echoType === self::ECHO_TEXT)
+ $codewriter->doEcho($code);
+ else
+ $codewriter->doEchoRaw($code);
+ }
+
+ protected function parseSetExpression($exp)
+ {
+ $exp = trim($exp);
+ // (dest) (value)
+ if (preg_match('/^([a-z0-9:\-_]+)\s+(.*?)$/si', $exp, $m)) {
+ return array($m[1], trim($m[2]));
+ }
+ // (dest)
+ return array($exp, null);
+ }
+
+ protected $_echoType = PHPTAL_Php_Attribute::ECHO_TEXT;
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/I18N/Attributes.php b/lib/phptal/PHPTAL/Php/Attribute/I18N/Attributes.php
new file mode 100644
index 0000000..5eb3fac
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/I18N/Attributes.php
@@ -0,0 +1,118 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * i18n:attributes
+ *
+ * This attribute will allow us to translate attributes of HTML tags, such
+ * as the alt attribute in the img tag. The i18n:attributes attribute
+ * specifies a list of attributes to be translated with optional message
+ * IDs? for each; if multiple attribute names are given, they must be
+ * separated by semi-colons. Message IDs? used in this context must not
+ * include whitespace.
+ *
+ * Note that the value of the particular attributes come either from the
+ * HTML attribute value itself or from the data inserted by tal:attributes.
+ *
+ * If an attibute is to be both computed using tal:attributes and translated,
+ * the translation service is passed the result of the TALES expression for
+ * that attribute.
+ *
+ * An example:
+ *
+ * <img src="http://foo.com/logo" alt="Visit us"
+ * tal:attributes="alt here/greeting"
+ * i18n:attributes="alt"
+ * />
+ *
+ *
+ * In this example, let tal:attributes set the value of the alt attribute to
+ * the text "Stop by for a visit!". This text will be passed to the
+ * translation service, which uses the result of language negotiation to
+ * translate "Stop by for a visit!" into the requested language. The example
+ * text in the template, "Visit us", will simply be discarded.
+ *
+ * Another example, with explicit message IDs:
+ *
+ * <img src="../icons/uparrow.png" alt="Up"
+ * i18n:attributes="src up-arrow-icon; alt up-arrow-alttext"
+ * >
+ *
+ * Here, the message ID up-arrow-icon will be used to generate the link to
+ * an icon image file, and the message ID up-arrow-alttext will be used for
+ * the "alt" text.
+ *
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.i18n
+ */
+class PHPTAL_Php_Attribute_I18N_Attributes extends PHPTAL_Php_Attribute
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ // split attributes to translate
+ foreach ($codewriter->splitExpression($this->expression) as $exp) {
+ list($qname, $key) = $this->parseSetExpression($exp);
+
+ // if the translation key is specified and not empty (but may be '0')
+ if (strlen($key)) {
+ // we use it and replace the tag attribute with the result of the translation
+ $code = $this->_getTranslationCode($codewriter, $key);
+ } else {
+ $attr = $this->phpelement->getAttributeNode($qname);
+ if (!$attr) throw new PHPTAL_TemplateException("Unable to translate attribute $qname, because there is no translation key specified",
+ $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+
+ if ($attr->getReplacedState() === PHPTAL_Dom_Attr::NOT_REPLACED) {
+ $code = $this->_getTranslationCode($codewriter, $attr->getValue());
+ } elseif ($attr->getReplacedState() === PHPTAL_Dom_Attr::VALUE_REPLACED && $attr->getOverwrittenVariableName()) {
+ // sadly variables won't be interpolated in this translation
+ $code = 'echo '.$codewriter->escapeCode($codewriter->getTranslatorReference(). '->translate('.$attr->getOverwrittenVariableName().', false)');
+ } else {
+ throw new PHPTAL_TemplateException("Unable to translate attribute $qname, because other TAL attributes are using it",
+ $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+ }
+ }
+ $this->phpelement->getOrCreateAttributeNode($qname)->overwriteValueWithCode($code);
+ }
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ }
+
+ /**
+ * @param key - unescaped string (not PHP code) for the key
+ */
+ private function _getTranslationCode(PHPTAL_Php_CodeWriter $codewriter, $key)
+ {
+ $code = '';
+ if (preg_match_all('/\$\{(.*?)\}/', $key, $m)) {
+ array_shift($m);
+ $m = array_shift($m);
+ foreach ($m as $name) {
+ $code .= "\n".$codewriter->getTranslatorReference(). '->setVar('.$codewriter->str($name).','.PHPTAL_Php_TalesInternal::compileToPHPExpression($name).');'; // allow more complex TAL expressions
+ }
+ $code .= "\n";
+ }
+
+ // notice the false boolean which indicate that the html is escaped
+ // elsewhere looks like an hack doesn't it ? :)
+ $code .= 'echo '.$codewriter->escapeCode($codewriter->getTranslatorReference().'->translate('.$codewriter->str($key).', false)');
+ return $code;
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/I18N/Data.php b/lib/phptal/PHPTAL/Php/Attribute/I18N/Data.php
new file mode 100644
index 0000000..bad310f
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/I18N/Data.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * i18n:data
+ *
+ * Since TAL always returns strings, we need a way in ZPT to translate
+ * objects, the most obvious case being DateTime objects. The data attribute
+ * will allow us to specify such an object, and i18n:translate will provide
+ * us with a legal format string for that object. If data is used,
+ * i18n:translate must be used to give an explicit message ID, rather than
+ * relying on a message ID computed from the content.
+ *
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.i18n
+ */
+class PHPTAL_Php_Attribute_I18N_Data extends PHPTAL_Php_Attribute
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter){}
+ public function after(PHPTAL_Php_CodeWriter $codewriter){}
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/I18N/Domain.php b/lib/phptal/PHPTAL/Php/Attribute/I18N/Domain.php
new file mode 100644
index 0000000..92ece11
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/I18N/Domain.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * i18n:domain
+ *
+ * The i18n:domain attribute is used to specify the domain to be used to get
+ * the translation. If not specified, the translation services will use a
+ * default domain. The value of the attribute is used directly; it is not
+ * a TALES expression.
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.i18n
+ */
+class PHPTAL_Php_Attribute_I18N_Domain extends PHPTAL_Php_Attribute
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ // ensure a domain stack exists or create it
+ $codewriter->doIf('!isset($_i18n_domains)');
+ $codewriter->pushCode('$_i18n_domains = array()');
+ $codewriter->doEnd('if');
+
+ $expression = $codewriter->interpolateTalesVarsInString($this->expression);
+
+ // push current domain and use new domain
+ $code = '$_i18n_domains[] = '.$codewriter->getTranslatorReference().'->useDomain('.$expression.')';
+ $codewriter->pushCode($code);
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ // restore domain
+ $code = $codewriter->getTranslatorReference().'->useDomain(array_pop($_i18n_domains))';
+ $codewriter->pushCode($code);
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/I18N/Name.php b/lib/phptal/PHPTAL/Php/Attribute/I18N/Name.php
new file mode 100644
index 0000000..8a8f4e7
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/I18N/Name.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/** i18n:name
+ *
+ * Name the content of the current element for use in interpolation within
+ * translated content. This allows a replaceable component in content to be
+ * re-ordered by translation. For example:
+ *
+ * <span i18n:translate=''>
+ * <span tal:replace='here/name' i18n:name='name' /> was born in
+ * <span tal:replace='here/country_of_birth' i18n:name='country' />.
+ * </span>
+ *
+ * would cause this text to be passed to the translation service:
+ *
+ * "${name} was born in ${country}."
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.i18n
+ */
+class PHPTAL_Php_Attribute_I18N_Name extends PHPTAL_Php_Attribute
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->pushCode('ob_start()');
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->pushCode($codewriter->getTranslatorReference().'->setVar('.$codewriter->str($this->expression).', ob_get_clean())');
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/I18N/Source.php b/lib/phptal/PHPTAL/Php/Attribute/I18N/Source.php
new file mode 100644
index 0000000..9575fae
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/I18N/Source.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * i18n:source
+ *
+ * The i18n:source attribute specifies the language of the text to be
+ * translated. The default is "nothing", which means we don't provide
+ * this information to the translation services.
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.i18n
+ */
+class PHPTAL_Php_Attribute_I18N_Source extends PHPTAL_Php_Attribute
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ // ensure that a sources stack exists or create it
+ $codewriter->doIf('!isset($_i18n_sources)');
+ $codewriter->pushCode('$_i18n_sources = array()');
+ $codewriter->end();
+
+ // push current source and use new one
+ $codewriter->pushCode('$_i18n_sources[] = ' . $codewriter->getTranslatorReference(). '->setSource('.$codewriter->str($this->expression).')');
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ // restore source
+ $code = $codewriter->getTranslatorReference().'->setSource(array_pop($_i18n_sources))';
+ $codewriter->pushCode($code);
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/I18N/Target.php b/lib/phptal/PHPTAL/Php/Attribute/I18N/Target.php
new file mode 100644
index 0000000..9cf2a67
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/I18N/Target.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * i18n:target
+ *
+ * The i18n:target attribute specifies the language of the translation we
+ * want to get. If the value is "default", the language negotiation services
+ * will be used to choose the destination language. If the value is
+ * "nothing", no translation will be performed; this can be used to suppress
+ * translation within a larger translated unit. Any other value must be a
+ * language code.
+ *
+ * The attribute value is a TALES expression; the result of evaluating the
+ * expression is the language code or one of the reserved values.
+ *
+ * Note that i18n:target is primarily used for hints to text extraction
+ * tools and translation teams. If you had some text that should only be
+ * translated to e.g. German, then it probably shouldn't be wrapped in an
+ * i18n:translate span.
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.i18n
+ */
+class PHPTAL_Php_Attribute_I18N_Target extends PHPTAL_Php_Attribute
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter){}
+ public function after(PHPTAL_Php_CodeWriter $codewriter){}
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/I18N/Translate.php b/lib/phptal/PHPTAL/Php/Attribute/I18N/Translate.php
new file mode 100644
index 0000000..a0e26c2
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/I18N/Translate.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * ZPTInternationalizationSupport
+ *
+ * i18n:translate
+ *
+ * This attribute is used to mark units of text for translation. If this
+ * attribute is specified with an empty string as the value, the message ID
+ * is computed from the content of the element bearing this attribute.
+ * Otherwise, the value of the element gives the message ID.
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.i18n
+ */
+class PHPTAL_Php_Attribute_I18N_Translate extends PHPTAL_Php_Attribute_TAL_Content
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $escape = true;
+ $this->_echoType = PHPTAL_Php_Attribute::ECHO_TEXT;
+ if (preg_match('/^(text|structure)(?:\s+(.*)|\s*$)/', $this->expression, $m)) {
+ if ($m[1]=='structure') { $escape=false; $this->_echoType = PHPTAL_Php_Attribute::ECHO_STRUCTURE; }
+ $this->expression = isset($m[2])?$m[2]:'';
+ }
+
+ $this->_prepareNames($codewriter, $this->phpelement);
+
+ // if no expression is given, the content of the node is used as
+ // a translation key
+ if (strlen(trim($this->expression)) == 0) {
+ $key = $this->_getTranslationKey($this->phpelement, !$escape, $codewriter->getEncoding());
+ $key = trim(preg_replace('/\s+/sm'.($codewriter->getEncoding()=='UTF-8'?'u':''), ' ', $key));
+ if ('' === trim($key)) {
+ throw new PHPTAL_TemplateException("Empty translation key",
+ $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+ }
+ $code = $codewriter->str($key);
+ } else {
+ $code = $codewriter->evaluateExpression($this->expression);
+ if (is_array($code))
+ return $this->generateChainedContent($codewriter, $code);
+
+ $code = $codewriter->evaluateExpression($this->expression);
+ }
+
+ $codewriter->pushCode('echo '.$codewriter->getTranslatorReference().'->translate('.$code.','.($escape ? 'true':'false').');');
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ }
+
+ public function talesChainPart(PHPTAL_Php_TalesChainExecutor $executor, $exp, $islast)
+ {
+ $codewriter = $executor->getCodeWriter();
+
+ $escape = !($this->_echoType == PHPTAL_Php_Attribute::ECHO_STRUCTURE);
+ $exp = $codewriter->getTranslatorReference()."->translate($exp, " . ($escape ? 'true':'false') . ')';
+ if (!$islast) {
+ $var = $codewriter->createTempVariable();
+ $executor->doIf('!phptal_isempty('.$var.' = '.$exp.')');
+ $codewriter->pushCode("echo $var");
+ $codewriter->recycleTempVariable($var);
+ } else {
+ $executor->doElse();
+ $codewriter->pushCode("echo $exp");
+ }
+ }
+
+ private function _getTranslationKey(PHPTAL_Dom_Node $tag, $preserve_tags, $encoding)
+ {
+ $result = '';
+ foreach ($tag->childNodes as $child) {
+ if ($child instanceof PHPTAL_Dom_Text) {
+ if ($preserve_tags) {
+ $result .= $child->getValueEscaped();
+ } else {
+ $result .= $child->getValue($encoding);
+ }
+ } elseif ($child instanceof PHPTAL_Dom_Element) {
+ if ($attr = $child->getAttributeNodeNS('http://xml.zope.org/namespaces/i18n', 'name')) {
+ $result .= '${' . $attr->getValue() . '}';
+ } else {
+
+ if ($preserve_tags) {
+ $result .= '<'.$child->getQualifiedName();
+ foreach ($child->getAttributeNodes() as $attr) {
+ if ($attr->getReplacedState() === PHPTAL_Dom_Attr::HIDDEN) continue;
+
+ $result .= ' '.$attr->getQualifiedName().'="'.$attr->getValueEscaped().'"';
+ }
+ $result .= '>'.$this->_getTranslationKey($child, $preserve_tags, $encoding) . '</'.$child->getQualifiedName().'>';
+ } else {
+ $result .= $this->_getTranslationKey($child, $preserve_tags, $encoding);
+ }
+ }
+ }
+ }
+ return $result;
+ }
+
+ private function _prepareNames(PHPTAL_Php_CodeWriter $codewriter, PHPTAL_Dom_Node $tag)
+ {
+ foreach ($tag->childNodes as $child) {
+ if ($child instanceof PHPTAL_Dom_Element) {
+ if ($child->hasAttributeNS('http://xml.zope.org/namespaces/i18n', 'name')) {
+ $child->generateCode($codewriter);
+ } else {
+ $this->_prepareNames($codewriter, $child);
+ }
+ }
+ }
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/METAL/DefineMacro.php b/lib/phptal/PHPTAL/Php/Attribute/METAL/DefineMacro.php
new file mode 100644
index 0000000..ef04840
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/METAL/DefineMacro.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * METAL Specification 1.0
+ *
+ * argument ::= Name
+ *
+ * Example:
+ *
+ * <p metal:define-macro="copyright">
+ * Copyright 2001, <em>Foobar</em> Inc.
+ * </p>
+ *
+ * PHPTAL:
+ *
+ * <?php function XXX_macro_copyright($tpl) { ? >
+ * <p>
+ * Copyright 2001, <em>Foobar</em> Inc.
+ * </p>
+ * <?php } ? >
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.metal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_METAL_DefineMacro extends PHPTAL_Php_Attribute
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $macroname = strtr(trim($this->expression), '-', '_');
+ if (!preg_match('/^[a-z0-9_]+$/i', $macroname)) {
+ throw new PHPTAL_ParserException('Bad macro name "'.$macroname.'"',
+ $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+ }
+
+ if ($codewriter->functionExists($macroname)) {
+ throw new PHPTAL_TemplateException("Macro $macroname is defined twice",
+ $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+ }
+
+ $codewriter->doFunction($macroname, 'PHPTAL $_thistpl, PHPTAL $tpl');
+ $codewriter->doSetVar('$tpl', 'clone $tpl');
+ $codewriter->doSetVar('$ctx', '$tpl->getContext()');
+ $codewriter->doInitTranslator();
+ $codewriter->doXmlDeclaration(true);
+ $codewriter->doDoctype(true);
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->doEnd('function');
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/METAL/DefineSlot.php b/lib/phptal/PHPTAL/Php/Attribute/METAL/DefineSlot.php
new file mode 100644
index 0000000..010849a
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/METAL/DefineSlot.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * METAL Specification 1.0
+ *
+ * argument ::= Name
+ *
+ * Example:
+ *
+ * <table metal:define-macro="sidebar">
+ * <tr><th>Links</th></tr>
+ * <tr><td metal:define-slot="links">
+ * <a href="/">A Link</a>
+ * </td></tr>
+ * </table>
+ *
+ * PHPTAL: (access to slots may be renamed)
+ *
+ * <?php function XXXX_macro_sidebar($tpl) { ? >
+ * <table>
+ * <tr><th>Links</th></tr>
+ * <tr>
+ * <?php if (isset($tpl->slots->links)): ? >
+ * <?php echo $tpl->slots->links ? >
+ * <?php else: ? >
+ * <td>
+ * <a href="/">A Link</a>
+ * </td></tr>
+ * </table>
+ * <?php } ? >
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.metal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_METAL_DefineSlot extends PHPTAL_Php_Attribute
+{
+ private $tmp_var;
+
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $this->tmp_var = $codewriter->createTempVariable();
+
+ $codewriter->doSetVar($this->tmp_var, $codewriter->interpolateTalesVarsInString($this->expression));
+ $codewriter->doIf('$ctx->hasSlot('.$this->tmp_var.')');
+ $codewriter->pushCode('$ctx->echoSlot('.$this->tmp_var.')');
+ $codewriter->doElse();
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->doEnd('if');
+
+ $codewriter->recycleTempVariable($this->tmp_var);
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/METAL/FillSlot.php b/lib/phptal/PHPTAL/Php/Attribute/METAL/FillSlot.php
new file mode 100644
index 0000000..2dfda3a
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/METAL/FillSlot.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * METAL Specification 1.0
+ *
+ * argument ::= Name
+ *
+ * Example:
+ *
+ * <table metal:use-macro="here/doc1/macros/sidebar">
+ * <tr><th>Links</th></tr>
+ * <tr><td metal:fill-slot="links">
+ * <a href="http://www.goodplace.com">Good Place</a><br>
+ * <a href="http://www.badplace.com">Bad Place</a><br>
+ * <a href="http://www.otherplace.com">Other Place</a>
+ * </td></tr>
+ * </table>
+ *
+ * PHPTAL:
+ *
+ * 1. evaluate slots
+ *
+ * <?php ob_start(); ? >
+ * <td>
+ * <a href="http://www.goodplace.com">Good Place</a><br>
+ * <a href="http://www.badplace.com">Bad Place</a><br>
+ * <a href="http://www.otherplace.com">Other Place</a>
+ * </td>
+ * <?php $tpl->slots->links = ob_get_contents(); ob_end_clean(); ? >
+ *
+ * 2. call the macro (here not supported)
+ *
+ * <?php echo phptal_macro($tpl, 'master_page.html/macros/sidebar'); ? >
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.metal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_METAL_FillSlot extends PHPTAL_Php_Attribute
+{
+ private static $uid = 0;
+ private $function_name;
+
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($this->shouldUseCallback()) {
+ $function_base_name = 'slot_'.preg_replace('/[^a-z0-9]/', '_', $this->expression).'_'.(self::$uid++);
+ $codewriter->doFunction($function_base_name, 'PHPTAL $_thistpl, PHPTAL $tpl');
+ $this->function_name = $codewriter->getFunctionPrefix().$function_base_name;
+
+ $codewriter->doSetVar('$ctx', '$tpl->getContext()');
+ $codewriter->doInitTranslator();
+ } else {
+ $codewriter->pushCode('ob_start()');
+ $this->function_name = null;
+ }
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($this->function_name !== null) {
+ $codewriter->doEnd();
+ $codewriter->pushCode('$ctx->fillSlotCallback('.$codewriter->str($this->expression).', '.$codewriter->str($this->function_name).', $_thistpl, clone $tpl)');
+ } else {
+ $codewriter->pushCode('$ctx->fillSlot('.$codewriter->str($this->expression).', ob_get_clean())');
+ }
+ }
+
+ // rough guess
+ const CALLBACK_THRESHOLD = 10000;
+
+ /**
+ * inspects contents of the element to decide whether callback makes sense
+ */
+ private function shouldUseCallback()
+ {
+ // since callback is slightly slower than buffering,
+ // use callback only for content that is large to offset speed loss by memory savings
+ return $this->estimateNumberOfBytesOutput($this->phpelement, false) > self::CALLBACK_THRESHOLD;
+ }
+
+ /**
+ * @param bool $is_nested_in_repeat true if any parent element has tal:repeat
+ *
+ * @return rough guess
+ */
+ private function estimateNumberOfBytesOutput(PHPTAL_Dom_Element $element, $is_nested_in_repeat)
+ {
+ // macros don't output anything on their own
+ if ($element->hasAttributeNS('http://xml.zope.org/namespaces/metal', 'define-macro')) {
+ return 0;
+ }
+
+ $estimated_bytes = 2*(3+strlen($element->getQualifiedName()));
+
+ foreach ($element->getAttributeNodes() as $attr) {
+ $estimated_bytes += 4+strlen($attr->getQualifiedName());
+ if ($attr->getReplacedState() === PHPTAL_Dom_Attr::NOT_REPLACED) {
+ $estimated_bytes += strlen($attr->getValueEscaped()); // this is shoddy for replaced attributes
+ }
+ }
+
+ $has_repeat_attr = $element->hasAttributeNS('http://xml.zope.org/namespaces/tal', 'repeat');
+
+ if ($element->hasAttributeNS('http://xml.zope.org/namespaces/tal', 'content') ||
+ $element->hasAttributeNS('http://xml.zope.org/namespaces/tal', 'replace')) {
+ // assume that output in loops is shorter (e.g. table rows) than outside (main content)
+ $estimated_bytes += ($has_repeat_attr || $is_nested_in_repeat) ? 500 : 2000;
+ } else {
+ foreach ($element->childNodes as $node) {
+ if ($node instanceof PHPTAL_Dom_Element) {
+ $estimated_bytes += $this->estimateNumberOfBytesOutput($node, $has_repeat_attr || $is_nested_in_repeat);
+ } else {
+ $estimated_bytes += strlen($node->getValueEscaped());
+ }
+ }
+ }
+
+ if ($element->hasAttributeNS('http://xml.zope.org/namespaces/metal', 'use-macro')) {
+ $estimated_bytes += ($has_repeat_attr || $is_nested_in_repeat) ? 500 : 2000;
+ }
+
+ if ($element->hasAttributeNS('http://xml.zope.org/namespaces/tal', 'condition')) {
+ $estimated_bytes /= 2; // naively assuming 50% chance, that works well with if/else pattern
+ }
+
+ if ($element->hasAttributeNS('http://xml.zope.org/namespaces/tal', 'repeat')) {
+ // assume people don't write big nested loops
+ $estimated_bytes *= $is_nested_in_repeat ? 5 : 10;
+ }
+
+ return $estimated_bytes;
+ }
+}
diff --git a/lib/phptal/PHPTAL/Php/Attribute/METAL/UseMacro.php b/lib/phptal/PHPTAL/Php/Attribute/METAL/UseMacro.php
new file mode 100644
index 0000000..83e8144
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/METAL/UseMacro.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * METAL Specification 1.0
+ *
+ * argument ::= expression
+ *
+ * Example:
+ *
+ * <hr />
+ * <p metal:use-macro="here/master_page/macros/copyright">
+ * <hr />
+ *
+ * PHPTAL: (here not supported)
+ *
+ * <?php echo phptal_macro( $tpl, 'master_page.html/macros/copyright'); ? >
+ *
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.metal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_METAL_UseMacro extends PHPTAL_Php_Attribute
+{
+ static $ALLOWED_ATTRIBUTES = array(
+ 'fill-slot'=>'http://xml.zope.org/namespaces/metal',
+ 'define-macro'=>'http://xml.zope.org/namespaces/metal',
+ 'define'=>'http://xml.zope.org/namespaces/tal',
+ );
+
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $this->pushSlots($codewriter);
+
+ foreach ($this->phpelement->childNodes as $child) {
+ $this->generateFillSlots($codewriter, $child);
+ }
+
+ $macroname = strtr($this->expression, '-', '_');
+
+ // throw error if attempting to define and use macro at same time
+ // [should perhaps be a TemplateException? but I don't know how to set that up...]
+ if ($defineAttr = $this->phpelement->getAttributeNodeNS(
+ 'http://xml.zope.org/namespaces/metal', 'define-macro')) {
+ if ($defineAttr->getValue() == $macroname)
+ throw new PHPTAL_TemplateException("Cannot simultaneously define and use macro '$macroname'",
+ $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+ }
+
+ // local macro (no filename specified) and non dynamic macro name
+ // can be called directly if it's a known function (just generated or seen in previous compilation)
+ if (preg_match('/^[a-z0-9_]+$/i', $macroname) && $codewriter->functionExists($macroname)) {
+ $code = $codewriter->getFunctionPrefix() . $macroname . '($_thistpl, $tpl)';
+ $codewriter->pushCode($code);
+ }
+ // external macro or ${macroname}, use PHPTAL at runtime to resolve it
+ else {
+ $code = $codewriter->interpolateTalesVarsInString($this->expression);
+ $codewriter->pushCode('$tpl->_executeMacroOfTemplate('.$code.', $_thistpl)');
+ }
+
+ $this->popSlots($codewriter);
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ }
+
+ /**
+ * reset template slots on each macro call ?
+ *
+ * NOTE: defining a macro and using another macro on the same tag
+ * means inheriting from the used macro, thus slots are shared, it
+ * is a little tricky to understand but very natural to use.
+ *
+ * For example, we may have a main design.html containing our main
+ * website presentation with some slots (menu, content, etc...) then
+ * we may define a member.html macro which use the design.html macro
+ * for the general layout, fill the menu slot and let caller templates
+ * fill the parent content slot without interfering.
+ */
+ private function pushSlots(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if (!$this->phpelement->hasAttributeNS('http://xml.zope.org/namespaces/metal', 'define-macro')) {
+ $codewriter->pushCode('$ctx->pushSlots()');
+ }
+ }
+
+ /**
+ * generate code that pops macro slots
+ * (restore slots if not inherited macro)
+ */
+ private function popSlots(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if (!$this->phpelement->hasAttributeNS('http://xml.zope.org/namespaces/metal', 'define-macro')) {
+ $codewriter->pushCode('$ctx->popSlots()');
+ }
+ }
+
+ /**
+ * recursively generates code for slots
+ */
+ private function generateFillSlots(PHPTAL_Php_CodeWriter $codewriter, PHPTAL_Dom_Node $phpelement)
+ {
+ if (false == ($phpelement instanceof PHPTAL_Dom_Element)) {
+ return;
+ }
+
+ // if the tag contains one of the allowed attribute, we generate it
+ foreach (self::$ALLOWED_ATTRIBUTES as $qname => $uri) {
+ if ($phpelement->hasAttributeNS($uri, $qname)) {
+ $phpelement->generateCode($codewriter);
+ return;
+ }
+ }
+
+ foreach ($phpelement->childNodes as $child) {
+ $this->generateFillSlots($codewriter, $child);
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Cache.php b/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Cache.php
new file mode 100644
index 0000000..3504a22
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Cache.php
@@ -0,0 +1,97 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * phptal:cache (note that's not tal:cache) caches element's HTML for a given time. Time is a number with 'd', 'h', 'm' or 's' suffix.
+ * There's optional parameter that defines how cache should be shared. By default cache is not sensitive to template's context at all
+ * - it's shared between all pages that use that template.
+ * You can add per url to have separate copy of given element for every URL.
+ *
+ * You can add per expression to have different cache copy for every different value of an expression (which MUST evaluate to a string).
+ * Expression cannot refer to variables defined using tal:define on the same element.
+ *
+ * NB:
+ * * phptal:cache blocks can be nested, but outmost block will cache other blocks regardless of their freshness.
+ * * you cannot use metal:fill-slot inside elements with phptal:cache
+ *
+ * Examples:
+ * <div phptal:cache="3h">...</div> <!-- <div> to be evaluated at most once per 3 hours. -->
+ * <ul phptal:cache="1d per object/id">...</ul> <!-- <ul> be cached for one day, separately for each object. -->
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.phptal
+*/
+class PHPTAL_Php_Attribute_PHPTAL_Cache extends PHPTAL_Php_Attribute
+{
+ private $cache_filename_var;
+
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ // number or variable name followed by time unit
+ // optional per expression
+ if (!preg_match('/^\s*([0-9]+\s*|[a-zA-Z][\/a-zA-Z0-9_]*\s+)([dhms])\s*(?:\;?\s*per\s+([^;]+)|)\s*$/', $this->expression, $matches)) {
+ throw new PHPTAL_ParserException("Cache attribute syntax error: ".$this->expression,
+ $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+ }
+
+ $cache_len = $matches[1];
+ if (!is_numeric($cache_len)) {
+ $cache_len = $codewriter->evaluateExpression($cache_len);
+
+ if (is_array($cache_len)) throw new PHPTAL_ParserException("Chained expressions in cache length are not supported",
+ $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+ }
+ switch ($matches[2]) {
+ case 'd': $cache_len .= '*24'; /* no break */
+ case 'h': $cache_len .= '*60'; /* no break */
+ case 'm': $cache_len .= '*60'; /* no break */
+ }
+
+ $cache_tag = '"'.addslashes( $this->phpelement->getQualifiedName() . ':' . $this->phpelement->getSourceLine()).'"';
+
+ $cache_per_expression = isset($matches[3])?trim($matches[3]):null;
+ if ($cache_per_expression == 'url') {
+ $cache_tag .= '.$_SERVER["REQUEST_URI"]';
+ } elseif ($cache_per_expression == 'nothing') {
+ /* do nothing */
+ } elseif ($cache_per_expression) {
+ $code = $codewriter->evaluateExpression($cache_per_expression);
+
+ if (is_array($code)) throw new PHPTAL_ParserException("Chained expressions in per-cache directive are not supported",
+ $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+
+ $cache_tag = '('.$code.')."@".' . $cache_tag;
+ }
+
+ $this->cache_filename_var = $codewriter->createTempVariable();
+ $codewriter->doSetVar($this->cache_filename_var, $codewriter->str($codewriter->getCacheFilesBaseName()).'.md5('.$cache_tag.')' );
+
+ $cond = '!file_exists('.$this->cache_filename_var.') || time() - '.$cache_len.' >= filemtime('.$this->cache_filename_var.')';
+
+ $codewriter->doIf($cond);
+ $codewriter->doEval('ob_start()');
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->doEval('file_put_contents('.$this->cache_filename_var.', ob_get_flush())');
+ $codewriter->doElse();
+ $codewriter->doEval('readfile('.$this->cache_filename_var.')');
+ $codewriter->doEnd('if');
+
+ $codewriter->recycleTempVariable($this->cache_filename_var);
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Debug.php b/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Debug.php
new file mode 100644
index 0000000..d0e9c9b
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Debug.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+/**
+ * @package PHPTAL
+ * @subpackage Php.attribute.phptal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_PHPTAL_Debug extends PHPTAL_Php_Attribute
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $this->_oldMode = $codewriter->setDebug(true);
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->setDebug($this->_oldMode);
+ }
+
+ private $_oldMode;
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Id.php b/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Id.php
new file mode 100644
index 0000000..dbee2a9
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Id.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+/**
+ * @package PHPTAL
+ * @subpackage Php.attribute.phptal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_PHPTAL_ID extends PHPTAL_Php_Attribute
+{
+ private $var;
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ // retrieve trigger
+ $this->var = $codewriter->createTempVariable();
+
+ $codewriter->doSetVar(
+ $this->var,
+ '$tpl->getTrigger('.$codewriter->str($this->expression).')'
+ );
+
+ // if trigger found and trigger tells to proceed, we execute
+ // the node content
+ $codewriter->doIf($this->var.' &&
+ '.$this->var.'->start('.$codewriter->str($this->expression).', $tpl) === PHPTAL_Trigger::PROCEED');
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ // end of if PROCEED
+ $codewriter->doEnd('if');
+
+ // if trigger found, notify the end of the node
+ $codewriter->doIf($this->var);
+ $codewriter->pushCode(
+ $this->var.'->end('.$codewriter->str($this->expression).', $tpl)'
+ );
+ $codewriter->doEnd('if');
+ $codewriter->recycleTempVariable($this->var);
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Tales.php b/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Tales.php
new file mode 100644
index 0000000..f1fb4d7
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/PHPTAL/Tales.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+/**
+ * @package PHPTAL
+ * @subpackage Php.attribute.phptal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_PHPTAL_TALES extends PHPTAL_Php_Attribute
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $mode = trim($this->expression);
+ $mode = strtolower($mode);
+
+ if ($mode == '' || $mode == 'default')
+ $mode = 'tales';
+
+ if ($mode != 'php' && $mode != 'tales') {
+ throw new PHPTAL_TemplateException("Unsupported TALES mode '$mode'",
+ $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+ }
+
+ $this->_oldMode = $codewriter->setTalesMode($mode);
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->setTalesMode($this->_oldMode);
+ }
+
+ private $_oldMode;
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/TAL/Attributes.php b/lib/phptal/PHPTAL/Php/Attribute/TAL/Attributes.php
new file mode 100644
index 0000000..158d079
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/TAL/Attributes.php
@@ -0,0 +1,213 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * TAL Specifications 1.4
+ *
+ * argument ::= attribute_statement [';' attribute_statement]*
+ * attribute_statement ::= attribute_name expression
+ * attribute_name ::= [namespace ':'] Name
+ * namespace ::= Name
+ *
+ * examples:
+ *
+ * <a href="/sample/link.html"
+ * tal:attributes="href here/sub/absolute_url">
+ * <textarea rows="80" cols="20"
+ * tal:attributes="rows request/rows;cols request/cols">
+ *
+ * IN PHPTAL: attributes will not work on structured replace.
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.tal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_TAL_Attributes
+extends PHPTAL_Php_Attribute
+implements PHPTAL_Php_TalesChainReader
+{
+ /** before creates several variables that need to be freed in after */
+ private $vars_to_recycle = array();
+
+ /**
+ * value for default keyword
+ */
+ private $_default_escaped;
+
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ // split attributes using ; delimiter
+ $attrs = $codewriter->splitExpression($this->expression);
+ foreach ($attrs as $exp) {
+ list($qname, $expression) = $this->parseSetExpression($exp);
+ if ($expression) {
+ $this->prepareAttribute($codewriter, $qname, $expression);
+ }
+ }
+ }
+
+ private function prepareAttribute(PHPTAL_Php_CodeWriter $codewriter, $qname, $expression)
+ {
+ $tales_code = $this->extractEchoType($expression);
+ $code = $codewriter->evaluateExpression($tales_code);
+
+ // XHTML boolean attribute does not appear when empty or false
+ if (PHPTAL_Dom_Defs::getInstance()->isBooleanAttribute($qname)) {
+
+ // I don't want to mix code for boolean with chained executor
+ // so compile it again to simple expression
+ if (is_array($code)) {
+ $code = PHPTAL_Php_TalesInternal::compileToPHPExpression($tales_code);
+ }
+ return $this->prepareBooleanAttribute($codewriter, $qname, $code);
+ }
+
+ // if $code is an array then the attribute value is decided by a
+ // tales chained expression
+ if (is_array($code)) {
+ return $this->prepareChainedAttribute($codewriter, $qname, $code);
+ }
+
+ // i18n needs to read replaced value of the attribute, which is not possible if attribute is completely replaced with conditional code
+ if ($this->phpelement->hasAttributeNS('http://xml.zope.org/namespaces/i18n', 'attributes')) {
+ $this->prepareAttributeUnconditional($codewriter, $qname, $code);
+ } else {
+ $this->prepareAttributeConditional($codewriter, $qname, $code);
+ }
+ }
+
+ /**
+ * attribute will be output regardless of its evaluated value. NULL behaves just like "".
+ */
+ private function prepareAttributeUnconditional(PHPTAL_Php_CodeWriter $codewriter, $qname, $code)
+ {
+ // regular attribute which value is the evaluation of $code
+ $attkey = $this->getVarName($qname, $codewriter);
+ if ($this->_echoType == PHPTAL_Php_Attribute::ECHO_STRUCTURE) {
+ $value = $codewriter->stringifyCode($code);
+ } else {
+ $value = $codewriter->escapeCode($code);
+ }
+ $codewriter->doSetVar($attkey, $value);
+ $this->phpelement->getOrCreateAttributeNode($qname)->overwriteValueWithVariable($attkey);
+ }
+
+ /**
+ * If evaluated value of attribute is NULL, it will not be output at all.
+ */
+ private function prepareAttributeConditional(PHPTAL_Php_CodeWriter $codewriter, $qname, $code)
+ {
+ // regular attribute which value is the evaluation of $code
+ $attkey = $this->getVarName($qname, $codewriter);
+
+ $codewriter->doIf("null !== ($attkey = ($code))");
+
+ if ($this->_echoType !== PHPTAL_Php_Attribute::ECHO_STRUCTURE)
+ $codewriter->doSetVar($attkey, $codewriter->str(" $qname=\"").".".$codewriter->escapeCode($attkey).".'\"'");
+ else
+ $codewriter->doSetVar($attkey, $codewriter->str(" $qname=\"").".".$codewriter->stringifyCode($attkey).".'\"'");
+
+ $codewriter->doElse();
+ $codewriter->doSetVar($attkey, "''");
+ $codewriter->doEnd('if');
+
+ $this->phpelement->getOrCreateAttributeNode($qname)->overwriteFullWithVariable($attkey);
+ }
+
+ private function prepareChainedAttribute(PHPTAL_Php_CodeWriter $codewriter, $qname, $chain)
+ {
+ $this->_default_escaped = false;
+ $this->_attribute = $qname;
+ if ($default_attr = $this->phpelement->getAttributeNode($qname)) {
+ $this->_default_escaped = $default_attr->getValueEscaped();
+ }
+ $this->_attkey = $this->getVarName($qname, $codewriter);
+ $executor = new PHPTAL_Php_TalesChainExecutor($codewriter, $chain, $this);
+ $this->phpelement->getOrCreateAttributeNode($qname)->overwriteFullWithVariable($this->_attkey);
+ }
+
+ private function prepareBooleanAttribute(PHPTAL_Php_CodeWriter $codewriter, $qname, $code)
+ {
+ $attkey = $this->getVarName($qname, $codewriter);
+
+ if ($codewriter->getOutputMode() === PHPTAL::HTML5) {
+ $value = "' $qname'";
+ } else {
+ $value = "' $qname=\"$qname\"'";
+ }
+ $codewriter->doIf($code);
+ $codewriter->doSetVar($attkey, $value);
+ $codewriter->doElse();
+ $codewriter->doSetVar($attkey, '\'\'');
+ $codewriter->doEnd('if');
+ $this->phpelement->getOrCreateAttributeNode($qname)->overwriteFullWithVariable($attkey);
+ }
+
+ private function getVarName($qname, PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $var = $codewriter->createTempVariable();
+ $this->vars_to_recycle[] = $var;
+ return $var;
+ }
+
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ foreach ($this->vars_to_recycle as $var) $codewriter->recycleTempVariable($var);
+ }
+
+ public function talesChainNothingKeyword(PHPTAL_Php_TalesChainExecutor $executor)
+ {
+ $codewriter = $executor->getCodeWriter();
+ $executor->doElse();
+ $codewriter->doSetVar(
+ $this->_attkey,
+ "''"
+ );
+ $executor->breakChain();
+ }
+
+ public function talesChainDefaultKeyword(PHPTAL_Php_TalesChainExecutor $executor)
+ {
+ $codewriter = $executor->getCodeWriter();
+ $executor->doElse();
+ $attr_str = ($this->_default_escaped !== false)
+ ? ' '.$this->_attribute.'='.$codewriter->quoteAttributeValue($this->_default_escaped) // default value
+ : ''; // do not print attribute
+ $codewriter->doSetVar($this->_attkey, $codewriter->str($attr_str));
+ $executor->breakChain();
+ }
+
+ public function talesChainPart(PHPTAL_Php_TalesChainExecutor $executor, $exp, $islast)
+ {
+ $codewriter = $executor->getCodeWriter();
+
+ if (!$islast) {
+ $condition = "!phptal_isempty($this->_attkey = ($exp))";
+ } else {
+ $condition = "null !== ($this->_attkey = ($exp))";
+ }
+ $executor->doIf($condition);
+
+ if ($this->_echoType == PHPTAL_Php_Attribute::ECHO_STRUCTURE)
+ $value = $codewriter->stringifyCode($this->_attkey);
+ else
+ $value = $codewriter->escapeCode($this->_attkey);
+
+ $codewriter->doSetVar($this->_attkey, $codewriter->str(" {$this->_attribute}=\"").".$value.'\"'");
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/TAL/Comment.php b/lib/phptal/PHPTAL/Php/Attribute/TAL/Comment.php
new file mode 100644
index 0000000..4e5896e
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/TAL/Comment.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+/**
+ * @package PHPTAL
+ * @subpackage Php.attribute.tal
+ */
+class PHPTAL_Php_Attribute_TAL_Comment extends PHPTAL_Php_Attribute
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->doComment($this->expression);
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/TAL/Condition.php b/lib/phptal/PHPTAL/Php/Attribute/TAL/Condition.php
new file mode 100644
index 0000000..d86b94b
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/TAL/Condition.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * TAL Specifications 1.4
+ *
+ * argument ::= expression
+ *
+ * Example:
+ *
+ * <p tal:condition="here/copyright"
+ * tal:content="here/copyright">(c) 2000</p>
+ *
+ *
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.tal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_TAL_Condition
+extends PHPTAL_Php_Attribute
+implements PHPTAL_Php_TalesChainReader
+{
+ private $expressions = array();
+
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $code = $codewriter->evaluateExpression($this->expression);
+
+ // If it's a chained expression build a new code path
+ if (is_array($code)) {
+ $this->expressions = array();
+ $executor = new PHPTAL_Php_TalesChainExecutor($codewriter, $code, $this);
+ return;
+ }
+
+ // Force a falsy condition if the nothing keyword is active
+ if ($code == PHPTAL_Php_TalesInternal::NOTHING_KEYWORD) {
+ $code = 'false';
+ }
+
+ $codewriter->doIf('phptal_true(' . $code . ')');
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->doEnd('if');
+ }
+
+
+ public function talesChainPart(PHPTAL_Php_TalesChainExecutor $executor, $exp, $islast)
+ {
+ // check if the expression is empty
+ if ($exp !== 'false') {
+ $this->expressions[] = '!phptal_isempty(' . $exp . ')';
+ }
+
+ if ($islast) {
+ // for the last one in the chain build a ORed condition
+ $executor->getCodeWriter()->doIf( implode(' || ', $this->expressions ) );
+ // The executor will always end an if so we output a dummy if
+ $executor->doIf('false');
+ }
+ }
+
+ public function talesChainNothingKeyword(PHPTAL_Php_TalesChainExecutor $executor)
+ {
+ // end the chain
+ $this->talesChainPart($executor, 'false', true);
+ $executor->breakChain();
+ }
+
+ public function talesChainDefaultKeyword(PHPTAL_Php_TalesChainExecutor $executor)
+ {
+ throw new PHPTAL_ParserException('\'default\' keyword not allowed on conditional expressions',
+ $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+ }
+
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/TAL/Content.php b/lib/phptal/PHPTAL/Php/Attribute/TAL/Content.php
new file mode 100644
index 0000000..aef5865
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/TAL/Content.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/** TAL Specifications 1.4
+ *
+ * argument ::= (['text'] | 'structure') expression
+ *
+ * Example:
+ *
+ * <p tal:content="user/name">Fred Farkas</p>
+ *
+ *
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.tal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_TAL_Content
+extends PHPTAL_Php_Attribute
+implements PHPTAL_Php_TalesChainReader
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $expression = $this->extractEchoType($this->expression);
+
+ $code = $codewriter->evaluateExpression($expression);
+
+ if (is_array($code)) {
+ return $this->generateChainedContent($codewriter, $code);
+ }
+
+ if ($code == PHPTAL_Php_TalesInternal::NOTHING_KEYWORD) {
+ return;
+ }
+
+ if ($code == PHPTAL_Php_TalesInternal::DEFAULT_KEYWORD) {
+ return $this->generateDefault($codewriter);
+ }
+
+ $this->doEchoAttribute($codewriter, $code);
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ }
+
+ private function generateDefault(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $this->phpelement->generateContent($codewriter, true);
+ }
+
+ protected function generateChainedContent(PHPTAL_Php_CodeWriter $codewriter, $code)
+ {
+ $executor = new PHPTAL_Php_TalesChainExecutor($codewriter, $code, $this);
+ }
+
+ public function talesChainPart(PHPTAL_Php_TalesChainExecutor $executor, $exp, $islast)
+ {
+ if (!$islast) {
+ $var = $executor->getCodeWriter()->createTempVariable();
+ $executor->doIf('!phptal_isempty('.$var.' = '.$exp.')');
+ $this->doEchoAttribute($executor->getCodeWriter(), $var);
+ $executor->getCodeWriter()->recycleTempVariable($var);
+ } else {
+ $executor->doElse();
+ $this->doEchoAttribute($executor->getCodeWriter(), $exp);
+ }
+ }
+
+ public function talesChainNothingKeyword(PHPTAL_Php_TalesChainExecutor $executor)
+ {
+ $executor->breakChain();
+ }
+
+ public function talesChainDefaultKeyword(PHPTAL_Php_TalesChainExecutor $executor)
+ {
+ $executor->doElse();
+ $this->generateDefault($executor->getCodeWriter());
+ $executor->breakChain();
+ }
+}
diff --git a/lib/phptal/PHPTAL/Php/Attribute/TAL/Define.php b/lib/phptal/PHPTAL/Php/Attribute/TAL/Define.php
new file mode 100644
index 0000000..f5c074b
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/TAL/Define.php
@@ -0,0 +1,193 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * TAL spec 1.4 for tal:define content
+ *
+ * argument ::= define_scope [';' define_scope]*
+ * define_scope ::= (['local'] | 'global') define_var
+ * define_var ::= variable_name expression
+ * variable_name ::= Name
+ *
+ * Note: If you want to include a semi-colon (;) in an expression, it must be escaped by doubling it (;;).*
+ *
+ * examples:
+ *
+ * tal:define="mytitle template/title; tlen python:len(mytitle)"
+ * tal:define="global company_name string:Digital Creations, Inc."
+ *
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.tal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_TAL_Define
+extends PHPTAL_Php_Attribute
+implements PHPTAL_Php_TalesChainReader
+{
+ private $tmp_content_var;
+ private $_buffered = false;
+ private $_defineScope = null;
+ private $_defineVar = null;
+ private $_pushedContext = false;
+ /**
+ * Prevents generation of invalid PHP code when given invalid TALES
+ */
+ private $_chainPartGenerated=false;
+
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $expressions = $codewriter->splitExpression($this->expression);
+ $definesAnyNonGlobalVars = false;
+
+ foreach ($expressions as $exp) {
+ list($defineScope, $defineVar, $expression) = $this->parseExpression($exp);
+ if (!$defineVar) {
+ continue;
+ }
+
+ $this->_defineScope = $defineScope;
+
+ // <span tal:define="global foo" /> should be invisible, but <img tal:define="bar baz" /> not
+ if ($defineScope != 'global') $definesAnyNonGlobalVars = true;
+
+ if ($this->_defineScope != 'global' && !$this->_pushedContext) {
+ $codewriter->pushContext();
+ $this->_pushedContext = true;
+ }
+
+ $this->_defineVar = $defineVar;
+ if ($expression === null) {
+ // no expression give, use content of tag as value for newly defined var.
+ $this->bufferizeContent($codewriter);
+ continue;
+ }
+
+ $code = $codewriter->evaluateExpression($expression);
+ if (is_array($code)) {
+ $this->chainedDefine($codewriter, $code);
+ } elseif ( $code == PHPTAL_Php_TalesInternal::NOTHING_KEYWORD) {
+ $this->doDefineVarWith($codewriter, 'null');
+ } else {
+ $this->doDefineVarWith($codewriter, $code);
+ }
+ }
+
+ // if the content of the tag was buffered or the tag has nothing to tell, we hide it.
+ if ($this->_buffered || (!$definesAnyNonGlobalVars && !$this->phpelement->hasRealContent() && !$this->phpelement->hasRealAttributes())) {
+ $this->phpelement->hidden = true;
+ }
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($this->tmp_content_var) $codewriter->recycleTempVariable($this->tmp_content_var);
+ if ($this->_pushedContext) {
+ $codewriter->popContext();
+ }
+ }
+
+ private function chainedDefine(PHPTAL_Php_CodeWriter $codewriter, $parts)
+ {
+ $executor = new PHPTAL_Php_TalesChainExecutor(
+ $codewriter, $parts, $this
+ );
+ }
+
+ public function talesChainNothingKeyword(PHPTAL_Php_TalesChainExecutor $executor)
+ {
+ if (!$this->_chainPartGenerated) throw new PHPTAL_TemplateException("Invalid expression in tal:define", $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+
+ $executor->doElse();
+ $this->doDefineVarWith($executor->getCodeWriter(), 'null');
+ $executor->breakChain();
+ }
+
+ public function talesChainDefaultKeyword(PHPTAL_Php_TalesChainExecutor $executor)
+ {
+ if (!$this->_chainPartGenerated) throw new PHPTAL_TemplateException("Invalid expression in tal:define", $this->phpelement->getSourceFile(), $this->phpelement->getSourceLine());
+
+ $executor->doElse();
+ $this->bufferizeContent($executor->getCodeWriter());
+ $executor->breakChain();
+ }
+
+ public function talesChainPart(PHPTAL_Php_TalesChainExecutor $executor, $exp, $islast)
+ {
+ $this->_chainPartGenerated=true;
+
+ if ($this->_defineScope == 'global') {
+ $var = '$tpl->getGlobalContext()->'.$this->_defineVar;
+ } else {
+ $var = '$ctx->'.$this->_defineVar;
+ }
+
+ $cw = $executor->getCodeWriter();
+
+ if (!$islast) {
+ // must use temp variable, because expression could refer to itself
+ $tmp = $cw->createTempVariable();
+ $executor->doIf('('.$tmp.' = '.$exp.') !== null');
+ $cw->doSetVar($var, $tmp);
+ $cw->recycleTempVariable($tmp);
+ } else {
+ $executor->doIf('('.$var.' = '.$exp.') !== null');
+ }
+ }
+
+ /**
+ * Parse the define expression, already splitted in sub parts by ';'.
+ */
+ public function parseExpression($exp)
+ {
+ $defineScope = false; // (local | global)
+ $defineVar = false; // var to define
+
+ // extract defineScope from expression
+ $exp = trim($exp);
+ if (preg_match('/^(local|global)\s+(.*?)$/ism', $exp, $m)) {
+ list(, $defineScope, $exp) = $m;
+ $exp = trim($exp);
+ }
+
+ // extract varname and expression from remaining of expression
+ list($defineVar, $exp) = $this->parseSetExpression($exp);
+ if ($exp !== null) $exp = trim($exp);
+ return array($defineScope, $defineVar, $exp);
+ }
+
+ private function bufferizeContent(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if (!$this->_buffered) {
+ $this->tmp_content_var = $codewriter->createTempVariable();
+ $codewriter->pushCode( 'ob_start()' );
+ $this->phpelement->generateContent($codewriter);
+ $codewriter->doSetVar($this->tmp_content_var, 'ob_get_clean()');
+ $this->_buffered = true;
+ }
+ $this->doDefineVarWith($codewriter, $this->tmp_content_var);
+ }
+
+ private function doDefineVarWith(PHPTAL_Php_CodeWriter $codewriter, $code)
+ {
+ if ($this->_defineScope == 'global') {
+ $codewriter->doSetVar('$tpl->getGlobalContext()->'.$this->_defineVar, $code);
+ } else {
+ $codewriter->doSetVar('$ctx->'.$this->_defineVar, $code);
+ }
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/TAL/OmitTag.php b/lib/phptal/PHPTAL/Php/Attribute/TAL/OmitTag.php
new file mode 100644
index 0000000..d7530b3
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/TAL/OmitTag.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * TAL Specifications 1.4
+ *
+ * argument ::= [expression]
+ *
+ * Example:
+ *
+ * <div tal:omit-tag="" comment="This tag will be removed">
+ * <i>...but this text will remain.</i>
+ * </div>
+ *
+ * <b tal:omit-tag="not:bold">I may not be bold.</b>
+ *
+ * To leave the contents of a tag in place while omitting the surrounding
+ * start and end tag, use the omit-tag statement.
+ *
+ * If its expression evaluates to a false value, then normal processing
+ * of the element continues.
+ *
+ * If the expression evaluates to a true value, or there is no
+ * expression, the statement tag is replaced with its contents. It is up to
+ * the interface between TAL and the expression engine to determine the
+ * value of true and false. For these purposes, the value nothing is false,
+ * and cancellation of the action has the same effect as returning a
+ * false value.
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.tal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_TAL_OmitTag extends PHPTAL_Php_Attribute
+{
+ private $varname;
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if (trim($this->expression) == '') {
+ $this->phpelement->headFootDisabled = true;
+ } else {
+
+ $this->varname = $codewriter->createTempVariable();
+
+ // print tag header/foot only if condition is false
+ $cond = $codewriter->evaluateExpression($this->expression);
+ $this->phpelement->headPrintCondition = '('.$this->varname.' = !phptal_unravel_closure('.$cond.'))';
+ $this->phpelement->footPrintCondition = $this->varname;
+ }
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($this->varname) $codewriter->recycleTempVariable($this->varname);
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/TAL/OnError.php b/lib/phptal/PHPTAL/Php/Attribute/TAL/OnError.php
new file mode 100644
index 0000000..382d387
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/TAL/OnError.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * TAL Specifications 1.4
+ *
+ * argument ::= (['text'] | 'structure') expression
+ *
+ * Example:
+ *
+ * <p tal:on-error="string: Error! This paragraph is buggy!">
+ * My name is <span tal:replace="here/SlimShady" />.<br />
+ * (My login name is
+ * <b tal:on-error="string: Username is not defined!"
+ * tal:content="user">Unknown</b>)
+ * </p>
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.tal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_TAL_OnError extends PHPTAL_Php_Attribute
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->doTry();
+ $codewriter->pushCode('ob_start()');
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $var = $codewriter->createTempVariable();
+
+ $codewriter->pushCode('ob_end_flush()');
+ $codewriter->doCatch('Exception '.$var);
+ $codewriter->pushCode('$tpl->addError('.$var.')');
+ $codewriter->pushCode('ob_end_clean()');
+
+ $expression = $this->extractEchoType($this->expression);
+
+ $code = $codewriter->evaluateExpression($expression);
+ switch ($code) {
+ case PHPTAL_Php_TalesInternal::NOTHING_KEYWORD:
+ break;
+
+ case PHPTAL_Php_TalesInternal::DEFAULT_KEYWORD:
+ $codewriter->pushHTML('<pre class="phptalError">');
+ $codewriter->doEcho($var);
+ $codewriter->pushHTML('</pre>');
+ break;
+
+ default:
+ $this->doEchoAttribute($codewriter, $code);
+ break;
+ }
+ $codewriter->doEnd('catch');
+
+ $codewriter->recycleTempVariable($var);
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/TAL/Repeat.php b/lib/phptal/PHPTAL/Php/Attribute/TAL/Repeat.php
new file mode 100644
index 0000000..d0e4c2d
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/TAL/Repeat.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * TAL Specifications 1.4
+ *
+ * argument ::= variable_name expression
+ * variable_name ::= Name
+ *
+ * Example:
+ *
+ * <p tal:repeat="txt python:'one', 'two', 'three'">
+ * <span tal:replace="txt" />
+ * </p>
+ * <table>
+ * <tr tal:repeat="item here/cart">
+ * <td tal:content="repeat/item/index">1</td>
+ * <td tal:content="item/description">Widget</td>
+ * <td tal:content="item/price">$1.50</td>
+ * </tr>
+ * </table>
+ *
+ * The following information is available from an Iterator:
+ *
+ * * index - repetition number, starting from zero.
+ * * number - repetition number, starting from one.
+ * * even - true for even-indexed repetitions (0, 2, 4, ...).
+ * * odd - true for odd-indexed repetitions (1, 3, 5, ...).
+ * * start - true for the starting repetition (index 0).
+ * * end - true for the ending, or final, repetition.
+ * * length - length of the sequence, which will be the total number of repetitions.
+ *
+ * * letter - count reps with lower-case letters: "a" - "z", "aa" - "az", "ba" - "bz", ..., "za" - "zz", "aaa" - "aaz", and so forth.
+ * * Letter - upper-case version of letter.
+ * * roman - count reps with lower-case roman numerals: "i", "ii", "iii", "iv", "v", "vi" ...
+ * * Roman - upper-case version of roman numerals.
+ * * first - true for the first item in a group - see note below
+ * * lasst - true for the last item in a group - see note below
+ *
+ * Note: first and last are intended for use with sorted sequences. They try to
+ * divide the sequence into group of items with the same value. If you provide
+ * a path, then the value obtained by following that path from a sequence item
+ * is used for grouping, otherwise the value of the item is used. You can
+ * provide the path by appending it to the path from the repeat variable,
+ * as in "repeat/item/first/color".
+ *
+ * PHPTAL: index, number, even, etc... will be stored in the
+ * $ctx->repeat->'item' object. Thus $ctx->repeat->item->odd
+ *
+ *
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.tal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_TAL_Repeat extends PHPTAL_Php_Attribute
+{
+ private $var;
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $this->var = $codewriter->createTempVariable();
+
+ // alias to repeats handler to avoid calling extra getters on each variable access
+ $codewriter->doSetVar($this->var, '$ctx->repeat');
+
+ list($varName, $expression) = $this->parseSetExpression($this->expression);
+ $code = $codewriter->evaluateExpression($expression);
+
+ // instantiate controller using expression
+ $codewriter->doSetVar( $this->var.'->'.$varName, 'new PHPTAL_RepeatController('.$code.')'."\n" );
+
+ $codewriter->pushContext();
+
+ // Lets loop the iterator with a foreach construct
+ $codewriter->doForeach('$ctx->'.$varName, $this->var.'->'.$varName);
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->doEnd('foreach');
+ $codewriter->popContext();
+
+ $codewriter->recycleTempVariable($this->var);
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/Attribute/TAL/Replace.php b/lib/phptal/PHPTAL/Php/Attribute/TAL/Replace.php
new file mode 100644
index 0000000..b72cafa
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Attribute/TAL/Replace.php
@@ -0,0 +1,117 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+/**
+ * TAL Specifications 1.4
+ *
+ * argument ::= (['text'] | 'structure') expression
+ *
+ * Default behaviour : text
+ *
+ * <span tal:replace="template/title">Title</span>
+ * <span tal:replace="text template/title">Title</span>
+ * <span tal:replace="structure table" />
+ * <span tal:replace="nothing">This element is a comment.</span>
+ *
+ *
+ *
+ * @package PHPTAL
+ * @subpackage Php.attribute.tal
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Attribute_TAL_Replace
+extends PHPTAL_Php_Attribute
+implements PHPTAL_Php_TalesChainReader
+{
+ public function before(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ // tal:replace="" => do nothing and ignore node
+ if (trim($this->expression) == "") {
+ return;
+ }
+
+ $expression = $this->extractEchoType($this->expression);
+ $code = $codewriter->evaluateExpression($expression);
+
+ // chained expression
+ if (is_array($code)) {
+ return $this->replaceByChainedExpression($codewriter, $code);
+ }
+
+ // nothing do nothing
+ if ($code == PHPTAL_Php_TalesInternal::NOTHING_KEYWORD) {
+ return;
+ }
+
+ // default generate default tag content
+ if ($code == PHPTAL_Php_TalesInternal::DEFAULT_KEYWORD) {
+ return $this->generateDefault($codewriter);
+ }
+
+ // replace tag with result of expression
+ $this->doEchoAttribute($codewriter, $code);
+ }
+
+ public function after(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ }
+
+ /**
+ * support expressions like "foo | bar"
+ */
+ private function replaceByChainedExpression(PHPTAL_Php_CodeWriter $codewriter, $expArray)
+ {
+ $executor = new PHPTAL_Php_TalesChainExecutor(
+ $codewriter, $expArray, $this
+ );
+ }
+
+ public function talesChainNothingKeyword(PHPTAL_Php_TalesChainExecutor $executor)
+ {
+ $executor->continueChain();
+ }
+
+ public function talesChainDefaultKeyword(PHPTAL_Php_TalesChainExecutor $executor)
+ {
+ $executor->doElse();
+ $this->generateDefault($executor->getCodeWriter());
+ $executor->breakChain();
+ }
+
+ public function talesChainPart(PHPTAL_Php_TalesChainExecutor $executor, $exp, $islast)
+ {
+ if (!$islast) {
+ $var = $executor->getCodeWriter()->createTempVariable();
+ $executor->doIf('!phptal_isempty('.$var.' = '.$exp.')');
+ $this->doEchoAttribute($executor->getCodeWriter(), $var);
+ $executor->getCodeWriter()->recycleTempVariable($var);
+ } else {
+ $executor->doElse();
+ $this->doEchoAttribute($executor->getCodeWriter(), $exp);
+ }
+ }
+
+ /**
+ * don't replace - re-generate default content
+ */
+ private function generateDefault(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $this->phpelement->generateSurroundHead($codewriter);
+ $this->phpelement->generateHead($codewriter);
+ $this->phpelement->generateContent($codewriter);
+ $this->phpelement->generateFoot($codewriter);
+ $this->phpelement->generateSurroundFoot($codewriter);
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/CodeWriter.php b/lib/phptal/PHPTAL/Php/CodeWriter.php
new file mode 100644
index 0000000..44ee063
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/CodeWriter.php
@@ -0,0 +1,511 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+/**
+ * Helps generate php representation of a template.
+ *
+ * @package PHPTAL
+ * @subpackage Php
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_CodeWriter
+{
+ /**
+ * max id of variable to give as temp
+ */
+ private $temp_var_counter=0;
+ /**
+ * stack with free'd variables
+ */
+ private $temp_recycling=array();
+
+ /**
+ * keeps track of seen functions for function_exists
+ */
+ private $known_functions = array();
+
+
+ public function __construct(PHPTAL_Php_State $state)
+ {
+ $this->_state = $state;
+ }
+
+ public function createTempVariable()
+ {
+ if (count($this->temp_recycling)) return array_shift($this->temp_recycling);
+ return '$_tmp_'.(++$this->temp_var_counter);
+ }
+
+ public function recycleTempVariable($var)
+ {
+ if (substr($var, 0, 6)!=='$_tmp_') throw new PHPTAL_Exception("Invalid variable recycled");
+ $this->temp_recycling[] = $var;
+ }
+
+ public function getCacheFilesBaseName()
+ {
+ return $this->_state->getCacheFilesBaseName();
+ }
+
+ public function getResult()
+ {
+ $this->flush();
+ if (version_compare(PHP_VERSION, '5.3', '>=') && __NAMESPACE__) {
+ return '<?php use '.'PHPTALNAMESPACE as P; ?>'.trim($this->_result);
+ } else {
+ return trim($this->_result);
+ }
+ }
+
+ /**
+ * set full '<!DOCTYPE...>' string to output later
+ *
+ * @param string $dt
+ *
+ * @return void
+ */
+ public function setDocType($dt)
+ {
+ $this->_doctype = $dt;
+ }
+
+ /**
+ * set full '<?xml ?>' string to output later
+ *
+ * @param string $dt
+ *
+ * @return void
+ */
+ public function setXmlDeclaration($dt)
+ {
+ $this->_xmldeclaration = $dt;
+ }
+
+ /**
+ * functions later generated and checked for existence will have this prefix added
+ * (poor man's namespace)
+ *
+ * @param string $prefix
+ *
+ * @return void
+ */
+ public function setFunctionPrefix($prefix)
+ {
+ $this->_functionPrefix = $prefix;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFunctionPrefix()
+ {
+ return $this->_functionPrefix;
+ }
+
+ /**
+ * @see PHPTAL_Php_State::setTalesMode()
+ *
+ * @param string $mode
+ *
+ * @return string
+ */
+ public function setTalesMode($mode)
+ {
+ return $this->_state->setTalesMode($mode);
+ }
+
+ public function splitExpression($src)
+ {
+ preg_match_all('/(?:[^;]+|;;)+/sm', $src, $array);
+ $array = $array[0];
+ foreach ($array as &$a) $a = str_replace(';;', ';', $a);
+ return $array;
+ }
+
+ public function evaluateExpression($src)
+ {
+ return $this->_state->evaluateExpression($src);
+ }
+
+ public function indent()
+ {
+ $this->_indentation ++;
+ }
+
+ public function unindent()
+ {
+ $this->_indentation --;
+ }
+
+ public function flush()
+ {
+ $this->flushCode();
+ $this->flushHtml();
+ }
+
+ public function noThrow($bool)
+ {
+ if ($bool) {
+ $this->pushCode('$ctx->noThrow(true)');
+ } else {
+ $this->pushCode('$ctx->noThrow(false)');
+ }
+ }
+
+ public function flushCode()
+ {
+ if (count($this->_codeBuffer) == 0) return;
+
+ // special treatment for one code line
+ if (count($this->_codeBuffer) == 1) {
+ $codeLine = $this->_codeBuffer[0];
+ // avoid adding ; after } and {
+ if (!preg_match('/\}\s*$|\{\s*$/', $codeLine))
+ $this->_result .= '<?php '.$codeLine."; ?>\n"; // PHP consumes newline
+ else
+ $this->_result .= '<?php '.$codeLine." ?>\n"; // PHP consumes newline
+ $this->_codeBuffer = array();
+ return;
+ }
+
+ $this->_result .= '<?php '."\n";
+ foreach ($this->_codeBuffer as $codeLine) {
+ // avoid adding ; after } and {
+ if (!preg_match('/[{};]\s*$/', $codeLine)) {
+ $codeLine .= ' ;'."\n";
+ }
+ $this->_result .= $codeLine;
+ }
+ $this->_result .= "?>\n";// PHP consumes newline
+ $this->_codeBuffer = array();
+ }
+
+ public function flushHtml()
+ {
+ if (count($this->_htmlBuffer) == 0) return;
+
+ $this->_result .= implode('', $this->_htmlBuffer);
+ $this->_htmlBuffer = array();
+ }
+
+ /**
+ * Generate code for setting DOCTYPE
+ *
+ * @param bool $called_from_macro for error checking: unbuffered output doesn't support that
+ */
+ public function doDoctype($called_from_macro = false)
+ {
+ if ($this->_doctype) {
+ $code = '$ctx->setDocType('.$this->str($this->_doctype).','.($called_from_macro?'true':'false').')';
+ $this->pushCode($code);
+ }
+ }
+
+ /**
+ * Generate XML declaration
+ *
+ * @param bool $called_from_macro for error checking: unbuffered output doesn't support that
+ */
+ public function doXmlDeclaration($called_from_macro = false)
+ {
+ if ($this->_xmldeclaration && $this->getOutputMode() !== PHPTAL::HTML5) {
+ $code = '$ctx->setXmlDeclaration('.$this->str($this->_xmldeclaration).','.($called_from_macro?'true':'false').')';
+ $this->pushCode($code);
+ }
+ }
+
+ public function functionExists($name)
+ {
+ return isset($this->known_functions[$this->_functionPrefix . $name]);
+ }
+
+ public function doTemplateFile($functionName, PHPTAL_Dom_Element $treeGen)
+ {
+ $this->doComment("\n*** DO NOT EDIT THIS FILE ***\n\nGenerated by PHPTAL from ".$treeGen->getSourceFile()." (edit that file instead)");
+ $this->doFunction($functionName, 'PHPTAL $tpl, PHPTAL_Context $ctx');
+ $this->setFunctionPrefix($functionName . "_");
+ $this->doSetVar('$_thistpl', '$tpl');
+ $this->doInitTranslator();
+ $treeGen->generateCode($this);
+ $this->doComment("end");
+ $this->doEnd('function');
+ }
+
+ public function doFunction($name, $params)
+ {
+ $name = $this->_functionPrefix . $name;
+ $this->known_functions[$name] = true;
+
+ $this->pushCodeWriterContext();
+ $this->pushCode("function $name($params) {\n");
+ $this->indent();
+ $this->_segments[] = 'function';
+ }
+
+ public function doComment($comment)
+ {
+ $comment = str_replace('*/', '* /', $comment);
+ $this->pushCode("/* $comment */");
+ }
+
+ public function doInitTranslator()
+ {
+ if ($this->_state->isTranslationOn()) {
+ $this->doSetVar('$_translator', '$tpl->getTranslator()');
+ }
+ }
+
+ public function getTranslatorReference()
+ {
+ if (!$this->_state->isTranslationOn()) {
+ throw new PHPTAL_ConfigurationException("i18n used, but Translator has not been set");
+ }
+ return '$_translator';
+ }
+
+ public function doEval($code)
+ {
+ $this->pushCode($code);
+ }
+
+ public function doForeach($out, $source)
+ {
+ $this->_segments[] = 'foreach';
+ $this->pushCode("foreach ($source as $out):");
+ $this->indent();
+ }
+
+ public function doEnd($expects = null)
+ {
+ if (!count($this->_segments)) {
+ if (!$expects) $expects = 'anything';
+ throw new PHPTAL_Exception("Bug: CodeWriter generated end of block without $expects open");
+ }
+
+ $segment = array_pop($this->_segments);
+ if ($expects !== null && $segment !== $expects) {
+ throw new PHPTAL_Exception("Bug: CodeWriter generated end of $expects, but needs to close $segment");
+ }
+
+ $this->unindent();
+ if ($segment == 'function') {
+ $this->pushCode("\n}\n\n");
+ $this->flush();
+ $functionCode = $this->_result;
+ $this->popCodeWriterContext();
+ $this->_result = $functionCode . $this->_result;
+ } elseif ($segment == 'try')
+ $this->pushCode('}');
+ elseif ($segment == 'catch')
+ $this->pushCode('}');
+ else
+ $this->pushCode("end$segment");
+ }
+
+ public function doTry()
+ {
+ $this->_segments[] = 'try';
+ $this->pushCode('try {');
+ $this->indent();
+ }
+
+ public function doSetVar($varname, $code)
+ {
+ $this->pushCode($varname.' = '.$code);
+ }
+
+ public function doCatch($catch)
+ {
+ $this->doEnd('try');
+ $this->_segments[] = 'catch';
+ $this->pushCode('catch('.$catch.') {');
+ $this->indent();
+ }
+
+ public function doIf($condition)
+ {
+ $this->_segments[] = 'if';
+ $this->pushCode('if ('.$condition.'): ');
+ $this->indent();
+ }
+
+ public function doElseIf($condition)
+ {
+ if (end($this->_segments) !== 'if') {
+ throw new PHPTAL_Exception("Bug: CodeWriter generated elseif without if");
+ }
+ $this->unindent();
+ $this->pushCode('elseif ('.$condition.'): ');
+ $this->indent();
+ }
+
+ public function doElse()
+ {
+ if (end($this->_segments) !== 'if') {
+ throw new PHPTAL_Exception("Bug: CodeWriter generated else without if");
+ }
+ $this->unindent();
+ $this->pushCode('else: ');
+ $this->indent();
+ }
+
+ public function doEcho($code)
+ {
+ if ($code === "''") return;
+ $this->flush();
+ $this->pushCode('echo '.$this->escapeCode($code));
+ }
+
+ public function doEchoRaw($code)
+ {
+ if ($code === "''") return;
+ $this->pushCode('echo '.$this->stringifyCode($code));
+ }
+
+ public function interpolateHTML($html)
+ {
+ return $this->_state->interpolateTalesVarsInHtml($html);
+ }
+
+ public function interpolateCDATA($str)
+ {
+ return $this->_state->interpolateTalesVarsInCDATA($str);
+ }
+
+ public function pushHTML($html)
+ {
+ if ($html === "") return;
+ $this->flushCode();
+ $this->_htmlBuffer[] = $html;
+ }
+
+ public function pushCode($codeLine)
+ {
+ $this->flushHtml();
+ $codeLine = $this->indentSpaces() . $codeLine;
+ $this->_codeBuffer[] = $codeLine;
+ }
+
+ /**
+ * php string with escaped text
+ */
+ public function str($string)
+ {
+ return "'".strtr($string,array("'"=>'\\\'','\\'=>'\\\\'))."'";
+ }
+
+ public function escapeCode($code)
+ {
+ return $this->_state->htmlchars($code);
+ }
+
+ public function stringifyCode($code)
+ {
+ return $this->_state->stringify($code);
+ }
+
+ public function getEncoding()
+ {
+ return $this->_state->getEncoding();
+ }
+
+ public function interpolateTalesVarsInString($src)
+ {
+ return $this->_state->interpolateTalesVarsInString($src);
+ }
+
+ public function setDebug($bool)
+ {
+ return $this->_state->setDebug($bool);
+ }
+
+ public function isDebugOn()
+ {
+ return $this->_state->isDebugOn();
+ }
+
+ public function getOutputMode()
+ {
+ return $this->_state->getOutputMode();
+ }
+
+ public function quoteAttributeValue($value)
+ {
+ // FIXME: interpolation is done _after_ that function, so ${} must be forbidden for now
+
+ if ($this->getEncoding() == 'UTF-8') // HTML 5: 8.1.2.3 Attributes ; http://code.google.com/p/html5lib/issues/detail?id=93
+ {
+ // regex excludes unicode control characters, all kinds of whitespace and unsafe characters
+ // and trailing / to avoid confusion with self-closing syntax
+ $unsafe_attr_regex = '/^$|[&=\'"><\s`\pM\pC\pZ\p{Pc}\p{Sk}]|\/$|\${/u';
+ } else {
+ $unsafe_attr_regex = '/^$|[&=\'"><\s`\0177-\377]|\/$|\${/';
+ }
+
+ if ($this->getOutputMode() == PHPTAL::HTML5 && !preg_match($unsafe_attr_regex, $value)) {
+ return $value;
+ } else {
+ return '"'.$value.'"';
+ }
+ }
+
+ public function pushContext()
+ {
+ $this->doSetVar('$ctx', '$tpl->pushContext()');
+ }
+
+ public function popContext()
+ {
+ $this->doSetVar('$ctx', '$tpl->popContext()');
+ }
+
+ // ~~~~~ Private members ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ private function indentSpaces()
+ {
+ return str_repeat("\t", $this->_indentation);
+ }
+
+ private function pushCodeWriterContext()
+ {
+ $this->_contexts[] = clone $this;
+ $this->_result = "";
+ $this->_indentation = 0;
+ $this->_codeBuffer = array();
+ $this->_htmlBuffer = array();
+ $this->_segments = array();
+ }
+
+ private function popCodeWriterContext()
+ {
+ $oldContext = array_pop($this->_contexts);
+ $this->_result = $oldContext->_result;
+ $this->_indentation = $oldContext->_indentation;
+ $this->_codeBuffer = $oldContext->_codeBuffer;
+ $this->_htmlBuffer = $oldContext->_htmlBuffer;
+ $this->_segments = $oldContext->_segments;
+ }
+
+ private $_state;
+ private $_result = "";
+ private $_indentation = 0;
+ private $_codeBuffer = array();
+ private $_htmlBuffer = array();
+ private $_segments = array();
+ private $_contexts = array();
+ private $_functionPrefix = "";
+ private $_doctype = "";
+ private $_xmldeclaration = "";
+}
+
diff --git a/lib/phptal/PHPTAL/Php/State.php b/lib/phptal/PHPTAL/Php/State.php
new file mode 100644
index 0000000..cc4f193
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/State.php
@@ -0,0 +1,254 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * @package PHPTAL
+ * @subpackage Php
+ */
+class PHPTAL_Php_State
+{
+ private $debug = false;
+ private $tales_mode = 'tales';
+ private $encoding;
+ private $output_mode;
+ private $phptal;
+
+ function __construct(PHPTAL $phptal)
+ {
+ $this->phptal = $phptal;
+ $this->encoding = $phptal->getEncoding();
+ $this->output_mode = $phptal->getOutputMode();
+ }
+
+ /**
+ * used by codewriter to get information for phptal:cache
+ */
+ public function getCacheFilesBaseName()
+ {
+ return $this->phptal->getCodePath();
+ }
+
+ /**
+ * true if PHPTAL has translator set
+ */
+ public function isTranslationOn()
+ {
+ return !!$this->phptal->getTranslator();
+ }
+
+ /**
+ * controlled by phptal:debug
+ */
+ public function setDebug($bool)
+ {
+ $old = $this->debug;
+ $this->debug = $bool;
+ return $old;
+ }
+
+ /**
+ * if true, add additional diagnostic information to generated code
+ */
+ public function isDebugOn()
+ {
+ return $this->debug;
+ }
+
+ /**
+ * Sets new and returns old TALES mode.
+ * Valid modes are 'tales' and 'php'
+ *
+ * @param string $mode
+ *
+ * @return string
+ */
+ public function setTalesMode($mode)
+ {
+ $old = $this->tales_mode;
+ $this->tales_mode = $mode;
+ return $old;
+ }
+
+ public function getTalesMode()
+ {
+ return $this->tales_mode;
+ }
+
+ /**
+ * encoding used for both template input and output
+ */
+ public function getEncoding()
+ {
+ return $this->encoding;
+ }
+
+ /**
+ * Syntax rules to follow in generated code
+ *
+ * @return one of PHPTAL::XHTML, PHPTAL::XML, PHPTAL::HTML5
+ */
+ public function getOutputMode()
+ {
+ return $this->output_mode;
+ }
+
+ /**
+ * Load prefilter
+ */
+ public function getPreFilterByName($name)
+ {
+ return $this->phptal->getPreFilterByName($name);
+ }
+
+ /**
+ * compile TALES expression according to current talesMode
+ * @return string with PHP code or array with expressions for TalesChainExecutor
+ */
+ public function evaluateExpression($expression)
+ {
+ if ($this->getTalesMode() === 'php') {
+ return PHPTAL_Php_TalesInternal::php($expression);
+ }
+ return PHPTAL_Php_TalesInternal::compileToPHPExpressions($expression, false);
+ }
+
+ /**
+ * compile TALES expression according to current talesMode
+ * @return string with PHP code
+ */
+ private function compileTalesToPHPExpression($expression)
+ {
+ if ($this->getTalesMode() === 'php') {
+ return PHPTAL_Php_TalesInternal::php($expression);
+ }
+ return PHPTAL_Php_TalesInternal::compileToPHPExpression($expression, false);
+ }
+
+ /**
+ * returns PHP code that generates given string, including dynamic replacements
+ *
+ * It's almost unused.
+ */
+ public function interpolateTalesVarsInString($string)
+ {
+ return PHPTAL_Php_TalesInternal::parseString($string, false, ($this->getTalesMode() === 'tales') ? '' : 'php:' );
+ }
+
+ /**
+ * replaces ${} in string, expecting HTML-encoded input and HTML-escapes output
+ */
+ public function interpolateTalesVarsInHTML($src)
+ {
+ return preg_replace_callback('/((?:\$\$)*)\$\{(structure |text )?(.*?)\}|((?:\$\$)+)\{/isS',
+ array($this,'_interpolateTalesVarsInHTMLCallback'), $src);
+ }
+
+ /**
+ * callback for interpolating TALES with HTML-escaping
+ */
+ private function _interpolateTalesVarsInHTMLCallback($matches)
+ {
+ return $this->_interpolateTalesVarsCallback($matches, 'html');
+ }
+
+ /**
+ * replaces ${} in string, expecting CDATA (basically unescaped) input,
+ * generates output protected against breaking out of CDATA in XML/HTML
+ * (depending on current output mode).
+ */
+ public function interpolateTalesVarsInCDATA($src)
+ {
+ return preg_replace_callback('/((?:\$\$)*)\$\{(structure |text )?(.*?)\}|((?:\$\$)+)\{/isS',
+ array($this,'_interpolateTalesVarsInCDATACallback'), $src);
+ }
+
+ /**
+ * callback for interpolating TALES with CDATA escaping
+ */
+ private function _interpolateTalesVarsInCDATACallback($matches)
+ {
+ return $this->_interpolateTalesVarsCallback($matches, 'cdata');
+ }
+
+ private function _interpolateTalesVarsCallback($matches, $format)
+ {
+ // replaces $${ with literal ${ (or $$$${ with $${ etc)
+ if (!empty($matches[4])) {
+ return substr($matches[4], strlen($matches[4])/2).'{';
+ }
+
+ // same replacement, but before executed expression
+ $dollars = substr($matches[1], strlen($matches[1])/2);
+
+ $code = $matches[3];
+ if ($format == 'html') {
+ $code = html_entity_decode($code, ENT_QUOTES, $this->getEncoding());
+ }
+
+ $code = $this->compileTalesToPHPExpression($code);
+
+ if (rtrim($matches[2]) == 'structure') { // regex captures a space there
+ return $dollars.'<?php echo '.$this->stringify($code)." ?>\n";
+ } else {
+ if ($format == 'html') {
+ return $dollars.'<?php echo '.$this->htmlchars($code)." ?>\n";
+ }
+ if ($format == 'cdata') {
+ // quite complex for an "unescaped" section, isn't it?
+ if ($this->getOutputMode() === PHPTAL::HTML5) {
+ return $dollars."<?php echo str_replace('</','<\\\\/', ".$this->stringify($code).") ?>\n";
+ } elseif ($this->getOutputMode() === PHPTAL::XHTML) {
+ // both XML and HMTL, because people will inevitably send it as text/html :(
+ return $dollars."<?php echo strtr(".$this->stringify($code)." ,array(']]>'=>']]]]><![CDATA[>','</'=>'<\\/')) ?>\n";
+ } else {
+ return $dollars."<?php echo str_replace(']]>',']]]]><![CDATA[>', ".$this->stringify($code).") ?>\n";
+ }
+ }
+ assert(0);
+ }
+ }
+
+ /**
+ * expects PHP code and returns PHP code that will generate escaped string
+ * Optimizes case when PHP string is given.
+ *
+ * @return php code
+ */
+ public function htmlchars($php)
+ {
+ // PHP strings can be escaped at compile time
+ if (preg_match('/^\'((?:[^\'{]+|\\\\.)*)\'$/s', $php, $m)) {
+ return "'".htmlspecialchars(str_replace('\\\'', "'", $m[1]), ENT_QUOTES, $this->encoding)."'";
+ }
+ return 'phptal_escape('.$php.', \''.$this->encoding.'\')';
+ }
+
+ /**
+ * allow proper printing of any object
+ * (without escaping - for use with structure keyword)
+ *
+ * @return php code
+ */
+ public function stringify($php)
+ {
+ // PHP strings don't need to be changed
+ if (preg_match('/^\'(?>[^\'\\\\]+|\\\\.)*\'$|^\s*"(?>[^"\\\\]+|\\\\.)*"\s*$/s', $php)) {
+ return $php;
+ }
+ return 'phptal_tostring('.$php.')';
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Php/TalesChainExecutor.php b/lib/phptal/PHPTAL/Php/TalesChainExecutor.php
new file mode 100644
index 0000000..da94724
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/TalesChainExecutor.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * @package PHPTAL
+ * @subpackage Php
+ */
+class PHPTAL_Php_TalesChainExecutor
+{
+ const CHAIN_BREAK = 1;
+ const CHAIN_CONT = 2;
+
+ public function __construct(PHPTAL_Php_CodeWriter $codewriter, array $chain, PHPTAL_Php_TalesChainReader $reader)
+ {
+ $this->_chain = $chain;
+ $this->_chainStarted = false;
+ $this->codewriter = $codewriter;
+ $this->_reader = $reader;
+ $this->_executeChain();
+ }
+
+ public function getCodeWriter()
+ {
+ return $this->codewriter;
+ }
+
+ public function doIf($condition)
+ {
+ if ($this->_chainStarted == false) {
+ $this->_chainStarted = true;
+ $this->codewriter->doIf($condition);
+ } else {
+ $this->codewriter->doElseIf($condition);
+ }
+ }
+
+ public function doElse()
+ {
+ $this->codewriter->doElse();
+ }
+
+ public function breakChain()
+ {
+ $this->_state = self::CHAIN_BREAK;
+ }
+
+ public function continueChain()
+ {
+ $this->_state = self::CHAIN_CONT;
+ }
+
+ private function _executeChain()
+ {
+ $this->codewriter->noThrow(true);
+
+ end($this->_chain); $lastkey = key($this->_chain);
+
+ foreach ($this->_chain as $key => $exp) {
+ $this->_state = 0;
+
+ if ($exp == PHPTAL_Php_TalesInternal::NOTHING_KEYWORD) {
+ $this->_reader->talesChainNothingKeyword($this);
+ } elseif ($exp == PHPTAL_Php_TalesInternal::DEFAULT_KEYWORD) {
+ $this->_reader->talesChainDefaultKeyword($this);
+ } else {
+ $this->_reader->talesChainPart($this, $exp, $lastkey === $key);
+ }
+
+ if ($this->_state == self::CHAIN_BREAK)
+ break;
+ if ($this->_state == self::CHAIN_CONT)
+ continue;
+ }
+
+ $this->codewriter->doEnd('if');
+ $this->codewriter->noThrow(false);
+ }
+
+ private $_state = 0;
+ private $_chain;
+ private $_chainStarted = false;
+ private $codewriter = null;
+}
diff --git a/lib/phptal/PHPTAL/Php/TalesChainReader.php b/lib/phptal/PHPTAL/Php/TalesChainReader.php
new file mode 100644
index 0000000..4992bfe
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/TalesChainReader.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * @package PHPTAL
+ * @subpackage Php
+ */
+interface PHPTAL_Php_TalesChainReader
+{
+ public function talesChainNothingKeyword(PHPTAL_Php_TalesChainExecutor $executor);
+ public function talesChainDefaultKeyword(PHPTAL_Php_TalesChainExecutor $executor);
+ public function talesChainPart(PHPTAL_Php_TalesChainExecutor $executor, $expression, $islast);
+}
diff --git a/lib/phptal/PHPTAL/Php/TalesInternal.php b/lib/phptal/PHPTAL/Php/TalesInternal.php
new file mode 100644
index 0000000..4e84a66
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/TalesInternal.php
@@ -0,0 +1,503 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Moritz Bechler <mbechler@eenterphace.org>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * TALES Specification 1.3
+ *
+ * Expression ::= [type_prefix ':'] String
+ * type_prefix ::= Name
+ *
+ * Examples:
+ *
+ * a/b/c
+ * path:a/b/c
+ * nothing
+ * path:nothing
+ * python: 1 + 2
+ * string:Hello, ${username}
+ *
+ *
+ * Builtin Names in Page Templates (for PHPTAL)
+ *
+ * * nothing - special singleton value used by TAL to represent a
+ * non-value (e.g. void, None, Nil, NULL).
+ *
+ * * default - special singleton value used by TAL to specify that
+ * existing text should not be replaced.
+ *
+ * * repeat - the repeat variables (see RepeatVariable).
+ *
+ *
+ */
+
+/**
+ * @package PHPTAL
+ * @subpackage Php
+ */
+class PHPTAL_Php_TalesInternal implements PHPTAL_Tales
+{
+ const DEFAULT_KEYWORD = 'new PHPTAL_DefaultKeyword';
+ const NOTHING_KEYWORD = 'new PHPTAL_NothingKeyword';
+
+ static public function true($src, $nothrow)
+ {
+ return 'phptal_true(' . self::compileToPHPExpression($src, true) . ')';
+ }
+
+ /**
+ * not:
+ *
+ * not: Expression
+ *
+ * evaluate the expression string (recursively) as a full expression,
+ * and returns the boolean negation of its value
+ *
+ * return boolean based on the following rules:
+ *
+ * 1. integer 0 is false
+ * 2. integer > 0 is true
+ * 3. an empty string or other sequence is false
+ * 4. a non-empty string or other sequence is true
+ * 5. a non-value (e.g. void, None, Nil, NULL, etc) is false
+ * 6. all other values are implementation-dependent.
+ *
+ * Examples:
+ *
+ * not: exists: foo/bar/baz
+ * not: php: object.hasChildren()
+ * not: string:${foo}
+ * not: foo/bar/booleancomparable
+ */
+ static public function not($expression, $nothrow)
+ {
+ return '!phptal_true(' . self::compileToPHPExpression($expression, $nothrow) . ')';
+ }
+
+
+ /**
+ * path:
+ *
+ * PathExpr ::= Path [ '|' Path ]*
+ * Path ::= variable [ '/' URL_Segment ]*
+ * variable ::= Name
+ *
+ * Examples:
+ *
+ * path: username
+ * path: user/name
+ * path: object/method/10/method/member
+ * path: object/${dynamicmembername}/method
+ * path: maybethis | path: maybethat | path: default
+ *
+ * PHPTAL:
+ *
+ * 'default' may lead to some 'difficult' attributes implementation
+ *
+ * For example, the tal:content will have to insert php code like:
+ *
+ * if (isset($ctx->maybethis)) {
+ * echo $ctx->maybethis;
+ * }
+ * elseif (isset($ctx->maybethat) {
+ * echo $ctx->maybethat;
+ * }
+ * else {
+ * // process default tag content
+ * }
+ *
+ * @returns string or array
+ */
+ static public function path($expression, $nothrow=false)
+ {
+ $expression = trim($expression);
+ if ($expression == 'default') return self::DEFAULT_KEYWORD;
+ if ($expression == 'nothing') return self::NOTHING_KEYWORD;
+ if ($expression == '') return self::NOTHING_KEYWORD;
+
+ // split OR expressions terminated by a string
+ if (preg_match('/^(.*?)\s*\|\s*?(string:.*)$/sm', $expression, $m)) {
+ list(, $expression, $string) = $m;
+ }
+ // split OR expressions terminated by a 'fast' string
+ elseif (preg_match('/^(.*?)\s*\|\s*\'((?:[^\'\\\\]|\\\\.)*)\'\s*$/sm', $expression, $m)) {
+ list(, $expression, $string) = $m;
+ $string = 'string:'.stripslashes($string);
+ }
+
+ // split OR expressions
+ $exps = preg_split('/\s*\|\s*/sm', $expression);
+
+ // if (many expressions) or (expressions or terminating string) found then
+ // generate the array of sub expressions and return it.
+ if (count($exps) > 1 || isset($string)) {
+ $result = array();
+ foreach ($exps as $i=>$exp) {
+ if(isset($string) || $i < count($exps) - 1) {
+ $result[] = self::compileToPHPExpressions(trim($exp), true);
+ }
+ else {
+ // the last expression can thorw exception.
+ $result[] = self::compileToPHPExpressions(trim($exp), false);
+ }
+ }
+ if (isset($string)) {
+ $result[] = self::compileToPHPExpressions($string, true);
+ }
+ return $result;
+ }
+
+
+ // see if there are subexpressions, but skip interpolated parts, i.e. ${a/b}/c is 2 parts
+ if (preg_match('/^((?:[^$\/]+|\$\$|\${[^}]+}|\$))\/(.+)$/s', $expression, $m))
+ {
+ if (!self::checkExpressionPart($m[1])) {
+ throw new PHPTAL_ParserException("Invalid TALES path: '$expression', expected '{$m[1]}' to be variable name");
+ }
+
+ $next = self::string($m[1]);
+ $expression = self::string($m[2]);
+ } else {
+ if (!self::checkExpressionPart($expression)) {
+ throw new PHPTAL_ParserException("Invalid TALES path: '$expression', expected variable name. Complex expressions need php: modifier.");
+ }
+
+ $next = self::string($expression);
+ $expression = null;
+ }
+
+ if ($nothrow) {
+ return '$ctx->path($ctx, ' . $next . ($expression === null ? '' : '."/".'.$expression) . ', true)';
+ }
+
+ if (preg_match('/^\'[a-z][a-z0-9_]*\'$/i', $next)) $next = substr($next, 1, -1); else $next = '{'.$next.'}';
+
+ // if no sub part for this expression, just optimize the generated code
+ // and access the $ctx->var
+ if ($expression === null) {
+ return '$ctx->'.$next;
+ }
+
+ // otherwise we have to call PHPTAL_Context::path() to resolve the path at runtime
+ // extract the first part of the expression (it will be the PHPTAL_Context::path()
+ // $base and pass the remaining of the path to PHPTAL_Context::path()
+ return '$ctx->path($ctx->'.$next.', '.$expression.')';
+ }
+
+ /**
+ * check if part of exprssion (/foo/ or /foo${bar}/) is alphanumeric
+ */
+ private static function checkExpressionPart($expression)
+ {
+ $expression = preg_replace('/\${[^}]+}/', 'a', $expression); // pretend interpolation is done
+ return preg_match('/^[a-z_][a-z0-9_]*$/i', $expression);
+ }
+
+ /**
+ * string:
+ *
+ * string_expression ::= ( plain_string | [ varsub ] )*
+ * varsub ::= ( '$' Path ) | ( '${' Path '}' )
+ * plain_string ::= ( '$$' | non_dollar )*
+ * non_dollar ::= any character except '$'
+ *
+ * Examples:
+ *
+ * string:my string
+ * string:hello, $username how are you
+ * string:hello, ${user/name}
+ * string:you have $$130 in your bank account
+ */
+ static public function string($expression, $nothrow=false)
+ {
+ return self::parseString($expression, $nothrow, '');
+ }
+
+ /**
+ * @param string $tales_prefix prefix added to all TALES in the string
+ */
+ static public function parseString($expression, $nothrow, $tales_prefix)
+ {
+ // This is a simple parser which evaluates ${foo} inside
+ // 'string:foo ${foo} bar' expressions, it returns the php code which will
+ // print the string with correct interpollations.
+ // Nothing special there :)
+
+ $inPath = false;
+ $inAccoladePath = false;
+ $lastWasDollar = false;
+ $result = '';
+ $len = strlen($expression);
+ for ($i=0; $i<$len; $i++) {
+ $c = $expression[$i];
+ switch ($c) {
+ case '$':
+ if ($lastWasDollar) {
+ $lastWasDollar = false;
+ } elseif ($inAccoladePath) {
+ $subPath .= $c;
+ $c = '';
+ } else {
+ $lastWasDollar = true;
+ $c = '';
+ }
+ break;
+
+ case '\\':
+ if ($inAccoladePath) {
+ $subPath .= $c;
+ $c = '';
+ }
+ else {
+ $c = '\\\\';
+ }
+ break;
+
+ case '\'':
+ if ($inAccoladePath) {
+ $subPath .= $c;
+ $c = '';
+ }
+ else {
+ $c = '\\\'';
+ }
+ break;
+
+ case '{':
+ if ($inAccoladePath) {
+ $subPath .= $c;
+ $c = '';
+ } elseif ($lastWasDollar) {
+ $lastWasDollar = false;
+ $inAccoladePath = true;
+ $subPath = '';
+ $c = '';
+ }
+ break;
+
+ case '}':
+ if ($inAccoladePath) {
+ $inAccoladePath = false;
+ $subEval = self::compileToPHPExpression($tales_prefix.$subPath,false);
+ $result .= "'.(" . $subEval . ").'";
+ $subPath = '';
+ $lastWasDollar = false;
+ $c = '';
+ }
+ break;
+
+ default:
+ if ($lastWasDollar) {
+ $lastWasDollar = false;
+ $inPath = true;
+ $subPath = $c;
+ $c = '';
+ } elseif ($inAccoladePath) {
+ $subPath .= $c;
+ $c = '';
+ } elseif ($inPath) {
+ $t = strtolower($c);
+ if (($t >= 'a' && $t <= 'z') || ($t >= '0' && $t <= '9') || ($t == '_')) {
+ $subPath .= $c;
+ $c = '';
+ } else {
+ $inPath = false;
+ $subEval = self::compileToPHPExpression($tales_prefix.$subPath,false);
+ $result .= "'.(" . $subEval . ").'";
+ }
+ }
+ break;
+ }
+ $result .= $c;
+ }
+ if ($inPath) {
+ $subEval = self::compileToPHPExpression($tales_prefix.$subPath, false);
+ $result .= "'.(" . $subEval . ").'";
+ }
+
+ // optimize ''.foo.'' to foo
+ $result = preg_replace("/^(?:''\.)?(.*?)(?:\.'')?$/", '\1', '\''.$result.'\'');
+
+ /*
+ The following expression (with + in first alternative):
+ "/^\(((?:[^\(\)]+|\([^\(\)]*\))*)\)$/"
+
+ did work properly for (aaaaaaa)aa, but not for (aaaaaaaaaaaaaaaaaaaaa)aa
+ WTF!?
+ */
+
+ // optimize (foo()) to foo()
+ $result = preg_replace("/^\(((?:[^\(\)]|\([^\(\)]*\))*)\)$/", '\1', $result);
+
+ return $result;
+ }
+
+ /**
+ * php: modifier.
+ *
+ * Transform the expression into a regular PHP expression.
+ */
+ static public function php($src)
+ {
+ return PHPTAL_Php_Transformer::transform($src, '$ctx->');
+ }
+
+ /**
+ * phptal-internal-php-block: modifier for emulation of <?php ?> in attributes.
+ *
+ * Please don't use it in the templates!
+ */
+ static public function phptal_internal_php_block($src)
+ {
+ $src = rawurldecode($src);
+
+ // Simple echo can be supported via regular method
+ if (preg_match('/^\s*echo\s+((?:[^;]+|"[^"\\\\]*"|\'[^\'\\\\]*\'|\/\*.*?\*\/)+);*\s*$/s',$src,$m))
+ {
+ return $m[1];
+ }
+
+ // <?php block expects statements, but modifiers must return expressions.
+ // unfortunately this ugliness is the only way to support it currently.
+ // ? > keeps semicolon optional
+ return "eval(".self::string($src.'?>').")";
+ }
+
+ /**
+ * exists: modifier.
+ *
+ * Returns the code required to invoke Context::exists() on specified path.
+ */
+ static public function exists($src, $nothrow)
+ {
+ $src = trim($src);
+ if (ctype_alnum($src)) return 'isset($ctx->'.$src.')';
+ return '(null !== ' . self::compileToPHPExpression($src, true) . ')';
+ }
+
+ /**
+ * number: modifier.
+ *
+ * Returns the number as is.
+ */
+ static public function number($src, $nothrow)
+ {
+ if (!is_numeric(trim($src))) throw new PHPTAL_ParserException("'$src' is not a number");
+ return trim($src);
+ }
+
+ /**
+ * json: modifier. Serializes anything as JSON.
+ */
+ static public function json($src, $nothrow)
+ {
+ return 'json_encode('.phptal_tale($src,$nothrow).')';
+ }
+
+ /**
+ * urlencode: modifier. Escapes a string.
+ */
+ static public function urlencode($src, $nothrow)
+ {
+ return 'rawurlencode('.phptal_tale($src,$nothrow).')';
+ }
+
+ /**
+ * translates TALES expression with alternatives into single PHP expression.
+ * Identical to compileToPHPExpressions() for singular expressions.
+ *
+ * @see PHPTAL_Php_TalesInternal::compileToPHPExpressions()
+ * @return string
+ */
+ public static function compileToPHPExpression($expression, $nothrow=false)
+ {
+ $r = self::compileToPHPExpressions($expression, $nothrow);
+ if (!is_array($r)) return $r;
+
+ // this weird ternary operator construct is to execute noThrow inside the expression
+ return '($ctx->noThrow(true)||1?'.self::convertExpressionsToExpression($r, $nothrow).':"")';
+ }
+
+ /*
+ * helper function for compileToPHPExpression
+ * @access private
+ */
+ private static function convertExpressionsToExpression(array $array, $nothrow)
+ {
+ if (count($array)==1) return '($ctx->noThrow('.($nothrow?'true':'false').')||1?('.
+ ($array[0]==self::NOTHING_KEYWORD?'null':$array[0]).
+ '):"")';
+
+ $expr = array_shift($array);
+
+ return "(!phptal_isempty(\$_tmp5=$expr) && (\$ctx->noThrow(false)||1)?\$_tmp5:".self::convertExpressionsToExpression($array, $nothrow).')';
+ }
+
+ /**
+ * returns PHP code that will evaluate given TALES expression.
+ * e.g. "string:foo${bar}" may be transformed to "'foo'.phptal_escape($ctx->bar)"
+ *
+ * Expressions with alternatives ("foo | bar") will cause it to return array
+ * Use PHPTAL_Php_TalesInternal::compileToPHPExpression() if you always want string.
+ *
+ * @param bool $nothrow if true, invalid expression will return NULL (at run time) rather than throwing exception
+ *
+ * @return string or array
+ */
+ public static function compileToPHPExpressions($expression, $nothrow=false)
+ {
+ $expression = trim($expression);
+
+ // Look for tales modifier (string:, exists:, Namespaced\Tale:, etc...)
+ if (preg_match('/^([a-z](?:[a-z0-9._\\\\-]*[a-z0-9])?):(.*)$/si', $expression, $m)) {
+ list(, $typePrefix, $expression) = $m;
+ }
+ // may be a 'string'
+ elseif (preg_match('/^\'((?:[^\']|\\\\.)*)\'$/s', $expression, $m)) {
+ $expression = stripslashes($m[1]);
+ $typePrefix = 'string';
+ }
+ // failback to path:
+ else {
+ $typePrefix = 'path';
+ }
+
+ // is a registered TALES expression modifier
+ $callback = PHPTAL_TalesRegistry::getInstance()->getCallback($typePrefix);
+ if ($callback !== NULL)
+ {
+ $result = call_user_func($callback, $expression, $nothrow);
+ self::verifyPHPExpressions($typePrefix, $result);
+ return $result;
+ }
+
+ $func = 'phptal_tales_'.str_replace('-', '_', $typePrefix);
+ throw new PHPTAL_UnknownModifierException("Unknown phptal modifier '$typePrefix'. Function '$func' does not exist", $typePrefix);
+ }
+
+ private static function verifyPHPExpressions($typePrefix,$expressions)
+ {
+ if (!is_array($expressions)) {
+ $expressions = array($expressions);
+ }
+
+ foreach($expressions as $expr) {
+ if (preg_match('/;\s*$/', $expr)) {
+ throw new PHPTAL_ParserException("Modifier $typePrefix generated PHP statement rather than expression (don't add semicolons)");
+ }
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/Php/Transformer.php b/lib/phptal/PHPTAL/Php/Transformer.php
new file mode 100644
index 0000000..c07608d
--- /dev/null
+++ b/lib/phptal/PHPTAL/Php/Transformer.php
@@ -0,0 +1,418 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Tranform php: expressions into their php equivalent.
+ *
+ * This transformer produce php code for expressions like :
+ *
+ * - a.b["key"].c().someVar[10].foo()
+ * - (a or b) and (c or d)
+ * - not myBool
+ * - ...
+ *
+ * The $prefix variable may be changed to change the context lookup.
+ *
+ * example:
+ *
+ * $res = PHPTAL_Php_Transformer::transform('a.b.c[x]', '$ctx->');
+ * $res == '$ctx->a->b->c[$ctx->x]';
+ *
+ * @package PHPTAL
+ * @subpackage Php
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Php_Transformer
+{
+ const ST_WHITE = -1; // start of string or whitespace
+ const ST_NONE = 0; // pass through (operators, parens, etc.)
+ const ST_STR = 1; // 'foo'
+ const ST_ESTR = 2; // "foo ${x} bar"
+ const ST_VAR = 3; // abcd
+ const ST_NUM = 4; // 123.02
+ const ST_EVAL = 5; // $somevar
+ const ST_MEMBER = 6; // abcd.x
+ const ST_STATIC = 7; // class::[$]static|const
+ const ST_DEFINE = 8; // @MY_DEFINE
+
+ /**
+ * transform PHPTAL's php-like syntax into real PHP
+ */
+ public static function transform($str, $prefix='$')
+ {
+ $len = strlen($str);
+ $state = self::ST_WHITE;
+ $result = '';
+ $i = 0;
+ $inString = false;
+ $backslashed = false;
+ $instanceof = false;
+ $eval = false;
+
+
+ for ($i = 0; $i <= $len; $i++) {
+ if ($i == $len) $c = "\0";
+ else $c = $str[$i];
+
+ switch ($state) {
+
+ // after whitespace a variable-variable may start, ${var} → $ctx->{$ctx->var}
+ case self::ST_WHITE:
+ if ($c === '$' && $i+1 < $len && $str[$i+1] === '{')
+ {
+ $result .= $prefix;
+ $state = self::ST_NONE;
+ continue;
+ }
+ /* NO BREAK - ST_WHITE is almost the same as ST_NONE */
+
+ // no specific state defined, just eat char and see what to do with it.
+ case self::ST_NONE:
+ // begin of eval without {
+ if ($c === '$' && $i+1 < $len && self::isAlpha($str[$i+1])) {
+ $state = self::ST_EVAL;
+ $mark = $i+1;
+ $result .= $prefix.'{';
+ }
+ elseif (self::isDigit($c))
+ {
+ $state = self::ST_NUM;
+ $mark = $i;
+ }
+ // that an alphabetic char, then it should be the begining
+ // of a var or static
+ // && !self::isDigit($c) checked earlier
+ elseif (self::isVarNameChar($c)) {
+ $state = self::ST_VAR;
+ $mark = $i;
+ }
+ // begining of double quoted string
+ elseif ($c === '"') {
+ $state = self::ST_ESTR;
+ $mark = $i;
+ $inString = true;
+ }
+ // begining of single quoted string
+ elseif ($c === '\'') {
+ $state = self::ST_STR;
+ $mark = $i;
+ $inString = true;
+ }
+ // closing a method, an array access or an evaluation
+ elseif ($c === ')' || $c === ']' || $c === '}') {
+ $result .= $c;
+ // if next char is dot then an object member must
+ // follow
+ if ($i+1 < $len && $str[$i+1] === '.') {
+ $result .= '->';
+ $state = self::ST_MEMBER;
+ $mark = $i+2;
+ $i+=2;
+ }
+ }
+ // @ is an access to some defined variable
+ elseif ($c === '@') {
+ $state = self::ST_DEFINE;
+ $mark = $i+1;
+ }
+ elseif (ctype_space($c)) {
+ $state = self::ST_WHITE;
+ $result .= $c;
+ }
+ // character we don't mind about
+ else {
+ $result .= $c;
+ }
+ break;
+
+ // $xxx
+ case self::ST_EVAL:
+ if (!self::isVarNameChar($c)) {
+ $result .= $prefix . substr($str, $mark, $i-$mark);
+ $result .= '}';
+ $state = self::ST_NONE;
+ }
+ break;
+
+ // single quoted string
+ case self::ST_STR:
+ if ($c === '\\') {
+ $backslashed = true;
+ } elseif ($backslashed) {
+ $backslashed = false;
+ }
+ // end of string, back to none state
+ elseif ($c === '\'') {
+ $result .= substr($str, $mark, $i-$mark+1);
+ $inString = false;
+ $state = self::ST_NONE;
+ }
+ break;
+
+ // double quoted string
+ case self::ST_ESTR:
+ if ($c === '\\') {
+ $backslashed = true;
+ } elseif ($backslashed) {
+ $backslashed = false;
+ }
+ // end of string, back to none state
+ elseif ($c === '"') {
+ $result .= substr($str, $mark, $i-$mark+1);
+ $inString = false;
+ $state = self::ST_NONE;
+ }
+ // instring interpolation, search } and transform the
+ // interpollation to insert it into the string
+ elseif ($c === '$' && $i+1 < $len && $str[$i+1] === '{') {
+ $result .= substr($str, $mark, $i-$mark) . '{';
+
+ $sub = 0;
+ for ($j = $i; $j<$len; $j++) {
+ if ($str[$j] === '{') {
+ $sub++;
+ } elseif ($str[$j] === '}' && (--$sub) == 0) {
+ $part = substr($str, $i+2, $j-$i-2);
+ $result .= self::transform($part, $prefix);
+ $i = $j;
+ $mark = $i;
+ }
+ }
+ }
+ break;
+
+ // var state
+ case self::ST_VAR:
+ if (self::isVarNameChar($c)) {
+ }
+ // end of var, begin of member (method or var)
+ elseif ($c === '.') {
+ $result .= $prefix . substr($str, $mark, $i-$mark);
+ $result .= '->';
+ $state = self::ST_MEMBER;
+ $mark = $i+1;
+ }
+ // static call, the var is a class name
+ elseif ($c === ':' && $i+1 < $len && $str[$i+1] === ':') {
+ $result .= substr($str, $mark, $i-$mark+1);
+ $mark = $i+1;
+ $i++;
+ $state = self::ST_STATIC;
+ break;
+ }
+ // function invocation, the var is a function name
+ elseif ($c === '(') {
+ $result .= substr($str, $mark, $i-$mark+1);
+ $state = self::ST_NONE;
+ }
+ // array index, the var is done
+ elseif ($c === '[') {
+ if ($str[$mark]==='_') { // superglobal?
+ $result .= '$' . substr($str, $mark, $i-$mark+1);
+ } else {
+ $result .= $prefix . substr($str, $mark, $i-$mark+1);
+ }
+ $state = self::ST_NONE;
+ }
+ // end of var with non-var-name character, handle keywords
+ // and populate the var name
+ else {
+ $var = substr($str, $mark, $i-$mark);
+ $low = strtolower($var);
+ // boolean and null
+ if ($low === 'true' || $low === 'false' || $low === 'null') {
+ $result .= $var;
+ }
+ // lt, gt, ge, eq, ...
+ elseif (array_key_exists($low, self::$TranslationTable)) {
+ $result .= self::$TranslationTable[$low];
+ }
+ // instanceof keyword
+ elseif ($low === 'instanceof') {
+ $result .= $var;
+ $instanceof = true;
+ }
+ // previous was instanceof
+ elseif ($instanceof) {
+ // last was instanceof, this var is a class name
+ $result .= $var;
+ $instanceof = false;
+ }
+ // regular variable
+ else {
+ $result .= $prefix . $var;
+ }
+ $i--;
+ $state = self::ST_NONE;
+ }
+ break;
+
+ // object member
+ case self::ST_MEMBER:
+ if (self::isVarNameChar($c)) {
+ }
+ // eval mode ${foo}
+ elseif ($c === '$' && ($i >= $len-2 || $str[$i+1] !== '{')) {
+ $result .= '{' . $prefix;
+ $mark++;
+ $eval = true;
+ }
+ // x.${foo} x->{foo}
+ elseif ($c === '$') {
+ $mark++;
+ }
+ // end of var member var, begin of new member
+ elseif ($c === '.') {
+ $result .= substr($str, $mark, $i-$mark);
+ if ($eval) { $result .='}'; $eval = false; }
+ $result .= '->';
+ $mark = $i+1;
+ $state = self::ST_MEMBER;
+ }
+ // begin of static access
+ elseif ($c === ':') {
+ $result .= substr($str, $mark, $i-$mark+1);
+ if ($eval) { $result .='}'; $eval = false; }
+ $state = self::ST_STATIC;
+ break;
+ }
+ // the member is a method or an array
+ elseif ($c === '(' || $c === '[') {
+ $result .= substr($str, $mark, $i-$mark+1);
+ if ($eval) { $result .='}'; $eval = false; }
+ $state = self::ST_NONE;
+ }
+ // regular end of member, it is a var
+ else {
+ $var = substr($str, $mark, $i-$mark);
+ if ($var !== '' && !preg_match('/^[a-z][a-z0-9_\x7f-\xff]*$/i',$var)) {
+ throw new PHPTAL_ParserException("Invalid field name '$var' in expression php:$str");
+ }
+ $result .= $var;
+ if ($eval) { $result .='}'; $eval = false; }
+ $state = self::ST_NONE;
+ $i--;
+ }
+ break;
+
+ // wait for separator
+ case self::ST_DEFINE:
+ if (self::isVarNameChar($c)) {
+ } else {
+ $state = self::ST_NONE;
+ $result .= substr($str, $mark, $i-$mark);
+ $i--;
+ }
+ break;
+
+ // static call, can be const, static var, static method
+ // Klass::$static
+ // Klass::const
+ // Kclass::staticMethod()
+ //
+ case self::ST_STATIC:
+ if (self::isVarNameChar($c)) {
+ }
+ // static var
+ elseif ($c === '$') {
+ }
+ // end of static var which is an object and begin of member
+ elseif ($c === '.') {
+ $result .= substr($str, $mark, $i-$mark);
+ $result .= '->';
+ $mark = $i+1;
+ $state = self::ST_MEMBER;
+ }
+ // end of static var which is a class name
+ elseif ($c === ':') {
+ $result .= substr($str, $mark, $i-$mark+1);
+ $state = self::ST_STATIC;
+ break;
+ }
+ // static method or array
+ elseif ($c === '(' || $c === '[') {
+ $result .= substr($str, $mark, $i-$mark+1);
+ $state = self::ST_NONE;
+ }
+ // end of static var or const
+ else {
+ $result .= substr($str, $mark, $i-$mark);
+ $state = self::ST_NONE;
+ $i--;
+ }
+ break;
+
+ // numeric value
+ case self::ST_NUM:
+ if (!self::isDigitCompound($c)) {
+ $var = substr($str, $mark, $i-$mark);
+
+ if (self::isAlpha($c) || $c === '_') {
+ throw new PHPTAL_ParserException("Syntax error in number '$var$c' in expression php:$str");
+ }
+ if (!is_numeric($var)) {
+ throw new PHPTAL_ParserException("Syntax error in number '$var' in expression php:$str");
+ }
+
+ $result .= $var;
+ $state = self::ST_NONE;
+ $i--;
+ }
+ break;
+ }
+ }
+
+ $result = trim($result);
+
+ // CodeWriter doesn't like expressions that look like blocks
+ if ($result[strlen($result)-1] === '}') return '('.$result.')';
+
+ return $result;
+ }
+
+ private static function isAlpha($c)
+ {
+ $c = strtolower($c);
+ return $c >= 'a' && $c <= 'z';
+ }
+
+ private static function isDigit($c)
+ {
+ return ($c >= '0' && $c <= '9');
+ }
+
+ private static function isDigitCompound($c)
+ {
+ return ($c >= '0' && $c <= '9' || $c === '.');
+ }
+
+ private static function isVarNameChar($c)
+ {
+ return self::isAlpha($c) || ($c >= '0' && $c <= '9') || $c === '_' || $c === '\\';
+ }
+
+ private static $TranslationTable = array(
+ 'not' => '!',
+ 'ne' => '!=',
+ 'and' => '&&',
+ 'or' => '||',
+ 'lt' => '<',
+ 'gt' => '>',
+ 'ge' => '>=',
+ 'le' => '<=',
+ 'eq' => '==',
+ );
+}
+
diff --git a/lib/phptal/PHPTAL/PreFilter.php b/lib/phptal/PHPTAL/PreFilter.php
new file mode 100644
index 0000000..25c7969
--- /dev/null
+++ b/lib/phptal/PHPTAL/PreFilter.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id: $
+ * @link http://phptal.org/
+ */
+
+/**
+ * Base class for prefilters.
+ *
+ * You should extend this class and override methods you're interested in.
+ *
+ * Order of calls is undefined and may change.
+ *
+ * @package PHPTAL
+ */
+abstract class PHPTAL_PreFilter implements PHPTAL_Filter
+{
+ /**
+ * @see getPHPTAL()
+ */
+ private $phptal;
+
+
+ /**
+ * Receives DOMElement (of PHP5 DOM API) of parsed file (documentElement), or element
+ * that has phptal:filter attribute. Should edit DOM in place.
+ * Prefilters are called only once before template is compiled, so they can be slow.
+ *
+ * Default implementation does nothing. Override it.
+ *
+ * @param DOMElement $node PHP5 DOM node to modify in place
+ *
+ * @return void
+ */
+ public function filterElement(DOMElement $node)
+ {
+ }
+
+ /**
+ * Receives root PHPTAL DOM node of parsed file and should edit it in place.
+ * Prefilters are called only once before template is compiled, so they can be slow.
+ *
+ * Default implementation does nothing. Override it.
+ *
+ * @see PHPTAL_Dom_Element class for methods and fields available.
+ *
+ * @param PHPTAL_Dom_Element $root PHPTAL DOM node to modify in place
+ *
+ * @return void
+ */
+ public function filterDOM(PHPTAL_Dom_Element $root)
+ {
+ }
+
+ /**
+ * Receives DOM node that had phptal:filter attribute calling this filter.
+ * Should modify node in place.
+ * Prefilters are called only once before template is compiled, so they can be slow.
+ *
+ * Default implementation calls filterDOM(). Override it.
+ *
+ * @param PHPTAL_Dom_Element $node PHPTAL DOM node to modify in place
+ *
+ * @return void
+ */
+ public function filterDOMFragment(PHPTAL_Dom_Element $node)
+ {
+ $this->filterDOM($node);
+ }
+
+ /**
+ * Receives template source code and is expected to return new source.
+ * Prefilters are called only once before template is compiled, so they can be slow.
+ *
+ * Default implementation does nothing. Override it.
+ *
+ * @param string $src markup to filter
+ *
+ * @return string
+ */
+ public function filter($src)
+ {
+ return $src;
+ }
+
+ /**
+ * Returns (any) string that uniquely identifies this filter and its settings,
+ * which is used to (in)validate template cache.
+ *
+ * Unlike other filter methods, this one is called on every execution.
+ *
+ * Override this method if result of the filter depends on its configuration.
+ *
+ * @return string
+ */
+ public function getCacheId()
+ {
+ return get_class($this);
+ }
+
+ /**
+ * Returns PHPTAL class instance that is currently using this prefilter.
+ * May return NULL if PHPTAL didn't start filtering yet.
+ *
+ * @return PHPTAL or NULL
+ */
+ final protected function getPHPTAL()
+ {
+ return $this->phptal;
+ }
+
+ /**
+ * Set which instance of PHPTAL is using this filter.
+ * Must be done before calling any filter* methods.
+ *
+ * @param PHPTAL $phptal instance
+ */
+ final function setPHPTAL(PHPTAL $phptal)
+ {
+ $this->phptal = $phptal;
+ }
+}
+
+
diff --git a/lib/phptal/PHPTAL/PreFilter/Compress.php b/lib/phptal/PHPTAL/PreFilter/Compress.php
new file mode 100644
index 0000000..c1cedbc
--- /dev/null
+++ b/lib/phptal/PHPTAL/PreFilter/Compress.php
@@ -0,0 +1,282 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id: $
+ * @link http://phptal.org/
+ */
+
+/**
+ * Removes all unnecessary whitespace from XHTML documents.
+ *
+ * extends Normalize only to re-use helper methods
+ */
+class PHPTAL_PreFilter_Compress extends PHPTAL_PreFilter_Normalize
+{
+ /**
+ * keeps track whether last element had trailing whitespace (or didn't need it).
+ * If had_space==false, next element must keep leading space.
+ */
+ private $had_space=false;
+
+ /**
+ * last text node before closing tag that may need trailing whitespace trimmed.
+ * It's often last-child, but comments, multiple end tags make that trickier.
+ */
+ private $most_recent_text_node=null;
+
+ function filterDOM(PHPTAL_Dom_Element $root)
+ {
+ // let xml:space=preserve preserve everything
+ if ($root->getAttributeNS("http://www.w3.org/XML/1998/namespace", 'space') == 'preserve') {
+ $this->most_recent_text_node = null;
+ $this->findElementToFilter($root);
+ return;
+ }
+
+ // tal:replace makes element behave like text
+ if ($root->getAttributeNS('http://xml.zope.org/namespaces/tal','replace')) {
+ $this->most_recent_text_node = null;
+ $this->had_space = false;
+ return;
+ }
+
+ $this->normalizeAttributes($root);
+ $this->elementSpecificOptimizations($root);
+
+ // <head>, <tr> don't have any significant whitespace
+ $no_spaces = $this->hasNoInterelementSpace($root);
+
+ // mostly block-level elements
+ // if element is conditional, it may not always break the line
+ $breaks_line = $no_spaces || ($this->breaksLine($root) && !$root->getAttributeNS('http://xml.zope.org/namespaces/tal','condition'));
+
+ // start tag newline
+ if ($breaks_line) {
+ if ($this->most_recent_text_node) {
+ $this->most_recent_text_node->setValueEscaped(rtrim($this->most_recent_text_node->getValueEscaped()));
+ $this->most_recent_text_node = null;
+ }
+ $this->had_space = true;
+ } else if ($this->isInlineBlock($root)) {
+ // spaces around <img> must be kept
+ $this->most_recent_text_node = null;
+ $this->had_space = false;
+ }
+
+ // <pre>, <textarea> are handled separately from xml:space, because they may have attributes normalized
+ if ($this->isSpaceSensitiveInXHTML($root)) {
+ $this->most_recent_text_node = null;
+
+ // HTML 5 (9.1.2.5) specifies quirk that a first *single* newline in <pre> can be removed
+ if (count($root->childNodes) && $root->childNodes[0] instanceof PHPTAL_Dom_Text) {
+ if (preg_match('/^\n[^\n]/', $root->childNodes[0]->getValueEscaped())) {
+ $root->childNodes[0]->setValueEscaped(substr($root->childNodes[0]->getValueEscaped(),1));
+ }
+ }
+ $this->findElementToFilter($root);
+ return;
+ }
+
+ foreach ($root->childNodes as $node) {
+
+ if ($node instanceof PHPTAL_Dom_Text) {
+ // replaces runs of whitespace with ' '
+ $norm = $this->normalizeSpace($node->getValueEscaped(), $node->getEncoding());
+
+ if ($no_spaces) {
+ $norm = trim($norm);
+ } elseif ($this->had_space) {
+ $norm = ltrim($norm);
+ }
+
+ $node->setValueEscaped($norm);
+
+ // collapsed whitespace-only nodes are ignored (otherwise trimming of most_recent_text_node would be useless)
+ if ($norm !== '') {
+ $this->most_recent_text_node = $node;
+ $this->had_space = (substr($norm,-1) == ' ');
+ }
+ } else if ($node instanceof PHPTAL_Dom_Element) {
+ $this->filterDOM($node);
+ } else if ($node instanceof PHPTAL_Dom_DocumentType || $node instanceof PHPTAL_Dom_XMLDeclaration) {
+ $this->had_space = true;
+ } else if ($node instanceof PHPTAL_Dom_ProcessingInstruction) {
+ // PI may output something requiring spaces
+ $this->most_recent_text_node = null;
+ $this->had_space = false;
+ }
+ }
+
+ // repeated element may need trailing space.
+ if (!$breaks_line && $root->getAttributeNS('http://xml.zope.org/namespaces/tal','repeat')) {
+ $this->most_recent_text_node = null;
+ }
+
+ // tal:content may replace element with something without space
+ if (!$breaks_line && $root->getAttributeNS('http://xml.zope.org/namespaces/tal','content')) {
+ $this->had_space = false;
+ $this->most_recent_text_node = null;
+ }
+
+ // line break caused by end tag
+ if ($breaks_line) {
+ if ($this->most_recent_text_node) {
+ $this->most_recent_text_node->setValueEscaped(rtrim($this->most_recent_text_node->getValueEscaped()));
+ $this->most_recent_text_node = null;
+ }
+ $this->had_space = true;
+ }
+ }
+
+ private static $no_interelement_space = array(
+ 'html','head','table','thead','tfoot','select','optgroup','dl','ol','ul','tr','datalist',
+ );
+
+ private function hasNoInterelementSpace(PHPTAL_Dom_Element $element)
+ {
+ if ($element->getLocalName() === 'block'
+ && $element->parentNode
+ && $element->getNamespaceURI() === 'http://xml.zope.org/namespaces/tal') {
+ return $this->hasNoInterelementSpace($element->parentNode);
+ }
+
+ return in_array($element->getLocalName(), self::$no_interelement_space)
+ && ($element->getNamespaceURI() === 'http://www.w3.org/1999/xhtml' || $element->getNamespaceURI() === '');
+ }
+
+ /**
+ * li is deliberately omitted, as it's commonly used with display:inline in menus.
+ */
+ private static $breaks_line = array(
+ 'address','article','aside','base','blockquote','body','br','dd','div','dl','dt','fieldset','figure',
+ 'footer','form','h1','h2','h3','h4','h5','h6','head','header','hgroup','hr','html','legend','link',
+ 'meta','nav','ol','option','p','param','pre','section','style','table','tbody','td','th','thead',
+ 'title','tr','ul','details',
+ );
+
+ private function breaksLine(PHPTAL_Dom_Element $element)
+ {
+ if ($element->getAttributeNS('http://xml.zope.org/namespaces/metal','define-macro')) {
+ return true;
+ }
+
+ if (!$element->parentNode) {
+ return true;
+ }
+
+ if ($element->getNamespaceURI() !== 'http://www.w3.org/1999/xhtml'
+ && $element->getNamespaceURI() !== '') {
+ return false;
+ }
+
+ return in_array($element->getLocalName(), self::$breaks_line);
+ }
+
+ /**
+ * replaced elements need to preserve spaces before and after
+ */
+ private static $inline_blocks = array(
+ 'select','input','button','img','textarea','output','progress','meter',
+ );
+
+ private function isInlineBlock(PHPTAL_Dom_Element $element)
+ {
+ if ($element->getNamespaceURI() !== 'http://www.w3.org/1999/xhtml'
+ && $element->getNamespaceURI() !== '') {
+ return false;
+ }
+
+ return in_array($element->getLocalName(), self::$inline_blocks);
+ }
+
+ /**
+ * Consistent sorting of attributes might give slightly better gzip performance
+ */
+ protected function normalizeAttributes(PHPTAL_Dom_Element $element)
+ {
+ parent::normalizeAttributes($element);
+
+ $attrs_by_qname = array();
+ foreach ($element->getAttributeNodes() as $attrnode) {
+ // safe, as there can't be two attrs with same qname
+ $attrs_by_qname[$attrnode->getQualifiedName()] = $attrnode;
+ }
+
+ if (count($attrs_by_qname) > 1) {
+ uksort($attrs_by_qname, array($this, 'compareQNames'));
+ $element->setAttributeNodes(array_values($attrs_by_qname));
+ }
+ }
+
+ /**
+ * pre-defined order of attributes roughly by popularity
+ */
+ private static $attributes_order = array(
+ 'href','src','class','rel','type','title','width','height','alt','content','name','style','lang','id',
+ );
+
+ /**
+ * compare names according to $attributes_order array.
+ * Elements that are not in array, are considered greater than all elements in array,
+ * and are sorted alphabetically.
+ */
+ private static function compareQNames($a, $b) {
+ $a_index = array_search($a, self::$attributes_order);
+ $b_index = array_search($b, self::$attributes_order);
+
+ if ($a_index !== false && $b_index !== false) {
+ return $a_index - $b_index;
+ }
+ if ($a_index === false && $b_index === false) {
+ return strcmp($a, $b);
+ }
+ return ($a_index === false) ? 1 : -1;
+ }
+
+ /**
+ * HTML5 doesn't care about boilerplate
+ */
+ private function elementSpecificOptimizations(PHPTAL_Dom_Element $element)
+ {
+ if ($element->getNamespaceURI() !== 'http://www.w3.org/1999/xhtml'
+ && $element->getNamespaceURI() !== '') {
+ return;
+ }
+
+ if ($this->getPHPTAL()->getOutputMode() !== PHPTAL::HTML5) {
+ return;
+ }
+
+ // <meta charset>
+ if ('meta' === $element->getLocalName() &&
+ $element->getAttributeNS('','http-equiv') === 'Content-Type') {
+ $element->removeAttributeNS('','http-equiv');
+ $element->removeAttributeNS('','content');
+ $element->setAttributeNS('','charset',strtolower($this->getPHPTAL()->getEncoding()));
+ }
+ elseif (('link' === $element->getLocalName() && $element->getAttributeNS('','rel') === 'stylesheet') ||
+ ('style' === $element->getLocalName())) {
+ // There's only one type of stylesheets that works.
+ $element->removeAttributeNS('','type');
+
+ } elseif ('script' === $element->getLocalName()) {
+ $element->removeAttributeNS('','language');
+
+ // Only remove type that matches default. E4X, vbscript, coffeescript, etc. must be preserved
+ $type = $element->getAttributeNS('','type');
+ $is_std = preg_match('/^(?:text|application)\/(?:ecma|java)script(\s*;\s*charset\s*=\s*[^;]*)?$/', $type);
+
+ // Remote scripts should have type specified in HTTP headers.
+ if ($is_std || $element->getAttributeNS('','src')) {
+ $element->removeAttributeNS('','type');
+ }
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/PreFilter/Normalize.php b/lib/phptal/PHPTAL/PreFilter/Normalize.php
new file mode 100644
index 0000000..dd6f46e
--- /dev/null
+++ b/lib/phptal/PHPTAL/PreFilter/Normalize.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id: $
+ * @link http://phptal.org/
+ */
+
+/**
+ * Collapses conscutive whitespace, trims attributes, merges adjacent text nodes
+ */
+class PHPTAL_PreFilter_Normalize extends PHPTAL_PreFilter
+{
+ function filter($src)
+ {
+ return str_replace("\r\n", "\n", $src);
+ }
+
+ function filterDOM(PHPTAL_Dom_Element $root)
+ {
+ // let xml:space=preserve preserve attributes as well
+ if ($root->getAttributeNS("http://www.w3.org/XML/1998/namespace", 'space') == 'preserve') {
+ $this->findElementToFilter($root);
+ return;
+ }
+
+ $this->normalizeAttributes($root);
+
+ // <pre> may have attributes normalized
+ if ($this->isSpaceSensitiveInXHTML($root)) {
+ $this->findElementToFilter($root);
+ return;
+ }
+
+ $lastTextNode = null;
+ foreach ($root->childNodes as $node) {
+
+ // CDATA is not normalized by design
+ if ($node instanceof PHPTAL_Dom_Text) {
+ $norm = $this->normalizeSpace($node->getValueEscaped(), $node->getEncoding());
+ $node->setValueEscaped($norm);
+
+ if ('' === $norm) {
+ $root->removeChild($node);
+ } else if ($lastTextNode) {
+ // "foo " . " bar" gives 2 spaces.
+ $norm = $lastTextNode->getValueEscaped().ltrim($norm,' ');
+
+ $lastTextNode->setValueEscaped($norm); // assumes all nodes use same encoding (they do)
+ $root->removeChild($node);
+ } else {
+ $lastTextNode = $node;
+ }
+ } else {
+ $lastTextNode = null;
+ if ($node instanceof PHPTAL_Dom_Element) {
+ $this->filterDOM($node);
+ }
+ }
+ }
+ }
+
+ protected function isSpaceSensitiveInXHTML(PHPTAL_Dom_Element $element)
+ {
+ $ln = $element->getLocalName();
+ return ($ln === 'script' || $ln === 'pre' || $ln === 'textarea')
+ && ($element->getNamespaceURI() === 'http://www.w3.org/1999/xhtml' || $element->getNamespaceURI() === '');
+ }
+
+ protected function findElementToFilter(PHPTAL_Dom_Element $root)
+ {
+ foreach ($root->childNodes as $node) {
+ if (!$node instanceof PHPTAL_Dom_Element) continue;
+
+ if ($node->getAttributeNS("http://www.w3.org/XML/1998/namespace", 'space') == 'default') {
+ $this->filterDOM($node);
+ }
+ }
+ }
+
+ /**
+ * does not trim
+ */
+ protected function normalizeSpace($text, $encoding)
+ {
+ $utf_regex_mod = ($encoding=='UTF-8'?'u':'');
+
+ return preg_replace('/[ \t\r\n]+/'.$utf_regex_mod, ' ', $text); // \s removes nbsp
+ }
+
+ protected function normalizeAttributes(PHPTAL_Dom_Element $element)
+ {
+ foreach ($element->getAttributeNodes() as $attrnode) {
+
+ // skip replaced attributes (because getValueEscaped on them is meaningless)
+ if ($attrnode->getReplacedState() !== PHPTAL_Dom_Attr::NOT_REPLACED) continue;
+
+ $val = $this->normalizeSpace($attrnode->getValueEscaped(), $attrnode->getEncoding());
+ $attrnode->setValueEscaped(trim($val, ' '));
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/PreFilter/StripComments.php b/lib/phptal/PHPTAL/PreFilter/StripComments.php
new file mode 100644
index 0000000..89aaabe
--- /dev/null
+++ b/lib/phptal/PHPTAL/PreFilter/StripComments.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id: $
+ * @link http://phptal.org/
+ */
+
+class PHPTAL_PreFilter_StripComments extends PHPTAL_PreFilter
+{
+ function filterDOM(PHPTAL_Dom_Element $element)
+ {
+ $defs = PHPTAL_Dom_Defs::getInstance();
+
+ foreach ($element->childNodes as $node) {
+ if ($node instanceof PHPTAL_Dom_Comment) {
+ if ($defs->isCDATAElementInHTML($element->getNamespaceURI(), $element->getLocalName())) {
+ $textNode = new PHPTAL_Dom_CDATASection($node->getValueEscaped(), $node->getEncoding());
+ $node->parentNode->replaceChild($textNode, $node);
+ } else {
+ $node->parentNode->removeChild($node);
+ }
+ } else if ($node instanceof PHPTAL_Dom_Element) {
+ $this->filterDOM($node);
+ }
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/RepeatController.php b/lib/phptal/PHPTAL/RepeatController.php
new file mode 100644
index 0000000..5d33914
--- /dev/null
+++ b/lib/phptal/PHPTAL/RepeatController.php
@@ -0,0 +1,323 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @author Iván Montes <drslump@pollinimini.net>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Stores tal:repeat information during template execution.
+ *
+ * An instance of this class is created and stored into PHPTAL context on each
+ * tal:repeat usage.
+ *
+ * repeat/item/index
+ * repeat/item/number
+ * ...
+ * are provided by this instance.
+ *
+ * 'repeat' is an stdClass instance created to handle RepeatControllers,
+ * 'item' is an instance of this class.
+ *
+ * @package PHPTAL
+ * @subpackage Php
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_RepeatController implements Iterator
+{
+ public $key;
+ private $current;
+ private $valid;
+ private $validOnNext;
+
+ private $uses_groups = false;
+
+ protected $iterator;
+ public $index;
+ public $end;
+
+ /**
+ * computed lazily
+ */
+ private $length = null;
+
+ /**
+ * Construct a new RepeatController.
+ *
+ * @param $source array, string, iterator, iterable.
+ */
+ public function __construct($source)
+ {
+ if ( is_string($source) ) {
+ $this->iterator = new ArrayIterator( str_split($source) ); // FIXME: invalid for UTF-8 encoding, use preg_match_all('/./u') trick
+ } elseif ( is_array($source) ) {
+ $this->iterator = new ArrayIterator($source);
+ } elseif ($source instanceof IteratorAggregate) {
+ $this->iterator = $source->getIterator();
+ } elseif ($source instanceof DOMNodeList) {
+ $array = array();
+ foreach ($source as $k=>$v) {
+ $array[$k] = $v;
+ }
+ $this->iterator = new ArrayIterator($array);
+ } elseif ($source instanceof Iterator) {
+ $this->iterator = $source;
+ } elseif ($source instanceof Traversable) {
+ $this->iterator = new IteratorIterator($source);
+ } elseif ($source instanceof Closure) {
+ $this->iterator = new ArrayIterator( (array) $source() );
+ } elseif ($source instanceof stdClass) {
+ $this->iterator = new ArrayIterator( (array) $source );
+ } else {
+ $this->iterator = new ArrayIterator( array() );
+ }
+ }
+
+ /**
+ * Returns the current element value in the iteration
+ *
+ * @return Mixed The current element value
+ */
+ public function current()
+ {
+ return $this->current;
+ }
+
+ /**
+ * Returns the current element key in the iteration
+ *
+ * @return String/Int The current element key
+ */
+ public function key()
+ {
+ return $this->key;
+ }
+
+ /**
+ * Tells if the iteration is over
+ *
+ * @return bool True if the iteration is not finished yet
+ */
+ public function valid()
+ {
+ $valid = $this->valid || $this->validOnNext;
+ $this->validOnNext = $this->valid;
+
+ return $valid;
+ }
+
+ public function length()
+ {
+ if ($this->length === null) {
+ if ($this->iterator instanceof Countable) {
+ return $this->length = count($this->iterator);
+ } elseif ( is_object($this->iterator) ) {
+ // for backwards compatibility with existing PHPTAL templates
+ if ( method_exists($this->iterator, 'size') ) {
+ return $this->length = $this->iterator->size();
+ } elseif ( method_exists($this->iterator, 'length') ) {
+ return $this->length = $this->iterator->length();
+ }
+ }
+ $this->length = '_PHPTAL_LENGTH_UNKNOWN_';
+ }
+
+ if ($this->length === '_PHPTAL_LENGTH_UNKNOWN_') // return length if end is discovered
+ {
+ return $this->end ? $this->index + 1 : null;
+ }
+ return $this->length;
+ }
+
+ /**
+ * Restarts the iteration process going back to the first element
+ *
+ */
+ public function rewind()
+ {
+ $this->index = 0;
+ $this->length = null;
+ $this->end = false;
+
+ $this->iterator->rewind();
+
+ // Prefetch the next element
+ if ($this->iterator->valid()) {
+ $this->validOnNext = true;
+ $this->prefetch();
+ } else {
+ $this->validOnNext = false;
+ }
+
+ if ($this->uses_groups) {
+ // Notify the grouping helper of the change
+ $this->groups->reset();
+ }
+ }
+
+ /**
+ * Fetches the next element in the iteration and advances the pointer
+ *
+ */
+ public function next()
+ {
+ $this->index++;
+
+ // Prefetch the next element
+ if ($this->validOnNext) $this->prefetch();
+
+ if ($this->uses_groups) {
+ // Notify the grouping helper of the change
+ $this->groups->reset();
+ }
+ }
+
+ /**
+ * Ensures that $this->groups works.
+ *
+ * Groups are rarely-used feature, which is why they're lazily loaded.
+ */
+ private function initializeGroups()
+ {
+ if (!$this->uses_groups) {
+ $this->groups = new PHPTAL_RepeatControllerGroups();
+ $this->uses_groups = true;
+ }
+ }
+
+ /**
+ * Gets an object property
+ *
+ * @return $var Mixed The variable value
+ */
+ public function __get($var)
+ {
+ switch ($var) {
+ case 'number':
+ return $this->index + 1;
+ case 'start':
+ return $this->index === 0;
+ case 'even':
+ return ($this->index % 2) === 0;
+ case 'odd':
+ return ($this->index % 2) === 1;
+ case 'length':
+ return $this->length();
+ case 'letter':
+ return strtolower( $this->int2letter($this->index+1) );
+ case 'Letter':
+ return strtoupper( $this->int2letter($this->index+1) );
+ case 'roman':
+ return strtolower( $this->int2roman($this->index+1) );
+ case 'Roman':
+ return strtoupper( $this->int2roman($this->index+1) );
+
+ case 'groups':
+ $this->initializeGroups();
+ return $this->groups;
+
+ case 'first':
+ $this->initializeGroups();
+ // Compare the current one with the previous in the dictionary
+ $res = $this->groups->first($this->current);
+ return is_bool($res) ? $res : $this->groups;
+
+ case 'last':
+ $this->initializeGroups();
+ // Compare the next one with the dictionary
+ $res = $this->groups->last( $this->iterator->current() );
+ return is_bool($res) ? $res : $this->groups;
+
+ default:
+ throw new PHPTAL_VariableNotFoundException("Unable to find part '$var' in repeat variable");
+ }
+ }
+
+ /**
+ * Fetches the next element from the source data store and
+ * updates the end flag if needed.
+ *
+ * @access protected
+ */
+ protected function prefetch()
+ {
+ $this->valid = true;
+ $this->current = $this->iterator->current();
+ $this->key = $this->iterator->key();
+
+ $this->iterator->next();
+ if ( !$this->iterator->valid() ) {
+ $this->valid = false;
+ $this->end = true;
+ }
+ }
+
+ /**
+ * Converts an integer number (1 based) to a sequence of letters
+ *
+ * @param int $int The number to convert
+ *
+ * @return String The letters equivalent as a, b, c-z ... aa, ab, ac-zz ...
+ * @access protected
+ */
+ protected function int2letter($int)
+ {
+ $lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ $size = strlen($lookup);
+
+ $letters = '';
+ while ($int > 0) {
+ $int--;
+ $letters = $lookup[$int % $size] . $letters;
+ $int = floor($int / $size);
+ }
+ return $letters;
+ }
+
+ /**
+ * Converts an integer number (1 based) to a roman numeral
+ *
+ * @param int $int The number to convert
+ *
+ * @return String The roman numeral
+ * @access protected
+ */
+ protected function int2roman($int)
+ {
+ $lookup = array(
+ '1000' => 'M',
+ '900' => 'CM',
+ '500' => 'D',
+ '400' => 'CD',
+ '100' => 'C',
+ '90' => 'XC',
+ '50' => 'L',
+ '40' => 'XL',
+ '10' => 'X',
+ '9' => 'IX',
+ '5' => 'V',
+ '4' => 'IV',
+ '1' => 'I',
+ );
+
+ $roman = '';
+ foreach ($lookup as $max => $letters) {
+ while ($int >= $max) {
+ $roman .= $letters;
+ $int -= $max;
+ }
+ }
+
+ return $roman;
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/RepeatControllerGroups.php b/lib/phptal/PHPTAL/RepeatControllerGroups.php
new file mode 100644
index 0000000..e6690ba
--- /dev/null
+++ b/lib/phptal/PHPTAL/RepeatControllerGroups.php
@@ -0,0 +1,199 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @author Iván Montes <drslump@pollinimini.net>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Keeps track of variable contents when using grouping in a path (first/ and last/)
+ *
+ * @package PHPTAL
+ * @subpackage Php
+ */
+class PHPTAL_RepeatControllerGroups
+{
+ protected $dict = array();
+ protected $cache = array();
+ protected $data = null;
+ protected $vars = array();
+ protected $branch;
+
+
+ public function __construct()
+ {
+ $this->dict = array();
+ $this->reset();
+ }
+
+ /**
+ * Resets the result caches. Use it to signal an iteration in the loop
+ *
+ */
+ public function reset()
+ {
+ $this->cache = array();
+ }
+
+ /**
+ * Checks if the data passed is the first one in a group
+ *
+ * @param mixed $data The data to evaluate
+ *
+ * @return Mixed True if the first item in the group, false if not and
+ * this same object if the path is not finished
+ */
+ public function first($data)
+ {
+ if ( !is_array($data) && !is_object($data) && !is_null($data) ) {
+
+ if ( !isset($this->cache['F']) ) {
+
+ $hash = md5($data);
+
+ if ( !isset($this->dict['F']) || $this->dict['F'] !== $hash ) {
+ $this->dict['F'] = $hash;
+ $res = true;
+ } else {
+ $res = false;
+ }
+
+ $this->cache['F'] = $res;
+ }
+
+ return $this->cache['F'];
+ }
+
+ $this->data = $data;
+ $this->branch = 'F';
+ $this->vars = array();
+ return $this;
+ }
+
+ /**
+ * Checks if the data passed is the last one in a group
+ *
+ * @param mixed $data The data to evaluate
+ *
+ * @return Mixed True if the last item in the group, false if not and
+ * this same object if the path is not finished
+ */
+ public function last($data)
+ {
+ if ( !is_array($data) && !is_object($data) && !is_null($data) ) {
+
+ if ( !isset($this->cache['L']) ) {
+
+ $hash = md5($data);
+
+ if (empty($this->dict['L'])) {
+ $this->dict['L'] = $hash;
+ $res = false;
+ } elseif ($this->dict['L'] !== $hash) {
+ $this->dict['L'] = $hash;
+ $res = true;
+ } else {
+ $res = false;
+ }
+
+ $this->cache['L'] = $res;
+ }
+
+ return $this->cache['L'];
+ }
+
+ $this->data = $data;
+ $this->branch = 'L';
+ $this->vars = array();
+ return $this;
+ }
+
+ /**
+ * Handles variable accesses for the tal path resolver
+ *
+ * @param string $var The variable name to check
+ *
+ * @return Mixed An object/array if the path is not over or a boolean
+ *
+ * @todo replace the PHPTAL_Context::path() with custom code
+ */
+ public function __get($var)
+ {
+ // When the iterator item is empty we just let the tal
+ // expression consume by continuously returning this
+ // same object which should evaluate to true for 'last'
+ if ( is_null($this->data) ) {
+ return $this;
+ }
+
+ // Find the requested variable
+ $value = PHPTAL_Context::path($this->data, $var, true);
+
+ // Check if it's an object or an array
+ if ( is_array($value) || is_object($value) ) {
+ // Move the context to the requested variable and return
+ $this->data = $value;
+ $this->addVarName($var);
+ return $this;
+ }
+
+ // get a hash of the variable contents
+ $hash = md5($value);
+
+ // compute a path for the variable to use as dictionary key
+ $path = $this->branch . $this->getVarPath() . $var;
+
+ // If we don't know about this var store in the dictionary
+ if ( !isset($this->cache[$path]) ) {
+
+ if ( !isset($this->dict[$path]) ) {
+ $this->dict[$path] = $hash;
+ $res = $this->branch === 'F';
+ } else {
+ // Check if the value has changed
+ if ($this->dict[$path] !== $hash) {
+ $this->dict[$path] = $hash;
+ $res = true;
+ } else {
+ $res = false;
+ }
+ }
+
+ $this->cache[$path] = $res;
+ }
+
+ return $this->cache[$path];
+
+ }
+
+ /**
+ * Adds a variable name to the current path of variables
+ *
+ * @param string $varname The variable name to store as a path part
+ * @access protected
+ */
+ protected function addVarName($varname)
+ {
+ $this->vars[] = $varname;
+ }
+
+ /**
+ * Returns the current variable path separated by a slash
+ *
+ * @return String The current variable path
+ * @access protected
+ */
+ protected function getVarPath()
+ {
+ return implode('/', $this->vars) . '/';
+ }
+}
diff --git a/lib/phptal/PHPTAL/Source.php b/lib/phptal/PHPTAL/Source.php
new file mode 100644
index 0000000..5d54e08
--- /dev/null
+++ b/lib/phptal/PHPTAL/Source.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * You can implement this interface to load templates from various sources (see SourceResolver)
+ *
+ * @package PHPTAL
+ */
+interface PHPTAL_Source
+{
+ /**
+ * unique path identifying the template source.
+ * must not be empty. must be as unique as possible.
+ *
+ * it doesn't have to be path on disk.
+ *
+ * @return string
+ */
+ public function getRealPath();
+
+ /**
+ * template source last modified time (unix timestamp)
+ * Return 0 if unknown.
+ *
+ * If you return 0:
+ * • PHPTAL won't know when to reparse the template,
+ * unless you change realPath whenever template changes.
+ * • clearing of cache will be marginally slower.
+ *
+ * @return long
+ */
+ public function getLastModifiedTime();
+
+ /**
+ * the template source
+ *
+ * @return string
+ */
+ public function getData();
+}
diff --git a/lib/phptal/PHPTAL/SourceResolver.php b/lib/phptal/PHPTAL/SourceResolver.php
new file mode 100644
index 0000000..1b03190
--- /dev/null
+++ b/lib/phptal/PHPTAL/SourceResolver.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * @package PHPTAL
+ */
+interface PHPTAL_SourceResolver
+{
+ /**
+ * Returns PHPTAL_Source or null.
+ */
+ public function resolve($path);
+}
diff --git a/lib/phptal/PHPTAL/StringSource.php b/lib/phptal/PHPTAL/StringSource.php
new file mode 100644
index 0000000..b2de2cb
--- /dev/null
+++ b/lib/phptal/PHPTAL/StringSource.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+/**
+ * Fake template source that makes PHPTAL->setString() work
+ *
+ * @package PHPTAL
+ */
+class PHPTAL_StringSource implements PHPTAL_Source
+{
+ const NO_PATH_PREFIX = '<string ';
+
+ public function __construct($data, $realpath = null)
+ {
+ $this->_data = $data;
+ $this->_realpath = $realpath ? $realpath : self::NO_PATH_PREFIX.md5($data).'>';
+ }
+
+ public function getLastModifiedTime()
+ {
+ if (substr($this->_realpath, 0, 8) !== self::NO_PATH_PREFIX && file_exists($this->_realpath)) {
+ return @filemtime($this->_realpath);
+ }
+ return 0;
+ }
+
+ public function getData()
+ {
+ return $this->_data;
+ }
+
+ /**
+ * well, this is not always a real path. If it starts with self::NO_PATH_PREFIX, then it's fake.
+ */
+ public function getRealPath()
+ {
+ return $this->_realpath;
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Tales.php b/lib/phptal/PHPTAL/Tales.php
new file mode 100644
index 0000000..571e85e
--- /dev/null
+++ b/lib/phptal/PHPTAL/Tales.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * You can implement this interface to create custom tales modifiers
+ *
+ * Methods suitable for modifiers must be static.
+ *
+ * @package PHPTAL
+ * @subpackage Php
+ */
+interface PHPTAL_Tales
+{
+}
+
+
+/**
+ * translates TALES expression with alternatives into single PHP expression.
+ * Identical to phptal_tales() for singular expressions.
+ *
+ * Please use this function rather than PHPTAL_Php_TalesInternal methods.
+ *
+ * @see PHPTAL_Php_TalesInternal::compileToPHPExpressions()
+ * @return string
+ */
+function phptal_tale($expression, $nothrow=false)
+{
+ return PHPTAL_Php_TalesInternal::compileToPHPExpression($expression, $nothrow);
+}
+
+/**
+ * returns PHP code that will evaluate given TALES expression.
+ * e.g. "string:foo${bar}" may be transformed to "'foo'.phptal_escape($ctx->bar)"
+ *
+ * Expressions with alternatives ("foo | bar") will cause it to return array
+ * Use phptal_tale() if you always want string.
+ *
+ * @param bool $nothrow if true, invalid expression will return NULL (at run time) rather than throwing exception
+ * @return string or array
+ */
+function phptal_tales($expression, $nothrow=false)
+{
+ return PHPTAL_Php_TalesInternal::compileToPHPExpressions($expression, $nothrow);
+}
+
diff --git a/lib/phptal/PHPTAL/TalesRegistry.php b/lib/phptal/PHPTAL/TalesRegistry.php
new file mode 100644
index 0000000..f519a1a
--- /dev/null
+++ b/lib/phptal/PHPTAL/TalesRegistry.php
@@ -0,0 +1,185 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Global registry of TALES expression modifiers
+ *
+ * @package PHPTAL
+ * @subpackage Php
+ */
+class PHPTAL_TalesRegistry
+{
+ private static $instance;
+
+ /**
+ * This is a singleton
+ *
+ * @return PHPTAL_TalesRegistry
+ */
+ static public function getInstance()
+ {
+ if (!self::$instance) {
+ self::$instance = new PHPTAL_TalesRegistry();
+ }
+
+ return self::$instance;
+ }
+
+ protected function __construct()
+ {
+ $this->registerPrefix('not', array('PHPTAL_Php_TalesInternal', 'not'));
+ $this->registerPrefix('path', array('PHPTAL_Php_TalesInternal', 'path'));
+ $this->registerPrefix('string', array('PHPTAL_Php_TalesInternal', 'string'));
+ $this->registerPrefix('php', array('PHPTAL_Php_TalesInternal', 'php'));
+ $this->registerPrefix('phptal-internal-php-block', array('PHPTAL_Php_TalesInternal', 'phptal_internal_php_block'));
+ $this->registerPrefix('exists', array('PHPTAL_Php_TalesInternal', 'exists'));
+ $this->registerPrefix('number', array('PHPTAL_Php_TalesInternal', 'number'));
+ $this->registerPrefix('true', array('PHPTAL_Php_TalesInternal', 'true'));
+
+ // these are added as fallbacks
+ $this->registerPrefix('json', array('PHPTAL_Php_TalesInternal', 'json'), true);
+ $this->registerPrefix('urlencode', array('PHPTAL_Php_TalesInternal', 'urlencode'), true);
+ }
+
+ /**
+ * Unregisters a expression modifier
+ *
+ * @param string $prefix
+ *
+ * @throws PHPTAL_ConfigurationException
+ */
+ public function unregisterPrefix($prefix)
+ {
+ if (!$this->isRegistered($prefix)) {
+ throw new PHPTAL_ConfigurationException("Expression modifier '$prefix' is not registered");
+ }
+
+ unset($this->_callbacks[$prefix]);
+ }
+
+ /**
+ *
+ * Expects an either a function name or an array of class and method as
+ * callback.
+ *
+ * @param string $prefix
+ * @param mixed $callback
+ * @param bool $is_fallback if true, method will be used as last resort (if there's no phptal_tales_foo)
+ */
+ public function registerPrefix($prefix, $callback, $is_fallback = false)
+ {
+ if ($this->isRegistered($prefix) && !$this->_callbacks[$prefix]['is_fallback']) {
+ if ($is_fallback) {
+ return; // simply ignored
+ }
+ throw new PHPTAL_ConfigurationException("Expression modifier '$prefix' is already registered");
+ }
+
+ // Check if valid callback
+
+ if (is_array($callback)) {
+
+ $class = new ReflectionClass($callback[0]);
+
+ if (!$class->isSubclassOf('PHPTAL_Tales')) {
+ throw new PHPTAL_ConfigurationException('The class you want to register does not implement "PHPTAL_Tales".');
+ }
+
+ $method = new ReflectionMethod($callback[0], $callback[1]);
+
+ if (!$method->isStatic()) {
+ throw new PHPTAL_ConfigurationException('The method you want to register is not static.');
+ }
+
+ // maybe we want to check the parameters the method takes
+
+ } else {
+ if (!function_exists($callback)) {
+ throw new PHPTAL_ConfigurationException('The function you are trying to register does not exist.');
+ }
+ }
+
+ $this->_callbacks[$prefix] = array('callback'=>$callback, 'is_fallback'=>$is_fallback);
+ }
+
+ /**
+ * true if given prefix is taken
+ */
+ public function isRegistered($prefix)
+ {
+ if (array_key_exists($prefix, $this->_callbacks)) {
+ return true;
+ }
+ }
+
+ private function findUnregisteredCallback($typePrefix)
+ {
+ // class method
+ if (strpos($typePrefix, '.')) {
+ $classCallback = explode('.', $typePrefix, 2);
+ $callbackName = null;
+ if (!is_callable($classCallback, false, $callbackName)) {
+ throw new PHPTAL_UnknownModifierException("Unknown phptal modifier $typePrefix. Function $callbackName does not exists or is not statically callable", $typePrefix);
+ }
+ $ref = new ReflectionClass($classCallback[0]);
+ if (!$ref->implementsInterface('PHPTAL_Tales')) {
+ throw new PHPTAL_UnknownModifierException("Unable to use phptal modifier $typePrefix as the class $callbackName does not implement the PHPTAL_Tales interface", $typePrefix);
+ }
+ return $classCallback;
+ }
+
+ // check if it is implemented via code-generating function
+ $func = 'phptal_tales_'.str_replace('-', '_', $typePrefix);
+ if (function_exists($func)) {
+ return $func;
+ }
+
+ // The following code is automatically modified in version for PHP 5.3
+ $func = 'PHPTALNAMESPACE\\phptal_tales_'.str_replace('-', '_', $typePrefix);
+ if (function_exists($func)) {
+ return $func;
+ }
+
+ return null;
+ }
+
+ /**
+ * get callback for the prefix
+ *
+ * @return callback or NULL
+ */
+ public function getCallback($prefix)
+ {
+ if ($this->isRegistered($prefix) && !$this->_callbacks[$prefix]['is_fallback']) {
+ return $this->_callbacks[$prefix]['callback'];
+ }
+
+ if ($callback = $this->findUnregisteredCallback($prefix)) {
+ return $callback;
+ }
+
+ if ($this->isRegistered($prefix)) {
+ return $this->_callbacks[$prefix]['callback'];
+ }
+
+ return null;
+ }
+
+ /**
+ * {callback, bool is_fallback}
+ */
+ private $_callbacks = array();
+}
+
diff --git a/lib/phptal/PHPTAL/TemplateException.php b/lib/phptal/PHPTAL/TemplateException.php
new file mode 100644
index 0000000..ce72b6b
--- /dev/null
+++ b/lib/phptal/PHPTAL/TemplateException.php
@@ -0,0 +1,160 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Exception that is related to location within a template.
+ * You can check srcFile and srcLine to find source of the error.
+ *
+ * @package PHPTAL
+ * @subpackage Exception
+ */
+class PHPTAL_TemplateException extends PHPTAL_Exception
+{
+ public $srcFile;
+ public $srcLine;
+ private $is_src_accurate;
+
+ public function __construct($msg, $srcFile='', $srcLine=0)
+ {
+ parent::__construct($msg);
+
+ if ($srcFile && $srcLine) {
+ $this->srcFile = $srcFile;
+ $this->srcLine = $srcLine;
+ $this->is_src_accurate = true;
+ } else {
+ $this->is_src_accurate = $this->setTemplateSource();
+ }
+
+ if ($this->is_src_accurate) {
+ $this->file = $this->srcFile;
+ $this->line = (int)$this->srcLine;
+ }
+ }
+
+ public function __toString()
+ {
+ if (!$this->srcFile || $this->is_src_accurate) return parent::__toString();
+ return "From {$this->srcFile} around line {$this->srcLine}\n".parent::__toString();
+ }
+
+ /**
+ * Set new TAL source file/line if it isn't known already
+ */
+ public function hintSrcPosition($srcFile, $srcLine)
+ {
+ if ($srcFile && $srcLine) {
+ if (!$this->is_src_accurate) {
+ $this->srcFile = $srcFile;
+ $this->srcLine = $srcLine;
+ $this->is_src_accurate = true;
+ } else if ($this->srcLine <= 1 && $this->srcFile === $srcFile) {
+ $this->srcLine = $srcLine;
+ }
+ }
+
+ if ($this->is_src_accurate) {
+ $this->file = $this->srcFile;
+ $this->line = (int)$this->srcLine;
+ }
+ }
+
+ private function isTemplatePath($path)
+ {
+ return preg_match('/[\\\\\/]tpl_[0-9a-f]{8}_[^\\\\]+$/', $path);
+ }
+
+ private function findFileAndLine()
+ {
+ if ($this->isTemplatePath($this->file)) {
+ return array($this->file, $this->line);
+ }
+
+ $eval_line = 0;
+ $eval_path = NULL;
+
+ // searches backtrace to find template file
+ foreach($this->getTrace() as $tr) {
+ if (!isset($tr['file'],$tr['line'])) continue;
+
+ if ($this->isTemplatePath($tr['file'])) {
+ return array($tr['file'], $tr['line']);
+ }
+
+ // PHPTAL.php uses eval() on first run to catch fatal errors. This makes template path invisible.
+ // However, function name matches template path and eval() is visible in backtrace.
+ if (false !== strpos($tr['file'], 'eval()')) {
+ $eval_line = $tr['line'];
+ }
+ else if ($eval_line && isset($tr['function'],$tr['args'],$tr['args'][0]) &&
+ $this->isTemplatePath("/".$tr['function'].".php") && $tr['args'][0] instanceof PHPTAL) {
+ return array($tr['args'][0]->getCodePath(), $eval_line);
+ }
+ }
+
+ return array(NULL,NULL);
+ }
+
+ /**
+ * sets srcLine and srcFile to template path and source line
+ * by checking error backtrace and scanning PHP code file
+ *
+ * @return bool true if found accurate data
+ */
+ private function setTemplateSource()
+ {
+ // not accurate, but better than null
+ $this->srcFile = $this->file;
+ $this->srcLine = $this->line;
+
+ list($file,$line) = $this->findFileAndLine();
+
+ if (NULL === $file) {
+ return false;
+ }
+
+ // this is not accurate yet, hopefully will be overwritten later
+ $this->srcFile = $file;
+ $this->srcLine = $line;
+
+ $lines = @file($file);
+ if (!$lines) {
+ return false;
+ }
+
+ $found_line=false;
+ $found_file=false;
+
+ // scan lines backwards looking for "from line" comments
+ $end = min(count($lines), $line)-1;
+ for($i=$end; $i >= 0; $i--) {
+ if (preg_match('/tag "[^"]*" from line (\d+)/', $lines[$i], $m)) {
+ $this->srcLine = intval($m[1]);
+ $found_line=true;
+ break;
+ }
+ }
+
+ foreach(preg_grep('/Generated by PHPTAL from/',$lines) as $line) {
+ if (preg_match('/Generated by PHPTAL from (.*) \(/', $line, $m)) {
+ $this->srcFile = $m[1];
+ $found_file=true;
+ break;
+ }
+ }
+
+ return $found_line && $found_file;
+ }
+}
diff --git a/lib/phptal/PHPTAL/Tokenizer.php b/lib/phptal/PHPTAL/Tokenizer.php
new file mode 100644
index 0000000..25ff332
--- /dev/null
+++ b/lib/phptal/PHPTAL/Tokenizer.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id:$
+ * @link http://phptal.org/
+ */
+
+class PHPTAL_Tokenizer
+{
+ private $regex, $names, $offset, $str;
+
+ private $current_token, $current_value;
+
+ function __construct($str, array $tokens)
+ {
+ $this->offset = 0;
+ $this->str = $str;
+ $this->end = strlen($str);
+
+ $this->regex = '/('.str_replace('/', '\/', implode(')|(', $tokens)).')|(.)/Ssi';
+ $this->names = array_keys($tokens);
+ $this->names[] = 'OTHER';
+ }
+
+ function eof()
+ {
+ return $this->offset >= $this->end;
+ }
+
+ function skipSpace()
+ {
+ while ($this->current_token === 'SPACE') $this->nextToken();
+ }
+
+ function nextToken()
+ {
+ if ($this->offset >= $this->end) {
+ $this->current_value = null;
+ return $this->current_token = 'EOF';
+ }
+
+ //if (!preg_match_all($this->regex, $this->str, $m, PREG_SET_ORDER, $this->offset)) throw new Exception("FAIL {$this->regex} at {$this->offset}");
+ if (!preg_match($this->regex, $this->str, $m, null, $this->offset)) throw new Exception("FAIL {$this->regex} didn't match '{$this->str}' at {$this->offset}");
+
+ $this->offset += strlen($m[0]); // in bytes
+
+ $this->current_value = $m[0];
+ $this->current_token = $this->names[count($m)-2]; // -1 for usual length/offset confusion, and minus one extra for $m[0]
+
+ return $this->current_token;
+ }
+
+ function token()
+ {
+ return $this->current_token;
+ }
+
+ function tokenValue()
+ {
+ return $this->current_value;
+ }
+}
diff --git a/lib/phptal/PHPTAL/TranslationService.php b/lib/phptal/PHPTAL/TranslationService.php
new file mode 100644
index 0000000..0a63e3f
--- /dev/null
+++ b/lib/phptal/PHPTAL/TranslationService.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * @package PHPTAL
+ */
+interface PHPTAL_TranslationService
+{
+ /**
+ * Set the target language for translations.
+ *
+ * When set to '' no translation will be done.
+ *
+ * You can specify a list of possible language for exemple :
+ *
+ * setLanguage('fr_FR', 'fr_FR@euro')
+ *
+ * @return string - chosen language
+ */
+ function setLanguage(/*...*/);
+
+ /**
+ * PHPTAL will inform translation service what encoding page uses.
+ * Output of translate() must be in this encoding.
+ */
+ function setEncoding($encoding);
+
+ /**
+ * Set the domain to use for translations (if different parts of application are translated in different files. This is not for language selection).
+ */
+ function useDomain($domain);
+
+ /**
+ * Set XHTML-escaped value of a variable used in translation key.
+ *
+ * You should use it to replace all ${key}s with values in translated strings.
+ *
+ * @param string $key - name of the variable
+ * @param string $value_escaped - XHTML markup
+ */
+ function setVar($key, $value_escaped);
+
+ /**
+ * Translate a gettext key and interpolate variables.
+ *
+ * @param string $key - translation key, e.g. "hello ${username}!"
+ * @param string $htmlescape - if true, you should HTML-escape translated string. You should never HTML-escape interpolated variables.
+ */
+ function translate($key, $htmlescape=true);
+}
diff --git a/lib/phptal/PHPTAL/Trigger.php b/lib/phptal/PHPTAL/Trigger.php
new file mode 100644
index 0000000..4eef8fc
--- /dev/null
+++ b/lib/phptal/PHPTAL/Trigger.php
@@ -0,0 +1,29 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Interface for Triggers (phptal:id)
+ *
+ * @package PHPTAL
+ */
+interface PHPTAL_Trigger
+{
+ const SKIPTAG = 1;
+ const PROCEED = 2;
+
+ public function start($id, $tpl);
+
+ public function end($id, $tpl);
+}
diff --git a/lib/phptal/PHPTAL/UnknownModifierException.php b/lib/phptal/PHPTAL/UnknownModifierException.php
new file mode 100644
index 0000000..4430a51
--- /dev/null
+++ b/lib/phptal/PHPTAL/UnknownModifierException.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * ${unknown:foo} found in template
+ *
+ * @package PHPTAL
+ * @subpackage Exception
+ */
+class PHPTAL_UnknownModifierException extends PHPTAL_TemplateException
+{
+ private $modifier_name;
+ public function __construct($msg, $modifier_name = null)
+ {
+ $this->modifier_name = $modifier_name;
+ parent::__construct($msg);
+ }
+
+ public function getModifierName()
+ {
+ return $this->modifier_name;
+ }
+}
diff --git a/lib/phptal/PHPTAL/VariableNotFoundException.php b/lib/phptal/PHPTAL/VariableNotFoundException.php
new file mode 100644
index 0000000..dfadaf2
--- /dev/null
+++ b/lib/phptal/PHPTAL/VariableNotFoundException.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Runtime error in TALES expression
+ *
+ * @package PHPTAL
+ * @subpackage Exception
+ */
+class PHPTAL_VariableNotFoundException extends PHPTAL_TemplateException
+{
+}