hibernal.pl 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503
  1. #!/usr/bin/env perl
  2. use strict;
  3. use v5.10;
  4. use utf8;
  5. # ====================[ hibernal.pl ]====================
  6. =encoding utf8
  7. =head1 NAME
  8. hibernal - An Oddmuse module for improved multi- and single-blogging.
  9. =head1 SYNOPSIS
  10. hibernal extends Oddmuse (and, optionally, Oddmuse's Calendar and SmartTitles
  11. extensions) with reliable, scaleable support for both multi-blogging - in which
  12. one Oddmuse Wiki hosts multiple blogs, each blog singly, separately authored by
  13. one Oddmuse Wiki user - and single-blogging - in which one Oddmuse Wiki hosts
  14. one and only one blog.
  15. =head1 INSTALLATION
  16. hibernal is simply installable; simply:
  17. =over
  18. =item 1. Save this file to the B<wiki/modules/> directory of your Oddmuse Wiki.
  19. =item 2. Optionally, download and install the Calendar extension; see:
  20. http://www.oddmuse.org/cgi-bin/oddmuse/Calendar_Extension
  21. =item 3. Optionally, download and install the Smarttitles extension; see:
  22. http://www.oddmuse.org/cgi-bin/oddmuse/Smarttitles_Extension
  23. =back
  24. Optionally downloading and installing the Calendar extension adds archive
  25. functionality to hibernal. Specifically, it adds a "Posts archive" link to the
  26. foot of every hibernal page that, when browsed to, prints a year-navigable
  27. calendar consisting of all blog posts for this blog.
  28. Optionally downloading and installing the SmartTitles extensions adds subtitle
  29. functionality to hibernal. Specifically, it adds a "Year ${CURRENT_YEAR}"
  30. subtitle to each hibernal archive page; and prints subtitles for each blog post,
  31. for posts having such a subtitle.
  32. =cut
  33. # FIXME: to add to Hibernal: correct Oddmuse's failure to link comment author-names
  34. # having spaces; e.g., entering a username of "David Curry" should auto-link to
  35. # "David_Curry".
  36. AddModuleDescription('hibernal.pl', 'Hibernal Extension');
  37. our ($q, $bol, %Action, %Page, $OpenPageName, %IndexHash, $Now, $Today, %RuleOrder, @MyRules, @MyInitVariables, $CommentsPrefix, $DeletedPage, $CalAsTable);
  38. # ....................{ CONFIGURATION }....................
  39. =head1 CONFIGURATION
  40. hibernal is easily configurable; set these variables in the B<wiki/config.pl>
  41. file for your Oddmuse Wiki.
  42. =cut
  43. our ($HibernalTitleOrSubtitleSuffix,
  44. $HibernalArchiveTitleOrSubtitleSuffix,
  45. $HibernalNewPostLinkText,
  46. $HibernalNewerPostsLinkText,
  47. $HibernalOlderPostsLinkText,
  48. $HibernalArchiveLinkText,
  49. $HibernalArchiveYearLinkText,
  50. $HibernalPostCommentLinkText,
  51. $HibernalPostCommentsLinkText,
  52. $HibernalPostCommentsCreateLinkText,
  53. $HibernalPostCommentsDemarcatorMarkup,
  54. $HibernalPostCommentsAuthorshipMarkup,
  55. $HibernalDefaultPostNameRegexp,
  56. $HibernalDefaultPostsPerPage, $HibernalMaximumPostsPerPage,
  57. $HibernalDefaultTitle, $HibernalDefaultSubtitle,
  58. $HibernalDefaultArchiveTitle, $HibernalDefaultArchiveSubtitle,
  59. $HibernalIsCurrentlyPrinting,
  60. $HibernalDefaultDateRegexp);
  61. =head2 $HibernalTitleOrSubtitleSuffix
  62. A string to to be appended the title or subtitle, as appropriate, for each
  63. hibernal page. This string provides explanatory context for that page. Now,
  64. here's how it works: if that page provides a subtitle, hibernal appends this
  65. string to its subtitle; otherwise, hibernal appends this string to its title.
  66. This prioritization is necessary, so as to keep the SmartTitles extension an
  67. only optional dependency of this extension.
  68. hibernal performs variable substitution on this string, as follows:
  69. =over
  70. =item The first '%s' in this string, if present, is replaced with the index of
  71. the first blog post to be displayed for this hibernal page.
  72. =item The second '%s' in this string, if present, is replaced with the index of
  73. the last blog post to be displayed for this hibernal page.
  74. =back
  75. =cut
  76. $HibernalTitleOrSubtitleSuffix = ' ~ Posts %s — %s';
  77. =head2 $HibernalArchiveTitleOrSubtitleSuffix
  78. A string to to be appended the title or subtitle, as appropriate, for each
  79. hibernal archive page. This string provides explanatory context for that page,
  80. and is context-sensitively applied as in the C<HibernalTitleOrSubtitleSuffix>,
  81. above.
  82. hibernal performs variable substitution on this string, as follows:
  83. =over
  84. =item The first '%s' in this string, if present, is replaced with the year
  85. currently being viewed in this hibernal archive page.
  86. =back
  87. =cut
  88. $HibernalArchiveTitleOrSubtitleSuffix = ' ~ Posts for %s';
  89. =head2 $HibernalNewPostLinkText
  90. The text for the navigational link to create new blog posts (at the foot of each
  91. hibernal page), if the current user is authorized to create such a post. If the
  92. current user is not authorized to create such a post, hibernal displays nothing.
  93. =cut
  94. $HibernalNewPostLinkText = 'New post';
  95. =head2 $HibernalOlderPostsLinkText
  96. The text for the navigational link to older blog posts (at the foot of each
  97. hibernal page).
  98. =cut
  99. $HibernalOlderPostsLinkText = 'Older posts...';
  100. =head2 $HibernalNewerPostsLinkText
  101. The text for the navigational link to newer blog posts (at the foot of each
  102. hibernal page).
  103. =cut
  104. $HibernalNewerPostsLinkText = 'Newer posts...';
  105. =head2 $HibernalArchiveLinkText
  106. The text for the navigational link to the hibernal archive (at the foot of each
  107. hibernal page), if the Calender extension is installed.
  108. =cut
  109. $HibernalArchiveLinkText = 'Posts archive!';
  110. =head2 $HibernalArchiveYearLinkText
  111. The text for each year-specific navigational link at the head of each hibernal
  112. archive page.
  113. hibernal dynamically peruses the set of all blog posts matched by this archive
  114. and, for each calendar year for that archive having at least one blog post,
  115. displays a navigational link to that archive year at the top of each hibernal
  116. archive page.
  117. hibernal performs variable substitution on this text, as follows:
  118. =over
  119. =item The first '%s' in this text, if present, is replaced with the year
  120. currently being linked to.
  121. =back
  122. =cut
  123. $HibernalArchiveYearLinkText = '%s...';
  124. =head2 $HibernalPostCommentLinkText
  125. The text for the navigational link to add a comment to the current blog post
  126. (at the foot of that post).
  127. =cut
  128. $HibernalPostCommentLinkText = 'Add a comment...';
  129. =head2 $HibernalPostCommentsLinkText
  130. The text for the navigational link to the comments for the current blog post
  131. (at the foot of that post), for posts having at least one comment. Note that,
  132. as Oddmuse displays these comments on a page having at its foot an edit box for
  133. adding some comments, we needn't build a separate navigational link for that.
  134. =cut
  135. $HibernalPostCommentsLinkText = 'Comments';
  136. =head2 $HibernalPostCommentsCreateLinkText
  137. The text for the navigational link to create the comments for the current blog
  138. post (at the foot of that post), for posts having no existing comments.
  139. =cut
  140. $HibernalPostCommentsCreateLinkText = 'Comment on this post';
  141. =head2 $HibernalPostCommentsDemarcatorMarkup
  142. Markup for demarcating blog post comments from each other. As Oddmuse
  143. concentrates all blog post comments for a blog post on one Wiki page, hibernal
  144. must provide some markup for differentiating where one blog post comment ends
  145. and another begins. That's what this is.
  146. Specifically, this markup is prepended to all blog post comments for a blog post
  147. (except the first blog post comment for that blog post, since no comments
  148. precede it.)
  149. This is Wiki markup; hibernal expands this text to HTML by applying all Oddmuse
  150. markup rules to it, just as it does for "normal" Wiki page text. (The default
  151. value for this text usually expands to an </hr> tag.)
  152. =cut
  153. $HibernalPostCommentsDemarcatorMarkup = qq`----\n`;
  154. =head2 $HibernalPostCommentsAuthorshipMarkup
  155. Markup for demarcating the author of a blog post comment from the body text of
  156. that blog post comment. Typically, this includes that author’s name, an optional
  157. link to that author’s external homepage or internal Wiki page, and the time at
  158. which that author added that comment.
  159. For customizability, Hibernal performs blog post comment-specific variable
  160. substitution on this markup; this is:
  161. =over
  162. =item The first '%s' in this markup, if present, is replaced with that author.
  163. =item The first '%s' in this markup, if present, is replaced with that time.
  164. =back
  165. This is Wiki markup; Hibernal expands this text to HTML by applying all Oddmuse markup rules to it, just as it does for “normal” Wiki page text. The default value for this text depends on which other markup extensions are also installed. The algorithm is as follows:
  166. =over
  167. =item If the Creole Additions markup extension is installed, this markup
  168. defaults to C<qq`\n\n"""\n%s. %s.\n"""`> -- a blockquote having a bold
  169. author and non-bold time.
  170. =item Otherwise, if the Creole markup extension is installed, this markup
  171. defaults to C<qq`\n\n|%s. %s.|`> -- a table having a bold author and
  172. non-bold time.
  173. =item Otherwise, if the Usemod markup extension is installed, this markup
  174. defaults to C<qq`\n\n||%s. %s.||> -- a table having a bold author and
  175. non-bold time.
  176. =item Otherwise, if the Markup extension is installed, this markup defaults to
  177. C<qq`\n\n⇒ **%s.** %s.`>.
  178. =item Otherwise, if all else fails, this markup defaults to a simple
  179. C<qq`\n\n⇒ %s. %s.`>.
  180. =back
  181. =cut
  182. $HibernalPostCommentsAuthorshipMarkup = undef;
  183. # ....................{ CONFIGURATION =defaults }....................
  184. =head2 $HibernalDefaultPostNameRegexp
  185. The default regular expression for matching blog post page names.
  186. This variable is only a fail-safe; hibernal only applies it to <hibernal...>
  187. markup having no such regular expression.
  188. =cut
  189. $HibernalDefaultPostNameRegexp = '^\d\d\d\d-\d\d-\d\d';
  190. =head2 $HibernalDefaultPostsPerPage
  191. The default number of blog posts to display per hibernal page. This number is
  192. overwritable on a per-blog basis; simply add the desired number of blog posts
  193. to the <hibernal...> markup for that page, ala:
  194. <hibernal "User1--Blog--\d\d\d\d-\d\d-\d\d" 0 16>
  195. =cut
  196. $HibernalDefaultPostsPerPage = 8;
  197. =head2 $HibernalMaximumPostsPerPage
  198. The maximum number of blog posts to display per hibernal page. This number is
  199. not overwritable on a per-blog basis; it serves as a "hard limit" to prevent
  200. abuse of <hibernal...> markup.
  201. =cut
  202. $HibernalMaximumPostsPerPage = 16;
  203. =head2 $HibernalDefaultTitle
  204. The default title for each hibernal page.
  205. This variable is only a fail-safe; hibernal only applies it when failing to
  206. dynamically parse the proper title from the prior hibernal page. Therefore,
  207. you shouldn't need to redefine it.
  208. =cut
  209. $HibernalDefaultTitle = 'Blog';
  210. =head2 $HibernalDefaultSubtitle
  211. The default subtitle for each hibernal page.
  212. This variable is only a fail-safe; hibernal only applies it when failing to
  213. dynamically parse the proper subtitle from the prior hibernal page. Therefore,
  214. you shouldn't need to redefine it.
  215. =cut
  216. $HibernalDefaultSubtitle = '';
  217. =head2 $HibernalDefaultArchiveTitle
  218. The default title for each hibernal archive page.
  219. This variable is only a fail-safe, as above.
  220. =cut
  221. $HibernalDefaultArchiveTitle = 'Blog Archive';
  222. =head2 $HibernalDefaultArchiveSubtitle
  223. The default subtitle for each hibernal archive page.
  224. This variable is only a fail-safe, as above.
  225. =cut
  226. $HibernalDefaultArchiveSubtitle = '';
  227. # ....................{ INITIALIZATION }....................
  228. my ($second_now, $minute_now, $hour_now, $day_now, $month_now, $year_now,
  229. $is_calendar_installed,
  230. $is_creoleaddition_installed,
  231. $is_smarttitles_installed);
  232. push(@MyInitVariables, \&HibernalInit);
  233. sub HibernalInit {
  234. # Convert the current time to machine-readable values.
  235. ($second_now, $minute_now, $hour_now, $day_now, $month_now, $year_now) =
  236. localtime($Now);
  237. $month_now += 1;
  238. $year_now += 1900;
  239. # Test which of our several (optionally) dependent, third-party modules are
  240. # also installed on this Oddmuse Wiki.
  241. $is_calendar_installed = defined &draw_month;
  242. $is_smarttitles_installed = defined &GetSmartTitles;
  243. # Declare which actions we provide based on which modules we have available.
  244. $Action{hibernal} = \&DoHibernal;
  245. $Action{hibernal_archive} = \&DoHibernalArchive if $is_calendar_installed;
  246. # The SmartTitles extension redefines the GetHeader() function. Unfortunately,
  247. # this extension also redefines that function - so as to obtain the page title
  248. # and subtitle for the current Hibernal blog page and propagate the page title
  249. # and subtitle to the next and previous Hibernal blog pages. So, so as to
  250. # correctly piggyback our redefinition of the GetHeader() function on the
  251. # back of the SmartTitles refefinition, we forceably reassign that typeglob
  252. # here, rather than outside a function definition as we'd commonly do.
  253. *GetHibernalHeaderOld = \&GetHeader;
  254. *GetHeader = \&GetHibernalHeader;
  255. # Provide default values for comments authorship markup, depending on which
  256. # other markup modules - if any - are installed.
  257. if (not defined $HibernalPostCommentsAuthorshipMarkup) {
  258. if (defined &CreoleAdditionRule) {
  259. $HibernalPostCommentsAuthorshipMarkup = qq`\n\n"""\n**%s.** %s.\n"""`;
  260. }
  261. elsif (defined &CreoleRule) {
  262. $HibernalPostCommentsAuthorshipMarkup = qq`\n\n|**%s.** %s.|`;
  263. }
  264. elsif (defined &UsemodRule) {
  265. $HibernalPostCommentsAuthorshipMarkup = qq`\n\n||''%s.'' %s.||`;
  266. }
  267. elsif (defined &MarkupRule) {
  268. $HibernalPostCommentsAuthorshipMarkup = qq`\n\n⇒ //**%s.** %s.//`;
  269. }
  270. else {
  271. $HibernalPostCommentsAuthorshipMarkup = qq`\n\n⇒ %s. %s.`;
  272. }
  273. }
  274. }
  275. # ....................{ MARKUP }....................
  276. =head1 MARKUP
  277. This extension handles page markup resembling:
  278. <hibernal post_names="$PostNamesRegexp"
  279. post_bodies="$PostBodiesRegexp"
  280. posts_start_at="$PostsStartAt"
  281. posts_per_page="$PostsPerPage"
  282. posts_ordering="$PostsOrdering">
  283. Or, in its abbreviated form:
  284. <hibernal "$PostNamesRegexp" "$PostBodiesRegexp"
  285. $PostsStartAt $PostsPerPage $PostsOrdering>
  286. Or, in its commonly abbreviated form:
  287. <hibernal "$PostNamesRegexp" $PostsStartAt $PostsPerPage>
  288. Or, in its maximally abbreviated form:
  289. <hibernal "$PostNamesRegexp">
  290. C<$PostNamesRegexp> is a regular expression matching blog post names for this
  291. blog. Usually, blog post names include the full date on which those blog posts
  292. were posted to that blog; e.g., "Brian_Curry--Blog--2008-04-20". Thus, this
  293. regular expression should include an expression matching such dates. Though not
  294. strictly necessary, most blog frontpages should define this regular expression
  295. in a blog-specific way; e.g., "^Brian_Curry--Blog--/d/d/d/d-/d/d-/d/d". See
  296. L<DATE STANDARDS>, below, for discussion of which date formats this extension
  297. supports. (Hint: it's not all of them! Your dates must adhere to a standard
  298. supported by this extension. Ah, shucks.)
  299. C<$PostBodiesRegexp> is a regular expression further matching blog post body
  300. text. (Defining this regular expression introduces noticeable "slowdown"; as
  301. such, most blogs probably not want to define it. It's quite optional, anyway.)
  302. C<$PostsStartAt> is the index of the first blog post to be displayed on this
  303. blog frontpage. It defaults to "0", the most recent blog post.
  304. C<$PostsPerPage> is the number of blog posts to be displayed per blog page. It
  305. defaults to "8", which is quite reasonable.
  306. C<$PostsOrdering> is a string enumeration, taking one of three possible values:
  307. =over
  308. =item reverse
  309. =item past
  310. =item future
  311. =back
  312. And yes - the above regular expressions must be double-quoted, though the other
  313. attributes need not (but also can) be.
  314. =head2 DATE STANDARDS
  315. hibernal only supports two date-matching regular expressions, at the moment.
  316. hibernal only matches blog posts with page names having dates matched by these
  317. regular expressions. (Blog posts named according to "non-standard" date formats
  318. are ignored, by default, by hibernal.) These are, specifically:
  319. =over
  320. =item '\d\d\d\d-\d\d-\d\d': 4-digit year, 2-digit month, 2-digit day; default.
  321. =item '\d\d-\d\d-\d\d\d\d': 2-digit day, 2-digit month, 4-digit year.
  322. =back
  323. hibernal can be extended to support custom date standards, for blogs with blog
  324. post names not obeying either of the above date standards. To effect this,
  325. simply redefine the C<GetHibernalDaySpecificPostNameRegexp> function.
  326. =cut
  327. push(@MyRules, \&HibernalRule);
  328. # Insist this come before conventional markup rules, so as to avoid conflict
  329. # (e.g., expansion of any '~' characters in your passed regular expressions).
  330. $RuleOrder{\&HibernalRule} = -32;
  331. sub HibernalRule {
  332. # <hibernal "regexp" 10> includes 10 pages matching that regular expression.
  333. if ($bol &&
  334. m~\G(\&lt;hibernal
  335. (\s+(?:post_names\s*=\s*)?"(.+?)")?
  336. (\s+(?:post_bodies\s*=\s*)?"(.+?)")?
  337. (\s+(?:posts_start_at\s*=\s*)?"?(\d+)"?)?
  338. (\s+(?:posts_per_page\s*=\s*)?"?(\d+)"?)?
  339. (\s+(?:posts_ordering\s*=\s*)?"?(reverse|past|future)"?)?
  340. \&gt;[ \t]*\n?)~cgix) {
  341. Clean(CloseHtmlEnvironments());
  342. Dirty($1); # do not cache the prefixing "\G"
  343. my ($oldpos, $old_) = (pos, $_);
  344. PrintHibernal($3, $5, $7, $9, $11);
  345. Clean(AddHtmlEnvironment('p')); # if dirty block is looked at later, this will disappear
  346. ($_, pos) = ($old_, $oldpos); # restore \G (assignment order matters!)
  347. return '';
  348. }
  349. return;
  350. }
  351. # ....................{ ACTIONS }....................
  352. =head1 ACTIONS
  353. hibernal provides the following actions.
  354. =head2 hibernal
  355. Prints all blog posts (Wiki pages) matching the query parameters passed to this
  356. action. See the C<DoHibernal> function, below.
  357. =head2 hibernal_archive
  358. Prints a calendar-driven archive of all blog posts (Wiki pages) matching the
  359. query parameters passed to this action. See the C<DoHibernalArchive> function,
  360. below.
  361. =head1 FUNCTIONS
  362. hibernal provides the following functions (for implementing those actions).
  363. =cut
  364. # ....................{ CORE REFACTORS }....................
  365. *AddComment = \&AddHibernalComment;
  366. =head2 AddHibernalComment
  367. Refactors several incongruities in the default C<AddComment> function.
  368. =cut
  369. sub AddHibernalComment {
  370. my ($comments, $comment) = @_;
  371. $comment =~ s~\r~~g; # remove all "\r" (0x0d) characters
  372. $comment =~ s~\s+$~~gs; # remove all trailing whitespace
  373. if ($comment) {
  374. my $author = GetParam('username', T('Anonymous'));
  375. my $homepage = GetParam('homepage', '');
  376. if ($homepage) {
  377. $homepage = "http://$homepage" if not substr($homepage, 0, 7) eq 'http://';
  378. $author = "[[$homepage|$author]]";
  379. }
  380. else {
  381. my $author_page_name = FreeToNormal($author);
  382. if ($IndexHash{$author_page_name}) {
  383. $author = $author_page_name eq $author
  384. ? "[[$author_page_name]]"
  385. : "[[$author_page_name|$author]]";
  386. }
  387. }
  388. # If at least one comment preceded this comment, separate this comment
  389. # from that comment with one hard-break.
  390. if ($comments and $comments =~ m~\S~) {
  391. $comments .= $HibernalPostCommentsDemarcatorMarkup;
  392. }
  393. # Append this comment's author onto this comment.
  394. $comments .= $comment.
  395. Tss($HibernalPostCommentsAuthorshipMarkup, $author, TimeToText($Now));
  396. }
  397. return $comments;
  398. }
  399. # ....................{ PAGE HEADERS }....................
  400. my ($page_title, $page_subtitle);
  401. =head2 GetHibernalHeader
  402. Acquires the title and subtitle from the hibernal front page, for subsequently
  403. passing that title and subtitle to other hibernal and hibernal archive pages.
  404. =cut
  405. sub GetHibernalHeader {
  406. my $html_header = GetHibernalHeaderOld(@_);
  407. (undef, $page_title) = $html_header =~ m~\Q<h1>\E(<a.*>)?(.+?)(</a>)?\Q</h1>\E~;
  408. ($page_subtitle) = $html_header =~ m~\Q<p class="subtitle">\E(.+?)\Q</p>\E~;
  409. return $html_header;
  410. }
  411. =head2 PrintHibernalHeader
  412. Prints the title and subtitle for other hibernal and hibernal archive pages.
  413. (This does not print the title or subtitle for the hibernal front page, as
  414. that's embedded in the physical markup for that page.)
  415. =cut
  416. sub PrintHibernalHeader {
  417. my ($page_title_default, $page_subtitle_default, $suffix) = @_;
  418. $page_title = GetParam('title', $page_title_default);
  419. $page_subtitle = GetParam('subtitle', $page_subtitle_default);
  420. # Avoid tainting the $page_title and $page_subtitle globals with the suffix.
  421. my ($page_title_suffixed, $page_subtitle_suffixed) =
  422. ($page_title, $page_subtitle);
  423. if ($is_smarttitles_installed and $page_subtitle) {
  424. $page_subtitle_suffixed .= $suffix;
  425. }
  426. else {
  427. $page_title_suffixed .= $suffix;
  428. }
  429. # Note: we musn't call "GetHibernalHeader", as that could, conceivably, record
  430. # the suffix for this page's title or subtitle within the string for
  431. # that title or subtitle - which, in recursive turn, would badly cause
  432. # that suffix to be appended to the "next" page's title or subtitle,
  433. # again. (Good grief, eh? There's little relief, here, for insanity...)
  434. print GetHibernalHeaderOld(undef, $page_title_suffixed, undef, undef, undef,
  435. undef, $page_subtitle_suffixed);
  436. }
  437. # ....................{ HIBERNAL }....................
  438. =head2 DoHibernal
  439. Prints all blog posts matched by the passed regular expression and limit bounds.
  440. =cut
  441. sub DoHibernal {
  442. my $post_name_regexp = GetParam('post_name_regexp', $HibernalDefaultPostNameRegexp);
  443. my $post_body_regexp = GetParam('post_body_regexp', '');
  444. my $posts_start_at = GetParam('posts_start_at', 0);
  445. my $posts_per_page = GetParam('posts_per_page', $HibernalDefaultPostsPerPage);
  446. my $posts_ordering = GetParam('posts_ordering', '');
  447. PrintHibernalHeader(T($HibernalDefaultTitle),
  448. T($HibernalDefaultSubtitle),
  449. Tss($HibernalTitleOrSubtitleSuffix,
  450. $posts_start_at,
  451. $posts_start_at + $posts_per_page - 1));
  452. print $q->start_div({-class=> 'content'});
  453. PrintHibernal($post_name_regexp, $post_body_regexp,
  454. $posts_start_at, $posts_per_page, $posts_ordering);
  455. print $q->end_div();
  456. PrintFooter();
  457. }
  458. =head2 PrintHibernal
  459. Prints all blog posts for the current set of blog posts, followed by a
  460. set of links for navigating, managing, and otherwise munging those entries.
  461. =cut
  462. sub PrintHibernal {
  463. return if $HibernalIsCurrentlyPrinting; # avoid infinite loops
  464. local $HibernalIsCurrentlyPrinting = 1;
  465. my ($post_name_regexp, $post_body_regexp,
  466. $posts_start_at, $posts_per_page, $posts_ordering) = @_;
  467. # As this function may, also, be called by HibernalRule(), we must establish
  468. # some decent defaults.
  469. $post_name_regexp = $HibernalDefaultPostNameRegexp unless $post_name_regexp;
  470. $posts_start_at = 0 unless $posts_start_at;
  471. $posts_per_page = $HibernalDefaultPostsPerPage unless $posts_per_page > 0;
  472. $posts_per_page = $HibernalMaximumPostsPerPage unless $posts_per_page <=
  473. $HibernalMaximumPostsPerPage;
  474. # Implicitly ensure the regular expression also includes comments on all
  475. # pages matched by this regular expression.
  476. if ($post_name_regexp !~ m~^\Q^($CommentsPrefix)?\E~ and not
  477. $post_name_regexp =~ s~^\^~^($CommentsPrefix)?~) {
  478. $post_name_regexp = "^($CommentsPrefix)?.*$post_name_regexp";
  479. }
  480. my @post_names =
  481. sort SortHibernalPostNames ( # passes, not calls, SortHibernalPostNames()
  482. grep(/$post_name_regexp/, $post_body_regexp
  483. ? SearchTitleAndBody($post_body_regexp)
  484. : AllPagesList()));
  485. $posts_ordering and OrderHibernalPostNames(\@post_names, $posts_ordering);
  486. if (defined $post_names[$posts_start_at]) {
  487. my $posts_end_at;
  488. # If this Oddmuse Wiki supports comment pages, the determination of how many
  489. # posts to display becomes a complex to this linear calculation.
  490. if ($CommentsPrefix) {
  491. ($posts_start_at, $posts_end_at) = AssayHibernalPostBounds(\@post_names,
  492. $posts_start_at, $posts_end_at, $posts_per_page);
  493. }
  494. # If this Oddmuse Wiki doesn't support comment pages, the determination of
  495. # how many posts P to display devolves to this linear calculation.
  496. else {
  497. $posts_end_at = Max($posts_start_at + $posts_per_page - 1, $#post_names);
  498. }
  499. # Calculate this prior to performing array splices.
  500. my $is_older_posts = $#post_names > $posts_end_at;
  501. @post_names = @post_names[$posts_start_at..$posts_end_at]; # ...now, do it!
  502. # Note: we pass the boolean signifying whether there are older posts; since
  503. # we have truncated the @post_names array, it's no longer sufficient to test
  504. # that array's length to determine whether there are such posts.
  505. @post_names and PrintHibernalContent(\@post_names,
  506. $post_name_regexp, $post_body_regexp,
  507. $posts_start_at, $posts_end_at, $posts_per_page,
  508. $posts_ordering, $is_older_posts);
  509. }
  510. }
  511. =head2 SortHibernalPostNames
  512. Sorts the posts on a hibernal page, according to the Wiki names for those posts
  513. and ensuring that the comment page for a post is sorted after that post.
  514. This function should, probably, be the C<JournalSort>'s default implementation.
  515. =cut
  516. sub SortHibernalPostNames {
  517. my ($A, $B) = ($a, $b);
  518. $A .= 'z' unless $A =~ s/^$CommentsPrefix//;
  519. $B .= 'z' unless $B =~ s/^$CommentsPrefix//;
  520. $B cmp $A;
  521. }
  522. =head2 OrderHibernalPostNames
  523. Orders the posts on a hibernal page, according to whether those posts should be
  524. ordered in date-descending (the default ordering) or date-ascending (the
  525. 'future' and 'reverse' orderings).
  526. =cut
  527. sub OrderHibernalPostNames {
  528. my ($post_names, $posts_ordering) = @_;
  529. if ($posts_ordering eq 'future' or $posts_ordering eq 'reverse') {
  530. @$post_names = reverse @$post_names;
  531. }
  532. # $a and $b, below, are global variables accessed by SortHibernalPostNames().
  533. if ($posts_ordering eq 'future' or $posts_ordering eq 'past') {
  534. $b = defined($Today) ? $Today : CalcDay($Now);
  535. if ($posts_ordering eq 'future') {
  536. for (my $i = 0; $i < @$post_names; $i++) {
  537. $a = $$post_names[$i];
  538. if (SortHibernalPostNames() == -1) {
  539. @$post_names = @$post_names[$i..$#$post_names];
  540. last;
  541. }
  542. }
  543. }
  544. else {
  545. for (my $i = 0; $i < @$post_names; $i++) {
  546. $a = $$post_names[$i];
  547. if (SortHibernalPostNames() == 1) {
  548. @$post_names = @$post_names[$i..$#$post_names];
  549. last;
  550. }
  551. }
  552. }
  553. }
  554. }
  555. =head2 AssayHibernalPostBounds
  556. Returns the boundary indices for posts on the current page. These are the
  557. starting and closing indices for posts as dynamically calculated by inspection
  558. of the post names of all potential posts for this page.
  559. Users expect the number of posts per page to be strictly that, and not the
  560. number of posts per page plus the number of posts having comments per page;
  561. however, as the "@post_names" array has posts comingled with comments, the
  562. number of posts per page plus the number of posts having comments per page
  563. is precisely what we get when we test "$#post_names".
  564. A few calculations to correct that, then!
  565. For any given number of posts P, there are at most P*2 comment pages for
  566. those posts (since each post may have at most one comment page). Let us
  567. call the number of comment pages for those posts C. We may determine the
  568. exact value for C, then, by grepping the
  569. "@post_names[$posts_start_at...($posts_per_page*2-1)]" array slice for all
  570. post names beginning with "$CommentsPrefix". Adding P+C provides the total
  571. number of posts and comment pages to be displayed for this hibernal page,
  572. with which we definitively, finally, slice the "@post_names" array.
  573. =cut
  574. sub AssayHibernalPostBounds {
  575. my ($post_names, $posts_start_at, $posts_end_at, $posts_per_page) = @_;
  576. my $posts_sans_comments;
  577. my $posts_end_at_max =
  578. Max($posts_start_at + $posts_per_page*2 - 1, $#$post_names);
  579. # A bit of an entangling "for" loop, isn't she? "Beware, intrepid code-
  580. # vagabond: off-one-harshities abound, and eat all who enter here."
  581. for ($posts_sans_comments = 0,
  582. $posts_end_at = $posts_start_at - 1,
  583. my $post_index = $posts_start_at;
  584. $post_index <= $posts_end_at_max;
  585. $post_index++, $posts_end_at++) {
  586. if ($$post_names[$post_index] =~ m~^$CommentsPrefix~) {
  587. # If the first post is, actually, a comments page (as possibly, though
  588. # rarely, can occur), faithfully ignore that page by iterating the
  589. # first post to be displayed one past that comments page (which is
  590. # guaranteed to be an actual post by the innate constraints of how
  591. # Oddmuse maintains comments pages). The ignored comments page will,
  592. # presumably, be displayed upon browsing to the "Older posts..." of
  593. # the current posts page.
  594. $posts_start_at++ if $post_index == $posts_start_at;
  595. }
  596. # If we've seen as many non-comment posts ($posts_sans_comments) as the
  597. # user expects ($posts_per_page), then we're done. The index of the post
  598. # we just looked at ($post_index) specifies the index of the last post
  599. # to be shown to that user.
  600. #
  601. # So. Why don't we just add a "$posts_sans_comments < $posts_per_page"
  602. # conditional to the above "for" loop? Doesn't the sudden falsity of that
  603. # conditional imply that we must stop looking and looping? Unfortunately,
  604. # no. There is a subtle off-by-one trap, here.
  605. #
  606. # Consider the edge case in which all posts have comments pages on those
  607. # posts. Let us say that there are four such posts, altogether: two posts
  608. # and two comments pages on those pages. Let us also say that the user
  609. # wants two non-comment posts per page. Then immediately after we look at
  610. # the second non-comment post, we increment $posts_sans_comments, here,
  611. # from its former value of 1 to its new value of 2. Thus, the hypothetical
  612. # conditional described above would cause the loop to stop.
  613. #
  614. # That's bad. Why? Because $post_index would have a value of 2, at that
  615. # point. Posts are indexed from 0. So, that implies that this function
  616. # would return the range (0, 2) -- or, the first post, its comment page,
  617. # and the second post. But this fails to include the second post's comment
  618. # page. We should be returning the range (0, 3), instead. What went wrong?
  619. # That hypothetical conditional terminated the loop too early.
  620. #
  621. # By embedding that conditional here, we ensure that we consider the
  622. # comments page for the last post. (Wee! Wasn't that gleeful fun?)
  623. elsif ($posts_sans_comments++ == $posts_per_page) { last; }
  624. }
  625. return ($posts_start_at, $posts_end_at);
  626. }
  627. =head2 AssayHibernalPostBoundsForNewerPosts
  628. Returns the boundary indices for newer posts on the "prior" page. These are the
  629. starting and closing indices for posts as dynamically calculated by inspection
  630. of the post names of all potential posts for this page. Please note: this
  631. function is, at present, only crudely implemented.
  632. Since the index of the post starting the page of newer posts may not,
  633. necessarily, be strictly governed by the linear calculation
  634. "$posts_start_at - $posts_per_page", due to the presence of intervening
  635. comment pages that can, unfortunately, muck with that calculation, we first
  636. attempt to retrieve its proper value from client-provided query parameters.
  637. While this provides an "adequate" solution, it should probably be improved.
  638. I suppose that the only "genuine" solution is, in the absence of a client-
  639. provided query parameter (which we may always assume to be knowledgeably
  640. correct), to dynamically inspect the set of previous posts for a correct
  641. starting index. As hibernal is, already, fairly heavy-weight, we shall wait
  642. on this "improvement," for a bit. It's quite minor in any advent.
  643. O.K.; I've considered this a bit. I can't reuse the above algorithm, though
  644. the algorithm for discerning this, here, can be quite similar. Essentially,
  645. whereas the above algorithm iterates forward from
  646. [$posts_start_at..$posts_start_at+$posts_per_page*2-1], the algorithm here
  647. must iterate backwards from
  648. [$posts_start_at-1..$posts_start_at-$posts_per_page*2]. (Note the slight
  649. "off-by-one"-ness, here.)
  650. =cut
  651. sub AssayHibernalPostBoundsForNewerPosts {
  652. my ($post_names, $posts_start_at, $posts_end_at, $posts_per_page) = @_;
  653. my $posts_start_at_newer = Max(0,
  654. GetParam('posts_start_at_newer', $posts_start_at - $posts_per_page));
  655. return ($posts_start_at_newer, $posts_start_at - 1);
  656. }
  657. =head2 PrintHibernalContent
  658. Prints blog posts and a set of navigational links after those posts. This
  659. function is separate from C<PrintHibernal>, so as to permit Wiki-specific
  660. redefinition of this function.
  661. =cut
  662. sub PrintHibernalContent {
  663. my ($post_names, $post_name_regexp, $post_body_regexp,
  664. $posts_start_at, $posts_end_at, $posts_per_page,
  665. $posts_ordering, $is_older_posts) = @_;
  666. # Now save information required for saving the cache of the current page.
  667. local %Page;
  668. local $OpenPageName = '';
  669. print $q->start_div({-class=> 'hibernal'});
  670. PrintHibernalPosts($post_names);
  671. PrintHibernalNav (@_);
  672. print $q->end_div();
  673. }
  674. =head2 PrintHibernalPosts
  675. Prints all blog posts for the current set of blog posts.
  676. If the SmartTitles extension is installed, this also changes the titles for
  677. blog posts to reflect "#TITLE" or "#SUBTITLE" markup in the content for those
  678. blog posts.
  679. =cut
  680. sub PrintHibernalPosts {
  681. my $post_names = shift;
  682. my $lang = GetParam('lang', 0);
  683. my ($post_title, $post_subtitle);
  684. my ($prior_post_name, $is_prior_post_commented_on) = ('', 1);
  685. print $q->start_div({-class=> 'posts'});
  686. for my $post_name (@$post_names) {
  687. OpenPage($post_name);
  688. my @languages = split(/,/, $Page{languages});
  689. # Skip this post, if this post's language is not this user's language or if
  690. # marked for deletion but not yet deleted.
  691. next if
  692. ($lang and @languages and not grep(/$lang/, @languages)) or
  693. ($Page{text} =~ m~^$DeletedPage~);
  694. # If this post is a comment, ...
  695. if ($post_name =~ m~^$CommentsPrefix~) {
  696. $is_prior_post_commented_on = 1;
  697. print
  698. $q->start_div({-class=> 'post_comments'})
  699. .$q->div ({-class=> 'post_comments_header'},
  700. GetPageLink($post_name,
  701. Ts($HibernalPostCommentsLinkText)))
  702. .$q->start_div({-class=> 'post_comments_body hibernal_include'});
  703. PrintPageHtml();
  704. print $q->end_div().$q->end_div();
  705. }
  706. # If this post is an actual post, ...
  707. else {
  708. ($post_title, $post_subtitle) = $is_smarttitles_installed
  709. ? GetSmartTitles()
  710. : (NormalToFree($post_name), '');
  711. PrintHibernalPostCommentsCreateLink($prior_post_name, $is_prior_post_commented_on);
  712. $is_prior_post_commented_on = '';
  713. print
  714. $q->start_div({-class=> 'post'})
  715. .$q->div({-class=> 'post_header'},
  716. $q->h1(GetPageLink($post_name, $post_title))
  717. .($post_subtitle
  718. ? $q->p({-class=> 'subtitle'}, $post_subtitle) : ''))
  719. .$q->start_div({-class=> 'post_body hibernal_include'});
  720. PrintPageHtml();
  721. print $q->end_div().$q->end_div();
  722. }
  723. # Retain the most recent post name, for use immediately below.
  724. $prior_post_name = $post_name;
  725. }
  726. # If the final post had no comments, prints a link for creating the first
  727. # comments on that post.
  728. $prior_post_name and
  729. PrintHibernalPostCommentsCreateLink($prior_post_name, $is_prior_post_commented_on);
  730. print $q->end_div();
  731. }
  732. =head2 PrintHibernalPostCommentsCreateLink
  733. If the prior post had no comments, prints a link for creating the first
  734. comments on that post.
  735. =cut
  736. sub PrintHibernalPostCommentsCreateLink {
  737. my ($prior_post_name, $is_prior_post_commented_on) = @_;
  738. print $q->div({-class=> 'post_comments'},
  739. $q->div({-class=> 'post_comments_header'},
  740. GetPageLink($CommentsPrefix.$prior_post_name,
  741. Ts($HibernalPostCommentsCreateLinkText))))
  742. if $CommentsPrefix and $prior_post_name and not $is_prior_post_commented_on;
  743. }
  744. =head2 PrintHibernalNav
  745. Prints links for navigating, managing, and otherwise munging blog posts.
  746. If the Calendar extension is installed, this also prints a link to the calendar-
  747. driven archives for these blog posts.
  748. =cut
  749. #FIXME: Per the Oddmuse norm, the link to create a new post should be displayed
  750. # even when the present user is locked from creating such a post; the link's
  751. # text, then, should probably read something resembling
  752. # "Blogger login". Also, per the Google norm, when there are no older or newer
  753. # posts to be linked to, the links to those pages should devolve into greyed-
  754. # out plaintext.
  755. sub PrintHibernalNav {
  756. my ($post_names, $post_name_regexp, $post_body_regexp,
  757. $posts_start_at, $posts_end_at, $posts_per_page,
  758. $posts_ordering, $is_older_posts) = @_;
  759. my $post_name_regexp_sans_comments =
  760. GetHibernalCommentlessPostNameRegexp($post_name_regexp);
  761. my $hibernal_action = "action=hibernal"
  762. .";post_name_regexp=$post_name_regexp"
  763. .";post_body_regexp=$post_body_regexp"
  764. .";posts_ordering=$posts_ordering";
  765. my $hibernal_archive_action = "action=hibernal_archive"
  766. .";post_name_regexp=$post_name_regexp_sans_comments";
  767. my $action_suffix = '';
  768. # The page title and subtitle were parsed, earlier, by GetHibernalHeader().
  769. if ($page_title ) { $action_suffix .= ';title='. $page_title; }
  770. if ($page_subtitle) { $action_suffix .= ';subtitle='.$page_subtitle; }
  771. $hibernal_action .= $action_suffix;
  772. $hibernal_archive_action .= $action_suffix;
  773. my ($older_posts_link_text, $newer_posts_link_text);
  774. if ($posts_ordering eq 'future' or $posts_ordering eq 'reverse') {
  775. $newer_posts_link_text = $HibernalOlderPostsLinkText;
  776. $older_posts_link_text = $HibernalNewerPostsLinkText;
  777. }
  778. else {
  779. $newer_posts_link_text = $HibernalNewerPostsLinkText;
  780. $older_posts_link_text = $HibernalOlderPostsLinkText;
  781. }
  782. print $q->start_div({-class=> 'nav'});
  783. # If the current user is authorized to edit the page corresponding to today's
  784. # blog post, display a link to that.
  785. my $new_post_name =
  786. GetHibernalDaySpecificPostName($post_name_regexp_sans_comments);
  787. if (UserCanEdit($new_post_name, 0)) {
  788. print GetEditLink($new_post_name, T($HibernalNewPostLinkText), undef, T('e'));
  789. }
  790. # If there are newer posts to be displayed, display a link to them.
  791. if ( $posts_start_at > 0) {
  792. my ($posts_start_at_newer, $posts_end_at_newer) =
  793. AssayHibernalPostBoundsForNewerPosts($post_names,
  794. $posts_start_at, $posts_end_at, $posts_per_page);
  795. print ScriptLink($hibernal_action
  796. .";posts_start_at=$posts_start_at_newer"
  797. .";posts_per_page=$posts_per_page",
  798. T($newer_posts_link_text));
  799. }
  800. # If there are older posts to be displayed, display a link to them. (Display
  801. # this link afore the link to newer posts, as that better coincides with
  802. # aesthetic expectations - or some such jiggery.)
  803. #
  804. # As for why we pass the relatively hacky "posts_start_at_newer" query
  805. # parameter, see AssayHibernalPostBoundsForNewerPosts() comments.
  806. if ($is_older_posts) {
  807. print ScriptLink($hibernal_action
  808. .";posts_start_at_newer=$posts_start_at"
  809. .";posts_start_at=".($posts_end_at + 1)
  810. .";posts_per_page=$posts_per_page",
  811. T($older_posts_link_text));
  812. }
  813. # If the Calendar extension is also installed, display a link to the archive.
  814. if ($is_calendar_installed) {
  815. print ScriptLink($hibernal_archive_action, T($HibernalArchiveLinkText));
  816. }
  817. print $q->end_div();
  818. }
  819. # ....................{ HIBERNAL ARCHIVE }....................
  820. =head2 DoHibernalArchive
  821. Prints a yearly calendar of all blog posts matched by the passed regular
  822. expression and desired year.
  823. This action requires the third-party Calendar extension.
  824. =cut
  825. sub DoHibernalArchive {
  826. my $post_name_regexp = GetParam('post_name_regexp', $HibernalDefaultDateRegexp);
  827. my $year = GetParam('year', $year_now);
  828. PrintHibernalHeader(T($HibernalDefaultArchiveTitle),
  829. T($HibernalDefaultArchiveSubtitle),
  830. Ts($HibernalArchiveTitleOrSubtitleSuffix, $year));
  831. print $q->start_div({-class=> 'content'});
  832. PrintHibernalArchive($post_name_regexp, $year);
  833. print $q->end_div();
  834. PrintFooter();
  835. }
  836. =head2 PrintHibernalArchive
  837. This supplants the old C<PrintYearCalendar> function, which provided fewer settings,
  838. less CSS, and, in general, just less.
  839. =cut
  840. sub PrintHibernalArchive {
  841. my ($post_name_regexp, $year) = @_;
  842. # Most bloggers are unlikely to want comment pages in their blog archives;
  843. # consequently, this filters those pages away by preventing this regular
  844. # expression from matching them.
  845. $post_name_regexp = GetHibernalCommentlessPostNameRegexp($post_name_regexp);
  846. print $q->start_div({-class=> 'hibernal_archive cal'});
  847. PrintHibernalArchiveNav ($post_name_regexp, $year);
  848. PrintHibernalArchiveYear($post_name_regexp, $year);
  849. print $q->end_div();
  850. }
  851. sub PrintHibernalArchiveNav {
  852. my ($post_name_regexp, $year) = @_;
  853. my @post_names = AllPagesList();
  854. my %matching_years;
  855. my $match_year_regexp = $post_name_regexp;
  856. $match_year_regexp =~ s~(\Q\d\d\d\d\E)~($1)~;
  857. foreach my $post_name (@post_names) {
  858. if ($post_name =~ m~$match_year_regexp~) { $matching_years{$1} = 1; }
  859. }
  860. print $q->start_div({-class=> 'nav'});
  861. my $hibernal_archive_action =
  862. "action=hibernal_archive;post_name_regexp=$post_name_regexp";
  863. # The page title and subtitle were parsed, earlier, by "GetHibernalHeader".
  864. if ($page_title ) { $hibernal_archive_action .= ';title='. $page_title; }
  865. if ($page_subtitle) { $hibernal_archive_action .= ';subtitle='.$page_subtitle; }
  866. foreach my $matching_year (sort keys %matching_years) {
  867. print ScriptLink($hibernal_archive_action.";year=$matching_year",
  868. Ts($HibernalArchiveYearLinkText, $matching_year));
  869. }
  870. print $q->end_div();
  871. }
  872. sub PrintHibernalArchiveYear {
  873. my ($post_name_regexp, $year) = @_;
  874. print $q->start_div({-class=> 'year'});
  875. if ($CalAsTable) {
  876. print '<table><tr>';
  877. for my $month (1..12) {
  878. print '<td>' . GetHibernalArchiveMonth($post_name_regexp, $year, $month) . '</td>';
  879. # Enforce the customary calendar layout of three months per calendar row.
  880. print '</tr><tr>' if $month == 3 or $month == 6 or $month == 9;
  881. }
  882. print '</tr></table>';
  883. }
  884. else {
  885. for my $month (1..12) {
  886. print GetHibernalArchiveMonth($post_name_regexp, $year, $month);
  887. }
  888. }
  889. # See documention internal to the GetHibernalArchiveMonth() function, below.
  890. #
  891. # Note, this must be nested within the <div class="year">...</div> tag-set.
  892. # Failure to do this causes borders on that year (and, probably, other CSS
  893. # flourishes) to deceitfully vanish.
  894. print $q->div({-class=> 'year_end', -style=> 'clear: left'})
  895. .$q->end_div();
  896. }
  897. =head2 GetHibernalArchiveMonth
  898. Unfortunately, as the default C<Cal> function is a bit monolithic, this
  899. necessarily reduplicates a large part of that function. Such is life in the code
  900. trenches.
  901. =cut
  902. sub GetHibernalArchiveMonth {
  903. my ($post_name_regexp, $year, $month) = @_; # example: 2004, 12
  904. #FIXME: Should use a well-defined Oddmuse CSS error class.
  905. if ($year < 1) { return $q->p(T('Illegal year value: Use 0001-9999')); }
  906. my $html_month = draw_month($month, $year).'</div>';
  907. # Order of substitution is not important, here.
  908. $html_month =~ s~\s*(\S+) \d\d\d\d\n(.+?\n)~
  909. GetHibernalArchiveMonthHeader($year, $month, $1, $2)
  910. ~e;
  911. $html_month =~ s~( {1,2})(\d{1,2})\b~
  912. $1.GetHibernalArchiveMonthDay($post_name_regexp, $year, $month, $2)
  913. ~eg;
  914. # Float the HTML for each month horizontally past the month preceding it;
  915. # failure to float months in this manner causes these months to stack
  916. # vertically, than horizontally. (Vertically stacking months makes the month-
  917. # driven user interface unusable, effectively.) As such, this function
  918. # enforces horizontally stacking months as a CSS default via the following
  919. # inline style. Usually, inline styles are anathema, as they take dictatorial
  920. # precedence over external stylesheets in CSS's cascade model. (Inline styles
  921. # cannot be overridden on a per-site basis.) In this instance, given the poor
  922. # usability of horizontally stacking months, it makes an acceptable exception.
  923. #
  924. # Note, also, that floating months requires we "clear" the floating attribute
  925. # away, afterwards. Failure to do this will propagate that floating attribute
  926. # to all proceeding block-level elements, which, as expected, unfashionably
  927. # disrupts the remainder of the CSS-entangled user interface. We thus emit a
  928. # companion inline style to forcefully "clear" the floating attribute; of
  929. # necessity, we emit this style following emission of the set of all HTML
  930. # months, above.
  931. return $q->div({-class=> 'month', -style=> 'float: left'}, $html_month);
  932. }
  933. sub GetHibernalArchiveMonthHeader {
  934. my ($year, $month, $month_text, $day_labels) = @_;
  935. my $date = sprintf('%d-%02d', $year, $month);
  936. return
  937. $q->div({-class=> 'month_header'},
  938. ScriptLink("action=collect;match=%5e$date",
  939. "$month_text $year",
  940. 'local collection month'))
  941. .$q->start_div({-class=> 'month_body'})
  942. .$q->span({-class=> 'day_labels'}, $day_labels);
  943. }
  944. sub GetHibernalArchiveMonthDay {
  945. my ($post_name_regexp, $year, $month, $day) = @_;
  946. my $class = $day == $day_now && $month == $month_now && $year == $year_now
  947. ? ' today'
  948. : ''
  949. ;
  950. $post_name_regexp = GetHibernalDaySpecificPostNameRegexp(@_);
  951. my @post_name_matches = grep(/$post_name_regexp/, AllPagesList());
  952. if (@post_name_matches == 0) { # not using GetEditLink because of $class
  953. return ScriptLink('action=edit;id='.UrlEncode(GetHibernalDaySpecificPostName(@_)),
  954. $day, 'edit'.$class);
  955. }
  956. elsif (@post_name_matches == 1) { # not using GetPageLink because of $class
  957. return ScriptLink($post_name_matches[0],
  958. $day, 'local exact'.$class);
  959. }
  960. else {
  961. return ScriptLink('action=collect;match='.UrlEncode($post_name_regexp),
  962. $day, 'local collection'.$class);
  963. }
  964. }
  965. # ....................{ UTILITY FUNCTIONS }....................
  966. =head2 Tss
  967. Translates a variable number of format variables through one format string.
  968. This function leverages the C<Ts> function; and, thus, could be considered the
  969. expanded, var-arg version of that funcion.
  970. =cut
  971. sub Tss {
  972. my $format_string = shift;
  973. my @format_variables = @_;
  974. $format_string = Ts($format_string, $_) foreach (@format_variables);
  975. return $format_string;
  976. }
  977. sub Max {
  978. my ($x, $y) = @_;
  979. return $x >= $y ? $x : $y;
  980. }
  981. sub GetHibernalDaySpecificPostName {
  982. my $post_name = GetHibernalDaySpecificPostNameRegexp(@_);
  983. $post_name =~ s~^\^~~;
  984. $post_name =~ s~\$$~~;
  985. return $post_name;
  986. }
  987. sub GetHibernalDaySpecificPostNameRegexp {
  988. my ($post_name_regexp, $year, $month, $day) = @_;
  989. $year = $year_now unless $year;
  990. $month = $month_now unless $month;
  991. $day = $day_now unless $day;
  992. my ($date_regexp) = $post_name_regexp =~ m~(\\d\\d(?:\\d|-)+)~;
  993. if ($date_regexp) {
  994. if ($date_regexp eq '\d\d\d\d-\d\d-\d\d') {
  995. $post_name_regexp =~
  996. s~\Q$date_regexp\E~sprintf('%d-%02d-%02d', $year, $month, $day)~e;
  997. }
  998. elsif ($date_regexp eq '\d\d-\d\d-\d\d\d\d') {
  999. $post_name_regexp =~
  1000. s~\Q$date_regexp\E~sprintf('%02d-%02d-%d', $day, $month, $year)~e;
  1001. }
  1002. else { undef $date_regexp; }
  1003. }
  1004. # If this page name does not conform to a hibernal-recognized date standard,
  1005. # we still try to salvage things by "best guessing" it.
  1006. if (not $date_regexp) {
  1007. $post_name_regexp =~ s/\Q\d\d\d\d\E/$year/;
  1008. $post_name_regexp =~ s/\Q\d\d\E/sprintf('%02d', $month)/e;
  1009. $post_name_regexp =~ s/\Q\d\d\E/sprintf('%02d', $day)/e;
  1010. }
  1011. return $post_name_regexp;
  1012. }
  1013. sub GetHibernalCommentlessPostNameRegexp {
  1014. my $post_name_regexp = shift;
  1015. $post_name_regexp =~ s~^\Q^($CommentsPrefix)?\E~^~ if $CommentsPrefix;
  1016. return $post_name_regexp;
  1017. }
  1018. =head1 EXAMPLES
  1019. hibernal builds blogs in a similar way to Oddmuse's own <journal...> markup.
  1020. This is pretty simple; so, let's examine a pretty simple example (or three).
  1021. =head2 A UNI-BLOGGING EXAMPLE
  1022. Suppose there exists a page named "Blog" on some Oddmuse Wiki that contains,
  1023. anywhere in its page content, the following markup:
  1024. <hibernal>
  1025. Then, the page named "Blog" becomes the front page for this Wiki's blog. It
  1026. automatically collects the most recent of all pages whose page names match
  1027. the (default) regular expression "\d\d\d\d-\d\d-\d\d" (i.e., consisting of a
  1028. 4-digit year, 2-digit month, and 2-digit day); and, also, automatically provides
  1029. one navigational link for creating a new blog post (corresponding to today),
  1030. one navigational link for browsing newer blog posts (if there are newer posts),
  1031. one navigational link for browsing older blog posts (if there are older posts),
  1032. and one navigational link for browsing the archive of all blog posts via a
  1033. (somewhat intuitive) calendar-driven interface.
  1034. This is as good - and simple - as it gets. hibernal performs all the "heavy
  1035. lifting," behind the code scenes, to glue, link, and conform all of the above
  1036. components. (All you have to do is include the <hibernal> markup! There is a
  1037. respectable bargain, if ever there was.)
  1038. This is the "uni-blogging" scenario. One Oddmuse Wiki collects all matching
  1039. blog posts onto one blog front page. Now, let's examine a somewhat less simple
  1040. example.
  1041. =head2 A MULTI-BLOGGING EXAMPLE
  1042. Suppose there exists some page named "User1--Blog" on some Oddmuse Wiki that
  1043. contains, anywhere in its page content, the following markup:
  1044. <hibernal "User1--Blog--\d\d\d\d-\d\d-\d\d">
  1045. Then suppose there exists another page named "User2--Blog" on that Wiki that
  1046. contains, anywhere in its page content, the following similar markup:
  1047. <hibernal "User2--Blog--\d\d\d\d-\d\d-\d\d">
  1048. The page named "User1--Blog" becomes the front page for User1's blog; likewise,
  1049. the page named "User2--Blog" becomes the front page for User2's blog. (User1
  1050. and User2 are, presumably, two users on this Wiki.) The "User1--Blog" page
  1051. collects blog posts matching the regular expression for that user's blog
  1052. ("User1--Blog--\d\d\d\d-\d\d-\d\d"); similarly, the "User2--Blog" page
  1053. collects blog posts matching the regular expression for that user's blog
  1054. ("User2--Blog--\d\d\d\d-\d\d-\d\d"). As above, both front pages automatically
  1055. provide navigational links for managing these blogs and blog posts.
  1056. And all is well that ends well, and simple.
  1057. This is the "multi-blogging" scenario. One Oddmuse Wiki collects all separately
  1058. matching blog posts onto two separate blog front pages. Now, let's examine a
  1059. somewhat similar example: how, exactly, do users create new blog posts?
  1060. =head2 A MULTI-BLOGGING "NEW POSTS" EXAMPLE
  1061. Suppose the above two users and corresponding user-specific Wiki pages. Also,
  1062. suppose there exist six pages on the Wiki: "User1--Blog--2008-08-08",
  1063. "User1--Blog--2007-02-26", "User1--Blog--2000-04-24",
  1064. "User2--Blog--2004-05-16", "User2--Blog--2004-05-14", and
  1065. "User2--Blog--2004-04-28".
  1066. Then, the page named "User1--Blog" shows the "User1--Blog--2008-08-08" page
  1067. first (as the first blog post for the blog), "User1--Blog--2007-02-26" second
  1068. (as the second-most blog post for the blog), and "User1--Blog--2000-04-24"
  1069. list (as the oldest blog post for the blog); and "User2--Blog", similarly, shows
  1070. its matching pages as blog posts in chronological order.
  1071. To add a new blog post to the front page for User1's blog, that user must:
  1072. =over
  1073. =item Create a new Wiki page with name following the above naming convention; or
  1074. =item Click the "New post" link at the footer of each hibernal page.
  1075. =back
  1076. And that new post, of hibernal's built-in magic, is automatically collected into
  1077. its proper chronological ordering on that front page.
  1078. =head1 MULTI-BLOGGING
  1079. hibernal, as L<A MULTI-BLOGGING EXAMPLE> (above) demonstrates, has amply capable
  1080. support for such multi-blogging. Indeed! This example's infinitely scaleable to
  1081. two or more user-defined blogs, as desired by this Wiki. Each user follows some
  1082. unique naming convention for blog post pages; and makes:
  1083. =over
  1084. =item One front page named anything, containing a <hibernal...> expression
  1085. matching other pages following that naming convention, and
  1086. =item One or more blog post pages following that convention.
  1087. =back
  1088. Huzzah!
  1089. =head1 MOTIVATION
  1090. By default, Oddmuse comes with poor to (frankly) no support for multi-blogging
  1091. and merely unstylish, unconfigurable, unsupportable support for uni-blogging.
  1092. Oddmuse administrators have "corrected" this, in the darkling past, by:
  1093. =over
  1094. =item Hackishly isolating each blog on a website onto one distinctly separate
  1095. Oddmuse Wiki installations on that website; by
  1096. =item Hacking Wiki functions, non-reusably; and, occasionally, by
  1097. =item Hack-installing an incommunicado-ish hodgepodge of unsupported third-party
  1098. Oddmuse Wiki extensions.
  1099. Of course, Oddmuse is a Wiki consisting of one Perl script! (This is its
  1100. genuis; and its conceit.) It is not, and not intended, to masquerade as a full-
  1101. blown Content Management System (CMS) or proper "publishing platform."
  1102. Nonetheless, hibernal demonstrates that core Oddmuse functionality can be
  1103. improved, substantially, so as to permit one Oddmuse Wiki to mimic conventional
  1104. blogging frameworks and, thereby, scaleably host one or more blogs on that Wiki.
  1105. Furthermore, hibernal improves support for single-blogging. It redefines most
  1106. journal- and calendar-specific functionality with fine-grained, user-settable
  1107. CSS, HTML, and RSS customizations, reusability, and code coherence.
  1108. =head1 THANKLIST
  1109. hibernal is the hive-minded product of "prior art" and artful code. For that,
  1110. our dutiful thanks is due: to Alex Schröder (for initializing code-work on the
  1111. Journal, RSS, and Calendar extensions), to Charles Mauch (for code-work on the
  1112. SmartTitles extension), or to all those hapless, nameless others, whose names,
  1113. unremembered, unfurl away. Here's codin' at you, Oddmuse kiddos.
  1114. =head1 COPYRIGHT AND LICENSE
  1115. The information below applies to everything in this distribution,
  1116. except where noted.
  1117. Copyleft 2008 by B.w.Curry <http://www.raiazome.com>.
  1118. This program is free software; you can redistribute it and/or modify
  1119. it under the terms of the GNU General Public License as published by
  1120. the Free Software Foundation; either version 3 of the License, or
  1121. (at your option) any later version.
  1122. This program is distributed in the hope that it will be useful,
  1123. but WITHOUT ANY WARRANTY; without even the implied warranty of
  1124. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  1125. GNU General Public License for more details.
  1126. You should have received a copy of the GNU General Public License
  1127. along with this program. If not, see L<http://www.gnu.org/licenses/>.
  1128. =cut