diff --git a/.travis.yml b/.travis.yml index 52f9524..e955644 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: elixir -elixir: '1.5.2' +elixir: '1.8.1' +otp_release: '21.3' script: - "mix test --trace" -- "mix dialyzer" \ No newline at end of file +- "mix dialyzer" diff --git a/lib/org/document.ex b/lib/org/document.ex index d695b30..0928627 100644 --- a/lib/org/document.ex +++ b/lib/org/document.ex @@ -102,6 +102,22 @@ defmodule Org.Document do %Org.Document{doc | sections: [Org.Section.prepend_content(current_section, content) | rest]} end + + @doc ~S""" + Prepend property to the currently deepest section. + + While preserving order is usually not needed for parsing and + interpreting properties, order is still preserved here to e.g. allow + re-serialization that preserves line order. This would be desirable + e.g. since version control is often based on lines, and works better + if there is less noise in the commit history. + + See prepend_content for usage. + """ + def prepend_property(%Org.Document{sections: [current_section | rest]} = doc, property) do + %Org.Document{doc | sections: [Org.Section.prepend_property(current_section, property) | rest]} + end + @doc ~S""" Update the last prepended content. Yields the content to the given updater. diff --git a/lib/org/lexer.ex b/lib/org/lexer.ex index 035638b..5abd894 100644 --- a/lib/org/lexer.ex +++ b/lib/org/lexer.ex @@ -11,7 +11,7 @@ defmodule Org.Lexer do @type t :: %Org.Lexer{ tokens: list(token), - mode: :normal | :raw + mode: :normal | :raw | :property } @moduledoc ~S""" @@ -58,6 +58,9 @@ defmodule Org.Lexer do @section_title_re ~r/^(\*+) (.+)$/ @empty_line_re ~r/^\s*$/ @table_row_re ~r/^\s*(?:\|[^|]*)+\|\s*$/ + @begin_props_re ~r/^\s*\:PROPERTIES\:$/ + @property_re ~r/^\s*\:([A-Za-z]+)\:\s*(.+)$/ + @end_drawer_re ~r/^\s*\:END\:$/ defp lex_line(line, %Org.Lexer{mode: :normal} = lexer) do cond do @@ -78,6 +81,8 @@ defmodule Org.Lexer do |> List.flatten |> Enum.map(&String.trim/1) append_token(lexer, {:table_row, cells}) + Regex.run(@begin_props_re, line) -> + append_token(lexer, {:begin_drawer, "PROPERTIES"}) |> set_mode(:property) true -> append_token(lexer, {:text, line}) end @@ -91,6 +96,16 @@ defmodule Org.Lexer do end end + defp lex_line(line, %Org.Lexer{mode: :property} = lexer) do + cond do + Regex.run(@end_drawer_re, line) -> + append_token(lexer, {:end_drawer}) |> set_mode(:normal) + match = Regex.run(@property_re, line) -> + [_, key, value] = match + append_token(lexer, {:property, key, value}) + end + end + defp append_token(%Org.Lexer{} = lexer, token) do %Org.Lexer{lexer | tokens: [token | lexer.tokens]} end diff --git a/lib/org/parser.ex b/lib/org/parser.ex index 77e5c49..fc92268 100644 --- a/lib/org/parser.ex +++ b/lib/org/parser.ex @@ -3,7 +3,7 @@ defmodule Org.Parser do @type t :: %Org.Parser{ doc: Org.Document.t, - mode: :paragraph | :table | :code_block | nil, + mode: :paragraph | :properties | :table | :code_block | nil, } @moduledoc ~S""" @@ -88,4 +88,18 @@ defmodule Org.Parser do defp parse_token({:end_src}, %Org.Parser{mode: :code_block} = parser) do %Org.Parser{parser | mode: nil} end + + defp parse_token({:begin_drawer, "PROPERTIES"}, parser) do + %Org.Parser{parser | mode: :properties} + end + + defp parse_token({:property, key, value}, %Org.Parser{mode: :properties} = parser) do + doc = Org.Document.prepend_property(parser.doc, {key |> String.to_atom(), value}) + + %Org.Parser{parser | doc: doc} + end + + defp parse_token({:end_drawer}, %Org.Parser{mode: :properties} = parser) do + %Org.Parser{parser | mode: nil} + end end diff --git a/lib/org/section.ex b/lib/org/section.ex index b23ccc9..d9d0009 100644 --- a/lib/org/section.ex +++ b/lib/org/section.ex @@ -1,11 +1,11 @@ defmodule Org.Section do - defstruct title: "", children: [], contents: [] + defstruct title: "", children: [], contents: [], properties: [] @moduledoc ~S""" Represents a section of a document with a title and possible contents & subsections. Example: - iex> source = "* Hello\nWorld\n** What's up?\nNothing much.\n** How's it going?\nAll fine, whow are you?\n" + 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" iex> doc = Org.Parser.parse(source) iex> section = Org.section(doc, ["Hello"]) iex> section.contents @@ -14,12 +14,16 @@ defmodule Org.Section do 2 iex> for child <- section.children, do: child.title ["What's up?", "How's it going?"] + iex> subsection_with_props = Org.section(doc, ["Hello", "What's up?"]) + iex> subsection_with_props.properties + [Register: "non-formal", Intent: "inquisitive"] """ @type t :: %Org.Section{ title: String.t, children: list(Org.Section.t), contents: list(Org.Content.t), + properties: list(Keyword.t), } def add_nested(parent, 1, child) do @@ -39,6 +43,7 @@ defmodule Org.Section do section | children: Enum.reverse(Enum.map(section.children, &reverse_recursive/1)), contents: Enum.reverse(Enum.map(section.contents, &Org.Content.reverse_recursive/1)), + properties: Enum.reverse(section.properties), } end @@ -82,4 +87,13 @@ defmodule Org.Section do def update_content(%Org.Section{children: [current_section | rest]} = section, updater) do %Org.Section{section | children: [update_content(current_section, updater) | rest]} end + + @doc "Adds property to the last prepended section" + def prepend_property(%Org.Section{children: []} = section, property) do + %Org.Section{section | properties: [property | section.properties]} + end + + def prepend_property(%Org.Section{children: [current_child | children]} = section, property) do + %Org.Section{section | children: [prepend_property(current_child, property) | children]} + end end diff --git a/mix.exs b/mix.exs index 08c1a38..46a14a6 100644 --- a/mix.exs +++ b/mix.exs @@ -35,8 +35,8 @@ defmodule Org.Mixfile do [ # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, - {:ex_doc, "~> 0.16", only: :dev, runtime: false}, - {:dialyxir, "~> 0.5.1"} + {:ex_doc, "~> 0.19", only: :dev, runtime: false}, + {:dialyxir, "~> 1.0.0-rc.4", only: :dev, runtime: false} ] end end diff --git a/mix.lock b/mix.lock index 854b31f..d96d095 100644 --- a/mix.lock +++ b/mix.lock @@ -1,3 +1,9 @@ -%{"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [], [], "hexpm"}, - "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}} +%{ + "dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, + "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"}, + "erlex": {:hex, :erlex, "0.2.1", "cee02918660807cbba9a7229cae9b42d1c6143b768c781fa6cee1eaf03ad860b", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, +} diff --git a/test/org/lexer_test.exs b/test/org/lexer_test.exs index 23176b2..6384d69 100644 --- a/test/org/lexer_test.exs +++ b/test/org/lexer_test.exs @@ -28,6 +28,13 @@ defmodule Org.LexerTest do {:section_title, 2, "another"}, {:text, "2"}, {:section_title, 3, "thing"}, + {:begin_drawer, "PROPERTIES"}, + {:property, "Title", "Goldberg Variations"}, + {:property, "Composer", "J.S. Bach"}, + {:property, "Artist", "Glenn Gould"}, + {:property, "Publisher", "Deutsche Grammophon"}, + {:property, "NDisks", "1"}, + {:end_drawer}, {:text, "3"}, {:section_title, 4, "is nesting"}, {:text, "4"}, diff --git a/test/org/parser_test.exs b/test/org/parser_test.exs index 95f97ae..af46e40 100644 --- a/test/org/parser_test.exs +++ b/test/org/parser_test.exs @@ -31,5 +31,15 @@ defmodule Org.ParserTest do %Org.CodeBlock{lang: "sql", details: "", lines: ["SELECT * FROM products;"]}, ] end + + test "section with properties", %{doc: doc} do + assert Org.section(doc, ["Also", "another", "thing"]).properties == [ + {:Title, "Goldberg Variations"}, + {:Composer, "J.S. Bach"}, + {:Artist, "Glenn Gould"}, + {:Publisher, "Deutsche Grammophon"}, + {:NDisks, "1"}, + ] + end end end diff --git a/test/org_test.exs b/test/org_test.exs index 0b3ae8a..fb7491a 100644 --- a/test/org_test.exs +++ b/test/org_test.exs @@ -20,6 +20,13 @@ defmodule OrgTest do ** another 2 *** thing + :PROPERTIES: + :Title: Goldberg Variations + :Composer: J.S. Bach + :Artist: Glenn Gould + :Publisher: Deutsche Grammophon + :NDisks: 1 + :END: 3 **** is nesting 4