Task 495: .OTL File Format

Task 495: .OTL File Format

File Format Specifications for .OTL

The .OTL file format, as used in Vim Outliner (and compatible tools), is a plain-text, human-readable format for creating hierarchical outlines. It relies on tab indentation to define structure, with no binary headers or magic numbers. The format supports nested headings, body text, and optional executable commands. It is not a strict binary format but a convention for structuring text files, making it portable and editable in any text editor. Files are UTF-8 or ASCII encoded by default.

Key features:

  • Hierarchical structure defined by leading tabs (\t) for nesting levels.
  • Headings are standard lines at their indentation level.
  • Body text lines start with a colon followed by a space (": ") after the tabs.
  • Executable lines use a "exe" separator between description and command.
  • No formal version number or footer; the file is just a sequence of lines.
  • Comments or other markup are not natively supported unless defined in extensions.

Example structure:

Top Level Heading
	Child Heading
		Grandchild Heading
	: Body text that can wrap and span multiple lines if needed.
	Description _exe_ command to execute

1. List of All Properties of This File Format Intrinsic to Its File System

Since .OTL is a plain-text format, its "properties" refer to the structural elements parsed from the file content. These are not file system metadata (like size or permissions) but intrinsic format attributes derived from parsing lines. The key properties for each node in the outline hierarchy are:

  • Level: The nesting depth, determined by the number of leading tabs (integer, starting from 0 for top-level).
  • Type: The node category (string: "headline" for standard headings, "body" for lines starting with ": ", "executable" for lines containing "exe").
  • Content: The text content of the node after removing leading tabs and type indicators (string).
  • Children Count: Number of direct child nodes (integer; 0 if leaf node).

These properties are recursive, as the format represents a tree. When dumping or printing, traverse the tree and list them for each node.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .OTL File Dump

This is a self-contained HTML page with JavaScript that allows dragging and dropping a .OTL file. It parses the file into a tree, then dumps all properties (level, type, content, children count) to the screen in a readable format.

OTL File Dumper
Drag and drop .OTL file here

    

4. Python Class for .OTL Handling

import sys

class OtlHandler:
    def __init__(self):
        self.tree = []

    def read(self, filepath):
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
        self.tree = self._parse(content)

    def _parse(self, content):
        lines = [line for line in content.split('\n') if line.strip()]
        root = {'level': -1, 'type': 'root', 'content': '', 'children': [], 'children_count': 0}
        stack = [root]
        for line in lines:
            level = len(line) - len(line.lstrip('\t'))
            trimmed = line.lstrip('\t')
            node_type = 'headline'
            node_content = trimmed
            if trimmed.startswith(': '):
                node_type = 'body'
                node_content = trimmed[2:]
            elif ' _exe_ ' in trimmed:
                node_type = 'executable'
            node = {'level': level, 'type': node_type, 'content': node_content, 'children': [], 'children_count': 0}
            while stack[-1]['level'] >= level:
                stack.pop()
            stack[-1]['children'].append(node)
            stack[-1]['children_count'] += 1
            stack.append(node)
        return root['children']

    def print_properties(self, nodes=None, indent=''):
        if nodes is None:
            nodes = self.tree
        for node in nodes:
            print(f"{indent}Level: {node['level']}")
            print(f"{indent}Type: {node['type']}")
            print(f"{indent}Content: {node['content']}")
            print(f"{indent}Children Count: {node['children_count']}\n")
            if node['children']:
                self.print_properties(node['children'], indent + '  ')

    def write(self, filepath):
        with open(filepath, 'w', encoding='utf-8') as f:
            f.write(self._serialize(self.tree))

    def _serialize(self, nodes, level=0):
        result = ''
        for node in nodes:
            tabs = '\t' * level
            if node['type'] == 'body':
                result += f"{tabs}: {node['content']}\n"
            elif node['type'] == 'executable':
                result += f"{tabs}{node['content']}\n"
            else:
                result += f"{tabs}{node['content']}\n"
            if node['children']:
                result += self._serialize(node['children'], level + 1)
        return result

# Example usage:
# handler = OtlHandler()
# handler.read('example.otl')
# handler.print_properties()
# handler.write('output.otl')

5. Java Class for .OTL Handling

import java.io.*;
import java.util.*;

public class OtlHandler {
    private List<Node> tree = new ArrayList<>();

    static class Node {
        int level;
        String type;
        String content;
        List<Node> children = new ArrayList<>();
        int childrenCount = 0;

        Node(int level, String type, String content) {
            this.level = level;
            this.type = type;
            this.content = content;
        }
    }

    public void read(String filepath) throws IOException {
        StringBuilder content = new StringBuilder();
        try (BufferedReader br = new BufferedReader(new FileReader(filepath))) {
            String line;
            while ((line = br.readLine()) != null) {
                if (!line.trim().isEmpty()) {
                    content.append(line).append("\n");
                }
            }
        }
        tree = parse(content.toString());
    }

