(ns outliner.model.core.text-ops (:require [clojure.string :as string] [outliner.model.core.annotations :as annotations])) (def invisible-chars-pattern #"[\uFEFF\u200B\u200C\u200D\u200E\u200F\u2060]") (defn clean-text [s] (if (nil? s) "" (string/replace s invisible-chars-pattern ""))) (defn render-annotated-text ([node] (render-annotated-text node nil)) ([{:keys [text annotations]} markdown-normalize-fn] (if (empty? text) "" (let [md-links (->> annotations (filter #(= (:type %) :markdown-link)) (sort-by :range-start)) ;; Replace markdown link ranges with their display labels replaced-text (loop [links md-links last-pos 0 acc []] (if (seq links) (let [link (first links) start (:range-start link) end (:range-end link) label (:display-text link)] (recur (rest links) end (conj acc (subs text last-pos start) label))) (apply str (conj acc (subs text last-pos))))) ;; If normalize-fn is provided, strip style markers from the result final-node (if markdown-normalize-fn (markdown-normalize-fn replaced-text []) {:text replaced-text})] (:text final-node))))) (defn get-segments ([text annotations] (get-segments text annotations nil)) ([text annotations extra-boundaries] (let [boundaries (->> annotations (mapcat (fn [a] [(:range-start a) (:range-end a)])) (concat [0 (count text)]) (concat (or extra-boundaries [])) (filter #(<= 0 % (count text))) distinct sort)] (loop [bs boundaries segments []] (if (>= (count bs) 2) (let [start (first bs) end (second bs) content (subs text start end) applicable-anns (filter (fn [a] (and (<= (:range-start a) start) (>= (:range-end a) end))) annotations)] (recur (rest bs) (conj segments {:text content :anns applicable-anns :end end}))) segments))))) (defn- annotations-match? [a1 a2] (and (= (:type a1) (:type a2)) (case (:type a1) :style (= (:style a1) (:style a2)) :link (= (:url a1) (:url a2)) :markdown-link (= (:url a1) (:url a2)) true))) (defn trim-node-text [node] (let [text (:text node) annotations (:annotations node []) trimmed-text (string/replace text #"\s+$" "") diff (- (count text) (count trimmed-text))] (if (pos? diff) (let [new-end (count trimmed-text) new-annotations (->> annotations (map (fn [ann] (cond-> ann (> (:range-start ann) new-end) (assoc :range-start new-end) (> (:range-end ann) new-end) (assoc :range-end new-end)))) (filter #(> (:range-end %) (:range-start %))) vec)] (assoc node :text trimmed-text :annotations new-annotations)) node))) (defn split-annotations [annotations pos] (reduce (fn [ [before after] ann] (let [{:keys [range-start range-end]} ann] (cond (<= range-end pos) [(conj before ann) after] (>= range-start pos) [before (conj after (-> ann (update :range-start - pos) (update :range-end - pos)))] :else [(conj before (assoc ann :range-end pos)) (conj after (-> ann (assoc :range-start 0) (update :range-end - pos)))]))) [[] []] annotations)) (defn toggle-style [annotations start end style] (let [matching-ann (some (fn [a] (when (and (= (:type a) :style) (= (:style a) style) (<= (:range-start a) start) (>= (:range-end a) end)) a)) annotations)] (if matching-ann (let [as (vec (remove #(= % matching-ann) annotations)) s (:range-start matching-ann) e (:range-end matching-ann)] (cond-> as (< s start) (conj (assoc matching-ann :range-end start)) (> e end) (conj (assoc matching-ann :range-start end)))) (conj (vec annotations) {:range-start start :range-end end :type :style :style style})))) (defn merge-style-annotations "Merges touching/overlapping annotations of the same type, except links." [annotations] (let [links (filterv #(#{:link :markdown-link} (:type %)) annotations) non-links (filterv #(not (#{:link :markdown-link} (:type %))) annotations)] (into (annotations/merge-annotations non-links) links)))