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]} + }