my eye

Bootstrap

Committed 1133e4

index 0000000..e38eefc
--- /dev/null

+name: Run Tests and Analysis
+
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - main
+
+jobs:
+  build-linux:
+    strategy:
+      matrix:
+        python-version: ["3.10"]
+    runs-on: "ubuntu-latest"
+    steps:
+      - name: Install graphviz
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y graphviz
+
+      - uses: actions/checkout@v3
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Install Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Remove development dependencies
+        run: sed -i '/\[tool.poetry.group.dev.dependencies\]/,/\[/d' pyproject.toml
+
+      - name: Install Poetry
+        uses: snok/install-poetry@v1
+        with:
+          version: 1.2.2
+          virtualenvs-in-project: true
+
+      - name: Install dependencies
+        run: poetry install --no-interaction --no-root
+
+      - name: Install library
+        run: poetry install --no-interaction
+
+      - name: Install development tools
+        run: poetry add gmpg
+
+      - uses: psf/black@stable
+        with:
+          options: "--check --verbose"
+          src: "."
+          version: "23.7"
+
+      - uses: isort/isort-action@v1
+        with:
+          configuration: "--profile black"
+
+      # - name: Run tests
+      #   run: poetry run gmpg test
+
+      - name: Run analysis
+        run: poetry run gmpg analyze
+
+      - name: Generate dependency graph
+        run: poetry run gmpg graph
+
+      - uses: actions/upload-artifact@v3
+        with:
+          name: analysis
+          path: |
+            test_coverage.xml
+            test_results.xml
+            api_python.json
+            deps.svg

index 0000000..8995423
--- /dev/null

+[tool.poetry]
+name = "webint-editor"
+version = "0.0.170"
+description = "an editor for your website"
+authors = ["Angelo Gladding <angelo@ragt.ag>"]
+license = "AGPL-3.0-or-later"
+keywords = ["Micropub"]
+packages = [{include="webint_editor"}]
+
+[tool.poetry.plugins.webapps]
+editor = "webint_editor:app"
+
+[tool.poetry.dependencies]
+python = ">=3.10,<3.11"
+webint = ">=0.0"
+
+[tool.poetry.dev-dependencies]
+gmpg = {path="../gmpg", develop=true}
+bgq = {path="../bgq", develop=true}
+newmath = {path="../newmath", develop=true}
+sqlyte = {path="../sqlyte", develop=true}
+webint = {path="../webint", develop=true}
+webagt = {path="../webagt", develop=true}
+microformats = {path="../python-microformats", develop=true}
+
+# [[tool.poetry.source]]
+# name = "main"
+# url = "https://ragt.ag/code/pypi"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"

index 0000000..609534b
--- /dev/null

