Skip to content

Instantly share code, notes, and snippets.

@scripting
Created March 1, 2026 14:44
Show Gist options
  • Select an option

  • Save scripting/098f65350dce691f95a65fbbe6570366 to your computer and use it in GitHub Desktop.

Select an option

Save scripting/098f65350dce691f95a65fbbe6570366 to your computer and use it in GitHub Desktop.
opmlProjectEditor format

opmlProjectEditor Format

A reference document for working with the opmlProjectEditor format used by Dave Winer's OPML Editor and Frontier-based tools.

Overview

opmlProjectEditor is a way of storing a multi-file software project in a single OPML file. Each top-level outline node under the project headline represents a file. The file's contents are stored as child outline nodes, one node per line, with indentation representing code structure.

File Structure

<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
  <head>
    <title>project title</title>
    <dateCreated>...</dateCreated>
    <dateModified>...</dateModified>
    <ownerName>Dave Winer</ownerName>
    <ownerId>http://davewiner.com/</ownerId>
  </head>
  <body>
    <outline text="/path/to/project/">
      <outline text="worknotes.md" created="..."/>
      <outline text="styles.css" created="...">
        <outline text="body {">
          <outline text="background-color: white;"/>
          <outline text="}"/>
        </outline>
      </outline>
      <outline text="code.js" created="...">
        ...
      </outline>
      <outline text="index.html" created="...">
        ...
      </outline>
    </outline>
  </body>
</opml>

Key Conventions

Top-level structure

  • The <body> contains one outline node whose text attribute is the project path (e.g. /scripting.com/code/testing/domplayground2/).
  • All files live as direct children of that project node.

File nodes

  • Each file is an outline node whose text is the filename (e.g. styles.css, code.js, index.html).
  • File contents are stored as child outline nodes, one per line of the file.
  • Indentation in the outline represents indentation in the file — each level of nesting adds one tab.

Line nodes

  • Each line of code is stored as the text attribute of an outline node.
  • Empty lines are stored as <outline text=""/> or <outline text="" created="..."/>.
  • XML special characters are escaped: <&lt;, >&gt;, "&quot;, &&amp;, '&apos;.
  • For HTML/JS content stored inside text attributes, all angle brackets and quotes must be entity-escaped.

Comments

  • A node with isComment="true" is a commented-out line.
  • The node AND all its descendants must be skipped when reading the file.
  • This is critical — a commented-out CSS rule block means the selector line AND all its property lines are excluded.

Timestamps

  • Most nodes have a created attribute with an RFC 822 date string (e.g. Wed, 18 Dec 2024 17:19:15 GMT).
  • The <head> has dateCreated and dateModified.
  • Timestamps are optional on generated nodes but conventional.

Encoding

  • The file should be declared as UTF-8: <?xml version="1.0" encoding="UTF-8"?>.
  • Use numeric XML character references for special typographic characters to ensure they survive any encoding round-trip: &#8212; (em-dash), &#8220; (left double quote), &#8221; (right double quote), &#8217; (right single quote).
  • Do NOT use named HTML entities like &mdash; inside text attributes — the XML parser will reject them, and a code generator will double-escape them to &amp;mdash;.

Generating OPML from Source Files

When generating an OPML file programmatically from source files (e.g. with Python), the key steps are:

  1. Read each source file as UTF-8.
  2. Split on newlines.
  3. For each line, determine its indent level (count leading tabs).
  4. Emit the line as an outline node at the appropriate nesting depth.
  5. XML-escape the text attribute content (&, ", <, >, ').
  6. Close outline nodes properly when indent level decreases.

Python snippet for line-to-outline conversion

def text_to_outline_nodes(text, base_indent):
    lines = text.split('\n')
    result = []
    indent_stack = [base_indent]

    for line in lines:
        if not line.strip():
            tabs = '\t' * base_indent
            result.append(f'{tabs}<outline text=""/>')
            continue

        stripped = line.lstrip('\t')
        level = len(line) - len(stripped)
        depth = base_indent + level
        tabs = '\t' * depth
        escaped = (stripped
            .replace('&', '&amp;')
            .replace('"', '&quot;')
            .replace('<', '&lt;')
            .replace('>', '&gt;'))
        result.append(f'{tabs}<outline text="{escaped}"/>')

    return '\n'.join(result)

Reading OPML Back to Source Files

When reconstructing source files from OPML:

  1. Find the file node by its text attribute (filename).
  2. Walk its child outline nodes recursively.
  3. Skip any node where isComment="true" — and skip all its descendants.
  4. For each non-comment node, the text attribute is one line of the file.
  5. The nesting depth relative to the file node determines the indentation (one tab per level).
  6. Unescape XML entities back to their characters.

worknotes.md Convention

The worknotes.md file is a running log of development notes, written in the outline. Convention:

  • Top-level entries are dated headings: #### 3/1/26; by [author]
  • Sub-items are notes, decisions, architecture docs.
  • Author is whoever wrote the entry — can be a person or "Claude".
  • Newest entries at top.

CSS in opmlProjectEditor

CSS files stored in OPML follow the same line-per-node convention. Additional notes:

  • No multi-line block comments in CSS (they would require complex nesting). Use inline end-of-line comments only.
  • CSS rules nest naturally: the selector line is a parent node, property lines are children, closing } is a child at the same level as the properties.
  • Commented-out CSS rules appear as isComment="true" on the selector node, which excludes the entire rule including all properties.

HTML/JS in opmlProjectEditor

For HTML and JS files, content stored in text attributes must have all <, >, ", & escaped. This means:

  • <div class="foo"> becomes &lt;div class=&quot;foo&quot;&gt;
  • Template literal strings with embedded HTML are heavily escaped.
  • When reading back, unescape in the correct order: &amp; last.

Project Path Convention

The project's top-level outline text is a URL path identifying where the project lives on the server, e.g.:

/scripting.com/code/testing/domplayground2/

This is used by the build system to know where to publish the files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment