Skip to content

Commit 1880a37

Browse files
committed
Add basic support for properties
See [1]. Does not support property inheritance. Does not support allowed values. Does not prevent clashes between two keys of different case, e.g. "key", and "Key". Does not prevent use of special keys. Keys can only consist of letters a-z and A-Z. [1] https://orgmode.org/org.html#Properties-and-Columns
1 parent 4274ea5 commit 1880a37

File tree

7 files changed

+87
-4
lines changed

7 files changed

+87
-4
lines changed

lib/org/document.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,22 @@ defmodule Org.Document do
102102
%Org.Document{doc | sections: [Org.Section.prepend_content(current_section, content) | rest]}
103103
end
104104

105+
106+
@doc ~S"""
107+
Prepend property to the currently deepest section.
108+
109+
While preserving order is usually not needed for parsing and
110+
interpreting properties, order is still preserved here to e.g. allow
111+
re-serialization that preserves line order. This would be desirable
112+
e.g. since version control is often based on lines, and works better
113+
if there is less noise in the commit history.
114+
115+
See prepend_content for usage.
116+
"""
117+
def prepend_property(%Org.Document{sections: [current_section | rest]} = doc, property) do
118+
%Org.Document{doc | sections: [Org.Section.prepend_property(current_section, property) | rest]}
119+
end
120+
105121
@doc ~S"""
106122
Update the last prepended content. Yields the content to the given updater.
107123

lib/org/lexer.ex

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ defmodule Org.Lexer do
1111

1212
@type t :: %Org.Lexer{
1313
tokens: list(token),
14-
mode: :normal | :raw
14+
mode: :normal | :raw | :property
1515
}
1616

1717
@moduledoc ~S"""
@@ -58,6 +58,9 @@ defmodule Org.Lexer do
5858
@section_title_re ~r/^(\*+) (.+)$/
5959
@empty_line_re ~r/^\s*$/
6060
@table_row_re ~r/^\s*(?:\|[^|]*)+\|\s*$/
61+
@begin_props_re ~r/^\s*\:PROPERTIES\:$/
62+
@property_re ~r/^\s*\:([A-Za-z]+)\:\s*(.+)$/
63+
@end_drawer_re ~r/^\s*\:END\:$/
6164

6265
defp lex_line(line, %Org.Lexer{mode: :normal} = lexer) do
6366
cond do
@@ -78,6 +81,8 @@ defmodule Org.Lexer do
7881
|> List.flatten
7982
|> Enum.map(&String.trim/1)
8083
append_token(lexer, {:table_row, cells})
84+
Regex.run(@begin_props_re, line) ->
85+
append_token(lexer, {:begin_drawer, "PROPERTIES"}) |> set_mode(:property)
8186
true ->
8287
append_token(lexer, {:text, line})
8388
end
@@ -91,6 +96,16 @@ defmodule Org.Lexer do
9196
end
9297
end
9398

99+
defp lex_line(line, %Org.Lexer{mode: :property} = lexer) do
100+
cond do
101+
Regex.run(@end_drawer_re, line) ->
102+
append_token(lexer, {:end_drawer}) |> set_mode(:normal)
103+
match = Regex.run(@property_re, line) ->
104+
[_, key, value] = match
105+
append_token(lexer, {:property, key, value})
106+
end
107+
end
108+
94109
defp append_token(%Org.Lexer{} = lexer, token) do
95110
%Org.Lexer{lexer | tokens: [token | lexer.tokens]}
96111
end

lib/org/parser.ex

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule Org.Parser do
33

44
@type t :: %Org.Parser{
55
doc: Org.Document.t,
6-
mode: :paragraph | :table | :code_block | nil,
6+
mode: :paragraph | :properties | :table | :code_block | nil,
77
}
88