+"""
+An editor for your website.
+
+Implements a Micropub editor.
+
+"""
+
+import json
+import pprint
+
+import sqlyte
+import web
+import webagt
+
+app = web.application(__name__, prefix="editor")
+
+
+@app.control("")
+class Drafts:
+    """All Drafts."""
+
+    def get(self):
+        """Return a list of all drafts."""
+        return app.view.drafts()
+
+    def post(self):
+        return "requires javascript"
+        # form = web.form("content")
+        # web.application("understory.posts").model.create(
+        #     "entry", {"content": form.content}
+        # )
+        # return "entry created"
+
+
+@app.control("draft")
+class Draft:
+    """A draft."""
+
+    def get(self):
+        """Return the draft."""
+        permalink = web.form(permalink=None).permalink
+        post = {}
+        if permalink:
+            post = web.application("webint_posts").model.read(permalink)["resource"]
+        else:
+            permalink, _ = web.application("webint_posts").model.create("entry")
+            raise web.SeeOther(f"/editor/draft?permalink={permalink}")
+        return app.view.draft(
+            post,
+            web.application("webint_owner").model.get_identities(),
+            [],  # web.application("webint_guests").model.get_guests(),
+        )
+
+    def post(self):
+        return "requires javascript"
+        # form = web.form("content")
+        # web.application("understory.posts").model.create(
+        #     "entry", {"content": form.content}
+        # )
+        # return "entry created"
+
+
+@app.control("preview/markdown")
+class PreviewMarkdown:
+    """"""
+
+    def get(self):
+        return (
+            "<form method=post>"
+            "<textarea name=content></textarea>"
+            "<button>Preview</button>"
+            "</form>"
+        )
+
+    def post(self):
+        form = web.form("pad_id", context=None)
+        etherpad_content = sqlyte.db(
+            "/home/admin/app/run/media/pads/etherpad-lite/var/sqlite.db"
+        ).select("store", where="key = ?", vals=[f"pad:{form.pad_id}"])[0]["value"]
+        content = json.loads(etherpad_content)["atext"]["text"]
+        rendered = str(
+            web.mkdn(
+                str(
+                    web.template(
+                        content,
+                        globals={"get": webagt.get, "pformat": pprint.pformat},
+                        restricted=True,
+                    )()
+                ),
+                form.context,
+            )  # , globals=micropub.markdown_globals)
+        )
+        web.header("Content-Type", "application/json")
+        return {
+            "content": rendered,
+            # "readability": micropub.readability.Readability(form.content).metrics,
+        }
+
+
+@app.control("preview/resource")
+class PreviewResource:
+    """"""
+
+    def get(self):
+        url = web.form(url=None).url
+        web.header("Content-Type", "application/json")
+        if not url:
+            return {}
+        resource = web.get(url)
+        if resource.entry.data:
+            return resource.entry
+        if resource.event.data:
+            return resource.event
+        if resource.feed.data:
+            return resource.feed
+        return {}
+
+        # XXX data = cache.parse(url)
+        # XXX if "license" in data["data"]["rels"]:
+        # XXX     data["license"] = data["data"]["rels"]["license"][0]
+        # XXX try:
+        # XXX     edit_page = data["html"].cssselect("#ca-viewsource a")[0]
+        # XXX except IndexError:
+        # XXX     # h = html2text.HTML2Text()
+        # XXX     # try:
+        # XXX     #     data["content"] = h.handle(data["entry"]["content"]).strip()
+        # XXX     # except KeyError:
+        # XXX     #     pass
+        # XXX     try:
+        # XXX         markdown_input = ("html", data["entry"]["content"])
+        # XXX     except (KeyError, TypeError):
+        # XXX         markdown_input = None
+        # XXX else:
+        # XXX     edit_url = web.uri.parse(str(data["url"]))
+        # XXX     edit_url.path = edit_page.attrib["href"]
+        # XXX     edit_page = fromstring(requests.get(edit_url).text)
+        # XXX     data["mediawiki"] = edit_page.cssselect("#wpTextbox1")[0].value
+        # XXX     data["mediawiki"] = (
+        # XXX         data["mediawiki"].replace("{{", r"{!{").replace("}}", r"}!}")
+        # XXX     )
+        # XXX     markdown_input = ("mediawiki", data["mediawiki"])
+
+        # XXX if markdown_input:
+        # XXX     markdown = str(
+        # XXX         sh.pandoc(
+        # XXX         "-f", markdown_input[0], "-t", "markdown", _in=markdown_input[1]
+        # XXX         )
+        # XXX     )
+        # XXX     for n in range(1, 5):
+        # XXX         indent = "    " * n
+        # XXX         markdown = markdown.replace(f"\n{indent}-",
+        # XXX                                     f"\n{indent}\n{indent}-")
+        # XXX     markdown = re.sub(r'\[(\w+)\]\(\w+ "wikilink"\)', r"[[\1]]", markdown)
+        # XXX     markdown = markdown.replace("–", "--")
+        # XXX     markdown = markdown.replace("—", "---")
+        # XXX     data["content"] = markdown
+
+        # XXX data.pop("html")
+        # XXX # XXX data["category"] = list(set(data["entry"].get("category", [])))
+        # XXX web.header("Content-Type", "application/json")
+        # XXX return dump_json(data)

index 0000000..60da55c
--- /dev/null

+from pprint import pformat
+
+import pendulum
+from web import tx
+
+__all__ = ["pendulum", "tx", "pformat"]

index 0000000..5b218a7
--- /dev/null

