diff --git a/.gitignore b/.gitignore
index e3bc71f..ccd2580 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@
.lsp
.vscode
keystore/
+.tmp
diff --git a/customers-x/needs.zd b/customers-x/needs.zd
deleted file mode 100644
index a0596e5..0000000
--- a/customers-x/needs.zd
+++ /dev/null
@@ -1,3 +0,0 @@
-:title "needs"
-:menu-order 100
-:desc /
\ No newline at end of file
diff --git a/customers-x/orgs.zd b/customers-x/orgs.zd
deleted file mode 100644
index 24a8ce1..0000000
--- a/customers-x/orgs.zd
+++ /dev/null
@@ -1,14 +0,0 @@
-:menu-order 1000
-
-:title "orgs"
-:desc /
-
-:report ?/
-
-c :type :case
-
-
-> c
-> c:case
-> c:need
-> c:parent
\ No newline at end of file
diff --git a/customers-x/orgs/o1.zd b/customers-x/orgs/o1.zd
deleted file mode 100644
index fe822a2..0000000
--- a/customers-x/orgs/o1.zd
+++ /dev/null
@@ -1,18 +0,0 @@
-
-:title "O1"
-:desc /
-
-:need needs.hl7
-
-
-&case-big-data
-:type :case
-:need needs.big-data
-:case /
-Here is desc
-
-&case-hl7
-:type :case
-:need needs.hl7
-:case /
-Here is desc
\ No newline at end of file
diff --git a/customers-x/orgs/o2.zd b/customers-x/orgs/o2.zd
deleted file mode 100644
index adc1058..0000000
--- a/customers-x/orgs/o2.zd
+++ /dev/null
@@ -1,16 +0,0 @@
-
-:title "O2"
-:desc /
-
-
-&case-big-data
-:type :case
-:need needs.big-data
-:case /
-Here is desc djfaljflkdajfl
-
-&case-ccd
-:type :case
-:need needs.hl7
-:case /
-Here is desc
\ No newline at end of file
diff --git a/docs/Modeling.zd b/docs/Modeling.zd
new file mode 100644
index 0000000..446c012
--- /dev/null
+++ b/docs/Modeling.zd
@@ -0,0 +1,122 @@
+:title "Modeling Docs"
+:icon [:fa-duotone :fa-face-cowboy-hat]
+:menu-order 3
+
+:desc /
+
+:absolute-names /
+
+There is a special document Resource, where you can define
+attributes which should not be prefixed with namespace
+
+```zd
+ :zd/docname Resource
+
+ &name
+ :zd/type zd.Property
+ :zd/data-type zd.string
+
+```
+
+Property `Resource.name` can be used as
+
+```zd
+ :zd/docname mydoc
+ :name "Just a name"
+```
+
+:intro /
+How to model with zendoc?
+
+In zendoc everything is a resource or document. Some docs may represent
+entities from real world and others are abstract concepts.
+
+Every doc may have multiple types - `:type` property, which is reference to
+other doc.
+
+It's recommended for concepts to have a `:type Class`
+For class you can define a properties with nested docs of type `Property`
+
+```zd
+ :zd/docname person
+ :zd/type zd.Class
+ :zd/required #{ .name, .telegram }
+
+ &name
+ :zd/type zd.Property
+ :zd/data-type zd.string
+ :zd/annotation zd.badge
+
+ &telegram
+ :zd/type zd.Property
+ :zd/data-type zd.string
+ :zd/annotation zd.badge
+
+ &male
+ :zd/type zd.Class
+
+ &female
+ :zd/type zd.Class
+
+ &gender
+ :zd/type zd.Property
+ :zd/enum #{ .male .female }
+ :zd/annotation zd.badge
+
+```
+
+
+```zd
+ :zd/docname person.ivan
+ :zd/type person
+ :person/name "Ivan"
+ :person/telegram "ivantelega"
+ :person/gender person.male
+
+```
+
+Attribute of name `namespace/name` will look for definition in `namespace.name`.
+
+:subclasses /
+
+While defining Class you may state that this is a subclass of another class.
+That means all instances of this class are instances of superclass.
+Zendoc will be able to use this for inference.
+
+```zd
+ :zd/type zd.Class
+ :zd/subclass person
+
+```
+
+```zd
+ :zd/type Samurai
+```
+
+Now you can search "Nikolai" like a `person`
+
+```datalog
+ e :zd/type person
+ > e
+```
+
+:same-as /
+
+If property has a `zd.same-as` zendoc will do the inference of this attributes.
+
+```zd
+ :zd/type zd.Property
+ :zd/same-as :foaf/name
+
+```
+
+```zd
+ :person.name Nikolai
+```
+
+You may search it as
+
+```zd
+ e :foaf/name "Nikolai"
+ > e
+```
\ No newline at end of file
diff --git a/docs/_.zd b/docs/_.zd
new file mode 100644
index 0000000..9fe9f88
--- /dev/null
+++ b/docs/_.zd
@@ -0,0 +1,5 @@
+:title "Global Schema"
+
+
+&date zd.prop
+:zd/data-type zd.date
\ No newline at end of file
diff --git a/docs/artists.zd b/docs/artists.zd
new file mode 100644
index 0000000..44f9fbb
--- /dev/null
+++ b/docs/artists.zd
@@ -0,0 +1,8 @@
+:zd/menu-order 201
+:title "Artists"
+
+:number-of-songs ?/
+
+s :music/written-by a
+> a
+> (count s)
\ No newline at end of file
diff --git a/docs/artists/bethoven.zd b/docs/artists/bethoven.zd
new file mode 100644
index 0000000..d983e78
--- /dev/null
+++ b/docs/artists/bethoven.zd
@@ -0,0 +1,3 @@
+:zd/type music.artist
+:title "Bethoven"
+:music/artist-name "Bethoven"
\ No newline at end of file
diff --git a/docs/artists/john-lenon.zd b/docs/artists/john-lenon.zd
new file mode 100644
index 0000000..8b64f75
--- /dev/null
+++ b/docs/artists/john-lenon.zd
@@ -0,0 +1,4 @@
+:zd/type music.artist
+
+:music/artist-name "John Lenon"
+:title "John Lenon"
\ No newline at end of file
diff --git a/docs/backlog.zd b/docs/backlog.zd
new file mode 100644
index 0000000..8fd6b29
--- /dev/null
+++ b/docs/backlog.zd
@@ -0,0 +1,52 @@
+:menu-order 20
+:icon [:fa-solid :fa-rectangle-list]
+
+^link-badge
+:repo "https://github.com/zen-lang/zendoc"
+:section "first"
+
+:title "Backlog"
+:desc /
+
+features and issues
+
+:backlog /
+
+* [x] rename parent should rename directory and all children
+* [x] delete parent should delete directory of children
+* [x] implement zd.summary on backlinks
+* [x] unnamespaced keys as _.keyname
+* [x] zd.props for autocomplete
+* [ ] refactor errors to separate page (not doc)
+* [x] validation of optional set/array
+* [ ] do not allow to override existing doc with new document
+* [ ] implement git pull (automatic) and push
+* [ ] work on documentation
+* [ ] zd.annotations
+* [ ] do not suggest + if exact symbol exists
+* [ ] fix ctrl-l conflict with browser for autocomplete
+* [ ] create an example sematic knowledge base - about semantic technologies
+* [ ] build jar
+* [ ] code highlight for zd/, end and code
+* [ ] paging/search on backlinks
+* [ ] track local changes untill commit
+* [ ] structured write api
+* [ ] fix macros
+* [ ] nested navigation
+* [ ] menu subsections
+* [ ] validate subdocs while edit
+* [ ] :zd/type inference with :zd/child-type and :zd/subclass
+* [ ] implement file upload
+* [ ] subdoc in-the-middle of doc (close with? &&?)
+
+
+```zd
+ :title "Bla bla"
+
+ &subdoc Class
+ :title "Subdoc"
+ &
+
+ :another attribute
+
+```
\ No newline at end of file
diff --git a/docs/backlogs.zd b/docs/backlogs.zd
new file mode 100644
index 0000000..e69de29
diff --git a/docs/backlogs/tags.zd b/docs/backlogs/tags.zd
new file mode 100644
index 0000000..e69de29
diff --git a/docs/city.zd b/docs/city.zd
new file mode 100644
index 0000000..e8ff773
--- /dev/null
+++ b/docs/city.zd
@@ -0,0 +1,4 @@
+:title "Cities"
+:desc /
+
+cities dictionary
diff --git a/docs/city/Estoril.zd b/docs/city/Estoril.zd
new file mode 100644
index 0000000..970f77c
--- /dev/null
+++ b/docs/city/Estoril.zd
@@ -0,0 +1,6 @@
+:title "Estoril"
+^badge
+:type loc.City
+^badge
+:loc.in country.Portugal
+:desc /
\ No newline at end of file
diff --git a/docs/city/Lisboa.zd b/docs/city/Lisboa.zd
new file mode 100644
index 0000000..035a7e6
--- /dev/null
+++ b/docs/city/Lisboa.zd
@@ -0,0 +1,7 @@
+
+:title "Lisboa"
+^badge
+:type loc.City
+^badge
+:loc.in country.Portugal
+:desc /
\ No newline at end of file
diff --git a/docs/contract.zd b/docs/contract.zd
new file mode 100644
index 0000000..31f5935
--- /dev/null
+++ b/docs/contract.zd
@@ -0,0 +1,3 @@
+:zd/type zd.class
+:zd/require #{:contract/start :contract/organization }
+:zd/summary [:contract/start :contract/end]
\ No newline at end of file
diff --git a/docs/country.zd b/docs/country.zd
new file mode 100644
index 0000000..e69de29
diff --git a/docs/country/Portugal.zd b/docs/country/Portugal.zd
new file mode 100644
index 0000000..192d9de
--- /dev/null
+++ b/docs/country/Portugal.zd
@@ -0,0 +1,3 @@
+:title "Portugal"
+:type loc.Country
+:desc /
\ No newline at end of file
diff --git a/docs/country/US.zd b/docs/country/US.zd
new file mode 100644
index 0000000..d36cdf2
--- /dev/null
+++ b/docs/country/US.zd
@@ -0,0 +1,2 @@
+:title "US"
+:desc /
\ No newline at end of file
diff --git a/docs/features.zd b/docs/features.zd
index ec35bfd..e69de29 100644
--- a/docs/features.zd
+++ b/docs/features.zd
@@ -1 +0,0 @@
-:title "Features"
diff --git a/docs/features/zentext.zd b/docs/features/zentext.zd
index cf4f7e7..6e83ae1 100644
--- a/docs/features/zentext.zd
+++ b/docs/features/zentext.zd
@@ -1,2 +1,2 @@
-:title "zentext"
-:authror team.vganshin
+:features/maturity 5
+:title "Zentext"
\ No newline at end of file
diff --git a/customers-x/file.txt b/docs/file.txt
similarity index 100%
rename from customers-x/file.txt
rename to docs/file.txt
diff --git a/docs/getting-started.zd b/docs/getting-started.zd
new file mode 100644
index 0000000..ffa78de
--- /dev/null
+++ b/docs/getting-started.zd
@@ -0,0 +1,20 @@
+:title "Getting Started"
+:section "first"
+:icon [:fa-brands :fa-dochub]
+:menu-order 1
+
+:desc /
+
+In this tutorial we will create a simple knowlege base for music
+
+:Install /
+
+:Create /
+
+:Edit /
+
+:Delete /
+
+:Vaidate /
+
+:Query /
\ No newline at end of file
diff --git a/docs/index.zd b/docs/index.zd
new file mode 100644
index 0000000..f5d046d
--- /dev/null
+++ b/docs/index.zd
@@ -0,0 +1,98 @@
+:menu-order 0
+:title "Home"
+:zd/icon [:fa-duotone :fa-house-chimney-medical]
+:desc /
+
+
+Zendoc allows you to iteratively develop structured & linked knowledge base starting from text!
+
+:Document /
+
+The core concept of Zendoc revolves around the notion of a "document." In the system, a document is stored as a file with a ".zd" extension on the disk and is also represented as a record in the database. Document names are mapped to file system by replacing `.` with `/` - so `person.niquola` will be located in `person/niquola.zd` file.
+
+The ".zd" format employs a specialized, lightweight syntax for recording data.
+Importantly, each document consists of a set of key-value pairs.
+
+The entire document is composed of keys, which start at the very beginning of lines with a colon `:keyname`. On the same line after any number of spaces you can put the *value* in [[a https://github.com/edn-format/edn EDN]] format.
+
+```zd
+;; string should be in ""
+:title "Title"
+:number 1000
+;; dates should be #inst"[date]"
+:date #inst"2012"
+;; symbol (unquoted string)
+:person person.niquola
+;; #{...} is a set - unordered collection of unique values
+:tags #{tags.postgres tags.clojure}
+;; [...] is a vector - ordered collection of values
+:interests [tags.fhir tags.rdf]
+```
+
+If on key line ends with `/` the following lines untill next key are
+treated as multiline string in #zentext format.
+
+```zd
+
+:zentext-key /
+Here is a long text which shoud not start with ':'
+Text will stop here where new key ':mardown' starts
+
+:another-key /
+Here is a markdown text
+
+```
+
+:Links /
+
+Values of type `symbol` work as references to other docs. For example:
+
+```zd
+:zd/docnmae person.nikolai
+:name "Nikolai"
+:friend-of #{ person.ivan person.john }
+```
+
+Document `person.nikolai` refers documents `person.ivan` and `person.john` and will appear in as a link.
+
+:Backlinks /
+
+Links are bi-directional and if document `a` refers document `b` by some
+ :property. You will see "backlink" to `a` in `b`.
+
+:Menu /
+
+To make document appear in menu you should set property `:zd/menu-order` into a number, which will work as an order for menu.
+
+```zd
+:title "Intro"
+:zd/menu-order 1
+:text /
+----
+:title "Chapter 1"
+:zd/menu-order 2
+:text /
+```
+
+:Schemas /
+
+Documents can be categorized and validated by schemas.
+Schema is a document with `:zd/type zd.class`. One or multiple schemas
+can be linked to document by `:zd/type` property.
+Schema document may force required fields by `:zd/required #{:prop1 :prop2}`
+
+Property can be defined by document with `:zd/type zd.property`. Data type restrictions can be specified with property `:zd/data-type [zd.string zd.symbole zd.int]`. "Property document is translated into namespaced keys by following rules `name.space.prop-name` -> `:name.space/prop-name`
+
+There is a special document with name `_` and properties defined as children of it will not need namespece - i.e. `_.title zd.prop` will define `:title` property. But we heighly recomend to define most of properties as namespaced, this will help you in queries!
+
+:Queries /
+
+Zendoc has built-in datalog engine [[a xtdb ]] to query knowledge base. There are two dialects - official **xtdb** and **simplified** version.
+
+Queries
+```zd
+:query ?/
+e :parent #organizaition
+> e
+
+```
\ No newline at end of file
diff --git a/docs/loc.zd b/docs/loc.zd
new file mode 100644
index 0000000..e69de29
diff --git a/docs/loc/City.zd b/docs/loc/City.zd
new file mode 100644
index 0000000..be32bc5
--- /dev/null
+++ b/docs/loc/City.zd
@@ -0,0 +1,2 @@
+:title "City"
+:desc /
\ No newline at end of file
diff --git a/docs/loc/Country.zd b/docs/loc/Country.zd
new file mode 100644
index 0000000..96fcd67
--- /dev/null
+++ b/docs/loc/Country.zd
@@ -0,0 +1,2 @@
+:title "Country"
+:desc /
\ No newline at end of file
diff --git a/docs/macros.zd b/docs/macros.zd
new file mode 100644
index 0000000..bea9960
--- /dev/null
+++ b/docs/macros.zd
@@ -0,0 +1,12 @@
+:title "Macroses"
+:zd/icon [:fa-duotone :fa-circle-m]
+:menu-order 4
+
+
+:office-locations (load "office-locations.json" :json)
+:yaml-example (load "sample.yaml" :yaml)
+:not-found (load "not-found.json")
+:string-file (load "file.txt")
+:macro-notfound (unload "nofile.txt")
+
+:desc /
\ No newline at end of file
diff --git a/docs/music.zd b/docs/music.zd
new file mode 100644
index 0000000..43e3815
--- /dev/null
+++ b/docs/music.zd
@@ -0,0 +1,22 @@
+:title "Music Schema"
+:zd/type zd.class
+:zd/menu-order 200
+
+&song zd.class
+:zd/require #{:music/song-name :title :music/written-by :date}
+:zd/summary [:music/written-by :date]
+:title "Song"
+
+&artist zd.class
+:zd/require #{:music/artist-name}
+:title "Artist"
+
+&song-name zd.prop
+:zd/data-type zd.string
+
+&artist-name zd.prop
+:zd/data-type zd.string
+
+&written-by zd.prop
+:zd/data-type zd.symbol
+:zd/link-class music.artist
\ No newline at end of file
diff --git a/docs/needs.zd b/docs/needs.zd
new file mode 100644
index 0000000..05d2da0
--- /dev/null
+++ b/docs/needs.zd
@@ -0,0 +1,16 @@
+:zd/menu-order 110
+:zd/icon [:fa-duotone :fa-lamp]
+
+:title "Needs"
+:desc /
+Company Needs
+
+:test (csv 1,2,3)
+
+:needs ?/
+
+c :zd/type #prod.Case
+c :need n
+> n
+> n:phase
+> (count c)
\ No newline at end of file
diff --git a/customers-x/needs/big-data.zd b/docs/needs/big-data.zd
similarity index 100%
rename from customers-x/needs/big-data.zd
rename to docs/needs/big-data.zd
diff --git a/docs/needs/hl7.zd b/docs/needs/hl7.zd
new file mode 100644
index 0000000..0282c92
--- /dev/null
+++ b/docs/needs/hl7.zd
@@ -0,0 +1,20 @@
+
+:due-date #inst "2023-11-01"
+:phase :new
+
+:title "HL7"
+:desc /
+HL7 Parsing and generation
+
+
+:phases /
+
+* -> new
+* -> with solutions
+* -> solution choose
+* -> in implementation
+* -> feedback
+
+:design /
+
+:team /
\ No newline at end of file
diff --git a/docs/needs/sql-on-fhir.zd b/docs/needs/sql-on-fhir.zd
new file mode 100644
index 0000000..a2e9633
--- /dev/null
+++ b/docs/needs/sql-on-fhir.zd
@@ -0,0 +1 @@
+:title "SQL on FHIR"
\ No newline at end of file
diff --git a/customers-x/office-locations.json b/docs/office-locations.json
similarity index 100%
rename from customers-x/office-locations.json
rename to docs/office-locations.json
diff --git a/docs/org.zd b/docs/org.zd
new file mode 100644
index 0000000..5dbf10f
--- /dev/null
+++ b/docs/org.zd
@@ -0,0 +1,2 @@
+:title "org ontology"
+:desc /
\ No newline at end of file
diff --git a/docs/orgs.zd b/docs/orgs.zd
new file mode 100644
index 0000000..6394273
--- /dev/null
+++ b/docs/orgs.zd
@@ -0,0 +1,24 @@
+:icon [:fa-duotone :fa-building]
+:menu-order 100
+:zd/summary [:orgs/tags ]
+:zd/child-type prod.org
+
+:title "Organizations"
+:desc /
+
+:roadmap ?/
+
+c :zd/type #prod.Case
+c :need n
+
+> n | need
+> (count c) | count
+
+:ctos ?/
+
+e :org/role #org.CTO
+e :org/memberOf o
+e :org/person p
+> p
+> e:org/role
+> o
\ No newline at end of file
diff --git a/docs/orgs/google.zd b/docs/orgs/google.zd
new file mode 100644
index 0000000..095d535
--- /dev/null
+++ b/docs/orgs/google.zd
@@ -0,0 +1,11 @@
+:zd/type #{ prod.org }
+
+:prod.org/location country.US
+:prod.org/linkedin "?"
+:prod.org/site "google.com"
+:prod.org/kind prod.org.cloud
+:prod.org/number-of-employees 100000
+
+:title "Google"
+:desc /
+Global organization
\ No newline at end of file
diff --git a/docs/orgs/health-samurai.zd b/docs/orgs/health-samurai.zd
new file mode 100644
index 0000000..2ce1b6c
--- /dev/null
+++ b/docs/orgs/health-samurai.zd
@@ -0,0 +1,42 @@
+:title "Health Samurai"
+
+^badge
+:zd/type prod.org
+:prod.org/site "https://health-samurai.io"
+:prod.org/location #{ country.US }
+:prod.org/linkedin "https://www.linkedin.com/company/6653460/admin/feed/posts/"
+:prod.org/kind prod.org.vendor
+
+:desc /
+
+:employees ?/
+
+emp :emp/organization #orgs.health-samurai
+emp :emp/person p
+> p
+> emp:emp/role
+> emp:emp/since
+> emp:emp/untill
+
+:partners ?/
+
+e :prod/partner-of #orgs.health-samurai
+> e:prod/partner-org
+> e:desc
+> e:prod/champion
+
+:members ?/
+
+e :zd/type #org.Member
+e :org/organization #orgs.health-samurai
+e :org/person p
+
+> p
+> e:org/role
+
+&partner prod.org.partner
+:partner-since "2019"
+:organizations #{ orgs.google .}
+:desc /
+
+We tried few times to partner with Google
\ No newline at end of file
diff --git a/docs/orgs/o1.zd b/docs/orgs/o1.zd
new file mode 100644
index 0000000..0590f1a
--- /dev/null
+++ b/docs/orgs/o1.zd
@@ -0,0 +1,36 @@
+:zd/type prod.org
+
+:prod.org/site "?"
+:prod.org/location country.US
+:prod.org/linkedin "todo"
+:prod.org/kind prod.org.vendor
+
+:title "Acme"
+:desc /
+
+&contract contract
+:contract/start #inst"2023-07"
+:contract/organization .
+:desc /
+Contract
+
+
+&partner prod.org.partner
+:organizations #{orgs.health-samurai .}
+:partner-since #inst"2011"
+
+:desc /
+We partnered with Acme
+
+
+&case-big-data prod.Case
+:need needs.big-data
+:title "Big Data Case"
+:desc /
+Here is desc
+
+&case-hl7 prod.Case
+:need needs.hl7
+:title "HL7 Case"
+:desc /
+Here is desc
\ No newline at end of file
diff --git a/docs/orgs/o2.zd b/docs/orgs/o2.zd
new file mode 100644
index 0000000..67ffac8
--- /dev/null
+++ b/docs/orgs/o2.zd
@@ -0,0 +1,32 @@
+:zd/type prod.org
+:prod.org/site "site"
+:prod.org/location city.Lisboa
+:prod.org/linkedin "?"
+:prod.org/kind prod.org.ehr-vendor
+
+:title "O2"
+:desc /
+
+
+& prod.Case
+:need needs.big-data
+:prod.Case/date #inst"2023-06-07"
+:prod.Case/severity prod.Case.critical
+:desc /
+We have 30M of patients
+
+& prod.Case
+:need needs.hl7
+:case /
+Here is desc
+
+& prod.Case
+:need needs.sql-on-fhir
+:case /
+Here is desc
+
+&todo tasks
+:tasks/assignee person.niquola
+:tasks/severity tasks.critical
+:desc /
+update profile
\ No newline at end of file
diff --git a/docs/person.zd b/docs/person.zd
new file mode 100644
index 0000000..090cfd5
--- /dev/null
+++ b/docs/person.zd
@@ -0,0 +1,27 @@
+
+:zd/type zd.class
+:zd/require #{ :person/email }
+:zd/summary [:person/email :person/telegram ]
+:title "Person"
+:icon [:fa-duotone :fa-person]
+:desc /
+
+:menu-order 102
+
+:persons ?/
+
+e :zd/type #person
+m :org/organization o
+m :org/person e
+m :zd/type #org.Member
+
+> m:org/organization
+> e
+> m:org/role
+> m:org/memberSince
+
+
+&email zd.prop
+:zd/data-type zd.string
+:desc /
+Email of person
\ No newline at end of file
diff --git a/docs/person/andrew.zd b/docs/person/andrew.zd
new file mode 100644
index 0000000..9032fc0
--- /dev/null
+++ b/docs/person/andrew.zd
@@ -0,0 +1,4 @@
+:zd/type person
+
+:person/email "email"
+:title "Andrew X"
\ No newline at end of file
diff --git a/docs/person/ivan.zd b/docs/person/ivan.zd
new file mode 100644
index 0000000..4c2de8d
--- /dev/null
+++ b/docs/person/ivan.zd
@@ -0,0 +1,3 @@
+:zd/type person
+:person/email "ivan@hs.io"
+:title "Ivan"
\ No newline at end of file
diff --git a/docs/person/niquola.zd b/docs/person/niquola.zd
new file mode 100644
index 0000000..d7729d0
--- /dev/null
+++ b/docs/person/niquola.zd
@@ -0,0 +1,24 @@
+
+:zd/type person
+:foaf.based_near #{ country.Portugal city.Lisboa city.Estoril}
+:person/email "niquola@gmail.com"
+:person/telegram "niquola"
+:person/organization orgs.health-samurai
+:title "Nikolai Ryzhikov"
+
+
+:desc /
+
+&hs-emp prod.org.employee
+:emp/since #inst"2001"
+:emp/organization orgs.health-samurai
+:emp/role prod.org.CTO
+:emp/person .
+
+& tasks
+:tasks/assignee person.tim
+:tasks/severity tasks.critical
+:title "Finish Profile"
+:desc /
+
+Complete Profile for Nikolai
\ No newline at end of file
diff --git a/docs/person/tim.zd b/docs/person/tim.zd
new file mode 100644
index 0000000..50e8b36
--- /dev/null
+++ b/docs/person/tim.zd
@@ -0,0 +1,4 @@
+:zd/type person
+:person/email "time@hs.io"
+:title "Tim"
+:desc /
\ No newline at end of file
diff --git a/docs/prod.zd b/docs/prod.zd
new file mode 100644
index 0000000..20b0b5a
--- /dev/null
+++ b/docs/prod.zd
@@ -0,0 +1 @@
+:title "Product ontology"
\ No newline at end of file
diff --git a/docs/prod/Case.zd b/docs/prod/Case.zd
new file mode 100644
index 0000000..eccc694
--- /dev/null
+++ b/docs/prod/Case.zd
@@ -0,0 +1,13 @@
+:zd/type zd.class
+:zd/icon [:fa-duotone :fa-suitcase]
+:zd/summary [:zd/parent :need :desc]
+:zd/require #{}
+:title "Case"
+:desc /
+
+
+&severty zd.prop
+:zd/data-type zd.symbol
+
+&critical prod.Case.severty
+:title "Critical"
\ No newline at end of file
diff --git a/docs/prod/EHR-Vendor.zd b/docs/prod/EHR-Vendor.zd
new file mode 100644
index 0000000..8b2e83a
--- /dev/null
+++ b/docs/prod/EHR-Vendor.zd
@@ -0,0 +1 @@
+:title "EHR Vendor"
\ No newline at end of file
diff --git a/docs/prod/Partner.zd b/docs/prod/Partner.zd
new file mode 100644
index 0000000..baab160
--- /dev/null
+++ b/docs/prod/Partner.zd
@@ -0,0 +1,3 @@
+:title "Partner"
+:icon [:fa-duotone :fa-handshake]
+:desc /
\ No newline at end of file
diff --git a/docs/prod/org.zd b/docs/prod/org.zd
new file mode 100644
index 0000000..6e7d42f
--- /dev/null
+++ b/docs/prod/org.zd
@@ -0,0 +1,46 @@
+:zd/type zd.class
+:zd/require #{:title :prod.org/kind :prod.org/linkedin }
+:zd/summary [:prod.org/kind :prod.org/number-of-employees]
+:title "Organization"
+:desc /
+Class for organization
+
+
+&site zd.prop
+:zd/annotation zd.link-badge
+
+&location zd.prop
+:zd/annotation zd.badge
+
+&linkedin zd.prop
+:zd/annotation zd.link-badge
+
+&tags zd.prop
+:zd/annotation zd.badge
+:zd/data-type zd.symbol
+
+&number-of-employees zd.prop
+:zd/data-type zd.int
+
+&kind zd.prop
+:title "Type for tags"
+
+&vendor zd.class
+:title "Vendor"
+
+&ehr-vendor zd.class
+:title "EHR Vendor"
+:zd/subclass-of prod.org.vendor
+
+
+&cloud zd.class
+:title "Cloud"
+
+&partner zd.class
+:zd/require #{:organizations :partner-since}
+
+&employee zd.class
+:zd/require #{:emp/organization :emp/person :emp/role}
+
+&CTO zd.class
+:title "CTO"
\ No newline at end of file
diff --git a/docs/queries.zd b/docs/queries.zd
new file mode 100644
index 0000000..a6159e5
--- /dev/null
+++ b/docs/queries.zd
@@ -0,0 +1,61 @@
+:icon [:fa-brands :fa-searchengin]
+:menu-order 10
+:title "Queries"
+:desc /
+
+You can query documents in knowledge base using datalog dialect
+
+```zd
+:q ?/
+e :zd/type #music.song
+e :music/written-by m
+> e | Song
+> m | Artist
+```
+
+This query may be read as:
+* Find me all `e` such that `:zd/type = music.song`
+* Join all `m` referenced by `e :music/written-by`
+* first column reference to `e` with title "Song"
+* second column reference to `m` with title "Artist"
+
+```zd
+:q ?/
+e :zd/type #music.song
+e :music/written-by m
+> m | Artist
+> (count e) | Number of songs
+< desc (count e)
+```
+
+
+
+
+
+:query ?/
+e :zd/parent #person
+> e
+
+:songs ?/
+
+e :zd/type #music.song
+> e
+
+:items ?/
+
+e :type :needs
+e :value v
+
+> (min v)
+> (max v)
+> (avg v)
+> (count v)
+
+
+&need-1
+:type :needs
+:value 5
+
+&need-2
+:type :needs
+:value 4
\ No newline at end of file
diff --git a/docs/rdf.zd b/docs/rdf.zd
new file mode 100644
index 0000000..0ddc072
--- /dev/null
+++ b/docs/rdf.zd
@@ -0,0 +1,9 @@
+:title "rdf"
+:desc /
+RDF schema
+
+&Property
+:rdf.type rdf.Property
+
+&type
+:rdf.type rdf.Property
\ No newline at end of file
diff --git a/customers-x/sample.yaml b/docs/sample.yaml
similarity index 100%
rename from customers-x/sample.yaml
rename to docs/sample.yaml
diff --git a/docs/songs.zd b/docs/songs.zd
new file mode 100644
index 0000000..5ed2cdc
--- /dev/null
+++ b/docs/songs.zd
@@ -0,0 +1,14 @@
+:zd/menu-order 202
+:zd/child-type music.song
+:title "Songs"
+
+
+
+
+
+&sub zd.class
+:attr "value"
+
+&&&
+
+:other-key "?"
\ No newline at end of file
diff --git a/docs/songs/imagine.zd b/docs/songs/imagine.zd
new file mode 100644
index 0000000..2edbf7e
--- /dev/null
+++ b/docs/songs/imagine.zd
@@ -0,0 +1,6 @@
+:zd/type music.song
+
+:music/song-name "Imagine"
+:music/written-by artists.john-lenon
+:title "Imagine"
+:date #inst"1976"
\ No newline at end of file
diff --git a/docs/songs/moon-sonate.zd b/docs/songs/moon-sonate.zd
new file mode 100644
index 0000000..5149009
--- /dev/null
+++ b/docs/songs/moon-sonate.zd
@@ -0,0 +1,6 @@
+:zd/type music.song
+
+:music/written-by artists.bethoven
+:title "Moon Sonate"
+:date #inst"1750"
+:music/song-name "Moon Sonate"
\ No newline at end of file
diff --git a/docs/songs/sonet-5.zd b/docs/songs/sonet-5.zd
new file mode 100644
index 0000000..e2d33d2
--- /dev/null
+++ b/docs/songs/sonet-5.zd
@@ -0,0 +1,2 @@
+:attr "value"
+:title "Sonet 5"
\ No newline at end of file
diff --git a/docs/tasks.zd b/docs/tasks.zd
new file mode 100644
index 0000000..391835c
--- /dev/null
+++ b/docs/tasks.zd
@@ -0,0 +1,25 @@
+:zd/menu-order 100
+:zd/icon [:fa-regular :fa-clipboard-check]
+:zd/summary [:desc]
+:zd/type zd.class
+:title "Tasks"
+:desc /
+
+
+:table ?/
+
+e :zd/type #tasks
+
+> e
+> e:tasks/assignee
+> e:tasks/severity
+
+&severity zd.prop
+
+&assignee zd.prop
+
+&severity zd.class
+
+&critical tasks.severity
+:title "Critical"
+:zd/icon [:fa-regular :fa-engine-warning]
\ No newline at end of file
diff --git a/docs/tech.zd b/docs/tech.zd
new file mode 100644
index 0000000..e69de29
diff --git a/docs/tech/DataBase.zd b/docs/tech/DataBase.zd
new file mode 100644
index 0000000..7ae1c8b
--- /dev/null
+++ b/docs/tech/DataBase.zd
@@ -0,0 +1,2 @@
+:title "Data Base"
+:desc /
\ No newline at end of file
diff --git a/docs/tech/PostgreSQL.zd b/docs/tech/PostgreSQL.zd
new file mode 100644
index 0000000..f3f3b54
--- /dev/null
+++ b/docs/tech/PostgreSQL.zd
@@ -0,0 +1,4 @@
+^badge
+:type tech.DataBase
+:title "PostgreSQL"
+:desc /
\ No newline at end of file
diff --git a/docs/zendoc.edn b/docs/zendoc.edn
new file mode 100644
index 0000000..a774503
--- /dev/null
+++ b/docs/zendoc.edn
@@ -0,0 +1,3 @@
+{ns zendoc
+
+ }
diff --git a/docs/zentext.zd b/docs/zentext.zd
index 1df0449..e7320af 100644
--- a/docs/zentext.zd
+++ b/docs/zentext.zd
@@ -1,6 +1,58 @@
-:menu-order 3
-:title "zentext"
+:zd/menu-order 2
+:title "Zentext"
+:desc /
-:summary md/
+Zentext syntax is markdown-like syntax
-zentext format
+:links /
+
+Link to existing document can be done as `#doc.name`
+Link to external pages `[link](title)` or `[[a link title]]`
+Link to person - `@person.name`
+
+:lists /
+
+:images /
+
+
+:code /
+Examples of code hieghtlight:
+
+
+```code js
+var x = 1;
+function(x) {
+ return x + x;
+}
+
+````
+
+```code clj
+(def a 1)
+```
+
+```code edn
+{:a 1}
+```
+
+```zd
+:key value
+:anothor-key 100
+:desc /
+ Here is a text
+ Multiline #some
+```
+
+```http
+GET /Patient?param
+@header: header
+---
+@response-header: ....
+resourceType: Patient
+gender: male
+```
+
+```sql
+SELECT * FROM patient
+LIMIT 10
+```
\ No newline at end of file
diff --git a/legacy/.#components.clj b/legacy/.#components.clj
new file mode 120000
index 0000000..c2c927e
--- /dev/null
+++ b/legacy/.#components.clj
@@ -0,0 +1 @@
+niquola@pavels-MacBook-Pro-2.local.37542
\ No newline at end of file
diff --git a/legacy/api.clj b/legacy/api.clj
new file mode 100644
index 0000000..b131b79
--- /dev/null
+++ b/legacy/api.clj
@@ -0,0 +1,218 @@
+;; merge into core
+(ns zd.api
+ (:require
+ [clojure.pprint :as pprint]
+ [zd.layout :as layout]
+ [zd.memstore :as memstore]
+ [zd.meta :as meta]
+ [zd.datalog]
+ [hiccup.core :as hiccup]
+ [zd.reader :as reader]
+ [clojure.java.io :as io]
+ [clojure.string :as str]
+ [zen.core :as zen]
+ [zd.fs :as fs]
+ [zd.methods :as methods]
+ [zd.render :as render]
+ [zen-web.core :as web]
+ [zd.datalog :as datalog]
+ [zd.fs.utils :as futils]
+ [zd.gitsync :as gitsync])
+ (:import [java.io StringReader]))
+
+;; TODO move to zen-web.http
+(defn get-state [ztx]
+ (->> [:zen/state :http :state]
+ (get-in @ztx)))
+
+(defn zendoc-config [ztx]
+ (->> [:config :zendoc]
+ (get-in (get-state ztx))
+ (zen/get-symbol ztx)))
+
+;; ISSUE mw does not work for / request
+;; TODO remove this mw
+(defmethod web/middleware-in 'zd/append-doc
+ [ztx _cfg {{id :id} :route-params :as req} & opts]
+ (when (some? id)
+ ;; TODO make root required in render ops with zen sch
+ {:zd/root (:root (zendoc-config ztx))
+ :doc (memstore/get-doc ztx (symbol id))}))
+
+(defmethod zen/op 'zd/render-doc
+ [ztx config {{id :id} :route-params
+ uri :uri
+ hs :headers
+ doc :doc :as req} & opts]
+ (let [{r :root ps :paths :as config} (zendoc-config ztx)]
+ (cond
+ (= uri "/")
+ {:status 301
+ :headers {"Location" (str "/" r "?" (:query-string req))
+ "Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}}
+
+ (nil? doc)
+ {:status 301
+ :headers {"Location" (str "/" id "/edit" "?" (:query-string req))
+ "Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}}
+
+ (get hs "x-body")
+ {:status 200
+ :headers {"Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}
+ :body (hiccup/html (render/render-doc ztx {:request req :paths ps :doc doc :root r :config config} doc))}
+
+ :else
+ {:status 200
+ :headers {"Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}
+ :body (render/doc-view ztx {:request req :paths ps :doc doc :root r :config config} doc)})))
+
+(defmethod web/middleware-out 'zd/layout
+ [ztx config {page :page :as req} {bdy :body :as resp} & args]
+ (when (and (not (string? bdy)) (= 200 (:status resp)))
+ {:headers {"Content-Type" "text/html"}
+ :body (layout/sidebar ztx {:request req} bdy)}))
+
+(defmethod zen/op 'zd/render-widget
+ [ztx _cfg {{id :id wgt :widget-id} :route-params r :root :keys [doc] :as req} & opts]
+ (if-not (nil? doc)
+ {:status 200
+ :body (methods/widget ztx {:widget (keyword wgt) :root r :request req} doc)}
+ {:status 200
+ :body [:div "Error: " id " is not found"]}))
+
+;; TODO: add check for errors endpoint
+(defmethod zen/op 'zd/errors-page
+ [ztx _cfg req & _opts]
+ (let [{r :root ps :paths :as config} (zendoc-config ztx)
+ doc {:zd/name '_errors
+ :title "Errors"
+ :zd/readonly true
+ :zd/meta {:doc [:title :errors-view]}
+ :errors-view true}]
+ (if (get-in req [:headers "x-body"])
+ {:status 200
+ :headers {"Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}
+ :body (render/render-doc ztx {:request req :paths ps :doc doc :root r :config config} doc)}
+ {:status 200
+ :headers {"Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}
+ :body (render/doc-view ztx {:request req :paths ps :doc doc :root r :config config} doc)})))
+
+(defn extract-id [lines]
+ (-> (->> lines
+ (filter #(or
+ (str/starts-with? % ":zd/rename") ;; OBSOLETE
+ (str/starts-with? % ":zd/docname")))
+ (first))
+ (str/trim)
+ (str/split #"\s+")
+ (second)))
+
+(defn name-to-dir [pths docname]
+ (->> (str/split docname #"\.")
+ butlast
+ (str/join "/")
+ (str (first pths) "/")))
+
+;; TODO remove all subdocs
+;; (when rename
+;; (symbol? rename)
+;; (fs/delete-doc ztx docname)
+;; (datalog/evict ztx (str docname)))
+;; (zen/pub ztx 'zd.events/fs-save {:docname docname :rename rename})
+;; docname (if (symbol? rename) (str rename) (str docname))
+
+(defn delete-doc [ztx docname]
+ (let [{pths :paths} (fs/get-state ztx)
+ filepath (str (first pths) "/" (futils/docpath docname))
+ docname-sym (symbol docname)
+ doc (memstore/get-doc ztx docname-sym)]
+ (when-let [subdocs (:zd/subs doc)]
+ (->> subdocs (mapv (fn [s] (delete-doc ztx s)))))
+ (swap! ztx update :zdb dissoc docname-sym)
+ (when (.exists (io/file filepath))
+ (io/delete-file filepath))
+ (memstore/remove-links ztx docname-sym)
+ (datalog/evict ztx (str "'" docname))))
+
+;; todo validate links
+(defn save-doc [ztx docname content]
+ (let [{root :root pths :paths} (fs/get-state ztx)
+ docpath (futils/docpath docname)
+ filepath (str (first pths) "/" docpath)
+ dirname (futils/name-to-dir pths docname)
+ resource-path docpath
+ docname-sym (symbol docname)
+ doc (memstore/get-doc ztx docname-sym)
+ subdocs (:zd/subs doc)
+ docs (memstore/read-docs ztx {:path filepath :root root :resource-path resource-path :content content})
+ docs-idx (group-by :zd/docname docs)]
+ (.mkdirs (io/file dirname))
+ (spit filepath content)
+ (->> docs (mapv (fn [doc]
+ (memstore/remove-links ztx (:zd/docname doc))
+ (memstore/put-doc ztx doc)
+ (let [idoc (memstore/infere-doc ztx (:zd/docname doc))]
+ (datalog/save-doc ztx idoc)))))
+ (->> subdocs
+ (mapv (fn [subdoc]
+ (when-not (contains? docs-idx subdoc)
+ (delete-doc ztx subdoc)))))))
+
+
+(defn get-lines [s]
+ (->> s
+ (StringReader.)
+ (io/reader)
+ (line-seq)))
+
+(defn cleanup-doc [lines]
+ (->> lines
+ (remove #(str/starts-with? % ":zd/docname"))
+ (remove #(str/starts-with? % ":zd/rename"))
+ (str/join "\n")))
+
+(defmethod zen/op 'zd/save-doc
+ [ztx _cfg {{id :id} :route-params r :zd/root :as req} & opts]
+ (let [lines (get-lines (slurp (:body req)))
+ content (cleanup-doc lines)
+ new-id (extract-id lines)
+ rename-to (when (and new-id (not (= new-id id))) (symbol new-id))]
+ (if rename-to
+ (do
+ (delete-doc ztx id)
+ (save-doc ztx new-id content))
+ (save-doc ztx id content))
+ {:status 200 :body (str "/" (or rename-to id))}))
+
+(defn parent-link [id]
+ (let [parts (seq (butlast (str/split id #"\.")))]
+ (str "/" (str/join "." parts))))
+
+(defmethod zen/op 'zd/delete-doc
+ [ztx _cfg {{:keys [id]} :route-params :as req} & opts]
+ (let [{r :root} (zendoc-config ztx)]
+ (delete-doc ztx id)
+ {:status 200 :body (parent-link id)}))
+
+(defmethod zen/op 'zd/render-editor
+ [ztx _cfg {{id :id} :route-params :as req} & opts]
+ (let [doc (or (:doc req) {:zd/meta {:docname (symbol id)}})
+ {r :root ps :paths :as config} (zendoc-config ztx)]
+ {:status 200
+ :body (render/editor ztx {:root r :paths ps :request req :doc doc :config config} doc)}))
+
+(defmethod zen/op 'zd/render-preview
+ [ztx _ {{id :id} :route-params :as req} & opts]
+ (let [{r :root ps :paths :as config} (zendoc-config ztx)]
+ {:headers {"Content-Type" "text/html"}
+ :body (-> (render/preview ztx {:request req :paths ps :config config :root r} (slurp (:body req)))
+ (hiccup/html))
+ :status 200}))
+
+(defmethod zen/op 'zd.events/logger
+ [ztx config {ev-name :ev :as ev} & opts]
+ ;; TODO filter out
+ (when-not (or (= ev-name 'zd.events/on-doc-save)
+ (= ev-name 'zd.events/on-doc-load))
+ ;; TODO do not print large events
+ (println (assoc ev ::ts (str (java.util.Date.))))))
diff --git a/test/zd/api/backlinks_test.clj b/legacy/api/backlinks_test.clj
similarity index 100%
rename from test/zd/api/backlinks_test.clj
rename to legacy/api/backlinks_test.clj
diff --git a/test/zd/api/templates_test.clj b/legacy/api/templates_test.clj
similarity index 100%
rename from test/zd/api/templates_test.clj
rename to legacy/api/templates_test.clj
diff --git a/test/zd/api/validation_test.clj b/legacy/api/validation_test.clj
similarity index 100%
rename from test/zd/api/validation_test.clj
rename to legacy/api/validation_test.clj
diff --git a/legacy/api_test.clj b/legacy/api_test.clj
new file mode 100644
index 0000000..0c119e7
--- /dev/null
+++ b/legacy/api_test.clj
@@ -0,0 +1,325 @@
+(ns zd.api-test
+ (:require
+ [zd.api]
+ [clojure.string :as str]
+ [matcho.core :as matcho]
+ [clojure.test :refer [deftest is testing]]
+ [zd.test-utils :as t]))
+
+
+
+(deftest test-api
+
+ (t/reset-project {'default (t/zd [:title "Default"])
+ 'other (t/zd [:title "Other"]
+ [:link 'default])})
+
+ (testing "initila load"
+ (t/http-match
+ {:uri "/default"}
+ {:status 200})
+
+ (:zrefs @@t/ctx)
+
+ (is (t/get-doc 'default))
+
+ (matcho/match (t/query '{:find [t] :where [[e :xt/id "'default"] [e :title t]]}) #{["Default"]}))
+
+ (testing "when document not found redirects to editor"
+ (t/http-match
+ {:uri "/testdoc"}
+ {:status 301 :headers {"Location" "/testdoc/edit?"}}))
+
+ (testing "editor config is rendered"
+ (t/http-match
+ {:uri "/testdoc/edit"}
+ {:status 200}))
+
+ (is (empty? (t/query '{:find [e] :where [[e :xt/id "'testdoc"]]})))
+
+ ;; we are not throwing errors we save them
+ (testing "saving document"
+ (t/http-match
+ {:uri "/testdoc/edit"
+ :request-method :put
+ :body (t/zd [:zd/docname 'testdoc]
+ [:title "testdoc"]
+ [:tags #{}]
+ [:desc "some"]
+ [:link 'default]
+ [:otherlink 'other]
+ ['&subdoc1]
+ [:title "subdoc1"]
+ ['&subdoc2]
+ [:title "subdoc2"])}
+ {:status 200})
+
+ (:zrefs @@t/ctx)
+
+ (t/match-doc 'testdoc {:title "testdoc"})
+ (t/match-doc 'testdoc.subdoc1 {:title "subdoc1"})
+ (t/match-doc 'testdoc.subdoc2 {:title "subdoc2"})
+
+ (t/query '{:find [e] :where [[e :xt/id id]]})
+
+ (is (seq (t/query '{:find [e] :where [[e :xt/id "'testdoc"]]})))
+ (is (seq (t/query '{:find [e] :where [[e :xt/id "'testdoc.subdoc1"]]})))
+ (is (seq (t/query '{:find [e] :where [[e :xt/id "'testdoc.subdoc2"]]})))
+
+ (t/match-doc 'default {:zd/backlinks {'testdoc #{[:link]}}})
+ (t/match-doc 'other {:zd/backlinks {'testdoc #{[:otherlink]}}})
+
+ ;; now lets edit
+ ;; remove :link, change fields, and change subentities
+
+ (t/http-match
+ {:uri "/testdoc/edit"
+ :request-method :put
+ :body (t/zd [:zd/docname 'testdoc]
+ [:title "testdoc-change"]
+ [:tags #{}]
+ [:desc "some"]
+ [:otherlink 'other]
+ ['&subdoc1]
+ [:title "subdoc1-change"]
+ ['&subdoc3]
+ [:title "subdoc3"])}
+ {:status 200})
+
+ (matcho/match (t/get-doc 'testdoc) {:title "testdoc-change"})
+ (matcho/match (t/get-doc 'testdoc.subdoc1) {:title "subdoc1-change"})
+ (matcho/match (is (nil? (t/get-doc 'testdoc.subdoc2))))
+ (matcho/match (t/get-doc 'testdoc.subdoc3) {:title "subdoc3"})
+
+
+ (t/query '{:find [p] :where
+ [[e :xt/id "'testdoc"]
+ [p :parent e]
+ [p :zd/subdoc true]]})
+
+
+ (matcho/match
+ (t/query '{:find [t] :where [[e :xt/id "'testdoc.subdoc1"] [e :title t]]})
+ #{["subdoc1-change"]})
+
+ (is (empty?
+ (t/query '{:find [t] :where [[e :xt/id "'testdoc.subdoc2"] [e :title t]]})))
+
+ (matcho/match
+ (t/query '{:find [t] :where [[e :xt/id "'testdoc.subdoc3"] [e :title t]]})
+ #{["subdoc3"]})
+
+ (matcho/match (t/get-doc 'default)
+ {:zd/backlinks {'testdoc nil?}})
+
+ (matcho/match (t/get-doc 'other)
+ {:zd/backlinks {'testdoc #{[:otherlink]}}})
+
+ ;; TODO: tests for gitsync
+
+ )
+
+ (testing "delete document"
+ (t/http-match
+ {:uri "/testdoc" :request-method :delete}
+ {:status 200 })
+
+ (is (nil? (t/get-doc 'testdoc)))
+
+ (is (empty? (t/query '{:find [e] :where [[e :xt/id "'testdoc"]]})))
+
+ (t/http-match
+ {:uri "/testdoc"}
+ {:status 301 :headers {"Location" "/testdoc/edit?"}})
+
+
+ (is (empty? (t/query '{:find [t] :where [[e :xt/id "'testdoc.subdoc1"] [e :title t]]})))
+ (is (empty? (t/query '{:find [t] :where [[e :xt/id "'testdoc.subdoc3"] [e :title t]]})))
+
+ (t/match-doc 'default {:zd/backlinks {'testdoc nil?}})
+ (t/match-doc 'other {:zd/backlinks nil?})
+ )
+
+ (testing "rename document"
+
+ (t/http-match
+ {:uri "/testdoc/edit"
+ :request-method :put
+ :body ":zd/docname testdoc\n:title \"testdoc\"\n:tags #{}\n:desc /"}
+ {:status 200})
+
+ (t/get-doc 'testdoc)
+
+ (matcho/match (t/get-doc 'testdoc) {:title "testdoc"})
+
+
+ (t/http-match
+ {:uri "/testdoc/edit"
+ :request-method :put
+ :body ":zd/docname newname\n:title \"testdoc\"\n:tags #{}\n:desc /"}
+ {:status 200})
+
+
+ (is (nil? (t/get-doc 'testdoc)))
+ (is (empty? (t/query '{:find [e] :where [[e :xt/id "'testdoc"]]})))
+
+ (is (t/get-doc 'newname))
+ (is (seq (t/query '{:find [e] :where [[e :xt/id "'newname"]]})))
+
+ (t/http-match
+ {:uri "/testdoc"}
+ {:status 301
+ :headers {"Location" "/testdoc/edit?"}})
+
+
+ )
+
+ (testing "validation"
+ (t/http-match
+ {:uri "/org/edit"
+ :request-method :put
+ :body (t/zd [:zd/docname 'org]
+ [:title "Organization"]
+ [:zd/type 'zd.class]
+ [:zd/require #{:title :org/prop}]
+ ['&prop]
+ [:zd/type 'zd.prop]
+ [:zd/data-type 'zd.string])}
+ {:status 200})
+
+ (t/http-match
+ {:uri "/org.o1/edit"
+ :request-method :put
+ :body (t/zd [:zd/docname 'org.o1]
+ [:zd/type 'org]
+ [:title "O1"])}
+ {:status 200})
+
+ (t/get-doc 'org)
+
+ (t/match-doc 'org.o1 {:zd/errors [{:path [:org/prop]} nil?]})
+
+ (def resp (t/http-match {:uri "/org.o1"} {:status 200}))
+
+ (->
+ (t/hiccup-find resp "doc-errors")
+ (t/hiccup-text "[:org/prop]")
+ is)
+
+ (t/http-match
+ {:uri "/org.o1/edit"
+ :request-method :put
+ :body (t/zd [:zd/docname 'org.o1]
+ [:zd/type 'org]
+ [:title "O1"]
+ [:org/prop "value"])}
+ {:status 200})
+
+ (t/match-doc 'org.o1 {:zd/errors nil?})
+
+ (-> (t/hiccup-find resp "doc-errors") (t/hiccup-text "[:org/prop]") is not)
+
+
+ (t/http-match
+ {:uri "/org.o1/edit"
+ :request-method :put
+ :body (t/zd [:zd/docname 'org.o1]
+ [:zd/type 'org]
+ [:title "O1"]
+ [:org/prop "value"]
+ [:borken-link 'ups])}
+ {:status 200})
+
+ (t/match-doc 'org.o1 {:zd/errors [{:message "could not find 'ups"}]})
+
+ (t/http-match {:uri "/_errors"} {:status 200})
+ (t/all-errors)
+
+ )
+
+ )
+
+(deftest document-rename)
+
+;; (deftest document-rename
+
+;; (zen/stop-system ztx)
+
+;; (zen/read-ns ztx 'zd)
+
+;; (zen/read-ns ztx 'zd.test)
+
+;; (zen/start-system ztx 'zd.test/system)
+
+;; (def to-rename
+;; ;; TODO for some reason whitespace is required after :desc /\n
+;; ":zd/docname testdoc\n:title \"testdoc\"\n:tags #{}\n:desc /\n #testdoc2 link")
+
+;; (def to-link
+;; ":zd/docname testdoc2\n:title \"testdoc2\"\n:tags #{}\n:desc /")
+
+;; (matcho/assert
+;; {:status 200 :body string?}
+;; (web/handle ztx 'zd/api
+;; {:uri "/testdoc/edit"
+;; :request-method :put
+;; :body (t/req-body to-link)}))
+
+;; (is (seq (t/read-doc "testdoc2.zd")))
+
+;; (matcho/assert
+;; {:status 200 :body string?}
+;; (web/handle ztx 'zd/api
+;; {:uri "/testdoc/edit"
+;; :request-method :put
+;; :body (t/req-body to-rename)}))
+
+;; (is (not (str/blank? (t/read-doc "testdoc.zd"))))
+
+;; (def renamed
+;; (str to-rename "\n:zd/rename newdoc"))
+
+;; ;; old file deleted, new added
+;; ;; links are re calculated
+
+;; (matcho/assert
+;; {:status 200 :body "/newdoc"}
+;; (web/handle ztx 'zd/api
+;; {:uri "/testdoc/edit"
+;; :request-method :put
+;; :body (t/req-body renamed)}))
+
+;; ;; TODO think checkpoint api call
+;; (await fs/queue)
+;; (xtdb/sync (:node (d/get-state ztx)))
+
+;; (testing "on rename old file is deleted"
+;; (is (nil? (t/read-doc "testdoc.zd")))
+;; (is (not (str/blank? (t/read-doc "newdoc.zd")))))
+
+;; (testing "backlinks are reloaded"
+;; ;; TODO use designated api op
+;; (matcho/assert
+;; {:zd/meta {:backlinks #{'{:to testdoc2, :path [:desc], :doc newdoc}}}}
+;; (memstore/get-doc ztx 'testdoc2)))
+
+;; (testing "old file is removed from storage"
+;; ;; TODO use designated api op
+;; (is (empty? (zen/op-call ztx 'zd/query '{:find [?e]
+;; :where [[?e :xt/id ?id]
+;; [(= ?id "testdoc")]]}))))
+;; ;; cleanup
+
+;; (matcho/assert
+;; {:status 200 :body string?}
+;; (web/handle ztx 'zd/api {:uri "/newdoc"
+;; :request-method :delete}))
+
+;; ;; TODO for some reason root is nil
+;; (matcho/assert
+;; {:status 200 :body string?}
+;; (web/handle ztx 'zd/api {:uri "/testdoc2"
+;; :request-method :delete}))
+
+;; (is (nil? (t/read-doc "testdoc2.zd")))
+;; (is (nil? (t/read-doc "newdoc.zd"))))
diff --git a/src/zd/blocks.clj b/legacy/blocks.clj
similarity index 100%
rename from src/zd/blocks.clj
rename to legacy/blocks.clj
diff --git a/src/zd/blocks/anns.clj b/legacy/blocks/anns.clj
similarity index 100%
rename from src/zd/blocks/anns.clj
rename to legacy/blocks/anns.clj
diff --git a/src/zd/blocks/content.clj b/legacy/blocks/content.clj
similarity index 89%
rename from src/zd/blocks/content.clj
rename to legacy/blocks/content.clj
index ac4c28f..420fdc6 100644
--- a/src/zd/blocks/content.clj
+++ b/legacy/blocks/content.clj
@@ -59,25 +59,31 @@
(set? v) [:div {:class (c :flex :items-center [:space-x 2])}
(->> v (map (fn [x] [:div {:key (str x)} (render-table-value ztx x block)])))]
(keyword? v) (str v)
+ (inst? v) (str/replace (str v) #"00:00:00 " "")
:else (with-out-str (clojure.pprint/pprint v))))
(defmethod methods/rendercontent :?
- [ztx ctx {{headers :table-of} :ann data :data :as block}]
+ [ztx _ctx {data :data :as block}]
(let [{res :result cols :columns q :query} (d/sugar-query ztx data)]
[:div
- [:details {:class (c :text-xs [:mb 2])}
- [:summary {:class (c [:text :gray-500]) }"query"]
- [:pre {:class (c :border [:p 2] [:bg :gray-100])}
- (with-out-str (clojure.pprint/pprint (dissoc q :columns :index :args)))]]
[:table
(into [:thead]
- (->> cols (map (fn [col] [:th {:class (c [:py 1] [:bg :gray-100] :border {:font-weight "500"})} (str col)]))))
+ (->> cols
+ (map (fn [col]
+ [:th {:class (c [:py 1] [:px 2] [:bg :gray-100] :border {:font-weight "500"})}
+ (cond (keyword? col) (name col)
+ (nil? col) "~"
+ :else (str col))]))))
(into [:tbody]
(for [vs res]
[:tr {:key (hash vs)}
(for [v vs]
[:td {:key v :class (c :border [:px 2] [:py 1] {:vertical-align "top"})}
- (render-table-value ztx v block)])]))]]))
+ (render-table-value ztx v block)])]))]
+ [:details {:class (c :text-xs [:mt 0.5])}
+ [:summary {:class (c [:text :gray-500]) }"query"]
+ [:pre {:class (c :border [:p 2] [:bg :gray-100])}
+ (with-out-str (clojure.pprint/pprint (dissoc q :columns :index :args)))]]]))
(defmethod methods/rendercontent :mm
[ztx ctx {d :data :as block}]
diff --git a/src/zd/blocks/keys.clj b/legacy/blocks/keys.clj
similarity index 53%
rename from src/zd/blocks/keys.clj
rename to legacy/blocks/keys.clj
index 25ede49..9c9ef1f 100644
--- a/src/zd/blocks/keys.clj
+++ b/legacy/blocks/keys.clj
@@ -1,9 +1,25 @@
(ns zd.blocks.keys
(:require
[zd.methods :as methods]
+ [zd.link :as link]
+ [zd.memstore]
[clojure.string :as str]
[stylo.core :refer [c]]))
+
+(defmethod methods/renderkey :errors-view
+ [ztx ctx {d :data :as block}]
+ [:div {:class (c)}
+ (for [[docname errors] (->> (zd.memstore/get-all-errors ztx)
+ (sort-by #(str (first %))))]
+ [:div {:class (c [:py 1] [:mb 1])}
+ [:div {:class (c [:py 1] :border-b [:mb 1])} (str docname) " > "(link/symbol-link ztx docname)]
+ (for [e errors]
+ [:div {:class (c :flex [:space-x 3] :borer-b :items-baseline)}
+ [:div "⚬ " (str (:type e))]
+ [:div (str (:path e))]
+ [:div (:message e)]])])])
+
;; TODO use link badge for linkedin prop?
(defmethod methods/renderkey :linkedin
[ztx {{m :zd/meta} :doc} {data :data :as block}]
@@ -21,13 +37,11 @@
[:i.fa-brands.fa-linkedin]]))
(defmethod methods/renderkey :title
- [ztx {doc :doc} {title :data :as block}]
- [:h1 {:class (c :flex :items-center [:m 0] [:py 4]) :id "title"}
+ [ztx {doc :doc} {title :data}]
+ [:h1 {:class (c :flex :items-center [:m 0] [:pt 2] [:pb 1] [:border-b :gray-400] [:mb 4] :text-xl) :id "title"}
(if-let [img (or (:avatar doc) (:logo doc))]
[:img {:src img
:class (c [:w 8] [:h 8] :inline-block [:mr 2] {:border-radius "100%"})}]
- (when-let [icon (:icon doc)]
- [:i {:class (str (str/join " " (map name icon))
- " "
- (name (c [:mr 2] [:text :gray-600])))}]))
+ (when-let [icon (or (:icon doc) (:zd/icon doc))]
+ [:i {:class (str (str/join " " (map name icon)) " " (name (c [:mr 2] [:text :gray-600])))}]))
title])
diff --git a/src/zd/blocks/timeline.clj b/legacy/blocks/timeline.clj
similarity index 100%
rename from src/zd/blocks/timeline.clj
rename to legacy/blocks/timeline.clj
diff --git a/src/zd/blocks/zd.clj b/legacy/blocks/zd.clj
similarity index 69%
rename from src/zd/blocks/zd.clj
rename to legacy/blocks/zd.clj
index a5f3776..d3eaf3e 100644
--- a/src/zd/blocks/zd.clj
+++ b/legacy/blocks/zd.clj
@@ -30,18 +30,14 @@
(defmethod methods/renderkey :zd/index
[ztx ctx block]
(let [docs (->> '{:find [?docname]
- :where [[?e :meta/docname ?docname]
+ :where [[?e :xt/id ?docname]
[?e :title ?title]]
:order-by [[?docname :asc]]}
(d/query ztx)
- (map (fn [v] {:s (first v)
- :ps (str/split (str (first v)) #"\.")}))
+ (mapv (fn [v] {:s (first v) :ps (str/split (str (first v)) #"\.")}))
(partition-by #(= 1 (count (:ps %))))
(partition 2)
- (map (fn [[h t]]
- (if (> (count t) 20)
- h
- (concat h t)))))
+ (mapv (fn [[h t]] (if (> (count t) 20) h (concat h t)))))
total (reduce + (map count docs))
group-idx (loop [[[idx item] & oth] (map-indexed vector (map count docs))
left (int (/ total 2))]
@@ -87,54 +83,40 @@
;; TODO move this processing to memstore
links
(->> data
+ (mapcat (fn [[from paths]] (for [p paths] {:to dn :path p :doc from})))
(map (fn [{d :doc p :path t :to}]
{:to t
:doc d
:parent (when (str/includes? (str d) ".") (str/join "." (butlast (str/split (str d) #"\."))))
- :path (->> (map (fn [f] (if (keyword? f) (name f) (str f))) p)
- (str/join ".")
- (str ":"))}))
+ :path (->> (map (fn [f] (if (keyword? f) (name f) (str f))) p) (str/join ".") (str ":"))}))
(sort-by (juxt :parent :path :doc))
(group-by :parent)
- (map (fn [[p links]]
- [p (sort-by (fn [{d :doc}]
- (:title (memstore/get-doc ztx d)))
- links)])))]
+ (map (fn [[p links]] [p (sort-by (fn [{d :doc}] (:title (memstore/get-doc ztx d))) links)])))]
(for [[parent links] links]
(let [*parent (or parent r)]
[:div {:class (c [:py 4] #_[:text :gray-600])}
[:div {:class (c :flex :flex-row :items-center :border-b :justify-between [:py 1])}
[:div
- [:span {:class (c :text-sm)}
- [:i.fas.fa-regular.fa-link]]
- [:a {:id (str "backlinks-" *parent) :class (c :uppercase {:font-weight "600"})}
+ [:span {:class (c :text-sm [:mr 1])}
+ [:i.fa-solid.fa-arrow-up]]
+ [:a {:id (str "backlinks-" *parent)
+ :class (c :text-lg {:font-weight "600"})}
(str *parent ".*")]
[:span {:class (c [:pl 2] :text-sm [:text :gray-500])}
- (str/join ", " (set (map :path links)))]]
- [:a {:class (c :block [:p 1] :text-lg :cursor-pointer [:hover [:text :green-600]])
- :href (if (some? parent)
- (str parent "." "_draft/edit")
- "_draft/edit")}
- [:i.fas.fa-plus]]]
+ (str/join ", " (set (map :path links)))]]]
;; TODO think if path is needed in each link
- (for [{p :path docname :doc} (distinct (map #(dissoc % :path) links))]
- (let [{{anns :ann lu :last-updated} :zd/meta :as doc}
- (memstore/get-doc ztx (symbol docname))]
- [:div {:class (c [:pt 4] :flex :flex-col)}
- [:div {:class (c :inline-flex :items-center)}
- [:a {:href (str "/" docname)
- :class (c :inline-flex :items-center [:text "#4B5BA0"]
- [:hover [:underline]] :whitespace-no-wrap)}
- (:title doc)]
+ (for [{docname :doc} (distinct (map #(dissoc % :path) links))]
+ (let [{{anns :ann} :zd/meta :as doc} (memstore/get-doc ztx (symbol docname))]
+ [:div {:class (c [:py 1] :flex :border-b)}
+ [:div {:class (c :flex :flex-1)}
+ (link/symbol-link ztx docname {:force-icon true :icon-class (c {:min-width "1em"})})
[:div {:class (c :flex :self-center)}
(when (str/includes? (str docname) "_template")
[:span {:class (c :text-xs [:text :orange-500] [:pl 2])}
"_template"])
(when (str/includes? (str docname) "_schema")
[:span {:class (c :text-xs [:text :orange-500] [:pl 2])}
- "_schema"])
- #_[:div {:class (c [:text :gray-500])}
- "upd: " lu]]]
+ "_schema"])]]
[:div {:class (c :flex :flex-wrap :overflow-x-hidden)}
(doall
(for [[k v] (select-keys doc summary-keys)]
@@ -162,30 +144,18 @@
(str s))]]
[:span (pr-str s)]))
v)))
- ;; render single link
(symbol? v)
(link/symbol-link ztx v)
- #_(let [res (memstore/get-doc ztx v)]
- [:a {:href (str "/" v)
- :class (c :inline-flex
- :items-center
- [:hover [:text :blue-600] :underline]
- :whitespace-no-wrap
- {:text-decoration-thickness "0.5px"})}
- [:span (:title res)]])
-
:else [:span (pr-str v)])])))]]))]))))
+;; TODO: obsolete
(defmethod methods/renderkey :zd/errors
[ztx ctx {errors :data k :key :as block}]
- [:div {:class (c [:text :red-700] [:p 4] [:my 4] :rounded
- {:background-color "#fff5f5"})}
- [:ul {:class (c :font-bold :text-lg [:mb 2] [:ml 0] {:color "#e53e3e"})}
- "Document errors"]
+ [:div {:class (c [:text :red-700] [:my 2] :rounded :text-sm [:border :red-300])}
+ [:ul {:class (c :font-bold [:mb 1] [:py 1] [:px 3] [:ml 0] [:text :red-600] [:bg :red-100] [:border-b :red-300])} "Document errors"]
(for [err (sort-by :type errors)]
- [:li {:class (c [:mb 1] [:py 1] :flex [:space-x 3] [:text :gray-600])}
- [:span (->> (:path err)
- (map (fn [p] (if (keyword? p) (name p) (str p))))
- (str/join ".")
- (str ":"))]
+ [:li {:class (c [:py 0.5] :flex [:space-x 3] [:text :red-600] [:px 3])}
+ [:span (pr-str (:path err))]
[:span {:class (c [:ml 4] {:text-align "right"})} (:message err)]])])
+
+
diff --git a/src/zd/blocks/zentext.clj b/legacy/blocks/zentext.clj
similarity index 100%
rename from src/zd/blocks/zentext.clj
rename to legacy/blocks/zentext.clj
diff --git a/src/zd/components.clj b/legacy/components.clj
similarity index 100%
rename from src/zd/components.clj
rename to legacy/components.clj
diff --git a/legacy/datalog.clj b/legacy/datalog.clj
new file mode 100644
index 0000000..a985856
--- /dev/null
+++ b/legacy/datalog.clj
@@ -0,0 +1,185 @@
+(ns zd.datalog
+ (:require [zen.core :as zen]
+ [clojure.walk]
+ [clojure.string :as str]
+ [edamame.core]
+ [xtdb.api :as xt]))
+
+(defn get-state [ztx]
+ (get-in @ztx [:zen/state :datalog :state]))
+
+(defn submit [ztx data]
+ (if-let [{n :node} (get-state ztx)]
+ (let [res (xt/submit-tx n [[::xt/put data]])]
+ (xt/sync n)
+ res)
+ (do (println :no/xtdb)
+ :no/xtdb)))
+
+(defn evict [ztx data]
+ (if-let [{n :node} (get-state ztx)]
+ (do (xt/submit-tx n [[::xt/evict data]])
+ (xt/sync n))
+ :no/xtdb))
+
+
+(defn query [ztx query & params]
+ (if-let [{n :node} (get-state ztx)]
+ (clojure.walk/postwalk
+ (fn [x] (if (and (string? x) (str/starts-with? x "'"))
+ (symbol (subs x 1))
+ x))
+ (apply xt/q (xt/db n) query params))
+ :no/xtdb))
+
+(defn evict-by-query [ztx q]
+ (doseq [res (query ztx q)]
+ (evict ztx (str "'" (first res))))
+ (xt/sync (:node (get-state ztx))))
+
+
+(defn flatten-doc [ztx {{dn :docname :as m} :zd/meta :as doc}]
+ (let [meta (->> m
+ (map (fn [[k v]] [(keyword "zd" (name k)) v]))
+ (into {}))
+ doc (-> (dissoc doc :zd/backlinks :zd/subdocs :zd/meta)
+ (merge meta)
+ (assoc :xt/id (str "'" (:docname m))))]
+ (clojure.walk/postwalk (fn [x] (if (symbol? x) (str "'" x) x)) doc)))
+
+;; TODO rename to zd.datalog
+(defmethod zen/op 'zd/query
+ [ztx config params & [session]]
+ (query ztx params))
+
+(defmethod zen/op 'zd/submit
+ [ztx _config params & [_session]]
+ (submit ztx params))
+
+(defn save-doc [ztx doc]
+ (submit ztx (flatten-doc ztx doc)))
+
+(defmethod zen/op 'zd.events/datalog-sync
+ [ztx _config {_ev :ev doc :params} & [_session]]
+ (let [xtdb-doc (flatten-doc ztx doc)
+ result (submit ztx xtdb-doc)]
+ ;; TODO und where does result go in pub/sub
+ (doseq [[k sd] (:zd/subdocs doc)]
+ (let [pid (get-in doc [:zd/meta :docname])
+ id (str "'" pid "." (name k))]
+ (submit ztx (assoc (flatten-doc ztx sd) :xt/id id :parent (str "'" pid) :zd/subdoc true))))
+ result))
+
+(defmethod zen/op 'zd.events/datalog-delete
+ [ztx _config {{dn :docname} :params} & [_session]]
+ (cond
+ (or (string? dn) (symbol? dn)) (evict ztx (str dn))))
+
+(defmethod zen/start 'zd.engines/datalog
+ [ztx {zd-config :zendoc :as config} & opts]
+ (let [{r :root} (zen/get-symbol ztx zd-config)]
+ {:config config
+ :root r
+ :node (xt/start-node {:xtdb.lucene/lucene-store {}})}))
+
+(defmethod zen/stop 'zd.engines/datalog
+ [ztx config {n :node}]
+ (.close n))
+
+
+(defn parse-query [q]
+ (let [xs (->> (str/split q #"\n")
+ (mapv str/trim)
+ (remove (fn [s] (or (str/blank? s) (str/starts-with? s "\\")))))
+ columns (->> xs
+ (filterv #(re-matches #"^\s?>.*" %))
+ (mapv #(subs % 1))
+ (mapv str/trim)
+ (filterv #(not (str/blank? %)))
+ (mapv (fn [x]
+ (if (str/starts-with? x "(")
+ ['expr (edamame.core/parse-string x {:regex true})]
+ (let [[e k] (str/split x #":" 2)]
+ [(symbol e) (cond
+ (= k "*") (symbol k)
+ :else (keyword k))])))))
+ index (atom {})
+ find-items (->> (group-by first columns)
+ (reduce (fn [acc [k xs]]
+ (if (= 'expr k)
+ (->> (mapv second xs)
+ (reduce (fn [acc e]
+ (swap! index assoc e (count acc))
+ (conj acc e))
+ acc))
+ (let [cs (->> (mapv second xs) (dedupe) (into []))]
+ (swap! index assoc k (count acc))
+ (if (seq (filter (fn [x] (contains? #{'* :?} x)) cs))
+ (conj acc (list 'pull k ['*]))
+ (if (= cs [nil])
+ (conj acc k)
+ (conj acc (list 'pull k (mapv (fn [x] (if (nil? x) :xt/id x))cs))))))))
+ []))
+ where-items
+ (->> xs
+ (filterv (every-pred #(not (str/ends-with? % " :asc"))
+ #(not (str/ends-with? % " :desc"))
+ #(not (re-matches #"^\s?>.*" %))))
+ (mapv (fn [x] (let [res (edamame.core/parse-string (str/replace (str "[" x "]") #"#" ":symbol/")
+ {:regex true})]
+ (cond
+ (list? (get res 1))
+ (vector res)
+
+ :else
+ res)
+ ))))
+ where (->> where-items
+ (mapv (fn [x]
+ (clojure.walk/postwalk
+ (fn [y]
+ (if (and (keyword? y) (= "symbol" (namespace y)))
+ (str "'" (name y))
+ y)) x))))
+
+ order-items
+ (->> xs
+ (filterv (every-pred #(or (str/ends-with? % " :asc")
+ (str/ends-with? % " :desc"))
+ #(not (re-matches #"^\s?>.*" %))))
+ (mapv (fn [x] (edamame.core/parse-string (str/replace (str "[" x "]") #"#" ":symbol/") {:regex true}))))
+
+ order
+ (->> order-items
+ (mapv (fn [x]
+ (clojure.walk/postwalk
+ (fn [y]
+ (if (and (keyword? y) (= "symbol" (namespace y)))
+ (str "'" (name y))
+ y)) x))))]
+ (into {:where where
+ :order order
+ :find find-items
+ :columns columns
+ :index @index} )))
+
+
+(defn sugar-query [ztx q]
+ (let [q (parse-query q)
+ _ (def q q)
+ idx (:index q)
+ res (->>
+ (query ztx (dissoc q :columns :index))
+ (mapv (fn [x]
+ (->> (:columns q)
+ (mapv (fn [[e c]]
+ (cond
+ (nil? c) (or (get-in x [(get idx e) :xt/id]) (get-in x [(get idx e)]))
+ (list? c) (get-in x [(get idx c)])
+ (= c '*) (get-in x [(get idx e)])
+ (= c :?) (keys (get-in x [(get idx e)]))
+ :else (get-in x [(get idx e) c]))))))))
+ cols (->> (:columns q) (mapv second))]
+ {:result res
+ :query (dissoc q :columns :index)
+ :columns cols}))
diff --git a/legacy/datalog_test.clj b/legacy/datalog_test.clj
new file mode 100644
index 0000000..505f403
--- /dev/null
+++ b/legacy/datalog_test.clj
@@ -0,0 +1,178 @@
+(ns zd.datalog-test
+ (:require
+ [zd.test-utils :as tutils]
+ [zen-web.core :as web]
+ [xtdb.api :as xtdb]
+ [zd.api]
+ [zd.datalog :as datalog]
+ [clojure.test :refer [deftest is testing]]
+ [zen.core :as zen]
+ [matcho.core :as matcho]))
+
+(def ztx (zen/new-context {}))
+
+(zen/read-ns ztx 'zd)
+(zen/read-ns ztx 'zd.test)
+(zen/start-system ztx 'zd.test/system)
+(xtdb/sync (:node (datalog/get-state ztx)))
+
+(comment
+ (def ztx (zen/new-context {}))
+ )
+
+(deftest datalog-engine
+
+ (zen/start-system ztx 'zd.test/system)
+ (xtdb/sync (:node (datalog/get-state ztx)))
+
+ (datalog/query ztx '{:find [?e]
+ :where [[?e :xt/id ?id]]})
+
+ (datalog/query ztx '{:find [?e] :where [[?e :xt/id "'customers"]]})
+
+ (testing "metadata is loaded into xtdb"
+ (matcho/assert
+ #{['customers]}
+ (datalog/query ztx '{:find [?e] :where [[?e :xt/id "'customers"]]})))
+
+ (datalog/query ztx '{:find [e]
+ :where [[e :parent "'customers"]]})
+
+ (matcho/assert
+ #{'[customers.partners-list] '[customers.flame] '[customers._schema]}
+ (datalog/query ztx '{:find [e]
+ :where [[e :parent "'customers"]]}))
+
+ (matcho/assert
+ #{[#:xt{:id 'people.john}]}
+ (datalog/query ztx '{:find [(pull e [:xt/id :name])]
+ :where [[e :role "ceo"]]}))
+
+ )
+
+(deftest xtdb-sync
+
+ (zen/start-system ztx 'zd.test/system)
+
+ (xtdb/sync (:node (datalog/get-state ztx)))
+
+ (datalog/evict-by-query ztx '{:where [[e :xt/id "'people.bob"] ] :find [e]})
+
+ (datalog/query ztx '{:where [[e :xt/id id]] :find [e]})
+
+ (testing "add another person with role = ceo"
+ (matcho/assert
+ #{['people.john]}
+ (datalog/query ztx '{:find [?id]
+ :where [[?e :role "ceo"] [?e :xt/id ?id]]})
+ )
+
+ (def doc ":zd/docname people.bob\n:title \"Bob Barns\"\n:desc \"bob is the best\"\n:role #{\"ceo\"} ")
+
+ (matcho/assert
+ {:status 200}
+ (web/handle ztx 'zd/api
+ {:uri "/people._draft/edit"
+ :request-method :put
+ :body (tutils/req-body doc)}))
+
+ (is (tutils/read-doc "people/bob.zd"))
+ (xtdb/sync (:node (datalog/get-state ztx)))
+
+ (matcho/assert
+ #{['people.john] ['people.bob]}
+ (datalog/query ztx '{:find [?id]
+ :where [[?e :role "ceo"] [?e :xt/id ?id]]})))
+
+ (testing "edit bob's role"
+ (def doc ":zd/docname people.bob\n:title \"Bob Barns\"\n:desc \"bob is the best\"\n:role #{\"cpo\"} ")
+
+ (matcho/assert
+ {:status 200}
+ (web/handle ztx 'zd/api
+ {:uri "/people.bob/edit"
+ :request-method :put
+ :body (tutils/req-body doc)}))
+
+ (is (tutils/read-doc "people/bob.zd"))
+ (xtdb/sync (:node (datalog/get-state ztx)))
+
+ (matcho/assert
+ #{['people.john]}
+ (datalog/query ztx '{:find [?id] :where [[?e :role "ceo"] [?e :xt/id ?id]]}))
+
+ (matcho/assert
+ #{['people.bob]}
+ (datalog/query ztx '{:find [?id] :where [[?e :role "cpo"] [?e :xt/id ?id]]})))
+
+ (is (= 200 (:status (web/handle ztx 'zd/api {:uri "/people.bob" :request-method :delete}))))
+
+ (is (nil? (tutils/read-doc "people/bob.zd")))
+
+ ;; (is (empty? (datalog/query ztx '{:find [?id] :where [[?e :role "cpo"] [?e :xt/id ?id]]})))
+
+ )
+
+
+(deftest datalog-sugar
+
+
+ (def q
+ "
+e :parent #organizations
+e :rel #rel.partner
+p :organization e
+p :role #roles.cto
+> d
+> e:xt/id
+> e:rel
+> (count e)
+> (mean e)
+")
+
+ (def q2
+ "
+e :parent #customers
+e :category cat
+(clojure.string/starts-with? cat \"s\")
+e :customer-since since
+e :asc
+
+> e:name
+> (count e)
+"
+ )
+
+ (def q3
+ "
+e :type #customers
+> e
+")
+
+ (datalog/submit ztx {:xt/id "'i1" :type :type})
+ (datalog/submit ztx {:xt/id "'i2" :type :type})
+
+ (matcho/match
+ (datalog/parse-query q3)
+ '{:where [[e :type "'customers"]],
+ :order []
+ :find [e]})
+
+ (def q
+ "
+e :title t
+> e
+> t
+> e:title
+"
+ )
+
+
+ (datalog/parse-query q)
+
+ (datalog/sugar-query ztx "
+e :parent #people
+> e:* ")
+
+
+ )
diff --git a/src/zd/db.clj b/legacy/db.clj
similarity index 94%
rename from src/zd/db.clj
rename to legacy/db.clj
index d5bc9f6..4755c5c 100644
--- a/src/zd/db.clj
+++ b/legacy/db.clj
@@ -53,5 +53,6 @@
'{:find [?id ?mu (pull ?e [:section])]
:where [[?e :xt/id ?id]
[(some? ?mu)]
- [?e :menu-order ?mu]]
+ (or [?e :menu-order ?mu]
+ [?e :zd/menu-order ?mu])]
:order-by [[?mu :asc]]})}))
diff --git a/src/zd/fs.clj b/legacy/fs.clj
similarity index 74%
rename from src/zd/fs.clj
rename to legacy/fs.clj
index 557d6a2..4951c27 100644
--- a/src/zd/fs.clj
+++ b/legacy/fs.clj
@@ -18,10 +18,21 @@
(->> [:zen/state :zd.fs :state]
(get-in @ztx)))
+(defn name-to-dir [pths docname]
+ (->> (str/split docname #"\.")
+ butlast
+ (str/join "/")
+ (str (first pths) "/")))
+
(defn load-docs! [ztx root dirs]
+ (println :load-docs)
+ (->> (memstore/read-docs ztx {:path "~" :resource-path "zd.zd" :content (slurp (io/resource "zd.zd"))})
+ (mapv (fn [doc] (memstore/put-doc ztx (assoc doc :zd/readonly true)))))
(doseq [dir dirs]
(let [dir (io/file dir)
dir-path (.getPath dir)]
+ ;; TODO: if we go with zd/type zd.class how do we find all classes before loading other documents?
+ ;; TODO: load zd self schemas
;; load metadata
(doseq [f (->> (file-seq dir)
(filter (fn [f] (str/includes? (.getName f) "_schema.zd"))))]
@@ -37,10 +48,9 @@
(not (str/starts-with? (.getName f) ".")))
(let [resource-path (subs path (inc (count dir-path)))
content (slurp f)]
- (memstore/load-document! ztx {:path path
- :root root
- :resource-path resource-path
- :content content})))))))
+ (->> (memstore/read-docs ztx {:path path :root root :resource-path resource-path :content content})
+ (mapv (fn [doc] (memstore/put-doc ztx doc {:dont-validate true})))))))))
+ (memstore/inference ztx))
(defonce queue (agent nil))
@@ -50,7 +60,7 @@
(zen/pub ztx 'zd.events/on-load-start {})
(swap! ztx dissoc :zdb :zd/schema :zrefs :zd/macros)
(load-docs! ztx root paths)
- (memstore/load-links! ztx)
+ ;; (memstore/load-links! ztx)
(memstore/eval-macros! ztx)
(zen/pub ztx 'zd.events/on-load-complete {})
;; TODO think about return value
@@ -58,15 +68,17 @@
(defn fs-delete [ztx {:keys [filepath docname]}]
(let [{r :root pths :paths} (get-state ztx)
- fs-delete*
- (fn [ag]
- (io/delete-file filepath)
- (when-let [repo (get-gistate ztx)]
- (gitsync/delete-doc ztx repo {:docpath filepath :docname docname}))
- ;; TODO impl delta delete
- (reload ztx r pths))]
- (utils/safecall ztx fs-delete* {:type 'zd.fs/delete-doc-error})))
+ docname-sym (symbol docname)]
+ (swap! ztx update :zdb dissoc docname-sym)
+ ;; TODO: cleanup links and clear datalog
+ (io/delete-file filepath)
+ (when-let [repo (get-gistate ztx)]
+ (gitsync/delete-doc ztx repo {:docpath filepath :docname docname}))
+ (reload ztx r pths)))
+
+
+;; TODO: obsolete
(defmethod zen/op 'zd.events/fs-delete
[ztx config {_ev :ev {docname :docname} :params} & [_session]]
(println :zd.fs/delete docname)
@@ -81,27 +93,26 @@
;; TODO remove await
(await queue)))
+
(defn fs-save [ztx {dn :docname cnt :content :keys [rename-to]} {:keys [resource-path dirname filepath prev-filepath]}]
(let [{r :root pths :paths :as st} (get-state ztx)
- fs-save*
- (fn [ag]
- (.mkdirs (io/file dirname))
- (spit filepath cnt)
- ;; when rename delete old doc
- (when (symbol? rename-to)
- ((fs-delete ztx {:filepath prev-filepath :docname dn}) ag)
- (d/evict ztx (str dn))
- ;; TODO when awaits are gone
- #_(zen/pub ztx 'zd.events/on-doc-delete {:docname dn :root r}))
- ;; when schema edit initiate full reload
- (if (str/includes? filepath "_schema")
- (reload ztx r pths)
- (do (memstore/load-document! ztx {:path filepath
- :root r
- :resource-path resource-path
- :content cnt})
- (memstore/load-links! ztx)))
- 'ok)]
+ fs-save* (fn [ag]
+ (let [old-doc (memstore/get-doc ztx dn)])
+ (.mkdirs (io/file dirname))
+ (spit filepath cnt)
+ (when (symbol? rename-to)
+ ((fs-delete ztx {:filepath prev-filepath :docname dn}) ag)
+ (d/evict ztx (str dn)))
+ (if (str/includes? filepath "_schema")
+ (reload ztx r pths)
+ (->> (memstore/read-docs ztx {:path filepath
+ :root r
+ :resource-path resource-path
+ :content cnt})
+ (mapv (fn [doc]
+ (memstore/put-doc ztx doc)
+ (memstore/infere-doc ztx (:zd/docname doc))))))
+ 'ok)]
(utils/safecall ztx fs-save* {:type :zd.fs/save-error})))
(defn fs-commit [ztx {:keys [docname]} {:keys [filepath]}]
@@ -112,6 +123,8 @@
'ok)]
(utils/safecall ztx fs-commit* {:type :zd.fs/reload-error})))
+
+;; TODO: obsolete
(defmethod zen/op 'zd.events/fs-save
[ztx config {_ev :ev {dn :docname rename :rename-to :as ev} :params} & [_session]]
(zen/pub ztx 'zd.events/fs-save {:docname dn :rename rename})
diff --git a/legacy/fs/utils.clj b/legacy/fs/utils.clj
new file mode 100644
index 0000000..f5d7889
--- /dev/null
+++ b/legacy/fs/utils.clj
@@ -0,0 +1,11 @@
+(ns zd.fs.utils
+ (:require [clojure.string :as str]))
+
+(defn docpath [docname]
+ (str (str/replace (str docname) #"\." "/") ".zd"))
+
+(defn name-to-dir [pths docname]
+ (->> (str/split docname #"\.")
+ butlast
+ (str/join "/")
+ (str (first pths) "/")))
diff --git a/test/zd/fs_test.clj b/legacy/fs_test.clj
similarity index 88%
rename from test/zd/fs_test.clj
rename to legacy/fs_test.clj
index dfa1903..9c5cdee 100644
--- a/test/zd/fs_test.clj
+++ b/legacy/fs_test.clj
@@ -7,9 +7,31 @@
[clojure.test :refer [deftest is testing]]
[zd.fs :as fs]
[zd.api]
- [zd.memstore :as memstore]
+ ;; [zd.memstore :as memstore]
[zen.core :as zen]
- [zen-web.core :as web]))
+ [zen-web.core :as web]
+ [zd.test-utils :as t]))
+
+
+
+
+(deftest test-fs
+
+ (t/reset-project {'person ":title \"Person\"\n:zd/type zd.Class"
+ 'org ":title \"Organizatin\""})
+
+ (t/get-doc 'person)
+
+ (t/hiccup-find (t/http-get "person") "title")
+
+ (t/hiccup-match (t/http-get "person") "title"
+ [[:h1 {} "Person"]])
+
+ (is (t/get-doc 'person))
+
+
+
+ )
(defonce ztx (zen/new-context {}))
@@ -107,7 +129,7 @@
(deftest block-meta-added
(load! ztx)
- (matcho/assert
+ #_(matcho/assert
{:zd/meta
{:ann {:rel {:zd/content-type :edn :badge {}}
:tags {:zd/content-type :edn :badge {}}
@@ -131,5 +153,5 @@
(def subdoc-ann
(get-in doc [:zd/subdocs :partners-list :zd/meta :ann]))
- (is (contains? (:tags subdoc-ann) :badge))
+ ;; (is (contains? (:tags subdoc-ann) :badge))
(is (contains? (:countries subdoc-ann) :badge)))
diff --git a/src/zd/gitsync.clj b/legacy/gitsync.clj
similarity index 98%
rename from src/zd/gitsync.clj
rename to legacy/gitsync.clj
index bd96e97..6829d25 100644
--- a/src/zd/gitsync.clj
+++ b/legacy/gitsync.clj
@@ -3,6 +3,8 @@
[clojure.java.io :as io]
[clj-jgit.porcelain :as git]))
+;; TODO: may be use shell git integration
+
(defn commit-doc [ztx {:keys [repo ident]} {p :docpath d :docname}]
(git/with-identity ident
(let [;; TODO sync all untracked docs at gitsync start?
diff --git a/test/zd/gitsync_test.clj b/legacy/gitsync_test.clj
similarity index 100%
rename from test/zd/gitsync_test.clj
rename to legacy/gitsync_test.clj
diff --git a/src/zd/icons.clj b/legacy/icons.clj
similarity index 100%
rename from src/zd/icons.clj
rename to legacy/icons.clj
diff --git a/src/zd/layout.clj b/legacy/layout.clj
similarity index 100%
rename from src/zd/layout.clj
rename to legacy/layout.clj
diff --git a/legacy/link.clj b/legacy/link.clj
new file mode 100644
index 0000000..edd242b
--- /dev/null
+++ b/legacy/link.clj
@@ -0,0 +1,49 @@
+(ns zd.link
+ (:require
+ [stylo.core :refer [c]]
+ [zd.memstore :as memstore]
+ [clojure.string :as str]))
+
+(defn get-parent [ztx res]
+ (when-let [nm (:zd/name res)]
+ (let [pn (->> (str/split (str nm) #"\.")
+ (butlast)
+ (str/join "."))]
+ (when-not (str/blank? pn)
+ (or (memstore/get-doc ztx (symbol pn))
+ {:zd/name (symbol pn)})))))
+
+(defn resolve-icon [ztx res]
+ (if-let [ava (or (get-in res [:avatar]) (get-in res [:logo]))]
+ {:type :img :img ava}
+ (if-let [icon (or (get res :icon) (get res :zd/icon))]
+ {:type :ico :icon icon}
+ (when-let [parent (get-parent ztx res)]
+ (resolve-icon ztx parent)))))
+
+(def icon-c (name (c [:mr 1] [:text :gray-500])))
+
+(defn icon [ztx res & [opts]]
+ (let [icon-class (when-let [ic (:icon-class opts)] (name ic))]
+ (if-let [icon (resolve-icon ztx res)]
+ (cond (= (:type icon) :img)
+ [:img {:src (:img icon)
+ :class (c :inline-block [:mr 1] [:h 4] [:w 4] :border {:border-radius "100%" :margin-bottom "1px"})}]
+ (= (:type icon) :ico)
+ [:i {:class (conj (map name (:icon icon)) icon-c icon-class)}])
+ (when (:force-icon opts)
+ [:i {:class ["fa-solid" "fa-file" icon-c icon-class]}]))))
+
+(defn symbol-link [ztx s & [opts]]
+ (if-let [res (memstore/get-doc ztx (symbol s))]
+ (if (get-in res [:zd/meta :subdoc])
+ (let [parent (get-in res [:zd/parent])]
+ [:a {:href (str "/" parent "#subdocs-" (:zd/name res))
+ :class (c :inline-flex :items-center [:text "#4B5BA0"] [:hover [:underline]] :whitespace-no-wrap)}
+ (icon ztx res opts)
+ (when-not (:compact opts) (or (:title res) s))])
+ [:a {:href (str "/" s) :class (c :inline-flex :items-center [:text "#4B5BA0"] [:hover [:underline]] :whitespace-no-wrap)}
+ (icon ztx res opts)
+ (when-not (:compact opts)
+ (or (:title res) s))])
+ [:a {:href (str "/" s) :class (c [:text :red-600] [:bg :red-100]) :title "Broken Link"} s]))
diff --git a/src/zd/macros.clj b/legacy/macros.clj
similarity index 100%
rename from src/zd/macros.clj
rename to legacy/macros.clj
diff --git a/src/zd/memstore.clj b/legacy/memstore.clj
similarity index 50%
rename from src/zd/memstore.clj
rename to legacy/memstore.clj
index 7c4470a..3977db8 100644
--- a/src/zd/memstore.clj
+++ b/legacy/memstore.clj
@@ -4,12 +4,61 @@
[zd.methods :as methods]
[zd.macros]
[zen.core :as zen]
- [zen-web.utils :refer [deep-merge]]
[zd.reader :as reader]
- [clojure.string :as str]))
+ [clojure.string :as str]
+ [zd.datalog :as datalog]
+ [zd.utils :as u]))
+
+(defn validate-refs [ztx doc]
+ (->> doc
+ (reduce
+ (fn [acc [k v]]
+ (if (symbol? v)
+ (if-not (get (:zdb @ztx) v)
+ (conj acc {:type :doc-validation :message (str "could not find " v) :path [k]})
+ acc)
+ (if (set? v)
+ (->> v
+ (reduce (fn [acc x]
+ (if (and (symbol? x) (not (get (:zdb @ztx) x)))
+ (conj acc {:type :doc-validation :message (str "could not find '" x) :path [k]})
+ acc))
+ acc))
+ acc)))
+ [])))
+
+(defn doc-errors [ztx doc]
+ (let [cls (when-let [tp (:zd/type doc)] (if (set? tp) tp #{tp}))
+ errors
+ (->> cls
+ (mapcat
+ (fn [cn]
+ (let [c (get (:zdb @ztx) cn)]
+ (->> (:zd/require c)
+ (reduce (fn [acc k]
+ (if (contains? doc k)
+ acc
+ (conj acc
+ {:type :doc-validation
+ :message (str " required by " cn)
+ :path [k]})))
+ []))))))
+ errors (into errors (into (validate-refs ztx doc)))]
+ errors))
+
+(defn enrich [ztx doc]
+ (let [nm (:zd/docname doc)
+ nm (when nm (if (string? nm) nm (symbol nm)))
+ backlinks (when nm (when nm (get (:zrefs @ztx) nm)))
+ errors (seq (doc-errors ztx doc))]
+ (when doc
+ (cond-> doc
+ (seq backlinks) (assoc :zd/backlinks backlinks)
+ (seq errors) (assoc :zd/errors errors)))))
(defn get-doc [ztx nm]
- (get-in @ztx [:zdb nm]))
+ (let [doc (get (:zdb @ztx) nm)]
+ (enrich ztx doc)))
(defn *edn-links [acc docname path node]
(cond
@@ -119,12 +168,98 @@
(reduce (fn [acc [k v]] (*collect-macros acc [k] v))
{})))
+(defn read-docs [ztx {:keys [root resource-path path content] :as doc}]
+ (let [docname (meta/path->docname ztx resource-path)
+ parts (str/split (str docname) #"\.")
+ parent-link (->> parts (butlast) (str/join "."))
+ local-name (last parts)
+ parent (cond
+ (= (str docname) root) ""
+ (str/blank? parent-link) (and root (symbol root))
+ :else (symbol parent-link))
+ doc-body {:zd/meta {:docname docname
+ :file resource-path
+ :ann {:parent {:zd/content-type :edn}}
+ ;; TODO add last updated from git to a document here?
+ :path path}
+ :parent parent
+ :zd/docname docname
+ :zd/name local-name
+ :zd/parent parent}
+ doc (->> content (reader/parse ztx {}) (u/deep-merge doc-body))
+ subdocs (->> (:zd/subdocs doc)
+ (mapv (fn [[k subdoc]]
+ (let [subdoc-name (symbol (str docname "." (name k)))]
+ (assoc subdoc :zd/meta
+ {:docname subdoc-name
+ :subdoc true
+ :file resource-path
+ :path path}
+ ;; TODO: deprecated
+ :parent docname
+ :zd/docname subdoc-name
+ :zd/name local-name
+ :zd/parent docname)))))
+ doc (assoc doc :zd/subs (into #{} (map :zd/docname subdocs)))]
+ (into [doc] subdocs)))
+
+(defn *remove-links [zrefs docname]
+ (->> zrefs
+ (reduce (fn [acc [k lnks]]
+ (if (contains? lnks docname)
+ (assoc acc k (dissoc lnks docname))
+ acc))
+ zrefs)))
+
+(*remove-links {'a {'b {}}} 'b)
+
+(defn remove-links [ztx docname]
+ (swap! ztx update :zrefs (fn [zrefs] (*remove-links zrefs docname))))
+
+(defn put-doc [ztx {docname :zd/docname :as doc} & [opts]]
+ (let [links (collect-links ztx doc)
+ macros (collect-macros ztx doc)]
+ (swap! ztx assoc-in [:zdb docname] doc)
+ (when-not (:dont-validate opts)
+ (let [errors (doc-errors ztx doc)]
+ (swap! ztx assoc-in [:zd/errors docname] errors)))
+ (swap! ztx update :zrefs patch-links links)
+ (swap! ztx update :zd/keys (fnil into #{}) (keys doc))
+ (swap! ztx assoc-in [:zd/macros docname] macros)))
+
+(defn get-all-errors [ztx]
+ (get @ztx :zd/errors))
+
+(defn is? [x c]
+ (if (set? x) (contains? x c) (= x c)))
+
+;; TODO: render infered attrs in a specific way
+(defn infere [ztx docname {tp :zd/type p :zd/parent :as doc}]
+ (let [doc (if (and (not tp) (is? (:zd/type (get-doc ztx p)) 'zd.class))
+ (assoc doc :zd/type p)
+ doc)]
+ doc))
+
+(defn infere-doc [ztx docname]
+ (let [doc (get-doc ztx docname)
+ idoc (infere ztx docname doc)]
+ (swap! ztx assoc-in [:zdb docname] idoc)
+ (datalog/save-doc ztx idoc)
+ idoc))
+
+(defn inference [ztx]
+ (println :inference)
+ (doseq [[docname doc] (:zdb @ztx)]
+ (infere-doc ztx docname)
+ (let [errors (doc-errors ztx doc)]
+ (if (seq errors)
+ (swap! ztx assoc-in [:zd/errors docname] errors)
+ (swap! ztx update-in [:zd/errors] dissoc docname)))))
+
+;; OBSOLETE
(defn load-document! [ztx {:keys [root resource-path path content] :as doc}]
(let [docname (meta/path->docname ztx resource-path)
- parent-link
- (->> (str/split (str docname) #"\.")
- (butlast)
- (str/join "."))
+ parent-link (->> (str/split (str docname) #"\.") (butlast) (str/join "."))
doc-body {:zd/meta {:docname docname
:file resource-path
:ann {:parent {:zd/content-type :edn}}
@@ -137,7 +272,7 @@
:else (symbol parent-link))}
doc (->> content
(reader/parse ztx {})
- (deep-merge doc-body)
+ (u/deep-merge doc-body)
(meta/append-meta ztx))
links (collect-links ztx doc)
macros (collect-macros ztx doc)]
diff --git a/src/zd/meta.clj b/legacy/meta.clj
similarity index 91%
rename from src/zd/meta.clj
rename to legacy/meta.clj
index 458b75d..71ccea7 100644
--- a/src/zd/meta.clj
+++ b/legacy/meta.clj
@@ -1,6 +1,6 @@
(ns zd.meta
(:require
- [zen-web.utils :as utils]
+ [zd.utils :as u]
[zd.reader :as reader]
[clojure.string :as str]
[zen.core :as zen]))
@@ -76,6 +76,7 @@
(into {:zd/docname {:type 'zen/symbol}}))
meta-sch
{:type 'zen/map
+ :values {:type 'zen/any}
:keys {:doc {:type 'zen/vector
:every {:type 'zen/keyword}}
:text-values {:type 'zen/any}
@@ -100,7 +101,7 @@
[k (:schema v)]))
(into {}))}
toplevel
- (utils/deep-merge (get-in @ztx [:zd/schema :schemas (get-parent ztx docname)])
+ (u/deep-merge (get-in @ztx [:zd/schema :schemas (get-parent ztx docname)])
(get-in @ztx [:zd/schema :schemas :zd/root]))]
(-> {:zen/name 'zd.schema/document
@@ -116,12 +117,12 @@
"validate doc with zen schema compiled from a _schema.zd"
[ztx doc]
(let [docname (str (:zd/docname doc))
- sch (zen-schema ztx docname)
- errs (->> (zen/validate-schema ztx sch doc)
- (:errors)
- (map (fn [e] {:type :doc-validation
- :message (:message e)
- :path (:path e)})))]
+ sch (zen-schema ztx docname)
+ errs (->> (zen/validate-schema ztx sch doc)
+ (:errors)
+ (map (fn [e] {:type :doc-validation
+ :message (:message e)
+ :path (:path e)})))]
(update-in doc [:zd/meta :errors] into
(cond-> errs
(or (empty? docname) (str/ends-with? docname "."))
@@ -129,11 +130,13 @@
:path [:zd/docname]
:message "Add not empty :zd/docname"})
+ ;; This is a bad idea
(str/ends-with? docname "_draft")
(conj {:type :docname-validation
:path [:zd/docname]
:message "Rename :zd/docname from _draft"})))))
+
(defn annotations
"list annotations used in _schema"
[ztx]
diff --git a/src/zd/reader.clj b/legacy/reader.clj
similarity index 100%
rename from src/zd/reader.clj
rename to legacy/reader.clj
diff --git a/test/zd/reader.zd b/legacy/reader.zd
similarity index 100%
rename from test/zd/reader.zd
rename to legacy/reader.zd
diff --git a/test/zd/reader_test.clj b/legacy/reader_test.clj
similarity index 100%
rename from test/zd/reader_test.clj
rename to legacy/reader_test.clj
diff --git a/src/zd/render.clj b/legacy/render.clj
similarity index 77%
rename from src/zd/render.clj
rename to legacy/render.clj
index 62991ba..98f88a4 100644
--- a/src/zd/render.clj
+++ b/legacy/render.clj
@@ -14,6 +14,8 @@
[zd.methods :as methods]
[stylo.core :refer [c]]
[zd.memstore :as memstore]
+ [clojure.pprint]
+ [zd.blocks.zd]
[zd.db :as db]))
(defn actions [ztx {{uri :uri qs :query-string :as req} :request :as ctx} {{:keys [docname]} :zd/meta :as doc}]
@@ -26,12 +28,11 @@
del-script
(format "if (confirm(\"delete document?\") == true){
fetch('/%s', {method: 'DELETE'}).then((resp)=> {
- resp.text().then((docid) => {window.location.href = docid})})}"
+ resp.text().then((docid) => { window.location.pathname = docid})})}"
docname)
del-btn
[:a {:class (c [:text :gray-600] [:hover [:text :red-600]] [:ml 4])
- :href ""
:onclick (when-not (= docname 'index) del-script)}
[:i.fas.fa-trash-xmark]]
@@ -52,14 +53,13 @@
(let [parts (str/split (str docname) #"\.")
icon-class (c :cursor-pointer [:text :gray-500] [:hover [:text :orange-600]])]
[:div {:class (c :flex [:py 2])}
- (if (= (symbol root) docname)
+ (if (and root (= (symbol root) docname))
[:a {:href (str "/") :class icon-class}
[:span.fa-regular.fa-house]]
[:div {:class (c :flex :flex-flow :items-baseline)}
[:a {:href (str "/") :class icon-class}
[:span.fa-regular.fa-house]]
- [:span {:class (c [:mx 1.5] [:text :gray-500])}
- "/"]
+ [:span {:class (c [:mx 1.5] [:text :gray-500])} "/"]
(for [x (range 1 (+ 1 (count parts)))]
(let [pth (into [] (take x parts))
nm (str/join "." pth)]
@@ -69,8 +69,7 @@
:class (c [:text "#4B5BA0"])}
(last pth)]
(when-not (= x (count parts))
- [:span {:class (c [:text :gray-500] [:mx 1.5] {:font-size "18px"})}
- "/"])]))])]))
+ [:span {:class (c [:text :gray-500] [:mx 1.5] {:font-size "18px"})} "/"])]))])]))
(defn topbar [ztx ctx doc]
[:div {:class (c :flex :items-center)}
@@ -78,46 +77,73 @@
(actions ztx ctx doc)])
(defn render-key [ztx ctx {k :key :as block}]
- (try (methods/renderkey ztx ctx block)
- (catch Exception e
- (let [err {:message (str "error rendering " (.getMessage e))
- :trace (str/split (with-out-str (trace/print-stack-trace e))
- #"\n")
- :path [k]
- :type :zd/renderkey-error}
- err-block {:data [err] :key :zd/errors}]
- (zen/pub ztx 'zd.events/on-renderkey-error {:key k :error err})
- (methods/renderkey ztx ctx err-block)))))
+ (try
+ (methods/renderkey ztx ctx block)
+ (catch Exception e
+ (let [err {:message (str "error rendering " (.getMessage e))
+ :trace (str/split (with-out-str (trace/print-stack-trace e)) #"\n")
+ :path [k]
+ :type :zd/renderkey-error}
+ err-block {:data [err] :key :zd/errors}]
+ (zen/pub ztx 'zd.events/on-renderkey-error {:key k :error err})
+ (methods/renderkey ztx ctx err-block)))))
+
+(defn *key-to-docname [k]
+ (symbol (str (when-let [ns (namespace k)] (str ns ".")) (name k))))
+
+(defn key-schema [ztx k]
+ (let [schema (memstore/get-doc ztx (*key-to-docname k))]
+ {:schema schema
+ :key k
+ :schema-name (str (when-let [ns (namespace k)] (str ns ".")) (name k))
+ :ann (if-let [ann (:zd/annotation schema)]
+ {(keyword (last (str/split (str ann) #"\."))) {}}
+ {})}))
+
+(defn render-errors [_ztx errors]
+ [:div#doc-errors {:class (c [:text :red-700] [:my 2] :rounded :text-sm [:border :red-300])}
+ [:ul {:class (c :font-bold [:mb 1] [:py 1] [:px 3] [:ml 0] [:text :red-600] [:bg :red-100] [:border-b :red-300]
+ {:border-radius "4px 4px 0 0"})} "Document errors"]
+ (for [err (sort-by :type errors)]
+ [:li {:class (c [:py 0.5] :flex [:space-x 3] [:text :red-600] [:px 3])}
+ [:span (pr-str (:path err))]
+ [:span {:class (c [:ml 4] {:text-align "right"})} (:message err)]])])
(defn render-blocks [ztx ctx {m :zd/meta subs :zd/subdocs :as doc} & [render-subdoc?]]
[:div {:class (if (:zd/render-preview? ctx)
(c [:overflow-x-auto] [:w-max "50vw"])
(c [:w "60vw"] #_[:overflow-x-auto] [:w-max "60rem"]))}
;; TODO render errors in doc view
- (when-let [errs (seq (:errors m))]
- (methods/renderkey ztx ctx {:data errs :ann {} :key :zd/errors}))
- (doall
- (for [k (distinct (filter #(get doc %) (:doc m)))]
- (let [block {:data (get doc k)
- :key k
- :ann (assoc (get-in doc [:zd/meta :ann k]) :zd/render-subdoc? render-subdoc?)}]
- (render-key ztx ctx block))))
- (let [links (seq (get-in doc [:zd/meta :backlinks]))]
+ (when-let [errs (seq (:zd/errors doc))]
+ (render-errors ztx errs))
+ (->> (:doc m)
+ (filter #(get doc %)) distinct
+ (map (fn [k]
+ (let [schema (key-schema ztx k)
+ block {:data (get doc k)
+ :key k
+ :ann (-> (assoc (get-in doc [:zd/meta :ann k]) :zd/render-subdoc? render-subdoc?)
+ (merge (:ann schema)))}]
+ (render-key ztx ctx block)))))
+ (let [links (seq (get doc :zd/backlinks))]
(when-not render-subdoc?
(methods/renderkey ztx ctx {:data links :key :zd/backlinks})))
(when-let [subdocs (seq (filter #(get subs %) (:doc m)))]
[:div {:class (c [:py 4])}
(doall
(for [sub-key (distinct subdocs)]
- [:div {:class (c [:my 2])}
+ [:div {:class (c [:my 3] :border [:p 4] :rounded :shadow-sm)}
[:div {:class (c :flex :flex-row :border-b)}
[:a {:id (str "subdocs-" (name sub-key))}
[:span "&"]
[:span {:class (c :uppercase {:font-weight "600"})} (name sub-key)]]]
- (render-blocks ztx ctx (get-in doc [:zd/subdocs sub-key]) true)]))])])
+ (render-blocks ztx ctx (get-in doc [:zd/subdocs sub-key]) true)]))])
+ [:details
+ [:summary {:class (c [:text :gray-300] :text-xs :cursor-pointer [:hover [:text :gray-500]])} "data"]
+ [:pre {:class (c [:bg :gray-100] :border [:p 2] :text-xs)}(with-out-str (clojure.pprint/pprint doc))]]])
-(defn contents-sidebar [ztx {r :root :as ctx} {{order :doc anns :ann :as m} :zd/meta
- links :zd/backlinks subs :zd/subdocs :as doc}]
+(defn contents-sidebar
+ [ztx {r :root :as ctx} {{order :doc anns :ann :as m} :zd/meta links :zd/backlinks subs :zd/subdocs :as doc}]
(let [dockeys
;; TODO fix case when subdocs and dockey are the same
(->> order
@@ -143,18 +169,18 @@
subdocs (->> order
(filter #(get subs %))
(distinct))
- root (c :flex :flex-col :text-sm [:p 6] [:bg "white"] [:w-max "16rem"])
col (c :flex :flex-col [:py 2])
- head (c :uppercase [:pb 3])
- item (c [:py 0.6])]
- [:div {:class root}
+ head (c [:pb 0.5] [:pt 1] :border-b :text-lg [:mb 1] [:text :gray-600] {:font-weight "500"})
+ item (c [:py 0.5] [:hover [:text :gray-800]])]
+ [:div {:class (c :flex :flex-col :text-sm [:p 6] [:bg "white"] [:w-max "16rem"]
+ :text-sm [:text :gray-600]
+ {:position "absolute" :top "0rem" :right "1rem"})}
(when (seq dockeys)
[:div {:class col}
[:div {:class head} "keys"]
;; TODO make items clickable
(for [{d :display h :href} dockeys]
- [:a {:class item
- :href (str "#" h)} d])])
+ [:a {:class item :href (str "#" h)} d])])
(when (seq doclinks)
[:div {:class col}
[:div {:class head} "backlinks"]
@@ -276,7 +302,7 @@
(let [parsed (reader/parse ztx ctx text)]
(->> parsed
(meta/append-meta ztx)
- (meta/validate-doc ztx)
+ (memstore/enrich ztx)
(render-blocks ztx (assoc ctx :zd/render-preview? true)))))
(defn editor [ztx ctx {m :zd/meta :as doc}]
diff --git a/src/zd/runner.clj b/legacy/runner.clj
similarity index 99%
rename from src/zd/runner.clj
rename to legacy/runner.clj
index 436b2b0..0f34eb3 100644
--- a/src/zd/runner.clj
+++ b/legacy/runner.clj
@@ -1,3 +1,4 @@
+;; TODO: move to core
(ns zd.runner
(:require [clojure.java.io :as io]
[clojure.string :as str])
diff --git a/src/zd/system.clj b/legacy/system.clj
similarity index 56%
rename from src/zd/system.clj
rename to legacy/system.clj
index b5b15c4..6a57d9d 100644
--- a/src/zd/system.clj
+++ b/legacy/system.clj
@@ -1,3 +1,4 @@
+;; TODO move to core
(ns zd.system
(:require [zen.core :as zen]
[zd.datalog]
@@ -18,19 +19,26 @@
(reset! dtx ztx)
(restart ztx)))
-(defn query [q]
- (zd.datalog/query @dtx q))
+(defn query [ztx q]
+ (zd.datalog/query ztx q))
(comment
(-main)
- (restart @dtx)
+ ;; TODO: fix document update
+ ;; TODO: add validation by class
+ ;; TODO: add annotations by prop
- (zen/stop-system @dtx)
+ (zen/stop-system ztx)
+ (def ztx (zen/new-context {}))
+ (restart ztx)
+
+
+ (query ztx '{:where [[e :xt/id id]]
+ :find [(pull e [:xt/id :title])]})
- (query '{:where [[e :parent p]]
- :find [(pull e [:xt/id :title])]})
+ (zd.memstore/get-doc ztx 'needs)
(:zrefs @@dtx)
diff --git a/test/zd/system_test.clj b/legacy/system_test.clj
similarity index 100%
rename from test/zd/system_test.clj
rename to legacy/system_test.clj
diff --git a/src/js/core.js b/src/js/core.js
index 890db63..d7ca006 100644
--- a/src/js/core.js
+++ b/src/js/core.js
@@ -38,7 +38,7 @@ var stop = (ev) => {
};
var is_alphanum = (c) => {
- return (c || false) && c.match(/[-.a-zA-Z0-9]/i) !== null;
+ return (c || false) && c.match(/[-/.a-zA-Z0-9]/i) !== null;
};
var is_in_key = (txt, sym) => {
@@ -50,7 +50,7 @@ var is_in_key = (txt, sym) => {
}
}
var lastc = txt[line_start + 1];
- return (lastc == ':' || lastc == '^') && !(quote_count & 1);
+ return (lastc == ':' || lastc == '^' || lastc == '&') && !(quote_count & 1);
};
var set = (nel, attrs) => {
@@ -175,8 +175,9 @@ var update_widgets = () => {
//mermaid.initialize();
};
+// TODO: handle params
var load_page = (href, do_push) => {
- fetch(href + '?only-body=true', {
+ fetch(href + '/content', {
headers: {
'x-body': 'true',
'cache-control': 'no-cache'
@@ -250,7 +251,10 @@ var on_link_click = (ev) => {
}
};
-var open_search = (ev) => {};
+var open_search = (ev) => {
+ console.log('search');
+ window.location.href = "/_search";
+};
var close_search = (ev) => {};
@@ -319,8 +323,21 @@ main(() => {
});
// TODO check prefix to use in browser app
- // document.addEventListener('keydown', on_hotkey);
- document.addEventListener('click', on_link_click);
+ document.addEventListener('keydown', on_hotkey);
+ // document.addEventListener('click', on_link_click);
+
+ // setInterval(()=> {
+ // fetch('/_errors').then((resp)=> {
+ // if(resp.status == 200){
+ // _id('errors-link').style.display = "flex";
+ // resp.text().then((t)=>{
+ // _id('errors-count').innerText = t;
+ // });
+ // } else {
+ // _id('errors-link').style.display = "none";
+ // }
+ // });
+ // }, 1000 );
toa(document.getElementsByClassName("zd-toggle")).map(function(el){
el.querySelector('.zd-block-title').addEventListener("click", function () {
diff --git a/src/js/editor.js b/src/js/editor.js
index a8117a4..0bb88b0 100644
--- a/src/js/editor.js
+++ b/src/js/editor.js
@@ -19,6 +19,18 @@ var auto_close = (key, textarea) => {
}
}
+var zdhl= (elid)=> {
+ let el = document.getElementById(elid);
+ let code = el.parentElement;
+ let v = el.innerText;
+ code.innerHTML = v
+ .replace(/:[a-zA-Z0-9][-.:\/_a-zA-Z0-9]+/gi, (x)=> {return `${x}`;})
+ .replace(/#[-.:_a-zA-Z0-9]+/gi, (x)=> {return `${x}`;})
+ .replace(/\^[-_a-zA-Z0-9]+/gi, (x)=> {return `${x}`;})
+ .replace(/(\(\([^\)]+\)\))/gi, (x)=> { return `${x}`; })
+ .replace(/(\[\[[^\]]+\]\])/gi, (x)=> { return `${x}`; });
+};
+
var hl = (ctx, v)=>{
var t = ctx.editor.els.textarea;
var h = ctx.editor.els.hl;
@@ -55,7 +67,14 @@ var options = {
}
},
symbol: (ctx, token)=>{
- return ctx.symbols.search(token).map((x)=> { return x.item; });
+ console.log('token',token);
+ let res = ctx.symbols.search(token).map((x)=> { console.log(x.item); return x.item; });
+ if(token == '/'){
+ res.unshift({title: '', name: "/\n"});
+ } else {
+ res.unshift({name: token, icon: ["fa-solid", "fa-plus"]});
+ }
+ return res;
},
annotation: (ctx, token)=>{
return ctx.annotations.search(token).map((x)=> { return x.item; });
@@ -125,7 +144,8 @@ var autocompl = (ctx, v)=> {
pos_type='annotation';
token_start = i;
break;
- } else if(c == ':' && (p_aln || j == 1) && is_in_key(btxt)) {
+ } else if(c == ':' ) {
+ console.log('here');
pos_type = 'key';
token_start = i;
break;
@@ -170,7 +190,7 @@ var autocompl = (ctx, v)=> {
class: ['menu-item'],
on: {click: (ev)=> { ctx.selection = i; insert_selection(ctx); } },
els: {icon: icon,
- name: {tag: 'b', style: {'padding-right': 5}, text: item.name},
+ name: {tag: 'b', style: {'padding-right': 5}, text: (item.name || '').trim()},
title: {tag: 'span', text: item.title}},
style: {padding: 5, 'font-size': 12, cursor: 'point'}};
if(i == 0) { opts.style = merge(opts.style, selection_style); }
@@ -268,8 +288,7 @@ var sanitize = (s) => {
return s.replace(/[\u0020\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, " ");
};
-var _render = (ev)=> {
- var content = sanitize(ev.target.value);
+var _render = (content)=> {
fetch(`/${ctx.doc}/preview`, {method: 'POST', body: content}).then((resp)=> {
resp.text().then((txt)=> {
ctx.preview.innerHTML = txt;
@@ -287,7 +306,7 @@ var render = debounce(_render, 300);
var on_editor_keyup = (ctx, ev) => {
if (!ctx.in_chrome){
- render(ev);
+ render(sanitize(ev.target.value));
}
if(ctx.skip_up) {
@@ -332,13 +351,13 @@ var caret_style = merge(poss, {color: transparent,
var textarea_style = merge(poss, {overflow: 'hidden', position: absolute, margin: 0, top: 0, left: 0});
var editor = (zendoc) => {
- var symIdx = new quickScore.QuickScore(zendoc.symbols, ["name", "title"]);
+ var symIdx = new quickScore.QuickScore(zendoc.symbols || [], ["name", "title"]);
symIdx.config.maxIterations = 1000;
- var keysIdx = new quickScore.QuickScore(zendoc.keys, ["name"]);
+ var keysIdx = new quickScore.QuickScore(zendoc.keys || [], ["name"]);
keysIdx.config.maxIterations = 1000;
- var iconsIdx = new quickScore.QuickScore(zendoc.icons, ["name", "title"]);
+ var iconsIdx = new quickScore.QuickScore(zendoc.icons || [], ["name", "title"]);
iconsIdx.config.maxIterations = 1000;
- var annotationsIdx = new quickScore.QuickScore(zendoc.annotations, ["name"]);
+ var annotationsIdx = new quickScore.QuickScore(zendoc.annotations || [], ["name"]);
annotationsIdx.config.maxIterations = 1000;
var ctx = {symbols: symIdx, keys: keysIdx, icons: iconsIdx, annotations: annotationsIdx, doc: zendoc.doc};
@@ -411,11 +430,11 @@ var editor = (zendoc) => {
pop: {tag: 'div', style: popup_style }}});
hl(ctx, zendoc.text);
- var preview = new DOMParser().parseFromString(zendoc.preview, "text/html");
+ // var preview = new DOMParser().parseFromString(zendoc.preview, "text/html");
// TODO think if in_chrome is still needed
if(! in_chrome) {
- ctx.preview = el({tag: 'div', html: preview.documentElement.textContent,
+ ctx.preview = el({tag: 'div', html: 'loading...', // preview.documentElement.textContent,
style: {height: 'calc(100vh - 20px)',
padding: 20,
width: '60vw',
@@ -428,6 +447,7 @@ var editor = (zendoc) => {
}
window.ctx = ctx;
+ render(zendoc.text);
update_widgets();
return ctx;
};
diff --git a/src/zd.zd b/src/zd.zd
new file mode 100644
index 0000000..dea659d
--- /dev/null
+++ b/src/zd.zd
@@ -0,0 +1,73 @@
+:title "Zendoc"
+:desc /
+
+Welcome to zendoc built-in documentation.
+
+
+&class zd.class
+:title "Class"
+:zd/data-type zd.symbol
+:zd/maybe-set? true
+
+&prop zd.class
+:title "Property"
+
+&type zd.prop
+:title ":zd/type"
+:zd/data-type zd.symbol
+:zd/ref-type zd.data-type
+:zd/maybe-set? true
+
+&require zd.prop
+:title ":zd/require"
+:zd/data-type zd.keyword
+:zd/set? true
+
+&summary zd.prop
+:title ":zd/summary"
+:zd/data-type zd.keyword
+:zd/vector? true
+
+
+&data-type zd.prop
+:title ":zd/data-type"
+:zd/data-type zd.symbol
+
+&icon zd.prop
+:title "zd/type"
+:zd/data-type zd.keyword
+:zd/vector? true
+
+&menu-order zd.prop
+:title "zd/type"
+:zd/data-type zd.int
+
+&readonly zd.prop
+:zd/data-type zd.boolean
+
+;; datatypes
+&set zd.class
+&vector zd.class
+&boolean zd.class
+&string zd.class
+&inst zd.class
+&date zd.class
+&number zd.class
+&int zd.class
+&url zd.class
+&symbol zd.class
+&keyword zd.class
+&email zd.class
+
+&annotation zd.prop
+:zd/annotation zd.badge
+
+&annotation-class
+
+&hide zd.annotation-class
+
+&badge zd.annotation-class
+
+&link-badge zd.annotation-class
+
+&attribute zd.annotation-class
\ No newline at end of file
diff --git a/src/zd/api.clj b/src/zd/api.clj
deleted file mode 100644
index 766f1d6..0000000
--- a/src/zd/api.clj
+++ /dev/null
@@ -1,140 +0,0 @@
-(ns zd.api
- (:require
- [clojure.pprint :as pprint]
- [zd.layout :as layout]
- [zd.memstore :as memstore]
- [zd.meta :as meta]
- [zd.datalog]
- [hiccup.core :as hiccup]
- [zd.reader :as reader]
- [clojure.java.io :as io]
- [clojure.string :as str]
- [zen.core :as zen]
- [zd.fs :as fs]
- [zd.methods :as methods]
- [zd.render :as render]
- [zen-web.core :as web])
- (:import [java.io StringReader]))
-
-;; TODO move to zen-web.http
-(defn get-state [ztx]
- (->> [:zen/state :http :state]
- (get-in @ztx)))
-
-(defn zendoc-config [ztx]
- (->> [:config :zendoc]
- (get-in (get-state ztx))
- (zen/get-symbol ztx)))
-
-;; ISSUE mw does not work for / request
-;; TODO remove this mw
-(defmethod web/middleware-in 'zd/append-doc
- [ztx _cfg {{id :id} :route-params :as req} & opts]
- (when (some? id)
- ;; TODO make root required in render ops with zen sch
- {:zd/root (:root (zendoc-config ztx))
- :doc (memstore/get-doc ztx (symbol id))}))
-
-(defmethod zen/op 'zd/render-doc
- [ztx config {{id :id} :route-params
- uri :uri
- hs :headers
- doc :doc :as req} & opts]
- (let [{r :root ps :paths :as config} (zendoc-config ztx)]
- (cond
- (= uri "/")
- {:status 301
- :headers {"Location" (str "/" r "?" (:query-string req))
- "Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}}
-
- (nil? doc)
- {:status 301
- :headers {"Location" (str "/" id "/edit" "?" (:query-string req))
- "Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}}
-
- (get hs "x-body")
- {:status 200
- :headers {"Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}
- :body (hiccup/html (render/render-doc ztx {:request req :paths ps :doc doc :root r :config config} doc))}
-
- :else
- {:status 200
- :headers {"Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}
- :body (render/doc-view ztx {:request req :paths ps :doc doc :root r :config config} doc)})))
-
-(defmethod web/middleware-out 'zd/layout
- [ztx config {page :page :as req} {bdy :body :as resp} & args]
- (when (and (not (string? bdy)) (= 200 (:status resp)))
- {:headers {"Content-Type" "text/html"}
- :body (layout/sidebar ztx {:request req} bdy)}))
-
-(defmethod zen/op 'zd/render-widget
- [ztx _cfg {{id :id wgt :widget-id} :route-params r :root :keys [doc] :as req} & opts]
- (if-not (nil? doc)
- {:status 200
- :body (methods/widget ztx {:widget (keyword wgt) :root r :request req} doc)}
- {:status 200
- :body [:div "Error: " id " is not found"]}))
-
-(defmethod zen/op 'zd/save-doc
- [ztx _cfg {{id :id} :route-params r :zd/root :as req} & opts]
- ;; TODO emit zen event
- (println :zd.api/save-doc id)
- (let [lines (slurp (:body req))
- lineseq (->> lines
- (StringReader.)
- (io/reader)
- (line-seq))
- content (->> lineseq
- (remove #(str/starts-with? % ":zd/docname"))
- (remove #(str/starts-with? % ":zd/rename"))
- (str/join "\n"))
- doc (->> (reader/parse ztx {:req req} lines)
- (meta/append-meta ztx)
- (meta/validate-doc ztx))
- ;; TODO check if coerce is needed
- docname (str (:zd/docname doc))]
- (if-let [errs (seq (get-in doc [:zd/meta :errors]))]
- {:status 422 :body {:message "document validation failed"
- :docname docname
- :root r
- :errors errs}}
- (do (zen/pub ztx 'zd.events/on-doc-save {:docname docname
- :rename-to (:zd/rename doc)
- :content content
- :root r})
- {:status 200 :body (str "/" (or (:zd/rename doc) docname))}))))
-
-(defmethod zen/op 'zd/delete-doc
- [ztx _cfg {{:keys [id]} :route-params :as req} & opts]
- (let [{r :root} (zendoc-config ztx)
- parts (str/split id #"\.")
- redirect
- (if-let [parent (seq (butlast parts))]
- (str "/" (str/join "." parent))
- (str "/" r))]
- (zen/pub ztx 'zd.events/on-doc-delete {:docname id :root r})
- {:status 200 :body redirect}))
-
-(defmethod zen/op 'zd/render-editor
- [ztx _cfg {{id :id} :route-params :as req} & opts]
- (let [doc (or (:doc req) {:zd/meta {:docname (symbol id)}})
- {r :root ps :paths :as config} (zendoc-config ztx)]
- {:status 200
- :body (render/editor ztx {:root r :paths ps :request req :doc doc :config config} doc)}))
-
-(defmethod zen/op 'zd/render-preview
- [ztx _ {{id :id} :route-params :as req} & opts]
- (let [{r :root ps :paths :as config} (zendoc-config ztx)]
- {:headers {"Content-Type" "text/html"}
- :body (-> (render/preview ztx {:request req :paths ps :config config :root r} (slurp (:body req)))
- (hiccup/html))
- :status 200}))
-
-(defmethod zen/op 'zd.events/logger
- [ztx config {ev-name :ev :as ev} & opts]
- ;; TODO filter out
- (when-not (or (= ev-name 'zd.events/on-doc-save)
- (= ev-name 'zd.events/on-doc-load))
- ;; TODO do not print large events
- (pprint/pprint (assoc ev ::ts (.toString (java.util.Date.))))))
diff --git a/src/zd/blobstore/core.clj b/src/zd/blobstore/core.clj
new file mode 100644
index 0000000..ddb01f0
--- /dev/null
+++ b/src/zd/blobstore/core.clj
@@ -0,0 +1,2 @@
+(ns zd.blobstore.core)
+;; interfaces to work with blob store
diff --git a/src/zd/blobstore/gcp.clj b/src/zd/blobstore/gcp.clj
new file mode 100644
index 0000000..ed35a11
--- /dev/null
+++ b/src/zd/blobstore/gcp.clj
@@ -0,0 +1,128 @@
+(ns zd.blobstore.gcp
+ (:require
+ [cheshire.core]
+ [clojure.string :as str])
+ (:import (java.security Signature
+ KeyFactory
+ MessageDigest)
+ (java.security.spec PKCS8EncodedKeySpec)
+ (java.time ZonedDateTime
+ ZoneOffset)
+ java.util.Base64
+ org.apache.commons.codec.binary.Hex
+ java.net.URLEncoder
+ (java.time.format DateTimeFormatter)))
+
+(def ^:private timestamp-fmt (DateTimeFormatter/ofPattern "yyyyMMdd'T'HHmmss'Z'"))
+(def ^:private datestamp-fmt (DateTimeFormatter/ofPattern "yyyyMMdd"))
+
+(defn get-sa []
+ (cheshire.core/parse-string (slurp "gcp.json") keyword))
+
+(defn- quote-url
+ "Replace special characters in string using the %xx escape. Letters, digits, and the characters '_.-' are never quoted. By default, this function is intended for quoting the path section of the URL. The optional safe parameter specifies additional characters that should not be quoted — its default value is '/'.
+ Translation of https://docs.python.org/2/library/urllib.html#urllib.quote"
+ [^String s & {:keys [safe] :or {safe "/"}}]
+ (let [not-safe? (-> safe (str "_.-") set complement)]
+ (loop [[chars [safe-char & rest-chars]] (split-with not-safe? s)
+ r ""]
+ (let [r (str r (URLEncoder/encode (str/join chars) "UTF-8") safe-char)]
+ (if (seq rest-chars)
+ (recur (split-with not-safe? rest-chars) r)
+ r)))))
+
+(defn- hex-str [#^bytes b]
+ (Hex/encodeHexString b))
+
+(defn- sha-256-str [^String input]
+ (->> (.getBytes input "UTF-8")
+ (.digest (MessageDigest/getInstance "SHA-256"))
+ hex-str))
+
+(defn- signSHA256RSA
+ "RSA-SHA256 with PKCS1v15 padding"
+ [^String pk ^String input]
+ (let [pk (-> (str pk)
+ (str/replace #"-----BEGIN PRIVATE KEY-----" "")
+ (str/replace #"-----END PRIVATE KEY-----" "")
+ (str/replace #"\n" "")
+ (->> (.decode (Base64/getDecoder))
+ (new PKCS8EncodedKeySpec)))
+
+ private-signature (doto (Signature/getInstance "SHA256withRSA")
+ (.initSign (.generatePrivate (KeyFactory/getInstance "RSA") pk))
+ (.update (.getBytes input "UTF-8")))]
+ (hex-str (.sign private-signature))))
+
+"Tranlation of https://github.com/GoogleCloudPlatform/python-docs-samples/blob/9e5c2fb563f1cd5de76a72c4d8ae57ce884f66bd/storage/signed_urls/generate_signed_urls.py"
+(defn generate-signed-url
+ [{service-account :account
+ bucket-name :bucket
+ object-name :object
+ http-method :method
+ :keys [subresource expiration query-parameters headers]}]
+ (let [escaped-object-name (quote-url object-name :safe "/~")
+ canonical-uri (str \/ escaped-object-name)
+
+ datetime-now (ZonedDateTime/now (ZoneOffset/UTC))
+ request-timestamp (.format timestamp-fmt datetime-now)
+ datestamp (.format datestamp-fmt datetime-now)
+
+ client-email (:client_email service-account)
+ credential-scope (str datestamp "/auto/storage/goog4_request")
+ credential (str client-email \/ credential-scope)
+
+ bucket-name (or bucket-name (:bucket service-account))
+
+ host (str bucket-name ".storage.googleapis.com")
+ headers (assoc headers :host host)
+
+ ordered-headers (sort-by key headers)
+ canonical-headers (str (->> ordered-headers
+ (map (comp (partial str/join \:)
+ (partial map str/lower-case)
+ (juxt (comp name key) (comp str val))))
+ (str/join \newline))
+ \newline)
+ signed-headers (->> ordered-headers
+ (map (comp str/lower-case name key))
+ (str/join \;))
+
+ query-parameters (cond-> (assoc query-parameters
+ :X-Goog-Algorithm "GOOG4-RSA-SHA256"
+ :X-Goog-Credential credential
+ :X-Goog-Date request-timestamp
+ :X-Goog-Expires (or expiration "604800")
+ :X-Goog-SignedHeaders signed-headers)
+ subresource (assoc subresource ""))
+
+ canonical-query-string (->> query-parameters
+ (sort-by key)
+ (map (comp (partial str/join \=)
+ (partial map #(quote-url % :safe ""))
+ (juxt (comp name key) (comp str val))))
+ (str/join \&))
+
+ canonical-request (str/join \newline
+ [(or http-method "GET")
+ canonical-uri
+ canonical-query-string
+ canonical-headers
+ signed-headers
+ "UNSIGNED-PAYLOAD"])
+ canonical-request-hash (sha-256-str canonical-request)
+ string-to-sign (str/join \newline
+ ["GOOG4-RSA-SHA256"
+ request-timestamp
+ credential-scope
+ canonical-request-hash])
+
+ signature (signSHA256RSA (:private_key service-account) string-to-sign)
+ signed-url (str "https://" host canonical-uri \? canonical-query-string "&x-goog-signature=" signature)]
+ signed-url))
+
+
+(comment
+ (generate-signed-url {:account (get-sa) :object "ups" :method "PUT"})
+
+ )
diff --git a/src/zd/core.clj b/src/zd/core.clj
new file mode 100644
index 0000000..3550ac5
--- /dev/null
+++ b/src/zd/core.clj
@@ -0,0 +1,209 @@
+(ns zd.core
+ (:require
+ [zen.core :as zen]
+ [zd.store :as store]
+ [zd.methods :as methods]
+ [zen-web.core]
+ [hiccup.core]
+ [zd.git :as git]
+ [clojure.walk]
+ [ring.util.codec]
+ [zd.view.core :as view])
+ (:import [org.httpkit BytesInputStream]))
+
+
+(defn form-decode [s]
+ (clojure.walk/keywordize-keys (ring.util.codec/form-decode s)))
+
+(defn config [ztx]
+ (zen/get-state ztx :zd/config))
+
+{:zd/name '_errors
+ :title "Errors"
+ :zd/readonly true
+ :zd/view [[:title] [:errors-view]]
+ :errors-view true}
+
+
+;; (defmethod web/middleware-out 'zd/layout
+;; [ztx config {page :page :as req} {bdy :body :as resp} & args]
+;; (when (and (not (string? bdy)) (= 200 (:status resp)))
+;; {:headers {"Content-Type" "text/html"}
+;; :body (layout/sidebar ztx {:request req} bdy)}))
+
+(defmethod zen/op 'zd/preview-doc
+ [ztx config {{id :id} :route-params uri :uri hs :headers doc :doc :as req} & opts]
+ (let [docname (symbol (or id "index"))
+ doc (store/doc-get ztx docname)]
+ {:status 200
+ :body (hiccup.core/html (view/preview ztx req doc))}))
+
+(defmethod zen/op 'zd/render-doc
+ [ztx config {{id :id} :route-params :as req} & opts]
+ (try
+ (if-let [doc (store/doc-get ztx (symbol (or id "index")))]
+ {:status 200
+ :headers {"Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}
+ :body (hiccup.core/html (view/page ztx req doc))}
+ {:status 301
+ :headers {"Location" (str "/new?docname=" id)
+ "Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}})
+ (catch Exception e
+ {:status 500
+ :headers {"Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}
+ :body (hiccup.core/html [:pre (pr-str e)])})))
+
+(defmethod zen/op 'zd/render-editor
+ [ztx _cfg {{id :id} :route-params :as req} & opts]
+ (let [docname (symbol (or id "index"))
+ doc (store/doc-get ztx docname)
+ content (store/file-content ztx docname)]
+ {:status 200
+ :body (hiccup.core/html (view/editor ztx req doc content))}))
+
+;;TODO: move to store and write tests
+(defn preview-doc [ztx doc]
+ (let [errors (store/doc-validate ztx doc)
+ doc (cond-> doc (seq errors) (assoc :zd/errors errors))
+ doc (if (:zd/subdocs doc)
+ (-> doc (update :zd/subdocs (fn [subdocs]
+ (->> subdocs
+ (mapv (fn [subdoc]
+ (let [errors (store/doc-validate ztx subdoc)]
+ (cond-> subdoc (seq errors) (assoc :zd/errors errors)))))))))
+ doc)]
+ doc))
+
+;; TODO: add validation and inference
+(defmethod zen/op 'zd/render-preview
+ [ztx _cfg {{id :id} :route-params body :body :as req} & opts]
+ (let [docname (symbol id)
+ content (if (= BytesInputStream (type body)) (slurp body) body)
+ doc (store/to-doc ztx docname content {})
+ doc (preview-doc ztx doc)]
+ {:status 200
+ :body (hiccup.core/html (view/preview ztx req doc))}))
+
+(defmethod zen/op 'zd/new-doc
+ [ztx _cfg {{docname :docname parent :parent} :params :as req} & opts]
+ {:status 200
+ :body (hiccup.core/html (view/editor ztx req {:zd/docname (cond docname (symbol docname)
+ parent (symbol (str parent ".<>" ))
+ :else 'new)} ""))})
+
+(defmethod zen/op 'zd/new-preview
+ [ztx _cfg {body :body :as req} & opts]
+ (let [docname 'new
+ content (if (= BytesInputStream (type body)) (slurp body) body)
+ doc (store/to-doc ztx docname content {})
+ doc (preview-doc ztx doc)]
+ {:status 200
+ :body (hiccup.core/html (view/preview ztx req doc))}))
+
+(defmethod zen/op 'zd/create-doc
+ [ztx _cfg { body :body} & opts]
+ (let [content (if (= BytesInputStream (type body)) (slurp body) body)
+ docname (store/extract-docname content)
+ doc (store/file-save ztx docname content)]
+ {:status 200
+ :body (str "/" (:zd/docname doc))}))
+
+
+(defmethod zen/op 'zd/save-doc
+ [ztx _cfg {{id :id} :route-params body :body :as req} & opts]
+ (let [docname (symbol id)
+ content (if (= BytesInputStream (type body)) (slurp body) body)
+ doc (store/file-save ztx docname content)]
+ {:status 200
+ :body (str "/" (:zd/docname doc))}))
+
+(defmethod zen/op 'zd/check-errors
+ [ztx _cfg req & opts]
+ (if-let [errs (seq (store/errors ztx))]
+ {:status 200
+ :body (str (count errs))}
+ {:status 404
+ :body "0"}))
+
+(defmethod zen/op 'zd/doc-content
+ [ztx config {{id :id} :route-params uri :uri hs :headers doc :doc :as req} & opts]
+ (let [docname (symbol (or id "index"))
+ doc (store/doc-get ztx docname)]
+ {:status 200
+ :headers {"Cache-Control" "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"}
+ :body (hiccup.core/html (view/preview ztx req doc))}))
+
+(defmethod zen/op 'zd/git-changes
+ [ztx _cfg {{:keys [id]} :route-params :as req} & opts]
+ (let [changes (git/changes ztx)
+ history (git/history ztx 100)]
+ {:status 200
+ :body (hiccup.core/html (view/timeline ztx req {:changes changes :history history}))}))
+
+(defmethod zen/op 'zd/git-commit
+ [ztx _cfg {params :params :as req} & opts]
+ {:status 200
+ :body (hiccup.core/html [:pre (pr-str (form-decode (slurp (:body req))))])})
+
+(defmethod zen/op 'zd/search-page
+ [ztx _cfg {params :params :as req} & opts]
+ (let [res (store/search ztx (:query params))]
+ {:status 200
+ :body (hiccup.core/html (view/search ztx req (assoc params :results res)))}))
+
+
+(defmethod zen/op 'zd/render-widget
+ [ztx _cfg {{id :id wgt :widget-id} :route-params r :root :as req} & opts]
+ (let [doc (store/doc-get ztx (symbol id))]
+ (if-not (nil? doc)
+ {:status 200
+ :body (hiccup.core/html (methods/widget ztx {:widget (keyword wgt) :root r :request req} doc))}
+ {:status 200
+ :body (hiccup.core/html [:div "Error: " id " is not found"])})))
+
+(defmethod zen/op 'zd/delete-doc
+ [ztx _cfg {{:keys [id]} :route-params :as req} & opts]
+ (let [docname (symbol id)]
+ (store/file-delete ztx docname)
+ {:status 200
+ :body (str (store/parent-name docname))}))
+
+
+(defmethod zen/start 'zd/zendoc
+ [ztx config & opts]
+ (println :zd/start config)
+ (swap! ztx update :zd/dir (fn [x] (or x (:dir config) "docs")))
+ (store/dir-load ztx (:dir config))
+ config)
+
+(defmethod zen/stop 'zd/zendoc
+ [ztx config state]
+ (println :zd/stop state)
+ (swap! ztx dissoc :zdb :zd/backlinks))
+
+(defmethod zen/op 'zd.events/logger
+ [ztx config {ev-name :ev :as ev} & opts]
+ (println (assoc ev ::ts (str (java.util.Date.)))))
+
+(defn start [& [dir gitsync]]
+ (let [ztx (zen/new-context {:zd/dir dir :zd/gitsync gitsync})]
+ (zen/read-ns ztx 'zd)
+ (zen/start-system ztx 'zd/system)
+ ztx))
+
+(defn stop [ztx]
+ (zen/stop-system ztx))
+
+(comment
+ (def ztx (start))
+
+ (stop ztx)
+
+ (:zd/backlinks @ztx)
+
+ (config ztx)
+ (store/re-validate ztx)
+
+ (edamame.core/parse-string "{%name name %type fhir/Patient}")
+
+ )
diff --git a/src/zd/datalog.clj b/src/zd/datalog.clj
index 370102b..c1adc9a 100644
--- a/src/zd/datalog.clj
+++ b/src/zd/datalog.clj
@@ -1,178 +1,203 @@
(ns zd.datalog
- (:require [zen.core :as zen]
+ (:require [xtdb.api :as xt]
[clojure.walk]
[clojure.string :as str]
- [edamame.core]
- [xtdb.api :as xt]))
-
-(defn get-state [ztx]
- (get-in @ztx [:zen/state :datalog :state]))
-
-(defn submit [ztx data]
- (if-let [{n :node} (get-state ztx)]
- (xt/submit-tx n [[::xt/put data]])
- :no/xtdb))
-
-(defn evict [ztx data]
- (if-let [{n :node} (get-state ztx)]
- (xt/submit-tx n [[::xt/evict data]])
- :no/xtdb))
-
-
-(defn query [ztx query & params]
- (if-let [{n :node} (get-state ztx)]
- (clojure.walk/postwalk
- (fn [x] (if (and (string? x) (str/starts-with? x "'"))
- (symbol (subs x 1))
- x))
- (apply xt/q (xt/db n) query params))
- :no/xtdb))
-
-(defn evict-by-query [ztx q]
- (doseq [res (query ztx q)]
- (evict ztx (str "'" (first res))))
- (xt/sync (:node (get-state ztx))))
-
-
-(defn flatten-doc [ztx {{dn :docname :as m} :zd/meta :as doc}]
- (let [meta (->> m
- (map (fn [[k v]] [(keyword "meta" (name k)) v]))
- (into {}))
- doc (-> (dissoc doc :zd/backlinks :zd/subdocs :zd/meta)
- (merge meta)
- (assoc :xt/id (str "'" (:docname m))))]
- (clojure.walk/postwalk (fn [x] (if (symbol? x) (str "'" x) x)) doc)))
-
-;; TODO rename to zd.datalog
-(defmethod zen/op 'zd/query
- [ztx config params & [session]]
- (query ztx params))
-
-(defmethod zen/op 'zd/submit
- [ztx _config params & [_session]]
- (submit ztx params))
-
-(defmethod zen/op 'zd.events/datalog-sync
- [ztx _config {_ev :ev doc :params} & [_session]]
- (let [xtdb-doc (flatten-doc ztx doc)
- result (submit ztx xtdb-doc)]
- ;; TODO und where does result go in pub/sub
- (doseq [[k sd] (:zd/subdocs doc)]
- (let [pid (get-in doc [:zd/meta :docname])
- id (str "'" pid "." (name k))]
- (submit ztx (assoc (flatten-doc ztx sd) :xt/id id :parent (str "'" pid) :zd/subdoc true))))
- result))
-
-(defmethod zen/op 'zd.events/datalog-delete
- [ztx _config {{dn :docname} :params} & [_session]]
- (cond
- (or (string? dn) (symbol? dn)) (evict ztx (str dn))))
-
-(defmethod zen/start 'zd.engines/datalog
- [ztx {zd-config :zendoc :as config} & opts]
- (let [{r :root} (zen/get-symbol ztx zd-config)]
- {:config config
- :root r
- :node (xt/start-node {:xtdb.lucene/lucene-store {}})}))
-
-(defmethod zen/stop 'zd.engines/datalog
- [ztx config {n :node}]
- (.close n))
-
+ [edamame.core]))
+
+
+(defmulti add-instruction (fn [acc k v] k))
+
+(defmethod add-instruction :limit
+ [acc k v]
+ (if (int? v)
+ (assoc acc :limit v)
+ acc))
+
+(defmethod add-instruction :desc
+ [acc k v]
+ (-> acc
+ (update :order-by (fn [x] (conj (or x []) [v :desc])))
+ (update :columns (fn [x] (conj (or x []) {:name v :hidden true})))))
+
+(defmethod add-instruction :asc
+ [acc k v]
+ (-> acc
+ (update :order-by (fn [x] (conj (or x []) [v :asc])))
+ (update :columns (fn [x] (conj (or x []) {:name v :hidden true})))))
+
+(defmethod add-instruction :default
+ [acc k v]
+ (println :unknown/instruction k v)
+ acc)
+
+(defn parse-instruction [acc l]
+ (let [[k v] (str/split (str/trim (subs l 1)) #"\s+" 2)
+ k (str/trim k)
+ v (try (edamame.core/parse-string v) (catch Exception _e (println :datalog.edn/error v) v))]
+ (if k
+ (add-instruction acc (keyword k) v)
+ acc)))
+
+(defn symbolize [x]
+ (clojure.walk/postwalk
+ (fn [y]
+ (if (and (keyword? y) (= "symbol" (namespace y)))
+ (str "'" (name y))
+ y)) x))
+
+(defn parse-condition [acc x]
+ (let [res (edamame.core/parse-string (str/replace (str "[" x "]") #"#" ":symbol/") {:regex true})]
+ (update acc :where conj (symbolize res))))
+
+(defn parse-select [acc x]
+ (let [x (str/trim (subs x 1))
+ [x lbl] (mapv str/trim (str/split x #"\|" 2))]
+ (if (str/starts-with? x "(")
+ (update acc :columns conj {:name 'expr
+ :expr (edamame.core/parse-string x {:regex true})
+ :label (or lbl x)})
+ (let [[e k] (str/split x #":" 2)
+ k (cond (= k "*") (symbol k) :else (keyword k))]
+ (update acc :columns conj {:name (symbol e)
+ :prop k
+ :label (or lbl e)})))))
+
+(defn auto-columns [query]
+ (if (and (seq (:where query)) (empty? (:columns query)))
+ (assoc query :columns
+ (->> (:where query)
+ (reduce (fn [fnd expr]
+ (->> expr
+ (reduce (fn [fnd s] (if (symbol? s)
+ (conj fnd {:name s :label (str s)})
+ fnd))
+ fnd)))
+ [])))
+ query))
+
+#_(->> (group-by :name (:columns query))
+ (reduce (fn [acc [k xs]]
+ (if (= 'expr k)
+ (->> (mapv :expr xs)
+ (reduce (fn [acc e]
+ (swap! index assoc e (count acc))
+ (conj acc e))
+ acc))
+ (let [cs (->> (mapv :prop xs) (dedupe) (into []))]
+ (swap! index assoc k (count acc))
+ (if (seq (filter (fn [x] (contains? #{'* :?} x)) cs))
+ (conj acc (list 'pull k ['*]))
+ (if (= cs [nil])
+ (conj acc k)
+ (conj acc (list 'pull k (mapv (fn [x] (if (nil? x) :xt/id x))cs))))))))
+ []))
+
+(defn make-find [query]
+ (let [index (atom {})
+ fnd (->> (:columns query)
+ (mapv (fn [{nm :name prop :prop expr :expr}]
+ (cond
+ (contains? #{'* :?} prop) (list 'pull nm ['*])
+ prop (list 'pull nm [prop])
+ expr expr
+ :else nm))))]
+ (assoc query :find fnd :index @index)))
(defn parse-query [q]
- (let [xs (->> (str/split q #"\n")
- (mapv str/trim)
- (remove (fn [s] (or (str/blank? s) (str/starts-with? s "\\")))))
- columns (->> xs
- (filterv #(re-matches #"^\s?>.*" %))
- (mapv #(subs % 1))
- (mapv str/trim)
- (filterv #(not (str/blank? %)))
- (mapv (fn [x]
- (if (str/starts-with? x "(")
- ['expr (edamame.core/parse-string x {:regex true})]
- (let [[e k] (str/split x #":" 2)]
- [(symbol e) (cond
- (= k "*") (symbol k)
- :else (keyword k))])))))
- index (atom {})
- find-items (->> (group-by first columns)
- (reduce (fn [acc [k xs]]
- (if (= 'expr k)
- (->> (mapv second xs)
- (reduce (fn [acc e]
- (swap! index assoc e (count acc))
- (conj acc e))
- acc))
- (let [cs (->> (mapv second xs) (dedupe) (into []))]
- (swap! index assoc k (count acc))
- (if (seq (filter (fn [x] (contains? #{'* :?} x)) cs))
- (conj acc (list 'pull k ['*]))
- (if (= cs [nil])
- (conj acc k)
- (conj acc (list 'pull k (mapv (fn [x] (if (nil? x) :xt/id x))cs))))))))
- []))
- where-items
- (->> xs
- (filterv (every-pred #(not (str/ends-with? % " :asc"))
- #(not (str/ends-with? % " :desc"))
- #(not (re-matches #"^\s?>.*" %))))
- (mapv (fn [x] (let [res (edamame.core/parse-string (str/replace (str "[" x "]") #"#" ":symbol/")
- {:regex true})]
- (cond
- (list? (get res 1))
- (vector res)
-
- :else
- res)
- ))))
- where (->> where-items
+ (let [query (loop [[l & ls] (str/split q #"\n")
+ acc {:where [] :columns []}]
+ (if (and (nil? l) (empty? ls))
+ acc
+ (let [l (str/trim l)]
+ (cond
+ (str/blank? l) (recur ls acc)
+ (str/starts-with? l "//") (recur ls acc)
+ (str/starts-with? l ">") (recur ls (parse-select acc l))
+ (str/starts-with? l "<") (recur ls (parse-instruction acc l))
+ :else (recur ls (parse-condition acc l))))))]
+ (-> query
+ (auto-columns)
+ (make-find))))
+
+;; dir - one
+;; index.zd - fixed entry point
+;; everything is sync
+
+(defn get-db [ztx]
+ (if-let [db (:db @ztx)]
+ db
+ (let [db (xt/start-node {})]
+ (swap! ztx assoc :db db)
+ db)))
+
+(defn encode-query [q]
+ (clojure.walk/postwalk (fn [x] (if (and (list? x) (= 'quote (first x)))
+ (str "'" (second x))
+ x)) q))
+
+(defn encode-data [q]
+ (clojure.walk/postwalk (fn [x] (if (symbol? x) (str "'" x) x)) q))
+
+(defn decode-data [res]
+ (clojure.walk/postwalk
+ (fn [x] (if (and (string? x) (str/starts-with? x "'")) (symbol (subs x 1)) x))
+ res))
+
+(defn datalog-put [ztx data]
+ (assert (:zd/docname data) (pr-str data))
+ (let [db (get-db ztx)
+ data (if (:xt/id data) data (assoc data :xt/id (:zd/docname data)))
+ res (xt/submit-tx db [[::xt/put (encode-data (dissoc data :zd/docname :zd/view))]])]
+ (xt/sync db)
+ res))
+
+(defn datalog-delete [ztx docname]
+ (let [db (get-db ztx)
+ res (xt/submit-tx db [[::xt/evict (str "'" docname)]])]
+ (xt/sync db)
+ res))
+
+(defn datalog-get [ztx id]
+ (let [db (get-db ztx)]
+ (decode-data (xt/entity (xt/db db) (str "'" id)))))
+
+;; cache based on database status
+(defn datalog-query
+ "run datalog query"
+ [ztx query & params]
+ (let [db (get-db ztx)]
+ (-> (apply xt/q (xt/db db) (encode-query query) params)
+ (decode-data))))
+
+;; (cond
+;; (nil? c) (or (get-in x [(get idx e) :xt/id]) (get-in x [(get idx e)]))
+;; (list? c) (get-in x [(get idx c)])
+;; (= c '*) (get-in x [(get idx e)])
+;; (= c :?) (keys (get-in x [(get idx e)]))
+;; :else (get-in x [(get idx e) c]))
+
+(defn datalog-sugar-query [ztx q]
+ (try
+ (let [q (parse-query q)
+ res (->> (datalog-query ztx (dissoc q :columns :index))
(mapv (fn [x]
- (clojure.walk/postwalk
- (fn [y]
- (if (and (keyword? y) (= "symbol" (namespace y)))
- (str "'" (name y))
- y)) x))))
-
- order-items
- (->> xs
- (filterv (every-pred #(or (str/ends-with? % " :asc")
- (str/ends-with? % " :desc"))
- #(not (re-matches #"^\s?>.*" %))))
- (mapv (fn [x] (edamame.core/parse-string (str/replace (str "[" x "]") #"#" ":symbol/") {:regex true}))))
-
- order
- (->> order-items
- (mapv (fn [x]
- (clojure.walk/postwalk
- (fn [y]
- (if (and (keyword? y) (= "symbol" (namespace y)))
- (str "'" (name y))
- y)) x))))]
- (into {:where where
- :order order
- :find find-items
- :columns columns
- :index @index} )))
-
-
-(defn sugar-query [ztx q]
- (let [q (parse-query q)
- _ (def q q)
- idx (:index q)
- res (->>
- (query ztx (dissoc q :columns :index))
- (mapv (fn [x]
- (->> (:columns q)
- (mapv (fn [[e c]]
- (cond
- (nil? c) (or (get-in x [(get idx e) :xt/id]) (get-in x [(get idx e)]))
- (list? c) (get-in x [(get idx c)])
- (= c '*) (get-in x [(get idx e)])
- (= c :?) (keys (get-in x [(get idx e)]))
- :else (get-in x [(get idx e) c]))))))))
- cols (->> (:columns q) (mapv second))]
- {:result res
- :query (dissoc q :columns :index)
- :columns cols}))
+ (loop [[{prop :prop :as c} & cs] (:columns q)
+ i 0
+ acc []]
+ (if (and (nil? c) (empty? cs))
+ acc
+ (cond (:hidden c)
+ (recur cs (inc i) acc)
+ (= :* prop)
+ (recur cs (inc i) (conj acc (get x i)))
+ (= :? prop)
+ (recur cs (inc i) (conj acc (keys (get x i))))
+ prop
+ (recur cs (inc i) (conj acc (get-in x [i prop])))
+ :else
+ (recur cs (inc i) (conj acc (get x i)))))))))]
+ {:result res
+ :query (dissoc q :columns :index)
+ :columns (->> (:columns q) (remove :hidden) (mapv :label))})
+ (catch Exception e
+ {:error (.getMessage e)})))
diff --git a/src/zd/fs/utils.clj b/src/zd/fs/utils.clj
deleted file mode 100644
index 821e6d3..0000000
--- a/src/zd/fs/utils.clj
+++ /dev/null
@@ -1,5 +0,0 @@
-(ns zd.fs.utils
- (:require [clojure.string :as str]))
-
-(defn docpath [docname]
- (str (str/replace (str docname) "." "/") ".zd"))
diff --git a/src/zd/git.clj b/src/zd/git.clj
new file mode 100644
index 0000000..e285925
--- /dev/null
+++ b/src/zd/git.clj
@@ -0,0 +1,249 @@
+(ns zd.git
+ (:require [clojure.java.io :as io]
+ [clojure.string :as str]
+ [stylo.core :refer [c]]
+ [zd.methods :as methods])
+ (:import [java.lang ProcessBuilder]
+ [java.util.concurrent TimeUnit]))
+
+(defn read-env [override]
+ (->
+ (->> (System/getenv)
+ (reduce (fn [acc [k v]]
+ (assoc acc (keyword k) v)) {}))
+ (merge override)))
+
+(defn read-stream [s]
+ (let [r (io/reader s)]
+ (loop [acc []]
+ (if-let [l (.readLine r)]
+ (recur (conj acc l))
+ acc))))
+
+(defn proc [{dir :dir env :env args :exec}]
+ (let [proc (ProcessBuilder. (into-array String args))
+ _ (when dir (.directory proc (io/file dir)))
+ _ (when env
+ (let [e (.environment proc)]
+ #_(.clear e)
+ (doseq [[k v] env]
+ (.put e (name k) (str v)))))]
+ proc))
+
+(defn exec [{dir :dir env :env args :exec :as opts}]
+ (let [prc (proc opts)
+ p (.start prc)
+ inp (.getInputStream p)
+ err (.getErrorStream p)]
+ (loop [stdout []
+ stderr []]
+ (if (.waitFor p 30 TimeUnit/SECONDS)
+ {:status (.exitValue p)
+ :stdout (into stdout (read-stream inp))
+ :stderr (into stderr (read-stream err))}
+ (recur (into stdout (read-stream inp))
+ (into stderr (read-stream err)))))))
+
+(defn run [opts]
+ (let [prc (proc opts)]
+ (.start prc)))
+
+(defn init-env [{gh-key :ZO_GH_KEY dir :ZO_DIR repo :ZO_REPO :as opts}]
+ {:GIT_SSH_COMMAND (format "ssh -i %s -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" gh-key)})
+
+(defn init-repo [{app :ZO_APP gh-key :ZO_GH_KEY dir :ZO_DIR repo :ZO_REPO :as opts}]
+ (assert dir "ZO_DIR")
+ (assert repo "ZO_REPO")
+ (assert gh-key "ZO_GH_KEY")
+ (assert app "ZO_APP")
+ (assert (.exists (io/file gh-key)))
+ (let [env (init-env opts)
+ _ (println :env env)
+ res (exec {:env env
+ :exec ["git" "clone" repo dir]
+ :dir "/tmp"})]
+ (println res)))
+
+(defn current-commit [{dir :ZO_DIR :as opts}]
+ (let [env (init-env opts)]
+ (-> (exec {:exec ["git" "rev-parse" "HEAD"] :env env :dir dir})
+ (get-in [:stdout 0]))))
+
+(defn start-process [opts]
+ (println "START PROCESSS!" (str/split (:ZO_APP opts) #"\s+"))
+ (let [dir (:ZO_DIR opts)
+ env (init-env opts)
+ args (str/split (:ZO_APP opts) #"\s+")]
+ (exec {:exec ["git" "submodule" "init"] :env env :dir dir})
+ (exec {:exec ["git" "submodule" "update"] :env env :dir dir})
+ (let [prc (proc {:exec args :dir dir})]
+ (.inheritIO prc)
+ (.start prc))))
+
+(defn start [{port :PORT gh-key :ZO_GH_KEY dir :ZO_DIR repo :ZO_REPO timeout :ZO_TIMEOUT :as opts}]
+ (let [timeout (if timeout (Integer/parseInt timeout) 10000)
+ dir (io/file dir)
+ env (init-env opts)]
+ (println (exec {:exec ["cp" gh-key "/tmp/gh-key"]}))
+ (println (exec {:exec ["chmod" "400" "/tmp/gh-key"]}))
+ (println (exec {:exec ["ls" "-lah" "/tmp/gh-key"]}))
+ (let [opts (assoc opts :ZO_GH_KEY "/tmp/gh-key")
+ env (init-env opts)]
+ (when-not (.exists dir) (init-repo opts))
+ (prn "Repo inited.")
+ (loop [version (current-commit opts)
+ p (start-process opts)]
+ (exec {:exec ["git" "pull" "--rebase"] :env env :dir dir})
+ (print "*") (flush)
+ (let [test-version (current-commit opts)]
+ (if (= test-version version)
+ (do
+ (Thread/sleep timeout)
+ (recur version p))
+ (do
+ (println :restart-app test-version)
+ (println "DESTROY" (.destroy p))
+ (let [p (start-process opts)]
+ (println :start p)
+ (Thread/sleep timeout)
+ (recur test-version p)))))))))
+
+;; TODO try to re impl using java git
+
+(defn create-timeline-data
+ [[[_ author date] comment files]]
+ (let [author (or author "")
+ comment (or comment "")
+ files (or files "")
+ date (or date "")]
+ {:comment (-> comment
+ first
+ clojure.string/trim)
+
+ :user (-> author
+ (clojure.string/split #"\s")
+ second)
+
+ :email (-> author
+ (clojure.string/split #"\s")
+ last
+ rest
+ butlast
+ (clojure.string/join))
+
+ :time (-> date
+ (clojure.string/split #"\s")
+ rest
+ (->> (interpose " ")
+ (apply str)
+ (clojure.string/trim)))
+
+ :files (vec files)}))
+
+(defn get-history
+ []
+ (->> (exec {:exec ["git" "log"
+ "--name-only"
+ "--date=format-local:%Y-%m-%d %H:%M"
+ "--no-merges"
+ "-n" "30"]})
+ :stdout
+ (partition-by empty?)
+ (remove (fn [x] (-> x first empty?)))
+ (partition 3)
+ (mapv create-timeline-data)
+ (group-by (fn [l] (first (str/split (:time l) #"\s" 2))))))
+
+(defn gh-index [ztx]
+ (->> (map second (:zdb @ztx))
+ (filter (fn [x]
+ (str/starts-with? (name (get-in x [:zd/meta :docname])) "people.")))
+ (reduce (fn [acc {ghn :git/names gh :github :as res}]
+ (if (or gh ghn)
+ (->> ghn
+ (reduce (fn [acc nm]
+ (assoc acc nm res))
+ (assoc acc gh res)))
+ acc)))))
+
+(defn dir [ztx] (:zd/dir @ztx))
+
+(defn init [ztx]
+ (exec {:dir (dir ztx) :exec ["git" "init"]}))
+
+
+(defn commit [ztx message]
+ (exec {:dir (dir ztx) :exec ["git" "add" "."]})
+ (exec {:dir (dir ztx) :exec ["git" "commit" "-m" message]}))
+
+(defn history [ztx & [limit]]
+ (->> (exec {:dir (dir ztx)
+ :exec ["git" "log"
+ "--name-only"
+ "--date=format-local:%Y-%m-%d %H:%M"
+ "--no-merges"
+ "-n" (str (or limit 30))
+ ]})
+ :stdout
+ (partition-by empty?)
+ (remove (fn [x] (-> x first empty?)))
+ (partition 3)
+ (mapv create-timeline-data)
+ (group-by (fn [l] (first (str/split (:time l) #"\s" 2))))
+ (sort-by first)
+ (reverse)
+ (mapv (fn [[date commits]] {:date date :commits commits}))))
+
+(defn changes [ztx]
+ (->> (exec {:dir (dir ztx)
+ :exec ["git" "status" "-s"]})
+ :stdout
+ (mapv (fn [s]
+ (let [[change file] (str/split (str/trim s) #"\s+" 2)]
+ {:change (get {"??" :new "M" :modified "D" :deleted} change) :file file})))))
+
+
+(defn pull [ztx])
+
+
+;; (defn gh-user [ztx gh-idx l]
+;; (if-let [u (get gh-idx (when-let [un (:user l)] (str/trim un)))]
+;; (link/symbol-link ztx (:zd/name u))
+;; [:b (or (:user l) (:email l))]))
+
+;; (defmethod methods/renderkey :git/timeline
+;; [ztx {ps :paths :as ctx} block]
+;; (let [gh-idx (gh-index ztx)]
+;; [:div
+;; ;; TODO show file deletion separately
+;; (for [[date ls] (->> (get-history)
+;; (sort-by first)
+;; (reverse))]
+;; [:div
+;; [:div {:class (c :border-b :font-bold [:mt 2])} date]
+;; (for [l (reverse (sort-by :time ls))]
+;; [:div
+;; [:div {:class (c :flex :items-baseline [:space-x 2] [:ml 0] [:py 1])}
+;; [:div {:class (c [:text :gray-600])} (last (str/split (:time l) #"\s" 2))]
+;; [:div (gh-user ztx gh-idx l)]
+;; [:div (:comment l)]]
+;; [:ul {:class (c [:ml 6])}
+;; (->> (:files l)
+;; (filter (fn [x]
+;; (some #(str/starts-with? x %) ps)))
+;; (map (fn [x]
+;; (let [docname (->> ps
+;; (map (fn [p]
+;; (let [to-replace
+;; (if (str/ends-with? p "/")
+;; p
+;; (str p "/"))]
+;; (str/replace x to-replace ""))))
+;; (filter #(not= % x))
+;; (first))]
+;; (symbol (-> docname
+;; (str/replace "/" ".")
+;; (str/replace ".zd" ""))))))
+;; (sort)
+;; (mapv (fn [x] [:li (link/symbol-link ztx x)]))
+;; (apply conj [:div]))]])])]))
diff --git a/src/zd/link.clj b/src/zd/link.clj
deleted file mode 100644
index f11189b..0000000
--- a/src/zd/link.clj
+++ /dev/null
@@ -1,50 +0,0 @@
-(ns zd.link
- (:require
- [stylo.core :refer [c]]
- [zd.memstore :as memstore]
- [clojure.string :as str]))
-
-(defn get-parent [ztx res]
- (when-let [nm (:zd/name res)]
- (let [pn (->> (str/split (str nm) #"\.")
- (butlast)
- (str/join "."))]
- (when-not (str/blank? pn)
- (or (memstore/get-doc ztx (symbol pn))
- {:zd/name (symbol pn)})))))
-
-(defn resolve-icon [ztx res]
- (if-let [ava (or (get-in res [:avatar]) (get-in res [:logo]))]
- {:type :img :img ava}
- (if-let [icon (get res :icon)]
- {:type :ico :icon icon}
- (when-let [parent (get-parent ztx res)]
- (resolve-icon ztx parent)))))
-
-(defn icon [ztx res]
- (when-let [icon (resolve-icon ztx res)]
- (cond (= (:type icon) :img)
- [:img {:src (:img icon)
- :class (c :inline-block [:mr 1]
- [:h 4] [:w 4]
- :border
- {:border-radius "100%"
- :margin-bottom "1px"})}]
- (= (:type icon) :ico)
- [:i {:class (str (str/join " " (map name (:icon icon))) " " (name (c [:mr 1] [:text :gray-500])))}])))
-
-(defn symbol-link [ztx s & [opts]]
- (if-let [res (memstore/get-doc ztx (symbol s))]
- [:a {:href (str "/" s) :class (c :inline-flex :items-center [:text "#4B5BA0"] [:hover [:underline]] :whitespace-no-wrap)}
- (icon ztx res)
- (when-not (:compact opts)
- (or (:title res) s))]
- (let [parts (str/split (str s) #"\.")
- ss (str/join "." (butlast parts))
- sub (last parts)]
- (if-let [sres (when-let [p (memstore/get-doc ztx (symbol ss))]
- (get-in p [:zd/subdocs (keyword sub)]))]
- [:a {:href (str "/" ss "#subdocs-" sub) :class (c :inline-flex :items-center [:text "#4B5BA0"] [:hover [:underline]] :whitespace-no-wrap)}
- (icon ztx sres)
- (when-not (:compact opts) (or (:title sres) s))]
- [:a {:href (str "/" s) :class (c [:text :red-600] [:bg :red-100]) :title "Broken Link"} s]))))
diff --git a/src/zd/methods.clj b/src/zd/methods.clj
index dfa2326..3f58aad 100644
--- a/src/zd/methods.clj
+++ b/src/zd/methods.clj
@@ -6,56 +6,45 @@
[stylo.core :refer [c]]
[zd.methods :as methods]))
+(defmulti annotation (fn [name params] name))
+
+(defmethod annotation :badge [nm params] {:as :badge})
+
+(defmethod annotation :link-badge [nm params] {:as :badge})
+
+(defmethod annotation :hide [nm params] {:as :none})
+
+(defmethod annotation :block [nm params] {:as :block})
+
+(defmethod annotation :attribute [nm params] {:as :attribute})
+
+(defmethod annotation :default [nm params] (assoc {} nm params))
+
+
+(defmulti do-parse (fn [ctx tp s] tp))
+(defmethod do-parse :default [ctx _tp s] (str/trim s))
+
;; renders content of a block with :zd/content-type annotation
-(defmulti rendercontent (fn [ztx ctx block]
- (get-in block [:ann :zd/content-type])))
+(defmulti rendercontent (fn [ztx ctx block] (or (get-in block [:annotations :type])
+ (type (:data block)))))
-;; by default just pretty prints the content
(defmethod rendercontent :default
- [ztx ctx {:keys [data] :as block}]
- [:span (with-out-str (pprint/pprint data))])
+ [ztx ctx {data :data annotations :annotations doc :doc :as block}]
+ [:div
+ [:b (pr-str annotations)]
+ [:span (with-out-str (pprint/pprint data))]])
+
+(defmulti renderkey (fn [ztx ctx {data :data annotations :annotations doc :doc :as block}] (:key block)))
-;; renders key of a document with provided annotation
-;; or by a block name
-(defmulti renderkey (fn [ztx ctx block]
- (:key block)))
+(defmethod renderkey :icon [& args])
+(defmethod renderkey :zd/icon [& args])
(defn get-anns [block]
(->> (:ann block)
(remove (fn [[k _]]
(= "zd" (namespace k)))) ))
-(defmulti renderann (fn [ztx ctx block]
- ;; TODO pub error if more then 1 ann?
- (when-let [[block-key _] (first (get-anns block))]
- block-key)))
-
-;; by default add a header and renders content of a block
-(defmethod renderkey :default [ztx ctx {kp :key d :data anns :ann :as block}]
- ;; TODO fix render inline for bb run
- ;; TODO think if render inline is usable at all
- (let [render-inline?
- (and (not (:zd/multiline anns))
- (= (:zd/content-type anns) :edn)
- (not (map? d)))
- basic-style (c [:py 1] [:mb "0.8rem"] :border-b)
- embedded-style (c :flex :flex-row :items-center)
- multiline-embedded (c :flex :flex-row :items-baseline [:py 4])
- cnt (when-not (and (string? d) (str/blank? d))
- (rendercontent ztx ctx block))]
- (if (seq (get-anns block))
- (methods/renderann ztx ctx (assoc block :content cnt))
- [:div {:class (c [:py 4])}
- [:div {:class (if (:zd/render-subdoc? anns)
- embedded-style
- basic-style)}
- [:span {:class (c :uppercase)} ":"]
- [:a {:id kp}
- [:span {:class (c :uppercase {:font-weight "600"})} kp]]]
- ;; TODO think about rendering flow
- (try (hiccup/html cnt)
- (catch Exception e
- (with-out-str (pprint/pprint cnt))))])))
+(defmulti renderann (fn [ztx ctx block] (get-in block [:annotations :as])))
;; zentext methods
(defmulti inline-method (fn [ztx m arg ctx] (keyword m)))
diff --git a/src/zd/parser.clj b/src/zd/parser.clj
new file mode 100644
index 0000000..1fcb634
--- /dev/null
+++ b/src/zd/parser.clj
@@ -0,0 +1,251 @@
+(ns zd.parser
+ (:require
+ [clojure.string :as str]
+ [edamame.core :as reader]
+ [clojure.walk]
+ [zd.methods :as methods]
+ [clojure.java.io :as io])
+ (:import [java.io StringReader]))
+
+(defn get-lines [s]
+ (line-seq (io/reader (StringReader. s))))
+
+(re-matches #":([^ ]*)\s+([^/]*)/(.*)$" ":key zen/ text")
+(re-matches #":([^ ]*)\s+([^/]*)/(.*)$" ":key /")
+(re-matches #":([^ ]*)\s+([^/]*)/(.*)$" ":key text")
+
+
+(defn parse-annotation [s]
+ (let [[nm s] (str/split (str/trim (subs s 1)) #"\s+" 2)
+ v (if (re-matches #"^(\[|\{|\"|:).*" (or s ""))
+ (try
+ (edamame.core/parse-string s)
+ (catch Exception e s))
+ s)]
+ [(keyword nm) v]))
+
+(parse-annotation "^title Here is some title")
+(parse-annotation "^title \"Here is some title\"")
+(parse-annotation "^title {:a 1}")
+(parse-annotation "^title [1 2 3]")
+
+(defmethod methods/do-parse :unknown [_ _ s] s)
+(defmethod methods/do-parse :str [_ _ s] (str/trim s))
+(defmethod methods/do-parse :zentext [_ _ s] (str/trim s))
+(defmethod methods/do-parse :? [_ _ s] (str/trim s))
+
+(defmethod methods/do-parse
+ :edn [{parent :zd/parent} _ s]
+ (try
+ (->> (edamame.core/parse-string s)
+ (clojure.walk/postwalk (fn [x] (if (= x '.) parent x))))
+ (catch Exception e
+ (str "Error(" (.getMessage e) ")" s))))
+
+(defn parse-key-name [l]
+ (if-let [[_ k tp] (re-matches #":([^ ]*)\s+([^/]*)/$" l)]
+ [(keyword k) (if (= "" tp) :zentext (keyword tp)) "" true]
+ (if-let [[_ k s] (re-matches #":([^ ]*)\s+\|(.*)$" l)]
+ [(keyword k) :str (str/trim s) false]
+ (let [[k l] (str/split l #"\s" 2)]
+ [(keyword (subs k 1)) :edn l false]))))
+
+(defn parse-key [{anns :anns [l & ls] :key} docname parent]
+ (let [[k tp l multiline] (parse-key-name l)
+ annotations (->> anns
+ (reduce
+ (fn [acc ls]
+ (let [s (str/join "\n" ls)
+ [nm v] (parse-annotation s)
+ ann (methods/annotation (keyword nm) v)]
+ (merge acc ann)))
+ (cond-> {:type tp} multiline (assoc :multiline true))))
+ data (if multiline (->> (into [l] ls) (str/join "\n")) l)
+ value (methods/do-parse {:docname docname :zd/parent parent}
+ (or (:type annotations) :unknown) data)]
+ {:key k
+ :value value
+ :annotations annotations}))
+
+
+(defn parse-doc-name [s docname i]
+ (let [[_ nm _ tp] (re-matches #"^&([^ ]*)(\s+(.*))?$" s)
+ tp (when tp (try (edamame.core/parse-string tp)
+ (catch Exception _
+ (println :error/parsing-subdoc s)
+ nil)))]
+ [(symbol (str docname "." (if (str/blank? nm) (str "doc-" i) nm))) tp]))
+
+(parse-doc-name "&name" 'doc 1)
+(parse-doc-name "&name type" 'doc 1)
+(parse-doc-name "&name #{a.b c.d}" 'doc 1)
+(parse-doc-name "& type" 'doc 1)
+(parse-doc-name "&" 'doc 1)
+(parse-doc-name "& " 'doc 1)
+
+(defn parse-doc [{nm :name ks :keys} rootname i]
+ (let [[docname tp] (if nm (parse-doc-name nm rootname i) [rootname nil])]
+ (->> ks
+ (reduce (fn [acc k]
+ (let [{k :key ann :annotations v :value} (parse-key k docname rootname)]
+ (-> acc
+ (assoc k v)
+ (update :zd/view conj [k ann]))))
+ (cond-> {:zd/view []
+ :zd/docname docname}
+ nm (assoc :zd/subdoc? true :zd/parent rootname)
+ tp (assoc :zd/type tp :zd/view [[:zd/type {:type :edn}]]))))))
+
+;; :start
+;; ^ -> annotation | :ann
+;; : -> key | :key or :zentext
+;; other -> ignore | :start
+;; :ann
+;; ^ -> new annotation | :ann
+;; : -> key | :key or zentext
+;; other -> conj to current annotation | :ann
+;; :zentext
+;; ^ -> finalize key | :ann
+;; : -> finalize key | :key
+;; ``` -> start | :block
+;; other -> add to key | :key
+;; :block
+;; ``` -> finalize block
+;; other -> add to block | block
+;; :key
+;; ^ -> finalize key | :ann
+;; : -> finalize key | :key
+;; other -> add to key | :key
+
+(defn close-annotation [ctx]
+ (if-let [ann (seq (:ann ctx))]
+ (-> ctx
+ (update :anns conj ann)
+ (assoc :ann []))
+ ctx))
+
+(defn add-to-annotation [ctx l]
+ (update ctx :ann conj l))
+
+(defn close-key [ctx]
+ (if (seq (:key ctx))
+ (-> ctx
+ (update :keys conj (select-keys ctx [:anns :key]))
+ (assoc :key [] :ann [] :anns []))
+ ctx))
+
+(defn add-to-key [ctx l]
+ (update ctx :key conj l))
+
+(defn split-keys [lines docname]
+ (loop [[l & ls :as pls] lines
+ state :start
+ ctx {:anns []
+ :ann []
+ :key []
+ :keys []}]
+ (if (nil? l)
+ (cond-> (:keys ctx)
+ (seq (:key ctx)) (conj (select-keys ctx [:anns :key])))
+ (cond
+ (= state :start)
+ (cond
+ (str/starts-with? l "^")
+ (let [ctx (add-to-annotation ctx l)]
+ (recur ls :ann ctx))
+
+ (str/starts-with? l ":")
+ (let [ctx (-> (close-annotation ctx)
+ (assoc :key [l]))]
+ (if (str/ends-with? (str/trim l) "/")
+ (let [state (if (str/ends-with? (str/trim l) " /") :zentext :key)]
+ (recur ls state ctx))
+ (let [ctx (close-key ctx)]
+ (recur ls :start ctx))))
+
+ (str/blank? l) (recur ls :start ctx)
+
+ :else (do (println :parser/ignore docname (pr-str l))
+ (recur ls :start ctx)))
+
+ (= state :ann)
+ (cond
+ (str/starts-with? l "^")
+ (let [ctx (-> (close-annotation ctx)
+ (add-to-annotation l))]
+ (recur ls :ann ctx))
+
+ (str/starts-with? l ":")
+ (let [ctx (close-annotation ctx)]
+ (recur pls :start ctx))
+
+ :else
+ (let [ctx (add-to-annotation ctx l)]
+ (recur ls state ctx)))
+
+ (= state :key)
+ (cond
+ (or (str/starts-with? l "^") (str/starts-with? l ":"))
+ (let [ctx (close-key ctx)]
+ (recur pls :start ctx))
+
+ :else
+ (let [ctx (add-to-key ctx l)]
+ (recur ls :key ctx)))
+
+ (= state :zentext)
+ (cond
+ (or (str/starts-with? l "^") (str/starts-with? l ":"))
+ (let [ctx (close-key ctx)]
+ (recur pls :start ctx))
+
+ (str/starts-with? l "```")
+ (let [ctx (add-to-key ctx l)]
+ (recur ls :block ctx))
+
+ :else
+ (let [ctx (add-to-key ctx l)]
+ (recur ls :zentext ctx)))
+
+ (= state :block)
+ (cond
+ (str/starts-with? l "```")
+ (let [ctx (add-to-key ctx l)]
+ (recur ls :zentext ctx))
+
+ :else
+ (let [ctx (add-to-key ctx l)]
+ (recur ls :block ctx)))
+
+ :else
+ (do
+ (println :parser/ignore docname l)
+ (recur ls state ctx))))))
+
+(defn split-docs [lines]
+ (loop [[l & ls] lines
+ cur {:lines []}
+ acc []]
+ (if (nil? l)
+ (conj acc cur)
+ (if (str/starts-with? l "&")
+ (recur ls {:name l :lines []} (conj acc cur))
+ (recur ls (update cur :lines conj l) acc)))))
+
+(split-docs ["a" "b" "&c" "d" "&e" "f"])
+
+;; split into documents
+(defn parse [ztx docname text & [meta]]
+ (let [lines (->> (get-lines text)
+ (remove #(str/starts-with? % ";;"))
+ (mapv str/trimr))
+ [doc & subdocs] (->> (split-docs lines)
+ (map-indexed (fn [i doc]
+ (-> doc
+ (dissoc :lines)
+ (assoc :keys (split-keys (:lines doc) docname))
+ (parse-doc docname i)
+ ;; TODO: handle parent
+ (merge (dissoc meta :zd/parent))
+ (update :zd/parent (fn [x] (or x (:zd/parent meta))))))))]
+ (cond-> doc (seq subdocs) (assoc :zd/subdocs (into [] subdocs)))))
diff --git a/src/zd/schema.clj b/src/zd/schema.clj
new file mode 100644
index 0000000..9cd979d
--- /dev/null
+++ b/src/zd/schema.clj
@@ -0,0 +1,184 @@
+(ns zd.schema
+ (:require [clojure.string :as str]))
+
+
+(defn to-keyname [docname]
+ (let [parts (str/split (str docname) #"\.")
+ ns (str/join "." (butlast parts))]
+ (if (= ns "_")
+ (keyword (last parts))
+ (keyword ns (last parts)))))
+
+;; TODO: implement other API
+(defn add-class [ztx {docname :zd/docname :as doc}]
+ (swap! ztx assoc-in [:zd/classes docname] doc)
+ doc)
+
+(defn remove-class [ztx docname]
+ (swap! ztx update :zd/classes dissoc docname)
+ docname)
+
+(defn get-class [ztx docname]
+ (or (get-in @ztx [:zd/classes docname])
+ (get-in @ztx [:zdb docname])))
+
+(defn add-prop [ztx {docname :zd/docname :as doc}]
+ (let [keyname (to-keyname docname)]
+ (swap! ztx assoc-in [:zd/props keyname] doc)
+ (swap! ztx update :zd/keys (fn [ks] (conj (or ks #{}) keyname)))
+ doc))
+
+(defn remove-prop [ztx docname]
+ (let [keyname (to-keyname docname)]
+ (swap! ztx update :zd/props dissoc keyname)
+ docname))
+
+(defn get-prop [ztx keyname]
+ (get-in @ztx [:zd/props keyname]))
+
+(defn infere [ztx doc]
+ (if-let [tp (and (nil? (:zd/type doc))
+ (when-let [p (:zd/parent doc)]
+ (or (get-in @ztx [:zdb p :zd/child-type])
+ (get-in @ztx [:zd/classes p :zd/child-type]))))]
+ (-> doc
+ (assoc :zd/type tp)
+ (update :zd/infered (fn [x] (conj (or x []) :zd/type))))
+ doc))
+
+(defmulti validate-rule (fn [ztx errors rules rule-name rule-value k v] rule-name))
+(defmulti validate-type (fn [ztx errors rules type-name k v] type-name))
+
+(defmethod validate-rule :default
+ [ztx errors rules rule-name rule-value k v]
+ errors)
+
+(defmethod validate-type :default
+ [ztx errors rules type-name k v]
+ (println :unknown/type type-name)
+ errors)
+
+(defmethod validate-type 'zd.string
+ [ztx errors rules _ k v]
+ (if (and v (not (string? v)))
+ (conj errors {:type :type :path [k] :message (str "Expected string, got " (type v))})
+ errors))
+
+
+(defmethod validate-type 'zd.boolean
+ [ztx errors rules _ k v]
+ (if (and v (not (boolean? v)))
+ (conj errors {:type :type :path [k] :message (str "Expected boolean, got " (type v))})
+ errors))
+
+(defmethod validate-type 'zd.number
+ [ztx errors rules _ k v]
+ (if (and v (not (number? v)))
+ (conj errors {:type :type :path [k] :message (str "Expected number, got " (type v))})
+ errors))
+
+(defmethod validate-type 'zd.int
+ [ztx errors rules _ k v]
+ (if (and v (not (int? v)))
+ (conj errors {:type :type :path [k] :message (str "Expected int, got " (type v))})
+ errors))
+
+(defmethod validate-type 'zd.symbol
+ [ztx errors rules _ k v]
+ (if (and v (not (symbol? v)))
+ (conj errors {:type :type :path [k] :message (str "Expected symbol, got " (type v))})
+ errors))
+
+(defmethod validate-type 'zd.date
+ [ztx errors rules _ k v]
+ (if (and v (not (inst? v)))
+ (conj errors {:type :type :path [k] :message (str "Expected inst, got " (type v))})
+ errors))
+
+(defmethod validate-type 'zd.keyword
+ [ztx errors rules _ k v]
+ (if (and v (not (keyword? v)))
+ (conj errors {:type :type :path [k] :message (str "Expected keyword, got " (type v))})
+ errors))
+
+(defmethod validate-rule :zd/data-type
+ [ztx errors rules _rule-name rule-value k v]
+ (cond
+ (and (:zd/maybe-set? rules) (set? v))
+ (->> v (reduce (fn [errors v] (validate-type ztx errors rules rule-value k v)) errors))
+
+ (and (:zd/maybe-vector? rules) (sequential? v))
+ (->> v (reduce (fn [errors v] (validate-type ztx errors rules rule-value k v)) errors))
+
+ (:zd/set? rules)
+ (if (not (set? v))
+ (conj errors {:type :type :path [k] :message (str "Expected set, got " (type v))})
+ (->> v (reduce (fn [errors v] (validate-type ztx errors rules rule-value k v)) errors)))
+
+ (:zd/vector? rules)
+ (if (not (sequential? v))
+ (conj errors {:type :type :path [k] :message (str "Expected vector, got " (type v))})
+ (->> v (reduce (fn [errors v] (validate-type ztx errors rules rule-value k v)) errors)))
+
+ :else
+ (validate-type ztx errors rules rule-value k v)))
+
+(defn validate-prop [ztx errors prop-schema k v]
+ (->> prop-schema
+ (reduce (fn [errors [rule-name rule-value]]
+ (validate-rule ztx errors prop-schema rule-name rule-value k v))
+ errors)))
+
+
+;; TODO: fix this hack with empty schema
+(defn get-schema [ztx doc]
+ (when-let [docnames (:zd/type doc)]
+ (let [docnames (if (symbol? docnames) #{docnames} docnames)]
+ (->> docnames
+ (map (fn [docname] (get-class ztx docname)))
+ (filter identity)
+ (seq)))))
+
+
+(defn validate-refs [ztx errors k v]
+ (cond (and (symbol? v) (not (get-in @ztx [:zdb v])))
+ (conj errors {:type :reference :message (str "`" v "` not found") :path [k]})
+
+ (set? v)
+ (->> v
+ (reduce (fn [errors v]
+ (if (and (symbol? v) (not (get-in @ztx [:zdb v])))
+ (conj errors {:type :reference :message (str "`" v "` not found") :path [k]})
+ errors))
+ errors))
+ :else errors))
+
+;; TODO one walk over data (not per schema)
+(defn validate [ztx doc]
+ (let [schemas (get-schema ztx doc)
+ errors (->> schemas
+ (reduce (fn [errors schema]
+ (->> (:zd/require schema)
+ (reduce (fn [errors prop]
+ (if (not (contains? doc prop))
+ (conj errors {:type :required
+ :message (str prop " is required")
+ :path [prop]})
+ errors))
+ errors)))
+ []))]
+ (->> doc
+ (reduce (fn [errors [k v]]
+ (let [errors (if-let [prop-schema (get-prop ztx k)]
+ (validate-prop ztx errors prop-schema k v)
+ errors)
+ errors (validate-refs ztx errors k v)]
+ errors))
+ errors))))
+
+
+(defn summary [ztx schema & [doc]]
+ (when-let [summary (get-in @ztx [:zd/classes schema :zd/summary])]
+ (if doc
+ (select-keys doc summary)
+ summary)))
diff --git a/src/zd/store.clj b/src/zd/store.clj
new file mode 100644
index 0000000..d6a6f4e
--- /dev/null
+++ b/src/zd/store.clj
@@ -0,0 +1,549 @@
+(ns zd.store
+ (:require [zen.core :as zen]
+ [clj-jgit.porcelain :as git]
+ [zd.parser]
+ [xtdb.api :as xt]
+ [clojure.walk]
+ [clojure.java.io :as io]
+ [zd.zentext]
+ [zd.schema]
+ [zd.datalog :as datalog]
+ [edamame.core]
+ [clojure.set]
+ [clojure.string :as str]))
+
+;; dir - one
+;; index.zd - fixed entry point
+;; everything is sync
+
+(defn *docname-to-path [docname]
+ (-> (->> (str/split (str docname) #"\.") (str/join "/"))
+ (str ".zd")))
+
+(def ident
+ ;; TODO support for other os ?
+ {:name ["id_rsa" "id_dsa" "id_ecdsa" "id_ed25519" "pubkey"]
+ :trust-all? true
+ :key-dir (if (.isDirectory (io/file "keystore"))
+ (str (System/getProperty "user.dir") "/keystore")
+ (str (System/getProperty "user.home") "/.ssh"))})
+
+(def docname-to-path (memoize *docname-to-path))
+
+(defn *path-to-docname [path]
+ (-> (str/replace path #"\.zd$" "")
+ (str/split #"/")
+ (->> (str/join "."))
+ (symbol)))
+
+(def datalog-query datalog/datalog-query)
+(def datalog-sugar-query datalog/datalog-sugar-query)
+(def encode-data datalog/encode-data)
+(def decode-data datalog/decode-data)
+(def datalog-get datalog/datalog-get)
+
+(def path-to-docname (memoize *path-to-docname))
+
+(defn child-docname [docname child]
+ (symbol (str docname "." (name child))))
+
+(defn parent-name [docname]
+ (let [parts (str/split (str docname) #"\.")
+ parent (butlast parts)]
+ (if (= 'zd docname)
+ 'zd
+ (if (empty? parent)
+ 'index
+ (symbol (str/join "." parent))))))
+
+(defn parent-dir [filename]
+ (let [parts (str/split (str filename) #"/")
+ parent (butlast parts)]
+ (str/join "/" parent)))
+
+;; (parent-name 'a.b.c)
+;; (parent-name 'a)
+;; (parent-name 'a.b)
+
+
+(defn get-reference
+ "return reference for docs and subdocs"
+ [ztx docname])
+
+(defn get-doc
+ [ztx docname]
+ (get-in @ztx [:zdb docname]))
+
+
+(defn errors-clear [ztx docname]
+ (swap! ztx update :zd/errors dissoc docname))
+
+(defn backlinks-clear [ztx docname]
+ (swap! ztx update :zd/backlinks
+ (fn [bl]
+ (->> bl
+ (reduce (fn [bl [target refs]]
+ (let [refs' (reduce (fn [refs [k v]]
+ (if (= k docname)
+ refs
+ (assoc refs k v)))
+ {} refs)]
+ (if (seq refs')
+ (assoc bl target refs')
+ bl)))
+ {})))))
+
+(defn delete-doc
+ [ztx docname]
+ (swap! ztx update :zdb dissoc docname))
+
+(defn symbolize-subdocs [doc]
+ (if-let [subdocs (seq (:zd/subdocs doc))]
+ (assoc doc :zd/subdocs (mapv :zd/docname subdocs))
+ doc))
+
+(defn put-doc
+ [ztx {docname :zd/docname :as doc}]
+ (if (and doc docname)
+ (swap! ztx assoc-in [:zdb docname] (assoc doc :zd/parent (parent-name docname)))
+ (println :put/error doc))
+ doc)
+
+(defn walk-docs
+ "call (f docname doc)"
+ [ztx f]
+ (doseq [[docname doc] (:zdb @ztx)]
+ (f docname doc)))
+
+(defn update-docs [ztx f]
+ (doseq [[docname doc] (:zdb @ztx)]
+ (if docname
+ (f docname doc)
+ (println :bad-doc doc))))
+
+(defn put-errors [ztx docname errors]
+ (if (seq errors)
+ (swap! ztx assoc-in [:zd/errors docname] errors)
+ (swap! ztx update :zd/errors dissoc docname)))
+
+(defn clear-menu [ztx docname]
+ (swap! ztx update :zd/menu dissoc docname))
+
+(defn update-menu [ztx {docname :zd/docname :as doc}]
+ (if-let [mo (or (:zd/menu-order doc) (:menu-order doc))]
+ (swap! ztx update :zd/menu assoc docname (assoc doc :zd/menu-order mo))
+ (clear-menu ztx docname)))
+
+
+(defn get-errors [ztx docname]
+ (get-in @ztx [:zd/errors docname]))
+
+
+(defn doc-validate
+ "validate document"
+ [ztx doc]
+ (zd.schema/validate ztx doc))
+
+
+(defn validate-doc [ztx docname & [doc]]
+ (let [doc (or (get-doc ztx docname) doc)
+ errors (doc-validate ztx doc)]
+ (put-errors ztx docname errors)
+ errors))
+
+(defn re-validate
+ "re-validate broken resources"
+ [ztx]
+ (swap! ztx assoc :zd/errors {})
+ (walk-docs ztx (fn [docname _doc] (validate-doc ztx docname))))
+
+(defn doc-inference
+ "run inference"
+ [ztx doc]
+ (zd.schema/infere ztx doc))
+
+
+(defn get-backlinks [ztx target]
+ (->> (get-in @ztx [:zd/backlinks target])
+ (reduce (fn [acc [docname attrs]]
+ (->> attrs
+ (reduce (fn [acc path]
+ (let [doc (get-doc ztx docname)
+ ztype (:zd/type doc)]
+ (if (not (and (:zd/subdoc? doc) (= target (:zd/parent doc))))
+ (if (coll? ztype)
+ (->> ztype
+ (reduce (fn [acc ztype]
+ (update acc (if ztype (conj path ztype) path)
+ (fn [xs] (sort (conj (or xs []) docname)))))
+ acc))
+ (update acc (if ztype (conj path ztype) path)
+ (fn [xs] (sort (conj (or xs []) docname)))))
+ acc)))
+ acc))) {})))
+
+(defn backlinked [ztx docname]
+ (->> (get-in @ztx [:zd/backlinks docname])
+ (keys)
+ (into #{})))
+
+
+(defn schema-clear [ztx docname]
+ (zd.schema/remove-class ztx docname)
+ (zd.schema/remove-prop ztx docname))
+
+(defn get-type [doc]
+ (when-let [tp (:zd/type doc)]
+ (cond (symbol? tp) #{tp}
+ (set? tp) tp
+ :else nil)))
+
+(defn update-schema [ztx doc]
+ (cond (contains? (get-type doc) 'zd.class)
+ (zd.schema/add-class ztx doc)
+
+ (contains? (get-type doc) 'zd.prop)
+ (zd.schema/add-prop ztx doc)))
+
+(defn schema [ztx type-name]
+ (get-in @ztx [:zd/schema type-name]))
+
+
+;; emit delete event
+(defn doc-delete
+ "delete document"
+ [ztx docname]
+ (let [doc (get-doc ztx docname)
+ backlinks (backlinked ztx docname)]
+ (datalog/datalog-delete ztx docname)
+ (delete-doc ztx docname)
+ (backlinks-clear ztx docname)
+ (errors-clear ztx docname)
+ (schema-clear ztx docname)
+ ;; revalidate docs looking at this doc
+ (->> backlinks (mapv #(validate-doc ztx %)))))
+
+(defn to-doc
+ "return docs from text representation"
+ [ztx docname content & [{docpath :docpath lm :last-modified parent :zd/parent}]]
+ (zd.parser/parse ztx docname content (cond-> {}
+ parent (assoc :zd/parent parent)
+ docpath (assoc :zd/file docpath) lm (assoc :zd/last-modified lm))))
+
+(defn file-content [ztx docname]
+ (let [docpath (str (:zd/dir @ztx) "/" (docname-to-path docname))]
+ (when (.exists (io/file docpath))
+ (slurp docpath))))
+
+;; TODO remove dir param
+(defn file-read
+ "read file and return vector of doc and subdocs"
+ [ztx dir path & [opts]]
+ (let [docpath (str dir "/" path)
+ docname (path-to-docname path)
+ content (slurp docpath)]
+ (to-doc ztx docname content (merge {:docpath docpath :zd/parent (parent-name docname)} opts))))
+
+
+(defn doc-get
+ "get document from memory, validate, add backlinks etc"
+ [ztx docname]
+ (when-let [doc (get-doc ztx docname)]
+ (let [errors (get-errors ztx docname)
+ backlinks (get-backlinks ztx docname)
+ subdocs (->> (:zd/subdocs doc)
+ (mapv #(doc-get ztx %)))]
+ (when doc
+ (cond-> (get-doc ztx docname)
+ (seq errors) (assoc :zd/errors errors)
+ (seq backlinks) (assoc :zd/backlinks backlinks)
+ (seq subdocs) (assoc :zd/subdocs subdocs))))))
+
+(defn doc-summary
+ "return doc summary based on class"
+ [ztx docname])
+
+(defn summary
+ "return doc summary based on class"
+ [ztx schemaname]
+ (zd.schema/summary ztx schemaname))
+
+(defn edn-links [acc docname path node]
+ (cond
+ (and (not (= :zd/subdocs (first path))) (symbol? node) (not (= node docname)))
+ (update-in acc [node docname] (fnil conj #{}) path)
+
+ (map? node)
+ (reduce (fn [acc [k v]] (edn-links acc docname (conj path k) v)) acc node)
+
+ (set? node)
+ (reduce #(edn-links %1 docname path %2) acc node)
+
+ (and (sequential? node) (not (list? node)))
+ (->> (map-indexed vector node)
+ (reduce (fn [acc [idx v]] (edn-links acc docname (conj path idx) v)) acc))
+
+ :else acc))
+(defn zentext-links [acc docname doc]
+ (->> (:zd/view doc)
+ (reduce (fn [acc [k {tp :type}]]
+ (if-let [s (and (= tp :zentext) (get doc k))]
+ (->> (zd.zentext/extract-links s)
+ (reduce (fn [acc l] (update-in acc [l docname] (fnil conj #{}) [k])) acc))
+ acc))
+ acc)))
+
+(defn collect-links [{docname :zd/docname :as doc}]
+ (-> {}
+ (edn-links docname [] doc)
+ (zentext-links docname doc)))
+
+(defn update-backlinks [ztx doc]
+ (let [links (collect-links doc)]
+ (swap! ztx update :zd/backlinks (fn [ls] (merge-with merge ls links)))))
+
+(defn update-keys-index [ztx doc]
+ (swap! ztx update :zd/keys (fn [ks] (into (or ks #{}) (keys doc)))))
+
+
+(defn re-index-doc [ztx {docname :zd/docname :as doc} & [{dont-validate :dont-validate}]]
+ (put-doc ztx doc)
+ (datalog/datalog-put ztx doc)
+ (update-backlinks ztx doc)
+ (update-menu ztx doc)
+ (update-keys-index ztx doc)
+ (update-schema ztx doc)
+ ;; this used while initial load - load then validate
+ (when (not dont-validate)
+ (validate-doc ztx docname)))
+
+;; emit save event
+(defn doc-save
+ "upsert document into memory databases & indexes"
+ [ztx {docname :zd/docname :as doc} {dont-validate :dont-validate :as opts}]
+ (let [idoc (doc-inference ztx doc)]
+ (put-doc ztx idoc)
+ (re-index-doc ztx idoc opts)
+ idoc))
+
+(defn children [ztx docname]
+ (when-let [links (get-in @ztx [:zd/backlinks docname])]
+ (->> links
+ (reduce (fn [acc [doc props]]
+ (->> props
+ (reduce (fn [acc prop]
+ (if (= [:zd/parent] prop)
+ (conj acc doc)
+ acc))
+ acc)))
+ #{}))))
+
+(defn commit-delete
+ "remove file from git index and push new commit"
+ [ztx docpath docname]
+ (when-let [repo (:zd/repo @ztx)]
+ (git/with-identity ident
+ (git/git-pull repo {:ff-mode :ff :rebase-mode :none :strategy :ours})
+ (let [{:keys [missing]} (git/git-status repo)
+ git-config (git/git-config-load repo)
+ uname (or (.getString git-config "user" nil "name") "unknown editor")
+ email (or (.getString git-config "user" nil "email") "unknown-editor@zendoc.me")]
+ (doseq [m missing]
+ (when (str/includes? docpath m)
+ (git/git-rm repo m)
+ (git/git-commit repo (str "Delete " docname) :committer {:name uname :email email}))))
+ (future (git/git-push repo)))))
+
+(defn file-delete
+ "save document content into file and recalculate databases"
+ [ztx docname]
+ (let [dir (:zd/dir @ztx)
+ path (docname-to-path docname)
+ docpath (str dir "/" path)
+ doc (get-doc ztx docname)
+ file (io/file docpath)
+ filedir (io/file (str/replace docname #"\.zd$" ""))]
+ (->> (:zd/subdocs doc)
+ (mapv (fn [docname] (doc-delete ztx docname))))
+ (doc-delete ztx docname)
+ (clear-menu ztx docname)
+ (->> (children ztx docname)
+ (mapv (fn [child] (file-delete ztx child))))
+ (when (.exists file) (.delete file))
+ (when (.exists filedir) (.delete filedir))
+ (commit-delete ztx docpath docname)
+ (->> (backlinked ztx docname)
+ (mapv (fn [d] (validate-doc ztx d))))
+ doc))
+
+;; TODO: this dirty think a better way
+(defn extract-docname
+ "look for zd/docname in content"
+ [content]
+ (let [lines (zd.parser/get-lines content)
+ docname-line (->> lines (filter #(str/starts-with? % ":zd/docname")) (first))
+ docname (when docname-line (when-let [s (-> docname-line (str/split #"\s+") (second))] (symbol s)))
+ content' (->> lines
+ (remove #(str/starts-with? % ":zd/docname"))
+ (str/join "\n"))]
+ [(when docname (symbol docname)) content']))
+
+(extract-docname ":a 1\n:zd/docname docname\n:b 1")
+
+(defn commit-changes
+ "commit added, changed files and push"
+ [ztx docpath docname]
+ (when-let [repo (:zd/repo @ztx)]
+ (git/with-identity ident
+ (let [;; TODO sync all untracked docs at gitsync start?
+ {:keys [untracked modified] :as status} (git/git-status repo)
+ git-config (git/git-config-load repo)]
+ (git/git-pull repo {:ff-mode :ff :rebase-mode :none :strategy :ours})
+ (doseq [m (into untracked modified)]
+ (when (str/includes? docpath m)
+ (let [uname (or (.getString git-config "user" nil "name") "unknown editor")
+ email (or (.getString git-config "user" nil "email") "unknown-editor@zendoc.me")]
+ (git/git-add repo m)
+ (let [msg (if (contains? untracked m)
+ (str "Create " docname)
+ (str "Edit " docname))]
+ (git/git-commit repo msg :committer {:name uname :email email}))))
+ (future (git/git-push repo)))))))
+
+(defn file-save
+ "save document content into file and recalculate databases"
+ [ztx docname content & [{dont-validate :dont-validate rename :rename :as opts}]]
+ (let [[new-docname content] (extract-docname content)
+ new-docname (or new-docname rename docname)
+ dir (:zd/dir @ztx)
+ path (docname-to-path new-docname)
+ docpath (str dir "/" path)
+ doc (to-doc ztx new-docname content {:docpath docpath :zd/parent (parent-name new-docname)})
+ doc' (symbolize-subdocs doc)]
+ (when-let [old-doc (get-doc ztx docname)]
+ (if (not (= new-docname docname))
+ (do
+ (->> (children ztx docname)
+ (mapv (fn [childname]
+ (let [new-childname (symbol (str new-docname (subs (str childname) (count (str docname)))))]
+ (file-save ztx childname (file-content ztx childname) {:rename new-childname})))))
+ (file-delete ztx docname))
+ ;; calculate removed subdocs
+ (let [to-remove (clojure.set/difference (into #{} (:zd/subdocs old-doc)) (into #{} (:zd/subdocs doc')))]
+ (->> to-remove (mapv #(doc-delete ztx %))))))
+ (.mkdirs (io/file (parent-dir docpath)))
+ (spit docpath content)
+ (commit-changes ztx docpath new-docname)
+ (doc-save ztx doc' opts)
+ (->> (:zd/subdocs doc)
+ (mapv (fn [subdoc]
+ (doc-save ztx subdoc opts)
+ (->> (backlinked ztx (:zd/docname subdoc))
+ (mapv (fn [d] (validate-doc ztx d)))))))
+ ;; fix broken links to this doc
+ (->> (backlinked ztx new-docname)
+ (mapv (fn [d] (validate-doc ztx d))))
+ doc))
+
+(defn dir-read
+ "read docs from filesystem"
+ [ztx & [dir]]
+ (let [dir (or dir (:zd/dir @ztx))
+ dir (io/file dir)
+ dir-path (.getPath dir)
+ docs (->> (file-seq dir)
+ (map (fn [f]
+ (let [p (.getPath f)]
+ (when (and (str/ends-with? p ".zd") (.exists f))
+ (let [path (subs p (inc (count dir-path)))]
+ (file-read ztx dir-path path {:last-modified (.lastModified f)}))))))
+ (filter identity))]
+ docs))
+
+(defn load-meta
+ "load zd schema"
+ [ztx]
+ (let [doc (-> (to-doc ztx 'zd (slurp (io/resource "zd.zd")) {:zd/parent 'zd})
+ (assoc :zd/readonly true :zd/parent 'zd))]
+ (put-doc ztx (symbolize-subdocs doc))
+ (->> (:zd/subdocs doc)
+ (mapv #(put-doc ztx %)))
+ (put-doc ztx {:zd/docname 'errors
+ :zd/view [ [:title] [:zd/all-errors]]
+ :zd/icon [:fa-solid :fa-triangle-exclamation]
+ :title "Errors"
+ :zd/all-errors true})))
+
+(defn load-repo [ztx]
+ (when (:zd/gitsync @ztx)
+ (git/with-identity ident
+ (let [repo (git/load-repo (System/getProperty "user.dir"))
+ pull-result (git/git-pull repo {:ff-mode :ff :rebase-mode :none :strategy :ours})]
+ (when (.isSuccessful pull-result)
+ (swap! ztx assoc :zd/repo repo))))))
+
+(defn dir-load
+ "read docs from filesystem and load into memory"
+ [ztx & [dir]]
+ (let [dir (or dir (:zd/dir @ztx))]
+ (load-meta ztx)
+ (load-repo ztx)
+ (->> (dir-read ztx dir)
+ (mapv (fn [doc]
+ (put-doc ztx (symbolize-subdocs doc))
+ (->> (:zd/subdocs doc)
+ (mapv #(put-doc ztx %))))))
+ (update-docs ztx
+ (fn [_docname doc]
+ (let [idoc (doc-inference ztx doc)]
+ (re-index-doc ztx idoc)
+ idoc)))))
+
+(defn menu
+ "return navigation"
+ [ztx]
+ (->> (vals (get @ztx :zd/menu))
+ (sort-by (fn [x]
+ (let [mo (:zd/menu-order x)]
+ [(if (number? mo) mo 100) (str (:zd/docname x))])))))
+
+(defn errors
+ "return all errors"
+ [ztx]
+ (get @ztx :zd/errors))
+
+
+(defn breadcrump [ztx docname]
+ (let [parts (str/split (str docname) #"\.")]
+ (loop [[p & ps] parts nm [] acc []]
+ (if (nil? p)
+ acc
+ (recur ps (conj nm p) (conj acc (symbol (str/join "." (conj nm p)))))))))
+
+(defn props
+ "return all used props (keys)"
+ [ztx]
+ (->> (:zd/keys @ztx)
+ (mapv (fn [k] {:name (str k)}))))
+
+
+(defn annotations [ztx] )
+
+(defn symbols [ztx]
+ (->> (:zdb @ztx)
+ (mapv (fn [[k {old-ico :icon ico :zd/icon logo :logo tit :title}]]
+ {:title tit
+ :name k
+ :logo logo
+ :icon (or old-ico ico)}))))
+
+(defn search [ztx query]
+ (let [parts (str/split (str/lower-case (str/trim (or query ""))) #"\s+")
+ regex (re-pattern (str/join ".*" parts))]
+ (->> (:zdb @ztx)
+ (filter (fn [[k {t :title d :desc :as doc}]]
+ (and k (re-find regex (str/lower-case (str t " " k " " d))))))
+ (map (fn [[id doc]]
+ (-> doc (assoc :zd/docname id))))
+ (sort-by :zd/docname)
+ (take 30))))
diff --git a/src/zd/utils.clj b/src/zd/utils.clj
index b0afd84..e35ece5 100644
--- a/src/zd/utils.clj
+++ b/src/zd/utils.clj
@@ -10,3 +10,28 @@
(let [err* (assoc err :message (.getMessage e) #_:trace #_(.getStackTrace e))]
(zen/pub ztx 'zd.events/safecall-error {:err err*})
{:type :zd.utils/safecall :error err*})))))
+
+(defn deep-merge
+ "efficient deep merge"
+ ([a b & more]
+ (apply deep-merge (deep-merge a b) more))
+ ([a b]
+ (cond
+ (and (map? a) (map? b))
+ (loop [[[k v :as i] & ks] b, acc a]
+ (if (nil? i)
+ acc
+ (let [av (get a k)]
+ (if (= v av)
+ (recur ks acc)
+ (recur ks
+ (cond
+ (and (map? v) (map? av)) (assoc acc k (deep-merge av v))
+ (and (set? v) (set? av)) (assoc acc k (into v av))
+ (and (nil? v) (map? av)) (assoc acc k av)
+ :else (assoc acc k v)))))))
+ (and (nil? a) (map? b)) b
+ (and (nil? b) (map? a)) b
+ (and (nil? b) (nil? a)) nil
+ :else
+ (do (println :error "deep-merge type missmatch: " a b) b))))
diff --git a/src/zd/view/core.clj b/src/zd/view/core.clj
new file mode 100644
index 0000000..2a726bc
--- /dev/null
+++ b/src/zd/view/core.clj
@@ -0,0 +1,35 @@
+(ns zd.view.core
+ (:require
+ [stylo.core :refer [c]]
+ [zd.store :as store]
+ [clojure.string :as str]
+ [zd.view.doc]
+ [zd.view.zentext]
+ [zd.view.datalog]
+ [zd.view.menu]
+ [zd.view.multimedia]
+ [zd.view.timeline :as timeline]
+ [zd.view.editor]
+ [zd.view.layout :as layout]
+ [zd.view.errors]
+ [zd.view.utils]
+ [zd.view.search :as search]
+ [hiccup.core]))
+
+(defn preview [ztx ctx doc]
+ (zd.view.doc/preview ztx ctx doc))
+
+(defn view [ztx ctx doc]
+ (zd.view.doc/view ztx ctx doc))
+
+(defn page [ztx ctx doc]
+ (layout/layout-with-menu ztx ctx doc (view ztx ctx doc)))
+
+(defn editor [ztx ctx doc content]
+ (layout/layout ztx ctx (zd.view.editor/editor ztx ctx doc content)))
+
+(defn timeline [ztx ctx data]
+ (layout/layout-with-menu ztx ctx {:zd/docname 'git} (timeline/view ztx data)))
+
+(defn search [ztx ctx params]
+ (layout/layout-with-menu ztx ctx {:zd/docname 'search} (search/search-view ztx params)))
diff --git a/src/zd/view/datalog.clj b/src/zd/view/datalog.clj
new file mode 100644
index 0000000..7a5b5e0
--- /dev/null
+++ b/src/zd/view/datalog.clj
@@ -0,0 +1,41 @@
+(ns zd.view.datalog
+ (:require [zd.view.utils :as utils]
+ [zd.zentext :as zentext]
+ [clojure.string :as str]
+ [clojure.pprint]
+ [zd.store :as store]
+ [stylo.core :refer [c]]
+ [zd.methods :as methods]))
+
+(defn render-table-value [ztx v block]
+ (cond
+ (symbol? v) (utils/symbol-link ztx v)
+ (string? v) (zentext/parse-block ztx v block)
+ (number? v) v
+ (nil? v) "~"
+ (set? v) [:div {:class (c :flex :items-center [:space-x 2])}
+ (->> v (map (fn [x] [:div {:key (str x)} (render-table-value ztx x block)])))]
+ (keyword? v) (str v)
+ (inst? v) (str/replace (str v) #"00:00:00 " "")
+ :else (with-out-str (clojure.pprint/pprint v))))
+
+(defmethod methods/rendercontent :?
+ [ztx _ctx {data :data :as block}]
+ (let [{err :error res :result cols :columns q :query} (store/datalog-sugar-query ztx data)]
+ (when err [:div {:class (c [:bg :red-100] [:border :red-500] [:p 4])} err])
+ [:div
+ [:table
+ (into [:thead]
+ (->> cols
+ (map (fn [col]
+ [:th {:class (c [:py 1] [:px 2] [:bg :gray-100] :border {:font-weight "500"})}
+ (cond (keyword? col) (name col)
+ (nil? col) "~"
+ :else (str col))]))))
+ (into [:tbody]
+ (for [vs res]
+ [:tr {:key (hash vs)}
+ (for [v vs]
+ [:td {:key v :class (c :border [:px 2] [:py 1] {:vertical-align "top"})}
+ (render-table-value ztx v block)])]))]
+ (utils/pprint "query" q)]))
diff --git a/src/zd/view/doc.clj b/src/zd/view/doc.clj
new file mode 100644
index 0000000..8cb68ef
--- /dev/null
+++ b/src/zd/view/doc.clj
@@ -0,0 +1,228 @@
+(ns zd.view.doc
+ (:require [zd.methods :as methods]
+ [clojure.pprint]
+ [clojure.string :as str]
+ [zd.view.utils :as utils]
+ [zd.view.topbar :as topbar]
+ [zd.store :as store]
+ [stylo.core :refer [c]]))
+
+(defmethod methods/renderkey :logo [& args])
+
+(defmethod methods/renderkey :title
+ [ztx ctx {doc :doc title :data :as block}]
+ [:h1 {:class (c :flex :items-center [:m 0] [:pt 2] [:pb 1] [:border-b :gray-400] [:mb 4] :text-2xl) :id "title"}
+ (if-let [img (or (:avatar doc) (:logo doc))]
+ [:img {:src img
+ :class (c [:w 8] [:h 8] :inline-block [:mr 2] {:border-radius "100%"})}]
+ (when-let [icon (or (:icon doc) (:zd/icon doc))]
+ [:i {:class (str (str/join " " (map name icon)) " " (name (c [:mr 2] [:text :gray-600])))}]))
+ title])
+
+(defmethod methods/renderkey :desc
+ [ztx ctx block]
+ [:div {:class (c {:opacity 0.8})}
+ (methods/rendercontent ztx ctx block)])
+
+(defn render-edn [ztx ctx data]
+ (cond
+ (or (string? data) (number? data)) (str data)
+ (symbol? data) (utils/symbol-link ztx data)
+ (inst? data) (str data)
+ (keyword? data) [:span {:class (c [:text :green-700])} (str data)]
+
+ (coll? data)
+ (->> data
+ (mapv (fn [x] [:div (render-edn ztx ctx x)]))
+ (into [:div {:class (c :flex [:space-x 2] {:flex-wrap "wrap"})}
+ [:div {:class (c [:text :gray-500] :text-sm)} "#"]]))
+ :else
+ [:div {:style {:background "white" :word-wrap "break-word"}}
+ (if (string? data)
+ data
+ (with-out-str (clojure.pprint/pprint data)))]))
+
+(defmethod methods/rendercontent :edn
+ [ztx ctx {:keys [data] :as block}]
+ (render-edn ztx ctx data))
+
+(defmethod methods/renderann :table
+ [ztx ctx {{headers :table} :ann k :key d :data cnt :content {cnt-type :zd/content-type} :ann :as block}]
+ (let [pull-query? (and (= :datalog cnt-type) (set? cnt) (vector? (first cnt)) (map? (ffirst cnt)) (seq headers))
+ edn-table? (and (= :edn cnt-type) (vector? d) (every? map? d))]
+ [:div {:class (c [:py 4])}
+ [:div {:class (c [:py 1] [:mb "0.8rem"] :border-b)}
+ [:span {:class (c :uppercase)} ":"]
+ [:a {:id k}
+ [:span {:class (c :uppercase {:font-weight "600"})} k]]]
+ (cond pull-query? (utils/table ztx ctx headers (map first cnt))
+ edn-table? (utils/table ztx ctx (set (mapcat keys d)) d)
+ :else [:div
+ [:div (str "No table impl for " k ", " cnt-type)]
+ [:div (pr-str d)]])]))
+
+
+(defmethod methods/renderann :link-badge
+ [ztx ctx {data :data k :key}]
+ [:div {:class (c :border [:m 1] :inline-flex :rounded [:p 0])}
+ [:a {:href data
+ :target "_blank"
+ :class (c :inline-block
+ [:bg :gray-100]
+ [:hover [:bg :gray-200]]
+ [:px 2]
+ [:py 0.5]
+ [:text "#4B5BA0"]
+ :text-sm
+ {:font-weight "400" :border-radius "4px 0 0 4px"})}
+ k]])
+
+(defmethod methods/renderann :badge
+ [ztx ctx {key :key :as block}]
+ [:div {:class (c :border [:my 1] [:mr 2] :inline-flex :items-center :rounded :shadow-sm)}
+ [:div {:class (c :inline-block [:px 2] [:bg :gray-100] [:py 0.5] :text-sm [:text :gray-700]
+ :border-r
+ {:font-weight "400" :border-radius "4px 0 0 4px"})}
+ key]
+ [:div {:class (c [:px 1] [:py 0.5] :inline-block :text-sm)}
+ (methods/rendercontent ztx ctx block)]])
+
+(defn badge [ztx ctx key data]
+ [:div {:class (c :border [:my 1] [:mr 2] :inline-flex :items-center :rounded :shadow-sm)}
+ [:div {:class (c :inline-block [:px 2] [:bg :gray-100] [:py 0.5] :text-sm [:text :gray-700]
+ :border-r
+ {:font-weight "400" :border-radius "4px 0 0 4px"
+ :opacity "0.6"})}
+ key]
+ [:div {:class (c [:px 1] [:py 0.5] :inline-block :text-sm)}
+ (render-edn ztx ctx data)]])
+
+(defmethod methods/renderann :attribute
+ [ztx ctx {k :key :as block}]
+ [:div {:title "attribute" :class (c [:py 0.5] :flex :items-center [:space-x 4])}
+ [:div {:class (c [:text :gray-600] {:font-weight "500"})} k]
+ [:div (methods/rendercontent ztx ctx block)]])
+
+(defmethod methods/renderann :none
+ [ztx ctx block])
+
+(defmethod methods/renderann :hide
+ [ztx ctx block])
+
+(defmethod methods/renderann :default
+ [ztx ctx {kp :key d :data :as block}]
+ (let [basic-style (c [:pb 0.2] [:pt 1.5] [:mb 3] :text-lg :border-b)]
+ [:div {:class (c [:py 4] )}
+ [:div {:class basic-style}
+ [:a {:id kp :href (str "#" (name kp)) :class (c {:font-weight "600"})} (name kp)]]
+ ;; [:pre (pr-str annotations)]
+ (methods/rendercontent ztx ctx block)]))
+
+(defmethod methods/renderkey :default
+ [ztx ctx block]
+ (methods/renderann ztx ctx (update block :annotations (fn [ann]
+ (if (and (contains? #{:edn :str} (:type ann)) (nil? (:as ann)))
+ (assoc ann :as :badge)
+ ann)))))
+
+(def h2 (c :text-xl :font-bold [:py 1] :border-b [:mt 2]))
+
+(def th-c (c [:px 2] [:py 1] :border [:bg :gray-100]))
+(def td-c (c [:px 2] [:py 1] :border))
+
+;; TODO: move this logic to store
+(defn backlinks-block [ztx backlinks]
+ [:div
+ ;; [:div {:class h2} "Backlinks:"]
+ (->> backlinks
+ (sort-by #(str (first %)))
+ (map (fn [[path docnames]]
+ (when-let [docs (->> docnames (sort) (seq))]
+ [:div {:class (c [:mb 4] [:mt 2])}
+ [:div {:class (c [:mb 2] [:pt 1] [:pb 0.5] :flex :items-center :border-b :font-bold [:text :gray-700] [:space-x 2])}
+ [:i.fa-solid.fa-arrow-left-to-line]
+ (let [tp (last path)]
+ (if (symbol? tp)
+ (list
+ [:div (str/join " " (butlast path))]
+ (utils/symbol-link ztx tp))
+
+ [:div (str/join " " path)]))]
+ (if-let [summary (store/summary ztx (last path))]
+ [:table
+ [:thead
+ [:th {:class th-c} "Doc"]
+ (->> summary
+ (map (fn [col]
+ [:th {:class th-c :title (str col)} (name col)])))]
+ [:tbody
+ (->> docs
+ (map (fn [docname]
+ [:tr
+ [:td {:class td-c} (utils/symbol-link ztx docname)]
+ (let [doc (store/get-doc ztx docname)]
+ (->> summary
+ (map (fn [col]
+ [:td {:class td-c :title (str col)}
+ (if-let [v (get doc col)]
+ (render-edn ztx {} v)
+ "~")]))))])))]]
+ [:div
+ (->> docs
+ (map (fn [docname]
+ [:div {:class (c {:border-bottom "1px dotted #f1f1f1"})}
+ (utils/menu-link ztx docname)])))])]))))])
+
+(declare document)
+
+(defn localname [s]
+ (last (str/split (str s) #"\.")))
+
+(defn subdocs-block [ztx ctx subdocs]
+ [:div
+ ;; [:div {:class h2} "Subdocs:"]
+ (->> subdocs
+ (map (fn [doc]
+ [:div {:class (c :border [:px 4] [:py 2] [:my 2] :rounded :shadow-sm)}
+ [:a {:id (str "subdoc-" (:zd/docname doc))
+ :class (c :block :font-bold :border-b [:pt 1] [:pb 0.5] [:text :gray-600])}
+ "&" (localname (:zd/docname doc))]
+ (document ztx ctx doc)])))])
+
+
+(defn errors-view [ztx errors]
+ [:div {:class (c [:text :red-700] [:my 2] [:pb 2] :rounded :text-sm [:border :red-300])}
+ [:ul {:class (c :font-bold [:mb 1] [:py 1] [:px 3] [:ml 0] [:text :red-600] [:bg :red-100] [:border-b :red-300])} "Document errors"]
+ (for [err (sort-by :type errors)]
+ [:li {:class (c [:py 0.5] :flex [:space-x 3] [:text :red-600] [:px 3])}
+ [:span (pr-str (:path err))]
+ [:span {:class (c [:ml 4] {:text-align "right"})} (:message err)]])])
+
+(defn document [ztx ctx doc]
+ [:div
+ (when-let [errors (seq (:zd/errors doc))]
+ (errors-view ztx errors))
+ (->> (:zd/infered doc)
+ (map (fn [x] (badge ztx ctx x (get doc x)))))
+ (->> (:zd/view doc)
+ (map (fn [[k annotations]]
+ (methods/renderkey ztx ctx {:doc doc
+ :key k
+ :data (get doc k)
+ :annotations annotations}))))
+ (when-let [backlinks (seq (:zd/backlinks doc))]
+ (backlinks-block ztx backlinks))
+ (when-let [subdocs (seq (:zd/subdocs doc))]
+ (subdocs-block ztx ctx subdocs))
+ (utils/pprint "doc" (dissoc doc :zd/view))])
+
+(defn view [ztx ctx doc]
+ [:div {:class (c [:w 200])}
+ [:div (topbar/topbar ztx ctx doc)]
+ (document ztx ctx doc)])
+
+(defn preview [ztx ctx doc]
+ [:div {:class (c [:w 200])}
+ (document ztx ctx doc)])
+
+;; TODO: choose between badge and block based on (type: edn)-> badge; others -> block
diff --git a/src/zd/view/editor.clj b/src/zd/view/editor.clj
new file mode 100644
index 0000000..727b555
--- /dev/null
+++ b/src/zd/view/editor.clj
@@ -0,0 +1,21 @@
+(ns zd.view.editor
+ (:require [cheshire.core :as json]
+ [hiccup.core]
+ [hiccup.util]
+ [zd.view.icons :as icons]
+ [zd.store :as store]))
+
+
+;; TODO: templates
+(defn editor [ztx ctx {docname :zd/docname :as doc} content]
+ (let [header (str ":zd/docname " (:zd/docname doc) "\n")
+ text (str header content)
+ symbols (store/symbols ztx)
+ anns (store/annotations ztx)
+ zendoc {:text text
+ :symbols symbols
+ :keys (store/props ztx)
+ :icons icons/icons
+ :annotations anns
+ :doc (:zd/docname doc)}]
+ [:script#editor-config (str "var zendoc=" (json/generate-string zendoc) ";")]))
diff --git a/src/zd/view/errors.clj b/src/zd/view/errors.clj
new file mode 100644
index 0000000..4ef49f4
--- /dev/null
+++ b/src/zd/view/errors.clj
@@ -0,0 +1,22 @@
+(ns zd.view.errors
+ (:require
+ [stylo.core :refer [c]]
+ [zd.store :as store]
+ [zd.view.utils :as utils]
+ [zd.methods :as methods]))
+
+(defmethod methods/renderkey :zd/all-errors
+ [ztx _ctx _block]
+ [:div {:class (c)}
+ (for [[docname errors] (->> (store/errors ztx)
+ (sort-by #(str (first %))))]
+ [:div {:class (c [:py 1] [:mb 1])}
+ [:div {:class (c [:pt 1] [:pb 0.5] :border-b [:mb 1])}
+ [:b (utils/symbol-link ztx docname)]
+ " "
+ (str docname)]
+ (for [e errors]
+ [:div {:class (c :flex [:space-x 3] :borer-b :items-baseline)}
+ [:div "⚬ " (str (:type e))]
+ [:div (str (:path e))]
+ [:div (:message e)]])])])
diff --git a/src/zd/view/icons.clj b/src/zd/view/icons.clj
new file mode 100644
index 0000000..3963199
--- /dev/null
+++ b/src/zd/view/icons.clj
@@ -0,0 +1,10 @@
+(ns zd.view.icons
+ (:require [clj-yaml.core]
+ [clojure.java.io :as io]))
+
+(def icons
+ (->> (clj-yaml.core/parse-string (slurp (io/resource "icons.yml")))
+ (mapcat (fn [[k x]]
+ (->> (:styles x)
+ (mapv (fn [x] {:name (format "[:fa-%s :fa-%s]" x (name k))
+ :icon [(str "fa-" x) (str "fa-" (name k))]})))))))
diff --git a/src/zd/view/layout.clj b/src/zd/view/layout.clj
new file mode 100644
index 0000000..dac58a2
--- /dev/null
+++ b/src/zd/view/layout.clj
@@ -0,0 +1,130 @@
+(ns zd.view.layout
+ (:require
+ [garden.core]
+ [stylo.core :refer [c]]
+ [stylo.rule :refer [join-rules]]
+ [zd.view.menu]))
+
+(defn c* [& args]
+ (join-rules args))
+
+;; TODO groom these global styles, delete unused css
+(def common-style
+ [:body {:font-family "sohne, \"Helvetica Neue\", Helvetica, Arial, sans-serif;" :padding "0" :margin "0"}
+ [:img (c* {:display "inline-block"})]
+ [:h1 (c* {:font-size "32px" :margin-top "10px" :margin-bottom "16px" :font-weight "600"})]
+ [:h2 (c* :border-b {:font-size "24px" :margin-top "20px" :margin-bottom "14px" :font-weight "600" :line-height "30px"})]
+ [:h3 (c* :border-b {:font-size "20px" :margin-top "20px" :margin-bottom "14px" :font-weight "600" :line-height "25px"})]
+ [:h4 (c* {:font-size "16px" :margin-top "20px" :margin-bottom "14px" :font-weight "600" :line-height "20px"})]
+ [:h5 (c* {:font-size "14px" :margin-top "20px" :margin-bottom "14px" :font-weight "600" :line-height "16px"})]
+ [:.menu-item (c* :cursor-pointer [:hover [:bg :blue-700]])]
+ [:.screenshot (c* :shadow-lg :border :rounded [:m 1] [:p 1])]
+ [:.zd-search-popup
+ (c* :border :rounded
+ [:bg :white]
+ [:p 4]
+ :box-shadow {:z-index 100 :position "absolute"
+ :top "1rem"
+ :left "1rem"
+ :right "1rem"
+ :bottom "1rem"})
+ [:.zd-search
+ (c* :border :text-xl
+ :rounded
+ [:px 4] [:py 1]
+ {:width "100%"})]
+ [:.zd-comments (c* :text-sm [:text :gray-500] [:py 1])]
+ [:.zd-results (c* [:mt 2])
+ [:.zd-search-item (c* :cursor-pointer [:py 1] [:px 4]
+ :flex [:space-x 2] :items-center
+ [:hover [:bg :gray-200]] {:border "1px solid transparent"})
+ [:&.current (c* [:bg :gray-200] :border)]
+ [:i (c* [:text :gray-400])]
+ [:.zd-search-desc (c* :text-sm [:text :gray-500])]]]]
+ [:.zd-menu-item (c* :flex [:space-x 2] :items-center [:py 0.2] :cursor-pointer [:px 6]
+ {:white-space "nowrap"})
+ [:.zd-folder (c* [:text :transparent])]
+ [:&:hover (c* [:bg :gray-200])
+ [:.zd-folder.zd-empty (c* [:text :gray-500])]
+ [:.zd-folder (c* [:text :orange-400])]]
+ [:i (c* [:w 5] {:text-align "center"})]]
+ [:ul (c* [:ml 4] [:mb 4])
+ {:list-style "inside"
+ :line-height "24px"}
+ [:li {:display "list-item"}]
+ [:ul (c* [:mb 0])]]
+ [:ol (c* [:ml 4]
+ {:list-style "disk inside"
+ :line-height "24px"})
+ [:li (c* [:my 1]
+ {:display "list-item"
+ :list-style "decimal"})]
+ [:ol (c* [:ml 4])]]
+
+ [:p (c* {:line-height "1.5rem"})]
+
+ [:.hljs (c* [:bg :gray-100] :shadow-sm
+ :border)]
+ [:pre {:margin-top "1rem" :margin-bottom "1rem"}]
+ [:.bolder (c* :font-bold)]
+ [:.badge
+ [:p {:margin 0}]]
+
+ [:.visible {:visibility "visible"}]
+ [:.pl-4 {:padding-left "1rem"}]
+ [:.mindmap
+ [:.node
+ [:circle {:fill "#aaa"}]
+ [:text {}]]
+ [:.node--internal [:circle {:fill "#999"}]]
+ [:.link {:fill "none"
+ :stroke "#aaa"
+ :stroke-opacity "0.4"
+ :stroke-width "1.5px"}]]
+
+ [:.zd-toggle [:.zd-content {:height "0"
+ :transform "scaleY(0) "
+ :transform-origin "top"
+ :transition "all 0.26s ease"}]]
+ [:.zd-toggle.zd-open
+ [:.zd-content {:transform "scaleY(1)"
+ :height "auto"}]
+ [:.zd-block-title [:.fas {:transform "rotate(90deg)"
+ :transition "all 0.26s"}]]]])
+
+
+(defn layout [ztx ctx content]
+ [:html
+ [:head
+ [:style (stylo.core/compile-styles @stylo.core/styles)]
+ [:style (garden.core/css common-style)]
+ [:link {:rel "icon" :href "data:,"}]
+ [:meta {:charset "UTF-8"}]
+ [:link {:href "//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/default.min.css", :rel "stylesheet"}]
+ [:link {:href "/static/js/fa/css/all.min.css", :rel "stylesheet"}]
+ [:link {:href "/static/js/spinner.css" :rel "stylesheet"}]
+ ;; TODO move scripts from head to body
+ [:script {:src "//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/highlight.min.js"}]
+ [:script {:src "/static/js/mindmap.js"}]
+ [:script {:src "https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"}]
+ [:script {:src "//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/languages/clojure.min.js"}]
+ [:script {:src "//cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"}]
+ [:script {:src "/static/js/core.js"}]
+ [:script {:src "https://cdn.jsdelivr.net/npm/mermaid@10.2.4/dist/mermaid.min.js"}]
+ [:title "zendoc"]
+ #_[:script {:src "/static/js/vega.min.js"}]
+ [:script {:src "/static/js/quick-score.min.js"}]
+ [:script {:src "/static/js/editor.js"}]
+ ]
+ [:body {:class (c :overflow-hidden [:h "100vh"] [:text "#353B50"])}
+ content
+ [:script "mermaid.initialize({ startOnLoad: false});"]]])
+
+(defn layout-with-menu [ztx ctx doc cnt]
+ (layout ztx ctx
+ [:div {:class (c :flex [:h "100%"])}
+ (zd.view.menu/menu ztx ctx doc)
+ [:div#page-content {:class (c [:py 6] [:px 12] :overflow-y-auto
+ :flex-1
+ {:flex-basis "100%"})}
+ cnt]]))
diff --git a/src/zd/view/menu.clj b/src/zd/view/menu.clj
new file mode 100644
index 0000000..b8dea5b
--- /dev/null
+++ b/src/zd/view/menu.clj
@@ -0,0 +1,48 @@
+(ns zd.view.menu
+ (:require [zd.store :as store]
+ [clojure.string :as str]
+ [zd.view.utils :as utils]
+ [stylo.core :refer [c]]))
+
+(defn menu [ztx {{{search-text :search} :query-params :as req} :request r :root :as ctx} doc]
+ [:div#left-nav {:class (c :border-r
+ [:pt 8]
+ [:bg "#fbfbfb"]
+ [:h "100vh"]
+ :overflow-y-auto
+ [:px 8]
+ [:w 80])}
+
+ ;; [:div {:class (c :flex :flex-row :items-baseline [:py 2] [:mb 4])}
+ ;; [:input#zd-search-input
+ ;; {:type "search"
+ ;; :placeholder ":title"
+ ;; :value search-text
+ ;; :class (c :border :outline-none [:w "100%"] [:rounded 4] :text-base [:px 2] [:py 1])}]]
+
+ [:div
+ [:a {:class utils/menu-link-c :href "/_search"}
+ [:div {:class utils/menu-icon-c} [:i.fa-solid.fa-search]]
+ [:div "Search"]]
+
+ (let [cnt (count (or (store/errors ztx) []))]
+ (when (> cnt 0)
+ [:div#errors-link {:class (c :flex [:py 1.5] [:space-x 2] :items-center [:text :red-500])}
+ [:div {:class (c :flex-1)} (utils/menu-link ztx 'errors)] [:b#errors-count {:class (c :text-sm)} cnt]]))
+
+ [:a {:class utils/menu-link-c :href "/git"}
+ [:div {:class utils/menu-icon-c} [:i.fa-solid.fa-timeline]]
+ [:span "Timeline"]]
+
+ [:a {:class utils/menu-link-c :href "/new"}
+ [:div {:class utils/menu-icon-c} [:i.fa-solid.fa-circle-plus]]
+ [:div "New Doc"]]]
+
+ [:hr {:class (c [:my 4])}]
+ (->> (store/menu ztx)
+ (map (fn [doc]
+ [:div {:class (c :flex :items-center :flex-row [:pseudo ":hover>a:last-child" :block] :justify-between)}
+ (utils/menu-link ztx (:zd/docname doc))
+ [:a {:class (c :cursor-pointer :text-lg [:text :gray-500] :hidden [:hover [:text :green-600]])
+ :href (str "/new?parent=" (:zd/docname doc))}
+ [:i.fas.fa-plus]]])))])
diff --git a/src/zd/view/mindmap.clj b/src/zd/view/mindmap.clj
new file mode 100644
index 0000000..5d00b60
--- /dev/null
+++ b/src/zd/view/mindmap.clj
@@ -0,0 +1 @@
+(ns zd.view.midmap)
diff --git a/src/zd/view/multimedia.clj b/src/zd/view/multimedia.clj
new file mode 100644
index 0000000..ab7def2
--- /dev/null
+++ b/src/zd/view/multimedia.clj
@@ -0,0 +1,26 @@
+(ns zd.view.multimedia
+ (:require
+ [zd.methods :as methods]
+ [stylo.core :refer [c]]
+ [clojure.string :as str]))
+
+
+(defn render-yt
+ "renders video player from link"
+ [link & [opts]]
+ (when-not (str/blank? link)
+ [:div {:class (c [:px 0] [:py 2] [:bg :white])}
+ (if (or (str/starts-with? link "https://youtu.be")
+ (str/starts-with? link "https://www.youtube.com"))
+ [:iframe {:src (str "https://www.youtube.com/embed/" (last (str/split link #"/")))
+ :width (or (:width opts) "560")
+ :height (or (:height opts) "315")
+ :frameborder 0
+ :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"}]
+ [:div
+ [:video {:width "100%" :height "300px" :controls "controls"}
+ [:source {:src link :type "video/mp4"}]]])]))
+
+(defmethod methods/inline-method :youtube
+ [ztx m d ctx]
+ (render-yt d))
diff --git a/src/zd/view/search.clj b/src/zd/view/search.clj
new file mode 100644
index 0000000..f528b8e
--- /dev/null
+++ b/src/zd/view/search.clj
@@ -0,0 +1,25 @@
+(ns zd.view.search
+ (:require
+ ;; [zd.runner :as runner]
+ [stylo.core :refer [c]]
+ [zd.methods :as methods]
+ [zd.view.utils :as utils :refer [btn-c]]
+ [clojure.string :as str]))
+
+(defn search-view [ztx {res :results q :query}]
+ [:div
+ [:form {:action "/_search"}
+ [:input#search {:class (c :block :border [:px 4] [:py 2] :rounded {:width "100%"})
+ :value (or q "") :name "query"}]]
+ [:div#results {:class (c :divide-y)}
+ (->> res
+ (map (fn [{id :zd/docname :as doc}]
+ [:div {:class (c [:py 1] :flex :items-baseline [:space-x 4])}
+ [:div {:class (c :flex-1)} (utils/symbol-link ztx id)]
+ [:div {:class (c :text-sm [:text :gray-600])} (when-let [desc (:desc doc)]
+ (subs desc 0 (min (count desc) 100)))]])))]
+ [:script "
+ var search = document.getElementById('search');
+ search.focus();
+ var end = search.value.length;
+ search.setSelectionRange(end, end);"]])
diff --git a/src/zd/view/timeline.clj b/src/zd/view/timeline.clj
new file mode 100644
index 0000000..54477d0
--- /dev/null
+++ b/src/zd/view/timeline.clj
@@ -0,0 +1,61 @@
+(ns zd.view.timeline
+ (:require
+ ;; [zd.runner :as runner]
+ [stylo.core :refer [c]]
+ [zd.methods :as methods]
+ [zd.view.utils :as utils :refer [btn-c]]
+ [clojure.string :as str]))
+
+(defn changes-view [ztx changes]
+ [:form {:action "/git" :method "POST"}
+ [:div {:class (c :font-bold :text-lg :border-b :flex [:space-x 4] [:py 0.5] :divide-y)}
+ [:div {:class (c :flex-1)} "Changes"]]
+ [:textarea {:class (c [:my 4] :border [:px 4] :block [:py 2] :rounded {:width "100%"})
+ :name "comment"
+ :placeholder "Comment"}]
+ [:div
+ (->> changes
+ (map (fn [{m :change f :file}]
+ [:div {:class (c [:py 0.5] :flex :items-baseline [:space-x 4])}
+ [:input {:type "checkbox" :name (str f) :value "true"}]
+ [:div {:class (c [:w 3])}
+ (get {:deleted [:i.fa-solid.fa-trash]
+ :new [:i.fa-solid.fa-square-plus]
+ :modified [:i.fa-solid.fa-pen-to-square]} m (str m))]
+ [:div f]])))]
+ [:div {:class (c [:my 2] [:py 2] :border-t)}
+ [:button {:class btn-c} "Commit"]]])
+
+(defn history-view [ztx history]
+ [:div {:class (c [:mt 4])}
+ [:div {:class (c :font-bold :text-lg :border-b)} "History"]
+ (->> history
+ (map (fn [{date :date commits :commits}]
+ [:details {:class (c [:my 4])}
+ [:summary {:class (c :block :border-b [:pt 1] [:pb 0.5] :font-bold [:text :gray-600] :cursor-pointer [:hover [:bg :gray-100]])} date]
+ [:div
+ (->> commits
+ (map (fn [com]
+ [:details {:class (c [:py 1])}
+ [:summary {:class (c :border-b [:space-x 4] :cursor-pointer [:hover [:bg :gray-100]])}
+ [:span {:class (c [:w 8] [:text :gray-600])}
+ (last (str/split (:time com) #"\s"))]
+ [:span (:user com)]
+ [:span "-"]
+ [:span (:comment com)]]
+ [:div {:class (c [:pt 2])}
+ (->> (:files com)
+ (map (fn [f]
+ [:div {:class (c [:ml 4] [:py 0.5] :flex :items-center [:space-x 2])}
+ [:i.fa-regular.fa-file {:class (str (c [:gray-400]))}]
+ [:space f]])))]])))]])))])
+
+(defn view [ztx {changes :changes history :history}]
+ [:div {:class (c [:w 200])}
+ [:h1 {:class (c :flex :items-center {:border-bottom "1px solid #ddd"})}
+ [:div {:class (c :flex-1)} "Timeline"]
+ [:button {:class btn-c} "Pull Changes"]]
+ (when (seq changes)
+ (changes-view ztx changes))
+ (when (seq history)
+ (history-view ztx history))])
diff --git a/src/zd/view/topbar.clj b/src/zd/view/topbar.clj
new file mode 100644
index 0000000..1b278c7
--- /dev/null
+++ b/src/zd/view/topbar.clj
@@ -0,0 +1,57 @@
+(ns zd.view.topbar
+ (:require
+ [zd.view.utils :as utils]
+ [clojure.string :as str]
+ [stylo.core :refer [c]]
+ [clojure.pprint]
+ [zd.store :as store]))
+
+(defn actions [ztx {{uri :uri qs :query-string :as req} :request :as ctx} {docname :zd/docname :as doc}]
+ (let [edit-btn
+ [:a {:class (c [:text :gray-600] [:hover [:text :green-600]] [:ml 4])
+ :href (str docname "/edit" "?" qs)}
+ [:i.fas.fa-edit]]
+
+ ;; TODO move to .js
+ del-script
+ (format "if (confirm(\"delete document?\") == true){
+ fetch('/%s', {method: 'DELETE'}).then((resp)=> {
+ resp.text().then((docid) => { window.location.pathname = docid})})}"
+ docname)
+
+ del-btn
+ [:a {:class (c [:text :gray-600] [:hover [:text :red-600]] [:ml 4])
+ :onclick (when-not (= docname 'index) del-script)}
+ [:i.fas.fa-trash-xmark]]
+
+ plus-btn
+ [:a {:class (c :cursor-pointer
+ [:text :gray-600]
+ [:ml 4]
+ [:hover [:text :green-600]])
+ :href (str "/new?parent=" docname)}
+ [:i.fas.fa-plus]]
+
+ container
+ [:div {:class (c :flex :items-center :border-l [:ml 4])}]]
+
+ (conj container edit-btn del-btn plus-btn)))
+
+(def separator [:span {:class (c [:mx 1.5] [:text :gray-500])} "/"])
+(defn breadcrumbs [ztx ctx {docname :zd/docname :as doc}]
+ (let [icon-class (c :cursor-pointer [:text :gray-500] [:hover [:text :orange-600]])]
+ [:div {:class (c :flex [:py 2])}
+ [:div {:class (c :flex :flex-flow :items-baseline [:space-x 2])}
+ [:a {:href (str "/") :class icon-class}
+ [:span.fa-regular.fa-house]]
+ separator
+ (->> (store/breadcrump ztx docname)
+ (map (fn [docname]
+ [:div {:class (c :flex :flex-row :items-baseline)}
+ (utils/symbol-link ztx docname)]))
+ (interpose separator))]]))
+
+(defn topbar [ztx ctx doc]
+ [:div {:class (c :flex :items-center)}
+ (breadcrumbs ztx ctx doc)
+ (actions ztx ctx doc)])
diff --git a/src/zd/view/utils.clj b/src/zd/view/utils.clj
new file mode 100644
index 0000000..15c819c
--- /dev/null
+++ b/src/zd/view/utils.clj
@@ -0,0 +1,131 @@
+(ns zd.view.utils
+ (:require
+ [stylo.core :refer [c]]
+ [zd.store :as store]
+ [clojure.pprint]
+ [zd.methods :as methods]
+ [zd.zentext :as zentext]
+ [zd.store :as store]
+ [clojure.string :as str]))
+
+
+(def btn-c (c :border :rounded [:py 1] [:px 2] [:bg :gray-200] :shadow-sm [:hover [:bg :gray-300]]))
+
+(defn get-parent [ztx res]
+ (when-let [nm (:zd/name res)]
+ (let [pn (->> (str/split (str nm) #"\.")
+ (butlast)
+ (str/join "."))]
+ (when-not (str/blank? pn)
+ (or (store/get-doc ztx (symbol pn)) {:zd/name (symbol pn)})))))
+
+(defn resolve-icon [ztx res]
+ (if-let [ava (or (get-in res [:avatar]) (get-in res [:logo]))]
+ {:type :img :img ava}
+ (if-let [icon (or (get res :icon) (get res :zd/icon))]
+ {:type :ico :icon icon}
+ (when-let [parent (get-parent ztx res)]
+ (resolve-icon ztx parent)))))
+
+(def icon-c (name (c [:mr 1] )))
+
+(defn icon [ztx res & [opts]]
+ (let [icon-class (when-let [ic (:icon-class opts)] (name ic))]
+ (if-let [icon (resolve-icon ztx res)]
+ (cond (= (:type icon) :img)
+ [:img {:src (:img icon)
+ :class (c :inline-block [:mr 1] [:h 4] [:w 4] :border {:border-radius "100%" :margin-bottom "1px"})}]
+ (= (:type icon) :ico)
+ [:i {:class (conj (map name (:icon icon)) icon-c icon-class)}])
+ (when (:force-icon opts)
+ [:i {:class ["fa-solid" "fa-file" icon-c icon-class]}]))))
+
+(defn symbol-link [ztx s & [opts]]
+ (if-let [res (store/get-doc ztx (symbol s))]
+ (if (:zd/subdoc? res)
+ (let [parent-name (get-in res [:zd/parent])
+ parent (store/get-doc ztx parent-name)]
+ [:a {:href (str "/" parent-name "#subdocs-" (:zd/docname res))
+ :class (c :inline-flex :items-center [:text :blue-700] [:hover [:underline]] :whitespace-no-wrap)}
+ (icon ztx res opts)
+ (when-not (:compact opts) (or (:title res) (str (:title parent) " (" s ")")))])
+ [:a {:href (str "/" s) :class (c :inline-flex :items-center [:text :blue-700] [:hover [:underline]] :whitespace-no-wrap)}
+ (icon ztx res opts)
+ (when-not (:compact opts)
+ (or (:title res) s))])
+ [:a {:href (str "/" s) :class (c [:text :red-600] [:bg :red-100]) :title "Broken Link"} s]))
+
+(def menu-link-c (c :flex :items-center [:space-x 2] [:text :gray-700] [:py 1] [:hover [:text :blue-600] [:bg :gray-100]]))
+(def menu-icon-c (c [:w 5] :text-center {:text-align "center"}))
+
+(defn menu-link [ztx s & [opts]]
+ (if-let [res (store/get-doc ztx (symbol s))]
+ (if (:zd/subdoc? res)
+ (let [parent-name (get-in res [:zd/parent])
+ parent (store/get-doc ztx parent-name)]
+ [:a {:href (str "/" parent-name "#subdocs-" (:zd/docname res))
+ :class menu-link-c}
+ [:div {:class menu-icon-c}
+ (or (icon ztx res opts) [:i.fa-solid.fa-file])]
+ [:div (when-not (:compact opts)
+ (str s " (" (str (:title parent) ) ")"))]])
+ [:a {:href (str "/" s) :class menu-link-c}
+ [:div {:class menu-icon-c}
+ (or (icon ztx res opts) [:i.fa-solid.fa-file {:class (name (c [:text :gray-500]))}])]
+ [:div
+ (when-not (:compact opts)
+ (or (:title res) s))]])
+ [:a {:href (str "/" s) :class (c [:text :red-600] [:bg :red-100]) :title "Broken Link"} s]))
+
+(defn pprint [title data]
+ [:details {:class (c :text-xs [:mt 0.5])}
+ [:summary {:class (c [:text :gray-500]) } title]
+ [:pre {:class (c :border [:p 2] [:bg :gray-100])}
+ (with-out-str (clojure.pprint/pprint data))]])
+
+(defn table
+ "renders table from vector of hashmaps. each hashmap is a memstore document"
+ [ztx ctx headers data]
+ [:div "TBD" [:table {:class (c :rounded [:py 2] [:w-max "80rem"] {:display "block" :table-layout "fixed"})}
+ #_[:thead
+ (->> headers
+ (map (fn [k]
+ [:th {:class (c [:px 4] [:py 2] :border [:bg :gray-100])}
+ (str/lower-case (name k))]))
+ (into [:tr]))]
+ #_(->> data
+ (mapv (fn [row]
+ [:tr
+ (doall
+ (for [h headers]
+ [:td {:class (c [:px 4] [:py 2] :border {:vertical-align "top"})}
+ (let [v (get row h)
+ docname (get row :xt/id)
+ doc (when docname (store/doc-get ztx (symbol docname)))
+ block {:key h :data v}]
+
+ (cond (= :xt/id h)
+ [:a {:href (str "/" docname)
+ :class (c :inline-flex :items-center [:text "#4B5BA0"] [:hover [:underline]])}
+ (icon ztx doc)
+ (or (:title doc) docname)]
+
+ ;; (= :zentext (:zd/content-type key-ann))
+ ;; [:div {:class (c [:w-min "16rem"] :text-sm)}
+ ;; (zentext/parse-block ztx v block)]
+
+ (= :edn (:zd/content-type key-ann))
+ (cond
+ (set? v)
+ (->> v
+ (mapv (fn [e]
+ (if (symbol? e)
+ (symbol-link ztx e)
+ (zentext/parse-block ztx (str e) block))))
+ (into [:div {:class (c :flex :flex-col :text-sm {:flex-wrap "wrap"})}]))
+
+ :else (methods/rendercontent ztx ctx block))
+
+ (some? v)
+ (methods/rendercontent ztx ctx {:data v :key h :ann {:zd/content-type :edn}})))]))]))
+ (into [:tbody]))]])
diff --git a/src/zd/view/zentext.clj b/src/zd/view/zentext.clj
new file mode 100644
index 0000000..2147919
--- /dev/null
+++ b/src/zd/view/zentext.clj
@@ -0,0 +1,176 @@
+(ns zd.view.zentext
+ (:require
+ [zd.view.utils :as link]
+ [stylo.core :refer [c]]
+ [clojure.string :as str]
+ [zd.zentext :as zentext]
+ [zd.methods :as methods]))
+
+(defn github-disc [d]
+ [:a {:href (str "https://github.com/fhir-ru/core/discussions/" d)
+ :target "_blank"
+ ;; TODO make default color profile in macro css
+ :class (c [:text "#4B5BA0"] [:space-x 0.5])}
+ [:i.fa-regular.fa-comments
+ {:class (name (c [:text :blue-400] :text-sm))}]
+ [:span d]])
+
+(defmethod methods/inline-method :github/disc
+ [ztx m d ctx]
+ (github-disc d))
+
+(defmethod methods/inline-method :d
+ [ztx m d ctx]
+ (github-disc d))
+
+
+(defmethod methods/inline-method :symbol-link
+ [ztx m s ctx]
+ (link/symbol-link ztx s))
+
+(defmethod methods/inline-method :mention
+ [ztx m s ctx]
+ (link/symbol-link ztx (symbol (str "people." s))))
+
+(defmethod methods/inline-method :bold
+ [ztx m s ctx]
+ [:b s])
+
+(defmethod methods/inline-method :italic
+ [ztx m s ctx]
+ [:i s])
+
+(defmethod methods/inline-method :x
+ [ztx m s ctx]
+ [:i.fa-solid.fa-square-check {:class (name (c [:text :green-600]))}])
+
+(defmethod methods/inline-method :fa
+ [ztx m s ctx]
+ (let [cls (->>
+ (str/split s #"\s")
+ (mapv str/trim)
+ (remove str/blank?)
+ (mapv (fn [x] (if (str/starts-with? x ":") (subs x 1) x)))
+ (str/join " "))]
+ [:i {:class cls}]))
+
+(defmethod methods/inline-method :md/link
+ [ztx m s ctx]
+ (let [[txt href] (str/split s #"\]\(" 2)]
+ [:a {:href href :target :_blank :class (c [:text "#4B5BA0"] [:hover [:underline]])} txt]))
+
+(defmethod methods/inline-method :md/img
+ [ztx m s ctx]
+ (let [[txt href] (str/split s #"\]\(" 2)]
+ [:img {:src href :alt txt}]))
+
+(defmethod methods/inline-method :code
+ [ztx m s ctx]
+ [:code {:class (c [:px 1.5] [:py 0.5] [:bg :gray-100] [:text :pink-600]
+ :text-sm
+ {:border-radius "4px"
+ :font-family "ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace"})}
+ s])
+
+(defmethod methods/inline-method :b
+ [ztx m s ctx]
+ [:b s])
+
+(defn img-src [ctx src]
+ (when src
+ (cond (str/starts-with? src "http") src
+ (str/starts-with? src "/") (str "/static" src)
+ :else (str "/static/" src))))
+
+(defmethod methods/inline-method
+ :img
+ [ztx m arg ctx]
+ (let [[src alt] (str/split arg #"\s+" 2)
+ src (img-src ctx src)]
+ [:img {:src src :alt alt}]))
+
+(defmethod methods/inline-function
+ :img
+ [ztx m args ctx]
+ (let [[src opts & _] args
+ src (img-src ctx src)]
+ [:img (assoc opts :src src)]))
+
+(defmethod methods/inline-function
+ :echo
+ [ztx m args ctx]
+ [:span "((" m (pr-str args) "))"])
+
+;; TODO fix this zentext function
+#_(defmethod methods/inline-function
+ :resource
+ [ztx m [sym & path] ctx]
+ (if-let [sym (zd.db/get-resource ztx sym)]
+ (get-in sym path)
+ [:div "Could not find " (pr-str sym)]))
+
+(defmethod methods/inline-method :a
+ [ztx m arg ctx]
+ (let [[src text] (str/split arg #"\s+" 2)]
+ [:a {:href src :target :_blank :class (c [:text "#4B5BA0"] [:hover [:underline]])}
+ " " (or text src)]))
+
+(defmethod methods/inline-method
+ :src
+ [ztx m arg ctx]
+ [:a {:class (c [:text :green-600] :title "TODO")}
+ (str arg)])
+
+(defmethod methods/inline-method :default
+ [ztx m arg ctx]
+ [:span {:class (c [:text :red-600] [:bg :red-100])} (str "No inline-method for " m " arg:" arg)])
+
+(defmethod methods/process-block "code" [ztx _ lang cnt]
+ (let [id (str "code-" (hash cnt))]
+ [:div.code-block
+ [:pre {:class (c :text-sm)
+ :style {:position "relative" :white-space "pre-wrap"}}
+ [:i.fa-solid.fa-copy.copy-button {:class (name (c :border [:p 2] :rounded
+ [:text :gray-500]
+ [:hover
+ [:text :orange-600]
+ [:bg :gray-200]]
+ :cursor-pointer
+ {:position "absolute" :right "0.5rem" :top "0.5rem"}))}]
+ [:code {:id id :style {:word-wrap "break-word"} :class [(str "language-" lang " hljs") (name (c :rounded [:p 2]))]} cnt]
+ [:script (str "hljs.highlightElement(document.getElementById('" id "'));")]]]))
+
+(defmethod methods/process-block "zd" [ztx _ lang cnt]
+ (let [id (str "code-" (hash cnt))]
+ [:div.code-block {:style {:position "relative" :white-space "pre-wrap"}}
+ [:i.fa-solid.fa-copy.copy-button {:class (name (c :border [:p 2] :rounded
+ [:text :gray-500]
+ [:hover
+ [:text :orange-600]
+ [:bg :gray-200]]
+ :cursor-pointer
+ {:position "absolute" :right "0.5rem" :top "0.5rem"}))}]
+ [:pre {:class (c :text-sm :rounded [:p 4] [:bg :gray-100]
+ :shadow-sm :border
+ {:font-family "Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace"})}
+ [:code {:id id :style {:word-wrap "break-word"}} cnt]
+ [:script (str "zdhl('" id "');")]]]))
+
+(defmethod methods/process-block :default [ztx tp args cnt]
+ [:pre {:params args :tp tp}
+ [:code.hljs cnt]])
+
+(defmethod methods/process-block "table" [ztx _ _ args]
+ (let [[cols & rows] (->> (str/split-lines args) (mapv (fn [x] (str/split x #"\|"))))]
+ [:table {:class (c :shadow-sm :rounded)}
+ [:thead
+ (into [:tr] (->> cols (mapv (fn [k] [:th {:class (c [:px 4] [:py 2] :border [:bg :gray-100])} k]))))]
+ (into [:tbody]
+ (->> rows
+ (mapv (fn [x]
+ (into [:tr]
+ (->> x (mapv (fn [v] [:td {:class (c [:px 4] [:py 2] :border)} v]))))))))]))
+
+(defmethod methods/rendercontent :zentext
+ [ztx ctx {:keys [data] :as block}]
+ [:div (zentext/parse-block ztx data block)])
diff --git a/src/zd/zentext.clj b/src/zd/zentext.clj
index 7fced42..dc452c7 100644
--- a/src/zd/zentext.clj
+++ b/src/zd/zentext.clj
@@ -124,7 +124,7 @@
(defmethod apply-transition :p-end
[ztx tr {lns :lines :as ctx} line]
(-> (update ctx :result conj
- (let [res (into [:p {:class (c [:pb 4])}]
+ (let [res (into [:p {:class (c [:pb 2])}]
(mapcat (fn [l] (let [parsed (parse-inline ztx l ctx)]
(if-not (or (some #{"."} parsed) (some #{","} parsed))
(conj parsed "\n")
@@ -218,3 +218,30 @@
(let [lines (get-lines s)
res (into [:div] (parse-block* ztx lines block))]
res))
+
+(defn quick-parse-links [l]
+ (let [m (re-matcher #"(\s|^)(#|@)([-._a-zA-Z0-9]+)" l)]
+ (loop [acc #{}]
+ (if-let [[_ _ tp link :as x] (re-find m)]
+ (recur (conj acc (if (= tp "#")
+ (symbol link)
+ (symbol (str "person." link)))))
+ acc))))
+
+(comment
+ (quick-parse-links "#at-start abc#d some @ivan \\@andrey text \\#esaped #e some text #doted.name text #word-x #at-end"))
+
+(defn extract-links [text]
+ (let [lines (get-lines text)]
+ (loop [[l & ls] lines
+ collect? true
+ acc #{}]
+ (if (and (nil? l) (empty? ls))
+ acc
+ (if (str/starts-with? l "```")
+ (if collect?
+ (recur ls false acc)
+ (recur ls true acc))
+ (if collect?
+ (recur ls collect? (into acc (quick-parse-links l)))
+ (recur ls collect? acc)))))))
diff --git a/test/zd/api_test.clj b/test/zd/api_test.clj
deleted file mode 100644
index e86c774..0000000
--- a/test/zd/api_test.clj
+++ /dev/null
@@ -1,153 +0,0 @@
-(ns zd.api-test
- (:require
- [xtdb.api :as xtdb]
- [clojure.string :as str]
- [zd.datalog :as d]
- [zd.fs :as fs]
- [zd.memstore :as memstore]
- [zd.api]
- [matcho.core :as matcho]
- [clojure.test :refer [deftest is testing]]
- [zd.test-utils :as tutils]
- [zen.core :as zen]
- [zen-web.core :as web]))
-
-;; TODO impl memstore -> str test util
-
-(defonce ztx (zen/new-context {}))
-
-(deftest create-delete
-
- (zen/stop-system ztx)
-
- (zen/read-ns ztx 'zd)
-
- (zen/read-ns ztx 'zd.test)
-
- (zen/start-system ztx 'zd.test/system)
-
- (testing "when document not found redirects to editor"
- (matcho/assert
- {:status 301 :headers {"Location" "/testdoc/edit?"}}
- (web/handle ztx 'zd/api {:uri "/testdoc"})))
-
- (testing "editor config is rendered"
- (matcho/assert
- {:status 200}
- (web/handle ztx 'zd/api {:uri "/testdoc/edit"})))
-
- (testing "saving document"
- (matcho/assert
- {:status 422 :body {:message string?}}
- (web/handle ztx 'zd/api
- {:uri "/testdoc/edit"
- :request-method :put
- :body (tutils/req-body ":zd/docname testdoc._draft\n:desc /\n no docname present")}))
-
- (matcho/assert
- {:status 422 :body {:message string?}}
- (web/handle ztx 'zd/api
- {:uri "/testdoc/edit"
- :request-method :put
- :body (tutils/req-body ":desc /\n no docname present")}))
-
- (matcho/assert
- {:status 200 :body string?}
- (web/handle ztx 'zd/api
- {:uri "/testdoc/edit"
- :request-method :put
- :body (tutils/req-body ":zd/docname testdoc\n:title \"testdoc\"\n:tags #{}\n:desc /")}))
-
- (is (not (str/blank? (tutils/read-doc "testdoc.zd")))))
-
- (testing "delete document"
- (matcho/assert
- {:status 200 :body string?}
- (web/handle ztx 'zd/api {:uri "/testdoc" :request-method :delete}))
-
- (is (nil? (tutils/read-doc "testdoc.zd"))))
-
- (zen/stop-system ztx))
-
-(deftest document-rename
-
- (zen/stop-system ztx)
-
- (zen/read-ns ztx 'zd)
-
- (zen/read-ns ztx 'zd.test)
-
- (zen/start-system ztx 'zd.test/system)
-
- (def to-rename
- ;; TODO for some reason whitespace is required after :desc /\n
- ":zd/docname testdoc\n:title \"testdoc\"\n:tags #{}\n:desc /\n #testdoc2 link")
-
- (def to-link
- ":zd/docname testdoc2\n:title \"testdoc2\"\n:tags #{}\n:desc /")
-
- (matcho/assert
- {:status 200 :body string?}
- (web/handle ztx 'zd/api
- {:uri "/testdoc/edit"
- :request-method :put
- :body (tutils/req-body to-link)}))
-
- (is (seq (tutils/read-doc "testdoc2.zd")))
-
- (matcho/assert
- {:status 200 :body string?}
- (web/handle ztx 'zd/api
- {:uri "/testdoc/edit"
- :request-method :put
- :body (tutils/req-body to-rename)}))
-
- (is (not (str/blank? (tutils/read-doc "testdoc.zd"))))
-
- (def renamed
- (str to-rename "\n:zd/rename newdoc"))
-
- ;; old file deleted, new added
- ;; links are re calculated
-
- (matcho/assert
- {:status 200 :body "/newdoc"}
- (web/handle ztx 'zd/api
- {:uri "/testdoc/edit"
- :request-method :put
- :body (tutils/req-body renamed)}))
-
- ;; TODO think checkpoint api call
- (await fs/queue)
- (xtdb/sync (:node (d/get-state ztx)))
-
- (testing "on rename old file is deleted"
- (is (nil? (tutils/read-doc "testdoc.zd")))
- (is (not (str/blank? (tutils/read-doc "newdoc.zd")))))
-
- (testing "backlinks are reloaded"
- ;; TODO use designated api op
- (matcho/assert
- {:zd/meta {:backlinks #{'{:to testdoc2, :path [:desc], :doc newdoc}}}}
- (memstore/get-doc ztx 'testdoc2)))
-
- (testing "old file is removed from storage"
- ;; TODO use designated api op
- (is (empty? (zen/op-call ztx 'zd/query '{:find [?e]
- :where [[?e :xt/id ?id]
- [(= ?id "testdoc")]]}))))
- ;; cleanup
-
- (matcho/assert
- {:status 200 :body string?}
- (web/handle ztx 'zd/api {:uri "/newdoc"
- :request-method :delete}))
-
- ;; TODO for some reason root is nil
- (matcho/assert
- {:status 200 :body string?}
- (web/handle ztx 'zd/api {:uri "/testdoc2"
- :request-method :delete}))
-
- (is (nil? (tutils/read-doc "testdoc2.zd")))
- (is (nil? (tutils/read-doc "newdoc.zd"))))
diff --git a/test/zd/datalog_test.clj b/test/zd/datalog_test.clj
index 505f403..c14b3e1 100644
--- a/test/zd/datalog_test.clj
+++ b/test/zd/datalog_test.clj
@@ -1,178 +1,182 @@
(ns zd.datalog-test
(:require
- [zd.test-utils :as tutils]
- [zen-web.core :as web]
- [xtdb.api :as xtdb]
- [zd.api]
[zd.datalog :as datalog]
[clojure.test :refer [deftest is testing]]
[zen.core :as zen]
+ [xtdb.api :as xt]
[matcho.core :as matcho]))
(def ztx (zen/new-context {}))
-(zen/read-ns ztx 'zd)
-(zen/read-ns ztx 'zd.test)
-(zen/start-system ztx 'zd.test/system)
-(xtdb/sync (:node (datalog/get-state ztx)))
+(defmacro match-parse [q pat]
+ `(let [res# (datalog/parse-query ~q)]
+ (matcho/match res# ~pat)
+ res#))
-(comment
- (def ztx (zen/new-context {}))
- )
-
-(deftest datalog-engine
-
- (zen/start-system ztx 'zd.test/system)
- (xtdb/sync (:node (datalog/get-state ztx)))
-
- (datalog/query ztx '{:find [?e]
- :where [[?e :xt/id ?id]]})
-
- (datalog/query ztx '{:find [?e] :where [[?e :xt/id "'customers"]]})
-
- (testing "metadata is loaded into xtdb"
- (matcho/assert
- #{['customers]}
- (datalog/query ztx '{:find [?e] :where [[?e :xt/id "'customers"]]})))
-
- (datalog/query ztx '{:find [e]
- :where [[e :parent "'customers"]]})
-
- (matcho/assert
- #{'[customers.partners-list] '[customers.flame] '[customers._schema]}
- (datalog/query ztx '{:find [e]
- :where [[e :parent "'customers"]]}))
-
- (matcho/assert
- #{[#:xt{:id 'people.john}]}
- (datalog/query ztx '{:find [(pull e [:xt/id :name])]
- :where [[e :role "ceo"]]}))
-
- )
-
-(deftest xtdb-sync
+(defmacro match-query [q pat]
+ `(let [res# (datalog/datalog-query ztx ~q)]
+ (matcho/match res# ~pat)
+ res#))
- (zen/start-system ztx 'zd.test/system)
+(defmacro match? [q pat]
+ `(let [res# (datalog/datalog-sugar-query ztx ~q)]
+ (matcho/match res# ~pat)
+ res#))
- (xtdb/sync (:node (datalog/get-state ztx)))
+(defmacro match-find [cols fnd]
+ `(let [res# (:find (datalog/make-find {:columns ~cols}))]
+ (matcho/match res# ~fnd)
+ res#))
- (datalog/evict-by-query ztx '{:where [[e :xt/id "'people.bob"] ] :find [e]})
+(deftest datalog-engine
- (datalog/query ztx '{:where [[e :xt/id id]] :find [e]})
+ (match-find [{:name 'x}] ['x])
+ (match-find [{:name 'x}] ['x])
+ (match-find '[{:name z, :prop :name, :label "z"}] ['(pull z [:name])])
+ (match-find '[{:name z, :hidden true}] ['z])
- (testing "add another person with role = ceo"
- (matcho/assert
- #{['people.john]}
- (datalog/query ztx '{:find [?id]
- :where [[?e :role "ceo"] [?e :xt/id ?id]]})
- )
+ (match-parse "z :a 1\nc (count z)\n< desc c" {})
+ (match-parse "z :a 1\n> z:name" {})
- (def doc ":zd/docname people.bob\n:title \"Bob Barns\"\n:desc \"bob is the best\"\n:role #{\"ceo\"} ")
+ (match-parse "x :zd/type c\n>c\n>x \n>(count x) | count"
+ '{:where [[x :zd/type c]],
+ :find [c x (count x)],})
- (matcho/assert
- {:status 200}
- (web/handle ztx 'zd/api
- {:uri "/people._draft/edit"
- :request-method :put
- :body (tutils/req-body doc)}))
+ (matcho/match
+ (match-parse "z :a 1\n>z:attr1\n>z:attr2" {})
+ '{:columns [{:name z, :prop :attr1, :label "z"} {:name z, :prop :attr2, :label "z"}]})
+
+ (match-find '[{:name z, :prop :attr1, :label "z"} {:name z, :prop :attr2, :label "z"}]
+ '[(pull z [:attr1]) (pull z [:attr2])])
+
+ (match-parse
+ "x :attr y\n> x"
+ '{:where [[x :attr y]] :find [x]})
+
+ (match-parse
+ "x :attr y\n> x:name"
+ '{:where [[x :attr y]] :find [(pull x [:name])]})
+
+ (match-parse
+ "x :attr y\n> x:*"
+ '{:where [[x :attr y]],
+ :find [(pull x [*])]})
+
+ (match-parse
+ "x :attr y\n> x:?"
+ '{:where [[x :attr y]],
+ :find [(pull x [*])],
+ :index {}})
+
+ (match-parse
+ "
+x :attr y
+y :other z
+> x
+< desc x
+< limit 10
+"
+ '{:where [[x :attr y] [y :other z]],
+ :order-by [[x :desc]],
+ :limit 10,
+ :find [x x]})
+
+ (match-parse
+ "x :attr #y"
+ '{:where [[x :attr "'y"]],
+ :find [x]})
+
+ (match-parse
+ "
+x :attr y
+> y
+> (count x)
+"
+ '{:where [[x :attr y]],
+ :find [y (count x)]})
+
+ (match-parse
+ "
+x :attr y
+c (count x)
+> y | title
+> c | count
+"
- (is (tutils/read-doc "people/bob.zd"))
- (xtdb/sync (:node (datalog/get-state ztx)))
+ '{:where [[x :attr y] [c (count x)]],
+ :find [y c]})
- (matcho/assert
- #{['people.john] ['people.bob]}
- (datalog/query ztx '{:find [?id]
- :where [[?e :role "ceo"] [?e :xt/id ?id]]})))
+ (match-parse
+ "
+x :attr y
+< desc x
+"
+ '{:where [[x :attr y]],
+ :order-by [[x :desc]],
+ :find [x]})
- (testing "edit bob's role"
- (def doc ":zd/docname people.bob\n:title \"Bob Barns\"\n:desc \"bob is the best\"\n:role #{\"cpo\"} ")
+ (matcho/match
+ (match-parse "x :zd/type c\nz (count x)\n> z" {})
+ '{:where [[x :zd/type c] [z (count x)]],
+ :find [z]})
+
+ (match-parse
+ "
+x :attr y
+z (count x)
+> x
+< desc z
+"
+ '{:where [[x :attr y] [z (count x)]],
+ :order-by [[z :desc]],
+ :find [x]})
- (matcho/assert
- {:status 200}
- (web/handle ztx 'zd/api
- {:uri "/people.bob/edit"
- :request-method :put
- :body (tutils/req-body doc)}))
- (is (tutils/read-doc "people/bob.zd"))
- (xtdb/sync (:node (datalog/get-state ztx)))
+ (datalog/datalog-put ztx {:zd/docname 'a :a 1})
- (matcho/assert
- #{['people.john]}
- (datalog/query ztx '{:find [?id] :where [[?e :role "ceo"] [?e :xt/id ?id]]}))
+ (match-query '{:where [[x :a y]],
+ :find [x y]
+ :order-by [[y :desc]],}
+ [['a 1] nil?])
- (matcho/assert
- #{['people.bob]}
- (datalog/query ztx '{:find [?id] :where [[?e :role "cpo"] [?e :xt/id ?id]]})))
- (is (= 200 (:status (web/handle ztx 'zd/api {:uri "/people.bob" :request-method :delete}))))
+ (match? "x :a y\n> x\n> y"
+ '{:result [[a 1]],
+ :query {:where [[x :a y]],
+ :find [x y]},
+ :columns ["x" "y"]})
- (is (nil? (tutils/read-doc "people/bob.zd")))
+ (datalog/datalog-put ztx {:zd/docname 'typed :zd/type 'zd.class})
- ;; (is (empty? (datalog/query ztx '{:find [?id] :where [[?e :role "cpo"] [?e :xt/id ?id]]})))
+ (match? "x :zd/type #zd.class\n> x"
+ '{:result [[typed]],
+ :columns ["x"]})
- )
+ (match? "x :zd/type c\n> x\n< desc c"
+ '{:result [[typed]],
+ :columns ["x"]})
-(deftest datalog-sugar
-
-
- (def q
- "
-e :parent #organizations
-e :rel #rel.partner
-p :organization e
-p :role #roles.cto
-> d
-> e:xt/id
-> e:rel
-> (count e)
-> (mean e)
-")
-
- (def q2
- "
-e :parent #customers
-e :category cat
-(clojure.string/starts-with? cat \"s\")
-e :customer-since since
-e :asc
-
-> e:name
-> (count e)
-"
- )
+ (match? "x :zd/type c\n> x\n< desc (count c)"
+ '{:result [[typed]],
+ :columns ["x"]})
- (def q3
- "
-e :type #customers
-> e
-")
+ (match-parse "x :zd/type c\n> x\n< desc (count c)" {})
- (datalog/submit ztx {:xt/id "'i1" :type :type})
- (datalog/submit ztx {:xt/id "'i2" :type :type})
+ (match-parse "x :zd/type c\n> (count x) | cnt" {})
- (matcho/match
- (datalog/parse-query q3)
- '{:where [[e :type "'customers"]],
- :order []
- :find [e]})
-
- (def q
- "
-e :title t
-> e
-> t
-> e:title
-"
- )
+ (match? "x :zd/type c\n>c \n>(count x) | cnt"
+ '{:result [[zd.class 1]],
+ :query {:where [[x :zd/type c]], :find [c (count x)]},})
+ (match? "x :zd/type c\n>c \n>(count x) | cnt\n< desc (count x)"
+ '{:result [[zd.class 1]],
+ :columns ["c" "cnt"]
+ :query {:where [[x :zd/type c]], :find [c (count x)]},})
- (datalog/parse-query q)
+ (datalog/datalog-query ztx '{:where [[x :zd/type c]]
+ :find [(pull x [:zd/type]) (pull x [:b])]})
- (datalog/sugar-query ztx "
-e :parent #people
-> e:* ")
)
diff --git a/test/zd/git_test.clj b/test/zd/git_test.clj
new file mode 100644
index 0000000..ddc1216
--- /dev/null
+++ b/test/zd/git_test.clj
@@ -0,0 +1,65 @@
+(ns zd.git-test
+ (:require
+ [zd.test-utils :as tu]
+ [zd.git :as git]
+ [matcho.core :as matcho]
+ [clojure.test :refer [deftest is testing]]))
+
+(deftest test-git
+ (def dir ".tmp/git-test")
+ (def ztx (tu/context dir))
+
+ (git/init ztx)
+
+ (tu/write-file ztx 'index ":title \"index\"")
+ (git/commit ztx "initial commit")
+
+ (matcho/match
+ (git/history ztx 30)
+ [{:date string?
+ :commits
+ [{:comment "initial commit",
+ :user string?
+ :time string?
+ :files ["index.zd"]}]}])
+
+ (tu/write-file ztx 'other ":title \"other\"")
+ (tu/write-file ztx 'andonemore ":title \"andonemore\"")
+
+ (matcho/match
+ (git/changes ztx)
+ [{:change :new, :file "andonemore.zd"}
+ {:change :new, :file "other.zd"}])
+
+ (git/commit ztx "second")
+
+ (matcho/match
+ (git/history ztx 30)
+ [{:date string?
+ :commits
+ [{:comment "second"}
+ {:comment "initial commit"}]}])
+
+ (git/history ztx 30)
+
+ (tu/write-file ztx 'other ":title \"change\"")
+ (tu/write-file ztx 'new ":title \"new\"")
+
+ (matcho/match
+ (git/changes ztx)
+ [{:change :modified, :file "other.zd"}
+ {:change :new, :file "new.zd"}])
+
+
+ (git/commit ztx "third")
+
+
+ (matcho/match
+ (git/history ztx 30)
+ [{:date string?
+ :commits
+ [{:comment "third"}
+ {:comment "second"}
+ {:comment "initial commit"}]}])
+
+ )
diff --git a/test/zd/parser/example.zd b/test/zd/parser/example.zd
new file mode 100644
index 0000000..9285b75
--- /dev/null
+++ b/test/zd/parser/example.zd
@@ -0,0 +1,72 @@
+;; oneliners are parsed as edn
+:string "Title"
+:symbol symbol
+:symbols #{a b}
+:zd/require #{ :title :orgs/site :orgs/location :orgs/linkedin :orgs/tags }
+:int 1
+:long 1.0
+:date #inst"2001"
+:map {:a 1, :b 2}
+:vector [1 2 3 4]
+
+;; there are special oneliner for string
+:stringstyle | Here is a long string
+
+;; edn multilne should be started with edn/
+:map-multiline edn/
+
+{:a 1
+ :b 2}
+
+;; zentext is started with `/` or `zentext/`
+:multiline /
+
+String 1
+String 2
+
+;; zentext with block
+:with-block /
+Text
+```
+:key-in-block "value"
+```
+Text
+
+;;annotation is started ^ edn or ^ | string
+^title "Title"
+:with-title /
+Here is some
+^title | Here is a long title
+:with-title-2 /
+
+Here is a just a text
+
+;; multiple annotations
+^title "Title"
+^hide
+^key {:a 1}
+:multiple-annotations /
+line 1
+line 2
+
+;;datalog query is started with ?/
+:query ?/
+e :zd/type zd.class
+> e
+
+;; subdoc is & or &
+&subdoc-1
+:title "S1"
+:local-ref .
+
+;; after & you can set :zd/type
+
+&subdoc-2 zd/class
+:title "S2"
+
+;; anonimous subdoc will be assigned autoincrement name doc-
+&
+:title "Anonimous1"
+
+& #{zd/task zd/anything}
+:title "Anonimous2"
\ No newline at end of file
diff --git a/test/zd/parser_test.clj b/test/zd/parser_test.clj
new file mode 100644
index 0000000..578b713
--- /dev/null
+++ b/test/zd/parser_test.clj
@@ -0,0 +1,100 @@
+(ns zd.parser-test
+ (:require
+ [matcho.core :as matcho]
+ [zen.core :as zen]
+ [clojure.java.io :as io]
+ [zd.parser :as parser]
+ [clojure.test :refer [deftest is]]))
+
+
+(deftest test-parser
+
+ (def to-read (slurp (io/resource "zd/parser/example.zd")))
+ (def ztx (zen/new-context {}))
+
+ (parser/parse-key-name ":key value")
+ (parser/parse-key-name ":key {:a 1}")
+ (parser/parse-key-name ":key edn/ {:a 1}")
+ (parser/parse-key-name ": value")
+ (parser/parse-key-name ":key | text")
+ (parser/parse-key-name ":key |Long text")
+ (parser/parse-key-name ":key type/ extra")
+ (parser/parse-key-name ":key type/")
+ (parser/parse-key-name ":key / extra")
+ (parser/parse-key-name ":key /")
+ (parser/parse-key-name ":key ?/")
+ (parser/parse-key-name ":zd/key #{:a :b :c}")
+ (parser/parse-annotation "^title | Long title")
+
+ (parser/parse ztx 'mydoc to-read)
+
+ (def result (parser/parse ztx 'mydoc to-read))
+
+ (def doc
+ {:zd/docname 'mydoc,
+ :zd/view
+ [[:string {:type :edn}]
+ [:symbol {:type :edn}]
+ [:symbols {:type :edn}]
+ [:zd/require {:type :edn}]
+ [:int {:type :edn}]
+ [:long {:type :edn}]
+ [:date {:type :edn}]
+ [:map {:type :edn}]
+ [:vector {:type :edn}]
+ [:stringstyle {:type :str}]
+ [:map-multiline {:type :edn, :multiline true}]
+ [:multiline {:type :zentext, :multiline true}]
+ [:with-block {:type :zentext, :multiline true}]
+ [:with-title {:type :zentext, :multiline true, :title "Title"}]
+ [:with-title-2 {:type :zentext, :multiline true, :title "| Here is a long title"}]
+ [:multiple-annotations {:type :zentext, :multiline true, :title "Title", :as :none, :key {:a 1}}]
+ [:query {:type :?, :multiline true}]],
+ :string "Title",
+ :symbol 'symbol,
+ :symbols #{'a 'b}
+ :zd/require #{:orgs/location :title :orgs/linkedin :orgs/tags :orgs/site},
+ :int 1,
+ :long 1.0,
+ :date #inst "2001-01-01T00:00:00.000-00:00",
+ :map {:a 1, :b 2},
+ :vector [1 2 3 4],
+ :stringstyle "Here is a long string",
+ :map-multiline {:a 1, :b 2},
+ :multiline "String 1\nString 2",
+ :with-block "Text\n```\n:key-in-block \"value\"\n```\nText",
+ :key-in-block nil?
+ :with-title "Here is some",
+ :with-title-2 "Here is a just a text",
+ :multiple-annotations "line 1\nline 2",
+ :query "e :zd/type zd.class\n> e"
+ :zd/subdocs [{:zd/view [[:title {:type :edn}] [:local-ref {:type :edn}]],
+ :zd/docname 'mydoc.subdoc-1,
+ :zd/subdoc? true,
+ :zd/parent 'mydoc,
+ :title "S1",
+ :local-ref 'mydoc}
+ {:zd/view [[:zd/type {:type :edn}][:title {:type :edn}]],
+ :zd/docname 'mydoc.subdoc-2
+ :zd/type 'zd/class,
+ :zd/subdoc? true,
+ :zd/parent 'mydoc
+ :title "S2"}
+ {:zd/view [[:title {:type :edn}]],
+ :zd/docname 'mydoc.doc-3,
+ :zd/subdoc? true,
+ :zd/parent 'mydoc,
+ :title "Anonimous1"}
+ {:zd/view [[:zd/type {:type :edn}][:title {:type :edn}]],
+ :zd/docname 'mydoc.doc-4
+ :zd/type #{'zd/task 'zd/anything},
+ :zd/subdoc? true,
+ :zd/parent 'mydoc,
+ :title "Anonimous2"}]})
+
+ (matcho/match result doc)
+
+ (parser/parse ztx 'mydoc ":title \"doc\"\n&sub zd/class\n" {:zd/parent 'index})
+
+ )
+
diff --git a/test/zd/rdfs.zd b/test/zd/rdfs.zd
deleted file mode 100644
index 0dbf084..0000000
--- a/test/zd/rdfs.zd
+++ /dev/null
@@ -1,94 +0,0 @@
-:title "RDFS"
-
-:rdfs.comment /
-
-&rdf.Property
-:a rdf.Class
-
-&.Resource
-:a rdfs.Class
-:rdfs.comment "The class resource, everything."
-
-&.Class
-:a rdfs.Class
-:rdfs.comment "The class of classes."
-:rdfs.subClassOf rdfs.Resource
-
-&.subClassOf
-:a rdf.Property
-:rdfs.comment "The subject is a subclass of a class."
-:rdfs.range rdfs.Class
-:rdfs.domain rdfs.Class
-
-&.subPropertyOf
-:a rdf.Property
-:rdfs.comment "The subject is a subproperty of a property."
-:rdfs.range rdf.Property
-:rdfs.domain rdf.Property
-
-&.comment
-:a rdf:Property
-:rdfs.comment "A description of the subject resource."
-:rdfs.domain rdfs.Resource
-:rdfs.range rdfs.Literal
-
-&.label
-:a rdf:Property
-:rdfs.comment "A human-readable name for the subject."
-:rdfs.domain rdfs.Resource
-:rdfs.range rdfs.Literal
-
-&.domain
-:a rdf:Property
-:rdfs.comment "A domain of the subject property."
-:rdfs.range rdfs.Class
-:rdfs.domain rdf:Property
-
-&.range
-:a rdf:Property
-:rdfs.comment "A range of the subject property."
-:rdfs.range rdfs.Class
-:rdfs.domain rdf:Property
-
-&.seeAlso
-:a rdf:Property
-:rdfs.comment "Further information about the subject resource."
-:rdfs.range rdfs.Resource
-:rdfs.domain rdfs.Resource
-
-&.isDefinedBy
-:a rdf:Property
-:rdfs.subPropertyOf rdfs.seeAlso
-:rdfs.comment "The defininition of the subject resource."
-:rdfs.range rdfs.Resource
-:rdfs.domain rdfs.Resource
-
-&.Literal
-:a rdfs.Class
-:rdfs.comment "The class of literal values, eg. textual strings and integers."
-:rdfs.subClassOf rdfs.Resource
-
-&.Container
-:a rdfs.Class
-:rdfs.subClassOf rdfs.Resource
-:rdfs.comment "The class of RDF containers."
-
-&.ContainerMembershipProperty
-:a rdfs.Class
-:rdfs.comment /
-
- The class of container membership properties, rdf:_1, rdf:_2, ...,
- all of which are sub-properties of 'member'.
-
-:rdfs.subClassOf rdf:Property
-
-&.member
-:a rdf:Property
-:rdfs.comment "A member of the subject resource."
-:rdfs.domain rdfs.Resource
-:rdfs.range rdfs.Resource
-
-&.Datatype
-:a rdfs.Class
-:rdfs.comment "The class of RDF datatypes."
-:rdfs.subClassOf rdfs.Class
diff --git a/test/zd/schema_test.clj b/test/zd/schema_test.clj
new file mode 100644
index 0000000..6cbcd59
--- /dev/null
+++ b/test/zd/schema_test.clj
@@ -0,0 +1,114 @@
+
+(ns zd.schema-test
+ (:require [zd.schema :as schema]
+ [zen.core :as zen]
+ [matcho.core :as matcho]
+ [clojure.test :as t]))
+
+;;TODO: add registry
+;; (validate ztx 'schema-name data)
+
+(t/deftest test-schema
+
+ (def ztx (zen/new-context {:zdb {'req {} 's.str {}}}))
+
+ (schema/add-class ztx {:zd/docname 'req :zd/require [:a :b]})
+
+ (:zd/classes @ztx)
+ (:zdb @ztx)
+
+
+ (matcho/match
+ (schema/validate ztx {:zd/type 'req})
+ [{:type :required, :message ":a is required", :path [:a]}
+ {:type :required, :message ":b is required", :path [:b]}])
+
+ (matcho/match
+ (schema/validate ztx {:zd/type #{'req}})
+ [{:type :required, :message ":a is required", :path [:a]}
+ {:type :required, :message ":b is required", :path [:b]}])
+
+ (t/is (empty? (schema/validate ztx {:zd/type #{'req} :a 1 :b 1})))
+
+ (schema/add-prop ztx {:zd/docname 's.str :zd/type 'zd.prop :zd/data-type 'zd.string})
+
+ (matcho/match
+ (schema/validate ztx {:s/str 1})
+ [{:type :type :message string?}])
+
+ (t/is (empty? (schema/validate ztx {:s/str "str"})))
+
+ (schema/add-prop ztx {:zd/docname 's.number :zd/data-type 'zd.number})
+ (matcho/match
+ (schema/validate ztx {:s/number "x"})
+ [{:type :type :message string?}])
+
+ (t/is (empty? (schema/validate ztx {:s/number 1})))
+ (t/is (empty? (schema/validate ztx {:s/number 1.1})))
+
+
+ (schema/add-prop ztx {:zd/docname 's.symbol :zd/data-type 'zd.symbol})
+
+ (swap! ztx assoc-in [:zdb 'symbol] {})
+
+ (matcho/match
+ (schema/validate ztx {:s/symbol "x"})
+ [{:type :type :message string?}])
+
+ (matcho/match
+ (schema/validate ztx {:s/symbol 'wrong})
+ [{:type :reference :message string?}])
+
+ (t/is (empty? (schema/validate ztx {:s/symbol 'symbol})))
+
+
+ (schema/add-prop ztx {:zd/docname 's.symbol-maybeset :zd/data-type 'zd.symbol :zd/maybe-set? true})
+
+ (t/is (empty? (schema/validate ztx {:s/symbol-maybeset 'symbol})))
+ (t/is (empty? (schema/validate ztx {:s/symbol-maybeset #{'symbol}})))
+
+
+
+ (schema/add-prop ztx {:zd/docname 's.symbol-set :zd/data-type 'zd.symbol :zd/set? true})
+ (t/is (not (empty? (schema/validate ztx {:s/symbol-set 'symbol}))))
+ (t/is (empty? (schema/validate ztx {:s/symbol-set #{'symbol}})))
+
+
+ (schema/add-prop ztx {:zd/docname 's.int :zd/data-type 'zd.int})
+
+ (t/is (empty? (schema/validate ztx {:s/int 1})))
+ (t/is (seq (schema/validate ztx {:s/int 10.1})))
+ (t/is (seq (schema/validate ztx {:s/int ""})))
+
+ (schema/add-class ztx {:zd/docname 'summary :zd/summary [:a :b]})
+
+ (schema/summary ztx 'summary)
+
+ (matcho/match
+ (schema/summary ztx 'summary {:a 1 :b 2 :c 3})
+ {:a 1, :b 2})
+
+
+ (schema/add-prop ztx {:zd/docname '_.title :zd/data-type 'zd.string})
+ (schema/get-prop ztx :title)
+
+ (t/is (not (empty? (schema/validate ztx {:title 1}))))
+
+
+ (schema/add-prop ztx {:zd/docname '_.date :zd/data-type 'zd.date})
+
+ (t/is (not (empty? (schema/validate ztx {:date 1}))))
+ (t/is (empty? (schema/validate ztx {:date #inst"2011"})))
+
+ (schema/add-class ztx {:zd/docname 'org :zd/child-type 'org})
+
+ (get-in @ztx [:zd/classes 'org :zd/child-type])
+
+ (matcho/match
+ (schema/infere ztx {:zd/docname 'org.o1 :zd/parent 'org :title "Org"})
+ '{:zd/docname org.o1,
+ :zd/type org,
+ :zd/infered [:zd/type]})
+
+
+ )
diff --git a/test/zd/store_test.clj b/test/zd/store_test.clj
new file mode 100644
index 0000000..eb44897
--- /dev/null
+++ b/test/zd/store_test.clj
@@ -0,0 +1,445 @@
+(ns zd.store-test
+ (:require
+ [zd.store :as store]
+ [matcho.core :as matcho]
+ [clojure.set]
+ [clojure.test :refer [deftest is testing]]
+ [zd.test-utils :as tu]))
+
+(deftest store-unit-tests
+
+ (is (= (store/docname-to-path "a.b.c") "a/b/c.zd"))
+ (is (= (store/docname-to-path 'a.b.c) "a/b/c.zd"))
+ (is (= (store/path-to-docname "a/b/c.zd") 'a.b.c))
+ (is (= (store/child-docname 'a.b 'c) 'a.b.c))
+
+ (def ztx (tu/context ".tmp/integration"))
+
+ (tu/write-file ztx 'index ":zd/menu-order 1\n:title \"Index\"\n&sub1\n:title \"Subdoc\"\n")
+ (tu/write-file ztx 'other ":zd/menu-order 2\n:title \"Other\"\n&sub1\n:title \"Subdoc\"\n")
+
+ (matcho/match (store/to-doc ztx 'mydoc ":zd/menu-order 1\n:title \"Index\"\n&sub1\n:title \"Subdoc\"\n" {:zd/parent 'index})
+ {:title "Index"
+ :zd/parent 'index
+ :zd/subdocs [{:title "Subdoc" :zd/parent 'mydoc}]})
+
+ (matcho/match (store/file-read ztx (:zd/dir @ztx) "index.zd")
+ {:title "Index"
+ :zd/subdocs [{:title "Subdoc"}]})
+
+ (matcho/match (store/dir-read ztx)
+ [{:title "Index"
+ :zd/subdocs [{:title "Subdoc" :zd/parent 'index}]}
+ {:title "Other"
+ :zd/parent 'index
+ :zd/subdocs [{:title "Subdoc" :zd/parent 'other}]}])
+
+ (testing "encoding/decoding for datadog"
+ (matcho/match
+ (store/encode-data {:xt/id 'doc :zd/docname 'doc})
+ {:xt/id "'doc", :zd/docname "'doc"})
+
+ (matcho/match
+ (store/decode-data {:xt/id "'doc", :zd/docname "'doc"})
+ {:xt/id 'doc :zd/docname 'doc})
+ )
+
+ (store/datalog-query
+ ztx
+ '{:where [[e :xt/id id]]
+ :find [(pull e [*])]})
+
+ (store/dir-load ztx)
+
+
+ (is (nil? (store/errors ztx)))
+
+ (matcho/match
+ (store/datalog-query
+ ztx
+ '{:where [[e :zd/parent p]]
+ :find [e p]
+ :order-by [[e :asc] [p :asc]]})
+ '[[errors index] [index index] [index.sub1 index] [other index] [other.sub1 other]])
+
+ (matcho/match
+ (store/get-backlinks ztx 'index)
+ '{[:zd/parent] [errors other]})
+
+
+ ;; (println :q (store/datalog-query ztx '{:where [[e :xt/id 'index] [e :title t]] :find [e t]}))
+
+ (matcho/match
+ (store/datalog-query ztx '{:where [[e :xt/id 'index] [e :title t]] :find [e t]})
+ #{['index "Index"]})
+
+ (tu/doc? ztx 'index
+ {:zd/docname 'index
+ :title "Index"
+ :zd/subdocs [{:zd/docname 'index.sub1
+ :title "Subdoc"}]})
+
+ (matcho/match
+ (store/datalog-query ztx '{:where [[e :xt/id 'index] [e :title t]] :find [e t]})
+ #{['index "Index"]})
+
+ (store/file-save ztx 'newone ":title \"newone\"\n&sub\n:title \"newonesub\"")
+
+ (tu/doc? ztx 'newone
+ {:zd/docname 'newone
+ :title "newone"
+ :zd/parent 'index
+ :zd/subdocs [{:zd/docname 'newone.sub}]})
+
+ (matcho/match
+ (store/get-backlinks ztx 'index)
+ {[:zd/parent] '[errors newone other]})
+
+
+ (tu/doc? ztx 'newone.sub
+ {:zd/docname 'newone.sub
+ :zd/parent 'newone
+ :zd/subdoc? true
+ :title "newonesub"})
+
+ (matcho/match (store/datalog-get ztx 'newone) {:title "newone"})
+ (tu/doc? ztx 'newone {:title "newone"})
+
+ (matcho/match (store/datalog-get ztx 'newone.sub) {:title "newonesub"})
+ (tu/doc? ztx 'newone.sub {:title "newonesub"})
+
+ (store/file-save ztx 'newone ":title \"newone-change\"\n")
+
+ (matcho/match (store/datalog-get ztx 'newone) {:title "newone-change"})
+ (tu/doc? ztx 'newone {:title "newone-change"})
+
+ (is (nil? (store/datalog-get ztx 'newone.sub)))
+
+ (testing "errors"
+
+ (store/file-save ztx 'newone ":title \"newone\"\n:broken broken")
+
+ (matcho/match (store/datalog-get ztx 'newone)
+ {:title "newone"
+ :broken 'broken})
+
+ (store/get-errors ztx 'newone)
+
+ (tu/doc? ztx 'newone
+ {:title "newone"
+ :broken 'broken
+ :zd/errors [{:type :reference :path [:broken]}]})
+
+ (store/errors ztx)
+
+ (store/file-save ztx 'newone ":title \"newone\"\n:fixed index")
+
+ (tu/doc? ztx 'newone
+ {:title "newone"
+ :fixed 'index
+ :zd/errors nil?})
+ )
+
+ (testing "backlinks"
+
+ (store/file-save ztx 'target ":title \"target\"\n")
+ (store/file-save ztx 'backref-1 ":title \"backref-1\"\n:ref target")
+ (store/file-save ztx 'backref-2 ":title \"backref-2\"\n:ref target\n&sub\n:title \"sub\"\n:ref target")
+
+ (:zd/backlinks @ztx)
+
+ (tu/doc? ztx 'target {:title "target" :zd/backlinks {[:ref] '[backref-1 backref-2 backref-2.sub]}})
+
+ (store/file-delete ztx 'backref-2)
+
+ (is (nil? (store/doc-get ztx 'backref-2)))
+ (is (nil? (store/doc-get ztx 'backref-2.sub)))
+
+ (tu/doc? ztx 'target {:title "target" :zd/backlinks {[:ref] #(= '[backref-1] %)}})
+
+
+ (testing "zentext refs"
+ (store/file-save ztx 'zentext ":title \"zentext\"\n:desc /\nText #target")
+ (store/doc-get ztx 'zentext)
+ (tu/doc? ztx 'target {:title "target" :zd/backlinks {[:desc] '[zentext]}})
+ (store/file-delete ztx 'zentext)
+ (tu/doc? ztx 'target {:title "target" :zd/backlinks {'zentext nil?}})
+ )
+ )
+
+ (testing "validation after delete"
+
+ (store/file-save ztx 'a ":title \"a\"\n")
+ (store/file-save ztx 'b ":title \"b\"\n:ref a")
+
+ (tu/doc? ztx 'b {:title "b" :zd/errors nil?})
+ (tu/doc? ztx 'a {:title "a" :zd/backlinks {[:ref] ['b]}})
+
+ (store/file-delete ztx 'a)
+
+ (tu/doc? ztx 'b {:title "b" :zd/errors [{:type :reference }]}))
+
+ (testing "navigation"
+ (matcho/match (store/menu ztx)
+ [{:zd/docname 'index}
+ {:zd/docname 'other}])
+
+ (store/file-save ztx 'to-menu ":title \"a\"\n:zd/menu-order 3")
+
+ (matcho/match (store/menu ztx)
+ [{:zd/docname 'index}
+ {:zd/docname 'other}
+ {:zd/docname 'to-menu}])
+
+ (store/file-delete ztx 'to-menu)
+
+ (matcho/match (store/menu ztx)
+ [{:zd/docname 'index}
+ {:zd/docname 'other}
+ nil?]))
+
+ (store/re-validate ztx)
+ (is (store/errors ztx))
+
+ (store/breadcrump ztx 'orgs.o1)
+
+ (matcho/match
+ (->
+ (store/datalog-sugar-query ztx "e :xt/id id\n> e\n> e:title\n < asc e")
+ (update :result #(sort-by first %)))
+ '{:result
+ [[b "b"]
+ [backref-1 "backref-1"]
+ [errors "Errors"]
+ [index "Index"]
+ [index.sub1 "Subdoc"]
+ [newone "newone"]
+ [other "Other"]
+ [other.sub1 "Subdoc"]
+ [target "target"]]})
+
+
+ (store/to-doc ztx 'mydoc ":title \"title\"\n^badge\n:key /\n value\n^ann 1\n:another some/\ntext")
+
+ (testing "nested docs"
+
+ (store/file-save ztx 'nested.one ":title \"one\"")
+ (tu/doc? ztx 'nested.one {:title "one"}))
+
+ (testing "props"
+ (is (clojure.set/subset? #{":zd/subdoc?" ":broken"} (into #{} (mapv :name (store/props ztx))))))
+
+ (testing "rename"
+ (store/file-save ztx 'torename ":title \"torename\"\n:ref other")
+
+ (is (store/file-content ztx 'torename))
+
+ (tu/doc? ztx 'torename {:title "torename"})
+
+ (is (contains? (store/backlinked ztx 'index) 'torename))
+
+ (store/file-save ztx 'torename ":zd/docname renamed\n:title \"torename\"\n:ref other")
+
+ (is (tu/file-exists ztx "renamed.zd"))
+ (is (not (tu/file-exists ztx "torename.zd")))
+
+ (is (nil? (store/doc-get ztx 'torename)))
+
+ (is (nil? (store/file-content ztx 'torename)))
+
+ (is (store/file-content ztx 'renamed))
+
+ (tu/doc? ztx 'renamed {:title "torename"})
+
+ (is (not (contains? (store/backlinked ztx 'index) 'torename)))
+ (is (contains? (store/backlinked ztx 'index) 'renamed))
+
+ )
+
+ (testing "zd root is preloaded"
+ (tu/doc? ztx 'zd {:title "Zendoc"}))
+
+ (testing "errors page is preloaded"
+ (is (seq (store/errors ztx)))
+ (tu/doc? ztx 'errors {:zd/all-errors true})
+
+ )
+
+ )
+
+(deftest test-link-errors
+ (def ztx (tu/context ".tmp/link-errors"))
+
+ (tu/write-file ztx {:zd/docname 'index :other 'other})
+ (tu/write-file ztx {:zd/docname 'other :index 'index :text "text"})
+ (tu/write-file ztx {:zd/docname 'other.child :index 'index})
+ (tu/write-file ztx {:zd/docname 'other.child2 :index 'index})
+
+ (store/dir-load ztx)
+
+ (tu/doc?
+ ztx 'index
+ {:zd/docname 'index,
+ :other 'other
+ :zd/errors nil?
+ :zd/backlinks {[:index] '[other other.child other.child2]}})
+
+ (tu/doc?
+ ztx 'other.child
+ {:zd/docname 'other.child
+ :zd/errors nil?})
+
+ (is (empty? (store/errors ztx)))
+
+ (store/file-save ztx 'other ":zd/title \"Other\"")
+
+ (is (empty? (store/errors ztx))))
+
+(deftest test-errors-fixed
+ (def ztx (tu/context ".tmp/errors-fixed"))
+
+ (tu/write-file ztx {:zd/docname 'index :link 'unexisting})
+
+ (:zd/classes @ztx)
+
+ (store/dir-load ztx)
+
+ (tu/doc?
+ ztx 'index
+ {:zd/docname 'index,
+ :link 'unexisting
+ :zd/errors [{:type :reference,
+ :message "`unexisting` not found",
+ :path [:link]}]})
+
+ ;; (assert false "!")
+
+ (matcho/match
+ (store/errors ztx)
+ '{index [{:type :reference, :message "`unexisting` not found", :path [:link]}]})
+
+ (store/backlinked ztx 'unexisting)
+
+ (store/file-save ztx 'unexisting ":zd/title \"Fixed\"")
+
+ (is (empty? (store/errors ztx)))
+ )
+
+(deftest test-errors-appers
+ (def ztx (tu/context ".tmp/errors-appers"))
+
+ (tu/write-file ztx {:zd/docname 'index :link 'existing})
+ (tu/write-file ztx {:zd/docname 'existing})
+
+ (store/dir-load ztx)
+ (store/backlinked ztx 'existing)
+
+ (is (empty? (store/errors ztx)))
+
+ (store/file-delete ztx 'existing)
+
+ (matcho/match (store/errors ztx)
+ {'index [{:type :reference :path [:link]}]})
+ )
+
+
+
+(deftest test-backlinks
+ (def ztx (tu/context ".tmp/errors-appers"))
+
+ (tu/write-file ztx {:zd/docname 'index})
+
+ (tu/write-file ztx 'org "
+:zd/type zd.class
+:zd/summary [:org/name :org/address]
+:zd/require #{:org/name}
+&name zd.prop
+&address zd.prop
+:zd/summary true
+")
+
+ (tu/write-file ztx 'org.o1 "
+:zd/type org
+:org/name \"o1\"
+:org/address \"a1\"
+:extra 5
+")
+
+ (tu/write-file ztx 'org.o2 "
+:zd/type org
+:org/name \"o2\"
+:org/address \"a2\"
+:extra 5
+")
+
+ (store/dir-load ztx)
+
+ (store/search ztx "o2")
+ (store/search ztx "")
+ (store/search ztx nil)
+
+ (tu/doc? ztx 'org
+ {:zd/backlinks
+ '{[:zd/parent org] [org.o1 org.o2,] [:zd/type org] [org.o1 org.o2]}})
+
+ )
+
+(deftest test-rename
+ (def ztx (tu/context ".tmp/test-rename"))
+
+ (tu/write-file ztx {:zd/docname 'org})
+ (tu/write-file ztx {:zd/docname 'org.o1})
+ (tu/write-file ztx {:zd/docname 'org.o2})
+ (tu/write-file ztx {:zd/docname 'org.o2.item1})
+
+ (store/dir-load ztx)
+
+ (is (tu/file-exists ztx "org.zd"))
+ (is (tu/file-exists ztx "org/o1.zd"))
+ (is (tu/file-exists ztx "org/o2.zd"))
+ (is (tu/file-exists ztx "org/o2/item1.zd"))
+
+ (matcho/match
+ (store/children ztx 'org)
+ #{'org.o2 'org.o1})
+
+ (store/file-save ztx 'org ":zd/docname organization")
+
+ (is (tu/file-exists ztx "organization.zd"))
+ (is (tu/file-exists ztx "organization/o1.zd"))
+ (is (tu/file-exists ztx "organization/o2.zd"))
+ (is (tu/file-exists ztx "organization/o2/item1.zd"))
+
+
+ (is (not (tu/file-exists ztx "org.zd")))
+ (is (not (tu/file-exists ztx "org/o1.zd")))
+ (is (not (tu/file-exists ztx "org/o2.zd")))
+ (is (not (tu/file-exists ztx "org/o2/item1.zd")))
+
+ (store/file-delete ztx 'organization)
+
+ (is (not (tu/file-exists ztx "organization.zd")))
+ (is (not (tu/file-exists ztx "organization/o1.zd")))
+ (is (not (tu/file-exists ztx "organization/o2.zd")))
+ (is (not (tu/file-exists ztx "organization/o2/item1.zd")))
+
+ )
+
+(deftest test-inference
+ (def ztx (tu/context ".tmp/test-inference"))
+
+ (tu/write-file ztx {:zd/docname 'index})
+ (tu/write-file ztx {:zd/docname 'org :zd/child-type 'org :zd/require #{:a :b}})
+ (tu/write-file ztx {:zd/docname 'org.o1})
+
+ (store/dir-load ztx)
+
+ (matcho/match
+ (store/doc-get ztx 'org.o1)
+ #:zd{:errors
+ [{:type :required, :message ":b is required", :path [:b]}
+ {:type :required, :message ":a is required", :path [:a]}]})
+
+ (store/errors ztx)
+
+ )
diff --git a/test/zd/test_utils.clj b/test/zd/test_utils.clj
index c94de2b..8b53473 100644
--- a/test/zd/test_utils.clj
+++ b/test/zd/test_utils.clj
@@ -1,7 +1,20 @@
(ns zd.test-utils
(:require
[zen.core :as zen]
- [clojure.java.io :as io]))
+ [clojure.string :as str]
+ [zen-web.core]
+ [hickory.core]
+ [matcho.core :as matcho]
+ [clojure.java.io :as io]
+ [clojure.walk]
+ [zd.store :as store])
+ (:import [java.nio.file Files Path]))
+
+(def wd (System/getProperty "user.dir"))
+
+(defn path [x]
+ (Path/of (java.net.URI. (str "file:" wd "/" x))))
+
(defn req-body [s]
(io/input-stream (.getBytes s)))
@@ -12,6 +25,165 @@
(when (.exists f)
(slurp f))))
+(def test-system
+
+ '{:ns zd.test
+ :import #{zd}
+
+ zendoc
+ {:zen/tags #{zd/zendoc}
+ :paths [".tmp"]
+ :root "index"}
+
+ datalog
+ {:zen/tags #{zen/start zd.engines/datalog}
+ :engine zd.engines/datalog
+ :zendoc zendoc}
+
+ fs
+ {:zen/tags #{zen/start zd.engines/fs}
+ :engine zd.engines/fs
+ :zendoc zendoc}
+
+ system
+ {:zen/tags #{zen/system}
+ :start [datalog fs]}})
+
+(defn mk-dir [dir]
+ (Files/createDirectories (path dir) (make-array java.nio.file.attribute.FileAttribute 0)))
+
+(defn mk-doc [docname content]
+ (spit (str ".tmp/" (str/replace (str docname) #"\\." "/") ".zd") content))
+
+
+
+
+(defn rm-dir [dir]
+ (when (Files/exists (path dir) (make-array java.nio.file.LinkOption 0))
+ (let [dir (java.io.File. dir)]
+ (doseq [f (->> (file-seq dir) (sort) (reverse))]
+ (.delete f))
+ (.delete dir))))
+
+(defn clear-dir [dir]
+ (rm-dir dir)
+ (mk-dir dir))
+
+(defn context [dir]
+ (clear-dir dir)
+ (zen/new-context {:zd/dir dir}))
+
+
+(defn write-file
+ ([ztx docname content]
+ (let [dir (:zd/dir @ztx)
+ file (store/docname-to-path docname)
+ file-dir (store/parent-dir file)]
+ (mk-dir (str dir "/" file-dir))
+ (spit (str dir "/" file) content)))
+ ([ztx content]
+ (let [dir (:zd/dir @ztx)
+ file (store/docname-to-path (:zd/docname content))
+ file-dir (store/parent-dir file)]
+ (mk-dir (str dir "/" file-dir))
+ (spit (str dir "/" file)
+ (->> (dissoc content :zd/docname)
+ (mapv (fn [[k v]]
+ (str (pr-str k) " " (pr-str v))))
+ (str/join "\n"))))))
+
+(defn file-exists [ztx file]
+ (let [dir (:zd/dir @ztx)]
+ (.exists (io/file (str dir "/" file)))))
+
+(defmacro doc? [ztx docname pat]
+ `(let [doc# (store/doc-get ~ztx ~docname)]
+ (matcho/match doc# ~pat)
+ doc#))
+
+(defonce ctx (atom (zen/new-context)))
+
+(defn reset-project [docs]
+ (when-let [ztx @ctx]
+ (zen/stop-system ztx))
+ (rm-dir ".tmp")
+ (mk-dir ".tmp")
+ (doseq [[docname doc] docs]
+ (mk-doc docname doc))
+ (let [ztx (zen/new-context)]
+ (reset! ctx ztx)
+ (zen/load-ns ztx test-system)
+ (zen/start-system ztx 'zd.test/system)))
+
+(defn op [method params]
+ (zen/op-call @ctx method params))
+
+(defn http-get [uri & [params headers]]
+ (let [resp (zen-web.core/dispatch @ctx 'zd/api {:hiccup true :request-method :get :uri uri})]
+ (if (:body resp)
+ (-> (hickory.core/as-hiccup (hickory.core/parse (:body resp)))
+ (nth 0)
+ (nth 3))
+ resp)))
+
+(defn get-symbol [s]
+ (zen/get-symbol @ctx s))
+
+;; TODO: this logic should live in codebase
+;; (defn get-doc [s]
+;; (zd.memstore/get-doc @ctx s))
+
+;; (defn all-errors []
+;; (zd.memstore/get-all-errors @ctx))
+
+(defmacro match-doc [s pat]
+ `(let [d# (get-doc ~s)]
+ (matcho/match d# ~pat)
+ d#))
+
+(defn hiccup-find [body id]
+ (let [res (atom [])]
+ (clojure.walk/postwalk (fn [x]
+ (when (and (vector? x) (map? (second x)) (= id (:id (second x))))
+ (swap! res conj x))
+ x) body)
+ @res))
+
+(defn hiccup-text [body text]
+ (let [res (atom [])]
+ (clojure.walk/postwalk (fn [x]
+ (when (and (string? x) (= x text))
+ (swap! res conj x))
+ x) body)
+ @res))
+
+(defn http [req]
+ (-> (zen-web.core/handle @ctx 'zd/api (update req :body (fn [x] (when x (req-body x)))))
+ (update :body (fn [b]
+ (when b
+ (-> (hickory.core/as-hiccup (hickory.core/parse b))
+ (nth 0)
+ (nth 3)))))))
+
+(defmacro http-match [req pat]
+ `(let [resp# (http ~req)]
+ (matcho/match
+ resp# ~pat)
+ resp#))
+
+(defmacro hiccup-match [body id patt]
+ `(let [res# (hiccup-find ~body ~id)]
+ (matcho/match res# ~patt)
+ res#))
+
+;; (defn query [q & params]
+;; (apply zd.datalog/query @ctx q params))
+
+;; (defn datalog-save [data]
+;; (zd.datalog/save-doc )
+;; )
+
+
(defn prepare! [ztx]
(zen/stop-system ztx)
@@ -20,3 +192,22 @@
(zen/read-ns ztx 'zd.test)
(zen/start-system ztx 'zd.test/system))
+
+
+(defn zd [ & xs]
+ (->> xs
+ (mapv (fn [x]
+ (->> x (mapv pr-str) (str/join " "))))
+ (str/join "\n")))
+
+(comment
+ (reset-project {'person ":title \"Person\""})
+ (get-doc 'person)
+ (-> "person" http-get (hiccup-find "title"))
+
+ (require 'hickory.core)
+
+
+
+
+ )
diff --git a/test/zd/zentext_test.clj b/test/zd/zentext_test.clj
index 68027cc..82a9378 100644
--- a/test/zd/zentext_test.clj
+++ b/test/zd/zentext_test.clj
@@ -1,6 +1,7 @@
(ns zd.zentext-test
(:require
- [zd.api]
+ ;; [zd.api]
+ [zd.view.zentext]
[zd.zentext :as zentext]
[zen.core :as zen]
[matcho.core :as matcho]
@@ -287,3 +288,12 @@ select 1
(match "\\" [:div [:p {:class keyword?} "\n" "\\"]])
(match "foo@foo.bar" [:div [:p {:class keyword?} "\n" "foo@foo.bar"]]))
+
+(t/deftest collect-links
+ (t/is (= (zentext/extract-links " Here is a \\not-link text #link and #another.link + @ivan\n```\nText with #no-link\n```\n But this #should-be-link")
+ #{'link 'another.link 'person.ivan 'should-be-link}))
+
+ (zentext/extract-links "#at-start abc#d some \ntext \\#esaped #e some text #doted.name \ntext #word-x #at-end")
+ #{'doted.name 'at-start 'word-x 'at-end 'e}
+
+ )
diff --git a/todo.org b/todo.org
new file mode 100644
index 0000000..2691580
--- /dev/null
+++ b/todo.org
@@ -0,0 +1,25 @@
+* [x] subdocs render
+* [x] save
+* [x] symbols autocomplete
+* [x] default block type (badge, attribute, block)
+* [x] delete
+* [x] symbols autocomplete in subdoc name
+* [x] rename
+* [x] fix validation
+* [x] global errors page
+* [x] load zd schema
+* [x] index
+* [ ] group backlinks by type [. <- attr [type]]
+* [ ] fix errors on parents during initial load
+* [ ] remove backlinks for :zd/subdocs
+* [ ] do not collect links from subs to parent
+* [ ] new item
+* [ ] content navigation
+* [ ] subdoc page
+* [ ] nested menu
+* [ ] search (name, title, desc)
+* [ ] timeline
+* [ ] parents puzzle
+* [ ] invisible ancor for subdocs
+* [ ] link preview load with ancor
+* [ ] style doc preview
diff --git a/docs/basics.zd b/zd-docs/basics.zd
similarity index 100%
rename from docs/basics.zd
rename to zd-docs/basics.zd
diff --git a/docs/boxdoc.edn b/zd-docs/boxdoc.edn
similarity index 100%
rename from docs/boxdoc.edn
rename to zd-docs/boxdoc.edn
diff --git a/zd-docs/customers-x/Modeling.zd b/zd-docs/customers-x/Modeling.zd
new file mode 100644
index 0000000..79b8b72
--- /dev/null
+++ b/zd-docs/customers-x/Modeling.zd
@@ -0,0 +1,122 @@
+:title "Modeling Docs"
+:icon [:fa-duotone :fa-face-cowboy-hat]
+:menu-order 2
+
+:desc /
+
+:absolute-names /
+
+There is a special document Resource, where you can define
+attributes which should not be prefixed with namespace
+
+```zd
+ :zd/docname Resource
+
+ &name
+ :zd/type zd.Property
+ :zd/data-type zd.string
+
+```
+
+Property `Resource.name` can be used as
+
+```zd
+ :zd/docname mydoc
+ :name "Just a name"
+```
+
+:intro /
+How to model with zendoc?
+
+In zendoc everything is a resource or document. Some docs may represent
+entities from real world and others are abstract concepts.
+
+Every doc may have multiple types - `:type` property, which is reference to
+other doc.
+
+It's recommended for concepts to have a `:type Class`
+For class you can define a properties with nested docs of type `Property`
+
+```zd
+ :zd/docname person
+ :zd/type zd.Class
+ :zd/required #{ .name, .telegram }
+
+ &name
+ :zd/type zd.Property
+ :zd/data-type zd.string
+ :zd/annotation zd.badge
+
+ &telegram
+ :zd/type zd.Property
+ :zd/data-type zd.string
+ :zd/annotation zd.badge
+
+ &male
+ :zd/type zd.Class
+
+ &female
+ :zd/type zd.Class
+
+ &gender
+ :zd/type zd.Property
+ :zd/enum #{ .male .female }
+ :zd/annotation zd.badge
+
+```
+
+
+```zd
+ :zd/docname person.ivan
+ :zd/type person
+ :person/name "Ivan"
+ :person/telegram "ivantelega"
+ :person/gender person.male
+
+```
+
+Attribute of name `namespace/name` will look for definition in `namespace.name`.
+
+:subclasses /
+
+While defining Class you may state that this is a subclass of another class.
+That means all instances of this class are instances of superclass.
+Zendoc will be able to use this for inference.
+
+```zd
+ :zd/type zd.Class
+ :zd/subclass person
+
+```
+
+```zd
+ :zd/type Samurai
+```
+
+Now you can search "Nikolai" like a `person`
+
+```datalog
+ e :zd/type person
+ > e
+```
+
+:same-as /
+
+If property has a `zd.same-as` zendoc will do the inference of this attributes.
+
+```zd
+ :zd/type zd.Property
+ :zd/same-as :foaf/name
+
+```
+
+```zd
+ :person.name Nikolai
+```
+
+You may search it as
+
+```datalog
+ e :foaf/name "Nikolai"
+ > e
+```
\ No newline at end of file
diff --git a/customers-x/_index.zd b/zd-docs/customers-x/_index.zd
similarity index 95%
rename from customers-x/_index.zd
rename to zd-docs/customers-x/_index.zd
index a9e8318..73b2f1f 100644
--- a/customers-x/_index.zd
+++ b/zd-docs/customers-x/_index.zd
@@ -1,7 +1,10 @@
-:title "Index"
:menu-order 1
:icon [:fa-duotone :fa-book]
+^badge
:section "zd-system"
+
+:title "Index"
:desc /
shows database structure in a compact format. folds documents which have >20 children.
+
:zd/index
\ No newline at end of file
diff --git a/customers-x/_schema.zd b/zd-docs/customers-x/_schema.zd
similarity index 98%
rename from customers-x/_schema.zd
rename to zd-docs/customers-x/_schema.zd
index ad160be..fb23964 100644
--- a/customers-x/_schema.zd
+++ b/zd-docs/customers-x/_schema.zd
@@ -1,10 +1,12 @@
+:zd/menu-order 2
:title "Default schema"
-:menu-order 2
+
:desc /
+
this document sets the knowledge base structure
:icon [:fa-regular :fa-file-chart-pie]
diff --git a/customers-x/backlog.zd b/zd-docs/customers-x/backlog.zd
similarity index 96%
rename from customers-x/backlog.zd
rename to zd-docs/customers-x/backlog.zd
index 6a349ec..d2b0e8d 100644
--- a/customers-x/backlog.zd
+++ b/zd-docs/customers-x/backlog.zd
@@ -1,13 +1,14 @@
-:title "Zendoc backlog"
-
+:menu-order 1
:icon [:fa-solid :fa-rectangle-list]
^link-badge
:repo "https://github.com/zen-lang/zendoc"
-:menu-order 1
+^badge
:section "first"
+
+:title "Zendoc backlog"
:desc /
features and issues
\ No newline at end of file
diff --git a/customers-x/backlog/_schema.zd b/zd-docs/customers-x/backlog/_schema.zd
similarity index 100%
rename from customers-x/backlog/_schema.zd
rename to zd-docs/customers-x/backlog/_schema.zd
diff --git a/customers-x/backlog/features.zd b/zd-docs/customers-x/backlog/features.zd
similarity index 100%
rename from customers-x/backlog/features.zd
rename to zd-docs/customers-x/backlog/features.zd
diff --git a/customers-x/backlog/features/attachments.zd b/zd-docs/customers-x/backlog/features/attachments.zd
similarity index 100%
rename from customers-x/backlog/features/attachments.zd
rename to zd-docs/customers-x/backlog/features/attachments.zd
diff --git a/customers-x/backlog/features/browser.zd b/zd-docs/customers-x/backlog/features/browser.zd
similarity index 100%
rename from customers-x/backlog/features/browser.zd
rename to zd-docs/customers-x/backlog/features/browser.zd
diff --git a/customers-x/backlog/features/comments.zd b/zd-docs/customers-x/backlog/features/comments.zd
similarity index 100%
rename from customers-x/backlog/features/comments.zd
rename to zd-docs/customers-x/backlog/features/comments.zd
diff --git a/customers-x/backlog/features/datalog.zd b/zd-docs/customers-x/backlog/features/datalog.zd
similarity index 100%
rename from customers-x/backlog/features/datalog.zd
rename to zd-docs/customers-x/backlog/features/datalog.zd
diff --git a/customers-x/backlog/features/dataviz.zd b/zd-docs/customers-x/backlog/features/dataviz.zd
similarity index 100%
rename from customers-x/backlog/features/dataviz.zd
rename to zd-docs/customers-x/backlog/features/dataviz.zd
diff --git a/customers-x/backlog/features/desktop.zd b/zd-docs/customers-x/backlog/features/desktop.zd
similarity index 100%
rename from customers-x/backlog/features/desktop.zd
rename to zd-docs/customers-x/backlog/features/desktop.zd
diff --git a/customers-x/backlog/features/docmodel.zd b/zd-docs/customers-x/backlog/features/docmodel.zd
similarity index 100%
rename from customers-x/backlog/features/docmodel.zd
rename to zd-docs/customers-x/backlog/features/docmodel.zd
diff --git a/customers-x/backlog/features/editor.zd b/zd-docs/customers-x/backlog/features/editor.zd
similarity index 100%
rename from customers-x/backlog/features/editor.zd
rename to zd-docs/customers-x/backlog/features/editor.zd
diff --git a/customers-x/backlog/features/export.zd b/zd-docs/customers-x/backlog/features/export.zd
similarity index 100%
rename from customers-x/backlog/features/export.zd
rename to zd-docs/customers-x/backlog/features/export.zd
diff --git a/customers-x/backlog/features/fts.zd b/zd-docs/customers-x/backlog/features/fts.zd
similarity index 100%
rename from customers-x/backlog/features/fts.zd
rename to zd-docs/customers-x/backlog/features/fts.zd
diff --git a/customers-x/backlog/features/github.zd b/zd-docs/customers-x/backlog/features/github.zd
similarity index 100%
rename from customers-x/backlog/features/github.zd
rename to zd-docs/customers-x/backlog/features/github.zd
diff --git a/customers-x/backlog/features/gitsync.zd b/zd-docs/customers-x/backlog/features/gitsync.zd
similarity index 100%
rename from customers-x/backlog/features/gitsync.zd
rename to zd-docs/customers-x/backlog/features/gitsync.zd
diff --git a/customers-x/backlog/features/linkedin.zd b/zd-docs/customers-x/backlog/features/linkedin.zd
similarity index 100%
rename from customers-x/backlog/features/linkedin.zd
rename to zd-docs/customers-x/backlog/features/linkedin.zd
diff --git a/customers-x/backlog/features/ml.zd b/zd-docs/customers-x/backlog/features/ml.zd
similarity index 100%
rename from customers-x/backlog/features/ml.zd
rename to zd-docs/customers-x/backlog/features/ml.zd
diff --git a/customers-x/backlog/features/multitenancy.zd b/zd-docs/customers-x/backlog/features/multitenancy.zd
similarity index 100%
rename from customers-x/backlog/features/multitenancy.zd
rename to zd-docs/customers-x/backlog/features/multitenancy.zd
diff --git a/customers-x/backlog/features/navigation.zd b/zd-docs/customers-x/backlog/features/navigation.zd
similarity index 100%
rename from customers-x/backlog/features/navigation.zd
rename to zd-docs/customers-x/backlog/features/navigation.zd
diff --git a/customers-x/backlog/features/pipedrive.zd b/zd-docs/customers-x/backlog/features/pipedrive.zd
similarity index 100%
rename from customers-x/backlog/features/pipedrive.zd
rename to zd-docs/customers-x/backlog/features/pipedrive.zd
diff --git a/customers-x/backlog/features/popup.zd b/zd-docs/customers-x/backlog/features/popup.zd
similarity index 100%
rename from customers-x/backlog/features/popup.zd
rename to zd-docs/customers-x/backlog/features/popup.zd
diff --git a/customers-x/backlog/features/publish.zd b/zd-docs/customers-x/backlog/features/publish.zd
similarity index 100%
rename from customers-x/backlog/features/publish.zd
rename to zd-docs/customers-x/backlog/features/publish.zd
diff --git a/customers-x/backlog/features/pubsub.zd b/zd-docs/customers-x/backlog/features/pubsub.zd
similarity index 100%
rename from customers-x/backlog/features/pubsub.zd
rename to zd-docs/customers-x/backlog/features/pubsub.zd
diff --git a/customers-x/backlog/features/render.zd b/zd-docs/customers-x/backlog/features/render.zd
similarity index 100%
rename from customers-x/backlog/features/render.zd
rename to zd-docs/customers-x/backlog/features/render.zd
diff --git a/customers-x/backlog/features/sidelinks.zd b/zd-docs/customers-x/backlog/features/sidelinks.zd
similarity index 100%
rename from customers-x/backlog/features/sidelinks.zd
rename to zd-docs/customers-x/backlog/features/sidelinks.zd
diff --git a/customers-x/backlog/features/subdocuments.zd b/zd-docs/customers-x/backlog/features/subdocuments.zd
similarity index 100%
rename from customers-x/backlog/features/subdocuments.zd
rename to zd-docs/customers-x/backlog/features/subdocuments.zd
diff --git a/customers-x/backlog/features/toggle.zd b/zd-docs/customers-x/backlog/features/toggle.zd
similarity index 100%
rename from customers-x/backlog/features/toggle.zd
rename to zd-docs/customers-x/backlog/features/toggle.zd
diff --git a/customers-x/backlog/features/userspace.zd b/zd-docs/customers-x/backlog/features/userspace.zd
similarity index 100%
rename from customers-x/backlog/features/userspace.zd
rename to zd-docs/customers-x/backlog/features/userspace.zd
diff --git a/customers-x/backlog/features/validation.zd b/zd-docs/customers-x/backlog/features/validation.zd
similarity index 100%
rename from customers-x/backlog/features/validation.zd
rename to zd-docs/customers-x/backlog/features/validation.zd
diff --git a/customers-x/backlog/features/zentext.zd b/zd-docs/customers-x/backlog/features/zentext.zd
similarity index 100%
rename from customers-x/backlog/features/zentext.zd
rename to zd-docs/customers-x/backlog/features/zentext.zd
diff --git a/customers-x/backlog/issues.zd b/zd-docs/customers-x/backlog/issues.zd
similarity index 100%
rename from customers-x/backlog/issues.zd
rename to zd-docs/customers-x/backlog/issues.zd
diff --git a/customers-x/backlog/issues/delete-redirect.zd b/zd-docs/customers-x/backlog/issues/delete-redirect.zd
similarity index 100%
rename from customers-x/backlog/issues/delete-redirect.zd
rename to zd-docs/customers-x/backlog/issues/delete-redirect.zd
diff --git a/customers-x/backlog/issues/editor.zd b/zd-docs/customers-x/backlog/issues/editor.zd
similarity index 100%
rename from customers-x/backlog/issues/editor.zd
rename to zd-docs/customers-x/backlog/issues/editor.zd
diff --git a/customers-x/backlog/issues/favicon.zd b/zd-docs/customers-x/backlog/issues/favicon.zd
similarity index 100%
rename from customers-x/backlog/issues/favicon.zd
rename to zd-docs/customers-x/backlog/issues/favicon.zd
diff --git a/customers-x/backlog/issues/subdocs-lastkey.zd b/zd-docs/customers-x/backlog/issues/subdocs-lastkey.zd
similarity index 100%
rename from customers-x/backlog/issues/subdocs-lastkey.zd
rename to zd-docs/customers-x/backlog/issues/subdocs-lastkey.zd
diff --git a/customers-x/backlog/issues/timeline.zd b/zd-docs/customers-x/backlog/issues/timeline.zd
similarity index 100%
rename from customers-x/backlog/issues/timeline.zd
rename to zd-docs/customers-x/backlog/issues/timeline.zd
diff --git a/customers-x/backlog/issues/unicode.zd b/zd-docs/customers-x/backlog/issues/unicode.zd
similarity index 100%
rename from customers-x/backlog/issues/unicode.zd
rename to zd-docs/customers-x/backlog/issues/unicode.zd
diff --git a/customers-x/backlog/tags/business-features.zd b/zd-docs/customers-x/backlog/tags/business-features.zd
similarity index 100%
rename from customers-x/backlog/tags/business-features.zd
rename to zd-docs/customers-x/backlog/tags/business-features.zd
diff --git a/customers-x/backlog/tags/database-engine.zd b/zd-docs/customers-x/backlog/tags/database-engine.zd
similarity index 100%
rename from customers-x/backlog/tags/database-engine.zd
rename to zd-docs/customers-x/backlog/tags/database-engine.zd
diff --git a/customers-x/backlog/tags/tech-platform.zd b/zd-docs/customers-x/backlog/tags/tech-platform.zd
similarity index 100%
rename from customers-x/backlog/tags/tech-platform.zd
rename to zd-docs/customers-x/backlog/tags/tech-platform.zd
diff --git a/customers-x/backlog/tags/usability.zd b/zd-docs/customers-x/backlog/tags/usability.zd
similarity index 100%
rename from customers-x/backlog/tags/usability.zd
rename to zd-docs/customers-x/backlog/tags/usability.zd
diff --git a/zd-docs/customers-x/city.zd b/zd-docs/customers-x/city.zd
new file mode 100644
index 0000000..99af3d9
--- /dev/null
+++ b/zd-docs/customers-x/city.zd
@@ -0,0 +1,2 @@
+:title "Cities"
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/city/Estoril.zd b/zd-docs/customers-x/city/Estoril.zd
new file mode 100644
index 0000000..970f77c
--- /dev/null
+++ b/zd-docs/customers-x/city/Estoril.zd
@@ -0,0 +1,6 @@
+:title "Estoril"
+^badge
+:type loc.City
+^badge
+:loc.in country.Portugal
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/city/Lisboa.zd b/zd-docs/customers-x/city/Lisboa.zd
new file mode 100644
index 0000000..035a7e6
--- /dev/null
+++ b/zd-docs/customers-x/city/Lisboa.zd
@@ -0,0 +1,7 @@
+
+:title "Lisboa"
+^badge
+:type loc.City
+^badge
+:loc.in country.Portugal
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/country/Portugal.zd b/zd-docs/customers-x/country/Portugal.zd
new file mode 100644
index 0000000..192d9de
--- /dev/null
+++ b/zd-docs/customers-x/country/Portugal.zd
@@ -0,0 +1,3 @@
+:title "Portugal"
+:type loc.Country
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/country/US.zd b/zd-docs/customers-x/country/US.zd
new file mode 100644
index 0000000..d36cdf2
--- /dev/null
+++ b/zd-docs/customers-x/country/US.zd
@@ -0,0 +1,2 @@
+:title "US"
+:desc /
\ No newline at end of file
diff --git a/customers-x/customers.zd b/zd-docs/customers-x/customers.zd
similarity index 80%
rename from customers-x/customers.zd
rename to zd-docs/customers-x/customers.zd
index e0d52a9..03a58df 100644
--- a/customers-x/customers.zd
+++ b/zd-docs/customers-x/customers.zd
@@ -1,29 +1,27 @@
:icon [:fa-solid :fa-store]
+:menu-order 1
+^badge
+:country #{countries.ru}
+^badge
+:tags [tags.telemed]
:title "customers list"
-:menu-order 1
:desc /
a description that refers to #people.john and mentions @todd.
-
have you seen it @todd?
-
it also refers to #customers.flame and even mentions #people.john!
+there is also a link that includes hash char - [[a https://docs.google.com/spreadsheets/d/1BbaAQLo2RaktkKJQxyDN2cqTNbit4mnA/edit#gid=1568783766 Link]]
-there is also a link that includes hash char - [[a https://docs.google.com/spreadsheets/d/1BbaAQLo2RaktkKJQxyDN2cqTNbit4mnA/edit#gid=1568783766]]
-
-:country #{countries.ru}
-:tags [tags.telemed]
:product-champion people.john
-^table [:xt/id]
-:customers datalog/
-{:find [(pull e [:xt/id])]
-:where [[e :parent customers]]
-:in [customers]}
+
+:customers ?/
+e :parent #customers
+> e
&partners-list
diff --git a/customers-x/customers/_schema.zd b/zd-docs/customers-x/customers/_schema.zd
similarity index 100%
rename from customers-x/customers/_schema.zd
rename to zd-docs/customers-x/customers/_schema.zd
diff --git a/customers-x/customers/flame.zd b/zd-docs/customers-x/customers/flame.zd
similarity index 100%
rename from customers-x/customers/flame.zd
rename to zd-docs/customers-x/customers/flame.zd
diff --git a/zd-docs/customers-x/file.txt b/zd-docs/customers-x/file.txt
new file mode 100644
index 0000000..f3f6d3d
--- /dev/null
+++ b/zd-docs/customers-x/file.txt
@@ -0,0 +1 @@
+just a string
diff --git a/zd-docs/customers-x/foaf/Person.zd b/zd-docs/customers-x/foaf/Person.zd
new file mode 100644
index 0000000..191375b
--- /dev/null
+++ b/zd-docs/customers-x/foaf/Person.zd
@@ -0,0 +1,2 @@
+:title "Person"
+:desc /
\ No newline at end of file
diff --git a/customers-x/index.zd b/zd-docs/customers-x/index.zd
similarity index 51%
rename from customers-x/index.zd
rename to zd-docs/customers-x/index.zd
index c1c9828..317d72a 100644
--- a/customers-x/index.zd
+++ b/zd-docs/customers-x/index.zd
@@ -1,3 +1,5 @@
-:title "Customers X knowledge base"
+:menu-order 0
+:title "Home"
+:zd/icon [:fa-duotone :fa-house-chimney-medical]
:desc /
X company wants to gather knowledge and get insights in customer relations.
\ No newline at end of file
diff --git a/zd-docs/customers-x/loc/City.zd b/zd-docs/customers-x/loc/City.zd
new file mode 100644
index 0000000..be32bc5
--- /dev/null
+++ b/zd-docs/customers-x/loc/City.zd
@@ -0,0 +1,2 @@
+:title "City"
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/loc/Country.zd b/zd-docs/customers-x/loc/Country.zd
new file mode 100644
index 0000000..96fcd67
--- /dev/null
+++ b/zd-docs/customers-x/loc/Country.zd
@@ -0,0 +1,2 @@
+:title "Country"
+:desc /
\ No newline at end of file
diff --git a/customers-x/macros.zd b/zd-docs/customers-x/macros.zd
similarity index 72%
rename from customers-x/macros.zd
rename to zd-docs/customers-x/macros.zd
index 3774f33..fb07193 100644
--- a/customers-x/macros.zd
+++ b/zd-docs/customers-x/macros.zd
@@ -1,10 +1,13 @@
:title "Macros examples"
-:icon [:fa-duotone :fa-circle-m]
-:tags #{tags.builtin}
+:zd/icon [:fa-duotone :fa-circle-m]
:menu-order 4
+:tags #{tags.builtin}
+
:office-locations (load "office-locations.json" :json)
:yaml-example (load "sample.yaml" :yaml)
:not-found (load "not-found.json")
:string-file (load "file.txt")
-:macro-notfound (unload "nofile.txt")
\ No newline at end of file
+:macro-notfound (unload "nofile.txt")
+
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/needs.zd b/zd-docs/customers-x/needs.zd
new file mode 100644
index 0000000..5097114
--- /dev/null
+++ b/zd-docs/customers-x/needs.zd
@@ -0,0 +1,13 @@
+:zd/menu-order 110
+:zd/icon [:fa-duotone :fa-lamp]
+
+:title "needs"
+:desc /
+Company Needs
+
+:needs ?/
+
+c :zd/type #prod.Case
+c :need n
+> n
+> (count c)
diff --git a/zd-docs/customers-x/needs/big-data.zd b/zd-docs/customers-x/needs/big-data.zd
new file mode 100644
index 0000000..6659df6
--- /dev/null
+++ b/zd-docs/customers-x/needs/big-data.zd
@@ -0,0 +1,2 @@
+:title "big data"
+:desc /
\ No newline at end of file
diff --git a/customers-x/needs/hl7.zd b/zd-docs/customers-x/needs/hl7.zd
similarity index 100%
rename from customers-x/needs/hl7.zd
rename to zd-docs/customers-x/needs/hl7.zd
diff --git a/zd-docs/customers-x/office-locations.json b/zd-docs/customers-x/office-locations.json
new file mode 100644
index 0000000..ed4b565
--- /dev/null
+++ b/zd-docs/customers-x/office-locations.json
@@ -0,0 +1,12 @@
+{
+ "meta": {
+ "name": "office-location.json"
+ },
+ "locations": [
+ {
+ "Bangkok, Thailand": [
+ "street"
+ ]
+ }
+ ]
+}
diff --git a/zd-docs/customers-x/org.zd b/zd-docs/customers-x/org.zd
new file mode 100644
index 0000000..5dbf10f
--- /dev/null
+++ b/zd-docs/customers-x/org.zd
@@ -0,0 +1,2 @@
+:title "org ontology"
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/org/CTO.zd b/zd-docs/customers-x/org/CTO.zd
new file mode 100644
index 0000000..2607e9e
--- /dev/null
+++ b/zd-docs/customers-x/org/CTO.zd
@@ -0,0 +1,2 @@
+:title "CTO"
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/org/Engineer.zd b/zd-docs/customers-x/org/Engineer.zd
new file mode 100644
index 0000000..c5d8427
--- /dev/null
+++ b/zd-docs/customers-x/org/Engineer.zd
@@ -0,0 +1 @@
+:title "Engineer"
\ No newline at end of file
diff --git a/zd-docs/customers-x/org/Member.zd b/zd-docs/customers-x/org/Member.zd
new file mode 100644
index 0000000..5b22668
--- /dev/null
+++ b/zd-docs/customers-x/org/Member.zd
@@ -0,0 +1,13 @@
+^badge
+:zd/type #{ zd.class }
+:title "Member"
+:desc /
+ This is i a class
+
+:query ?/
+
+e :type #org.Member
+> e:org.organization
+> e:org.person
+> e:org.role
+> e:org.memberSince
\ No newline at end of file
diff --git a/zd-docs/customers-x/org/Organization.zd b/zd-docs/customers-x/org/Organization.zd
new file mode 100644
index 0000000..0e88e59
--- /dev/null
+++ b/zd-docs/customers-x/org/Organization.zd
@@ -0,0 +1,26 @@
+:title "Organization"
+:desc /
+
+:zd/type zd.class
+
+
+&site
+^badge
+:type Property
+^badge
+:required true
+^badge
+:data-type url
+^badge
+:annotation zd.link-badge
+
+&linkedin
+:zd/type zd.prop
+:zd/data-type zd.url
+:zd/annotation zd.link-badge
+
+&location
+:zd/type zd.prop
+:zd/subsdata-type zd.symbol
+:zd/ref-type loc.Country
+:zd/annotation zd.link-badge
\ No newline at end of file
diff --git a/zd-docs/customers-x/org/Person.zd b/zd-docs/customers-x/org/Person.zd
new file mode 100644
index 0000000..ea7a079
--- /dev/null
+++ b/zd-docs/customers-x/org/Person.zd
@@ -0,0 +1,4 @@
+^badge
+:zd/type zd.class
+:title "Person"
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/org/TechLead.zd b/zd-docs/customers-x/org/TechLead.zd
new file mode 100644
index 0000000..a88b929
--- /dev/null
+++ b/zd-docs/customers-x/org/TechLead.zd
@@ -0,0 +1,3 @@
+
+:title "Tech Lead"
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/orgs.zd b/zd-docs/customers-x/orgs.zd
new file mode 100644
index 0000000..a68e7af
--- /dev/null
+++ b/zd-docs/customers-x/orgs.zd
@@ -0,0 +1,43 @@
+:icon [:fa-duotone :fa-building]
+:menu-order 100
+:zd/type zd.class
+:zd/require #{ :title :orgs/site :orgs/location :orgs/linkedin :orgs/tags }
+:zd/summary #{ :orgs/tags }
+
+:title "orgs"
+:desc /
+
+:needs ?/
+
+c :zd/type #prod.Case
+c :need n
+
+> n
+> (count c)
+
+:ctos ?/
+
+e :org/role #org.CTO
+e :org/memberOf o
+e :org/person p
+> p
+> e:org/role
+> o
+
+&site
+:zd/type zd.prop
+:zd/annotation zd.link-badge
+
+&location
+:zd/type zd.prop
+:zd/annotation zd.badge
+
+&linkedin
+:zd/type zd.prop
+:zd/annotation zd.link-badge
+
+&tags
+:zd/type zd.prop
+:zd/annotation zd.badge
+:zd/data-type zd.symbol
+:zd/ref-type tag
\ No newline at end of file
diff --git a/zd-docs/customers-x/orgs/health-samurai.zd b/zd-docs/customers-x/orgs/health-samurai.zd
new file mode 100644
index 0000000..5edf67a
--- /dev/null
+++ b/zd-docs/customers-x/orgs/health-samurai.zd
@@ -0,0 +1,26 @@
+:title "Health Samurai"
+
+^badge
+:zd/type orgs
+:orgs/site "https://health-samurai.io"
+:orgs/location #{ country.US }
+:orgs/linkedin "https://www.linkedin.com/company/6653460/admin/feed/posts/"
+
+:desc /
+
+:partners ?/
+
+e :prod/partner-of #orgs.health-samurai
+> e:prod/partner-org
+> e:desc
+> e:prod/champion
+
+
+:members ?/
+
+e :zd/type #org.Member
+e :org/organization #orgs.health-samurai
+e :org/person p
+
+> p
+> e:org/role
\ No newline at end of file
diff --git a/zd-docs/customers-x/orgs/neworg.zd b/zd-docs/customers-x/orgs/neworg.zd
new file mode 100644
index 0000000..2bdc444
--- /dev/null
+++ b/zd-docs/customers-x/orgs/neworg.zd
@@ -0,0 +1 @@
+:title "NewOrg"
\ No newline at end of file
diff --git a/zd-docs/customers-x/orgs/o1.zd b/zd-docs/customers-x/orgs/o1.zd
new file mode 100644
index 0000000..6696958
--- /dev/null
+++ b/zd-docs/customers-x/orgs/o1.zd
@@ -0,0 +1,46 @@
+:zd/type orgs
+
+:zd/type #{ orgs prod.EHR-Vendor }
+:orgs/site "?"
+:orgs/location country.US
+
+:title "Acme"
+:desc /
+
+
+
+&partner
+^badge
+:zd/type #{ prod.Partner }
+^badge
+:prod/partner-org orgs.o1
+^badge
+:prod/partner-of orgs.health-samurai
+^badge
+:prod/partner-since #inst"2011"
+^badge
+:prod/champion #{ people.john }
+
+
+
+:desc /
+We partnered with Acme
+
+
+&case-big-data
+:title "Big Data Case"
+^badge
+:zd/type prod.Case
+^badge
+:need needs.big-data
+:desc /
+Here is desc
+
+&case-hl7
+:title "HL7 Case"
+^badge
+:zd/type prod.Case
+^badge
+:need needs.hl7
+:desc /
+Here is desc
\ No newline at end of file
diff --git a/zd-docs/customers-x/orgs/o2.zd b/zd-docs/customers-x/orgs/o2.zd
new file mode 100644
index 0000000..784f61e
--- /dev/null
+++ b/zd-docs/customers-x/orgs/o2.zd
@@ -0,0 +1,33 @@
+:zd/type orgs
+:orgs/site "site"
+:orgs/location loc.Country
+:orgs/linkedin "?"
+:title "O2"
+:desc /
+
+
+:broken-link link-to-nowhere
+
+&case-big-data
+^badge
+:zd/type prod.Case
+^badge
+:need needs.big-data
+:desc /
+We have 30M of patients
+
+&case-ccd
+^badge
+:zd/type prod.Case
+^badge
+:need needs.hl7
+:case /
+Here is desc
+
+&todo
+:zd/type tasks
+^badge
+:tasks/assignee person.niquola
+^badge
+:tasks/severity tasks.critical
+:title "Update profile"
\ No newline at end of file
diff --git a/customers-x/people.zd b/zd-docs/customers-x/people.zd
similarity index 100%
rename from customers-x/people.zd
rename to zd-docs/customers-x/people.zd
diff --git a/customers-x/people/_schema.zd b/zd-docs/customers-x/people/_schema.zd
similarity index 100%
rename from customers-x/people/_schema.zd
rename to zd-docs/customers-x/people/_schema.zd
diff --git a/customers-x/people/_template.zd b/zd-docs/customers-x/people/_template.zd
similarity index 100%
rename from customers-x/people/_template.zd
rename to zd-docs/customers-x/people/_template.zd
diff --git a/customers-x/people/john.zd b/zd-docs/customers-x/people/john.zd
similarity index 100%
rename from customers-x/people/john.zd
rename to zd-docs/customers-x/people/john.zd
diff --git a/customers-x/people/todd.zd b/zd-docs/customers-x/people/todd.zd
similarity index 100%
rename from customers-x/people/todd.zd
rename to zd-docs/customers-x/people/todd.zd
diff --git a/zd-docs/customers-x/person.zd b/zd-docs/customers-x/person.zd
new file mode 100644
index 0000000..89adcb6
--- /dev/null
+++ b/zd-docs/customers-x/person.zd
@@ -0,0 +1,39 @@
+
+^badge
+:zd/type zd.class
+^badge
+:zd/require #{ :person/email }
+
+:title "Person"
+:icon [:fa-duotone :fa-person]
+:desc /
+
+:menu-order 102
+
+:persons ?/
+
+e :zd/type #person
+m :org/organization o
+m :org/person e
+m :zd/type #org.Member
+
+> m:org/organization
+> e
+> m:org/role
+> m:org/memberSince
+
+:check ?/
+
+e :zd/type #person
+> e
+
+&email
+^badge
+:zd/type zd.prop
+^badge
+:zd/data-type zd.string
+
+^badge
+:zd/annotation zd.badge
+:desc /
+Email of person
\ No newline at end of file
diff --git a/zd-docs/customers-x/person/ivan.zd b/zd-docs/customers-x/person/ivan.zd
new file mode 100644
index 0000000..a31af0e
--- /dev/null
+++ b/zd-docs/customers-x/person/ivan.zd
@@ -0,0 +1,20 @@
+:title "Ivan"
+
+&hs-memb
+^badge
+:zd/type org.Member
+^badge
+:org/memberSince #inst"2005-01-01"
+^badge
+:org/organization orgs.health-samurai
+^badge
+:org/role org.Engineer
+^badge
+:org/person person.ivan
+
+&todo
+^badge
+:zd/type tasks
+^badge
+:tasks/assignee person.niquola
+:title "Help here"
\ No newline at end of file
diff --git a/zd-docs/customers-x/person/niquola.zd b/zd-docs/customers-x/person/niquola.zd
new file mode 100644
index 0000000..86d19da
--- /dev/null
+++ b/zd-docs/customers-x/person/niquola.zd
@@ -0,0 +1,49 @@
+
+
+^badge
+:org.memberOf orgs.health-samurai
+^badge
+:org.role org.CTO
+^badge
+:foaf.based_near #{ country.Portugal city.Lisboa city.Estoril}
+^badge
+:person/email "niquola@gmail.com"
+^badge
+:person/telegram "niquola"
+:title "Nikolai Ryzhikov"
+
+:desc /
+
+&hs-memb
+^badge
+:zd/type org.Member
+^badge
+:org/memberSince #inst"2005-01-01"
+^badge
+:org/memberOf orgs.health-samurai
+^badge
+:org/role org.CTO
+^badge
+:org/person person.niquola
+
+&int-postgres
+^badge
+:zd/type foaf.Iterest
+^badge
+:subject #{ tech.PostgreSQL }
+:desc /
+Interested in Clojure
+
+&fill-profile
+:title "Finish Profile"
+^badge
+:zd/type tasks
+^badge
+:tasks/assignee person.tim
+^badge
+:tasks/severity tasks.critical
+
+
+:desc /
+
+Complete Profile for Nikolai
\ No newline at end of file
diff --git a/zd-docs/customers-x/person/tim.zd b/zd-docs/customers-x/person/tim.zd
new file mode 100644
index 0000000..d1ac4f1
--- /dev/null
+++ b/zd-docs/customers-x/person/tim.zd
@@ -0,0 +1,15 @@
+:title "Tim"
+:desc /
+
+
+&hs-memb
+^badge
+:zd/type org.Member
+^badge
+:org/memberSince #inst"2010-01-01"
+^badge
+:org/organization orgs.health-samurai
+^badge
+:org/role org.TechLead
+^badge
+:org/person person.tim
\ No newline at end of file
diff --git a/zd-docs/customers-x/prod.zd b/zd-docs/customers-x/prod.zd
new file mode 100644
index 0000000..3381a98
--- /dev/null
+++ b/zd-docs/customers-x/prod.zd
@@ -0,0 +1,3 @@
+:title "Prod Ontology"
+:desc /
+
diff --git a/zd-docs/customers-x/prod/Case.zd b/zd-docs/customers-x/prod/Case.zd
new file mode 100644
index 0000000..f590494
--- /dev/null
+++ b/zd-docs/customers-x/prod/Case.zd
@@ -0,0 +1,5 @@
+^badge
+:zd/type zd.class
+:zd/icon [:fa-duotone :fa-suitcase]
+:title "Case"
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/prod/EHR-Vendor.zd b/zd-docs/customers-x/prod/EHR-Vendor.zd
new file mode 100644
index 0000000..8b2e83a
--- /dev/null
+++ b/zd-docs/customers-x/prod/EHR-Vendor.zd
@@ -0,0 +1 @@
+:title "EHR Vendor"
\ No newline at end of file
diff --git a/zd-docs/customers-x/prod/Partner.zd b/zd-docs/customers-x/prod/Partner.zd
new file mode 100644
index 0000000..baab160
--- /dev/null
+++ b/zd-docs/customers-x/prod/Partner.zd
@@ -0,0 +1,3 @@
+:title "Partner"
+:icon [:fa-duotone :fa-handshake]
+:desc /
\ No newline at end of file
diff --git a/customers-x/queries.zd b/zd-docs/customers-x/queries.zd
similarity index 87%
rename from customers-x/queries.zd
rename to zd-docs/customers-x/queries.zd
index bf0ff28..4244374 100644
--- a/customers-x/queries.zd
+++ b/zd-docs/customers-x/queries.zd
@@ -1,4 +1,5 @@
:title "Queries"
+:icon [:fa-brands :fa-searchengin]
:desc /
diff --git a/zd-docs/customers-x/rdf.zd b/zd-docs/customers-x/rdf.zd
new file mode 100644
index 0000000..0ddc072
--- /dev/null
+++ b/zd-docs/customers-x/rdf.zd
@@ -0,0 +1,9 @@
+:title "rdf"
+:desc /
+RDF schema
+
+&Property
+:rdf.type rdf.Property
+
+&type
+:rdf.type rdf.Property
\ No newline at end of file
diff --git a/zd-docs/customers-x/sample.yaml b/zd-docs/customers-x/sample.yaml
new file mode 100644
index 0000000..7bc575d
--- /dev/null
+++ b/zd-docs/customers-x/sample.yaml
@@ -0,0 +1,2 @@
+key: myvalue
+another-key: another-value
diff --git a/zd-docs/customers-x/tasks.zd b/zd-docs/customers-x/tasks.zd
new file mode 100644
index 0000000..837ca8f
--- /dev/null
+++ b/zd-docs/customers-x/tasks.zd
@@ -0,0 +1,21 @@
+:zd/menu-order 100
+:zd/icon [:fa-regular :fa-clipboard-check]
+^badge
+:zd/type zd.class
+:title "Tasks"
+:desc /
+
+
+:table ?/
+
+e :zd/type #tasks
+
+> e:tasks/severity
+> e
+> e:tasks/assignee
+
+&severity
+:zd/type zd.prop
+
+&assignee
+:zd/type zd.prop
\ No newline at end of file
diff --git a/zd-docs/customers-x/tasks/TODO.zd b/zd-docs/customers-x/tasks/TODO.zd
new file mode 100644
index 0000000..85fd0db
--- /dev/null
+++ b/zd-docs/customers-x/tasks/TODO.zd
@@ -0,0 +1,4 @@
+:title "TODO"
+:icon [:fa-regular :fa-clipboard-list-check]
+:subclassOf tasks.Task
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/tasks/Task.zd b/zd-docs/customers-x/tasks/Task.zd
new file mode 100644
index 0000000..03e1c6b
--- /dev/null
+++ b/zd-docs/customers-x/tasks/Task.zd
@@ -0,0 +1,2 @@
+:title "Task"
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/tasks/critical.zd b/zd-docs/customers-x/tasks/critical.zd
new file mode 100644
index 0000000..c4e312b
--- /dev/null
+++ b/zd-docs/customers-x/tasks/critical.zd
@@ -0,0 +1,3 @@
+:icon [:fa-regular :fa-triangle-exclamation]
+:title "Critical"
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/tech/DataBase.zd b/zd-docs/customers-x/tech/DataBase.zd
new file mode 100644
index 0000000..7ae1c8b
--- /dev/null
+++ b/zd-docs/customers-x/tech/DataBase.zd
@@ -0,0 +1,2 @@
+:title "Data Base"
+:desc /
\ No newline at end of file
diff --git a/zd-docs/customers-x/tech/PostgreSQL.zd b/zd-docs/customers-x/tech/PostgreSQL.zd
new file mode 100644
index 0000000..f3f3b54
--- /dev/null
+++ b/zd-docs/customers-x/tech/PostgreSQL.zd
@@ -0,0 +1,4 @@
+^badge
+:type tech.DataBase
+:title "PostgreSQL"
+:desc /
\ No newline at end of file
diff --git a/customers-x/timeline.zd b/zd-docs/customers-x/timeline.zd
similarity index 100%
rename from customers-x/timeline.zd
rename to zd-docs/customers-x/timeline.zd
diff --git a/customers-x/tutorial.zd b/zd-docs/customers-x/tutorial.zd
similarity index 100%
rename from customers-x/tutorial.zd
rename to zd-docs/customers-x/tutorial.zd
diff --git a/customers-x/tutorial/2validation.zd b/zd-docs/customers-x/tutorial/2validation.zd
similarity index 100%
rename from customers-x/tutorial/2validation.zd
rename to zd-docs/customers-x/tutorial/2validation.zd
diff --git a/customers-x/tutorial/3ui.zd b/zd-docs/customers-x/tutorial/3ui.zd
similarity index 100%
rename from customers-x/tutorial/3ui.zd
rename to zd-docs/customers-x/tutorial/3ui.zd
diff --git a/customers-x/tutorial/4model.zd b/zd-docs/customers-x/tutorial/4model.zd
similarity index 100%
rename from customers-x/tutorial/4model.zd
rename to zd-docs/customers-x/tutorial/4model.zd
diff --git a/customers-x/tutorial/5datalog.zd b/zd-docs/customers-x/tutorial/5datalog.zd
similarity index 100%
rename from customers-x/tutorial/5datalog.zd
rename to zd-docs/customers-x/tutorial/5datalog.zd
diff --git a/customers-x/tutorial/6ext.zd b/zd-docs/customers-x/tutorial/6ext.zd
similarity index 100%
rename from customers-x/tutorial/6ext.zd
rename to zd-docs/customers-x/tutorial/6ext.zd
diff --git a/customers-x/tutorial/7collaboration.zd b/zd-docs/customers-x/tutorial/7collaboration.zd
similarity index 100%
rename from customers-x/tutorial/7collaboration.zd
rename to zd-docs/customers-x/tutorial/7collaboration.zd
diff --git a/customers-x/tutorial/8next.zd b/zd-docs/customers-x/tutorial/8next.zd
similarity index 100%
rename from customers-x/tutorial/8next.zd
rename to zd-docs/customers-x/tutorial/8next.zd
diff --git a/customers-x/tutorial/a-doc.zd b/zd-docs/customers-x/tutorial/a-doc.zd
similarity index 100%
rename from customers-x/tutorial/a-doc.zd
rename to zd-docs/customers-x/tutorial/a-doc.zd
diff --git a/zd-docs/customers-x/tutorial/doc.zd b/zd-docs/customers-x/tutorial/doc.zd
new file mode 100644
index 0000000..e69de29
diff --git a/customers-x/tutorial/doc/syntax.zd b/zd-docs/customers-x/tutorial/doc/syntax.zd
similarity index 100%
rename from customers-x/tutorial/doc/syntax.zd
rename to zd-docs/customers-x/tutorial/doc/syntax.zd
diff --git a/customers-x/tutorial/doc/visuals.zd b/zd-docs/customers-x/tutorial/doc/visuals.zd
similarity index 100%
rename from customers-x/tutorial/doc/visuals.zd
rename to zd-docs/customers-x/tutorial/doc/visuals.zd
diff --git a/customers-x/tutorial/doc/zentext.zd b/zd-docs/customers-x/tutorial/doc/zentext.zd
similarity index 100%
rename from customers-x/tutorial/doc/zentext.zd
rename to zd-docs/customers-x/tutorial/doc/zentext.zd
diff --git a/zd-docs/docs/basics.zd b/zd-docs/docs/basics.zd
new file mode 100644
index 0000000..478b1cc
--- /dev/null
+++ b/zd-docs/docs/basics.zd
@@ -0,0 +1,146 @@
+:menu-order 2
+^badge
+:authors #{team.niquola}
+
+:title "Basics"
+
+:summary md/
+
+Zendoc is data annotated to look like structured dynamic text, which may use other data
+
+^table
+:key [{:person team.niquola}]
+
+:document md/
+
+Each document got its name from path in file system
+starting from project root.
+To get name from path - strip [[code .zd]] and replace [[code /]] with [[code .]].
+
+For example path [[code mywiki/people/niquola.zd]] will be translated into name [[code mywiki.people.niquola]]
+This name as a symbol may be used to reference doc/resource.
+Each document is loaded into database as data (resource), and can be accessed from other documents.
+
+:diagram mm/
+
+flowchart TB
+zendoc-->document
+zendoc-->resource
+resource-->db
+document-->db
+db[(Database)]
+
+:syntax md/
+
+zendoc syntax is a superset of [[a https://github.com/edn-format/edn EDN]]
+
+
+```code edn
+ ;; PRIMITIVES
+ "string" ;; string
+ 100.0 ;; number
+ true ;; boolean
+ :my-ns/key ;; keyword
+ my-ns/symb ;; symbol
+
+ ;; COLLECTIONS
+ {:key "value" :age 10} ;; map
+ [1 2 3 4] ;; vector
+ #{:a :b :c} ;; set
+ (plus 1 2) ;; list
+
+```
+
+Each document in zendoc consists of sequence of keys (keypaths), values and annotations.
+
+Let's take a look at sample document:
+
+```code edn
+ :title "My Page"
+ ^badge
+ :status :draft
+ ^table
+ :authors [{:person people.niquola}]
+ :example (load "file.json")
+ :content md/
+ Here is some text content...
+ :nested#:key "value"
+
+```
+
+Document loaded in two forms as [[b a Resource]] and as [[b a Page]].
+
+Resource is data representation of document,
+where each keypath and value is inserted into a map (dictionary, object).
+
+```code edn
+{:zd/name my.document
+ :title "MyPage"
+ :status :draft
+ :authors [{:person people.niquola}]
+ :example {}
+ :content "Here is some text content..."
+ :nested {:key "value"}}
+
+```
+
+Resource is stored into in-memory database and can be accessed by name or with filter.
+
+Another representation of document is a page. Page is used for rendering.
+Page consists of sequence of blocks, each block has [[code :key, :value, :annotations]] keys
+
+
+```code edn
+[{:key [:title] :value "My Page"}
+ {:key [:status] :value :draft :annotations {:block :badge}}
+ {:key [:authors] :value [{:person people.niquola}] :annotations {:block :table}}
+ {:key [:example] :value {}}
+ {:key [:content] :value "Here is some text content..."}
+ {:key [:nested :key] :value "value"}]
+```
+
+Grammar for zendoc
+
+```code bnf
+ document = block+
+ block = (annotation)* keypath value
+ annotation = '^' annotation-name edn-data
+ keypath = key ( '#' key)
+ key = ':[a-z]+'
+ value = edn-data | multi-string
+ multi-string = content-type '/$' string ;; till next annotation or keypath
+
+```
+
+:macros md/
+
+```code edn
+ :title "Example"
+ ^yaml
+ :example (load "data.json")
+```
+
+:dynamic-content md/
+
+
+```code edn
+ ^hiccup
+ :team
+ [:div
+ (for [mem (search {:namespace "team.*"})]
+ [:div {}
+ [:img {:src (:avatar mem)}]
+ [:b (:title mem)]])]
+```
+
+:extensibility md/
+
+:extensibility:annotation
+:extensibility:render-key
+:extensibility:render-block
+:extensibility:render-content
+:extensibility:key-data
+
+:zd/resource
+
+:zd/page
diff --git a/zd-docs/docs/boxdoc.edn b/zd-docs/docs/boxdoc.edn
new file mode 100644
index 0000000..c4267e5
--- /dev/null
+++ b/zd-docs/docs/boxdoc.edn
@@ -0,0 +1,85 @@
+{ns boxdoc
+
+ markdown
+ {:zen/tags #{zen/schema}
+ :type zen/map
+ :keys {:format {:type zen/string}
+ :content {:type zen/string}}}
+ doc
+ {:zen/tags #{zen/schema}
+ :type zen/map
+ :keys {:zd/title {:type zen/string}
+ :zd/tags {:type zen/set :every {:type zen/symbol}}
+ :zd/add-tags {:type zen/set :every {:type zen/symbol}}
+ :zd/name {:type zen/symbol}
+ :zd/desc {:confirms #{markdown}}}}
+
+
+ release
+ {:zen/tags #{zen/tag zen/schema}}
+
+ comp
+ {:zen/desc "Aidbox Component"
+ :zen/tags #{zen/schema}
+ :type zen/map
+ :confirms #{doc}
+ :require #{:summary :status :started-at :authors :release}
+ :keys {:authors {:type zen/set :every {:type zen/symbol}}
+ :related-to {:type zen/set :every {:type zen/symbol}}
+ :release {:type zen/string}
+ :status {:type zen/keyword :enum [{:value :draft} {:value :released}]}
+ :phase {:type zen/keyword :enum [{:value :dev} {:value :stable} {:value :obsolete}]}
+ :started-at {:type zen/datetime}
+ :summary {:confirms #{markdown}}
+ :first-release {:type zen/symbol :tags #{release}}}}
+
+
+ cust
+ {:zen/desc "Aidbox Customer"
+ :zen/tags #{zen/schema}
+ :type zen/map
+ :confirms #{doc}
+ :require #{:summary :status :started-at :support :support-chat}
+ :keys {:status {:type zen/keyword :enum [{:value :pre-sale}
+ {:value :active}
+ {:value :hold}
+ {:value :lost}]}
+ :started-at {:type zen/datetime}
+ :summary {:confirms #{markdown}}
+ :support-chat {:type zen/string}
+ :contacts {:type zen/set :every {:type zen/symbol}}
+ :support {:type zen/keyword :enum [{:value :basic}
+ {:value :professional}
+ {:value :enterprise}
+ {:value :project}]}
+ :country {:type zen/string}
+ :timezone {:type zen/string}}}
+
+ incident
+ {:zen/desc "Incident report"
+ :zen/tags #{zen/schema}
+ :type zen/map
+ :confirms #{doc}
+ :require #{}
+ :keys {:status {:type zen/keyword :enum [{:value :active} {:value :hold} {:value :lost}]}
+ :started-at {:type zen/datetime}
+ :issue {:type zen/string}
+ :resolution {:type zen/keyword}}}
+
+
+ person
+ {:zen/desc "Person"
+ :zen/tags #{zen/schema zen/tag}
+ :type zen/map
+ :confirms #{doc}
+ :require #{:name :telgram}
+ :keys {:name {:type zen/string}
+ :telegram {:type zen/string :zd/annotations {:block :badge}}
+ :github {:type zen/string :zd/annotations {:block :badge}}
+ :email {:type zen/string}
+ :phone {:type zen/string}
+ :twitter {:type zen/string}
+ :person/avatar {:type zen/string}}}
+
+
+ }
diff --git a/docs/example.zd b/zd-docs/docs/example.zd
similarity index 100%
rename from docs/example.zd
rename to zd-docs/docs/example.zd
diff --git a/zd-docs/docs/features.zd b/zd-docs/docs/features.zd
new file mode 100644
index 0000000..ec35bfd
--- /dev/null
+++ b/zd-docs/docs/features.zd
@@ -0,0 +1 @@
+:title "Features"
diff --git a/docs/features/emacs.zd b/zd-docs/docs/features/emacs.zd
similarity index 100%
rename from docs/features/emacs.zd
rename to zd-docs/docs/features/emacs.zd
diff --git a/docs/features/hiccup.zd b/zd-docs/docs/features/hiccup.zd
similarity index 100%
rename from docs/features/hiccup.zd
rename to zd-docs/docs/features/hiccup.zd
diff --git a/docs/features/logo.zd b/zd-docs/docs/features/logo.zd
similarity index 100%
rename from docs/features/logo.zd
rename to zd-docs/docs/features/logo.zd
diff --git a/docs/features/macros.zd b/zd-docs/docs/features/macros.zd
similarity index 100%
rename from docs/features/macros.zd
rename to zd-docs/docs/features/macros.zd
diff --git a/docs/features/navigation.zd b/zd-docs/docs/features/navigation.zd
similarity index 100%
rename from docs/features/navigation.zd
rename to zd-docs/docs/features/navigation.zd
diff --git a/docs/features/project.zd b/zd-docs/docs/features/project.zd
similarity index 100%
rename from docs/features/project.zd
rename to zd-docs/docs/features/project.zd
diff --git a/docs/features/sample.yaml b/zd-docs/docs/features/sample.yaml
similarity index 100%
rename from docs/features/sample.yaml
rename to zd-docs/docs/features/sample.yaml
diff --git a/docs/features/schemas.zd b/zd-docs/docs/features/schemas.zd
similarity index 100%
rename from docs/features/schemas.zd
rename to zd-docs/docs/features/schemas.zd
diff --git a/docs/features/search.zd b/zd-docs/docs/features/search.zd
similarity index 100%
rename from docs/features/search.zd
rename to zd-docs/docs/features/search.zd
diff --git a/docs/features/syntax.zd b/zd-docs/docs/features/syntax.zd
similarity index 100%
rename from docs/features/syntax.zd
rename to zd-docs/docs/features/syntax.zd
diff --git a/docs/features/v0.jpg b/zd-docs/docs/features/v0.jpg
similarity index 100%
rename from docs/features/v0.jpg
rename to zd-docs/docs/features/v0.jpg
diff --git a/docs/features/v1.jpg b/zd-docs/docs/features/v1.jpg
similarity index 100%
rename from docs/features/v1.jpg
rename to zd-docs/docs/features/v1.jpg
diff --git a/docs/features/v2.jpg b/zd-docs/docs/features/v2.jpg
similarity index 100%
rename from docs/features/v2.jpg
rename to zd-docs/docs/features/v2.jpg
diff --git a/docs/features/v3.jpg b/zd-docs/docs/features/v3.jpg
similarity index 100%
rename from docs/features/v3.jpg
rename to zd-docs/docs/features/v3.jpg
diff --git a/docs/features/v4.jpg b/zd-docs/docs/features/v4.jpg
similarity index 100%
rename from docs/features/v4.jpg
rename to zd-docs/docs/features/v4.jpg
diff --git a/docs/features/zen.zd b/zd-docs/docs/features/zen.zd
similarity index 100%
rename from docs/features/zen.zd
rename to zd-docs/docs/features/zen.zd
diff --git a/zd-docs/docs/features/zentext.zd b/zd-docs/docs/features/zentext.zd
new file mode 100644
index 0000000..cf4f7e7
--- /dev/null
+++ b/zd-docs/docs/features/zentext.zd
@@ -0,0 +1,2 @@
+:title "zentext"
+:authror team.vganshin
diff --git a/docs/install.zd b/zd-docs/docs/install.zd
similarity index 100%
rename from docs/install.zd
rename to zd-docs/docs/install.zd
diff --git a/docs/logo.png b/zd-docs/docs/logo.png
similarity index 100%
rename from docs/logo.png
rename to zd-docs/docs/logo.png
diff --git a/docs/logo.zd b/zd-docs/docs/logo.zd
similarity index 100%
rename from docs/logo.zd
rename to zd-docs/docs/logo.zd
diff --git a/docs/methods.zd b/zd-docs/docs/methods.zd
similarity index 100%
rename from docs/methods.zd
rename to zd-docs/docs/methods.zd
diff --git a/docs/readme.zd b/zd-docs/docs/readme.zd
similarity index 100%
rename from docs/readme.zd
rename to zd-docs/docs/readme.zd
diff --git a/docs/syntax.zd b/zd-docs/docs/syntax.zd
similarity index 100%
rename from docs/syntax.zd
rename to zd-docs/docs/syntax.zd
diff --git a/docs/team.zd b/zd-docs/docs/team.zd
similarity index 100%
rename from docs/team.zd
rename to zd-docs/docs/team.zd
diff --git a/docs/team/ApricotLace.zd b/zd-docs/docs/team/ApricotLace.zd
similarity index 100%
rename from docs/team/ApricotLace.zd
rename to zd-docs/docs/team/ApricotLace.zd
diff --git a/docs/team/mput.zd b/zd-docs/docs/team/mput.zd
similarity index 100%
rename from docs/team/mput.zd
rename to zd-docs/docs/team/mput.zd
diff --git a/docs/team/niquola.zd b/zd-docs/docs/team/niquola.zd
similarity index 100%
rename from docs/team/niquola.zd
rename to zd-docs/docs/team/niquola.zd
diff --git a/docs/team/sample.yaml b/zd-docs/docs/team/sample.yaml
similarity index 100%
rename from docs/team/sample.yaml
rename to zd-docs/docs/team/sample.yaml
diff --git a/docs/team/vganshin.zd b/zd-docs/docs/team/vganshin.zd
similarity index 100%
rename from docs/team/vganshin.zd
rename to zd-docs/docs/team/vganshin.zd
diff --git a/docs/team/zlata.zd b/zd-docs/docs/team/zlata.zd
similarity index 100%
rename from docs/team/zlata.zd
rename to zd-docs/docs/team/zlata.zd
diff --git a/zd-docs/docs/zentext.zd b/zd-docs/docs/zentext.zd
new file mode 100644
index 0000000..1df0449
--- /dev/null
+++ b/zd-docs/docs/zentext.zd
@@ -0,0 +1,6 @@
+:menu-order 3
+:title "zentext"
+
+:summary md/
+
+zentext format
diff --git a/zd-docs/example.zd b/zd-docs/example.zd
new file mode 100644
index 0000000..2f9f991
--- /dev/null
+++ b/zd-docs/example.zd
@@ -0,0 +1,138 @@
+:rdf.type pub.Post
+:title "This is article ...."
+:pub.author person.niquola
+:pub.date "2023-01-29"
+
+.home :address-of
+:country loc.countries.ru
+:city loc.cities.spb
+
+:pub.summary /
+
+ Here is some text
+ bla bla bla
+
+:table-of-content
+
+;; example of inlined resource
+.intro :post.chapter-of
+:post.title "Introduction"
+:post.text /
+ Here is some text
+
+.example-1 :post.chapter-of
+:post.title "Example"
+:post.text /
+
+ Here is some text #.intro
+
+
+
+
+[{:zd.id 'posts.mdm
+ :zd.file "[base]/posts/mdm"
+ :zd.doc [:pub.author :pub.summary 'posts.mdm.intro 'posts.mdm.example-1]
+ :zd.ann {:pub.author {:loc [[10 0] [10 40]]}}
+ :zd.errors [{} {}]
+ :zd.readonly true
+
+ :title "...."
+ ;; deduced from .title sameAs rdf.label
+ :rdfs.label "...."
+
+ :pub.author ""
+ :pub.summary ""}
+
+ {:zd.id 'posts.mdm.intro
+ :zd.doc [:title :text]
+ :chapter-of 'posts.mdm
+ :title "..."
+ :text "....."}]
+
+---
+ent
+
+.work-in
+:reverse :ent.employee
+
+.employee
+:reverse ent.work-in
+
+person.john
+:ent.work-in org.google
+
+person.ivan
+:ent.work-in org.google
+
+org.google
+:ent.employee #{person.john person.ivan}
+
+
+----
+
+:zd.id person.john
+
+;; :employeed #{}
+.& :emploee.person
+:company org.google
+:role roles.cto
+:status :active
+
+----
+
+:zd.id org.my-org
+:title "...."
+
+.linkedin :profile-of
+:id "......"
+:db x.linkedin
+:employees.number 3000
+
+.crunchbase :profile-of
+:id "..."
+:db x.crunchbase
+:funding "..."
+
+----
+
+tags.cql
+:link tech.cql -> redirect
+
+----
+
+:zd.id tech.cql
+:type tech.spec tech.lang
+:author persons.brin-rodes
+:desc /
+ This is ....
+
+----
+
+:zd.id my-report
+
+:zd.-menu-order
+:zd.-icon ""
+:zd.-logo ""
+:zd.-file ""
+
+^table
+:data (load "file.csv")
+
+^widget
+:pipedrive.org "234"
+
+^datalog true
+:query
+{:where
+ [[?e :rdf.type terms.organization]
+ [?e :rdf.type terms.vendor]
+ [?e :vendor-of terms.cds]
+ [?e :customer-of orgs.health-samurai]
+ [?e :country loc.us]
+ [?p :employee ?e]
+ [?p :role roles.cto]
+ [?p :member-of communities.hdh]]}
+
+<- match version
+{?e {:rdf.type terms.org ...}
+ ?p {:employee ?e :attr ...}}
diff --git a/zd-docs/features.zd b/zd-docs/features.zd
new file mode 100644
index 0000000..ec35bfd
--- /dev/null
+++ b/zd-docs/features.zd
@@ -0,0 +1 @@
+:title "Features"
diff --git a/zd-docs/features/emacs.zd b/zd-docs/features/emacs.zd
new file mode 100644
index 0000000..abc99d2
--- /dev/null
+++ b/zd-docs/features/emacs.zd
@@ -0,0 +1,2 @@
+:title "Emacs Support"
+:authror team.mput
diff --git a/zd-docs/features/hiccup.zd b/zd-docs/features/hiccup.zd
new file mode 100644
index 0000000..98ac8fb
--- /dev/null
+++ b/zd-docs/features/hiccup.zd
@@ -0,0 +1,27 @@
+^badge
+:author team.niquola
+
+:title "Hiccup"
+:summary "Support for dynamic hiccup"
+
+
+
+:intro md/
+
+Parts of document can be generated dynamically with ^hiccup expressions
+
+This expressions may use a lot of useful functions and helpers
+
+```code colojure
+ ^hiccup
+ :team [:div "Hello"]
+```
+
+
+^hiccup
+:team
+[:div (for [mem (search {:namespace ""})]
+ [:div {:style "display: flex; padding: 1rem; border-bottom: 1px solid #ddd;"}
+ [:img {:src (:avatar mem) :style "width:40px; margin-left: 2rem; margin-right:2rem;"}]
+ [:b (:title mem)]])]
+
diff --git a/zd-docs/features/logo.zd b/zd-docs/features/logo.zd
new file mode 100644
index 0000000..b246a13
--- /dev/null
+++ b/zd-docs/features/logo.zd
@@ -0,0 +1,24 @@
+^badge
+:authror team.zlata
+
+:title "Logo"
+:summary md/
+
+Logo was develped by 12 year designer #team.zlata after school classes
+
+Vectorized by #team.niquola
+
+
+^img {:style "height: 180px;"}
+:version:current "/logo.png"
+
+^img {:style "height: 240px;"}
+:versions:v1 "v0.jpg"
+^img {:style "height: 240px;"}
+:versions:v1 "v1.jpg"
+^img {:style "height: 240px;"}
+:versions:v1 "v2.jpg"
+^img {:style "height: 240px;"}
+:versions:v1 "v3.jpg"
+^img {:style "height: 240px;"}
+:versions:v1 "v4.jpg"
diff --git a/zd-docs/features/macros.zd b/zd-docs/features/macros.zd
new file mode 100644
index 0000000..ac0baa4
--- /dev/null
+++ b/zd-docs/features/macros.zd
@@ -0,0 +1,4 @@
+:title "Macros"
+:authror team.vganshin
+
+:macro.sample (load "sample.yaml" :yaml)
diff --git a/zd-docs/features/navigation.zd b/zd-docs/features/navigation.zd
new file mode 100644
index 0000000..8f25ff7
--- /dev/null
+++ b/zd-docs/features/navigation.zd
@@ -0,0 +1,18 @@
+^badge
+:author team.ApricotLace
+
+:title "Navigation"
+
+
+
+:menu /
+
+TBD
+
+:files /
+
+TBD
+
+:links /
+
+TBD
diff --git a/zd-docs/features/project.zd b/zd-docs/features/project.zd
new file mode 100644
index 0000000..85c7fc2
--- /dev/null
+++ b/zd-docs/features/project.zd
@@ -0,0 +1,23 @@
+:title "Project"
+:summary md/
+ How project is organazide
+
+:file-layout md/
+
+zen project is just a directory where you put *.zd files
+
+```code text
+ wiki/
+ readme.zd
+ releases/
+ v1.zd
+ v2.zd
+ team/
+ member-1.zd
+ member-1.zd
+```
+
+At start each file is loaded into database as resource with name #dir.subdir.filename
+
+You may refer one document from another using this name as a symbol
+
diff --git a/zd-docs/features/sample.yaml b/zd-docs/features/sample.yaml
new file mode 100644
index 0000000..f879caf
--- /dev/null
+++ b/zd-docs/features/sample.yaml
@@ -0,0 +1,3 @@
+text: SomeFancyText
+field: FieldContent
+anotherField: AnotherContent
diff --git a/zd-docs/features/schemas.zd b/zd-docs/features/schemas.zd
new file mode 100644
index 0000000..ed9ee94
--- /dev/null
+++ b/zd-docs/features/schemas.zd
@@ -0,0 +1,62 @@
+^badge
+:author team.vganshin
+
+:title "Apply Schema"
+
+:summary md/
+
+As far as zd-file is a valid zen-resource you can tag it with :zen/tag.
+
+
+:tutorial md/
+
+Let's create a schema for test cases
+
+```
+{ns aidbox.test
+
+ testcase
+ {:zen/tags #{zen/schema zen/tag}
+ :type zen/map
+ :require #{:id :steps :expected-result}
+ :keys {:id {:type zen/string}
+ :steps {:type zen/vector
+ :every {:type zen/map
+ :keys {:desc {:type zen/string}
+ :tip {:type zen/string :zd/annotations {:block :tooltip :type :warn}}}}}
+ :expected-result {:type zen/string}}}}
+```
+
+Let's create a test case in zen doc. Name aidbox.testcase.create-new-box
+
+```code clojure
+ ;; :zd/file aidbox.create-new-box
+ ;; :zd/path aidbox/create-new-box.zd
+
+ :title "Create new box on aidbox.app"
+ :zen/tags #{aidbox.test/testcase}
+
+ :id "create-new-box"
+ ;; ^tooltip :warn will taken from zen-schema
+ :steps:0:tip "Check your internet connection"
+ :steps:0:desc "Open https://aidbox.app"
+
+ ^tooltip :info
+ :steps:1:tip "Create new box"
+ :steps:1:desc "Create new box"
+
+```
+
+Finally, you will see an error which says that :expected-result is required field. Profit!
+
+
+:tag-inheritance md/
+
+You can create doc aidbox.zd with :zd/child-tags and all child docs will be marked with corresponding tags. So you don't have to specify same tag every time in all near docs.
+
+```code clojure
+ ;; :zd/file aidbox
+ ;; :zd/path aidbox.zd
+
+ :zd/child-tags #{aidbox.test/tase-case}
+```
diff --git a/zd-docs/features/search.zd b/zd-docs/features/search.zd
new file mode 100644
index 0000000..2144f43
--- /dev/null
+++ b/zd-docs/features/search.zd
@@ -0,0 +1,2 @@
+:title "Search"
+:author team.ApricotLace
diff --git a/zd-docs/features/syntax.zd b/zd-docs/features/syntax.zd
new file mode 100644
index 0000000..99385bd
--- /dev/null
+++ b/zd-docs/features/syntax.zd
@@ -0,0 +1,107 @@
+:title "Syntax"
+
+
+:zendoc:keypaths md/
+
+Zen document is a collection of keypathes and values.
+
+```code clojure
+
+ :title "Title"
+ :date "2021-12-21"
+ :section:intro "Value"
+ :section:more "Value"
+ :any-edn {:key "value"}
+
+```
+
+This will be compiled into:
+
+```code clojure
+
+ :title "Title"
+ :date "2021-12-21"
+ :section {:intro "Value" :more "Value"}
+
+```
+
+:zendoc:multiline md/
+
+If keypath ends with `/` it is a multiline string:
+
+```code clojure
+
+ :summary md/
+ Here is some text
+ which is multiline
+
+```
+
+This will be compiled into:
+
+```code clojure
+
+{:summary "Here is some text\nwhich is multiline"}
+
+```
+
+
+:zendoc:annotations md/
+
+Keypathes may be annotated with `^name params`.
+
+```code clojure
+
+ ^badge
+ :author "ivan"
+
+ ^img {:style "height:20px"}
+ :image "/logo.png"
+
+ ^table {:columns [:id :name]}
+ :table
+ [{:id "1" :name "col 1"}
+ {:id "2" :name "col 1"}
+ {:id "3" :name "col 1"}]
+
+```
+
+^badge
+:example:a:author "Ivan"
+^img {:style "height:20px"}
+:example:a:image "/logo.png"
+
+^table {:columns [:id :name]}
+:example:a:table
+[{:id "1" :name "col 1"}
+ {:id "2" :name "col 1"}
+ {:id "3" :name "col 1"}]
+
+:zentext md/
+
+If multiline is md/ we render string with #features.zentext
+
+```text
+ * paragraphs
+ * lists
+ * inline links #
+ * inline methods [[a github.com This is github]]
+ * inline functions ((echo function))
+ ```code yaml
+ key: value
+ key: value
+ ```
+```
+
+Will produce >
+
+* paragraphs
+* lists
+* inline links #
+* inline methods [[a github.com This is github]]
+* inline functions ((echo function))
+
+```code yaml
+key: value
+key: value
+```
diff --git a/zd-docs/features/v0.jpg b/zd-docs/features/v0.jpg
new file mode 100644
index 0000000..583a5cd
Binary files /dev/null and b/zd-docs/features/v0.jpg differ
diff --git a/zd-docs/features/v1.jpg b/zd-docs/features/v1.jpg
new file mode 100644
index 0000000..d297158
Binary files /dev/null and b/zd-docs/features/v1.jpg differ
diff --git a/zd-docs/features/v2.jpg b/zd-docs/features/v2.jpg
new file mode 100644
index 0000000..14a317a
Binary files /dev/null and b/zd-docs/features/v2.jpg differ
diff --git a/zd-docs/features/v3.jpg b/zd-docs/features/v3.jpg
new file mode 100644
index 0000000..a3f25f9
Binary files /dev/null and b/zd-docs/features/v3.jpg differ
diff --git a/zd-docs/features/v4.jpg b/zd-docs/features/v4.jpg
new file mode 100644
index 0000000..d069d3a
Binary files /dev/null and b/zd-docs/features/v4.jpg differ
diff --git a/zd-docs/features/zen.zd b/zd-docs/features/zen.zd
new file mode 100644
index 0000000..59a10a7
--- /dev/null
+++ b/zd-docs/features/zen.zd
@@ -0,0 +1,5 @@
+:title "Integration with zen"
+:authror team.vganshin
+
+:summary /
+ Integration with zen-lang
diff --git a/zd-docs/features/zentext.zd b/zd-docs/features/zentext.zd
new file mode 100644
index 0000000..cf4f7e7
--- /dev/null
+++ b/zd-docs/features/zentext.zd
@@ -0,0 +1,2 @@
+:title "zentext"
+:authror team.vganshin
diff --git a/zd-docs/index.zd b/zd-docs/index.zd
new file mode 100644
index 0000000..9e55cdc
--- /dev/null
+++ b/zd-docs/index.zd
@@ -0,0 +1,70 @@
+:menu-order 0
+
+^link-badge
+:repository "https://github.com/zen-lang/zd"
+
+^badge
+:version "1"
+
+:title "ZEN DOC"
+
+
+:avatar "logo.png"
+
+:summary md/
+
+Semantic knowledge base library.
+
+
+^title "Мотивация"
+:motivation md/
+
+zendoc is an extensible engine/library to create
+semnatic knowledge bases.
+
+
+
+:problem md/
+
+How to create knowledge base as a data and docs under the git
+
+* What if OrgMode was developed by clojurians?
+* What if (semantic) wiki was developed by clojurians?
+
+Data is Document - Document is Data!
+
+^attribute
+:inspirations #{"OrgMode" "Jekyll" "Semantic WEB" "ROAM" "Notion" "zen-lang"}
+
+^table {:columns [:name :role]}
+:team
+[{:name "Николай Рыжиков" :role "engineer"}
+ {:name "Максим Путинцев" :role "engineer"}
+ {:name "Влад Ганшин" :role "engineer"}
+ {:name "Евгений Муха" :role "engineer"}
+ {:name "Злата Рыжикова" :role "designer"}]
+
+
+^hiccup
+:intro:schema (zen/schema 'symbol)
+
+^title "Features"
+:features
+
+:features:core "Parser for zendoc and extensible markdown"
+:features:ui "Nice UI for zen-doc"
+:features:scripting "Script documents with clojure"
+:features:emacs "Support for zd-mode - syntax and navigation"
+:features:site "Generate static site"
+:features:zen "Write schemas in zen and validate documents"
+:features:plugins "Write plugins which use knowledge base"
+
+:show-case /
+
+ We want to demonstrate power of zen doc on Aidbox (FHIR Backend)
+ use cases
+
+ * Customers
+ * Incidents
+ * Documentation
+ * Team
diff --git a/zd-docs/install.zd b/zd-docs/install.zd
new file mode 100644
index 0000000..d12a9d0
--- /dev/null
+++ b/zd-docs/install.zd
@@ -0,0 +1,13 @@
+:title "Installation"
+
+
+:run-with-clj md/
+
+
+```code bash
+ git clone zd repos/zd
+ mkdir mywiki
+ clojure -Sdeps \
+ '{:deps {zen-lang/zd {:local/root "/repos/zd"}}}' \
+ -M -m zd.dev mywiki
+```
diff --git a/zd-docs/logo.png b/zd-docs/logo.png
new file mode 100644
index 0000000..12e7c27
Binary files /dev/null and b/zd-docs/logo.png differ
diff --git a/zd-docs/logo.zd b/zd-docs/logo.zd
new file mode 100644
index 0000000..5a75c4a
--- /dev/null
+++ b/zd-docs/logo.zd
@@ -0,0 +1,2 @@
+:logo "logo.png"
+:title "ZEN DOC logo"
diff --git a/zd-docs/methods.zd b/zd-docs/methods.zd
new file mode 100644
index 0000000..b349553
--- /dev/null
+++ b/zd-docs/methods.zd
@@ -0,0 +1,2 @@
+:title "Methods"
+:zd/features
diff --git a/zd-docs/syntax.zd b/zd-docs/syntax.zd
new file mode 100644
index 0000000..3e339e7
--- /dev/null
+++ b/zd-docs/syntax.zd
@@ -0,0 +1,88 @@
+:title "Syntax"
+
+:resource /
+
+Every resource has an :zd.id 'a.b.c (type symbol)
+it means resource is located in '[paths]/a/b/c.zd' file or as nested in 'a.b
+
+:types /
+
+Resource has a :rdf.type (rdf:type) #{} (from rdf schema), which should point to instance of rdf.Class
+
+:properties /
+
+Every property recomended to be defined by resource with same name - :rdf.type => 'rdf.type
+Property may have a domain - default domain is rdf.Resource
+
+:contained-resources /
+
+Contained resources may be defined with . notation inside parent resources
+full name of nested resource is .. Nested resource will have :zd.defined-in attribute
+Only one level of nesting is required
+
+```zd
+ file: person.niquola
+ :name "..."
+
+ .gmail :person
+ :type #{ foaf.SocialAccount }
+
+ .linkedin :person
+ :type #{ foaf.SocialAccount }
+ :network social.linkedin
+
+
+```
+
+:anonimous-resources /
+
+Resource may be anonimous if it's name is .&
+
+```zd
+ file: person.niquola
+ .& :job.person
+ :type #{ job } ;; can be deduced from attribute job.person
+ :company org.health-samurai
+ :status job.status.active
+
+ .& :job.person
+ :company institude of human brain
+```
+
+:annotations /
+
+ Any resource property can be annotated for rendering
+
+```zd
+ ^zd.badge {}
+ :name "..."
+```
+
+:resource-format /
+
+
+```edn
+ {:zd.id 'a.b.c
+ :prop-1 "val"
+ :prop-2 "val"
+ :zd.ann {:key {'ann {params...}} }
+ :zd.doc [:prop-1 :prop-2]}
+
+```
+
+
+zd resource
+----
+zd.zd
+
+.id :defined-in .
+:type #{ rdf.Property }
+:title "..."
+
+.file :defined-in .
+:rdf.domain rdf.Resource
+
+.title :defined-in .
+:rdf.domain rdf.Resource
+:rdf.same-as rdf.label
+
diff --git a/zd-docs/team.zd b/zd-docs/team.zd
new file mode 100644
index 0000000..d9ceb2e
--- /dev/null
+++ b/zd-docs/team.zd
@@ -0,0 +1,2 @@
+:title "Team"
+:zd/child-tags #{boxdoc/person}
diff --git a/zd-docs/team/ApricotLace.zd b/zd-docs/team/ApricotLace.zd
new file mode 100644
index 0000000..d2399c5
--- /dev/null
+++ b/zd-docs/team/ApricotLace.zd
@@ -0,0 +1,7 @@
+:title "Eugeny Mukha"
+
+^img
+:avatar "https://avatars.githubusercontent.com/u/43912637?v=4"
+:github "https://github.com/ApricotLace"
+
+:ascii.macro (load "sample.yaml" :yaml)
diff --git a/zd-docs/team/mput.zd b/zd-docs/team/mput.zd
new file mode 100644
index 0000000..b880fa1
--- /dev/null
+++ b/zd-docs/team/mput.zd
@@ -0,0 +1,4 @@
+:title "Max Putincev"
+
+^img
+:avatar "https://avatars.githubusercontent.com/u/1230663?v=4"
diff --git a/zd-docs/team/niquola.zd b/zd-docs/team/niquola.zd
new file mode 100644
index 0000000..b852564
--- /dev/null
+++ b/zd-docs/team/niquola.zd
@@ -0,0 +1,11 @@
+:title "Nikolai Ryzhkov"
+
+:summary /
+ Nikolai is tech lead in @health-samurai and ....
+
+:avatar "https://avatars.githubusercontent.com/u/32066?v=4"
+
+:github "niquola"
+:twitter "niquola"
+:gmail "niquola"
+
diff --git a/zd-docs/team/sample.yaml b/zd-docs/team/sample.yaml
new file mode 100644
index 0000000..f879caf
--- /dev/null
+++ b/zd-docs/team/sample.yaml
@@ -0,0 +1,3 @@
+text: SomeFancyText
+field: FieldContent
+anotherField: AnotherContent
diff --git a/zd-docs/team/vganshin.zd b/zd-docs/team/vganshin.zd
new file mode 100644
index 0000000..0dffc53
--- /dev/null
+++ b/zd-docs/team/vganshin.zd
@@ -0,0 +1,4 @@
+:title "Vlad Ganshin"
+
+^img
+:avatar "https://avatars.githubusercontent.com/u/1931520?v=4"
diff --git a/zd-docs/team/zlata.zd b/zd-docs/team/zlata.zd
new file mode 100644
index 0000000..8e54fca
--- /dev/null
+++ b/zd-docs/team/zlata.zd
@@ -0,0 +1 @@
+:title "Zlata Ryzhikova"
diff --git a/zd-docs/zentext.zd b/zd-docs/zentext.zd
new file mode 100644
index 0000000..1df0449
--- /dev/null
+++ b/zd-docs/zentext.zd
@@ -0,0 +1,6 @@
+:menu-order 3
+:title "zentext"
+
+:summary md/
+
+zentext format
diff --git a/zrc/zd.edn b/zrc/zd.edn
index 90be4dd..82ce3b9 100644
--- a/zrc/zd.edn
+++ b/zrc/zd.edn
@@ -14,7 +14,7 @@
:to {:type zen/string}
:branch {:type zen/string}}}
- zendoc
+ zendoc-config
{:zen/tags #{zen/tag zen/schema}
:type zen/map
:keys {:root {:type zen/string}
@@ -65,6 +65,9 @@
save-doc
{:zen/tags #{zen-web/op zen/op}}
+ doc-content
+ {:zen/tags #{zen-web/op zen/op}}
+
render-editor
{:zen/tags #{zen-web/op zen/op}}
@@ -74,19 +77,66 @@
render-preview
{:zen/tags #{zen-web/op zen/op}}
+ errors-page
+ {:zen/tags #{zen-web/op zen/op}}
+
+ check-errors
+ {:zen/tags #{zen-web/op zen/op}}
+
+ new-doc
+ {:zen/tags #{zen-web/op zen/op}}
+
+ new-preview
+ {:zen/tags #{zen-web/op zen/op}}
+
+ create-doc
+ {:zen/tags #{zen-web/op zen/op}}
+
+ git-changes
+ {:zen/tags #{zen-web/op zen/op}}
+
+ git-commit
+ {:zen/tags #{zen-web/op zen/op}}
+
+ search-page
+ {:zen/tags #{zen-web/op zen/op}}
+
api
{:zen/tags #{zen-web/api}
:engine zen-web/routemap
;; TODO move zen web defaults to zen web core
- :mw [zen-web/defaults]
+ ;; :mw [zen-web/defaults]
+ "_errors" {:GET check-errors}
+ "_search" {:GET search-page}
+ "git" {:GET git-changes
+ :POST git-commit}
+ "new" {:GET new-doc
+ :POST create-doc
+ "preview" {:POST new-preview}}
:GET render-doc
- [:id] {:mw [append-doc layout]
+ [:id] {;;:mw [append-doc layout]
:DELETE delete-doc
:POST save-doc
:GET render-doc
"preview" {:POST render-preview}
- "widgets" {[:widget-id]
- {:GET render-widget}}
- "edit" {:PUT save-doc
- :GET render-editor}}
- "static" {:* {:GET serve-static}}}}
+ "content" {:GET doc-content}
+ "widgets" {[:widget-id] {:GET render-widget}}
+ "edit" {:GET render-editor}}
+ "static" {:* {:GET serve-static}}}
+
+ http
+ {:zen/tags #{zen/start zen-web/http zd.engines/http}
+ :engine zen-web/httpkit
+ :port 4444
+ :zendoc zendoc
+ :api api}
+
+ zendoc
+ {:zen/tags #{zen/start}
+ :zen/state-key :zd/config
+ :dir "docs"}
+
+ system
+ {:zen/tags #{zen/system}
+ :start [zendoc http]}
+ }
diff --git a/zrc/zd/demo.edn b/zrc/zd/demo.edn
index 9047ac6..adf5c0b 100644
--- a/zrc/zd/demo.edn
+++ b/zrc/zd/demo.edn
@@ -41,4 +41,5 @@
system
{:zen/tags #{zen/system}
- :start [datalog fs http #_web-ui]}}
+ :start [datalog fs http #_web-ui]}
+ }