/dylan/wiki.dylan

http://github.com/cgay/wiki · Unknown · 600 lines · 479 code · 121 blank · 0 comment · 0 complexity · a44dd700579d81c64d41c9c2d56577a5 MD5 · raw file

  1. Module: %wiki
  2. Synopsis: Utilities, globals, protocols, base classes, ...
  3. Basically anything that needs to be defined first.
  4. define taglib wiki () end;
  5. // Represents a DSP maintained in our source code tree. Not to be confused
  6. // with <wiki-page>, which is a user-editable wiki page.
  7. //
  8. define class <wiki-dsp> (<dylan-server-page>)
  9. end;
  10. // These are both set to something else when the config file is loaded.
  11. define variable *static-directory* :: <directory-locator> = working-directory();
  12. define variable *template-directory* :: <directory-locator> = working-directory();
  13. define method make
  14. (class :: subclass(<wiki-dsp>), #rest args, #key source :: <pathname>)
  15. => (page :: <wiki-dsp>)
  16. apply(next-method, class,
  17. source: merge-locators(as(<file-locator>, source),
  18. *template-directory*),
  19. args)
  20. end;
  21. // The following will all be set after *template-directory* is set.
  22. define variable *edit-page-page* = #f;
  23. define variable *view-page-page* = #f;
  24. define variable *remove-page-page* = #f;
  25. define variable *page-history-page* = #f;
  26. define variable *connections-page* = #f;
  27. define variable *view-diff-page* = #f;
  28. define variable *search-page* = #f;
  29. define variable *non-existing-page-page* = #f;
  30. define variable *view-user-page* = #f;
  31. define variable *list-users-page* = #f;
  32. define variable *edit-user-page* = #f;
  33. define variable *deactivate-user-page* = #f;
  34. define variable *non-existing-user-page* = #f;
  35. define variable *list-groups-page* = #f;
  36. define variable *non-existing-group-page* = #f;
  37. define variable *view-group-page* = #f;
  38. define variable *edit-group-page* = #f;
  39. define variable *remove-group-page* = #f;
  40. define variable *edit-group-members-page* = #f;
  41. define variable *registration-page* = #f;
  42. define variable *edit-access-page* = #f;
  43. //// Wiki object caches
  44. // TODO: why's this not in table-extensions?
  45. define class <case-insensitive-string-table> (<table>)
  46. end;
  47. define sealed method table-protocol
  48. (table :: <case-insensitive-string-table>)
  49. => (test :: <function>, hash :: <function>)
  50. values(case-insensitive-equal, case-insensitive-string-hash)
  51. end;
  52. /// All objects stored in the wiki (pages, users, groups) must subclass this.
  53. ///
  54. define class <wiki-object> (<object>)
  55. constant slot creation-date :: <date> = current-date(),
  56. init-keyword: creation-date:;
  57. // TODO:
  58. //constant slot modification-date :: <date> = <same as creation-date>;
  59. slot object-name :: <string>,
  60. required-init-keyword: name:;
  61. end;
  62. // If you need to hold more than one of these locks, acquire them in
  63. // this order: $group-lock, $user-lock, $page-lock.
  64. /// Maps user name to newest revision of <wiki-user>.
  65. define variable *users* :: <case-insensitive-string-table>
  66. = make(<case-insensitive-string-table>);
  67. /// Hold this when modifying *users*.
  68. define constant $user-lock :: <lock> = make(<lock>);
  69. /// Maps group name to newest revision of <wiki-group>.
  70. define variable *groups* :: <case-insensitive-string-table>
  71. = make(<case-insensitive-string-table>);
  72. /// Hold this when modifying *groups*.
  73. define constant $group-lock :: <lock> = make(<lock>);
  74. /// Maps page titles to newest revision of <wiki-page>.
  75. define variable *pages* :: <case-insensitive-string-table>
  76. = make(<case-insensitive-string-table>);
  77. /// Hold this when modifying *pages*.
  78. define constant $page-lock :: <lock> = make(<lock>);
  79. //// General-use DSP tags
  80. // Prefix for all wiki URLs. Set to "" for no prefix.
  81. // Note that some templates still hard-code this value,
  82. // until URL generation works.
  83. define variable *wiki-url-prefix* :: <string> = "/wiki";
  84. // This shouldn't be needed once generate-url is working.
  85. define tag base in wiki
  86. (page :: <wiki-dsp>) ()
  87. output("%s", *wiki-url-prefix*);
  88. end;
  89. define tag base-url in wiki
  90. (page :: <wiki-dsp>)
  91. ()
  92. let url = current-request().request-absolute-url; // this may make a new url
  93. output("%s", build-uri(make(<url>,
  94. scheme: url.uri-scheme,
  95. host: url.uri-host,
  96. port: url.uri-port)));
  97. end;
  98. // Mostly for use in setting the "redirect" parameter in templates.
  99. define tag current in wiki
  100. (page :: <wiki-dsp>) ()
  101. output("%s", build-uri(request-url(current-request())));
  102. end;
  103. define function wiki-url
  104. (format-string, #rest format-args)
  105. => (url :: <url>)
  106. parse-url(concatenate(*wiki-url-prefix*,
  107. apply(format-to-string, format-string, format-args)))
  108. end;
  109. define constant $past-tense-table
  110. = make-table(<string-table>,
  111. "activate" => "activated",
  112. "create" => "created",
  113. "edit" => "edited",
  114. "remove" => "removed",
  115. "rename" => "renamed",
  116. "add-members" => "group member added",
  117. "remove-members" => "group member removed",
  118. // I don't think these two are currently used. --cgay Apr 2011
  119. "add-group-owner" => "added owner",
  120. "remove-group-owner" => "removed owner");
  121. define generic permanent-link (obj :: <object>) => (url :: <url>);
  122. //// Storage protocol
  123. /// Any back-end storage mechanism must be a subclass of this and support
  124. /// the generics that specialize on it.
  125. define class <storage> (<object>)
  126. end;
  127. /// This is initialized when the config file is loaded.
  128. define variable *storage* :: false-or(<storage>) = #f;
  129. /// Initialize storage upon startup
  130. define generic initialize-storage-for-reads
  131. (storage :: <storage>) => ();
  132. define generic initialize-storage-for-writes
  133. (storage :: <storage>, admin-user :: <wiki-user>) => ();
  134. define generic load
  135. (storage :: <storage>, class :: subclass(<wiki-object>), name :: <string>,
  136. #key)
  137. => (obj :: <wiki-object>);
  138. define generic load-all
  139. (storage :: <storage>, class :: subclass(<wiki-object>))
  140. => (objects :: <sequence>);
  141. define generic find-or-load-pages-with-tags
  142. (storage :: <storage>, tags :: <sequence>) => (pages :: <sequence>);
  143. // If 'type' ~= <wiki-object>, the 'name' keyword argument is supported.
  144. // If 'type' == <wiki-page>, the 'diff?' keyword argument is supported.
  145. define generic find-changes
  146. (storage :: <storage>, type :: subclass(<wiki-object>), #key start, count, #all-keys)
  147. => (changes :: <sequence>);
  148. define generic store
  149. (storage :: <storage>, obj :: <wiki-object>, author :: <wiki-user>,
  150. comment :: <string>, meta-data :: <string-table>)
  151. => (revision :: <string>);
  152. define generic delete
  153. (storage :: <storage>, obj :: <wiki-object>, author :: <wiki-user>,
  154. comment :: <string>, meta-data :: <string-table>)
  155. => ();
  156. define generic rename
  157. (storage :: <storage>, obj :: <wiki-object>, new-name :: <string>,
  158. author :: <wiki-user>, comment :: <string>, meta-data :: <string-table>)
  159. => (revision :: <string>);
  160. /// This is what the above methods should signal if they can't fullfill
  161. /// their contract.
  162. define class <storage-error> (<format-string-condition>, <serious-condition>)
  163. end;
  164. //// Changes
  165. define class <wiki-change> (<object>)
  166. constant slot change-revision :: <string>, required-init-keyword: revision:;
  167. constant slot change-author :: <string>, required-init-keyword: author:;
  168. constant slot change-date :: <date>, required-init-keyword: date:;
  169. constant slot change-comment :: <string>, required-init-keyword: comment:;
  170. constant slot change-diff :: <string> = "", init-keyword: diff:;
  171. // Keys that always exist: "name", "type", "action".
  172. // TODO: Be resilient to by-hand edits, in which case these items may not have
  173. // been stored in the Notes for the commit. This info could be recovered
  174. // by grovelling over the output of "git whatchanged".
  175. constant slot change-meta-data :: <string-table>, required-init-keyword: meta-data:;
  176. end;
  177. define function change-object-name
  178. (change :: <wiki-change>) => (name :: <string>)
  179. change.change-meta-data["name"]
  180. end;
  181. define function change-type-name
  182. (change :: <wiki-change>) => (name :: <string>)
  183. change.change-meta-data["type"]
  184. end;
  185. /* unused
  186. define function change-object-type
  187. (change :: <wiki-change>) => (type :: subclass(<wiki-object>))
  188. select (change.change-type-name by \=)
  189. "page" => <wiki-page>;
  190. "user" => <wiki-user>;
  191. "group" => <wiki-group>;
  192. end
  193. end;
  194. */
  195. define function change-action
  196. (change :: <wiki-change>) => (action :: <string>)
  197. element(change.change-meta-data, "action", default: "change")
  198. end;
  199. define function standard-meta-data
  200. (object :: <wiki-object>, action :: <string>)
  201. => (meta-data :: <string-table>)
  202. let meta-data = make(<string-table>);
  203. meta-data["action"] := action;
  204. meta-data["name"] := select (object.object-class)
  205. <wiki-page> => object.page-title;
  206. <wiki-user> => object.user-name;
  207. <wiki-group> => object.group-name;
  208. end;
  209. meta-data["type"] := select (object.object-class)
  210. <wiki-page> => "page";
  211. <wiki-user> => "user";
  212. <wiki-group> => "group";
  213. end;
  214. meta-data
  215. end;
  216. define method permanent-link
  217. (change :: <wiki-change>) => (url :: <url>)
  218. // Yet another place that needs to be fixed by using generate-url.
  219. let location = wiki-url("/%s/view/%s/%s",
  220. change.change-type-name,
  221. change.change-object-name,
  222. change.change-revision);
  223. transform-uris(request-url(current-request()), location, as: <url>)
  224. end;
  225. //// Recent Changes page
  226. define class <recent-changes-page> (<wiki-dsp>)
  227. end;
  228. define method respond-to-get
  229. (page :: <recent-changes-page>, #key)
  230. let changes = sort(find-recent-changes(),
  231. test: method (change1, change2)
  232. change1.change-date > change2.change-date
  233. end);
  234. let page-number = get-query-value("page", as: <integer>) | 1;
  235. let paginator = make(<paginator>,
  236. sequence: changes,
  237. current-page-number: page-number);
  238. set-attribute(page-context(), "recent-changes", paginator);
  239. next-method();
  240. end;
  241. /// Synopsis: Find changes for wiki objects of type 'for-type'.
  242. ///
  243. /// Arguments:
  244. /// for-type - Should be <wiki-page>, <wiki-user>, or <wiki-group>
  245. /// or <wiki-object> (the default). <wiki-object> will
  246. /// find changes for any object.
  247. /// start - A revision number at which to start searching (backward)
  248. /// for changes. With the git back-end this is a hash.
  249. /// The default (#f) means to start with the most recent change.
  250. /// name - Only find changes for objects matching this name exactly.
  251. /// For pages this matches the title. The default (#f) matches
  252. /// anything.
  253. /// Values:
  254. /// changes - a sequence of <wiki-change> objects representing object
  255. /// creations, edits, deletions, or renames.
  256. ///
  257. define method find-recent-changes
  258. (#key for-type :: subclass(<wiki-object>) = <wiki-object>,
  259. start :: false-or(<string>),
  260. name :: false-or(<string>))
  261. => (changes :: <sequence>)
  262. find-changes(*storage*, for-type, start: start, name: name, count: 300)
  263. end;
  264. define body tag list-recent-changes in wiki
  265. (page :: <wiki-dsp>, do-body :: <function>)
  266. ()
  267. let pc = page-context();
  268. let previous-change = #f;
  269. let paginator :: <paginator> = get-attribute(pc, "recent-changes");
  270. for (change :: <wiki-change> in paginator)
  271. set-attribute(pc, "day", standard-date(change.change-date));
  272. set-attribute(pc, "previous-day",
  273. previous-change & standard-date(previous-change.change-date));
  274. set-attribute(pc, "time", standard-time(change.change-date));
  275. set-attribute(pc, "revision-url", as(<string>, permanent-link(change)));
  276. set-attribute(pc, "newest-url", as(<string>, permanent-link(change)));
  277. set-attribute(pc, "diff-url",
  278. as(<string>, wiki-url("/page/diff/%s/%s",
  279. change.change-object-name,
  280. change.change-revision)));
  281. set-attribute(pc, "object-type", change.change-type-name);
  282. set-attribute(pc, "title", change.change-object-name);
  283. set-attribute(pc, "action", as(<string>, change.change-action));
  284. set-attribute(pc, "comment", change.change-comment);
  285. set-attribute(pc, "version", change.change-revision);
  286. set-attribute(pc, "verb",
  287. element($past-tense-table, change.change-action, default: #f)
  288. | change.change-action);
  289. set-attribute(pc, "author", change.change-author);
  290. do-body();
  291. previous-change := change;
  292. end;
  293. end tag list-recent-changes;
  294. // TODO: replace all these date-related tags with one tag like
  295. // <wiki:date object="page|group|user" format="..." />
  296. // Standard date format. The plan is to make this customizable per user
  297. // and to use the user's timezone. For now just ISO 8601...
  298. //
  299. define method standard-date-and-time
  300. (date :: <date>) => (date-and-time :: <string>)
  301. as-iso8601-string(date)
  302. end;
  303. define method standard-date
  304. (date :: <date>) => (date :: <string>)
  305. format-date("%Y.%m.%d", date)
  306. end;
  307. define method standard-time
  308. (date :: <date>) => (time :: <string>)
  309. format-date("%H:%M", date)
  310. end;
  311. define tag show-version-published in wiki
  312. (page :: <wiki-dsp>)
  313. (formatted :: <string>)
  314. output("%s", format-date(formatted, *page*.creation-date));
  315. end;
  316. define tag show-page-published in wiki
  317. (page :: <wiki-dsp>)
  318. (formatted :: <string>)
  319. if (*page*)
  320. output("%s", format-date(formatted, *page*.creation-date));
  321. end if;
  322. end;
  323. define tag page-creation-date in wiki
  324. (page :: <wiki-dsp>)
  325. ()
  326. output("%s", as-iso8601-string(*page*.creation-date));
  327. end;
  328. // Rename to show-comment
  329. define tag show-version-comment in wiki
  330. (page :: <wiki-dsp>)
  331. ()
  332. output("%s", *page*.page-comment);
  333. end;
  334. define variable *not-logged-in-page* = #f;
  335. //// References (to other wiki objects)
  336. /// A wiki reference is a pointer to another wiki object.
  337. define class <wiki-reference> (<object>)
  338. /// This holds the name of the page, user, or group.
  339. constant slot reference-name :: <string>,
  340. required-init-keyword: name:;
  341. // The text to display for this reference. Often the same as the name.
  342. constant slot reference-text :: <string>,
  343. required-init-keyword: text:;
  344. end class <wiki-reference>;
  345. /// Map from page name to a sequence of <wiki-object>s that refer to it.
  346. /// The keys here may be for non-existant objects. e.g. pages that
  347. /// haven't yet been created, or that have been deleted.
  348. define constant $page-reference-map :: <case-insensitive-string-table>
  349. = make(<case-insensitive-string-table>);
  350. /// Map from user name to a sequence of <wiki-object>s that refer to it.
  351. /// The keys here may be for non-existant objects. e.g. users that
  352. /// haven't yet been created, or that have been deleted.
  353. define constant $user-reference-map :: <case-insensitive-string-table>
  354. = make(<case-insensitive-string-table>);
  355. /// Map from group name to a sequence of <wiki-object>s that refer to it.
  356. /// The keys here may be for non-existant objects. e.g. groups that
  357. /// haven't yet been created, or that have been deleted.
  358. define constant $group-reference-map :: <case-insensitive-string-table>
  359. = make(<case-insensitive-string-table>);
  360. define generic as-rst
  361. (x :: <object>) => (rst :: <string>);
  362. define generic resolve-reference
  363. (ref :: <wiki-reference>) => (object :: false-or(<wiki-object>));
  364. define class <page-reference> (<wiki-reference>) end;
  365. define class <user-reference> (<wiki-reference>) end;
  366. define class <group-reference> (<wiki-reference>) end;
  367. define method as-rst
  368. (rst-markup :: <string>) => (rst :: <string>)
  369. rst-markup
  370. end;
  371. define method as-rst
  372. (ref :: <page-reference>) => (rst :: <string>)
  373. format-to-string("`%s %s<%s/page/view/%s>`_",
  374. ref.reference-text,
  375. iff(ref.resolve-reference, "", "(?) "),
  376. *wiki-url-prefix*,
  377. percent-encode($uri-pchar, ref.reference-name))
  378. end;
  379. define method as-rst
  380. (ref :: <user-reference>) => (rst :: <string>)
  381. format-to-string("`%s %s<%s/user/view/%s>`_",
  382. ref.reference-text,
  383. iff(ref.resolve-reference, "", "(?) "),
  384. *wiki-url-prefix*,
  385. percent-encode($uri-pchar, ref.reference-name))
  386. end;
  387. define method as-rst
  388. (ref :: <group-reference>) => (rst :: <string>)
  389. format-to-string("`%s %s<%s/group/view/%s>`_",
  390. ref.reference-text,
  391. iff(ref.resolve-reference, "", "(?) "),
  392. *wiki-url-prefix*,
  393. percent-encode($uri-pchar, ref.reference-name))
  394. end;
  395. define method resolve-reference
  396. (ref :: <page-reference>) => (page :: false-or(<wiki-page>))
  397. find-page(ref.reference-name)
  398. end;
  399. define method resolve-reference
  400. (ref :: <user-reference>) => (user :: false-or(<wiki-user>))
  401. find-user(ref.reference-name)
  402. end;
  403. define method resolve-reference
  404. (ref :: <group-reference>) => (group :: false-or(<wiki-group>))
  405. find-group(ref.reference-name)
  406. end;
  407. define constant $reference-map-lock :: <lock> = make(<lock>);
  408. /// Update reference tables to reflect changes when an object is created,
  409. /// modified, or deleted.
  410. /// Arguments:
  411. /// source - The source of the references.
  412. /// old-refs - Sequence of <wiki-reference> from before the object was changed.
  413. /// new-refs - Sequence of <wiki-reference> from after the object was changed.
  414. define function update-reference-tables!
  415. (source :: <wiki-object>, old-refs :: <sequence>, new-refs :: <sequence>)
  416. => ()
  417. local method same? (object1 :: <wiki-object>, object2 :: <wiki-object>)
  418. case-insensitive-equal(object1.object-name, object2.object-name)
  419. end;
  420. let table = select (source by instance?)
  421. <wiki-page> => $page-reference-map;
  422. <wiki-user> => $user-reference-map;
  423. <wiki-group> => $group-reference-map;
  424. end;
  425. with-lock ($reference-map-lock)
  426. for (ref in old-refs)
  427. let target-name = ref.reference-name;
  428. let refs = element(table, target-name, default: #f);
  429. if (refs)
  430. table[target-name] := remove!(refs, source, test: same?);
  431. end;
  432. end;
  433. for (ref in new-refs)
  434. let target-name = ref.reference-name;
  435. let refs = element(table, target-name, default: #());
  436. table[target-name] := add!(refs, source);
  437. end;
  438. end;
  439. end function update-reference-tables!;
  440. /// Find references to the given wiki object. Currently this cannot handle
  441. /// references to specific revisions of objects; all references are assumed
  442. /// to be to the latest revision.
  443. /// Arguments:
  444. /// target - The object being referred to.
  445. /// Values:
  446. /// wiki-objects - A sequence of <wiki-objects>s.
  447. define generic inbound-references
  448. (target :: <wiki-object>) => (wiki-objects :: <sequence>);
  449. define method inbound-references
  450. (target :: <wiki-page>) => (wiki-objects :: <sequence>)
  451. element($page-reference-map, target.object-name, default: #())
  452. end;
  453. define method inbound-references
  454. (target :: <wiki-user>) => (wiki-objects :: <sequence>)
  455. element($user-reference-map, target.object-name, default: #())
  456. end;
  457. define method inbound-references
  458. (target :: <wiki-group>) => (wiki-objects :: <sequence>)
  459. element($group-reference-map, target.object-name, default: #())
  460. end;
  461. /// Return a sequence of <wiki-object>s that are referred to by 'source'.
  462. define generic outbound-references
  463. (source :: <wiki-object>) => (refs :: <sequence>);
  464. define method outbound-references
  465. (source :: <wiki-object>) => (refs :: <sequence>)
  466. #() // nothing yet
  467. end;
  468. define method outbound-references
  469. (page :: <wiki-page>) => (refs :: <sequence>)
  470. choose(rcurry(instance?, <wiki-reference>),
  471. page.page-parsed-source)
  472. end method outbound-references;