defmodule Naiveical.Modificator do
@moduledoc """
Allows creation and modifications of an icalendar file.
"""
alias Naiveical.Helpers
alias Naiveical.Extractor
@datetime_format_str "{YYYY}{0M}{0D}T{h24}{m}Z"
defp remove_carrier_returns(txt), do: String.replace(txt, "\r\n", "\n")
defp add_carrier_returns(txt), do: String.replace(txt, ~r/\r?\n/, "\r\n")
defp update_line(tag, new_value, new_properties) do
if String.contains?(new_value, tag) do
# we replace a raw value
new_value
else
if String.length(new_value) > 0 do
if String.length(new_properties) > 0 do
"#{tag};#{new_properties}:#{new_value}"
else
"#{tag}:#{new_value}"
end
else
""
end
end
end
def change_value_txt(ical_text, "", new_value, new_properties), do: ical_text
def change_value_txt(ical_text, tag, new_value, new_properties) do
{start_idx, str_len, tag} =
if String.contains?(ical_text, tag) do
{:ok, regex} = Regex.compile("^#{tag}[;]?.*:.*$", [:multiline])
[{start_idx, str_len}] =
Regex.run(regex, remove_carrier_returns(ical_text), return: :index)
{start_idx, str_len, tag}
else
{:ok, regex} = Regex.compile("^BEGIN:.*$", [:caseless, :multiline])
[{start_idx, str_len}] =
Regex.run(regex, remove_carrier_returns(ical_text), return: :index)
{start_idx + str_len, 0, "\n" <> tag}
end
ics_before = String.slice(ical_text, 0, start_idx)
ics_after = String.slice(ical_text, start_idx + str_len, String.length(ical_text))
new_line = update_line(tag, new_value, new_properties)
new_line = Helpers.fold(new_line)
(ics_before <> "#{new_line}" <> ics_after)
|> add_carrier_returns
end
def change_value_txt(ical_text, tag, new_value) do
{tag, properties, values} = Extractor.extract_contentline_by_tag(ical_text, tag)
change_value_txt(ical_text, tag, new_value, "")
end
def change_value(ical_text, tag, new_value) when is_binary(new_value) do
change_value_txt(ical_text, tag, new_value)
end
def change_value(ical_text, tag, new_value) when is_nil(new_value) do
if String.contains?(ical_text, tag) do
change_value_txt(ical_text, tag, new_value)
else
ical_text
end
end
def change_value(
ical_text,
tag,
%DateTime{
year: year,
month: month,
day: day,
zone_abbr: zone_abbr,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
utc_offset: utc_offset,
std_offset: std_offset,
time_zone: time_zone
} = datetime
) do
change_value_txt(ical_text, tag, Timex.format(datetime, "{ISO:Basic:Z}"))
end
@doc """
Change a number of values in the ical_text.
"""
def change_values(ical_text, tag_values) do
Enum.reduce(tag_values, ical_text, fn {key, value}, acc ->
change_value(acc, to_string(key), value)
end)
end
@doc """
Inserts another element (or any text) into the ical_text just before the ending of the element.
"""
def insert_into(ical_text, new_content, element, opts \\ []) do
# normalize new element, add newlines if needed
new_content =
if String.match?(new_content, ~r/.*\r?\n/) do
new_content
else
new_content <> "\r\n"
end
|> String.replace(~r/\r?\n/, "\r\n")
if String.contains?(ical_text, "END:#{element}") do
{:ok, regex} =
if opts[:at] == :beginning do
Regex.compile("BEGIN:#{element}")
else
Regex.compile("END:#{element}")
end
[{start_idx, str_len}] =
Regex.run(regex, String.replace(ical_text, "\r\n", "\n"), return: :index)
{ics_before, ics_after} =
if opts[:at] == :beginning do
{String.slice(ical_text, 0, start_idx + str_len + 1),
String.slice(ical_text, start_idx + str_len + 1, String.length(ical_text))}
else
{String.slice(ical_text, 0, start_idx),
String.slice(ical_text, start_idx, String.length(ical_text))}
end
{:ok,
(ics_before <> "#{new_content}" <> ics_after)
|> String.replace(~r/\r?\n/, "\r\n")}
else
{:error, "There is no ending of element #{element}"}
end
end
@doc """
Remove all elements of a specific type.
"""
def delete_all(ical_text, tag) do
if String.contains?(ical_text, "END:#{tag}") do
ical_text = String.replace(ical_text, "\r\n", "\n")
{:ok, regex_begin} = Regex.compile("BEGIN:#{tag}", [:multiline, :ungreedy])
{:ok, regex_end} = Regex.compile("END:#{tag}", [:multiline, :ungreedy])
begins = Regex.scan(regex_begin, ical_text, return: :index)
ends = Regex.scan(regex_end, ical_text, return: :index)
if length(begins) == length(ends) do
[{first_begin_start, first_begin_length}] = Enum.at(begins, 0)
[{last_end_start, last_end_length}] = Enum.at(ends, -1)
last_part =
String.slice(
ical_text,
last_end_start + last_end_length,
String.length(ical_text) - last_end_start + last_end_length
)
new_ical =
Enum.reduce(
0..(length(begins) - 2),
String.slice(ical_text, 0, first_begin_start - 1),
fn i, acc ->
[{end_start, end_length}] = Enum.at(ends, i)
[{begin_start, begin_length}] = Enum.at(begins, i + 1)
start_idx = end_start + end_length
str_len = begin_start - (end_start + end_length) - 1
acc <> String.slice(ical_text, start_idx, str_len)
end
) <>
last_part
{:ok, String.replace(new_ical, ~r/\r?\n/, "\r\n")}
else
{:error, "BEGIN/END do not match"}
end
else
{:ok, ical_text}
end
end
def delete_all!(ical_text, tag) do
case delete_all(ical_text, tag) do
{:ok, res} -> res
{:error, reason} -> raise(reason)
end
end
def add_timezone_info(ical_text) do
# find all timezone informations
timezones = Regex.scan(~r/TZID=(.*):/, ical_text) |> Enum.uniq()
# collect all timezone info
timezones =
Enum.reduce(timezones, "", fn [_, tzid], acc ->
tz = File.read!("priv/zoneinfo/#{tzid}.ics")
tz =
Naiveical.Extractor.extract_sections_by_tag(tz, "VTIMEZONE")
|> Enum.at(0)
if String.length(acc) == 0 do
tz
else
acc <> "\r\n" <> tz
end
end)
insert_into(ical_text, timezones, "VCALENDAR", at: :beginning)
end
end