{"id":152,"date":"2006-02-23T19:21:22","date_gmt":"2006-02-23T17:21:22","guid":{"rendered":"http:\/\/blogs.bl0rg.net\/netzstaub\/?p=152"},"modified":"2006-02-23T19:21:22","modified_gmt":"2006-02-23T17:21:22","slug":"building-openlaszlo-applications-using-common-lisp","status":"publish","type":"post","link":"https:\/\/blogs.bl0rg.net\/netzstaub\/2006\/02\/23\/building-openlaszlo-applications-using-common-lisp\/","title":{"rendered":"Building OpenLaszlo applications using Common Lisp"},"content":{"rendered":"<p>I have to gotten myself a small university job where I have to build a<br \/>\nweb application using OpenLaszlo (http:\/\/openlaszlo.org\/). OpenLaszlo<br \/>\nis a programming language \/ development framework to build web<br \/>\napplications based on javascript and XML. Actually it is pretty<br \/>\nsimilar to all the AJAX stuff we see sprouting everywhere, except that<br \/>\nHTML is replaced by some kind of XML layout language. The application<br \/>\nis compiled to a flash swf file by the OpenLaszlo compiler, and can be<br \/>\ndeployed in a static way (copying the swf file to a web server), or<br \/>\nusing the OpenLaszlo middleware, which can convert HTTP calls from the<br \/>\nflash file to Java calls, cache resources, proxy stuff, etc&#8230; The<br \/>\napproach is pretty neat, the GUI is slick, quite a lot of<br \/>\ndocumentation and demo applications are available, there are no<br \/>\nportability problems across browsers (once you&#8217;ve got flash, but that<br \/>\nruns almost everywhere). Really neat, but who wants to write a<br \/>\nmashed-up mess of XML and Javascript?<\/p>\n<p><!--more--><\/p>\n<p>That&#8217;s where Common Lisp comes into play. I have reworked my muddy<br \/>\npile of crap ParenScript, which is some kind of Lisp to Javascript<br \/>\ncompiler, and taken the HTML generator from Peter Seibel&#8217;s wonderful<br \/>\nbook &#8220;Practical Common Lisp&#8221;, and glued them together to produce<br \/>\nhuman-readable OpenLaszlo XML files (called LZX files). I used my old<br \/>\nsubversion checkout of Parenscript, and not the version maintained by<br \/>\nMarco Baringer, although I should do that, I guess. Let&#8217;s take a look<br \/>\nat such an LZX file:<\/p>\n<pre>\n&lt;canvas&gt;\n  &lt;text&gt;foobar&lt;\/text&gt;\n&lt;\/canvas&gt;\n<\/pre>\n<p>This is a very simple Laszlo application, which will only show the<br \/>\ntext &#8220;foobar&#8221;. The Lisp equivalent of this program is:<\/p>\n<pre>\n(laszlo-file \"empty.lzx\"\n  (:canvas\n    (:text \"foobar\")))\n<\/pre>\n<p>LASZLO-FILE is some kind of workaround, it will interpret the body as<br \/>\nan XML construct and write the results to the file &#8220;empty.lzx&#8221; in my<br \/>\nopenlaszlo development directory, where it can be accessed through the<br \/>\ntomcat webserver runinng the openlaszlo java program.<\/p>\n<p><pre>(:canvas (:text \"foobar\"))<\/pre>\n<p> is converted to XML using a hacked-up<br \/>\nversion of Peter Seibel&#8217;s HTML generator. It has a few different<br \/>\nfeatures. For example, node symbols are interpreted using<br \/>\nParenScript&#8217;s SYMBOL-TO-JS function, so for example the following<br \/>\nlaszlo program:<\/p>\n<pre>\n(laszlo-file \"symbols.lzx\"\n  (:canvas\n    ((:dataset :name \"foobar\")\n        (:my-node (:my-data \"Foobar\")))))\n<\/pre>\n<p>is converted to (notice the camelcase):<\/p>\n<pre>\n&lt;canvas&gt;\n  &lt;dataset name='foobar'&gt;\n    &lt;myNode&gt;\n      &lt;myData&gt;Foobar&lt;\/myData&gt;\n    &lt;\/myNode&gt;\n  &lt;\/dataset&gt;\n&lt;\/canvas&gt;\n<\/pre>\n<p>\nThe most important feature though is the integration of parenscript<br \/>\ninto attributes which begin with &#8220;on&#8221; (for events in Laszlo), and into<br \/>\nthe :method and :script nodes. For example:\n<\/p>\n<pre>\n(laszlo-file \"js-example.lzx\"\n  (:canvas\n   (:view\n    (:simplelayout :axis :y :spacing 15)\n    (:text :onclick (parent.toggle-text.set-visible\n\t\t     (not parent.toggle-text.visible))\n\t   \"Click here\")\n    (:text :name :toggle-text :bgcolor \"#CDCDCD\" :visible :false \"Toggle text\"))))\n<\/pre>\n<p>\nHere the onclick attribute of the first text is a ParenScript<br \/>\nexpression, the resulting XML is:\n<\/p>\n<pre>\n1: &lt;canvas&gt;\n2:   &lt;view&gt;\n3:     &lt;simplelayout axis='y' spacing='15'\/&gt;\n4:     &lt;text onclick='parent.toggleText.setVisible(!parent.toggleText.visible)'&gt;Click here&lt;\/text&gt;\n5:     &lt;text name='toggleText' bgcolor='#CDCDCD' visible='false'&gt;Toggle text&lt;\/text&gt;\n6:   &lt;\/view&gt;\n7: \n8: &lt;\/canvas&gt;\n<\/pre>\n<p>\nThe XML generator has a few special operators designed for laszlo<br \/>\napplications: :JS emits javascript code, :CONSTRAINT generate a laszlo<br \/>\nconstraint expression, :JSMETHOD allows us to use a more &#8220;traditional&#8221;<br \/>\nstyle of declaring methods, and :JSEVENT a more traditional way to<br \/>\ndeclare event methods:\n<\/p>\n<pre>\n(laszlo-file \"xml-showoff.lzx\"\n  (:canvas :debug :true\n    (:text :width (:constraint :once parent.width) \"Hello\"\n      (:jsevent ondata ()\n         (-debug.write \"Hello\"))\n      (:jsmethod my-method (arg1 arg2 arg3)\n         (return (+ arg1 arg2 arg3))))))\n<\/pre>\n<p>generates:<\/p>\n<pre>\n1: &lt;canvas debug='true'&gt;\n2:   &lt;text width='$once{parent.width}'&gt;\n3:     Hello\n4:     &lt;method event='ondata' args=''&gt;\n5:       Debug.write(\"Hello\");\n6:     &lt;\/method&gt;\n7: \n8:     &lt;method name='myMethod' args='arg1,arg2,arg3'&gt;\n9:       return arg1 + arg2 + arg3;\n10:     &lt;\/method&gt;\n11: \n12:   &lt;\/text&gt;\n13: &lt;\/canvas&gt;\n<\/pre>\n<p>ParenScript also includes a feature to generate LzDataElement XML<br \/>\nrepresentations (this uses a small library fragment, which may be<br \/>\nexpanded as needed in future times):<\/p>\n<pre>\n(laszlo-file \"xml-lib.lzx\"\n  (:library\n   (:script\n    (defun make-simple-node (name attrs text)\n      (return (new (-lz-data-element name attrs (list (new (-lz-data-text text))))))))))\n\n(laszlo-file \"xml-generation.lzx\"\n  (:canvas :debug :true\n   (:include :href \"xml-lib.lzx\")\n   (:text \"Generate XML\"\n     (:jsevent onclick ()\n       (xml ret (:foobar (:a :arg 1 \"data\")\n\t\t\t (:b :arg 2 \"more data\")\n\t\t\t (:c (:d))))\n       (-debug.write (ret.serialize))))))\n<\/pre>\n<p>which generates:<\/p>\n<pre>\n1: &lt;canvas debug='true'&gt;\n2:   &lt;include href='xml-lib.lzx'\/&gt;\n3:   &lt;text&gt;\n4:     Generate XML\n5:     &lt;method event='onclick' args=''&gt;\n6:       var ret = new LzDataElement(\"foobar\");\n7:       ret.appendChild(makeSimpleNode(\"a\", { arg : 1 }, \"data\"));\n8:       ret.appendChild(makeSimpleNode(\"b\", { arg : 2 }, \"more data\"));\n9:       var ret0 = new LzDataElement(\"c\");\n10:       ret.appendChild(ret0);\n11:       var ret1 = new LzDataElement(\"d\");\n12:       ret0.appendChild(ret1);\n13:       Debug.write(ret.serialize());\n14:     &lt;\/method&gt;\n15: \n16:   &lt;\/text&gt;\n17: &lt;\/canvas&gt;\n<\/pre>\n<p>Furthermore, the BKNR xml-impex functionality was expanded to allow<br \/>\nfor parsing of XML update files. For example, when we have the<br \/>\nfollowing class declaration:<\/p>\n<pre>\n(defparameter *root-path*\n  \"C:\/Documents and Settings\/manuel\/My Documents\/local-svn\/laszlo\/\")\n\n(defparameter *contact-dtd*\n  (cxml:parse-dtd-file (merge-pathnames \"contact.dtd\" *root-path*)))\n\n(defclass contact ()\n  ((first-name :initarg :first-name :accessor first-name\n\t       :attribute \"firstName\")\n   (last-name :initarg :last-name :accessor last-name\n\t      :attribute \"lastName\")\n   (phone :initarg :phone :accessor phone\n\t  :attribute \"phone\")\n   (email :initarg :email :accessor email\n\t  :index-type unique-index\n\t  :index-initargs (:test #'equal)\n\t  :index-reader contact-with-email\n\t  :index-values all-contacts\n\t  :attribute \"email\"))\n  (:unique-id-slot email)\n  (:unique-id-reader #'contact-with-email)\n  (:metaclass xml-class)\n  (:dtd *contact-dtd*)\n  (:element \"contact\"))\n\n(defmethod print-object ((c contact) stream)\n  (print-unreadable-object (c stream :type t)\n    (with-slots (first-name last-name email) c\n      (format stream \"\\\"~A ~A\\\" (~A)\" first-name last-name email))))\n\n\n(net.aserve:publish :path \"\/phonebook\"\n\t\t    :content-type \"text\/xml\"\n\t\t    :function #'(lambda (req ent)\n\t\t\t\t  (xml-to-http (req ent)\n\n(make-instance 'contact\n\t       :first-name \"Manuel\"\n\t       :last-name \"Odendahl\"\n\t       :phone \"2394802934\"\n\t       :email \"manuel@bl0rg.net\")\n\nEXAMPLES&gt; (all-contacts)\n(#&lt;CONTACT \"Manuel Odendahl\" (manuel@bl0rg.net)&gt;)\n\nEXAMPLES&gt; (write-to-xml (all-contacts) :name \"contacts\")\n&lt;contacts&gt;\n   &lt;contact email=\"manuel@bl0rg.net\" firstName=\"Manuel\" lastName=\"Odendahl\" phone=\"2394802934\"\/&gt;\n&lt;\/contacts&gt;\n<\/pre>\n<p>We can parse the following &#8220;update.xml&#8221; file:<\/p>\n<pre>\n&lt;updates&gt;&lt;contact email='manuel@bl0rg.net'lastName='foobar'\/&gt;&lt;\/updates&gt;\n\nEXAMPLES&gt; (parse-xml-update-file \"update.xml\" (list (find-class 'contact)))\nupdating slot LAST-NAME with \"foobar\"\n(:CONTACT (#lt;CONTACT \"Manuel foobar\" (manuel@bl0rg.net))&gt;)\n<\/pre>\n<p>This can be further wrapped into a web service:<\/p>\n<pre>\n(net.aserve:publish :path \"\/phonebook\"\n\t\t    :content-type \"text\/xml\"\n\t\t    :function #'(lambda (req ent)\n\t\t\t\t  (xml-to-http (req ent)\n\t\t\t\t\t       (all-contacts) :name \"phonebook\")))\n\n(defun phonebook-update (req)\n  (with-query-params (req action xml pk)\n    (warn \"action ~A, xml ~A\" action xml)\n    (cond\n      ((string= action \"insert\")\n       (insert-xml-class 'contact xml :parse #'bknr.impex:parse-xml-stream))\n      ((string= action \"update\")\n       (update-xml-class 'contact xml :parse #'bknr.impex:parse-xml-update-stream))\n      ((string= action \"delete\")\n       (destroy-object (contact-with-email pk))))))\n\n(net.aserve:publish :path \"\/phonebook-update\"\n\t\t    :content-type \"text\/xml\"\n\t\t    :function #'(lambda (req ent)\n\t\t\t\t  (handler-case (phonebook-update req)\n\t\t\t\t    (error (e)\n\t\t\t\t      (warn \"error: ~A\" e)\n\t\t\t\t      (xml-http-output (req ent)\n\t\t\t\t\t\t       (:result \"failure\")))\n\t\t\t\t    (:no-error (e)\n\t\t\t\t      (declare (ignore e))\n\t\t\t\t      (xml-http-output (req ent)\n\t\t\t\t\t\t       (:result \"success\"))))))\n\n\n(net.aserve:start :port 8081)\n<\/pre>\n<p>Which allows to rebuild the example data application out of the laszlo<br \/>\ndeveloper manual using Lisp:<\/p>\n<pre>\n(laszlo-file \"data-app8.lzx\"\n  ((:canvas :bgcolor \"#D4D0C8\" :debug \"true\")\n   (:include :href \"xml-lib.lzx\")\n   (:dataset :name \"dset\" :src \"http:\/\/localhost:8081\/phonebook\" :request \"true\" :type \"http\")\n\n   ((:dataset :name \"dsSendData\" :request \"false\" :src \"http:\/\/localhost:8081\/phonebook-update\"\n\t      :type \"http\"))\n\n   ((:datapointer :xpath \"dsSendData:\/\")\n    ((:method :event \"ondata\")\n     (if (= (this.xpath-query \"result\/text()\") \"success\")\n\t (-Debug.write \"Operation succeeded\")\n\t (-Debug.write \"Operation failed\"))))\n\n   ((:class :name \"contactview\" :extends \"view\" :visible \"false\" :x 20 :height 120)\n    (:text :name \"pk\" :visible \"false\" :datapath \"@email\")\n    (:text :y 10 \"First Name:\")\n    (:edittext :name \"firstName\" :datapath \"@firstName\" :x 80 :y 10)\n    (:text :y 35 \"Last Name:\")\n    (:edittext :name \"lastName\" :datapath \"@lastName\" :x 80 :y 35)\n    (:text :y 60 \"Phone:\")\n    (:edittext :name \"phone\" :datapath \"@phone\" :x 80 :y 60)\n    (:text :y 85 \"Email:\")\n    (:edittext :name \"email\" :datapath \"@email\" :x 80 :y 85)\n\n    ((:method :name \"sendData\" :args \"action\")\n     (let ((d canvas.datasets.ds-send-data)\n\t   (p (new -lz-param)))\n       (xml xmlret (:update (:contact :first-name (first-name.get-text)\n\t\t\t\t      :last-name (last-name.get-text)\n\t\t\t\t      :phone (phone.get-text)\n\t\t\t\t      :email (email.get-text))))\n       (p.add-value \"action\" action t)\n       (p.add-value \"xml\" (xmlret.serialize) t)\n       (p.add-value \"pk\" (pk.get-text) t)\n       (d.set-query-string p)\n       (d.do-request))))\n\n   (:simplelayout :axis \"y\")\n\n   (:view\n    (:simplelayout :axis \"y\")\n    (:text :onclick (parent.new-contact.set-visible (not parent.new-contact.visible))\n\t   \"New Entry...\")\n\n    ((:contactview :name \"newContact\" :datapath \"new:\/contact\")\n     ((:button :width 80 :x 200 :y 10) \"Add\"\n      ((:method :event \"onclick\")\n       (parent.send-data \"insert\")\n       (parent.datapath.update-data)\n       (let ((dp (canvas.datasets.dset.get-pointer)))\n\t (dp.select-child)\n\t (dp.add-node-from-pointer parent.datapath)\n\t (parent.set-visible false)\n\t (parent.set-datapath \"new:\/contact\")))))\n    \n    ((:view :datapath \"dset:\/phonebook\/contact\")\n     (:simplelayout :axis \"y\")\n     ((:view :name \"list\" :onclick (parent.update-contact.set-visible\n\t\t\t\t    (not parent.update-contact.visible)))\n      (:simplelayout :axis \"x\")\n      (:text :datapath \"@firstName\")\n      (:text :datapath \"@lastName\")\n      (:text :datapath \"@phone\")\n      (:text :datapath \"@email\"))\n\n     ((:contactview :name \"updateContact\")\n      ((:button :width 80 :x 200 :y 10) \"Update\"\n       ((:method :event \"onclick\")\n\t(parent.send-data \"update\")\n\t(parent.parent.datapath.update-data)))\n\n      ((:button :width 80 :x 200 :y 40) \"Delete\"\n       ((:method :event \"onclick\")\n\t(parent.send-data \"delete\")\n\t(parent.parent.datapath.delete-node))))))\n   \n    (:debug :width 600 :height 120)))\n<\/pre>\n<p>\nNow this is still much to write, and there is no persistence on the<br \/>\nserver side. The persistence is handled by the BKNR datastore, and a<br \/>\nfew wrappers enable us to pack everything into a few lines:\n<\/p>\n<pre>\n(defvar *user-dtd*\n  (cxml:parse-dtd-file \"user.dtd\"))\n\n(define-persistent-class user (xml-store-object)\n  ((first-name :update :element \"firstName\")\n   (last-name :update :element \"lastName\")\n   (vat-number :update :element \"vatNumber\")\n   (email :update :index-type string-unique-index\n\t  :index-reader user-with-email\n\t  :index-values all-users\n\t  :element \"email\"))\n  (:metaclass persistent-xml-class)\n  (:dtd *user-dtd*)\n  (:element \"user\"))\n\n(defmethod print-object ((user user) stream)\n  (print-unreadable-object (user stream :type t)\n    (with-slots (first-name last-name email) user\n      (format stream \"~a ~a (~a)\" first-name last-name email))))\n\n(make-instance 'mp-store :directory \"C:\/Documents and Settings\/manuel\/My Documents\/laszlo-store\/\"\n\t       :subsystems (list (make-instance 'store-object-subsystem)))\n\n(make-object 'user\n\t     :first-name \"Manuel\"\n\t     :last-name \"Odendahl\"\n\t     :vat-number \"23849\"\n\t     :email \"manuel@bl0rg.net\")\n\n(publish-xml-handler 'user)\n\n(laszlo-file \"userdemo.lzx\"\n  ((:canvas :height 400 :debug \"true\")\n\n   (:include :href \"xml-lib.lzx\")\n\n   (:dataset :name \"user\" :src \"http:\/\/127.0.0.1:8081\/user\/0\" :type \"http\" :autorequest \"true\")\n\n   ((:dataset :name \"updateDs\" :src \"http:\/\/127.0.0.1:8081\/user\" :type \"http\"\n\t      :request \"false\" :autorequest \"false\"))\n\n   ;;; wieso wird der scheiss nicht dann aufgerufen, wenn er aufgerufen werden soll? XXX\n   ((:datapointer :xpath \"updateDs:\/\")\n    ((:method :event \"ondata\")\n     (-debug.write (+ \"on-data\" (.serialize (this.get-dataset))))\n     (-debug.write (this.xpath-query \"\/result[1]\/text()\"))\n     (if (= (this.xpath-query \"\/result[1]\/text()\") \"success\")\n\t (-debug.write \"update was successful\")\n\t (-debug.write \"update was not successful\"))))\n   \n   (:datapointer :id \"userdp\" :xpath \"user:\/user[1]\")\n\n   (:simplelayout :axis \"y\")\n\n   (:jsmethod edit-user ()\n     (if this.edit-data.visible\n\t (this.user.do-request)\n\t (this.edit-data.set-data))\n\t \n     (this.show-data.set-visible (not this.show-data.visible))\n     (this.edit-data.set-visible (not this.edit-data.visible)))\n\n   ((:view :name \"showData\" :datapath \"user:\/user[1]\" :visible \"true\")\n    (:text :x 0 :y 0 \"First Name:\")\n    (:text :x 150 :y 0 :name \"firstName\" :datapath \"firstName\/text()\")\n    (:text :x 0 :y 25 \"Last Name:\")\n    (:text :x 150 :y 25 :name \"lastName\" :datapath \"lastName\/text()\")\n    (:text :x 0 :y 50 \"VAT Number:\")\n    (:text :x 150 :y 50 :name \"vatNumber\" :datapath \"vatNumber\/text()\")\n    (:button :y 100 :onclick (parent.parent.edit-user) \"Edit Data\"))\n   \n   ((:view :name \"editData\" :visible \"false\")\n    (:jsmethod set-data ()\n       (this.first-name.set-text (userdp.xpath-query \"\/user[1]\/firstName\/text()\"))\n       (this.last-name.set-text  (userdp.xpath-query \"\/user[1]\/lastName\/text()\"))\n       (this.vat-number.set-text (userdp.xpath-query \"\/user[1]\/vatNumber\/text()\")))\n\n    (:jsmethod update-data ()\n       (xml update (:update ((:user :id (userdp.xpath-query \"\/user[1]\/@id\"))\n\t\t\t     (:first-name (this.first-name.get-text))\n\t\t\t     (:last-name  (this.last-name.get-text))\n\t\t\t     (:vat-number (this.vat-number.get-text)))))\n       (let ((p (new -lz-param)))\n\t (p.add-value \"action\" \"update\" t)\n\t (p.add-value \"update\" (update.serialize) t)\n\t (update-ds.set-query-string p)\n\t (-debug.write \"update-ds.do-request\")\n\t (update-ds.do-request)\n\t (this.parent.edit-user)))\n    \n    (:text :x 0 :y 0 \"First Name:\")\n    (:edittext :x 150 :y 0 :name \"firstName\")\n    (:text :x 0 :y 25 \"Last Name:\")\n    (:edittext :x 150 :y 25 :name \"lastName\")\n    (:text :x 0 :y 50 \"VAT Number:\")\n    (:edittext :x 150 :y 50 :name \"vatNumber\")\n\n    (:button :y 100 :onclick (parent.update-data) \"Update Data\"))\n\n   ((:button :onclick (progn (parent.form.datapath.update-data)\n\t\t\t     (t.set-text (userprofile.serialize)))) \"Show XML data\")\n   (:inputtext :multiline \"true\"\n\t       :width (:constraint nil canvas.width)\n\t       :bgcolor \"0xa0a0a0\"\n\t       :id \"t\"\n\t       :height 300)\n   (:debug)))\n<\/pre>\n<p>\nNow all this code is extremely brittle (it has been about a year since<br \/>\nmy last programming), quickly hacked together and by no means<br \/>\nfinished. I won&#8217;t even suggest you to get it to work, and I only<br \/>\ninclude a dump of my local-svn containing the XML and ParenScript for<br \/>\nsakes of completeness (the IMPEX and persistence stuff is not ready<br \/>\nfor release) <a href=\"http:\/\/bl0rg.net\/~manuel\/laszlo-lisp.zip\">laszlo-lisp.zip<\/a>).<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I have to gotten myself a small university job where I have to build a web application using OpenLaszlo (http:\/\/openlaszlo.org\/). OpenLaszlo is a programming language \/ development framework to build web applications based on javascript and XML. Actually it is &hellip; <a href=\"https:\/\/blogs.bl0rg.net\/netzstaub\/2006\/02\/23\/building-openlaszlo-applications-using-common-lisp\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[19,3],"tags":[],"class_list":["post-152","post","type-post","status-publish","format-standard","hentry","category-english","category-lispstaub"],"_links":{"self":[{"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/posts\/152","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/comments?post=152"}],"version-history":[{"count":0,"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/posts\/152\/revisions"}],"wp:attachment":[{"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/media?parent=152"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/categories?post=152"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.bl0rg.net\/netzstaub\/wp-json\/wp\/v2\/tags?post=152"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}