+$def with (post, identities, guests)
+$var body_classes = ["widescreen"]
+
+$code:
+    def get_property(prop):
+        return post.pop(prop, [""])[0]
+
+$ permalink = get_property("url")
+$ pad_id = permalink.lstrip('/').replace('/', '--')
+$var title: Editing $permalink
+$var show_title = False
+$var hide_footer = True
+
+<form id=editor method=post action=$tx.origin/editor>
+<div id=preview>
+    <div id=replyContext></div>
+    <div id=previewName></div>
+    <div id=previewContent></div>
+</div>
+<div id=properties>
+    <div id=general_properties>
+        $# <fieldset id=author>
+        $# <legend>Author</legend>
+        $# $ author = get_property("author")
+        $# $for identity in identities:
+        $#     $ card = identity["card"]
+        $#     <label class=radio><input required type=radio name=author value=$card["uid"][0]
+        $#     $if author and author["uid"] == card["uid"]:
+        $#         checked
+        $#     $elif card["uid"][0] == "/":
+        $#         checked
+        $#     > $card["name"][0]</label>
+        $# </label>
+        $# </fieldset>
+
+        $# <div id=coauthor>
+        $#     <label><small>Coauthor</small><br>
+        $#     <label class=bounding><fieldset>
+        $#     $# $for rsvp in ["yes", "no", "maybe"]:
+        $#     $#     <label class=radio><input required type=radio name=rsvp value=$rsvp
+        $#     $#     $if "rsvp" in post and rsvp == post["rsvp"][0] or rsvp == "public":
+        $#     $#         checked
+        $#     $#     > $rsvp</label>
+        $#     </fieldset></label>
+        $#     </label>
+        $# </div>
+        $# <ul>
+        $# <p><small>Guests currently signed in:</small></p>
+        $# $for guest in guests:
+        $#     <li><label><input type=checkbox name=coauthor value=$guest["url"]>
+        $#     $guest["name"]</label> <small><a href=$guest["url"]>$guest["url"]</a></small></li>
+        $# </ul>
+        $# </fieldset>
+
+        $# <fieldset id=audience>
+        $# <legend>Audience</legend>
+        $# $if post:
+        $#     $for member_url in post.get("audience", []):
+        $#         <label><input checked type=checkbox name=audience value=$member_url>
+        $#         $member_url</label>
+        $# </fieldset>
+
+        <fieldset id=visibility>
+        <legend>Visibility</legend>
+        $for vis in ["public", "protected", "private"]:
+            <label class=radio><input required type=radio name=visibility value=$vis
+            $if vis == get_property("visibility"):
+                checked
+            > $vis</label>
+        </fieldset>
+
+        $# <fieldset id=syndication>
+        $# <legend>Syndication</legend>
+        $# $for syn in ["mastodon"]:
+        $#     <label class=radio><input required type=checkbox name=syndication value=$syn
+        $#     $if syn == get_property("syndication"):
+        $#         checked
+        $#     > $syn</label>
+        $# </fieldset>
+
+        $# $ type = get_property("type")
+        $# <fieldset id=type>
+        $# <legend>Type</legend>
+        $# $for _type in ["card", "entry", "event", "recipe", "review"]:
+        $#     <label class=radio><input required type=radio name=h value=$_type
+        $#     $if (not type and _type == "entry") or _type == type:
+        $#         checked
+        $#     $else:
+        $#         disabled
+        $#     > $_type</label>
+        $# </fieldset>
+    </div>
+
+    $# <div id=card_properties>
+    $#     <div id=name>
+    $#         <label><small>Name</small><br>
+    $#         <label class=bounding><input type=text name=name
+    $#         value="$get_property('name')"></label></label>
+    $#     </div>
+    $# </div>
+
+    <style>
+    #entry_properties div {
+        display: inline-block;
+        width: 100%; }
+    </style>
+    <div id=entry_properties>
+        <div id=in_reply_to>
+            <label><small>In reply to</small><br>
+            <label class=bounding><input type=text name=in_reply_to
+            value="$get_property('in-reply-to')"></label></label>
+        </div>
+        <div id=rsvp>
+            <label><small>RSVP</small><br>
+            <label class=bounding><fieldset>
+            $for rsvp in ["yes", "no", "maybe"]:
+                <label class=radio><input type=radio name=rsvp value=$rsvp
+                $if "rsvp" in post and rsvp == post["rsvp"][0] or rsvp == "public":
+                    checked
+                > $rsvp</label>
+            </fieldset></label>
+            </label>
+        </div>
+        <div id=like_of>
+            <label><small>Like of</small><br>
+            <label class=bounding><input type=text name=like_of
+            value="$get_property('like-of')"></label></label>
+        </div>
+        <div id=bookmark_of>
+            <label><small>Bookmark of</small><br>
+            <label class=bounding><input type=text name=bookmark_of
+            value="$get_property('bookmark-of')"></label></label>
+        </div>
+        <div id=listen_of>
+            <label><small>Listen of</small><br>
+            <label class=bounding><input type=text name=listen_of
+            value="$get_property('listen-of')"></label></label>
+        </div>
+        <div id=name>
+            <label><small>Name</small><br>
+            <label class=bounding><input type=text name=name
+            value="$get_property('name')"></label></label>
+        </div>
+        <div id=summary>
+            <label><small>Summary</small><br>
+            <label class=bounding><input type=text name=summary
+            value="$get_property('summary')"></label></label>
+        </div>
+        <div id=photo>
+            <label><small>Photo</small><br>
+            <label class=bounding><input type=text name=name
+            value="$get_property('photo')"></label></label>
+        </div>
+        <div id=econtent>
+            <label><small>Content</small><br>
+            <label class=bounding>
+            $# XXX <div id=editor></div>
+            $# XXX <div id=editorStatus></div>
+            $# TODO FIXME <textarea id=content-editor name=content>$get_property('content')["html"]</textarea>
+            <!--textarea id=content-editor name=content>$get_property('content')</textarea-->
+            <iframe src="$tx.origin/pads/p/$pad_id"
+                frameborder=0 style=height:30em;width:100%></iframe>
+            </label></label>
+            $# XXX <div id=connection></div>
+            $# XXX <div id=version></div>
+        </div>
+    </div>
+
+    $# <div id=event_properties>
+    $#     <div id=name>
+    $#         <label><small>Name</small><br>
+    $#         <label class=bounding><input type=text name=name
+    $#         value="$get_property('name')"></label></label>
+    $#     </div>
+    $# </div>
+    $# <div id=resume_properties>
+    $#     <div id=name>
+    $#         <label><small>Name</small><br>
+    $#         <label class=bounding><input type=text name=name
+    $#         value="$get_property('name')"></label></label>
+    $#     </div>
+    $# </div>
+    $# <div id=review_properties>
+    $#     <div id=name>
+    $#         <label><small>Name</small><br>
+    $#         <label class=bounding><input type=text name=name
+    $#         value="$get_property('name')"></label></label>
+    $#     </div>
+    $# </div>
+
+    $# <fieldset id=categories>
+    $#   <legend>Categories</legend>
+    $#   <!-- XXX todo populate with call to mp?q=config as with channels -->
+    $#   <input type=text><button>Add</button>
+    $#   <ul>
+    $#   $# $for category in c
+    $#   $#     <li><input type=checkbox id=coding name=category value=coding>
+    $#   $#     <label for=coding>Coding</label></li>
+    $#   </ul>
+    $# </fieldset>
+
+    $if published := get_property("published"):
+        <div id=published>
+        <label for=published><small>Published</small></label><br>
+        <input style="border:1px solid #333" type=datetime-local name=published_datetime
+        value="$published.to_date_string()T$published.to_time_string()">
+        <input style="font-family:monospace;font-size:.9em;border:1px solid #333;width:10em" type=text name=published_microseconds
+        value="$published.microsecond">
+        <select name=published_tz>
+        $for tz in pendulum.timezones:
+            $if "/" not in tz:
+                $continue
+            <option
+            $if tz == getattr(published, "timezone_name", "America/Los_Angeles"):
+                selected
+            >$tz</option>
+        </select>
+        </div>
+    <div id=permalink>
+        <label><small>Permalink</small><br>
+        <label class=bounding><input readonly type=text name=url
+        value=$tx.origin/$permalink.lstrip("/")></label></label>
+    </div>
+    </div>
+
+    <div class=buttons>
+        $# <label><input type=checkbox> Draft Live</label>
+        $# <button name=action value=save>Save</button>
+        $# if permalink:
+        <button name=action value=update>Update</button>
+        $# $else:
+        $#     <button name=action value=create>Create</button>
+    </div>
+</form>
+
+$if post:
+    <h4>Unused properties</h4>
+    <pre>$pformat(post)</pre>
+
+<style>
+form#editor {
+  column-gap: 2%;
+  display: grid;
+  grid-template-columns: 49% 49%; }
+form#editor input[type=text], textarea {
+  background-color: #ddd;
+  border: 0;
+  padding: 0;
+  width: 100%; }
+form#editor textarea {
+  height: 20em;
+  width: 100%; }
+form#editor input[type=text] {
+  height: 1.5em; }
+#categories ul {
+  list-style: none;
+  padding-left: 0; }
+.bounding {
+  background-color: #ddd;
+  border: .1em solid #333;
+  display: block;
+  padding: .125em .25em; }
+.radio {
+  cursor: pointer;
+  font-size:.8em }
+label small, legend {
+  font-size: .8em;
+  font-weight: bold; }
+fieldset {
+  border: 0;
+  margin: 0;
+  padding: 0; }
+ul {
+  list-style: none;
+  margin: 0;
+  padding: 0; }
+.buttons {
+  margin-top: 1em;
+  text-align: right; }
+#author, #coauthor, #type, #in_reply_to, #rsvp, #like_of, #bookmark_of, #listen_of, #name, #summary,
+#photo, #econtent, #coauthors, #visibility, #published, #permalink {
+  margin-bottom: .5em; }
+#in_reply_to input[type=checkbox], #name input[type=checkbox] {
+  font-size: .9em; }
+#in_reply_to input[type=text], #name input[type=text], #permalink input[type=text] {
+  outline: none; }
+#in_reply_to input[type=text], #permalink input[type=text] {
+  font-size: .9em; }
+$# #name input {
+$#   font-size: 1.5em;
+$#   height: 1.2em; }
+#content {
+  height: 35em; }
+</style>
+
+<script type=module>
+// XXX import { load, diamondMonaco, MicropubClient } from '/static/web.js'
+import { load, MicropubClient } from '/static/web.js'
+
+const pub = new MicropubClient('/posts')
+
+load(() => {
+  // document.querySelector('#card_properties').style.display = 'none'
+  // document.querySelector('#event_properties').style.display = 'none'
+  // document.querySelector('#resume_properties').style.display = 'none'
+  // document.querySelector('#review_properties').style.display = 'none'
+  const enlivenOptionalProperty = (type, property) => {
+    const textbox = document.querySelector(`#$${type}_properties #$${property} .bounding`)
+    textbox.style.display = 'none'
+    const checkbox = document.createElement('input')
+    checkbox.type = 'checkbox'
+    checkbox.addEventListener('click', ev => {
+      textbox.style.display = ev.target.checked ? 'block' : 'none'
+      textbox.focus()
+    })
+    document.querySelector(`#$${type}_properties #$${property} small`).prepend(checkbox)
+  }
+  // enlivenOptionalProperty('general', 'coauthor')
+  enlivenOptionalProperty('entry', 'in_reply_to')
+  enlivenOptionalProperty('entry', 'rsvp')
+  enlivenOptionalProperty('entry', 'like_of')
+  enlivenOptionalProperty('entry', 'bookmark_of')
+  enlivenOptionalProperty('entry', 'listen_of')
+  enlivenOptionalProperty('entry', 'name')
+  enlivenOptionalProperty('entry', 'summary')
+  enlivenOptionalProperty('entry', 'photo')
+  enlivenOptionalProperty('entry', 'econtent')
+
+  const getContent = () => {
+    // XXX monaco.getValue()
+    // return document.querySelector('#content-editor').value
+    const request = new XMLHttpRequest()
+    request.open("GET", '/pads/p/$pad_id/export/txt', false)
+    request.send(null);
+    if (request.status === 200) {
+      return request.responseText
+      // const response = JSON.parse(request.responseText)
+      // return response['pad:$pad_id'].atext.text
+    }
+  }
+
+  var frequency = 1;  // seconds
+  var count = 0;
+  var clean = true;
+  const tick = () => {
+    setTimeout(() => {
+      if (count++ > frequency) // && !clean)
+        updatePreview();
+      tick();
+    }, 1000);
+  }
+  tick();
+  const updatePreview = () => {
+    clean = false;
+    if (count > frequency) {
+      previewMarkdown();
+      clean = true;
+      count = 0;
+    }
+  }
+  updatePreview()
+
+  pub.getConfig().then(config => console.log("Micropub Config:", config))
+  // pub.getCategories().then(data => {
+  //   data.categories.forEach(cat => {
+  //     addCategory(cat)
+  //   })
+  // })
+
+  // TODO on form control change create a permalink and save a draft
+  const permalink = '$permalink'
+  const properties = {'content': '', 'post-status': 'draft'}
+
+  $# let monaco
+  $# pub.create('entry', properties).then(permalink => {
+  $#   $# $# document.querySelector('#permalink input[type=text]').value =
+  $#   $# $#   `$tx.origin$${permalink}`
+  $#   $# monaco = diamondMonaco(
+  $#   $#   permalink, editor, editorStatus, connection, version,
+  $#   $#   {
+  $#   $#     language: 'markdown'
+  $#   $#   },
+  $#   $#   '$(tx.user.session["uid"][0] if tx.user.session else tx.user.ip)',
+  $#   $#   true
+  $#   $# )
+  $#   $# monaco.onKeyUp(updatePreview)
+  $# })
+
+  // document.querySelector('#content-editor').addEventListener('keyup', updatePreview)
+  document.querySelector('#in_reply_to input[type=text]').addEventListener('blur', ev => {
+    previewReplyContext(ev.target.value)
+  })
+
+  addEventListener('beforeunload', ev => {
+    ev.stopPropagation()
+    ev.preventDefault()
+    return false
+  }, true)
+
+  document.querySelector('form#editor').addEventListener('submit', ev => {
+    ev.preventDefault()
+    // let myForm = document.getElementById('editor')
+    // let formData = new FormData(myForm)
+    // for (let p of formData) {
+    //   console.log(p)
+    // }
+    pub.update(document.querySelector('input[name=url]').value,
+      "replace",
+      {
+        "visibility": [document.querySelector('input[name=visibility]').value],
+        "syndication": [document.querySelector('input[name=syndication]').value],
+        "name": [document.querySelector('input[name=name]').value],
+        "content": [getContent()]
+      }
+    )
+  
+    // const properties = {}
+    // const name = document.querySelector('input[name=name]').value
+    // if (name) {
+    //   properties.name = [name]
+    // }
+  
+    // const content = editor.value
+    // if (content) {
+    //   properties.content = [content]
+    // }
+  
+    // const published_date = document.querySelector('input[name=published_date]').value
+    // const published_time = document.querySelector('input[name=published_time]').value
+    // const published_tz = document.querySelector('select[name=published_tz]').value
+    // console.log(published_date)
+    // console.log(published_time)
+    // console.log(published_tz)
+    // if (published_date) {
+    //   let published = published_date
+    //   if (published_time) {
+    //     published = `$${published_date}T$${published_time}:00+00:00`
+    //   }
+    //   properties.published = [{datetime: published, timezone: published_tz}]
+    // }
+  
+    // const categories = []
+    // document.querySelectorAll('#categories input[type=checkbox]').forEach(el => {
+    //   if (el.checked) {
+    //     categories.push(el.value)
+    //   }
+    // })
+    // if (categories.length) {
+    //   properties['category'] = categories
+    // }
+  
+    // console.log(properties)
+    // pub.create('entry', properties, 'public').then(permalink => {
+    //   window.location.href = permalink
+    // })
+  })
+})
+
+const previewMarkdown = () => {
+  name = document.querySelector('#name input[type=text]').value
+  previewName.innerHTML = `<h1>$${name}</h1>`
+  let body = new FormData()
+  body.append('pad_id', '$pad_id')
+  fetch(
+    '/editor/preview/markdown',
+    {
+      method: 'POST',
+      body: body
+    }
+  ).then(response => {
+    if (response.status === 200) {
+      return response.json().then(data => {
+        previewContent.innerHTML = data['content']
+      })
+    }
+  })
+}
+
+const previewReplyContext = url => {
+  if (url == '') {
+    replyContext.innerHTML = '';
+    return
+  }
+  fetch(
+    '/editor/preview/resource?url=' + encodeURIComponent(url),
+  ).then(response => {
+    if (response.status === 200) {
+      return response.json().then(data => {
+        replyContext.innerHTML = `<big><strong>$${data['name']}</strong></big><p>$${data['summary']}</p>`
+      })
+    }
+  })
+}
+
+$# const addCategory = (cat) => {
+$#   const ul = document.querySelector('#categories ul')
+$#   const li = document.createElement('li')
+$#   const checkbox = document.createElement('input')
+$#   const tagname = document.createElement('span')
+$#   checkbox.type = 'checkbox'
+$#   checkbox.value = cat
+$#   li.appendChild(checkbox)
+$#   tagname.innerText = cat
+$#   li.appendChild(tagname)
+$#   ul.appendChild(li)
+$# }
+
+$# const { MicropubClient } = web
+$# const pub = new MicropubClient('/posts')
+$# document.addEventListener('DOMContentLoaded', () => {
+$#   pub.getConfig().then(data => console.log(data))
+$#   pub.getCategories().then(data => {
+$#     data.categories.forEach(cat => {
+$#       addCategory(cat)
+$#     })
+$#   })
+
+$#   document.querySelector('#categories button').addEventListener('click', event => {
+$#     event.preventDefault()
+$#     const textbox = document.querySelector('#categories input')
+$#     addCategory(textbox.value)
+$#     textbox.value = ''
+$#   })
+</script>

index 0000000..02926a7
--- /dev/null

+$def with ()
+$var title: Drafts
+
+<p>..</p>
+
+<form method=post>
+<button>New</button>
+</form>

index 0000000..d68c997
--- /dev/null

+$def with (resource)
+$:resource
+$# $:resource.aside()