From fa446589d31e0e1e633fc4edc3f7257c7facb2a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Sun, 1 Feb 2026 00:36:36 -0700 Subject: [PATCH] basic rendering! --- .envrc | 1 + deps-lock.json | 10 +- doerg/deps.edn | 4 +- .../net/deertopia/doerg/deerstar.css | 11 + .../net/deertopia/doerg/tuftesque.css | 544 ++++++++++++++++++ doerg/src/net/deertopia/doerg/config.clj | 10 + doerg/src/net/deertopia/doerg/element.clj | 101 ++++ doerg/src/net/deertopia/doerg/html.clj | 37 ++ doerg/src/net/deertopia/doerg/parse.clj | 17 - doerg/src/net/deertopia/doerg/render.clj | 76 +++ .../test/net/deertopia/doerg/config_test.clj | 8 + .../test/net/deertopia/doerg/element_test.clj | 54 ++ .../doerg/element_test/greater-elements.org | 5 + 13 files changed, 859 insertions(+), 19 deletions(-) create mode 100644 doerg/resources/net/deertopia/doerg/deerstar.css create mode 100644 doerg/resources/net/deertopia/doerg/tuftesque.css create mode 100644 doerg/src/net/deertopia/doerg/config.clj create mode 100644 doerg/src/net/deertopia/doerg/element.clj create mode 100644 doerg/src/net/deertopia/doerg/html.clj delete mode 100644 doerg/src/net/deertopia/doerg/parse.clj create mode 100644 doerg/src/net/deertopia/doerg/render.clj create mode 100644 doerg/test/net/deertopia/doerg/config_test.clj create mode 100644 doerg/test/net/deertopia/doerg/element_test.clj create mode 100644 doerg/test/net/deertopia/doerg/element_test/greater-elements.org diff --git a/.envrc b/.envrc index 3550a30..e809d8e 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ +IBM_TELEMETRY_DISABLED='true' use flake diff --git a/deps-lock.json b/deps-lock.json index 7edfe30..986bedb 100644 --- a/deps-lock.json +++ b/deps-lock.json @@ -1,6 +1,14 @@ { "lock-version": 4, - "git-deps": [], + "git-deps": [ + { + "lib": "io.github.msyds/spec-dict", + "url": "https://github.com/msyds/spec-dict.git", + "rev": "531d629b7f05f37232261cf9e8927a4b5915714f", + "git-dir": "https/github.com/msyds/spec-dict", + "hash": "sha256-5hMdPsB8OhOCtByPZS+CHXzVLq0H+OBKKnXec21xwmg=" + } + ], "mvn-deps": [ { "mvn-path": "babashka/fs/0.5.24/fs-0.5.24.jar", diff --git a/doerg/deps.edn b/doerg/deps.edn index d6beb3d..ad4373c 100644 --- a/doerg/deps.edn +++ b/doerg/deps.edn @@ -2,5 +2,7 @@ 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"}} + babashka/process {:mvn/version "0.6.25"} + io.github.msyds/spec-dict + {:git/sha "531d629b7f05f37232261cf9e8927a4b5915714f"}} :paths ["src" "resources" "test"]} diff --git a/doerg/resources/net/deertopia/doerg/deerstar.css b/doerg/resources/net/deertopia/doerg/deerstar.css new file mode 100644 index 0000000..d8b50eb --- /dev/null +++ b/doerg/resources/net/deertopia/doerg/deerstar.css @@ -0,0 +1,11 @@ +@charset "UTF-8"; + +:root +{ --ds-hoof-black: #0c0c0f +; --ds-velvet-grey: #4a4241 +; --ds-untitled-1: rgb(47, 35, 28) +; --ds-untitled-2: rgb(168, 134, 109) +; --ds-buck-brown: #b57b4c +; --ds-antler-tan: #fce7ca +; --ds-fawn-spot-white: #fffffa +} diff --git a/doerg/resources/net/deertopia/doerg/tuftesque.css b/doerg/resources/net/deertopia/doerg/tuftesque.css new file mode 100644 index 0000000..af1619c --- /dev/null +++ b/doerg/resources/net/deertopia/doerg/tuftesque.css @@ -0,0 +1,544 @@ +@charset "UTF-8"; + +@import "/vendor/ibm-plex-serif/css/ibm-plex-serif-default.min.css"; +@import "/vendor/ibm-plex-sans-kr/css/ibm-plex-sans-kr-default.min.css"; +@import "/vendor/ibm-plex-math/css/ibm-plex-math-default.min.css"; +@import "deerstar.css"; + +html { + font-size: 15px; +} + +body { + font-family: "IBM Plex Serif", "IBM Plex Sans KR", "IBM Plex Math", Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif; + counter-reset: sidenote-counter; + background-color: var(--ds-antler-tan); + color: var(--ds-hoof-black); +} + +/* Adds dark mode */ +@media (prefers-color-scheme: dark) { + body { + background-color: var(--ds-untitled-1); + color: var(--ds-antler-tan); + } +} + +math { font-family: "IBM Plex Math"; } + +h1 { + font-weight: 400; + margin-top: 4rem; + margin-bottom: 1.5rem; + font-size: 3.2rem; + line-height: 1; +} + +h2 { + font-style: italic; + font-weight: 400; + margin-top: 2.1rem; + margin-bottom: 1.4rem; + font-size: 2.2rem; + line-height: 1; +} + +h3 { + font-style: italic; + font-weight: 400; + font-size: 1.7rem; + margin-top: 2rem; + margin-bottom: 1.4rem; + line-height: 1; +} + +hr { + display: block; + height: 1px; + width: 55%; + border: 0; + border-top: 1px solid var(--ds-velvet-grey); + margin: 1em 0; + padding: 0; +} + +@media (prefers-color-scheme: dark) { + hr { + border-color: var(--ds-velvet-grey); + } +} + +p.subtitle { + font-style: italic; + margin-top: 1rem; + margin-bottom: 1rem; + font-size: 1.8rem; + display: block; + line-height: 1; +} + +.numeral { + font-family: et-book-roman-old-style; +} + +.danger { + color: red; +} + +article { + padding-top: 3rem; + padding-bottom: 5rem; + padding-left: 6%; + padding-right: 0; + width: 87.5%; + margin-left: auto; + margin-right: auto; + max-width: 1400px; +} + +section { + padding-top: 1rem; + padding-bottom: 1rem; +} + +p, +dl, +ol, +ul { + font-size: 1.4rem; + line-height: 2rem; +} + +p { + margin-top: 1.4rem; + margin-bottom: 1.4rem; + padding-right: 0; + vertical-align: baseline; +} + +/* Chapter Epigraphs */ +div.epigraph { + margin: 5em 0; +} + +div.epigraph > blockquote { + margin-top: 3em; + margin-bottom: 3em; +} + +div.epigraph > blockquote, +div.epigraph > blockquote > p { + font-style: italic; +} + +div.epigraph > blockquote > footer { + font-style: normal; +} + +div.epigraph > blockquote > footer > cite { + font-style: italic; +} +/* end chapter epigraphs styles */ + +blockquote { + font-size: 1.4rem; +} + +blockquote p { + width: 55%; + margin-right: 40px; +} + +blockquote footer { + width: 55%; + font-size: 1.1rem; + text-align: right; +} + +section > p, +section > footer, +section > table { + width: 55%; +} + +/* 50 + 5 == 55, to be the same width as paragraph */ +section > dl, +section > ol, +section > ul { + width: 50%; + -webkit-padding-start: 5%; + /* Accounts for the padding from the bullet, resulting in the same width as + paragraphs. */ + width: calc(55% - 40px); +} + +dt:not(:first-child), +li:not(:first-child) { + margin-top: 0.25rem; +} + +figure { + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + max-width: 55%; + -webkit-margin-start: 0; + -webkit-margin-end: 0; + margin: 0 0 3em 0; +} + +figcaption { + float: right; + clear: right; + margin-top: 0; + margin-bottom: 0; + font-size: 1.1rem; + line-height: 1.6; + vertical-align: baseline; + position: relative; + max-width: 40%; + text-align: left +} + +figure.fullwidth figcaption { + margin-right: 24%; +} + +a:link, +a:visited { + color: inherit; + text-underline-offset: 0.1em; + text-decoration-thickness: 0.05em; +} + +/* Sidenotes, margin notes, figures, captions */ +img { + max-width: 100%; +} + +.sidenote, +.margin-note { + float: right; + clear: right; + margin-right: -60%; + width: 50%; + margin-top: 0.3rem; + /* margin-bottom: 0; */ + margin-bottom: 1em; + font-size: 1.1rem; + line-height: 1.3; + vertical-align: baseline; + position: relative; +} + +.sidenote-number { + counter-increment: sidenote-counter; +} + +li .sidenote, +li .margin-note { + /* it's so close lol */ + transform: translateX(4px) +} + +.sidenote-number:after, +.sidenote:before { + font-family: et-book-roman-old-style; + position: relative; + vertical-align: baseline; +} + +.sidenote-number:after { + content: counter(sidenote-counter); + font-size: 1rem; + top: -0.5rem; + left: 0.1rem; +} + +.sidenote:before { + content: counter(sidenote-counter) " "; + font-size: 1rem; + top: -0.5rem; +} + +blockquote .sidenote, +blockquote .margin-note { + margin-right: -82%; + min-width: 59%; + text-align: left; +} + +div.fullwidth, +table.fullwidth { + width: 100%; +} + +div.table-wrapper { + overflow-x: auto; + font-family: "Trebuchet MS", "Gill Sans", "Gill Sans MT", sans-serif; +} + +.sans { + font-family: "Gill Sans", "Gill Sans MT", Calibri, sans-serif; + letter-spacing: .03em; +} + +code, pre > code { + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 1.0rem; + line-height: 1.42; + -webkit-text-size-adjust: 100%; /* Prevent adjustments of font size after orientation changes in iOS. See https://github.com/edwardtufte/tufte-css/issues/81#issuecomment-261953409 */ +} + +.sans > code { + font-size: 1.2rem; +} + +h1 > code, +h2 > code, +h3 > code { + font-size: 0.80em; +} + +.margin-note > code, +.sidenote > code { + font-size: 1rem; +} + +pre > code { + font-size: 0.9rem; + width: 52.5%; + margin-left: 2.5%; + overflow-x: auto; + display: block; +} + +pre.fullwidth > code { + width: 90%; +} + +.fullwidth { + max-width: 90%; + clear:both; +} + +span.newthought { + font-variant: small-caps; + font-size: 1.2em; +} + +input.margin-toggle { + display: none; +} + +label.sidenote-number { + display: inline-block; + max-height: 2rem; /* should be less than or equal to paragraph line-height */ +} + +label.margin-toggle:not(.sidenote-number) { + display: none; +} + +.iframe-wrapper { + position: relative; + padding-bottom: 56.25%; /* 16:9 */ + padding-top: 25px; + height: 0; +} + +.iframe-wrapper iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +@media (max-width: 760px) { + article { + width: 84%; + padding-left: 8%; + padding-right: 8%; + } + + hr, + section > p, + section > footer, + section > table { + width: 100%; + } + + pre > code { + width: 97%; + } + + section > dl, + section > ol, + section > ul { + width: 90%; + } + + figure { + max-width: 90%; + } + + figcaption, + figure.fullwidth figcaption { + margin-right: 0%; + max-width: none; + } + + blockquote { + margin-left: 1.5em; + margin-right: 0em; + } + + blockquote p, + blockquote footer { + width: 100%; + } + + label.margin-toggle:not(.sidenote-number) { + display: inline; + } + + .sidenote, + .margin-note { + display: none; + } + + .margin-toggle:checked + .sidenote, + .margin-toggle:checked + .margin-note { + display: block; + float: left; + left: 1rem; + clear: both; + width: 95%; + margin: 1rem 2.5%; + vertical-align: baseline; + position: relative; + } + + label { + cursor: pointer; + } + + div.table-wrapper, + table { + width: 85%; + } + + img { + width: 100%; + } +} + +.link.external::after +{ content: "↗" +; vertical-align: super +; font-size: 1rem +; line-height: 0 +} + +.center +{ align-items: "center" +; justify-content: "center" +; display: flex +; max-width: 55% +} + +@media (max-width: 760px) { + .center + { max-width: 90% + } +} + +.navbar +{ padding: 0 1rem +; font-size: 1rem +; color: var(--ds-velvet-grey) +; height: 2rem +} + +@media (prefers-color-scheme: dark) { + .navbar { + color: var(--ds-untitled-2) + } +} + +.navbar-list +{ list-style-type: none +; padding-left: 0 +; font-size: 1.2rem +; display: flex +; flex-direction: row +; flex-wrap: wrap +; column-gap: 1.5rem +} + +.navbar-list li +{ display: inline +; margin-top: 0 +} + +#page-info +{ list-style-type: none +; padding-left: 0 +; font-size: 0.8rem +; color: var(--ds-velvet-grey) +; text-align: center +} + +@media (prefers-color-scheme: dark) { + #page-info { + color: var(--ds-untitled-2) + } +} + +.home-link +{ text-decoration: none +} + +#references ul +{ width: auto +} + +figcaption { + margin-right: -60%; + width: 50%; + margin-top: 1.4rem; + line-height: 1.3; + max-width: unset; +} + +figure.fullwidth { + display: inline-block; +} + +figure.fullwidth figcaption { + margin-right: 0%; + float: none; + width: auto; + text-align: center; +} + +@media (max-width: 760px) { + figcaption, + figure.fullwidth figcaption { + margin-right: 0%; + float: none; + width: auto; + text-align: center; + } +} + +.empty-section-message +{ color: var(--ds-untitled-2); +; font-style: italic +; text-align: center +; max-width: 55% +; font-size: 1.5rem +} diff --git a/doerg/src/net/deertopia/doerg/config.clj b/doerg/src/net/deertopia/doerg/config.clj new file mode 100644 index 0000000..f86565b --- /dev/null +++ b/doerg/src/net/deertopia/doerg/config.clj @@ -0,0 +1,10 @@ +(ns net.deertopia.doerg.config + (:require [clojure.spec.alpha :as s] + [spec-dict.main :refer [dict]])) + +(s/def ::config + (s/keys :req [])) + +(def default {}) + +(def ^:dynamic *cfg* default) diff --git a/doerg/src/net/deertopia/doerg/element.clj b/doerg/src/net/deertopia/doerg/element.clj new file mode 100644 index 0000000..16efec8 --- /dev/null +++ b/doerg/src/net/deertopia/doerg/element.clj @@ -0,0 +1,101 @@ +(ns net.deertopia.doerg.element + (:require [babashka.process :as p] + [clojure.string :as str] + [clojure.zip :as z] + [babashka.fs :as fs] + [clojure.java.io :as io] + [cheshire.core :as json] + [clojure.spec.alpha :as s] + [spec-dict.main :refer [dict]] + [net.deertopia.doerg.config :as cfg]) + (:refer-clojure :exclude [read-string])) + + +(defonce ^:private uniorg-script-path-atom (atom nil)) + +(def ^:dynamic *uniorg-timeout-after-milliseconds* + (* 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) + (str/join "-"))) + +(defn uniorg [& {:keys [in] + :or {in *in*}}] + (let [r (-> (p/process + {:in in :out :string} + "doerg-parser") + (deref-with-timeout *uniorg-timeout-after-milliseconds*))] + (if (zero? (:exit r)) + (-> r :out (json/parse-string (comp keyword camel->kebab)))))) + +(defn read-string [s] + (with-in-str s + (uniorg :in *in*))) + + + +(defn greater-element? + "Return truthy if `e` is a greater org-element; i.e. one that can + have children." + [e] + ;; Not 100% sure if this is a valid definition. It seems that + ;; Uniorg sets `:children` to an empty vector when a great element + ;; lacks children. + (contains? e :children)) + +(defn org-element? [element] + #_ + (s/valid? ::org-element element) + (and (map? element) + (contains? element :type))) + +(defn of-type? [element type] + (= (:type element) type)) + + +;;; 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?)})) + + +;;; Zipper + +(defn doerg-zip [document] + (z/zipper greater-element? + :children + #(assoc %1 :children %2) + document)) + +(defn cata + "Catamorphism on a zipper." + [loc f] + (let [loc* (if-some [child (z/down loc)] + (loop [current child] + (let [current* (cata current f)] + (if-some [right (z/right current*)] + (recur right) + (z/up current*)))) + loc)] + (z/replace loc* (f (z/node loc*))))) diff --git a/doerg/src/net/deertopia/doerg/html.clj b/doerg/src/net/deertopia/doerg/html.clj new file mode 100644 index 0000000..74f01cd --- /dev/null +++ b/doerg/src/net/deertopia/doerg/html.clj @@ -0,0 +1,37 @@ +(ns net.deertopia.doerg.html + "Common HTML elements and utilities" + (:require [clojure.java.io :as io])) + +#_ +(def navbar + "Hiccup element for Deertopia.net's navbar." + [:nav.navbar + [:ol.navbar-list + [:li + [:a.home-link {:href "/"} + "🦌 deertopia.net"]] + [:li + [:a.home-link {:href "/graph"} + "graph"]] + #_ + [:li + [:a.home-link {:onclick "alert('unimplemented }:(')"} + "search"]]]]) + +(def viewport + [:meta {:name "viewport" + :content "width=device-width, initial-scale=1.0"}]) + +(def charset + [:meta {:charset "utf-8"}]) + +(def tuftesque + #_ + [:link {:rel "stylesheet" + :type "text/css" + :href "/resources/tuftesque.css"}] + [:style + (slurp (io/resource "net/deertopia/doerg/tuftesque.css"))]) + +(def head + (list viewport charset tuftesque)) diff --git a/doerg/src/net/deertopia/doerg/parse.clj b/doerg/src/net/deertopia/doerg/parse.clj deleted file mode 100644 index be33f2b..0000000 --- a/doerg/src/net/deertopia/doerg/parse.clj +++ /dev/null @@ -1,17 +0,0 @@ -(ns net.deertopia.doerg.parse - (:require [babashka.process :as p] - [babashka.fs :as fs] - [clojure.java.io :as io] - [cheshire.core :as json]) - (:refer-clojure :exclude [read-string])) - -(defonce ^:private uniorg-script-path-atom (atom nil)) - -(defn- uniorg [] - @(p/process {:in (slurp "/home/msyds/org/20260124165717-if_so_in_korean.org") - :out :string} - "doerg-parser")) - -(defn read-string [s] - #_ - (p/process "node" (uniorg-script-path))) diff --git a/doerg/src/net/deertopia/doerg/render.clj b/doerg/src/net/deertopia/doerg/render.clj new file mode 100644 index 0000000..548b7a5 --- /dev/null +++ b/doerg/src/net/deertopia/doerg/render.clj @@ -0,0 +1,76 @@ +(ns net.deertopia.doerg.render + (:require [net.deertopia.doerg.element :as element] + [clojure.tools.logging :as l] + [clojure.tools.logging.readable :as lr] + [net.deertopia.doerg.html :as doerg-html] + [clojure.zip :as z])) + +;;; Top-level API + +(defmulti org-element + "Render an Org element to Hiccup." + #(do (assert (element/org-element? %) + "Not an org-node!") + (:type %))) + +(defmulti org-link + "Render an Org-mode link element to Hiccup. Dispatches on link + type/protocol." + #(do (assert (element/of-type? % "link")) + (:link-type %))) + +(defmulti org-special-block + "Render an Org-mode special block to Hiccup. Dispatches on special + block type (as in #+begin_«type» … #+end_«type»)." + #(do (assert (element/of-type? % "special-block")) + (:block-type %))) + +(defmulti org-keyword + "Render an Org-mode keyword." + #(do (assert (element/of-type? % "keyword")) + (:key %))) + +(def ^:dynamic ^:private *document-info*) + +(declare ^:private gather-footnotes renderer-error) + +(defn org-element-recursive + "Recursively render an Org-mode element to Hiccup." + [e] + (let [loc (element/doerg-zip e)] + (-> loc + (element/cata + (fn [node] + (try (org-element node) + (catch Throwable e + (lr/error e "Error in renderer" {:node node}) + (renderer-error e))))) + z/node))) + +(defn org-document + "Recursively render an Org-mode document to Hiccup." + [doc] + (let [loc (element/doerg-zip doc)] + (binding [*document-info* {:footnotes (gather-footnotes loc)}] + (let [rendered (org-element-recursive doc)] + [:html + [:head + [:title "org document"] + doerg-html/viewport + doerg-html/charset + doerg-html/tuftesque] + [:body + [:article + rendered]]])))) + + + +(defn- gather-footnotes [loc] + {}) + + + +(defn- renderer-error + "Render a `Throwable` to display within the document." + [e] + "aaaa an error!") diff --git a/doerg/test/net/deertopia/doerg/config_test.clj b/doerg/test/net/deertopia/doerg/config_test.clj new file mode 100644 index 0000000..e7902eb --- /dev/null +++ b/doerg/test/net/deertopia/doerg/config_test.clj @@ -0,0 +1,8 @@ +(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)))) diff --git a/doerg/test/net/deertopia/doerg/element_test.clj b/doerg/test/net/deertopia/doerg/element_test.clj new file mode 100644 index 0000000..d2f0790 --- /dev/null +++ b/doerg/test/net/deertopia/doerg/element_test.clj @@ -0,0 +1,54 @@ +(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])) + +(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))) + +(t/deftest known-greater-elements + (t/testing "known greater elements satisfy `greater-element?`" + (let [s (-> "net/deertopia/doerg/element_test/greater-elements.org" + io/resource slurp) + root (sut/read-string s) + section (first-child-of-type root "section") + 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)))))) diff --git a/doerg/test/net/deertopia/doerg/element_test/greater-elements.org b/doerg/test/net/deertopia/doerg/element_test/greater-elements.org new file mode 100644 index 0000000..e458d47 --- /dev/null +++ b/doerg/test/net/deertopia/doerg/element_test/greater-elements.org @@ -0,0 +1,5 @@ +#+title: greater elements test + +* a headline/section + +this should be a greater element