Files
logseq/web/src/main/frontend/format/block.cljs
2020-07-21 17:46:30 +08:00

297 lines
11 KiB
Clojure

(ns frontend.format.block
(:require [frontend.util :as util :refer-macros [profile]]
[clojure.walk :as walk]
[clojure.string :as string]
[frontend.format :as format]
[frontend.utf8 :as utf8]
[medley.core :as medley]
[frontend.config :as config]
[datascript.core :as d]
[clojure.set :as set]))
(defn heading-block?
[block]
(and
(vector? block)
(= "Heading" (first block))))
(defn get-tag
[block]
(and (vector? block)
(= "Tag" (first block))
(second block)))
(defn get-page-reference
[block]
(when (and (vector? block) (= "Link" (first block)))
(let [typ (first (:url (second block)))]
(or
(and
(= typ "Search")
(not (contains? #{\# \* \/ \( \[} (first (second (:url (second block))))))
(let [page (second (:url (second block)))]
(when (and (not (string/starts-with? page "http"))
(not (string/starts-with? page "file"))
(not (string/ends-with? page ".html")))
page)))
(and
(= typ "Complex")
(= (:protocol (second (:url (second block)))) "file")
(:link (second (:url (second block)))))))))
(defn task-block?
[block]
(and
(heading-block? block)
(some? (:marker (second block)))))
;; FIXME:
(defn extract-title
[block]
(-> (:title (second block))
first
second))
(defn- paragraph-block?
[block]
(and
(vector? block)
(= "Paragraph" (first block))))
(defn- timestamp-block?
[block]
(and
(vector? block)
(= "Timestamp" (first block))))
(defn- properties-block?
[block]
(and
(vector? block)
(= "Property_Drawer" (first block))))
(defn extract-properties
[[_ properties start-pos end-pos :as all]]
{:properties (into {} properties)
:start-pos start-pos
:end-pos end-pos})
(defn- paragraph-timestamp-block?
[block]
(and (paragraph-block? block)
(timestamp-block? (first (second block)))))
(defn extract-timestamp
[block]
(-> block
second
first
second))
(defn with-refs
[{:keys [title body tags] :as heading}]
(let [tags (mapv :tag/name (util/->tags (map :tag/name tags)))
ref-pages (atom tags)]
(walk/postwalk
(fn [form]
(when-let [page (get-page-reference form)]
(swap! ref-pages conj (string/lower-case page)))
(when-let [tag (get-tag form)]
(when (util/tag-valid? tag)
(swap! ref-pages conj (string/lower-case tag))))
form)
(concat title body))
(let [ref-pages (remove string/blank? @ref-pages)]
(assoc heading :ref-pages (vec ref-pages)))))
(defn safe-headings
[headings]
(map (fn [heading]
(let [heading (util/remove-nils heading)]
(medley/map-keys
(fn [k] (keyword "heading" k))
heading)))
headings))
(defn collect-heading-tags
[{:keys [title body tags] :as heading}]
(cond-> heading
(seq tags)
(assoc :tags (util/->tags tags))))
(defn extract-headings
[blocks last-pos encoded-content]
(let [headings
(loop [headings []
heading-body []
blocks (reverse blocks)
timestamps {}
properties {}
last-pos last-pos
last-level 1000
children []]
(if (seq blocks)
(let [block (first blocks)
level (:level (second block))]
(cond
(paragraph-timestamp-block? block)
(let [timestamp (extract-timestamp block)
timestamps' (conj timestamps timestamp)]
(recur headings heading-body (rest blocks) timestamps' properties last-pos last-level children))
(properties-block? block)
(let [properties (extract-properties block)]
(recur headings heading-body (rest blocks) timestamps properties last-pos last-level children))
(heading-block? block)
(let [id (or (when-let [custom-id (get-in properties [:properties "CUSTOM_ID"])]
(when (util/uuid-string? custom-id)
(uuid custom-id)))
(d/squuid))
heading (second block)
level (:level heading)
[children current-heading-children]
(cond
(>= level last-level)
[(conj children [id level])
#{}]
(< level last-level)
(let [current-heading-children (set (->> (filter #(< level (second %)) children)
(map first)))
others (vec (remove #(< level (second %)) children))]
[(conj others [id level])
current-heading-children]))
heading (-> (assoc heading
:uuid id
:body (vec (reverse heading-body))
:timestamps timestamps
:properties (:properties properties)
:properties-meta (dissoc properties :properties)
:children (or current-heading-children []))
(assoc-in [:meta :end-pos] last-pos))
heading (collect-heading-tags heading)
heading (with-refs heading)
last-pos' (get-in heading [:meta :pos])]
(recur (conj headings heading) [] (rest blocks) {} {} last-pos' (:level heading) children))
:else
(let [heading-body' (conj heading-body block)]
(recur headings heading-body' (rest blocks) timestamps properties last-pos last-level children))))
(-> (reverse headings)
safe-headings)))]
(let [first-heading (first headings)
first-heading-start-pos (get-in first-heading [:heading/meta :pos])]
(if (and
(not (string/blank? encoded-content))
(or (empty? headings)
(> first-heading-start-pos 1)))
(cons
(merge
(let [content (utf8/substring encoded-content 0 first-heading-start-pos)
uuid (d/squuid)]
{:heading/uuid uuid
:heading/content content
:heading/anchor (str uuid)
:heading/level 2
:heading/meta {:pos 0
:end-pos (or first-heading-start-pos
(utf8/length encoded-content))}
:heading/body (take-while (fn [block] (not (heading-block? block))) blocks)
:heading/pre-heading? true})
(select-keys first-heading [:heading/file :heading/format :heading/page]))
headings)
headings))))
(defn parse-heading
[{:heading/keys [uuid content meta file page] :as heading} format]
(when-not (string/blank? content)
(let [ast (format/to-edn content format nil)
start-pos (:pos meta)
encoded-content (utf8/encode content)
content-length (utf8/length encoded-content)
headings (extract-headings ast content-length encoded-content)
ref-pages-atom (atom [])
headings (doall
(map-indexed
(fn [idx {:heading/keys [ref-pages meta] :as heading}]
(let [heading (collect-heading-tags heading)
heading (merge
heading
{:heading/meta meta
:heading/marker (get heading :heading/marker "nil")
:heading/properties (get heading :heading/properties [])
:heading/properties-meta (get heading :heading/properties-meta [])
:heading/file file
:heading/format format
:heading/page page
:heading/content (utf8/substring encoded-content
(:pos meta)
(:end-pos meta))}
;; Preserve the original heading id
(when (and (zero? idx)
;; not custom-id
(not (get-in heading [:heading/properties "CUSTOM_ID"])))
{:heading/uuid uuid})
(when (seq ref-pages)
{:heading/ref-pages
(mapv
(fn [page]
(let [page-name (string/lower-case page)
page {:page/name page-name}]
(swap! ref-pages-atom conj page)
page))
ref-pages)}))]
(-> heading
(assoc-in [:heading/meta :pos] (+ (:pos meta) start-pos))
(assoc-in [:heading/meta :end-pos] (+ (:end-pos meta) start-pos)))))
headings))
pages (vec (distinct @ref-pages-atom))]
{:headings headings
:pages pages
:start-pos start-pos
:end-pos (+ start-pos content-length)})))
(defn with-levels
[text format {:heading/keys [level pre-heading?]}]
(let [pattern (config/get-heading-pattern format)
prefix (if pre-heading? "" (str (apply str (repeat level pattern)) " "))]
(str prefix (string/triml text))))
(defn macro-subs
[macro-content arguments]
(loop [s macro-content
args arguments
n 1]
(if (seq args)
(recur
(string/replace s (str "$" n) (first args))
(rest args)
(inc n))
s)))
(comment
(defn sort-tasks
[headings]
(let [markers ["NOW" "LATER" "DOING" "IN-PROGRESS" "TODO" "WAITING" "WAIT" "DONE" "CANCELED" "CANCELLED"]
markers (zipmap markers (reverse (range 1 (count markers))))
priorities ["A" "B" "C" "D" "E" "F" "G"]
priorities (zipmap priorities (reverse (range 1 (count priorities))))]
(sort (fn [t1 t2]
(let [m1 (get markers (:heading/marker t1) 0)
m2 (get markers (:heading/marker t2) 0)
p1 (get priorities (:heading/priority t1) 0)
p2 (get priorities (:heading/priority t2) 0)]
(cond
(and (= m1 m2)
(= p1 p2))
(compare (str (:heading/title t1))
(str (:heading/title t2)))
(= m1 m2)
(> p1 p2)
:else
(> m1 m2))))
headings))))