99
@moduledoc ~S"""
@@ -88,4 +88,18 @@ defmodule Org.Parser do
8888
defp parse_token({:end_src}, %Org.Parser{mode: :code_block} = parser) do
8989
%Org.Parser{parser | mode: nil}
9090
end
91+
92+
defp parse_token({:begin_drawer, "PROPERTIES"}, parser) do
93+
%Org.Parser{parser | mode: :properties}
94+
end
95+
96+
defp parse_token({:property, key, value}, %Org.Parser{mode: :properties} = parser) do
97+
doc = Org.Document.prepend_property(parser.doc, {key |> String.to_atom(), value})
98+
99+
%Org.Parser{parser | doc: doc}
100+
end
101+
102+
defp parse_token({:end_drawer}, %Org.Parser{mode: :properties} = parser) do
103+
%Org.Parser{parser | mode: nil}
104+
end
91105
end

lib/org/section.ex

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
defmodule Org.Section do
2-
defstruct title: "", children: [], contents: []
2+
defstruct title: "", children: [], contents: [], properties: []
33

44
@moduledoc ~S"""
55
Represents a section of a document with a title and possible contents & subsections.
66
77
Example:
8-
iex> source = "* Hello\nWorld\n** What's up?\nNothing much.\n** How's it going?\nAll fine, whow are you?\n"
8+
iex> source = "* Hello\nWorld\n** What's up?\n :PROPERTIES:\n :Register: non-formal\n :Intent: inquisitive\n :END:\nNothing much.\n** How's it going?\nAll fine, whow are you?\n"
99
iex> doc = Org.Parser.parse(source)
1010
iex> section = Org.section(doc, ["Hello"])
1111
iex> section.contents
@@ -14,12 +14,16 @@ defmodule Org.Section do
1414
2
1515
iex> for child <- section.children, do: child.title
1616
["What's up?", "How's it going?"]
17+
iex> subsection_with_props = Org.section(doc, ["Hello", "What's up?"])
18+
iex> subsection_with_props.properties
19+
[Register: "non-formal", Intent: "inquisitive"]
1720
"""
1821

1922
@type t :: %Org.Section{
2023
title: String.t,
2124
children: list(Org.Section.t),
2225
contents: list(Org.Content.t),
26+
properties: list(Keyword.t),
2327
}
2428

2529
def add_nested(parent, 1, child) do
@@ -39,6 +43,7 @@ defmodule Org.Section do
3943
section |
4044
children: Enum.reverse(Enum.map(section.children, &reverse_recursive/1)),
4145
contents: Enum.reverse(Enum.map(section.contents, &Org.Content.reverse_recursive/1)),
46+
properties: Enum.reverse(section.properties),
4247
}
4348
end
4449

@@ -82,4 +87,13 @@ defmodule Org.Section do
8287
def update_content(%Org.Section{children: [current_section | rest]} = section, updater) do
8388
%Org.Section{section | children: [update_content(current_section, updater) | rest]}
8489
end
90+
91+
@doc "Adds property to the last prepended section"
92+
def prepend_property(%Org.Section{children: []} = section, property) do
93+
%Org.Section{section | properties: [property | section.properties]}
94+
end
95+
96+
def prepend_property(%Org.Section{children: [current_child | children]} = section, property) do
97+
%Org.Section{section | children: [prepend_property(current_child, property) | children]}
98+
end
8599
end

test/org/lexer_test.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ defmodule Org.LexerTest do
2828
{:section_title, 2, "another"},
2929
{:text, "2"},
3030
{:section_title, 3, "thing"},
31+
{:begin_drawer, "PROPERTIES"},
32+
{:property, "Title", "Goldberg Variations"},
33+
{:property, "Composer", "J.S. Bach"},
34+
{:property, "Artist", "Glenn Gould"},
35+
{:property, "Publisher", "Deutsche Grammophon"},
36+
{:property, "NDisks", "1"},
37+
{:end_drawer},
3138
{:text, "3"},
3239
{:section_title, 4, "is nesting"},
3340
{:text, "4"},

test/org/parser_test.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,15 @@ defmodule Org.ParserTest do
3131
%Org.CodeBlock{lang: "sql", details: "", lines: ["SELECT * FROM products;"]},
3232
]
3333
end
34+
35+
test "section with properties", %{doc: doc} do
36+
assert Org.section(doc, ["Also", "another", "thing"]).properties == [
37+
{:Title, "Goldberg Variations"},
38+
{:Composer, "J.S. Bach"},
39+
{:Artist, "Glenn Gould"},
40+
{:Publisher, "Deutsche Grammophon"},
41+
{:NDisks, "1"},
42+
]
43+
end
3444
end
3545
end

test/org_test.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ defmodule OrgTest do
2020
** another
2121
2
2222
*** thing
23+
:PROPERTIES:
24+
:Title: Goldberg Variations
25+
:Composer: J.S. Bach
26+
:Artist: Glenn Gould
27+
:Publisher: Deutsche Grammophon
28+
:NDisks: 1
29+
:END:
2330
3
2431
**** is nesting
2532
4

0 commit comments

Comments
 (0)