    private List<Node> parse(String content) {
        String[] lines = content.split("\n");
        Node root = new Node(-1, "root", "");
        Deque<Node> stack = new ArrayDeque<>();
        stack.push(root);
        for (String line : lines) {
            if (line.trim().isEmpty()) continue;
            int level = 0;
            while (level < line.length() && line.charAt(level) == '\t') level++;
            String trimmed = line.substring(level);
            String type = "headline";
            String nodeContent = trimmed;
            if (trimmed.startsWith(": ")) {
                type = "body";
                nodeContent = trimmed.substring(2);
            } else if (trimmed.contains(" _exe_ ")) {
                type = "executable";
            }
            Node node = new Node(level, type, nodeContent);
            while (stack.peek().level >= level) stack.pop();
            stack.peek().children.add(node);
            stack.peek().childrenCount++;
            stack.push(node);
        }
        return root.children;
    }

    public void printProperties() {
        printProperties(tree, "");
    }

    private void printProperties(List<Node> nodes, String indent) {
        for (Node node : nodes) {
            System.out.println(indent + "Level: " + node.level);
            System.out.println(indent + "Type: " + node.type);
            System.out.println(indent + "Content: " + node.content);
            System.out.println(indent + "Children Count: " + node.childrenCount + "\n");
            if (!node.children.isEmpty()) {
                printProperties(node.children, indent + "  ");
            }
        }
    }

    public void write(String filepath) throws IOException {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(filepath))) {
            bw.write(serialize(tree, 0));
        }
    }

    private String serialize(List<Node> nodes, int level) {
        StringBuilder sb = new StringBuilder();
        for (Node node : nodes) {
            String tabs = "\t".repeat(level);
            if (node.type.equals("body")) {
                sb.append(tabs).append(": ").append(node.content).append("\n");
            } else if (node.type.equals("executable")) {
                sb.append(tabs).append(node.content).append("\n");
            } else {
                sb.append(tabs).append(node.content).append("\n");
            }
            if (!node.children.isEmpty()) {
                sb.append(serialize(node.children, level + 1));
            }
        }
        return sb.toString();
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     OtlHandler handler = new OtlHandler();
    //     handler.read("example.otl");
    //     handler.printProperties();
    //     handler.write("output.otl");
    // }
}

6. JavaScript Class for .OTL Handling

class OtlHandler {
    constructor() {
        this.tree = [];
    }

    read(content) {  // Accepts file content as string (e.g., from fs.readFileSync in Node)
        this.tree = this._parse(content);
    }

    _parse(content) {
        const lines = content.split('\n').filter(line => line.trim() !== '');
        const root = { level: -1, type: 'root', content: '', children: [], childrenCount: 0 };
        const stack = [root];
        lines.forEach(line => {
            const level = line.match(/^\t*/)[0].length;
            let trimmed = line.replace(/^\t+/, '');
            let type = 'headline';
            let nodeContent = trimmed;
            if (trimmed.startsWith(': ')) {
                type = 'body';
                nodeContent = trimmed.slice(2);
            } else if (trimmed.includes(' _exe_ ')) {
                type = 'executable';
            }
            const node = { level, type, content: nodeContent, children: [], childrenCount: 0 };
            while (stack[stack.length - 1].level >= level) stack.pop();
            stack[stack.length - 1].children.push(node);
            stack[stack.length - 1].childrenCount++;
            stack.push(node);
        });
        return root.children;
    }

    printProperties(nodes = this.tree, indent = '') {
        let output = '';
        nodes.forEach(node => {
            output += `${indent}Level: ${node.level}\n`;
            output += `${indent}Type: ${node.type}\n`;
            output += `${indent}Content: ${node.content}\n`;
            output += `${indent}Children Count: ${node.childrenCount}\n\n`;
            if (node.children.length > 0) {
                output += this.printProperties(node.children, indent + '  ');
            }
        });
        console.log(output);
    }

    write() {
        return this._serialize(this.tree);
    }

    _serialize(nodes, level = 0) {
        let result = '';
        nodes.forEach(node => {
            const tabs = '\t'.repeat(level);
            if (node.type === 'body') {
                result += `${tabs}: ${node.content}\n`;
            } else if (node.type === 'executable') {
                result += `${tabs}${node.content}\n`;
            } else {
                result += `${tabs}${node.content}\n`;
            }
            if (node.children.length > 0) {
                result += this._serialize(node.children, level + 1);
            }
        });
        return result;
    }
}

