PageRenderTime 75ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/dylan/page.dylan

http://github.com/cgay/wiki
Unknown | 765 lines | 666 code | 99 blank | 0 comment | 0 complexity | cdefc73b2d102f6e4cf2f01fd4a566cd MD5 | raw file
  1. Module: %wiki
  2. /// Default number of pages to show on the list-pages page.
  3. define constant $default-list-size :: <integer> = 25;
  4. // Represents a user-editable wiki page revision. Not to be confused
  5. // with <wiki-dsp>, which is a DSP maintained in our source code tree.
  6. //
  7. define class <wiki-page> (<wiki-object>)
  8. constant slot page-source :: <string>,
  9. required-init-keyword: source:;
  10. // A sequence of <string>s (of RST source) or <wiki-reference>s.
  11. constant slot page-parsed-source :: <sequence>,
  12. init-keyword: parsed-source:;
  13. // Comment entered by the user describing the changes for this revision.
  14. constant slot page-comment :: <string>,
  15. required-init-keyword: comment:;
  16. // The owner has special rights over the page, depending on the ACLs.
  17. // The owner only changes if explicitly changed via the edit-acls page.
  18. // TODO: move this into <acls>.
  19. slot page-owner :: <wiki-user>,
  20. required-init-keyword: owner:;
  21. // The author is the one who saved this particular revision of the page.
  22. constant slot page-author :: <wiki-user>,
  23. required-init-keyword: author:;
  24. slot page-access-controls :: <acls>,
  25. required-init-keyword: access-controls:;
  26. // Tags (strings) entered by the author when the page was saved.
  27. constant slot page-tags :: <sequence> = #(),
  28. init-keyword: tags:;
  29. // e.g. a git commit hash or a revision number
  30. // Filled in by the storage back-end.
  31. slot page-revision :: <string>,
  32. init-keyword: revision:;
  33. end class <wiki-page>;
  34. /// Provide defaulting and a copy-from argument.
  35. define method make
  36. (class == <wiki-page>,
  37. #rest args,
  38. #key copy-from :: false-or(<wiki-page>),
  39. name, source, parsed-source, comment, owner,
  40. author, tags, access-controls, revision)
  41. => (page :: <wiki-page>)
  42. let p = copy-from;
  43. let name = name | (p & p.object-name);
  44. let source = source | (p & p.page-source);
  45. let owner = owner | (p & p.page-owner) | author;
  46. apply(next-method,
  47. class,
  48. name: name,
  49. source: source,
  50. parsed-source: parsed-source
  51. | (source & parse-wiki-markup(source, name))
  52. | (p & p.parsed-source),
  53. comment: comment | "",
  54. owner: owner,
  55. author: author | (p & p.page-author) | owner,
  56. tags: tags | (p & p.page-tags) | #(),
  57. access-controls: access-controls
  58. | (p & p.page-access-controls)
  59. | $default-access-controls,
  60. args)
  61. end method make;
  62. // back compat
  63. define inline function page-title
  64. (page :: <wiki-page>) => (title :: <string>)
  65. page.object-name
  66. end;
  67. // back compat
  68. /* unused
  69. define inline function page-title-setter
  70. (new-name :: <string>, page :: <wiki-page>) => (new-name :: <string>)
  71. page.object-name := new-name
  72. end;
  73. */
  74. define thread variable *page* :: false-or(<wiki-page>) = #f;
  75. define named-method page? in wiki
  76. (page :: <dylan-server-page>)
  77. *page* ~= #f
  78. end;
  79. //// URLs
  80. define method permanent-link
  81. (page :: <wiki-page>)
  82. => (url :: <url>)
  83. page-permanent-link(page.page-title)
  84. end;
  85. define method page-permanent-link
  86. (title :: <string>)
  87. => (url :: <url>)
  88. let location = wiki-url("/page/view/%s", title);
  89. transform-uris(request-url(current-request()), location, as: <url>)
  90. end;
  91. define method redirect-to (page :: <wiki-page>)
  92. redirect-to(permanent-link(page));
  93. end;
  94. /// Find a cached page.
  95. define method find-page
  96. (title :: <string>)
  97. => (page :: false-or(<wiki-page>))
  98. element(*pages*, title, default: #f)
  99. end;
  100. /* unused
  101. define method page-exists?
  102. (title :: <string>) => (exists? :: <boolean>)
  103. find-page(title) & #t
  104. end;
  105. */
  106. // The latest revisions of all pages are loaded at startup for now (to
  107. // simplify searches and iteration over lists of pages) so this will only
  108. // load anything if the 'revision' arg is supplied. Note that 'revision'
  109. // should never be "head" or any other symbolic revision.
  110. //
  111. define method find-or-load-page
  112. (title :: <string>, #key revision :: false-or(<string>))
  113. => (page :: false-or(<wiki-page>))
  114. let page = find-page(title);
  115. if (page & (~revision | page.page-revision = revision))
  116. page
  117. else
  118. block ()
  119. if (revision)
  120. // Don't attempt to cache older revisions of pages.
  121. load(*storage*, <wiki-page>, title, revision: revision);
  122. else
  123. // Load page is slow, do it without the lock held.
  124. let loaded-page = load(*storage*, <wiki-page>, title);
  125. with-lock ($page-lock)
  126. // check again with lock held
  127. find-page(title)
  128. | (*pages*[title] := loaded-page)
  129. end
  130. end
  131. exception (ex :: <git-storage-error>)
  132. #f
  133. end
  134. end
  135. end method find-or-load-page;
  136. // The plan is for this to eventually support many more search criteria,
  137. // such as searching by owner, author, date ranges, etc.
  138. //
  139. define method find-pages
  140. (#key tags :: <sequence> = #[],
  141. order-by :: <function> = title-less?)
  142. => (pages :: <sequence>)
  143. let pages = sort(with-lock ($page-lock)
  144. value-sequence(*pages*)
  145. end,
  146. test: order-by);
  147. if (~empty?(tags))
  148. local method page-has-tags? (page :: <wiki-page>)
  149. any?(method (tag)
  150. member?(tag, page.page-tags, test: \=)
  151. end,
  152. tags)
  153. end;
  154. pages := choose(page-has-tags?, pages);
  155. end;
  156. pages
  157. end;
  158. define function title-less?
  159. (p1 :: <wiki-page>, p2 :: <wiki-page>) => (less? :: <boolean>)
  160. as-lowercase(p1.page-title) < as-lowercase(p2.page-title)
  161. //case-insensitive-less?(p1.page-title, p2.page-title)
  162. end;
  163. define function creation-date-newer?
  164. (p1 :: <wiki-page>, p2 :: <wiki-page>) => (less? :: <boolean>)
  165. p1.creation-date > p2.creation-date
  166. end;
  167. // todo -- Implement this as a wiki page.
  168. define constant $reserved-tags :: <sequence> = #["news"];
  169. define method reserved-tag?
  170. (tag :: <string>) => (reserved? :: <boolean>)
  171. member?(tag, $reserved-tags, test: \=)
  172. end;
  173. define method save-page
  174. (title :: <string>, source :: <string>, comment :: <string>, tags :: <sequence>)
  175. => (page :: <wiki-page>)
  176. let user = authenticated-user();
  177. let old-page = find-page(title);
  178. let page = make(<wiki-page>,
  179. copy-from: old-page,
  180. name: title,
  181. source: source,
  182. tags: tags,
  183. comment: comment,
  184. author: user);
  185. update-reference-tables!(page,
  186. iff(old-page,
  187. outbound-references(old-page),
  188. #()),
  189. outbound-references(page));
  190. let action = "create";
  191. with-lock ($page-lock)
  192. if (key-exists?(*pages*, title))
  193. action := "edit";
  194. end;
  195. *pages*[title] := page;
  196. end;
  197. page.page-revision := store(*storage*, page, page.page-author, comment,
  198. standard-meta-data(page, action));
  199. /*
  200. TODO:
  201. block ()
  202. generate-connections-graph(page);
  203. exception (ex :: <serious-condition>)
  204. // we don't care about the graph (yet?)
  205. // maybe the server doesn't have "dot" installed.
  206. log-error("Error generating connections graph for page %s: %s",
  207. title, ex);
  208. end;
  209. */
  210. page
  211. end method save-page;
  212. /* Not converted to new git-backed wiki yet...
  213. define method generate-connections-graph
  214. (page :: <wiki-page>) => ()
  215. let graph = make(gvr/<graph>);
  216. let node = gvr/create-node(graph, label: page.page-title);
  217. let backlinks = find-backlinks(page);
  218. backlinks := map(page-title, backlinks);
  219. gvr/add-predecessors(node, backlinks);
  220. gvr/add-successors(node, last(page.page-versions).references);
  221. for (node in gvr/nodes(graph))
  222. node.gvr/attributes["URL"] := build-uri(page-permanent-link(node.gvr/label));
  223. node.gvr/attributes["color"] := "blue";
  224. node.gvr/attributes["style"] := "filled";
  225. node.gvr/attributes["fontname"] := "Verdana";
  226. node.gvr/attributes["shape"] := "note";
  227. end for;
  228. let temporary-graph = gvr/generate-graph(graph, node, format: "svg");
  229. let graph-file = as(<file-locator>, temporary-graph);
  230. if (file-exists?(graph-file))
  231. let destination = as(<file-locator>,
  232. concatenate("graphs/", page.page-title, ".svg"));
  233. rename-file(graph-file, destination, if-exists: #"replace");
  234. end if;
  235. end;
  236. */
  237. /* unused
  238. define method rename-page
  239. (page :: <wiki-page>, new-title :: <string>, comment ::<string>)
  240. => ()
  241. let author = authenticated-user();
  242. let old-title = page.page-title;
  243. let revision = rename(*storage*, page, new-title, author, comment,
  244. standard-meta-data(page, "rename"));
  245. with-lock ($page-lock)
  246. remove-key!(*pages*, old-title);
  247. *pages*[new-title] := page;
  248. end;
  249. page.page-title := new-title;
  250. page.page-revision := revision;
  251. end method rename-page;
  252. */
  253. define method discussion-page?
  254. (page :: <wiki-page>)
  255. => (is? :: <boolean>)
  256. let (matched?, discussion, title)
  257. = regex-search-strings(compile-regex("(Discussion: )(.*)"),
  258. page.page-title);
  259. matched? = #t;
  260. end;
  261. //// List Versions
  262. define class <page-history-page> (<wiki-dsp>)
  263. end;
  264. define method respond-to-get
  265. (dsp :: <page-history-page>,
  266. #key title :: <string>, revision :: false-or(<string>))
  267. let title = percent-decode(title);
  268. let page = find-or-load-page(title);
  269. if (page)
  270. dynamic-bind (*page* = page)
  271. let pc = page-context();
  272. set-attribute(pc, "title", title);
  273. local method change-to-table (change)
  274. // TODO: a way to define DSP accessors for objects such as
  275. // <wiki-change> so this isn't necessary.
  276. make-table(<string-table>,
  277. "author" => change.change-author,
  278. "date" => as-iso8601-string(change.change-date),
  279. "rev" => change.change-revision,
  280. "comment" => change.change-comment)
  281. end;
  282. set-attribute(pc, "page-changes",
  283. map(change-to-table,
  284. find-changes(*storage*, <wiki-page>,
  285. name: title, start: revision)));
  286. next-method();
  287. end;
  288. else
  289. respond-to-get(*non-existing-page-page*, title: title);
  290. end;
  291. end;
  292. //// Page references (connections, backlinks)
  293. define class <connections-page> (<wiki-dsp>)
  294. end;
  295. define method respond-to-get
  296. (page :: <connections-page>, #key title :: <string>)
  297. let title = percent-decode(title);
  298. dynamic-bind (*page* = find-or-load-page(title))
  299. if (*page*)
  300. next-method();
  301. else
  302. respond-to-get(*non-existing-page-page*, title: title);
  303. end;
  304. end;
  305. end method respond-to-get;
  306. // rename to list-referring-pages
  307. define body tag list-page-backlinks in wiki
  308. (page :: <wiki-dsp>, do-body :: <function>)
  309. ()
  310. let backlinks = sort(inbound-references(*page*),
  311. test: method (x, y)
  312. as-lowercase(x.object-name) < as-lowercase(y.object-name)
  313. end);
  314. if (empty?(backlinks))
  315. output("There are no connections to this page.");
  316. else
  317. let pc = page-context();
  318. for (backlink in backlinks)
  319. set-attribute(pc, "backlink", backlink.page-title);
  320. set-attribute(pc, "backlink-url", as(<string>, permanent-link(backlink)));
  321. do-body();
  322. end for;
  323. end if;
  324. end;
  325. //// List Pages
  326. define class <list-pages-page> (<wiki-dsp>) end;
  327. /// GET lists the pages in alphabetical order
  328. ///
  329. define method respond-to-get
  330. (dsp :: <list-pages-page>, #key)
  331. let pc = page-context();
  332. local method page-info (page :: <wiki-page>)
  333. make-table(<string-table>,
  334. "title" => page.page-title,
  335. "when-published" => standard-date-and-time(page.creation-date),
  336. "latest-authors" => page.page-author.user-name)
  337. end;
  338. let current-page = get-query-value("page", as: <integer>) | 1;
  339. let paginator = make(<paginator>,
  340. sequence: map(page-info, find-pages()),
  341. page-size: $default-list-size,
  342. current-page-number: current-page);
  343. set-attribute(pc, "wiki-pages", paginator);
  344. next-method();
  345. end method respond-to-get;
  346. /// POST finds a particular page (the 'query') and displays it.
  347. ///
  348. define method respond-to-post
  349. (dsp :: <list-pages-page>, #key)
  350. redirect-to(page-permanent-link(get-query-value("query")));
  351. end;
  352. //// Remove page
  353. define class <remove-page-page> (<wiki-dsp>)
  354. end;
  355. define method respond-to-get
  356. (dsp :: <remove-page-page>, #key title :: <string>)
  357. dynamic-bind (*page* = find-or-load-page(title))
  358. process-template(dsp);
  359. end;
  360. end;
  361. define method respond-to-post
  362. (dsp :: <remove-page-page>, #key title :: <string>)
  363. let page = find-or-load-page(percent-decode(title));
  364. if (page)
  365. delete(*storage*, page, authenticated-user(),
  366. get-query-value("comment") | "",
  367. standard-meta-data(page, "delete"));
  368. with-lock ($page-lock)
  369. remove-key!(*pages*, title);
  370. end;
  371. add-page-note("Page %= has been deleted.", title);
  372. redirect-to(wiki-url("/") /* generate-url("wiki.home") */);
  373. else
  374. respond-to-get(*non-existing-page-page*, title: title);
  375. end;
  376. end;
  377. //// View Page
  378. // Provide backward compatibility with old wiki URLs
  379. // /wiki/view.dsp?title=t&version=v
  380. //
  381. define method show-page-back-compatible
  382. (#key)
  383. with-query-values (title, version)
  384. let title = percent-decode(title);
  385. let version = version & percent-decode(version);
  386. let default = current-request().request-absolute-url;
  387. let url = make(<url>,
  388. scheme: default.uri-scheme,
  389. host: default.uri-host,
  390. port: default.uri-port,
  391. // No, I don't understand the empty string either.
  392. path: concatenate(list("", "pages", title),
  393. iff(version,
  394. list("versions", version),
  395. #())));
  396. let location = as(<string>, url);
  397. moved-permanently-redirect(location: location,
  398. header-name: "Location",
  399. header-value: location);
  400. end;
  401. end;
  402. define class <view-page-page> (<wiki-dsp>)
  403. end;
  404. define method respond-to-get
  405. (dsp :: <view-page-page>,
  406. #key title :: <string>, version :: false-or(<string>))
  407. let title = percent-decode(title);
  408. dynamic-bind (*page* = find-or-load-page(title, revision: version))
  409. if (*page*)
  410. process-template(dsp);
  411. elseif (authenticated-user())
  412. // Give the user a change to create the page.
  413. respond-to-get(*edit-page-page*, title: title);
  414. else
  415. respond-to-get(*non-existing-page-page*, title: title);
  416. end;
  417. end;
  418. end method respond-to-get;
  419. define tag render-page in wiki
  420. (page :: <wiki-dsp>)
  421. ()
  422. output("%s", as-html(*page*, *page*.page-title))
  423. end;
  424. //// Edit Page
  425. define class <edit-page-page> (<wiki-dsp>)
  426. end;
  427. define method respond-to-get
  428. (page :: <edit-page-page>, #key title :: <string>)
  429. let title = percent-decode(title);
  430. let pc = page-context();
  431. if (authenticated-user())
  432. set-attribute(pc, "title", title);
  433. set-attribute(pc, "previewing?", #f);
  434. dynamic-bind (*page* = find-or-load-page(title))
  435. set-attribute(pc, "original-title", title);
  436. if (*page*)
  437. // TODO: change this to "source"
  438. set-attribute(pc, "content", *page*.page-source);
  439. set-attribute(pc, "owner", *page*.page-owner);
  440. set-attribute(pc, "tags", unparse-tags(*page*.page-tags));
  441. end;
  442. next-method();
  443. end;
  444. else
  445. // This shouldn't happen unless the user typed in the /edit url,
  446. // since the edit option shouldn't be available unless logged in.
  447. add-page-error("You must be logged in to edit wiki pages.");
  448. respond-to-get(*view-page-page*, title: title);
  449. end;
  450. end method respond-to-get;
  451. // Note that when the title is changed and the page is being previewed
  452. // we have to keep track of the old title. The POST is always to the
  453. // existing title, and when it's not a preview, the rename is done.
  454. //
  455. define method respond-to-post
  456. (wiki-dsp :: <edit-page-page>, #key title :: <string>)
  457. let title = percent-decode(title);
  458. let page = find-or-load-page(title);
  459. with-query-values (content, comment, tags, button)
  460. let source = content | "";
  461. let tags = iff(tags, parse-tags(tags), #[]);
  462. let previewing? = (button = "Preview");
  463. let author = authenticated-user();
  464. if (page & ~has-permission?(author, page, $modify-content))
  465. add-page-error("You do not have permission to edit this page.");
  466. end;
  467. let reserved-tags = choose(reserved-tag?, tags);
  468. if (~empty?(reserved-tags) & ~administrator?(author))
  469. add-field-error("tags", "The tag%s %s %s reserved for administrator use.",
  470. iff(reserved-tags.size = 1, "", "s"),
  471. join(tags, ", ", conjunction: " and "),
  472. iff(reserved-tags.size = 1, "is", "are"));
  473. end;
  474. if (previewing? | page-has-errors?())
  475. dynamic-bind (*page* = make(<wiki-page>,
  476. copy-from: page,
  477. name: title,
  478. source: source,
  479. comment: comment,
  480. author: author))
  481. let pc = page-context();
  482. set-attribute(pc, "previewing?", #t);
  483. set-attribute(pc, "title", title);
  484. set-attribute(pc, "preview", as-html(source, title));
  485. process-template(wiki-dsp);
  486. end;
  487. else
  488. let page = save-page(title, source, comment, tags);
  489. redirect-to(page);
  490. end;
  491. end;
  492. end method respond-to-post;
  493. //// View Diff
  494. define class <view-diff-page> (<wiki-dsp>)
  495. end;
  496. // /page/diff/Title/n -- Show the diff for revision n.
  497. //
  498. define method respond-to-get
  499. (dsp :: <view-diff-page>,
  500. #key title :: <string>,
  501. revision :: false-or(<string>))
  502. let title = percent-decode(title);
  503. let changes = find-changes(*storage*, <wiki-page>,
  504. start: revision, name: title, count: 1, diff?: #t);
  505. if (empty?(changes))
  506. add-page-error("No diff for page %= found.", title);
  507. redirect-to(find-page(title) | *non-existing-page-page*);
  508. else
  509. let change :: <wiki-change> = changes[0];
  510. let pc = page-context();
  511. set-attribute(pc, "name", change.change-object-name);
  512. set-attribute(pc, "diff", change.change-diff);
  513. set-attribute(pc, "author", change.change-author);
  514. set-attribute(pc, "comment", change.change-comment);
  515. set-attribute(pc, "date", as-iso8601-string(change.change-date));
  516. process-template(dsp);
  517. end;
  518. end method respond-to-get;
  519. define method print-diff-entry
  520. (entry :: <insert-entry>, seq1 :: <sequence>, seq2 :: <sequence>)
  521. let lineno1 = entry.source-index + 1;
  522. let lineno2 = entry.element-count + entry.source-index;
  523. if (lineno1 = lineno2)
  524. output("Added line %d:<br/>", lineno1);
  525. else
  526. output("Added lines %d - %d:<br/>", lineno1, lineno2);
  527. end;
  528. for (line in copy-sequence(seq2, start: lineno1 - 1, end: lineno2),
  529. lineno from lineno1)
  530. output("%d: %s<br/>", lineno, line);
  531. end;
  532. end method print-diff-entry;
  533. define method print-diff-entry
  534. (entry :: <delete-entry>, seq1 :: <sequence>, seq2 :: <sequence>)
  535. let lineno1 = entry.dest-index + 1;
  536. let lineno2 = entry.element-count + entry.dest-index;
  537. if (lineno1 = lineno2)
  538. output("Removed line %d:<br/>", lineno1);
  539. else
  540. output("Removed lines %d - %d:<br/>", lineno1, lineno2);
  541. end;
  542. for (line in copy-sequence(seq1, start: lineno1 - 1, end: lineno2),
  543. lineno from lineno1)
  544. output("%d: %s<br/>", lineno, line);
  545. end;
  546. end method print-diff-entry;
  547. define tag show-diff-entry in wiki
  548. (page :: <view-diff-page>)
  549. (name :: <string>)
  550. let pc = page-context();
  551. let entry = get-attribute(pc, name);
  552. let seq1 = get-attribute(pc, "seq1");
  553. let seq2 = get-attribute(pc, "seq2");
  554. print-diff-entry(entry, seq1, seq2);
  555. end tag show-diff-entry;
  556. //// Tags
  557. define tag show-page-permanent-link in wiki
  558. (page :: <wiki-dsp>)
  559. ()
  560. if (*page*)
  561. output("%s", permanent-link(*page*))
  562. end;
  563. end;
  564. // Show the title of the main page corresponding to a discussion page.
  565. define tag show-main-page-title in wiki
  566. (page :: <wiki-dsp>) ()
  567. if (*page*)
  568. let main-title = regex-replace(*page*.page-title, compile-regex("^Discussion: "), "");
  569. output("%s", escape-xml(main-title));
  570. end;
  571. end tag show-main-page-title;
  572. // Show the title of the discussion page corresponding to a main page.
  573. define tag show-discussion-page-title in wiki
  574. (page :: <wiki-dsp>) ()
  575. if (*page*)
  576. let discuss-title = concatenate("Discussion: ", *page*.page-title);
  577. output("%s", escape-xml(discuss-title));
  578. end;
  579. end tag show-discussion-page-title;
  580. define tag show-page-title in wiki
  581. (page :: <wiki-dsp>)
  582. ()
  583. if (*page*)
  584. output("%s", escape-xml(*page*.page-title));
  585. end;
  586. end;
  587. define tag show-page-owner in wiki
  588. (page :: <wiki-dsp>)
  589. ()
  590. if (*page*)
  591. output("%s", escape-xml(*page*.page-owner.user-name))
  592. end;
  593. end;
  594. define tag show-version in wiki
  595. (page :: <wiki-dsp>)
  596. ()
  597. output("%s", *page*.page-revision);
  598. end;
  599. define tag include-page in wiki
  600. (dsp :: <wiki-dsp>)
  601. (title :: <string>)
  602. let page = find-or-load-page(title);
  603. if (page)
  604. output("%s", as-html(page, title));
  605. else
  606. output("PAGE '%S' NOT FOUND", title);
  607. end;
  608. end;
  609. // body tags
  610. define body tag list-page-tags in wiki
  611. (page :: <wiki-dsp>, do-body :: <function>)
  612. ()
  613. if (*page*)
  614. // Is it correct to be using the tags from the newest page version?
  615. // At least this DSP tag should be called show-latest-page-tags ...
  616. for (tag in *page*.page-tags)
  617. dynamic-bind(*tag* = tag)
  618. do-body();
  619. end;
  620. end for;
  621. elseif (get-query-value("tags"))
  622. output("%s", escape-xml(get-query-value("tags")));
  623. end if;
  624. end;
  625. // This is only used is main.dsp now, and only for news.
  626. // May want to make a special one for news instead.
  627. define body tag list-pages in wiki
  628. (page :: <wiki-dsp>, do-body :: <function>)
  629. (tags :: false-or(<string>),
  630. order-by :: false-or(<string>),
  631. use-query-tags :: <boolean>)
  632. let tagged = get-query-value("tagged");
  633. let tags = iff(use-query-tags & instance?(tagged, <string>),
  634. parse-tags(tagged),
  635. iff(tags, parse-tags(tags), #[]));
  636. for (page in find-pages(tags: tags, order-by: creation-date-newer?))
  637. dynamic-bind(*page* = page)
  638. do-body();
  639. end;
  640. end for;
  641. end;
  642. // named methods
  643. define named-method is-discussion-page? in wiki
  644. (page :: <wiki-dsp>)
  645. *page* & discussion-page?(*page*);
  646. end;
  647. define named-method latest-page-version? in wiki
  648. (page :: <wiki-dsp>)
  649. // TODO: Currently we assume the latest revision of the page is always
  650. // stored in *pages*.
  651. *page* & *page* == element(*pages*, *page*.page-title, default: $unfound)
  652. end;
  653. define named-method active-page-tags in wiki
  654. (page :: <wiki-dsp>) => (tags :: <sequence>)
  655. iff(*page*,
  656. sort(*page*.page-tags, test: \=),
  657. #[])
  658. end;