64 Commits

Author SHA1 Message Date
dcaac98252 refactor: doerg는 publisher와 결합
All checks were successful
build / build (push) Successful in 5s
2026-04-03 13:31:16 -06:00
5ca59fdb5e feat: 많은 테스트...
All checks were successful
build / build (push) Successful in 45s
2026-04-02 13:26:38 -06:00
55fa2473a2 feat: backlinks
All checks were successful
build / build (push) Successful in 27s
2026-03-30 15:39:20 -06:00
9349293684 feat: text art
All checks were successful
build / build (push) Successful in 1m19s
2026-03-28 23:05:02 -06:00
4d310afa18 fix: css 2026-03-28 23:04:38 -06:00
7634303b13 feat: epigraphs
All checks were successful
build / build (push) Successful in 5s
해결 #11
2026-03-28 22:24:12 -06:00
cf242b778d feat: 테스트 데이터베이스
All checks were successful
build / build (push) Successful in 5s
해결 #28
2026-03-28 13:19:42 -06:00
9cc0def4d5 feat: html 캐시
All checks were successful
build / build (push) Successful in 4s
2026-03-27 19:42:51 -06:00
e02b77e1e4 fix: temml prelude
All checks were successful
build / build (push) Successful in 4s
2026-03-24 16:53:14 -06:00
c74e36d8af fix: throw when temml output is not string 2026-03-24 16:52:41 -06:00
36e328915e chore: flake-utils를 안 사용하는데 뺌
All checks were successful
build / build (push) Successful in 6s
2026-03-24 16:03:24 -06:00
b607ab8cae chore: 청소함
All checks were successful
build / build (push) Successful in 1m10s
2026-03-24 15:56:09 -06:00
fb2974bb21 feat: 서버는 기초의 테스트 더함
All checks were successful
build / build (push) Successful in 4s
2026-03-24 15:50:08 -06:00
79735cc0ba fix: config에서 포트를 읽음
All checks were successful
build / build (push) Successful in 4s
2026-03-23 09:08:32 -06:00
931f6f9006 fix: fix elisp shadowing clojure.core warnings
All checks were successful
build / build (push) Successful in 4s
2026-03-23 08:48:34 -06:00
811d07de39 refactor: config.edn → default-config.edn
All checks were successful
build / build (push) Successful in 30s
2026-03-23 08:43:33 -06:00
29d5cdc85a feat: o pona e ijo Config kepeken lipu Library
All checks were successful
build / build (push) Successful in 5s
2026-03-23 08:40:50 -06:00
d4ff27ada4 feat: very primitive publisher
All checks were successful
build / build (push) Successful in 5s
2026-03-21 20:37:16 -06:00
5bca7890c4 feat: css hyphenation
All checks were successful
build / build (push) Successful in 43s
2026-03-19 15:46:25 -06:00
befa724551 fix: native latex prelude 2026-03-19 15:46:07 -06:00
2b80c15cc8 chore: cleanup
All checks were successful
build / build (push) Successful in 42s
2026-03-19 14:47:01 -06:00
7805de06f2 fix: latex paragraphs again lol
All checks were successful
build / build (push) Successful in 4s
2026-03-19 14:45:53 -06:00
1629efb378 fix: latex-environment belongs to paragraph
All checks were successful
build / build (push) Successful in 5s
wip: feat: org-element specs
2026-03-17 21:07:33 -06:00
2198b5f409 fix: ????
All checks were successful
build / build (push) Successful in 4s
2026-03-05 15:59:55 -07:00
bc5138086d fix: remove debug code again idiot
Some checks failed
build / build (push) Failing after 19s
2026-03-05 15:58:14 -07:00
6d1d94194b fix: display math scales with text
All checks were successful
build / build (push) Successful in 44s
2026-03-05 15:51:13 -07:00
fd9322740d fix: remove debug code
All checks were successful
build / build (push) Successful in 39s
2026-03-05 15:11:26 -07:00
e5b47898a5 Merge pull request 'fix: tikz svgs' (#10) from fix-tikz-svgs into main
All checks were successful
build / build (push) Successful in 5s
Reviewed-on: #10
2026-03-05 15:10:46 -07:00
26410655d9 fix: tikz svgs
All checks were successful
build / build (push) Successful in 43s
Fixes #9
2026-03-05 15:09:37 -07:00
faa84f986d doc: doerg-temml-worker
All checks were successful
build / build (push) Successful in 1m24s
2026-03-05 11:47:59 -07:00
9ec8bca383 feat: warning message on timeout
All checks were successful
build / build (push) Successful in 40s
2026-03-05 09:52:29 -07:00
d946c57bff Merge pull request 'fix: namespace error lol' (#8) from ns-error into main
All checks were successful
build / build (push) Successful in 4s
Reviewed-on: #8
2026-03-05 09:49:24 -07:00
7c4ad64d58 fix: increase snippet timeout to 20s
All checks were successful
build / build (push) Successful in 5s
2026-03-05 09:49:04 -07:00
56742cf72d fix: namespace error lol
Some checks failed
build / build (push) Failing after 1m1s
2026-03-05 09:36:49 -07:00
a92b387e63 refactor: more.
Some checks failed
build / build (push) Failing after 3h13m17s
2026-03-02 15:18:35 -07:00
98c106b3cf fix: trim temml/render input 2026-03-02 15:17:57 -07:00
b336aa873e refactor: more cleanup >_<
Some checks failed
build / build (push) Has been cancelled
2026-03-02 14:59:11 -07:00
e49f847f20 refactor: move texslop into tex module
Some checks failed
build / build (push) Has been cancelled
2026-03-02 14:53:07 -07:00
ec4ad7c9c2 fix: set doerg-parser in config 2026-03-02 14:12:52 -07:00
762324d9f0 fix: config spec 2026-03-02 14:06:50 -07:00
c97b7ab0dc chore: clean doerg sources
All checks were successful
build / build (push) Successful in 5s
2026-03-01 12:18:14 -07:00
c45852097b chore: package.nix → default.nix
All checks were successful
build / build (push) Successful in 5s
2026-03-01 12:08:21 -07:00
74e48264ea chore: clean up serialise
Some checks failed
build / build (push) Has been cancelled
2026-03-01 11:56:06 -07:00
283c18fd9e chore: remove demo action
All checks were successful
build / build (push) Successful in 4s
2026-03-01 11:52:58 -07:00
570fca833d Merge pull request 'tex' (#7) from tex into main
All checks were successful
build / build (push) Successful in 4s
Gitea Actions Demo / build (push) Successful in 4s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1s
Reviewed-on: #7
2026-03-01 11:50:07 -07:00
eb53516a2d feat: build on push
All checks were successful
build / build (push) Successful in 57s
Gitea Actions Demo / build (push) Successful in 4s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1s
2026-03-01 11:46:40 -07:00
8b2fb7a010 refactor: rename doerg-tex → doerg-temml-worker 2026-03-01 11:46:22 -07:00
6ef81a4a45 fix: nix stuff and config 2026-03-01 11:46:22 -07:00
17e61aabc8 fix: config spec 2026-03-01 11:46:22 -07:00
c34b239d01 feat: kaocha 2026-03-01 11:46:22 -07:00
de96a6245b fix: align display math 2026-03-01 11:46:22 -07:00
c077186c10 feat: tests 2026-03-01 11:46:22 -07:00
c0a69bdbed feat: to-html 2026-03-01 11:44:13 -07:00
99bf9aae03 refactor: split tex modules 2026-03-01 11:44:13 -07:00
18350f6600 fix: render-temml 2026-03-01 11:44:13 -07:00
5c92dc153c fix: temml css 2026-03-01 11:44:13 -07:00
66586e6239 refactor: split up render-temml 2026-03-01 11:44:13 -07:00
49990228c9 feat: xelatex rendering 2026-03-01 11:44:02 -07:00
651ed4f26c feat: tex rendering via temml 2026-02-22 18:40:36 -07:00
a26df4011a margin notes 2026-02-16 03:40:52 -07:00
71b370147e feat: subscript superscript 2026-02-09 17:53:21 -07:00
703bcff905 feat: more sane placeholders 2026-02-09 17:53:06 -07:00
3fa63d03d8 feat: ignore comments 2026-02-09 17:49:54 -07:00
98c0be72e0 different placeholders for unimplemented elements 2026-02-09 17:44:49 -07:00
89 changed files with 3972 additions and 953 deletions

1
.dir-locals.el Normal file
View File

@@ -0,0 +1 @@
((nil . ((cider-clojure-cli-aliases . ":dev:test"))))

View File

@@ -0,0 +1,11 @@
name: build
on: [push]
jobs:
build:
runs-on: nixos
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: build doerg
run: nix build -L .#doerg

1
.gitignore vendored
View File

@@ -9,7 +9,6 @@ result-*
.lsp
.nrepl
.direnv/
resources/vendor/*
node_modules
.cljs_node_repl
build/

2
bb.edn
View File

@@ -3,6 +3,6 @@
lock
{:doc "Update the clj-nix lockfile"
:task (-> (p/sh {:out :inherit :err :inherit}
"nix run github:jlesquembre/clj-nix#deps-lock")
"nix run github:jlesquembre/clj-nix#deps-lock")
:exit
System/exit)}}}

77
default.nix Normal file
View File

@@ -0,0 +1,77 @@
{ mkCljBin
, callPackage
, lib
, doerg-parser
, doerg-temml-worker
, ibm-plex-web
, test-emacs ? callPackage ./test-emacs.nix {}
, fake-git
, our-tex
, makeWrapper
, writeText
}:
let
# mkCljBin sans fake-git. We don't need it, and I don't want it in
# my dev shell.
mkCljBin' = args: (mkCljBin args).overrideAttrs (final: prev: {
nativeBuildInputs =
builtins.filter
# A possibly-sketchy predicate, lol.
(x: x != fake-git)
prev.nativeBuildInputs;
});
plex = ibm-plex-web.override {
families = [ "math" "serif" "sans" "sans-kr" ];
};
bin-path = lib.makeBinPath [
doerg-parser
our-tex
doerg-temml-worker
];
doerg-config = writeText "doerg-extra-config.edn" ''
#:net.deertopia.doerg
{:ibm-plex-web "${ibm-plex-web}"
:latex "${lib.getExe' our-tex "xelatex"}"
:dvisvgm "${lib.getExe' our-tex "dvisvgm"}"
:doerg-temml-worker "${lib.getExe doerg-temml-worker}"
:doerg-parser "${lib.getExe doerg-parser}"}
'';
in mkCljBin' {
name = "net.deertopia/doerg";
version = "0.1.0";
projectSrc = lib.cleanSource ./.;
lockfile = ./deps-lock.json;
main-ns = "net.deertopia.doerg.main";
nativeBuildInputs = [
plex
makeWrapper
];
buildInputs = [
doerg-parser
doerg-temml-worker
plex
our-tex
];
nativeCheckInputs = [
doerg-parser
doerg-temml-worker
test-emacs
plex
our-tex
];
preBuild = ''
cp ${doerg-config} resources/net/deertopia/doerg/extra-config.edn
'';
doCheck = true;
checkPhase = ''
export \
EMACS=test-emacs \
XDG_STATE_HOME=$(mktemp -d "state-home-XXXXXX")
clojure -M:test
'';
passthru = { inherit plex our-tex test-emacs; };
}

File diff suppressed because it is too large Load Diff

24
deps.edn Normal file
View File

@@ -0,0 +1,24 @@
{:deps {aero/aero #:mvn{:version "1.1.6"}
babashka/fs #:mvn{:version "0.5.24"}
babashka/process #:mvn{:version "0.6.25"}
ch.qos.logback/logback-classic #:mvn{:version "1.1.3"}
cheshire/cheshire #:mvn{:version "6.1.0"}
com.github.seancorfield/next.jdbc #:mvn{:version "1.3.1070"}
com.rpl/specter #:mvn{:version "1.1.6"}
hiccup/hiccup #:mvn{:version "2.0.0-RC4"}
http-kit/http-kit #:mvn{:version "2.8.0"}
instaparse/instaparse #:mvn{:version "1.5.0"}
metosin/reitit #:mvn{:version "0.10.1"}
mvxcvi/clj-cbor #:mvn{:version "1.1.1"}
org.clojars.pntblnk/clj-ldap #:mvn{:version "0.0.17"}
org.clojure/clojure #:mvn{:version "1.12.0"}
org.clojure/core.match #:mvn{:version "1.1.0"}
org.clojure/test.check #:mvn{:version "1.1.3"}
org.clojure/tools.logging #:mvn{:version "1.3.0"}
org.xerial/sqlite-jdbc #:mvn{:version "3.47.1.0"}}
:paths ["src" "resources"]
:aliases
{:test {:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}
:extra-paths ["test"]
:main-opts ["-m" "kaocha.runner"]}
:dev {:extra-paths ["dev"]}}}

View File

@@ -10,4 +10,5 @@ buildNpmPackage {
npmDeps = importNpmLock { npmRoot = ./.; };
npmConfigHook = importNpmLock.npmConfigHook;
dontNpmBuild = true;
meta.mainProgram = "doerg-parser";
}

6
doerg/doerg-parser/index.js → doerg-parser/index.js Normal file → Executable file
View File

@@ -2,13 +2,17 @@
const { parse } = require ("uniorg-parse/lib/parser.js");
const opts = {
trackPosition: true
}
async function main () {
const chunks = []
for await (const chunk of process.stdin) {
chunks.push (chunk)
}
const orgText = Buffer.concat (chunks).toString ("utf8")
process.stdout.write (JSON.stringify (parse (orgText)))
process.stdout.write (JSON.stringify (parse (orgText, opts)))
}
main ()

View File

@@ -1,11 +1,17 @@
{
"name": "doerg-parser",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "doerg-parser",
"version": "0.1.0",
"dependencies": {
"uniorg-parse": "^3.2.0"
},
"bin": {
"doerg-parser": "index.js"
}
},
"node_modules/@types/unist": {

View File

@@ -0,0 +1,12 @@
#+title: doerg-temml-worker
This is a tiny Node script that is used by doerg. For each CBOR-encoded value on stdin (a "command"), a response value is spit to stdout.
Currently, the following commands are supported:
- Any string: Return the Temml-rendered MathML code as a string (inline).
- An array with a single string element: Return the Temml-rendered MathML code as a string (display).
If something goes wrong, either of these commands may return an object of form
#+begin_src json
{"type": "error"
,"error": «error object»}
#+end_src

View File

@@ -0,0 +1,22 @@
{ buildNpmPackage
, importNpmLock
, makeWrapper
, ibm-plex
, callPackage
}:
buildNpmPackage {
pname = "doerg-temml-worker";
version = "0.1.0";
src =
builtins.filterSource
(name: _file-type: ! builtins.elem name [
"deserialise.clj" "deps.edn" "default.nix"
"README.org" "serialise.clj" "node_modules"
])
./.;
npmDeps = importNpmLock { npmRoot = ./.; };
npmConfigHook = importNpmLock.npmConfigHook;
dontNpmBuild = true;
meta.mainProgram = "doerg-temml-worker";
}

55
doerg-temml-worker/index.js Executable file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env node
const commandLineArgs = require ("command-line-args")
const temml = require ("temml")
const fs = require ("node:fs")
const path = require ("node:path")
const { DecoderStream, EncoderStream } = require ("cbor-x")
const { Transform } = require('node:stream')
const cli_spec =
[ { name: "preamble" }
]
function load_preambles (preamble) {
const data = fs.readFileSync (preamble, "utf8") .toString ()
return temml.definePreamble (data)
}
let macros = {}
function is_render_command (cmd) {
return cmd instanceof Array
&& cmd.length === 1
&& typeof cmd[0] === "string"
}
function do_command (cmd) {
try {
if (typeof cmd === "string") {
return temml.renderToString (cmd, {macros})
} else if (is_render_command (cmd)) {
return temml.renderToString (cmd[0], {displayMode: true,macros})
} else {
return null
}
} catch (e) {
console.error (e)
return {type: "error", error: e}
}
}
function main () {
const options = commandLineArgs (cli_spec)
macros = load_preambles (options.preamble)
const decoder = new DecoderStream ()
const encoder = new EncoderStream ()
process.stdin.pipe (decoder)
const command_responses = decoder.map (do_command)
decoder
.map (do_command)
.pipe (encoder)
.pipe (process.stdout)
}
main ()

227
doerg-temml-worker/package-lock.json generated Normal file
View File

@@ -0,0 +1,227 @@
{
"name": "doerg-temml-worker",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "doerg-temml-worker",
"version": "0.1.0",
"dependencies": {
"cbor-x": "^1.6.0",
"command-line-args": "^6.0.1",
"temml": "^0.13.1"
},
"bin": {
"doerg-temml-worker": "index.js"
}
},
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz",
"integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@cbor-extract/cbor-extract-darwin-x64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz",
"integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@cbor-extract/cbor-extract-linux-arm": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz",
"integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@cbor-extract/cbor-extract-linux-arm64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz",
"integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@cbor-extract/cbor-extract-linux-x64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz",
"integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@cbor-extract/cbor-extract-win32-x64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz",
"integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/array-back": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz",
"integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/cbor-extract": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz",
"integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build-optional-packages": "5.1.1"
},
"bin": {
"download-cbor-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
"@cbor-extract/cbor-extract-darwin-arm64": "2.2.0",
"@cbor-extract/cbor-extract-darwin-x64": "2.2.0",
"@cbor-extract/cbor-extract-linux-arm": "2.2.0",
"@cbor-extract/cbor-extract-linux-arm64": "2.2.0",
"@cbor-extract/cbor-extract-linux-x64": "2.2.0",
"@cbor-extract/cbor-extract-win32-x64": "2.2.0"
}
},
"node_modules/cbor-x": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.0.tgz",
"integrity": "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==",
"license": "MIT",
"optionalDependencies": {
"cbor-extract": "^2.2.0"
}
},
"node_modules/command-line-args": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz",
"integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==",
"license": "MIT",
"dependencies": {
"array-back": "^6.2.2",
"find-replace": "^5.0.2",
"lodash.camelcase": "^4.3.0",
"typical": "^7.2.0"
},
"engines": {
"node": ">=12.20"
},
"peerDependencies": {
"@75lb/nature": "latest"
},
"peerDependenciesMeta": {
"@75lb/nature": {
"optional": true
}
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/find-replace": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz",
"integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==",
"license": "MIT",
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@75lb/nature": "latest"
},
"peerDependenciesMeta": {
"@75lb/nature": {
"optional": true
}
}
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/node-gyp-build-optional-packages": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz",
"integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==",
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.1"
},
"bin": {
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-optional-packages-optional": "optional.js",
"node-gyp-build-optional-packages-test": "build-test.js"
}
},
"node_modules/temml": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/temml/-/temml-0.13.1.tgz",
"integrity": "sha512-/fL1utq8QUD9YpcLeZHPRnp9Cbzbexq5hZl5uSBhf8mNYiKkcS4eYbLidDB+/nF8C+RHAcBQbKw2bKoS83mz1Q==",
"license": "MIT",
"engines": {
"node": ">=18.13.0"
}
},
"node_modules/typical": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz",
"integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
}
}
}

View File

@@ -0,0 +1,6 @@
{"dependencies":{"temml":"^0.13.1"
,"command-line-args":"^6.0.1"
,"cbor-x":"^1.6.0"}
,"name":"doerg-temml-worker"
,"version":"0.1.0"
,"bin":{"doerg-temml-worker":"index.js"}}

View File

@@ -1,6 +0,0 @@
#+title: Doerg specification
#+author: Guppy
* Footnotes
- A bunch of metadata should be read into a node of type =doerg-data=

View File

@@ -1,271 +0,0 @@
{
"lock-version": 4,
"git-deps": [],
"mvn-deps": [
{
"mvn-path": "babashka/fs/0.5.24/fs-0.5.24.jar",
"mvn-repo": "https://repo.clojars.org/",
"hash": "sha256-owunNIWFqt6xNGhNV6vFs7GyYEz8z8wHRT7TSw1MDlU="
},
{
"mvn-path": "babashka/fs/0.5.24/fs-0.5.24.pom",
"mvn-repo": "https://repo.clojars.org/",
"hash": "sha256-E1jX6M3DeoTXVU6Vzd6xIpE0s9QAZpZALJl4nSKU8E8="
},
{
"mvn-path": "babashka/process/0.6.25/process-0.6.25.jar",
"mvn-repo": "https://repo.clojars.org/",
"hash": "sha256-XWTJ6kUgLD3/UJGe++Lf0+/Ja9SPViS50v33v3JOf4s="
},
{
"mvn-path": "babashka/process/0.6.25/process-0.6.25.pom",
"mvn-repo": "https://repo.clojars.org/",
"hash": "sha256-4bSR/Z0g4rXVwv2u6e9rkoL8d1nV5b9iNIs/U2o8l8Y="
},
{
"mvn-path": "cheshire/cheshire/6.1.0/cheshire-6.1.0.jar",
"mvn-repo": "https://repo.clojars.org/",
"hash": "sha256-ruCrbkSBlaoJXyLEKOINK8YQWRxUwQ7D4YPdOwgWOcI="
},
{
"mvn-path": "cheshire/cheshire/6.1.0/cheshire-6.1.0.pom",
"mvn-repo": "https://repo.clojars.org/",
"hash": "sha256-KKpi9U7eZ2YjAyQI14sMgqv91IiMfFagshIrg+Pad+M="
},
{
"mvn-path": "com/fasterxml/jackson/core/jackson-core/2.20.0/jackson-core-2.20.0.jar",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-vAz0YHWHcgH4QG7n3idBrn32wGb18EV72AYypxjAbnI="
},
{
"mvn-path": "com/fasterxml/jackson/core/jackson-core/2.20.0/jackson-core-2.20.0.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-XxZVisIjBzpvJHRA8wX1nwXv9QqvHurguq1BKAABb2g="
},
{
"mvn-path": "com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.20.0/jackson-dataformat-cbor-2.20.0.jar",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-qwdWFsvWf1Z2+Jgl7JyLx7VMhNvpNLFiFFs0FyNwtdY="
},
{
"mvn-path": "com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.20.0/jackson-dataformat-cbor-2.20.0.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-SYvwMYvqLnY2Wf0r/k8W8pHt8JNaMMnysKKP7jrXz8c="
},
{
"mvn-path": "com/fasterxml/jackson/dataformat/jackson-dataformat-smile/2.20.0/jackson-dataformat-smile-2.20.0.jar",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-NFwALLesnlQ0Go8ExWoR3CGUVCbqg5n/VuAvuOkSpm4="
},
{
"mvn-path": "com/fasterxml/jackson/dataformat/jackson-dataformat-smile/2.20.0/jackson-dataformat-smile-2.20.0.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-IM1JgnnQUcaQMkt6R1+Ee/wQoAcQS6uc371ga1sFhDk="
},
{
"mvn-path": "com/fasterxml/jackson/dataformat/jackson-dataformats-binary/2.20.0/jackson-dataformats-binary-2.20.0.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-ZJ/OQKV9Qhx82erCP919XPbzwdG5NAbuJ3xFw1Mm5Yg="
},
{
"mvn-path": "com/fasterxml/jackson/jackson-base/2.20.0/jackson-base-2.20.0.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-/IgCjRnuqjftpZbmuY03SfIs/IrdGeoZdFq+g2iDzmY="
},
{
"mvn-path": "com/fasterxml/jackson/jackson-bom/2.20.0/jackson-bom-2.20.0.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-9ig2R+cB/aHNMS3MIcsTKkD3mPGejkL6/D/jR8WlG7s="
},
{
"mvn-path": "com/fasterxml/jackson/jackson-parent/2.20/jackson-parent-2.20.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-tDt/XGLoaxZPrnCuF9aRHF22B5mvAQVzYK/aguSEW+U="
},
{
"mvn-path": "com/fasterxml/oss-parent/70/oss-parent-70.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-JsqO1vgsnS7XzTIpgQW7ZcD52JnbYXV6CXQVhvqTpjk="
},
{
"mvn-path": "org/clojure/clojure/1.10.3/clojure-1.10.3.jar",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-fxJHLa7Y9rUXSYqqKrE6ViR1w+31FHjkWBzHYemJeaM="
},
{
"mvn-path": "org/clojure/clojure/1.10.3/clojure-1.10.3.pom",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-GJwAxDNAdJai+7DsyzeQjJSVXZHq0b5IFWdE7MGBbZQ="
},
{
"mvn-path": "org/clojure/clojure/1.11.0/clojure-1.11.0.jar",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-PiH6daB+yd278bK1A1bPGAcQ0DmN6qT0TpHNYwRVWUc="
},
{
"mvn-path": "org/clojure/clojure/1.11.0/clojure-1.11.0.pom",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-SQjMS0yeYsmoFJb5PLWsb2lBd8xkXc87jOXkkavOHro="
},
{
"mvn-path": "org/clojure/clojure/1.11.1/clojure-1.11.1.jar",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-I4G26UI6tGUVFFWUSQPROlYkPWAGuRlK/Bv0+HEMtN4="
},
{
"mvn-path": "org/clojure/clojure/1.11.1/clojure-1.11.1.pom",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-IMRaGr7b2L4grvk2BQrjGgjBZ0CzL4dAuIOM3pb/y4o="
},
{
"mvn-path": "org/clojure/clojure/1.11.2/clojure-1.11.2.jar",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-iPqZkT1pIs+39kn1xGdQOHfLb8yMwW02948mSAhLqZc="
},
{
"mvn-path": "org/clojure/clojure/1.11.2/clojure-1.11.2.pom",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-FzbP/xCV4dT+/raogrut9ttB7+MV8pbw/aMtt//EExE="
},
{
"mvn-path": "org/clojure/clojure/1.11.3/clojure-1.11.3.jar",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-nDBUCTKOK5boXdK160t1gQxnt2unCuTQ9t3pvPtVsbc="
},
{
"mvn-path": "org/clojure/clojure/1.11.3/clojure-1.11.3.pom",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-DA2+Ge4NKpxXMQzr3dNWRD8NFlFMQmBHsGLjpXwNuK0="
},
{
"mvn-path": "org/clojure/clojure/1.11.4/clojure-1.11.4.jar",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-/H/xtmENDjSUp1zBHvgYEL2kAqwVcBL+TjuJlYbPQTM="
},
{
"mvn-path": "org/clojure/clojure/1.11.4/clojure-1.11.4.pom",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-a6YADmhI+Cw5y5tJqyqmo6Vi9MJNUrMeUZCuZJXwwwk="
},
{
"mvn-path": "org/clojure/clojure/1.12.0/clojure-1.12.0.jar",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-xFMzAGRBoFnqn9sTQfxsH0C5IaENzNgmZTEeSKA4R2M="
},
{
"mvn-path": "org/clojure/clojure/1.12.0/clojure-1.12.0.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-KfRiqonLl2RXWEGKXwjUwagrc1yW569JgX0WqpuQgVA="
},
{
"mvn-path": "org/clojure/core.match/1.1.0/core.match-1.1.0.jar",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-10V6tjEIWae9cTmEM4IEX6PN7A0T97qSEpfy8/uZj1M="
},
{
"mvn-path": "org/clojure/core.match/1.1.0/core.match-1.1.0.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-NnHYN2UlIwq6Ah8fYmx54g86ELYrXfgXIiWJDsSv4EU="
},
{
"mvn-path": "org/clojure/core.specs.alpha/0.2.56/core.specs.alpha-0.2.56.jar",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-/PRCveArBKhj8vzFjuaiowxM8Mlw99q4VjTwq3ERZrY="
},
{
"mvn-path": "org/clojure/core.specs.alpha/0.2.56/core.specs.alpha-0.2.56.pom",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-AarxdIP/HHSCySoHKV1+e8bjszIt9EsptXONAg/wB0A="
},
{
"mvn-path": "org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.jar",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-Bu6owHC75FwVhWfkQ0OWgbyMRukSNBT4G/oyukLWy8g="
},
{
"mvn-path": "org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.pom",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-F3i70Ti9GFkLgFS+nZGdG+toCfhbduXGKFtn1Ad9MA4="
},
{
"mvn-path": "org/clojure/core.specs.alpha/0.4.74/core.specs.alpha-0.4.74.jar",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-63OsCM9JuoQMiLpnvu8RM2ylVDM9lAiAjXiUbg/rnds="
},
{
"mvn-path": "org/clojure/core.specs.alpha/0.4.74/core.specs.alpha-0.4.74.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-M0EOuKpz1S2Vez3G4KZfOZisBiPL2BPZDDPm5onEJCk="
},
{
"mvn-path": "org/clojure/pom.contrib/0.3.0/pom.contrib-0.3.0.pom",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-fxgrOypUPgV0YL+T/8XpzvasUn3xoTdqfZki6+ee8Rk="
},
{
"mvn-path": "org/clojure/pom.contrib/1.1.0/pom.contrib-1.1.0.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-EOzku1+YKQENwWVh9C67g7ry9HYFtR+RBbkvPKoIlxU="
},
{
"mvn-path": "org/clojure/pom.contrib/1.2.0/pom.contrib-1.2.0.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-CRbXpBVYuVAKQnyIb6KYJ6zlJZIGvjrTPmTilvwaYRE="
},
{
"mvn-path": "org/clojure/spec.alpha/0.2.194/spec.alpha-0.2.194.jar",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-z2iZ+YUpjGSxPqEplGrZAo3uja3w6rmuGORVAn04JJw="
},
{
"mvn-path": "org/clojure/spec.alpha/0.2.194/spec.alpha-0.2.194.pom",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-WhHw4eizwFLmUcSYxpRbRNs1Nb8sGHGf3PZd8fiLE+Y="
},
{
"mvn-path": "org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.jar",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-Z+yJjrVcZqlXpVJ53YXRN2u5lL2HZosrDeHrO5foquA="
},
{
"mvn-path": "org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.pom",
"mvn-repo": "https://repo.maven.apache.org/maven2/",
"hash": "sha256-bY3hTDrIdXYMX/kJVi/5hzB3AxxquTnxyxOeFp/pB1g="
},
{
"mvn-path": "org/clojure/spec.alpha/0.5.238/spec.alpha-0.5.238.jar",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-lM2ZtupjlkHzevSGCmQ7btOZ7lqL5dcXz/C2Y8jXUHc="
},
{
"mvn-path": "org/clojure/spec.alpha/0.5.238/spec.alpha-0.5.238.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-PLp+DcwIXEzpLd3/6iJhJP+sF4vnm9A3m1suMKlpy+o="
},
{
"mvn-path": "org/clojure/tools.logging/1.3.0/tools.logging-1.3.0.jar",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-gmlpt42a2jJ95rfaDxdkV9lWFPo4woAyZhDzGmtRXJE="
},
{
"mvn-path": "org/clojure/tools.logging/1.3.0/tools.logging-1.3.0.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-E15H98p5wKoYG2kJPML50aYyKt1qgli2aXxQCNIwv00="
},
{
"mvn-path": "org/junit/junit-bom/5.13.4/junit-bom-5.13.4.pom",
"mvn-repo": "https://repo1.maven.org/maven2/",
"hash": "sha256-16CKmbJQLwu2jNTh+YTwv2kySqogi9D3M2bAP8NUikI="
},
{
"mvn-path": "tigris/tigris/0.1.2/tigris-0.1.2.jar",
"mvn-repo": "https://repo.clojars.org/",
"hash": "sha256-SapkjttsFOVwlaEbOR6u5gZXgyP7eXVfkjMaxjAPl6A="
},
{
"mvn-path": "tigris/tigris/0.1.2/tigris-0.1.2.pom",
"mvn-repo": "https://repo.clojars.org/",
"hash": "sha256-H9VZA1l1INzUrnbmoz7/XjWmFUIrutKo7ZrDMqr75KA="
}
]
}

View File

@@ -1,11 +0,0 @@
{:deps {org.clojure/tools.logging {:mvn/version "1.3.0"}
babashka/fs {:mvn/version "0.5.24"}
org.clojure/core.match {:mvn/version "1.1.0"}
cheshire/cheshire {:mvn/version "6.1.0"}
babashka/process {:mvn/version "0.6.25"}
io.github.msyds/spec-dict
{:git/sha "531d629b7f05f37232261cf9e8927a4b5915714f"}
hiccup/hiccup {:mvn/version "2.0.0-RC4"}
com.rpl/specter {:mvn/version "1.1.6"}
lambdaisland/deep-diff2 {:mvn/version "2.12.219"}}
:paths ["src" "resources" "test"]}

View File

@@ -1,34 +0,0 @@
{ mkCljBin
, callPackage
, doerg-parser
, ibm-plex-web
, fake-git
}:
let
# mkCljBin sans fake-git.
mkCljBin' = args: (mkCljBin args).overrideAttrs (final: prev: {
nativeBuildInputs =
builtins.filter
# A possibly-sketchy predicate, lol.
(x: x != fake-git)
prev.nativeBuildInputs;
});
plex = ibm-plex-web.override {
families = [ "math" "serif" "sans" "sans-kr" ];
};
in mkCljBin' {
name = "net.deertopia/doerg";
version = "0.1.0";
projectSrc = ./.;
lockfile = ../deps-lock.json;
main-ns = "net.deertopia.doerg.main";
nativeBuildInputs = [
plex
];
buildInputs = [
doerg-parser
plex
];
}

View File

@@ -1,19 +0,0 @@
(ns net.deertopia.doerg.config
(:require [clojure.spec.alpha :as s]
[babashka.fs :as fs]
[spec-dict.main :refer [dict]]))
(s/def ::config
(s/keys :req [::ibm-plex-web]))
(def default
{::ibm-plex-web
(fs/file
(or (System/getenv "IBM_PLEX_WEB")
(some #(let [x (fs/path % "ibm-plex-web")]
(and (fs/exists? x) x))
(fs/split-paths (System/getenv "XDG_DATA_DIRS")))))})
(def ^:dynamic *cfg* default)
(s/def ::ibm-plex-web string?)

View File

@@ -1,39 +0,0 @@
(ns net.deertopia.doerg.repl
(:require [net.deertopia.doerg.element :as element]
[net.deertopia.doerg.render :as render]
[net.deertopia.doerg.config :as cfg]
[clojure.java.io :as io]
[hiccup2.core :as h]
[clojure.pprint]
[babashka.fs :as fs]))
(def some-org-file
#_
"/home/msyds/org/20251228003307-prerequisite_context_in_korean.org"
"/home/msyds/org/20251111182118-path_induction.org")
(defn- force-create-sym-link [path target]
(fs/delete-if-exists path)
(fs/create-sym-link path target))
(defn render-html [& {:keys [src dest]
:or {src some-org-file
dest "/tmp/doerg-test"}}]
(fs/create-dirs dest)
(force-create-sym-link (fs/file dest "ibm-plex-web")
(-> cfg/*cfg* ::cfg/ibm-plex-web))
(force-create-sym-link (fs/file dest "deerstar.css")
(io/resource "net/deertopia/doerg/deerstar.css"))
(force-create-sym-link (fs/file dest "tuftesque.css")
(io/resource "net/deertopia/doerg/tuftesque.css"))
(fs/delete-if-exists (fs/file dest "index.html"))
(->> (h/html (-> src slurp element/read-string render/org-document))
str (spit (fs/file dest "index.html"))))
(defn render-edn [& {:keys [src dest]
:or {src some-org-file
dest "/tmp/doerg-test/index.edn"}}]
(fs/create-dirs (fs/parent dest))
(with-open [f (io/writer dest)]
(binding [*out* f]
(-> src slurp element/read-string clojure.pprint/pprint))))

View File

@@ -1,8 +0,0 @@
(ns net.deertopia.doerg.config-test
(:require [net.deertopia.doerg.config :as sut]
[clojure.test :as t]
[clojure.spec.alpha :as s]))
(t/deftest default-config-is-config
(t/testing "default config is valid"
(t/is (s/valid? ::sut/config sut/default))))

View File

@@ -1,79 +0,0 @@
(ns net.deertopia.doerg.element-test
(:require [net.deertopia.doerg.element :as sut]
[babashka.process :as p]
[clojure.test :as t]
[clojure.zip :as z]
[clojure.java.io :as io]
[com.rpl.specter :as sp]))
(defn sleep-vs-timeout [& {:keys [sleep timeout]}]
(sut/deref-with-timeout
(p/process "sleep" (format "%ds" sleep))
(* timeout 1000)))
;; Ideally we would test the following property:
;;
;; For natural numbers n and m, evaluating the form
;; (sut/deref-with-timeout
;; (p/process "sleep" (format "%ds" n))
;; (* m 1000))
;; will throw an exception iff n < m (probably with some margin of
;; error lol).
;;
;; But, this is not something that we want to run dozens-to-hundreds
;; of times. }:p
(t/deftest long-sleep-vs-short-timeout
(t/testing "long sleep vs. short timeout"
(t/is (thrown-with-msg?
Exception #".*timed out.*"
(sleep-vs-timeout :sleep 5 :timeout 1)))))
(t/deftest short-sleep-vs-long-timeout
(t/testing "short sleep vs. long timeout"
(t/is (instance? babashka.process.Process
(sleep-vs-timeout :sleep 1 :timeout 5)))))
(defn- first-child-of-type [parent type]
(some #(and (sut/of-type? % type) %) (:children parent)))
(defn- parse-resource [path]
(-> (str "net/deertopia/doerg/element_test/" path)
io/resource slurp sut/read-string))
(t/deftest known-greater-elements
(t/testing "known greater elements satisfy `greater-element?`"
(let [root (parse-resource "greater-elements.org")
section (->> root
(sp/select [sut/children-walker
#(sut/of-type? % "section")])
second)
headline (first-child-of-type section "headline")
headline-text (first-child-of-type headline "text")
paragraph (first-child-of-type section "paragraph")
paragraph-text (first-child-of-type paragraph "text")]
(t/is (sut/greater-element? root))
(t/is (sut/greater-element? section))
(t/is (sut/greater-element? headline))
(t/is (not (sut/greater-element? headline-text)))
(t/is (sut/greater-element? paragraph))
(t/is (not (sut/greater-element? paragraph-text))))))
(defn- first-paragraph-belongs-to-first-section? [doc]
(let [first-paragraph (sp/select-first [sut/postorder-walker
#(sut/of-type? % "paragraph")]
doc)
first-section (sp/select-first [sut/postorder-walker
#(sut/of-type? % "section")]
doc)]
(if (and first-paragraph first-section)
(boolean (some #(= % first-paragraph)
(:children first-section)))
true)))
(t/deftest first-paragraph-under-first-section
(t/testing "first paragraph should belong to a section"
(t/is (-> (parse-resource "first-paragraph-under-first-section.org")
first-paragraph-belongs-to-first-section?))
(t/is (not (-> (parse-resource "first-paragraph-under-heading.org")
first-paragraph-belongs-to-first-section?)))))

73
flake.lock generated
View File

@@ -59,24 +59,6 @@
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-fetcher-data": {
"inputs": {
"flake-parts": "flake-parts",
@@ -129,11 +111,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1760038930,
"narHash": "sha256-Oncbh0UmHjSlxO7ErQDM3KM0A5/Znfofj2BSzlHLeVw=",
"lastModified": 1774386573,
"narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0b4defa2584313f3b781240b29d61f6f9f7e0df3",
"rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9",
"type": "github"
},
"original": {
@@ -143,25 +125,44 @@
"type": "github"
}
},
"root": {
"inputs": {
"clj-nix": "clj-nix",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
}
},
"systems": {
"nixpkgs_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"lastModified": 1750386251,
"narHash": "sha256-1ovgdmuDYVo5OUC5NzdF+V4zx2uT8RtsgZahxidBTyw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "076e8c6678d8c54204abcb4b1b14c366835a58bb",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"clj-nix": "clj-nix",
"nixpkgs": "nixpkgs_2",
"sydpkgs": "sydpkgs"
}
},
"sydpkgs": {
"inputs": {
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1767975357,
"narHash": "sha256-MDVh3/aVhkD1bh/r8c0gs9DL4e78CrUbUxOZHWlCwLM=",
"owner": "msyds",
"repo": "sydpkgs",
"rev": "233479ab277d47b1dbda202eafca50e61c659151",
"type": "github"
},
"original": {
"owner": "msyds",
"repo": "sydpkgs",
"type": "github"
}
}

View File

@@ -1,9 +1,8 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
clj-nix.url = "github:jlesquembre/clj-nix";
# clj-nix.url = "path:///home/msyds/git/clj-nix";
sydpkgs.url = "github:msyds/sydpkgs";
};
outputs = { self, nixpkgs, clj-nix, ... }@inputs:
@@ -16,14 +15,15 @@
];
each-system = f: nixpkgs.lib.genAttrs supportedSystems (system: f rec {
pkgs = import nixpkgs {
inherit system;
overlays = [
clj-nix.overlays.default
self.overlays.default
];
};
inherit (pkgs) lib;
pkgs = import nixpkgs {
inherit system;
overlays = [
inputs.sydpkgs.overlays.default
clj-nix.overlays.default
self.overlays.default
];
};
inherit (pkgs) lib;
inherit system;
});
in {
@@ -31,42 +31,45 @@
_pkgs = each-system ({ pkgs, ... }: pkgs);
packages = each-system ({ pkgs, ... }: {
inherit (pkgs) publisher doerg doerg-parser;
default = pkgs.publisher;
inherit (pkgs) doerg doerg-parser doerg-temml-worker;
default = pkgs.doerg;
});
overlays.default = final: prev:
let graal = x: final.mkGraalBin { cljDrv = x; };
vendored = final.callPackage ./vendor {};
in {
inherit (vendored) ibm-plex-web;
publisher = final.callPackage ./publisher/package.nix {};
doerg = final.callPackage ./doerg/package.nix {};
doerg-parser = final.callPackage ./doerg/doerg-parser/package.nix {};
};
in {
ibm-plex-web = final.callPackage ./ibm-plex-web.nix {};
doerg = final.callPackage ./. {};
doerg-parser = final.callPackage ./doerg-parser {};
doerg-temml-worker = final.callPackage ./doerg-temml-worker {};
our-tex = final.callPackage ./our-tex.nix {};
};
checks = each-system ({ pkgs, system, ... }: {
packages =
pkgs.linkFarmFromDrvs
"all"
(pkgs.lib.attrValues self.packages.${system});
build-all =
pkgs.linkFarmFromDrvs
"all"
(pkgs.lib.attrValues self.packages.${system});
});
devShells = each-system ({ pkgs, system, ... }: {
default = pkgs.mkShell {
inputsFrom = [ pkgs.doerg ];
packages = with pkgs; [
clojure-lsp
doerg-parser
# wahhh ibm-plex-web is a dependency of doerg... why must
# i specify it hereeee.
# ibm-plex-web
zprint
clojure
babashka
sqlite-web
];
};
default = pkgs.mkShell {
inputsFrom = [
pkgs.doerg
pkgs.doerg-parser
pkgs.doerg-temml-worker
];
packages = with pkgs; [
clojure-lsp
doerg-parser
doerg-temml-worker
zprint
clojure
babashka
sqlite-web
pkgs.doerg.test-emacs
];
};
});
};
}

44
our-tex.nix Normal file
View File

@@ -0,0 +1,44 @@
{ texlive
, syd-plex-latex
}:
texlive.combine {
inherit syd-plex-latex;
inherit (texlive)
scheme-basic
xkeyval
changepage
fancyhdr
geometry
natbib
paralist
placeins
ragged2e
setspace
textcase
titlesec
xcolor
hardwrap
xifthen
catchfile
fontspec
latexmk
xetex
dvisvgm dvipng wrapfig # For Org-mode previews/export.
amsmath # Essential for mathematics.
spath3 # TikZ dependency?
ulem hyperref
capt-of
pgf # Includes TikZ.
tikz-cd # Commutative diagrams w/ TikZ.
quiver # Commutative diagrams w/ TikZ & q.uiver.app.
bookmark
metafont
preview # For new-gen org-latex-preview.
mylatexformat # For new-gen org-latex-preview.
collection-fontsrecommended # Essential fonts.
etoolbox # For Org-mode exports.
caption
standalone
;
}

View File

@@ -1,11 +0,0 @@
((nil
. ((cider-clojure-cli-aliases . ":dev")
(eval
. (progn
(defun start-deertopia-server ()
(let ((n (cider-format-connection-params "%j" cider-launch-params)))
(when (equal n "net-deertopia")
(cider-interactive-eval
"(do (require '[net.deertopia.publisher.server :as server])
(server/start!))"))))
(add-hook 'cider-connected-hook #'start-deertopia-server))))))

View File

@@ -1,23 +0,0 @@
{:deps {org.clojure/clojure {:mvn/version "1.12.0"}
http-kit/http-kit {:mvn/version "2.8.0"}
org.clojure/tools.logging {:mvn/version "1.3.0"}
hiccup/hiccup {:mvn/version "2.0.0-RC4"}
compojure/compojure {:mvn/version "1.7.1"}
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
ring/ring-defaults {:mvn/version "0.6.0"}
babashka/fs {:mvn/version "0.5.24"}
org.clojure/core.match {:mvn/version "1.1.0"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.1070"}
org.xerial/sqlite-jdbc {:mvn/version "3.47.1.0"}
cheshire/cheshire {:mvn/version "6.1.0"}
instaparse/instaparse {:mvn/version "1.5.0"}
babashka/process {:mvn/version "0.6.25"}
org.clojure/test.check {:mvn/version "1.1.2"}
#_#_
io.github.msyds/spec-dict
{:git/sha "531d629b7f05f37232261cf9e8927a4b5915714f"}
net.deertopia/doerg {:local/root "../doerg"}}
:paths ["src" "resources" "test"]
:aliases
{:dev
{:extra-deps {vvvvalvalval/scope-capture-nrepl {:mvn/version "0.3.1"}}}}}

View File

@@ -1,53 +0,0 @@
;;; -*- mode:clojure -*-
;;;
;;; USAGE:
;;;
;;; bb -cp . -m override-deps -- [DEP-NAME INFO]…
;;;
;;; This script takes a series of deps substitutions on the command
;;; line, applies them to a deps.edn file (read on stdin), and spits
;;; the result to stdout.
;;;
;;; It is used to build the Doerg server with Nix, since Clj-nix does
;;; not resolve the local deps itself.
(ns override-deps
(:require [rewrite-clj.zip :as z]
[babashka.fs :as fs]
[clojure.edn :as edn]))
(defn apply-overrides [zloc overrides]
(loop [os (seq overrides)
loc zloc]
(if-some [[[k v] & xs] os]
(recur xs (z/assoc loc k v))
loc)))
(defn args->overrides [args]
(assert (even? (count args)))
(->> args (map edn/read-string) (apply hash-map)))
(defn -main [& args]
(let [zloc (-> (slurp *in*) z/of-string)
overrides (args->overrides args)]
(-> zloc
z/down
(z/find-value :deps) z/right
(apply-overrides overrides)
z/root-string
print)))
(comment
"Example overrides"
(def overrides '{net.deertopia/doerg "blah!!!!"
ring/ring-defaults {:mvn/version "xxxxx"}}))
(comment
"Behaviour of `args->overrides`."
(= (args->overrides ["net.deertopia/doerg" "{:mvn/version \"abc\"}"
"ring/ring-defaults" "{:local/root \"/path/to/jar\"}"])
'{ring/ring-defaults {:local/root "/path/to/jar"}
net.deertopia/doerg {:mvn/version "abc"}}))

View File

@@ -1,21 +0,0 @@
{ mkCljBin
, doerg
, babashka
}:
mkCljBin {
name = "net.deertopia/publisher";
version = "0.1.0";
projectSrc = ./.;
lockfile = ../deps-lock.json;
main-ns = "net.deertopia.publisher.main";
buildInputs = [];
nativeBuildInputs = [
babashka
];
postPatch = ''
mv deps.edn deps.edn.old
bb -cp . -m override-deps < deps.edn.old > deps.edn \
net.deertopia/doerg '{:local/root "${doerg.lib}/${doerg.name}.jar"}'
'';
}

View File

@@ -1,7 +0,0 @@
(ns net.deertopia.publisher.main
(:require [net.deertopia.doerg.main :as doerg])
(:gen-class))
(defn -main []
(doerg/-main)
(println "hi from publisher"))

View File

@@ -1,25 +0,0 @@
{ fetchzip
, fetchurl
}:
{
ibm-plex-serif = fetchzip {
url = "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-serif%401.1.0/ibm-plex-serif.zip";
hash = "sha256-8ygaAeMKygYS4GCub4YUQmkh87pYHfi3s0PQ6AbaeGw=";
};
ibm-plex-math = fetchzip {
url = "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-math%401.1.0/ibm-plex-math.zip";
hash = "sha256-dJA6uqxa/yb3eLY4l39NeP0yIl2NfrbaRpf6h0/F7Xc=";
};
ibm-plex-sans-kr = fetchzip {
url = "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-sans-kr%401.1.0/ibm-plex-sans-kr.zip";
hash = "sha256-FsHxMvLlI4yylgG96DOZIdW2DYpk7I+c5QgkVIkNZIE=";
};
"d3.v7.min.js" = fetchurl {
url = "https://d3js.org/d3.v7.min.js";
hash = "sha256-8glLv2FBs1lyLE/kVOtsSw8OQswQzHr5IfwVj864ZTk=";
};
}

View File

@@ -0,0 +1,343 @@
/* Based on Temml-local.css. */
math {
font-family: "IBM Plex Math", "Cambria Math", 'STIXTwoMath-Regular', 'NotoSansMath-Regular', math;
font-style: normal;
font-weight: normal;
line-height: normal;
font-size-adjust: none;
text-indent: 0;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
direction: ltr;
/* Prevent Firefox from omitting the dot on i or j. */
font-feature-settings: "dtls" off;
}
math * {
border-color: currentColor;
}
/* display: block is necessary in Firefox and Safari.
* Not in Chromium, which recognizes display: "block math" written inline. */
math.tml-display {
display: block;
width: 100%;
}
*.mathcal {
/* NotoSans */
font-feature-settings: 'ss01';
}
math .mathscr {
font-family: "IBM Plex Math";
}
mo.tml-prime {
font-family: "IBM Plex Math";
}
/* Cramped superscripts in WebKit */
mfrac > :nth-child(2),
msqrt,
mover > :first-child {
math-shift: compact
}
.menclose {
display: inline-block;
position: relative;
padding: 0.5ex 0ex;
}
.tml-cancelto {
display: inline-block;
position: absolute;
top: 0;
left: 0;
padding: 0.5ex 0ex;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><defs><marker id='a' markerHeight='5' markerUnits='strokeWidth' markerWidth='7' orient='auto' refX='7' refY='2.5'><path fill='currentColor' d='m0 0 7 2.5L0 5z'/></marker></defs><line x2='100%' y1='100%' stroke='currentColor' stroke-width='.06em' marker-end='url(%23a)' vector-effect='non-scaling-stroke'/></svg>");
}
@supports (-moz-appearance: none) {
/* \vec w/o italic correction for Firefox */
.tml-vec {
transform: scale(0.75)
}
/* Fix \cancelto in Firefox */
.ff-narrow {
width: 0em;
}
.ff-nudge-left {
margin-left: -0.2em;
}
}
@supports (not (-moz-appearance: none)) {
/* Chromium and WebKit */
/* prime vertical alignment */
mo.tml-prime {
font-family: "IBM Plex Math";
}
/* Italic correction on superscripts */
.tml-sml-pad {
padding-left: 0.05em;
}
.tml-med-pad {
padding-left: 0.10em;
}
.tml-lrg-pad {
padding-left: 0.15em;
}
}
@supports (-webkit-backdrop-filter: blur(1px)) {
/* WebKit vertical & italic correction on accents */
.wbk-acc {
/* lower by x-height distance */
transform: translate(0em, 0.431em);
}
.wbk-sml {
transform: translate(0.07em, 0);
}
.wbk-sml-acc {
transform: translate(0.07em, 0.431em);
}
.wbk-sml-vec {
transform: scale(0.75) translate(0.07em, 0);
}
.wbk-med {
transform: translate(0.14em, 0);
}
.wbk-med-acc {
transform: translate(0.14em, 0.431em);
}
.wbk-med-vec {
transform: scale(0.75) translate(0.14em, 0);
}
.wbk-lrg {
transform: translate(0.21em, 0);
}
.wbk-lrg-acc {
transform: translate(0.21em, 0.431em);
}
.wbk-lrg-vec {
transform: scale(0.75) translate(0.21em, 0);
}
}
/* \cancel & \phase use background images. Get them to print. */
menclose {
-webkit-print-color-adjust: exact; /* Chrome & Edge */
print-color-adjust: exact;
}
/* Array cell justification in Firefox & WebKit */
.tml-right {
text-align: right;
}
.tml-left {
text-align: left;
}
/* For CD labels that grow to the left in Firefox and WebKit */
.tml-shift-left { margin-left:-200% }
/* Styles for Chromium only */
@supports (not (-webkit-backdrop-filter: blur(1px))) and (not (-moz-appearance: none)) {
/* Italic correction on accents */
.chr-sml {
transform: translate(0.07em, 0)
}
.chr-sml-vec {
transform: scale(0.75) translate(0.07em, 0)
}
.chr-med {
transform: translate(0.14em, 0)
}
.chr-med-vec {
transform: scale(0.75) translate(0.14em, 0)
}
.chr-lrg {
transform: translate(0.21em, 0)
}
.chr-lrg-vec {
transform: scale(0.75) translate(0.21em, 0)
}
/* For CD labels that grow to the left */
.tml-shift-left { margin-left:-100% }
/* MathML Core & Chromium do not support the MathML 3.0 element <menclose> attributes. */
/* So use styles. */
menclose {
position: relative;
padding: 0.5ex 0ex;
}
.tml-overline {
padding: 0.1em 0 0 0;
border-top: 0.065em solid;
}
.tml-underline {
padding: 0 0 0.1em 0;
border-bottom: 0.065em solid;
}
.tml-cancel {
display: inline-block;
position: absolute;
left: 0.5px;
bottom: 0;
width: 100%;
height: 100%;
background-color: currentColor;
}
.upstrike {
clip-path: polygon(0.05em 100%, 0em calc(100% - 0.05em), calc(100% - 0.05em) 0em, 100% 0.05em);
}
.downstrike {
clip-path: polygon(0em 0.05em, 0.05em 0em, 100% calc(100% - 0.05em), calc(100% - 0.05em) 100%);
}
.sout {
clip-path: polygon(0em calc(55% + 0.0333em), 0em calc(55% - 0.0333em), 100% calc(55% - 0.0333em), 100% calc(55% + 0.0333em));
}
.tml-xcancel {
background: linear-gradient(to top left,
rgba(0,0,0,0) 0%,
rgba(0,0,0,0) calc(50% - 0.06em),
rgba(0,0,0,1) 50%,
rgba(0,0,0,0) calc(50% + 0.06em),
rgba(0,0,0,0) 100%),
linear-gradient(to top right,
rgba(0,0,0,0) 0%,
rgba(0,0,0,0) calc(50% - 0.06em),
rgba(0,0,0,1) 50%,
rgba(0,0,0,0) calc(50% + 0.06em),
rgba(0,0,0,0) 100%)
}
.longdiv-top {
border-top: 0.067em solid;
padding: 0.1em 0.2em 0.2em 0.433em;
}
.longdiv-arc {
position: absolute;
top: 0;
bottom: 0.1em;
left: -0.4em;
width: 0.7em;
border: 0.067em solid;
transform: translateY(-0.067em);
border-radius: 70%;
clip-path: inset(0 0 0 0.4em);
box-sizing: border-box;}
.menclose {display: inline-block;
text-align: left;
position: relative;
}
.phasor-bottom {
border-bottom: 0.067em solid;
padding: 0.2em 0.2em 0.1em 0.6em;
}
.phasor-angle {
display: inline-block;
position: absolute;
left: 0.5px;
bottom: -0.04em;
height: 100%;
aspect-ratio: 0.5;
background-color: currentColor;
clip-path: polygon(0.05em 100%, 0em calc(100% - 0.05em), calc(100% - 0.05em) 0em, 100% 0.05em);
}
.tml-fbox {
padding: 3pt;
border: 1px solid;
}
.circle-pad {
padding: 0.267em;
}
.textcircle {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
border: 0.067em solid;
border-radius: 50%;
}
.actuarial {
padding: 0.03889em 0.03889em 0 0.03889em;
border-width: 0.08em 0.08em 0em 0em;
border-style: solid;
margin-right: 0.03889em;
}
/* Stretch \widetilde */
.tml-crooked-2 {
transform: scale(2.0, 1.1)
}
.tml-crooked-3 {
transform: scale(3.0, 1.3)
}
.tml-crooked-4 {
transform: scale(4.0, 1.4)
}
/* set array cell justification */
.tml-right {
text-align: -webkit-right;
}
.tml-left {
text-align: -webkit-left;
}
}
.special-fraction {
font-family: "IBM Plex Math", 'STIX TWO', 'Times New Roman', Times, Tinos, serif;
}
/* flex-wrap for line-breaking in Chromium */
math {
display: inline-flex;
flex-wrap: wrap;
align-items: baseline;
}
math > mrow {
padding: 0.5ex 0ex;
}
/* Default mtd top padding is 0.5ex per MathML-Core and user-agent CSS */
/* We adjust for jot and small */
mtable.tml-jot mtd {
padding-top: 0.7ex;
padding-bottom: 0.7ex;
}
mtable.tml-small mtd {
padding-top: 0.35ex;
padding-bottom: 0.35ex;
}
/* Firefox */
@-moz-document url-prefix() {
/* Avoid flex-wrap */
math { display: inline; }
math > mrow { padding: 0 }
/* Adjust Firefox spacing between array rows */
mtd, mtable.tml-small mtd { padding-top: 0; padding-bottom: 0; }
mtable.tml-jot mtd { padding-top: 0.2ex; padding-bottom: 0.ex; }
}
/* AMS environment auto-numbering via CSS counter. */
.tml-eqn::before {
counter-increment: tmlEqnNo;
content: "(" counter(tmlEqnNo) ")";
}
body {
counter-reset: tmlEqnNo;
}

View File

@@ -0,0 +1,18 @@
#:net.deertopia.doerg.config
{:ibm-plex-web #or [#xdg-data-dir "ibm-plex-web"
#env IBM_PLEX_WEB]
:latex "xelatex"
:dvisvgm "dvisvgm"
:doerg-temml-worker
#profile {:dev #file "../../../../doerg-temml-worker/index.js"
:default "doerg-temml-worker"}
:doerg-parser
#profile {:dev #file "../../../../doerg-parser/index.js"
:default "doerg-parser"}
:state-directory #join [#or [#env XDG_STATE_HOME
#envf ["%s/.local/share" HOME]]
"/doerg-server"]
:org-roam-db-path
#profile {:default #join [#env HOME "/.cache/emacs/org-roam.db"]
:test #join [#or [#env TMP "/tmp"] "/doerg-org-roam-test.db"]}
:port #profile {:default 8080}}

View File

@@ -0,0 +1,30 @@
<file> ::= elements
<elements> ::= ws? element ws? elements
| ws? ε
<element> ::= string
| list
| symbol
| integer
| property-string
string ::= <'"'> #'([^"\\]|\\.|\\\n)*' <'"'>
property-string
::= <'#('> ws? string text-property* ws? <')'>
text-property ::= ws? element ws? element ws? element
list ::= <'('> elements dot-cdr? <')'>
symbol ::= #'([^?#0-9 \n\s\f()\[\]"\'\\.]|\\.)([^ \n\s\f()\[\]"\'\\]|\\.)*'
| #'\.([^ \n\s\f()\[\]"\'\\]|\\.)+'
integer ::= #'[-+]?[0-9]+' <#'.'>?
dot-cdr ::= <'.'> ws? element
<text> ::= ws? (string | property-string) ws?
<ws> ::= <#'(\s| |\n)'>+

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,5 @@
% Default uses arrow glyphs from the active font, which are kinda ugly in the
% case of Plex.
\tikzcdset{
arrow style=tikz
}

View File

@@ -0,0 +1,154 @@
% Of the highest importance:
\renewcommand{\phi}{\varphi}
\renewcommand{\epsilon}{\varepsilon}
% Saner abbreviations for font faces (take an argument rather than
% applying to the current context).
\renewcommand{\rm}[1]{\mathrm{#1}}
\newcommand{\bb}[1]{\mathbb{#1}}
\renewcommand{\bf}[1]{\mathbf{#1}}
% \cA … \cZ
\newcommand{\cA}{\mathcal{A}}
\newcommand{\cB}{\mathcal{B}}
\newcommand{\cC}{\mathcal{C}}
\newcommand{\cD}{\mathcal{D}}
\newcommand{\cE}{\mathcal{E}}
\newcommand{\cF}{\mathcal{F}}
\newcommand{\cG}{\mathcal{G}}
\newcommand{\cH}{\mathcal{H}}
\newcommand{\cI}{\mathcal{I}}
\newcommand{\cJ}{\mathcal{J}}
\newcommand{\cK}{\mathcal{K}}
\newcommand{\cL}{\mathcal{L}}
\newcommand{\cM}{\mathcal{M}}
\newcommand{\cN}{\mathcal{N}}
\newcommand{\cO}{\mathcal{O}}
\newcommand{\cP}{\mathcal{P}}
\newcommand{\cQ}{\mathcal{Q}}
\newcommand{\cR}{\mathcal{R}}
\newcommand{\cS}{\mathcal{S}}
\newcommand{\cT}{\mathcal{T}}
\newcommand{\cU}{\mathcal{U}}
\newcommand{\cV}{\mathcal{V}}
\newcommand{\cW}{\mathcal{W}}
\newcommand{\cX}{\mathcal{X}}
\newcommand{\cY}{\mathcal{Y}}
\newcommand{\cZ}{\mathcal{Z}}
% \bA … \bZ
\newcommand{\bA}{\mathbb{A}}
\newcommand{\bB}{\mathbb{B}}
\newcommand{\bC}{\mathbb{C}}
\newcommand{\bD}{\mathbb{D}}
\newcommand{\bE}{\mathbb{E}}
\newcommand{\bF}{\mathbb{F}}
\newcommand{\bG}{\mathbb{G}}
\newcommand{\bH}{\mathbb{H}}
\newcommand{\bI}{\mathbb{I}}
\newcommand{\bJ}{\mathbb{J}}
\newcommand{\bK}{\mathbb{K}}
\newcommand{\bL}{\mathbb{L}}
\newcommand{\bM}{\mathbb{M}}
\newcommand{\bN}{\mathbb{N}}
\newcommand{\bO}{\mathbb{O}}
\newcommand{\bP}{\mathbb{P}}
\newcommand{\bQ}{\mathbb{Q}}
\newcommand{\bR}{\mathbb{R}}
\newcommand{\bS}{\mathbb{S}}
\newcommand{\bT}{\mathbb{T}}
\newcommand{\bU}{\mathbb{U}}
\newcommand{\bV}{\mathbb{V}}
\newcommand{\bW}{\mathbb{W}}
\newcommand{\bX}{\mathbb{X}}
\newcommand{\bY}{\mathbb{Y}}
\newcommand{\bZ}{\mathbb{Z}}
% Notation for lambda abstractions and function application. $~$
% is the spacing that should be used between arguments.
\newcommand{\lam}[2]{\lambda #1.\ #2}
% Notation for paths, the interval, and its operations.
\newcommand{\PathP}[3]{\rm{PathP}~ {#1}~ {#2}~ {#3}}
\newcommand{\Path}[3]{\rm{Path}~ {#1}~ {#2}~ {#3}}
\newcommand{\Square}[4]{\rm{Square}~ {#1}~ {#2}~ {#3}~ {#4}}
\newcommand{\ap}[2]{\rm{ap}~ {#1}~ {#2}}
\newcommand{\subst}{\operatorname{subst}}
\newcommand{\ua}{\operatorname{ua}}
\newcommand{\iZ}{\rm{i0}}
\newcommand{\iO}{\rm{i1}}
\newcommand{\ineg}{\lnot}
\newcommand{\imin}{\land}
\newcommand{\imax}{\lor}
\newcommand{\transport}[2]{\rm{transport}~ #1~ #2}
\newcommand{\transp}[3]{\rm{transp}~ #1~ #2~ #3}
\newcommand{\Partial}[2]{\rm{Partial}~ #1~ #2}
\newcommand{\dcomp}{\mathrel{\cdot\cdot}}
\newcommand{\Extn}[2]{{#1}[#2]}
\newcommand{\thecat}[1]{\mathbf{#1}} % Names of "concrete" categories
\newcommand{\ca}[1]{\mathcal{#1}} % Names of variable categories
%% \newcommand{\knowncat}[1]{\newcommand{\csname #1\endcsname}{\thecat{#1}}}
%% \newcommand{\knownbicat}[1]{\newcommand{\csname #1\endcsname}{\thebicat{#1}}}
\newcommand{\ty}{\rm{Type}}
%% \newcommand{\set}{\rm{Set}}
\newcommand{\prop}{\rm{Prop}}
%% \DeclareMathOperator{\id}{id}
%% \DeclareMathOperator{\Id}{Id}
%% \knowncat{Sets}
%% \knowncat{Ab}
%% \knowncat{Graphs}
%% \knowncat{MarkedGraphs}
%% \knowncat{Props}
%% \knowncat{FinSets}
%% \knowncat{Rings}
%% \knowncat{Grp}
%% \knowncat{Rel}
%% \knowncat{Par}
%% \knowncat{Pos}
\renewcommand{\Set}{\thecat{Set}}
% "Postfix" operators that have a subscript and are annoying to write
% out:
\newcommand{\inv}{^{-1}}
\renewcommand{\op}{^{\rm{op}}}
\newcommand{\eps}{\varepsilon}
\newcommand{\B}[1]{\mathbf{B} #1}
\newcommand{\point}[1]{\bullet_{#1}}
\newcommand{\List}[1]{\operatorname{List}(#1)}
\renewcommand{\hom}{\mathbf{Hom}}
\newcommand{\refl}{\mathrm{refl}}
\DeclareMathOperator{\isiso}{is-iso}
\DeclareMathOperator{\isequiv}{is-equiv}
% Relations
\newcommand{\pathto}{\equiv}
\newcommand{\is}{\pathto}
\newcommand{\definedto}{}
\newcommand{\equivto}{\simeq}
\newcommand{\homotopicto}{\sim}
\newcommand{\homotopyto}{\sim}
\newcommand{\naturalto}{\Rightarrow}
\newcommand{\isoto}{\cong}
\newcommand{\monicto}{\rightarrowtail}
\newcommand{\epicto}{\twoheadrightarrow}
% Category names
\newcommand{\C}[1]{\mathbf{#1}}
\newcommand{\homset}[3]{{{#1} \left[ {#2} \to {#3} \right]}}
\newcommand{\horizontalcompose}{\ast}

View File

@@ -0,0 +1,19 @@
\documentclass[tikz,dvisvgm]{standalone}
\usepackage[active,tightpage,auctex,dvips]{preview}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{ifxetex}
\usepackage{syd-plex}
\usepackage{quiver}
\usepackage{tikz}
\usepackage{amscd}
\usepackage{metalogo}
% {{preamble}}
\begin{document}
\setlength\abovedisplayskip{0pt} % Remove padding before equation environments.
% {{contents}}
\end{document}

View File

@@ -0,0 +1 @@
../Temml-Plex.css

View File

@@ -0,0 +1 @@
../deerstar.css

View File

@@ -0,0 +1 @@
../tuftesque.css

View File

@@ -104,9 +104,10 @@ section {
p,
dl,
ol,
.latex-fragment,
ul {
font-size: 1.4rem;
line-height: 2rem;
font-size: 1.2rem;
line-height: 1.5rem;
}
p {
@@ -114,6 +115,7 @@ p {
margin-bottom: 1.4rem;
padding-right: 0;
vertical-align: baseline;
hyphens: auto;
}
/* Chapter Epigraphs */
@@ -219,7 +221,7 @@ img {
}
.sidenote,
.margin-note {
.marginnote {
float: right;
clear: right;
margin-right: -60%;
@@ -436,7 +438,7 @@ label.margin-toggle:not(.sidenote-number) {
}
}
.link.external::after
.org-link.external::after
{ content: "↗"
; vertical-align: super
; font-size: 1rem
@@ -444,8 +446,8 @@ label.margin-toggle:not(.sidenote-number) {
}
.center
{ align-items: "center"
; justify-content: "center"
{ align-items: center
; justify-content: center
; display: flex
; max-width: 55%
}
@@ -542,3 +544,22 @@ figure.fullwidth figcaption {
; max-width: 55%
; font-size: 1.5rem
}
.latex-fragment
{ fill: currentColor
}
.latex-fragment.display-math
{ display: block
/* Center it — do we want to do that? */
; align-items: center
; justify-content: center
; display: flex
; max-width: 55%
; width: 55%
}
p > .latex-fragment.display-math
{ max-width: 100%
; width: 100%
}

View File

@@ -0,0 +1,25 @@
(ns net.deertopia.doerg.cached-file
(:require [babashka.fs :as fs]))
(defn newer-than?
"Return `true` if fs `file₁` was last modified sooner or at the same
time as `file₂`, or if `file₂` does not exist."
[file file]
(or (not (fs/exists? file))
(<= 0 (compare (fs/last-modified-time file)
(fs/last-modified-time file)))))
(def ^:dynamic *use-cache?*
"Bind to `false` to disable caching for debugging purposes."
true)
(defn cached-file
"Return a file path after potentially regenerating the file by
calling `compute` with no arguments only if stale? is logical true."
[& {:keys [file stale? compute]}]
(when (or (not *use-cache?*) stale?)
(let [r (compute)]
(assert (string? r))
(fs/create-dirs (fs/parent file))
(spit file r)))
file)

View File

@@ -0,0 +1,125 @@
(ns net.deertopia.doerg.common
(:require [babashka.process :as p]
[clojure.string :as str]
[clojure.tools.logging :as l]
[clojure.java.io :as io])
(:import (java.io FilterInputStream StringWriter InputStream
OutputStream PrintStream ByteArrayOutputStream
ByteArrayInputStream FilterOutputStream)
(java.nio.charset StandardCharsets)))
(defn deref-with-timeout [process ms]
(let [p (promise)
process-future (future (deliver p @process))
timeout-future (future (Thread/sleep ms)
(future-cancel process-future)
(p/destroy-tree process)
(deliver p ::timed-out))]
(if (= @p ::timed-out)
(throw (ex-info (format "external command `%s' timed out after %.2fs."
(str/join " " (:cmd process))
(/ (double ms) 1000))
{:process process
:timed-out-after-milliseconds ms}))
@p)))
(defn tee-input-stream
"Return a wrapped `InputStream` that writes all bytes read from
input-stream to sink, à la the UNIX command tee(1)."
[input-stream sink]
(proxy [FilterInputStream] [input-stream]
(read
([]
(let [c (proxy-super read)]
(when (not= c -1)
(.write sink c))
c))
([^bytes bs]
(let [n (proxy-super read bs)]
(when (not= n -1)
(.write sink bs 0 n))
n))
([^bytes bs off len]
(let [n (proxy-super read bs off len)]
(when (not= n -1)
(.write sink bs off n))
n)))
(close []
(try (proxy-super close)
(finally (.close sink))))))
(defn tee-output-stream
"Return a wrapped `OutputStream` that writes all bytes written to
output-stream to sink, à la the UNIX command tee(1)."
[output-stream sink]
(proxy [FilterOutputStream] [output-stream]
(write
([bs-or-b]
(proxy-super write bs-or-b)
(.write sink bs-or-b))
([^bytes bs off len]
(proxy-super write bs off len)
(.write sink bs off len)))
(close []
(try (proxy-super close)
(finally (.close sink))))))
#_
(defn hook-input-stream [input-stream hook]
(proxy [FilterInputStream] [input-stream]
(read
([]
(let [c (proxy-super read)]
(when (not= c -1)
(hook (byte-array [c])))
c))
([^bytes bs]
(let [n (proxy-super read bs)]
(when (not= n -1)
(let [bs* (byte-array n 0)]
(System/arraycopy bs 0 bs* 0 n)
(hook bs*)))
n))
([^bytes bs off len]
(let [n (proxy-super read bs off len)]
(when (not= n -1)
(.write sink bs off n))
n)))
(close []
(try (proxy-super close)
(finally (.close sink))))))
(comment
(with-open [sink (ByteArrayOutputStream.)
out (ByteArrayOutputStream.)
in (ByteArrayInputStream. (.getBytes "hello worms"))]
(io/copy (tee-input-stream in sink) out)
(def the-out out)
(def the-sink sink)
{:out out
:sink sink})
(with-open [sink (l/log-stream :info "blah")
out (ByteArrayOutputStream.)
in (ByteArrayInputStream. (.getBytes "hello worms"))]
(io/copy (tee-input-stream in sink) out)
(def the-out out)
(def the-sink sink)
{:out out
:sink sink}))
(comment
(let [out (ByteArrayOutputStream.)]
(p/shell {:out (tee-output-stream
out (l/log-stream :info "blah"))}
"echo" "hello\n" "worms")
(.toString out)))
(defn invoke [opts & cmd]
(l/info (str/join " " (cons "$" cmd)))
(let [r (apply p/shell
(merge {:continue true
:in nil :out :string :err :string}
opts)
cmd)
bin (first cmd)]
r))

View File

@@ -0,0 +1,82 @@
(ns net.deertopia.doerg.config
(:require [clojure.spec.alpha :as s]
[babashka.fs :as fs]
[aero.core :as aero]
[clojure.java.io :as io]))
(s/def ::config
(s/keys :req [::ibm-plex-web
::latex
::dvisvgm
::doerg-temml-worker
::doerg-parser
::state-directory
::org-roam-db-path]))
(s/def ::directory
(s/conformer #(fs/file %)))
(s/def ::file
(s/conformer #(-> % fs/expand-home fs/file)))
(s/def ::executable
(s/conformer
;; I'd love to use `fs/which` here, but it's fairly problematic to
;; check `fs/executable?` at… build time (which `fs/which` does)?
;; Wait… what? Do I know how Clojure compilation works?
#(or #_(some-> % fs/expand-home fs/which fs/file)
(some-> % fs/expand-home fs/file)
::s/invalid)))
(s/def ::ibm-plex-web ::directory)
(s/def ::latex ::executable)
(s/def ::dvisvgm ::executable)
(s/def ::doerg-temml-worker ::executable)
(s/def ::doerg-parser ::executable)
(s/def ::state-directory ::file)
(s/def ::org-roam-db-path ::file)
(defmethod aero/reader 'xdg-data-dir
[_opts tag value]
"Aero tag to search for a directory on $XDG_DATA_DIRS."
(some #(let [x (fs/path % value)]
(and (fs/exists? x) x))
(fs/split-paths (System/getenv "XDG_DATA_DIRS"))))
(defmethod aero/reader 'file
[{:keys [source]} tag value]
"Aero tag to reference a `java.io.File` relative to the config
file."
(-> (aero/relative-resolver source value)
fs/file))
(defn read-config [files & {:as opts}]
(let [r (->> files
(filter identity)
(map #(aero/read-config % opts))
(apply merge))
conformed (s/conform ::config r)]
(if-not (s/invalid? conformed)
conformed
(throw (ex-info "Failed to conform config"
(s/explain-data ::config r))))))
(defn load-config! [var spec files & {:as opts}]
(alter-var-root var (constantly (read-config files opts))))
(def sources
[;; Default config.
(io/resource "net/deertopia/doerg/default-config.edn")
;; Defaults set at build time, if any.
(io/resource "net/deertopia/doerg/extra-config.edn")
;; Config set at runtime.
(System/getenv "DOERG_CONFIG")])
(def default (read-config sources))
(def ^:dynamic *cfg* default)

View File

@@ -1,39 +1,31 @@
(ns net.deertopia.doerg.element
(:require [babashka.process :as p]
[clojure.string :as str]
[clojure.zip]
[babashka.fs :as fs]
[clojure.java.io :as io]
(:refer-clojure :exclude [read-string type])
(:require [babashka.fs :as fs]
[babashka.process :as p]
[cheshire.core :as json]
[clojure.spec.alpha :as s]
[spec-dict.main :refer [dict]]
[net.deertopia.doerg.config :as cfg]
[clojure.core.match :refer [match]]
[net.deertopia.doerg :as-alias doerg]
[clojure.java.io :as io]
[clojure.set :as set]
[clojure.string :as str]
[clojure.test.check.generators :as gen]
[clojure.tools.logging.readable :as lr]
[clojure.zip :as z]
[com.rpl.specter :as sp]
[clojure.tools.logging.readable :as lr])
(:import (java.util UUID))
(:refer-clojure :exclude [read-string]))
[com.rpl.specter.zipper :as sz]
[net.deertopia.doerg.common :as common]
[net.deertopia.doerg.config :as cfg]
[clojure.tools.logging :as l])
(:import
(java.util UUID)))
(defonce ^:private uniorg-script-path-atom (atom nil))
(def ^:dynamic *uniorg-timeout-after-milliseconds*
(def ^:dynamic *uniorg-timeout-duration*
"Number of milliseconds to wait before killing the external Uniorg
process."
(* 10 1000))
(defn deref-with-timeout [process ms]
(let [p (promise)
process-future (future (deliver p @process))
timeout-future (future (Thread/sleep ms)
(future-cancel process-future)
(p/destroy-tree process)
(deliver p ::timed-out))]
(if (= @p ::timed-out)
(throw (ex-info (format "external command `%s' timed out after %.2fs."
(str/join " " (:cmd process))
(/ (double ms) 1000))
{:process process
:timed-out-after-milliseconds ms}))
@p)))
(defn- camel->kebab [s]
(->> (str/split s #"(?<=[a-z])(?=[A-Z])")
(map str/lower-case)
@@ -43,19 +35,21 @@
:or {in *in*}}]
(let [r (-> (p/process
{:in in :out :string}
"doerg-parser")
(deref-with-timeout *uniorg-timeout-after-milliseconds*))]
(if (zero? (:exit r))
(-> cfg/*cfg* ::doerg/doerg-parser str))
(common/deref-with-timeout *uniorg-timeout-duration*))]
(when (zero? (:exit r))
(-> r :out (json/parse-string (comp keyword camel->kebab))))))
(declare gather-first-section)
(declare gather-first-section gather-latex-paragraphs element-types)
(defn read-string [s & {:keys [post-processors]
:or {post-processors [gather-first-section]}}]
(defn read-string
[s & {:keys [post-processors]
:or {post-processors [gather-first-section
gather-latex-paragraphs]}}]
(let [apply-post-processors (apply comp (reverse post-processors))]
(with-in-str s
(-> (uniorg :in *in*)
apply-post-processors))))
(-> (uniorg :in *in*)
apply-post-processors))))
@@ -69,11 +63,12 @@
(and (map? e) (contains? e :children)))
(defn org-element? [element]
#_
(s/valid? ::org-element element)
(and (map? element)
(contains? element :type)))
(defn type [element]
(:type element))
(defn of-type?
"Return truthy if the Org node `element` is of type `type`. In the
vararg case, return truthy if `element` is of any of the types
@@ -93,15 +88,12 @@
(when-some [footnotes-headline (first (:children element))]
(= "Footnotes" (:raw-value footnotes-headline)))))
;;; Spec
(s/def ::org-element
(dict {:type string?}
^:opt {:contents-begin nat-int?
:contents-end nat-int?
:children (s/coll-of ::org-element
:kind seq?)}))
(defn display-math?
"Return truthy if `element` should be considered display math."
[element]
(or (of-type? element "latex-environment")
(and (of-type? element "latex-fragment")
(-> element :contents (str/starts-with? "\\[")))))
;;; Zipper
@@ -192,8 +184,8 @@
parser, return a map with the following keys
:top-level-nodes The nodes that /should/ be at the top-level.
:first-section-nodes The nodes that should be wrapped in a new
section node
:rest Everything else!"
section node.
:rest Everything else."
[nodes]
(let [[of-top-level remaining-nodes]
(->> nodes (split-with #(of-type? % "property-drawer" "keyword")))
@@ -203,6 +195,22 @@
:first-section-nodes of-first-section
:rest remaining-nodes*}))
(defn- element-bounds [& nodes]
(reduce (fn [acc {:keys [contents-begin contents-end]}]
(if (and (nat-int? contents-begin)
(nat-int? contents-end))
(-> acc
(update
:contents-begin
#(min (or % Integer/MAX_VALUE) contents-begin))
(update
:contents-end
#(max (or % Integer/MIN_VALUE) contents-end)))
acc))
{:contents-begin nil
:contents-end nil}
nodes))
(defn gather-first-section [node]
(assert (of-type? node "org-data")
"`gather-doerg-data` should be applied to the document root.")
@@ -210,8 +218,86 @@
(split-sections (:children node))
;; TODO: Construct `:contents-begin` and `:contents-end` data
;; by spanning the children.
new-children (concat top-level-nodes
(list {:type "section"
:children first-section-nodes})
rest)]
first-section (merge {:type "section"
:children (vec first-section-nodes)}
(apply element-bounds first-section-nodes))
new-children (vec (concat top-level-nodes
(list first-section)
rest))]
(assoc node :children new-children)))
(defn- newline-final-paragraph?
"Is `e` a paragraph, and does it end with a newline?"
[e]
(and (of-type? e "paragraph")
(some-> (-> e :position :end :column)
(= 1))))
(defn consequtive-elements?
"Returh truthy if each successive pair of elements is NOT separated
by at least one explicit paragraph break; i.e. a blank line."
[& elements]
(match elements
([(e :guard newline-final-paragraph?) e & es] :seq)
(and (= (-> e :position :end :line)
(-> e :position :start :line))
(recur es))
([e e & es] :seq)
(and (= (-> e :position :end :line inc)
(-> e :position :start :line))
(recur es))
([_] :seq) true
([] :seq) true))
(defn swallow
([predator prey]
(assert (greater-element? predator))
(-> predator
(update :children #(conj % prey))
(assoc-in [:position :end] (-> prey :position :end))))
([predator prey & more-prey]
(reduce swallow predator (cons prey more-prey))))
(defn- paragraph-followed-by-tex? [children]
(match children
[(para :guard #(of-type? % "paragraph"))
(tex :guard #(of-type? % "latex-environment"))
& _]
(consequtive-elements? para tex)
:else false))
(defn- paragraph-followed-by-paragraph? [children]
(match children
[(para :guard #(of-type? % "paragraph"))
(para :guard #(of-type? % "paragraph"))
& _]
(consequtive-elements? para para)
:else false))
(defn gather-latex-paragraphs [node]
(->> node
(sp/transform
[postorder-walker (sp/must :children)]
(fn [children]
(loop [acc []
cs (vec children)]
(match cs
;; CASE: A paragraph followed by a LaTeX environment.
;; If there are no blank lines separating the paragraph
;; from the LaTeX environment, the LaTeX environment
;; shall become a child of the paragraph.
([para tex & rest] :guard paragraph-followed-by-tex?)
(recur acc (vec (cons (swallow para tex) rest)))
;; CASE: Similar to the paragraph-followed-by-tex case,
;; but instead of swallowing the entire second element,
;; we swallow the /children/ of the second element,
;; since paragraphs cannot be nested.
([para para & rest]
:guard paragraph-followed-by-paragraph?)
(recur acc (vec (cons (apply swallow para (:children para))
rest)))
;; CASE: Irrelevant or empty!
[c & rest]
(recur (conj acc c) rest)
[] acc))))))

View File

@@ -0,0 +1,119 @@
(ns net.deertopia.doerg.elisp
(:require [clojure.core.match :refer [match]]
[clojure.java.io :as io]
[clojure.spec.alpha :as s]
[instaparse.core :as ip])
(:refer-clojure :exclude [print read read-string]))
(ip/defparser read*
(io/resource "net/deertopia/doerg/elisp/grammar"))
(defn- transform-string [s]
(let [s* (loop [s (seq s)
acc ""]
(match s
([\\ c & cs] :seq)
(recur
cs
(str acc
(condp = c
\n \newline
\f \formfeed
\\ \\
\" \"
\newline nil
(throw (ex-info "IDK!" {:char c})))))
([c & cs] :seq) (recur cs (str acc c))
([] :seq) acc))]
[:string (apply str s*)]))
(defn- transform-integer [s]
[:integer (parse-long s)])
(defn- transform-property-string
([[_ text]]
[:string text])
([[_ text] & props]
[:string text (->> (for [[_ [_ beg] [_ end] prop] props]
{[beg end] prop})
(apply merge))]))
(defn- transform-list [& xs]
(match (last xs)
[:dot-cdr x] [:cons* (butlast xs) x]
_ [:cons* xs]))
(def transforms {:string transform-string
:list transform-list
:integer transform-integer
:property-string transform-property-string})
(defn read [s & args]
(->> (apply read* s args)
(ip/transform transforms)))
(defn read-string [s]
(read s :start :text))
(defn cons? [x]
(= (first x) :cons*))
(s/def ::alist
(s/tuple #{:list}
(s/and ::list
cons?)))
(defn car [x]
(match x
[:cons* xs y] (first xs)
[:cons* xs] (first xs)
[:symbol "nil"] nil
_ nil))
(defn cdr [x]
(match x
[:cons* xs y] (if (<= (count xs) 1)
y
[:cons* (rest xs) y])
[:cons* xs] [:cons* (rest xs)]
[:symbol "nil"] nil
_ nil))
(defn emacs-list? [x]
(match x
[:cons* xs] true
_ false))
(defn read-alist [s]
(let [r (->> s read*
(ip/transform
(merge transforms
{:symbol (fn [s] (symbol s))
:string (fn [s] s)}))
first)]
(match r
[:cons* pairs] (->> (for [pair pairs]
(let [x (car pair)
y (cdr pair)]
{x y}))
(apply merge))
_ nil)))
(defn read-string [s]
(match (-> s read first)
[:string x & props] x
:else nil))
(defn print [x]
;; TODO: this is really not how it should be done lol. at the
;; moment, `print` is only used in `net.deertopia.doerg.roam`
;; and only to serialise uuids, so it's not a /massive/ priority.
(cond (or (string? x) (uuid? x)) (str \" x \")
:else (throw (ex-info "`print` is unimplemented lol"
{:x x}))))
(comment
(do (ip/defparser parse* (io/resource "elisp-grammar"))
(read "#(\"blah\" 0 1 (doge))")
(read "\"bla\\nh\"")
(read-alist "((x . y))")))

View File

@@ -1,7 +1,6 @@
(ns net.deertopia.doerg.html
"Common HTML elements and utilities"
(:require [clojure.java.io :as io]
[net.deertopia.doerg.config :as cfg]
[babashka.fs :as fs]))
#_
@@ -28,31 +27,20 @@
[:meta {:charset "utf-8"}])
(defn external-stylesheet [href]
[:link {:rel "stylesheet" :type "text/css" :href href}])
[:link {:rel "stylesheet" :type "text/css" :href (str "/resource/" href)}])
(def ibm-plex
(for [family ["serif" "sans-kr" "math"]]
#_
[:style (-> cfg/*cfg* ::cfg/ibm-plex-web
(fs/file (format "css/ibm-plex-%s-default.min.css" family))
slurp)]
(external-stylesheet
(format "ibm-plex-web/css/ibm-plex-%s-all.min.css" family))))
(concat
(for [family ["serif" "sans-kr" "math"]]
(external-stylesheet
(format "ibm-plex-web/css/ibm-plex-%s-all.min.css" family)))
[(external-stylesheet "Temml-Plex.css")]))
(def deerstar
(external-stylesheet "deerstar.css")
#_
[:style (slurp (io/resource "net/deertopia/doerg/deerstar.css"))])
(external-stylesheet "deerstar.css"))
(def tuftesque
(external-stylesheet "tuftesque.css")
#_
[:link {:rel "stylesheet"
:type "text/css"
:href "/resources/tuftesque.css"}]
#_
[:style
(slurp (io/resource "net/deertopia/doerg/tuftesque.css"))])
(external-stylesheet "tuftesque.css"))
(def head
(list viewport charset ibm-plex deerstar tuftesque))

View File

@@ -1,13 +1,19 @@
(ns net.deertopia.doerg.render
(:require [net.deertopia.doerg.element :as element]
[clojure.stacktrace]
[clojure.string :as str]
[clojure.tools.logging :as l]
[clojure.core.match :refer [match]]
[clojure.tools.logging.readable :as lr]
[com.rpl.specter :as sp]
[net.deertopia.doerg.html :as doerg-html]
[hiccup2.core :as hiccup]
[clojure.pprint]
[clojure.zip :as z]))
[net.deertopia.doerg.tex :as tex]
[net.deertopia.doerg.tex.temml :as tex-temml]
[clojure.zip :as z]
[babashka.fs :as fs]
[clojure.edn :as edn]))
;;; Top-level API
@@ -34,35 +40,51 @@
#(do (assert (element/of-type? % "keyword"))
(:key %)))
(def ^:dynamic ^:private *document-info*)
(def ^:dynamic ^:private *opts*)
(declare ^:private gather-footnotes render-renderer-error
view-children-as-seq)
view-children-as-seq render-tex-snippets)
(defn org-element-recursive
"Recursively render an Org-mode element to Hiccup."
[e]
(->> e
;; gather-footnotes
(sp/transform
[element/postorder-walker view-children-as-seq]
(fn [node]
(try (org-element node)
(catch Throwable e
(lr/error e "Error in renderer" {:node node})
(render-renderer-error e)))))))
[element/postorder-walker view-children-as-seq]
(fn [node]
(try (org-element node)
(catch Throwable e
(lr/error e "Error in renderer" {:node node})
(render-renderer-error e)))))))
(def default-language
"Default language, used in the lang attribute of the body tag."
"en")
(defn org-document
"Recursively render an Org-mode document to Hiccup."
[doc]
(let [rendered (org-element-recursive (gather-footnotes doc))]
[:html
[:head
[:title "org document"]
doerg-html/head]
[:body
[:article
rendered]]]))
[doc & {:as opts :keys [postamble]}]
(binding [*opts* opts]
(tex-temml/binding-worker
(let [rendered (-> doc gather-footnotes render-tex-snippets
org-element-recursive)]
[:html
[:head
[:title "org document"]
doerg-html/head]
[:body {:lang default-language}
[:article
rendered
(when postamble
[:footer
[:hr]
postamble])]]]))))
(defn to-html
"Read `f` with `slurp` as an Org document and return a string of
rendered HTML. See `org-document` for opts."
[f & {:as opts}]
(str (hiccup/html {} (-> f slurp element/read-string (org-document opts)))))
;;; Further dispatching on `org-element`
@@ -73,6 +95,9 @@
(defmethod org-element "link" [e]
(org-link e))
(defmethod org-element "special-block" [e]
(org-special-block e))
(def view-children-as-seq
@@ -83,16 +108,18 @@
(sp/view #(update % :children seq))
sp/STAY))
#_
(defn- gather-footnotes [doc]
(->> doc
(sp/select
[element/children-walker element/footnotes-section?
element/children-walker
#(element/of-type? % "footnote-definition")
(sp/view (fn [d]
{(:label d) d}))])
(apply merge)))
(defn center [& es]
[:div.center es])
(defn doerg-attrs [e]
(->> e :affiliated :attr_doerg (str/join " ")
(format "{%s}") edn/read-string))
(defn em [x]
(format "%.4fem" x))
(defn wrap-if [x c f]
(if c (f x) x))
(defn- contains-footnote-refs? [node]
(some #(element/of-type? % "footnote-reference")
@@ -130,9 +157,48 @@
element/footnotes-section?]
sp/NONE))))
(defn- collect-latex-headers [doc]
(->> doc
(sp/select
[element/postorder-walker
#(element/of-keyword-type? % "LATEX_HEADER")
(sp/view :value)])))
(defn- timeout-snippet-promises [snippet-promises fut]
;; Time out after twenty seconds. With all the LaTeX and IPC, there
;; are so many opportunities for things to go wrong </3.
(let [ms (* 20 1000)
fut-res (deref fut ms ::timed-out)]
(if (= fut-res ::timed-out)
(do (l/warnf "Giving up on rendering TeX snippets after %.3f seconds."
(/ ms 1000))
(future-cancel fut)
(doseq [[_snippet p] snippet-promises]
(deliver p ::timed-out)))
fut-res)))
(defn render-tex-snippets
"Traverse doc, adorning each LaTeX node with a promise resolving to,
optimistically, Hiccup-rendered SVG or MathML code."
[doc]
(let [snippet-promises (atom [])
r (->> doc (sp/transform
[element/postorder-walker
#(element/of-type?
% "latex-fragment" "latex-environment")]
(fn [node]
(let [p (promise)]
(swap! snippet-promises #(conj % [(:value node) p]))
(assoc node ::rendered p)))))
sp @snippet-promises
fut (-> #(tex/render-snippets sp)
bound-fn* future-call)]
(timeout-snippet-promises sp fut)
r))
(defn- render-pprint
(defn render-pprint
"Render the argument inline as `clojure.pprint/pprint` output."
[x & {:keys [text]
:or {text "debug!"}}]
@@ -163,6 +229,10 @@
:dd (apply vector :dd dds)}
_ nil))
(defn- same-tag? [x y]
(let [x* (-> x name (str/replace #"^([^\.#]).*" "$1") keyword)]
(= x* y)))
;; In HTML5, </p> tags cannot be nested for… reasons. In fact, no
;; block-level elements are allowed within paragraphs. This stupid
;; hack works around that restriction by stripping </p> tags }:).
@@ -170,8 +240,10 @@
(apply concat
(for [x elements]
(match x
[:p (_ :guard map?) & xs] (seq xs)
[:p & xs] (seq xs)
[(_ :guard #(same-tag? % :p)) (_ :guard map?) & xs]
(seq xs)
[(_ :guard #(same-tag? % :p)) & xs]
(seq xs)
_ x))))
@@ -201,6 +273,18 @@
(defmethod org-element "bold" [{:keys [children]}]
[:b children])
(defmethod org-element "subscript" [{:keys [children]}]
[:sub children])
(defmethod org-element "superscript" [{:keys [children]}]
[:super children])
(defmethod org-element "italic" [{:keys [children]}]
[:em children])
(defmethod org-element "verbatim" [{:keys [value]}]
value)
(defmethod org-element "code" [{:keys [value]}]
[:code value])
@@ -247,14 +331,62 @@
(defmethod org-element "node-property" [{:keys [key value]}]
[:tr [:th key] [:td value]])
(defmethod org-element "citation" [{:keys [prefix suffix children]}]
[:div prefix children suffix])
(defmethod org-element "citation" [{:keys [prefix suffix children] :as e}]
;; TODO: Real citations.
[:span "[cite:" prefix children suffix "]"])
(defmethod org-element "citation-reference" [{:keys [key]}]
(str "@" key))
(defmethod org-element "latex-fragment" [{:keys [contents value] :as e}]
[:span {:class (if (element/display-math? e)
"latex-fragment display-math"
"latex-fragment")}
(-> e ::rendered deref)])
(defmethod org-element "latex-environment" [{:keys [value] :as e}]
[:span.latex-fragment.display-math
(-> e ::rendered deref)])
(defmethod org-element "example-block" [{:keys [value] :as e}]
(let [{:keys [center? alt scale img?]} (doerg-attrs e)]
(-> [:pre (merge (and img? {:role "img"
:aria-label alt
:title alt})
(and scale {:style {:font-size (em scale)}}))
value]
(wrap-if center? center))))
(defmethod org-element "src-block" [{:keys [value]}]
[:pre [:code value]])
(defn- split-quote-block-children [children]
(match (split-with #(not= % [:hr]) children)
[x ([[:hr] & ys] :seq)] [x (strip-paragraphs ys)]
x x))
(defmethod org-element "quote-block" [{:keys [children] :as e}]
(let [{:keys [epigraph?]} (doerg-attrs e)
[content footer] (split-quote-block-children children)]
(-> [:blockquote
content
(when footer
[:footer footer])]
(wrap-if epigraph? (fn [c] [:div.epigraph c])))))
(defmethod org-element "horizontal-rule" [_]
[:hr])
(defmethod org-element "comment" [_] nil)
(defmethod org-keyword "TITLE" [{:keys [value]}]
[:h1 value])
;; Completely ignore the LATEX_COMPILER keyword.
(defmethod org-keyword "LATEX_COMPILER" [_] nil)
(defmethod org-keyword "LATEX_HEADER" [_] nil)
;; Not sure how to deal with this one yet.
(defmethod org-keyword "AUTHOR" [_] nil)
(defmethod org-element :default [x]
(render-pprint x :text "unimplemented!"))
@@ -262,11 +394,14 @@
(defmethod org-keyword :default [x]
(render-pprint x :text "unimplemented!"))
(defmethod org-special-block "margin-note" [{:keys [children]}]
[:p [:span.marginnote (strip-paragraphs children)]])
#_
(defmethod org-special-block :default [x]
(render-pprint x :text "unimplemented!"))
(defmethod org-link :default [{:keys [raw-link children]}]
[:span {:class "org-link external"}
[:span.org-link.external
[:a {:href raw-link}
(or (seq children) raw-link)]])

View File

@@ -0,0 +1,52 @@
(ns net.deertopia.doerg.repl
(:require [net.deertopia.doerg.element :as element]
[net.deertopia.doerg.render :as render]
[net.deertopia.doerg.config :as cfg]
[clojure.java.io :as io]
[hiccup2.core :as h]
[clojure.pprint]
[babashka.fs :as fs]
[net.deertopia.doerg :as-alias doerg]))
(def some-org-file
#_
"/home/msyds/org/20251228003307-prerequisite_context_in_korean.org"
#_
"/home/msyds/org/20250919114912-homepage.org"
#_
"/home/msyds/org/20251111182118-path_induction.org"
;; #_
"/home/msyds/org/20250512144715-natural_transformation_category_theory.org"
#_
"/home/msyds/org/20251021155921-path_action.org"
#_
"test/net/deertopia/doerg/render_test/fallbacks.org"
#_
"/home/msyds/org/20250910115311-men_who_would_make_stunning_dykes.org")
(defn- force-create-sym-link [path target]
(fs/delete-if-exists path)
(fs/create-sym-link path target))
(defn render-html [& {:keys [src dest]
:or {src some-org-file
dest "/tmp/doerg-test"}}]
(let [resource-dir (fs/file dest "resource")]
(fs/create-dirs dest)
(fs/create-dirs resource-dir)
(force-create-sym-link (fs/file resource-dir "ibm-plex-web")
(-> cfg/*cfg* ::doerg/ibm-plex-web))
(doseq [x #{"Temml-Plex.css" "tuftesque.css" "deerstar.css"}]
(force-create-sym-link
(fs/file resource-dir x)
(io/resource (str "net/deertopia/doerg/" x))))
(fs/delete-if-exists (fs/file dest "index.html"))
(->> src render/to-html str (spit (fs/file dest "index.html")))))
(defn render-edn [& {:keys [src dest]
:or {src some-org-file
dest "/tmp/doerg-test/index.edn"}}]
(fs/create-dirs (fs/parent dest))
(with-open [f (io/writer dest)]
(binding [*out* f]
(-> src slurp element/read-string clojure.pprint/pprint))))

View File

@@ -0,0 +1,183 @@
(ns net.deertopia.doerg.roam
(:require [babashka.fs :as fs]
[net.deertopia.doerg.config :as cfg]
[net.deertopia.doerg.elisp :as elisp]
[net.deertopia.doerg.slug :as slug]
[next.jdbc :as sql])
(:import (java.util UUID)))
;;; Global database
(defonce ^:dynamic *use-db-cache?* true)
(defn ds []
(sql/get-datasource
{:dbtype "sqlite"
:dbname (-> cfg/*cfg* ::cfg/org-roam-db-path str)}))
;;; Elisp sexp (de)serialisation
(defn id [node]
(-> node :id))
(defn slug [node]
(-> node :id slug/from-uuid))
(defn- print-id [node]
(-> node id elisp/print))
;;; Node
(defrecord Node [id cache])
(defn uuid-exists? [uuid]
(sql/execute-one! (ds)
["select 1 from nodes where id = ? limit 1"
(-> uuid str elisp/print)]))
(defn make-node
([uuid] (make-node uuid {}))
([uuid props]
(and (uuid-exists? uuid)
(->Node uuid (atom props)))))
(defn- fetch-with-cache [node field fetch]
(if *use-db-cache?*
(-> (:cache node)
(swap! (fn [cache]
(update cache field #(or % (fetch node)))))
(get field))
(fetch node)))
(defn org-file [node]
(fetch-with-cache
node :org-file
(fn [node]
(when-some [r (sql/execute-one!
(ds)
["select file from nodes where id = ?"
(-> node :id str elisp/print)])]
(-> r :nodes/file elisp/read-string)))))
(defn title [node]
(fetch-with-cache
node :title
#(when-some [r (sql/execute-one!
(ds)
["select title from nodes where id = ?"
(print-id %)])]
(-> r :nodes/title elisp/read-string))))
(defprotocol GetNode
(get-node [this]
"Return the node associated with `this` or nil."))
(extend-protocol GetNode
String
(get-node [this]
(or (some-> this slug/from-string get-node)
(some-> this parse-uuid get-node)
(throw (IllegalArgumentException.
"Give `get-node` a UUID or slug string plz. }:)"))))
java.util.UUID
(get-node [this]
(make-node this))
net.deertopia.doerg.slug.Slug
(get-node [this]
(-> this slug/to-uuid make-node))
Node
(get-node [this]
this))
(comment
(def node (get-node "68XqhHerTWCbE--RYLEdHw"))
(fetch-with-cache
node :title
#(do (println "fetch")
(sql/execute-one! (ds) ["select title from nodes where id = ?"
(elisp/print (:id %))]))))
;;; Node operations
(defn level [node]
(fetch-with-cache
node :level
#(-> (sql/execute-one!
(ds) ["select level from nodes where id = ?"
(print-id %)])
:nodes/level)))
(defn top-level? [node]
(zero? (level node)))
(defn file [node]
(fetch-with-cache
node :file
#(-> (sql/execute-one!
(ds) ["select file from nodes where id = ?"
(print-id %)])
:nodes/file
elisp/read-string)))
(defn properties [node]
(fetch-with-cache
node :properties
#(-> (sql/execute-one!
(ds) ["select properties from nodes where id = ?"
(print-id %)])
:nodes/properties
elisp/read-alist)))
(defn public? [node]
(-> node properties (get "DEERTOPIAVISIBILITY") (= "public")))
(defn graph-visible? [node]
(#{"public" "graphonly"}
(-> node properties (get "DEERTOPIAVISIBILITY"))))
(defn backlinks
"Returns a collection of nodes linking to `node`."
[node]
(for [{id :nodes/id title :nodes/title}
(sql/execute! (ds) ["select distinct nodes.id, nodes.title from links
inner join nodes
on nodes.id = links.source
where links.dest = ?"
(elisp/print (str (:id node)))])
:let [id' (elisp/read-string id)]
:when (-> id' parse-uuid get-node public?)]
(make-node id' {:title (elisp/read-string title)})))
;;; Graph support
(defn- read-string-field [n field]
(-> n (get field) elisp/read-string))
(defn- uuid-graph-visible? [uuid]
(-> uuid parse-uuid get-node graph-visible?))
(defn get-graph []
(let [nodes (sql/execute! (ds) ["select id, title from nodes"])
links (sql/execute!
(ds)
["select n1.id as source, nodes.id as target from
((nodes as n1) join links on n1.id = links.source)
join (nodes as n2) on links.dest = nodes.id
where links.type = '\"id\"'"])]
{:nodes (for [n nodes
:let [id (read-string-field n :nodes/id)]
:when (uuid-graph-visible? id)]
{:id id
:title (read-string-field n :nodes/title)})
:links (for [l links
:let [source (read-string-field l :nodes/source)
target (read-string-field l :nodes/target)]
:when (and (uuid-graph-visible? source)
(uuid-graph-visible? target))]
{:source source
:target target})}))

View File

@@ -0,0 +1,188 @@
(ns net.deertopia.doerg.server
(:require [clojure.pprint :refer [pprint]]
[clojure.tools.logging :as l]
[hiccup2.core :as hiccup]
[net.deertopia.doerg.html :as doerg-html]
[net.deertopia.doerg.config :as-alias cfg]
[net.deertopia.doerg.slug :as slug]
[net.deertopia.doerg.config :as cfg]
[net.deertopia.doerg.roam :as roam]
[org.httpkit.server :as http]
[reitit.coercion]
[reitit.coercion.spec]
[reitit.ring.coercion]
[reitit.core :as r]
[reitit.ring]
[reitit.ring.middleware.exception :as reitit-exception]
[ring.util.response :as response]
[spec-tools.spell]
[reitit.spec]
[reitit.dev.pretty]
[clojure.spec.alpha :as s]
[net.deertopia.doerg.render :as doerg-render]
[net.deertopia.doerg.cached-file :as cached-file]
[babashka.fs :as fs]
[aero.core :as aero]
[clojure.string :as str]
[net.deertopia.doerg :as-alias doerg]
[net.deertopia.doerg.config :as doerg-config]))
;;; Routes
(def homepage-slug "68XqhHerTWCbE--RYLEdHw")
(def not-found-slug "PGDHTvUzQ62Js1Y5db-A8g")
(defn hello [req]
(-> (hiccup/html {}
[:html
[:head
[:title "hello"]
doerg-html/charset
doerg-html/viewport]
[:body
[:pre
(with-out-str
(pprint req))]]])
str
response/response
(response/content-type "text/html")))
(defn html-dir []
(-> cfg/*cfg* ::cfg/state-directory (fs/file "html")))
(defn not-found [req]
(response/not-found "not found"))
(defn org-file->html-file [org-file]
(fs/file (html-dir)
(-> org-file
fs/file-name
(fs/strip-ext {:ext "org"})
(str ".html"))))
(defn slug-link [slug & contents]
[:a {:href (str "/n/" slug)}
contents])
(defmethod doerg-render/org-link "id"
[{:keys [path raw-link children]}]
[:span.org-link
(slug-link (slug/from-uuid path)
(or (seq children) raw-link))
#_[:a {:href (str "/n/" (slug/from-uuid path))}
(or (seq children) raw-link)]])
(defn backlinks-postamble [node]
[:section#backlinks
[:h2 "Backlinks"]
[:ul
(for [n (->> (roam/backlinks node)
(sort-by (comp str/lower-case roam/title)))]
[:li (slug-link (roam/slug n)
(roam/title n))])]])
(defn node-by-slug [{{:keys [slug]} :path-params :as req}]
(if-some [node (some-> slug slug/from-string roam/get-node)]
(let [org-file (roam/org-file node)
html-file (org-file->html-file org-file)]
(cached-file/cached-file
:file html-file
:stale? (cached-file/newer-than? org-file html-file)
:compute #(doerg-render/to-html
org-file
:postamble (backlinks-postamble node)))
(-> (str html-file)
response/file-response
(response/content-type "text/html")))
(not-found req)))
(defn node-by-id [req]
(hello req))
(def exception-middleware
(reitit-exception/create-exception-middleware
(merge
reitit-exception/default-handlers
{::reitit-exception/wrap
(fn [handler e request]
(l/error e "error in fucking somwhere dude")
(handler e request))})))
(defn handle-homepage [req]
(-> req
(assoc-in [:path-params :slug] homepage-slug)
node-by-slug))
(defn handle-resource [{:keys [uri]}]
(if-some [[_ resource] (re-matches #"^/resource/ibm-plex-web/(.*)" uri)]
(-> resource
(response/file-response
{:root (-> doerg-config/*cfg* ::doerg/ibm-plex-web str)}))
(-> uri
(str/replace-first #"^/resource/" "")
(response/resource-response
{:root "net/deertopia/doerg/public"
:allow-symlinks? true}))))
(defn handle-favicon [_]
(response/resource-response "net/deertopia/doerg/favicon.ico"))
(def router
(reitit.ring/router
#{["/" #'handle-homepage]
["/n/:slug" #'node-by-slug]
["/id/:id" #'node-by-id]
["/resource/*" #'handle-resource]
["/myreq" #'hello]
["/favicon.ico" #'handle-favicon]}
{:validate reitit.spec/validate
:exception reitit.dev.pretty/exception
:spec :reitit.spec/default-data
:data
{:coercion reitit.coercion.spec/coercion
:middleware [exception-middleware
reitit.ring.coercion/coerce-request-middleware
reitit.ring.coercion/coerce-response-middleware
#_reitit.ring.coercion/coerce-exceptions-middleware]}}))
;;; Server API
(def app (reitit.ring/ring-handler router))
(defonce server (atom nil))
(defn stop! []
(when @server
(http/server-stop! @server {:timeout 100})
(reset! server nil)
(l/info "Stopped server")))
;; For some reason, the log messages from `stop!` are not flushed
;; before the JVM shuts dowm. Nevertheless, the server /does/ come to
;; a graceful halt.
(def ^:private shutdown-hook (Thread. stop!))
(defn start! []
(if @server
(throw (IllegalStateException. "Server already started"))
(do (reset! server
(http/run-server (bound-fn* #'app)
{:port (-> cfg/*cfg* ::cfg/port)
:legacy-return-value? false}))
;; For some reason, the log messages are not flushed before
;; the JVM shuts dowm. Nevertheless, the server /does/ come
;; to a graceful halt.
(try (.addShutdownHook (Runtime/getRuntime) shutdown-hook)
(catch IllegalArgumentException e
(when (not= "Hook previously registered"
(ex-message e))
(throw e))))
(l/infof "Server started on port %d"
(-> cfg/*cfg* ::cfg/port)))))
(defn status []
(if @server
(http/server-status @server)
:stopped))

View File

@@ -0,0 +1,64 @@
(ns net.deertopia.doerg.slug
(:require [clojure.spec.alpha :as s]
[spec-tools.core :as st])
(:import (java.nio ByteBuffer)
(java.util Base64 UUID)))
(defrecord Slug [slug-string]
Object
(toString [this]
(:slug-string this)))
(defn from-string [s]
(try (let [decoder (Base64/getUrlDecoder)]
(when (= 16 (count (.decode decoder s)))
(Slug. s)))
;; really stupid
(catch IllegalArgumentException _
nil)))
(defn to-string [s]
(str s))
(defn- coerce-to-uuid [string-or-uuid]
(cond (string? string-or-uuid) (UUID/fromString string-or-uuid)
(uuid? string-or-uuid) string-or-uuid))
(defn- uuid->bytes [string-or-uuid]
(let [uuid (coerce-to-uuid string-or-uuid)]
(.array (doto (ByteBuffer/wrap (byte-array 16))
(.putLong (.getMostSignificantBits uuid))
(.putLong (.getLeastSignificantBits uuid))))))
(defn- bytes->uuid [bytes]
(when (= (count bytes) 16)
(let [bb (ByteBuffer/wrap bytes)
high (.getLong bb)
low (.getLong bb)]
(UUID. high low))))
(defn from-uuid [string-or-uuid]
(let [uuid (coerce-to-uuid string-or-uuid)
encoder (.withoutPadding (Base64/getUrlEncoder))]
(Slug. (.encodeToString encoder (uuid->bytes uuid)))))
(defn to-uuid [slug]
(let [decoder (Base64/getUrlDecoder)]
(bytes->uuid (.decode decoder (str slug)))))
(comment
(let [uuid #uuid "f9eab66e-7773-4b87-b854-0bfc8f563809"
slug (from-uuid uuid)
round-tripped (to-uuid slug)]
{:uuid uuid, :slug slug, :round-tripped round-tripped}))
(defn make-slug [string]
(assert (try (to-uuid string)
(catch Throwable _
nil))
"invalid slug")
(->Slug string))
(s/def ::slug
(s/conformer #(or (some-> % from-string)
::s/invalid)))

View File

@@ -0,0 +1,62 @@
(ns net.deertopia.doerg.tex
(:require [net.deertopia.doerg.tex.native :as native]
[net.deertopia.doerg.tex.temml :as temml]
[babashka.fs :as fs]
[clojure.string :as str]
[hiccup2.core :as hiccup]
[clojure.tools.logging :as l]
[clojure.tools.logging.readable :as lr]))
(defn- read-and-patch-generated-svg [{:keys [file height depth]}]
;; dvisvgm writes standalone SVG files, to which we need to make a
;; few changes to use them inline within our HTML.
;; • XML header: Bad syntax when embedded in an HTML doc. Remove
;; it.
;; • Width and height: We override these with our own values
;; computed by `net.deertopia.doerg.tex` to ensure correct
;; positioning relative to the surrounding text. More
;; accurately, we remove the height and width attributes from
;; the SVG tag, and set the new values for height and
;; vertical-align in the style attribute
;; • Viewbox: Must be removed entirely for correct positioning.
(-> (slurp file)
(str/replace-first #"<\?xml version='1.0' encoding='UTF-8'\?>\n?" "")
(str/replace-first #" height=['\"][^\"']+[\"']" "")
(str/replace-first #" width=['\"][^\"']+[\"']" "")
(str/replace-first
#"viewBox=['\"][^\"']+[\"']"
(fn [s]
(format "%s style=\"%s\""
s
(format "height:%.4fem;vertical-align:%.4fem;display:inline-block"
height (- depth)))))
;; Stupid hack. --currentcolor on dvisvgm should be enough, but
;; it doesn't get e.g. TikZ arrows.
(str/replace #"stroke=['\"]#000['\"]" "stroke=\"currentColor\"")))
(defn render-snippets [snippet-promises]
(fs/with-temp-dir [svg-dir {:prefix "doerg-svg-"}]
(let [rendered-snippets
(delay (->> snippet-promises
(map first)
(apply native/render svg-dir)))]
(doseq [[snippet p] snippet-promises]
(try (let [temml (temml/render snippet)]
(->> (if (temml/erroneous-output? temml)
(let [tex (get @rendered-snippets snippet)]
(if (:errors tex)
temml
(read-and-patch-generated-svg tex)))
temml)
hiccup/raw (deliver p)))
(catch Exception e
(l/error e "Error in TeX thread")
(throw e)))))))
(comment
(let [snippets (for [x ["\\(\\ifxetex blah \\fi\\)"
"\\(\\sqrt{x^2 + y^2}\\)"]]
[x (promise)])]
(temml/binding-worker
(render-snippets snippets)
(map #(-> % second deref) snippets))))

View File

@@ -0,0 +1,165 @@
(ns net.deertopia.doerg.tex.native
"Shelling out to (Xe)LaTeX and dvisvgm. Much magic borrowed from
the org-latex-preview package for Emacs."
(:require [babashka.process :as p]
[net.deertopia.doerg.common :as common]
[clojure.java.io :as io]
[clojure.string :as str]
[clojure.tools.logging :as l]
[babashka.fs :as fs]
[net.deertopia.doerg.config :as cfg]
[net.deertopia.doerg :as-alias doerg])
(:import (java.io ByteArrayOutputStream)))
(def ^:private scale-divisor 66873.46948423679)
(def ^:private font-size 10)
(def ^:private tightpage-regexp
#"Preview: Tightpage (-?\d+) *(-?\d+) *(-?\d+) *(-?\d+)")
(def ^:private preview-start-regexp
#"! Preview: Snippet (\d+) started.")
(def ^:private preview-end-regexp
#"(?:^Preview: Tightpage.*$)?\n! Preview: Snippet (\d+) ended.\((\d+)\+(\d+)x(\d+)\)")
(defn- invoke [extra-opts & args]
(let [namespace (or (::ns extra-opts) (first args))
out-bytes (ByteArrayOutputStream.)
out-stream (common/tee-output-stream
out-bytes
(l/log-stream :info (str namespace "/out")))
err-stream (l/log-stream :info (str namespace "/err"))
opts (merge extra-opts
{:out out-stream :err err-stream :continue true
:shutdown p/destroy-tree
:pre-start-fn (fn [{:keys [cmd]}]
(l/infof "$ %s"
(str/join " " cmd)))
:exit-fn (fn [{:keys [cmd exit]}]
(l/infof "%s exited w/ status %d"
(first cmd) exit))})
r (apply p/shell opts args)
out (.toString out-bytes)]
(-> r
(assoc ::out out))))
(defn- parse-tightpage [latex-out]
(->> (re-find tightpage-regexp latex-out)
(drop 1)
(map parse-long)))
(defn- compute-geometry [[tp1 tp2 tp3 tp4] [d1 d2 d3]]
(let [depth (/ (- d2 tp2) scale-divisor font-size)]
{:depth depth
:height (+ depth
(/ (+ d1 tp4)
scale-divisor
font-size))
:width (/ (+ d3 tp3 (- tp2))
scale-divisor
font-size)}))
(defn- parse-latex-output [out]
(let [tightpage-info (parse-tightpage out)
m-start (re-matcher preview-start-regexp out)
m-end (re-matcher preview-end-regexp out)]
(loop [acc []]
(if-some [[_ snippet-ix] (re-find m-start)]
(let [r (re-find m-end)
[_ snippet-ix* _ _ _] r
dimensional-info (->> r (drop 2) (map parse-long))
errors (-> out
(subs (.end m-start) (.start m-end))
(str/replace-first #"[^!]*" "")
str/trim)]
(assert (= snippet-ix snippet-ix*))
(recur (conj acc (-> (compute-geometry
tightpage-info dimensional-info)
(assoc :errors (if (empty? errors)
nil
errors))))))
acc))))
(defn- invoke-latex [& {:keys [file output-dir]}]
(let [latex (-> cfg/*cfg* ::doerg/latex)]
(invoke
{:dir output-dir}
latex "-no-pdf" "-interaction" "nonstopmode"
"-output-directory" output-dir file)))
(defn- invoke-dvisvgm [& {:keys [file output-dir]}]
(let [dvisvgm (-> cfg/*cfg* ::doerg/dvisvgm)]
(invoke
{:dir output-dir}
dvisvgm "--page=1-" "--optimize" "--clipjoin"
"--relative" "--no-fonts" "-v3" "--currentcolor"
"--message=processing page {?pageno}: output written to {?svgpath}"
"--bbox=preview" "-o" "%9p.svg" file)))
(defn- snippet-file-names
"Return a map of TeX snippets (as strings, including the math
delimiters) to file names as would be output by
`invoke-dvisvgm`. The returned file names are relative to dvisvgm's
output directory."
[snippets]
(let [svgs (for [i (range)]
(format "%09d.svg" i))]
(zipmap (reverse snippets) svgs)))
(defn- read-prelude []
(str (-> "net/deertopia/doerg/prelude.tex" io/resource slurp)
\newline
(-> "net/deertopia/doerg/native-prelude.tex" io/resource slurp)))
(defn- instantiate-preview-template [snippets]
(let [contents (->> (for [s snippets]
(format "\\begin{preview}\n%s\n\\end{preview}" s))
(str/join "\n"))]
(-> (io/resource "net/deertopia/doerg/preview-template.tex")
slurp
(str/replace #"% \{\{(contents|preamble)}}"
#(case (second %)
"contents" contents
"preamble" (read-prelude))))))
(defn render
"Render a collection of `snippets` to SVGs in `output-dir` using a
LaTeX engine (XeLaTeX at the moment) and dvisvgm. Returns a map
whose keys are `snippets` and whose values are maps containing
geometry info, a string of errors output by LaTeX, and the path to
the generated SVG file. Math delimiters are *not* implicitly added
to each snippet."
[output-dir & snippets]
(fs/with-temp-dir [dir {:prefix "doerg-latex"}]
(let [preview-tex (fs/file dir "preview.tex")
preview-xdv (fs/file dir "preview.xdv")
distinct-snippets (distinct snippets)]
(fs/create-dirs output-dir)
(->> (instantiate-preview-template distinct-snippets)
(spit preview-tex))
(let [dimensions (-> (invoke-latex :output-dir dir :file preview-tex)
::out parse-latex-output)
_ (invoke-dvisvgm :output-dir output-dir :file preview-xdv)]
;; Adorn each snippet with dimensions and errors parsed from
;; LaTeX's output, and the paths to SVG files generated by
;; dvisvgm.
(assert (= (count distinct-snippets) (count dimensions)))
(->> (map (fn [ix snippet dimensions]
{snippet
(-> dimensions
(assoc
:file (fs/file output-dir
(format "%09d.svg" (inc ix)))))})
(range)
distinct-snippets
dimensions)
(into {}))))))
(comment
(render "/tmp/doerg-tex-svgs"
"\\(c = \\sqrt{x^2 + y^2}\\)"
"\\(x\\)" "\\(y\\)" "\\(x\\)"
"\\(\\undefinedcommandlol\\)"))

View File

@@ -0,0 +1,90 @@
(ns net.deertopia.doerg.tex.temml
(:require [babashka.process :as p]
[net.deertopia.doerg.common :as common]
[net.deertopia.doerg.config :as cfg]
[clj-cbor.core :as cbor]
[clojure.java.io :as io]
[clojure.string :as str]
[clojure.tools.logging :as l]
[babashka.fs :as fs]
[net.deertopia.doerg :as-alias doerg])
(:import (java.io ByteArrayOutputStream)))
(def ^:dynamic *worker-timeout-duration*
"Number of milliseconds to wait before killing the external Uniorg
process."
(* 10 1000))
(def ^:dynamic *worker*)
;; 외부의 브로그램이 JVM resource를 사용 위해서 파일 시스템에서 써야
;; 합니다.
(defonce ^:private prelude-file
(-> (fs/create-temp-file {:prefix "doerg-prelude-"
:suffix ".tex"})
fs/file))
(defn worker []
(let [doerg-temml-worker (-> cfg/*cfg* ::doerg/doerg-temml-worker)]
(when (or (not (fs/exists? prelude-file))
(zero? (fs/size prelude-file)))
(-> "net/deertopia/doerg/prelude.tex"
io/resource
io/input-stream
(io/copy prelude-file)))
(p/process
{:shutdown p/destroy-tree
:err (l/log-stream :info "temml/err")}
doerg-temml-worker
"--preamble" prelude-file)))
(defn close-worker [tw]
(.close (:in tw)))
(defmacro with-worker [tw & body]
`(let [~tw (worker)]
(try
(do ~@body)
(finally
(close-worker ~tw)
(p/destroy-tree ~tw)))))
(defmacro binding-worker [& body]
`(binding [*worker* (worker)]
(try
~@body
(finally
(close-worker *worker*)))))
(defn command-worker [x]
(cbor/encode cbor/default-codec (:in *worker*) x)
(.flush (:in *worker*))
(let [r (cbor/decode cbor/default-codec (:out *worker*))]
(if (string? r)
r
(throw (ex-info "bad data from temml worker"
{:data r})))))
(defn render-inline [s]
(command-worker s))
(defn render-display [s]
(command-worker [s]))
(defn render [s]
(let [s (str/trim s)]
(if-let [[_ inner] (re-matches #"(?s)\\\[(.*)\\]" s)]
(render-display inner)
(if (re-matches #"(?s)\\begin\{.+?}(.*?)\\end\{.+?}" s)
(render-display s)
(if-let [[_ inner] (re-matches #"(?s)\\\((.*)\\\)" s)]
(render-inline inner)
(throw (IllegalArgumentException.
(ex-info
(str "`net.deertopia.doerg.tex.temml` argument should"
" be enclosed in math delimiters.")
{:arg s}))))))))
;; hackky....
(defn erroneous-output? [s]
(re-find #"(#b22222|temml-error)" s))

10
test-emacs.nix Normal file
View File

@@ -0,0 +1,10 @@
{ emacsPackages
, symlinkJoin
, writeScriptBin
, lib
}:
let emacs = emacsPackages.emacsWithPackages (epkgs: [ epkgs.org-roam ]);
in writeScriptBin "test-emacs" ''
exec ${lib.getExe emacs} "$@"
''

View File

@@ -0,0 +1,32 @@
(ns net.deertopia.doerg.common-test
(:require [net.deertopia.doerg.common :as sut]
[babashka.process :as p]
[clojure.test :as t]))
(defn sleep-vs-timeout [& {:keys [sleep timeout]}]
(sut/deref-with-timeout
(p/process "sleep" (format "%ds" sleep))
(* timeout 1000)))
;; Ideally we would test the following property:
;;
;; For natural numbers n and m, evaluating the form
;; (sut/deref-with-timeout
;; (p/process "sleep" (format "%ds" n))
;; (* m 1000))
;; will throw an exception iff n < m (probably with some margin of
;; error lol).
;;
;; But, this is not something that we want to run dozens-to-hundreds
;; of times. }:p
(t/deftest long-sleep-vs-short-timeout
(t/testing "long sleep vs. short timeout"
(t/is (thrown-with-msg?
Exception #".*timed out.*"
(sleep-vs-timeout :sleep 5 :timeout 1)))))
(t/deftest short-sleep-vs-long-timeout
(t/testing "short sleep vs. long timeout"
(t/is (instance? babashka.process.Process
(sleep-vs-timeout :sleep 1 :timeout 5)))))

View File

@@ -0,0 +1,9 @@
(ns net.deertopia.doerg.config-test
(:require [clojure.test :as t]
[net.deertopia.doerg.config :as cfg]))
(defn test-config-fixture
"`clojure.test` fixture to run tests with the :test configuration."
[f]
(binding [cfg/*cfg* (cfg/read-config cfg/sources :profile :test)]
(f)))

View File

@@ -0,0 +1,115 @@
(ns net.deertopia.doerg.element-test
(:require [net.deertopia.doerg.element :as sut]
[babashka.process :as p]
[clojure.test :as t]
[clojure.zip :as z]
[clojure.java.io :as io]
[com.rpl.specter :as sp]))
(defn- first-child-of-type [parent type]
(some #(and (sut/of-type? % type) %) (:children parent)))
(defn- parse-resource [path]
(-> (str "net/deertopia/doerg/element_test/" path)
io/resource slurp
(sut/read-string)))
(t/deftest known-greater-elements
(t/testing "known greater elements satisfy `greater-element?`"
(let [root (parse-resource "greater-elements.org")
section (->> root
(sp/select [sut/children-walker
#(sut/of-type? % "section")])
second)
headline (first-child-of-type section "headline")
headline-text (first-child-of-type headline "text")
paragraph (first-child-of-type section "paragraph")
paragraph-text (first-child-of-type paragraph "text")]
(t/is (sut/greater-element? root))
(t/is (sut/greater-element? section))
(t/is (sut/greater-element? headline))
(t/is (not (sut/greater-element? headline-text)))
(t/is (sut/greater-element? paragraph))
(t/is (not (sut/greater-element? paragraph-text))))))
(defn- first-paragraph-belongs-to-first-section? [doc]
(let [first-paragraph (sp/select-first [sut/postorder-walker
#(sut/of-type? % "paragraph")]
doc)
first-section (sp/select-first [sut/postorder-walker
#(sut/of-type? % "section")]
doc)]
(if (and first-paragraph first-section)
(boolean (some #(= % first-paragraph)
(:children first-section)))
true)))
(t/deftest first-paragraph-under-first-section
(t/is (-> (parse-resource "first-paragraph-under-first-section.org")
first-paragraph-belongs-to-first-section?)))
(t/deftest first-paragraph-under-heading
(t/is (-> (parse-resource "first-paragraph-under-heading.org")
first-paragraph-belongs-to-first-section?
not)))
(defn- walk-types [type & types]
[sut/postorder-walker #(apply sut/of-type? % type types)])
(t/deftest paragraph-ending-with-latex
(let [doc (parse-resource "paragraph-ending-with-latex.org")
type (-> (sp/select-first [(walk-types "paragraph")
(sp/must :children)
sp/LAST]
doc)
sut/type)]
(t/is (= "latex-environment" type))))
(t/deftest paragraph-surrounding-latex
(let [doc (parse-resource "paragraph-surrounding-latex.org")
children (->> doc
(sp/select-first [(walk-types "paragraph")])
:children
(map sut/type))]
(t/is (= ["text" "latex-environment" "text"]
children))))
(t/deftest paragraph-ending-in-bold-surrounding-latex
(let [doc (parse-resource "paragraph-ending-in-bold-surrounding-latex.org")
children (->> doc
(sp/select-first [(walk-types "paragraph")])
:children
(map sut/type))]
(t/is (= ["text" "bold" "latex-environment" "text"]
children))))
(t/deftest paragraph-with-multiple-latex
(let [doc (parse-resource "paragraph-with-multiple-latex.org")
paragraphs (sp/select (walk-types "paragraph") doc)]
(t/is (= 2 (count paragraphs)))
(let [[p p] paragraphs]
(doseq [[p ts] [[p ["text" "latex-environment"
"text" "latex-environment"]]
[p ["text" "latex-environment"
"text" "latex-environment" "text"]]]]
(t/is (= ts (sp/select [(sp/must :children)
sp/ALL (sp/view sut/type)] p)))))))
(t/deftest paragraph-with-separate-latex
(let [doc (parse-resource "paragraph-with-separate-latex.org")
cs (sp/select [(walk-types "section")
(sp/must :children)
sp/ALL
(sp/view sut/type)]
doc)]
(t/is (= ["paragraph" "latex-environment"] cs))))
(t/deftest paragraph-surrounding-separate-latex
(let [doc (parse-resource "paragraph-surrounding-separate-latex.org")
cs (sp/select [(walk-types "section")
(sp/must :children)
sp/ALL
(sp/view sut/type)]
doc)]
(t/is (= ["paragraph" "latex-environment" "paragraph"] cs))))

View File

@@ -0,0 +1,7 @@
#+title: bold-final paragraph surrounding latex
first part of *paragraph*
\begin{equation*}
\text{some \LaTeX \}:)}
\end{equation*}
last part of paragraph

View File

@@ -0,0 +1,7 @@
#+title: paragraph ending with latex
here is the paragraph,
\begin{align*}
\text{and here} &
\\ & \text{is the \LaTeX}
\end{align*}

View File

@@ -0,0 +1,7 @@
#+title: paragraph surrounding latex
first part of paragraph
\begin{equation*}
\text{some \LaTeX \}:)}
\end{equation*}
last part of paragraph

View File

@@ -0,0 +1,9 @@
#+title: paragraphs surrounding separate latex
a paragraph!
\begin{gather*}
\text{and now, an unrelated latex fragment}
\end{gather*}
more unrelated text

View File

@@ -0,0 +1,24 @@
#+title: paragraph with multiple latex environments
* interleaved
first part of paragraph
\begin{equation*}
\text{first \LaTeX\ environment}
\end{equation*}
second part of paragraph
\begin{equation*}
\text{second \LaTeX\ environment}
\end{equation*}
* fenceposted
first fencepost
\begin{equation*}
\text{first fenceposted \LaTeX\ environment}
\end{equation*}
second fencepost
\begin{equation*}
\text{second fenceposted \LaTeX\ environment}
\end{equation*}
third fencepost

View File

@@ -0,0 +1,7 @@
#+title: paragraph with separate latex
a paragraph!
\begin{gather*}
\text{and now, an unrelated latex fragment}
\end{gather*}

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env -S emacs -Q -x
(require 'org-roam)
(setq org-roam-directory (expand-file-name (car command-line-args-left)))
(setq org-roam-db-location (expand-file-name (cadr command-line-args-left)))
(org-roam-db-sync)

View File

@@ -0,0 +1,60 @@
(ns net.deertopia.doerg.render-test
(:require [net.deertopia.doerg.render :as sut]
[net.deertopia.doerg.element :as element]
[net.deertopia.doerg.tex.temml :as temml]
[net.deertopia.doerg.tex.native :as native]
[com.rpl.specter :as sp]
[clojure.edn :as edn]
[clojure.test :as t]
[clojure.java.io :as io]
[clojure.string :as str]))
;; Stupid and hacky.
(defn mathml? [s]
(str/starts-with? s "<math"))
;; Also stupid and hacky. }:)
(defn svg? [s]
(some? (re-matches #"(?s)(<!--.*?-->\n)<svg.*" s)))
(defn read-resource [s]
(let [p (-> (format "net/deertopia/doerg/render_test/%s" s)
io/resource slurp)]
(cond (str/ends-with? s ".edn") (edn/read-string p)
(str/ends-with? s ".org") (element/read-string p))))
(t/deftest latex-fallbacks
(t/testing "LaTeX fallback behaviour"
(let [doc (temml/binding-worker
(-> "fallbacks.org" read-resource sut/render-tex-snippets))
snippets (->> doc
(sp/select
[element/postorder-walker
#(element/of-type?
% "latex-fragment" "latex-environment")
(sp/view #(-> % ::sut/rendered deref str))]))
expectations (-> "fallbacks.edn" read-resource)]
(doall (map (fn [s e]
(let [mathml (mathml? s)
svg (svg? s)]
(assert
(not= mathml svg)
"`mathml?` and `svg?` should be mutually-exclusive.")
(case e
:mathml (t/is mathml)
:svg (t/is svg))))
snippets expectations)))))
(t/deftest latex-laziness
(t/testing "LaTeX laziness"
(let [ex (Exception. "you're supposed to be lazy!")
bad (fn [& _] (throw ex))
doc (read-resource "latexless.org")
r (try (with-redefs-fn {#'native/render bad
#'temml/render bad}
#(sut/render-tex-snippets doc))
(catch Exception e
(if (= e ex)
false
(throw e))))]
(t/is r))))

View File

@@ -0,0 +1,7 @@
[:mathml
:mathml
:mathml
:mathml
:svg
:mathml
:svg]

View File

@@ -0,0 +1,20 @@
#+title: aghhh
- blah blah prose prose prose \(c = \sqrt{x^2 + y^2}\), alal.
- this thing is \(x\)
- another thing \(y\)
- this thing is also \(x\) and uses the same svg
- ifxetex: \(\ifxetex alalala\fi \)
balahahahahahaj
\begin{align*}
x &= y
\\ &= zzz.
\end{align*}
awawawa
cool ass tbale
\begin{tabular}{|c|c|c|}
blah & glah & zlah
\\ abdwa & www &dj
\end{tabular}

View File

@@ -0,0 +1,3 @@
#+title: 이 파일은 LaTeX 코드가 포함되지 않습니다.
🦌!

View File

@@ -0,0 +1,7 @@
:PROPERTIES:
:ID: 23ee464d-b13e-4649-826f-622d0edef24e
:DeertopiaVisibility: public
:END:
#+title: awesome file
wow!

View File

@@ -0,0 +1,23 @@
:PROPERTIES:
:ID: 3c60c74e-f533-43ad-89b3-563975bf80f2
:DeertopiaVisibility: public
:END:
#+title: page not found
the page you're looking for doesn't exist, or you lack the permissions necessary to view it........
#+attr_doerg: :center? true :alt "A doe sitting and sobbing" :scale 1.4
#+begin_example
\{ / \ }/
\/ . . \/
.. \Y` `Y/ ..
\ `\|~==~|/' /
'". T T ."`
{`^' }
| |
/</3 \
|| ||
_| || |_
.,_) ) || ( (_,.
|__ /_||_\ __|
#+end_example

View File

@@ -0,0 +1,7 @@
:PROPERTIES:
:ID: 90f23e03-f746-42cb-862f-1af2d4bde3cc
:DeertopiaVisibility: public
:END:
#+title: Category theory
*Category theory* is the mathematical study of categories and functors between them.

View File

@@ -0,0 +1,7 @@
:PROPERTIES:
:ID: ebc5ea84-77ab-4d60-9b13-ef9160b11d1f
:DeertopiaVisibility: public
:END:
#+title: deertopia.net!!!!!!!!
homeee

View File

@@ -0,0 +1,22 @@
:PROPERTIES:
:ID: 897bfc9d-94ce-4c58-8d21-93f13372b17b
:END:
#+title: Monomorphisms and epimorphisms
In [[id:90f23e03-f746-42cb-862f-1af2d4bde3cc][category theory]], *monomorphisms* and *epimorphisms* are types of cancellative morphisms generalising injective and surjective functions, respectively.[fn:1]
\begin{tikzcd}
% https://q.uiver.app/#q=WzAsNixbMCwwLCJYIl0sWzEsMCwiWSJdLFsyLDAsIloiXSxbMSwxLCJZIl0sWzIsMSwiWiJdLFswLDEsIlgiXSxbMCwxLCJnXzEiLDAseyJvZmZzZXQiOi0yfV0sWzAsMSwiZ18yIiwyLHsib2Zmc2V0IjoyfV0sWzEsMiwiZiIsMl0sWzMsNCwiZ18xIiwwLHsib2Zmc2V0IjotMn1dLFszLDQsImdfMiIsMix7Im9mZnNldCI6Mn1dLFs1LDMsImYiLDJdXQ==
X & Y & Z \\
X & Y & Z
\arrow["{g_1}", shift left=2, from=1-1, to=1-2]
\arrow["{g_2}"', shift right=2, from=1-1, to=1-2]
\arrow["f"', from=1-2, to=1-3]
\arrow["f"', from=2-1, to=2-2]
\arrow["{g_1}", shift left=2, from=2-2, to=2-3]
\arrow["{g_2}"', shift right=2, from=2-2, to=2-3]
\end{tikzcd}
* Footnotes
[fn:1] blahahahahah blah blah

View File

@@ -0,0 +1,63 @@
(ns net.deertopia.doerg.roam-test
(:require [net.deertopia.doerg.roam :as sut]
[clojure.test :as t]
[clojure.java.io :as io]
[net.deertopia.doerg.config :as cfg]
[babashka.fs :as fs]
[babashka.process :as p]
[net.deertopia.doerg.config-test :refer [test-config-fixture]]
[next.jdbc :as sql]
[clojure.string :as str]
[net.deertopia.doerg.elisp :as elisp]
[com.rpl.specter :as sp]))
(def org-roam-directory
(fs/file "test/net/deertopia/doerg/roam-test"))
(defn org-roam-db-sync [db-file]
(let [script-file (fs/create-temp-file {:prefix "org-roam-db-sync-"
:suffix ".el"})
emacs (->> [(System/getenv "EMACS") "test-emacs" "emacs"]
(some #(some-> % fs/which)))]
(io/copy (-> "net/deertopia/doerg/org-roam-db-sync.el"
io/resource io/reader)
(fs/file script-file))
(p/shell {:out :string :err :string}
emacs "-Q" "-x" script-file org-roam-directory db-file)
(fs/delete script-file)))
(defn test-db-fixture [f]
(let [db-file (-> cfg/*cfg* ::cfg/org-roam-db-path)]
(assert (->> db-file fs/canonicalize str
(re-matches #"^/(build|tmp)/.*"))
(format "i'm scared to delete a non-test database... %s"
(str db-file)))
(fs/delete-if-exists db-file)
(org-roam-db-sync db-file)
(f)
(fs/delete db-file)))
(t/use-fixtures
:once (t/join-fixtures [test-config-fixture test-db-fixture]))
(t/deftest all-nodes-exist
(let [known-node-files (->> (fs/list-dir org-roam-directory)
(map (comp str fs/canonicalize))
(into #{}))
database-nodes
(->> (sql/execute!
(sut/ds) ["select file, id from nodes"])
(map (fn [x]
{:file (-> x :nodes/file elisp/read-string)
:id (-> x :nodes/id elisp/read-string parse-uuid)}))
(into #{}))]
(t/testing "database has a node for each file?"
(t/is (= known-node-files (sp/transform
[sp/ALL]
#(:file %)
database-nodes))))
(t/testing "each uuid exists?"
(t/is (every? (comp sut/uuid-exists? :id)
database-nodes)))))

View File

@@ -0,0 +1,52 @@
(ns net.deertopia.doerg.server-test
(:require [net.deertopia.doerg.server :as sut]
[reitit.ring]
[clojure.test :as t]
[net.deertopia.doerg.config :as cfg]
[net.deertopia.doerg.roam :as roam]
[babashka.fs :as fs]
[clojure.java.io :as io]
[net.deertopia.doerg.roam-test :refer [test-db-fixture]]
[net.deertopia.doerg.config-test :refer [test-config-fixture]]))
(t/use-fixtures
:once (t/join-fixtures [test-config-fixture test-db-fixture]))
(defn with-server [f]
(let [was-already-running? (= :running (sut/status))]
(when-not was-already-running?
(sut/start!))
(f)
(when-not was-already-running?
(sut/stop!))))
(defn get-sut [uri]
(sut/app {:request-method :get
:uri uri}))
(t/deftest server-is-running
;; 서버는 벌써 시작한 다음에 이 테스트 하면 잘못됩니다.
;; (assert (not= :running (sut/status)))
(with-server
(fn []
(t/is (= :running (sut/status)))
(t/is (->> (format "http://localhost:%d"
(::cfg/port cfg/*cfg*))
slurp
string?)))))
(t/deftest get-nonexistent-node
(let [slug "3Lxvxnb0QrivoU3DX-l_5w"]
(assert (nil? (roam/make-node slug)))
(t/is (= 404
(-> (str "/n/" slug)
get-sut :status)))))
(t/deftest get-homepage
(let [resp (-> (str "/n/" sut/homepage-slug)
get-sut)]
(t/is (= 200 (:status resp)))
(t/is (= (-> "/" get-sut :body)
(-> resp :body)))))

1
tests.edn Normal file
View File

@@ -0,0 +1 @@
#kaocha/v1 {}

8
vendor/default.nix vendored
View File

@@ -1,8 +0,0 @@
{ fetchzip
, fetchurl
, callPackage
}:
{
ibm-plex-web = callPackage ./ibm-plex-web.nix {};
}