An OCaml library and CLI tool for parsing Org-mode documents into a typed AST and rendering them to HTML.
- Headings with TODO/DONE keyword support
- Inline markup:
*bold*,/italic/,~code~, ===verbatim=== - Links:
[[url][description]]and[[url]](image URLs render as<img>) - Image support: links to
.png,.jpg,.svg, etc. auto-render as images - Lists: ordered and unordered
- Source blocks with language annotation
- Example blocks
- Quote blocks
- Tables with optional header rows
- Horizontal rules
- Directives:
#+TITLE,#+AUTHOR,#+DATE, etc. parsed intodocument.properties
Build the CLI:
nix build .#orgcaml-bin
./result/bin/orgcaml-bin document.orgEnter a dev shell:
nix develop
dune build
dune runtestopam install angstrom
dune build
dune runtestConvert an Org file to an HTML fragment:
orgcaml-bin document.orgGenerate a full HTML page with <html>, <head>, and <body> tags:
orgcaml-bin --full-page document.org > output.htmlAdd orgcaml to your dune dependencies:
(executable
(name my_app)
(libraries orgcaml))Then parse and render:
let () =
let input = {|
* Hello World
This is *bold* and /italic/ text with a [[https://example.com][link]].
- First item
- Second item
print("hello")Directives like #+TITLE: are parsed into document.properties:
let () =
let doc = Orgcaml.Parser.parse "#+TITLE: My Doc\n#+DATE: 2026-04-22\n* Hello" in
List.iter (fun (key, value) ->
Printf.printf "%s = %s\n" key value
) doc.properties
(* prints: TITLE = My Doc, DATE = 2026-04-22 *)Orgcaml.Parser.languages returns all languages used in #+BEGIN_SRC blocks:
let () =
let doc = Orgcaml.Parser.parse input in
let langs = Orgcaml.Parser.languages doc in
List.iter print_endline langsYou can also work with the parsed AST directly for custom processing:
let () =
let doc = Orgcaml.Parser.parse "* TODO My heading\n\nA paragraph." in
List.iter (function
| Orgcaml.Ast.Heading { level; keyword; title } ->
Printf.printf "H%d (kw=%s): %d inline elements\n"
level
(Option.value ~default:"none" keyword)
(List.length title)
| Orgcaml.Ast.Paragraph _ -> print_endline "Paragraph"
| _ -> ()
) doc.contenttype inline =
| Text of string
| Bold of inline list
| Italic of inline list
| Code of string
| Verbatim of string
| Link of { url : string; desc : inline list option }
type block =
| Heading of { level : int; keyword : string option; title : inline list }
| Paragraph of inline list
| List of list_kind * list_item list
| Src_block of { language : string option; contents : string }
| Example_block of string
| Quote_block of block list
| Table of { header : inline list list option; rows : inline list list list }
| Horizontal_rule
type document = { properties : (string * string) list; content : block list }| Output | Description |
|---|---|
packages.orgcaml | Library package (SDK) |
packages.orgcaml-bin | CLI binary |
packages.default | Alias for orgcaml-bin |
devShells.default | Dev shell with OCaml 5.3, dune, lsp, utop, odoc |
checks.default | Runs the test suite |
orgcaml/ ├── lib/ │ ├── ast.ml # Document AST types │ ├── inline_parser.ml # Angstrom-based inline markup parser │ ├── block_parser.ml # Line-classification block parser │ ├── parser.ml # Top-level parse entry point │ ├── html.ml # AST → HTML renderer │ └── orgcaml.ml # Public module interface ├── examples/ │ ├── org_to_html.ml # Org → full HTML page example │ └── ast_walk.ml # Parse and inspect the AST ├── bin/ │ └── main.ml # CLI entry point ├── test/ │ └── test_orgcaml.ml # Test suite ├── dune-project ├── flake.nix ├── orgcaml.opam # Generated — library package └── orgcaml-bin.opam # Generated — CLI package
MIT