Current section

Files

Jump to
microformats2 lib items dt_prop.ex
Raw

lib/items/dt_prop.ex

defmodule Microformats2.Items.DtProp do
@moduledoc false
import Microformats2.Helpers
alias Microformats2.Helpers.DateTimeNormalizer
alias Microformats2.ParserState
defp parse_infos_from_state(state) do
%{
date_parts: state.dates,
implied_timezone: state.implied_timezone,
value: nil,
date: nil,
time: nil,
zone: nil,
timezone_offset: nil
}
end
def parsed_prop(child = {elem, _, _}, state) do
dt = Floki.attribute([child], "datetime") |> List.first()
title = Floki.attribute([child], "title") |> List.first()
value = Floki.attribute([child], "value") |> List.first()
parse_state = parse_value_class([child], parse_infos_from_state(state))
{value, timezone_offset, dates} =
if present?(parse_state) do
{parse_state[:value], parse_state[:implied_timezone], parse_state[:date_parts]}
else
value =
cond do
elem in ["time", "ins", "del"] && present?(dt) -> dt
elem == "abbr" && !is_nil(title) -> title
elem in ["data", "input"] && !is_nil(value) -> value
true -> [child] |> cleanup_html() |> text_content()
end
|> stripped_or_nil()
timezone =
if !Regex.match?(~r/^(\d{4}-\d{2}-\d{2})$/, value) do
data = (Regex.run(~r/Z|[+-]\d{1,2}:?(\d{2})?$/i, value) || []) |> List.first()
if blank?(state.implied_timezone) && present?(data),
do: data,
else: state.implied_timezone
end
data = (Regex.run(~r/(\d{4}-\d{2}-\d{2})/, value) || []) |> List.first()
if data,
do: {value, timezone, [data | state.dates]},
else: {value, timezone, state.dates}
end
fixed_value =
if (value =~ ~r/^\d{1,2}:\d{2}(:\d{2})?(Z|[+-]\d{2}:?\d{2}?)?$/ ||
value =~ ~r/^\d{1,2}(:\d{2})?(:\d{2})?[ap]\.?m\.?$/i) && present?(dates) do
{time, _offset} = DateTimeNormalizer.normalized_time_zone_offset(value)
time = DateTimeNormalizer.normalized_time_format(time)
[date | _] = dates
"#{date} #{stripped_or_nil(time)}"
else
value
end
{fixed_value, %ParserState{state | implied_timezone: timezone_offset, dates: dates}}
end
defp parse_value_class(node, parse_infos) do
nodes = Floki.find(node, ">[class~=value], >[class~=value-title]")
if present?(nodes) do
nodes
|> Enum.map(&value_for_node/1)
|> parse_date_values(parse_infos)
end
end
defp value_for_node({elem, _, _} = node) do
alt = Floki.attribute([node], "alt") |> List.first()
cond do
Enum.member?(attr_list([node], "class"), "value-title") ->
Floki.attribute([node], "title") |> List.first()
elem in ~w[img area] && !is_nil(alt) ->
alt
elem == "data" ->
value = Floki.attribute([node], "value") |> List.first()
if is_nil(value),
do: [node] |> Floki.text() |> stripped_or_nil(),
else: value
elem == "abbr" ->
title = Floki.attribute([node], "title") |> List.first()
if is_nil(title),
do: [node] |> Floki.text() |> stripped_or_nil(),
else: title
elem in ~w[del ins time] ->
datetime = Floki.attribute([node], "datetime") |> List.first()
if is_nil(datetime),
do: [node] |> Floki.text() |> stripped_or_nil(),
else: datetime
true ->
[node] |> Floki.text() |> stripped_or_nil()
end
end
defp parse_date_values(parts, parse_infos)
defp parse_date_values([], parse_info), do: parse_info
defp parse_date_values([nil | rest], parse_infos),
do: parse_date_values(rest, parse_infos)
defp parse_date_values([part | rest], parse_infos) do
updated_parse_infos =
cond do
# we found the date/time value, return it and be done with this
part =~ ~r/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?(Z|[+-]\d{2}:?\d{2})?$/ ->
part
# we found a time value, possibly with time zone information
part =~ ~r/^\d{1,2}:\d{2}(:\d{2})?(Z|[+-]\d{1,2}:?\d{2})?$/ ||
(part =~ ~r/^\d{1,2}(:\d{2})?(:\d{2})?[ap]\.?m\.?$/i && blank?(parse_infos[:time])) ->
{time, offset} = DateTimeNormalizer.normalized_time_zone_offset(part)
parse_infos
|> Map.merge(%{time: time, timezone_offset: offset})
|> maybe_put_implied_timezone(offset)
# we found a valid date and no other date has been found
part =~ ~r/^\d{4}-\d{2}-\d{2}$/ && blank?(parse_infos[:date]) ->
Map.put(parse_infos, :date, part)
# we found a ordinal date and no other date has been found
part =~ ~r/^\d{4}-\d{3}$/ && blank?(parse_infos[:date]) ->
Map.put(parse_infos, :date, DateTimeNormalizer.normalized_ordinal_date(part))
# we found a valid time zone and no other zone has been found
part =~ ~r/^(Z|[+-]\d{1,2}:?(\d{2})?)$/ && blank?(parse_infos[:zone]) ->
{_, offset} = DateTimeNormalizer.normalized_time_zone_offset(part)
parse_infos
|> Map.merge(%{zone: part, timezone_offset: offset})
|> maybe_put_implied_timezone(offset)
# nothing valid found, no state change
true ->
{:next, parse_infos}
end
|> maybe_save_date_part()
|> maybe_fix_time_part()
|> maybe_save_date_or_time()
if is_binary(updated_parse_infos) do
date = String.replace(updated_parse_infos, ~r/[T ].*$/, "")
parse_infos
|> Map.put(:date, updated_parse_infos)
|> Map.update!(:date_parts, &[date | &1])
else
parse_date_values(rest, updated_parse_infos)
end
end
defp maybe_save_date_or_time(str) when is_binary(str), do: str
defp maybe_save_date_or_time({:next, parse_infos}), do: {:next, parse_infos}
defp maybe_save_date_or_time(parse_infos) do
cond do
blank?(parse_infos[:date]) && present?(parse_infos[:time]) ->
time = DateTimeNormalizer.normalized_time_format(parse_infos[:time])
Map.merge(parse_infos, %{value: stripped_or_nil(time), time: time})
present?(parse_infos[:date]) && blank?(parse_infos[:time]) ->
Map.put(parse_infos, :value, String.trim_trailing(parse_infos[:date], "T"))
true ->
time = DateTimeNormalizer.normalized_time_format(parse_infos[:time])
date = String.trim_trailing(parse_infos[:date], "T")
value = "#{date} #{stripped_or_nil(time)}"
Map.merge(parse_infos, %{value: value, time: time})
end
end
defp maybe_save_date_part(str) when is_binary(str), do: str
defp maybe_save_date_part({:next, parse_infos}), do: {:next, parse_infos}
defp maybe_save_date_part(parse_infos) do
if present?(parse_infos[:date]) && !Enum.member?(parse_infos[:date_parts], parse_infos[:date]),
do: Map.update!(parse_infos, :date_parts, &[parse_infos[:date] | &1]),
else: parse_infos
end
defp maybe_fix_time_part(str) when is_binary(str), do: str
defp maybe_fix_time_part({:next, parse_infos}), do: {:next, parse_infos}
defp maybe_fix_time_part(parse_infos) do
if present?(parse_infos[:zone]) && present?(parse_infos[:time]),
do: Map.put(parse_infos, :time, "#{parse_infos[:time]}#{parse_infos[:zone]}"),
else: parse_infos
end
defp maybe_put_implied_timezone(map, value) do
if blank?(map[:implied_timezone]) && present?(value),
do: Map.put(map, :implied_timezone, value),
else: map
end
end