Initial import of LogDarioSeq baseline

Establishes version control for the personal Logseq DB fork before
starting the server-client architecture migration and domain features
described in docs/agent-guide and the approved implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 15:32:15 +02:00
commit 40631362b3
2016 changed files with 543898 additions and 0 deletions

View File

@@ -0,0 +1,906 @@
(ns logseq.graph-parser.block
"Given mldoc ast, prepares block data in preparation for db transaction.
Used by file and DB graphs"
(:require [clojure.set :as set]
[clojure.string :as string]
[clojure.walk :as walk]
[datascript.core :as d]
[logseq.common.config :as common-config]
[logseq.common.date :as common-date]
[logseq.common.util :as common-util]
[logseq.common.util.block-ref :as block-ref]
[logseq.common.util.date-time :as date-time-util]
[logseq.common.util.page-ref :as page-ref]
[logseq.common.uuid :as common-uuid]
[logseq.db :as ldb]
[logseq.db.common.entity-plus :as entity-plus]
[logseq.db.common.order :as db-order]
[logseq.db.frontend.class :as db-class]
[logseq.db.frontend.entity-util :as entity-util]
[logseq.graph-parser.mldoc :as gp-mldoc]
[logseq.graph-parser.property :as gp-property]
[logseq.graph-parser.text :as text]
[logseq.graph-parser.utf8 :as utf8]))
(defn heading-block?
[block]
(and
(vector? block)
(= "Heading" (first block))))
(defn get-tag
[block]
(when-let [tag-value (and (vector? block)
(= "Tag" (first block))
(second block))]
(->> tag-value
(map (fn [[elem value]]
(case elem
"Plain" value
"Link" (:full_text value)
"Nested_link" (:content value)
"")))
(string/join))))
(defn get-page-reference
[block format]
(let [page (cond
(and (vector? block) (= "Link" (first block)))
(let [url-type (first (:url (second block)))
value (second (:url (second block)))]
;; {:url ["File" "file:../pages/hello_world.org"], :label [["Plain" "hello world"]], :title nil}
(or
(and
(= url-type "Page_ref")
(and (string? value)
(not (common-config/local-relative-asset? value)))
value)
(and
(= url-type "Search")
(page-ref/page-ref? value)
(text/page-ref-un-brackets! value))
(and (= url-type "Search")
(= format :org)
(not (common-config/local-relative-asset? value))
value)
(and
(= url-type "File")
(second (first (:label (second block)))))))
(and (vector? block) (= "Nested_link" (first block)))
(let [content (:content (last block))]
(subs content 2 (- (count content) 2)))
(and (vector? block)
(= "Macro" (first block)))
(let [{:keys [name arguments]} (second block)
argument (string/join ", " arguments)]
(if (= name "embed")
(when (page-ref/page-ref? argument)
(text/page-ref-un-brackets! argument))
{:type "macro"
:name name
:arguments arguments}))
(and (vector? block)
(= "Tag" (first block)))
(let [text (get-tag block)]
(text/page-ref-un-brackets! text))
:else
nil)]
(when page (or (when (string? page)
(block-ref/get-block-ref-id page))
page))))
(defn get-block-reference
[block]
(when-let [block-id (cond
(and (vector? block)
(= "Block_reference" (first block)))
(last block)
(and (vector? block)
(= "Link" (first block))
(map? (second block))
(= "Block_ref" (first (:url (second block)))))
(second (:url (second block)))
(and (vector? block)
(= "Macro" (first block)))
(let [{:keys [name arguments]} (second block)]
(when (and (= name "embed")
(string? (first arguments))
(block-ref/string-block-ref? (first arguments)))
(block-ref/get-string-block-ref-id (first arguments))))
(and (vector? block)
(= "Link" (first block))
(map? (second block)))
(if (= "id" (:protocol (second (:url (second block)))))
(:link (second (:url (second block))))
(let [id (second (:url (second block)))]
;; these can be maps
(when (string? id)
(or (block-ref/get-block-ref-id id) id))))
:else
nil)]
(when (some-> block-id parse-uuid)
block-id)))
(defn paragraph-block?
[block]
(and
(vector? block)
(= "Paragraph" (first block))))
(defn timestamp-block?
[block]
(and
(vector? block)
(= "Timestamp" (first block))))
(defn- get-page-refs-from-property-names
[properties {:property-pages/keys [enabled? excludelist]}]
(if (contains? #{true nil} enabled?)
(sequence
(comp (map (comp name first))
(remove string/blank?)
(remove (set (map name excludelist)))
;; Remove built-in properties as we don't want pages
;; created for them by default
(remove (into #{}
(map name)
(apply conj
(apply disj
(gp-property/editable-built-in-properties)
gp-property/editable-linkable-built-in-properties)
(gp-property/hidden-built-in-properties))))
(distinct))
properties)
[]))
(defn- extract-refs-from-property-value
[value format]
(cond
(coll? value)
(filter (fn [v] (and (string? v) (not (string/blank? v)))) value)
(and (string? value) (= \" (first value) (last value)))
nil
(string? value)
(let [ast (gp-mldoc/inline->edn value (gp-mldoc/default-config format))]
(text/extract-refs-from-mldoc-ast ast))
:else
nil))
(defn- get-page-ref-names-from-properties
[properties user-config]
(let [page-refs (->>
properties
(remove (fn [[k _]]
(contains?
(set/union (apply disj
(gp-property/editable-built-in-properties)
gp-property/editable-linkable-built-in-properties)
(gp-property/hidden-built-in-properties))
(keyword k))))
;; get links ast
(map last)
(mapcat (fn [value]
(extract-refs-from-property-value value (get user-config :format :markdown))))
;; comma separated collections
(concat (->> (map second properties)
(filter coll?)
(apply concat))))
page-refs-from-property-names (get-page-refs-from-property-names properties user-config)]
(->> (concat page-refs page-refs-from-property-names)
(remove string/blank?)
distinct)))
(defn- extract-block-refs
[nodes]
(let [ref-blocks (atom nil)]
(walk/postwalk
(fn [form]
(when-let [block (get-block-reference form)]
(swap! ref-blocks conj block))
form)
nodes)
(keep (fn [block]
(when-let [id (parse-uuid block)]
[:block/uuid id]))
@ref-blocks)))
(defn extract-properties
[properties user-config]
(when (seq properties)
(let [properties (seq properties)
*invalid-properties (atom #{})
properties (->> properties
(map (fn [[k v mldoc-ast]]
(let [k (if (or (keyword? k) (symbol? k))
(subs (str k) 1)
k)
k (-> (string/lower-case k)
(string/replace "/" "-")
(string/replace " " "-")
(string/replace "_" "-"))]
(if (gp-property/valid-property-name? (str ":" k))
(let [k' (keyword
(if (contains? #{"custom_id" "custom-id"} k)
"id"
k))
v' (text/parse-property k v mldoc-ast user-config)]
[k' v' mldoc-ast v])
(do (swap! *invalid-properties conj k)
nil)))))
(remove #(nil? (second %))))
page-refs (get-page-ref-names-from-properties properties user-config)
block-refs (extract-block-refs properties)
properties-text-values (->> (map (fn [[k _v _refs original-text]] [k original-text]) properties)
(into {}))
properties (map (fn [[k v _]] [k v]) properties)
properties' (into {} properties)]
{:properties properties'
:properties-order (map first properties)
:properties-text-values properties-text-values
:invalid-properties @*invalid-properties
:page-refs page-refs
:block-refs block-refs})))
(defn- paragraph-timestamp-block?
[block]
(and (paragraph-block? block)
(or (timestamp-block? (first (second block)))
(timestamp-block? (second (second block))))))
(defn- extract-timestamps
[block]
(some->>
(second block)
(filter timestamp-block?)
(map last)
(into {})))
(defn- timestamp->scheduled-or-deadline-value
[{:keys [date repetition] timestamp-time :time}]
(let [{:keys [year month day]} date
day (js/parseInt (str year (common-util/zero-pad month) (common-util/zero-pad day)))]
(if (or timestamp-time repetition)
(cond-> {:date-int day}
timestamp-time
(assoc :time timestamp-time)
repetition
(assoc :repetition repetition))
day)))
;; {"Deadline" {:date {:year 2020, :month 10, :day 20}, :wday "Tue", :time {:hour 8, :min 0}, :repetition [["DoublePlus"] ["Day"] 1], :active true}}
(defn timestamps->scheduled-and-deadline
[timestamps]
(let [timestamps (update-keys timestamps (comp keyword string/lower-case))
m (some->> (select-keys timestamps [:scheduled :deadline])
(map (fn [[k v]]
(let [value (timestamp->scheduled-or-deadline-value v)]
(case k
:scheduled
{:scheduled value}
:deadline
{:deadline value})))))]
(apply merge m)))
(defn- convert-page-if-journal-impl
"Convert journal file name to user' custom date format"
[original-page-name date-formatter & {:keys [export-to-db-graph?]}]
(when original-page-name
(let [page-name (common-util/page-name-sanity-lc original-page-name)
day (when date-formatter
(date-time-util/journal-title->int
page-name
;; When exporting, only use the configured date-formatter. Allowing for other date formatters allows
;; for page names to change which breaks looking up journal refs for unconfigured journal pages
(if export-to-db-graph? [date-formatter] (date-time-util/safe-journal-title-formatters date-formatter))))]
(if day
(let [original-page-name' (date-time-util/int->journal-title day date-formatter)
default-journal-page-name (date-time-util/int->journal-title day date-time-util/default-journal-title-formatter)]
[original-page-name' (common-util/page-name-sanity-lc default-journal-page-name) day])
[original-page-name page-name day]))))
(def convert-page-if-journal (memoize convert-page-if-journal-impl))
;; Hack to detect export as some fns are too deeply nested to be refactored to get explicit option
(def *export-to-db-graph? (atom false))
(defn- get-page
"Similar to get-page but only for file graphs"
[db page-name]
(when (and db (string? page-name))
(d/entity db
(first (sort (map :e (entity-util/get-pages-by-name db page-name)))))))
(defn- page-name-string->map
[original-page-name db date-formatter
{:keys [with-timestamp? page-uuid from-page class? skip-existing-page-check? skip-journal?]}]
(let [db-based? (entity-plus/db-based-graph? db)
original-page-name (common-util/remove-boundary-slashes original-page-name)
[original-page-name' page-name journal-day]
(if skip-journal?
[original-page-name (common-util/page-name-sanity-lc original-page-name) nil]
(convert-page-if-journal original-page-name date-formatter {:export-to-db-graph? @*export-to-db-graph?}))
namespace? (and (or (not db-based?) @*export-to-db-graph?)
(not journal-day)
(not (boolean (text/get-nested-page-name original-page-name')))
(text/namespace-page? original-page-name'))
page-entity (when (and db (not skip-existing-page-check?))
(cond
journal-day
(ldb/get-journal-page-by-day db journal-day)
(and class? db-based?)
(some->> (ldb/page-exists? db original-page-name' #{:logseq.class/Tag})
first
(d/entity db))
:else
(get-page db original-page-name')))
original-page-name' (or from-page (:block/title page-entity) original-page-name')
page-name' (if (and journal-day page-entity)
(:block/name page-entity)
page-name)
page (merge
{:block/name page-name'
:block/title original-page-name'}
(when (and original-page-name
(not= (string/lower-case original-page-name)
(string/lower-case original-page-name'))
(not @*export-to-db-graph?))
{:block.temp/original-page-name original-page-name})
(if (and class? page-entity (:db/ident page-entity))
{:block/uuid (:block/uuid page-entity)
:db/ident (:db/ident page-entity)}
(let [new-uuid* (if (uuid? page-uuid)
page-uuid
(if journal-day
(common-uuid/gen-uuid :journal-page-uuid journal-day)
(common-uuid/gen-uuid)))
new-uuid (if skip-existing-page-check?
new-uuid*
(or
(cond page-entity (:block/uuid page-entity)
(uuid? page-uuid) page-uuid)
new-uuid*))]
{:block/uuid new-uuid}))
(when namespace?
(let [namespace' (first (common-util/split-last "/" original-page-name))]
(when-not (string/blank? namespace')
{:block/namespace {:block/name (string/trim (common-util/page-name-sanity-lc namespace'))}})))
(when (and with-timestamp? (or skip-existing-page-check? (not page-entity))) ;; Only assign timestamp on creating new entity
(let [current-ms (common-util/time-ms)]
{:block/created-at current-ms
:block/updated-at current-ms}))
(if journal-day
(cond-> {:block/journal-day journal-day}
db-based?
(assoc :block/tags [:logseq.class/Journal])
(not db-based?)
(assoc :block/type "journal"))
{}))]
[page page-entity]))
(defn sanitize-hashtag-name
"This must be kept in sync with its reverse operation in logseq.db.frontend.content"
[s]
(string/replace s "#" "HashTag-"))
(defn- page-entity?
"Support DB or file graphs because of exporter"
[entity]
(or (entity-util/page? entity)
(contains? #{"page" "journal"} (:block/type entity))))
;; TODO: refactor
(defn page-name->map
"Create a page's map structure given a original page name (string).
map as input is supported for legacy compatibility.
`with-timestamp?`: assign timestampes to the map structure.
Useful when creating new pages from references or namespaces,
as there's no chance to introduce timestamps via editing in page
`skip-existing-page-check?`: if true, allows pages to have the same name"
[original-page-name db with-timestamp? date-formatter
& {:keys [page-uuid class?] :as options}]
(when-not (and db (common-util/uuid-string? original-page-name)
(not (page-entity? (d/entity db [:block/uuid (uuid original-page-name)]))))
(let [db-based? (entity-plus/db-based-graph? db)
original-page-name (cond-> (string/trim original-page-name)
db-based?
sanitize-hashtag-name)
[page _page-entity] (cond
(and original-page-name (string? original-page-name))
(page-name-string->map original-page-name db date-formatter
(assoc options :with-timestamp? with-timestamp?))
:else
(let [page (cond (and (map? original-page-name) (:block/uuid original-page-name))
original-page-name
(map? original-page-name)
(assoc original-page-name :block/uuid (or page-uuid (d/squuid)))
:else
nil)]
[page nil]))]
(when page
(if db-based?
(let [tags (if class? [:logseq.class/Tag]
(or (:block/tags page)
[:logseq.class/Page]))]
(assoc page :block/tags tags))
(assoc page :block/type (or (:block/type page) "page")))))))
(defn- db-namespace-page?
"Namespace page that're not journal pages"
[db-based? page]
(and db-based?
(text/namespace-page? page)
(not (common-date/valid-journal-title-with-slash? page))))
(defn- ref->map
[db *col {:keys [date-formatter *name->id tag? db-based? structured-tags]}]
(let [col (distinct (remove string/blank? @*col))
children-pages (->> (mapcat (fn [p]
(let [p (if (map? p)
(:block/title p)
p)]
(when (string? p)
(let [p (or (text/get-nested-page-name p) p)]
(if (and (text/namespace-page? p)
(not (common-date/valid-journal-title-with-slash? p))
(not tag?))
(common-util/split-namespace-pages p)
[p])))))
col)
(remove string/blank?)
(distinct))
col (->> (distinct (concat col children-pages))
(remove nil?))
export-to-db-graph? @*export-to-db-graph?]
(map
(fn [item]
(let [macro? (and (map? item)
(= "macro" (:type item)))
tag? (if export-to-db-graph?
tag?
(or (contains? structured-tags item) tag?))]
(when-not macro?
(let [m (page-name->map item db true date-formatter {:class? tag?})
result (cond->> m
(and db-based? tag? (not (:db/ident m)))
(db-class/build-new-class db))
page-name (if db-based? (:block/title result) (:block/name result))
id (get @*name->id page-name)]
(when (nil? id)
(swap! *name->id assoc page-name (:block/uuid result)))
;; Changing a :block/uuid should be done cautiously here as it can break
;; the identity of built-in concepts in db graphs
(if (and id
(or (when-let [ident (:db/ident result)]
(nil? (d/entity db ident)))
export-to-db-graph?))
(assoc result :block/uuid id)
result))))) col)))
(defn- with-page-refs-and-tags
[{:keys [title body tags refs marker priority] :as block} db date-formatter {:keys [structured-tags]
:or {structured-tags #{}}}]
(let [db-based? (and (entity-plus/db-based-graph? db) (not @*export-to-db-graph?))
refs (->> (concat tags refs (when-not db-based? [marker priority]))
(remove string/blank?)
(distinct))
*refs (atom refs)
*structured-tags (atom (set structured-tags))]
(walk/prewalk
(fn [form]
;; skip custom queries
(when-not (and (vector? form)
(= (first form) "Custom")
(= (second form) "query"))
(when-let [page (get-page-reference form (get block :format :markdown))]
(when-let [page' (when-not (db-namespace-page? db-based? page)
page)]
(swap! *refs conj page')))
(when-let [tag (get-tag form)]
(let [tag (text/page-ref-un-brackets! tag)]
(when-let [tag' (when-not (db-namespace-page? db-based? tag)
tag)]
(when (common-util/tag-valid? tag')
(swap! *refs conj tag')
(swap! *structured-tags conj tag')))))
form))
(concat title body))
(swap! *refs #(remove string/blank? %))
(let [*name->id (atom {})
ref->map-options {:db-based? db-based?
:date-formatter date-formatter
:*name->id *name->id
:structured-tags (set @*structured-tags)}
refs (->> (ref->map db *refs ref->map-options)
(remove nil?)
(map (fn [ref]
(cond-> ref
(:block.temp/original-page-name ref)
(assoc :block.temp/original-page-name (:block.temp/original-page-name ref))))))
tags (ref->map db *structured-tags (assoc ref->map-options :tag? true))]
(assoc block
:refs refs
:tags tags))))
(defn- with-block-refs
[{:keys [title body] :as block}]
(let [ref-blocks (extract-block-refs (concat title body))
refs (distinct (concat (:refs block) ref-blocks))]
(assoc block :refs refs)))
(defn block-keywordize
[block]
(update-keys
block
(fn [k]
(if (namespace k)
k
(keyword "block" k)))))
(defn- sanity-blocks-data
"Clean up blocks data and add `block` ns to all keys"
[blocks]
(map (fn [block]
(if (map? block)
(block-keywordize (common-util/remove-nils-non-nested block))
block))
blocks))
(defn get-block-content
[utf8-content block format meta' block-pattern]
(let [content (if-let [end-pos (:end_pos meta')]
(utf8/substring utf8-content
(:start_pos meta')
end-pos)
(utf8/substring utf8-content
(:start_pos meta')))
content (when content
(let [content (text/remove-level-spaces content format block-pattern)]
(if (or (:pre-block? block)
(= (get block :format :markdown) :org))
content
(gp-mldoc/remove-indentation-spaces content (inc (:level block)) false))))]
(if (= format :org)
content
(gp-property/->new-properties content))))
(defn get-custom-id-or-new-id
[properties]
(or (when-let [custom-id (or (get-in properties [:properties :custom-id])
(get-in properties [:properties :custom_id])
(get-in properties [:properties :id]))]
;; guard against non-string custom-ids
(when-let [custom-id (and (string? custom-id) (string/trim custom-id))]
(some-> custom-id parse-uuid)))
(d/squuid)))
(defn get-page-refs-from-properties
[properties db date-formatter user-config]
(let [page-refs (get-page-ref-names-from-properties properties user-config)]
(map (fn [page] (page-name->map page db true date-formatter)) page-refs)))
(defn- with-page-block-refs
[block db date-formatter opts]
(some-> block
(with-page-refs-and-tags db date-formatter opts)
with-block-refs
(update :refs (fn [col] (remove nil? col)))))
(defn- macro->block
"macro: {:name \"\" arguments [\"\"]}"
[macro]
{:block/uuid (common-uuid/gen-uuid)
:block/type "macro"
:block/properties {:logseq.macro-name (:name macro)
:logseq.macro-arguments (:arguments macro)}})
(defn extract-macros-from-ast
[ast]
(let [*result (atom #{})]
(walk/postwalk
(fn [f]
(if (and (vector? f) (= (first f) "Macro"))
(do
(swap! *result conj (second f))
nil)
f))
ast)
(mapv macro->block @*result)))
(defn with-pre-block-if-exists
[blocks body pre-block-properties encoded-content {:keys [db date-formatter user-config]}]
(let [first-block (first blocks)
first-block-start-pos (get-in first-block [:block/meta :start_pos])
;; Add pre-block
blocks (if (or (> first-block-start-pos 0)
(empty? blocks))
(cons
(merge
(let [content (utf8/substring encoded-content 0 first-block-start-pos)
{:keys [properties properties-order properties-text-values invalid-properties]} pre-block-properties
id (get-custom-id-or-new-id {:properties properties})
property-refs (->> (get-page-refs-from-properties
properties db date-formatter
user-config)
(map :block/title))
pre-block? (if (:heading properties) false true)
block {:block/uuid id
:block/title content
:block/level 1
:block/properties properties
:block/properties-order (vec properties-order)
:block/properties-text-values properties-text-values
:block/invalid-properties invalid-properties
:block/pre-block? pre-block?
:block/macros (extract-macros-from-ast body)
:block.temp/ast-body body}
{:keys [tags refs]}
(with-page-block-refs {:body body :refs property-refs} db date-formatter {})]
(cond-> block
tags
(assoc :block/tags tags)
true
(assoc :block/refs (concat refs (:block-refs pre-block-properties)))))
(select-keys first-block [:block/format :block/page]))
blocks)
blocks)]
blocks))
(defn- with-heading-property
[properties markdown-heading? size]
(if markdown-heading?
(assoc properties :heading size)
properties))
(defn- construct-block
[block properties* timestamps body encoded-content format pos-meta {:keys [block-pattern db date-formatter remove-properties? remove-logbook? remove-deadline-scheduled? db-graph-mode? export-to-db-graph?]}]
(let [id (get-custom-id-or-new-id properties*)
block-tags (and export-to-db-graph? (get-in properties* [:properties :tags]))
;; For export, remove tags from properties as they are being converted to classes
properties (if (seq block-tags)
(-> properties*
(update :properties #(dissoc % :tags))
(update :properties-text-values #(dissoc % :tags))
(update :properties-order (fn [v] (remove #(= :tags %) v)))
(update :page-refs (fn [v] (remove #(= "tags" %) v))))
properties*)
ref-pages-in-properties (->> (:page-refs properties)
(remove string/blank?))
block (second block)
unordered? (:unordered block)
markdown-heading? (and (:size block) (= :markdown format))
block (if markdown-heading?
(assoc block
:level (if unordered? (:level block) 1))
block)
block (cond->
(-> (assoc block
:uuid id
:refs ref-pages-in-properties
:format format
:meta pos-meta)
(dissoc :size :unordered))
(or (seq (:properties properties)) markdown-heading?)
(assoc :properties (with-heading-property (:properties properties) markdown-heading? (:size block))
:properties-text-values (:properties-text-values properties)
:properties-order (vec (:properties-order properties)))
(seq (:invalid-properties properties))
(assoc :invalid-properties (:invalid-properties properties)))
block (if (get-in block [:properties :collapsed])
(-> (assoc block :collapsed? true)
(update :properties (fn [m] (dissoc m :collapsed)))
(update :properties-text-values dissoc :collapsed)
(update :properties-order (fn [keys'] (vec (remove #{:collapsed} keys')))))
block)
title (cond->> (get-block-content encoded-content block format pos-meta block-pattern)
remove-properties?
(gp-property/remove-properties (get block :format :markdown))
remove-logbook?
(gp-property/remove-logbook)
remove-deadline-scheduled?
(gp-property/remove-deadline-scheduled))
block (assoc block :block/title title)
block (if (seq timestamps)
(merge block (timestamps->scheduled-and-deadline timestamps))
block)
db-based? (or db-graph-mode? export-to-db-graph?)
block (-> block
(assoc :body body)
(with-page-block-refs db date-formatter
(cond-> {} (seq block-tags) (assoc :structured-tags block-tags))))
block (if db-based? block
(-> block
(update :tags (fn [tags] (map #(assoc % :block/format format) tags)))
(update :refs (fn [refs] (map #(if (map? %) (assoc % :block/format format) %) refs)))))
block (update block :refs concat (:block-refs properties))
{:keys [created-at updated-at]} (:properties properties)
block (cond-> block
(and created-at (integer? created-at))
(assoc :block/created-at created-at)
(and updated-at (integer? updated-at))
(assoc :block/updated-at updated-at))]
(dissoc block :title :body :anchor)))
(defn fix-duplicate-id
[block]
(println "Logseq will assign a new id for block with content:" (pr-str (:block/title block)))
(-> block
(assoc :block/uuid (d/squuid))
(update :block/properties dissoc :id)
(update :block/properties-text-values dissoc :id)
(update :block/properties-order #(vec (remove #{:id} %)))
(update :block/title (fn [c]
(let [replace-str (re-pattern
(str
"\n*\\s*"
(if (= :markdown (get block :block/format :markdown))
(str "id" gp-property/colons " " (:block/uuid block))
(str (gp-property/colons-org "id") " " (:block/uuid block)))))]
(string/replace-first c replace-str ""))))))
(defn fix-block-id-if-duplicated!
"If the block exists in another page or the current page, we need to fix it"
[db page-name *extracted-block-ids block]
(let [block-page-name (:block/name (:block/page (d/entity db [:block/uuid (:block/uuid block)])))
block (if (or (and block-page-name (not= block-page-name page-name))
(contains? @*extracted-block-ids (:block/uuid block)))
(fix-duplicate-id block)
block)]
(swap! *extracted-block-ids conj (:block/uuid block))
block))
(defn extract-blocks
"Extract headings from mldoc ast. Args:
* `ast`: mldoc ast.
* `content`: markdown or org-mode text.
* `format`: content's format, it could be either :markdown or :org-mode.
* `options`: Options are :user-config, :block-pattern, :date-formatter, :db and
* :db-graph-mode? : Set when a db graph in the frontend
* :export-to-db-graph? : Set when exporting to a db graph"
[ast content format {:keys [user-config db-graph-mode? export-to-db-graph?] :as options}]
{:pre [(seq ast) (string? content) (contains? #{:markdown :org} format)]}
(let [encoded-content (utf8/encode content)
all-blocks (vec (reverse ast))
[blocks body pre-block-properties]
(loop [headings []
ast-blocks (reverse ast)
block-idx 0
timestamps {}
properties {}
body []
prev-block-num 0]
(if (seq ast-blocks)
(let [[ast-block pos-meta] (first ast-blocks)]
(cond
(paragraph-timestamp-block? ast-block)
(let [ts (extract-timestamps ast-block)
timestamps' (merge timestamps ts)]
(recur headings (rest ast-blocks) (inc block-idx) timestamps' properties body (inc prev-block-num)))
(gp-property/properties-ast? ast-block)
(let [properties (extract-properties (second ast-block) (assoc user-config :format format))]
(recur headings (rest ast-blocks) (inc block-idx) timestamps properties body (inc prev-block-num)))
(heading-block? ast-block)
(let [cut-multiline? (and export-to-db-graph? (= prev-block-num 0))
prev-blocks (map first (subvec all-blocks (max 0 (- block-idx prev-block-num)) block-idx))
pos-meta' (if cut-multiline?
pos-meta
;; fix start_pos
(assoc pos-meta :end_pos
(if (seq headings)
(get-in (last headings) [:meta :start_pos])
nil)))
;; Remove properties, deadline/scheduled and logbook text from title in db graphs
options' (assoc options
:remove-properties?
(and export-to-db-graph? (some gp-property/properties-ast? prev-blocks))
:remove-logbook?
(and export-to-db-graph? (some #(= ["Drawer" "logbook"] (take 2 %)) prev-blocks))
:remove-deadline-scheduled?
(and export-to-db-graph? (some #(seq (set/intersection (set (flatten %)) #{"Deadline" "Scheduled"})) prev-blocks)))
block' (construct-block ast-block properties timestamps body encoded-content format pos-meta' options')
block'' (cond
db-graph-mode?
block'
export-to-db-graph?
(assoc block' :block.temp/ast-blocks (cons ast-block body))
:else
(assoc block' :macros (extract-macros-from-ast (cons ast-block body))))]
(recur (conj headings block'') (rest ast-blocks) (inc block-idx) {} {} [] 0))
:else
(recur headings (rest ast-blocks) (inc block-idx) timestamps properties (conj body ast-block) (inc prev-block-num))))
[(-> (reverse headings)
sanity-blocks-data)
body
properties]))
result (with-pre-block-if-exists blocks body pre-block-properties encoded-content options)]
(map #(dissoc % :block/meta) result)))
(defn with-parent-and-order
[page-id blocks]
(let [[blocks other-blocks] (split-with
(fn [b]
(not= "macro" (:block/type b)))
blocks)
result (loop [blocks (map (fn [block] (assoc block :block/level-spaces (:block/level block))) blocks)
parents [{:page/id page-id ; db id or a map {:block/name "xxx"}
:block/level 0
:block/level-spaces 0}]
result []]
(if (empty? blocks)
(map #(dissoc % :block/level-spaces) result)
(let [[block & others] blocks
level-spaces (:block/level-spaces block)
{uuid' :block/uuid :block/keys [level parent] :as last-parent} (last parents)
parent-spaces (:block/level-spaces last-parent)
[blocks parents result]
(cond
(= level-spaces parent-spaces) ; sibling
(let [block (assoc block
:block/parent parent
:block/level level)
parents' (conj (vec (butlast parents)) block)
result' (conj result block)]
[others parents' result'])
(> level-spaces parent-spaces) ; child
(let [parent (if uuid' [:block/uuid uuid'] (:page/id last-parent))
block (cond->
(assoc block
:block/parent parent)
;; fix block levels with wrong order
;; For example:
;; - a
;; - b
;; What if the input indentation is two spaces instead of 4 spaces
(>= (- level-spaces parent-spaces) 1)
(assoc :block/level (inc level)))
parents' (conj parents block)
result' (conj result block)]
[others parents' result'])
(< level-spaces parent-spaces)
(cond
(some #(= (:block/level-spaces %) (:block/level-spaces block)) parents) ; outdent
(let [parents' (vec (filter (fn [p] (<= (:block/level-spaces p) level-spaces)) parents))
blocks (cons (assoc (first blocks)
:block/level (dec level))
(rest blocks))]
[blocks parents' result])
:else
(let [[f r] (split-with (fn [p] (<= (:block/level-spaces p) level-spaces)) parents)
left (first r)
parent-id (if-let [block-id (:block/uuid (last f))]
[:block/uuid block-id]
page-id)
block (assoc block
:block/parent parent-id
:block/level (:block/level left)
:block/level-spaces (:block/level-spaces left))
parents' (->> (concat f [block]) vec)
result' (conj result block)]
[others parents' result'])))]
(recur blocks parents result))))
result' (map (fn [block] (assoc block :block/order (db-order/gen-key))) result)]
(concat result' other-blocks)))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,327 @@
(ns logseq.graph-parser.extract
"For file graphs, handles extraction of blocks, pages and mldoc ast in
preparation for db transaction"
;; Disable clj linters since we don't support clj
#?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
:unresolved-symbol {:level :off}}}})
(:require #?(:org.babashka/nbb [logseq.common.log :as log]
:default [lambdaisland.glogi :as log])
[clojure.set :as set]
[clojure.string :as string]
[clojure.walk :as walk]
[datascript.core :as d]
[logseq.common.util :as common-util]
[logseq.common.uuid :as common-uuid]
[logseq.db :as ldb]
[logseq.graph-parser.block :as gp-block]
[logseq.graph-parser.mldoc :as gp-mldoc]
[logseq.graph-parser.property :as gp-property]
[logseq.graph-parser.text :as text]))
(defn- mldoc-support?
[format']
(contains? #{:org :markdown :md} (keyword format')))
(defn- path->file-name
;; Only for internal paths, as they are converted to POXIS already
;; https://github.com/logseq/logseq/blob/48b8e54e0fdd8fbd2c5d25b7f1912efef8814714/deps/graph-parser/src/logseq/graph_parser/extract.cljc#L32
;; Should be converted to POXIS first for external paths
[path]
(if (string/includes? path "/")
(last (common-util/split-last "/" path))
path))
(defn- path->file-body
[path]
(when-let [file-name (path->file-name path)]
(if (string/includes? file-name ".")
(first (common-util/split-last "." file-name))
file-name)))
(defn- safe-url-decode
[string]
(if (string/includes? string "%")
(some-> string str common-util/safe-decode-uri-component)
string))
(defn- decode-namespace-underlines
"Decode namespace underlines to slashed;
If continuous underlines, only decode at start;
Having empty namespace is invalid."
[string]
(string/replace string "___" "/"))
(defn- make-valid-namespaces
"Remove those empty namespaces from title to make it a valid page name."
[title]
(->> (string/split title "/")
(remove empty?)
(string/join "/")))
(defn- tri-lb-title-parsing
"Parsing file name under the new file name format
Avoid calling directly"
[file-name]
(some-> file-name
(decode-namespace-underlines)
(string/replace common-util/url-encoded-pattern safe-url-decode)
(make-valid-namespaces)))
;; Keep for backward compatibility
;; Rule of dir-ver 0
;; Source: https://github.com/logseq/logseq/blob/e7110eea6790eda5861fdedb6b02c2a78b504cd9/deps/graph-parser/src/logseq/graph_parser/extract.cljc#L35
(defn- legacy-title-parsing
[file-name-body]
(let [title (string/replace file-name-body "." "/")]
(or (common-util/safe-decode-uri-component title) title)))
(defn title-parsing
"Convert file name in the given file name format to page title"
[file-name-body filename-format]
(case filename-format
:triple-lowbar (tri-lb-title-parsing file-name-body)
(legacy-title-parsing file-name-body)))
(defn- get-page-name
"Get page name with overridden order of
`title::` property
file name parsing
first block content
note: `page-name-order` is deprecated on Apr. 2021
uri-encoded? - since paths on mobile are uri-encoded, need to decode them first
filename-format - the format used to parse file name
"
[file-path ast uri-encoded? filename-format]
;; headline
(let [ast (map first ast)
file (if uri-encoded? (js/decodeURI file-path) file-path)]
;; check backward compatibility?
;; FIXME: use pre-config dir
(if (string/starts-with? file "pages/contents.")
"Contents"
(let [first-block (last (first (filter gp-block/heading-block? ast)))
property-name (some (fn [ast-node]
(when (contains? #{"Properties" "Property_Drawer"} (first ast-node))
(let [properties-ast (second ast-node)
properties (zipmap (map (comp keyword string/lower-case first) properties-ast)
(map second properties-ast))]
(:title properties))))
ast)
first-block-name (let [title (last (first (:title first-block)))]
(and first-block
(string? title)
title))
file-name (when-let [result (path->file-body file)]
(if (mldoc-support? (common-util/get-file-ext file))
(title-parsing result filename-format)
result))]
(or property-name
file-name
first-block-name)))))
(defn- extract-page-alias-and-tags
[page-m page-name properties]
(let [alias (:alias properties)
alias' (if (coll? alias) alias [(str alias)])
aliases (and alias'
(seq (remove #(or (= page-name (common-util/page-name-sanity-lc %))
(string/blank? %)) ;; disable blank alias
alias')))
aliases' (keep
(fn [alias]
(let [page-name (common-util/page-name-sanity-lc alias)]
{:block/name page-name
:block/title alias}))
aliases)
result (cond-> page-m
(seq aliases')
(assoc :block/alias aliases')
(:tags properties)
(assoc :block/tags (concat
(:block/tags page-m)
(let [tags (:tags properties)
tags (if (coll? tags) tags [(str tags)])
tags (remove string/blank? tags)]
(map (fn [tag] {:block/name (common-util/page-name-sanity-lc tag)
:block/title tag})
tags)))))]
(update result :block/properties #(apply dissoc % gp-property/editable-linkable-built-in-properties))))
(defn- build-page-map
[properties invalid-properties properties-text-values file page page-name
{:keys [date-formatter db from-page skip-journal?]}]
(let [[*valid-properties *invalid-properties]
((juxt filter remove)
(fn [[k _v]] (gp-property/valid-property-name? (str k))) properties)
valid-properties (into {} *valid-properties)
invalid-properties (set (->> (map (comp name first) *invalid-properties)
(concat invalid-properties)))
page-m (->
(common-util/remove-nils-non-nested
(assoc
(gp-block/page-name->map page db true date-formatter
:from-page from-page
:skip-journal? skip-journal?)
:block/file {:file/path (common-util/path-normalize file)}))
(extract-page-alias-and-tags page-name properties))]
(cond->
page-m
(seq valid-properties)
(assoc :block/properties valid-properties
:block/properties-text-values (select-keys properties-text-values (keys valid-properties)))
(seq invalid-properties)
(assoc :block/invalid-properties invalid-properties))))
(defn- attach-block-ids-if-match
"If block-ids are provided and match the number of blocks, attach them to blocks
If block-ids are provided but don't match the number of blocks, WARN and ignore
If block-ids are not provided (nil), just ignore"
[block-ids blocks]
(or (when block-ids
(if (= (count block-ids) (count blocks))
(mapv (fn [block-id block]
(if (some? block-id)
(assoc block :block/uuid (uuid block-id))
block))
block-ids blocks)
(log/error :gp-extract/attach-block-ids-not-match "attach-block-ids-if-match: block-ids provided, but doesn't match the number of blocks, ignoring")))
blocks))
(defn- build-pages-aux
[db page-map ref-pages date-formatter format]
(let [namespace-pages (let [page (:block/title page-map)]
(when (and (not (:block/journal-day page-map))
(text/namespace-page? page))
(->> (common-util/split-namespace-pages page)
(map (fn [page]
(-> (gp-block/page-name->map page db true date-formatter)
(assoc :block/format format)))))))
pages (->> (concat
[page-map]
@ref-pages
namespace-pages)
;; remove block references
(remove vector?)
(remove nil?)
(filter :block/name))
pages (common-util/distinct-by :block/name pages)
pages (remove nil? pages)]
(map (fn [page]
(let [page-id (if-let [journal-day (:block/journal-day page)]
(common-uuid/gen-uuid :journal-page-uuid journal-day)
(or (when db
(:block/uuid (ldb/get-page db (:block/name page))))
(d/squuid)))]
(assoc page :block/uuid page-id)))
pages)))
;; TODO: performance improvement
(defn- extract-pages-and-blocks
"uri-encoded? - if is true, apply URL decode on the file path
options -
:resolve-uuid-fn - Optional fn which is called to resolve uuids of each block. Enables diff-merge
(2 ways diff) based uuid resolution upon external editing.
returns a list of the uuids, given the receiving ast, or nil if not able to resolve.
Implemented in reset-file-handler/diff-merge-uuids-2ways for IoC
Called in gp-extract/extract as AST is being parsed and properties are extracted there"
[format ast properties file content {:keys [date-formatter db filename-format resolve-uuid-fn skip-journal?]
:or {resolve-uuid-fn (constantly nil)}
:as options}]
(assert db "Datascript DB is required")
(try
(let [page (get-page-name file ast false filename-format)
[page page-name _journal-day]
(if skip-journal?
[page (common-util/page-name-sanity-lc page) nil]
(gp-block/convert-page-if-journal page date-formatter))
options' (assoc options :page-name page-name)
;; In case of diff-merge (2way) triggered, use the uuids to override the ones extracted from the AST
override-uuids (resolve-uuid-fn format ast content options')
*extracted-block-ids (atom #{})
blocks (->> (gp-block/extract-blocks ast content format options')
(attach-block-ids-if-match override-uuids)
(mapv #(gp-block/fix-block-id-if-duplicated! db page-name *extracted-block-ids %))
;; FIXME: use page uuid
(gp-block/with-parent-and-order {:block/name page-name})
(vec))
ref-pages (atom #{})
blocks (map (fn [block]
(if (= (:block/type block) "macro")
block
(let [block-ref-pages (seq (:block/refs block))]
(when block-ref-pages
(swap! ref-pages set/union (set block-ref-pages)))
(-> block
(dissoc :ref-pages)
(assoc :block/page [:block/name page-name]
:block/refs block-ref-pages
:block/format format)))))
blocks)
[properties invalid-properties properties-text-values]
(if (:block/pre-block? (first blocks))
[(:block/properties (first blocks))
(:block/invalid-properties (first blocks))
(:block/properties-text-values (first blocks))]
[properties [] {}])
page-map (build-page-map properties invalid-properties properties-text-values file page page-name (assoc options' :from-page page))
pages (build-pages-aux db page-map ref-pages date-formatter format)
blocks (->> (remove nil? blocks)
(map (fn [b] (dissoc b :block.temp/ast-title :block.temp/ast-body :block/level :block/children :block/meta))))]
[pages blocks])
(catch :default e
(log/error :exception e))))
(defn extract
"Extracts pages, blocks and ast from given file"
[file-path content {:keys [user-config verbose] :or {verbose true} :as options}]
(if (string/blank? content)
{}
(let [format (common-util/get-format file-path)
_ (when verbose (println "Parsing start: " file-path))
ast (gp-mldoc/->edn content (gp-mldoc/default-config format
;; {:parse_outline_only? true}
))]
(when verbose (println "Parsing finished: " file-path))
(let [first-block (ffirst ast)
properties (let [properties (and (gp-property/properties-ast? first-block)
(->> (last first-block)
(map (fn [[x y mldoc-ast]]
(let [k (if (keyword? x)
(subs (str x) 1)
x)]
[(string/lower-case k) (text/parse-property k y mldoc-ast (assoc user-config :format format))])))
(into {})
(walk/keywordize-keys)))]
(when (and properties (seq properties))
(if (:filters properties)
(update properties :filters
(fn [v]
(string/replace (or v "") "\\" "")))
properties)))
[pages blocks] (extract-pages-and-blocks format ast properties file-path content options)]
{:pages pages
:blocks blocks
:ast ast}))))
(defn- with-block-uuid
[pages]
(->> (common-util/distinct-by :block/name pages)
(map (fn [page]
(if-let [journal-day (:block/journal-day page)]
(assoc page :block/uuid (common-uuid/gen-uuid :journal-page-uuid journal-day))
(cond-> page
(nil? (:block/uuid page))
(assoc :block/uuid (d/squuid))))))))
(defn with-ref-pages
[pages blocks]
(let [ref-pages (->> (mapcat :block/refs blocks)
(filter :block/name))]
(->> (concat pages ref-pages)
(group-by :block/name)
vals
(map (partial apply merge))
(with-block-uuid))))

View File

@@ -0,0 +1,417 @@
(ns logseq.graph-parser.mldoc
"Wraps https://github.com/logseq/mldoc to parse files into mldoc ast. This ns
encapsulates mldoc's json api by only taking and returning edn"
;; Disable clj linters since we don't support clj
#?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
:unresolved-symbol {:level :off}}}})
(:require #?(:org.babashka/nbb ["mldoc$default" :refer [Mldoc]]
:default ["mldoc" :refer [Mldoc]])
#?(:org.babashka/nbb [logseq.common.log :as log]
:default [lambdaisland.glogi :as log])
[cljs-bean.core :as bean]
[clojure.string :as string]
[clojure.walk :as walk]
[goog.object :as gobj]
[logseq.common.config :as common-config]
[logseq.common.util :as common-util]
[logseq.db.sqlite.util :as sqlite-util]
[logseq.graph-parser.utf8 :as utf8]))
(defonce parseJson (gobj/get Mldoc "parseJson"))
(defonce parseInlineJson (gobj/get Mldoc "parseInlineJson"))
(defonce astExportMarkdown (gobj/get Mldoc "astExportMarkdown"))
(defonce getReferences (gobj/get Mldoc "getReferences"))
(def default-references
(js/JSON.stringify
(clj->js {:embed_blocks []
:embed_pages []})))
(defn- convert-export-md-remove-options [opts]
(->> opts
(mapv (fn [opt]
(case opt
:page-ref ["Page_ref"]
:emphasis ["Emphasis"]
[])))
(remove empty?)))
(defn parse-json
[content config]
(parseJson content config))
(defn inline-parse-json
[text config]
(parseInlineJson text config))
(defn get-references
[text config]
(when-not (string/blank? text)
(common-util/json->clj (getReferences text config))))
(defn ast-export-markdown
[ast config references]
(astExportMarkdown ast
config
(or references default-references)))
(defn default-config-map
([format]
(default-config-map format {:export-heading-to-list? false}))
([format {:keys [export-heading-to-list? export-keep-properties? export-md-indent-style export-md-remove-options parse_outline_only?]}]
(let [format (string/capitalize (name (or format :markdown)))]
(->> {:toc false
:parse_outline_only (or parse_outline_only? false)
:heading_number false
:keep_line_break true
:format format
:heading_to_list (or export-heading-to-list? false)
:exporting_keep_properties export-keep-properties?
:export_md_indent_style export-md-indent-style
:export_md_remove_options
(convert-export-md-remove-options export-md-remove-options)}
(filter #(not (nil? (second %))))
(into {})))))
(defn default-config
([format]
(default-config format {:export-heading-to-list? false}))
([format opts]
(->> (default-config-map format opts)
bean/->js
js/JSON.stringify)))
(defn remove-indentation-spaces
"Remove the indentation spaces from the content. Only for markdown.
level - ast level + 1 (2 for the first level, 3 for the second level, etc., as the non-first line of multi-line block has 2 more space
Ex.
- level 1 multiline block first line
level 1 multiline block second line
\t- level 2 multiline block first line
\t level 2 multiline block second line
remove-first-line? - apply the indentation removal to the first line or not"
[s level remove-first-line?]
(let [lines (string/split-lines s)
[f & r] lines
body (map (fn [line]
;; Check if the indentation area only contains white spaces
;; Level = ast level + 1, 1-based indentation level
;; For markdown in Logseq, the indentation area for the non-first line of multi-line block is (ast level - 1) * "\t" + 2 * "(space)"
(if (string/blank? (common-util/safe-subs line 0 level))
;; If valid, then remove the indentation area spaces. Keep the rest of the line (might contain leading spaces)
(common-util/safe-subs line level)
;; Otherwise, trim these invalid spaces
(string/triml line)))
(if remove-first-line? lines r))
content (if remove-first-line? body (cons f body))]
(string/join "\n" content)))
(defn- update-src-full-content
[ast content]
(let [content (utf8/encode content)]
(map (fn [[block pos-meta]]
(if (and (vector? block)
(= "Src" (first block)))
(let [{:keys [start_pos end_pos]} pos-meta
content (utf8/substring content start_pos end_pos)
spaces (re-find #"^[\t ]+" (first (string/split-lines content)))
content (if spaces (remove-indentation-spaces content (count spaces) true)
content)
block ["Src" (assoc (second block) :full_content content)]]
[block pos-meta])
[block pos-meta])) ast)))
(def ^:private inline-ast-types
#{"Plain" "Spaces" "Link" "Nested_link" "Target" "Subscript" "Superscript"
"Footnote_Reference" "Cookie" "Latex_Fragment" "Macro" "Entity" "Timestamp"
"Radio_Target" "Export_Snippet" "Inline_Source_Block" "Email" "Inline_Hiccup"
"Inline_Html" "Emphasis" "Verbatim" "Code" "Break_Line" "Hard_Break_Line"})
(defn- inline-coll?
[x]
(and (vector? x)
(seq x)
(every? #(and (vector? %)
(string? (first %))
(contains? inline-ast-types (first %)))
x)))
(defn- inline-ast->source
;; Only reconstructs the source text for the node types that appear when
;; mldoc splits a macro containing ^{}/{_{}. Nested rich-inline types
;; (Emphasis, Code, etc.) inside a Superscript/Subscript will produce nil,
;; which (apply str ...) silently drops. This is intentional: the function
;; is only called during macro-recovery, not for general rendering.
[[typ content]]
(case typ
"Plain" content
"Spaces" content
"Link" (:full_text content)
;; Nested_link is the AST node for [[page name]]; :content is the page name
;; string. Without this case, collect-macro-source aborts via when-let
;; whenever a page ref appears inside a fragmented macro.
"Nested_link" (str "[[" (:content content) "]]")
"Superscript" (str "^{" (apply str (map inline-ast->source content)) "}")
"Subscript" (str "_{" (apply str (map inline-ast->source content)) "}")
nil))
(defn- starts-with-at?
[s prefix idx]
(let [end (+ idx (count prefix))]
(and (<= end (count s))
(= prefix (subs s idx end)))))
(defn- unclosed-script-markup?
[s]
(loop [idx 0
depth 0]
(if (< idx (count s))
(cond
(or (starts-with-at? s "^{" idx)
(starts-with-at? s "_{" idx))
(recur (+ idx 2) (inc depth))
(and (pos? depth) (= \} (nth s idx)))
(recur (inc idx) (dec depth))
:else
(recur (inc idx) depth))
(pos? depth))))
(defn- split-macro-arguments
[s]
(if (string/blank? s)
[]
(loop [idx 0
start 0
page-ref-depth 0
script-depth 0
quoted? false
escaped? false
result []]
(if (< idx (count s))
(let [c (nth s idx)]
(cond
escaped?
(recur (inc idx) start page-ref-depth script-depth quoted? false result)
(= \\ c)
(recur (inc idx) start page-ref-depth script-depth quoted? true result)
(= \" c)
(recur (inc idx) start page-ref-depth script-depth (not quoted?) false result)
quoted?
(recur (inc idx) start page-ref-depth script-depth quoted? false result)
(starts-with-at? s "[[" idx)
(recur (+ idx 2) start (inc page-ref-depth) script-depth quoted? false result)
(and (pos? page-ref-depth) (starts-with-at? s "]]" idx))
(recur (+ idx 2) start (dec page-ref-depth) script-depth quoted? false result)
(or (starts-with-at? s "^{" idx)
(starts-with-at? s "_{" idx))
(recur (+ idx 2) start page-ref-depth (inc script-depth) quoted? false result)
(and (pos? script-depth) (= \} c))
(recur (inc idx) start page-ref-depth (dec script-depth) quoted? false result)
(and (zero? page-ref-depth) (zero? script-depth) (= \, c))
(recur (inc idx) (inc idx) page-ref-depth script-depth quoted? false
(conj result (string/trim (subs s start idx))))
:else
(recur (inc idx) start page-ref-depth script-depth quoted? false result)))
(conj result (string/trim (subs s start)))))))
(defn- macro-source->ast
[s]
(when (and (string/starts-with? s "{{")
(string/ends-with? s "}}"))
(let [content (-> s
(subs 2 (- (count s) 2))
string/triml)
[_ name arguments] (re-matches #"([^\s]+)(?:\s+([\s\S]*))?" content)]
(when name
["Macro" {:name name
:arguments (split-macro-arguments arguments)}]))))
(defn- collect-macro-source
[items]
(loop [remaining items
source ""]
(when-let [[item & more] (seq remaining)]
(let [[typ content] item]
(cond
(= "Plain" typ)
(if-let [idx (string/index-of content "}}")]
(let [macro-source (str source (subs content 0 (+ idx 2)))]
(when-let [macro (macro-source->ast macro-source)]
(let [suffix (subs content (+ idx 2))]
{:macro macro
:suffix (when-not (string/blank? suffix) ["Plain" suffix])
:remaining more})))
(recur more (str source content)))
:else
(when-let [item-source (inline-ast->source item)]
(recur more (str source item-source))))))))
(defn- close-script-markup-in-macro
;; Appends a single "}" to the last argument to close the outermost
;; unmatched ^{/{_ bracket. Handles only depth-1 cases; deeper nesting
;; (e.g. Ca^{^{2}}) is not supported and is not expected in practice.
[macro]
(update-in macro [1 :arguments]
(fn [arguments]
(update arguments (dec (count arguments)) str "}"))))
(defn- recover-inline-macros
[inline-list]
(loop [remaining inline-list
result []]
(if-let [[item & more] (seq remaining)]
(let [[typ content] item]
(cond
(and (= "Plain" typ)
(string/includes? content "{{"))
(let [idx (string/index-of content "{{")
prefix (subs content 0 idx)
start-item ["Plain" (subs content idx)]]
(if-let [{:keys [macro suffix remaining]} (collect-macro-source (cons start-item more))]
(recur (if suffix
(cons suffix remaining)
remaining)
(cond-> result
(not (string/blank? prefix)) (conj ["Plain" prefix])
true (conj macro)))
(recur more (conj result item))))
(and (= "Macro" typ)
(seq (:arguments content))
(unclosed-script-markup? (last (:arguments content)))
(= "Plain" (ffirst more))
(string/starts-with? (second (first more)) "}"))
(let [[_ plain] (first more)
suffix (subs plain 1)]
(recur (if (string/blank? suffix)
(rest more)
(cons ["Plain" suffix] (rest more)))
(conj result (close-script-markup-in-macro item))))
:else
(recur more (conj result item))))
result)))
(defn- normalize-macro-asts
[ast]
(walk/postwalk
(fn [x]
(if (inline-coll? x)
(recover-inline-macros x)
x))
ast))
(defn- macro-with-script-markup?
[content]
(and (string/includes? content "{{")
(or (string/includes? content "^{")
(string/includes? content "_{"))))
(defn collect-page-properties
[ast config]
(when (seq ast)
(let [original-ast ast
directive? (fn [[item _]] (= "directive" (string/lower-case (first item))))
grouped-ast (group-by directive? original-ast)
[properties-ast other-ast] [(->> (get grouped-ast true)
(map first))
(get grouped-ast false)]
properties (map (fn [[_directive k v]]
[k v (get-references v config)])
properties-ast)]
(if (seq properties)
(cons [["Properties" properties] nil] other-ast)
original-ast))))
(defn get-default-config
"Gets a mldoc default config for the given format. Works for DB and file graphs"
[repo format]
(let [db-based? (sqlite-util/db-based-graph? repo)]
(->>
(cond-> (default-config-map format)
db-based?
(assoc :enable_drawers false
:parse_marker false
:parse_priority false))
bean/->js
js/JSON.stringify)))
(defn ->edn
;; TODO: Re-enable schema
;; {:malli/schema [:=> [:cat :string :string] mldoc-schema/block-ast-with-pos-coll-schema]}
([content config]
(if (string? content)
(try
(if (string/blank? content)
[]
(-> content
(parse-json config)
(common-util/json->clj)
(cond-> (macro-with-script-markup? content)
(normalize-macro-asts))
(update-src-full-content content)
(collect-page-properties config)))
(catch :default e
(log/error :unexpected-error e)
[]))
(log/error :edn/wrong-content-type content)))
([repo content format]
(->edn content (get-default-config repo format))))
(defn ->db-edn
"Wrapper around ->edn for DB graphs"
[content format]
(->edn "logseq_db_repo_stub" content format))
(defn inline->edn
[text config]
(try
(if (string/blank? text)
{}
(-> text
(inline-parse-json config)
(common-util/json->clj)
(cond-> (macro-with-script-markup? text)
(normalize-macro-asts))))
(catch :default _e
[])))
(defn ast-link?
[[type link]]
(let [[ref-type ref-value] (:url link)]
(and (= "Link" type)
(or
;; 1. url
(not (contains? #{"Page_ref" "Block_ref"} ref-type))
(and (contains? #{"Page_ref"} ref-type)
(boolean (common-config/local-relative-asset? ref-value)))))))
(defn mldoc-link?
"Check whether s is a link (including page/block refs)."
[format s]
(let [result (inline->edn s (default-config format))]
(and
(= 1 (count result))
(let [result' (first result)]
(or (contains? #{"Nested_link"} (first result'))
(contains? #{"Page_ref" "Block_ref" "Complex"} (first (:url (second result')))))))))
(defn block-with-title?
[type]
(contains? #{"Paragraph"
"Raw_Html"
"Hiccup"
"Heading"} type))

View File

@@ -0,0 +1,221 @@
(ns logseq.graph-parser.property
"For file graphs, core vars and util fns for properties"
(:require [clojure.set :as set]
[clojure.string :as string]
[goog.string :as gstring]
[goog.string.format]
[logseq.common.util :as common-util]))
(def colons "Property delimiter for markdown mode" "::")
(defn colons-org
"Property delimiter for org mode"
[property]
(str ":" property ":"))
(defn properties-ast?
[block]
(and
(vector? block)
(contains? #{"Property_Drawer" "Properties"}
(first block))))
(defn valid-property-name?
[s]
{:pre [(string? s)]}
(and (common-util/valid-edn-keyword? s)
(not (re-find #"[\"|^|(|)|{|}]+" s))
;; Disallow tags as property names
(not (re-find #"^:#" s))))
;; Built-in properties are properties that logseq uses for its features. Most of
;; these properties are hidden from the user but a few like the editable ones
;; are visible for the user to edit.
(def editable-linkable-built-in-properties
"Properties used by logseq that user can edit and that can have linkable property values"
#{:alias :aliases :tags})
(defn editable-built-in-properties
"Properties used by logseq that user can edit"
[]
(set/union #{:title :icon :template :template-including-parent :public :filters :exclude-from-graph-view
:logseq.query/nlp-date
;; org-mode only
:macro :filetags}
editable-linkable-built-in-properties))
(defn hidden-built-in-properties
"Properties used by logseq that user can't edit or see"
[]
#{:custom-id :background_color :created_at :last_modified_at ; backward compatibility only
:id :background-color :heading :collapsed
:created-at :updated-at :last-modified-at
:query-table :query-properties :query-sort-by :query-sort-desc :ls-type
:hl-type :hl-page :hl-stamp :hl-color :hl-value :logseq.macro-name :logseq.macro-arguments
:logseq.order-list-type
; task markers
:todo :doing :now :later :done})
(def built-in-property-types
"Types for built-in properties. Built-in properties whose values are to be
parsed by gp-text/parse-non-string-property-value should be added here"
{:template-including-parent :boolean
:public :boolean
:exclude-from-graph-view :boolean
:logseq.query/nlp-date :boolean
:heading :boolean ; FIXME: or integer
:collapsed :boolean
:created-at :integer
:created_at :integer
:updated-at :integer
:last-modified-at :integer
:last_modified_at :integer
:query-table :boolean
:query-sort-desc :boolean
:hl-page :integer
:hl-stamp :integer
:todo :integer
:doing :integer
:now :integer
:later :integer
:done :integer})
(assert (set/subset? (set (keys built-in-property-types))
(set/union (hidden-built-in-properties)
(editable-built-in-properties)))
"Keys of built-in-property-types must be valid built-in properties")
(defn unparsed-built-in-properties
"Properties whose values will not be parsed by gp-text/parse-property"
[]
(set/difference (set/union (hidden-built-in-properties)
(editable-built-in-properties))
;; Refs need to be parsed
editable-linkable-built-in-properties
;; All these should be parsed by gp-text/parse-non-string-property-value
(set (keys built-in-property-types))))
(defonce properties-start ":PROPERTIES:")
(defonce properties-end ":END:")
(defonce properties-end-pattern
(re-pattern (gstring/format "%s[\t\r ]*\n|(%s\\s*$)" properties-end properties-end)))
(defn- contains-properties?
[content]
(when content
(and (string/includes? content properties-start)
(re-find properties-end-pattern content))))
(defn ->new-properties
"New syntax: key:: value"
[content]
(if (contains-properties? content)
(let [lines (string/split-lines content)
start-idx (.indexOf lines properties-start)
end-idx (.indexOf lines properties-end)]
(if (and (>= start-idx 0) (> end-idx 0) (> end-idx start-idx))
(let [before (subvec lines 0 start-idx)
middle (->> (subvec lines (inc start-idx) end-idx)
(map (fn [text]
(let [[k v] (common-util/split-first ":" (subs text 1))]
(if (and k v)
(let [k (string/replace k "_" "-")
compare-k (keyword (string/lower-case k))
k (if (contains? #{:id :custom_id :custom-id} compare-k) "id" k)
k (if (contains? #{:last-modified-at} compare-k) "updated-at" k)]
(str k colons " " (string/trim v)))
text)))))
after (subvec lines (inc end-idx))
lines (concat before middle after)]
(string/join "\n" lines))
content))
content))
(defn simplified-property?
[line]
(boolean
(and (string? line)
(re-find (re-pattern (str "^\\s?[^ ]+" colons)) line))))
(defn remove-properties
[format content]
(cond
(contains-properties? content)
(let [lines (string/split-lines content)
[title-lines properties&body] (split-with #(-> (string/triml %)
string/upper-case
(string/starts-with? properties-start)
not)
lines)
body (drop-while #(-> (string/trim %)
string/upper-case
(string/starts-with? properties-end)
not
(or (string/blank? %)))
properties&body)
body (if (and (seq body)
(-> (first body)
string/triml
string/upper-case
(string/starts-with? properties-end)))
(let [line (string/replace (first body) #"(?i):END:\s?" "")]
(if (string/blank? line)
(rest body)
(cons line (rest body))))
body)]
(->> (concat title-lines body)
(string/join "\n")))
(not= format :org)
(let [lines (string/split-lines content)
lines (if (simplified-property? (first lines))
(drop-while simplified-property? lines)
(cons (first lines)
(drop-while simplified-property? (rest lines))))]
(string/join "\n" lines))
:else
content))
(defn remove-logbook
[content]
(when (string? content)
(let [lines (string/split-lines content)
[result _in-logbook?]
(reduce (fn [[acc in-logbook?] line]
(let [trimmed (string/trim line)
upper (string/upper-case trimmed)]
(cond
(string/starts-with? upper ":LOGBOOK:")
[acc true]
(and in-logbook? (string/starts-with? upper ":END:"))
[acc false]
in-logbook?
[acc true]
:else
[(conj acc line) in-logbook?])))
[[] false]
lines)]
(string/join "\n" result))))
(defn remove-deadline-scheduled
[content]
(when (string? content)
(let [lines (string/split-lines content)]
(if (= 1 (count lines))
content
(let [first-line (first lines)
rest-lines (rest lines)
rest-lines (keep (fn [line]
(let [upper (string/upper-case (string/triml line))]
(if (or (string/starts-with? upper "DEADLINE: ")
(string/starts-with? upper "SCHEDULED: "))
(let [cleaned (-> line (string/replace #"(?i)(?:^|\s)(DEADLINE|SCHEDULED):\s+<[^>]*>" "") string/trim)]
(when-not (string/blank? cleaned)
cleaned))
line)))
rest-lines)]
(string/join "\n" (cons first-line rest-lines)))))))

View File

@@ -0,0 +1,217 @@
(ns logseq.graph-parser.schema.mldoc
"Malli schema for mldoc AST")
(defn- field-optional-and-maybe-nil
[k v]
[k {:optional true} [:maybe v]])
(def pos-schema
[:map
[:start_pos :int]
[:end_pos :int]])
(def nested-link-schema
[:schema {:registry {::nested-link
[:map
[:content :string]
[:children [:sequential [:or
[:tuple [:= "Label"] :string]
[:tuple [:= "Nested_link"] [:ref ::nested-link]]]]]]}}
::nested-link])
(def timestamp-schema
[:map
[:date [:map
[:year :int]
[:month :int]
[:day :int]]]
[:wday :string]
(field-optional-and-maybe-nil
:time [:map
[:hour :int]
[:min :int]])
(field-optional-and-maybe-nil
:repetition
:any)
[:active :boolean]])
(def ^:private time-range-schema
[:map
[:start [:ref ::timestamp]]
[:stop [:ref ::timestamp]]])
(def ^:private link-schema
[:map
[:url [:or
[:cat [:= "File"] :string]
[:cat [:= "Search"] :string]
[:cat [:= "Complex"] [:map
[:protocol :string]
[:link :string]]]
[:cat [:= "Page_ref"] :string]
[:cat [:= "Block_ref"] :string]
[:cat [:= "Embed_data"] :string]]]
[:label [:sequential [:ref ::inline]]]
(field-optional-and-maybe-nil :title :string)
[:full_text :string]
[:metadata :string]])
(def latex-fragment-schema
[:or
[:tuple [:= "Inline"] :string]
[:tuple [:= "Displayed"] :string]])
(def inline-ast-schema
[:schema {:registry {::timestamp timestamp-schema
::time-range time-range-schema
::link link-schema
::inline
[:or
[:tuple [:= "Emphasis"]
[:tuple
[:tuple [:enum "Italic" "Bold" "Underline" "Strike_through" "Highlight"]]
[:sequential [:ref ::inline]]]]
[:tuple [:= "Break_Line"]]
[:tuple [:= "Hard_Break_Line"]]
[:tuple [:= "Verbatim"] :string]
[:tuple [:= "Code"] :string]
[:tuple [:= "Tag"] [:sequential [:ref ::inline]]]
[:tuple [:= "Spaces"] :string]
[:tuple [:= "Plain"] :string]
[:tuple [:= "Link"] [:ref ::link]]
[:tuple [:= "Nested_link"] nested-link-schema]
[:tuple [:= "Target"] :string]
[:tuple [:= "Subscript"] [:sequential [:ref ::inline]]]
[:tuple [:= "Superscript"] [:sequential [:ref ::inline]]]
[:tuple [:= "Footnote_Reference"] [:map
[:id :int]
[:name :string]
(field-optional-and-maybe-nil
:definition [:sequential [:ref ::inline]])]]
[:tuple [:= "Cookie"] [:or
[:tuple [:= "Percent"] :int]
[:catn [:label [:= "Absolute"]] [:current :int] [:total :int]]]]
[:tuple [:= "Latex_Fragment"] latex-fragment-schema]
[:tuple [:= "Macro"] [:map
[:name :string]
[:arguments [:sequential :string]]]]
[:tuple [:= "Entity"] [:map
[:name :string]
[:latex :string]
[:latex_mathp :boolean]
[:html :string]
[:ascii :string]
[:unicode :string]]]
[:tuple [:= "Timestamp"] [:or
[:tuple [:= "Scheduled"] [:ref ::timestamp]]
[:tuple [:= "Deadline"] [:ref ::timestamp]]
[:tuple [:= "Date"] [:ref ::timestamp]]
[:tuple [:= "Closed"] [:ref ::timestamp]]
[:tuple [:= "Clock"] [:or
[:tuple [:= "Started"] [:ref ::timestamp]]
[:tuple [:= "Stopped"] [:ref ::time-range]]]]
[:tuple [:= "Range"] [:ref ::time-range]]]]
[:tuple [:= "Radio_Target"] :string]
[:tuple [:= "Export_Snippet"] :string :string]
[:tuple [:= "Inline_Source_Block"] [:map
[:language :string]
[:options :string]
[:code :string]]]
[:tuple [:= "Email"] [:map
[:local_part :string]
[:domain :string]]]
[:tuple [:= "Inline_Hiccup"] :string]
[:tuple [:= "Inline_Html"] :string]]}}
::inline])
(def ^:private list-item-schema
[:map
[:content [:sequential [:ref ::block]]]
[:items [:sequential [:ref ::list-item]]]
(field-optional-and-maybe-nil
:number :int)
[:name [:sequential [:ref ::inline]]]
(field-optional-and-maybe-nil
:checkbox :boolean)
[:indent :int]
[:ordered :boolean]])
(def ^:private heading-schema
[:map
[:title [:sequential [:ref ::inline]]]
[:tags [:sequential :string]]
(field-optional-and-maybe-nil
:marker :string)
[:level :int]
(field-optional-and-maybe-nil
:numbering [:sequential :int])
(field-optional-and-maybe-nil
:priority :string)
[:anchor :string]
[:meta :map]
(field-optional-and-maybe-nil
:size :int)])
(def block-ast-schema
[:schema {:registry {::inline inline-ast-schema
::list-item list-item-schema
::block
[:or
[:tuple [:= "Paragraph"] [:sequential [:ref ::inline]]]
[:tuple [:= "Paragraph_Sep"] :int]
[:tuple [:= "Heading"] heading-schema]
[:tuple [:= "List"] [:sequential [:ref ::list-item]]]
[:tuple [:= "Directive"] :string :string]
[:tuple [:= "Results"]]
[:tuple [:= "Example"] [:sequential :string]]
[:tuple [:= "Src"] [:map
[:lines [:sequential :string]]
(field-optional-and-maybe-nil
:language :string)
(field-optional-and-maybe-nil
:options [:sequential :string])
[:pos_meta pos-schema]]]
[:tuple [:= "Quote"] [:sequential [:ref ::block]]]
[:catn
[:label [:= "Export"]]
[:type :string]
[:options [:maybe [:sequential :string]]]
[:content :string]]
[:tuple [:= "CommentBlock"] [:sequential :string]]
[:catn
[:label [:= "Custom"]]
[:type :string]
[:options [:maybe :string]]
[:result [:sequential [:ref ::block]]]
[:content :string]]
[:tuple [:= "Latex_Fragment"] latex-fragment-schema]
[:catn
[:label [:= "Latex_Environment"]]
[:name :string]
[:options [:maybe :string]]
[:content :string]]
[:tuple [:= "Displayed_Math"] :string]
[:tuple [:= "Drawer"] :string [:sequential :string]]
[:tuple [:= "Property_Drawer"]
[:sequential
[:catn [:k :string] [:v :string] [:other-info [:sequential [:ref ::inline]]]]]]
[:tuple [:= "Footnote_Definition"] :string [:sequential [:ref ::inline]]]
[:tuple [:= "Horizontal_Rule"]]
[:tuple [:= "Table"]
[:map
(field-optional-and-maybe-nil
:header [:sequential [:sequential [:ref ::inline]]])
[:groups [:sequential [:sequential [:sequential [:sequential [:ref ::inline]]]]]]
[:col_groups [:sequential :int]]]]
[:tuple [:= "Comment"] :string]
[:tuple [:= "Raw_Html"] :string]
[:tuple [:= "Hiccup"] :string]
;; this block type is not from mldoc,
;; but from `logseq.graph-parser.mldoc/collect-page-properties`
[:tuple [:= "Properties"] [:sequential :any]]]}}
::block])
(def block-ast-with-pos-coll-schema
[:sequential [:cat block-ast-schema [:maybe pos-schema]]])

View File

@@ -0,0 +1,171 @@
(ns logseq.graph-parser.text
"Miscellaneous text util fns for the parser. Used by file and DB graphs"
(:require ["path" :as node-path]
[clojure.set :as set]
[clojure.string :as string]
[goog.string :as gstring]
[logseq.common.util :as common-util]
[logseq.common.util.namespace :as ns-util]
[logseq.common.util.page-ref :as page-ref]
[logseq.graph-parser.mldoc :as gp-mldoc]
[logseq.graph-parser.property :as gp-property]))
(def get-file-basename page-ref/get-file-basename)
(defn- get-file-rootname
"Returns the rootname of a file path. e.g. /a/b/c.md -> c"
[path]
(when-not (string/blank? path)
(.-name (node-path/parse (string/replace path "+" "/")))))
(defn get-page-name
"Similar to page-ref/get-page-name but handles format-specific page-refs e.g. org/md"
[s]
(and (string? s)
(or (when-let [[_ label _path] (re-matches page-ref/markdown-page-ref-re s)]
(string/trim label))
(when-let [[_ path _label] (re-matches #"\[\[(file:.*)\]\[.+?\]\]" s)]
(some-> (get-file-rootname path)
(string/replace "." "/")))
(-> (re-matches page-ref/page-ref-any-re s)
second))))
(def page-ref-un-brackets! page-ref/page-ref-un-brackets!)
(defn get-nested-page-name
[page-name]
(when-let [first-match (re-find page-ref/page-ref-without-nested-re page-name)]
(second first-match)))
(defn- remove-level-space-aux!
[text pattern space? trim-left?]
(let [pattern (gstring/format
(if space?
"^[%s]+\\s+"
"^[%s]+\\s?")
pattern)
text (if trim-left? (string/triml text) text)]
(string/replace-first text (re-pattern pattern) "")))
(defn remove-level-spaces
([text format block-pattern]
(remove-level-spaces text format block-pattern false true))
([text format block-pattern space?]
(remove-level-spaces text format block-pattern space? true))
([text format block-pattern space? trim-left?]
(when format
(cond
(string/blank? text)
""
(and (= "markdown" (name format))
(string/starts-with? text "---"))
text
:else
(remove-level-space-aux! text block-pattern space? trim-left?)))))
(defn parse-non-string-property-value
"Return parsed non-string property value or nil if none is found"
[v]
(cond
(= v "true")
true
(= v "false")
false
(re-find #"^\d+$" v)
(parse-long v)))
(defn- get-ref-from-ast
[[typ data]]
(case typ
"Link"
(case (first (:url data))
"Page_ref"
(second (:url data))
"Search"
(second (:url data))
nil)
"Nested_link"
(get-page-name (:content data))
"Tag"
(if (= "Plain" (ffirst data))
(second (first data))
(get-ref-from-ast (first data)))
nil))
(defn extract-refs-from-mldoc-ast
[v]
(into #{}
(comp
(remove gp-mldoc/ast-link?)
(keep get-ref-from-ast)
(map string/trim))
v))
(defn- sep-by-comma
[s]
{:pre (string? s)}
(->>
(string/split s #"[\,,]{1}")
(map string/trim)
(remove string/blank?)
(set)))
(defn separated-by-commas?
[config-state k]
(let [k' (if (keyword? k) k (keyword k))]
(contains? (set/union gp-property/editable-linkable-built-in-properties
(set (get config-state :property/separated-by-commas)))
k')))
(defn- extract-refs-by-commas
[v format]
(let [plains (->> (map first (gp-mldoc/->edn v (gp-mldoc/default-config format)))
first
second
(filter #(and (vector? %) (= "Plain" (first %))))
(map second))]
(set (mapcat sep-by-comma plains))))
(defn- parse-property-refs [k v mldoc-references-ast config-state]
(let [refs (extract-refs-from-mldoc-ast mldoc-references-ast)
property-separated-by-commas? (separated-by-commas? config-state k)]
(if property-separated-by-commas?
(->> (extract-refs-by-commas v (get config-state :format :markdown))
(set/union refs))
refs)))
(defn parse-property
"Property value parsing that takes into account built-in properties, format
and user config"
[k v mldoc-references-ast config-state]
(let [v' (string/trim (str v))]
(cond
(contains? (set/union
(set (map name (gp-property/unparsed-built-in-properties)))
(get config-state :ignored-page-references-keywords))
(name k))
v'
(common-util/wrapped-by-quotes? v')
v'
;; parse property value as needed
:else
(let [refs (parse-property-refs k v' mldoc-references-ast config-state)]
(if (seq refs)
refs
(if-some [new-val (parse-non-string-property-value v')]
new-val
v'))))))
(def namespace-page? ns-util/namespace-page?)
(def get-namespace-last-part ns-util/get-last-part)

View File

@@ -0,0 +1,24 @@
(ns logseq.graph-parser.utf8
"Utf8 utilities used by the parser")
(defonce encoder
(js/TextEncoder. "utf-8"))
(defonce decoder
(js/TextDecoder. "utf-8"))
(defn encode
[s]
(.encode encoder s))
(defn decode
[arr]
(.decode decoder arr))
(defn substring
([arr start]
(decode (.subarray arr start)))
([arr start end]
(if end
(decode (.subarray arr start end))
(decode (.subarray arr start)))))