├── .clj-kondo └── config.edn ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .projectile ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docs ├── css │ ├── default.css │ └── highlight.css ├── index.html ├── js │ ├── highlight.min.js │ ├── jquery.min.js │ └── page_effects.js ├── organa.artworks.html ├── organa.config.html ├── organa.core.html ├── organa.dates.html ├── organa.egg.html ├── organa.files.html ├── organa.fs.html ├── organa.gallery.html ├── organa.html.html ├── organa.image.html ├── organa.io.html ├── organa.mirror.html ├── organa.pages.html ├── organa.parse.html └── organa.rss.html ├── organa.png ├── project.clj ├── resources └── banner-format.txt ├── src └── organa │ ├── artworks.clj │ ├── config.clj │ ├── core.clj │ ├── dates.clj │ ├── egg.clj │ ├── fs.clj │ ├── gallery.clj │ ├── html.clj │ ├── image.clj │ ├── pages.clj │ ├── parse.clj │ └── rss.clj ├── test └── organa │ ├── dates_test.clj │ └── fs_test.clj └── tests.edn /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {organa.html/a clj-kondo.lint-as/def-catch-all 2 | h/a clj-kondo.lint-as/def-catch-all} 3 | :linters {:unresolved-symbol 4 | {:exclude [a 5 | div 6 | em 7 | h1 8 | h2 9 | img 10 | li 11 | link 12 | meta-tag 13 | p 14 | pre 15 | script 16 | span 17 | strong 18 | strong 19 | style 20 | table 21 | tbody 22 | td 23 | th 24 | thead 25 | tr 26 | ul]} 27 | :unresolved-var 28 | {:level :off}}} 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | 7 | jobs: 8 | build: 9 | name: build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | - name: build 15 | run: make docker 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.html 2 | *.monopic 3 | *.pages 4 | *jar 5 | .clj-kondo/.cache/* 6 | .eastwood 7 | .lein-deps-sum 8 | .lein-failures 9 | .lein-plugins/ 10 | .lein-repl-history 11 | .nrepl-port 12 | /checkouts/ 13 | /classes/ 14 | /lib/ 15 | /target/ 16 | pom.xml 17 | pom.xml.asc 18 | -------------------------------------------------------------------------------- /.projectile: -------------------------------------------------------------------------------- 1 | - /target/ 2 | - /docs/ 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM adoptopenjdk:11-jre-hotspot 2 | 3 | RUN apt-get -qq -y update 4 | RUN apt-get -qq -y upgrade 5 | 6 | RUN apt-get install -qq -y leiningen make 7 | 8 | WORKDIR /home/janice 9 | COPY . /home/janice 10 | RUN make all 11 | 12 | # When there is an example site of .org/.html/image files, build it as part of this test. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2021 John Jacobsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all docker test uberjar lint ancient-advisory 2 | 3 | BINPATH = ${HOME}/bin 4 | JARPATH = target/uberjar/organa.jar 5 | 6 | all: test lint ancient-advisory uberjar doc install 7 | 8 | ${JARPATH}: src/organa/*.clj project.clj resources/* 9 | lein uberjar 10 | 11 | uberjar: 12 | make ${JARPATH} 13 | 14 | clean: 15 | rm -rf target/* 16 | 17 | install: 18 | mkdir -p ${BINPATH} 19 | cp ${JARPATH} ${BINPATH} 20 | 21 | doc: 22 | lein codox 23 | 24 | lint: 25 | lein do bikeshed, kibit, eastwood 26 | 27 | ancient-advisory: 28 | lein ancient && echo 'up to date!' || echo 'WARNING: updates needed!' 29 | 30 | test: 31 | lein kaocha 32 | 33 | docker: 34 | docker build -t organa . 35 | 36 | docker-quiet: 37 | docker build --quiet -t organa . 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Organa 2 | 3 | 4 | 5 | ![build](https://github.com/eigenhombre/organa/actions/workflows/build.yml/badge.svg) 6 | 7 | # Introduction 8 | 9 | This project implements a site generator for [my current 10 | website](http://johnj.com/) using [Emacs Org Mode](http://orgmode.org/). 11 | 12 | # Goals 13 | 14 | - To provide a [Jekyll](https://jekyllrb.com/)-like (or better) workflow: edit markup files, 15 | have them automatically processed into HTML... 16 | - using Org Mode as the markup language... 17 | - supporting all of 18 | - my art- and image-heavy pages and image galleries; 19 | - my math- and code-heavy software posts; 20 | - my narrative-heavy South Pole blog posts. 21 | 22 | # Strategy 23 | 24 | In the past I've written a few (incomplete) Org Mode parsers 25 | ([here](https://github.com/eigenhombre/blorg) is a previous attempt), but HTML 26 | is far easier to manipulate in Clojure using Hiccup or Enlive. The current 27 | code relies on one to export Org Mode to HTML first (with a few keystrokes in 28 | Emacs). The program parses the exported HTML, and modifies the parse tree as 29 | needed to create cross links to other posts, etc. 30 | 31 | # Status 32 | 33 | If I were you, I wouldn't use this yet, other than to steal ideas from it, 34 | since I haven't generalized it for multiple sites. 35 | 36 | # Workflow 37 | 38 | - Make edits to Org files in the source directory `site-source-dir` 39 | (or add new `.org` files there). 40 | - To tag a post (for showing the post type in the navigation section of each 41 | page), add an empty section with the relevant tag(s), e.g.: 42 | 43 | ``` 44 | * :mytag:othertag: 45 | ``` 46 | 47 | - Export changed/added `.org` file(s) to HTML using `\C-c e hh` 48 | (`org-expert-dispatch` to HTML). 49 | - To change the CSS for the site, edit `index.garden`; source will 50 | be interpreted by the `garden` Clojure library and included in 51 | every page. 52 | - Static files that should be copied /verbatim/ into the /top level of the 53 | target site/ are added to `/static`. These are synced 54 | whenever `.org` files are updated. 55 | - Directories of images in `/galleries` will be 56 | turned into static image galleries 57 | - Run the program to generate the Website. 58 | - To "publish," use the commented-out forms at the bottom of the 59 | `core` namespace to `rsync` the code to the remote Web site. 60 | 61 | # FAQ 62 | ## Why Org Mode? 63 | 64 | I really like writing in [Org Mode](http://orgmode.org/) (a text editing / 65 | outlining / To Do-list processing / scheduling / literate programming / 66 | ... mode for [Emacs](http://www.gnu.org/software/emacs/)). The outliner gets 67 | out of my way most of the time and lets me move ideas around while they are 68 | being formed, and lets me hide the portions that I'm not focusing on at any 69 | given time. I can export to a fairly nice looking PDF document in a few 70 | keystrokes. I also use the literate programming and LaTeX / math support from 71 | time to time. 72 | 73 | ## Why not *just* Org Mode? 74 | 75 | **I.e., why a Clojure app?** I find the export tools available for Org Mode are 76 | not quite powerful (or fast) enough for a large (> 100 posts) blog. I got 77 | pretty far trying to get the export features to suit, but not far enough -- 78 | generation of a large site took too long, and customization was too unweildy. 79 | In general I much prefer developing software in [Clojure](http://clojure.org) 80 | than in Emacs Lisp (though admittedly I'm less experienced with the latter). 81 | 82 | ## Why not Jekyll? 83 | 84 | I used Jekyll for a few years and was somewhat satisfied by it. But it 85 | doesn't support Org Mode, and I am simply not that fond of Ruby and its 86 | related ecosystems. Also I have a number of customizations relating to 87 | handling images that I'm unlikely to easily get working with Jekyll. 88 | 89 | ## Why not another existing site generator? 90 | 91 | Probably there are some great ones out there. I tend to have really specific 92 | wishes around Web site functionality, and this is the easiest way I've found 93 | to fulfill those. 94 | 95 | # Implementation 96 | 97 | ## Code / API documentation 98 | 99 | [Codox](https://github.com/weavejester/codox) docs are 100 | [here](https://raw.githack.com/eigenhombre/organa/master/docs/index.html). 101 | 102 | # License 103 | 104 | Copyright © 2016-2021, John Jacobsen. MIT License. 105 | 106 | # Disclaimer 107 | 108 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 109 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 110 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 111 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 112 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 113 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 114 | SOFTWARE. 115 | -------------------------------------------------------------------------------- /docs/css/default.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-size: 15px; 4 | } 5 | 6 | pre, code { 7 | font-family: Monaco, DejaVu Sans Mono, Consolas, monospace; 8 | font-size: 9pt; 9 | margin: 15px 0; 10 | } 11 | 12 | h1 { 13 | font-weight: normal; 14 | font-size: 29px; 15 | margin: 10px 0 2px 0; 16 | padding: 0; 17 | } 18 | 19 | h2 { 20 | font-weight: normal; 21 | font-size: 25px; 22 | } 23 | 24 | h5.license { 25 | margin: 9px 0 22px 0; 26 | color: #555; 27 | font-weight: normal; 28 | font-size: 12px; 29 | font-style: italic; 30 | } 31 | 32 | .document h1, .namespace-index h1 { 33 | font-size: 32px; 34 | margin-top: 12px; 35 | } 36 | 37 | #header, #content, .sidebar { 38 | position: fixed; 39 | } 40 | 41 | #header { 42 | top: 0; 43 | left: 0; 44 | right: 0; 45 | height: 22px; 46 | color: #f5f5f5; 47 | padding: 5px 7px; 48 | } 49 | 50 | #content { 51 | top: 32px; 52 | right: 0; 53 | bottom: 0; 54 | overflow: auto; 55 | background: #fff; 56 | color: #333; 57 | padding: 0 18px; 58 | } 59 | 60 | .sidebar { 61 | position: fixed; 62 | top: 32px; 63 | bottom: 0; 64 | overflow: auto; 65 | } 66 | 67 | .sidebar.primary { 68 | background: #e2e2e2; 69 | border-right: solid 1px #cccccc; 70 | left: 0; 71 | width: 250px; 72 | } 73 | 74 | .sidebar.secondary { 75 | background: #f2f2f2; 76 | border-right: solid 1px #d7d7d7; 77 | left: 251px; 78 | width: 200px; 79 | } 80 | 81 | #content.namespace-index, #content.document { 82 | left: 251px; 83 | } 84 | 85 | #content.namespace-docs { 86 | left: 452px; 87 | } 88 | 89 | #content.document { 90 | padding-bottom: 10%; 91 | } 92 | 93 | #header { 94 | background: #3f3f3f; 95 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); 96 | z-index: 100; 97 | } 98 | 99 | #header h1 { 100 | margin: 0; 101 | padding: 0; 102 | font-size: 18px; 103 | font-weight: lighter; 104 | text-shadow: -1px -1px 0px #333; 105 | } 106 | 107 | #header h1 .project-version { 108 | font-weight: normal; 109 | } 110 | 111 | .project-version { 112 | padding-left: 0.15em; 113 | } 114 | 115 | #header a, .sidebar a { 116 | display: block; 117 | text-decoration: none; 118 | } 119 | 120 | #header a { 121 | color: #f5f5f5; 122 | } 123 | 124 | .sidebar a { 125 | color: #333; 126 | } 127 | 128 | #header h2 { 129 | float: right; 130 | font-size: 9pt; 131 | font-weight: normal; 132 | margin: 4px 3px; 133 | padding: 0; 134 | color: #bbb; 135 | } 136 | 137 | #header h2 a { 138 | display: inline; 139 | } 140 | 141 | .sidebar h3 { 142 | margin: 0; 143 | padding: 10px 13px 0 13px; 144 | font-size: 19px; 145 | font-weight: lighter; 146 | } 147 | 148 | .sidebar h3 a { 149 | color: #444; 150 | } 151 | 152 | .sidebar h3.no-link { 153 | color: #636363; 154 | } 155 | 156 | .sidebar ul { 157 | padding: 7px 0 6px 0; 158 | margin: 0; 159 | } 160 | 161 | .sidebar ul.index-link { 162 | padding-bottom: 4px; 163 | } 164 | 165 | .sidebar li { 166 | display: block; 167 | vertical-align: middle; 168 | } 169 | 170 | .sidebar li a, .sidebar li .no-link { 171 | border-left: 3px solid transparent; 172 | padding: 0 10px; 173 | white-space: nowrap; 174 | } 175 | 176 | .sidebar li .no-link { 177 | display: block; 178 | color: #777; 179 | font-style: italic; 180 | } 181 | 182 | .sidebar li .inner { 183 | display: inline-block; 184 | padding-top: 7px; 185 | height: 24px; 186 | } 187 | 188 | .sidebar li a, .sidebar li .tree { 189 | height: 31px; 190 | } 191 | 192 | .depth-1 .inner { padding-left: 2px; } 193 | .depth-2 .inner { padding-left: 6px; } 194 | .depth-3 .inner { padding-left: 20px; } 195 | .depth-4 .inner { padding-left: 34px; } 196 | .depth-5 .inner { padding-left: 48px; } 197 | .depth-6 .inner { padding-left: 62px; } 198 | 199 | .sidebar li .tree { 200 | display: block; 201 | float: left; 202 | position: relative; 203 | top: -10px; 204 | margin: 0 4px 0 0; 205 | padding: 0; 206 | } 207 | 208 | .sidebar li.depth-1 .tree { 209 | display: none; 210 | } 211 | 212 | .sidebar li .tree .top, .sidebar li .tree .bottom { 213 | display: block; 214 | margin: 0; 215 | padding: 0; 216 | width: 7px; 217 | } 218 | 219 | .sidebar li .tree .top { 220 | border-left: 1px solid #aaa; 221 | border-bottom: 1px solid #aaa; 222 | height: 19px; 223 | } 224 | 225 | .sidebar li .tree .bottom { 226 | height: 22px; 227 | } 228 | 229 | .sidebar li.branch .tree .bottom { 230 | border-left: 1px solid #aaa; 231 | } 232 | 233 | .sidebar.primary li.current a { 234 | border-left: 3px solid #a33; 235 | color: #a33; 236 | } 237 | 238 | .sidebar.secondary li.current a { 239 | border-left: 3px solid #33a; 240 | color: #33a; 241 | } 242 | 243 | .namespace-index h2 { 244 | margin: 30px 0 0 0; 245 | } 246 | 247 | .namespace-index h3 { 248 | font-size: 16px; 249 | font-weight: bold; 250 | margin-bottom: 0; 251 | } 252 | 253 | .namespace-index .topics { 254 | padding-left: 30px; 255 | margin: 11px 0 0 0; 256 | } 257 | 258 | .namespace-index .topics li { 259 | padding: 5px 0; 260 | } 261 | 262 | .namespace-docs h3 { 263 | font-size: 18px; 264 | font-weight: bold; 265 | } 266 | 267 | .public h3 { 268 | margin: 0; 269 | float: left; 270 | } 271 | 272 | .usage { 273 | clear: both; 274 | } 275 | 276 | .public { 277 | margin: 0; 278 | border-top: 1px solid #e0e0e0; 279 | padding-top: 14px; 280 | padding-bottom: 6px; 281 | } 282 | 283 | .public:last-child { 284 | margin-bottom: 20%; 285 | } 286 | 287 | .members .public:last-child { 288 | margin-bottom: 0; 289 | } 290 | 291 | .members { 292 | margin: 15px 0; 293 | } 294 | 295 | .members h4 { 296 | color: #555; 297 | font-weight: normal; 298 | font-variant: small-caps; 299 | margin: 0 0 5px 0; 300 | } 301 | 302 | .members .inner { 303 | padding-top: 5px; 304 | padding-left: 12px; 305 | margin-top: 2px; 306 | margin-left: 7px; 307 | border-left: 1px solid #bbb; 308 | } 309 | 310 | #content .members .inner h3 { 311 | font-size: 12pt; 312 | } 313 | 314 | .members .public { 315 | border-top: none; 316 | margin-top: 0; 317 | padding-top: 6px; 318 | padding-bottom: 0; 319 | } 320 | 321 | .members .public:first-child { 322 | padding-top: 0; 323 | } 324 | 325 | h4.type, 326 | h4.dynamic, 327 | h4.added, 328 | h4.deprecated { 329 | float: left; 330 | margin: 3px 10px 15px 0; 331 | font-size: 15px; 332 | font-weight: bold; 333 | font-variant: small-caps; 334 | } 335 | 336 | .public h4.type, 337 | .public h4.dynamic, 338 | .public h4.added, 339 | .public h4.deprecated { 340 | font-size: 13px; 341 | font-weight: bold; 342 | margin: 3px 0 0 10px; 343 | } 344 | 345 | .members h4.type, 346 | .members h4.added, 347 | .members h4.deprecated { 348 | margin-top: 1px; 349 | } 350 | 351 | h4.type { 352 | color: #717171; 353 | } 354 | 355 | h4.dynamic { 356 | color: #9933aa; 357 | } 358 | 359 | h4.added { 360 | color: #508820; 361 | } 362 | 363 | h4.deprecated { 364 | color: #880000; 365 | } 366 | 367 | .namespace { 368 | margin-bottom: 30px; 369 | } 370 | 371 | .namespace:last-child { 372 | margin-bottom: 10%; 373 | } 374 | 375 | .index { 376 | padding: 0; 377 | font-size: 80%; 378 | margin: 15px 0; 379 | line-height: 16px; 380 | } 381 | 382 | .index * { 383 | display: inline; 384 | } 385 | 386 | .index p { 387 | padding-right: 3px; 388 | } 389 | 390 | .index li { 391 | padding-right: 5px; 392 | } 393 | 394 | .index ul { 395 | padding-left: 0; 396 | } 397 | 398 | .type-sig { 399 | clear: both; 400 | color: #088; 401 | } 402 | 403 | .type-sig pre { 404 | padding-top: 10px; 405 | margin: 0; 406 | } 407 | 408 | .usage code { 409 | display: block; 410 | color: #008; 411 | margin: 2px 0; 412 | } 413 | 414 | .usage code:first-child { 415 | padding-top: 10px; 416 | } 417 | 418 | p { 419 | margin: 15px 0; 420 | } 421 | 422 | .public p:first-child, .public pre.plaintext { 423 | margin-top: 12px; 424 | } 425 | 426 | .doc { 427 | margin: 0 0 26px 0; 428 | clear: both; 429 | } 430 | 431 | .public .doc { 432 | margin: 0; 433 | } 434 | 435 | .namespace-index .doc { 436 | margin-bottom: 20px; 437 | } 438 | 439 | .namespace-index .namespace .doc { 440 | margin-bottom: 10px; 441 | } 442 | 443 | .markdown p, .markdown li, .markdown dt, .markdown dd, .markdown td { 444 | line-height: 22px; 445 | } 446 | 447 | .markdown li { 448 | padding: 2px 0; 449 | } 450 | 451 | .markdown h2 { 452 | font-weight: normal; 453 | font-size: 25px; 454 | margin: 30px 0 10px 0; 455 | } 456 | 457 | .markdown h3 { 458 | font-weight: normal; 459 | font-size: 20px; 460 | margin: 30px 0 0 0; 461 | } 462 | 463 | .markdown h4 { 464 | font-size: 15px; 465 | margin: 22px 0 -4px 0; 466 | } 467 | 468 | .doc, .public, .namespace .index { 469 | max-width: 680px; 470 | overflow-x: visible; 471 | } 472 | 473 | .markdown pre > code { 474 | display: block; 475 | padding: 10px; 476 | } 477 | 478 | .markdown pre > code, .src-link a { 479 | border: 1px solid #e4e4e4; 480 | border-radius: 2px; 481 | } 482 | 483 | .markdown code:not(.hljs), .src-link a { 484 | background: #f6f6f6; 485 | } 486 | 487 | pre.deps { 488 | display: inline-block; 489 | margin: 0 10px; 490 | border: 1px solid #e4e4e4; 491 | border-radius: 2px; 492 | padding: 10px; 493 | background-color: #f6f6f6; 494 | } 495 | 496 | .markdown hr { 497 | border-style: solid; 498 | border-top: none; 499 | color: #ccc; 500 | } 501 | 502 | .doc ul, .doc ol { 503 | padding-left: 30px; 504 | } 505 | 506 | .doc table { 507 | border-collapse: collapse; 508 | margin: 0 10px; 509 | } 510 | 511 | .doc table td, .doc table th { 512 | border: 1px solid #dddddd; 513 | padding: 4px 6px; 514 | } 515 | 516 | .doc table th { 517 | background: #f2f2f2; 518 | } 519 | 520 | .doc dl { 521 | margin: 0 10px 20px 10px; 522 | } 523 | 524 | .doc dl dt { 525 | font-weight: bold; 526 | margin: 0; 527 | padding: 3px 0; 528 | border-bottom: 1px solid #ddd; 529 | } 530 | 531 | .doc dl dd { 532 | padding: 5px 0; 533 | margin: 0 0 5px 10px; 534 | } 535 | 536 | .doc abbr { 537 | border-bottom: 1px dotted #333; 538 | font-variant: none; 539 | cursor: help; 540 | } 541 | 542 | .src-link { 543 | margin-bottom: 15px; 544 | } 545 | 546 | .src-link a { 547 | font-size: 70%; 548 | padding: 1px 4px; 549 | text-decoration: none; 550 | color: #5555bb; 551 | } 552 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | github.com style (c) Vasily Polovnyov 3 | */ 4 | 5 | .hljs { 6 | display: block; 7 | overflow-x: auto; 8 | padding: 0.5em; 9 | color: #333; 10 | background: #f8f8f8; 11 | } 12 | 13 | .hljs-comment, 14 | .hljs-quote { 15 | color: #998; 16 | font-style: italic; 17 | } 18 | 19 | .hljs-keyword, 20 | .hljs-selector-tag, 21 | .hljs-subst { 22 | color: #333; 23 | font-weight: bold; 24 | } 25 | 26 | .hljs-number, 27 | .hljs-literal, 28 | .hljs-variable, 29 | .hljs-template-variable, 30 | .hljs-tag .hljs-attr { 31 | color: #008080; 32 | } 33 | 34 | .hljs-string, 35 | .hljs-doctag { 36 | color: #d14; 37 | } 38 | 39 | .hljs-title, 40 | .hljs-section, 41 | .hljs-selector-id { 42 | color: #900; 43 | font-weight: bold; 44 | } 45 | 46 | .hljs-subst { 47 | font-weight: normal; 48 | } 49 | 50 | .hljs-type, 51 | .hljs-class .hljs-title { 52 | color: #458; 53 | font-weight: bold; 54 | } 55 | 56 | .hljs-tag, 57 | .hljs-name, 58 | .hljs-attribute { 59 | color: #000080; 60 | font-weight: normal; 61 | } 62 | 63 | .hljs-regexp, 64 | .hljs-link { 65 | color: #009926; 66 | } 67 | 68 | .hljs-symbol, 69 | .hljs-bullet { 70 | color: #990073; 71 | } 72 | 73 | .hljs-built_in, 74 | .hljs-builtin-name { 75 | color: #0086b3; 76 | } 77 | 78 | .hljs-meta { 79 | color: #999; 80 | font-weight: bold; 81 | } 82 | 83 | .hljs-deletion { 84 | background: #fdd; 85 | } 86 | 87 | .hljs-addition { 88 | background: #dfd; 89 | } 90 | 91 | .hljs-emphasis { 92 | font-style: italic; 93 | } 94 | 95 | .hljs-strong { 96 | font-weight: bold; 97 | } 98 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 3 | Organa 0.0.1-SNAPSHOT

Organa 0.0.1-SNAPSHOT

Released under the MIT

An org-mode-based blogging engine.

Installation

To install, add the following dependency to your project or build file:

[organa "0.0.1-SNAPSHOT"]

Namespaces

organa.artworks

Experimental feature for showing artworks.

Public variables and functions:

organa.config

Generate a map of configuration values.

Public variables and functions:

organa.core

Public variables and functions:

organa.egg

Public variables and functions:

organa.fs

Functions for interacting with files, paths, and directories in the
4 | file system.

Public variables and functions:

organa.gallery

Functions for implementing galleries of images.

Public variables and functions:

organa.html

Implementations of the parts of HTML needed by
5 | this blog engine.

organa.pages

Public variables and functions:

organa.rss

Public variables and functions:

-------------------------------------------------------------------------------- /docs/js/highlight.min.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v9.6.0 | BSD3 License | git.io/hljslicense */ 2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/[&<>]/gm,function(e){return I[e]})}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return R(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||R(i))return i}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){l+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):E(a.k).forEach(function(e){c(e,a.k[e])}),a.k=u}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"===e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function l(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function g(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function h(e,n,t,r){var a=r?"":y.classPrefix,i='',i+n+o}function p(){var e,t,r,a;if(!E.k)return n(B);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(B);r;)a+=n(B.substr(t,r.index-t)),e=g(E,r),e?(M+=e[1],a+=h(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(B);return a+n(B.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!x[E.sL])return n(B);var t=e?l(E.sL,B,!0,L[E.sL]):f(B,E.sL.length?E.sL:void 0);return E.r>0&&(M+=t.r),e&&(L[E.sL]=t.top),h(t.language,t.value,!1,!0)}function b(){k+=null!=E.sL?d():p(),B=""}function v(e){k+=e.cN?h(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(B+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?B+=n:(t.eB&&(B+=n),b(),t.rB||t.eB||(B=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?B+=n:(a.rE||a.eE||(B+=n),b(),a.eE&&(B=n));do E.cN&&(k+=C),E.skip||(M+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return B+=n,n.length||1}var N=R(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var w,E=i||N,L={},k="";for(w=E;w!==N;w=w.parent)w.cN&&(k=h(w.cN,"",!0)+k);var B="",M=0;try{for(var I,j,O=0;;){if(E.t.lastIndex=O,I=E.t.exec(t),!I)break;j=m(t.substr(O,I.index-O),I[0]),O=I.index+j}for(m(t.substr(O)),w=E;w.parent;w=w.parent)w.cN&&(k+=C);return{r:M,value:k,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function f(e,t){t=t||y.languages||E(x);var r={r:0,value:n(e)},a=r;return t.filter(R).forEach(function(n){var t=l(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function g(e){return y.tabReplace||y.useBR?e.replace(M,function(e,n){return y.useBR&&"\n"===e?"
":y.tabReplace?n.replace(/\t/g,y.tabReplace):void 0}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function p(e){var n,t,r,o,s,p=i(e);a(p)||(y.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,s=n.textContent,r=p?l(p,s,!0):f(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=g(r.value),e.innerHTML=r.value,e.className=h(e.className,p,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function d(e){y=o(y,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");w.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function N(){return E(x)}function R(e){return e=(e||"").toLowerCase(),x[e]||x[L[e]]}var w=[],E=Object.keys,x={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",y={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},I={"&":"&","<":"<",">":">"};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=g,e.highlightBlock=p,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=R,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("clojure",function(e){var t={"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},r="a-zA-Z_\\-!.?+*=<>&#'",n="["+r+"]["+r+"0-9/;:]*",a="[-+]?\\d+(\\.\\d+)?",o={b:n,r:0},s={cN:"number",b:a,r:0},i=e.inherit(e.QSM,{i:null}),c=e.C(";","$",{r:0}),d={cN:"literal",b:/\b(true|false|nil)\b/},l={b:"[\\[\\{]",e:"[\\]\\}]"},m={cN:"comment",b:"\\^"+n},p=e.C("\\^\\{","\\}"),u={cN:"symbol",b:"[:]{1,2}"+n},f={b:"\\(",e:"\\)"},h={eW:!0,r:0},y={k:t,l:n,cN:"name",b:n,starts:h},b=[f,i,m,p,c,u,l,s,d,o];return f.c=[e.C("comment",""),y,h],h.c=b,l.c=b,{aliases:["clj"],i:/\S/,c:[f,i,m,p,c,u,l,s,d]}});hljs.registerLanguage("clojure-repl",function(e){return{c:[{cN:"meta",b:/^([\w.-]+|\s*#_)=>/,starts:{e:/$/,sL:"clojure"}}]}}); -------------------------------------------------------------------------------- /docs/js/page_effects.js: -------------------------------------------------------------------------------- 1 | function visibleInParent(element) { 2 | var position = $(element).position().top 3 | return position > -50 && position < ($(element).offsetParent().height() - 50) 4 | } 5 | 6 | function hasFragment(link, fragment) { 7 | return $(link).attr("href").indexOf("#" + fragment) != -1 8 | } 9 | 10 | function findLinkByFragment(elements, fragment) { 11 | return $(elements).filter(function(i, e) { return hasFragment(e, fragment)}).first() 12 | } 13 | 14 | function scrollToCurrentVarLink(elements) { 15 | var elements = $(elements); 16 | var parent = elements.offsetParent(); 17 | 18 | if (elements.length == 0) return; 19 | 20 | var top = elements.first().position().top; 21 | var bottom = elements.last().position().top + elements.last().height(); 22 | 23 | if (top >= 0 && bottom <= parent.height()) return; 24 | 25 | if (top < 0) { 26 | parent.scrollTop(parent.scrollTop() + top); 27 | } 28 | else if (bottom > parent.height()) { 29 | parent.scrollTop(parent.scrollTop() + bottom - parent.height()); 30 | } 31 | } 32 | 33 | function setCurrentVarLink() { 34 | $('.secondary a').parent().removeClass('current') 35 | $('.anchor'). 36 | filter(function(index) { return visibleInParent(this) }). 37 | each(function(index, element) { 38 | findLinkByFragment(".secondary a", element.id). 39 | parent(). 40 | addClass('current') 41 | }); 42 | scrollToCurrentVarLink('.secondary .current'); 43 | } 44 | 45 | var hasStorage = (function() { try { return localStorage.getItem } catch(e) {} }()) 46 | 47 | function scrollPositionId(element) { 48 | var directory = window.location.href.replace(/[^\/]+\.html$/, '') 49 | return 'scroll::' + $(element).attr('id') + '::' + directory 50 | } 51 | 52 | function storeScrollPosition(element) { 53 | if (!hasStorage) return; 54 | localStorage.setItem(scrollPositionId(element) + "::x", $(element).scrollLeft()) 55 | localStorage.setItem(scrollPositionId(element) + "::y", $(element).scrollTop()) 56 | } 57 | 58 | function recallScrollPosition(element) { 59 | if (!hasStorage) return; 60 | $(element).scrollLeft(localStorage.getItem(scrollPositionId(element) + "::x")) 61 | $(element).scrollTop(localStorage.getItem(scrollPositionId(element) + "::y")) 62 | } 63 | 64 | function persistScrollPosition(element) { 65 | recallScrollPosition(element) 66 | $(element).scroll(function() { storeScrollPosition(element) }) 67 | } 68 | 69 | function sidebarContentWidth(element) { 70 | var widths = $(element).find('.inner').map(function() { return $(this).innerWidth() }) 71 | return Math.max.apply(Math, widths) 72 | } 73 | 74 | function calculateSize(width, snap, margin, minimum) { 75 | if (width == 0) { 76 | return 0 77 | } 78 | else { 79 | return Math.max(minimum, (Math.ceil(width / snap) * snap) + (margin * 2)) 80 | } 81 | } 82 | 83 | function resizeSidebars() { 84 | var primaryWidth = sidebarContentWidth('.primary') 85 | var secondaryWidth = 0 86 | 87 | if ($('.secondary').length != 0) { 88 | secondaryWidth = sidebarContentWidth('.secondary') 89 | } 90 | 91 | // snap to grid 92 | primaryWidth = calculateSize(primaryWidth, 32, 13, 160) 93 | secondaryWidth = calculateSize(secondaryWidth, 32, 13, 160) 94 | 95 | $('.primary').css('width', primaryWidth) 96 | $('.secondary').css('width', secondaryWidth).css('left', primaryWidth + 1) 97 | 98 | if (secondaryWidth > 0) { 99 | $('#content').css('left', primaryWidth + secondaryWidth + 2) 100 | } 101 | else { 102 | $('#content').css('left', primaryWidth + 1) 103 | } 104 | } 105 | 106 | $(window).ready(resizeSidebars) 107 | $(window).ready(setCurrentVarLink) 108 | $(window).ready(function() { persistScrollPosition('.primary')}) 109 | $(window).ready(function() { 110 | $('#content').scroll(setCurrentVarLink) 111 | $(window).resize(setCurrentVarLink) 112 | }) 113 | -------------------------------------------------------------------------------- /docs/organa.artworks.html: -------------------------------------------------------------------------------- 1 | 3 | organa.artworks documentation

organa.artworks

4 | Experimental feature for showing artworks.
5 | 

spit-out-artworks-pages!

(spit-out-artworks-pages! css)
-------------------------------------------------------------------------------- /docs/organa.config.html: -------------------------------------------------------------------------------- 1 | 3 | organa.config documentation

organa.config

4 | Generate a map of configuration values.
5 | 
6 | This should eventually be more configurable.
7 | 
-------------------------------------------------------------------------------- /docs/organa.core.html: -------------------------------------------------------------------------------- 1 | 3 | organa.core documentation

organa.core

-main

(-main)
-------------------------------------------------------------------------------- /docs/organa.dates.html: -------------------------------------------------------------------------------- 1 | 3 | organa.dates documentation

organa.dates

article-date-format

Date format used when rendering blog posts
 4 | 

current-year

(current-year)

Current year (at the time the program is being run).

5 |

date->year

(date->year d)

Find year for a clj-time time (Joda DateTime).

6 |

date-for-file-by-os

(date-for-file-by-os path)

Get “update” date and time in milliseconds for a file on the file system.

7 |

Useful links: https://github.com/juxt/yada/blob/master/src/yada/resources/file_resource.clj http://stackoverflow.com/questions/2723838/
8 | determine-file-creation-date-in-java

9 |

date-for-org-file

(date-for-org-file site-source-dir basename)

Find Joda DateTime for an Org post in HTML format, first looking in the Org Mode header, and if not present there, using the filesystem time.

10 |
-------------------------------------------------------------------------------- /docs/organa.egg.html: -------------------------------------------------------------------------------- 1 | 3 | organa.egg documentation

organa.egg

easter-egg

(easter-egg)
4 | Generate a (potentially random) "easter egg" to embed as a comment
5 | in blog post HTML.
6 | 
-------------------------------------------------------------------------------- /docs/organa.files.html: -------------------------------------------------------------------------------- 1 | 3 | organa.files documentation

organa.files

basename

(basename f)

files-in-directory

(files-in-directory pathstr & {:keys [as], :or {as :file}})
 4 | Get list of (non-hidden) files in directory. Examples:
 5 | 
 6 |   (files-in-directory "/tmp")
 7 |   (files-in-directory "/tmp" :as :str)
 8 |   (files-in-directory "/tmp" :as :file)
 9 | 

splitext

(splitext f)
10 | Split non-extension portion of file name from extension.
11 | 
12 |   (splitext "a") ;;=> '("a" nil)
13 |   (splitext "a.b") ;;=> '("a" "b")
14 |   (splitext "a.b.c") ;;=> '("a.b" "c")
15 | 
-------------------------------------------------------------------------------- /docs/organa.fs.html: -------------------------------------------------------------------------------- 1 | 3 | organa.fs documentation

organa.fs

 4 | Functions for interacting with files, paths, and directories in the
 5 | file system.
 6 | 

basename

(basename f)
 7 | Get file (not directory) portion of name of `f`, where `f` is a
 8 | `File` or path string.
 9 | 

dirfile

(dirfile f)
10 | Find the directory File that contains File `f`, which must exist.
11 | 

files-in-directory

(files-in-directory pathstr & {:keys [as], :or {as :file}})
12 | Get list of (non-hidden) files in directory. Examples:
13 | 
14 |     (files-in-directory "/tmp")
15 |     (files-in-directory "/tmp" :as :str)
16 |     (files-in-directory "/tmp" :as :file)
17 | 

path

(path & args)
18 | Create a file or directory path (string) out of `args`.
19 | 
20 |     (path "/a" "b" "c.txt")
21 |     ;;=>
22 |     "a/b/c.txt"
23 | 

splitext

(splitext f)
24 | Split non-extension portion of file name from extension.
25 | 
26 |   (splitext "a") ;;=> '("a" nil)
27 |   (splitext "a.b") ;;=> '("a" "b")
28 |   (splitext "a.b.c") ;;=> '("a.b" "c")
29 | 

with-tmp-dir

macro

(with-tmp-dir dir-file & body)
30 | Create temporary file, bind to `dir-file`, and execute `body` in
31 | that context, removing the directory afterwards.
32 | 
-------------------------------------------------------------------------------- /docs/organa.gallery.html: -------------------------------------------------------------------------------- 1 | 3 | organa.gallery documentation

organa.gallery

4 | Functions for implementing galleries of images.
5 | 
-------------------------------------------------------------------------------- /docs/organa.html.html: -------------------------------------------------------------------------------- 1 | 3 | organa.html documentation

organa.html

4 | Implementations of the parts of HTML needed by
5 | this blog engine.
6 | 

a

(a content)(a attrs content)

div

(div content)(div attrs content)

em

(em content)(em attrs content)

h1

(h1 content)(h1 attrs content)

h2

(h2 content)(h2 attrs content)

img

(img content)(img attrs content)

li

(li content)(li attrs content)

meta-tag

(meta-tag content)(meta-tag attrs content)

p

(p content)(p attrs content)

parse-org-html

(parse-org-html html-text)(parse-org-html source-dir basename)

pre

(pre content)(pre attrs content)

script

(script content)(script attrs content)

span

(span content)(span attrs content)

strong

(strong content)(strong attrs content)

style

(style content)(style attrs content)

table

(table content)(table attrs content)

tbody

(tbody content)(tbody attrs content)

td

(td content)(td attrs content)

th

(th content)(th attrs content)

thead

(thead content)(thead attrs content)

tr

(tr content)(tr attrs content)

ul

(ul content)(ul attrs content)
-------------------------------------------------------------------------------- /docs/organa.image.html: -------------------------------------------------------------------------------- 1 | 3 | organa.image documentation

organa.image

create-thumbnail!

(create-thumbnail! maxh orig-path thumb-path)(create-thumbnail! orig-path thumb-path)

image-extensions

image-file-pattern

4 | image-file-pattern
5 | ;;=>
6 | #"\.PNG|\.JPG|\.JPEG|\.GIF|\.png|\.jpg|\.jpeg|\.gif"
7 | 

max-height

-------------------------------------------------------------------------------- /docs/organa.io.html: -------------------------------------------------------------------------------- 1 | 3 | organa.io documentation

organa.io

dirfile

(dirfile f)

path

(path & args)
-------------------------------------------------------------------------------- /docs/organa.mirror.html: -------------------------------------------------------------------------------- 1 | 3 | organa.mirror documentation

organa.mirror

->html

(->html posts)

abbrev

(abbrev s)

epoch-secs->local-date-time

(epoch-secs->local-date-time n)

parse-file

(parse-file txt)

post->enl

(post->enl {:keys [url]})

preview-file

(preview-file fname)

read-temp-posts-file

(read-temp-posts-file)

relevant-data

(relevant-data m)
-------------------------------------------------------------------------------- /docs/organa.pages.html: -------------------------------------------------------------------------------- 1 | 3 | organa.pages documentation

organa.pages

blog-body

(blog-body)

home-body

(home-body)
-------------------------------------------------------------------------------- /docs/organa.parse.html: -------------------------------------------------------------------------------- 1 | 3 | organa.parse documentation

organa.parse

clean-empty-string-values

(clean-empty-string-values m)

empty-org-table-entry?

(empty-org-table-entry? s)

parsed-org-html->table-metadata

(parsed-org-html->table-metadata parsed-html)

title-for-org-file

(title-for-org-file parsed-html)
-------------------------------------------------------------------------------- /docs/organa.rss.html: -------------------------------------------------------------------------------- 1 | 3 | organa.rss documentation

organa.rss

make-rss-feeds

(make-rss-feeds topic rss-file-name org-files)(make-rss-feeds rss-file-name org-files)

Create RSS feeds for blog.

4 |

FIXME: reduce arity FIXME: make more configurable

5 |
-------------------------------------------------------------------------------- /organa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eigenhombre/organa/b030f348df00bd7c0b5b189a9c1260afceb0faba/organa.png -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject organa "0.0.1-SNAPSHOT" 2 | :description "An org-mode-based blogging engine" 3 | :url "https://github.com/eigenhombre/organa" 4 | :license {:name "MIT"} 5 | :dependencies [[org.clojure/clojure "1.10.3"] 6 | [cheshire "5.10.1"] 7 | [clj-rss "0.3.0"] 8 | [clj-time "0.15.2"] 9 | [clj-yaml "0.4.0"] 10 | [clojure.java-time "0.3.3"] 11 | [enlive "1.1.6"] 12 | [environ "1.2.0"] 13 | [garden "1.3.10"] 14 | [hiccup "1.0.5"] 15 | [me.raynes/fs "1.4.6"] 16 | [mount "0.1.16"] 17 | [net.mikera/imagez "0.12.0"]] 18 | :target-path "target/%s" 19 | :uberjar-name "organa.jar" 20 | :aliases {"kaocha" ["with-profile" "+kaocha" "run" "-m" "kaocha.runner"]} 21 | :profiles {:uberjar {:aot :all} 22 | :kaocha {:dependencies [[lambdaisland/kaocha "1.60.945"]]} 23 | :dev {:plugins [[lein-bikeshed "0.5.2"] 24 | [lein-ancient "1.0.0-RC3"] 25 | [lein-codox "0.10.8"] 26 | [jonase/eastwood "0.9.9"] 27 | [lein-kibit "0.1.8"]]}} 28 | :codox {:output-path "docs" 29 | :source-uri 30 | {#".*" 31 | "https://github.com/eigenhombre/organa/blob/master/{filepath}#L{line}"}} 32 | :main ^:skip-aot organa.core) 33 | -------------------------------------------------------------------------------- /resources/banner-format.txt: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/organa/artworks.clj: -------------------------------------------------------------------------------- 1 | (ns organa.artworks 2 | " 3 | Experimental feature for showing artworks. 4 | " 5 | (:require [clj-yaml.core :as yaml] 6 | [clojure.java.io :as io] 7 | [clojure.pprint :as pprint] 8 | [clojure.string :as string] 9 | [hiccup.core :as hiccup] 10 | [mikera.image.core :as image] 11 | [organa.config :as config] 12 | [organa.html :as html] 13 | [organa.image :as img] 14 | [organa.fs :as fs] 15 | [organa.parse :as parse]) 16 | (:import [java.io File])) 17 | 18 | ;; EXPERIMENTAL 19 | 20 | ;; FIXME: DRY vs. core.clj (move to config.clj after creating that) 21 | (def ^:private site-source-dir (:site-source-dir config/config)) 22 | (def ^:private image-file-extensions #{"png" "gif" "jpg" "jpeg"}) 23 | (def ^:private artworks-dir (str site-source-dir "/artworks")) 24 | (def ^:private target-dir (str (:target-dir config/config) 25 | "/artworks")) 26 | (def ^:private gallery-html (str target-dir "/index.html")) 27 | 28 | (defn ^:private artworks-dirs [] 29 | {:post [(seq %) 30 | (every? clojure.java.io/file %)]} 31 | (filter #(.isDirectory ^File %) 32 | (.listFiles ^File (clojure.java.io/file artworks-dir)))) 33 | 34 | (defn ^:private artwork-file? [^File f] 35 | {:pre [(= File (type f))]} 36 | (-> f 37 | .getName 38 | (clojure.string/split #"\.") 39 | last 40 | image-file-extensions 41 | boolean)) 42 | 43 | (def ^:private max-thumb-side 600) 44 | 45 | (defn ^:private artwork-meta-path [{:keys [directory]}] 46 | (fs/path directory "meta.html")) 47 | 48 | (defn ^:private artwork-meta-path-yml [{:keys [directory]}] 49 | (fs/path directory "meta.yml")) 50 | 51 | (defn ^:private artwork-html [css {:keys [^File artworks-file] 52 | :as artwork}] 53 | (let [artworks-file-name (.getName artworks-file)] 54 | (hiccup/html 55 | [:html 56 | [:head [:style css]] 57 | [:body 58 | [:div 59 | [:a {:href artworks-file-name} 60 | [:img {:src artworks-file-name 61 | :width 800}]] 62 | [:pre (with-out-str 63 | (pprint/pprint artwork #_(dissoc artwork :meta)))]]]]))) 64 | 65 | (defn ^:private load-meta [artwork] 66 | ;; Try YAML first, then parsed Org->HTML: 67 | (let [meta-yml-path (artwork-meta-path-yml artwork)] 68 | (if (.exists (io/file meta-yml-path)) 69 | (yaml/parse-string (slurp meta-yml-path)) 70 | (let [meta-path (artwork-meta-path artwork) 71 | meta-html (when (.exists (io/file meta-path)) 72 | (slurp meta-path)) 73 | parsed-meta (html/parse-org-html meta-html) 74 | title (parse/title-for-org-file parsed-meta)] 75 | (merge (parse/parsed-org-html->table-metadata parsed-meta) 76 | (when title {:title title})))))) 77 | 78 | (defn ^:private enhance [{:keys [^File directory] :as artwork}] 79 | (let [dirname (.getName directory)] 80 | (-> artwork 81 | (assoc :html-abs-path (fs/path target-dir dirname "index.html") 82 | :html-rel-path (fs/path dirname "index.html")) 83 | (merge (load-meta artwork))))) 84 | 85 | (defn ^:private directory-of-file [f] 86 | (-> f 87 | io/file 88 | fs/dirfile)) 89 | 90 | (defn ^:private write-files! [css {:keys [html-abs-path 91 | ^File artworks-file] 92 | :as artwork}] 93 | (io/make-parents html-abs-path) 94 | (spit html-abs-path (artwork-html css artwork)) 95 | (io/copy artworks-file 96 | (-> html-abs-path 97 | directory-of-file 98 | (fs/path (.getName artworks-file)) 99 | io/file)) 100 | artwork) 101 | 102 | (defn ^:private artworks-for-dir [^File d] 103 | (let [artworks-file (first (filter artwork-file? 104 | (.listFiles d))) 105 | image-object (image/load-image artworks-file) 106 | height (image/height image-object) 107 | width (image/width image-object) 108 | ratio (/ width height) 109 | thumb-width (if (> width height) 110 | max-thumb-side 111 | (int (/ max-thumb-side ratio))) 112 | thumb-height (if (> width height) 113 | (int (/ max-thumb-side ratio)) 114 | max-thumb-side)] 115 | {:directory d 116 | :artworks-file artworks-file 117 | :image-object image-object 118 | :height height 119 | :width width 120 | :thumb-width thumb-width 121 | :thumb-height thumb-height})) 122 | 123 | (defn ^:private thumb-name [s] 124 | (let [[prefix _] (fs/splitext s)] 125 | (str prefix "-thumb.png"))) 126 | 127 | (defn ^:private artworks-html [css recs] 128 | (hiccup/html 129 | [:html 130 | [:head [:style css]] 131 | [:body 132 | [:div.margined 133 | [:table 134 | (for [{:keys [title 135 | year 136 | medium 137 | directory 138 | artworks-file 139 | html-rel-path]} recs] 140 | [:tr 141 | [:td [:div.artworkspic 142 | [:a {:href html-rel-path} 143 | [:img {:src 144 | (fs/path (.getName ^File directory) 145 | (thumb-name 146 | (.getName ^File artworks-file))) 147 | :width 200}]]]] 148 | [:td [:table 149 | [:tr [:td [:em (or title "")]]] 150 | [:tr [:td (->> [medium year] 151 | (remove nil?) 152 | (clojure.string/join ", "))]]]]])]]]])) 153 | 154 | (defn ^:private thumb-path [html-abs-path artworks-file] 155 | (fs/path (directory-of-file html-abs-path) 156 | (thumb-name (str artworks-file)))) 157 | 158 | ;; Could cache or memoize this... but there isn't too much work there, 159 | ;; yet...: 160 | (defn ^:private make-thumb! [{:keys [artworks-file 161 | html-abs-path] :as artwork}] 162 | (let [tp (thumb-path html-abs-path artworks-file)] 163 | (io/make-parents tp) 164 | (img/create-thumbnail! 600 artworks-file tp)) 165 | artwork) 166 | 167 | (defn spit-out-artworks-pages! [css] 168 | (->> (artworks-dirs) 169 | (pmap artworks-for-dir) 170 | (map enhance) 171 | (remove (comp #{"true"} :hidden)) 172 | (pmap make-thumb!) 173 | (map (partial write-files! css)) 174 | (sort-by (comp str :year)) 175 | reverse 176 | (artworks-html css) 177 | (spit gallery-html))) 178 | 179 | (comment 180 | (require '[clojure.java.shell :as shell] 181 | '[garden.core :refer [css] :rename {css to-css}]) 182 | (shell/sh "open" artworks-dir) 183 | 184 | (let [css (to-css (load-file (str site-source-dir "/" "index.garden")))] 185 | (spit-out-artworks-pages! css)) 186 | 187 | (clojure.java.shell/sh "open" gallery-html)) 188 | -------------------------------------------------------------------------------- /src/organa/config.clj: -------------------------------------------------------------------------------- 1 | (ns organa.config 2 | " 3 | Generate a map of configuration values. 4 | 5 | This should eventually be more configurable. 6 | " 7 | (:require [environ.core :as env])) 8 | 9 | (def config 10 | (let [home-dir (env/env :home) 11 | remote-host "johnj.com"] 12 | {:home-dir home-dir 13 | :remote-host remote-host 14 | :site-source-dir (str home-dir "/org/sites/" remote-host) 15 | :target-dir "/tmp/organa"})) 16 | -------------------------------------------------------------------------------- /src/organa/core.clj: -------------------------------------------------------------------------------- 1 | (ns organa.core 2 | (:gen-class) 3 | (:require [clj-time.format :as tformat] 4 | [clojure.java.io :as io] 5 | [clojure.java.shell] 6 | [clojure.pprint :as pprint] 7 | [clojure.string :as str] 8 | [clojure.string :as string] 9 | [clojure.walk] 10 | [environ.core :refer [env]] 11 | [garden.core :refer [css] :rename {css to-css}] 12 | [net.cgrand.enlive-html :as html] 13 | [organa.artworks :as artworks] 14 | [organa.config :refer [config]] 15 | [organa.dates :as dates] 16 | [organa.egg :refer [easter-egg]] 17 | [organa.fs :as fs] 18 | [organa.gallery :as gal] 19 | [organa.html :as h] 20 | [organa.image :as img] 21 | [organa.pages :as pages] 22 | [organa.parse :as parse] 23 | [organa.rss :as rss]) 24 | (:import [java.io File])) 25 | 26 | (defn ^:private remove-newline-strings [coll] 27 | (remove (partial = "\n") coll)) 28 | 29 | (defn ^:private content-remove-newline-strings 30 | " 31 | Remove newlines from `:content` portion of Enlive element `el`. 32 | " 33 | [el] 34 | (update-in el [:content] remove-newline-strings)) 35 | 36 | (defn ^:private execute-organa [m] 37 | (-> m 38 | :content 39 | first 40 | read-string 41 | eval)) 42 | 43 | (defn ^:private footer [] 44 | (h/p {:class "footer"} 45 | [(format "© 2006-%d John Jacobsen." (dates/current-year)) 46 | " Made with " 47 | (h/a {:href "https://github.com/eigenhombre/organa"} ["Organa"]) 48 | "."])) 49 | 50 | ;; Org mode exports changed how tags in section headings are handled... 51 | (defn ^:private tags-for-org-file-via-old-span-tag [parsed-html] 52 | (-> parsed-html 53 | (html/select [:span.tag]) 54 | first 55 | :content 56 | (#(mapcat :content %)))) 57 | 58 | (defn ^:private tags-bracketed-by-colons 59 | " 60 | 61 | (tags-bracketed-by-colons \":foo:baz:\") 62 | ;;=> [\"foo\" \"baz\"] 63 | (tags-bracketed-by-colons \"adfkljhsadf\") 64 | ;;=> nil 65 | " 66 | [s] 67 | (some-> (re-find #"^\:(.+?)\:$" s) 68 | second 69 | (clojure.string/split 70 | #":"))) 71 | 72 | ;; Org mode exports changed how tags in section headings are handled... 73 | (defn ^:private tags-for-org-file-using-h2 [parsed-html] 74 | (->> (html/select parsed-html [:h2]) 75 | (mapcat (comp tags-bracketed-by-colons first :content)) 76 | (remove nil?))) 77 | 78 | ;; Org mode exports changed how tags in section headings are handled... 79 | (defn ^:private tags-for-org-file [parsed-html] 80 | (concat 81 | (tags-for-org-file-via-old-span-tag parsed-html) 82 | (tags-for-org-file-using-h2 parsed-html))) 83 | 84 | (defn ^:private tag-markup [tags] 85 | (interleave 86 | (repeat " ") 87 | (for [t tags] 88 | (h/span {:class (str t "-tag tag")} 89 | [(h/a {:href (str t "-blog.html")} t)])))) 90 | 91 | (defn ^:private articles-nav-section [file-name 92 | available-files 93 | alltags 94 | parsed-org-file-map] 95 | (h/div 96 | `(~(h/a {:name "allposts"} []) 97 | ~(h/h2 {:class "allposts"} ["Blog Posts " 98 | (h/span {:class "postcount"} 99 | (->> available-files 100 | (remove :static?) 101 | (remove :draft?) 102 | count 103 | (format "(%d)")))]) 104 | ~(h/p (concat ["Select from below, " 105 | (h/a {:href "blog.html"} "view all posts") 106 | ", or choose only posts for:"] 107 | (for [tag (sort alltags)] 108 | (h/span {:class (format "%s-tag tag" tag)} 109 | [(h/a {:href (str tag "-blog.html")} 110 | tag) 111 | " "])))) 112 | ~@(when (not= file-name "index") 113 | [(h/hr) 114 | (h/p [(h/a {:href "index.html"} [(h/em ["Home"])])])]) 115 | ~(h/hr) 116 | ~@(for [{:keys [file-name date tags]} 117 | (->> available-files 118 | (remove :static?) 119 | (remove :draft?) 120 | (remove (comp #{file-name} :file-name))) 121 | :let [parsed-html 122 | (->> file-name 123 | parsed-org-file-map 124 | :parsed-html)]] 125 | (when parsed-html 126 | (h/p 127 | (concat 128 | [(h/a {:href (str file-name ".html")} 129 | [(parse/title-for-org-file parsed-html)])] 130 | " " 131 | (tag-markup tags) 132 | [" " 133 | (h/span {:class "article-date"} 134 | [(when date (tformat/unparse dates/article-date-format 135 | date))])])))) 136 | ~@(when (not= file-name "index") 137 | [(h/p [(h/a {:href "index.html"} [(h/em ["Home"])])])]) 138 | ~(rss/rss-links)))) 139 | 140 | (defn ^:private position-of-current-file [file-name available-files] 141 | (->> available-files 142 | (map-indexed vector) 143 | (filter (comp (partial = file-name) :file-name second)) 144 | ffirst)) 145 | 146 | (defn ^:private prev-next-tags [file-name available-files] 147 | (let [files (->> available-files 148 | (remove :static?) 149 | (remove :draft?) 150 | vec) 151 | current-pos (position-of-current-file file-name files) 152 | next-post (get files (dec current-pos)) 153 | prev-post (get files (inc current-pos))] 154 | [(when prev-post 155 | (h/p {:class "prev-next-post"} 156 | [(h/span {:class "post-nav-earlier-later"} 157 | ["Earlier post "]) 158 | (h/a {:href (str "./" (:file-name prev-post) ".html")} 159 | [(:title prev-post)]) 160 | (h/span (tag-markup (:tags prev-post)))])) 161 | (when next-post 162 | [(h/p {:class "prev-next-post"} 163 | [(h/span {:class "post-nav-earlier-later"} ["Later post "]) 164 | (h/a {:href (str "./" (:file-name next-post) ".html")} 165 | [(:title next-post)]) 166 | (h/span (tag-markup (:tags next-post)))])])])) 167 | 168 | (defn ^:private page-header [css] 169 | (let [analytics-id (:google-analytics-tracking-id env) 170 | site-id (:google-analytics-site-id env)] 171 | [(html/html-snippet (easter-egg)) 172 | (h/script {:type "text/javascript" 173 | :src (str "https://cdnjs.cloudflare.com" 174 | "/ajax/libs/mathjax/2.7.2/" 175 | "MathJax.js" 176 | "?config=TeX-MML-AM_CHTML")} 177 | []) 178 | (h/link {:href "./favicon.gif" 179 | :rel "icon" 180 | :type "image/gif"} 181 | []) 182 | 183 | ;; Analytics 184 | (h/script {:type "text/javascript" 185 | :async true 186 | :src 187 | (format "https://www.googletagmanager.com/gtag/js?id=UA-%s-%s" 188 | analytics-id site-id)} 189 | []) 190 | (h/script {:type "text/javascript"} 191 | ["window.dataLayer = window.dataLayer || [];" 192 | "function gtag(){dataLayer.push(arguments);}" 193 | "gtag('js', new Date());" 194 | (format "gtag('config', 'UA-%s-%s');" analytics-id site-id)]) 195 | (h/style css)])) 196 | 197 | (defn ^:private transform-enlive [file-name 198 | date 199 | available-files 200 | parsed-org-file-map 201 | tags 202 | alltags 203 | css 204 | static? 205 | draft? 206 | enl] 207 | (let [prev-next-tags (when-not (or static? draft?) 208 | (prev-next-tags file-name available-files)) 209 | nav-section (articles-nav-section file-name 210 | available-files 211 | alltags 212 | parsed-org-file-map)] 213 | (html/at enl 214 | [:head :style] nil 215 | [:head :script] nil 216 | [:div#postamble] nil 217 | ;; Old org mode: 218 | ;; Remove dummy header lines containting tags, in first 219 | ;; sections: 220 | [:h2#sec-1] (fn [thing] 221 | (when-not (-> thing 222 | :content 223 | second 224 | :attrs 225 | :class 226 | (= "tag")) 227 | thing)) 228 | ;; New org mode: 229 | ;; Remove dummy header lines containing tags: 230 | [:h2] (fn [thing] 231 | (when-not (-> thing 232 | :content 233 | first 234 | tags-bracketed-by-colons) 235 | thing)) 236 | [:body] content-remove-newline-strings 237 | [:ul] content-remove-newline-strings 238 | [:html] content-remove-newline-strings 239 | [:head] content-remove-newline-strings 240 | [:head] (html/append (page-header css)) 241 | [:pre.src-organa] execute-organa 242 | [:div#content :h1.title] 243 | (html/after 244 | `[~@(concat 245 | [(when-not static? 246 | (tag-markup (remove #{"static" "draft"} tags))) 247 | (h/p [(h/span {:class "author"} [(h/a {:href "index.html"} 248 | ["John Jacobsen"])]) 249 | (h/br) 250 | (h/span {:class "article-header-date"} 251 | [(tformat/unparse dates/article-date-format 252 | date)])]) 253 | (h/p [(h/a {:href "index.html"} [(h/strong ["Home"])]) 254 | " " 255 | (h/a {:href "blog.html"} ["Other Posts"])])] 256 | prev-next-tags 257 | [(h/div {:class "hspace"} [])])]) 258 | [:div#content] (html/append 259 | (h/div {:class "hspace"} []) 260 | prev-next-tags 261 | nav-section 262 | (footer))))) 263 | 264 | (defn ^:private as-string [x] 265 | (with-out-str 266 | (pprint/pprint x))) 267 | 268 | (defn ^:private process-html-file! [{:keys [target-dir]} 269 | {:keys [file-name date static? draft? 270 | parsed-html tags unparsed-html] :as r} 271 | available-files 272 | alltags 273 | css 274 | parsed-org-file-map] 275 | (->> parsed-html 276 | (transform-enlive file-name 277 | date 278 | available-files 279 | parsed-org-file-map 280 | tags 281 | alltags 282 | css 283 | static? 284 | draft?) 285 | html/emit* ;; turn back into html 286 | (apply str) 287 | (spit (str target-dir "/" file-name ".html"))) 288 | (->> r 289 | as-string 290 | (spit (str target-dir "/" file-name ".edn")))) 291 | 292 | (defn ^:private html-file-exists [org-file-name] 293 | (-> org-file-name 294 | (clojure.string/replace #"\.org$" ".html") 295 | clojure.java.io/file 296 | .exists)) 297 | 298 | (defn ^:private available-org-files [site-source-dir] 299 | (->> (fs/files-in-directory site-source-dir :as :file) 300 | (filter (comp #(.endsWith ^String % ".org") str)) 301 | (filter html-file-exists) 302 | (remove (comp #(.contains ^String % ".#") str)) 303 | (map #(.getName ^File %)) 304 | (map #(.substring ^String % 0 (.lastIndexOf ^String % "."))))) 305 | 306 | (defn ^:private sh [& cmds] 307 | (apply clojure.java.shell/sh 308 | (clojure.string/split (string/join cmds) #"\s+"))) 309 | 310 | (defn ^:private ensure-target-dir-exists! [target-dir] 311 | ;; FIXME: do it the Java way 312 | (sh "mkdir -p " target-dir)) 313 | 314 | (defn ^:private stage-site-image-files! [site-source-dir target-dir] 315 | ;; FIXME: avoid bash hack? 316 | (doseq [f (filter (partial re-find img/image-file-pattern) 317 | (fs/files-in-directory site-source-dir :as :str))] 318 | (sh "cp -p " f " " target-dir))) 319 | 320 | (defn ^:private stage-site-static-files! [site-source-dir target-dir] 321 | (println "Syncing files in static directory...") 322 | (apply clojure.java.shell/sh 323 | (clojure.string/split 324 | (format "rsync -vurt %s/static %s/galleries %s/artworks %s" 325 | site-source-dir 326 | site-source-dir 327 | site-source-dir 328 | target-dir) 329 | #" "))) 330 | 331 | ;; FIXME: Hack-y? 332 | (def ^:private base-enlive-snippet 333 | (html/html-snippet "")) 334 | 335 | (defn ^:private galleries-path [site-source-dir] 336 | (str site-source-dir "/galleries")) 337 | 338 | (defn ^:private generate-thumbnails-for-gallery! [galpath imagefiles] 339 | (doseq [img (remove #(.contains ^String % "-thumb") 340 | imagefiles) 341 | :let [[base _] (fs/splitext img) 342 | thumb-path (format "%s/%s-thumb.png" galpath base) 343 | orig-path (format "%s/%s" galpath img)]] 344 | (when-not (.exists (io/file thumb-path)) 345 | (printf "Creating thumbnail file %s from %s...\n" 346 | thumb-path 347 | orig-path) 348 | (img/create-thumbnail! orig-path thumb-path)))) 349 | 350 | (defn ^:private generate-thumbnails-in-galleries! [site-source-dir] 351 | (let [galleries-dir (galleries-path site-source-dir)] 352 | (doseq [galpath (fs/files-in-directory galleries-dir :as :str) 353 | :let [imagefiles (gal/gallery-images galpath)]] 354 | (printf "Making thumbnails for gallery '%s'\n" (fs/basename galpath)) 355 | (generate-thumbnails-for-gallery! galpath imagefiles)))) 356 | 357 | (defn ^:private gallery-html [css galfiles] 358 | (->> (html/at base-enlive-snippet 359 | [:head] (html/append (page-header css)) 360 | [:body] 361 | (html/append 362 | [(h/div {:class "gallery"} 363 | (for [f galfiles 364 | :let [[base _] (fs/splitext f) 365 | thumb-path (format "%s-thumb.png" base)]] 366 | (h/a {:href (str "./" f)} 367 | [(h/img {:src (str "./" thumb-path) 368 | :height "250px"} 369 | [])])))])) 370 | (html/emit*) 371 | (apply str))) 372 | 373 | (defn ^:private generate-html-for-galleries! [site-source-dir css] 374 | (let [galleries-dir (galleries-path site-source-dir)] 375 | (doseq [galpath (fs/files-in-directory galleries-dir :as :str) 376 | :let [galfiles (gal/gallery-images galpath)]] 377 | (printf "Making gallery '%s'\n" (fs/basename galpath)) 378 | (spit (str galpath "/index.html") 379 | (gallery-html css galfiles))))) 380 | 381 | (defn ^:private wait-futures [futures] 382 | (loop [cnt 0] 383 | (let [new-cnt (count (filter realized? futures))] 384 | (when-not (every? realized? futures) 385 | (Thread/sleep 500) 386 | (if (= cnt new-cnt) 387 | (recur cnt) 388 | (do 389 | (println new-cnt "completed...") 390 | (recur new-cnt)))))) 391 | (doseq [fu futures] 392 | (try 393 | (deref fu) 394 | (catch Throwable t 395 | (println t))))) 396 | 397 | (defn ^:private emit-html-to-file [target-dir file-name enlive-tree] 398 | (->> enlive-tree 399 | (html/emit*) 400 | (cons "\n") 401 | (apply str) 402 | (spit (str target-dir "/" file-name)))) 403 | 404 | (defn ^:private make-old-home-page [{:keys [target-dir]} 405 | org-files 406 | parsed-org-file-map 407 | tags 408 | css] 409 | (emit-html-to-file 410 | target-dir 411 | "index-old.html" 412 | (html/at base-enlive-snippet 413 | [:head] (html/append (page-header css)) 414 | [:body] (html/append 415 | [(pages/home-body) 416 | ;;(div {:class "hspace"} []) 417 | ]) 418 | [:div#blogposts] (html/append 419 | [(articles-nav-section "index" 420 | org-files 421 | tags 422 | parsed-org-file-map) 423 | (footer)])))) 424 | 425 | (defn ^:private make-blog-page 426 | ([config org-files parsed-org-file-map tags css] 427 | (make-blog-page config :all org-files parsed-org-file-map tags css)) 428 | ([{:keys [target-dir]} tag org-files parsed-org-file-map tags css] 429 | (println (format "Making blog page for tag '%s'" tag)) 430 | (let [tag-posts (if (= tag :all) 431 | org-files 432 | (filter (comp (partial some #{tag}) :tags) org-files))] 433 | (emit-html-to-file 434 | target-dir 435 | (str (if (= tag :all) "" (str tag "-")) 436 | "blog.html") 437 | (html/at base-enlive-snippet 438 | [:head] (html/append (page-header css)) 439 | [:body] (html/append 440 | [(pages/blog-body) 441 | ;;(div {:class "hspace"} []) 442 | ]) 443 | [:div#blogposts] (html/append 444 | [(articles-nav-section "blog" 445 | tag-posts 446 | tags 447 | parsed-org-file-map) 448 | (footer)])))))) 449 | 450 | (defn ^:private remove-gnu-junk [snippet] 451 | (html/at snippet 452 | [:script] nil 453 | [:style] nil)) 454 | 455 | (defn ^:private files->parsed [files-to-process] 456 | (let [ssd (:site-source-dir config)] 457 | (into {} 458 | (for [f files-to-process] 459 | (let [parsed-html (->> f 460 | (h/parse-org-html ssd) 461 | remove-gnu-junk) 462 | tags (tags-for-org-file parsed-html)] 463 | [f 464 | ;; FIXME: It's hacky to have f in both places 465 | {:file-name f 466 | ;; FIXME: don't re-parse for dates! 467 | :date (dates/date-for-org-file ssd f) 468 | :title (parse/title-for-org-file parsed-html) 469 | :tags tags 470 | :parsed-html parsed-html 471 | :unparsed-html (apply str (html/emit* parsed-html)) 472 | :static? (some #{"static"} tags) 473 | :draft? (some #{"draft"} tags)}]))))) 474 | 475 | (defn ^:private proof-files-to-process [all-org-files] 476 | (->> all-org-files 477 | (sort-by (fn [fname] 478 | (->> (str fname ".html") 479 | (str (:site-source-dir config) "/") 480 | (dates/date-for-file-by-os)))) 481 | reverse 482 | (take 10))) 483 | 484 | (defn ^:private generate-site [{:keys [target-dir proof?] :as config}] 485 | (println "The party commences....") 486 | (ensure-target-dir-exists! target-dir) 487 | (let [ssd (:site-source-dir config) 488 | css (->> (str ssd "/index.garden") 489 | load-file 490 | to-css) 491 | all-org-files (available-org-files ssd) 492 | files-to-process (if-not proof? 493 | all-org-files 494 | (proof-files-to-process all-org-files)) 495 | _ (println (format "Parsing %d HTML'ed Org files..." 496 | (count files-to-process))) 497 | parsed-org-file-map (files->parsed files-to-process) 498 | org-files (->> parsed-org-file-map 499 | vals 500 | (sort-by :date) 501 | reverse) 502 | alltags (->> org-files 503 | ;; Don't show draft/in progress posts: 504 | (remove (comp (partial some #{"draft"}) :tags)) 505 | (mapcat :tags) 506 | (remove #{"static" "draft"}) 507 | (into #{})) 508 | _ (generate-thumbnails-in-galleries! ssd) 509 | image-future (future (stage-site-image-files! ssd 510 | target-dir)) 511 | static-future (future (generate-html-for-galleries! ssd 512 | css) 513 | (stage-site-static-files! ssd 514 | target-dir))] 515 | (println "Making artworks pages...") 516 | (artworks/spit-out-artworks-pages! css) 517 | (make-old-home-page config org-files parsed-org-file-map alltags css) 518 | (make-blog-page config org-files parsed-org-file-map alltags css) 519 | (doseq [tag alltags] 520 | (make-blog-page config tag org-files parsed-org-file-map alltags css)) 521 | (println "Making RSS feed...") 522 | (rss/make-rss-feeds "feed.xml" org-files) 523 | (rss/make-rss-feeds "clojure" "feed.clojure.xml" org-files) 524 | (rss/make-rss-feeds "lisp" "feed.lisp.xml" org-files) 525 | (let [page-futures (for [f org-files] 526 | (future 527 | (process-html-file! config 528 | f 529 | org-files 530 | alltags 531 | css 532 | parsed-org-file-map)))] 533 | (println "Waiting for static copies to finish...") 534 | (wait-futures [static-future]) 535 | (println "Waiting for image future to finish...") 536 | (wait-futures [image-future]) 537 | (println (format "Waiting for %d per-page threads to finish..." 538 | (count page-futures))) 539 | (wait-futures page-futures) 540 | (println "OK")))) 541 | 542 | (defn -main [] 543 | (generate-site config) 544 | (shutdown-agents)) 545 | 546 | (comment 547 | (require '[marginalia.core :as marg]) 548 | (marg/run-marginalia ["src/organa/core.clj"]) 549 | (clojure.java.shell/sh "open" "docs/uberdoc.html") 550 | (generate-site config) 551 | (generate-site (assoc config :proof? true)) 552 | (clojure.java.shell/sh "open" "/tmp/organa/index-old.html") 553 | ) 554 | -------------------------------------------------------------------------------- /src/organa/dates.clj: -------------------------------------------------------------------------------- 1 | (ns organa.dates 2 | (:require [net.cgrand.enlive-html :as html] 3 | [clj-time.coerce :refer [from-long to-long]] 4 | [clojure.java.io :as io] 5 | [clj-time.format :as tformat]) 6 | (:import [java.nio.file Files] 7 | [java.nio.file.attribute PosixFileAttributeView] 8 | [java.util Calendar])) 9 | 10 | (def ^{:doc "Date format used when rendering blog posts"} 11 | article-date-format (tformat/formatter "EEEE, MMMM d, yyyy")) 12 | 13 | (defn ^:private date-for-org-file-by-header [path] 14 | (->> path 15 | slurp 16 | html/html-snippet 17 | (#(html/select % [:p.date])) 18 | (map (comp second (partial re-find #"Date: (.+?)$") first :content)) 19 | (remove nil?) 20 | first 21 | to-long)) 22 | 23 | (defn date-for-file-by-os 24 | " 25 | Get \"update\" date and time in milliseconds for a file on the file 26 | system. 27 | 28 | Useful links: 29 | https://github.com/juxt/yada/blob/master/src/yada/resources/file_resource.clj 30 | http://stackoverflow.com/questions/2723838/\\ 31 | determine-file-creation-date-in-java 32 | " 33 | {:doc/format :markdown} 34 | [path] 35 | (.toMillis 36 | ^java.nio.file.attribute.FileTime 37 | (.creationTime 38 | ^sun.nio.fs.UnixFileAttributes 39 | (.readAttributes 40 | ^sun.nio.fs.UnixFileAttributeViews$Posix 41 | (Files/getFileAttributeView ^sun.nio.fs.UnixPath (.toPath (io/file path)) 42 | PosixFileAttributeView 43 | (into-array java.nio.file.LinkOption 44 | [])))))) 45 | 46 | (defn date-for-org-file 47 | " 48 | Find Joda `DateTime` for an Org post in HTML format, first looking 49 | in the Org Mode header, and if not present there, using the filesystem 50 | time. 51 | " 52 | {:doc/format :markdown} 53 | [site-source-dir basename] 54 | (let [path (format "%s/%s.html" site-source-dir basename)] 55 | (from-long (or (date-for-org-file-by-header path) 56 | (date-for-file-by-os path))))) 57 | 58 | (defn date->year 59 | " 60 | Find year for a `clj-time` time (Joda `DateTime`). 61 | " 62 | {:doc/format :markdown} 63 | [d] 64 | (let [cal (Calendar/getInstance)] 65 | (.setTime cal d) 66 | (.get cal Calendar/YEAR))) 67 | 68 | (defn ^Integer current-year 69 | " 70 | Current year (at the time the program is being run). 71 | " 72 | {:doc/format :markdown} 73 | [] 74 | (date->year (java.util.Date.))) 75 | -------------------------------------------------------------------------------- /src/organa/egg.clj: -------------------------------------------------------------------------------- 1 | (ns organa.egg 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :as string])) 4 | 5 | (defn ^:private whodonit [] 6 | (rand-nth ["Produced by little elves with hand tools" 7 | "Make by perky little white bunnies" 8 | "Produced in high-energy, heavy ion impacts" 9 | "Forged in the fires of Odin" 10 | "Seen near the Tannhauser gates" 11 | "Swept under your carpet" 12 | "Crashed into brick walls at high speed" 13 | "Atomized into little silver flakes"])) 14 | 15 | (def ^:private egg-fmt (slurp (io/resource "banner-format.txt"))) 16 | 17 | (defn easter-egg 18 | " 19 | Generate a (potentially random) \"easter egg\" to embed as a comment 20 | in blog post HTML. 21 | " 22 | [] 23 | (let [who (whodonit) 24 | n (- 62 (count who)) 25 | line3 (str " ``` " 26 | who 27 | ", using " 28 | (string/join (repeat n "`")))] 29 | (format egg-fmt line3))) 30 | -------------------------------------------------------------------------------- /src/organa/fs.clj: -------------------------------------------------------------------------------- 1 | (ns organa.fs 2 | " 3 | Functions for interacting with files, paths, and directories in the 4 | file system. 5 | " 6 | (:require [clojure.java.io :as io] 7 | [clojure.string :as string] 8 | [me.raynes.fs :as fs]) 9 | (:import [java.io File])) 10 | 11 | (defmacro with-tmp-dir 12 | " 13 | Create temporary file, bind to `dir-file`, and execute `body` in 14 | that context, removing the directory afterwards. 15 | " 16 | [dir-file & body] 17 | `(let [~dir-file (fs/temp-dir "organa")] 18 | (try 19 | ~@body 20 | (finally 21 | ;; FIXME: Eliminate double-evaluation: 22 | (fs/delete-dir ~dir-file))))) 23 | 24 | (defn path 25 | " 26 | Create a file or directory path (string) out of `args`. 27 | 28 | (path \"/a\" \"b\" \"c.txt\") 29 | ;;=> 30 | \"a/b/c.txt\" 31 | " 32 | [& args] 33 | (string/join "/" args)) 34 | 35 | (defn dirfile 36 | " 37 | Find the directory File that contains File `f`, which must exist. 38 | " 39 | [^File f] 40 | (io/file (.getParent f))) 41 | 42 | (defn files-in-directory 43 | " 44 | Get list of (non-hidden) files in directory. Examples: 45 | 46 | (files-in-directory \"/tmp\") 47 | (files-in-directory \"/tmp\" :as :str) 48 | (files-in-directory \"/tmp\" :as :file) 49 | " 50 | [pathstr & {:keys [as] 51 | :or {as :file}}] 52 | (let [xform (get {:str str, :file identity} as identity)] 53 | (->> pathstr 54 | io/file 55 | .listFiles 56 | (map xform) 57 | (remove (comp #(.contains ^String % "/.") str))))) 58 | 59 | (defn ^:private ^File coerce-file 60 | " 61 | Make sure `f` is a `File`. FIXME: use protocols for this instead. 62 | " 63 | [f] 64 | (if (string? f) 65 | (io/file f) 66 | f)) 67 | 68 | (defn basename 69 | " 70 | Get file (not directory) portion of name of `f`, where `f` is a 71 | `File` or path string. 72 | " 73 | [f] 74 | (.getName (coerce-file f))) 75 | 76 | (defn splitext 77 | " 78 | Split non-extension portion of file name from extension. 79 | 80 | (splitext \"a\") ;;=> '(\"a\" nil) 81 | (splitext \"a.b\") ;;=> '(\"a\" \"b\") 82 | (splitext \"a.b.c\") ;;=> '(\"a.b\" \"c\") 83 | " 84 | [f] 85 | (->> f 86 | coerce-file 87 | basename 88 | (re-find #"^(.*?)(?:\.([^\.]*))?$") 89 | (drop 1))) 90 | -------------------------------------------------------------------------------- /src/organa/gallery.clj: -------------------------------------------------------------------------------- 1 | (ns organa.gallery 2 | " 3 | Functions for implementing galleries of images. 4 | " 5 | (:require [clojure.java.io :as io] 6 | [organa.config :as config] 7 | [organa.html :as h] 8 | [organa.image :as img]) 9 | (:import [java.io File])) 10 | 11 | (defn gallery-images [galpath] 12 | (->> galpath 13 | io/file 14 | .listFiles 15 | (map #(.getName ^File %)) 16 | sort 17 | (filter (partial re-find img/image-file-pattern)) 18 | (remove #(.contains ^String % "-thumb.png")))) 19 | 20 | (defn inline-gallery 21 | " 22 | This is invoked directly from one of the org files on the blog! 23 | FIXME: sketchy! 24 | " 25 | [gallery-name] 26 | (let [galpath (str (:site-source-dir config/config) 27 | "/galleries/" 28 | gallery-name)] 29 | (h/div {} (for [f (gallery-images galpath) 30 | :let [img-path (format "./galleries/%s/%s" 31 | gallery-name f)]] 32 | (h/a {:href img-path} 33 | [(h/img {:src img-path 34 | :class "inline-gallery-thumb"} 35 | [])]))))) 36 | -------------------------------------------------------------------------------- /src/organa/html.clj: -------------------------------------------------------------------------------- 1 | (ns organa.html 2 | " 3 | Implementations of the parts of HTML needed by 4 | this blog engine. 5 | " 6 | (:require [net.cgrand.enlive-html :as html])) 7 | 8 | (defmacro ^:private deftag 9 | ([fname tagname] 10 | `(defn ~fname 11 | ([~'content] (~fname {} ~'content)) 12 | ([~'attrs ~'content] 13 | {:tag ~(keyword tagname) 14 | :attrs ~'attrs 15 | :content ~'content}))) 16 | ([tagname] 17 | `(deftag ~tagname ~tagname))) 18 | 19 | (deftag a) 20 | (deftag div) 21 | (deftag h1) 22 | (deftag h2) 23 | (deftag img) 24 | (deftag li) 25 | (deftag link) 26 | (deftag p) 27 | (deftag pre) 28 | (deftag script) 29 | (deftag span) 30 | (deftag strong) 31 | (deftag style) 32 | (deftag table) 33 | (deftag tbody) 34 | (deftag thead) 35 | (deftag tr) 36 | (deftag td) 37 | (deftag th) 38 | (deftag em) 39 | (deftag ul) 40 | (deftag meta-tag meta) 41 | (defn br [] {:tag :br}) 42 | (defn hr [] {:tag :hr}) 43 | 44 | ;; FIXME: Move to an org-mode-specific namespace. 45 | (defn parse-org-html 46 | ([html-text] 47 | (html/html-snippet html-text)) 48 | ([source-dir basename] 49 | (-> (format "%s/%s.html" source-dir basename) 50 | slurp 51 | html/html-snippet))) 52 | -------------------------------------------------------------------------------- /src/organa/image.clj: -------------------------------------------------------------------------------- 1 | (ns organa.image 2 | (:require [clojure.string :as string] 3 | [mikera.image.core :as image])) 4 | 5 | (def image-extensions ["png" "jpg" "jpeg" "gif"]) 6 | 7 | (def ^{:doc 8 | " 9 | image-file-pattern 10 | ;;=> 11 | #\"\\.PNG|\\.JPG|\\.JPEG|\\.GIF|\\.png|\\.jpg|\\.jpeg|\\.gif\" 12 | "} 13 | image-file-pattern 14 | (->> image-extensions 15 | (concat (map string/upper-case image-extensions)) 16 | (map (partial str "\\.")) 17 | (clojure.string/join "|") 18 | re-pattern)) 19 | 20 | (def max-height 250) 21 | 22 | (defn create-thumbnail! 23 | ([maxh orig-path thumb-path] 24 | (let [img (image/load-image orig-path) 25 | h (.getHeight img) 26 | w (.getWidth img) 27 | new-h maxh 28 | new-w (int (* w (/ maxh h)))] 29 | (image/write (image/resize img new-w new-h) 30 | thumb-path 31 | "png" 32 | :quality 1.0))) 33 | ([orig-path thumb-path] 34 | (create-thumbnail! max-height orig-path thumb-path))) 35 | -------------------------------------------------------------------------------- /src/organa/pages.clj: -------------------------------------------------------------------------------- 1 | (ns organa.pages 2 | (:require [organa.html :as h])) 3 | 4 | (defn blog-body [] 5 | (h/div {:id "content"} ["Hi, I'm a blog!"]) 6 | (h/div {:id "blogposts"} [])) 7 | 8 | (defn home-body [] 9 | (h/div 10 | {:id "content"} 11 | [(h/h1 "John Jacobsen ...") 12 | (h/div {:class "figure"} 13 | [(h/a {:href "view-from-skylab-cropped-large.png" 14 | :class "img100"} 15 | [(h/img {:src "view-from-skylab-cropped.jpg"} [])]) 16 | (h/p {:class "figure-number" 17 | :style "margin-top:3px"} 18 | [(str "Clean Air facility as seen from Skylab, " 19 | "Amundsen-Scott South Pole Station, " 20 | "Antarctica, 1998")])]) 21 | (h/p [(str "... is an artist, software engineer and former " 22 | "physicist living in Chicago, Illinois, USA. ") 23 | (h/a {:href "contact.html"} "Contact me.")]) 24 | (h/div 25 | [(h/table 26 | {:class "frontpage-2col"} 27 | [(h/tbody 28 | [(h/tr [(h/td [(h/p 29 | ["As an " (h/strong ["engineer"]) 30 | ", I'm interested in helping companies " 31 | "and organizations create excellent " 32 | "(functional, reliable, maintainable) " 33 | "software, and teams to build the same. " 34 | (h/a {:href "tech.html"} "Read more here") 35 | "."]) 36 | (h/p [(h/a {:href "https://github.com/eigenhombre"} 37 | "GitHub") 38 | " / " 39 | (h/a {:href "https://twitter.com/eigenhombre"} 40 | "Twitter") 41 | " / " 42 | (h/a {:href 43 | (str "http://stackoverflow.com" 44 | "/users/611752/johnj")} 45 | "StackOverflow") 46 | " / " 47 | (h/a {:href 48 | "https://www.linkedin.com/in/eigenhombre"} 49 | "LinkedIn") 50 | " / " 51 | (h/a {:href "static/jacobsen-resume.pdf"} 52 | "Resume")]) 53 | ]) 54 | (h/td {:class "colspacer"} []) 55 | (h/td 56 | [(h/p 57 | ["As an " (h/strong ["artist"]) 58 | ", I'm primarily interested in figurative oil " 59 | "painting and drawing, from life and imagination. " 60 | "You can see samples of " 61 | "my recent work on " 62 | (h/a {:href "http://instagram.com/eigenhombre"} 63 | "Instagram") 64 | ;; " or " 65 | ;; (h/a {:href "http://toomanysketchbooks.tumblr.com"} 66 | ;; "Tumblr") 67 | ". See also " 68 | (h/a {:href "bio.html"} 69 | "bio") 70 | " and " 71 | (h/a {:href "exhibitions.html"} 72 | "exhibitions.")])])])])])]) 73 | (h/p ["Aside from the above links, this site is primarily 74 | here to present to you the following blog posts. Many of 75 | these were " 76 | (h/a {:href "southpole.html"} 77 | ["written at the Geographic South Pole"]) 78 | " between 2007 and 2011."]) 79 | (h/div {:id "blogposts"} [])])) 80 | -------------------------------------------------------------------------------- /src/organa/parse.clj: -------------------------------------------------------------------------------- 1 | (ns organa.parse 2 | (:require [clojure.string :as string] 3 | [clojure.walk :as walk] 4 | [net.cgrand.enlive-html :as html])) 5 | 6 | (defn title-for-org-file [parsed-html] 7 | (some-> parsed-html 8 | (html/select [:h1.title]) 9 | first 10 | :content 11 | first 12 | ;; For some reason ’ is rendering strangely in Chrome when 13 | ;; synced to zerolib: 14 | (string/replace #"’" "'") 15 | (string/replace #"…" "..."))) 16 | 17 | (defn empty-org-table-entry? [s] 18 | (let [s-cleaned (clojure.string/replace s #" " "")] 19 | (empty? s-cleaned))) 20 | 21 | (defn clean-empty-string-values [m] 22 | (into {} 23 | (for [[k v] m] 24 | (when-not (empty-org-table-entry? v) 25 | [k v])))) 26 | 27 | (defn parsed-org-html->table-metadata [parsed-html] 28 | (some->> (html/select parsed-html [:table]) 29 | first 30 | :content 31 | (filter (comp (partial = :tbody) :tag)) 32 | first 33 | :content 34 | (filter map?) 35 | (filter (comp (partial = :tr) :tag)) 36 | (map :content) 37 | (map (partial remove string?)) 38 | (map (partial map :content)) 39 | (mapcat vec) 40 | (map first) 41 | (apply hash-map) 42 | walk/keywordize-keys 43 | clean-empty-string-values)) 44 | 45 | (comment 46 | (require '[organa.html :as h]) 47 | (->> (str "/Users/jacobsen/org/sites/johnj.com/" 48 | "artworks/crows-and-civilization/meta.html") 49 | slurp 50 | h/parse-org-html 51 | parsed-org-html->table-metadata) 52 | ;;=> 53 | '{:width " ", :height " ", 54 | :medium "Oil on Linen", :year "2019", :price "2000"}) 55 | -------------------------------------------------------------------------------- /src/organa/rss.clj: -------------------------------------------------------------------------------- 1 | (ns organa.rss 2 | (:require [clj-rss.core :as rss] 3 | [clojure.walk :as walk] 4 | [net.cgrand.enlive-html :as html] 5 | [organa.config :refer [config]] 6 | [organa.html :as h]) 7 | (:import [java.time Instant])) 8 | 9 | ;; FIXME: 10 | (def ^:private target-dir (:target-dir config)) 11 | ;; FIXME: 12 | (def ^:private remote-host (:remote-host config)) 13 | 14 | (defn ^:private html-for-rss 15 | " 16 | Remove JavaScript and CSS bits from Org-generated HTML for RSS feed. 17 | " 18 | [parsed-html] 19 | (walk/prewalk 20 | (fn [x] 21 | (if (and (map? x) 22 | (#{"text/css" "text/javascript"} (get-in x [:attrs :type]))) 23 | (dissoc (into {} x) :content) 24 | x)) 25 | parsed-html)) 26 | 27 | (defn make-rss-feeds 28 | " 29 | Create RSS feeds for blog. 30 | 31 | FIXME: reduce arity 32 | FIXME: make more configurable 33 | " 34 | {:doc/format :markdown} 35 | ([topic rss-file-name org-files] 36 | (make-rss-feeds rss-file-name 37 | (filter (comp (partial some #{topic}) 38 | :tags) 39 | org-files))) 40 | ([rss-file-name org-files] 41 | (let [rss-file-path (str target-dir "/" rss-file-name) 42 | posts-for-feed (->> org-files 43 | (remove :static?) 44 | (remove :draft?) 45 | (take 20)) 46 | feed-items (for [f posts-for-feed 47 | :let [file-name (:file-name f) 48 | link-path (format "http://%s/%s.html" 49 | remote-host 50 | file-name)]] 51 | {:title (:title f) 52 | :link link-path 53 | :pubDate (Instant/ofEpochMilli 54 | (.getMillis ^org.joda.time.DateTime (:date f))) 55 | :description (format "" 56 | (->> f 57 | :parsed 58 | html-for-rss 59 | html/emit* 60 | (apply str)))})] 61 | (->> feed-items 62 | (rss/channel-xml {:title "John Jacobsen" 63 | :link (str "http://" remote-host) 64 | :description "Posts by John Jacobsen"}) 65 | (spit rss-file-path))))) 66 | 67 | (defn rss-links 68 | " 69 | Create HTML paragraph (`p`) of links for available RSS feeds. 70 | " 71 | {:doc/format :markdown} 72 | [] 73 | (h/p ["Subscribe: " 74 | (h/a {:href "feed.xml" 75 | :class "rss"} 76 | ["RSS feed ... all topics"]) 77 | " ... or " 78 | (h/a {:href "feed.clojure.xml" 79 | :class "rss"} 80 | ["Clojure only"]) 81 | " / " 82 | (h/a {:href "feed.lisp.xml" 83 | :class "rss"} 84 | ["Lisp only"])])) 85 | -------------------------------------------------------------------------------- /test/organa/dates_test.clj: -------------------------------------------------------------------------------- 1 | (ns organa.dates-test 2 | (:require [clojure.test :refer [deftest testing is]] 3 | [java-time :as jt] 4 | [organa.dates :as d] 5 | [organa.fs :as fs])) 6 | 7 | (deftest current-year-test 8 | (testing "I can get the current year, > 2020" 9 | (is (< 2020 (d/current-year))))) 10 | 11 | (deftest date-for-org-file-test 12 | (testing "file creation dates" 13 | (fs/with-tmp-dir d 14 | (testing "An empty 'org' file" 15 | (let [fpath (fs/path d "empty.html")] 16 | (spit fpath "") 17 | (testing "creation date is found" 18 | (is (.startsWith (str (d/date-for-org-file d "empty")) 19 | (str (d/current-year))))))) 20 | (testing "An 'org' file with a post date" 21 | (let [fpath (fs/path d "post.html")] 22 | (spit fpath " 23 |

Date: 2008-02-18 24 | ") 25 | (testing "creation date is correct" 26 | (is (.startsWith (str (d/date-for-org-file d "post")) 27 | "2008-02-18")))))))) 28 | -------------------------------------------------------------------------------- /test/organa/fs_test.clj: -------------------------------------------------------------------------------- 1 | (ns organa.fs-test 2 | (:require [clojure.java.shell :as shell] 3 | [clojure.test :refer [are deftest testing is]] 4 | [clojure.java.io :as io] 5 | [organa.fs :as fs] 6 | [clojure.string :as string]) 7 | (:import [java.io File])) 8 | 9 | (deftest with-tmp-dir-test 10 | (testing "It creates a file object" 11 | (is (instance? java.io.File 12 | (fs/with-tmp-dir d 13 | d)))) 14 | (testing "It contains `organa` in the path" 15 | (is (string/includes? (fs/with-tmp-dir d 16 | (:out (shell/sh "pwd" :dir d))) 17 | "organa"))) 18 | (testing "It cleans up after itself" 19 | (let [path (fs/with-tmp-dir d 20 | (string/trim (:out (shell/sh "pwd" :dir d))))] 21 | (is (not 22 | (.exists (io/file path))))))) 23 | 24 | (deftest path-test 25 | (are [args result] 26 | (testing (str args " => " (pr-str result)) 27 | (is (= result (apply fs/path args)))) 28 | 29 | [] "" 30 | nil "" 31 | ["a"] "a" 32 | ["a" "b"] "a/b")) 33 | 34 | (deftest directory-test 35 | (testing "file / directory operations" 36 | (fs/with-tmp-dir d 37 | (testing "in a temporary directory" 38 | (let [test-file (fs/path d "f.txt") 39 | _ (spit test-file "some text") 40 | parent (fs/dirfile (io/file test-file))] 41 | (testing (str "the parent found by `dirfile` " 42 | "has a single child, the test file we made") 43 | (is (= [(io/file test-file)] 44 | (into [] (.listFiles ^File parent)))) 45 | (testing "using `files-in-directory`, result is same" 46 | (is (= [(io/file test-file)] 47 | (fs/files-in-directory (str parent) 48 | :as :file))))) 49 | (testing "`basename` returns original file name" 50 | (is (= "f.txt" 51 | (fs/basename test-file)))) 52 | (testing "`splitext` returns `basename` and extension" 53 | (is (= ["f" "txt"] 54 | (fs/splitext test-file))))))))) 55 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | {:kaocha/reporter [kaocha.report/doc] 2 | :kaocha/tests [{:kaocha.testable/type :kaocha.type/clojure.test 3 | :kaocha.testable/id :unit 4 | :kaocha/ns-patterns ["-test$"] 5 | :kaocha/source-paths ["src"] 6 | :kaocha/test-paths ["test"]}]} 7 | --------------------------------------------------------------------------------