scriptorium 0.0.3 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. checksums.yaml +4 -4
  2. data/README.lt3 +324 -0
  3. data/README.md +3155 -1
  4. data/assets/.DS_Store +0 -0
  5. data/assets/README.md +44 -0
  6. data/assets/icons/social/reddit.png +0 -0
  7. data/assets/icons/social/x-logo.png +0 -0
  8. data/assets/icons/ui/.DS_Store +0 -0
  9. data/assets/icons/ui/back.png +0 -0
  10. data/assets/icons/ui/copy.png +0 -0
  11. data/assets/icons/ui/down.png +0 -0
  12. data/assets/icons/ui/end.png +0 -0
  13. data/assets/icons/ui/exit.png +0 -0
  14. data/assets/icons/ui/foo +10 -0
  15. data/assets/icons/ui/home.png +0 -0
  16. data/assets/icons/ui/left.png +0 -0
  17. data/assets/icons/ui/next.png +0 -0
  18. data/assets/icons/ui/right.png +0 -0
  19. data/assets/icons/ui/start.png +0 -0
  20. data/assets/icons/ui/up.png +0 -0
  21. data/assets/imagenotfound.jpg +0 -0
  22. data/assets/samples/placeholder.svg +9 -0
  23. data/assets/themes/standard/favicon.svg +6 -0
  24. data/bin/sblog +84 -5
  25. data/bin/scriptorium +1 -0
  26. data/doc/README.txt +6 -0
  27. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +94 -0
  28. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +2 -0
  29. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +45 -0
  30. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
  31. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
  32. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
  33. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +35 -0
  34. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +27 -0
  35. data/doc/anti-amnesia/20250807-213025.md +116 -0
  36. data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
  37. data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
  38. data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -0
  39. data/doc/banner_svg_config.md +114 -0
  40. data/doc/contrib.lt3 +8 -0
  41. data/doc/dependencies.md +281 -0
  42. data/doc/hacker.lt3 +5 -0
  43. data/doc/imported/0001-elixir-conf-2014/metadata.txt +7 -0
  44. data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
  45. data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
  46. data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
  47. data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
  48. data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
  49. data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
  50. data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
  51. data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
  52. data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
  53. data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
  54. data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
  55. data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
  56. data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
  57. data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
  58. data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
  59. data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
  60. data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
  61. data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
  62. data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
  63. data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
  64. data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
  65. data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
  66. data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
  67. data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
  68. data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
  69. data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
  70. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
  71. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
  72. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
  73. data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
  74. data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
  75. data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
  76. data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
  77. data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
  78. data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
  79. data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
  80. data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
  81. data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
  82. data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
  83. data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
  84. data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
  85. data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
  86. data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
  87. data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
  88. data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
  89. data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
  90. data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
  91. data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
  92. data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
  93. data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
  94. data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
  95. data/doc/imported/0018-ruby-puns/post.html +46 -0
  96. data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
  97. data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
  98. data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
  99. data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
  100. data/doc/imported/0020-fffff/metadata.txt +7 -0
  101. data/doc/imported/0020-fffff/post.html +24 -0
  102. data/doc/imported/0020-fffff/source.lt3 +12 -0
  103. data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
  104. data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
  105. data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
  106. data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
  107. data/doc/imported/0023-doctor-sleep/post.html +63 -0
  108. data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
  109. data/doc/imported/0024-just-a-test/metadata.txt +7 -0
  110. data/doc/imported/0024-just-a-test/post.html +24 -0
  111. data/doc/imported/0024-just-a-test/source.lt3 +12 -0
  112. data/doc/imported/import_summary.txt +98 -0
  113. data/doc/livetext-informal-spec.txt +65 -0
  114. data/doc/myuserdoc/ch-0.lt3 +31 -0
  115. data/doc/myuserdoc/ch-1.lt3 +37 -0
  116. data/doc/myuserdoc/ch-10.lt3 +22 -0
  117. data/doc/myuserdoc/ch-2.lt3 +37 -0
  118. data/doc/myuserdoc/ch-3.lt3 +19 -0
  119. data/doc/myuserdoc/ch-4.lt3 +43 -0
  120. data/doc/myuserdoc/ch-5.lt3 +22 -0
  121. data/doc/myuserdoc/ch-6.lt3 +19 -0
  122. data/doc/myuserdoc/ch-7.lt3 +16 -0
  123. data/doc/myuserdoc/ch-8.lt3 +13 -0
  124. data/doc/myuserdoc/ch-9.lt3 +19 -0
  125. data/doc/myuserdoc/tweak.rb +18 -0
  126. data/doc/myuserdoc/userdoc-toc.txt +88 -0
  127. data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
  128. data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
  129. data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
  130. data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
  131. data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
  132. data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
  133. data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
  134. data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
  135. data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
  136. data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
  137. data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
  138. data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
  139. data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
  140. data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
  141. data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
  142. data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
  143. data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
  144. data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
  145. data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
  146. data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
  147. data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
  148. data/doc/old-posts/0024-just-a-test.lt3 +12 -0
  149. data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
  150. data/doc/old-repo +1 -0
  151. data/doc/reddit_credentials_template.json +8 -0
  152. data/doc/reddit_integration.md +207 -0
  153. data/doc/user.lt3 +35 -0
  154. data/doc/user_guide_section_1.md +137 -0
  155. data/doc/user_guide_section_10.md +515 -0
  156. data/doc/user_guide_section_11.md +708 -0
  157. data/doc/user_guide_section_2.md +233 -0
  158. data/doc/user_guide_section_3.md +5 -0
  159. data/doc/user_guide_section_4.md +221 -0
  160. data/doc/user_guide_section_5.md +243 -0
  161. data/doc/user_guide_section_6.md +147 -0
  162. data/doc/user_guide_section_7.md +311 -0
  163. data/doc/user_guide_section_8.md +224 -0
  164. data/doc/user_guide_section_9.md +375 -0
  165. data/lib/rouge/lexers/livetext.rb +74 -0
  166. data/lib/scriptorium/api.rb +2373 -0
  167. data/lib/scriptorium/banner_svg.rb +729 -0
  168. data/lib/scriptorium/contract.rb +34 -0
  169. data/lib/scriptorium/exceptions.rb +201 -1
  170. data/lib/scriptorium/helpers.rb +675 -0
  171. data/lib/scriptorium/post.rb +259 -0
  172. data/lib/scriptorium/reddit.rb +83 -0
  173. data/lib/scriptorium/repo.rb +938 -0
  174. data/lib/scriptorium/standard_files.rb +149 -0
  175. data/lib/scriptorium/support/bootstrap/css.txt +5 -0
  176. data/lib/scriptorium/support/bootstrap/js.txt +4 -0
  177. data/lib/scriptorium/support/common_js/clipboard.js +35 -0
  178. data/lib/scriptorium/support/common_js/content-loader.js +187 -0
  179. data/lib/scriptorium/support/common_js/navigation.js +52 -0
  180. data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
  181. data/lib/scriptorium/support/config/reddit.txt +10 -0
  182. data/lib/scriptorium/support/config/reddit_template.txt +17 -0
  183. data/lib/scriptorium/support/config/social.txt +8 -0
  184. data/lib/scriptorium/support/highlight/css.txt +2 -0
  185. data/lib/scriptorium/support/highlight/custom.css +119 -0
  186. data/lib/scriptorium/support/highlight/js.txt +1 -0
  187. data/lib/scriptorium/support/post_index/config.txt +15 -0
  188. data/lib/scriptorium/support/post_index/style.css +55 -0
  189. data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
  190. data/lib/scriptorium/support/templates/initial_post.lt3 +12 -0
  191. data/lib/scriptorium/support/templates/layout.txt +5 -0
  192. data/lib/scriptorium/support/templates/post.lt3 +104 -0
  193. data/lib/scriptorium/support/theme/footer.lt3 +2 -0
  194. data/lib/scriptorium/support/theme/header.lt3 +4 -0
  195. data/lib/scriptorium/support/theme/left.lt3 +3 -0
  196. data/lib/scriptorium/support/theme/main.lt3 +5 -0
  197. data/lib/scriptorium/support/theme/right.lt3 +3 -0
  198. data/lib/scriptorium/theme.rb +192 -0
  199. data/lib/scriptorium/version.rb +1 -1
  200. data/lib/scriptorium/view.rb +1021 -0
  201. data/lib/scriptorium/widgets/featured_posts.rb +149 -0
  202. data/lib/scriptorium/widgets/links.rb +112 -0
  203. data/lib/scriptorium/widgets/pages.rb +133 -0
  204. data/lib/scriptorium/widgets/widget.rb +133 -0
  205. data/lib/scriptorium.rb +38 -34
  206. data/lib/skeleton.rb +10 -1
  207. data/scriptorium.gemspec +17 -5
  208. data/test/README.md +69 -0
  209. data/test/WEB_INTEGRATION_README.md +196 -0
  210. data/test/all +83 -0
  211. data/test/api_demo.rb +99 -0
  212. data/test/assets/imagenotfound.jpg +0 -0
  213. data/test/assets/images/.DS_Store +0 -0
  214. data/test/assets/images/README.md +27 -0
  215. data/test/assets/images/odd_aspect.png +0 -0
  216. data/test/assets/images/perfect.png +0 -0
  217. data/test/assets/images/small.png +0 -0
  218. data/test/assets/images/tall.png +0 -0
  219. data/test/assets/images/very_tall.png +0 -0
  220. data/test/assets/images/very_wide.png +0 -0
  221. data/test/assets/images/wide.png +0 -0
  222. data/test/assets/testbanner.jpg +0 -0
  223. data/test/banner_svg/simple_helpers.rb +13 -0
  224. data/test/banner_svg/unit.rb +1000 -0
  225. data/test/config/deployment.txt +5 -0
  226. data/test/ed_test.rb +204 -0
  227. data/test/integration/cursor_banner_combinations.rb +193 -0
  228. data/test/integration/cursor_banner_features.rb +374 -0
  229. data/test/integration/integration_test.rb +326 -0
  230. data/test/integration/preview_flow_test.rb +94 -0
  231. data/test/livetext_plugin_test.rb +500 -0
  232. data/test/manual/asset_mgmt.rb +67 -0
  233. data/test/manual/banner-tests/index.html +45 -0
  234. data/test/manual/banner-tests/svg.txt +3 -0
  235. data/test/manual/banner-tests/test01.html +122 -0
  236. data/test/manual/banner-tests/test02.html +122 -0
  237. data/test/manual/banner-tests/test03.html +122 -0
  238. data/test/manual/banner-tests/test04.html +129 -0
  239. data/test/manual/banner-tests/test05.html +129 -0
  240. data/test/manual/banner-tests/test06.html +129 -0
  241. data/test/manual/banner-tests/test07.html +129 -0
  242. data/test/manual/banner-tests/test08.html +123 -0
  243. data/test/manual/banner-tests/test09.html +123 -0
  244. data/test/manual/banner-tests/test10.html +123 -0
  245. data/test/manual/banner-tests/test11.html +123 -0
  246. data/test/manual/banner-tests/test12.html +123 -0
  247. data/test/manual/banner-tests/test13.html +123 -0
  248. data/test/manual/banner-tests/test14.html +123 -0
  249. data/test/manual/banner-tests/test15.html +122 -0
  250. data/test/manual/banner-tests/test16.html +122 -0
  251. data/test/manual/banner-tests/test17.html +122 -0
  252. data/test/manual/banner-tests/test18.html +132 -0
  253. data/test/manual/banner-tests/test19.html +132 -0
  254. data/test/manual/banner-tests/test20.html +132 -0
  255. data/test/manual/banner-tests/test21.html +132 -0
  256. data/test/manual/banner-tests/test22.html +132 -0
  257. data/test/manual/banner-tests/test23.html +132 -0
  258. data/test/manual/banner-tests/test24.html +132 -0
  259. data/test/manual/banner-tests/test25.html +131 -0
  260. data/test/manual/banner_environment.rb +205 -0
  261. data/test/manual/codemirror_demo.html +773 -0
  262. data/test/manual/create_posts_for_web.rb +114 -0
  263. data/test/manual/environment.rb +67 -0
  264. data/test/manual/make_banner.rb +153 -0
  265. data/test/manual/preview_manual_test.rb +129 -0
  266. data/test/manual/sample_banner_config.txt +12 -0
  267. data/test/manual/test_advanced_widgets.rb +73 -0
  268. data/test/manual/test_banner_combinations.rb +120 -0
  269. data/test/manual/test_banner_features.rb +306 -0
  270. data/test/manual/test_banner_integration.rb +115 -0
  271. data/test/manual/test_banner_radial.rb +87 -0
  272. data/test/manual/test_basic_posts.rb +47 -0
  273. data/test/manual/test_layout_widgets.rb +40 -0
  274. data/test/manual/test_pagination.rb +24 -0
  275. data/test/manual/test_random_posts.rb +38 -0
  276. data/test/manual/test_syntax_highlighting.rb +167 -0
  277. data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
  278. data/test/rubytext/rubytext_demo_test.rb +42 -0
  279. data/test/rubytext/rubytext_testing_guide.md +277 -0
  280. data/test/run_automated_tests.rb +45 -0
  281. data/test/staging/.DS_Store +0 -0
  282. data/test/support/preview_utils.rb +88 -0
  283. data/test/syntax_highlighting_test.lt3 +124 -0
  284. data/test/test_gem_assets.rb +48 -0
  285. data/test/test_helpers.rb +240 -0
  286. data/test/tui_editor_integration_test.rb +296 -0
  287. data/test/tui_integration_test.rb +883 -0
  288. data/test/unit/api.rb +1776 -0
  289. data/test/unit/asset_management.rb +219 -0
  290. data/test/unit/backup_test.rb +451 -0
  291. data/test/unit/clipboard_test.rb +60 -0
  292. data/test/unit/contract_test.rb +69 -0
  293. data/test/unit/core.rb +1211 -0
  294. data/test/unit/deploy_config_test.rb +248 -0
  295. data/test/unit/deploy_test.rb +478 -0
  296. data/test/unit/edit_post_test.rb +168 -0
  297. data/test/unit/gem_asset_management.rb +183 -0
  298. data/test/unit/livetext_basic.rb +57 -0
  299. data/test/unit/livetext_compatibility.rb +82 -0
  300. data/test/unit/parse_cmd_test.rb +260 -0
  301. data/test/unit/permalink_copy_test.rb +211 -0
  302. data/test/unit/post.rb +309 -0
  303. data/test/unit/post_index_config_test.rb +258 -0
  304. data/test/unit/post_state_helpers_test.rb +137 -0
  305. data/test/unit/read_commented_file_test.rb +278 -0
  306. data/test/unit/reddit_test.rb +235 -0
  307. data/test/unit/repo.rb +569 -0
  308. data/test/unit/social_test.rb +366 -0
  309. data/test/unit/syntax_highlighting.rb +70 -0
  310. data/test/unit/theme_management_test.rb +91 -0
  311. data/test/unit/view.rb +498 -0
  312. data/test/unit/widgets.rb +669 -0
  313. data/test/web_integration_test.rb +231 -0
  314. data/test/web_test_helper.rb +218 -0
  315. data/test/web_workflow_test.rb +527 -0
  316. data/test/wizard_test.rb +123 -0
  317. data/ui/README.md +67 -0
  318. data/ui/common/lib/ui_common.rb +8 -0
  319. data/ui/rubytext/README.md +191 -0
  320. data/ui/rubytext/bin/scriptorium-rubytext +402 -0
  321. data/ui/rubytext/lib/rubytext_ui.rb +300 -0
  322. data/ui/tui/bin/scriptorium +1890 -0
  323. data/ui/tui/test/tui_test.rb +23 -0
  324. data/ui/web/app/app.rb +2600 -0
  325. data/ui/web/app/assets/livetext_mode.js +244 -0
  326. data/ui/web/app/error_helpers.rb +150 -0
  327. data/ui/web/app/views/advanced_config.erb +196 -0
  328. data/ui/web/app/views/asset_management.erb +645 -0
  329. data/ui/web/app/views/backup_management.erb +238 -0
  330. data/ui/web/app/views/banner_config.erb +200 -0
  331. data/ui/web/app/views/config_widget.erb +232 -0
  332. data/ui/web/app/views/configure_view.erb +401 -0
  333. data/ui/web/app/views/dashboard.erb +154 -0
  334. data/ui/web/app/views/deploy_config.erb +149 -0
  335. data/ui/web/app/views/edit_pages.erb +363 -0
  336. data/ui/web/app/views/edit_post.erb +175 -0
  337. data/ui/web/app/views/edit_theme.erb +73 -0
  338. data/ui/web/app/views/edit_theme_file.erb +74 -0
  339. data/ui/web/app/views/error_page.erb +29 -0
  340. data/ui/web/app/views/header_config.erb +155 -0
  341. data/ui/web/app/views/layout_config.erb +147 -0
  342. data/ui/web/app/views/navbar_config.erb +411 -0
  343. data/ui/web/app/views/theme_management.erb +130 -0
  344. data/ui/web/app/views/view_dashboard.erb +779 -0
  345. data/ui/web/app/views/widgets.erb +249 -0
  346. data/ui/web/bin/scriptorium-web +164 -0
  347. data/ui/web/test/web_basic_test.rb +38 -0
  348. data/ui/web/test_navbar.txt +7 -0
  349. data/ui/web/tmp/timing.log +17 -0
  350. data/ui/web/tmp/web_server.log +0 -0
  351. metadata +434 -8
  352. data/lib/scriptorium/engine.rb +0 -22
  353. data/test/engine/unit.rb +0 -44
