root/HTTP/WikiContent.lua

Revision 1467 (checked in by rsz, 18 hours ago)

cleanup

Line 
1 --------------------------------------------------------------------------------
2 -- Title:               WikiContent.lua
3 -- Description:         Like a square peg in a round hole
4 -- Author:              Raphaël Szwarc http://alt.textdrive.com/lua/
5 -- Creation Date:       January 30, 2007
6 -- Legal:               Copyright (C) 2007 Raphaël Szwarc
7 --                      Under the terms of the MIT License
8 --                      http://www.opensource.org/licenses/mit-license.html
9 --------------------------------------------------------------------------------
10
11 -- import dependencies
12 local Wiki = require( 'Wiki' )
13
14 local io = require( 'io' )
15 local os = require( 'os' )
16 local table = require( 'table' )
17
18 local assert = assert
19 local error = error
20 local getmetatable = getmetatable
21 local rawget = rawget
22 local rawset = rawset
23 local require = require
24 local setmetatable = setmetatable
25 local tonumber = tonumber
26 local tostring = tostring
27
28 --------------------------------------------------------------------------------
29 -- WikiContent
30 --------------------------------------------------------------------------------
31
32 module( 'WikiContent' )
33 _VERSION = '1.0'
34
35 local self = setmetatable( _M, {} )
36 local meta = getmetatable( self )
37
38 local location = Wiki.Location()
39
40 --------------------------------------------------------------------------------
41 -- Name utilities
42 --------------------------------------------------------------------------------
43
44 local function Capitalize( aValue )
45     return ( aValue:lower():gsub( '^(.)([%w_\']*)', function( first, rest ) return first:upper() .. rest end ) )
46 end
47
48 local function Name( aName, aLength )
49     local aLength = aLength or 2
50     local Unidecode = require( 'Unidecode' )
51    
52     aName = aName or ''
53     aName = Unidecode( aName )
54     aName = aName:gsub( '%W', ' ' )
55     aName = aName:gsub( '(%l)(%u)', '%1 %2' )
56     aName = aName:gsub( '(%u)(%u)(%l)', "%1 %2%3" )
57     aName = aName:gsub( '%s+', ' ' )
58     aName = aName:gsub( '^%s', '' )
59     aName = aName:sub( 1, 128 )
60     aName = aName:gsub( '%s$', '' )
61     aName = aName:gsub( ' ', '-' )
62     aName = aName:lower()
63    
64     if aName:len() > aLength then
65         return aName
66     end
67 end
68
69 -- Based on Duncan Cross's implementation of John Gruber's 'Title Case'...
70 -- http://lua-users.org/lists/lua-l/2008-08/msg00353.html
71
72 local TitleWord = { a = true, [ 'and' ] = true, as = true, at = true, but = true, by = true, en = true, [ 'for' ] = true, [ 'if' ] = true, [ 'in' ] = true, of = true, on = true, the = true, to = true, vs = true, [ 'vs.' ] = true, v = true, [ 'v.' ] = true, via = true }
73
74 local function Title( aTitle )
75     local aHandler = function( aStart, aMatch, aSpace, anEnd )
76         if aStart > 1
77         and aTitle:sub( aStart - 2, aStart - 2 ) ~= ':'
78         and aTitle:len() > anEnd
79         and TitleWord[ aMatch:lower() ] then
80        
81             return aMatch:lower() .. aSpace
82            
83         elseif aTitle:sub( aStart - 1, aStart + 1 ):match( '[\'"_{%(%[]' ) then
84        
85             return aMatch:sub( 1, 1 ) .. aMatch:sub( 2, 2 ):upper() .. aMatch:sub( 3 ) .. aSpace
86            
87         elseif aMatch:sub( 2 ):match( '[A-Z&]' )
88         or aMatch:sub( 2 ):match( '%w[%._]%w' )
89         or aTitle:sub( aStart - 1, aStart + 1 ):match( '[])}]' ) then
90        
91             return aMatch .. aSpace
92            
93         end
94        
95         return aMatch:sub( 1, 1 ):upper() .. aMatch:sub( 2 ) .. aSpace
96     end
97    
98     aTitle = aTitle:gsub( '-', ' ' )
99    
100     return ( aTitle:gsub( '()([%w&`\'‘’"“%.@:/{%(%[<>_]+)(%s*)()', aHandler ) )
101 end
102
103 local function Prefix( aName )
104     return aName:gsub( '-', '' ):match( '^(%w)(%w)' )
105 end
106
107 --------------------------------------------------------------------------------
108 -- IO utilities
109 --------------------------------------------------------------------------------
110
111 local function Directory( aName )
112     local File = require( 'File' )
113     local aFirst, aSecond = Prefix( aName )
114     local aDirectory = File( location, 'wiki', 'content', aFirst, aSecond, aName )
115    
116     return aDirectory
117 end
118
119 local function DataFile( aName, aVersion )
120     local File = require( 'File' )
121     local aDirectory = Directory( aName )
122     local aFile = File( aDirectory.path, ( 'data.txt;%d' ):format( aVersion ) )
123    
124     return aFile
125 end
126
127 local function VersionIterator( aName )
128     local aDirectory = Directory( aName )
129
130     if aDirectory.exists then
131         local anIterator = aDirectory()
132
133         return function()
134             local aFile = anIterator()
135            
136             while aFile and not aFile.name:find( '^data%.txt;%d+$' ) do
137                 aFile = anIterator()
138             end
139            
140             if aFile and aFile.name:find( '^data%.txt;%d+$' ) then
141                 local aVersion = tonumber( aFile.name:match( '^data%.txt;(%d+)$' ) )
142                
143                 return aVersion, aFile
144             end
145         end
146     end 
147    
148     return function() end
149 end
150
151 local function FirstVersion( aName )
152     local firstVersion = nil
153    
154     for aVersion in VersionIterator( aName ) do
155         if not firstVersion or aVersion < firstVersion then
156             firstVersion = aVersion
157         end
158     end
159    
160     return firstVersion or 1
161 end
162
163 local function LastVersion( aName )
164     local lastVersion = nil
165    
166     for aVersion in VersionIterator( aName ) do
167         if not lastVersion or aVersion > lastVersion then
168             lastVersion = aVersion
169         end
170     end
171    
172     return lastVersion or 1
173 end
174
175 local function PreviousVersion( aName, aCurrentVersion )
176     local previousVersion = nil
177     local anInterval = nil
178    
179     for aVersion in VersionIterator( aName ) do
180         if aVersion < aCurrentVersion
181         and ( not previousVersion
182         or not anInterval
183         or ( aCurrentVersion - aVersion ) < anInterval ) then
184             previousVersion = aVersion
185             anInterval = aCurrentVersion - previousVersion
186         end
187     end
188
189     return previousVersion
190 end
191
192 local function PurgeVersion( aName, aLimit )
193     local aLimit = aLimit or 100
194     local aList = {}
195    
196     for aVersion in VersionIterator( aName ) do
197         aList[ #aList + 1 ] = aVersion
198     end
199    
200     if #aList > aLimit then
201         table.sort( aList )
202        
203         for anIndex = 1, #aList - aLimit do
204             local aVersion = aList[ anIndex ]
205             local aFile = DataFile( aName, aVersion )
206            
207             aFile.delete = true
208         end
209     end
210 end
211
212 local function ReadData( aPath, aCount )
213     local lfs = require( 'lfs' )
214     local aCount = aCount or 1
215     local aReader = assert( io.open( aPath, 'rb' ) )
216    
217     if lfs.lock( aReader, 'r' ) then
218         local aContent = aReader:read( '*a' )
219        
220         lfs.unlock( aReader )
221         aReader:close()
222        
223         return aContent
224     end
225    
226     aReader:close()
227    
228     aCount = aCount + 1
229    
230     if aCount > 10 then
231         error( ( 'Failed to read data from %q' ):format( aPath ) )
232     end
233    
234     return ReadData( aPath, aCount )
235 end
236
237 local function WriteData( aPath, aData )
238     local lfs = require( 'lfs' )
239     local aWriter = assert( io.open( aPath, 'wb' ) )
240    
241     if lfs.lock( aWriter, 'w' ) then
242         aWriter:write( aData )
243         aWriter:flush()
244         lfs.unlock( aWriter )
245         aWriter:close()
246        
247         return
248     end
249    
250     aWriter:close()
251
252     error( ( 'Failed to write data to %q' ):format( aPath ) )
253 end
254
255 local function ContentCreation( aName )
256     local aDirectory = Directory( aName )
257     local aFile = DataFile( aName, FirstVersion( aName ) )
258
259     if aFile.exists then
260         local Data = require( 'Data' )
261        
262         return Data( ReadData( aFile.path ) ).creation
263     end
264    
265     return os.time()
266 end
267
268 local function ContentData( aName, aVersion )
269     local aDirectory = Directory( aName )
270     local aFile = DataFile( aName, aVersion )
271    
272     if aFile.exists then
273         local Data = require( 'Data' )
274        
275         return Data( ReadData( aFile.path ) )
276     end
277    
278     return { title = Title( aName ), type = 'text/markdown', encoding = 'utf-8', creation = ContentCreation( aName ) }
279 end
280
281 local function Lock( aName )
282     local File = require( 'File' )
283     local aDirectory = Directory( aName )
284     local aFile = File( aDirectory.path, '.lock' )
285     local aLock = assert( io.open( aFile.path, 'wb' ) )
286    
287     return aLock
288 end
289
290 local function MakeDirectory( aName )
291     local lfs = require( 'lfs' )
292     local File = require( 'File' )
293     local aDirectory = Directory( aName )
294     local aPath = ''
295     local aMaker = function( aValue )
296         aPath = aPath .. aValue .. File.separator
297        
298         lfs.mkdir( aPath )
299     end
300    
301     if aDirectory.path:byte() == File.separator:byte() then
302         aPath = File.separator
303     end
304
305     aDirectory.path:gsub( '([^' .. File.separator .. ']+)', aMaker )
306    
307     return aDirectory
308 end
309
310 local function Save( aContent, aCount )
311     local lfs = require( 'lfs' )
312     local aCount = aCount or 0
313     local aDirectory = MakeDirectory( aContent.name )
314     local aLock = Lock( aContent.name )
315
316     if lfs.lock( aLock, 'w' ) then
317         local Data = require( 'Data' )
318         local aFile = DataFile( aContent.name, aContent.version )
319         local aData = Data( aContent.data )
320         local aPath = ''
321        
322         WriteData( aFile.path, aData )
323        
324         PurgeVersion( aContent.name )
325        
326         lfs.unlock( aLock )
327         aLock:close()
328        
329         return aContent
330     end
331    
332     aLock:close()
333     aCount = aCount + 1
334    
335     if aCount > 2 then
336         error( ( 'Failed to lock %q' ):format( aContent.name ) )
337     end
338    
339     return Save( aContent, aCount )
340 end
341
342 local function CanWrite( aName )
343     local File = require( 'File' )
344     local aDirectory = Directory( aName )
345     local aFile = File( aDirectory.path, 'nowrite' )
346    
347     if aFile.exists then
348         return false
349     end
350    
351     return true
352 end
353
354 local function SetWrite( aName, aValue )
355     local File = require( 'File' )
356     local aDirectory = Directory( aName )
357     local aFile = File( aDirectory.path, 'nowrite' )
358    
359     if aValue then
360         aFile.delete = true
361     else
362         aFile.content = 'nowrite'
363     end   
364 end
365
366 --------------------------------------------------------------------------------
367 -- File utilities
368 --------------------------------------------------------------------------------
369
370 local function FileDirectory( aName )
371     local File = require( 'File' )
372     local lfs = require( 'lfs' )
373     local aDirectory = Directory( aName )
374     local aFileDirectory = File( aDirectory.path, 'file' )
375    
376     lfs.mkdir( aFileDirectory.path )
377
378     return aFileDirectory
379 end
380
381 local function FileIterator( aName )
382     local aDirectory = FileDirectory( aName )
383    
384     return aDirectory()
385 end
386
387 local function AddFile( aName, aFile )
388     if aFile and aFile.name:byte() ~= 46 then
389         local File = require( 'File' )
390         local aDirectory = FileDirectory( aName )
391         local aFileExtension = aFile.extension
392         local aFileName = aFile.name:sub( 1, aFile.name:len() - aFileExtension:len() - 1 )
393         local aFileName = ( '%s.%s' ):format( Name( aFileName, 0 ), Name( aFileExtension, 0 ) )
394         local aNewFile = File( aDirectory.path, aFileName )
395        
396         aNewFile.content = aFile.content
397         aNewFile.modification = aFile.modification or os.time()
398     end
399 end
400
401 --------------------------------------------------------------------------------
402 -- Link utilities
403 --------------------------------------------------------------------------------
404
405 local function LinkDirectory( aName )
406     local File = require( 'File' )
407     local lfs = require( 'lfs' )
408     local aDirectory = Directory( aName )
409     local aFileDirectory = File( aDirectory.path, 'link' )
410    
411     lfs.mkdir( aFileDirectory.path )
412
413     return aFileDirectory
414 end
415
416 local function LinkIterator( aName )
417     return Wiki.NameIterator( LinkDirectory( aName ) )
418 end
419
420 local function AddLink( aName, aLink )
421     if aName ~= aLink then
422         local File = require( 'File' )
423         local lfs = require( 'lfs' )
424         local aDirectory = LinkDirectory( aName )
425         local aFile = File( aDirectory.path, ( '%s.id' ):format( aLink ) )
426        
427         lfs.mkdir( aFile.path )
428         aFile.modification = os.time()
429        
430         Wiki.PurgeDirectory( aDirectory )
431     end
432 end
433
434 --------------------------------------------------------------------------------
435 -- Metamethods
436 --------------------------------------------------------------------------------
437
438 local cache = setmetatable( {}, { __mode = 'v' } )
439
440 function meta:__call( aName, aVersion )
441     local aName = Name( aName )
442
443     if aName then
444         local aVersion = aVersion or LastVersion( aName )
445         local aKey = ( '%s;%d' ):format( aName, aVersion )
446         local aContent = cache[ aKey ]
447        
448         if not aContent then
449             aContent = { name = aName, version = aVersion }
450             setmetatable( aContent, self )
451             cache[ aKey ] = aContent
452         end
453        
454         return aContent
455     end
456 end
457
458 function meta:__index( aKey )
459     return Name( aKey, 0 )
460 end
461
462 function meta:__concat( aValue )
463     return tostring( self ) .. tostring( aValue )
464 end
465
466 function meta:__tostring()
467     return ( '%s/%s' ):format( self._NAME, self._VERSION )
468 end
469
470 function self:__call()
471     local aKey = ( '%s;%d' ):format( self.name, self.version )
472
473     cache[ aKey ] = nil
474
475     return Save( self )
476 end
477
478 function self:__index( aKey )
479     if aKey == 'canWrite' then
480         return CanWrite( self.name )
481     elseif aKey == 'data' then
482         local someData = ContentData( self.name, self.version )
483         rawset( self, 'data', someData )
484        
485         return someData
486     elseif aKey == 'directory' then
487         return Directory( self.name )
488     elseif aKey == 'exists' then
489         return DataFile( self.name, self.version ).exists
490     elseif aKey == 'file' then
491         return FileIterator( self.name )
492     elseif aKey == 'firstVersion' then
493         return FirstVersion( self.name )
494     elseif aKey == 'lastVersion' then
495         return LastVersion( self.name )
496     elseif aKey == 'link' then
497         return LinkIterator( self.name )
498     elseif aKey == 'lock' then
499         return Lock( self.name )
500     elseif aKey == 'modification' then
501         return DataFile( self.name, self.version ).modification
502     elseif aKey == 'prefix' then
503         return table.concat( { Prefix( self.name ) } )
504     elseif aKey == 'previousVersion' then
505         return PreviousVersion( self.name, self.version )
506     elseif aKey == 'title' and not rawget( self, 'data' ) then
507         return Title( self.name )
508     elseif aKey == 'size' then
509         return DataFile( self.name, self.version ).size
510     elseif aKey == 'versions' then
511         return VersionIterator( self.name )
512     end
513
514     return self.data[ aKey ]
515 end
516
517 function self:__newindex( aKey, aValue )
518     if aKey == 'canWrite' then
519         return SetWrite( self.name, aValue )
520     elseif aKey == 'file' then
521         return AddFile( self.name, aValue )
522     elseif aKey == 'link' then
523         return AddLink( self.name, aValue )
524     elseif aKey == 'modification' then
525         DataFile( self.name, self.version ).modification = aValue
526        
527         return
528     end
529
530     self.data[ aKey ] = aValue
531 end
532
533 function self:__concat( aValue )
534     return tostring( self ) .. tostring( aValue )
535 end
536
537 function self:__eq( aValue )
538     return tostring( self ) == tostring( aValue )
539 end
540
541 function self:__lt( aValue )
542     return tostring( self ) < tostring( aValue )
543 end
544
545 function self:__tostring()
546     return tostring( self.title )
547 end
Note: See TracBrowser for help on using the browser.