initial import of incubator apps and libraries - crumb, hippo, hydra, hound, and...
authorChuck Hagenbuch <chuck@nazik.horde.org>
Tue, 21 Oct 2008 01:56:33 +0000 (21:56 -0400)
committerChuck Hagenbuch <chuck@nazik.horde.org>
Tue, 21 Oct 2008 01:56:33 +0000 (21:56 -0400)
130 files changed:
crumb/COPYING [new file with mode: 0644]
crumb/LICENSE.ASL [new file with mode: 0644]
crumb/LICENSE.BSDL [new file with mode: 0644]
crumb/README [new file with mode: 0644]
crumb/addclient.php [new file with mode: 0644]
crumb/config/.cvsignore [new file with mode: 0644]
crumb/config/conf.bak.php [new file with mode: 0644]
crumb/config/conf.php [new file with mode: 0644]
crumb/config/conf.xml [new file with mode: 0644]
crumb/config/prefs.php.dist [new file with mode: 0644]
crumb/contactsearch.php [new file with mode: 0644]
crumb/docs/CHANGES [new file with mode: 0644]
crumb/docs/CREDITS [new file with mode: 0644]
crumb/docs/INSTALL [new file with mode: 0644]
crumb/docs/RELEASE_NOTES [new file with mode: 0644]
crumb/docs/TODO [new file with mode: 0644]
crumb/index.php [new file with mode: 0644]
crumb/lib/Block/example.php [new file with mode: 0644]
crumb/lib/Crumb.php [new file with mode: 0644]
crumb/lib/Driver.php [new file with mode: 0644]
crumb/lib/Driver/sql.php [new file with mode: 0644]
crumb/lib/Forms/AddClient.php [new file with mode: 0644]
crumb/lib/Forms/ContactSearch.php [new file with mode: 0644]
crumb/lib/base.php [new file with mode: 0644]
crumb/lib/version.php [new file with mode: 0644]
crumb/listclients.php [new file with mode: 0644]
crumb/locale/en_US/help.xml [new file with mode: 0644]
crumb/po/.cvsignore [new file with mode: 0644]
crumb/po/README [new file with mode: 0644]
crumb/scripts/sql/crumb.sql [new file with mode: 0644]
crumb/templates/common-header.inc [new file with mode: 0644]
crumb/templates/menu.inc [new file with mode: 0644]
crumb/themes/screen.css [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/History.txt [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/README.txt [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/chronic.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/grabber.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/handlers.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/ordinal.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/pointer.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeater.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_name.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_portion.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_fortnight.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_hour.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_minute.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month_name.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season_name.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_second.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_time.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_week.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_weekend.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_year.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/scalar.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/separator.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/chronic/time_zone.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/lib/numerizer/numerizer.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/suite.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_Chronic.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_Handler.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_Numerizer.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_RepeaterDayName.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_RepeaterFortnight.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_RepeaterHour.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_RepeaterMonth.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_RepeaterMonthName.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_RepeaterTime.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_RepeaterWeek.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_RepeaterWeekend.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_RepeaterYear.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_Span.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_Time.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_Token.rb [new file with mode: 0644]
framework/Horde_Date_Parser/chronic/test/test_parsing.rb [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Grabber.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Handlers.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Ordinal.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Parser.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Pointer.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeater.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Day.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayName.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayPortion.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Fortnight.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Hour.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Minute.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Month.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/MonthName.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Season.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/SeasonName.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Second.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Time.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Week.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Weekend.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Year.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Scalar.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/Separator.php [new file with mode: 0644]
framework/Horde_Date_Parser/lib/Horde/Date/Parser/TimeZone.php [new file with mode: 0644]
framework/Horde_Date_Parser/package.xml [new file with mode: 0644]
framework/Horde_Date_Parser/test/Horde/Date/Parser/AllTests.php [new file with mode: 0644]
hippo/SPEC.txt [new file with mode: 0644]
hippo/TODO.txt [new file with mode: 0644]
hippo/planet.horde.org/.htaccess [new file with mode: 0644]
hippo/planet.horde.org/favicon.ico [new file with mode: 0644]
hippo/planet.horde.org/libs/aggregator.php [new file with mode: 0644]
hippo/planet.horde.org/libs/scripts/aggregate.php [new file with mode: 0644]
hippo/planet.horde.org/libs/utf2entities.php [new file with mode: 0644]
hippo/planet.horde.org/themes/planet-horde/common.xsl [new file with mode: 0644]
hippo/planet.horde.org/themes/planet-horde/css/screen.css [new file with mode: 0644]
hippo/planet.horde.org/themes/planet-horde/css/style.css [new file with mode: 0644]
hippo/planet.horde.org/themes/planet-horde/img/content_corners.gif [new file with mode: 0644]
hippo/planet.horde.org/themes/planet-horde/img/feed-icon-10x10.png [new file with mode: 0644]
hippo/planet.horde.org/themes/planet-horde/img/logo.gif [new file with mode: 0644]
hippo/planet.horde.org/themes/planet-horde/img/planet-horde.psd [new file with mode: 0644]
hippo/planet.horde.org/themes/planet-horde/img/sidebar_bottom.gif [new file with mode: 0644]
hippo/planet.horde.org/themes/planet-horde/img/sidebar_head.gif [new file with mode: 0644]
hippo/planet.horde.org/themes/planet-horde/main.xsl [new file with mode: 0644]
hippo/planet.sql [new file with mode: 0644]
hound/urls [new file with mode: 0644]
hydra/lib/Page.php [new file with mode: 0644]
hydra/lib/PageMapper.php [new file with mode: 0644]
hydra/public/.htaccess [new file with mode: 0644]
hydra/public/index.php [new file with mode: 0644]
hydra/public/stylesheets/print.css [new file with mode: 0644]
hydra/public/stylesheets/screen.css [new file with mode: 0644]

diff --git a/crumb/COPYING b/crumb/COPYING
new file mode 100644 (file)
index 0000000..5a965fb
--- /dev/null
@@ -0,0 +1,280 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 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.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, 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 or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+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 give any other recipients of the Program a copy of this License
+along with the Program.
+
+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 Program or any portion
+of it, thus forming a work based on the Program, 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) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+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 Program, 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 Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) 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; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, 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 executable.  However, as a
+special exception, the source code 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.
+
+If distribution of executable or 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 counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program 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.
+
+  5. 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 Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program 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 to
+this License.
+
+  7. 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 Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program 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 Program.
+
+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.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program 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.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the 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 Program
+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 Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, 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
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "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 PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. 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 PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
diff --git a/crumb/LICENSE.ASL b/crumb/LICENSE.ASL
new file mode 100644 (file)
index 0000000..e0b7a13
--- /dev/null
@@ -0,0 +1,48 @@
+Version 1.0
+
+Copyright (c) 2006 The Horde Project. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+3. The end-user documentation included with the redistribution, if
+any, must include the following acknowledgment:
+
+   "This product includes software developed by the Horde Project
+    (http://www.horde.org/)."
+
+Alternately, this acknowledgment may appear in the software itself, if
+and wherever such third-party acknowledgments normally appear.
+
+4. The names "Horde", "The Horde Project", and "Skeleton" must not be
+used to endorse or promote products derived from this software without
+prior written permission. For written permission, please contact
+core@horde.org.
+
+5. Products derived from this software may not be called "Horde" or
+"Skeleton", nor may "Horde" or "Skeleton" appear in their name, without
+prior written permission of the Horde Project.
+
+THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE HORDE PROJECT OR ITS CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+This software consists of voluntary contributions made by many
+individuals on behalf of the Horde Project. For more information on
+the Horde Project, please see <http://www.horde.org/>.
diff --git a/crumb/LICENSE.BSDL b/crumb/LICENSE.BSDL
new file mode 100644 (file)
index 0000000..e2b4fbf
--- /dev/null
@@ -0,0 +1,24 @@
+Copyright (c) 2006 The Horde Project.  All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ - Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+ - Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HORDE PROJECT
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/crumb/README b/crumb/README
new file mode 100644 (file)
index 0000000..afbe1d5
--- /dev/null
@@ -0,0 +1,89 @@
+What is Crumb?
+=================
+
+:Last update:   $Date$
+:Revision:      $Revision$
+
+.. contents:: Contents
+.. section-numbering::
+
+Crumb is a light-weight CRM application.  By creating the concept of a client
+within Horde, Crumb allows a single place to manage functionality in
+several applications with data related to a client.
+
+This software is OSI Certified Open Source Software. OSI Certified is a
+certification mark of the `Open Source Initiative`_.
+
+.. _`Open Source Initiative`: http://www.opensource.org/
+
+
+Obtaining Crumb
+------------------
+
+Further information on Crumb and the latest version can be obtained at
+
+  http://www.horde.org/crumb/
+
+
+Documentation
+-------------
+
+The following documentation is available in the Crumb distribution:
+
+:README_:            This file
+:COPYING_:           Copyright and license information
+:LICENSE_:           Copyright and license information
+:`docs/CHANGES`_:    Changes by release
+:`docs/CREDITS`_:    Project developers
+:`docs/INSTALL`_:    Installation instructions and notes
+:`docs/TODO`_:       Development TODO list
+:`docs/UPGRADING`_:  Pointers on upgrading from previous Crumb versions
+
+
+Installation
+------------
+
+Instructions for installing Crumb can be found in the file INSTALL_ in the
+``docs/`` directory of the Crumb distribution.
+
+
+Assistance
+----------
+
+If you encounter problems with Crumb, help is available!
+
+The Horde Frequently Asked Questions List (FAQ), available on the Web at
+
+  http://www.horde.org/faq/
+
+The Horde Project runs a number of mailing lists, for individual applications
+and for issues relating to the project as a whole. Information, archives, and
+subscription information can be found at
+
+  http://www.horde.org/mail/
+
+Lastly, Horde developers, contributors and users also make occasional
+appearances on IRC, on the channel #horde on the freenode Network
+(irc.freenode.net).
+
+
+Licensing
+---------
+
+For licensing and copyright information, please see the file COPYING_/LICENSE_
+in the Crumb distribution.
+
+Thanks,
+
+The Crumb team
+
+
+.. _README: ?f=README.html
+.. _COPYING: http://www.horde.org/licenses/gpl.php
+.. _LICENSE: http://www.horde.org/licenses/asl.php
+.. _docs/CHANGES: ?f=CHANGES.html
+.. _docs/CREDITS: ?f=CREDITS.html
+.. _INSTALL:
+.. _docs/INSTALL: ?f=INSTALL.html
+.. _docs/TODO: ?f=TODO.html
+.. _docs/UPGRADING: ?f=UPGRADING.html
diff --git a/crumb/addclient.php b/crumb/addclient.php
new file mode 100644 (file)
index 0000000..80d4dcc
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+/**
+ * $Horde$
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Ben Klang <ben@alkaloid.net>
+ */
+
+@define('CRUMB_BASE', dirname(__FILE__));
+require_once CRUMB_BASE . '/lib/base.php';
+require_once 'Horde/Variables.php';
+require_once 'Horde/Form.php';
+require_once 'Horde/Form/Renderer.php';
+require_once CRUMB_BASE . '/lib/Forms/AddClient.php';
+
+$vars = Variables::getDefaultVariables();
+$formname = $vars->get('formname');
+
+$addform = new Horde_Form_AddClient($vars);
+if (is_a($addform, 'PEAR_Error')) {
+    Horde::logMessage($addform, __FILE__, __LINE__, PEAR_LOG_ERR);
+    $notification->push(_("An internal error has occurred.  Details have been logged for the administrator."));
+    $addform = null;
+}
+
+if ($addform->validate($vars)) {
+print_r($addform->getInfo());
+}
+
+$url = Horde::applicationUrl('addclient.php');
+$title = _("Add New Client");
+
+require CRUMB_TEMPLATES . '/common-header.inc';
+require CRUMB_TEMPLATES . '/menu.inc';
+
+if (!empty($addform)) {
+    $addform->renderActive(null, null, $url, 'post');
+}
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/crumb/config/.cvsignore b/crumb/config/.cvsignore
new file mode 100644 (file)
index 0000000..51adefa
--- /dev/null
@@ -0,0 +1,3 @@
+conf.php
+conf.bak.php
+prefs.php
diff --git a/crumb/config/conf.bak.php b/crumb/config/conf.bak.php
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/crumb/config/conf.php b/crumb/config/conf.php
new file mode 100644 (file)
index 0000000..55865e1
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+/* CONFIG START. DO NOT CHANGE ANYTHING IN OR AFTER THIS LINE. */
+$conf['storage']['params']['table'] = 'crumb_clients';
+$conf['storage']['params']['driverconfig'] = 'horde';
+$conf['storage']['driver'] = 'sql';
+$conf['menu']['apps'] = array();
+/* CONFIG END. DO NOT CHANGE ANYTHING IN OR BEFORE THIS LINE. */
diff --git a/crumb/config/conf.xml b/crumb/config/conf.xml
new file mode 100644 (file)
index 0000000..3812b72
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!-- $Horde$ -->
+<configuration>
+ <configsection name="storage">
+  <configheader>Storage System Settings</configheader>
+  <configswitch name="driver" desc="What storage driver should we use?">sql
+   <case name="sql" desc="SQL">
+    <configsection name="params">
+     <configsql switchname="driverconfig">
+      <configstring name="table" desc="Database
+      table">crumb_clients</configstring>
+     </configsql>
+    </configsection>
+   </case>
+  </configswitch>
+ </configsection>
+
+ <configsection name="menu">
+  <configheader>Menu Settings</configheader>
+  <configmultienum name="apps" desc="Select any applications that should be
+  linked in Crumb's menu">
+   <values>
+    <configspecial name="list-horde-apps" />
+   </values>
+  </configmultienum>
+ </configsection>
+</configuration>
diff --git a/crumb/config/prefs.php.dist b/crumb/config/prefs.php.dist
new file mode 100644 (file)
index 0000000..3f64c81
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+/**
+ * $Horde: skeleton/config/prefs.php.dist,v 1.3 2003/12/03 20:09:14 chuck Exp $
+ *
+ * See horde/config/prefs.php for documentation on the structure of this file.
+ */
+
+$prefGroups['Sample'] = array(
+    'column' => _("Sample Prefs"),
+    'label' => _("Sample Pref"),
+    'desc' => _("Set your sample preference."),
+    'members' => array('sample')
+);
+
+$_prefs['sample'] = array(
+    'value' => '',
+    'locked' => false,
+    'shared' => false,
+    'type' => 'text',
+    'desc' => _("This is your sample preference.")
+);
diff --git a/crumb/contactsearch.php b/crumb/contactsearch.php
new file mode 100644 (file)
index 0000000..9acec49
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+/**
+ * $Horde$
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Ben Klang <ben@alkaloid.net>
+ */
+
+@define('CRUMB_BASE', dirname(__FILE__));
+require_once CRUMB_BASE . '/lib/base.php';
+require_once 'Horde/Form.php';
+require_once 'Horde/Form/Renderer.php';
+require_once CRUMB_BASE . '/lib/Forms/ContactSearch.php';
+require_once 'Horde/Variables.php';
+
+$vars = Variables::getDefaultVariables();
+
+$searchform = new Horde_Form_ContactSearch($vars);
+
+$info = array();
+if ($searchform->validate($vars)) {
+echo "Success!";
+}
+
+$url = Horde::applicationUrl(basename(__FILE__));
+$title = $searchform->getTitle();
+
+require CRUMB_TEMPLATES . '/common-header.inc';
+require CRUMB_TEMPLATES . '/menu.inc';
+
+$searchform->renderActive(null, null, $url, 'post');
+
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/crumb/docs/CHANGES b/crumb/docs/CHANGES
new file mode 100644 (file)
index 0000000..141c0cc
--- /dev/null
@@ -0,0 +1,5 @@
+---
+0.1
+---
+
+[xyz] Initial Release
diff --git a/crumb/docs/CREDITS b/crumb/docs/CREDITS
new file mode 100644 (file)
index 0000000..62c26b7
--- /dev/null
@@ -0,0 +1,24 @@
+===========================
+ Crumb Development Team
+===========================
+
+
+Core Developers
+===============
+Ben Klang <ben@alkaloid.net>
+
+
+Drivers
+=======
+
+
+
+Localization
+============
+
+=====================   ======================================================
+=====================   ======================================================
+
+
+Contributions
+=============
diff --git a/crumb/docs/INSTALL b/crumb/docs/INSTALL
new file mode 100644 (file)
index 0000000..949162a
--- /dev/null
@@ -0,0 +1,243 @@
+=========================
+ Installing Crumb 1.0
+=========================
+
+:Last update:   $Date: 2007/12/14 17:44:26 $
+:Revision:      $Revision: 1.16 $
+
+.. contents:: Contents
+.. section-numbering::
+
+This document contains instructions for installing the Crumb ...
+
+For information on the capabilities and features of Crumb, see the file
+README_ in the top-level directory of the Crumb distribution.
+
+
+Obtaining Crumb
+==================
+
+Crumb can be obtained from the Horde website and FTP server, at
+
+   http://www.horde.org/crumb/
+
+   ftp://ftp.horde.org/pub/crumb/
+
+Or use the mirror closest to you:
+
+   http://www.horde.org/mirrors.php
+
+Bleeding-edge development versions of Crumb are available via CVS; see the
+file `horde/docs/HACKING`_ in the Horde distribution, or the website
+http://www.horde.org/source/, for information on accessing the Horde CVS
+repository.
+
+
+Prerequisites
+=============
+
+To function properly, Crumb **requires** the following:
+
+1. A working Horde installation.
+
+   Crumb runs within the `Horde Application Framework`_, a set of common
+   tools for Web applications written in PHP. You must install Horde before
+   installing Crumb.
+
+   .. Important:: Crumb 1.0 requires version 3.0+ of the Horde Framework -
+                  earlier versions of Horde will **not** work.
+
+   .. _`Horde Application Framework`: http://www.horde.org/horde/
+
+   The Horde Framework can be obtained from the Horde website and FTP server,
+   at
+
+      http://www.horde.org/horde/
+
+      ftp://ftp.horde.org/pub/horde/
+
+   Many of Crumb's prerequisites are also Horde prerequisites.
+
+   .. Important:: Be sure to have completed all of the steps in the
+                  `horde/docs/INSTALL`_ file for the Horde Framework before
+                  installing Crumb.
+
+2. The following PHP capabilities:
+
+   a. FOO support ``--with-foo`` [OPTIONAL]
+
+      Description of Foo and what it is used for.
+
+3. The following PEAR packages:
+   (See `horde/docs/INSTALL`_ for instructions on installing PEAR packages)
+
+   a. PEAR_Package x.x.x [OPTIONAL]
+
+      Crumb uses the Foo_Bar class for...
+
+4. The following PECL modules:
+   (See `horde/docs/INSTALL`_ for instructions on installing PECL modules)
+
+   a. pecl_package x.x.x [OPTIONAL]
+
+      pecl_package is required to...
+
+5. Something else.
+
+The following items are not required, but are strongly **recommended**:
+
+1. Yet something else.
+
+
+Installing Crumb
+===================
+
+Crumb is written in PHP, and must be installed in a web-accessible
+directory. The precise location of this directory will differ from system to
+system. Conventionally, Crumb is installed directly underneath Horde in the
+web server's document tree.
+
+Since Crumb is written in PHP, there is no compilation necessary; simply
+expand the distribution where you want it to reside and rename the root
+directory of the distribution to whatever you wish to appear in the URL. For
+example, with the Apache web server's default document root of
+``/usr/local/apache/htdocs``, you would type::
+
+   cd /usr/local/apache/htdocs/horde
+   tar zxvf /path/to/crumb-h3-x.y.z.tar.gz
+   mv crumb-h3-x.y.z crumb
+
+and would then find Crumb at the URL::
+
+   http://your-server/horde/crumb/
+
+
+Configuring Crumb
+====================
+
+1. Configuring Horde for Crumb
+
+   a. Register the application
+
+      In ``horde/config/registry.php``, find the ``applications['crumb']``
+      stanza. The default settings here should be okay, but you can change
+      them if desired. If you have changed the location of Crumb relative
+      to Horde, either in the URL, in the filesystem or both, you must update
+      the ``fileroot`` and ``webroot`` settings to their correct values.
+
+2. Creating the database tables
+
+   The specific steps to create Crumb's database tables depend on which
+   database you've chosen to use.
+
+   First, look in ``scripts/sql/`` to see if a script already exists for your
+   database type. If so, you should be able to simply execute that script as
+   superuser in your database. (Note that executing the script as the "horde"
+   user will probably fail when granting privileges.)
+
+   If such a script does not exist, you'll need to build your own, using the
+   file ``crumb.sql`` as a starting point.  If you need assistance in
+   creating database tables, you may wish to let us know on the Crumb
+   mailing list.
+
+   You will also need to make sure that the "horde" user in your database has
+   table-creation privileges, so that the tables that `PEAR DB`_ uses to
+   provide portable sequences can be created.
+
+   .. _`PEAR DB`: http://pear.php.net/DB
+
+3. Configuring Crumb
+
+   To configure Crumb, change to the ``config/`` directory of the installed
+   distribution, and make copies of all of the configuration ``dist`` files
+   without the ``dist`` suffix::
+
+      cd config/
+      for foo in *.dist; do cp $foo `basename $foo .dist`; done
+
+   Or on Windows::
+
+      copy *.dist *.
+
+   Documentation on the format and purpose of those files can be found in each
+   file. You may edit these files if you wish to customize Crumb's
+   appearance and behavior. With one exception (``foo.php``) the defaults will
+   be correct for most sites.
+
+   You must login to Horde as a Horde Administrator to finish the
+   configuration of Crumb. Use the Horde ``Administration`` menu item to
+   get to the administration page, and then click on the ``Configuration``
+   icon to get the configuration page. Select ``Crumb Name`` from the
+   selection list of applications. Fill in or change any configuration values
+   as needed. When done click on ``Generate Crumb Name Configuration`` to
+   generate the ``conf.php`` file. If your web server doesn't have write
+   permissions to the Crumb configuration directory or file, it will not be
+   able to write the file. In this case, go back to ``Configuration`` and
+   choose one of the other methods to create the configuration file
+   ``crumb/config/conf.php``.
+
+   Note for international users: Crumb uses GNU gettext to provide local
+   translations of text displayed by applications; the translations are found
+   in the ``po/`` directory. If a translation is not yet available for your
+   locale (and you wish to create one), see the ``horde/po/README`` file, or
+   if you're having trouble using a provided translation, please see the
+   `horde/docs/TRANSLATIONS`_ file for instructions.
+
+4. More instructions, upgrading, securing, etc.
+
+5. Testing Crumb
+
+   Once you have configured Crumb, bring up the included test page in your
+   Web browser to ensure that all necessary prerequisites have been met. See
+   the `horde/docs/INSTALL`_ document for further details on Horde test
+   scripts. If you installed Crumb as described above, the URL to the test
+   page would be::
+
+      http://your-server/horde/crumb/test.php
+
+   The test script will also allow you to test...
+
+   Next, use Crumb to.... Test at least the following:
+
+   - Foo
+   - Bar
+
+
+Known Problems
+==============
+
+...
+
+
+Obtaining Support
+=================
+
+If you encounter problems with Crumb, help is available!
+
+The Horde Frequently Asked Questions List (FAQ), available on the Web at
+
+  http://www.horde.org/faq/
+
+The Horde Project runs a number of mailing lists, for individual applications
+and for issues relating to the project as a whole. Information, archives, and
+subscription information can be found at
+
+  http://www.horde.org/mail/
+
+Lastly, Horde developers, contributors and users may also be found on IRC,
+on the channel #horde on the Freenode Network (irc.freenode.net).
+
+Please keep in mind that Crumb is free software written by volunteers.
+For information on reasonable support expectations, please read
+
+  http://www.horde.org/support.php
+
+Thanks for using Crumb!
+
+The Crumb team
+
+
+.. _README: ?f=README.html
+.. _`horde/docs/HACKING`: ../../horde/docs/?f=HACKING.html
+.. _`horde/docs/INSTALL`: ../../horde/docs/?f=INSTALL.html
+.. _`horde/docs/TRANSLATIONS`: ../../horde/docs/?f=TRANSLATIONS.html
diff --git a/crumb/docs/RELEASE_NOTES b/crumb/docs/RELEASE_NOTES
new file mode 100644 (file)
index 0000000..ef85eba
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Release focus. Possible values:
+ * 0 - N/A
+ * 1 - Initial freshmeat announcement
+ * 2 - Documentation
+ * 3 - Code cleanup
+ * 4 - Minor feature enhancements
+ * 5 - Major feature enhancements
+ * 6 - Minor bugfixes
+ * 7 - Major bugfixes
+ * 8 - Minor security fixes
+ * 9 - Major security fixes
+ */
+$this->notes['fm']['focus'] = 4;
+
+/* Mailing list release notes. */
+$this->notes['ml']['changes'] = <<<ML
+The Horde Team is pleased to announce the [first release candidate|final
+release] of the Crumb Foo Bar Application version H3 (x.x).
+
+The Crumb Foo Bar Application is a...
+
+[For alpha/beta releases:
+This is a preview version that should not be used on production systems.  This
+version is considered feature complete but there might still be a few bugs.
+You should not use this preview version over existing production data.
+
+We encourage widespread testing and feedback via the mailing lists or our bug
+tracking system.  Updated translations are very welcome, though some strings
+might still change before the final release.]
+
+[For release candidates:
+Barring any problems, this code will be released as Crumb H3 (x.x).
+Testing is requested and comments are encouraged.
+Updated translations would also be great.]
+
+The major changes compared to the Crumb version H3 (x.x) are:
+[or: Changes in this release:]
+    * ...
+ML;
+
+/* Freshmeat release notes, not more than 600 characters. */
+$this->notes['fm']['changes'] = <<<FM
+FM;
+
+$this->notes['name'] = 'Crumb';
+$this->notes['fm']['project'] = 'crumb';
+$this->notes['fm']['branch'] = 'Default';
diff --git a/crumb/docs/TODO b/crumb/docs/TODO
new file mode 100644 (file)
index 0000000..1bc73bd
--- /dev/null
@@ -0,0 +1,8 @@
+================================
+ Crumb Development TODO List
+================================
+
+:Last update:   $Date: 2007/04/22 04:51:54 $
+:Revision:      $Revision: 1.2 $
+
+- Example todo
diff --git a/crumb/index.php b/crumb/index.php
new file mode 100644 (file)
index 0000000..0a9b79e
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+/**
+ * $Horde: crumb/index.php,v 1.12 2008/01/02 11:14:00 jan Exp $
+ *
+ * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Ben Klang <ben@alkaloid.net>
+ */
+
+@define('CRUMB_BASE', dirname(__FILE__));
+$crumb_configured = (is_readable(CRUMB_BASE . '/config/conf.php'));
+
+if (!$crumb_configured) {
+    require CRUMB_BASE . '/../lib/Test.php';
+    Horde_Test::configFilesMissing('Crumb', CRUMB_BASE,
+                                   array('conf.php'));
+}
+
+require CRUMB_BASE . '/listclients.php';
diff --git a/crumb/lib/Block/example.php b/crumb/lib/Block/example.php
new file mode 100644 (file)
index 0000000..81a6160
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+$block_name = _("Example Block");
+
+/**
+ * $Horde: skeleton/lib/Block/example.php,v 1.2 2007/04/17 15:16:33 jan Exp $
+ *
+ * @package Horde_Block
+ */
+class Horde_Block_Skeleton_example extends Horde_Block {
+
+    var $_app = 'skeleton';
+
+    function _params()
+    {
+        return array('color' => array('type' => 'text',
+                                      'name' => _("Color"),
+                                      'default' => '#ff0000'));
+    }
+
+    /**
+     * The title to go in this block.
+     *
+     * @return string   The title text.
+     */
+    function _title()
+    {
+        return _("Color");
+    }
+
+    /**
+     * The content to go in this block.
+     *
+     * @return string   The content
+     */
+    function _content()
+    {
+        $html  = '<table width="100" height="100" bgcolor="%s">';
+        $html .= '<tr><td>&nbsp;</td></tr>';
+        $html .= '</table>';
+
+        return sprintf($html, $this->_params['color']);
+    }
+
+}
diff --git a/crumb/lib/Crumb.php b/crumb/lib/Crumb.php
new file mode 100644 (file)
index 0000000..088e141
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Crumb Base Class.
+ *
+ * $Horde$
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Ben Klang <ben@alkaloid.net>
+ * @package Crumb
+ */
+class Crumb {
+
+    /**
+     * Build Crumb's list of menu items.
+     */
+    function getMenu($returnType = 'object')
+    {
+        global $conf, $registry, $browser, $print_link;
+
+        require_once 'Horde/Menu.php';
+
+        $menu = new Menu(HORDE_MENU_MASK_ALL);
+        $menu->add(Horde::applicationUrl('listclients.php'), _("List Clients"), 'user.png', $registry->getImageDir('horde'));
+        $menu->add(Horde::applicationUrl('addclient.php'), _("Add Client"), 'user.png', $registry->getImageDir('horde'));
+
+        if ($returnType == 'object') {
+            return $menu;
+        } else {
+            return $menu->render();
+        }
+    }
+
+}
diff --git a/crumb/lib/Driver.php b/crumb/lib/Driver.php
new file mode 100644 (file)
index 0000000..a18b477
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Crumb_Driver:: defines an API for implementing storage backends for
+ * Crumb.
+ *
+ * $Horde$
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Ben Klang <ben@alkaloid.net>
+ * @package Crumb
+ */
+class Crumb_Driver {
+
+
+    /**
+     * Lists all clients.
+     *
+     * @return array  Returns a list of all foos.
+     */
+    function listClients()
+    {
+        return $this->_listClients();
+    }
+
+    /**
+     * Attempts to return a concrete Crumb_Driver instance based on $driver.
+     *
+     * @param string $driver  The type of the concrete Crumb_Driver subclass
+     *                        to return.  The class name is based on the
+     *                        storage driver ($driver).  The code is
+     *                        dynamically included.
+     *
+     * @param array $params   A hash containing any additional configuration
+     *                        or connection parameters a subclass might need.
+     *
+     * @return Crumb_Driver  The newly created concrete Crumb_Driver
+     *                          instance, or false on an error.
+     */
+    function factory($driver = null, $params = null)
+    {
+        if ($driver === null) {
+            $driver = $GLOBALS['conf']['storage']['driver'];
+        }
+        $driver = basename($driver);
+
+        if ($params === null) {
+            $params = Horde::getDriverConfig('storage', $driver);
+        }
+
+        $class = 'Crumb_Driver_' . $driver;
+        if (!class_exists($class)) {
+            include dirname(__FILE__) . '/Driver/' . $driver . '.php';
+        }
+        if (class_exists($class)) {
+            return new $class($params);
+        } else {
+            return false;
+        }
+    }
+
+}
diff --git a/crumb/lib/Driver/sql.php b/crumb/lib/Driver/sql.php
new file mode 100644 (file)
index 0000000..c08025c
--- /dev/null
@@ -0,0 +1,181 @@
+<?php
+/**
+ * Crumb storage implementation for PHP's PEAR database abstraction layer.
+ *
+ * Required values for $params:<pre>
+ *      'phptype'       The database type (e.g. 'pgsql', 'mysql', etc.).
+ *      'table'         The name of the foo table in 'database'.
+ *      'charset'       The database's internal charset.</pre>
+ *
+ * Required by some database implementations:<pre>
+ *      'database'      The name of the database.
+ *      'hostspec'      The hostname of the database server.
+ *      'protocol'      The communication protocol ('tcp', 'unix', etc.).
+ *      'username'      The username with which to connect to the database.
+ *      'password'      The password associated with 'username'.
+ *      'options'       Additional options to pass to the database.
+ *      'tty'           The TTY on which to connect to the database.
+ *      'port'          The port on which to connect to the database.</pre>
+ *
+ * The table structure can be created by the scripts/sql/crumb_foo.sql
+ * script.
+ *
+ * $Horde$
+ *
+ * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author  Ben Klang <ben@alkaloid.net>
+ * @package Crumb
+ */
+class Crumb_Driver_sql extends Crumb_Driver {
+
+    /**
+     * Hash containing connection parameters.
+     *
+     * @var array
+     */
+    var $_params = array();
+
+    /**
+     * Handle for the current database connection.
+     *
+     * @var DB
+     */
+    var $_db;
+
+    /**
+     * Handle for the current database connection, used for writing. Defaults
+     * to the same handle as $_db if a separate write database is not required.
+     *
+     * @var DB
+     */
+    var $_write_db;
+
+    /**
+     * Boolean indicating whether or not we're connected to the SQL server.
+     *
+     * @var boolean
+     */
+    var $_connected = false;
+
+    /**
+     * Constructs a new SQL storage object.
+     *
+     * @param array $params  A hash containing connection parameters.
+     */
+    function Crumb_Driver_sql($params = array())
+    {
+        $this->_params = $params;
+    }
+
+    /**
+     * Retrieves the list of clients from the database.
+     *
+     * @return boolean|PEAR_Error  True on success, PEAR_Error on failure.
+     */
+    function _listClients()
+    {
+        /* Make sure we have a valid database connection. */
+        $this->_connect();
+
+        /* Build the SQL query. */
+        $query = 'SELECT * FROM ' . $this->_params['table'];
+
+        /* Log the query at a DEBUG log level. */
+        Horde::logMessage(sprintf('Crumb_Driver_sql::_listClients(): %s', $query), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        /* Execute the query. */
+        $result = $this->_db->getAll($query, array(), DB_FETCHMODE_ASSOC);
+
+        return $result;
+    }
+
+    /**
+     * Attempts to open a persistent connection to the SQL server.
+     *
+     * @return boolean  True on success; exits (Horde::fatal()) on error.
+     */
+    function _connect()
+    {
+        if ($this->_connected) {
+            return true;
+        }
+
+        Horde::assertDriverConfig($this->_params, 'storage',
+                                  array('phptype', 'charset', 'table'));
+
+        if (!isset($this->_params['database'])) {
+            $this->_params['database'] = '';
+        }
+        if (!isset($this->_params['username'])) {
+            $this->_params['username'] = '';
+        }
+        if (!isset($this->_params['hostspec'])) {
+            $this->_params['hostspec'] = '';
+        }
+
+        /* Connect to the SQL server using the supplied parameters. */
+        require_once 'DB.php';
+        $this->_write_db = &DB::connect($this->_params,
+                                        array('persistent' => !empty($this->_params['persistent'])));
+        if (is_a($this->_write_db, 'PEAR_Error')) {
+            Horde::fatal($this->_write_db, __FILE__, __LINE__);
+        }
+
+        // Set DB portability options.
+        switch ($this->_write_db->phptype) {
+        case 'mssql':
+            $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+            break;
+        default:
+            $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+        }
+
+        /* Check if we need to set up the read DB connection seperately. */
+        if (!empty($this->_params['splitread'])) {
+            $params = array_merge($this->_params, $this->_params['read']);
+            $this->_db = &DB::connect($params,
+                                      array('persistent' => !empty($params['persistent'])));
+            if (is_a($this->_db, 'PEAR_Error')) {
+                Horde::fatal($this->_db, __FILE__, __LINE__);
+            }
+
+            // Set DB portability options.
+            switch ($this->_db->phptype) {
+            case 'mssql':
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM);
+                break;
+            default:
+                $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS);
+            }
+
+        } else {
+            /* Default to the same DB handle for the writer too. */
+            $this->_db =& $this->_write_db;
+        }
+
+        $this->_connected = true;
+
+        return true;
+    }
+
+    /**
+     * Disconnects from the SQL server and cleans up the connection.
+     *
+     * @return boolean  True on success, false on failure.
+     */
+    function _disconnect()
+    {
+        if ($this->_connected) {
+            $this->_connected = false;
+            $this->_db->disconnect();
+            $this->_write_db->disconnect();
+        }
+
+        return true;
+    }
+
+}
diff --git a/crumb/lib/Forms/AddClient.php b/crumb/lib/Forms/AddClient.php
new file mode 100644 (file)
index 0000000..a792e05
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+/**
+ * $Horde$
+ *
+ * Copyright 2008 The Horde Project <http://www.horde.org/>
+ *
+ * See the enclosed file LICENSE for license information (GPL). If you
+ * did not receive this file, see http://www.horde.org/licenses/gpl.php.
+ *
+ * @author Ben Klang <ben@alkaloid.net>
+ * @package Crumb
+ */
+require_once 'Horde/Form/Action.php';
+
+class Horde_Form_AddClient extends Horde_Form
+{
+    function Horde_Form_AddClient(&$vars)
+    {
+        parent::Horde_Form($vars, _("Add New Client"));
+
+        $addOrPick = array('create' => _("Create New"),
+                           'assign' => _("Assign Existing"));
+
+        $action = &Horde_Form_Action::factory('reload');
+       
+        $select = &$this->addVariable(_("Contact Information"), 'chooseContact', 'enum', true, false, null, array($addOrPick, true));
+        $select->setAction($action);
+        $select->setOption('trackchange', true);
+
+        if ($vars->get('chooseContact') == 'create') {
+            $turbaform = $GLOBALS['registry']->call('contacts/getAddClientForm', array(&$vars));
+            if (is_a($turbaform, 'PEAR_Error')) {
+                Horde::logMessage($addform, __FILE__, __LINE__, PEAR_LOG_ERR);
+                $notification->push(_("An internal error has occurred.  Details have beenlogged for the administrator."));
+                $addform = null;
+            }
+            $elements = $turbaform->getVariables();
+            foreach ($elements as $element) {
+                $this->importVariable($element);
+            }
+        } elseif ($vars->get('chooseContact') == 'assign') {
+            require_once CRUMB_BASE . '/lib/Forms/ContactSearch.php';
+            $searchform = new Horde_Form_ContactSearch($vars);
+            $elements = $searchform->getVariables();
+            foreach ($elements as $element) {
+                $this->importVariable($element);
+            }
+        }
+
+        $action = &Horde_Form_Action::factory('reload');
+        $select = &$this->addVariable(_("Ticket Queue"), 'chooseQueue', 'enum', true, false, null, array($addOrPick, true));
+        $select->setAction($action);
+        $select->setOption('trackchange', true);
+
+        if ($vars->get('chooseQueue') == 'create') {
+            $whupsform = $GLOBALS['registry']->call('tickets/getAddQueueForm', array(&$vars));
+            if (is_a($whupsform, 'PEAR_Error')) {
+                Horde::logMessage($addform, __FILE__, __LINE__, PEAR_LOG_ERR);
+                $notification->push(_("An internal error has occurred.  Details have been logged for the administrator."));
+                $addform = null;
+            }
+            $elements = $whupsform->getVariables();
+            foreach ($elements as $element) {
+                $this->importVariable($element);
+            }
+        } elseif ($vars->get('chooseQueue') == 'assign') {
+            $queues = $GLOBALS['registry']->listQueues();
+
+
+        $action = &Horde_Form_Action::factory('reload');
+        $select = &$this->addVariable(_("Group"), 'rectype', 'enum', true, false, null, array($addOrPick, true));
+        $select->setAction($action);
+        $select->setOption('trackchange', true);
+
+        return true;
+    }
+}
diff --git a/crumb/lib/Forms/ContactSearch.php b/crumb/lib/Forms/ContactSearch.php
new file mode 100644 (file)
index 0000000..50190e0
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+/**
+ * $Horde: beatnik/lib/Forms/EditRecord.php,v 1.6 2008/03/11 08:58:00 duck Exp $
+ *
+ * Copyright 2005-2007 Alkaloid Networks <http://www.alkaloid.net>
+ *
+ * See the enclosed file LICENSE for license information (GPL). If you
+ * did not receive this file, see http://www.horde.org/licenses/gpl.php.
+ *
+ * @author Ben Klang <ben@alkaloid.net>
+ * @package Beatnik
+ */
+class Horde_Form_ContactSearch extends Horde_Form
+{
+    function Horde_Form_ContactSearch(&$vars)
+    {
+        parent::Horde_Form($vars, _("Search for Client Contact Record"));
+
+        $this->addVariable(_("Name"), 'name', 'text', true, false, _("Enter a few characters to search for all clients whose names contain the search text"));
+
+        $name = $vars->get('name');
+        if (!empty($name)) {
+            $results = $GLOBALS['registry']->call('contacts/searchClients', array($name));
+            // We only pass one search string so there is only one element at
+            // the top level of the results array.
+            $results = array_pop($results);
+            $contacts = array();
+            foreach ($results as $contact) {
+                $contacts[$contact['__uid']] = $contact['name'];
+            }
+            asort($contacts);
+            $this->addVariable(_("Contact"), 'uid', 'radio', true, false, _("Select the matching contact record or begin a new search above"), array($contacts));
+            return true;
+        }
+    }
+}
diff --git a/crumb/lib/base.php b/crumb/lib/base.php
new file mode 100644 (file)
index 0000000..fd3c40b
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Crumb base application file.
+ *
+ * $Horde$
+ *
+ * Copyright 2008 The Horde Project <http://www.horde.org>
+ *
+ * This file brings in all of the dependencies that every Crumb script will
+ * need, and sets up objects that all scripts use.
+ *
+ * @author Ben Klang <ben@alkaloid.net>
+ * 
+ */
+
+// Check for a prior definition of HORDE_BASE (perhaps by an auto_prepend_file
+// definition for site customization).
+if (!defined('HORDE_BASE')) {
+    @define('HORDE_BASE', dirname(__FILE__) . '/../..');
+}
+
+// Load the Horde Framework core, and set up inclusion paths.
+require_once HORDE_BASE . '/lib/core.php';
+
+// Registry.
+$registry = &Registry::singleton();
+if (is_a(($pushed = $registry->pushApp('crumb', !defined('AUTH_HANDLER'))), 'PEAR_Error')) {
+    if ($pushed->getCode() == 'permission_denied') {
+        Horde::authenticationFailureRedirect();
+    }
+    Horde::fatal($pushed, __FILE__, __LINE__, false);
+}
+$conf = &$GLOBALS['conf'];
+@define('CRUMB_TEMPLATES', $registry->get('templates'));
+
+// Notification system.
+$notification = &Notification::singleton();
+$notification->attach('status');
+
+// Define the base file path of Crumb.
+@define('CRUMB_BASE', dirname(__FILE__) . '/..');
+
+// Crumb base library
+require_once CRUMB_BASE . '/lib/Crumb.php';
+
+// Crumb driver
+require_once CRUMB_BASE . '/lib/Driver.php';
+$crumb_driver = Crumb_Driver::factory();
+
+// Start output compression.
+Horde::compressOutput();
diff --git a/crumb/lib/version.php b/crumb/lib/version.php
new file mode 100644 (file)
index 0000000..a283b1c
--- /dev/null
@@ -0,0 +1 @@
+<?php define('SKELETON_VERSION', '0.1-cvs') ?>
\ No newline at end of file
diff --git a/crumb/listclients.php b/crumb/listclients.php
new file mode 100644 (file)
index 0000000..f7a3258
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+/**
+ * $Horde$
+ *
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Ben Klang <ben@alkaloid.net>
+ */
+
+@define('CRUMB_BASE', dirname(__FILE__));
+require_once CRUMB_BASE . '/lib/base.php';
+
+$clients = $crumb_driver->listClients();
+
+$title = _("List");
+
+require CRUMB_TEMPLATES . '/common-header.inc';
+require CRUMB_TEMPLATES . '/menu.inc';
+print_r($clients);
+require $registry->get('templates', 'horde') . '/common-footer.inc';
diff --git a/crumb/locale/en_US/help.xml b/crumb/locale/en_US/help.xml
new file mode 100644 (file)
index 0000000..42bfcf3
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version='1.0'?>
+<!-- $Horde: skeleton/locale/en_US/help.xml,v 1.1 2003/11/03 03:01:38 jwm Exp $ -->
+<help>
+
+<entry id="skeleton-overview">
+    <title>Skeleton Overview</title>
+
+    <heading>What is Skeleton?</heading>
+    <para>Use this module as a blank base for Horde appliations. All of the
+    files in this module are to be used as examples. Please customize them
+    for your own application's requirements.</para>
+</entry>
+
+</help>
diff --git a/crumb/po/.cvsignore b/crumb/po/.cvsignore
new file mode 100644 (file)
index 0000000..fd8854c
--- /dev/null
@@ -0,0 +1 @@
+messages.po
diff --git a/crumb/po/README b/crumb/po/README
new file mode 100644 (file)
index 0000000..a985e94
--- /dev/null
@@ -0,0 +1 @@
+see horde/po/README
diff --git a/crumb/scripts/sql/crumb.sql b/crumb/scripts/sql/crumb.sql
new file mode 100644 (file)
index 0000000..24024a9
--- /dev/null
@@ -0,0 +1,12 @@
+--
+-- $Horde$
+--
+CREATE TABLE crumb_clients (
+    client_id         INT NOT NULL,
+    turba_uid         VARCHAR(255),
+    whups_queue       INT,
+    group_id          VARCHAR(255),
+
+    PRIMARY KEY       (client_id)
+);
+
diff --git a/crumb/templates/common-header.inc b/crumb/templates/common-header.inc
new file mode 100644 (file)
index 0000000..248520c
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+if (isset($language)) {
+    header('Content-type: text/html; charset=' . NLS::getCharset());
+    header('Vary: Accept-Language');
+}
+?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
+<!-- Skeleton: Copyright 2004-2008 The Horde Project.  Skeleton is under a Horde license. -->
+<!--     Horde Project: http://www.horde.org/ | Skeleton: http://www.horde.org/horde/     -->
+<!--                 Horde Licenses: http://www.horde.org/licenses/                       -->
+<?php echo !empty($language) ? '<html lang="' . strtr($language, '_', '-') . '">' : '<html>' ?>
+<head>
+<?php
+
+$page_title = $registry->get('name');
+if (!empty($title)) $page_title .= ' :: ' . $title;
+if (!empty($refresh_time) && ($refresh_time > 0) && !empty($refresh_url)) {
+    echo "<meta http-equiv=\"refresh\" content=\"$refresh_time;url=$refresh_url\">\n";
+}
+
+Horde::includeScriptFiles();
+
+?>
+<title><?php echo htmlspecialchars($page_title) ?></title>
+<link href="<?php echo $GLOBALS['registry']->getImageDir()?>/favicon.ico" rel="SHORTCUT ICON" />
+<?php echo Horde::stylesheetLink('skeleton') ?>
+</head>
+
+<body<?php if ($bc = Util::nonInputVar('bodyClass')) echo ' class="' . $bc . '"' ?><?php if ($bi = Util::nonInputVar('bodyId')) echo ' id="' . $bi . '"'; ?>>
diff --git a/crumb/templates/menu.inc b/crumb/templates/menu.inc
new file mode 100644 (file)
index 0000000..97ab6d9
--- /dev/null
@@ -0,0 +1,4 @@
+<div id="menu">
+ <?php echo Crumb::getMenu('string') ?>
+</div>
+<?php $GLOBALS['notification']->notify(array('listeners' => 'status')) ?>
diff --git a/crumb/themes/screen.css b/crumb/themes/screen.css
new file mode 100644 (file)
index 0000000..c7b7e4f
--- /dev/null
@@ -0,0 +1,3 @@
+/**
+ * $Horde: skeleton/themes/screen.css,v 1.1 2004/11/25 02:22:34 chuck Exp $
+ */
diff --git a/framework/Horde_Date_Parser/chronic/History.txt b/framework/Horde_Date_Parser/chronic/History.txt
new file mode 100644 (file)
index 0000000..0f23646
--- /dev/null
@@ -0,0 +1,53 @@
+= 0.2.3
+
+* fixed 12am/12pm (by Nicholas Schlueter)
+
+= 0.2.2
+
+* added missing files (damn you manifest)
+
+= 0.2.1
+
+* fixed time overflow issue
+* implemented "next" for minute repeater
+* generalized time dealiasing to dealias regardless of day portion and time position
+* added additional token match for cases like "friday evening at 7" and "tomorrow evening at 7"
+* added support for Time#to_s output format: "Mon Apr 02 17:00:00 PDT 2007"
+
+= 0.2.0 2007-03-20
+
+* implemented numerizer, allowing the use of number words (e.g. five weeks ago) (by shalev)
+
+= 0.1.6 2006-01-15
+
+* added 'weekend' support (by eventualbuddha)
+
+= 0.1.5 2006-12-20
+
+* fixed 'aug 20' returning next year if current month is august
+* modified behavior of 'from now'
+* added support for seconds on times, and thus db timestamp format: "2006-12-20 18:04:23"
+* made Hoe compliant
+
+= 0.1.4
+
+* removed verbose error checking code. oops. :-/
+
+= 0.1.3
+
+* improved regexes for word variations (by Josh Goebel)
+* fixed a bug that caused "today at 3am" to return nil if current time is after 3am
+
+= 0.1.2
+
+* removed Date dependency (now works on windows properly without fiddling)
+
+= 0.1.1
+
+* run to_s on incoming object
+* fixed loop loading of repeaters files (out of order on some machines)
+* fixed find_within to use this instead of next (was breaking "today at 6pm")
+
+= 0.1.0
+
+* initial release
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/README.txt b/framework/Horde_Date_Parser/chronic/README.txt
new file mode 100644 (file)
index 0000000..2e4f179
--- /dev/null
@@ -0,0 +1,149 @@
+Chronic
+       http://chronic.rubyforge.org/
+       by Tom Preston-Werner
+
+== DESCRIPTION:
+
+Chronic is a natural language date/time parser written in pure Ruby. See below for the wide variety of formats Chronic will parse.
+
+== INSTALLATION:
+
+Chronic can be installed via RubyGems:
+
+  $ sudo gem install chronic
+
+== USAGE:
+
+You can parse strings containing a natural language date using the Chronic.parse method.
+
+  require 'rubygems'
+  require 'chronic'
+
+  Time.now   #=> Sun Aug 27 23:18:25 PDT 2006
+
+  #---
+
+  Chronic.parse('tomorrow')       
+    #=> Mon Aug 28 12:00:00 PDT 2006
+
+  Chronic.parse('monday', :context => :past)
+    #=> Mon Aug 21 12:00:00 PDT 2006
+
+  Chronic.parse('this tuesday 5:00')
+    #=> Tue Aug 29 17:00:00 PDT 2006
+
+  Chronic.parse('this tuesday 5:00', :ambiguous_time_range => :none)
+    #=> Tue Aug 29 05:00:00 PDT 2006
+
+  Chronic.parse('may 27th', :now => Time.local(2000, 1, 1))
+    #=> Sat May 27 12:00:00 PDT 2000
+
+  Chronic.parse('may 27th', :guess => false)
+    #=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007
+
+See Chronic.parse for detailed usage instructions.
+
+== EXAMPLES:
+
+Chronic can parse a huge variety of date and time formats. Following is a small sample of strings that will be properly parsed. Parsing is case insensitive and will handle common abbreviations and misspellings.
+
+Simple
+
+  thursday
+  november
+  summer
+  friday 13:00
+  mon 2:35
+  4pm
+  6 in the morning
+  friday 1pm
+  sat 7 in the evening
+  yesterday
+  today
+  tomorrow
+  this tuesday
+  next month
+  last winter
+  this morning
+  last night
+  this second
+  yesterday at 4:00
+  last friday at 20:00
+  last week tuesday
+  tomorrow at 6:45pm
+  afternoon yesterday
+  thursday last week
+
+Complex
+
+  3 years ago
+  5 months before now
+  7 hours ago
+  7 days from now
+  1 week hence
+  in 3 hours
+  1 year ago tomorrow
+  3 months ago saturday at 5:00 pm
+  7 hours before tomorrow at noon
+  3rd wednesday in november
+  3rd month next year
+  3rd thursday this september
+  4th day last week
+
+Specific Dates
+
+  January 5
+  dec 25
+  may 27th
+  October 2006
+  oct 06
+  jan 3 2010
+  february 14, 2004
+  3 jan 2000
+  17 april 85
+  5/27/1979
+  27/5/1979
+  05/06
+  1979-05-27
+  Friday
+  5
+  4:00
+  17:00
+  0800
+
+Specific Times (many of the above with an added time)
+
+  January 5 at 7pm
+  1979-05-27 05:00:00
+  etc
+
+== LIMITATIONS:
+  
+Chronic uses Ruby's built in Time class for all time storage and computation. Because of this, only times that the Time class can handle will be properly parsed. Parsing for times outside of this range will simply return nil. Support for a wider range of times is planned for a future release.
+
+Time zones other than the local one are not currently supported. Support for other time zones is planned for a future release.
+
+== LICENSE:
+
+(The MIT License)
+
+Copyright (c) 2006 Ryan Davis, Zen Spider Software
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic.rb b/framework/Horde_Date_Parser/chronic/lib/chronic.rb
new file mode 100644 (file)
index 0000000..6d0e7ba
--- /dev/null
@@ -0,0 +1,125 @@
+#=============================================================================
+#
+#  Name:       Chronic
+#  Author:     Tom Preston-Werner
+#  Purpose:    Parse natural language dates and times into Time or
+#              Chronic::Span objects
+#
+#=============================================================================
+
+$:.unshift File.dirname(__FILE__)     # For use/testing when no gem is installed
+
+require 'chronic/chronic'
+require 'chronic/handlers'
+
+require 'chronic/repeater'
+require 'chronic/repeaters/repeater_year'
+require 'chronic/repeaters/repeater_season'
+require 'chronic/repeaters/repeater_season_name'
+require 'chronic/repeaters/repeater_month'
+require 'chronic/repeaters/repeater_month_name'
+require 'chronic/repeaters/repeater_fortnight'
+require 'chronic/repeaters/repeater_week'
+require 'chronic/repeaters/repeater_weekend'
+require 'chronic/repeaters/repeater_day'
+require 'chronic/repeaters/repeater_day_name'
+require 'chronic/repeaters/repeater_day_portion'
+require 'chronic/repeaters/repeater_hour'
+require 'chronic/repeaters/repeater_minute'
+require 'chronic/repeaters/repeater_second'
+require 'chronic/repeaters/repeater_time'
+
+require 'chronic/grabber'
+require 'chronic/pointer'
+require 'chronic/scalar'
+require 'chronic/ordinal'
+require 'chronic/separator'
+require 'chronic/time_zone'
+
+require 'numerizer/numerizer'
+
+module Chronic
+  VERSION = "0.2.3"
+  
+  def self.debug; false; end
+end
+
+alias p_orig p
+
+def p(val)
+  p_orig val
+  puts
+end
+
+# class Time
+#   def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
+#     # extra_seconds = second > 60 ? second - 60 : 0
+#     # extra_minutes = minute > 59 ? minute - 59 : 0
+#     # extra_hours = hour > 23 ? hour - 23 : 0
+#     # extra_days = day > 
+#     
+#     if month > 12
+#       if month % 12 == 0
+#         year += (month - 12) / 12
+#         month = 12
+#       else
+#         year += month / 12
+#         month = month % 12
+#       end
+#     end
+#     
+#     base = Time.local(year, month)
+#     puts base
+#     offset = ((day - 1) * 24 * 60 * 60) + (hour * 60 * 60) + (minute * 60) + second
+#     puts offset.to_s
+#     date = base + offset
+#     puts date
+#     date
+#   end
+# end
+
+class Time
+  def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
+    if second >= 60
+      minute += second / 60
+      second = second % 60
+    end
+    
+    if minute >= 60
+      hour += minute / 60
+      minute = minute % 60
+    end
+    
+    if hour >= 24
+      day += hour / 24
+      hour = hour % 24
+    end
+    
+    # determine if there is a day overflow. this is complicated by our crappy calendar
+    # system (non-constant number of days per month)
+    day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
+    if day > 28
+      # no month ever has fewer than 28 days, so only do this if necessary
+      leap_year = (year % 4 == 0) && !(year % 100 == 0)
+      leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+      common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+      days_this_month = leap_year ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
+      if day > days_this_month
+        month += day / days_this_month
+        day = day % days_this_month
+      end
+    end
+    
+    if month > 12
+      if month % 12 == 0
+        year += (month - 12) / 12
+        month = 12
+      else
+        year += month / 12
+        month = month % 12
+      end
+    end
+    
+    Time.local(year, month, day, hour, minute, second)
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/chronic.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/chronic.rb
new file mode 100644 (file)
index 0000000..5e7779f
--- /dev/null
@@ -0,0 +1,239 @@
+module Chronic
+  class << self
+    
+    # Parses a string containing a natural language date or time. If the parser
+    # can find a date or time, either a Time or Chronic::Span will be returned 
+    # (depending on the value of <tt>:guess</tt>). If no date or time can be found,
+    # +nil+ will be returned.
+    #
+    # Options are:
+    #
+    # [<tt>:context</tt>]
+    #     <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
+    #
+    #     If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt> 
+    #     and if an ambiguous string is given, it will assume it is in the 
+    #     past. Specify <tt>:future</tt> or omit to set a future context.
+    #
+    # [<tt>:now</tt>]
+    #     Time (defaults to Time.now)
+    #
+    #     By setting <tt>:now</tt> to a Time, all computations will be based off
+    #     of that time instead of Time.now
+    #
+    # [<tt>:guess</tt>]
+    #     +true+ or +false+ (defaults to +true+)
+    #
+    #     By default, the parser will guess a single point in time for the
+    #     given date or time. If you'd rather have the entire time span returned,
+    #     set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
+    #     
+    # [<tt>:ambiguous_time_range</tt>]
+    #     Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
+    #
+    #     If an Integer is given, ambiguous times (like 5:00) will be 
+    #     assumed to be within the range of that time in the AM to that time
+    #     in the PM. For example, if you set it to <tt>7</tt>, then the parser will
+    #     look for the time between 7am and 7pm. In the case of 5:00, it would
+    #     assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
+    #     will be made, and the first matching instance of that time will 
+    #     be used.
+    def parse(text, specified_options = {})
+      # get options and set defaults if necessary
+      default_options = {:context => :future,
+                         :now => Time.now,
+                         :guess => true,
+                         :ambiguous_time_range => 6}
+      options = default_options.merge specified_options
+            
+      # ensure the specified options are valid
+      specified_options.keys.each do |key|
+        default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
+      end
+      [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
+      
+      # store now for later =)
+      @now = options[:now]
+      
+      # put the text into a normal format to ease scanning
+      text = self.pre_normalize(text)
+          
+      # get base tokens for each word
+      @tokens = self.base_tokenize(text)
+    
+      # scan the tokens with each token scanner
+      [Repeater].each do |tokenizer|
+        @tokens = tokenizer.scan(@tokens, options)
+      end
+      
+      [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer|
+        @tokens = tokenizer.scan(@tokens)
+      end
+      
+      # strip any non-tagged tokens
+      @tokens = @tokens.select { |token| token.tagged? }
+      
+      if Chronic.debug
+        puts "+---------------------------------------------------"
+        puts "| " + @tokens.to_s
+        puts "+---------------------------------------------------"
+      end
+      
+      # do the heavy lifting
+      begin
+        span = self.tokens_to_span(@tokens, options)
+      rescue
+        raise
+        return nil
+      end
+      
+      # guess a time within a span if required
+      if options[:guess]
+        return self.guess(span)
+      else
+        return span
+      end
+    end
+    
+    # Clean up the specified input text by stripping unwanted characters,
+    # converting idioms to their canonical form, converting number words
+    # to numbers (three => 3), and converting ordinal words to numeric
+    # ordinals (third => 3rd)
+    def pre_normalize(text) #:nodoc:
+      normalized_text = text.to_s.downcase
+      normalized_text = numericize_numbers(normalized_text)
+      normalized_text.gsub!(/['"\.]/, '')
+      normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
+      normalized_text.gsub!(/\btoday\b/, 'this day')
+      normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day')
+      normalized_text.gsub!(/\byesterday\b/, 'last day')
+      normalized_text.gsub!(/\bnoon\b/, '12:00')
+      normalized_text.gsub!(/\bmidnight\b/, '24:00')
+      normalized_text.gsub!(/\bbefore now\b/, 'past')
+      normalized_text.gsub!(/\bnow\b/, 'this second')
+      normalized_text.gsub!(/\b(ago|before)\b/, 'past')
+      normalized_text.gsub!(/\bthis past\b/, 'last')
+      normalized_text.gsub!(/\bthis last\b/, 'last')
+      normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
+      normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
+      normalized_text.gsub!(/\btonight\b/, 'this night')
+      normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1')
+      normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
+      normalized_text = numericize_ordinals(normalized_text)
+    end
+  
+    # Convert number words to numbers (three => 3)
+    def numericize_numbers(text) #:nodoc:
+      Numerizer.numerize(text)
+    end
+  
+    # Convert ordinal words to numeric ordinals (third => 3rd)
+    def numericize_ordinals(text) #:nodoc:
+      text
+    end
+  
+    # Split the text on spaces and convert each word into
+    # a Token
+    def base_tokenize(text) #:nodoc:
+      text.split(' ').map { |word| Token.new(word) }
+    end
+    
+    # Guess a specific time within the given span
+    def guess(span) #:nodoc:
+      return nil if span.nil?
+      if span.width > 1
+        span.begin + (span.width / 2)
+      else
+        span.begin
+      end
+    end
+  end
+  
+  class Token #:nodoc:
+    attr_accessor :word, :tags
+    
+    def initialize(word)
+      @word = word
+      @tags = []
+    end
+    
+    # Tag this token with the specified tag
+    def tag(new_tag)
+      @tags << new_tag
+    end
+    
+    # Remove all tags of the given class
+    def untag(tag_class)
+      @tags = @tags.select { |m| !m.kind_of? tag_class }
+    end
+    
+    # Return true if this token has any tags
+    def tagged?
+      @tags.size > 0
+    end
+    
+    # Return the Tag that matches the given class
+    def get_tag(tag_class)
+      matches = @tags.select { |m| m.kind_of? tag_class }
+      #matches.size < 2 || raise("Multiple identical tags found")
+      return matches.first
+    end
+    
+    # Print this Token in a pretty way
+    def to_s
+      @word << '(' << @tags.join(', ') << ') '
+    end
+  end
+  
+  # A Span represents a range of time. Since this class extends
+  # Range, you can use #begin and #end to get the beginning and
+  # ending times of the span (they will be of class Time)
+  class Span < Range   
+    # Returns the width of this span in seconds   
+    def width
+      (self.end - self.begin).to_i
+    end
+    
+    # Add a number of seconds to this span, returning the 
+    # resulting Span
+    def +(seconds)
+      Span.new(self.begin + seconds, self.end + seconds)
+    end
+    
+    # Subtract a number of seconds to this span, returning the 
+    # resulting Span
+    def -(seconds)
+      self + -seconds
+    end
+    
+    # Prints this span in a nice fashion
+    def to_s
+      '(' << self.begin.to_s << '..' << self.end.to_s << ')'
+    end
+  end
+
+  # Tokens are tagged with subclassed instances of this class when
+  # they match specific criteria
+  class Tag #:nodoc:
+    attr_accessor :type
+    
+    def initialize(type)
+      @type = type
+    end
+    
+    def start=(s)
+      @now = s
+    end
+  end
+  
+  # Internal exception
+  class ChronicPain < Exception #:nodoc:
+    
+  end
+  
+  # This exception is raised if an invalid argument is provided to
+  # any of Chronic's methods
+  class InvalidArgumentException < Exception
+    
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/grabber.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/grabber.rb
new file mode 100644 (file)
index 0000000..4162a26
--- /dev/null
@@ -0,0 +1,26 @@
+#module Chronic
+
+  class Chronic::Grabber < Chronic::Tag #:nodoc:
+    def self.scan(tokens)
+      tokens.each_index do |i|
+        if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end
+      end
+      tokens
+    end
+    
+    def self.scan_for_all(token)
+      scanner = {/last/ => :last,
+                 /this/ => :this,
+                 /next/ => :next}
+      scanner.keys.each do |scanner_item|
+        return self.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def to_s
+      'grabber-' << @type.to_s
+    end
+  end
+
+#end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/handlers.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/handlers.rb
new file mode 100644 (file)
index 0000000..551d632
--- /dev/null
@@ -0,0 +1,469 @@
+module Chronic
+
+       class << self
+         
+         def definitions #:nodoc:
+           @definitions ||= 
+      {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
+        
+       :date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
+                 Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
+                 Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
+                 Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
+                 Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
+                 Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
+                 Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
+                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
+                 Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
+                 Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
+                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
+                 
+       # tonight at 7pm
+       :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
+                   Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
+                   Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
+                   
+       # 3 weeks from now, in 2 months
+       :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
+                  Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
+                  Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
+                  
+       # 3rd week in march
+       :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
+                   Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
+      }
+    end
+    
+    def tokens_to_span(tokens, options) #:nodoc:                   
+      # maybe it's a specific date
+      
+      self.definitions[:date].each do |handler|
+        if handler.match(tokens, self.definitions)
+          puts "-date" if Chronic.debug
+          good_tokens = tokens.select { |o| !o.get_tag Separator }
+          return self.send(handler.handler_method, good_tokens, options)
+        end
+      end
+            
+      # I guess it's not a specific date, maybe it's just an anchor
+            
+      self.definitions[:anchor].each do |handler|
+        if handler.match(tokens, self.definitions)
+          puts "-anchor" if Chronic.debug
+          good_tokens = tokens.select { |o| !o.get_tag Separator }
+          return self.send(handler.handler_method, good_tokens, options)
+        end
+      end
+            
+      # not an anchor, perhaps it's an arrow
+      
+      self.definitions[:arrow].each do |handler|
+        if handler.match(tokens, self.definitions)
+          puts "-arrow" if Chronic.debug
+          good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
+          return self.send(handler.handler_method, good_tokens, options)
+        end
+      end
+      
+      # not an arrow, let's hope it's a narrow
+      
+      self.definitions[:narrow].each do |handler|
+        if handler.match(tokens, self.definitions)
+          puts "-narrow" if Chronic.debug
+          #good_tokens = tokens.select { |o| !o.get_tag Separator }
+          return self.send(handler.handler_method, tokens, options)
+        end
+      end
+      
+      # I guess you're out of luck!
+      puts "-none" if Chronic.debug
+      return nil
+    end
+    
+    #--------------
+    
+    def day_or_time(day_start, time_tokens, options)
+      outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
+      
+      if !time_tokens.empty?
+        @now = outer_span.begin
+        time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
+        return time
+      else
+        return outer_span
+      end
+    end
+    
+    #--------------
+    
+    def handle_m_d(month, day, time_tokens, options) #:nodoc:
+      month.start = @now
+      span = month.this(options[:context])
+      
+      day_start = Time.local(span.begin.year, span.begin.month, day)
+      
+      day_or_time(day_start, time_tokens, options)
+    end
+    
+    def handle_rmn_sd(tokens, options) #:nodoc:
+      handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options)
+    end
+    
+    def handle_rmn_od(tokens, options) #:nodoc:
+      handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options)
+    end
+    
+    def handle_rmn_sy(tokens, options) #:nodoc:
+      month = tokens[0].get_tag(RepeaterMonthName).index
+      year = tokens[1].get_tag(ScalarYear).type
+      
+      if month == 12
+        next_month_year = year + 1
+        next_month_month = 1
+      else
+        next_month_year = year
+        next_month_month = month + 1
+      end
+      
+      begin
+        Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
+      rescue ArgumentError
+        nil
+      end
+    end
+    
+    def handle_rdn_rmn_sd_t_tz_sy(tokens, options) #:nodoc:
+      month = tokens[1].get_tag(RepeaterMonthName).index
+      day = tokens[2].get_tag(ScalarDay).type
+      year = tokens[5].get_tag(ScalarYear).type
+      
+      begin
+        day_start = Time.local(year, month, day)
+        day_or_time(day_start, [tokens[3]], options)
+      rescue ArgumentError
+        nil
+      end
+    end
+    
+    def handle_rmn_sd_sy(tokens, options) #:nodoc:
+      month = tokens[0].get_tag(RepeaterMonthName).index
+      day = tokens[1].get_tag(ScalarDay).type
+      year = tokens[2].get_tag(ScalarYear).type
+      
+      time_tokens = tokens.last(tokens.size - 3)
+      
+      begin
+        day_start = Time.local(year, month, day)
+        day_or_time(day_start, time_tokens, options)
+      rescue ArgumentError
+        nil
+      end
+    end
+    
+    def handle_sd_rmn_sy(tokens, options) #:nodoc:
+      new_tokens = [tokens[1], tokens[0], tokens[2]]
+      time_tokens = tokens.last(tokens.size - 3)
+      self.handle_rmn_sd_sy(new_tokens + time_tokens, options)
+    end
+    
+    def handle_sm_sd_sy(tokens, options) #:nodoc:
+      month = tokens[0].get_tag(ScalarMonth).type
+      day = tokens[1].get_tag(ScalarDay).type
+      year = tokens[2].get_tag(ScalarYear).type
+      
+      time_tokens = tokens.last(tokens.size - 3)
+      
+      begin
+        day_start = Time.local(year, month, day) #:nodoc:
+        day_or_time(day_start, time_tokens, options)
+      rescue ArgumentError
+        nil
+      end
+    end
+    
+    def handle_sd_sm_sy(tokens, options) #:nodoc:
+      new_tokens = [tokens[1], tokens[0], tokens[2]]
+      time_tokens = tokens.last(tokens.size - 3)
+      self.handle_sm_sd_sy(new_tokens + time_tokens, options)
+    end
+    
+    def handle_sy_sm_sd(tokens, options) #:nodoc:
+      new_tokens = [tokens[1], tokens[2], tokens[0]]
+      time_tokens = tokens.last(tokens.size - 3)
+      self.handle_sm_sd_sy(new_tokens + time_tokens, options)
+    end
+    
+    def handle_sm_sy(tokens, options) #:nodoc:
+      month = tokens[0].get_tag(ScalarMonth).type
+      year = tokens[1].get_tag(ScalarYear).type
+      
+      if month == 12
+        next_month_year = year + 1
+        next_month_month = 1
+      else
+        next_month_year = year
+        next_month_month = month + 1
+      end
+      
+      begin
+        Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
+      rescue ArgumentError
+        nil
+      end
+    end
+    
+    # anchors
+    
+    def handle_r(tokens, options) #:nodoc:
+      dd_tokens = dealias_and_disambiguate_times(tokens, options)
+      self.get_anchor(dd_tokens, options)
+    end
+    
+    def handle_r_g_r(tokens, options) #:nodoc:
+      new_tokens = [tokens[1], tokens[0], tokens[2]]
+      self.handle_r(new_tokens, options)
+    end
+    
+    # arrows
+    
+    def handle_srp(tokens, span, options) #:nodoc:
+      distance = tokens[0].get_tag(Scalar).type
+      repeater = tokens[1].get_tag(Repeater)
+      pointer = tokens[2].get_tag(Pointer).type
+      
+      repeater.offset(span, distance, pointer)
+    end
+    
+    def handle_s_r_p(tokens, options) #:nodoc:
+      repeater = tokens[1].get_tag(Repeater)
+            
+      # span = 
+      # case true
+      # when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class)
+      #   self.parse("this hour", :guess => false, :now => @now)
+      # when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class)
+      #   self.parse("this minute", :guess => false, :now => @now)
+      # when [RepeaterMinute, RepeaterSecond].include?(repeater.class)
+      #   self.parse("this second", :guess => false, :now => @now)
+      # else
+      #   raise(ChronicPain, "Invalid repeater: #{repeater.class}")
+      # end
+      
+      span = self.parse("this second", :guess => false, :now => @now)
+      
+      self.handle_srp(tokens, span, options)
+    end
+    
+    def handle_p_s_r(tokens, options) #:nodoc:
+      new_tokens = [tokens[1], tokens[2], tokens[0]]
+      self.handle_s_r_p(new_tokens, options)
+    end
+    
+    def handle_s_r_p_a(tokens, options) #:nodoc:
+      anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
+      self.handle_srp(tokens, anchor_span, options)
+    end
+    
+    # narrows
+    
+    def handle_orr(tokens, outer_span, options) #:nodoc:
+      repeater = tokens[1].get_tag(Repeater)
+      repeater.start = outer_span.begin - 1
+      ordinal = tokens[0].get_tag(Ordinal).type
+      span = nil
+      ordinal.times do
+        span = repeater.next(:future)
+        if span.begin > outer_span.end
+          span = nil
+          break
+        end
+      end
+      span
+    end
+    
+    def handle_o_r_s_r(tokens, options) #:nodoc:
+      outer_span = get_anchor([tokens[3]], options)
+      handle_orr(tokens[0..1], outer_span, options)
+    end
+    
+    def handle_o_r_g_r(tokens, options) #:nodoc:
+      outer_span = get_anchor(tokens[2..3], options)
+      handle_orr(tokens[0..1], outer_span, options)
+    end
+    
+    # support methods
+    
+    def get_anchor(tokens, options) #:nodoc:
+      grabber = Grabber.new(:this)
+      pointer = :future
+      
+      repeaters = self.get_repeaters(tokens)
+      repeaters.size.times { tokens.pop }
+                    
+      if tokens.first && tokens.first.get_tag(Grabber)
+        grabber = tokens.first.get_tag(Grabber)
+        tokens.pop
+      end
+      
+      head = repeaters.shift
+      head.start = @now
+                  
+      case grabber.type
+        when :last
+          outer_span = head.next(:past)
+        when :this
+          if repeaters.size > 0
+            outer_span = head.this(:none)
+          else
+            outer_span = head.this(options[:context])
+          end
+        when :next
+          outer_span = head.next(:future)
+        else raise(ChronicPain, "Invalid grabber")
+      end
+      
+      puts "--#{outer_span}" if Chronic.debug
+      anchor = find_within(repeaters, outer_span, pointer)
+    end
+    
+    def get_repeaters(tokens) #:nodoc:
+      repeaters = []
+                 tokens.each do |token|
+                   if t = token.get_tag(Repeater)
+          repeaters << t
+        end
+      end
+      repeaters.sort.reverse
+    end
+    
+    # Recursively finds repeaters within other repeaters.
+    # Returns a Span representing the innermost time span
+    # or nil if no repeater union could be found
+    def find_within(tags, span, pointer) #:nodoc:
+      puts "--#{span}" if Chronic.debug
+      return span if tags.empty?
+      
+      head, *rest = tags
+      head.start = pointer == :future ? span.begin : span.end
+      h = head.this(:none)
+            
+      if span.include?(h.begin) || span.include?(h.end)
+        return find_within(rest, h, pointer)
+      else
+        return nil
+      end
+    end
+    
+    def dealias_and_disambiguate_times(tokens, options) #:nodoc:
+      # handle aliases of am/pm
+      # 5:00 in the morning -> 5:00 am
+      # 7:00 in the evening -> 7:00 pm
+      
+      day_portion_index = nil
+      tokens.each_with_index do |t, i|
+        if t.get_tag(RepeaterDayPortion)
+          day_portion_index = i
+          break
+        end
+      end
+       
+      time_index = nil
+      tokens.each_with_index do |t, i|
+        if t.get_tag(RepeaterTime)
+          time_index = i
+          break
+        end
+      end
+      
+      if (day_portion_index && time_index)
+        t1 = tokens[day_portion_index]
+        t1tag = t1.get_tag(RepeaterDayPortion)
+      
+        if [:morning].include?(t1tag.type)
+          puts '--morning->am' if Chronic.debug
+          t1.untag(RepeaterDayPortion)
+          t1.tag(RepeaterDayPortion.new(:am))
+        elsif [:afternoon, :evening, :night].include?(t1tag.type)
+          puts "--#{t1tag.type}->pm" if Chronic.debug
+          t1.untag(RepeaterDayPortion)
+          t1.tag(RepeaterDayPortion.new(:pm))
+        end
+      end
+      
+      # tokens.each_with_index do |t0, i|
+      #   t1 = tokens[i + 1]
+      #   if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime)
+      #     if [:morning].include?(t1tag.type)
+      #       puts '--morning->am' if Chronic.debug
+      #       t1.untag(RepeaterDayPortion)
+      #       t1.tag(RepeaterDayPortion.new(:am))
+      #     elsif [:afternoon, :evening, :night].include?(t1tag.type)
+      #       puts "--#{t1tag.type}->pm" if Chronic.debug
+      #       t1.untag(RepeaterDayPortion)
+      #       t1.tag(RepeaterDayPortion.new(:pm))
+      #     end
+      #   end
+      # end
+            
+      # handle ambiguous times if :ambiguous_time_range is specified
+      if options[:ambiguous_time_range] != :none
+        ttokens = []
+        tokens.each_with_index do |t0, i|
+          ttokens << t0
+          t1 = tokens[i + 1]
+          if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion))
+            distoken = Token.new('disambiguator')
+            distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
+            ttokens << distoken
+          end
+        end
+        tokens = ttokens
+      end
+      
+      tokens
+    end
+    
+  end
+  
+  class Handler #:nodoc:
+    attr_accessor :pattern, :handler_method
+    
+    def initialize(pattern, handler_method)
+      @pattern = pattern
+      @handler_method = handler_method
+    end
+    
+    def constantize(name)
+      camel = name.to_s.gsub(/(^|_)(.)/) { $2.upcase }
+      ::Chronic.module_eval(camel, __FILE__, __LINE__)
+    end
+    
+    def match(tokens, definitions)
+      token_index = 0
+      @pattern.each do |element|
+        name = element.to_s
+        optional = name.reverse[0..0] == '?'
+        name = name.chop if optional
+        if element.instance_of? Symbol
+          klass = constantize(name)
+          match = tokens[token_index] && !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty?
+          return false if !match && !optional
+          (token_index += 1; next) if match
+          next if !match && optional
+        elsif element.instance_of? String
+          return true if optional && token_index == tokens.size
+          sub_handlers = definitions[name.intern] || raise(ChronicPain, "Invalid subset #{name} specified")
+          sub_handlers.each do |sub_handler|
+            return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
+          end
+          return false
+        else
+          raise(ChronicPain, "Invalid match type: #{element.class}")
+        end
+      end
+      return false if token_index != tokens.size
+      return true
+    end
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/ordinal.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/ordinal.rb
new file mode 100644 (file)
index 0000000..45b8148
--- /dev/null
@@ -0,0 +1,40 @@
+module Chronic
+
+  class Ordinal < Tag #:nodoc:
+    def self.scan(tokens)
+      # for each token
+      tokens.each_index do |i|
+        if t = self.scan_for_ordinals(tokens[i]) then tokens[i].tag(t) end
+        if t = self.scan_for_days(tokens[i]) then tokens[i].tag(t) end
+      end
+      tokens
+    end
+  
+    def self.scan_for_ordinals(token)
+      if token.word =~ /^(\d*)(st|nd|rd|th)$/
+        return Ordinal.new($1.to_i)
+      end
+      return nil
+    end
+    
+    def self.scan_for_days(token)
+      if token.word =~ /^(\d*)(st|nd|rd|th)$/
+        unless $1.to_i > 31
+          return OrdinalDay.new(token.word.to_i)
+        end
+      end
+      return nil
+    end
+    
+    def to_s
+      'ordinal'
+    end
+  end
+  
+  class OrdinalDay < Ordinal #:nodoc:
+    def to_s
+      super << '-day-' << @type.to_s
+    end
+  end
+
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/pointer.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/pointer.rb
new file mode 100644 (file)
index 0000000..224efaf
--- /dev/null
@@ -0,0 +1,27 @@
+module Chronic
+
+  class Pointer < Tag #:nodoc:
+    def self.scan(tokens)
+      # for each token
+      tokens.each_index do |i|
+        if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t) end
+      end
+      tokens
+    end
+  
+    def self.scan_for_all(token)
+      scanner = {/\bpast\b/ => :past,
+                 /\bfuture\b/ => :future,
+                 /\bin\b/ => :future}
+      scanner.keys.each do |scanner_item|
+        return self.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def to_s
+      'pointer-' << @type.to_s
+    end
+  end
+
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeater.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeater.rb
new file mode 100644 (file)
index 0000000..9f80daf
--- /dev/null
@@ -0,0 +1,115 @@
+class Chronic::Repeater < Chronic::Tag #:nodoc:
+  def self.scan(tokens, options)
+    # for each token
+    tokens.each_index do |i|
+      if t = self.scan_for_month_names(tokens[i]) then tokens[i].tag(t); next end
+      if t = self.scan_for_day_names(tokens[i]) then tokens[i].tag(t); next end
+      if t = self.scan_for_day_portions(tokens[i]) then tokens[i].tag(t); next end
+      if t = self.scan_for_times(tokens[i], options) then tokens[i].tag(t); next end
+      if t = self.scan_for_units(tokens[i]) then tokens[i].tag(t); next end
+    end
+    tokens
+  end
+  
+  def self.scan_for_month_names(token)
+    scanner = {/^jan\.?(uary)?$/ => :january,
+               /^feb\.?(ruary)?$/ => :february,
+               /^mar\.?(ch)?$/ => :march,
+               /^apr\.?(il)?$/ => :april,
+               /^may$/ => :may,
+               /^jun\.?e?$/ => :june,
+               /^jul\.?y?$/ => :july,
+               /^aug\.?(ust)?$/ => :august,
+               /^sep\.?(t\.?|tember)?$/ => :september,
+               /^oct\.?(ober)?$/ => :october,
+               /^nov\.?(ember)?$/ => :november,
+               /^dec\.?(ember)?$/ => :december}
+    scanner.keys.each do |scanner_item|
+      return Chronic::RepeaterMonthName.new(scanner[scanner_item]) if scanner_item =~ token.word
+    end
+    return nil
+  end
+  
+  def self.scan_for_day_names(token)
+    scanner = {/^m[ou]n(day)?$/ => :monday,
+               /^t(ue|eu|oo|u|)s(day)?$/ => :tuesday,
+               /^tue$/ => :tuesday,
+               /^we(dnes|nds|nns)day$/ => :wednesday,
+               /^wed$/ => :wednesday,
+               /^th(urs|ers)day$/ => :thursday,
+               /^thu$/ => :thursday,
+               /^fr[iy](day)?$/ => :friday,
+               /^sat(t?[ue]rday)?$/ => :saturday,
+               /^su[nm](day)?$/ => :sunday}
+    scanner.keys.each do |scanner_item|
+      return Chronic::RepeaterDayName.new(scanner[scanner_item]) if scanner_item =~ token.word
+    end
+    return nil
+  end
+  
+  def self.scan_for_day_portions(token)
+    scanner = {/^ams?$/ => :am,
+               /^pms?$/ => :pm,
+               /^mornings?$/ => :morning,
+               /^afternoons?$/ => :afternoon,
+               /^evenings?$/ => :evening,
+               /^(night|nite)s?$/ => :night}
+    scanner.keys.each do |scanner_item|
+      return Chronic::RepeaterDayPortion.new(scanner[scanner_item]) if scanner_item =~ token.word
+    end
+    return nil
+  end
+  
+  def self.scan_for_times(token, options)
+    if token.word =~ /^\d{1,2}(:?\d{2})?([\.:]?\d{2})?$/
+      return Chronic::RepeaterTime.new(token.word, options)
+    end
+    return nil
+  end
+  
+  def self.scan_for_units(token)
+    scanner = {/^years?$/ => :year,
+               /^seasons?$/ => :season,
+               /^months?$/ => :month,
+               /^fortnights?$/ => :fortnight,
+               /^weeks?$/ => :week,
+               /^weekends?$/ => :weekend,
+               /^days?$/ => :day,
+               /^hours?$/ => :hour,
+               /^minutes?$/ => :minute,
+               /^seconds?$/ => :second}
+    scanner.keys.each do |scanner_item|
+      if scanner_item =~ token.word
+        klass_name = 'Chronic::Repeater' + scanner[scanner_item].to_s.capitalize
+        klass = eval(klass_name)
+        return klass.new(scanner[scanner_item]) 
+      end
+    end
+    return nil
+  end
+  
+  def <=>(other)
+    width <=> other.width
+  end
+  
+  # returns the width (in seconds or months) of this repeatable.
+  def width
+    raise("Repeatable#width must be overridden in subclasses")
+  end
+  
+  # returns the next occurance of this repeatable.
+  def next(pointer)
+    !@now.nil? || raise("Start point must be set before calling #next")
+    [:future, :none, :past].include?(pointer) || raise("First argument 'pointer' must be one of :past or :future")
+    #raise("Repeatable#next must be overridden in subclasses")
+  end
+  
+  def this(pointer)
+    !@now.nil? || raise("Start point must be set before calling #this")
+    [:future, :past, :none].include?(pointer) || raise("First argument 'pointer' must be one of :past, :future, :none")
+  end
+  
+  def to_s
+    'repeater'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day.rb
new file mode 100644 (file)
index 0000000..a92d83f
--- /dev/null
@@ -0,0 +1,47 @@
+class Chronic::RepeaterDay < Chronic::Repeater #:nodoc:
+  DAY_SECONDS = 86_400 # (24 * 60 * 60)
+  
+  def next(pointer)
+    super
+    
+    if !@current_day_start
+      @current_day_start = Time.local(@now.year, @now.month, @now.day)
+    end
+    
+    direction = pointer == :future ? 1 : -1
+    @current_day_start += direction * DAY_SECONDS
+    
+    Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      day_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1)
+      day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS
+    when :past
+      day_begin = Time.construct(@now.year, @now.month, @now.day)
+      day_end = Time.construct(@now.year, @now.month, @now.day, @now.hour)
+    when :none
+      day_begin = Time.construct(@now.year, @now.month, @now.day)
+      day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS
+    end
+    
+    Chronic::Span.new(day_begin, day_end)
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * DAY_SECONDS
+  end
+  
+  def width
+    DAY_SECONDS
+  end
+  
+  def to_s
+    super << '-day'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_name.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_name.rb
new file mode 100644 (file)
index 0000000..0486a4d
--- /dev/null
@@ -0,0 +1,46 @@
+class Chronic::RepeaterDayName < Chronic::Repeater #:nodoc:
+  DAY_SECONDS = 86400 # (24 * 60 * 60)
+  
+  def next(pointer)
+    super
+    
+    direction = pointer == :future ? 1 : -1
+    
+    if !@current_day_start
+      @current_day_start = Time.construct(@now.year, @now.month, @now.day)
+      @current_day_start += direction * DAY_SECONDS
+
+      day_num = symbol_to_number(@type)
+      
+      while @current_day_start.wday != day_num
+        @current_day_start += direction * DAY_SECONDS
+      end
+    else
+      @current_day_start += direction * 7 * DAY_SECONDS
+    end
+    
+    Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    pointer = :future if pointer == :none
+    self.next(pointer)
+  end
+  
+  def width
+    DAY_SECONDS
+  end
+  
+  def to_s
+    super << '-dayname-' << @type.to_s
+  end
+  
+  private
+  
+  def symbol_to_number(sym)
+    lookup = {:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6}
+    lookup[sym] || raise("Invalid symbol specified")
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_portion.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_day_portion.rb
new file mode 100644 (file)
index 0000000..c854933
--- /dev/null
@@ -0,0 +1,93 @@
+class Chronic::RepeaterDayPortion < Chronic::Repeater #:nodoc:
+  @@morning = (6 * 60 * 60)..(12 * 60 * 60) # 6am-12am
+  @@afternoon = (13 * 60 * 60)..(17 * 60 * 60) # 1pm-5pm
+  @@evening = (17 * 60 * 60)..(20 * 60 * 60) # 5pm-8pm
+  @@night = (20 * 60 * 60)..(24 * 60 * 60) # 8pm-12pm
+  
+  def initialize(type)
+    super
+    
+    if type.kind_of? Integer
+      @range = (@type * 60 * 60)..((@type + 12) * 60 * 60)
+    else
+      lookup = {:am => 0..(12 * 60 * 60 - 1),
+                :pm => (12 * 60 * 60)..(24 * 60 * 60 - 1),
+                :morning => @@morning,
+                :afternoon => @@afternoon,
+                :evening => @@evening,
+                :night => @@night}
+      @range = lookup[type]
+      lookup[type] || raise("Invalid type '#{type}' for RepeaterDayPortion")
+    end
+    @range || raise("Range should have been set by now")
+  end
+  
+  def next(pointer)
+    super
+    
+    full_day = 60 * 60 * 24
+    
+    if !@current_span
+      now_seconds = @now - Time.construct(@now.year, @now.month, @now.day)
+      if now_seconds < @range.begin
+        case pointer
+        when :future
+          range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin
+        when :past
+          range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin
+        end
+      elsif now_seconds > @range.end
+        case pointer
+        when :future
+          range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin
+        when :past
+          range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin
+        end
+      else
+        case pointer
+        when :future
+          range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin
+        when :past
+          range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin
+        end
+      end
+      
+      @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin))
+    else
+      case pointer
+      when :future
+        @current_span += full_day
+      when :past
+        @current_span -= full_day
+      end
+    end
+  end
+  
+  def this(context = :future)
+    super
+    
+    range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin
+    @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin))
+  end
+  
+  def offset(span, amount, pointer)
+    @now = span.begin
+    portion_span = self.next(pointer)
+    direction = pointer == :future ? 1 : -1
+    portion_span + (direction * (amount - 1) * Chronic::RepeaterDay::DAY_SECONDS)
+  end
+  
+  def width
+    @range || raise("Range has not been set")
+    return @current_span.width if @current_span
+    if @type.kind_of? Integer
+      return (12 * 60 * 60)
+    else
+      @range.end - @range.begin
+    end
+  end
+  
+  def to_s
+    super << '-dayportion-' << @type.to_s
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_fortnight.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_fortnight.rb
new file mode 100644 (file)
index 0000000..058fbb9
--- /dev/null
@@ -0,0 +1,65 @@
+class Chronic::RepeaterFortnight < Chronic::Repeater #:nodoc:
+  FORTNIGHT_SECONDS = 1_209_600 # (14 * 24 * 60 * 60)
+  
+  def next(pointer)
+    super
+    
+    if !@current_fortnight_start
+      case pointer
+      when :future
+        sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+        sunday_repeater.start = @now
+        next_sunday_span = sunday_repeater.next(:future)
+        @current_fortnight_start = next_sunday_span.begin
+      when :past
+        sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+        sunday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS)
+        2.times { sunday_repeater.next(:past) }
+        last_sunday_span = sunday_repeater.next(:past)
+        @current_fortnight_start = last_sunday_span.begin
+      end
+    else
+      direction = pointer == :future ? 1 : -1
+      @current_fortnight_start += direction * FORTNIGHT_SECONDS
+    end
+    
+    Chronic::Span.new(@current_fortnight_start, @current_fortnight_start + FORTNIGHT_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    pointer = :future if pointer == :none
+    
+    case pointer
+    when :future
+      this_fortnight_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS
+      sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+      sunday_repeater.start = @now
+      sunday_repeater.this(:future)
+      this_sunday_span = sunday_repeater.this(:future)
+      this_fortnight_end = this_sunday_span.begin
+      Chronic::Span.new(this_fortnight_start, this_fortnight_end)
+    when :past
+      this_fortnight_end = Time.construct(@now.year, @now.month, @now.day, @now.hour)
+      sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+      sunday_repeater.start = @now
+      last_sunday_span = sunday_repeater.next(:past)
+      this_fortnight_start = last_sunday_span.begin
+      Chronic::Span.new(this_fortnight_start, this_fortnight_end)
+    end
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * FORTNIGHT_SECONDS
+  end
+
+  def width
+    FORTNIGHT_SECONDS
+  end
+  
+  def to_s
+    super << '-fortnight'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_hour.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_hour.rb
new file mode 100644 (file)
index 0000000..f38a3f8
--- /dev/null
@@ -0,0 +1,52 @@
+class Chronic::RepeaterHour < Chronic::Repeater #:nodoc:
+  HOUR_SECONDS = 3600 # 60 * 60
+  
+  def next(pointer)
+    super
+    
+    if !@current_hour_start
+      case pointer
+      when :future
+        @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1)
+      when :past
+        @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour - 1)
+      end
+    else
+      direction = pointer == :future ? 1 : -1
+      @current_hour_start += direction * HOUR_SECONDS
+    end
+    
+    Chronic::Span.new(@current_hour_start, @current_hour_start + HOUR_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1)
+      hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1)
+    when :past
+      hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour)
+      hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
+    when :none
+      hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour)
+      hour_end = hour_begin + HOUR_SECONDS
+    end
+    
+    Chronic::Span.new(hour_start, hour_end)
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * HOUR_SECONDS
+  end
+  
+  def width
+    HOUR_SECONDS
+  end
+  
+  def to_s
+    super << '-hour'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_minute.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_minute.rb
new file mode 100644 (file)
index 0000000..342d3cd
--- /dev/null
@@ -0,0 +1,52 @@
+class Chronic::RepeaterMinute < Chronic::Repeater #:nodoc:
+  MINUTE_SECONDS = 60
+  
+  def next(pointer = :future)
+    super
+    
+    if !@current_minute_start
+      case pointer
+      when :future
+        @current_minute_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1)
+      when :past
+        @current_minute_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min - 1)
+      end
+    else
+      direction = pointer == :future ? 1 : -1
+      @current_minute_start += direction * MINUTE_SECONDS
+    end
+    
+    Chronic::Span.new(@current_minute_start, @current_minute_start + MINUTE_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      minute_begin = @now
+      minute_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
+    when :past
+      minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
+      minute_end = @now
+    when :none
+      minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
+      minute_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + MINUTE_SECONDS
+    end
+    
+    Chronic::Span.new(minute_begin, minute_end)
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * MINUTE_SECONDS
+  end
+  
+  def width
+    MINUTE_SECONDS
+  end
+  
+  def to_s
+    super << '-minute'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month.rb
new file mode 100644 (file)
index 0000000..edd89ee
--- /dev/null
@@ -0,0 +1,61 @@
+class Chronic::RepeaterMonth < Chronic::Repeater #:nodoc:
+  MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
+  YEAR_MONTHS = 12
+  
+  def next(pointer)
+    super
+    
+    if !@current_month_start
+      @current_month_start = offset_by(Time.construct(@now.year, @now.month), 1, pointer)
+    else
+      @current_month_start = offset_by(Time.construct(@current_month_start.year, @current_month_start.month), 1, pointer)
+    end
+    
+    Chronic::Span.new(@current_month_start, Time.construct(@current_month_start.year, @current_month_start.month + 1))
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      month_start = Time.construct(@now.year, @now.month, @now.day + 1)
+      month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future)
+    when :past
+      month_start = Time.construct(@now.year, @now.month)
+      month_end = Time.construct(@now.year, @now.month, @now.day)
+    when :none
+      month_start = Time.construct(@now.year, @now.month)
+      month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future)
+    end
+    
+    Chronic::Span.new(month_start, month_end)
+  end
+  
+  def offset(span, amount, pointer)      
+    Chronic::Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer))
+  end
+  
+  def offset_by(time, amount, pointer) 
+    direction = pointer == :future ? 1 : -1
+    
+    amount_years = direction * amount / YEAR_MONTHS
+    amount_months = direction * amount % YEAR_MONTHS
+    
+    new_year = time.year + amount_years
+    new_month = time.month + amount_months
+    if new_month > YEAR_MONTHS
+      new_year += 1
+      new_month -= YEAR_MONTHS
+    end
+    Time.construct(new_year, new_month, time.day, time.hour, time.min, time.sec)
+  end
+  
+  def width
+    MONTH_SECONDS
+  end
+  
+  def to_s
+    super << '-month'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month_name.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_month_name.rb
new file mode 100644 (file)
index 0000000..1f8b748
--- /dev/null
@@ -0,0 +1,93 @@
+class Chronic::RepeaterMonthName < Chronic::Repeater #:nodoc:
+  MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
+  
+  def next(pointer)
+    super
+    
+    if !@current_month_begin
+      target_month = symbol_to_number(@type)
+      case pointer
+      when :future
+        if @now.month < target_month
+          @current_month_begin = Time.construct(@now.year, target_month)
+        else @now.month > target_month
+          @current_month_begin = Time.construct(@now.year + 1, target_month)
+        end
+      when :none
+        if @now.month <= target_month
+          @current_month_begin = Time.construct(@now.year, target_month)
+        else @now.month > target_month
+          @current_month_begin = Time.construct(@now.year + 1, target_month)
+        end
+      when :past
+        if @now.month > target_month
+          @current_month_begin = Time.construct(@now.year, target_month)
+        else @now.month < target_month
+          @current_month_begin = Time.construct(@now.year - 1, target_month)
+        end
+      end
+      @current_month_begin || raise("Current month should be set by now")
+    else
+      case pointer
+      when :future
+        @current_month_begin = Time.construct(@current_month_begin.year + 1, @current_month_begin.month)
+      when :past
+        @current_month_begin = Time.construct(@current_month_begin.year - 1, @current_month_begin.month)
+      end
+    end
+    
+    cur_month_year = @current_month_begin.year
+    cur_month_month = @current_month_begin.month
+    
+    if cur_month_month == 12
+      next_month_year = cur_month_year + 1
+      next_month_month = 1
+    else
+      next_month_year = cur_month_year
+      next_month_month = cur_month_month + 1
+    end
+      
+    Chronic::Span.new(@current_month_begin, Time.construct(next_month_year, next_month_month))
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :past
+      self.next(pointer)
+    when :future, :none
+      self.next(:none)
+    end
+  end
+  
+  def width
+    MONTH_SECONDS
+  end
+  
+  def index
+    symbol_to_number(@type)
+  end
+  
+  def to_s
+    super << '-monthname-' << @type.to_s
+  end
+  
+  private
+  
+  def symbol_to_number(sym)
+    lookup = {:january => 1,
+              :february => 2,
+              :march => 3,
+              :april => 4,
+              :may => 5,
+              :june => 6,
+              :july => 7,
+              :august => 8,
+              :september => 9,
+              :october => 10,
+              :november => 11,
+              :december => 12}
+    lookup[sym] || raise("Invalid symbol specified")
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season.rb
new file mode 100644 (file)
index 0000000..a255865
--- /dev/null
@@ -0,0 +1,23 @@
+class Chronic::RepeaterSeason < Chronic::Repeater #:nodoc:
+  SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
+  
+  def next(pointer)
+    super
+    
+    raise 'Not implemented'
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    raise 'Not implemented'
+  end
+  
+  def width
+    SEASON_SECONDS
+  end
+  
+  def to_s
+    super << '-season'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season_name.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_season_name.rb
new file mode 100644 (file)
index 0000000..adfd1f2
--- /dev/null
@@ -0,0 +1,24 @@
+class Chronic::RepeaterSeasonName < Chronic::RepeaterSeason #:nodoc:
+  @summer = ['jul 21', 'sep 22']
+  @autumn = ['sep 23', 'dec 21']
+  @winter = ['dec 22', 'mar 19']
+  @spring = ['mar 20', 'jul 20']
+  
+  def next(pointer)
+    super
+    raise 'Not implemented'
+  end
+  
+  def this(pointer = :future)
+    super
+    raise 'Not implemented'
+  end
+  
+  def width
+    (91 * 24 * 60 * 60)
+  end
+  
+  def to_s
+    super << '-season-' << @type.to_s
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_second.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_second.rb
new file mode 100644 (file)
index 0000000..6d05545
--- /dev/null
@@ -0,0 +1,36 @@
+class Chronic::RepeaterSecond < Chronic::Repeater #:nodoc:
+  SECOND_SECONDS = 1 # haha, awesome
+  
+  def next(pointer = :future)
+    super
+    
+    direction = pointer == :future ? 1 : -1
+    
+    if !@second_start
+      @second_start = @now + (direction * SECOND_SECONDS)
+    else
+      @second_start += SECOND_SECONDS * direction
+    end
+    
+    Chronic::Span.new(@second_start, @second_start + SECOND_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    Chronic::Span.new(@now, @now + 1)
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * SECOND_SECONDS
+  end
+  
+  def width
+    SECOND_SECONDS
+  end
+  
+  def to_s
+    super << '-second'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_time.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_time.rb
new file mode 100644 (file)
index 0000000..f856014
--- /dev/null
@@ -0,0 +1,117 @@
+class Chronic::RepeaterTime < Chronic::Repeater #:nodoc:
+  class Tick #:nodoc:
+    attr_accessor :time
+    
+    def initialize(time, ambiguous = false)
+      @time = time
+      @ambiguous = ambiguous
+    end
+    
+    def ambiguous?
+      @ambiguous
+    end
+    
+    def *(other)
+      Tick.new(@time * other, @ambiguous)
+    end
+    
+    def to_f
+      @time.to_f
+    end
+    
+    def to_s
+      @time.to_s + (@ambiguous ? '?' : '')
+    end
+  end
+  
+  def initialize(time, options = {})
+    t = time.gsub(/\:/, '')
+    @type = 
+    if (1..2) === t.size
+      hours = t.to_i
+      hours == 12 ? Tick.new(0 * 60 * 60, true) : Tick.new(hours * 60 * 60, true)
+    elsif t.size == 3
+      Tick.new((t[0..0].to_i * 60 * 60) + (t[1..2].to_i * 60), true)
+    elsif t.size == 4
+      ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12
+      hours = t[0..1].to_i
+      hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60, ambiguous)
+    elsif t.size == 5
+      Tick.new(t[0..0].to_i * 60 * 60 + t[1..2].to_i * 60 + t[3..4].to_i, true)
+    elsif t.size == 6
+      ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12
+      hours = t[0..1].to_i
+      hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous)
+    else
+      raise("Time cannot exceed six digits")
+    end
+  end
+  
+  # Return the next past or future Span for the time that this Repeater represents
+  #   pointer - Symbol representing which temporal direction to fetch the next day
+  #             must be either :past or :future
+  def next(pointer)
+    super
+    
+    half_day = 60 * 60 * 12
+    full_day = 60 * 60 * 24
+    
+    first = false
+    
+    unless @current_time
+      first = true
+      midnight = Time.local(@now.year, @now.month, @now.day)
+      yesterday_midnight = midnight - full_day
+      tomorrow_midnight = midnight + full_day
+
+      catch :done do
+        if pointer == :future
+          if @type.ambiguous?
+            [midnight + @type, midnight + half_day + @type, tomorrow_midnight + @type].each do |t|
+              (@current_time = t; throw :done) if t >= @now
+            end
+          else
+            [midnight + @type, tomorrow_midnight + @type].each do |t|
+              (@current_time = t; throw :done) if t >= @now
+            end
+          end
+        else # pointer == :past
+          if @type.ambiguous?
+            [midnight + half_day + @type, midnight + @type, yesterday_midnight + @type * 2].each do |t|
+              (@current_time = t; throw :done) if t <= @now
+            end
+          else
+            [midnight + @type, yesterday_midnight + @type].each do |t|
+              (@current_time = t; throw :done) if t <= @now
+            end
+          end
+        end
+      end
+      
+      @current_time || raise("Current time cannot be nil at this point")
+    end
+    
+    unless first
+      increment = @type.ambiguous? ? half_day : full_day
+      @current_time += pointer == :future ? increment : -increment
+    end
+    
+    Chronic::Span.new(@current_time, @current_time + width)
+  end
+  
+  def this(context = :future)
+    super
+    
+    context = :future if context == :none
+    
+    self.next(context)
+  end
+  
+  def width
+    1
+  end
+  
+  def to_s
+    super << '-time-' << @type.to_s
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_week.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_week.rb
new file mode 100644 (file)
index 0000000..ec88ff1
--- /dev/null
@@ -0,0 +1,68 @@
+class Chronic::RepeaterWeek < Chronic::Repeater #:nodoc:
+  WEEK_SECONDS = 604800 # (7 * 24 * 60 * 60)
+  
+  def next(pointer)
+    super
+    
+    if !@current_week_start
+      case pointer
+      when :future
+        sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+        sunday_repeater.start = @now
+        next_sunday_span = sunday_repeater.next(:future)
+        @current_week_start = next_sunday_span.begin
+      when :past
+        sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+        sunday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS)
+        sunday_repeater.next(:past)
+        last_sunday_span = sunday_repeater.next(:past)
+        @current_week_start = last_sunday_span.begin
+      end
+    else
+      direction = pointer == :future ? 1 : -1
+      @current_week_start += direction * WEEK_SECONDS
+    end
+    
+    Chronic::Span.new(@current_week_start, @current_week_start + WEEK_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      this_week_start = Time.local(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS
+      sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+      sunday_repeater.start = @now
+      this_sunday_span = sunday_repeater.this(:future)
+      this_week_end = this_sunday_span.begin
+      Chronic::Span.new(this_week_start, this_week_end)
+    when :past
+      this_week_end = Time.local(@now.year, @now.month, @now.day, @now.hour)
+      sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+      sunday_repeater.start = @now
+      last_sunday_span = sunday_repeater.next(:past)
+      this_week_start = last_sunday_span.begin
+      Chronic::Span.new(this_week_start, this_week_end)
+    when :none
+      sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+      sunday_repeater.start = @now
+      last_sunday_span = sunday_repeater.next(:past)
+      this_week_start = last_sunday_span.begin
+      Chronic::Span.new(this_week_start, this_week_start + WEEK_SECONDS)
+    end
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * WEEK_SECONDS
+  end
+  
+  def width
+    WEEK_SECONDS
+  end
+  
+  def to_s
+    super << '-week'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_weekend.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_weekend.rb
new file mode 100644 (file)
index 0000000..f012267
--- /dev/null
@@ -0,0 +1,60 @@
+class Chronic::RepeaterWeekend < Chronic::Repeater #:nodoc:
+  WEEKEND_SECONDS = 172_800 # (2 * 24 * 60 * 60)
+  
+  def next(pointer)
+    super
+    
+    if !@current_week_start
+      case pointer
+      when :future
+        saturday_repeater = Chronic::RepeaterDayName.new(:saturday)
+        saturday_repeater.start = @now
+        next_saturday_span = saturday_repeater.next(:future)
+        @current_week_start = next_saturday_span.begin
+      when :past
+        saturday_repeater = Chronic::RepeaterDayName.new(:saturday)
+        saturday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS)
+        last_saturday_span = saturday_repeater.next(:past)
+        @current_week_start = last_saturday_span.begin
+      end
+    else
+      direction = pointer == :future ? 1 : -1
+      @current_week_start += direction * Chronic::RepeaterWeek::WEEK_SECONDS
+    end
+    
+    Chronic::Span.new(@current_week_start, @current_week_start + WEEKEND_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future, :none
+      saturday_repeater = Chronic::RepeaterDayName.new(:saturday)
+      saturday_repeater.start = @now
+      this_saturday_span = saturday_repeater.this(:future)
+      Chronic::Span.new(this_saturday_span.begin, this_saturday_span.begin + WEEKEND_SECONDS)
+    when :past
+      saturday_repeater = Chronic::RepeaterDayName.new(:saturday)
+      saturday_repeater.start = @now
+      last_saturday_span = saturday_repeater.this(:past)
+      Chronic::Span.new(last_saturday_span.begin, last_saturday_span.begin + WEEKEND_SECONDS)
+    end
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    weekend = Chronic::RepeaterWeekend.new(:weekend)
+    weekend.start = span.begin
+    start = weekend.next(pointer).begin + (amount - 1) * direction * Chronic::RepeaterWeek::WEEK_SECONDS
+    Chronic::Span.new(start, start + (span.end - span.begin))
+  end
+  
+  def width
+    WEEKEND_SECONDS
+  end
+  
+  def to_s
+    super << '-weekend'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_year.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/repeaters/repeater_year.rb
new file mode 100644 (file)
index 0000000..426371f
--- /dev/null
@@ -0,0 +1,58 @@
+class Chronic::RepeaterYear < Chronic::Repeater #:nodoc:
+  
+  def next(pointer)
+    super
+    
+    if !@current_year_start
+      case pointer
+      when :future
+        @current_year_start = Time.construct(@now.year + 1)
+      when :past
+        @current_year_start = Time.construct(@now.year - 1)
+      end
+    else
+      diff = pointer == :future ? 1 : -1
+      @current_year_start = Time.construct(@current_year_start.year + diff)
+    end
+    
+    Chronic::Span.new(@current_year_start, Time.construct(@current_year_start.year + 1))
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      this_year_start = Time.construct(@now.year, @now.month, @now.day) + Chronic::RepeaterDay::DAY_SECONDS
+      this_year_end = Time.construct(@now.year + 1, 1, 1)
+    when :past
+      this_year_start = Time.construct(@now.year, 1, 1)
+      this_year_end = Time.construct(@now.year, @now.month, @now.day)
+    when :none
+      this_year_start = Time.construct(@now.year, 1, 1)
+      this_year_end = Time.construct(@now.year + 1, 1, 1)
+    end
+    
+    Chronic::Span.new(this_year_start, this_year_end)
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    
+    sb = span.begin
+    new_begin = Time.construct(sb.year + (amount * direction), sb.month, sb.day, sb.hour, sb.min, sb.sec)
+    
+    se = span.end
+    new_end = Time.construct(se.year + (amount * direction), se.month, se.day, se.hour, se.min, se.sec)
+    
+    Chronic::Span.new(new_begin, new_end)
+  end
+  
+  def width
+    (365 * 24 * 60 * 60)
+  end
+  
+  def to_s
+    super << '-year'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/scalar.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/scalar.rb
new file mode 100644 (file)
index 0000000..b08cfee
--- /dev/null
@@ -0,0 +1,74 @@
+module Chronic
+
+  class Scalar < Tag #:nodoc:
+    def self.scan(tokens)
+      # for each token
+      tokens.each_index do |i|
+        if t = self.scan_for_scalars(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
+        if t = self.scan_for_days(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
+        if t = self.scan_for_months(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
+        if t = self.scan_for_years(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
+      end
+      tokens
+    end
+  
+    def self.scan_for_scalars(token, post_token)
+      if token.word =~ /^\d*$/
+        unless post_token && %w{am pm morning afternoon evening night}.include?(post_token)
+          return Scalar.new(token.word.to_i)
+        end
+      end
+      return nil
+    end
+    
+    def self.scan_for_days(token, post_token)
+      if token.word =~ /^\d\d?$/
+        unless token.word.to_i > 31 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token))
+          return ScalarDay.new(token.word.to_i)
+        end
+      end
+      return nil
+    end
+    
+    def self.scan_for_months(token, post_token)
+      if token.word =~ /^\d\d?$/
+        unless token.word.to_i > 12 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token))
+          return ScalarMonth.new(token.word.to_i)
+        end
+      end
+      return nil
+    end
+    
+    def self.scan_for_years(token, post_token)
+      if token.word =~ /^([1-9]\d)?\d\d?$/
+        unless post_token && %w{am pm morning afternoon evening night}.include?(post_token)
+          return ScalarYear.new(token.word.to_i)
+        end
+      end
+      return nil
+    end
+    
+    def to_s
+      'scalar'
+    end
+  end
+  
+  class ScalarDay < Scalar #:nodoc:
+    def to_s
+      super << '-day-' << @type.to_s
+    end
+  end
+  
+  class ScalarMonth < Scalar #:nodoc:
+    def to_s
+      super << '-month-' << @type.to_s
+    end
+  end
+  
+  class ScalarYear < Scalar #:nodoc:
+    def to_s
+      super << '-year-' << @type.to_s
+    end
+  end
+
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/separator.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/separator.rb
new file mode 100644 (file)
index 0000000..86c56e3
--- /dev/null
@@ -0,0 +1,76 @@
+module Chronic
+
+  class Separator < Tag #:nodoc:
+    def self.scan(tokens)
+      tokens.each_index do |i|
+        if t = self.scan_for_commas(tokens[i]) then tokens[i].tag(t); next end
+        if t = self.scan_for_slash_or_dash(tokens[i]) then tokens[i].tag(t); next end
+        if t = self.scan_for_at(tokens[i]) then tokens[i].tag(t); next end
+        if t = self.scan_for_in(tokens[i]) then tokens[i].tag(t); next end
+      end
+      tokens
+    end
+    
+    def self.scan_for_commas(token)
+      scanner = {/^,$/ => :comma}
+      scanner.keys.each do |scanner_item|
+        return SeparatorComma.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def self.scan_for_slash_or_dash(token)
+      scanner = {/^-$/ => :dash,
+                 /^\/$/ => :slash}
+      scanner.keys.each do |scanner_item|
+        return SeparatorSlashOrDash.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def self.scan_for_at(token)
+      scanner = {/^(at|@)$/ => :at}
+      scanner.keys.each do |scanner_item|
+        return SeparatorAt.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def self.scan_for_in(token)
+      scanner = {/^in$/ => :in}
+      scanner.keys.each do |scanner_item|
+        return SeparatorIn.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def to_s
+      'separator'
+    end
+  end
+  
+  class SeparatorComma < Separator #:nodoc:
+    def to_s
+      super << '-comma'
+    end
+  end
+  
+  class SeparatorSlashOrDash < Separator #:nodoc:
+    def to_s
+      super << '-slashordash-' << @type.to_s
+    end
+  end
+  
+  class SeparatorAt < Separator #:nodoc:
+    def to_s
+      super << '-at'
+    end
+  end
+  
+  class SeparatorIn < Separator #:nodoc:
+    def to_s
+      super << '-in'
+    end
+  end
+
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/chronic/time_zone.rb b/framework/Horde_Date_Parser/chronic/lib/chronic/time_zone.rb
new file mode 100644 (file)
index 0000000..41041ef
--- /dev/null
@@ -0,0 +1,22 @@
+module Chronic
+  class TimeZone < Tag #:nodoc:    
+    def self.scan(tokens)
+      tokens.each_index do |i|
+        if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end
+      end
+      tokens
+    end
+
+    def self.scan_for_all(token)
+      scanner = {/[PMCE][DS]T/i => :tz}
+      scanner.keys.each do |scanner_item|
+        return self.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+
+    def to_s
+      'timezone'
+    end
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/lib/numerizer/numerizer.rb b/framework/Horde_Date_Parser/chronic/lib/numerizer/numerizer.rb
new file mode 100644 (file)
index 0000000..8b02e62
--- /dev/null
@@ -0,0 +1,103 @@
+require 'strscan'
+
+class Numerizer
+
+  DIRECT_NUMS = [
+                  ['eleven', '11'],
+                  ['twelve', '12'],
+                  ['thirteen', '13'],
+                  ['fourteen', '14'],
+                  ['fifteen', '15'],
+                  ['sixteen', '16'],
+                  ['seventeen', '17'],
+                  ['eighteen', '18'],
+                  ['nineteen', '19'],
+                  ['ninteen', '19'], # Common mis-spelling
+                  ['zero', '0'],
+                  ['one', '1'],
+                  ['two', '2'],
+                  ['three', '3'],
+                  ['four(\W|$)', '4\1'],  # The weird regex is so that it matches four but not fourty
+                  ['five', '5'],
+                  ['six(\W|$)', '6\1'],
+                  ['seven(\W|$)', '7\1'],
+                  ['eight(\W|$)', '8\1'],
+                  ['nine(\W|$)', '9\1'],
+                  ['ten', '10'],
+                  ['\ba[\b^$]', '1'] # doesn't make sense for an 'a' at the end to be a 1
+                ]
+
+  TEN_PREFIXES = [ ['twenty', 20],
+                    ['thirty', 30],
+                    ['fourty', 40],
+                    ['fifty', 50],
+                    ['sixty', 60],
+                    ['seventy', 70],
+                    ['eighty', 80],
+                    ['ninety', 90]
+                  ]
+
+  BIG_PREFIXES = [ ['hundred', 100],
+                    ['thousand', 1000],
+                    ['million', 1_000_000],
+                    ['billion', 1_000_000_000],
+                    ['trillion', 1_000_000_000_000],
+                  ]
+
+class << self
+  def numerize(string)
+    string = string.dup
+  
+    # preprocess
+    string.gsub!(/ +|([^\d])-([^d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction
+    string.gsub!(/a half/, 'haAlf') # take the 'a' out so it doesn't turn into a 1, save the half for the end
+
+    # easy/direct replacements
+  
+    DIRECT_NUMS.each do |dn|
+      string.gsub!(/#{dn[0]}/i, dn[1])
+    end
+
+    # ten, twenty, etc.
+
+    TEN_PREFIXES.each do |tp|
+      string.gsub!(/(?:#{tp[0]})( *\d(?=[^\d]|$))*/i) { (tp[1] + $1.to_i).to_s }
+    end
+
+    # hundreds, thousands, millions, etc.
+
+    BIG_PREFIXES.each do |bp|
+      string.gsub!(/(\d*) *#{bp[0]}/i) { (bp[1] * $1.to_i).to_s}
+      andition(string)
+      #combine_numbers(string) # Should to be more efficient way to do this
+    end
+
+    # fractional addition
+    # I'm not combining this with the previous block as using float addition complicates the strings
+    # (with extraneous .0's and such )
+    string.gsub!(/(\d+)(?: | and |-)*haAlf/i) { ($1.to_f + 0.5).to_s }
+
+    string
+  end
+
+private
+  def andition(string)
+    sc = StringScanner.new(string)
+    while(sc.scan_until(/(\d+)( | and )(\d+)(?=[^\w]|$)/i))
+      if sc[2] =~ /and/ || sc[1].size > sc[3].size
+        string[(sc.pos - sc.matched_size)..(sc.pos-1)] = (sc[1].to_i + sc[3].to_i).to_s
+        sc.reset
+      end
+    end
+  end
+
+#  def combine_numbers(string)
+#    sc = StringScanner.new(string)
+#    while(sc.scan_until(/(\d+)(?: | and |-)(\d+)(?=[^\w]|$)/i))
+#      string[(sc.pos - sc.matched_size)..(sc.pos-1)] = (sc[1].to_i + sc[2].to_i).to_s
+#      sc.reset
+#    end
+#  end
+
+end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/suite.rb b/framework/Horde_Date_Parser/chronic/test/suite.rb
new file mode 100644 (file)
index 0000000..fa8bdaa
--- /dev/null
@@ -0,0 +1,9 @@
+require 'test/unit'
+
+tests = Dir["#{File.dirname(__FILE__)}/test_*.rb"]
+tests.delete_if { |o| o =~ /test_parsing/ }
+tests.each do |file|
+  require file
+end
+
+require File.dirname(__FILE__) + '/test_parsing.rb'
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_Chronic.rb b/framework/Horde_Date_Parser/chronic/test/test_Chronic.rb
new file mode 100644 (file)
index 0000000..04fedb5
--- /dev/null
@@ -0,0 +1,50 @@
+require 'chronic'
+require 'test/unit'
+
+class TestChronic < Test::Unit::TestCase
+  
+  def setup
+    # Wed Aug 16 14:00:00 UTC 2006
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+  
+  def test_post_normalize_am_pm_aliases
+    # affect wanted patterns
+    
+    tokens = [Chronic::Token.new("5:00"), Chronic::Token.new("morning")]
+    tokens[0].tag(Chronic::RepeaterTime.new("5:00"))
+    tokens[1].tag(Chronic::RepeaterDayPortion.new(:morning))
+    
+    assert_equal :morning, tokens[1].tags[0].type
+    
+    tokens = Chronic.dealias_and_disambiguate_times(tokens, {})
+    
+    assert_equal :am, tokens[1].tags[0].type
+    assert_equal 2, tokens.size
+    
+    # don't affect unwanted patterns
+    
+    tokens = [Chronic::Token.new("friday"), Chronic::Token.new("morning")]
+    tokens[0].tag(Chronic::RepeaterDayName.new(:friday))
+    tokens[1].tag(Chronic::RepeaterDayPortion.new(:morning))
+    
+    assert_equal :morning, tokens[1].tags[0].type
+    
+    tokens = Chronic.dealias_and_disambiguate_times(tokens, {})
+    
+    assert_equal :morning, tokens[1].tags[0].type
+    assert_equal 2, tokens.size
+  end
+  
+  def test_guess
+    span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0))
+    assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span)
+    
+    span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0, 0, 1))
+    assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span)
+    
+    span = Chronic::Span.new(Time.local(2006, 11), Time.local(2006, 12))
+    assert_equal Time.local(2006, 11, 16), Chronic.guess(span)
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_Handler.rb b/framework/Horde_Date_Parser/chronic/test/test_Handler.rb
new file mode 100644 (file)
index 0000000..4e36dfe
--- /dev/null
@@ -0,0 +1,110 @@
+require 'chronic'
+require 'test/unit'
+
+class TestHandler < Test::Unit::TestCase
+  
+  def setup
+    # Wed Aug 16 14:00:00 UTC 2006
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+
+  def test_handler_class_1
+    handler = Chronic::Handler.new([:repeater], :handler)
+    
+    tokens = [Chronic::Token.new('friday')]
+    tokens[0].tag(Chronic::RepeaterDayName.new(:friday))
+    
+    assert handler.match(tokens, Chronic.definitions)
+    
+    tokens << Chronic::Token.new('afternoon')
+    tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon))
+    
+    assert !handler.match(tokens, Chronic.definitions)
+  end
+  
+  def test_handler_class_2
+    handler = Chronic::Handler.new([:repeater, :repeater?], :handler)
+    
+    tokens = [Chronic::Token.new('friday')]
+    tokens[0].tag(Chronic::RepeaterDayName.new(:friday))
+    
+    assert handler.match(tokens, Chronic.definitions)
+    
+    tokens << Chronic::Token.new('afternoon')
+    tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon))
+    
+    assert handler.match(tokens, Chronic.definitions)
+    
+    tokens << Chronic::Token.new('afternoon')
+    tokens[2].tag(Chronic::RepeaterDayPortion.new(:afternoon))
+    
+    assert !handler.match(tokens, Chronic.definitions)
+  end
+  
+  def test_handler_class_3
+    handler = Chronic::Handler.new([:repeater, 'time?'], :handler)
+    
+    tokens = [Chronic::Token.new('friday')]
+    tokens[0].tag(Chronic::RepeaterDayName.new(:friday))
+    
+    assert handler.match(tokens, Chronic.definitions)
+    
+    tokens << Chronic::Token.new('afternoon')
+    tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon))
+    
+    assert !handler.match(tokens, Chronic.definitions)
+  end
+  
+  def test_handler_class_4
+    handler = Chronic::Handler.new([:repeater_month_name, :scalar_day, 'time?'], :handler)
+    
+    tokens = [Chronic::Token.new('may')]
+    tokens[0].tag(Chronic::RepeaterMonthName.new(:may))
+    
+    assert !handler.match(tokens, Chronic.definitions)
+    
+    tokens << Chronic::Token.new('27')
+    tokens[1].tag(Chronic::ScalarDay.new(27))
+    
+    assert handler.match(tokens, Chronic.definitions)
+  end
+  
+  def test_handler_class_5
+    handler = Chronic::Handler.new([:repeater, 'time?'], :handler)
+    
+    tokens = [Chronic::Token.new('friday')]
+    tokens[0].tag(Chronic::RepeaterDayName.new(:friday))
+    
+    assert handler.match(tokens, Chronic.definitions)
+    
+    tokens << Chronic::Token.new('5:00')
+    tokens[1].tag(Chronic::RepeaterTime.new('5:00'))
+    
+    assert handler.match(tokens, Chronic.definitions)
+    
+    tokens << Chronic::Token.new('pm')
+    tokens[2].tag(Chronic::RepeaterDayPortion.new(:pm))
+    
+    assert handler.match(tokens, Chronic.definitions)
+  end
+  
+  def test_handler_class_6
+    handler = Chronic::Handler.new([:scalar, :repeater, :pointer], :handler)
+    
+    tokens = [Chronic::Token.new('3'),
+              Chronic::Token.new('years'),
+              Chronic::Token.new('past')]
+              
+    tokens[0].tag(Chronic::Scalar.new(3))
+    tokens[1].tag(Chronic::RepeaterYear.new(:year))
+    tokens[2].tag(Chronic::Pointer.new(:past))
+    
+    assert handler.match(tokens, Chronic.definitions)
+  end
+  
+  def test_constantize
+    handler = Chronic::Handler.new([], :handler)
+    assert_equal Chronic::RepeaterTime, handler.constantize(:repeater_time)
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_Numerizer.rb b/framework/Horde_Date_Parser/chronic/test/test_Numerizer.rb
new file mode 100644 (file)
index 0000000..f70c51a
--- /dev/null
@@ -0,0 +1,48 @@
+require 'test/unit'
+require 'chronic'
+
+class ParseNumbersTest < Test::Unit::TestCase
+
+  def test_straight_parsing
+    strings = { 1 => 'one',
+               5 => 'five',
+               10 => 'ten',
+               11 => 'eleven',
+               12 => 'twelve',
+               13 => 'thirteen',
+               14 => 'fourteen',
+               15 => 'fifteen',
+               16 => 'sixteen',
+               17 => 'seventeen',
+               18 => 'eighteen',
+               19 => 'nineteen',
+               20 => 'twenty',
+               27 => 'twenty seven',
+               31 => 'thirty-one',
+               59 => 'fifty nine',
+               100 => 'a hundred',
+               100 => 'one hundred',
+               150 => 'one hundred and fifty',
+            #   150 => 'one fifty',
+               200 => 'two-hundred',
+               500 => '5 hundred',
+               999 => 'nine hundred and ninety nine',
+               1_000 => 'one thousand',
+               1_200 => 'twelve hundred',
+               1_200 => 'one thousand two hundred',
+               17_000 => 'seventeen thousand',
+               21_473 => 'twentyone-thousand-four-hundred-and-seventy-three',
+               74_002 => 'seventy four thousand and two',
+               99_999 => 'ninety nine thousand nine hundred ninety nine',
+               100_000 => '100 thousand',
+               250_000 => 'two hundred fifty thousand',
+               1_000_000 => 'one million',
+               1_250_007 => 'one million two hundred fifty thousand and seven',
+               1_000_000_000 => 'one billion',
+               1_000_000_001 => 'one billion and one' }
+               
+    strings.keys.sort.each do |key|
+      assert_equal key, Numerizer.numerize(strings[key]).to_i
+    end
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterDayName.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterDayName.rb
new file mode 100644 (file)
index 0000000..8e119db
--- /dev/null
@@ -0,0 +1,52 @@
+require 'chronic'
+require 'test/unit'
+
+class TestRepeaterDayName < Test::Unit::TestCase
+  
+  def setup
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+  
+  def test_match
+    token = Chronic::Token.new('saturday')
+    repeater = Chronic::Repeater.scan_for_day_names(token)
+    assert_equal Chronic::RepeaterDayName, repeater.class
+    assert_equal :saturday, repeater.type
+    
+    token = Chronic::Token.new('sunday')
+    repeater = Chronic::Repeater.scan_for_day_names(token)
+    assert_equal Chronic::RepeaterDayName, repeater.class
+    assert_equal :sunday, repeater.type
+  end
+
+  def test_next_future
+    mondays = Chronic::RepeaterDayName.new(:monday)
+    mondays.start = @now
+    
+    span = mondays.next(:future)
+    
+    assert_equal Time.local(2006, 8, 21), span.begin
+    assert_equal Time.local(2006, 8, 22), span.end 
+
+    span = mondays.next(:future)
+    
+    assert_equal Time.local(2006, 8, 28), span.begin
+    assert_equal Time.local(2006, 8, 29), span.end
+  end
+  
+  def test_next_past
+    mondays = Chronic::RepeaterDayName.new(:monday)
+    mondays.start = @now
+    
+    span = mondays.next(:past)
+    
+    assert_equal Time.local(2006, 8, 14), span.begin
+    assert_equal Time.local(2006, 8, 15), span.end 
+
+    span = mondays.next(:past)
+    
+    assert_equal Time.local(2006, 8, 7), span.begin
+    assert_equal Time.local(2006, 8, 8), span.end
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterFortnight.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterFortnight.rb
new file mode 100644 (file)
index 0000000..cb80c43
--- /dev/null
@@ -0,0 +1,63 @@
+require 'chronic'
+require 'test/unit'
+
+class TestRepeaterFortnight < Test::Unit::TestCase
+  
+  def setup
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+
+  def test_next_future
+    fortnights = Chronic::RepeaterFortnight.new(:fortnight)
+    fortnights.start = @now
+    
+    next_fortnight = fortnights.next(:future)
+    assert_equal Time.local(2006, 8, 20), next_fortnight.begin
+    assert_equal Time.local(2006, 9, 3), next_fortnight.end
+    
+    next_next_fortnight = fortnights.next(:future)
+    assert_equal Time.local(2006, 9, 3), next_next_fortnight.begin
+    assert_equal Time.local(2006, 9, 17), next_next_fortnight.end
+  end
+  
+  def test_next_past
+    fortnights = Chronic::RepeaterFortnight.new(:fortnight)
+    fortnights.start = @now
+    
+    last_fortnight = fortnights.next(:past)
+    assert_equal Time.local(2006, 7, 30), last_fortnight.begin
+    assert_equal Time.local(2006, 8, 13), last_fortnight.end
+    
+    last_last_fortnight = fortnights.next(:past)
+    assert_equal Time.local(2006, 7, 16), last_last_fortnight.begin
+    assert_equal Time.local(2006, 7, 30), last_last_fortnight.end
+  end
+  
+  def test_this_future
+    fortnights = Chronic::RepeaterFortnight.new(:fortnight)
+    fortnights.start = @now
+    
+    this_fortnight = fortnights.this(:future)
+    assert_equal Time.local(2006, 8, 16, 15), this_fortnight.begin
+    assert_equal Time.local(2006, 8, 27), this_fortnight.end
+  end
+  
+  def test_this_past
+    fortnights = Chronic::RepeaterFortnight.new(:fortnight)
+    fortnights.start = @now
+    
+    this_fortnight = fortnights.this(:past)
+    assert_equal Time.local(2006, 8, 13, 0), this_fortnight.begin
+    assert_equal Time.local(2006, 8, 16, 14), this_fortnight.end
+  end
+  
+  def test_offset
+    span = Chronic::Span.new(@now, @now + 1)
+    
+    offset_span = Chronic::RepeaterWeek.new(:week).offset(span, 3, :future)
+    
+    assert_equal Time.local(2006, 9, 6, 14), offset_span.begin
+    assert_equal Time.local(2006, 9, 6, 14, 0, 1), offset_span.end
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterHour.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterHour.rb
new file mode 100644 (file)
index 0000000..48f37c4
--- /dev/null
@@ -0,0 +1,65 @@
+require 'chronic'
+require 'test/unit'
+
+class TestRepeaterHour < Test::Unit::TestCase
+  
+  def setup
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+
+  def test_next_future
+    hours = Chronic::RepeaterHour.new(:hour)
+    hours.start = @now
+    
+    next_hour = hours.next(:future)
+    assert_equal Time.local(2006, 8, 16, 15), next_hour.begin
+    assert_equal Time.local(2006, 8, 16, 16), next_hour.end
+    
+    next_next_hour = hours.next(:future)
+    assert_equal Time.local(2006, 8, 16, 16), next_next_hour.begin
+    assert_equal Time.local(2006, 8, 16, 17), next_next_hour.end
+  end
+  
+  def test_next_past
+    hours = Chronic::RepeaterHour.new(:hour)
+    hours.start = @now
+    
+    past_hour = hours.next(:past)
+    assert_equal Time.local(2006, 8, 16, 13), past_hour.begin
+    assert_equal Time.local(2006, 8, 16, 14), past_hour.end
+    
+    past_past_hour = hours.next(:past)
+    assert_equal Time.local(2006, 8, 16, 12), past_past_hour.begin
+    assert_equal Time.local(2006, 8, 16, 13), past_past_hour.end
+  end
+  
+  def test_this
+    @now = Time.local(2006, 8, 16, 14, 30)
+    
+    hours = Chronic::RepeaterHour.new(:hour)
+    hours.start = @now
+    
+    this_hour = hours.this(:future)
+    assert_equal Time.local(2006, 8, 16, 14, 31), this_hour.begin
+    assert_equal Time.local(2006, 8, 16, 15), this_hour.end
+    
+    this_hour = hours.this(:past)
+    assert_equal Time.local(2006, 8, 16, 14), this_hour.begin
+    assert_equal Time.local(2006, 8, 16, 14, 30), this_hour.end
+  end
+  
+  def test_offset
+    span = Chronic::Span.new(@now, @now + 1)
+    
+    offset_span = Chronic::RepeaterHour.new(:hour).offset(span, 3, :future)
+    
+    assert_equal Time.local(2006, 8, 16, 17), offset_span.begin
+    assert_equal Time.local(2006, 8, 16, 17, 0, 1), offset_span.end
+    
+    offset_span = Chronic::RepeaterHour.new(:hour).offset(span, 24, :past)
+    
+    assert_equal Time.local(2006, 8, 15, 14), offset_span.begin
+    assert_equal Time.local(2006, 8, 15, 14, 0, 1), offset_span.end
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterMonth.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterMonth.rb
new file mode 100644 (file)
index 0000000..d0609c5
--- /dev/null
@@ -0,0 +1,47 @@
+require 'chronic'
+require 'test/unit'
+
+class TestRepeaterMonth < Test::Unit::TestCase
+  
+  def setup
+    # Wed Aug 16 14:00:00 2006
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+  
+  def test_offset_by
+    # future
+    
+    time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 1, :future)
+    assert_equal Time.local(2006, 9, 16, 14), time
+    
+    time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 5, :future)
+    assert_equal Time.local(2007, 1, 16, 14), time
+    
+    # past
+    
+    time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 1, :past)
+    assert_equal Time.local(2006, 7, 16, 14), time
+    
+    time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 10, :past)
+    assert_equal Time.local(2005, 10, 16, 14), time
+  end
+  
+  def test_offset
+    # future
+    
+    span = Chronic::Span.new(@now, @now + 60)
+    offset_span = Chronic::RepeaterMonth.new(:month).offset(span, 1, :future)
+    
+    assert_equal Time.local(2006, 9, 16, 14), offset_span.begin
+    assert_equal Time.local(2006, 9, 16, 14, 1), offset_span.end
+    
+   # past
+   
+   span = Chronic::Span.new(@now, @now + 60)
+   offset_span = Chronic::RepeaterMonth.new(:month).offset(span, 1, :past)
+   
+   assert_equal Time.local(2006, 7, 16, 14), offset_span.begin
+   assert_equal Time.local(2006, 7, 16, 14, 1), offset_span.end
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterMonthName.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterMonthName.rb
new file mode 100644 (file)
index 0000000..6326a45
--- /dev/null
@@ -0,0 +1,57 @@
+require 'chronic'
+require 'test/unit'
+
+class TestRepeaterMonthName < Test::Unit::TestCase
+  
+  def setup
+    # Wed Aug 16 14:00:00 2006
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+  
+  def test_next
+    # future
+    
+    mays = Chronic::RepeaterMonthName.new(:may)
+    mays.start = @now
+    
+    next_may = mays.next(:future)
+    assert_equal Time.local(2007, 5), next_may.begin
+    assert_equal Time.local(2007, 6), next_may.end
+    
+    next_next_may = mays.next(:future)
+    assert_equal Time.local(2008, 5), next_next_may.begin
+    assert_equal Time.local(2008, 6), next_next_may.end
+    
+    decembers = Chronic::RepeaterMonthName.new(:december)
+    decembers.start = @now
+    
+    next_december = decembers.next(:future)
+    assert_equal Time.local(2006, 12), next_december.begin
+    assert_equal Time.local(2007, 1), next_december.end
+    
+    # past
+    
+    mays = Chronic::RepeaterMonthName.new(:may)
+    mays.start = @now
+    
+    assert_equal Time.local(2006, 5), mays.next(:past).begin
+    assert_equal Time.local(2005, 5), mays.next(:past).begin
+  end
+  
+  def test_this
+    octobers = Chronic::RepeaterMonthName.new(:october)
+    octobers.start = @now
+    
+    this_october = octobers.this(:future)
+    assert_equal Time.local(2006, 10, 1), this_october.begin
+    assert_equal Time.local(2006, 11, 1), this_october.end
+    
+    aprils = Chronic::RepeaterMonthName.new(:april)
+    aprils.start = @now
+    
+    this_april = aprils.this(:past)
+    assert_equal Time.local(2006, 4, 1), this_april.begin
+    assert_equal Time.local(2006, 5, 1), this_april.end
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterTime.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterTime.rb
new file mode 100644 (file)
index 0000000..bb27735
--- /dev/null
@@ -0,0 +1,72 @@
+require 'chronic'
+require 'test/unit'
+
+class TestRepeaterTime < Test::Unit::TestCase
+  
+  def setup
+    # Wed Aug 16 14:00:00 2006
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+  
+  def test_next_future
+    t = Chronic::RepeaterTime.new('4:00')
+    t.start = @now
+    
+    assert_equal Time.local(2006, 8, 16, 16), t.next(:future).begin
+    assert_equal Time.local(2006, 8, 17, 4), t.next(:future).begin
+    
+    t = Chronic::RepeaterTime.new('13:00')
+    t.start = @now
+    
+    assert_equal Time.local(2006, 8, 17, 13), t.next(:future).begin
+    assert_equal Time.local(2006, 8, 18, 13), t.next(:future).begin
+    
+    t = Chronic::RepeaterTime.new('0400')
+    t.start = @now
+    
+    assert_equal Time.local(2006, 8, 17, 4), t.next(:future).begin
+    assert_equal Time.local(2006, 8, 18, 4), t.next(:future).begin
+  end
+  
+  def test_next_past
+    t = Chronic::RepeaterTime.new('4:00')
+    t.start = @now
+    
+    assert_equal Time.local(2006, 8, 16, 4), t.next(:past).begin
+    assert_equal Time.local(2006, 8, 15, 16), t.next(:past).begin
+    
+    t = Chronic::RepeaterTime.new('13:00')
+    t.start = @now
+    
+    assert_equal Time.local(2006, 8, 16, 13), t.next(:past).begin
+    assert_equal Time.local(2006, 8, 15, 13), t.next(:past).begin
+  end
+
+  def test_type
+    t1 = Chronic::RepeaterTime.new('4')
+    assert_equal 14_400, t1.type.time
+    
+    t1 = Chronic::RepeaterTime.new('14')
+    assert_equal 50_400, t1.type.time
+    
+    t1 = Chronic::RepeaterTime.new('4:00')
+    assert_equal 14_400, t1.type.time
+    
+    t1 = Chronic::RepeaterTime.new('4:30')
+    assert_equal 16_200, t1.type.time
+    
+    t1 = Chronic::RepeaterTime.new('1400')
+    assert_equal 50_400, t1.type.time
+    
+    t1 = Chronic::RepeaterTime.new('0400')
+    assert_equal 14_400, t1.type.time
+    
+    t1 = Chronic::RepeaterTime.new('04')
+    assert_equal 14_400, t1.type.time
+    
+    t1 = Chronic::RepeaterTime.new('400')
+    assert_equal 14_400, t1.type.time
+  end
+
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterWeek.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterWeek.rb
new file mode 100644 (file)
index 0000000..084ef4e
--- /dev/null
@@ -0,0 +1,63 @@
+require 'chronic'
+require 'test/unit'
+
+class TestRepeaterWeek < Test::Unit::TestCase
+  
+  def setup
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+
+  def test_next_future
+    weeks = Chronic::RepeaterWeek.new(:week)
+    weeks.start = @now
+    
+    next_week = weeks.next(:future)
+    assert_equal Time.local(2006, 8, 20), next_week.begin
+    assert_equal Time.local(2006, 8, 27), next_week.end
+    
+    next_next_week = weeks.next(:future)
+    assert_equal Time.local(2006, 8, 27), next_next_week.begin
+    assert_equal Time.local(2006, 9, 3), next_next_week.end
+  end
+  
+  def test_next_past
+    weeks = Chronic::RepeaterWeek.new(:week)
+    weeks.start = @now
+    
+    last_week = weeks.next(:past)
+    assert_equal Time.local(2006, 8, 6), last_week.begin
+    assert_equal Time.local(2006, 8, 13), last_week.end
+    
+    last_last_week = weeks.next(:past)
+    assert_equal Time.local(2006, 7, 30), last_last_week.begin
+    assert_equal Time.local(2006, 8, 6), last_last_week.end
+  end
+  
+  def test_this_future
+    weeks = Chronic::RepeaterWeek.new(:week)
+    weeks.start = @now
+    
+    this_week = weeks.this(:future)
+    assert_equal Time.local(2006, 8, 16, 15), this_week.begin
+    assert_equal Time.local(2006, 8, 20), this_week.end
+  end
+  
+  def test_this_past
+    weeks = Chronic::RepeaterWeek.new(:week)
+    weeks.start = @now
+    
+    this_week = weeks.this(:past)
+    assert_equal Time.local(2006, 8, 13, 0), this_week.begin
+    assert_equal Time.local(2006, 8, 16, 14), this_week.end
+  end
+  
+  def test_offset
+    span = Chronic::Span.new(@now, @now + 1)
+    
+    offset_span = Chronic::RepeaterWeek.new(:week).offset(span, 3, :future)
+    
+    assert_equal Time.local(2006, 9, 6, 14), offset_span.begin
+    assert_equal Time.local(2006, 9, 6, 14, 0, 1), offset_span.end
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterWeekend.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterWeekend.rb
new file mode 100644 (file)
index 0000000..44dc087
--- /dev/null
@@ -0,0 +1,75 @@
+require 'chronic'
+require 'test/unit'
+
+class TestRepeaterWeekend < Test::Unit::TestCase
+  
+  def setup
+    # Wed Aug 16 14:00:00 2006
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+  
+  def test_next_future
+    weekend = Chronic::RepeaterWeekend.new(:weekend)
+    weekend.start = @now
+    
+    next_weekend = weekend.next(:future)
+    assert_equal Time.local(2006, 8, 19), next_weekend.begin
+    assert_equal Time.local(2006, 8, 21), next_weekend.end
+  end
+  
+  def test_next_past
+    weekend = Chronic::RepeaterWeekend.new(:weekend)
+    weekend.start = @now
+    
+    next_weekend = weekend.next(:past)
+    assert_equal Time.local(2006, 8, 12), next_weekend.begin
+    assert_equal Time.local(2006, 8, 14), next_weekend.end
+  end
+  
+  def test_this_future
+    weekend = Chronic::RepeaterWeekend.new(:weekend)
+    weekend.start = @now
+    
+    next_weekend = weekend.this(:future)
+    assert_equal Time.local(2006, 8, 19), next_weekend.begin
+    assert_equal Time.local(2006, 8, 21), next_weekend.end
+  end
+  
+  def test_this_past
+    weekend = Chronic::RepeaterWeekend.new(:weekend)
+    weekend.start = @now
+    
+    next_weekend = weekend.this(:past)
+    assert_equal Time.local(2006, 8, 12), next_weekend.begin
+    assert_equal Time.local(2006, 8, 14), next_weekend.end
+  end
+  
+  def test_this_none
+    weekend = Chronic::RepeaterWeekend.new(:weekend)
+    weekend.start = @now
+    
+    next_weekend = weekend.this(:future)
+    assert_equal Time.local(2006, 8, 19), next_weekend.begin
+    assert_equal Time.local(2006, 8, 21), next_weekend.end
+  end
+  
+  def test_offset
+    span = Chronic::Span.new(@now, @now + 1)
+    
+    offset_span = Chronic::RepeaterWeekend.new(:weekend).offset(span, 3, :future)
+    
+    assert_equal Time.local(2006, 9, 2), offset_span.begin
+    assert_equal Time.local(2006, 9, 2, 0, 0, 1), offset_span.end
+    
+    offset_span = Chronic::RepeaterWeekend.new(:weekend).offset(span, 1, :past)
+    
+    assert_equal Time.local(2006, 8, 12), offset_span.begin
+    assert_equal Time.local(2006, 8, 12, 0, 0, 1), offset_span.end
+    
+    offset_span = Chronic::RepeaterWeekend.new(:weekend).offset(span, 0, :future)
+    
+    assert_equal Time.local(2006, 8, 12), offset_span.begin
+    assert_equal Time.local(2006, 8, 12, 0, 0, 1), offset_span.end
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_RepeaterYear.rb b/framework/Horde_Date_Parser/chronic/test/test_RepeaterYear.rb
new file mode 100644 (file)
index 0000000..eaebe25
--- /dev/null
@@ -0,0 +1,63 @@
+require 'chronic'
+require 'test/unit'
+
+class TestRepeaterYear < Test::Unit::TestCase
+  
+  def setup
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+
+  def test_next_future
+    years = Chronic::RepeaterYear.new(:year)
+    years.start = @now
+    
+    next_year = years.next(:future)
+    assert_equal Time.local(2007, 1, 1), next_year.begin
+    assert_equal Time.local(2008, 1, 1), next_year.end
+    
+    next_next_year = years.next(:future)
+    assert_equal Time.local(2008, 1, 1), next_next_year.begin
+    assert_equal Time.local(2009, 1, 1), next_next_year.end
+  end
+  
+  def test_next_past
+    years = Chronic::RepeaterYear.new(:year)
+    years.start = @now
+    
+    last_year = years.next(:past)
+    assert_equal Time.local(2005, 1, 1), last_year.begin
+    assert_equal Time.local(2006, 1, 1), last_year.end
+    
+    last_last_year = years.next(:past)
+    assert_equal Time.local(2004, 1, 1), last_last_year.begin
+    assert_equal Time.local(2005, 1, 1), last_last_year.end
+  end
+  
+  def test_this
+    years = Chronic::RepeaterYear.new(:year)
+    years.start = @now
+    
+    this_year = years.this(:future)
+    assert_equal Time.local(2006, 8, 17), this_year.begin
+    assert_equal Time.local(2007, 1, 1), this_year.end
+    
+    this_year = years.this(:past)
+    assert_equal Time.local(2006, 1, 1), this_year.begin
+    assert_equal Time.local(2006, 8, 16), this_year.end
+  end
+  
+  def test_offset
+    span = Chronic::Span.new(@now, @now + 1)
+    
+    offset_span = Chronic::RepeaterYear.new(:year).offset(span, 3, :future)
+    
+    assert_equal Time.local(2009, 8, 16, 14), offset_span.begin
+    assert_equal Time.local(2009, 8, 16, 14, 0, 1), offset_span.end
+    
+    offset_span = Chronic::RepeaterYear.new(:year).offset(span, 10, :past)
+    
+    assert_equal Time.local(1996, 8, 16, 14), offset_span.begin
+    assert_equal Time.local(1996, 8, 16, 14, 0, 1), offset_span.end
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_Span.rb b/framework/Horde_Date_Parser/chronic/test/test_Span.rb
new file mode 100644 (file)
index 0000000..099455a
--- /dev/null
@@ -0,0 +1,24 @@
+require 'chronic'
+require 'test/unit'
+
+class TestSpan < Test::Unit::TestCase
+  
+  def setup
+    # Wed Aug 16 14:00:00 UTC 2006
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+
+  def test_span_width
+    span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0))
+    assert_equal (60 * 60 * 24), span.width
+  end
+  
+  def test_span_math
+    s = Chronic::Span.new(1, 2)
+    assert_equal 2, (s + 1).begin
+    assert_equal 3, (s + 1).end
+    assert_equal 0, (s - 1).begin
+    assert_equal 1, (s - 1).end
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_Time.rb b/framework/Horde_Date_Parser/chronic/test/test_Time.rb
new file mode 100644 (file)
index 0000000..3ffc9c0
--- /dev/null
@@ -0,0 +1,50 @@
+require 'chronic'
+require 'test/unit'
+
+class TestTime < Test::Unit::TestCase
+  
+  def setup
+  end
+  
+  def test_normal
+    assert_equal Time.local(2006, 1, 2, 0, 0, 0), Time.construct(2006, 1, 2, 0, 0, 0)
+    assert_equal Time.local(2006, 1, 2, 3, 0, 0), Time.construct(2006, 1, 2, 3, 0, 0)
+    assert_equal Time.local(2006, 1, 2, 3, 4, 0), Time.construct(2006, 1, 2, 3, 4, 0)
+    assert_equal Time.local(2006, 1, 2, 3, 4, 5), Time.construct(2006, 1, 2, 3, 4, 5)
+  end
+  
+  def test_second_overflow
+    assert_equal Time.local(2006, 1, 1, 0, 1, 30), Time.construct(2006, 1, 1, 0, 0, 90)
+    assert_equal Time.local(2006, 1, 1, 0, 5, 0), Time.construct(2006, 1, 1, 0, 0, 300)
+  end
+  
+  def test_minute_overflow
+    assert_equal Time.local(2006, 1, 1, 1, 30), Time.construct(2006, 1, 1, 0, 90)
+    assert_equal Time.local(2006, 1, 1, 5), Time.construct(2006, 1, 1, 0, 300)
+  end
+  
+  def test_hour_overflow
+    assert_equal Time.local(2006, 1, 2, 12), Time.construct(2006, 1, 1, 36)
+    assert_equal Time.local(2006, 1, 7), Time.construct(2006, 1, 1, 144)
+  end
+  
+  def test_day_overflow
+    assert_equal Time.local(2006, 2, 1), Time.construct(2006, 1, 32)
+    assert_equal Time.local(2006, 3, 5), Time.construct(2006, 2, 33)
+    assert_equal Time.local(2004, 3, 4), Time.construct(2004, 2, 33)
+    assert_equal Time.local(2000, 3, 5), Time.construct(2000, 2, 33)
+    
+    assert_nothing_raised do
+      Time.construct(2006, 1, 56)
+    end
+    
+    assert_raise(RuntimeError) do
+      Time.construct(2006, 1, 57)
+    end
+  end
+  
+  def test_month_overflow
+    assert_equal Time.local(2006, 1), Time.construct(2005, 13)
+    assert_equal Time.local(2005, 12), Time.construct(2000, 72)
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_Token.rb b/framework/Horde_Date_Parser/chronic/test/test_Token.rb
new file mode 100644 (file)
index 0000000..80463d1
--- /dev/null
@@ -0,0 +1,26 @@
+require 'chronic'
+require 'test/unit'
+
+class TestToken < Test::Unit::TestCase
+  
+  def setup
+    # Wed Aug 16 14:00:00 UTC 2006
+    @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  end
+  
+  def test_token
+    token = Chronic::Token.new('foo')
+    assert_equal 0, token.tags.size
+    assert !token.tagged?
+    token.tag("mytag")
+    assert_equal 1, token.tags.size
+    assert token.tagged?
+    assert_equal String, token.get_tag(String).class
+    token.tag(5)
+    assert_equal 2, token.tags.size
+    token.untag(String)
+    assert_equal 1, token.tags.size
+    assert_equal 'foo', token.word
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/chronic/test/test_parsing.rb b/framework/Horde_Date_Parser/chronic/test/test_parsing.rb
new file mode 100644 (file)
index 0000000..d2216d6
--- /dev/null
@@ -0,0 +1,614 @@
+require 'chronic'
+require 'time'
+require 'test/unit'
+
+class TestParsing < Test::Unit::TestCase
+  # Wed Aug 16 14:00:00 UTC 2006
+  TIME_2006_08_16_14_00_00 = Time.local(2006, 8, 16, 14, 0, 0, 0)
+  
+  def setup
+    @time_2006_08_16_14_00_00 = TIME_2006_08_16_14_00_00
+  end
+  
+  def test_parse_guess_dates
+    # rm_sd
+
+    time = parse_now("may 27")
+    assert_equal Time.local(2007, 5, 27, 12), time
+    
+    time = parse_now("may 28", :context => :past)
+    assert_equal Time.local(2006, 5, 28, 12), time
+    
+    time = parse_now("may 28 5pm", :context => :past)
+    assert_equal Time.local(2006, 5, 28, 17), time
+    
+    time = parse_now("may 28 at 5pm", :context => :past)
+    assert_equal Time.local(2006, 5, 28, 17), time
+    
+    time = parse_now("may 28 at 5:32.19pm", :context => :past)
+    assert_equal Time.local(2006, 5, 28, 17, 32, 19), time
+    
+    # rm_od
+    
+    time = parse_now("may 27th")
+    assert_equal Time.local(2007, 5, 27, 12), time
+    
+    time = parse_now("may 27th", :context => :past)
+    assert_equal Time.local(2006, 5, 27, 12), time
+    
+    time = parse_now("may 27th 5:00 pm", :context => :past)
+    assert_equal Time.local(2006, 5, 27, 17), time
+    
+    time = parse_now("may 27th at 5pm", :context => :past)
+    assert_equal Time.local(2006, 5, 27, 17), time
+    
+    time = parse_now("may 27th at 5", :ambiguous_time_range => :none)
+    assert_equal Time.local(2007, 5, 27, 5), time
+    
+    # rm_sy
+    
+    time = parse_now("June 1979")
+    assert_equal Time.local(1979, 6, 16, 0), time
+    
+    time = parse_now("dec 79")
+    assert_equal Time.local(1979, 12, 16, 12), time
+    
+    # rm_sd_sy
+    
+    time = parse_now("jan 3 2010")
+    assert_equal Time.local(2010, 1, 3, 12), time
+    
+    time = parse_now("jan 3 2010 midnight")
+    assert_equal Time.local(2010, 1, 4, 0), time
+    
+    time = parse_now("jan 3 2010 at midnight")
+    assert_equal Time.local(2010, 1, 4, 0), time
+    
+    time = parse_now("jan 3 2010 at 4", :ambiguous_time_range => :none)
+    assert_equal Time.local(2010, 1, 3, 4), time
+    
+    #time = parse_now("January 12, '00")
+    #assert_equal Time.local(2000, 1, 12, 12), time
+    
+    time = parse_now("may 27 79")
+    assert_equal Time.local(1979, 5, 27, 12), time
+    
+    time = parse_now("may 27 79 4:30")
+    assert_equal Time.local(1979, 5, 27, 16, 30), time
+    
+    time = parse_now("may 27 79 at 4:30", :ambiguous_time_range => :none)
+    assert_equal Time.local(1979, 5, 27, 4, 30), time
+    
+    # sd_rm_sy
+
+    time = parse_now("3 jan 2010")
+    assert_equal Time.local(2010, 1, 3, 12), time
+    
+    time = parse_now("3 jan 2010 4pm")
+    assert_equal Time.local(2010, 1, 3, 16), time
+    
+    # sm_sd_sy
+    
+    time = parse_now("5/27/1979")
+    assert_equal Time.local(1979, 5, 27, 12), time
+    
+    time = parse_now("5/27/1979 4am")
+    assert_equal Time.local(1979, 5, 27, 4), time
+    
+    # sd_sm_sy
+    
+    time = parse_now("27/5/1979")
+    assert_equal Time.local(1979, 5, 27, 12), time
+    
+    time = parse_now("27/5/1979 @ 0700")
+    assert_equal Time.local(1979, 5, 27, 7), time
+    
+    # sm_sy
+    
+    time = parse_now("05/06")
+    assert_equal Time.local(2006, 5, 16, 12), time
+    
+    time = parse_now("12/06")
+    assert_equal Time.local(2006, 12, 16, 12), time
+    
+    time = parse_now("13/06")
+    assert_equal nil, time
+    
+    # sy_sm_sd
+    
+    time = parse_now("2000-1-1")
+    assert_equal Time.local(2000, 1, 1, 12), time
+    
+    time = parse_now("2006-08-20")
+    assert_equal Time.local(2006, 8, 20, 12), time
+    
+    time = parse_now("2006-08-20 7pm")
+    assert_equal Time.local(2006, 8, 20, 19), time
+    
+    time = parse_now("2006-08-20 03:00")
+    assert_equal Time.local(2006, 8, 20, 3), time
+    
+    time = parse_now("2006-08-20 03:30:30")
+    assert_equal Time.local(2006, 8, 20, 3, 30, 30), time
+    
+    time = parse_now("2006-08-20 15:30:30")
+    assert_equal Time.local(2006, 8, 20, 15, 30, 30), time
+    
+    time = parse_now("2006-08-20 15:30.30")
+    assert_equal Time.local(2006, 8, 20, 15, 30, 30), time
+    
+    # rdn_rm_rd_rt_rtz_ry
+    
+    time = parse_now("Mon Apr 02 17:00:00 PDT 2007")
+    assert_equal Time.local(2007, 4, 2, 17), time
+    
+    now = Time.now
+    time = parse_now(now.to_s)
+    assert_equal now.to_s, time.to_s
+    
+    # rm_sd_rt
+    
+    #time = parse_now("jan 5 13:00")
+    #assert_equal Time.local(2007, 1, 5, 13), time
+    
+    # due to limitations of the Time class, these don't work
+    
+    time = parse_now("may 40")
+    assert_equal nil, time
+    
+    time = parse_now("may 27 40")
+    assert_equal nil, time
+    
+    time = parse_now("1800-08-20")
+    assert_equal nil, time
+  end
+  
+  def test_foo
+    Chronic.parse('two months ago this friday')
+  end
+
+  def test_parse_guess_r
+    time = parse_now("friday")
+    assert_equal Time.local(2006, 8, 18, 12), time
+    
+    time = parse_now("tue")
+    assert_equal Time.local(2006, 8, 22, 12), time
+    
+    time = parse_now("5")
+    assert_equal Time.local(2006, 8, 16, 17), time
+    
+    time = Chronic.parse("5", :now => Time.local(2006, 8, 16, 3, 0, 0, 0), :ambiguous_time_range => :none)
+    assert_equal Time.local(2006, 8, 16, 5), time
+    
+    time = parse_now("13:00")
+    assert_equal Time.local(2006, 8, 17, 13), time
+    
+    time = parse_now("13:45")
+    assert_equal Time.local(2006, 8, 17, 13, 45), time
+    
+    time = parse_now("november")
+    assert_equal Time.local(2006, 11, 16), time
+  end
+  
+  def test_parse_guess_rr
+    time = parse_now("friday 13:00")
+    assert_equal Time.local(2006, 8, 18, 13), time
+    
+    time = parse_now("monday 4:00")
+    assert_equal Time.local(2006, 8, 21, 16), time
+    
+    time = parse_now("sat 4:00", :ambiguous_time_range => :none)
+    assert_equal Time.local(2006, 8, 19, 4), time
+    
+    time = parse_now("sunday 4:20", :ambiguous_time_range => :none)
+    assert_equal Time.local(2006, 8, 20, 4, 20), time
+    
+    time = parse_now("4 pm")
+    assert_equal Time.local(2006, 8, 16, 16), time
+    
+    time = parse_now("4 am", :ambiguous_time_range => :none)
+    assert_equal Time.local(2006, 8, 16, 4), time
+    
+    time = parse_now("12 pm")
+    assert_equal Time.local(2006, 8, 16, 12), time
+    
+    time = parse_now("12:01 pm")
+    assert_equal Time.local(2006, 8, 16, 12, 1), time
+    
+    time = parse_now("12:01 am")
+    assert_equal Time.local(2006, 8, 16, 0, 1), time
+    
+    time = parse_now("12 am")
+    assert_equal Time.local(2006, 8, 16), time
+    
+    time = parse_now("4:00 in the morning")
+    assert_equal Time.local(2006, 8, 16, 4), time
+    
+    time = parse_now("november 4")
+    assert_equal Time.local(2006, 11, 4, 12), time
+    
+    time = parse_now("aug 24")
+    assert_equal Time.local(2006, 8, 24, 12), time
+  end
+  
+  def test_parse_guess_rrr
+    time = parse_now("friday 1 pm")
+    assert_equal Time.local(2006, 8, 18, 13), time
+    
+    time = parse_now("friday 11 at night")
+    assert_equal Time.local(2006, 8, 18, 23), time
+    
+    time = parse_now("friday 11 in the evening")
+    assert_equal Time.local(2006, 8, 18, 23), time
+    
+    time = parse_now("sunday 6am")
+    assert_equal Time.local(2006, 8, 20, 6), time
+    
+    time = parse_now("friday evening at 7")
+    assert_equal Time.local(2006, 8, 18, 19), time
+  end
+  
+  def test_parse_guess_gr
+    # year
+    
+    time = parse_now("this year")
+    assert_equal Time.local(2006, 10, 24, 12, 30), time
+    
+    time = parse_now("this year", :context => :past)
+    assert_equal Time.local(2006, 4, 24, 12, 30), time
+    
+    # month
+    
+    time = parse_now("this month")
+    assert_equal Time.local(2006, 8, 24, 12), time
+    
+    time = parse_now("this month", :context => :past)
+    assert_equal Time.local(2006, 8, 8, 12), time
+    
+    time = Chronic.parse("next month", :now => Time.local(2006, 11, 15))
+    assert_equal Time.local(2006, 12, 16, 12), time
+    
+    # month name
+    
+    time = parse_now("last november")
+    assert_equal Time.local(2005, 11, 16), time
+    
+    # fortnight
+    
+    time = parse_now("this fortnight")
+    assert_equal Time.local(2006, 8, 21, 19, 30), time
+    
+    time = parse_now("this fortnight", :context => :past)
+    assert_equal Time.local(2006, 8, 14, 19), time
+    
+    # week
+    
+    time = parse_now("this week")
+    assert_equal Time.local(2006, 8, 18, 7, 30), time
+    
+    time = parse_now("this week", :context => :past)
+    assert_equal Time.local(2006, 8, 14, 19), time
+    
+    # weekend
+    
+    time = parse_now("this weekend")
+    assert_equal Time.local(2006, 8, 20), time
+    
+    time = parse_now("this weekend", :context => :past)
+    assert_equal Time.local(2006, 8, 13), time
+    
+    time = parse_now("last weekend")
+    assert_equal Time.local(2006, 8, 13), time
+    
+    # day
+    
+    time = parse_now("this day")
+    assert_equal Time.local(2006, 8, 16, 19, 30), time
+    
+    time = parse_now("this day", :context => :past)
+    assert_equal Time.local(2006, 8, 16, 7), time
+    
+    time = parse_now("today")
+    assert_equal Time.local(2006, 8, 16, 19, 30), time
+    
+    time = parse_now("yesterday")
+    assert_equal Time.local(2006, 8, 15, 12), time
+    
+    time = parse_now("tomorrow")
+    assert_equal Time.local(2006, 8, 17, 12), time
+    
+    # day name
+    
+    time = parse_now("this tuesday")
+    assert_equal Time.local(2006, 8, 22, 12), time
+    
+    time = parse_now("next tuesday")
+    assert_equal Time.local(2006, 8, 22, 12), time
+    
+    time = parse_now("last tuesday")
+    assert_equal Time.local(2006, 8, 15, 12), time
+    
+    time = parse_now("this wed")
+    assert_equal Time.local(2006, 8, 23, 12), time
+    
+    time = parse_now("next wed")
+    assert_equal Time.local(2006, 8, 23, 12), time
+    
+    time = parse_now("last wed")
+    assert_equal Time.local(2006, 8, 9, 12), time
+    
+    # day portion
+    
+    time = parse_now("this morning")
+    assert_equal Time.local(2006, 8, 16, 9), time
+    
+    time = parse_now("tonight")
+    assert_equal Time.local(2006, 8, 16, 22), time
+    
+    # minute
+    
+    time = parse_now("next minute")
+    assert_equal Time.local(2006, 8, 16, 14, 1, 30), time
+    
+    # second
+    
+    time = parse_now("this second")
+    assert_equal Time.local(2006, 8, 16, 14), time
+    
+    time = parse_now("this second", :context => :past)
+    assert_equal Time.local(2006, 8, 16, 14), time
+    
+    time = parse_now("next second")
+    assert_equal Time.local(2006, 8, 16, 14, 0, 1), time
+    
+    time = parse_now("last second")
+    assert_equal Time.local(2006, 8, 16, 13, 59, 59), time
+  end
+  
+  def test_parse_guess_grr    
+    time = parse_now("yesterday at 4:00")
+    assert_equal Time.local(2006, 8, 15, 16), time
+    
+    time = parse_now("today at 9:00")
+    assert_equal Time.local(2006, 8, 16, 9), time
+    
+    time = parse_now("today at 2100")
+    assert_equal Time.local(2006, 8, 16, 21), time
+    
+    time = parse_now("this day at 0900")
+    assert_equal Time.local(2006, 8, 16, 9), time
+    
+    time = parse_now("tomorrow at 0900")
+    assert_equal Time.local(2006, 8, 17, 9), time
+    
+    time = parse_now("yesterday at 4:00", :ambiguous_time_range => :none)
+    assert_equal Time.local(2006, 8, 15, 4), time
+    
+    time = parse_now("last friday at 4:00")
+    assert_equal Time.local(2006, 8, 11, 16), time
+    
+    time = parse_now("next wed 4:00")
+    assert_equal Time.local(2006, 8, 23, 16), time
+    
+    time = parse_now("yesterday afternoon")
+    assert_equal Time.local(2006, 8, 15, 15), time
+    
+    time = parse_now("last week tuesday")
+    assert_equal Time.local(2006, 8, 8, 12), time
+    
+    time = parse_now("tonight at 7")
+    assert_equal Time.local(2006, 8, 16, 19), time
+    
+    time = parse_now("tonight 7")
+    assert_equal Time.local(2006, 8, 16, 19), time
+    
+    time = parse_now("7 tonight")
+    assert_equal Time.local(2006, 8, 16, 19), time
+  end
+    
+  def test_parse_guess_grrr
+    time = parse_now("today at 6:00pm")
+    assert_equal Time.local(2006, 8, 16, 18), time
+    
+    time = parse_now("today at 6:00am")
+    assert_equal Time.local(2006, 8, 16, 6), time
+    
+    time = parse_now("this day 1800")
+    assert_equal Time.local(2006, 8, 16, 18), time
+    
+    time = parse_now("yesterday at 4:00pm")
+    assert_equal Time.local(2006, 8, 15, 16), time
+    
+    time = parse_now("tomorrow evening at 7")
+    assert_equal Time.local(2006, 8, 17, 19), time
+    
+    time = parse_now("tomorrow morning at 5:30")
+    assert_equal Time.local(2006, 8, 17, 5, 30), time
+    
+    time = parse_now("next monday at 12:01 am")
+    assert_equal Time.local(2006, 8, 21, 00, 1), time
+    
+    time = parse_now("next monday at 12:01 pm")
+    assert_equal Time.local(2006, 8, 21, 12, 1), time
+  end
+  
+  def test_parse_guess_rgr
+    time = parse_now("afternoon yesterday")
+    assert_equal Time.local(2006, 8, 15, 15), time
+    
+    time = parse_now("tuesday last week")
+    assert_equal Time.local(2006, 8, 8, 12), time
+  end
+  
+  def test_parse_guess_s_r_p
+    # past
+    
+    time = parse_now("3 years ago")
+    assert_equal Time.local(2003, 8, 16, 14), time
+    
+    time = parse_now("1 month ago")
+    assert_equal Time.local(2006, 7, 16, 14), time
+    
+    time = parse_now("1 fortnight ago")
+    assert_equal Time.local(2006, 8, 2, 14), time
+    
+    time = parse_now("2 fortnights ago")
+    assert_equal Time.local(2006, 7, 19, 14), time
+    
+    time = parse_now("3 weeks ago")
+    assert_equal Time.local(2006, 7, 26, 14), time
+    
+    time = parse_now("2 weekends ago")
+    assert_equal Time.local(2006, 8, 5), time
+    
+    time = parse_now("3 days ago")
+    assert_equal Time.local(2006, 8, 13, 14), time
+    
+    #time = parse_now("1 monday ago")
+    #assert_equal Time.local(2006, 8, 14, 12), time
+    
+    time = parse_now("5 mornings ago")
+    assert_equal Time.local(2006, 8, 12, 9), time
+    
+    time = parse_now("7 hours ago")
+    assert_equal Time.local(2006, 8, 16, 7), time
+    
+    time = parse_now("3 minutes ago")
+    assert_equal Time.local(2006, 8, 16, 13, 57), time
+    
+    time = parse_now("20 seconds before now")
+    assert_equal Time.local(2006, 8, 16, 13, 59, 40), time
+
+    # future
+    
+    time = parse_now("3 years from now")
+    assert_equal Time.local(2009, 8, 16, 14, 0, 0), time
+    
+    time = parse_now("6 months hence")
+    assert_equal Time.local(2007, 2, 16, 14), time
+    
+    time = parse_now("3 fortnights hence")
+    assert_equal Time.local(2006, 9, 27, 14), time
+    
+    time = parse_now("1 week from now")
+    assert_equal Time.local(2006, 8, 23, 14, 0, 0), time
+    
+    time = parse_now("1 weekend from now")
+    assert_equal Time.local(2006, 8, 19), time
+    
+    time = parse_now("2 weekends from now")
+    assert_equal Time.local(2006, 8, 26), time
+    
+    time = parse_now("1 day hence")
+    assert_equal Time.local(2006, 8, 17, 14), time
+    
+    time = parse_now("5 mornings hence")
+    assert_equal Time.local(2006, 8, 21, 9), time
+    
+    time = parse_now("1 hour from now")
+    assert_equal Time.local(2006, 8, 16, 15), time
+    
+    time = parse_now("20 minutes hence")
+    assert_equal Time.local(2006, 8, 16, 14, 20), time
+    
+    time = parse_now("20 seconds from now")
+    assert_equal Time.local(2006, 8, 16, 14, 0, 20), time
+    
+    time = Chronic.parse("2 months ago", :now => Time.parse("2007-03-07 23:30"))
+    assert_equal Time.local(2007, 1, 7, 23, 30), time
+  end
+  
+  def test_parse_guess_p_s_r
+    time = parse_now("in 3 hours")
+    assert_equal Time.local(2006, 8, 16, 17), time
+  end
+  
+  def test_parse_guess_s_r_p_a
+    # past
+    
+    time = parse_now("3 years ago tomorrow")
+    assert_equal Time.local(2003, 8, 17, 12), time
+    
+    time = parse_now("3 years ago this friday")
+    assert_equal Time.local(2003, 8, 18, 12), time
+    
+    time = parse_now("3 months ago saturday at 5:00 pm")
+    assert_equal Time.local(2006, 5, 19, 17), time
+    
+    time = parse_now("2 days from this second")
+    assert_equal Time.local(2006, 8, 18, 14), time
+    
+    time = parse_now("7 hours before tomorrow at midnight")
+    assert_equal Time.local(2006, 8, 17, 17), time
+    
+    # future
+  end
+  
+  def test_parse_guess_o_r_s_r
+    time = parse_now("3rd wednesday in november")
+    assert_equal Time.local(2006, 11, 15, 12), time
+    
+    time = parse_now("10th wednesday in november")
+    assert_equal nil, time
+    
+    # time = parse_now("3rd wednesday in 2007")
+    # assert_equal Time.local(2007, 1, 20, 12), time
+  end
+  
+  def test_parse_guess_o_r_g_r
+    time = parse_now("3rd month next year")
+    assert_equal Time.local(2007, 3, 16, 12, 30), time
+    
+    time = parse_now("3rd thursday this september")
+    assert_equal Time.local(2006, 9, 21, 12), time
+    
+    time = parse_now("4th day last week")
+    assert_equal Time.local(2006, 8, 9, 12), time
+  end
+  
+  def test_parse_guess_nonsense
+    time = parse_now("some stupid nonsense")
+    assert_equal nil, time
+  end
+  
+  def test_parse_span
+    span = parse_now("friday", :guess => false)
+    assert_equal Time.local(2006, 8, 18), span.begin
+    assert_equal Time.local(2006, 8, 19), span.end
+    
+    span = parse_now("november", :guess => false)
+    assert_equal Time.local(2006, 11), span.begin
+    assert_equal Time.local(2006, 12), span.end
+    
+    span = Chronic.parse("weekend" , :now => @time_2006_08_16_14_00_00, :guess => false)
+    assert_equal Time.local(2006, 8, 19), span.begin
+    assert_equal Time.local(2006, 8, 21), span.end
+  end
+  
+  def test_parse_words
+    assert_equal parse_now("33 days from now"), parse_now("thirty-three days from now")
+    assert_equal parse_now("2867532 seconds from now"), parse_now("two million eight hundred and sixty seven thousand five hundred and thirty two seconds from now")
+    assert_equal parse_now("may 10th"), parse_now("may tenth")
+  end
+  
+  def test_parse_only_complete_pointers
+    assert_equal parse_now("eat pasty buns today at 2pm"), @time_2006_08_16_14_00_00
+    assert_equal parse_now("futuristically speaking today at 2pm"), @time_2006_08_16_14_00_00
+    assert_equal parse_now("meeting today at 2pm"), @time_2006_08_16_14_00_00
+  end
+  
+  def test_argument_validation
+    assert_raise(Chronic::InvalidArgumentException) do
+      time = Chronic.parse("may 27", :foo => :bar)
+    end
+    
+    assert_raise(Chronic::InvalidArgumentException) do
+      time = Chronic.parse("may 27", :context => :bar)
+    end
+  end
+  
+  private
+  def parse_now(string, options={})
+    Chronic.parse(string, {:now => TIME_2006_08_16_14_00_00 }.merge(options))
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser.php
new file mode 100644 (file)
index 0000000..477240d
--- /dev/null
@@ -0,0 +1,53 @@
+
+alias p_orig p
+
+def p(val)
+  p_orig val
+  puts
+end
+
+class Time
+  def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
+    if second >= 60
+      minute += second / 60
+      second = second % 60
+    end
+    
+    if minute >= 60
+      hour += minute / 60
+      minute = minute % 60
+    end
+    
+    if hour >= 24
+      day += hour / 24
+      hour = hour % 24
+    end
+    
+    # determine if there is a day overflow. this is complicated by our crappy calendar
+    # system (non-constant number of days per month)
+    day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
+    if day > 28
+      # no month ever has fewer than 28 days, so only do this if necessary
+      leap_year = (year % 4 == 0) && !(year % 100 == 0)
+      leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+      common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+      days_this_month = leap_year ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
+      if day > days_this_month
+        month += day / days_this_month
+        day = day % days_this_month
+      end
+    end
+    
+    if month > 12
+      if month % 12 == 0
+        year += (month - 12) / 12
+        month = 12
+      else
+        year += month / 12
+        month = month % 12
+      end
+    end
+    
+    Time.local(year, month, day, hour, minute, second)
+  end
+end
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Grabber.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Grabber.php
new file mode 100644 (file)
index 0000000..4162a26
--- /dev/null
@@ -0,0 +1,26 @@
+#module Chronic
+
+  class Chronic::Grabber < Chronic::Tag #:nodoc:
+    def self.scan(tokens)
+      tokens.each_index do |i|
+        if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end
+      end
+      tokens
+    end
+    
+    def self.scan_for_all(token)
+      scanner = {/last/ => :last,
+                 /this/ => :this,
+                 /next/ => :next}
+      scanner.keys.each do |scanner_item|
+        return self.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def to_s
+      'grabber-' << @type.to_s
+    end
+  end
+
+#end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Handlers.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Handlers.php
new file mode 100644 (file)
index 0000000..551d632
--- /dev/null
@@ -0,0 +1,469 @@
+module Chronic
+
+       class << self
+         
+         def definitions #:nodoc:
+           @definitions ||= 
+      {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
+        
+       :date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
+                 Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
+                 Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
+                 Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
+                 Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
+                 Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
+                 Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
+                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
+                 Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
+                 Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
+                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
+                 
+       # tonight at 7pm
+       :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
+                   Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
+                   Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
+                   
+       # 3 weeks from now, in 2 months
+       :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
+                  Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
+                  Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
+                  
+       # 3rd week in march
+       :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
+                   Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
+      }
+    end
+    
+    def tokens_to_span(tokens, options) #:nodoc:                   
+      # maybe it's a specific date
+      
+      self.definitions[:date].each do |handler|
+        if handler.match(tokens, self.definitions)
+          puts "-date" if Chronic.debug
+          good_tokens = tokens.select { |o| !o.get_tag Separator }
+          return self.send(handler.handler_method, good_tokens, options)
+        end
+      end
+            
+      # I guess it's not a specific date, maybe it's just an anchor
+            
+      self.definitions[:anchor].each do |handler|
+        if handler.match(tokens, self.definitions)
+          puts "-anchor" if Chronic.debug
+          good_tokens = tokens.select { |o| !o.get_tag Separator }
+          return self.send(handler.handler_method, good_tokens, options)
+        end
+      end
+            
+      # not an anchor, perhaps it's an arrow
+      
+      self.definitions[:arrow].each do |handler|
+        if handler.match(tokens, self.definitions)
+          puts "-arrow" if Chronic.debug
+          good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
+          return self.send(handler.handler_method, good_tokens, options)
+        end
+      end
+      
+      # not an arrow, let's hope it's a narrow
+      
+      self.definitions[:narrow].each do |handler|
+        if handler.match(tokens, self.definitions)
+          puts "-narrow" if Chronic.debug
+          #good_tokens = tokens.select { |o| !o.get_tag Separator }
+          return self.send(handler.handler_method, tokens, options)
+        end
+      end
+      
+      # I guess you're out of luck!
+      puts "-none" if Chronic.debug
+      return nil
+    end
+    
+    #--------------
+    
+    def day_or_time(day_start, time_tokens, options)
+      outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
+      
+      if !time_tokens.empty?
+        @now = outer_span.begin
+        time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
+        return time
+      else
+        return outer_span
+      end
+    end
+    
+    #--------------
+    
+    def handle_m_d(month, day, time_tokens, options) #:nodoc:
+      month.start = @now
+      span = month.this(options[:context])
+      
+      day_start = Time.local(span.begin.year, span.begin.month, day)
+      
+      day_or_time(day_start, time_tokens, options)
+    end
+    
+    def handle_rmn_sd(tokens, options) #:nodoc:
+      handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options)
+    end
+    
+    def handle_rmn_od(tokens, options) #:nodoc:
+      handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options)
+    end
+    
+    def handle_rmn_sy(tokens, options) #:nodoc:
+      month = tokens[0].get_tag(RepeaterMonthName).index
+      year = tokens[1].get_tag(ScalarYear).type
+      
+      if month == 12
+        next_month_year = year + 1
+        next_month_month = 1
+      else
+        next_month_year = year
+        next_month_month = month + 1
+      end
+      
+      begin
+        Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
+      rescue ArgumentError
+        nil
+      end
+    end
+    
+    def handle_rdn_rmn_sd_t_tz_sy(tokens, options) #:nodoc:
+      month = tokens[1].get_tag(RepeaterMonthName).index
+      day = tokens[2].get_tag(ScalarDay).type
+      year = tokens[5].get_tag(ScalarYear).type
+      
+      begin
+        day_start = Time.local(year, month, day)
+        day_or_time(day_start, [tokens[3]], options)
+      rescue ArgumentError
+        nil
+      end
+    end
+    
+    def handle_rmn_sd_sy(tokens, options) #:nodoc:
+      month = tokens[0].get_tag(RepeaterMonthName).index
+      day = tokens[1].get_tag(ScalarDay).type
+      year = tokens[2].get_tag(ScalarYear).type
+      
+      time_tokens = tokens.last(tokens.size - 3)
+      
+      begin
+        day_start = Time.local(year, month, day)
+        day_or_time(day_start, time_tokens, options)
+      rescue ArgumentError
+        nil
+      end
+    end
+    
+    def handle_sd_rmn_sy(tokens, options) #:nodoc:
+      new_tokens = [tokens[1], tokens[0], tokens[2]]
+      time_tokens = tokens.last(tokens.size - 3)
+      self.handle_rmn_sd_sy(new_tokens + time_tokens, options)
+    end
+    
+    def handle_sm_sd_sy(tokens, options) #:nodoc:
+      month = tokens[0].get_tag(ScalarMonth).type
+      day = tokens[1].get_tag(ScalarDay).type
+      year = tokens[2].get_tag(ScalarYear).type
+      
+      time_tokens = tokens.last(tokens.size - 3)
+      
+      begin
+        day_start = Time.local(year, month, day) #:nodoc:
+        day_or_time(day_start, time_tokens, options)
+      rescue ArgumentError
+        nil
+      end
+    end
+    
+    def handle_sd_sm_sy(tokens, options) #:nodoc:
+      new_tokens = [tokens[1], tokens[0], tokens[2]]
+      time_tokens = tokens.last(tokens.size - 3)
+      self.handle_sm_sd_sy(new_tokens + time_tokens, options)
+    end
+    
+    def handle_sy_sm_sd(tokens, options) #:nodoc:
+      new_tokens = [tokens[1], tokens[2], tokens[0]]
+      time_tokens = tokens.last(tokens.size - 3)
+      self.handle_sm_sd_sy(new_tokens + time_tokens, options)
+    end
+    
+    def handle_sm_sy(tokens, options) #:nodoc:
+      month = tokens[0].get_tag(ScalarMonth).type
+      year = tokens[1].get_tag(ScalarYear).type
+      
+      if month == 12
+        next_month_year = year + 1
+        next_month_month = 1
+      else
+        next_month_year = year
+        next_month_month = month + 1
+      end
+      
+      begin
+        Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
+      rescue ArgumentError
+        nil
+      end
+    end
+    
+    # anchors
+    
+    def handle_r(tokens, options) #:nodoc:
+      dd_tokens = dealias_and_disambiguate_times(tokens, options)
+      self.get_anchor(dd_tokens, options)
+    end
+    
+    def handle_r_g_r(tokens, options) #:nodoc:
+      new_tokens = [tokens[1], tokens[0], tokens[2]]
+      self.handle_r(new_tokens, options)
+    end
+    
+    # arrows
+    
+    def handle_srp(tokens, span, options) #:nodoc:
+      distance = tokens[0].get_tag(Scalar).type
+      repeater = tokens[1].get_tag(Repeater)
+      pointer = tokens[2].get_tag(Pointer).type
+      
+      repeater.offset(span, distance, pointer)
+    end
+    
+    def handle_s_r_p(tokens, options) #:nodoc:
+      repeater = tokens[1].get_tag(Repeater)
+            
+      # span = 
+      # case true
+      # when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class)
+      #   self.parse("this hour", :guess => false, :now => @now)
+      # when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class)
+      #   self.parse("this minute", :guess => false, :now => @now)
+      # when [RepeaterMinute, RepeaterSecond].include?(repeater.class)
+      #   self.parse("this second", :guess => false, :now => @now)
+      # else
+      #   raise(ChronicPain, "Invalid repeater: #{repeater.class}")
+      # end
+      
+      span = self.parse("this second", :guess => false, :now => @now)
+      
+      self.handle_srp(tokens, span, options)
+    end
+    
+    def handle_p_s_r(tokens, options) #:nodoc:
+      new_tokens = [tokens[1], tokens[2], tokens[0]]
+      self.handle_s_r_p(new_tokens, options)
+    end
+    
+    def handle_s_r_p_a(tokens, options) #:nodoc:
+      anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
+      self.handle_srp(tokens, anchor_span, options)
+    end
+    
+    # narrows
+    
+    def handle_orr(tokens, outer_span, options) #:nodoc:
+      repeater = tokens[1].get_tag(Repeater)
+      repeater.start = outer_span.begin - 1
+      ordinal = tokens[0].get_tag(Ordinal).type
+      span = nil
+      ordinal.times do
+        span = repeater.next(:future)
+        if span.begin > outer_span.end
+          span = nil
+          break
+        end
+      end
+      span
+    end
+    
+    def handle_o_r_s_r(tokens, options) #:nodoc:
+      outer_span = get_anchor([tokens[3]], options)
+      handle_orr(tokens[0..1], outer_span, options)
+    end
+    
+    def handle_o_r_g_r(tokens, options) #:nodoc:
+      outer_span = get_anchor(tokens[2..3], options)
+      handle_orr(tokens[0..1], outer_span, options)
+    end
+    
+    # support methods
+    
+    def get_anchor(tokens, options) #:nodoc:
+      grabber = Grabber.new(:this)
+      pointer = :future
+      
+      repeaters = self.get_repeaters(tokens)
+      repeaters.size.times { tokens.pop }
+                    
+      if tokens.first && tokens.first.get_tag(Grabber)
+        grabber = tokens.first.get_tag(Grabber)
+        tokens.pop
+      end
+      
+      head = repeaters.shift
+      head.start = @now
+                  
+      case grabber.type
+        when :last
+          outer_span = head.next(:past)
+        when :this
+          if repeaters.size > 0
+            outer_span = head.this(:none)
+          else
+            outer_span = head.this(options[:context])
+          end
+        when :next
+          outer_span = head.next(:future)
+        else raise(ChronicPain, "Invalid grabber")
+      end
+      
+      puts "--#{outer_span}" if Chronic.debug
+      anchor = find_within(repeaters, outer_span, pointer)
+    end
+    
+    def get_repeaters(tokens) #:nodoc:
+      repeaters = []
+                 tokens.each do |token|
+                   if t = token.get_tag(Repeater)
+          repeaters << t
+        end
+      end
+      repeaters.sort.reverse
+    end
+    
+    # Recursively finds repeaters within other repeaters.
+    # Returns a Span representing the innermost time span
+    # or nil if no repeater union could be found
+    def find_within(tags, span, pointer) #:nodoc:
+      puts "--#{span}" if Chronic.debug
+      return span if tags.empty?
+      
+      head, *rest = tags
+      head.start = pointer == :future ? span.begin : span.end
+      h = head.this(:none)
+            
+      if span.include?(h.begin) || span.include?(h.end)
+        return find_within(rest, h, pointer)
+      else
+        return nil
+      end
+    end
+    
+    def dealias_and_disambiguate_times(tokens, options) #:nodoc:
+      # handle aliases of am/pm
+      # 5:00 in the morning -> 5:00 am
+      # 7:00 in the evening -> 7:00 pm
+      
+      day_portion_index = nil
+      tokens.each_with_index do |t, i|
+        if t.get_tag(RepeaterDayPortion)
+          day_portion_index = i
+          break
+        end
+      end
+       
+      time_index = nil
+      tokens.each_with_index do |t, i|
+        if t.get_tag(RepeaterTime)
+          time_index = i
+          break
+        end
+      end
+      
+      if (day_portion_index && time_index)
+        t1 = tokens[day_portion_index]
+        t1tag = t1.get_tag(RepeaterDayPortion)
+      
+        if [:morning].include?(t1tag.type)
+          puts '--morning->am' if Chronic.debug
+          t1.untag(RepeaterDayPortion)
+          t1.tag(RepeaterDayPortion.new(:am))
+        elsif [:afternoon, :evening, :night].include?(t1tag.type)
+          puts "--#{t1tag.type}->pm" if Chronic.debug
+          t1.untag(RepeaterDayPortion)
+          t1.tag(RepeaterDayPortion.new(:pm))
+        end
+      end
+      
+      # tokens.each_with_index do |t0, i|
+      #   t1 = tokens[i + 1]
+      #   if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime)
+      #     if [:morning].include?(t1tag.type)
+      #       puts '--morning->am' if Chronic.debug
+      #       t1.untag(RepeaterDayPortion)
+      #       t1.tag(RepeaterDayPortion.new(:am))
+      #     elsif [:afternoon, :evening, :night].include?(t1tag.type)
+      #       puts "--#{t1tag.type}->pm" if Chronic.debug
+      #       t1.untag(RepeaterDayPortion)
+      #       t1.tag(RepeaterDayPortion.new(:pm))
+      #     end
+      #   end
+      # end
+            
+      # handle ambiguous times if :ambiguous_time_range is specified
+      if options[:ambiguous_time_range] != :none
+        ttokens = []
+        tokens.each_with_index do |t0, i|
+          ttokens << t0
+          t1 = tokens[i + 1]
+          if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion))
+            distoken = Token.new('disambiguator')
+            distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
+            ttokens << distoken
+          end
+        end
+        tokens = ttokens
+      end
+      
+      tokens
+    end
+    
+  end
+  
+  class Handler #:nodoc:
+    attr_accessor :pattern, :handler_method
+    
+    def initialize(pattern, handler_method)
+      @pattern = pattern
+      @handler_method = handler_method
+    end
+    
+    def constantize(name)
+      camel = name.to_s.gsub(/(^|_)(.)/) { $2.upcase }
+      ::Chronic.module_eval(camel, __FILE__, __LINE__)
+    end
+    
+    def match(tokens, definitions)
+      token_index = 0
+      @pattern.each do |element|
+        name = element.to_s
+        optional = name.reverse[0..0] == '?'
+        name = name.chop if optional
+        if element.instance_of? Symbol
+          klass = constantize(name)
+          match = tokens[token_index] && !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty?
+          return false if !match && !optional
+          (token_index += 1; next) if match
+          next if !match && optional
+        elsif element.instance_of? String
+          return true if optional && token_index == tokens.size
+          sub_handlers = definitions[name.intern] || raise(ChronicPain, "Invalid subset #{name} specified")
+          sub_handlers.each do |sub_handler|
+            return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
+          end
+          return false
+        else
+          raise(ChronicPain, "Invalid match type: #{element.class}")
+        end
+      end
+      return false if token_index != tokens.size
+      return true
+    end
+  end
+  
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Ordinal.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Ordinal.php
new file mode 100644 (file)
index 0000000..45b8148
--- /dev/null
@@ -0,0 +1,40 @@
+module Chronic
+
+  class Ordinal < Tag #:nodoc:
+    def self.scan(tokens)
+      # for each token
+      tokens.each_index do |i|
+        if t = self.scan_for_ordinals(tokens[i]) then tokens[i].tag(t) end
+        if t = self.scan_for_days(tokens[i]) then tokens[i].tag(t) end
+      end
+      tokens
+    end
+  
+    def self.scan_for_ordinals(token)
+      if token.word =~ /^(\d*)(st|nd|rd|th)$/
+        return Ordinal.new($1.to_i)
+      end
+      return nil
+    end
+    
+    def self.scan_for_days(token)
+      if token.word =~ /^(\d*)(st|nd|rd|th)$/
+        unless $1.to_i > 31
+          return OrdinalDay.new(token.word.to_i)
+        end
+      end
+      return nil
+    end
+    
+    def to_s
+      'ordinal'
+    end
+  end
+  
+  class OrdinalDay < Ordinal #:nodoc:
+    def to_s
+      super << '-day-' << @type.to_s
+    end
+  end
+
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Parser.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Parser.php
new file mode 100644 (file)
index 0000000..5e7779f
--- /dev/null
@@ -0,0 +1,239 @@
+module Chronic
+  class << self
+    
+    # Parses a string containing a natural language date or time. If the parser
+    # can find a date or time, either a Time or Chronic::Span will be returned 
+    # (depending on the value of <tt>:guess</tt>). If no date or time can be found,
+    # +nil+ will be returned.
+    #
+    # Options are:
+    #
+    # [<tt>:context</tt>]
+    #     <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
+    #
+    #     If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt> 
+    #     and if an ambiguous string is given, it will assume it is in the 
+    #     past. Specify <tt>:future</tt> or omit to set a future context.
+    #
+    # [<tt>:now</tt>]
+    #     Time (defaults to Time.now)
+    #
+    #     By setting <tt>:now</tt> to a Time, all computations will be based off
+    #     of that time instead of Time.now
+    #
+    # [<tt>:guess</tt>]
+    #     +true+ or +false+ (defaults to +true+)
+    #
+    #     By default, the parser will guess a single point in time for the
+    #     given date or time. If you'd rather have the entire time span returned,
+    #     set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
+    #     
+    # [<tt>:ambiguous_time_range</tt>]
+    #     Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
+    #
+    #     If an Integer is given, ambiguous times (like 5:00) will be 
+    #     assumed to be within the range of that time in the AM to that time
+    #     in the PM. For example, if you set it to <tt>7</tt>, then the parser will
+    #     look for the time between 7am and 7pm. In the case of 5:00, it would
+    #     assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
+    #     will be made, and the first matching instance of that time will 
+    #     be used.
+    def parse(text, specified_options = {})
+      # get options and set defaults if necessary
+      default_options = {:context => :future,
+                         :now => Time.now,
+                         :guess => true,
+                         :ambiguous_time_range => 6}
+      options = default_options.merge specified_options
+            
+      # ensure the specified options are valid
+      specified_options.keys.each do |key|
+        default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
+      end
+      [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
+      
+      # store now for later =)
+      @now = options[:now]
+      
+      # put the text into a normal format to ease scanning
+      text = self.pre_normalize(text)
+          
+      # get base tokens for each word
+      @tokens = self.base_tokenize(text)
+    
+      # scan the tokens with each token scanner
+      [Repeater].each do |tokenizer|
+        @tokens = tokenizer.scan(@tokens, options)
+      end
+      
+      [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer|
+        @tokens = tokenizer.scan(@tokens)
+      end
+      
+      # strip any non-tagged tokens
+      @tokens = @tokens.select { |token| token.tagged? }
+      
+      if Chronic.debug
+        puts "+---------------------------------------------------"
+        puts "| " + @tokens.to_s
+        puts "+---------------------------------------------------"
+      end
+      
+      # do the heavy lifting
+      begin
+        span = self.tokens_to_span(@tokens, options)
+      rescue
+        raise
+        return nil
+      end
+      
+      # guess a time within a span if required
+      if options[:guess]
+        return self.guess(span)
+      else
+        return span
+      end
+    end
+    
+    # Clean up the specified input text by stripping unwanted characters,
+    # converting idioms to their canonical form, converting number words
+    # to numbers (three => 3), and converting ordinal words to numeric
+    # ordinals (third => 3rd)
+    def pre_normalize(text) #:nodoc:
+      normalized_text = text.to_s.downcase
+      normalized_text = numericize_numbers(normalized_text)
+      normalized_text.gsub!(/['"\.]/, '')
+      normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
+      normalized_text.gsub!(/\btoday\b/, 'this day')
+      normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day')
+      normalized_text.gsub!(/\byesterday\b/, 'last day')
+      normalized_text.gsub!(/\bnoon\b/, '12:00')
+      normalized_text.gsub!(/\bmidnight\b/, '24:00')
+      normalized_text.gsub!(/\bbefore now\b/, 'past')
+      normalized_text.gsub!(/\bnow\b/, 'this second')
+      normalized_text.gsub!(/\b(ago|before)\b/, 'past')
+      normalized_text.gsub!(/\bthis past\b/, 'last')
+      normalized_text.gsub!(/\bthis last\b/, 'last')
+      normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
+      normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
+      normalized_text.gsub!(/\btonight\b/, 'this night')
+      normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1')
+      normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
+      normalized_text = numericize_ordinals(normalized_text)
+    end
+  
+    # Convert number words to numbers (three => 3)
+    def numericize_numbers(text) #:nodoc:
+      Numerizer.numerize(text)
+    end
+  
+    # Convert ordinal words to numeric ordinals (third => 3rd)
+    def numericize_ordinals(text) #:nodoc:
+      text
+    end
+  
+    # Split the text on spaces and convert each word into
+    # a Token
+    def base_tokenize(text) #:nodoc:
+      text.split(' ').map { |word| Token.new(word) }
+    end
+    
+    # Guess a specific time within the given span
+    def guess(span) #:nodoc:
+      return nil if span.nil?
+      if span.width > 1
+        span.begin + (span.width / 2)
+      else
+        span.begin
+      end
+    end
+  end
+  
+  class Token #:nodoc:
+    attr_accessor :word, :tags
+    
+    def initialize(word)
+      @word = word
+      @tags = []
+    end
+    
+    # Tag this token with the specified tag
+    def tag(new_tag)
+      @tags << new_tag
+    end
+    
+    # Remove all tags of the given class
+    def untag(tag_class)
+      @tags = @tags.select { |m| !m.kind_of? tag_class }
+    end
+    
+    # Return true if this token has any tags
+    def tagged?
+      @tags.size > 0
+    end
+    
+    # Return the Tag that matches the given class
+    def get_tag(tag_class)
+      matches = @tags.select { |m| m.kind_of? tag_class }
+      #matches.size < 2 || raise("Multiple identical tags found")
+      return matches.first
+    end
+    
+    # Print this Token in a pretty way
+    def to_s
+      @word << '(' << @tags.join(', ') << ') '
+    end
+  end
+  
+  # A Span represents a range of time. Since this class extends
+  # Range, you can use #begin and #end to get the beginning and
+  # ending times of the span (they will be of class Time)
+  class Span < Range   
+    # Returns the width of this span in seconds   
+    def width
+      (self.end - self.begin).to_i
+    end
+    
+    # Add a number of seconds to this span, returning the 
+    # resulting Span
+    def +(seconds)
+      Span.new(self.begin + seconds, self.end + seconds)
+    end
+    
+    # Subtract a number of seconds to this span, returning the 
+    # resulting Span
+    def -(seconds)
+      self + -seconds
+    end
+    
+    # Prints this span in a nice fashion
+    def to_s
+      '(' << self.begin.to_s << '..' << self.end.to_s << ')'
+    end
+  end
+
+  # Tokens are tagged with subclassed instances of this class when
+  # they match specific criteria
+  class Tag #:nodoc:
+    attr_accessor :type
+    
+    def initialize(type)
+      @type = type
+    end
+    
+    def start=(s)
+      @now = s
+    end
+  end
+  
+  # Internal exception
+  class ChronicPain < Exception #:nodoc:
+    
+  end
+  
+  # This exception is raised if an invalid argument is provided to
+  # any of Chronic's methods
+  class InvalidArgumentException < Exception
+    
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Pointer.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Pointer.php
new file mode 100644 (file)
index 0000000..224efaf
--- /dev/null
@@ -0,0 +1,27 @@
+module Chronic
+
+  class Pointer < Tag #:nodoc:
+    def self.scan(tokens)
+      # for each token
+      tokens.each_index do |i|
+        if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t) end
+      end
+      tokens
+    end
+  
+    def self.scan_for_all(token)
+      scanner = {/\bpast\b/ => :past,
+                 /\bfuture\b/ => :future,
+                 /\bin\b/ => :future}
+      scanner.keys.each do |scanner_item|
+        return self.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def to_s
+      'pointer-' << @type.to_s
+    end
+  end
+
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeater.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeater.php
new file mode 100644 (file)
index 0000000..9f80daf
--- /dev/null
@@ -0,0 +1,115 @@
+class Chronic::Repeater < Chronic::Tag #:nodoc:
+  def self.scan(tokens, options)
+    # for each token
+    tokens.each_index do |i|
+      if t = self.scan_for_month_names(tokens[i]) then tokens[i].tag(t); next end
+      if t = self.scan_for_day_names(tokens[i]) then tokens[i].tag(t); next end
+      if t = self.scan_for_day_portions(tokens[i]) then tokens[i].tag(t); next end
+      if t = self.scan_for_times(tokens[i], options) then tokens[i].tag(t); next end
+      if t = self.scan_for_units(tokens[i]) then tokens[i].tag(t); next end
+    end
+    tokens
+  end
+  
+  def self.scan_for_month_names(token)
+    scanner = {/^jan\.?(uary)?$/ => :january,
+               /^feb\.?(ruary)?$/ => :february,
+               /^mar\.?(ch)?$/ => :march,
+               /^apr\.?(il)?$/ => :april,
+               /^may$/ => :may,
+               /^jun\.?e?$/ => :june,
+               /^jul\.?y?$/ => :july,
+               /^aug\.?(ust)?$/ => :august,
+               /^sep\.?(t\.?|tember)?$/ => :september,
+               /^oct\.?(ober)?$/ => :october,
+               /^nov\.?(ember)?$/ => :november,
+               /^dec\.?(ember)?$/ => :december}
+    scanner.keys.each do |scanner_item|
+      return Chronic::RepeaterMonthName.new(scanner[scanner_item]) if scanner_item =~ token.word
+    end
+    return nil
+  end
+  
+  def self.scan_for_day_names(token)
+    scanner = {/^m[ou]n(day)?$/ => :monday,
+               /^t(ue|eu|oo|u|)s(day)?$/ => :tuesday,
+               /^tue$/ => :tuesday,
+               /^we(dnes|nds|nns)day$/ => :wednesday,
+               /^wed$/ => :wednesday,
+               /^th(urs|ers)day$/ => :thursday,
+               /^thu$/ => :thursday,
+               /^fr[iy](day)?$/ => :friday,
+               /^sat(t?[ue]rday)?$/ => :saturday,
+               /^su[nm](day)?$/ => :sunday}
+    scanner.keys.each do |scanner_item|
+      return Chronic::RepeaterDayName.new(scanner[scanner_item]) if scanner_item =~ token.word
+    end
+    return nil
+  end
+  
+  def self.scan_for_day_portions(token)
+    scanner = {/^ams?$/ => :am,
+               /^pms?$/ => :pm,
+               /^mornings?$/ => :morning,
+               /^afternoons?$/ => :afternoon,
+               /^evenings?$/ => :evening,
+               /^(night|nite)s?$/ => :night}
+    scanner.keys.each do |scanner_item|
+      return Chronic::RepeaterDayPortion.new(scanner[scanner_item]) if scanner_item =~ token.word
+    end
+    return nil
+  end
+  
+  def self.scan_for_times(token, options)
+    if token.word =~ /^\d{1,2}(:?\d{2})?([\.:]?\d{2})?$/
+      return Chronic::RepeaterTime.new(token.word, options)
+    end
+    return nil
+  end
+  
+  def self.scan_for_units(token)
+    scanner = {/^years?$/ => :year,
+               /^seasons?$/ => :season,
+               /^months?$/ => :month,
+               /^fortnights?$/ => :fortnight,
+               /^weeks?$/ => :week,
+               /^weekends?$/ => :weekend,
+               /^days?$/ => :day,
+               /^hours?$/ => :hour,
+               /^minutes?$/ => :minute,
+               /^seconds?$/ => :second}
+    scanner.keys.each do |scanner_item|
+      if scanner_item =~ token.word
+        klass_name = 'Chronic::Repeater' + scanner[scanner_item].to_s.capitalize
+        klass = eval(klass_name)
+        return klass.new(scanner[scanner_item]) 
+      end
+    end
+    return nil
+  end
+  
+  def <=>(other)
+    width <=> other.width
+  end
+  
+  # returns the width (in seconds or months) of this repeatable.
+  def width
+    raise("Repeatable#width must be overridden in subclasses")
+  end
+  
+  # returns the next occurance of this repeatable.
+  def next(pointer)
+    !@now.nil? || raise("Start point must be set before calling #next")
+    [:future, :none, :past].include?(pointer) || raise("First argument 'pointer' must be one of :past or :future")
+    #raise("Repeatable#next must be overridden in subclasses")
+  end
+  
+  def this(pointer)
+    !@now.nil? || raise("Start point must be set before calling #this")
+    [:future, :past, :none].include?(pointer) || raise("First argument 'pointer' must be one of :past, :future, :none")
+  end
+  
+  def to_s
+    'repeater'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Day.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Day.php
new file mode 100644 (file)
index 0000000..a92d83f
--- /dev/null
@@ -0,0 +1,47 @@
+class Chronic::RepeaterDay < Chronic::Repeater #:nodoc:
+  DAY_SECONDS = 86_400 # (24 * 60 * 60)
+  
+  def next(pointer)
+    super
+    
+    if !@current_day_start
+      @current_day_start = Time.local(@now.year, @now.month, @now.day)
+    end
+    
+    direction = pointer == :future ? 1 : -1
+    @current_day_start += direction * DAY_SECONDS
+    
+    Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      day_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1)
+      day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS
+    when :past
+      day_begin = Time.construct(@now.year, @now.month, @now.day)
+      day_end = Time.construct(@now.year, @now.month, @now.day, @now.hour)
+    when :none
+      day_begin = Time.construct(@now.year, @now.month, @now.day)
+      day_end = Time.construct(@now.year, @now.month, @now.day) + DAY_SECONDS
+    end
+    
+    Chronic::Span.new(day_begin, day_end)
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * DAY_SECONDS
+  end
+  
+  def width
+    DAY_SECONDS
+  end
+  
+  def to_s
+    super << '-day'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayName.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayName.php
new file mode 100644 (file)
index 0000000..0486a4d
--- /dev/null
@@ -0,0 +1,46 @@
+class Chronic::RepeaterDayName < Chronic::Repeater #:nodoc:
+  DAY_SECONDS = 86400 # (24 * 60 * 60)
+  
+  def next(pointer)
+    super
+    
+    direction = pointer == :future ? 1 : -1
+    
+    if !@current_day_start
+      @current_day_start = Time.construct(@now.year, @now.month, @now.day)
+      @current_day_start += direction * DAY_SECONDS
+
+      day_num = symbol_to_number(@type)
+      
+      while @current_day_start.wday != day_num
+        @current_day_start += direction * DAY_SECONDS
+      end
+    else
+      @current_day_start += direction * 7 * DAY_SECONDS
+    end
+    
+    Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    pointer = :future if pointer == :none
+    self.next(pointer)
+  end
+  
+  def width
+    DAY_SECONDS
+  end
+  
+  def to_s
+    super << '-dayname-' << @type.to_s
+  end
+  
+  private
+  
+  def symbol_to_number(sym)
+    lookup = {:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6}
+    lookup[sym] || raise("Invalid symbol specified")
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayPortion.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/DayPortion.php
new file mode 100644 (file)
index 0000000..c854933
--- /dev/null
@@ -0,0 +1,93 @@
+class Chronic::RepeaterDayPortion < Chronic::Repeater #:nodoc:
+  @@morning = (6 * 60 * 60)..(12 * 60 * 60) # 6am-12am
+  @@afternoon = (13 * 60 * 60)..(17 * 60 * 60) # 1pm-5pm
+  @@evening = (17 * 60 * 60)..(20 * 60 * 60) # 5pm-8pm
+  @@night = (20 * 60 * 60)..(24 * 60 * 60) # 8pm-12pm
+  
+  def initialize(type)
+    super
+    
+    if type.kind_of? Integer
+      @range = (@type * 60 * 60)..((@type + 12) * 60 * 60)
+    else
+      lookup = {:am => 0..(12 * 60 * 60 - 1),
+                :pm => (12 * 60 * 60)..(24 * 60 * 60 - 1),
+                :morning => @@morning,
+                :afternoon => @@afternoon,
+                :evening => @@evening,
+                :night => @@night}
+      @range = lookup[type]
+      lookup[type] || raise("Invalid type '#{type}' for RepeaterDayPortion")
+    end
+    @range || raise("Range should have been set by now")
+  end
+  
+  def next(pointer)
+    super
+    
+    full_day = 60 * 60 * 24
+    
+    if !@current_span
+      now_seconds = @now - Time.construct(@now.year, @now.month, @now.day)
+      if now_seconds < @range.begin
+        case pointer
+        when :future
+          range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin
+        when :past
+          range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin
+        end
+      elsif now_seconds > @range.end
+        case pointer
+        when :future
+          range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin
+        when :past
+          range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin
+        end
+      else
+        case pointer
+        when :future
+          range_start = Time.construct(@now.year, @now.month, @now.day) + full_day + @range.begin
+        when :past
+          range_start = Time.construct(@now.year, @now.month, @now.day) - full_day + @range.begin
+        end
+      end
+      
+      @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin))
+    else
+      case pointer
+      when :future
+        @current_span += full_day
+      when :past
+        @current_span -= full_day
+      end
+    end
+  end
+  
+  def this(context = :future)
+    super
+    
+    range_start = Time.construct(@now.year, @now.month, @now.day) + @range.begin
+    @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin))
+  end
+  
+  def offset(span, amount, pointer)
+    @now = span.begin
+    portion_span = self.next(pointer)
+    direction = pointer == :future ? 1 : -1
+    portion_span + (direction * (amount - 1) * Chronic::RepeaterDay::DAY_SECONDS)
+  end
+  
+  def width
+    @range || raise("Range has not been set")
+    return @current_span.width if @current_span
+    if @type.kind_of? Integer
+      return (12 * 60 * 60)
+    else
+      @range.end - @range.begin
+    end
+  end
+  
+  def to_s
+    super << '-dayportion-' << @type.to_s
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Fortnight.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Fortnight.php
new file mode 100644 (file)
index 0000000..058fbb9
--- /dev/null
@@ -0,0 +1,65 @@
+class Chronic::RepeaterFortnight < Chronic::Repeater #:nodoc:
+  FORTNIGHT_SECONDS = 1_209_600 # (14 * 24 * 60 * 60)
+  
+  def next(pointer)
+    super
+    
+    if !@current_fortnight_start
+      case pointer
+      when :future
+        sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+        sunday_repeater.start = @now
+        next_sunday_span = sunday_repeater.next(:future)
+        @current_fortnight_start = next_sunday_span.begin
+      when :past
+        sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+        sunday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS)
+        2.times { sunday_repeater.next(:past) }
+        last_sunday_span = sunday_repeater.next(:past)
+        @current_fortnight_start = last_sunday_span.begin
+      end
+    else
+      direction = pointer == :future ? 1 : -1
+      @current_fortnight_start += direction * FORTNIGHT_SECONDS
+    end
+    
+    Chronic::Span.new(@current_fortnight_start, @current_fortnight_start + FORTNIGHT_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    pointer = :future if pointer == :none
+    
+    case pointer
+    when :future
+      this_fortnight_start = Time.construct(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS
+      sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+      sunday_repeater.start = @now
+      sunday_repeater.this(:future)
+      this_sunday_span = sunday_repeater.this(:future)
+      this_fortnight_end = this_sunday_span.begin
+      Chronic::Span.new(this_fortnight_start, this_fortnight_end)
+    when :past
+      this_fortnight_end = Time.construct(@now.year, @now.month, @now.day, @now.hour)
+      sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+      sunday_repeater.start = @now
+      last_sunday_span = sunday_repeater.next(:past)
+      this_fortnight_start = last_sunday_span.begin
+      Chronic::Span.new(this_fortnight_start, this_fortnight_end)
+    end
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * FORTNIGHT_SECONDS
+  end
+
+  def width
+    FORTNIGHT_SECONDS
+  end
+  
+  def to_s
+    super << '-fortnight'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Hour.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Hour.php
new file mode 100644 (file)
index 0000000..f38a3f8
--- /dev/null
@@ -0,0 +1,52 @@
+class Chronic::RepeaterHour < Chronic::Repeater #:nodoc:
+  HOUR_SECONDS = 3600 # 60 * 60
+  
+  def next(pointer)
+    super
+    
+    if !@current_hour_start
+      case pointer
+      when :future
+        @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1)
+      when :past
+        @current_hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour - 1)
+      end
+    else
+      direction = pointer == :future ? 1 : -1
+      @current_hour_start += direction * HOUR_SECONDS
+    end
+    
+    Chronic::Span.new(@current_hour_start, @current_hour_start + HOUR_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1)
+      hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour + 1)
+    when :past
+      hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour)
+      hour_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
+    when :none
+      hour_start = Time.construct(@now.year, @now.month, @now.day, @now.hour)
+      hour_end = hour_begin + HOUR_SECONDS
+    end
+    
+    Chronic::Span.new(hour_start, hour_end)
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * HOUR_SECONDS
+  end
+  
+  def width
+    HOUR_SECONDS
+  end
+  
+  def to_s
+    super << '-hour'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Minute.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Minute.php
new file mode 100644 (file)
index 0000000..342d3cd
--- /dev/null
@@ -0,0 +1,52 @@
+class Chronic::RepeaterMinute < Chronic::Repeater #:nodoc:
+  MINUTE_SECONDS = 60
+  
+  def next(pointer = :future)
+    super
+    
+    if !@current_minute_start
+      case pointer
+      when :future
+        @current_minute_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min + 1)
+      when :past
+        @current_minute_start = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min - 1)
+      end
+    else
+      direction = pointer == :future ? 1 : -1
+      @current_minute_start += direction * MINUTE_SECONDS
+    end
+    
+    Chronic::Span.new(@current_minute_start, @current_minute_start + MINUTE_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      minute_begin = @now
+      minute_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
+    when :past
+      minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
+      minute_end = @now
+    when :none
+      minute_begin = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min)
+      minute_end = Time.construct(@now.year, @now.month, @now.day, @now.hour, @now.min) + MINUTE_SECONDS
+    end
+    
+    Chronic::Span.new(minute_begin, minute_end)
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * MINUTE_SECONDS
+  end
+  
+  def width
+    MINUTE_SECONDS
+  end
+  
+  def to_s
+    super << '-minute'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Month.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Month.php
new file mode 100644 (file)
index 0000000..edd89ee
--- /dev/null
@@ -0,0 +1,61 @@
+class Chronic::RepeaterMonth < Chronic::Repeater #:nodoc:
+  MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
+  YEAR_MONTHS = 12
+  
+  def next(pointer)
+    super
+    
+    if !@current_month_start
+      @current_month_start = offset_by(Time.construct(@now.year, @now.month), 1, pointer)
+    else
+      @current_month_start = offset_by(Time.construct(@current_month_start.year, @current_month_start.month), 1, pointer)
+    end
+    
+    Chronic::Span.new(@current_month_start, Time.construct(@current_month_start.year, @current_month_start.month + 1))
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      month_start = Time.construct(@now.year, @now.month, @now.day + 1)
+      month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future)
+    when :past
+      month_start = Time.construct(@now.year, @now.month)
+      month_end = Time.construct(@now.year, @now.month, @now.day)
+    when :none
+      month_start = Time.construct(@now.year, @now.month)
+      month_end = self.offset_by(Time.construct(@now.year, @now.month), 1, :future)
+    end
+    
+    Chronic::Span.new(month_start, month_end)
+  end
+  
+  def offset(span, amount, pointer)      
+    Chronic::Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer))
+  end
+  
+  def offset_by(time, amount, pointer) 
+    direction = pointer == :future ? 1 : -1
+    
+    amount_years = direction * amount / YEAR_MONTHS
+    amount_months = direction * amount % YEAR_MONTHS
+    
+    new_year = time.year + amount_years
+    new_month = time.month + amount_months
+    if new_month > YEAR_MONTHS
+      new_year += 1
+      new_month -= YEAR_MONTHS
+    end
+    Time.construct(new_year, new_month, time.day, time.hour, time.min, time.sec)
+  end
+  
+  def width
+    MONTH_SECONDS
+  end
+  
+  def to_s
+    super << '-month'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/MonthName.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/MonthName.php
new file mode 100644 (file)
index 0000000..1f8b748
--- /dev/null
@@ -0,0 +1,93 @@
+class Chronic::RepeaterMonthName < Chronic::Repeater #:nodoc:
+  MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
+  
+  def next(pointer)
+    super
+    
+    if !@current_month_begin
+      target_month = symbol_to_number(@type)
+      case pointer
+      when :future
+        if @now.month < target_month
+          @current_month_begin = Time.construct(@now.year, target_month)
+        else @now.month > target_month
+          @current_month_begin = Time.construct(@now.year + 1, target_month)
+        end
+      when :none
+        if @now.month <= target_month
+          @current_month_begin = Time.construct(@now.year, target_month)
+        else @now.month > target_month
+          @current_month_begin = Time.construct(@now.year + 1, target_month)
+        end
+      when :past
+        if @now.month > target_month
+          @current_month_begin = Time.construct(@now.year, target_month)
+        else @now.month < target_month
+          @current_month_begin = Time.construct(@now.year - 1, target_month)
+        end
+      end
+      @current_month_begin || raise("Current month should be set by now")
+    else
+      case pointer
+      when :future
+        @current_month_begin = Time.construct(@current_month_begin.year + 1, @current_month_begin.month)
+      when :past
+        @current_month_begin = Time.construct(@current_month_begin.year - 1, @current_month_begin.month)
+      end
+    end
+    
+    cur_month_year = @current_month_begin.year
+    cur_month_month = @current_month_begin.month
+    
+    if cur_month_month == 12
+      next_month_year = cur_month_year + 1
+      next_month_month = 1
+    else
+      next_month_year = cur_month_year
+      next_month_month = cur_month_month + 1
+    end
+      
+    Chronic::Span.new(@current_month_begin, Time.construct(next_month_year, next_month_month))
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :past
+      self.next(pointer)
+    when :future, :none
+      self.next(:none)
+    end
+  end
+  
+  def width
+    MONTH_SECONDS
+  end
+  
+  def index
+    symbol_to_number(@type)
+  end
+  
+  def to_s
+    super << '-monthname-' << @type.to_s
+  end
+  
+  private
+  
+  def symbol_to_number(sym)
+    lookup = {:january => 1,
+              :february => 2,
+              :march => 3,
+              :april => 4,
+              :may => 5,
+              :june => 6,
+              :july => 7,
+              :august => 8,
+              :september => 9,
+              :october => 10,
+              :november => 11,
+              :december => 12}
+    lookup[sym] || raise("Invalid symbol specified")
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Season.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Season.php
new file mode 100644 (file)
index 0000000..a255865
--- /dev/null
@@ -0,0 +1,23 @@
+class Chronic::RepeaterSeason < Chronic::Repeater #:nodoc:
+  SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
+  
+  def next(pointer)
+    super
+    
+    raise 'Not implemented'
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    raise 'Not implemented'
+  end
+  
+  def width
+    SEASON_SECONDS
+  end
+  
+  def to_s
+    super << '-season'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/SeasonName.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/SeasonName.php
new file mode 100644 (file)
index 0000000..adfd1f2
--- /dev/null
@@ -0,0 +1,24 @@
+class Chronic::RepeaterSeasonName < Chronic::RepeaterSeason #:nodoc:
+  @summer = ['jul 21', 'sep 22']
+  @autumn = ['sep 23', 'dec 21']
+  @winter = ['dec 22', 'mar 19']
+  @spring = ['mar 20', 'jul 20']
+  
+  def next(pointer)
+    super
+    raise 'Not implemented'
+  end
+  
+  def this(pointer = :future)
+    super
+    raise 'Not implemented'
+  end
+  
+  def width
+    (91 * 24 * 60 * 60)
+  end
+  
+  def to_s
+    super << '-season-' << @type.to_s
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Second.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Second.php
new file mode 100644 (file)
index 0000000..6d05545
--- /dev/null
@@ -0,0 +1,36 @@
+class Chronic::RepeaterSecond < Chronic::Repeater #:nodoc:
+  SECOND_SECONDS = 1 # haha, awesome
+  
+  def next(pointer = :future)
+    super
+    
+    direction = pointer == :future ? 1 : -1
+    
+    if !@second_start
+      @second_start = @now + (direction * SECOND_SECONDS)
+    else
+      @second_start += SECOND_SECONDS * direction
+    end
+    
+    Chronic::Span.new(@second_start, @second_start + SECOND_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    Chronic::Span.new(@now, @now + 1)
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * SECOND_SECONDS
+  end
+  
+  def width
+    SECOND_SECONDS
+  end
+  
+  def to_s
+    super << '-second'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Time.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Time.php
new file mode 100644 (file)
index 0000000..f856014
--- /dev/null
@@ -0,0 +1,117 @@
+class Chronic::RepeaterTime < Chronic::Repeater #:nodoc:
+  class Tick #:nodoc:
+    attr_accessor :time
+    
+    def initialize(time, ambiguous = false)
+      @time = time
+      @ambiguous = ambiguous
+    end
+    
+    def ambiguous?
+      @ambiguous
+    end
+    
+    def *(other)
+      Tick.new(@time * other, @ambiguous)
+    end
+    
+    def to_f
+      @time.to_f
+    end
+    
+    def to_s
+      @time.to_s + (@ambiguous ? '?' : '')
+    end
+  end
+  
+  def initialize(time, options = {})
+    t = time.gsub(/\:/, '')
+    @type = 
+    if (1..2) === t.size
+      hours = t.to_i
+      hours == 12 ? Tick.new(0 * 60 * 60, true) : Tick.new(hours * 60 * 60, true)
+    elsif t.size == 3
+      Tick.new((t[0..0].to_i * 60 * 60) + (t[1..2].to_i * 60), true)
+    elsif t.size == 4
+      ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12
+      hours = t[0..1].to_i
+      hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60, ambiguous)
+    elsif t.size == 5
+      Tick.new(t[0..0].to_i * 60 * 60 + t[1..2].to_i * 60 + t[3..4].to_i, true)
+    elsif t.size == 6
+      ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12
+      hours = t[0..1].to_i
+      hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous)
+    else
+      raise("Time cannot exceed six digits")
+    end
+  end
+  
+  # Return the next past or future Span for the time that this Repeater represents
+  #   pointer - Symbol representing which temporal direction to fetch the next day
+  #             must be either :past or :future
+  def next(pointer)
+    super
+    
+    half_day = 60 * 60 * 12
+    full_day = 60 * 60 * 24
+    
+    first = false
+    
+    unless @current_time
+      first = true
+      midnight = Time.local(@now.year, @now.month, @now.day)
+      yesterday_midnight = midnight - full_day
+      tomorrow_midnight = midnight + full_day
+
+      catch :done do
+        if pointer == :future
+          if @type.ambiguous?
+            [midnight + @type, midnight + half_day + @type, tomorrow_midnight + @type].each do |t|
+              (@current_time = t; throw :done) if t >= @now
+            end
+          else
+            [midnight + @type, tomorrow_midnight + @type].each do |t|
+              (@current_time = t; throw :done) if t >= @now
+            end
+          end
+        else # pointer == :past
+          if @type.ambiguous?
+            [midnight + half_day + @type, midnight + @type, yesterday_midnight + @type * 2].each do |t|
+              (@current_time = t; throw :done) if t <= @now
+            end
+          else
+            [midnight + @type, yesterday_midnight + @type].each do |t|
+              (@current_time = t; throw :done) if t <= @now
+            end
+          end
+        end
+      end
+      
+      @current_time || raise("Current time cannot be nil at this point")
+    end
+    
+    unless first
+      increment = @type.ambiguous? ? half_day : full_day
+      @current_time += pointer == :future ? increment : -increment
+    end
+    
+    Chronic::Span.new(@current_time, @current_time + width)
+  end
+  
+  def this(context = :future)
+    super
+    
+    context = :future if context == :none
+    
+    self.next(context)
+  end
+  
+  def width
+    1
+  end
+  
+  def to_s
+    super << '-time-' << @type.to_s
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Week.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Week.php
new file mode 100644 (file)
index 0000000..ec88ff1
--- /dev/null
@@ -0,0 +1,68 @@
+class Chronic::RepeaterWeek < Chronic::Repeater #:nodoc:
+  WEEK_SECONDS = 604800 # (7 * 24 * 60 * 60)
+  
+  def next(pointer)
+    super
+    
+    if !@current_week_start
+      case pointer
+      when :future
+        sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+        sunday_repeater.start = @now
+        next_sunday_span = sunday_repeater.next(:future)
+        @current_week_start = next_sunday_span.begin
+      when :past
+        sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+        sunday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS)
+        sunday_repeater.next(:past)
+        last_sunday_span = sunday_repeater.next(:past)
+        @current_week_start = last_sunday_span.begin
+      end
+    else
+      direction = pointer == :future ? 1 : -1
+      @current_week_start += direction * WEEK_SECONDS
+    end
+    
+    Chronic::Span.new(@current_week_start, @current_week_start + WEEK_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      this_week_start = Time.local(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS
+      sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+      sunday_repeater.start = @now
+      this_sunday_span = sunday_repeater.this(:future)
+      this_week_end = this_sunday_span.begin
+      Chronic::Span.new(this_week_start, this_week_end)
+    when :past
+      this_week_end = Time.local(@now.year, @now.month, @now.day, @now.hour)
+      sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+      sunday_repeater.start = @now
+      last_sunday_span = sunday_repeater.next(:past)
+      this_week_start = last_sunday_span.begin
+      Chronic::Span.new(this_week_start, this_week_end)
+    when :none
+      sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
+      sunday_repeater.start = @now
+      last_sunday_span = sunday_repeater.next(:past)
+      this_week_start = last_sunday_span.begin
+      Chronic::Span.new(this_week_start, this_week_start + WEEK_SECONDS)
+    end
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    span + direction * amount * WEEK_SECONDS
+  end
+  
+  def width
+    WEEK_SECONDS
+  end
+  
+  def to_s
+    super << '-week'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Weekend.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Weekend.php
new file mode 100644 (file)
index 0000000..f012267
--- /dev/null
@@ -0,0 +1,60 @@
+class Chronic::RepeaterWeekend < Chronic::Repeater #:nodoc:
+  WEEKEND_SECONDS = 172_800 # (2 * 24 * 60 * 60)
+  
+  def next(pointer)
+    super
+    
+    if !@current_week_start
+      case pointer
+      when :future
+        saturday_repeater = Chronic::RepeaterDayName.new(:saturday)
+        saturday_repeater.start = @now
+        next_saturday_span = saturday_repeater.next(:future)
+        @current_week_start = next_saturday_span.begin
+      when :past
+        saturday_repeater = Chronic::RepeaterDayName.new(:saturday)
+        saturday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS)
+        last_saturday_span = saturday_repeater.next(:past)
+        @current_week_start = last_saturday_span.begin
+      end
+    else
+      direction = pointer == :future ? 1 : -1
+      @current_week_start += direction * Chronic::RepeaterWeek::WEEK_SECONDS
+    end
+    
+    Chronic::Span.new(@current_week_start, @current_week_start + WEEKEND_SECONDS)
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future, :none
+      saturday_repeater = Chronic::RepeaterDayName.new(:saturday)
+      saturday_repeater.start = @now
+      this_saturday_span = saturday_repeater.this(:future)
+      Chronic::Span.new(this_saturday_span.begin, this_saturday_span.begin + WEEKEND_SECONDS)
+    when :past
+      saturday_repeater = Chronic::RepeaterDayName.new(:saturday)
+      saturday_repeater.start = @now
+      last_saturday_span = saturday_repeater.this(:past)
+      Chronic::Span.new(last_saturday_span.begin, last_saturday_span.begin + WEEKEND_SECONDS)
+    end
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    weekend = Chronic::RepeaterWeekend.new(:weekend)
+    weekend.start = span.begin
+    start = weekend.next(pointer).begin + (amount - 1) * direction * Chronic::RepeaterWeek::WEEK_SECONDS
+    Chronic::Span.new(start, start + (span.end - span.begin))
+  end
+  
+  def width
+    WEEKEND_SECONDS
+  end
+  
+  def to_s
+    super << '-weekend'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Year.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Repeaters/Year.php
new file mode 100644 (file)
index 0000000..426371f
--- /dev/null
@@ -0,0 +1,58 @@
+class Chronic::RepeaterYear < Chronic::Repeater #:nodoc:
+  
+  def next(pointer)
+    super
+    
+    if !@current_year_start
+      case pointer
+      when :future
+        @current_year_start = Time.construct(@now.year + 1)
+      when :past
+        @current_year_start = Time.construct(@now.year - 1)
+      end
+    else
+      diff = pointer == :future ? 1 : -1
+      @current_year_start = Time.construct(@current_year_start.year + diff)
+    end
+    
+    Chronic::Span.new(@current_year_start, Time.construct(@current_year_start.year + 1))
+  end
+  
+  def this(pointer = :future)
+    super
+    
+    case pointer
+    when :future
+      this_year_start = Time.construct(@now.year, @now.month, @now.day) + Chronic::RepeaterDay::DAY_SECONDS
+      this_year_end = Time.construct(@now.year + 1, 1, 1)
+    when :past
+      this_year_start = Time.construct(@now.year, 1, 1)
+      this_year_end = Time.construct(@now.year, @now.month, @now.day)
+    when :none
+      this_year_start = Time.construct(@now.year, 1, 1)
+      this_year_end = Time.construct(@now.year + 1, 1, 1)
+    end
+    
+    Chronic::Span.new(this_year_start, this_year_end)
+  end
+  
+  def offset(span, amount, pointer)
+    direction = pointer == :future ? 1 : -1
+    
+    sb = span.begin
+    new_begin = Time.construct(sb.year + (amount * direction), sb.month, sb.day, sb.hour, sb.min, sb.sec)
+    
+    se = span.end
+    new_end = Time.construct(se.year + (amount * direction), se.month, se.day, se.hour, se.min, se.sec)
+    
+    Chronic::Span.new(new_begin, new_end)
+  end
+  
+  def width
+    (365 * 24 * 60 * 60)
+  end
+  
+  def to_s
+    super << '-year'
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Scalar.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Scalar.php
new file mode 100644 (file)
index 0000000..b08cfee
--- /dev/null
@@ -0,0 +1,74 @@
+module Chronic
+
+  class Scalar < Tag #:nodoc:
+    def self.scan(tokens)
+      # for each token
+      tokens.each_index do |i|
+        if t = self.scan_for_scalars(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
+        if t = self.scan_for_days(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
+        if t = self.scan_for_months(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
+        if t = self.scan_for_years(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
+      end
+      tokens
+    end
+  
+    def self.scan_for_scalars(token, post_token)
+      if token.word =~ /^\d*$/
+        unless post_token && %w{am pm morning afternoon evening night}.include?(post_token)
+          return Scalar.new(token.word.to_i)
+        end
+      end
+      return nil
+    end
+    
+    def self.scan_for_days(token, post_token)
+      if token.word =~ /^\d\d?$/
+        unless token.word.to_i > 31 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token))
+          return ScalarDay.new(token.word.to_i)
+        end
+      end
+      return nil
+    end
+    
+    def self.scan_for_months(token, post_token)
+      if token.word =~ /^\d\d?$/
+        unless token.word.to_i > 12 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token))
+          return ScalarMonth.new(token.word.to_i)
+        end
+      end
+      return nil
+    end
+    
+    def self.scan_for_years(token, post_token)
+      if token.word =~ /^([1-9]\d)?\d\d?$/
+        unless post_token && %w{am pm morning afternoon evening night}.include?(post_token)
+          return ScalarYear.new(token.word.to_i)
+        end
+      end
+      return nil
+    end
+    
+    def to_s
+      'scalar'
+    end
+  end
+  
+  class ScalarDay < Scalar #:nodoc:
+    def to_s
+      super << '-day-' << @type.to_s
+    end
+  end
+  
+  class ScalarMonth < Scalar #:nodoc:
+    def to_s
+      super << '-month-' << @type.to_s
+    end
+  end
+  
+  class ScalarYear < Scalar #:nodoc:
+    def to_s
+      super << '-year-' << @type.to_s
+    end
+  end
+
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Separator.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/Separator.php
new file mode 100644 (file)
index 0000000..86c56e3
--- /dev/null
@@ -0,0 +1,76 @@
+module Chronic
+
+  class Separator < Tag #:nodoc:
+    def self.scan(tokens)
+      tokens.each_index do |i|
+        if t = self.scan_for_commas(tokens[i]) then tokens[i].tag(t); next end
+        if t = self.scan_for_slash_or_dash(tokens[i]) then tokens[i].tag(t); next end
+        if t = self.scan_for_at(tokens[i]) then tokens[i].tag(t); next end
+        if t = self.scan_for_in(tokens[i]) then tokens[i].tag(t); next end
+      end
+      tokens
+    end
+    
+    def self.scan_for_commas(token)
+      scanner = {/^,$/ => :comma}
+      scanner.keys.each do |scanner_item|
+        return SeparatorComma.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def self.scan_for_slash_or_dash(token)
+      scanner = {/^-$/ => :dash,
+                 /^\/$/ => :slash}
+      scanner.keys.each do |scanner_item|
+        return SeparatorSlashOrDash.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def self.scan_for_at(token)
+      scanner = {/^(at|@)$/ => :at}
+      scanner.keys.each do |scanner_item|
+        return SeparatorAt.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def self.scan_for_in(token)
+      scanner = {/^in$/ => :in}
+      scanner.keys.each do |scanner_item|
+        return SeparatorIn.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+    
+    def to_s
+      'separator'
+    end
+  end
+  
+  class SeparatorComma < Separator #:nodoc:
+    def to_s
+      super << '-comma'
+    end
+  end
+  
+  class SeparatorSlashOrDash < Separator #:nodoc:
+    def to_s
+      super << '-slashordash-' << @type.to_s
+    end
+  end
+  
+  class SeparatorAt < Separator #:nodoc:
+    def to_s
+      super << '-at'
+    end
+  end
+  
+  class SeparatorIn < Separator #:nodoc:
+    def to_s
+      super << '-in'
+    end
+  end
+
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/lib/Horde/Date/Parser/TimeZone.php b/framework/Horde_Date_Parser/lib/Horde/Date/Parser/TimeZone.php
new file mode 100644 (file)
index 0000000..41041ef
--- /dev/null
@@ -0,0 +1,22 @@
+module Chronic
+  class TimeZone < Tag #:nodoc:    
+    def self.scan(tokens)
+      tokens.each_index do |i|
+        if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end
+      end
+      tokens
+    end
+
+    def self.scan_for_all(token)
+      scanner = {/[PMCE][DS]T/i => :tz}
+      scanner.keys.each do |scanner_item|
+        return self.new(scanner[scanner_item]) if scanner_item =~ token.word
+      end
+      return nil
+    end
+
+    def to_s
+      'timezone'
+    end
+  end
+end
\ No newline at end of file
diff --git a/framework/Horde_Date_Parser/package.xml b/framework/Horde_Date_Parser/package.xml
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/framework/Horde_Date_Parser/test/Horde/Date/Parser/AllTests.php b/framework/Horde_Date_Parser/test/Horde/Date/Parser/AllTests.php
new file mode 100644 (file)
index 0000000..7fac8b1
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Support
+ * @subpackage UnitTests
+ * @copyright  2008 The Horde Project (http://www.horde.org/)
+ * @license    http://opensource.org/licenses/bsd-license.php
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_Support_AllTests::main');
+}
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+class Horde_Support_AllTests {
+
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        set_include_path(dirname(__FILE__) . '/../../../lib' . PATH_SEPARATOR . get_include_path());
+        if (!spl_autoload_functions()) {
+            spl_autoload_register(create_function('$class', '$filename = str_replace(array(\'::\', \'_\'), \'/\', $class); include "$filename.php";'));
+        }
+
+        $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Horde_Support');
+
+        $basedir = dirname(__FILE__);
+        $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+            if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+                $pathname = $file->getPathname();
+                require $pathname;
+
+                $class = str_replace(DIRECTORY_SEPARATOR, '_',
+                                     preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+                $suite->addTestSuite('Horde_Support_' . $class);
+            }
+        }
+
+        return $suite;
+    }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Support_AllTests::main') {
+    Horde_Support_AllTests::main();
+}
diff --git a/hippo/SPEC.txt b/hippo/SPEC.txt
new file mode 100644 (file)
index 0000000..37d3fdf
--- /dev/null
@@ -0,0 +1,10 @@
+Hippo will be a new Horde 4, PHP 5 application that aggregates content. It will initially be written to aggregate feeds, both internal from Jonah natively, and external (through Horde_Feed). Hippo will work with Horde_Content types and have Input and Output classes. Input classes will allow reading a content type, such as Horde_Feed feeds, Jonah internal feeds, comics, etc. Output classes will allow outputting a "remixed" feed from one or more inputs (such as combining an RSS feed with a few comics into a daily feed).
+
+    * There will be no direct storage of inputs or outputs, but Horde_Cache will be used.
+    * Some content types such as comics might include code to fetch and store themselves locally; we might decide that Horde_Feed external feeds should be fetched into a local content table also
+    * This application should be able to provide the backend for Planet Horde (http://planet.horde.org/)
+    * Fields to aggregate for feeds: title, url, uid, summary, content if any, content-type for content, date
+    * During Hippo runs, Horde_Log will be used for logging
+    * There will be the ability to run filters on Hippo_Input plugins. For instance, if a certain feed has the same junk in every entry, a filter could be attached to the Hippo_Input for that feed that would strip it out. Generic (regex-type) filters should be configurable via the UI; it should be possible to configure any kind of filter in a config file or separate input plugin class.
+    * This application should eventually be able to replace Klutz
+    * Additional possible content types include podcasts, content from other Horde applications (iCalendar data from Kronolith), "friend information (http://hasin.wordpress.com/2008/05/31/building-friendfeed-using-php-part-1/, http://hasin.wordpress.com/2008/06/03/building-services-like-friendfeed-using-php-part2/), etc.
diff --git a/hippo/TODO.txt b/hippo/TODO.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/hippo/planet.horde.org/.htaccess b/hippo/planet.horde.org/.htaccess
new file mode 100644 (file)
index 0000000..c5c8612
--- /dev/null
@@ -0,0 +1,26 @@
+# The following php_* instruction only work, if you're running
+# php as apache-module, not as cgi.
+# they are not really needed...
+#php_value error_reporting 2039
+#php_flag display_errors on
+#php_value magic_quotes_gpc off
+
+RewriteEngine On
+
+RewriteCond     %{REQUEST_URI}  !^/*themes/planet-horde/.*
+RewriteRule ^/*themes/(.+)$  ./themes/planet-horde/$1 [L]
+
+RewriteCond     %{REQUEST_URI}  !^/*themes/planet-horde/.*
+RewriteCond     %{REQUEST_URI}  !^/*index.php
+RewriteCond     %{REQUEST_URI}  !^.*info.php
+RewriteCond     %{REQUEST_URI}  !^.*test.php
+RewriteCond     %{REQUEST_URI}  !^.*js/
+RewriteCond     %{REQUEST_URI}  !^.*images/
+RewriteCond     %{REQUEST_URI}  !^.*robots.txt
+RewriteCond     %{REQUEST_URI}  !^/*s9y/
+RewriteCond     %{REQUEST_URI}  !^.*admin/
+RewriteCond     %{REQUEST_URI}  !^.*_vti_.*
+RewriteCond     %{REQUEST_URI}  !favicon.ico
+RewriteCond     %{REQUEST_URI}  !search.html
+RewriteRule ^(.+)$ ./index.php?path=$1 [QSA]
+
diff --git a/hippo/planet.horde.org/favicon.ico b/hippo/planet.horde.org/favicon.ico
new file mode 100644 (file)
index 0000000..461907c
Binary files /dev/null and b/hippo/planet.horde.org/favicon.ico differ
diff --git a/hippo/planet.horde.org/libs/aggregator.php b/hippo/planet.horde.org/libs/aggregator.php
new file mode 100644 (file)
index 0000000..1f56567
--- /dev/null
@@ -0,0 +1,310 @@
+<?php
+
+
+include_once('MDB2.php');
+include_once('utf2entities.php');
+
+include_once('magpierss/rss_fetch.inc');
+
+class aggregator {
+    
+    var $mdb = null;
+    
+    function __construct() {
+        $this->mdb = MDB2::connect($GLOBALS['BX_config']['dsn']);
+        if(MDB2::isError($this->mdb)) {
+            die('unable to connect to db');
+        }
+    }
+    
+    function aggregateAllBlogs($id = null) {
+    $where = '';
+       if ($id) {
+               $where = "where ID = $id";
+       }
+       $res = $this->mdb->query("select ID,blogsID as blogsid, link, cats, section from feeds $where");
+       if (MDB2::isError($res)) {
+           print $res->getMessage();
+           print "\n";
+           print $res->getUserinfo();
+           die();
+       }
+       while ($row = $res->fetchRow(MDB2_FETCHMODE_ASSOC)) {
+           //get remote feed from magpie
+           $feed = $this->getRemoteFeed($row['link']);  
+           if(!$feed) {
+               continue;
+           }
+           //check if this blog already exists
+           if (!$feed->channel['link']) {
+               print "NO channel/link... PLEASE FIX THIS\n";
+               continue;
+           }
+          
+           $blog = $this->getBlogEntry($feed->channel['link']);
+           $blog = $this->mdb->queryRow ("select * from blogs where id = ".(int)$row['blogsid'], null,MDB2_FETCHMODE_ASSOC);
+           $newBlog = false;
+           if (!$blog) {
+               // $id = $this->insertBlogEntry($feed->channel);
+               // print "new Blog: " .$feed->channel['title'] ."\n";
+               // $newBlog = true;
+           } else {
+               //TODO: check for changed channel entries
+               $id = $blog['id'];
+               if ($feed->channel['title'] && $blog['title'] != $feed->channel['title'] && $row['section'] != 'comments') {
+                    // $this->updateBlogEntry($feed->channel, $id);   
+               }
+           }
+           // update id, if not the same
+           if ($row['blogsid'] != $id) {
+               // $this->updateFeedBlogID($row['link'], $id);   
+            }
+            
+            //loop through feeds
+
+            foreach ($feed->items as $item) {
+                if (isset($item['guid'])) {
+                    $guid = $item['guid'];
+                } else if (isset($item['id'])) {
+                    $guid = $item['id'];
+                   $item['guid'] = $item['id'];
+               } else {
+                    $guid = $item['link'];
+                   $item['guid'] = $item['link'];
+                }
+
+               if (!isset($item['content']['encoded']) && isset($item['atom_content'])) {
+                       $item['content']['encoded'] = $item['atom_content'];
+               } 
+                $item['md5']  = $this->generateMD5($item);
+                
+                $feedInDB = $this->getEntry($guid);
+                if (!$feedInDB) {
+                    // check for category stuff
+                    // we only do that for new entries 
+                    if (isset($item['dc']['subject'])) {
+                        $item['category'] = $item['dc']['subject']; 
+                    }
+                    
+                    if ($row['cats']) {
+                        $cats = explode(",",$row['cats']);
+                        $hit = false;
+                        foreach ($cats as $cat) {
+                            if (strpos($item['category'],$cat) !== false) {
+                                $hit = true;
+                            }
+                        }
+                        
+                        if (!$hit) {
+                            print $item['title'] . " - " . $item['category'] . " not in list\n";
+                            continue;
+                        }
+                    }
+                    // insert it in the db
+                    $item = $this->truncateEntries($item);
+                    $this->insertEntry($item, $row['id'], array("newBlog"=>$newBlog));
+                } else if ($item['md5'] != $feedInDB['md5']) {
+                    $item = $this->truncateEntries($item);
+                    $this->updateEntry($item,$feedInDB['id']);
+                }
+                
+            }
+       }
+    }
+    
+    function truncateEntries($item) {
+        $maxsize = 5000;
+        if (isset($item['content']['encoded']) && strlen($item['content']['encoded']) > $maxsize + 500) {
+            print "TRUNCATE content_encoded on ". $item['title'] ."\n";
+            $morebytes = (strlen($item['content']['encoded']) - $maxsize);
+            $item['content']['encoded'] = $this->getBody(substr($item['content']['encoded'],0,$maxsize));
+            $item['content']['encoded'] .= '<p><i>Truncated by Planet Horde, read more at <a href="'.$item['link'].'">the original</a> (another ' . $morebytes .' bytes)</i></p>'; 
+        } else if (isset($item['description']) && strlen($item['description']) > $maxsize + 500) {
+            print "TRUNCATE description ". $item['title'] ."\n";
+            $morebytes = (strlen($item['description']) - $maxsize);
+            
+            $item['description'] = $this->getBody(substr($item['description'],0,$maxsize));
+            $item['description'] .= '<p><i>Truncated by Planet Horde, read more at <a href="'.$item['link'].'">the original</a> (another ' . $morebytes .' bytes)</i></p>';
+        }
+        return $item;
+    }
+    
+    function getBody($html) {
+        
+        $d = new DomDocument();
+        $html = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>'.$html.'</body>';
+        @$d->loadHTML($html);
+        $xp = new domxpath($d);
+        $res = $xp->query("/html/body/node()");
+        $body = "";
+        foreach ($res as $node) {
+            $body .= $d->saveXML($node);
+        }
+        return $body;
+    }
+    
+    function generateMD5($item) {
+        return md5($item['title'] .$item['link'] . (isset($item['description']) ? $item['description'] : '') . (isset($item['content']['encoded']) ? $item['content']['encoded'] : ''));
+    }
+    
+    function updateEntry($item, $entryID) {
+       $date =  $this->getDcDate($item, 0,true);
+       $query = "update entries set " .
+       " link =  '" .mysql_escape_string(utf2entities($item['link'])) . "'," .
+       " title =  '" .mysql_escape_string(utf2entities($item['title'])) . "'," .
+       " description=  '" .mysql_escape_string(utf2entities($item['description'])) . "'," .
+       " content_encoded=  '" . mysql_escape_string(utf2entities($item['content']['encoded'])) . "',";
+       if ($date) {
+        $query .= " dc_date = '".$date."',";
+       }
+       
+       $query .= " md5=  '" .$item['md5'] . "' ".
+       " where ID = $entryID";
+       print "update " . $item['title'] ."\n";
+        $res = $this->mdb->query($query);
+       if (MDB2::isError($res)) {
+            print "DB ERROR: ". $res->getMessage() . "\n". $res->getUserInfo(). "\n";
+            return false;
+        } else {
+            return true;
+        }
+    }
+    function insertEntry($item,$feedID, $options = array()) {
+        $id =  $this->mdb->nextID("planet");
+        if (isset($options['newBlog']) && $options['newBlog']) {
+            $offset = - 3600 * 144; // offset back to 6 days ago.needed for new blogs without pubdate/dcdate
+        } else {
+            $offset = 0;
+        }
+        if (!isset($item['guid']) || $item['guid'] == '') {
+            $item['guid'] = $item['link'];
+        }
+       
+        $date =  $this->getDcDate($item, $offset);
+        
+        $query = "insert into entries (ID,feedsID, title,link, guid,description,dc_date, dc_creator, content_encoded, md5) VALUES (".        $id . "," .
+        $feedID . ",'" .
+        
+        mysql_escape_string(utf2entities($item['title'])) . "','" .
+        mysql_escape_string(trim($item['link'])) . "','" .
+        mysql_escape_string(($item['guid'])) . "','" .
+        mysql_escape_string(utf2entities($item['description'])) . "','".
+        $date . "','" .
+        $item['dc']['creator'] . "','" .
+        mysql_escape_string(utf2entities($item['content']['encoded'])) . "','".
+        $item['md5'] . "')";
+        
+        print "insert " . $item['title'] ."\n";
+        $res = $this->mdb->query($query);
+        if (MDB2::isError($res)) {
+            print "DB ERROR: ". $res->getMessage() . "\n". $res->getUserInfo(). "\n";
+            return false;
+        } else {
+            return $id;
+        }
+    }
+    
+    function getDcDate($item, $nowOffset = 0, $returnNull = false) {
+        //we want the dates in UTC... Looks like MySQL can't handle timezones...
+        //putenv("TZ=UTC");
+        if (isset($item['dc']['date'])) {
+            $dcdate = $this->fixdate($item['dc']['date']);
+        } elseif (isset($item['pubdate'])) {
+            $dcdate = $this->fixdate($item['pubdate']);
+        } elseif (isset($item['issued'])) {
+            $dcdate = $this->fixdate($item['issued']);
+        } elseif (isset($item['created'])) {
+            $dcdate = $this->fixdate($item['created']);
+        } elseif (isset($item['modified'])) {
+            $dcdate = $this->fixdate($item['modified']);
+        } elseif ($returnNull) {
+            return NULL;
+        } else {
+            //TODO: Find a better alternative here
+            $dcdate = gmdate("Y-m-d H:i:s O",time() + $nowOffset);
+        }
+        return $dcdate;
+        
+    }
+    
+    function fixdate($date) {
+        $date =  preg_replace("/([0-9])T([0-9])/","$1 $2",$date);
+        $date =  preg_replace("/([\+\-][0-9]{2}):([0-9]{2})/","$1$2",$date);
+       $time = strtotime($date);        
+        //if time is too much in the future (more than 1 hours)
+        // set it to now()                  
+        if (($time - time()) > 3600)  {              
+                $time = time();             
+        }
+        $date =  gmdate("Y-m-d H:i:s O",$time);
+        return $date;
+    }
+        
+    function updateFeedBlogID($url, $id) {
+        
+        $query = "update feeds set blogsID = $id where link = '$url'";
+        $res = $this->mdb->query($query);
+        if (MDB2::isError($res)) {
+            print "DB ERROR: ". $res->getMessage() . "\n". $res->getUserInfo(). "\n";
+            return false;
+        } else {
+            return $id;
+        }
+    }
+    
+    function insertBlogEntry($channel) {
+        
+        $id =  $this->mdb->nextID("planet");
+        $query = "insert into blogs (ID,title,link,description) VALUES (".
+        $id . ",'" .
+        mysql_escape_string($channel['title']) . "','" .
+        mysql_escape_string($channel['link']) . "','" .
+        mysql_escape_string($channel['description']) . "')";
+        $res = $this->mdb->query($query);
+        if (MDB2::isError($res)) {
+            print "DB ERROR: ". $res->getMessage() . "\n". $res->getUserInfo(). "\n";
+            return false;
+        } else {
+            return $id;
+        }
+    }
+    
+    
+     function updateBlogEntry($channel,$id) {
+        
+      
+        $query = "update blogs set
+        title =  '".mysql_escape_string($channel['title']) . "',
+        link = '".mysql_escape_string($channel['link']) . "',
+        description = '".mysql_escape_string($channel['description']) . "' where ID = ". $id;
+        
+        $res = $this->mdb->query($query);
+        if (MDB2::isError($res)) {
+            print "DB ERROR: ". $res->getMessage() . "\n". $res->getUserInfo(). "\n";
+            return false;
+        } else {
+            return $id;
+        }
+    }
+
+    function getBlogEntry($url) {
+         return  $this->mdb->queryRow ("select * from blogs where link = '$url'",null,MDB2_FETCHMODE_ASSOC);
+    }
+    
+    function getFeedEntry($url) {
+         return  $this->mdb->queryRow ("select * from feeds where link = '$url'",null,MDB2_FETCHMODE_ASSOC);
+    }
+    function getEntry($url) {
+         return  $this->mdb->queryRow ("select * from entries where guid = '$url'",null,MDB2_FETCHMODE_ASSOC);
+    }
+        
+    function getRemoteFeed($url) {
+        if ($feed = fetch_rss($url)) {
+            return $feed;
+        } else {
+            print "$url is not a valid feed \n";
+            return false;
+        } 
+    }
+    
+}
diff --git a/hippo/planet.horde.org/libs/scripts/aggregate.php b/hippo/planet.horde.org/libs/scripts/aggregate.php
new file mode 100644 (file)
index 0000000..282cdfd
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+include_once('../../inc/config.inc.php');
+include_once('aggregator.php');
+
+$agg = new aggregator();
+$agg->aggregateAllBlogs(isset($argv[1]) ? $argv[1] : null);
diff --git a/hippo/planet.horde.org/libs/utf2entities.php b/hippo/planet.horde.org/libs/utf2entities.php
new file mode 100644 (file)
index 0000000..fedcfdb
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+
+ function utf2entities($source) {
+        
+        // array used to figure what number to decrement from character order value
+        // according to number of characters used to map unicode to ascii by utf-8
+        $decrement[4] = 240;
+        $decrement[3] = 224;
+        $decrement[2] = 192;
+        $decrement[1] = 0;
+        
+        // the number of bits to shift each charNum by
+        $shift[1][0] = 0;
+        $shift[2][0] = 6;
+        $shift[2][1] = 0;
+        $shift[3][0] = 12;
+        $shift[3][1] = 6;
+        $shift[3][2] = 0;
+        $shift[4][0] = 18;
+        $shift[4][1] = 12;
+        $shift[4][2] = 6;
+        $shift[4][3] = 0;
+        
+        $pos = 0;
+        $len = strlen ($source);
+        $encodedString = '';
+        while ($pos < $len) {
+            $thisLetter = substr ($source, $pos, 1);
+            $asciiPos = ord ($thisLetter);
+            $asciiRep = $asciiPos >> 4;
+            
+            if ($asciiPos < 128) {
+                $pos += 1;
+                $thisLen = 1;
+            }
+            else if ($asciiRep == 12 or $asciiRep == 13) {
+                // 2 chars representing one unicode character
+                $thisLetter = substr ($source, $pos, 2);
+                $pos += 2;
+                $thisLen = 2;
+            }
+            else if ($asciiRep == 15) {
+                // 4 chars representing one unicode character
+                $thisLetter = substr ($source, $pos, 4);
+                $thisLen = 4;
+                $pos += 4;
+            }
+            else if ($asciiRep == 14) {
+                // 3 chars representing one unicode character
+                $thisLetter = substr ($source, $pos, 3);
+                $thisLen = 3;
+                $pos += 3;
+            }
+            
+            // process the string representing the letter to a unicode entity
+            
+            if ($thisLen == 1) {
+                $encodedLetter =$thisLetter;
+            } else {
+                $thisPos = 0;
+                $decimalCode = 0;
+                while ($thisPos < $thisLen) {
+                    $thisCharOrd = ord (substr ($thisLetter, $thisPos, 1));
+                    if ($thisPos == 0) {
+                        $charNum = intval ($thisCharOrd - $decrement[$thisLen]);
+                        $decimalCode += ($charNum << $shift[$thisLen][$thisPos]);
+                    }
+                    else {
+                        $charNum = intval ($thisCharOrd - 128);
+                        $decimalCode += ($charNum << $shift[$thisLen][$thisPos]);
+                    }
+                    
+                    $thisPos++;
+                }
+                if ($decimalCode < 65529) {
+                    $encodedLetter = "&#". $decimalCode. ';';
+                } else {
+                    $encodedLetter = "";
+                }
+            }
+            $encodedString .= $encodedLetter;
+            
+        }
+        return $encodedString;
+    }
+
diff --git a/hippo/planet.horde.org/themes/planet-horde/common.xsl b/hippo/planet.horde.org/themes/planet-horde/common.xsl
new file mode 100644 (file)
index 0000000..a4a919b
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml">
+    <xsl:output encoding="utf-8" method="xml" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN"/>
+
+    
+     <xsl:template name="htmlhead">
+
+<head>
+  <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+            <title>Planet Horde</title>
+
+            <link rel="shortcut icon" href="/favicon.ico" />
+            <link href="./themes/css/style.css" rel="stylesheet" type="text/css"/>
+            <link href="./themes/css/screen.css" rel="stylesheet" type="text/css"/>
+  
+<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss/" />
+<link rel="alternate" type="application/rdf+xml" title="RDF" href="/rdf/" />
+<link rel="alternate" type="application/x.atom+xml" title="Atom" href="/atom/" />
+        </head>
+    </xsl:template>
+
+<xsl:template name="bodyhead">
+  <div id="header" class="clr">
+    <div id="logo" class="left">
+      <span>
+      <a href="/"><img alt="Planet Horde" src="./themes/img/logo.gif" /></a>
+      </span>
+    </div>
+      <ul id="nav" class="right">
+      </ul>
+  </div>
+</xsl:template>
+
+  </xsl:stylesheet>
diff --git a/hippo/planet.horde.org/themes/planet-horde/css/screen.css b/hippo/planet.horde.org/themes/planet-horde/css/screen.css
new file mode 100644 (file)
index 0000000..4186b0a
--- /dev/null
@@ -0,0 +1,318 @@
+/* =ORDER
+ 1. display
+ 2. float and position
+ 3. width and height
+ 4. Specific element properties
+ 5. margin
+ 6. border
+ 7. padding
+ 8. background
+ 9. color
+10. font related properties
+----------------------------------------------- */
+
+/* =MAIN
+----------------------------------------------- */
+body, td, input, select, textarea, a {
+  font: 100%/1.45 'lucida grande', verdana, arial, sans-serif;
+}
+body {
+  margin: 0;
+  padding: 0;
+  background-color: #1B1B1B;
+  color: #333;
+}
+img, table {
+  border-width: 0;
+}
+table {
+  padding: 0;
+}
+td {
+  vertical-align: top;
+  margin: 0;
+  padding: 0;
+}
+a {
+  text-decoration: underline;
+}
+a:link, a:visited {
+  color: #4e5043;
+}
+a:hover {
+  color: #fff;
+  background-color: #3e4037;
+}
+h1 {
+  margin: 0 25px;
+  color: #4e5043;
+}
+h2 {
+  margin-top: 15px;
+}
+#main h4 {
+  margin: 7px 0 3px 0;
+}
+
+/* =COMMON
+----------------------------------------------- */
+/* clear float */
+.clr:after {
+  content: "."; 
+  display: block; 
+  height: 0; 
+  clear: both; 
+  visibility: hidden;
+}
+.clr {display: inline-block;}
+/* Hides from IE-mac \*/
+* html .clr {height: 1%;}
+.clr {display: block;}
+/* End hide from IE-mac */
+
+.none {
+  display: none;
+}
+.vtop {
+  vertical-align: top;
+}
+.vmid {
+  vertical-align: middle;
+}
+.vbot {
+  vertical-align: bottom;
+}
+.left {
+  float: left;
+}
+.right {
+  float: right;
+  text-align: right;
+}
+.center {
+  text-align: center;
+}
+.top {
+  margin-top: 5px;
+}
+code, .path {
+  background-color: #efefef;
+  font-family: "Courier New", Courier, monospace;
+  font-size: 90%;
+}
+
+/* =ERROR PAGES 404/500
+----------------------------------------------- */
+#error {
+  width: 318px;
+  margin: 70px auto;
+  padding: 5px 30px 15px 30px;
+  background-color: #f9f9f9;
+  border: 1px solid #14507F;
+}
+#error #logo {
+  margin-bottom: 5px;
+}
+#error p {
+  margin: 10px 5px 0 5px;
+}
+
+/* =LAYOUT
+----------------------------------------------- */
+#wrapper {
+  width: 790px;
+  margin: 0 auto;
+}
+#header, #footer, #nav, #sub_nav, #content {
+  width: 790px;
+}
+#main {
+  float: left;
+  width: 470px;
+  margin: 0 25px;
+  display: inline;
+}
+#sidebar {
+  float: right;
+  width: 239px;
+  margin-right: 25px;
+  display: inline;
+}
+
+/* =HEADER
+----------------------------------------------- */
+#header {
+  padding: 5px 0;
+}
+#logo {
+  width: 300px;
+}
+#logo span, #logo a {
+  display: block;
+  width: 400px;
+  height: 88px;
+  background: url("../img/logo.gif") no-repeat;
+}
+#logo img {
+  display: block;
+  width: 0;
+}
+
+#nav {
+  padding: 65px 0 0 0;
+  margin: 0;
+  width: 300px;
+  list-style: none;
+}
+#nav li {
+  float: left;
+  padding: 0 10px;
+  color: #fff;
+  font-size: 90%;
+}
+#nav li a {
+  color: #969696;
+}
+#nav li a:hover {
+  color: #fff;
+}
+
+/* =SIDEBAR
+----------------------------------------------- */
+.side_item {
+  background-color: #e1e2da;
+  margin-bottom: 15px;
+}
+.side_item h4 {
+  margin: 0;
+  padding: 4px 0 5px 0;
+  text-align: center;
+  background: url("../img/sidebar_head.gif") left top no-repeat;
+  font-size: 90%;
+}
+.side_item p, 
+.side_item dl,
+.side_item form {
+  padding: 2px 12px 4px 12px;
+  margin: 0;
+  border-color: #4e5043;
+  border-width: 0 2px;
+  border-style: solid;
+  font-size: 85%;
+}
+.side_item dd {
+  margin: 0 0 0 10px;
+  padding: 0 0 5px 0;
+}
+.side_item .side_bottom {
+  font-size: 5px;
+  background: #f3f3f3 url("../img/sidebar_bottom.gif") 0 0 no-repeat;
+}
+
+/* =CONTENT
+----------------------------------------------- */
+#content {
+  padding: 10px 0 25px 0;
+  background: #f3f3f3 url("../img/content_corners.gif") top no-repeat;
+}
+
+/* =FOOTER
+----------------------------------------------- */
+#footer {
+  width: 740px;
+  background-color: #f8f8f8;
+  padding: 15px 25px 10px 25px;
+  border-top: 1px solid #c0c0c0;
+  border-bottom: 1px solid #c0c0c0;
+  font-size: 80%;
+}
+
+/* =SYNTAX
+----------------------------------------------- */
+.syntax {
+  padding: 5px;
+  background-color: #f8f8f8;
+  border: 1px solid #c0c0c0;
+  margin: 10px 0;
+  width: 460px;
+  overflow: auto;
+  line-height: 100%;
+}
+.syntax pre {
+  font-size: 90%;
+  margin: 0;
+  padding: 0;
+  background: transparent;
+  font-family: monospace;
+}
+
+/* ruby */
+.syntax .normal {
+}
+.syntax .comment {
+  color: #808080; 
+  font-style: italic; 
+}
+.syntax .keyword {
+  color: #008800; 
+  font-weight: bold;
+}
+.syntax .method { 
+  color: #0066BB; 
+  font-weight: bold;
+}
+.syntax .class { 
+  color: #C40066; 
+}
+.syntax .module { 
+  color: #C40066; 
+}
+.syntax .punct { 
+  color: #333; 
+}
+.syntax .symbol { 
+  color: #AA6600; 
+}
+.syntax .string { 
+  color: #E6374B;
+  background-color: #FFF0F0;
+}
+.syntax .char { 
+  color: #FFFBC7; 
+}
+.syntax .ident { 
+  color: #333; 
+}
+.syntax .constant { 
+  color: #003366; 
+  font-weight: bold;
+}
+.syntax .regex { 
+  color: #98AEC2; 
+}
+.syntax .number { 
+  color: #0000DF; 
+}
+.syntax .attribute { 
+  color: #3333BB; 
+}
+.syntax .global { 
+  color: #FFCE90; 
+}
+.syntax .expr { 
+  color: #333; 
+}
+.syntax .escape { 
+  color: #333; 
+}
+.syntax .access {
+  color: #AA6600;
+}
+
+/* yaml */
+.syntax .key {
+  color: #0066BB;
+}
+.syntax_yaml .punct {
+  color: #666;
+}
\ No newline at end of file
diff --git a/hippo/planet.horde.org/themes/planet-horde/css/style.css b/hippo/planet.horde.org/themes/planet-horde/css/style.css
new file mode 100644 (file)
index 0000000..650056f
--- /dev/null
@@ -0,0 +1,37 @@
+#box, .box {
+       margin: 0 0 2em 0;
+       padding: 0;
+}
+
+.feedcontent {
+margin: 1.12em 0;
+/*overflow: auto;
+max-height: 500px;*/
+}
+
+#pageNav {
+    padding: 0;
+    margin: 1em;
+}
+
+.box h3 {
+  background-color: #e1e2da;
+  margin: 0 0 2px 0;
+  padding: 2px;
+}
+.box h3 a {
+  font-weight: bold;
+}
+
+.blogLinkPad {
+  display: block;
+  text-decoration: none;
+  background: url("../img/feed-icon-10x10.png") no-repeat center left;
+  padding: 2px 0 2px 12px;
+  text-indent: 0;
+}
+
+img {
+  max-width: 400px;
+  border: none;
+}
diff --git a/hippo/planet.horde.org/themes/planet-horde/img/content_corners.gif b/hippo/planet.horde.org/themes/planet-horde/img/content_corners.gif
new file mode 100644 (file)
index 0000000..e64f16e
Binary files /dev/null and b/hippo/planet.horde.org/themes/planet-horde/img/content_corners.gif differ
diff --git a/hippo/planet.horde.org/themes/planet-horde/img/feed-icon-10x10.png b/hippo/planet.horde.org/themes/planet-horde/img/feed-icon-10x10.png
new file mode 100644 (file)
index 0000000..cc869bc
Binary files /dev/null and b/hippo/planet.horde.org/themes/planet-horde/img/feed-icon-10x10.png differ
diff --git a/hippo/planet.horde.org/themes/planet-horde/img/logo.gif b/hippo/planet.horde.org/themes/planet-horde/img/logo.gif
new file mode 100644 (file)
index 0000000..e6b9b2d
Binary files /dev/null and b/hippo/planet.horde.org/themes/planet-horde/img/logo.gif differ
diff --git a/hippo/planet.horde.org/themes/planet-horde/img/planet-horde.psd b/hippo/planet.horde.org/themes/planet-horde/img/planet-horde.psd
new file mode 100644 (file)
index 0000000..911e50b
Binary files /dev/null and b/hippo/planet.horde.org/themes/planet-horde/img/planet-horde.psd differ
diff --git a/hippo/planet.horde.org/themes/planet-horde/img/sidebar_bottom.gif b/hippo/planet.horde.org/themes/planet-horde/img/sidebar_bottom.gif
new file mode 100644 (file)
index 0000000..9368ad7
Binary files /dev/null and b/hippo/planet.horde.org/themes/planet-horde/img/sidebar_bottom.gif differ
diff --git a/hippo/planet.horde.org/themes/planet-horde/img/sidebar_head.gif b/hippo/planet.horde.org/themes/planet-horde/img/sidebar_head.gif
new file mode 100644 (file)
index 0000000..4e4b8a4
Binary files /dev/null and b/hippo/planet.horde.org/themes/planet-horde/img/sidebar_head.gif differ
diff --git a/hippo/planet.horde.org/themes/planet-horde/main.xsl b/hippo/planet.horde.org/themes/planet-horde/main.xsl
new file mode 100644 (file)
index 0000000..0c040c8
--- /dev/null
@@ -0,0 +1,170 @@
+<?xml version="1.0"?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml"
+
+xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rss="http://purl.org/rss/1.0/" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/" xmlns:admin="http://webns.net/mvcb/"
+
+>
+    <xsl:output encoding="utf-8" method="xml" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN"/>
+    <xsl:include href="common.xsl"/>
+    <xsl:param name="startEntry" value="'0'"/>
+    <xsl:variable name="searchString" select="/planet/search/string"/>
+    <xsl:template match="/">
+
+        <html>
+
+            <xsl:call-template name="htmlhead"/>
+            <body>
+   <div id="wrapper">
+    <xsl:call-template name="bodyhead"/>
+    <div id="content" class="clr">
+     <xsl:call-template name="middlecol"/>
+     <xsl:call-template name="rightcol"/>
+    </div>
+    <div id="footer" class="clr">
+     <span class="right">
+       Powered by <a href="http://www.planet-php.net/">Planet PHP</a>, more or less.
+     </span>
+    </div>
+   </div>
+            </body>
+        </html>
+    </xsl:template>
+    <xsl:template name="rightcol">
+       <div id="sidebar">
+            <div class="side_item">
+                    <h4>Search Planet Horde</h4>
+                    <form onsubmit="niceURL(); return false;" name="search" method="get" action="/">
+                        <input id="searchtext" type="text" name="search">
+                            <xsl:if test="/planet/search/string">
+                                <xsl:attribute name="value">
+                                    <xsl:value-of select="/planet/search/string"/>
+                                </xsl:attribute>
+                            </xsl:if>
+                        </input>
+                       <input class="submit" type="submit" value="Go"/>
+                    </form>
+             <div class="side_bottom">&#160;</div>
+            </div>
+
+            <div class="side_item">
+              <h4>Blogs</h4>
+ <p>
+              <xsl:apply-templates select="/planet/blogs/blog"/>
+</p>
+              <div class="side_bottom">&#160;</div>
+            </div>
+        </div>
+    </xsl:template>
+
+    <xsl:template match="blogs/blog">
+<xsl:if test="maxdate &gt; border">
+        <a href="{link}" class="blogLinkPad">
+    <xsl:choose>
+                <xsl:when test="string-length(author) &gt; 0 ">           
+                <xsl:value-of select="author"/>     
+<xsl:if test="dontshowblogtitle = 0"> (<xsl:value-of select="title"/>) </xsl:if>
+                </xsl:when>
+                <xsl:otherwise>
+                <xsl:value-of select="title"/>
+                </xsl:otherwise>
+               </xsl:choose> 
+        </a>
+</xsl:if>
+    </xsl:template>
+    
+    
+        <xsl:template match="/planet/entries[@section='releases']/entry">
+
+        <a href="{link}" class="blogLinkPad">
+            <xsl:value-of select="title"/>
+        </a>
+    </xsl:template>
+    <xsl:template name="middlecol">
+        <div id="main">
+            <xsl:apply-templates select="/planet/entries[@section='default']/entry"/>
+            <xsl:variable name="nextEntries">
+                <xsl:choose>
+                    <xsl:when test="(/planet/search/count - (/planet/search/start + 10)) &gt;= 10">10</xsl:when>
+                    <xsl:otherwise>
+                        <xsl:value-of select="(/planet/search/count - (/planet/search/start + 10))"/>
+                    </xsl:otherwise>
+                </xsl:choose>
+            </xsl:variable>
+            <div id="pageNav">
+
+                    <span style="float: right;">
+
+                        <xsl:if test="$nextEntries &gt; 0">
+                            <xsl:choose>
+                                <xsl:when test="$searchString">
+                                    <a href="/search/{$searchString}?start={$startEntry + 10}">Next <xsl:value-of select="$nextEntries"/> Older Entries</a>
+                                </xsl:when>
+                                 <xsl:otherwise>
+                                    <a href="/?start={$startEntry + 10}">Next <xsl:value-of select="$nextEntries"/> Older Entries</a>
+                                </xsl:otherwise>
+                            </xsl:choose>
+                        </xsl:if>
+                   
+
+                    </span>
+                    <span style="float: left;">
+                        <xsl:choose>
+                            <xsl:when test="$startEntry = 0 and $nextEntries &lt;= 0">
+                             No More Entries
+                             </xsl:when>
+                            <xsl:when test="$startEntry &gt;= 10">
+                                <xsl:choose>
+                                    <xsl:when test="$searchString">
+                                        <a href="/search/{$searchString}?start={$startEntry - 10}">Previous 10 Newer Entries</a>
+                                    </xsl:when>
+                                    <xsl:otherwise>
+                                        <a href="/?start={$startEntry - 10}">Previous 10 Newer Entries</a>
+                                    </xsl:otherwise>
+                                </xsl:choose>
+                            </xsl:when>
+
+                        </xsl:choose>
+
+                    </span>
+            </div>
+
+        </div>
+
+    </xsl:template>
+
+    <xsl:template match="entries[@section='default']/entry">
+        <div class="box">
+                <h3>
+                <a href="{link}" class="blogTitle">
+                    <xsl:value-of select="title"/>
+                </a>
+                </h3>
+
+                   By <a href="{blog_link}">
+    <xsl:choose>
+                <xsl:when test="string-length(blog_author) &gt; 0 ">           
+                <xsl:value-of select="blog_author"/>     
+<xsl:if test="blog_dontshowblogtitle = 0"> (<xsl:value-of select="blog_title"/>) </xsl:if>
+                </xsl:when>
+                <xsl:otherwise>
+                <xsl:value-of select="blog_title"/>
+                </xsl:otherwise>
+               </xsl:choose> 
+                    </a>
+
+                <xsl:text> </xsl:text> 
+            (<xsl:value-of select="dc_date"/> UTC)
+<div class="feedcontent" >
+<xsl:choose>
+<xsl:when test="string-length(content_encoded) &gt; 0">
+             <xsl:value-of select="content_encoded" disable-output-escaping="yes"/>
+                        </xsl:when>
+                        <xsl:otherwise>
+                            <xsl:value-of select="description" disable-output-escaping="yes"/>
+                        </xsl:otherwise>
+                    </xsl:choose>
+                </div>
+        </div>
+    </xsl:template>
+
+</xsl:stylesheet>
diff --git a/hippo/planet.sql b/hippo/planet.sql
new file mode 100644 (file)
index 0000000..a6cfd39
--- /dev/null
@@ -0,0 +1,134 @@
+-- MySQL dump 10.11
+--
+-- Host: localhost    Database: planet
+-- ------------------------------------------------------
+-- Server version      5.0.45
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `blogs`
+--
+
+DROP TABLE IF EXISTS `blogs`;
+CREATE TABLE `blogs` (
+  `ID` int(11) NOT NULL auto_increment,
+  `link` varchar(255) NOT NULL default '',
+  `title` varchar(255) NOT NULL default '',
+  `description` tinytext,
+  `changed` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+  `author` varchar(100) NOT NULL default '',
+  `dontshowblogtitle` tinyint(4) NOT NULL default '1',
+  PRIMARY KEY  (`ID`),
+  UNIQUE KEY `link` (`link`)
+) ENGINE=MyISAM AUTO_INCREMENT=46 DEFAULT CHARSET=latin1;
+
+--
+-- Dumping data for table `blogs`
+--
+
+LOCK TABLES `blogs` WRITE;
+/*!40000 ALTER TABLE `blogs` DISABLE KEYS */;
+INSERT INTO `blogs` VALUES (2,'http://janschneider.de/','Jan Schneider','News from the Horde Project','2008-02-26 19:56:57','',1),(17,'http://hagenbu.ch/','Chuck Hagenbuch','Chuck\'s blog, links, news, ramblings','2008-02-26 19:56:30','',1),(28,'http://mikenaberezny.com','Mike Naberezny','','2008-02-27 04:18:55','',1),(31,'http://theupstairsroom.com/','Michael Rubinsky','News to display on theUpstairsRoom.com website.','2008-02-26 22:28:29','',1),(45,'http://log.onthebrink.de/','Gunnar Wrobel',NULL,'2008-02-27 16:47:03','',1);
+/*!40000 ALTER TABLE `blogs` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Table structure for table `entries`
+--
+
+DROP TABLE IF EXISTS `entries`;
+CREATE TABLE `entries` (
+  `ID` int(11) NOT NULL default '0',
+  `feedsID` int(11) NOT NULL default '0',
+  `title` tinytext,
+  `link` tinytext NOT NULL,
+  `guid` varchar(255) NOT NULL,
+  `description` text,
+  `dc_date` datetime default '0000-00-00 00:00:00',
+  `dc_creator` varchar(100) default NULL,
+  `content_encoded` text,
+  `changed` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+  `md5` varchar(32) NOT NULL default '',
+  PRIMARY KEY  (`ID`),
+  UNIQUE KEY `link` (`link`(250)),
+  KEY `rss_feed_ID` (`feedsID`),
+  FULLTEXT KEY `search` (`description`,`content_encoded`,`title`)
+) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+
+--
+-- Dumping data for table `entries`
+--
+
+LOCK TABLES `entries` WRITE;
+/*!40000 ALTER TABLE `entries` DISABLE KEYS */;
+INSERT INTO `entries` VALUES (7,1,'Patch library in PHP?','http://janschneider.de/news/35/318','http://janschneider.de/news/35/318','Does anybody know a good patch library in PHP? Like a counterpart to Text_Diff.\nI know that it\'s trivial to apply a clean patch file with a few lines of code, but I\'m looking for a more complete solution, especially with fuzzy patching capabilities.','2007-12-16 13:57:31','','','2008-02-25 21:42:48','7dcd12a5e870370a00fb52f16781b683'),(8,1,'Webmail comparison of DIMP, Roundcube and Scalix','http://janschneider.de/news/35/317','http://janschneider.de/news/35/317','As I mentioned in an earlier blog post about the article that I wrote for Linux Magazin Technical Review, there is another article from Linux Magazin which compares the AJAX webmail solutions DIMP, Roundcube, and the webmail component of Scalix. Here\'s a short summary.','2007-12-04 14:06:59','','','2008-02-25 21:42:48','e447af5e4850343c67d1baa7d1183608'),(9,1,'\"The Michigan Daily\" about Horde','http://janschneider.de/news/35/313','http://janschneider.de/news/35/313','The Michigan Daily released an article today about the University of Michigan NOT switching their mail system over to Google. They use Horde and IMP and the moment and seem to be very happy with it. There are more reasons why organizations and companies should not move to Gmail or Hotmail.','2007-11-21 23:26:00','','','2008-02-25 21:42:48','bf06d113f608c8a49036e3216b9176ff'),(10,1,'Article in Linux Magazin','http://janschneider.de/news/35/312','http://janschneider.de/news/35/312','An article about Horde has been released in a groupware edition of the German \"Linux Magazine - Technical Review\".','2007-11-20 12:19:00','','','2008-02-25 21:42:48','e01c82956ef670c0e0add982226eb894'),(11,1,'Kolab web client finished','http://janschneider.de/news/35/309','http://janschneider.de/news/35/309','From Gunnar Wrobel through the Kolab mailing lists: Kolab support in Horde is finally considered feature complete!','2007-05-30 22:02:28','','','2008-02-25 21:42:48','dcb37a899fd0d6f0c6e97b646ebeff9a'),(12,1,'Introduction to Horde_Lens','http://janschneider.de/news/35/308','http://janschneider.de/news/35/308','Chuck has written an introduction to the new Horde_Lens library: \"Horde_Lens is a decorating iterator implementation that lets you look at every element of a list through the lens of a Flyweight decorator object.\"','2007-05-14 22:11:32','','','2008-02-25 21:42:48','d0efa0368971d3a6539b096223ca183a'),(13,1,'Shop is open','http://janschneider.de/news/35/307','http://janschneider.de/news/35/307','It was just started for personal purposes, to get some Horde T-Shirts printed, but I was asked to make it open to the public. So here it is, the merchandising shop for the Horde Project.','2007-05-10 09:16:50','','','2008-02-25 21:42:48','0df3e97e67c60ad700ba60093617f824'),(14,1,'Nice new icon sets for Horde','http://janschneider.de/news/35/306','http://janschneider.de/news/35/306','Daniel Dembach has created a really nice icon set for Horde based on the FamFamFam silk icons, and a Tango icon set has been uploaded to our ticket system.','2007-03-21 10:39:26','','','2008-02-25 21:42:48','1ffac117115d4e1069689e09afed5ed8'),(15,1,'Vortrag bei der LUG Ravensberg','http://janschneider.de/news/35/305','http://janschneider.de/news/35/305','[To the English readers: this is about a (German) talk at a local linux user group.]\n\nAm 22. M&#228;rz werde ich in Bielefeld einen Vortrag &#252;ber Horde bei der Linux Usergroup Ravensberg halten.','2007-03-20 09:15:29','','','2008-02-25 21:42:48','4f5bf4fb26b54479d65982b626e1a127'),(16,1,'Preparations for Horde 3.2 release series started','http://janschneider.de/news/35/304','http://janschneider.de/news/35/304','The release of 13 Horde applications during the last few days was the starting signal for the preparations of the Horde 3.2 release cycle. This series will not only deliver exciting new features for the stable applications, but turn some applications that are still in development into a first stable version, e.g. DIMP, Wicked or Whups.','2007-03-19 16:07:38','','','2008-02-25 21:42:48','b668da8be8b3db90dc9d1f9c82397d2f'),(18,2,'Horde/Yaml 1.0.1 released with PHP object support','http://hagenbu.ch/blog/64','http://hagenbu.ch/blog/64','The latest Horde/Yaml package supports the same !php/object constructs that the PECL syck package does.','2008-02-16 20:56:44','','','2008-02-25 21:42:48','57e3ac513880aee421a5782d2a66efb8'),(19,2,'Horde/Routes gets a home on the web','http://hagenbu.ch/blog/62','http://hagenbu.ch/blog/62','Horde has a great request routing library, and now Horde/Routes has a great website, too.','2007-11-02 22:42:20','','','2008-02-25 21:42:48','4e60ca66d062fdca3556160d62cf0439'),(20,2,'New Features in Horde 3.2','http://hagenbu.ch/blog/58','http://hagenbu.ch/blog/58','Focusing on a few of the new features in Horde 3.2','2007-08-17 22:41:40','','','2008-02-25 21:42:48','c7367a3fc16e43fdbabe792123f7a89b'),(21,2,'Horde 3.2 alpha releases!','http://hagenbu.ch/blog/56','http://hagenbu.ch/blog/56','Current status on the Horde 3.2 releases','2007-08-03 17:34:19','','','2008-02-25 21:42:48','92bba890cbc676ac129ebb3ff047f12a'),(22,2,'ORM with Horde_Rdo','http://hagenbu.ch/blog/55','http://hagenbu.ch/blog/55','An intro to ORM with Horde\'s Rdo data mapping library.','2007-06-12 20:51:00','','','2008-02-25 21:42:48','a0844dd45e95b91707a5aded83885b22'),(23,2,'Horde Application Building Podcast','http://hagenbu.ch/blog/54','http://hagenbu.ch/blog/54','A Horde podcast, recorded from a Horde Framework application-building talk at Boston PHP','2007-06-05 21:00:00','','','2008-02-25 21:42:48','17d849fabaf36ddadd513e46dbd4d2aa'),(24,2,'Horde_Lens','http://hagenbu.ch/blog/48','http://hagenbu.ch/blog/48','Horde_Lens is a decorating iterator implementation that lets you look at every element of a list through the lens of a Flyweight decorator object.','2007-05-12 16:04:00','','','2008-02-25 21:42:48','94534540587e922e3ad13be66cd4bae9'),(25,2,'Plugs for Horde Groupware','http://hagenbu.ch/blog/49','http://hagenbu.ch/blog/49','Think Horde is too hard to install? Think again.','2007-04-28 01:55:00','','','2008-02-25 21:42:48','041d5630f814f4b14f774adaa51d7441'),(26,2,'NYPHPCon slides are up','http://hagenbu.ch/blog/44','http://marina.horde.org/horde/jonah/stories/view.php?channel_id=133&story_id=44','Also, I\'m not going to PHPVikinger.','2006-06-22 21:36:00','','','2008-02-25 21:42:48','793522b0d5dca7689e74bcaa7f9bb3f5'),(27,2,'Horde Tutorial at NYPHPCon','http://hagenbu.ch/blog/43','http://marina.horde.org/horde/jonah/stories/view.php?channel_id=133&story_id=43','Mashup web 2.0 mashup web 2.0 mashup web 2.mmppphhhhh!','2006-05-28 16:54:00','','','2008-02-25 21:42:48','ce2dfca573869aed82c3fc240bcdb495'),(29,3,'Horde/Yaml 1.0 Released','http://mikenaberezny.com/2008/01/08/hordeyaml-10-released/','http://mikenaberezny.com/archives/87','','2008-02-19 21:42:49','','<p><a href=\"http://pear.horde.org/index.php?package=yaml\">Horde/Yaml</a> is a PHP 5 library for easily working with <a href=\"http://yaml.org/\">YAML</a> data.  This is the package&#8217;s first stable release.</p>\n<p><a href=\"http://chuck.hagenbu.ch/\">Chuck Hagenbuch</a> started the library as an adaptation of <a href=\"http://spyc.sourceforge.net/\">Spyc</a> around six months ago.  Since then, he and I have been quietly using and improving it.  Along the way, we fixed many issues, added support for <a href=\"http://pecl.php.net/package/syck\">pecl/syck</a>, and wrote a test suite with PHPUnit.</p>\n<p>There are a couple of other libraries also derived from Spyc, notably the <code>sfYaml</code> class from the <a href=\"http://www.symfony-project.org/\">Symfony</a> framework.  Since these efforts also found and corrected issues, we incorporated as many of these fixes as we could find and added them to the test suite as we went along.</p>\n<p>At <a href=\"http://maintainable.com\">Maintainable Software</a>, we frequently use YAML files for configuring our custom applications because our clients tend to like the format more than the alternatives.  We&#8217;ve been using Horde/Yaml successfully for quite some time so we think it should generally work well for you also.</p>\n<p>There&#8217;s a nice tutorial on <a href=\"http://railsforphp.com/2008/01/08/php-meet-yaml/\">working with YAML in PHP 5</a> over on the new <a href=\"http://railsforphp.com\">Rails for PHP Developers</a> website.  It includes everything you need to get started with Horde/Yaml.</p>\n','2008-02-25 21:42:49','10168ad80892ea3bbcc08520bcd98460'),(30,3,'New in Horde: Routes','http://mikenaberezny.com/2007/09/15/new-in-horde-routes/','http://mikenaberezny.com/archives/80','','2008-02-19 21:42:49','','<p>I&#8217;m pleased to announce the first release of <a href=\"http://pear.horde.org/index.php?package=Horde_Routes\">Horde/Routes</a>, a new URL mapping system for PHP 5.  This package provides classes for mapping URLs into the controllers and actions of an MVC system, inspired by Ruby on Rails. </p>\n<p>There are already quite a few existing libraries that do this sort of thing for PHP.  Horde/Routes is a compelling alternative.  </p>\n<p>At <a href=\"http://maintainable.com\">Maintainable</a>, we examined most of these PHP solutions and found them all inadequate for various reasons, particularly because we wanted RESTful routing, named routes, sophisticated matching, PHP 5 E_STRICT, and extensive test coverage.  </p>\n<p>Since we do quite a bit of Ruby and Python programming, we surveyed the options in those languages and decided to do a full port of the Python library, <a href=\"http://routes.groovie.org\">Routes</a>.</p>\n<p>Horde/Routes provides these features and more:</p>\n<ul>\n<li>Supports route recognition and generation</li>\n<li>Sophisticated matching conditions like subdomains</li>\n<li>Named routes and RESTful route generation</li>\n<li>PEAR-style naming and coding standards</li>\n<li>PHP 5 E_STRICT compliant, web framework agnostic</li>\n<li>A comprehensive unit test suite</li>\n</ul>\n<p>Maintainable decided to contribute the code to Horde&#8217;s <a href=\"http://wiki.horde.org/RampageFramework\">Rampage</a> project because <a href=\"http://www.horde.org\">Horde</a> is one of PHP&#8217;s oldest and most successful projects.  We&#8217;re using and contributing to other Horde libraries and we think Rampage is worth your attention.</p>\n<p>The Python version has been around for some time and is very popular with different Python web frameworks.  We&#8217;re happy to be part of that ecosystem now and our full port has already resulted in patches being committed back to the Python version.</p>\n<p>At Maintainable, we&#8217;ve already used Horde_Routes on several applications.  While Horde/Routes is relatively new, it is very feature-rich and well-tested.</p>\n<p>Currently, Horde/Routes is a beta release.  Over the coming weeks, we&#8217;ll be making some minor changes to the API and adding more <a href=\"http://cvs.horde.org/co.php?r=1.1&#038;f=framework%2FRoutes%2Fdocs%2Fmanual.txt\">documentation</a>, and then it will quickly move to stable.</p>\n<p><b>Update</b>: <i>The project now has its own <a href=\"http://dev.horde.org/routes/\">pages</a> on the Horde website and was featured on <a href=\"http://chuck.hagenbu.ch/blog/62\">Chuck Hagenbuch&#8217;s blog</a>.</i></p>\n','2008-02-25 21:42:49','3cce684da4477be8c1c54010bd013df8'),(32,4,'Lighttpd configuration specific to Horde','http://theupstairsroom.com/61','http://theupstairsroom.com?story=61','Some example entries for lighttpd.conf file for duplicating various .htaccess rules in various Horde applications','2008-02-03 21:24:00','','','2008-03-29 16:00:12','f2b58275002a6277b03c204af0dbe5bd'),(33,4,'Ansel gets RSS support','http://theupstairsroom.com/58','http://rubinskyfamily.com/mike/58/','Ansel, the Horde Project\'s photo application gets support for RSS feeds.','2007-10-05 19:21:00','','','2008-03-29 16:00:12','ab9b22ea4675eec2c72550076b25b0db'),(34,4,'Aeros, the aviation based Horde theme has been updated','http://theupstairsroom.com/50','http://theupstairsroom.com/news/view.php?feed=124&story=50','I posted a new tarball for my aviation inspired Horde theme, Aeros.  It\'s a small update that includes a few tweaks and some CSS cleanup.','2007-06-21 14:27:00','','','2008-03-29 16:00:12','be53eba64112bcc979b045b3ef9555b5'),(35,4,'Some new work on Ansel','http://theupstairsroom.com/47','http://rubinskyfamily.com/news/138/47/','I\'ve been scratching some coding itches again with the Horde Project, adding some new features to Ansel, the image gallery application.','2007-05-09 17:28:00','','','2008-03-29 16:00:12','fa923b5fae42c71c6b34cd1d33e1ec73'),(36,4,'phpSysInfo Block updated','http://theupstairsroom.com/16','http://theupstairsroom.com/news/view.php?feed=124&story=16','Some minor updates and bugfixes for the phpSysInfo block have been released...','2006-07-30 20:18:00','','','2008-03-29 16:00:12','2a00a2d1858e255a43312a4772bc3cc9'),(37,4,'New Horde Block - phpSysInfo','http://theupstairsroom.com/7','http://theupstairsroom.com/news.php?channel_id=124&story_id=7','Displays stats from machines that are running phpSysInfo on the Horde portal page.','2006-03-16 20:59:00','','','2008-03-29 16:00:12','1f6f974d04a43425367558704b01f027'),(43,2,'Planet Horde Launches','http://hagenbu.ch/blog/65','http://hagenbu.ch/blog/65','We\'ve just launched Planet Horde: http://planet.horde.org/. It\'s more of a small moon right now, but it\'s the place to be for Horde news and articles.','2008-02-27 03:39:38','','','2008-02-27 04:16:13','51086e0c8176a5b1324ac891e19d08a8'),(45,5,'Horde: Synching with HEAD','http://log.onthebrink.de/2008/02/horde-synching-with-head.html','tag:blogger.com,1999:blog-998418780028044861.post-3593761677129855906','','2008-02-27 16:48:37','','<p>\nFeels good to be finally back working on Horde. Last week saw the generation of Kolab patches for Horde-3.2-RC2 and this week I finally synchronized my work environment with Horde CVS. The first Kolab commit to Horde CVS went in yesterday. It felt like the last one was ages ago.\n</p>\n<p>\nAnyhow there are more commits ahead. Now that most parts of Horde work with Kolab the time for the second round of coding is approaching fast: restructuring and optimization. There is still a lot that needs to be done and I\'m desperately waiting for Horde 4 to finally restructure the whole Kolab module and get it on a hopefully sane path for the future.\n</p>','2008-02-27 16:48:37','8b1cbe6b9bc952260d3681ee5f26e5bb'),(46,5,'K2G: Kolab2/Gentoo getting attention again','http://log.onthebrink.de/2007/12/finally-kolab2gentoo-project-gets-some.html','tag:blogger.com,1999:blog-998418780028044861.post-818745145701986043','','2008-02-27 16:48:37','','Finally the <a href=\"http://www.gentoo.org/proj/en/kolab/\">Kolab2Gentoo</a> project gets some attention again. I started fixing some older bugs but the main part will be to prepare for 2.2 now.\n\nI still hate the way the whole project is structured but there are too many places where massive improvements are needed. So I guess I have to be content with small progress.\n\nThe stuff that is currently on my mind concerning Kolab2/Gentoo-2.2:\n\n<ul>\n<li>Update to Horde-3.2</li>\n<li>Making the patched c-client and php packages recommended instead of required</li>\n<li>Switching to a specific cyrus-imapd-kolab package</li>\n<li>Template packages</li>\n</ul>\n\nAnd then there is the big, bad problem of the Kolab configuration concept that simply fails for Gentoo in its current form. The solution to that one will take even longer.','2008-02-27 16:48:37','94fe41b14e894979c8d96411e60bcdb0'),(47,2,'Creating a Site Specific Browser for Horde','http://hagenbu.ch/blog/66','http://hagenbu.ch/blog/66','Fluid lets you create a completely encapsulated, WebKit-based browser for your web application. Here\'s a guide to making one for Horde.','2008-03-03 04:16:08','','','2008-03-03 05:00:18','0da4d9d5315f6a9ffc42531022ae84fa'),(48,4,'Update on Ansel development','http://theupstairsroom.com/63','http://theupstairsroom.com?story=63','With Horde 3.2 quickly approaching final release status, I\'ve been working on getting Ansel ready for an upcoming release. Many new features and performance improvements are on the way.','2008-03-05 00:41:01','','','2008-03-29 16:00:12','c00e4fc2e57aac033969f90894c95f4b'),(49,1,'Nice helper to create RTL CSS files','http://janschneider.de/news/35/319','http://janschneider.de/news/35/319','Via Ajaxian: CSSJanus is a CSS parser utility designed to aid the conversion of a website\'s layout from left-to-right (LTR) to right-to-left (RTL).','2008-03-05 17:28:00','','','2008-03-15 17:00:14','ac6d5e0ece6a923420e0054668d36969'),(50,2,'March Horde Board Meeting Summary','http://hagenbu.ch/blog/67','http://hagenbu.ch/blog/67','The second Horde advisory board meeting discussed Horde 3.2, Google Summer of Code, and Planet Horde','2008-03-08 02:51:29','','','2008-03-08 03:00:11','69dd115c22761eb892f3b950edf04899'),(51,5,'Sync my Kolab','http://log.onthebrink.de/2008/03/sync-my-kolab.html','tag:blogger.com,1999:blog-998418780028044861.post-2300864698879546636','','2008-03-12 21:00:14','','<a onblur=\"try {parent.deselectBloggerImageGracefully();} catch(e) {}\" href=\"http://farm3.static.flickr.com/2026/2319768962_2190d14eb2_m_d.jpg\"><img style=\"float:right; margin:0 0 10px 10px;cursor:pointer; cursor:hand;\" src=\"http://farm3.static.flickr.com/2026/2319768962_2190d14eb2_m_d.jpg\" border=\"0\" alt=\"\" /></a>\n<p>\n<a href=\"http://en.wikipedia.org/wiki/SyncML\">SyncML</a> support for the <a href=\"http://www.kolab.org\">Kolab server</a> <a href=\"http://article.gmane.org/gmane.comp.kde.kolab.user/1660/match=syncml\">has been requested</a> for several years now. Supporting it via the modules available within Horde always seemed to be one the of easiest ways to get mobile clients to synchronize with the server. Since the newest Kolab server release candidates now provide Horde, how far is SyncML support away?\n</p>\n<p>\nNot far at all... <a href=\"http://www.univention.de/\">Univention</a> contracted <a href=\"http://www.pardus.de\">p@rdus</a> via the <a href=\"http://www.kolab-konsortium.de/en/index.html\">Kolab Konsortium</a> to implement SyncML support within the Kolab server.\n</p>\n<p>\nInitially a version that would require an additional MySQL database was planned but p@rdus invested some additional time into generating purely IMAP based drivers so that SyncML support will also be available within the next Kolab release (the Kolab server does not use MySQL at all by default).\n</p>\n<p>\nToday I was able to sync the Blackberry provided by the customer for the first time. Contacts, events, tasks all survived my minimal testing. Of course the same procedure failed once I gave the customer access to the test server...\n</p>\n<p>\nSo right now I\'m entering the debugging phase and I\'m starting to prepare some scripts so that people eager to try the SyncML support can install an experimental Horde version on an external web server.\n</p>\n<h4>Update:</h4>\n<p>\nA <a href=\"http://kolab.org/cgi-bin/viewcvs-kolab.cgi/server/horde/external-horde-cvs.sh\">script</a> for installing horde from CVS is now available. It also installs all the required Kolab patches for SyncML support.\n</p>\n<p>\nYou can fetch and run it like this:\n<pre>\nwget http://kolab.org/cgi-bin/viewcvs-kolab.cgi/*checkout*/server/horde/external-horde-cvs.sh\nchmod u+x external-horde-cvs.sh\n./external-horde-cvs.sh\n</pre>\n</p>','2008-03-13 08:00:15','d7a46ef783b154fcf305796488831209'),(52,2,'Horde 3.2: Serving multiple subdomains with one installation','http://hagenbu.ch/blog/68','http://hagenbu.ch/blog/68','Horde 3.2 includes improved support for spreading Horde apps across multiple subdomains. Want to know how to set up a single Horde install to serve, for example, bugs.horde.org, wiki.horde.org, cvs.horde.org, and dev.horde.org?','2008-03-19 03:35:42','','','2008-03-19 04:00:22','1b978091a8efba37d2ff2ece04b16e07'),(53,4,'Using the Horde API to Power External Sites or Applications - Part 1','http://theupstairsroom.com/65','http://theupstairsroom.com?story=65','Horde and all of it\'s applications have a powerful API through which a developer can obtain Horde content.  In this first part of a series, we\'ll look at the basics of what is required to interact with Horde via the API.','2008-03-29 15:12:00','','','2008-03-29 16:00:12','a5ac7bade123afc291b118e530487a37'),(54,2,'Auditing Horde applications with taint mode','http://hagenbu.ch/blog/69','http://hagenbu.ch/blog/69','Using Wietse Venema\'s taint mode modification to PHP to audit Horde','2008-04-04 20:58:39','','','2008-04-04 21:00:14','94248a828ee7fa67805baca26245d41e'),(55,5,'Another round of Horde bugs...','http://log.onthebrink.de/2008/04/another-round-of-horde-bugs.html','tag:blogger.com,1999:blog-998418780028044861.post-2482205407364385305','','2008-04-24 15:00:39','','I\'m back to Horde bug fixes and while their CVS server vanished in some kind of limbo I took the time to create a <a href=\"http://wiki.horde.org/Project/Kolab\">Horde/Kolab</a> project page. Maybe it is a useful overview to the people interested in Horde. I definitely have to update the <a href=\"http://wiki.kolab.org/index.php/Horde_development\">Kolab wiki</a>, too. But that might still take a while.','2008-04-24 15:00:39','d261c6b2137d94725520255f71386ef1'),(56,5,'The OpenSourceSchool opens its doors','http://log.onthebrink.de/2008/04/opensourceschool-opens-its-doors.html','tag:blogger.com,1999:blog-998418780028044861.post-1758429239832563689','','2008-04-25 09:00:25','','<a onblur=\"try {parent.deselectBloggerImageGracefully();} catch(e) {}\" href=\"http://www.opensourceschool.de/\"><img style=\"float:right; margin:0 0 10px 10px;cursor:pointer; cursor:hand;width: 79px;\" src=\"http://www.opensourceschool.de/fileadmin/images/logo.gif\" border=\"0\" alt=\"\" /></a>\n<p>\nMy <a href=\"http://www.opensourcepress.de\">publisher</a> started with his next endeavor in bringing knowledge to the masses: The <a href=\"http://www.opensourceschool.de\">OpenSourceSchool</a>. This time it is about spoken words - or courses - rather than written pages bound as books. Many <a href=\"http://www.opensourcepress.de\">OpenSourcePress</a> authors are offering seminars there.\n</p>\n<p>\nI would definitely have liked to offer a course about <a href=\"http://www.gentoo.org\">Gentoo</a> there. But I had to agree with them that this would probably not raise enough interest from paying customers. Or am I wrong about that?\n</p>\n<p>\nBut of course there was room for the second topic dear to my heart: <a href=\"http://www.gentoo.org/proj/en/kolab/\">Kolab</a>. <a href=\"http://www.opensourceschool.de/index.php?id=15&tx_ttproducts_pi1[backPID]=13&tx_ttproducts_pi1[product]=33&cHash=0e8d3864a9\">The course</a> will take five days and touch all major topics of the Kolab Server. Central components such as postfix, openldap, cyrus imap will provide the core components but I\'ll certainly also include a chapter about getting <a href=\"http://www.horde.org\">the Horde web client</a> successfully installed. So we will hopefully have a new batch of Kolab experts in October.\n</p>\n<p>\nAnd hopefully the preparations for the course will also help in laying the groundwork for a book about Kolab. This is the only book I still want to write after going through the pain of writing <a href=\"http://www.amazon.de/gp/product/3937514341?ie=UTF8&tag=aufkant-21&linkCode=as2&camp=1638&creative=6742&creativeASIN=3937514341\">the Gentoo book</a>.\n</p>','2008-04-25 10:00:39','7d941c49ba3832a392f885ea09e901cf'),(57,2,'Preventing Email Abuse through IMP','http://hagenbu.ch/blog/70','http://hagenbu.ch/blog/70','Some IMP installations have been targeted by brute-force or phishing attacks; compromised accounts are then used to send spam. Horde 3.2 and IMP 4.2 have some new features for limiting the damage of a compromised account or a rogue user.','2008-04-30 03:05:17','','','2008-04-30 04:00:17','39aec04ae0868557871d2499734dc41b'),(58,2,'May Horde Board Meeting Summary','http://hagenbu.ch/blog/71','http://hagenbu.ch/blog/71','Discussed: Horde 3.2 releases (this month), marketing material, showstoppers, and the new www.horde.org project.','2008-05-07 03:42:30','','','2008-05-07 04:00:33','2ae3e29192a1d1dacf6333eed74ebeb7'),(59,4,'Using the Horde API to Power External Sites or Applications - Part 2','http://theupstairsroom.com/66','http://theupstairsroom.com/66','In this second part of the series, we will look at using Horde\'s RPC server to get information from a remote Horde server via Horde\'s RPC interface.','2008-05-07 18:26:58','','','2008-05-07 19:00:22','43b3f6c56496b3c249e6a254a9e54486'),(60,4,'Using the Horde API to Power External Sites or Applications - Part 3','http://theupstairsroom.com/67','http://theupstairsroom.com/67','In this installment, we\'ll take a look at a real world example of using Horde to power an external website by building a custom photo gallery with Ansel.','2008-05-08 04:21:44','','','2008-05-08 05:00:33','ef109d9ee2d2b5901fdeca7222cfed69'),(61,2,'Birth Announcement','http://hagenbu.ch/blog/72','http://hagenbu.ch/blog/72','I\'m a dad! Some details and a few thumbnails after the jump.','2008-05-12 21:15:03','','','2008-05-12 22:00:28','c0b1fd939aeca7ddafd933f7b9459578');
+/*!40000 ALTER TABLE `entries` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Table structure for table `feeds`
+--
+
+DROP TABLE IF EXISTS `feeds`;
+CREATE TABLE `feeds` (
+  `ID` int(11) NOT NULL auto_increment,
+  `blogsID` int(11) NOT NULL default '0',
+  `link` varchar(255) NOT NULL default '',
+  `changed` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+  `cats` varchar(255) NOT NULL default '',
+  `section` varchar(50) NOT NULL default 'default',
+  PRIMARY KEY  (`ID`),
+  UNIQUE KEY `rssURL` (`link`),
+  KEY `blogID` (`blogsID`)
+) ENGINE=MyISAM AUTO_INCREMENT=6 DEFAULT CHARSET=latin1;
+
+--
+-- Dumping data for table `feeds`
+--
+
+LOCK TABLES `feeds` WRITE;
+/*!40000 ALTER TABLE `feeds` DISABLE KEYS */;
+INSERT INTO `feeds` VALUES (1,2,'http://janschneider.de/horde/jonah/delivery/rss.php?channel_id=35','2008-02-27 04:19:11','','default'),(2,17,'http://technest.org/horde/jonah/delivery/rss.php?channel_id=133&tag_id=4','2008-02-27 04:19:24','','default'),(3,28,'http://mikenaberezny.com/wp-atom.php?tag=horde','2008-02-25 21:42:49','','default'),(4,31,'http://portal.theupstairsroom.com/horde/jonah/delivery/rss.php?channel_id=124&tag_id=10','2008-02-27 04:19:37','','default'),(5,45,'http://log.onthebrink.de/feeds/posts/default/-/horde','2008-02-27 16:47:53','','default');
+/*!40000 ALTER TABLE `feeds` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Table structure for table `planet_seq`
+--
+
+DROP TABLE IF EXISTS `planet_seq`;
+CREATE TABLE `planet_seq` (
+  `sequence` int(11) NOT NULL auto_increment,
+  PRIMARY KEY  (`sequence`)
+) ENGINE=MyISAM AUTO_INCREMENT=62 DEFAULT CHARSET=latin1;
+
+--
+-- Dumping data for table `planet_seq`
+--
+
+LOCK TABLES `planet_seq` WRITE;
+/*!40000 ALTER TABLE `planet_seq` DISABLE KEYS */;
+INSERT INTO `planet_seq` VALUES (61);
+/*!40000 ALTER TABLE `planet_seq` ENABLE KEYS */;
+UNLOCK TABLES;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2008-05-13 19:18:38
diff --git a/hound/urls b/hound/urls
new file mode 100644 (file)
index 0000000..1c13b1c
--- /dev/null
@@ -0,0 +1,10 @@
+Great keyboard nav idea:
+http://parand.com/say/index.php/2008/09/15/readerscroll-google-reader-style-image-navigation-with-j-k-keys-bookmarklet/
+
+http://www.readwriteweb.com/archives/opera_96_launches_now_includes_magazine_style_rss.php
+http://www.readwriteweb.com/archives/will_gmail_get_google_reader-like_trends.php
+http://www.readwriteweb.com/archives/google_reader_now_lets_you_sha.php
+http://www.readwriteweb.com/archives/feedly_launches_a_river_of_news.php
+http://www.readwriteweb.com/archives/bloglines_is_still_alive_and_advertising.php
+http://www.readwriteweb.com/archives/mainstream_web_watch_why_alltop_rocks.php
+http://www.readwriteweb.com/archives/mainstreaming_rss_regator_public_beta.php
diff --git a/hydra/lib/Page.php b/hydra/lib/Page.php
new file mode 100644 (file)
index 0000000..7348bbc
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+/**
+ */
+class Page extends Horde_Content_Type
+{
+}
diff --git a/hydra/lib/PageMapper.php b/hydra/lib/PageMapper.php
new file mode 100644 (file)
index 0000000..24cab9d
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+/**
+ */
+class PageMapper extends Horde_Db_Mapper
+{
+}
diff --git a/hydra/public/.htaccess b/hydra/public/.htaccess
new file mode 100644 (file)
index 0000000..9340e69
--- /dev/null
@@ -0,0 +1,6 @@
+<IfModule mod_rewrite.c>
+    RewriteEngine On
+    RewriteCond   %{REQUEST_FILENAME}  !-d
+    RewriteCond   %{REQUEST_FILENAME}  !-f
+    RewriteRule ^(.*)$ index.php [QSA,L]
+</IfModule>
diff --git a/hydra/public/index.php b/hydra/public/index.php
new file mode 100644 (file)
index 0000000..a4abe2d
--- /dev/null
@@ -0,0 +1,2 @@
+<?php
+
diff --git a/hydra/public/stylesheets/print.css b/hydra/public/stylesheets/print.css
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/hydra/public/stylesheets/screen.css b/hydra/public/stylesheets/screen.css
new file mode 100644 (file)
index 0000000..c91c101
--- /dev/null
@@ -0,0 +1 @@
+/* Styles for hydra */