// Example usage in Node.js:
// const fs = require('fs');
// const handler = new OtlHandler();
// const content = fs.readFileSync('example.otl', 'utf8');
// handler.read(content);
// handler.printProperties();
// const output = handler.write();
// fs.writeFileSync('output.otl', output);

7. C Class (Struct-Based) for .OTL Handling

In C, we use structs instead of classes. This implementation uses dynamic memory for the tree.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct Node {
    int level;
    char *type;
    char *content;
    struct Node **children;
    int children_count;
} Node;

Node* create_node(int level, const char* type, const char* content) {
    Node* node = (Node*)malloc(sizeof(Node));
    node->level = level;
    node->type = strdup(type);
    node->content = strdup(content);
    node->children = NULL;
    node->children_count = 0;
    return node;
}

void add_child(Node* parent, Node* child) {
    parent->children = (Node**)realloc(parent->children, (parent->children_count + 1) * sizeof(Node*));
    parent->children[parent->children_count++] = child;
}

void free_tree(Node** tree, int size) {
    for (int i = 0; i < size; i++) {
        free(tree[i]->type);
        free(tree[i]->content);
        if (tree[i]->children_count > 0) {
            free_tree(tree[i]->children, tree[i]->children_count);
        }
        free(tree[i]->children);
        free(tree[i]);
    }
    free(tree);
}

Node** parse_otl(const char* content, int* tree_size) {
    *tree_size = 0;
    char* content_copy = strdup(content);
    char* line = strtok(content_copy, "\n");
    Node* root = create_node(-1, "root", "");
    Node** stack = (Node**)malloc(100 * sizeof(Node*)); // Arbitrary max depth
    int stack_size = 1;
    stack[0] = root;

    while (line) {
        if (strlen(line) == 0 || strspn(line, " \t") == strlen(line)) {
            line = strtok(NULL, "\n");
            continue;
        }
        int level = 0;
        while (line[level] == '\t') level++;
        char* trimmed = line + level;
        char* type = "headline";
        char* node_content = trimmed;
        char temp[1024];
        if (strncmp(trimmed, ": ", 2) == 0) {
            type = "body";
            node_content = trimmed + 2;
        } else if (strstr(trimmed, " _exe_ ")) {
            type = "executable";
        }
        strcpy(temp, node_content);
        Node* node = create_node(level, type, temp);
        while (stack[stack_size - 1]->level >= level) stack_size--;
        add_child(stack[stack_size - 1], node);
        stack[stack_size++] = node;
        line = strtok(NULL, "\n");
    }

    free(content_copy);
    free(stack);
    *tree_size = root->children_count;
    Node** tree = root->children;
    free(root->type);
    free(root->content);
    free(root);
    return tree;
}

void print_properties(Node** nodes, int size, const char* indent) {
    for (int i = 0; i < size; i++) {
        Node* node = nodes[i];
        printf("%sLevel: %d\n", indent, node->level);
        printf("%sType: %s\n", indent, node->type);
        printf("%sContent: %s\n", indent, node->content);
        printf("%sChildren Count: %d\n\n", indent, node->children_count);
        if (node->children_count > 0) {
            char new_indent[1024];
            snprintf(new_indent, sizeof(new_indent), "%s  ", indent);
            print_properties(node->children, node->children_count, new_indent);
        }
    }
}

void write_otl(FILE* fp, Node** nodes, int size, int level) {
    for (int i = 0; i < size; i++) {
        Node* node = nodes[i];
        for (int j = 0; j < level; j++) fprintf(fp, "\t");
        if (strcmp(node->type, "body") == 0) {
            fprintf(fp, ": %s\n", node->content);
        } else if (strcmp(node->type, "executable") == 0) {
            fprintf(fp, "%s\n", node->content);
        } else {
            fprintf(fp, "%s\n", node->content);
        }
        if (node->children_count > 0) {
            write_otl(fp, node->children, node->children_count, level + 1);
        }
    }
}

char* read_file(const char* filepath) {
    FILE* fp = fopen(filepath, "r");
    if (!fp) return NULL;
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    char* content = (char*)malloc(size + 1);
    fread(content, 1, size, fp);
    content[size] = '\0';
    fclose(fp);
    return content;
}

int main(int argc, char** argv) {
    if (argc < 2) {
        printf("Usage: %s <input.otl> [output.otl]\n", argv[0]);
        return 1;
    }
    char* content = read_file(argv[1]);
    if (!content) {
        printf("Failed to read file.\n");
        return 1;
    }
    int tree_size;
    Node** tree = parse_otl(content, &tree_size);
    free(content);

    print_properties(tree, tree_size, "");

    if (argc > 2) {
        FILE* out_fp = fopen(argv[2], "w");
        if (out_fp) {
            write_otl(out_fp, tree, tree_size, 0);
            fclose(out_fp);
        }
    }

    free_tree(tree, tree_size);
    return 0;
}