From 0f31ea8b0cb75c9f0f649e936741885c26deb18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Fankh=C3=A4nel?= <8aviav@gmail.com> Date: Sun, 17 Mar 2019 14:57:51 +0100 Subject: [PATCH 1/3] Use Elixir 1.8.1 and OTP 21.3 in Travis CI --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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" From 4274ea57a190dbec3d71d6f4a2c7f0157fce4a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Fankh=C3=A4nel?= <8aviav@gmail.com> Date: Sun, 17 Mar 2019 14:58:12 +0100 Subject: [PATCH 2/3] Update Dialyxir and Ex_Doc --- mix.exs | 4 ++-- mix.lock | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) 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"}, +} From 1880a37631f9e02234864c05b8c5d16748239ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Fankh=C3=A4nel?= <8aviav@gmail.com> Date: Sun, 17 Mar 2019 11:38:54 +0100 Subject: [PATCH 3/3] 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 --- lib/org/document.ex | 16 ++++++++++++++++ lib/org/lexer.ex | 17 ++++++++++++++++- lib/org/parser.ex | 16 +++++++++++++++- lib/org/section.ex | 18 ++++++++++++++++-- test/org/lexer_test.exs | 7 +++++++ test/org/parser_test.exs | 10 ++++++++++ test/org_test.exs | 7 +++++++ 7 files changed, 87 insertions(+), 4 deletions(-) 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/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