@@ -0,0 +1,94 @@
1
+ require 'minitest/autorun'
2
+ require_relative '../../lib/scriptorium'
3
+ require_relative '../test_helpers'
4
+ require 'fileutils'
5
+ require_relative '../support/preview_utils'
6
+
7
+ class PreviewFlowTest < Minitest::Test
8
+ include TestHelpers
9
+
10
+ def setup
11
+ @repo_name = "test/scriptorium-TEST"
12
+ @view_name = "preview_test_view"
13
+
14
+ @api = Scriptorium::API.new
15
+ # Ensure a clean slate before creating the repo
16
+ begin
17
+ FileUtils.rm_rf(@repo_name)
18
+ backup_dir = File.join(File.dirname(@repo_name), 'backup-scriptorium-TEST')
19
+ FileUtils.rm_rf(backup_dir)
20
+ rescue => e
21
+ puts "[Integration] Warning: cleanup failed: #{e.message}"
22
+ end
23
+ @api.create_repo(@repo_name)
24
+ @api.open_repo(@repo_name)
25
+ @api.create_view(@view_name, "Preview Test View")
26
+
27
+ # Add a couple of posts
28
+ @api.create_post("Final Test Post", "ADD BLURB HERE")
29
+ @api.create_post("Another New Post", "ADD BLURB HERE")
30
+
31
+ # Configure date format with break and small pagination (via shared helper)
32
+ PreviewUtils.write_post_index_config(@repo_name, @view_name, {
33
+ "posts.per.page" => "5",
34
+ "entry.date.format" => "month dd break yyyy"
35
+ })
36
+
37
+ # Upload a global image and reference it from the first post via .image
38
+ test_asset = Tempfile.new(['previewflow', '.jpg'])
39
+ begin
40
+ test_asset.write("x")
41
+ test_asset.close
42
+ @api.upload_asset(test_asset.path, filename: "previewflow.jpg")
43
+ ensure
44
+ test_asset.unlink rescue nil
45
+ end
46
+
47
+ # Insert image reference into post 0001
48
+ src1 = "#{@repo_name}/posts/0001/source.lt3"
49
+ File.write(src1, ".image previewflow.jpg\n\nHello")
50
+
51
+ # Generate view
52
+ @api.generate_view(@view_name)
53
+ end
54
+
55
+ def teardown
56
+ FileUtils.rm_rf(@repo_name)
57
+ end
58
+
59
+ def test_post_index_has_table_and_formatted_dates
60
+ post_index = "#{@repo_name}/views/#{@view_name}/output/post_index.html"
61
+ assert File.exist?(post_index), "Expected post_index.html to exist"
62
+ content = File.read(post_index)
63
+ assert_includes content, "<table", "post_index should be wrapped in a table"
64
+ assert_includes content, "<br>", "post_index should include a line break in the date"
65
+ end
66
+
67
+ def test_paginated_page_table_wrapper
68
+ page1 = "#{@repo_name}/views/#{@view_name}/output/page1.html"
69
+ assert File.exist?(page1), "Expected page1.html to exist"
70
+ content = File.read(page1)
71
+ assert_includes content, "<table", "page1 should be wrapped in a table"
72
+ end
73
+
74
+ def test_index_inlines_table_content
75
+ index = "#{@repo_name}/views/#{@view_name}/output/index.html"
76
+ assert File.exist?(index), "Expected index.html to exist"
77
+ content = File.read(index)
78
+ # The main section should include a table from post_index inclusion
79
+ assert_includes content, "<table", "index should inline post_index table"
80
+ end
81
+
82
+ def test_post_page_references_assets_relatively
83
+ # Verify that the first post page references ../assets/ for the image
84
+ # Find slug for post 0001 from its meta
85
+ meta = File.read("#{@repo_name}/posts/0001/meta.txt")
86
+ slug = meta.lines.grep(/^post.slug\s+/).first.split.last
87
+ post_html = "#{@repo_name}/views/#{@view_name}/output/posts/#{slug}"
88
+ assert File.exist?(post_html), "Expected post HTML to exist"
89
+ html = File.read(post_html)
90
+ assert_includes html, "<img src=../assets/previewflow.jpg", "Post image should use ../assets/ path"
91
+ end
92
+ end
93
+
94
+
@@ -0,0 +1,500 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../lib/scriptorium'
5
+ require 'tempfile'
6
+ require_relative 'test_helpers'
7
+
8
+ class LivetextPluginTest < Minitest::Test
9
+ include TestHelpers
10
+
11
+ def setup
12
+ @test_repo_path = "scriptorium-TEST"
13
+ cleanup_test_repo
14
+ end
15
+
16
+ def teardown
17
+ cleanup_test_repo
18
+ end
19
+
20
+ def create_test_environment_with_content
21
+ # Create test repo using API
22
+ @api = Scriptorium::API.new(testmode: true)
23
+ @api.create_repo(@test_repo_path)
24
+
25
+ # Create a view
26
+ @api.create_view("testview", "Test View")
27
+
28
+ # Create a page using the API
29
+ @api.create_page("testview", "testpage", "Test Page", "This is a test page for link testing.")
30
+
31
+ # Create a post using the API
32
+ @api.create_post("Test Post", "This is a test post for link testing.", views: "testview", tags: "test")
33
+ end
34
+
35
+ def test_001_basic_dot_commands
36
+ # Test basic dot commands that should work
37
+ content = <<~EOS
38
+ .title My Test Post
39
+ .created
40
+ .views sample
41
+ .tags ruby, testing
42
+ .blurb
43
+ This is a test post
44
+ .end
45
+ This is the body content
46
+ EOS
47
+
48
+ body, vars = process_livetext(content)
49
+
50
+ # Check that variables were set correctly
51
+ assert_equal("My Test Post", vars[:"post.title"])
52
+ assert_equal("sample", vars[:"post.views"])
53
+ assert_equal("ruby, testing", vars[:"post.tags"])
54
+ assert_equal("This is a test post", vars[:"post.blurb"])
55
+
56
+ # Check that body text is returned
57
+ assert_match(/This is the body content/, body)
58
+ end
59
+
60
+ def test_002_html_formatting_commands
61
+ # Test HTML formatting commands
62
+ content = <<~EOS
63
+ .h1 Main Heading
64
+ .h2 Sub Heading
65
+ .list
66
+ Item 1
67
+ Item 2
68
+ Item 3
69
+ .end
70
+ .nlist
71
+ Numbered 1
72
+ Numbered 2
73
+ .end
74
+ .quote
75
+ This is a quote
76
+ .end
77
+ .hr
78
+ EOS
79
+
80
+ body, vars = process_livetext(content)
81
+
82
+ # Check HTML output
83
+ assert_match(/<h1>Main Heading<\/h1>/, body)
84
+ assert_match(/<h2>Sub Heading<\/h2>/, body)
85
+ assert_match(/<ul>/, body)
86
+ assert_match(/<li>Item 1<\/li>/, body)
87
+ assert_match(/<ol>/, body)
88
+ assert_match(/<li>Numbered 1<\/li>/, body)
89
+ assert_match(/<blockquote>/, body)
90
+ assert_match(/<hr>/, body)
91
+ end
92
+
93
+ def test_003_special_formatting
94
+ # Test special formatting like dropcap and inset
95
+ content = <<~EOS
96
+ .dropcap
97
+ This is a dropcap test with multiple lines.
98
+ The dropcap should apply to the first letter of this content.
99
+ .end
100
+ .inset left 25
101
+ |This goes in the inset
102
+ This goes in the body
103
+ /This only goes in inset
104
+ .end
105
+ EOS
106
+
107
+ body, vars = process_livetext(content)
108
+
109
+ # Check for dropcap formatting - should be a span with class for first character
110
+ assert_match(/<span class="dropcap">/, body)
111
+ assert_match(/his is a dropcap test with multiple lines\./, body)
112
+ assert_match(/The dropcap should apply to the first letter/, body)
113
+
114
+ # Check for inset formatting
115
+ assert_match(/float:left/, body)
116
+ assert_match(/width: 25%/, body)
117
+ end
118
+
119
+ def test_004_functions
120
+ # Test Livetext functions
121
+ content = <<~EOS
122
+ $$br:2
123
+ $$h1[Function Heading]
124
+ $$h2[Sub Heading]
125
+ $$image:test.jpg
126
+ EOS
127
+
128
+ body, vars = process_livetext(content)
129
+
130
+ # Check function output
131
+ assert_match(/<br><br>/, body)
132
+ assert_match(/<h1>Function Heading<\/h1>/, body)
133
+ assert_match(/<h2>Sub Heading<\/h2>/, body)
134
+ # In non-post context with no real file, $$image resolves to missing image
135
+ assert_match(/<img src=assets\/imagenotfound\.jpg alt='No alt text'><\/img>/, body)
136
+ end
137
+
138
+ def test_024_center_dot_command_with_image_function
139
+ content = <<~EOS
140
+ .center
141
+ $$image:test.jpg
142
+ .end
143
+ EOS
144
+
145
+ body, vars = process_livetext(content)
146
+
147
+ assert_match(/<div style="text-align:center">/, body)
148
+ # In non-post context with no real file, $$image resolves to missing image
149
+ assert_match(/<img src=assets\/imagenotfound\.jpg alt='No alt text'><\/img>/, body)
150
+ end
151
+
152
+ def test_023_asset_dot_command_copies_to_post_assets
153
+ # Create repo/view/post context
154
+ create_test_environment_with_content
155
+ # Create a temp file to copy
156
+ tmp = Tempfile.new(['sample', '.txt'])
157
+ tmp.write("hello")
158
+ tmp.close
159
+
160
+ repo_root = 'scriptorium-TEST'
161
+ source_path = File.join(repo_root, 'posts', '0001', 'source.lt3')
162
+ # Append .asset and a reference line to the post source
163
+ File.open(source_path, 'a') do |f|
164
+ f.puts ".asset #{tmp.path}"
165
+ f.puts "Copied path: $$asset[#{File.basename(tmp.path)}]"
166
+ end
167
+
168
+ # Generate the post so the snippet is rendered
169
+ api = Scriptorium::API.new(testmode: true)
170
+ api.open_repo(repo_root)
171
+ api.generate_post(1)
172
+
173
+ # Read body.html and verify both copy and rendered reference
174
+ body_file = File.join(repo_root, 'posts', '0001', 'body.html')
175
+ body = File.read(body_file)
176
+ post_asset_path = File.join(repo_root, 'posts', '0001', 'assets', File.basename(tmp.path))
177
+ assert File.exist?(post_asset_path), "Expected copied asset under post assets"
178
+ assert_includes body, "Copied path: ../assets/posts/0001/#{File.basename(tmp.path)}"
179
+ ensure
180
+ tmp.unlink if tmp && File.exist?(tmp.path)
181
+ end
182
+
183
+ def test_005_dropcap_command
184
+ # Test the dropcap command
185
+ content = <<~EOS
186
+ .dropcap
187
+ Once upon a time there was a story that began with a dropcap.
188
+ This is the rest of the paragraph that flows naturally.
189
+ .end
190
+ EOS
191
+
192
+ body, vars = process_livetext(content)
193
+
194
+ # Check dropcap formatting
195
+ assert_match(/<span class="dropcap">/, body)
196
+ assert_match(/nce upon a time there was a story/, body)
197
+ assert_match(/This is the rest of the paragraph/, body)
198
+
199
+ # Should be a single paragraph, not split into multiple divs
200
+ refute_match(/mydrop/, body)
201
+ refute_match(/padding-top: 1px/, body)
202
+ end
203
+
204
+ def test_006_faq_command
205
+ # Test the FAQ command
206
+ content = <<~EOS
207
+ .faq What is this?
208
+ This is a test question and answer.
209
+ .end
210
+ .faq Another question?
211
+ Another answer.
212
+ .end
213
+ EOS
214
+
215
+ body, vars = process_livetext(content)
216
+
217
+ # Check FAQ formatting
218
+ assert_match(/data-toggle="collapse"/, body)
219
+ assert_match(/What is this\?/, body)
220
+ assert_match(/This is a test question and answer/, body)
221
+ assert_match(/Another question\?/, body)
222
+ end
223
+
224
+ def test_007_last_updated_command
225
+ skip "Possible LiveText bug with last_updated command"
226
+ content = <<~EOS
227
+ .created
228
+ .title Test Post
229
+ .last_updated
230
+ EOS
231
+ body, vars = process_livetext(content)
232
+ assert_match(/Published:/, body)
233
+ today = Time.now.strftime("%Y-%m-%d")
234
+ assert_match(/#{today}/, body)
235
+ end
236
+
237
+ def test_008_image_command
238
+ # Test the .image command
239
+ content = <<~EOS
240
+ .title Test Post
241
+ .image test.jpg
242
+ This is the body content.
243
+ EOS
244
+
245
+ body, vars = process_livetext(content)
246
+
247
+ # Check that image command worked
248
+ assert_match(/<img/, body)
249
+ assert_match(/src=assets\/imagenotfound\.jpg/, body)
250
+ assert_match(/alt='No alt text'/, body)
251
+ assert_match(/This is the body content/, body)
252
+ # Should not contain the literal ".image" if working
253
+ refute_match(/\.image/, body)
254
+ end
255
+
256
+ def test_010_image_exclamation_command
257
+ # Test the .image! command (featured image)
258
+ content = <<~EOS
259
+ .title Test Post
260
+ .image! featured.jpg 800 600 1 Featured image with alt text
261
+ This is the body content.
262
+ EOS
263
+
264
+ body, vars = process_livetext(content)
265
+
266
+ # Check that image! command worked
267
+ assert_match(/<img/, body)
268
+ assert_match(/src=assets\/imagenotfound\.jpg/, body)
269
+ assert_match(/width=800/, body)
270
+ assert_match(/height=600/, body)
271
+ assert_match(/alt='Featured image with alt text'/, body)
272
+ assert_match(/This is the body content/, body)
273
+ # Should not contain the literal ".image!" if working
274
+ refute_match(/\.image!/, body)
275
+ end
276
+
277
+ def test_011_image_with_alt_text
278
+ # Test image command with alt text
279
+ content = <<~EOS
280
+ .title Test Post
281
+ .image test.jpg Alt text for image
282
+ This is the body content.
283
+ EOS
284
+
285
+ body, vars = process_livetext(content)
286
+
287
+ # Check that image with alt text worked
288
+ assert_match(/<img/, body)
289
+ assert_match(/src=assets\/imagenotfound\.jpg/, body)
290
+ assert_match(/alt='Alt text for image'/, body)
291
+ assert_match(/This is the body content/, body)
292
+ end
293
+
294
+ def test_012_pullquote_command
295
+ # Test the pullquote command
296
+ content = <<~EOS
297
+ .title Test Post
298
+ This is some content before the pullquote.
299
+
300
+ .pullquote right 200px
301
+ This is an impactful sentence that will be highlighted in a box.
302
+ .end
303
+
304
+ This is content after the pullquote that should flow around it.
305
+
306
+ .pullquote left 180px
307
+ This is another important point on the left side.
308
+ .end
309
+
310
+ More content that flows around the left-aligned pullquote.
311
+ EOS
312
+
313
+ body, vars = process_livetext(content)
314
+
315
+ # Check that pullquote HTML was generated
316
+ assert_match(/<div class="pullquote pullquote-right"/, body)
317
+ assert_match(/<div class="pullquote pullquote-left"/, body)
318
+ assert_match(/style="width: 200px;"/, body)
319
+ assert_match(/style="width: 180px;"/, body)
320
+
321
+ # Check that the content is included
322
+ assert_match(/This is an impactful sentence/, body)
323
+ assert_match(/This is another important point/, body)
324
+
325
+ # Check that surrounding content is present
326
+ assert_match(/This is some content before/, body)
327
+ assert_match(/This is content after the pullquote/, body)
328
+ assert_match(/More content that flows around/, body)
329
+
330
+ # Should not contain the literal ".pullquote" if working
331
+ refute_match(/\.pullquote/, body)
332
+ end
333
+
334
+ def test_013_page_function_with_content
335
+ create_test_environment_with_content
336
+
337
+ content = <<~EOS
338
+ Check out this page: $$page[testpage]
339
+ EOS
340
+
341
+ # Set the view context for the LiveText functions
342
+ temp_file = "test/temp_livetext.lt3"
343
+ write_file(temp_file, content)
344
+ live = Livetext.customize(mix: "lt3scriptor", call: ".nopara", vars: { View: "testview" })
345
+ body, vars = live.process(file: temp_file)
346
+
347
+ assert_includes body, '<a href="testpage.html">Test Page</a>'
348
+ refute_includes body, '[link is broken]'
349
+ ensure
350
+ File.delete(temp_file) if temp_file && File.exist?(temp_file)
351
+ end
352
+
353
+ def test_014_page_function_missing
354
+ create_test_environment_with_content
355
+
356
+ content = <<~EOS
357
+ Check out this missing page: $$page[nonexistent]
358
+ EOS
359
+
360
+ temp_file = "test/temp_livetext.lt3"
361
+ write_file(temp_file, content)
362
+ live = Livetext.customize(mix: "lt3scriptor", call: ".nopara", vars: { View: "testview" })
363
+ body, vars = live.process(file: temp_file)
364
+
365
+ assert_includes body, '<a href="nonexistent.html">Nonexistent</a>'
366
+ assert_includes body, '[link is broken]'
367
+ ensure
368
+ File.delete(temp_file) if temp_file && File.exist?(temp_file)
369
+ end
370
+
371
+ def test_015_post_function_with_content
372
+ create_test_environment_with_content
373
+
374
+ content = <<~EOS
375
+ Check out this post: $$post[1]
376
+ EOS
377
+
378
+ temp_file = "test/temp_livetext.lt3"
379
+ write_file(temp_file, content)
380
+ live = Livetext.customize(mix: "lt3scriptor", call: ".nopara", vars: { View: "testview" })
381
+ body, vars = live.process(file: temp_file)
382
+
383
+ assert_includes body, '<a href="0001-test-post">Test Post</a>'
384
+ refute_includes body, '[link is broken]'
385
+ ensure
386
+ File.delete(temp_file) if temp_file && File.exist?(temp_file)
387
+ end
388
+
389
+ def test_016_post_function_missing
390
+ create_test_environment_with_content
391
+
392
+ content = <<~EOS
393
+ Check out this missing post: $$post[999]
394
+ EOS
395
+
396
+ temp_file = "test/temp_livetext.lt3"
397
+ write_file(temp_file, content)
398
+ live = Livetext.customize(mix: "lt3scriptor", call: ".nopara", vars: { View: "testview" })
399
+ body, vars = live.process(file: temp_file)
400
+
401
+ assert_includes body, '<a href="0999-untitled.html">Post 999</a>'
402
+ assert_includes body, '[link is broken]'
403
+ ensure
404
+ File.delete(temp_file) if temp_file && File.exist?(temp_file)
405
+ end
406
+
407
+ def test_017_imglink_function
408
+ content = <<~EOS
409
+ Check out this image: $$imglink[test.jpg|https://example.com|Click me]
410
+ EOS
411
+
412
+ body, vars = process_livetext(content)
413
+
414
+ assert_includes body, '<a href="https://example.com">'
415
+ assert_includes body, '<img src='
416
+ assert_includes body, 'alt="Click me"'
417
+ end
418
+
419
+ def test_018_imglink_function_minimal
420
+ content = <<~EOS
421
+ Simple image: $$imglink[icon.png]
422
+ EOS
423
+
424
+ body, vars = process_livetext(content)
425
+
426
+ assert_includes body, '<a href="#">'
427
+ assert_includes body, '<img src='
428
+ assert_includes body, 'alt="Image"'
429
+ end
430
+
431
+ def test_019_p_dotcmd
432
+ content = <<~EOS
433
+ .p This is a paragraph
434
+ EOS
435
+
436
+ body, vars = process_livetext(content)
437
+
438
+ assert_includes body, '<p>This is a paragraph</p>'
439
+ end
440
+
441
+ def test_020_para_dotcmd
442
+ content = <<~EOS
443
+ .para
444
+ This is a paragraph
445
+ with multiple lines
446
+ .end
447
+ EOS
448
+
449
+ body, vars = process_livetext(content)
450
+
451
+ assert_includes body, '<p>This is a paragraph with multiple lines</p>'
452
+ end
453
+
454
+ def test_021_quote_dotcmd
455
+ content = <<~EOS
456
+ .quote
457
+ This is a quote
458
+ with multiple lines
459
+ .end
460
+ EOS
461
+
462
+ body, vars = process_livetext(content)
463
+
464
+ assert_includes body, '<blockquote>'
465
+ assert_includes body, 'This is a quote with multiple lines'
466
+ assert_includes body, '</blockquote>'
467
+ end
468
+
469
+ def test_022_page_title_dotcmd
470
+ content = <<~EOS
471
+ .page_title My Page Title
472
+ EOS
473
+
474
+ body, vars = process_livetext(content)
475
+
476
+ assert_equal "My Page Title", vars[:"page.title"]
477
+ end
478
+
479
+ private
480
+
481
+ def process_livetext(content)
482
+ # Create a temporary file with the content
483
+ temp_file = "test/temp_livetext.lt3"
484
+ write_file(temp_file, content)
485
+ live = Livetext.customize(mix: "lt3scriptor", call: ".nopara")
486
+ body, vars = live.process(file: temp_file)
487
+ return [body, vars]
488
+ rescue => e
489
+ return [nil, { error: e.message, backtrace: e.backtrace.first }]
490
+ ensure
491
+ # Clean up
492
+ File.delete(temp_file) if File.exist?(temp_file)
493
+ end
494
+
495
+ def cleanup_test_repo
496
+ if Dir.exist?(@test_repo_path)
497
+ FileUtils.rm_rf(@test_repo_path)
498
+ end
499
+ end
500
+ end
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Manual test for asset management functionality
4
+ # This is a MANUAL test - you need to run it step by step
5
+
6
+ require_relative "../../lib/scriptorium"
7
+ require_relative "../test_helpers"
8
+ require "fileutils"
9
+ require_relative "./environment"
10
+
11
+ manual_setup
12
+
13
+ @repo.create_view("assetview", "Asset Test View", "A test view for asset management")
14
+
15
+ # Copy test images from dev_assets
16
+ FileUtils.cp("dev_assets/global.png", "test/scriptorium-TEST/assets/global-test.png")
17
+ FileUtils.cp("dev_assets/view.png", "test/scriptorium-TEST/views/assetview/assets/view-test.png")
18
+ FileUtils.mkdir_p("test/scriptorium-TEST/posts/0001/assets")
19
+ FileUtils.cp("dev_assets/post.png", "test/scriptorium-TEST/posts/0001/assets/post-test.png")
20
+ FileUtils.mkdir_p("test/scriptorium-TEST/assets/library")
21
+ FileUtils.cp("dev_assets/library.png", "test/scriptorium-TEST/assets/library/library-test.png")
22
+
23
+ # Create a post with asset references
24
+ draft_body = <<~BODY
25
+ .blurb Testing asset management with real images.
26
+
27
+ This post tests the asset management system with real PNG files.
28
+
29
+ Global asset: $$asset[global-test.png]
30
+
31
+ View asset: $$asset[view-test.png]
32
+
33
+ Post asset: $$asset[post-test.png]
34
+
35
+ Library asset: $$asset[library-test.png]
36
+
37
+ Missing asset: $$asset[nonexistent.png]
38
+
39
+ And with image tags:
40
+
41
+ $$image_asset[global-test.png]
42
+ $$image_asset[view-test.png]
43
+ $$image_asset[post-test.png]
44
+ $$image_asset[library-test.png]
45
+ $$image_asset[nonexistent.png]
46
+
47
+ The assets should be copied to the output directory and display correctly.
48
+ BODY
49
+
50
+ name = @repo.create_draft(title: "Asset Management Test", views: ["assetview"], body: draft_body)
51
+ num = @repo.finish_draft(name)
52
+ @repo.generate_post(num)
53
+
54
+ @repo.generate_front_page("assetview")
55
+
56
+ instruct <<~EOS
57
+ The post should display with:
58
+ - Global image (global-test.png) from assets/
59
+ - View image (view-test.png) from views/assetview/assets/
60
+ - Post image (post-test.png) from posts/0001/assets/
61
+ - Library image (library-test.png) from assets/library/
62
+ - Missing image placeholder for nonexistent.png
63
+
64
+ Check that images are copied to views/assetview/output/assets/
65
+ and that URLs are generated correctly.
66
+ EOS
67
+ examine("assetview")