An RSS/Atom feed reader utility for Rainmeter desktop widgets.
function Initialize()
-- CREATE MAIN TABLES
Feeds = {}
-- "Feeds" is the master database of all feeds' items
-- and properties.
Display = {}
-- "Display" is a dynamic table containing items from
-- one or more feeds, sorted by user preference.
Feedback = {}
-- "Feedback" is a chronology of success, error and
-- debug messages from the current session.
-- SET STARTING FEED
F = F or tonumber(SKIN:GetVariable('CurrentFeed', 1))
-- Capital "F" always refers to the global "real" feed
-- number. Lower-case "f" refers to the local "working"
-- feed number within a specific function.
-- GET MEASURE NAMES
local AllMeasureNames = SELF:GetOption('MeasureName', '')
for MeasureName in AllMeasureNames:gmatch('[^%|]+') do
table.insert(Feeds, {
Measure = SKIN:GetMeasure(MeasureName),
MeasureName = MeasureName
})
end
-- SET UPDATE DIVIDER
Script = SELF:GetName()
local UpdateDivider = SELF:GetNumberOption('UpdateDivider')
if UpdateDivider == 0 then
SKIN:Bang('!SetOption', Script, 'UpdateDivider', 60)
end
-- Since frequent updating is not necessary with this
-- script, it defaults to an update divider of 60
-- (once per minute) unless explicitly set.
-- SET UP MODULES
EventFile_Initialize()
HistoryFile_Initialize()
end
-----------------------------------------------------------------------
-- INPUT
function Input(f, EventType)
local InputTime = os.time()
local Feed = Feeds[f]
-- GET OPTIONS
local KeepOldItems = SELF:GetNumberOption('KeepOldItems', 0)
local MaxKeepItems = SELF:GetNumberOption('MaxKeepItems', 0)
local LimitNew = SELF:GetNumberOption('LimitNew', 5) * 60
local LimitUnread = SELF:GetNumberOption('LimitUnread', 0) * 60
-- CHECK FOR CONTENT
if (not Feed.Raw) or (Feed.Raw == '') then
Feed.Error = {
Description = 'Waiting for data from WebParser.',
Title = 'Loading...',
Link = 'http://enigma.kaelri.com/support'
}
return
end
-- RE-PARSE CONTENT
if EventType == 'Refresh' then
-- RESOLVE CDATA
if Feed.Raw:match('<!%[CDATA%[') then
Feed.Raw = Feed.Raw:gsub('<!%[CDATA%[(.-)%]%]>', EncodeCharacterReference)
end
-- DETERMINE FEED FORMAT AND CONTENTS
local t = IdentifyType(Feed.Raw)
if t then
Feed.Type = t
else
Feed.Error = {
Description = 'Could not identify a valid feed format.',
Title = 'Invalid Feed Format',
Link = 'http://enigma.kaelri.com/support'
}
SendFeedback(('Error parsing #%d (from %s). %s'):format(f, Feed.MeasureName, Feed.Error.Description))
return
end
local Type = Types[t]
-- GET NEW DATA
Feed.Title = IdentifyTag(Feed.Raw, {'<title.->(.-)</title>'})
Feed.Link = IdentifyTag(Feed.Raw, Type.Link)
local NewItems = {}
for RawItem in Feed.Raw:gmatch(Type.Item) do
local Item = {}
-- MATCH RAW DATA
Item.Origin = f
Item.New = 1
Item.Unread = 1
Item.Pull = InputTime
Item.Title = IdentifyTag(RawItem, {'<title.->(.-)</title>'})
Item.Link = IdentifyTag(RawItem, Type.ItemLink)
Item.Desc = IdentifyTag(RawItem, Type.ItemDesc)
Item.Date = IdentifyTag(RawItem, Type.ItemDate)
Item.Event = IdentifyTag(RawItem, Type.ItemEvent)
Item.ID = IdentifyTag(RawItem, Type.ItemID) or Item.Link or Item.Title or Item.Desc or Item.Date or Item.Event
-- DEFAULTS & ADDITIONAL PARSING
if not Item.Link then Item.Link = Feed.Link end
Item.Date = IdentifyDate(Item.Date, Type.ParseDate)
if not Item.Date then
Item.Date = Item.Pull
Item.NoDate = 1
end
if Type.ParseEvent then
Item.Event, Item.AllDay = IdentifyDate(Item.Event, Type.ParseEvent)
if not Item.Event then
Item.Event = Item.Date
Item.AllDay = 0
Item.NoEvent = 1
end
end
table.insert(NewItems, Item)
end
-- IDENTIFY DUPLICATES
for i, OldItem in ipairs(Feed) do
for j, NewItem in ipairs(NewItems) do
if NewItem.ID == OldItem.ID then
OldItem.Match = NewItem
NewItem.New = OldItem.New
NewItem.Unread = OldItem.Unread
NewItem.Pull = OldItem.Pull
if NewItem.NoDate then
NewItem.Date = OldItem.Date
end
if NewItem.NoEvent then
NewItem.Event = OldItem.Event
NewItem.AllDay = OldItem.AllDay
end
break
end
end
end
-- This process scans each item that already exists in the feed table,
-- and checks it against each item that has been discovered in the live
-- feed content. If it recognizes one of the new items as matching a
-- preexisting item, it keeps the old unread state and retrieval date,
-- as well as the publication and event dates (if expected). Finally, it
-- marks the old item as a duplicate for deletion.
-- CLEAR DUPLICATES OR ALL HISTORY
if (KeepOldItems == 1) then
for i = #Feed, 1, -1 do
if Feed[i].Match then
table.remove(Feed, i)
end
end
else
for i = 1, #Feed do
table.remove(Feed)
end
end
-- ADD NEW ITEMS
for i = #NewItems, 1, -1 do
if NewItems[i] then
table.insert(Feed, 1, NewItems[i])
end
end
-- CHECK NUMBER OF ITEMS
if #Feed == 0 then
Feed.Error = {
Description = 'No items found.',
Title = Feed.Title,
Link = Feed.Link
}
SendFeedback(('Error parsing #%d (from %s). %s'):format(f, Feed.MeasureName, Feed.Error.Description))
return
elseif (MaxKeepItems == 1) and (#Feed > MaxKeepItems) then
for i = #Feed, (MaxKeepItems + 1), -1 do
table.remove(Feed)
end
end
-- CLEAR ERROR FROM PREVIOUS REFRESH
Feed.Error = nil
elseif Feed.Error then
return
-- If the previous refresh encountered an error, and the
-- content has not changed, then the error is still present,
-- and the function should not continue.
end
-- LIMIT NEW/UNREAD STATES AND RECALCULATE TOTAL
-- Done for Refresh, Update and Mark.
Feed.New = 0
Feed.Unread = 0
for _, Item in ipairs(Feed) do
Item.New = (InputTime - Item.Pull < LimitNew) and Item.New or 0
Feed.New = Feed.New + Item.New
if LimitUnread > 0 then
Item.Unread = (InputTime - Item.Pull < LimitUnread) and Item.Unread or 0
end
Feed.Unread = Feed.Unread + Item.Unread
end
-- MODULES
EventFile_Update(f)
HistoryFile_Update(f)
SendFeedback(('Finished #%d (from %s). Name: "%s". Type: %s. Items: %d (%d new, %d unread).'):format(f, Feed.MeasureName, Feed.Title, Feed.Type, #Feed, Feed.New, Feed.Unread))
end
-----------------------------------------------------------------------
-- OUTPUT
function Output()
Display = {}
-- GET OPTIONS
local CombineFeeds = SELF:GetNumberOption('CombineFeeds', 0)
local MinShowItems = SELF:GetNumberOption('MinShowItems', nil)
local MaxShowItems = SELF:GetNumberOption('MaxShowItems', nil)
local Timestamp = SELF:GetOption ('Timestamp', '%I.%M %p on %d %B %Y')
local VariablePrefix = SELF:GetOption ('VariablePrefix', '')
local FinishAction = SELF:GetOption ('FinishAction', nil)
-- CLEAR AND REBUILD DISPLAY CONTENT
if (CombineFeeds == 1) then
Display.All = true
Display.Title = 'All Items'
Display.Link = ''
Display.New = 0
Display.Unread = 0
for f = 1, #Feeds do
if Feeds[f].Error then
Display.Error = Feeds[f].Error
break
else
Display.New = Display.New + Feeds[f].New
Display.Unread = Display.Unread + Feeds[f].Unread
for _, Item in ipairs(Feeds[f]) do
table.insert(Display, Item)
end
end
end
else
Display.All = false
for k, v in pairs(Feeds[F]) do
Display[k] = v
end
end
-- SORTING
if (CombineFeeds == 1) then
table.sort(Display, function(a, b) return a.Date > b.Date end)
end
-- BUILD QUEUE
local Queue = {}
local ShowRange = math.min(MaxShowItems, math.max(MinShowItems, #Display))
Queue['CurrentFeed'] = (CombineFeeds == 1) and 0 or F
Queue['NumberOfItems'] = #Display
-- CHECK FOR INPUT ERRORS
if Display.Error then
-- ERROR; QUEUE MESSAGES
Queue['FeedTitle'] = Display.Error.Title or 'Untitled'
Queue['FeedLink'] = Display.Error.Link or ''
Queue['FeedNew'] = 0
Queue['FeedUnread'] = 0
Queue['Item1Title'] = Display.Error.Description or ''
Queue['Item1Link'] = Display.Error.Link or ''
Queue['Item1Desc'] = ''
Queue['Item1Date'] = ''
Queue['Item1Event'] = ''
Queue['Item1Origin'] = ''
Queue['Item1Unread'] = 0
Queue['Item1New'] = 0
for i = 2, ShowRange do
Queue['Item'..i..'Title'] = ''
Queue['Item'..i..'Link'] = ''
Queue['Item'..i..'Desc'] = ''
Queue['Item'..i..'Unread'] = 0
Queue['Item'..i..'New'] = 0
Queue['Item'..i..'Date'] = ''
Queue['Item'..i..'Event'] = ''
Queue['Item'..i..'Origin'] = ''
end
else
-- NO ERROR; QUEUE FEED
Queue['FeedTitle'] = Display.Title or 'Untitled'
Queue['FeedLink'] = Display.Link or ''
Queue['FeedNew'] = Display.New
Queue['FeedUnread'] = Display.Unread
for i = 1, ShowRange do
local Item = Display[i] or {}
Queue['Item'..i..'Title'] = Item.Title or ''
Queue['Item'..i..'Link'] = Item.Link or Queue['FeedLink'] or ''
Queue['Item'..i..'Desc'] = Item.Desc or ''
Queue['Item'..i..'Unread'] = Item.Unread or 0
Queue['Item'..i..'New'] = Item.New or 0
Queue['Item'..i..'Event'] = Item.Event and os.date(Timestamp, Item.Event) or ''
Queue['Item'..i..'Date'] = Item.Date and os.date(Timestamp, Item.Date) or ''
Queue['Item'..i..'Origin'] = Item.Origin and Feeds[Item.Origin].Title or ''
end
end
-- SET VARIABLES
for k, v in pairs(Queue) do
SKIN:Bang('!SetVariable', VariablePrefix..k, v)
end
-- FINISH ACTION
if FinishAction then
SKIN:Bang(FinishAction)
end
end
-----------------------------------------------------------------------
-- EVENTS
function Update()
for f, Feed in ipairs(Feeds) do
Input(f, 'Update')
end
Output()
end
-----------------------
function Refresh(f)
f = tonumber(f) or F
local Feed = Feeds[f]
local Raw = Feed.Measure:GetStringValue()
-- IF FEED HAS CHANGED, UPDATE INPUT
if (Raw ~= Feed.Raw) then
Feed.Raw = Raw
Input(f, 'Refresh')
end
-- IF FEED IS DISPLAYING, UPDATE OUTPUT
if (f == F) or (Display.All) then
Output()
end
end
-----------------------
function Show(f)
F = tonumber(f)
Output()
end
function ShowNext()
F = (F % #Feeds) + 1
Output()
end
function ShowPrevious()
F = (F == 1) and #Feeds or (F - 1)
Output()
end
-----------------------
function Mark(Target, Unread)
Target = tonumber(Target)
Unread = tonumber(Unread)
-- DEFINE RANGE
local Start = (Target == 0) and 1 or Target
local End = (Target == 0) and #Display or Target
-- MARK ITEM AND UPDATE INPUT FOR AFFECTED FEEDS
local InputQueue = {}
for i = Start, End do
local Item = Display[i]
Item.Unread = (Unread == -1) and (1 - Item.Unread) or Unread
InputQueue[Item.Origin] = true
end
for f, _ in pairs(InputQueue) do
Input(f, 'Mark')
end
Output()
end
function MarkRead (a) Mark(a, 0) end
function MarkUnread (a) Mark(a, 1) end
function ToggleUnread (a) Mark(a, -1) end
function MarkAllRead ( ) Mark(0, 0) end
function MarkAllUnread ( ) Mark(0, 1) end
function ToggleAllUnread ( ) Mark(0, -1) end
-----------------------------------------------------------------------
-- SORTING
-----------------------------------------------------------------------
-- PARSING
function IdentifyType(s)
-- COLLAPSE CONTAINER TAGS
-- Unnecessary when DecodeCharacterReference is not used in WebParser.
-- for _, v in ipairs{ 'item', 'entry' } do
-- s = s:gsub('<'..v..'.->.+</'..v..'>', '<'..v..'></'..v..'>') -- e.g. '<entry.->.+</entry>' --> '<entry></entry>'
-- end
--DEFINE RSS MARKER TESTS
--Each of these test functions will be run in turn, until one of them gets a solid match on the format type.
local TestRSS = {
function(a)
-- If the feed contains these tags outside of <item> or <entry>, RSS is confirmed.
for _, v in ipairs{ '<rss', '<channel', '<lastBuildDate', '<pubDate', '<ttl', '<description' } do
if a:match(v) then
return 'RSS'
end
end
return false
end,
function(a)
-- Alternatively, if the feed contains these tags outside of <item> or <entry>, Atom is confirmed.
for _, v in ipairs{ '<feed', '<subtitle' } do
if a:match(v) then
return 'Atom'
end
end
return false
end,
function(a)
-- If no markers are present, we search for <item> or <entry> tags to confirm the type.
local HaveItems = a:match('<item')
local HaveEntries = a:match('<entry')
if HaveItems and not HaveEntries then
return 'RSS'
elseif HaveEntries and not HaveItems then
return 'Atom'
else
-- If both kinds of tags are present, and no markers are given, then I give up
-- because your feed is ridiculous. And if neither tag is present, then no type
-- can be confirmed (and there would be no usable data anyway).
return false
end
end
}
-- RUN RSS MARKER TESTS
local Class = false
for _, v in ipairs(TestRSS) do
Class = v(s)
if Class then break end
end
-- DETECT SUBTYPE AND RETURN
if Class == 'RSS' then
return 'RSS'
elseif Class == 'Atom' then
if s:match('xmlns:gCal') then
return 'GoogleCalendar'
elseif s:match('<subtitle>rememberthemilk.com</subtitle>') then
return 'RememberTheMilk'
else
return 'Atom'
end
else
return false
end
end
-----------------------
function IdentifyTag(s, p)
local Tag
-- IF BOTH STRING AND PATTERNS ARE GIVEN, FIND MATCH
if s and p then
for _, Pattern in ipairs(p) do
Tag = s:match(Pattern)
if Tag then break end
end
end
-- IF MATCH IS FOUND, PARSE
if Tag and Tag:match('[%w%p]') then
-- CLEAN UP FORMATTING
Tag = DecodeCharacterReference(Tag) -- Replace HTML character references with real values.
Tag = Tag:match('^%s*(.-)%s*$') -- Strip whitespace from beginning and end of value.
Tag = Tag:gsub('%s%s+', ' ') -- Condense whitespace within value.
else
-- VALUE CONTAINS NO REAL CONTENT; TREAT AS NONEXISTENT.
Tag = nil
end
return Tag
end
-------------------------
function IdentifyDate(s, p)
local Date, AllDay
-- IF BOTH STRING AND PATTERNS ARE GIVEN, FIND MATCH
if s and p then
for _, Pattern in ipairs(p) do
Date = Pattern(s)
if Date then break end
end
end
-- IF MATCH IS FOUND, PARSE
if Date then
Date.year = tonumber(Date.year)
Date.month = tonumber(Date.month) or MonthAcronyms[Date.month]
Date.day = tonumber(Date.day)
Date.hour = tonumber(Date.hour)
Date.min = tonumber(Date.min)
Date.sec = tonumber(Date.sec)
-- DETECT ALL-DAY EVENT
local AllDay
if (Date.hour and Date.min) then
AllDay = 0
else
AllDay = 1
Date.hour = 0
Date.min = 0
end
-- GET CURRENT LOCAL TIME, UTC OFFSET
-- These values are referenced in several procedures below.
local UTC = os.date('!*t')
local LocalTime = os.date('*t')
local DaylightSavings = LocalTime.isdst and 3600 or 0
local LocalOffset = os.time(LocalTime) - os.time(UTC) + DaylightSavings
-- CHANGE 12-HOUR to 24-HOUR
if Date.Meridiem then
if (Date.Meridiem == 'AM') and (Date.hour == 12) then
Date.hour = 0
elseif (Date.Meridiem == 'PM') and (Date.hour < 12) then
Date.hour = Date.hour + 12
end
end
-- FIND CLOSEST MATCH FOR TWO-DIGIT YEAR
if Date.year < 100 then
local CurrentYear = LocalTime.year
local CurrentCentury = math.floor(CurrentYear / 100) * 100
local IfThisCentury = CurrentCentury + Date.year
local IfNextCentury = CurrentCentury + Date.year + 100
Date.year = (math.abs(CurrentYear - IfThisCentury) < math.abs(CurrentYear - IfNextCentury)) and IfThisCentury or IfNextCentury
end
-- GET INPUT OFFSET FROM UTC (OR DEFAULT TO LOCAL)
if (Date.Offset) and (Date.Offset ~= '') then
if Date.Offset:match('%a') then
Date.Offset = TimeZones[Date.Offset] and (TimeZones[Date.Offset] * 3600) or 0
elseif Date.Offset:match('%d') then
local Direction, Hours, Minutes = Date.Offset:match('^([^%d]-)(%d+)[^%d]-(%d%d)$')
Direction = Direction:match('%-') and -1 or 1
Hours = tonumber(Hours) * 3600
Minutes = tonumber(Minutes) and (tonumber(Minutes) * 60) or 0
Date.Offset = (Hours + Minutes) * Direction
end
else
Date.Offset = LocalOffset
end
-- RETURN CONVERTED DATE
Date = os.time(Date) + LocalOffset - Date.Offset
end
return Date, AllDay
end
function ParseDateRSS(s)
local d = {}
d.day, d.month, d.year, d.hour, d.min, d.sec, d.Offset = s:match('(%d+) (%a+) (%d+) (%d+)%:(%d+)%:(%d+) (.-)$')
return next(d) and d or nil
end
function ParseDateAtom(s)
local d = {}
d.year, d.month, d.day, d.hour, d.min, d.sec, d.Offset = s:match('(%d+)%-(%d+)%-(%d+)T(%d+)%:(%d+)%:(%d+%.?%d*)(.-)$')
return next(d) and d or nil
end
function ParseEventGoogleCalendar(s)
local d = {}
d.year, d.month, d.day, d.hour, d.min, d.sec, d.Offset = s:match('(%d+)%-(%d+)%-(%d+)T(%d+)%:(%d+)%:(%d+)%.%d+(.-)$')
return next(d) and d or nil
end
function ParseEventGoogleCalendarAllDay(s)
local d = {}
d.year, d.month, d.day = s:match('(%d+)%-(%d+)%-(%d+)$')
return next(d) and d or nil
end
function ParseEventRememberTheMilk(s)
local d = {}
d.day, d.month, d.year, d.hour, d.min, d.Meridiem = s:match('%a+ (%d+) (%a+) (%d+) at (%d+)%:(%d+)(%a+)') -- e.g. 'Wed 7 Nov 12 at 3:17PM'
return next(d) and d or nil
end
function ParseEventRememberTheMilkAllDay(s)
local d = {}
d.day, d.month, d.year = s:match('%a+ (%d+) (%a+) (%d+)') -- e.g. 'Tue 25 Dec 12'
return next(d) and d or nil
end
-----------------------------------------------------------------------
-- EVENT FILE MODULE
function EventFile_Initialize()
local EventFiles = {}
local AllEventFiles = SELF:GetOption('EventFile', '')
for EventFile in AllEventFiles:gmatch('[^%|]+') do
table.insert(EventFiles, EventFile)
end
for i, v in ipairs(Feeds) do
local EventFile = EventFiles[i] or Script..'_Feed'..i..'Events.xml'
Feeds[i].EventFile = SKIN:MakePathAbsolute(EventFile)
end
end
function EventFile_Update(f)
f = f or F
local Feed = Feeds[f]
local WriteEvents = SELF:GetNumberOption('WriteEvents', 0)
if (WriteEvents == 1) and (Feed.Type == 'GoogleCalendar') then
-- CREATE XML TABLE
local WriteLines = {}
table.insert(WriteLines, '<EventFile Title="'..Feed.Title..'">')
for i, v in ipairs(Feed) do
local ItemDate = os.date('*t', v.Date)
table.insert(WriteLines, '<Event Month="'..ItemDate.month..'" Day="'..ItemDate.day..'" Desc="'..v.Title..'"/>')
end
table.insert(WriteLines, '</EventFile>')
-- WRITE FILE
local WriteFile = io.output(Feed.EventFile, 'w')
if WriteFile then
local WriteContent = table.concat(WriteLines, '\r\n')
WriteFile:write(WriteContent)
WriteFile:close()
else
SKIN:Bang('!Log', Script..': cannot open file: '..Feed.EventFile)
end
end
end
-----------------------------------------------------------------------
-- HISTORY FILE MODULE
function HistoryFile_Initialize()
-- DETERMINE FILEPATH
HistoryFile = SELF:GetOption('HistoryFile', Script..'History.xml')
HistoryFile = SKIN:MakePathAbsolute(HistoryFile)
-- CREATE HISTORY DATABASE
History = {}
-- CHECK IF FILE EXISTS
local ReadFile = io.open(HistoryFile)
if ReadFile then
local ReadContent = ReadFile:read('*all')
ReadFile:close()
-- PARSE HISTORY FROM LAST SESSION
for ReadFeedURL, ReadFeed in ReadContent:gmatch('<feed URL=(%b"")>(.-)</feed>') do
local ReadFeedURL = ReadFeedURL:match('^"(.-)"$')
History[ReadFeedURL] = {}
for ReadItem in ReadFeed:gmatch('<item>(.-)</item>') do
local Item = {}
for Key, Value in ReadItem:gmatch('<(.-)>(.-)</.->') do
Item[Key] = DecodeCharacterReference(Value)
Item[Key] = tonumber(Item[Key]) or Item[Key]
end
Item.Date = tonumber(Item.Date) or Item.Date
Item.Unread = tonumber(Item.Unread)
table.insert(History[ReadFeedURL], Item)
end
end
end
-- ADD HISTORY TO MAIN DATABASE
-- For each feed, if URLs match, add all contents from History[h] to Feeds[f].
for f, Feed in ipairs(Feeds) do
local h = Feed.Measure:GetOption('URL')
Feeds[f].URL = h
if History[h] then
for _, Item in ipairs(History[h]) do
Item.Origin = f
table.insert(Feeds[f], Item)
end
end
end
end
function HistoryFile_Update(f)
f = f or F
local Feed = Feeds[f]
-- CLEAR AND REBUILD HISTORY
local h = Feed.URL
History[h] = {}
for i, Item in ipairs(Feed) do
table.insert(History[h], Item)
end
-- WRITE HISTORY IF REQUESTED
WriteHistory()
end
function WriteHistory()
local WriteHistory = SELF:GetNumberOption('WriteHistory', 0)
if WriteHistory == 1 then
-- GENERATE XML TABLE
local WriteLines = {}
for WriteURL, WriteFeed in pairs(History) do
table.insert(WriteLines, ('<feed URL=%q>'):format(WriteURL))
for _, WriteItem in ipairs(WriteFeed) do
table.insert(WriteLines, '\t<item>')
for Key, Value in pairs(WriteItem) do
Value = EncodeCharacterReference(Value)
table.insert(WriteLines, ('\t\t<%s>%s</%s>'):format(Key, Value, Key))
end
table.insert(WriteLines, '\t</item>')
end
table.insert(WriteLines, '</feed>')
end
-- WRITE XML TO FILE
local WriteFile = io.open(HistoryFile, 'w')
if WriteFile then
local WriteContent = table.concat(WriteLines, '\n')
WriteFile:write(WriteContent)
WriteFile:close()
else
SKIN:Bang('!Log', Script..': cannot open file: '..HistoryFile)
end
end
end
function ClearHistory()
local DeleteFile = io.open(HistoryFile)
if DeleteFile then
DeleteFile:close()
os.remove(HistoryFile)
SKIN:Bang('!Log', Script..': deleted history cache at '..HistoryFile)
end
SKIN:Bang('!Refresh')
end
-----------------------------------------------------------------------
-- DECODE CHARACTER REFERENCE
function EncodeCharacterReference(s)
if type(s) == 'string' then
for Ref, Char in pairs(CharacterReferences) do
if Char < 256 then -- Temporary safeguard for Unicode characters.
s = s:gsub(string.char(Char), '&'..Ref..';')
end
end
end
return s
end
function DecodeCharacterReference(s, Max)
if type(s) == 'string' then
local Max = Max or 0
local Loops = 0
local Matches
-- DEFINE MATCHING FUNCTION FOR SINGLE VALID REFERENCE.
local function ReplaceReference(s)
-- STRIP CONTAINER
local Ref = s:match('^&(.+);$')
local Char
-- MATCH NUMBER OR HTML CODE
if Ref:match('^#') then
-- NUMBER
local Base = Ref:match('^#x') and 16 or 10
Char = tonumber(Ref:match('^#x?(%w+)'), Base)
else
-- HTML
Char = CharacterReferences[Ref]
end
if Char then
-- IF REFERENCE WAS FOUND, INDICATE MATCH AND RETURN TRUE CHARACTER.
Matches = Matches + 1
Char = (Char < 256) and string.char(Char) or '' -- Temporary safeguard for Unicode characters.
return Char
else
-- IF NO REFERENCE WAS FOUND, RETURN THE ORIGINAL INPUT UNCHANGED.
return s
end
end
-- FIND ALL VALID CHARACTER REFERENCES AND ATTEMPT TO MATCH.
repeat
Matches = 0
Loops = Loops + 1
s = s:gsub('&#?x?%w+;', ReplaceReference)
until (Loops == Max) or (Matches == 0)
end
return s
end
-----------------------------------------------------------------------
-- FEEDBACK
function SendFeedback(s)
table.insert(Feedback, s)
local Debug = SELF:GetNumberOption('Debug', 0)
if (Debug == 1) then
SKIN:Bang('!Log', ('%s: %s'):format(Script, s))
end
end
-----------------------------------------------------------------------
-- CONSTANTS
Types = {
RSS = {
Link = { '<link.->(.-)</link>' },
Item = '<item.-</item>',
ItemID = { '<guid.->(.-)</guid>' },
ItemLink = { '<link.->(.-)</link>' },
ItemDesc = { '<description.->(.-)</description>' },
ItemDate = { '<pubDate.->(.-)</pubDate>', '<dc:date>(.-)</dc:date>' },
ParseDate = { ParseDateRSS, ParseDateAtom }
},
Atom = {
Link = { '<link.-href=["\'](.-)["\']' },
Item = '<entry.-</entry>',
ItemID = { '<id.->(.-)</id>' },
ItemLink = { '<link.-href=["\'](.-)["\']' },
ItemDesc = { '<summary.->(.-)</summary>' },
ItemDate = { '<updated.->(.-)</updated>' },
ParseDate = { ParseDateAtom, ParseDateRSS }
},
GoogleCalendar = {
Link = { '<link.-rel=.-alternate.-href=["\'](.-)["\']' },
Item = '<entry.-</entry>',
ItemID = { '<id.->(.-)</id>' },
ItemLink = { '<link.-href=["\'](.-)["\']' },
ItemDesc = { '<summary.->(.-)</summary>' },
ItemDate = { 'startTime=["\'](.-)["\']' },
ParseDate = { ParseDateAtom },
ParseEvent = { ParseEventGoogleCalendar, ParseEventGoogleCalendarAllDay }
},
RememberTheMilk = {
Link = { '<link.-rel=.-alternate.-href=["\'](.-)["\']' },
Item = '<entry.-</entry>',
ItemID = { '<id.->(.-)</id>' },
ItemLink = { '<link.-href=["\'](.-)["\']' },
ItemDesc = { '<summary.->(.-)</summary>' },
ItemDate = { '<span class=["\']rtm_due_value["\']>(.-)</span>' },
ParseDate = { ParseDateAtom },
ParseEvent = { ParseEventRememberTheMilk, ParseEventRememberTheMilkAllDay }
}
}
CharacterReferences = {
-- This table matches HTML character codes with numeric
-- character codes. Converted from Rainmeter's WebParser.dll.
-- STANDARD ASCII CHARACTERS (0-255):
quot = 34,
amp = 38,
apos = 39,
lt = 60,
gt = 62,
nbsp = 160,
iexcl = 161,
cent = 162,
pound = 163,
curren = 164,
yen = 165,
brvbar = 166,
sect = 167,
uml = 168,
copy = 169,
ordf = 170,
laquo = 171,
['not'] = 172,
shy = 173,
reg = 174,
macr = 175,
deg = 176,
plusmn = 177,
sup2 = 178,
sup3 = 179,
acute = 180,
micro = 181,
para = 182,
middot = 183,
cedil = 184,
sup1 = 185,
ordm = 186,
raquo = 187,
frac14 = 188,
frac12 = 189,
frac34 = 190,
iquest = 191,
Agrave = 192,
Aacute = 193,
Acirc = 194,
Atilde = 195,
Auml = 196,
Aring = 197,
AElig = 198,
Ccedil = 199,
Egrave = 200,
Eacute = 201,
Ecirc = 202,
Euml = 203,
Igrave = 204,
Iacute = 205,
Icirc = 206,
Iuml = 207,
ETH = 208,
Ntilde = 209,
Ograve = 210,
Oacute = 211,
Ocirc = 212,
Otilde = 213,
Ouml = 214,
times = 215,
Oslash = 216,
Ugrave = 217,
Uacute = 218,
Ucirc = 219,
Uuml = 220,
Yacute = 221,
THORN = 222,
szlig = 223,
agrave = 224,
aacute = 225,
acirc = 226,
atilde = 227,
auml = 228,
aring = 229,
aelig = 230,
ccedil = 231,
egrave = 232,
eacute = 233,
ecirc = 234,
euml = 235,
igrave = 236,
iacute = 237,
icirc = 238,
iuml = 239,
eth = 240,
ntilde = 241,
ograve = 242,
oacute = 243,
ocirc = 244,
otilde = 245,
ouml = 246,
divide = 247,
oslash = 248,
ugrave = 249,
uacute = 250,
ucirc = 251,
uuml = 252,
yacute = 253,
thorn = 254,
yuml = 255,
-- EXTENDED UNICODE CHARACTERS
-- These will become usable if and when Rainmeter's Lua
-- libraries support extended Unicode character encodings.
OElig = 338,
oelig = 339,
Scaron = 352,
scaron = 353,
Yuml = 376,
fnof = 402,
circ = 710,
tilde = 732,
Alpha = 913,
Beta = 914,
Gamma = 915,
Delta = 916,
Epsilon = 917,
Zeta = 918,
Eta = 919,
Theta = 920,
Iota = 921,
Kappa = 922,
Lambda = 923,
Mu = 924,
Nu = 925,
Xi = 926,
Omicron = 927,
Pi = 928,
Rho = 929,
Sigma = 931,
Tau = 932,
Upsilon = 933,
Phi = 934,
Chi = 935,
Psi = 936,
Omega = 937,
alpha = 945,
beta = 946,
gamma = 947,
delta = 948,
epsilon = 949,
zeta = 950,
eta = 951,
theta = 952,
iota = 953,
kappa = 954,
lambda = 955,
mu = 956,
nu = 957,
xi = 958,
omicron = 959,
pi = 960,
rho = 961,
sigmaf = 962,
sigma = 963,
tau = 964,
upsilon = 965,
phi = 966,
chi = 967,
psi = 968,
omega = 969,
thetasym = 977,
upsih = 978,
piv = 982,
ensp = 8194,
emsp = 8195,
thinsp = 8201,
zwnj = 8204,
zwj = 8205,
lrm = 8206,
rlm = 8207,
ndash = 8211,
mdash = 8212,
lsquo = 8216,
rsquo = 8217,
sbquo = 8218,
ldquo = 8220,
rdquo = 8221,
bdquo = 8222,
dagger = 8224,
Dagger = 8225,
bull = 8226,
hellip = 8230,
permil = 8240,
prime = 8242,
Prime = 8243,
lsaquo = 8249,
rsaquo = 8250,
oline = 8254,
frasl = 8260,
euro = 8364,
image = 8465,
weierp = 8472,
real = 8476,
trade = 8482,
alefsym = 8501,
larr = 8592,
uarr = 8593,
rarr = 8594,
darr = 8595,
harr = 8596,
crarr = 8629,
lArr = 8656,
uArr = 8657,
rArr = 8658,
dArr = 8659,
hArr = 8660,
forall = 8704,
part = 8706,
exist = 8707,
empty = 8709,
nabla = 8711,
isin = 8712,
notin = 8713,
ni = 8715,
prod = 8719,
sum = 8721,
minus = 8722,
lowast = 8727,
radic = 8730,
prop = 8733,
infin = 8734,
ang = 8736,
['and'] = 8743,
['or'] = 8744,
cap = 8745,
cup = 8746,
int = 8747,
there4 = 8756,
sim = 8764,
cong = 8773,
asymp = 8776,
ne = 8800,
equiv = 8801,
le = 8804,
ge = 8805,
sub = 8834,
sup = 8835,
nsub = 8836,
sube = 8838,
supe = 8839,
oplus = 8853,
otimes = 8855,
perp = 8869,
sdot = 8901,
lceil = 8968,
rceil = 8969,
lfloor = 8970,
rfloor = 8971,
lang = 9001,
rang = 9002,
loz = 9674,
spades = 9824,
clubs = 9827,
hearts = 9829,
diams = 9830
}
TimeZones = {
IDLW = -12, -- International Date Line West
NT = -11, -- Nome
CAT = -10, -- Central Alaska
HST = -10, -- Hawaii Standard
HDT = -9, -- Hawaii Daylight
YST = -9, -- Yukon Standard
YDT = -8, -- Yukon Daylight
PST = -8, -- Pacific Standard
PDT = -7, -- Pacific Daylight
MST = -7, -- Mountain Standard
MDT = -6, -- Mountain Daylight
CST = -6, -- Central Standard
CDT = -5, -- Central Daylight
EST = -5, -- Eastern Standard
EDT = -4, -- Eastern Daylight
AST = -3, -- Atlantic Standard
ADT = -2, -- Atlantic Daylight
WAT = -1, -- West Africa
GMT = 0, -- Greenwich Mean
UTC = 0, -- Universal (Coordinated)
Z = 0, -- Zulu, alias for UTC
WET = 0, -- Western European
BST = 1, -- British Summer
CET = 1, -- Central European
MET = 1, -- Middle European
MEWT = 1, -- Middle European Winter
MEST = 2, -- Middle European Summer
CEST = 2, -- Central European Summer
MESZ = 2, -- Middle European Summer
FWT = 1, -- French Winter
FST = 2, -- French Summer
EET = 2, -- Eastern Europe, USSR Zone 1
EEST = 3, -- Eastern European Daylight
WAST = 7, -- West Australian Standard
WADT = 8, -- West Australian Daylight
CCT = 8, -- China Coast, USSR Zone 7
JST = 9, -- Japan Standard, USSR Zone 8
EAST = 10, -- Eastern Australian Standard
EADT = 11, -- Eastern Australian Daylight
GST = 10, -- Guam Standard, USSR Zone 9
NZT = 12, -- New Zealand
NZST = 12, -- New Zealand Standard
NZDT = 13, -- New Zealand Daylight
IDLE = 12 -- International Date Line East
}
MonthAcronyms = {
Jan = 1,
Feb = 2,
Mar = 3,
Apr = 4,
May = 5,
Jun = 6,
Jul = 7,
Aug = 8,
Sep = 9,
Oct = 10,
Nov = 11,
Dec = 12
}