[OCaml] Org mode parser.
1
(* Structural Menhir grammar for Org Mode documents.
2
Consumes tokens from the structural lexer and produces Raw_ast.document. *)
3
4
%{
5
open Raw_ast
6
7
let convert_list_item (item : Token.list_item_data) : raw_list_item =
8
{ rli_indent = item.Token.lid_indent;
9
rli_bullet = item.Token.lid_bullet;
10
rli_counter_set = item.Token.lid_counter_set;
11
rli_checkbox = item.Token.lid_checkbox;
12
rli_tag = None;
13
rli_body_lines = [item.Token.lid_rest];
14
}
15
16
let parse_planning_entries (line : string) : raw_planning list =
17
let results = ref [] in
18
let keywords = ["DEADLINE"; "SCHEDULED"; "CLOSED"] in
19
List.iter (fun kw ->
20
let pat = kw ^ ":" in
21
let len = String.length line in
22
let plen = String.length pat in
23
let rec find_from i =
24
if i + plen > len then ()
25
else if String.sub line i plen = pat then begin
26
let rest_start = i + plen in
27
let rest = String.trim (String.sub line rest_start (len - rest_start)) in
28
let ts_end =
29
let rec find_end j closer =
30
if j >= String.length rest then j
31
else if rest.[j] = closer then j + 1
32
else find_end (j + 1) closer
33
in
34
if String.length rest > 0 && rest.[0] = '<' then find_end 1 '>'
35
else if String.length rest > 0 && rest.[0] = '[' then find_end 1 ']'
36
else 0
37
in
38
if ts_end > 0 then
39
results := { rpl_keyword = kw; rpl_timestamp = String.sub rest 0 ts_end } :: !results
40
end
41
else find_from (i + 1)
42
in
43
find_from 0
44
) keywords;
45
List.rev !results
46
%}
47
48
%token BLANK
49
%token <int * string> HEADING
50
%token <string * string> KEYWORD
51
%token <string * string option * string> AFFILIATED_KEYWORD
52
%token <string * string option> BEGIN_BLOCK
53
%token <string> END_BLOCK
54
%token <string> DRAWER_BEGIN
55
%token DRAWER_END
56
%token <string * string option> PROPERTY
57
%token <string> PLANNING
58
%token <Token.list_item_data> LIST_ITEM
59
%token <string> TABLE_ROW
60
%token <string> FIXED_WIDTH
61
%token <string> COMMENT_LINE
62
%token HORIZONTAL_RULE
63
%token <string> TEXT_LINE
64
%token EOF
65
66
%start <Raw_ast.document> document
67
68
%%
69
70
document:
71
| preamble = list(element) headings = list(heading) EOF
72
{ { rd_preamble = preamble; rd_headings = headings } }
73
;
74
75
heading:
76
| h = HEADING body = list(element)
77
{ let (level, raw_title) = h in
78
{ rh_level = level; rh_raw_title = raw_title; rh_body = body } }
79
;
80
81
element:
82
| BLANK
83
{ Raw_paragraph [] }
84
| lines = nonempty_list(TEXT_LINE)
85
{ Raw_paragraph lines }
86
| items = nonempty_list(list_item_tok)
87
{ Raw_list_items (List.map convert_list_item items) }
88
| rows = nonempty_list(table_row_tok)
89
{ Raw_table rows }
90
| b = block
91
{ Raw_block b }
92
| d = drawer
93
{ Raw_drawer d }
94
| k = KEYWORD
95
{ let (key, value) = k in Raw_keyword (key, value) }
96
| a = AFFILIATED_KEYWORD
97
{ let (name, opt, value) = a in Raw_affiliated (name, opt, value) }
98
| comments = nonempty_list(COMMENT_LINE)
99
{ Raw_comment comments }
100
| lines = nonempty_list(FIXED_WIDTH)
101
{ Raw_fixed_width lines }
102
| HORIZONTAL_RULE
103
{ Raw_horizontal_rule }
104
| p = PLANNING
105
{ Raw_planning (parse_planning_entries p) }
106
;
107
108
list_item_tok:
109
| item = LIST_ITEM { item }
110
;
111
112
table_row_tok:
113
| row = TABLE_ROW
114
{ if String.length row > 1 && row.[1] = '-' then Raw_table_rule
115
else Raw_table_standard row }
116
;
117
118
block:
119
| b = BEGIN_BLOCK body = list(block_body_line) e = END_BLOCK
120
{ let (name, params) = b in let _ = e in
121
{ rb_name = name; rb_params = params; rb_body = Opaque_body (String.concat "\n" body) } }
122
| b = BEGIN_BLOCK body = list(block_body_line) EOF
123
{ let (name, params) = b in
124
{ rb_name = name; rb_params = params; rb_body = Opaque_body (String.concat "\n" body) } }
125
;
126
127
block_body_line:
128
| t = TEXT_LINE { t }
129
| BLANK { "" }
130
| k = KEYWORD { let (key, value) = k in "#+" ^ key ^ ": " ^ value }
131
| a = AFFILIATED_KEYWORD { let (n, _, v) = a in "#+" ^ n ^ ": " ^ v }
132
| c = COMMENT_LINE { "# " ^ c }
133
| f = FIXED_WIDTH { ": " ^ f }
134
| HORIZONTAL_RULE { "-----" }
135
| item = LIST_ITEM { item.Token.lid_rest }
136
| row = TABLE_ROW { row }
137
| p = PLANNING { p }
138
| prop = PROPERTY { let (n, v) = prop in ":" ^ n ^ ": " ^ (Option.value ~default:"" v) }
139
| d = DRAWER_BEGIN { ":" ^ d ^ ":" }
140
| DRAWER_END { ":END:" }
141
;
142
143
(* A drawer handles both property drawers and regular drawers.
144
Property drawers are distinguished during normalization by their name. *)
145
drawer:
146
| name = DRAWER_BEGIN contents = drawer_contents DRAWER_END
147
{ { rd_name = name; rd_contents = contents } }
148
| name = DRAWER_BEGIN contents = drawer_contents EOF
149
{ { rd_name = name; rd_contents = contents } }
150
;
151
152
(* Drawer contents can include properties (for property drawers)
153
or regular elements (for other drawers). We parse them uniformly
154
as elements, with properties becoming text that normalization handles. *)
155
drawer_contents:
156
| items = list(drawer_item)
157
{ items }
158
;
159
160
drawer_item:
161
| BLANK { Raw_paragraph [] }
162
| lines = nonempty_list(TEXT_LINE) { Raw_paragraph lines }
163
| p = PROPERTY
164
{ let (name, value) = p in Raw_keyword (name, Option.value ~default:"" value) }
165
| k = KEYWORD { let (key, value) = k in Raw_keyword (key, value) }
166
| a = AFFILIATED_KEYWORD { let (n, _, v) = a in Raw_affiliated (n, None, v) }
167
| items = nonempty_list(list_item_tok)
168
{ Raw_list_items (List.map convert_list_item items) }
169
| rows = nonempty_list(table_row_tok) { Raw_table rows }
170
| comments = nonempty_list(COMMENT_LINE) { Raw_comment comments }
171
| lines = nonempty_list(FIXED_WIDTH) { Raw_fixed_width lines }
172
| HORIZONTAL_RULE { Raw_horizontal_rule }
173
| pl = PLANNING { Raw_planning (parse_planning_entries pl) }
174
;
175