{"slug": "macro-macros", "title": "Macro Macros", "summary": "A developer detailed the challenges of building domain-specific languages for graph database queries in Clojure, focusing on handling variables in Datomic and SPARQL. The post explores using symbols for variables and the need for quoting to avoid evaluation, with references to libraries like HoneySQL and Flint.", "body_md": "This is a follow up to my recent posts on [Domain Specific Languages (DSLs) for database queries in Clojure](https://dev.to/quoll/clojure-query-dsls-137m), and [different quoting forms in Clojure](https://dev.to/quoll/quoting-difficulties-4mad) to enable this.\n\nI had originally thought that I would write this post in a \"Tutorial\" style. But then it occurred to me that I used to get the best responses from people when I just documented what I learned as I learned it. I think it's a more \"open\" and personable style of writing, which may be what people liked. This approach might have extra appeal in the current era of AI slop, since people ought to see that I've written it myself. Unless I usually sound like an AI 🙃\n\nI'm also hoping that this approach will be easier to write, since I don't need to restructure my exploration as an instructional post.\n\nOn the other hand, it may be a terrible idea. But I won't know unless I try…\n\nSQL is very structured around column selection, so it does not usually need anything like variables inside a query. This allows [HoneySQL](https://github.com/seancorfield/honeysql) to build most of its structures using functions that take keywords and values as arguments.\n\nOn the other hand, graph query languages like [Datomic](https://docs.datomic.com/), [SPARQL](https://www.w3.org/TR/sparql12-query/), and [GQL](https://en.wikipedia.org/wiki/Graph_Query_Language) typically use pattern matching on the graph, where parts of the pattern contain a variable that will be \"bound\" to associated values when a pattern matches. For instance:\n\n```\n?person :hasFriend ?friend\n```\n\n… is a pattern for matching edges in a graph where something is connected by a property labelled `:hasFriend`\n\nto another node. Every edge in the graph that matches this pattern, leads to a pair of nodes that the variables `?person`\n\nand `?friend`\n\nget bound to.\n\nGQL would do something similar with a `MATCH`\n\nclause of:\n\n``` php\n(person:Person)-[:HAS_FRIEND]->(friend:Person)\n```\n\nLike the SPARQL form, this binds the first node (which must be of type `Person`\n\n) to the variable `person`\n\n, and the second node (which is also a `Person`\n\n) to the variable `friend`\n\n.\n\nI'm focused on Datomic and SPARQL, which are more closely related to each other, so I won't address GQL here.\n\nThe issue with variables is that keywords are already being used in queries (such as for the `:hasFriend`\n\nproperty), so we need something else. The obvious candidate is the `Symbol`\n\n, and this is the route that Datomic took.\n\nHowever, Clojure uses symbols to refer to values, meaning that they need to be inserted in queries without being evaluated. That was why I did that post on quoting symbols: quoting is a mechanism to create a symbol as a part of a structure without asking Clojure to instead insert the value the symbol references. i.e. if I say:\n\n``` js\n(let [?person \"bad data\"]\n  [?person :hasName \"Fred\"])\n\n;; returns=> [\"bad data\" :hasName \"Fred\"]\n```\n\nThis isn't what I wanted. It is even worse if I don't have `?person`\n\npredefined (as the `let`\n\nexpression does), since the code can't even run.\n\nInstead, I want the *symbol* in the first position:\n\n``` js\n(let [?person \"bad data\"]\n  ['?person :hasName \"Fred\"])\n\n;; returns=> [?person :hasName \"Fred\"]\n```\n\nThis doesn't care if the symbol is defined or not. We get back the structure that we wanted with a symbol in it.\n\nA colleague has been using [Flint](https://github.com/yetanalytics/flint) to build SPARQL queries programmatically. I like Flint, because it uses Datomic-style queries to express SPARQL. Datomic has been designed to use from Clojure, with a query language based in Clojure data structures. So Flint should make it just as easy to query SPARQL.\n\nHowever, because the queries are being built programmatically, we run the risk of reusing a variable name each time a new expression is added into the query. Reusing a variable for a different purpose will generally break a query, so he was generating new variable names whenever he was generating a new query clause.\n\nThere are a couple of ways to generate new variables for a query. One way is to generate a new name, build a symbol with that name, and then insert it without quoting the name being used to carry that symbol:\n\n```\n(let [?container (gensym \"?container\")]\n  [[:my-object :contains ?container]\n   [?container :value \"hello\"]])\n\n;; result=> [[:my-object :contains ?container180]\n;;           [?container180 :value \"hello\"]]\n```\n\nAnother approach is to use an auto gensym:\n\n```\n`[[:my-object :contains ?container#]\n  [?container# :value \"hello\"]]\n\n;; result=> [[:my-object :contains ?container__2__auto__]\n;;           [?container__2__auto__ :value \"hello\"]]\n```\n\nBoth of these seem fine, but it gets harder to read and write when new constraints are added. For instance, I may be looking for a `:value`\n\nthat is a string containing the substring \"lo\":\n\n```\n(let [?container (gensym \"?container\")\n      ?value (gensym \"?value\")]\n  [[:my-object :contains ?container]\n   [?container :value ?value]\n   [:filter '(contains ~?value \"lo\")]])\n\n;; result=> [[:my-object :contains ?container140]\n;;           [?container140 :value ?value141]\n;;           [:filter (contains (clojure.core/unquote ?value) \"lo\")]]\n```\n\nWhich doesn't work. We need syntax quoting instead:\n\n```\n(let [?container (gensym \"?container\")\n      ?value (gensym \"?value\")]\n  [[:my-object :contains ?container]\n   [?container :value ?value]\n   [:filter `(contains ~?value \"lo\")]])\n\n;; result=> [[:my-object :contains ?container144]\n;;           [?container144 :value ?value145]\n;;           [:filter (user/contains ?value145 \"lo\")]]\n```\n\nAgain… the quoting didn't work:\n\n```\n(let [?container (gensym \"?container\")\n      ?value (gensym \"?value\")]\n  [[:my-object :contains ?container]\n   [?container :value ?value]\n   [:filter `(~'contains ~?value \"lo\")]])\n\n;; result=> [[:my-object :contains ?container148]\n;;           [?container148 :value ?value149]\n;;           [:filter (contains ?value149 \"lo\")]]\n```\n\nTo be fair, I did know how to quote that all along, but I wanted to demonstrate that it can catch people out.\n\nThis is all very contrived. But what about if I want to construct something more complex. Say, I want a general way to select an entity and its contained value, and then I decide that I want to filter that by containing `\"lo\"`\n\n. The first part would be in a function, but I will need to provide the binding `?value`\n\nvariable to filter on it:\n\n```\n(defn select-entity-value [?entity ?value]\n  (let [?container (gensym \"?container\")]\n    [[?entity :contains ?container]\n     [?container :value ?value]]))\n\n(let [?e (gensym \"?e\")\n      ?v (gensym \"?v\")]\n  `[~@(select-entity-value ?e ?v)\n    [:filter (~'contains ~?v \"lo\")]])\n\n;; result=> [[?e178 :contains ?container180]\n;;           [?container180 :value ?v179]\n;;           [:filter (contains ?v179 \"lo\")]]\n```\n\nSplice-unquoting like this (the `~@`\n\nsyntax) isn't really necessary, though quoting everything means that the expression inside the `:filter`\n\ndoesn't need its own quoting (since it is a list, and we have to quote lists, or else they get executed).\n\nWe could even avoid almost quotes with:\n\n```\n(let [?e (gensym \"?e\")\n      ?v (gensym \"?v\")]\n  (conj (select-entity-value ?e ?v)\n        [:filter (list 'contains ?v \"lo\")]))\n```\n\nBut now we have `conj`\n\nand `list`\n\nin the expression, we're still quoting `contains`\n\n, and we have the messy `gensym`\n\ndeclarations at the top. This DSL doesn't look all that easy to use.\n\nUntil now, I've been rehashing what I've talked about already, and some of the things my colleague was building. It all worked, but it just looked… clunky. I found myself thinking that surely we could do better, right?\n\nPatterns are not too complex to work with, since they are just short vectors, containing keywords, symbols, or simple values like strings and numbers. The mess really showed up when we tried to introduce a filter.\n\nThe filter we used here was the `contains`\n\nfunction. There are a lot of other functions in SPARQL, so there isn't anything particularly special about that function. Hopefully, whatever we do to address one function could be applied to all of them.\n\nIt would be nice to just insert the `contains`\n\nexpression directly into the query. Something like:\n\n```\n[:filter (contains ?v \"lo\")]\n```\n\nBindings work similarly, except instead of evaluating an expression and passing through everything that returns a `true`\n\nresult, they evaluate an expression and save it in a variable. For instance, to save a the lower-case form of the string `?v`\n\n, you can bind it:\n\n```\n[:bind (lcase ?v) ?lowv]\n```\n\nThe problems here are that `contains`\n\nand `+`\n\nlooks like a function. Even if we created a function for it, the `?v`\n\nis not bound to anything, so the function would fail. However, Clojure macros can accept any symbol as an argument. Can we use them somehow?\n\nMacros are Clojure code that generates Clojure data. The trick is that the macro is called during compilation, and the resulting data is inserted into the source code before it gets compiled. This lets you generate code using code.\n\nClojure itself uses macros everywhere. Most of the \"built in\" syntax is actually just macros that write out other code. At the end of the day, Clojure only contains a few \"[special forms](https://clojure.org/reference/special_forms)\" that get presented to the compiler. But don't let that list of special forms fool you: many of those (e.g. `defn`\n\n, `fn`\n\n, and `let`\n\n) are actually macros as well, with simpler special forms underlying them.\n\nMany of these macros accept symbols that are not bound, safely generating code that uses those symbols. For instance, consider an expression for an identity function: `(fn [x] x)`\n\nWe can see what the `fn`\n\nmacro expands to by using `macroexpand`\n\n:\n\n``` js\n=> (macroexpand '(fn [x] x))\n(fn* ([x] x))\n```\n\nThis is just a rewrite to use `fn*`\n\ninstead, which is much simpler: `fn*`\n\ndoes not do argument destructuring, it does not handle `pre`\n\nnor `post`\n\nconditions, it does not attach metadata, and it always expects parentheses around a function argument list and body even when there is only a single arity.\n\nBut importantly, notice how we can provide `fn`\n\nwith an expression that includes `x`\n\nand it returns a new expression that also includes `x`\n\n? We don't need `x`\n\nto exist before doing this. That may be what we need.\n\nLet's try it out. Can we create a `contains`\n\nmacro that accepts a symbol argument and returns a list with appropriate symbols in it?\n\nMy first attempt was laughable:\n\n```\n(defmacro contains [a b] `(~'contains ~a ~b))\n```\n\nSo when I call `(contains \"foot\" \"foo\")`\n\nit should return a list containing the symbol `contains`\n\n, the string \"foot\" and the string \"foo\". That was what was returned, but then that goes to the compiler, and the symbol `contains`\n\ndoesn't exist, so it fails. Doh.\n\nI needed an actual symbol object in the position of `contains`\n\n:\n\n```\n(defmacro contains [a b]\n (let [c# (symbol \"contains\")]\n  `(~c# ~a ~b)))\n```\n\nBut I didn't need to evaluate that to know what it would do… calling `(contains \"foot\" \"foo\")`\n\nwould generate the list `(contains \"foot\" \"foo\")`\n\nand that would be evaluated, which then repeats the process until the macro evaluation overflows the stack. I had forgotten that I'm not returning the data structure anymore. Now I'm returning code that evaluates to the required data structure.\n\nAside from quoting a list (which gets tricky, because macro expansion essentially unquotes what you've quoted, so you need to double-quote), you can create a list with the `list`\n\nfunction:\n\n```\n(defmacro contains [a b]\n (let [c# (symbol \"contains\")]\n  `(list ~c# ~a ~b)))\n```\n\nHow does this look?\n\n``` js\n=> (macroexpand '(contains \"foot\" \"foo\"))\n(clojure.core/list contains \"foot\" \"foo\")\n```\n\nOh. That was sort of obvious in hindsight.\n\nLet's forget inserting the symbol and just put it in place:\n\n``` js\n(defmacro contains [a b]\n `(list (symbol \"contains\") ~a ~b))\n=> (macroexpand '(contains \"foot\" \"foo\"))\n(clojure.core/list (clojure.core/symbol \"contains\") \"foot\" \"foo\")\n=> (contains \"foot\" \"foo\")\n(contains \"foot\" \"foo\")\n```\n\nSee why I don't usually like writing this way? Showing how long it takes to get to something that should be easy is embarrassing.\n\nBut I'm really not happy about embedding the symbol by generating one based on a string. Is there another way? What if I quote it?\n\n``` js\n(defmacro contains [a b] `(list 'contains ~a ~b))\n=> (macroexpand '(contains \"foot\" \"foo\"))\n(clojure.core/list (quote user/contains) \"foot\" \"foo\")\n```\n\nThat got me part of the way there, but I forgot that I'm in a syntax quote, so `contains`\n\nis resolved with its full namespace. I can write out `(quote ~'contains)`\n\n, but I'm curious… can I just chain the quote/unquote syntax here?\n\n``` js\n(defmacro contains [a b] `(list '~'contains ~a ~b))\n=> (macroexpand '(contains \"foot\" \"foo\"))\n(clojure.core/list (quote contains) \"foot\" \"foo\")\n=> (contains \"foot\" \"foo\")\n(contains \"foot\" \"foo\")\n```\n\nI might stick to the `(quote …)`\n\nform though, since cascading quote/unquote could get hard to read. It turns out that this mattered later on.\n\nThat did work, but what about the arguments? Right now they are being passed through, and they just appear verbatim in the final form. That doesn't work for variables like `?value`\n\n:\n\n``` js\n=> (contains ?value \"foo\")\nSyntax error compiling at (REPL:1:1).\nUnable to resolve symbol: ?value in this context\n```\n\nWe need to quote the arguments too.\n\n``` js\n(defmacro contains [a b] `(list (quote ~'contains) (quote ~a) (quote ~b)))\n=> (macroexpand '(contains ?v \"foo\"))\n(clojure.core/list (quote contains) (quote ?v) (quote \"foo\"))\n=> (contains ?v \"foo\")\n(contains ?v \"foo\")\n```\n\nThis looked great! Until my colleague pointed out that he couldn't pass non-literal values into the macro. Of course he couldn't 🤦♀️\n\nIn this case, I need to check if the argument is a symbol, and if it is, then look for the form `?name`\n\nor `$name`\n\n(SPARQL allows either `?`\n\nor `$`\n\nto indicate a variable). I'll create a helper function that tests if an argument is a symbol that starts with one of these characters, and only quote the ones that match. However, a function like that can't just return `(quote a)`\n\n, since that just evaluates to `a`\n\nand won't get inserted into the output correctly. Instead, I need to return a list of the form `(quote a)`\n\n:\n\n```\n(defmacro contains [a b]\n (let [vquote (fn [s]\n               (if (and (symbol? s) (#{\\? \\$} (first (name s))))\n                 (list 'quote s)\n                 s))\n       a# (vquote a)\n       b# (vquote b)]\n   `(list (quote ~'contains) ~a# ~b#)))\n\nuser=> (macroexpand '(contains ?v \"foo\"))\n(clojure.core/list (quote contains) (quote ?v) \"foo\")\nuser=> (macroexpand '(contains v \"foo\"))\n(clojure.core/list (quote contains) v \"foo\")\n```\n\nOK, I'll admit it… it took me a few iterations to get that right. Quoting `quote`\n\nwas not something I was expecting.\n\nThis works for `contains`\n\n, but what about all the other SPARQL functions? Do I have to write this out for all of them as well? Then there are functions like `regex`\n\nthat can take 2 or 3 arguments.\n\nIt would be nice if I could take a function name, and generate the appropriate macro.\n\nExcept, macros are just code, and macros generate code. So I could always try generating a macro with a macro, right?\n\nWell, sort of. It turned out to be harder than I thought.\n\nLet's try creating a macro that generates a macro for a single-argument function, like `lcase`\n\n. I decided to skip the argument handling to start with, just to get the basic structure down. So my first attempt was:\n\n```\n(defmacro sparql-fn1 [s]\n `(defmacro ~s [~'a] `(list (quote (unquote ~s)) ~'a)))\n```\n\nThis made it clear that I wasn't really sure of how to perform the `(quote ~'contains)`\n\nwhen I didn't have a literal value to put into the quoted position.\n\nI thought I'd try it anyway, and it complained about \"No such var: user/s\", which didn't really surprise me. So let's see, what did it look like? I'll reformat the output so it's not all on a single line:\n\n``` js\n=> (macroexpand '(sparql-fn1 lcase))\n(do (clojure.core/defn lcase\n      ([&form &env a]\n       (clojure.core/seq\n         (clojure.core/concat\n           (clojure.core/list (quote clojure.core/list))\n           (clojure.core/list\n             (clojure.core/seq\n               (clojure.core/concat\n                  (clojure.core/list (quote quote))\n                  (clojure.core/list\n                    (clojure.core/seq\n                      (clojure.core/concat\n                        (clojure.core/list (quote clojure.core/unquote))\n                        (clojure.core/list user/s)))))))\n           (clojure.core/list (quote user/a))))))\n    (. (var lcase) (setMacro))\n    (var lcase))\n```\n\nLet's remove the `clojure.core`\n\nnamespaces, the redundant `seq`\n\ncalls, and use some quoting syntax:\n\n```\n(do (defn lcase\n     ([&form &env a]\n      (concat\n        (list 'list)\n        (list (concat\n                (list 'quote)\n                (list (concat\n                        (list 'unquote)\n                        (list user/s)))))\n        (list 'user/a))))\n    (. (var lcase) (setMacro))\n    (var lcase))\n```\n\nThis helped me figure out where I'm passing things in poorly, but there was something much more important going on here.\n\nThe returned data structure was not the code to call `defmacro`\n\nbut instead was a `do`\n\nblock with 3 steps:\n\n`lcase`\n\n. This takes 3 arguments, prepending `&form`\n\nand `&env`\n\nbefore the argument I had declared.`var`\n\nfor it. The second step calls `setMacro`\n\non that var.`lcase`\n\nvar.The arguments `&form`\n\nand `&env`\n\nare [explained in the Clojure docs](https://clojure.org/reference/macros#_special_variables) as special variables that are available inside macros without having to declare them. We are seeing this being set up.\n\nAlso, rather than creating a \"macro\", a function is being created that is then *converted* to a macro. I'd seen elements of this before, but I'd never really dug into it. This prompted me to look at the `defmacro`\n\nsource (by typing `(source defmacro)`\n\na the repl), and… yes, this is what it's doing.\n\nMost surprisingly, the code of the macro is not in the function, but rather the list structure of the macro is being constructed and returned. Reading the body, it looks like the final structure evaluates to:\n\n```\n(list 'list (list 'quote (list 'unquote user/s)) 'user/a)\n```\n\nThe `user/s`\n\npart came about due to my unfortunate quote/unquote structure, and the `user/a`\n\nis because of how I'm calling `macroexpand`\n\n, but the rest of it actually evaluates to what I was expecting, to wit:\n\n```\n(list (quote (unquote user/s)) user/a)\n```\n\nAnd that structure would evaluate to what gets inserted into source code.\n\nSo the function here generates a structure that evals to what the macro returns, which then evals a *second time* to what gets inserted into the source.\n\nSo I can manually create a macro by creating a function that needs to evaluate twice to create the final code structure. That's going to be confusing.\n\nBut then I realized that I don't have to worry about the quote/unquote mess anymore. If I want to quote something, then using `quote`\n\ndoes not hide the quoted value, because now I am creating a list where `quote`\n\nis the first element, and the thing to be quoted is exactly what I want. So it's more complex, but more flexible.\n\nLet's try then. To start with, I decided on a form that just takes all of its arguments, and wraps each one in the helper function that quotes it if the argument is a symbol starting with `$`\n\nor `?`\n\n.\n\n```\n(defmacro sparql-fn\n  [s]\n  (let [argq# (fn [a]\n                (list 'if (list 'and (list 'symbol? 'a) (list #{\\? \\$} (list 'first (list 'name 'a))))\n                      (list 'list (list 'quote 'quote) a)\n                      a))]\n    (list 'do\n          (list 'defn s\n                '[&form &env & args]\n                (list 'list\n                      (list 'quote 'cons)\n                      (list 'list (list 'quote 'quote) (list 'quote s))\n                      (list 'concat\n                            (list 'list (list 'quote 'list))\n                            (list 'map\n                                  (list 'fn '[a] (argq# 'a))\n                                  'args))))\n          (list '. (list 'var s) '(setMacro))\n          (list 'var s))))\n```\n\nThis took me a few attempts, but it worked out pretty well:\n\n``` js\n=> (sparql-fn contains)\n#'user/contains\n=> (sparql-fn lcase))\n#'user/lcase\n=> (contains ?x \"lo\")\n(contains ?x \"lo\")\n=> (lcase \"Hello\")\n(lcase \"Hello\")\n=> (lcase a)\nSyntax error compiling at (REPL:1:1).\nUnable to resolve symbol: a in this context\n```\n\nLet's see what the generated code looks like:\n\n``` js\n=> (macroexpand '(sparql-fn lcase))\n(do\n  (defn lcase\n   [&form &env & args]\n   (list (quote cons)\n         (list (quote quote) (quote lcase))\n         (concat\n           (list (quote list))\n           (map (fn [a]\n                 (if (and (symbol? a) (#{\\? \\$} (first (name a))))\n                   (list (quote quote) a)\n                   a))\n                args))))\n  (. (var lcase) (setMacro))\n  (var lcase))\n```\n\nHow does the returned list structure eval then? Working it manually, I came up with:\n\n```\n(list 'cons (list 'quote 'lcase) (concat '(list) (map (fn [a] …) args))))\n```\n\nThe function here was a bit long, so I elided it. It's looking good, but evaluating it one more time, gets us to:\n\n```\n(cons (quote lcase) (list (map (fn [a] …) args)))\n```\n\nWhich, when called as `(lcase \"Hello\")`\n\nshould return a list containing `'lcase`\n\nfollowed by the function mapped over all arguments. The function returns its input for a string, so the final list would be `(lcase \"Hello\")`\n\n.\n\nSimilarly, for `(lcase ?value)`\n\nit's the same kind of thing, but now the function will return a list of `(quote ?value)`\n\n, so the final output will be `(lcase (quote ?value))`\n\n.\n\nThere is no checking on the arguments at all, but I know that Fink is already doing that kind of work, so I thought this was where I could stop.\n\nAt this point I decided to try it with all of the SPARQL functions, and immediately hit a wall with `concat`\n\n. This is a function that appears in both SPARQL and in the above macro. As soon as `concat`\n\nis declared as a SPARQL macro, then the next thing I try to declare as a SPARQL macro will use the new `concat`\n\nmacro instead of `clojure.core/concat`\n\n. Fink also tries to map `and`\n\nto SPARQL's `&&`\n\n, so that should be addressed as well:\n\n```\n(defmacro sparql-fn\n  [s]\n  (let [argq# (fn [a]\n                (list 'if (list 'clojure.core/and (list 'symbol? 'a) (list #{\\? \\$} (list 'first (list 'name 'a))))\n                      (list 'list (list 'quote 'quote) a)\n                      a))]\n    (list 'do\n          (list 'defn s\n                '[&form &env & args]\n                (list 'list\n                      (list 'quote 'cons)\n                      (list 'list (list 'quote 'quote) (list 'quote s))\n                      (list 'clojure.core/concat\n                            (list 'list (list 'quote 'list))\n                            (list 'map\n                                  (list 'fn '[a] (argq# 'a))\n                                  'args))))\n          (list '. (list 'var s) '(setMacro))\n          (list 'var s))))\n```\n\nFinally, I can write SPARQL queries using Fink with clean filters:\n\n```\n['[?entity :contains ?c]\n '[?c :name ?name]\n [:filter (contains ?name \"lo\")]]\n```\n\nWell, this wasn't a tutorial. It was more like a litany of my mistakes as I stumbled towards understanding. I'm sure an LLM could have told me how to do this immediately, but then I wouldn't have learned anything.\n\nI have no idea if anyone else will ever read this, but if you did, then thanks. If not, then that's OK. Writing these things down helps my clarify and consolidate what I learned. This is something I used to do regularly a long time ago, and I should do more of it.\n\nAs per my AI policy, I'm going to leave the typos alone so you know I really did type this. But let me know if you see any sentences that make no sense, and I'll clean them up.", "url": "https://wpnews.pro/news/macro-macros", "canonical_source": "https://dev.to/quoll/macro-macros-2c83", "published_at": "2026-08-26 20:35:41+00:00", "updated_at": "2026-08-26 21:19:33.653459+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Clojure", "Datomic", "SPARQL", "HoneySQL", "Flint", "GQL"], "alternates": {"html": "https://wpnews.pro/news/macro-macros", "markdown": "https://wpnews.pro/news/macro-macros.md", "text": "https://wpnews.pro/news/macro-macros.txt", "jsonld": "https://wpnews.pro/news/macro-macros.jsonld"}}