tiddlywiki.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. """
  2. A Python implementation of the Twee compiler.
  3. This code was written by Chris Klimas <klimas@gmail.com>
  4. It is licensed under the GNU General Public License v2
  5. http://creativecommons.org/licenses/GPL/2.0/
  6. This file defines two classes: Tiddler and TiddlyWiki. These match what
  7. you'd normally see in a TiddlyWiki; the goal here is to provide classes
  8. that translate between Twee and TiddlyWiki output seamlessly.
  9. """
  10. import re, time, locale, os, codecs
  11. import tweeregex
  12. from tweelexer import TweeLexer
  13. class TiddlyWiki(object):
  14. """An entire TiddlyWiki."""
  15. def __init__(self):
  16. self.tiddlers = {}
  17. self.storysettings = {}
  18. def hasTiddler(self, name):
  19. return name in self.tiddlers
  20. def toTwee(self, order = None):
  21. """Returns Twee source code for this TiddlyWiki.
  22. The 'order' argument is a sequence of passage titles specifying the order
  23. in which passages should appear in the output string; by default passages
  24. are returned in arbitrary order.
  25. """
  26. tiddlers = self.tiddlers
  27. if order is None:
  28. order = tiddlers.keys()
  29. return u''.join(tiddlers[i].toTwee() for i in order)
  30. def read(self, filename):
  31. try:
  32. source = codecs.open(filename, 'rU', 'utf_8_sig', 'strict')
  33. w = source.read()
  34. except UnicodeDecodeError:
  35. try:
  36. source = codecs.open(filename, 'rU', 'utf16', 'strict')
  37. w = source.read()
  38. except:
  39. source = open(filename, 'rU')
  40. w = source.read()
  41. source.close()
  42. return w
  43. def toHtml(self, app, header = None, order = None, startAt = '', defaultName = '', metadata = {}):
  44. """Returns HTML code for this TiddlyWiki."""
  45. if not order: order = self.tiddlers.keys()
  46. output = u''
  47. if not header:
  48. app.displayError("building: no story format was specified.\n"
  49. + "Please select another format from the Story Format submenu",
  50. stacktrace = False)
  51. return
  52. try:
  53. headerPath = header.path + 'header.html'
  54. # TODO: Move file reading to Header class.
  55. output = self.read(headerPath)
  56. except IOError:
  57. app.displayError("building: the story format '" + header.label + "' isn't available.\n"
  58. + "Please select another format from the Story Format submenu",
  59. stacktrace = False)
  60. return
  61. def insertEngine(app, output, filename, label, extra = ''):
  62. if output.count(label) > 0:
  63. try:
  64. enginecode = self.read(filename)
  65. return output.replace(label,enginecode + extra)
  66. except IOError:
  67. app.displayError("building: the file '" + filename + "' used by the story format '" + header.label + "' wasn't found",
  68. stacktrace = False)
  69. return ''
  70. else:
  71. return output
  72. # Insert version number
  73. output = output.replace('"VERSION"', "Made in " + app.NAME + " " + app.VERSION)
  74. # Insert timestamp
  75. # Due to Windows limitations, the timezone offset must be computed manually.
  76. tz_offset = (lambda t: '%s%02d%02d' % (('+' if t <= 0 else '-',) + divmod(abs(t) / 60, 60)))(time.timezone)
  77. # Obtain the encoding expected to be used by strftime in this locale
  78. strftime_encoding = locale.getlocale(locale.LC_TIME)[1] or locale.getpreferredencoding()
  79. # Write the timestamp
  80. output = output.replace('"TIME"', "Built on "+time.strftime("%d %b %Y at %H:%M:%S, "+tz_offset).decode(strftime_encoding))
  81. # Insert the test play "start at passage" value
  82. if startAt:
  83. output = output.replace('"START_AT"', '"' + startAt.replace('\\', r'\\').replace('"', '\"') + '"')
  84. else:
  85. output = output.replace('"START_AT"', '""')
  86. # Embed any engine related files required by the header.
  87. embedded = header.filesToEmbed()
  88. for key in embedded.keys():
  89. output = insertEngine(app, output, embedded[key], key)
  90. if not output: return ''
  91. # Insert the Backup Story Title
  92. if defaultName:
  93. name = defaultName.replace('"',r'\"')
  94. # Just gonna assume the <title> has no attributes
  95. output = re.sub(r'<title>.*?<\/title>', '<title>'+name+'</title>', output, count=1, flags=re.I|re.M) \
  96. .replace('"Untitled Story"', '"'+name+'"')
  97. # Insert the metadata
  98. metatags = ''
  99. for name, content in metadata.iteritems():
  100. if content:
  101. metatags += '<meta name="' + name.replace('"','&quot;') + '" content="' + content.replace('"','&quot;') + '">\n'
  102. if metatags:
  103. output = re.sub(r'<\/title>\s*\n?', lambda a: a.group(0) + metatags, output, flags=re.I, count=1)
  104. # Check if the scripts are personally requesting jQuery or Modernizr
  105. jquery = 'jquery' in self.storysettings and self.storysettings['jquery'] != "off"
  106. modernizr = 'modernizr' in self.storysettings and self.storysettings['modernizr'] != "off"
  107. blankCSS = 'blankcss' in self.storysettings and self.storysettings['blankcss'] != "off"
  108. for i in filter(lambda a: (a.isScript() or a.isStylesheet()), self.tiddlers.itervalues()):
  109. if not jquery and i.isScript() and re.search(r'requires? jquery', i.text, re.I):
  110. jquery = True
  111. if not modernizr and re.search(r'requires? modernizr', i.text, re.I):
  112. modernizr = True
  113. if not blankCSS and i.isStylesheet() and re.search(r'blank stylesheet', i.text, re.I):
  114. blankCSS = True
  115. if jquery and modernizr and not blankCSS:
  116. break
  117. # Insert jQuery
  118. if jquery:
  119. output = insertEngine(app, output, app.builtinTargetsPath + 'jquery.js', '"JQUERY"')
  120. if not output: return
  121. else:
  122. output = output.replace('"JQUERY"','')
  123. # Insert Modernizr
  124. if modernizr:
  125. output = insertEngine(app, output, app.builtinTargetsPath + 'modernizr.js', '"MODERNIZR"')
  126. if not output: return
  127. else:
  128. output = output.replace('"MODERNIZR"','')
  129. # Remove default CSS
  130. if blankCSS:
  131. # Just gonna assume the html id is quoted correctly if at all.
  132. output = re.sub(r'<style\s+id=["\']?defaultCSS["\']?\s*>(?:[^<]|<(?!\/style>))*<\/style>', '', output, flags=re.I|re.M, count=1)
  133. rot13 = 'obfuscate' in self.storysettings and \
  134. self.storysettings['obfuscate'] != 'off'
  135. # In case it was set to "swap" (legacy 1.4.1 file),
  136. # alter and remove old properties.
  137. if rot13:
  138. self.storysettings['obfuscate'] = "rot13"
  139. if 'obfuscatekey' in self.storysettings:
  140. del self.storysettings['obfuscatekey']
  141. # Finally add the passage data
  142. storyfragments = []
  143. for i in order:
  144. tiddler = self.tiddlers[i]
  145. # Strip out comments from storysettings and reflect any alterations made
  146. if tiddler.title == 'StorySettings':
  147. tiddler.text = ''.join([(str(k)+":"+str(v)+"\n") for k,v in self.storysettings.iteritems()])
  148. if self.NOINCLUDE_TAGS.isdisjoint(tiddler.tags):
  149. storyfragments.append(tiddler.toHtml(rot13 and tiddler.isObfuscateable()))
  150. storycode = u''.join(storyfragments)
  151. if output.count('"STORY_SIZE"') > 0:
  152. output = output.replace('"STORY_SIZE"', '"' + str(len(storyfragments)) + '"')
  153. if output.count('"STORY"') > 0:
  154. output = output.replace('"STORY"', storycode)
  155. else:
  156. output += storycode
  157. if header:
  158. footername = header.path + 'footer.html'
  159. if os.path.exists(footername):
  160. output += self.read(footername)
  161. else:
  162. output += '</div></body></html>'
  163. return output
  164. def toRtf(self, order = None):
  165. """Returns RTF source code for this TiddlyWiki."""
  166. if not order: order = self.tiddlers.keys()
  167. def rtf_encode_char(unicodechar):
  168. if ord(unicodechar) < 128:
  169. return str(unicodechar)
  170. return r'\u' + str(ord(unicodechar)) + r'?'
  171. def rtf_encode(unicodestring):
  172. return r''.join(rtf_encode_char(c) for c in unicodestring)
  173. # preamble
  174. output = r'{\rtf1\ansi\ansicpg1251' + '\n'
  175. output += r'{\fonttbl\f0\fswiss\fcharset0 Arial;\f1\fmodern\fcharset0 Courier;}' + '\n'
  176. output += r'{\colortbl;\red128\green128\blue128;\red51\green51\blue204;}' + '\n'
  177. output += r'\margl1440\margr1440\vieww9000\viewh8400\viewkind0' + '\n'
  178. output += r'\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx792' + '\n'
  179. output += r'\tx8640\ql\qnatural\pardirnatural\pgnx720\pgny720' + '\n'
  180. # content
  181. for i in order:
  182. # Handle the situation where items are in the order set but not in the tiddlers set.
  183. if i not in self.tiddlers:
  184. continue
  185. text = rtf_encode(self.tiddlers[i].text)
  186. text = re.sub(r'\n', '\\\n', text) # newlines
  187. text = re.sub(tweeregex.LINK_REGEX, r'\\b\cf2 \ul \1\ulnone \cf0 \\b0 ', text) # links
  188. text = re.sub(r"''(.*?)''", r'\\b \1\\b0 ', text) # bold
  189. text = re.sub(r'\/\/(.*?)\/\/', r'\i \1\i0 ', text) # italics
  190. text = re.sub(r"\^\^(.*?)\^\^", r'\\super \1\\nosupersub ', text) # sup
  191. text = re.sub(r"~~(.*?)~~", r'\\sub \1\\nosupersub ', text) # sub
  192. text = re.sub(r"==(.*?)==", r'\\strike \1\\strike0 ', text) # strike
  193. text = re.sub(r'(\<\<.*?\>\>)', r'\\f1\cf1 \1\cf0\\f0 ', text) # macros
  194. text = re.sub(tweeregex.HTML_REGEX, r'\\f1\cf1 \g<0>\cf0\\f0 ', text) # macros
  195. text = re.sub(tweeregex.MONO_REGEX, r'\\f1 \1\\f0 ', text) # monospace
  196. text = re.sub(tweeregex.COMMENT_REGEX, '', text) # comments
  197. output += r'\fs24\b1 ' + rtf_encode(self.tiddlers[i].title) + r'\b0\fs20 ' + '\\\n'
  198. output += text + '\\\n\\\n'
  199. output += '}'
  200. return output
  201. def addTwee(self, source):
  202. """Adds Twee source code to this TiddlyWiki.
  203. Returns the tiddler titles in the order they occurred in the Twee source.
  204. """
  205. source = source.replace("\r\n", "\n")
  206. source = '\n' + source
  207. tiddlers = source.split('\n::')[1:]
  208. order = []
  209. for i in tiddlers:
  210. tiddler = Tiddler('::' + i)
  211. self.addTiddler(tiddler)
  212. order.append(tiddler.title)
  213. return order
  214. def addHtml(self, source):
  215. """Adds HTML source code to this TiddlyWiki.
  216. Returns the tiddler titles in the order they occurred in the HTML.
  217. """
  218. order = []
  219. divs = re.search(r'<div\s+id=(["\']?)store(?:A|-a)rea\1(?:\s+data-size=(["\']?)\d+\2)?(?:\s+hidden)?\s*>(.*)</div>', source,
  220. re.DOTALL)
  221. if divs:
  222. divs = divs.group(3)
  223. # HTML may be obfuscated.
  224. obfuscatekey = ''
  225. storysettings_re = r'[^>]*\stiddler=["\']?StorySettings["\']?[^>]*>.*?</div>'
  226. storysettings = re.search(r'<div'+storysettings_re, divs, re.DOTALL)
  227. if storysettings:
  228. ssTiddler = self.addTiddler(Tiddler(storysettings.group(0), 'html'))
  229. obfuscate = re.search(r'obfuscate\s*:\s*((?:[^\no]|o(?!ff))*)\s*(?:\n|$)', ssTiddler.text, re.I)
  230. if obfuscate:
  231. if "swap" in obfuscate.group(1):
  232. # Find the legacy 'obfuscatekey' option from 1.4.0.
  233. match = re.search(r'obfuscatekey\s*:\s*(\w*)\s*(?:\n|$)', ssTiddler.text, re.I)
  234. if match:
  235. obfuscatekey = match.group(1)
  236. nss = u''
  237. for nsc in obfuscatekey:
  238. if nss.find(nsc) == -1 and not nsc in ':\\\"n0':
  239. nss = nss + nsc
  240. obfuscatekey = nss
  241. else:
  242. obfuscatekey = "anbocpdqerfsgthuivjwkxlymz"
  243. divs = divs[:storysettings.start(0)] + divs[storysettings.end(0):]
  244. for div in divs.split('<div'):
  245. div.strip()
  246. if div:
  247. tiddler = Tiddler('<div' + div, 'html', obfuscatekey)
  248. self.addTiddler(tiddler)
  249. order.append(tiddler.title)
  250. return order
  251. def addHtmlFromFilename(self, filename):
  252. return self.addHtml(self.read(filename))
  253. def addTweeFromFilename(self, filename):
  254. return self.addTwee(self.read(filename))
  255. def addTiddler(self, tiddler):
  256. """Adds a Tiddler object to this TiddlyWiki."""
  257. self.tiddlers[tiddler.title] = tiddler
  258. return tiddler
  259. FORMATTED_INFO_PASSAGES = frozenset([
  260. 'StoryMenu', 'StoryTitle', 'StoryAuthor', 'StorySubtitle', 'StoryInit'])
  261. UNFORMATTED_INFO_PASSAGES = frozenset(['StoryIncludes', 'StorySettings'])
  262. INFO_PASSAGES = FORMATTED_INFO_PASSAGES | UNFORMATTED_INFO_PASSAGES
  263. SPECIAL_TAGS = frozenset(['Twine.image'])
  264. NOINCLUDE_TAGS = frozenset(['Twine.private', 'Twine.system'])
  265. INFO_TAGS = frozenset(['script', 'stylesheet', 'annotation']) | SPECIAL_TAGS | NOINCLUDE_TAGS
  266. class Tiddler: # pylint: disable=old-style-class
  267. """A single tiddler in a TiddlyWiki.
  268. Note: Converting this to a new-style class breaks pickling of new TWS files on old Twine releases.
  269. """
  270. def __init__(self, source, type = 'twee', obfuscatekey = ""):
  271. # cache of passage names linked from this one
  272. self.links = []
  273. self.displays = []
  274. self.images = []
  275. self.macros = []
  276. """Pass source code, and optionally 'twee' or 'html'"""
  277. if type == 'twee':
  278. self.initTwee(source)
  279. else:
  280. self.initHtml(source, obfuscatekey)
  281. def __getstate__(self):
  282. """Need to retain pickle format backwards-compatibility with Twine 1.3.5 """
  283. now = time.localtime()
  284. return {
  285. 'created': now,
  286. 'modified': now,
  287. 'title': self.title,
  288. 'tags': self.tags,
  289. 'text': self.text,
  290. }
  291. def __repr__(self):
  292. return "<Tiddler '" + self.title + "'>"
  293. def initTwee(self, source):
  294. """Initializes a Tiddler from Twee source code."""
  295. # used only during builds
  296. self.pos = [0,0]
  297. # figure out our title
  298. lines = source.strip().split('\n')
  299. meta_bits = lines[0].split('[')
  300. self.title = meta_bits[0].strip(' :')
  301. # find tags
  302. self.tags = []
  303. if len(meta_bits) > 1:
  304. tag_bits = meta_bits[1].split(' ')
  305. for tag in tag_bits:
  306. self.tags.append(tag.strip('[]'))
  307. # and then the body text
  308. self.text = u''
  309. for line in lines[1:]:
  310. self.text += line + "\n"
  311. self.text = self.text.strip()
  312. def initHtml(self, source, obfuscatekey = ""):
  313. """Initializes a Tiddler from HTML source code."""
  314. def decode_obfuscate_swap(text):
  315. """
  316. Does basic character pair swapping obfuscation.
  317. No longer used since 1.4.2, but can decode passages from 1.4.0 and 1.4.1
  318. """
  319. r = ''
  320. for c in text:
  321. upper = c.isupper()
  322. p = obfuscatekey.find(c.lower())
  323. if p != -1:
  324. if p % 2 == 0:
  325. p1 = p + 1
  326. if p1 >= len(obfuscatekey):
  327. p1 = p
  328. else:
  329. p1 = p - 1
  330. c = obfuscatekey[p1].upper() if upper else obfuscatekey[p1]
  331. r = r + c
  332. return r
  333. # title
  334. self.title = 'Untitled Passage'
  335. title_re = re.compile(r'(?:data\-)?(?:tiddler|name)="([^"]*?)"')
  336. title = title_re.search(source)
  337. if title:
  338. self.title = title.group(1)
  339. # tags
  340. self.tags = []
  341. tags_re = re.compile(r'(?:data\-)?tags="([^"]*?)"')
  342. tags = tags_re.search(source)
  343. if tags and tags.group(1) != '':
  344. self.tags = tags.group(1).split(' ')
  345. # position
  346. self.pos = [0,0]
  347. pos_re = re.compile(r'(?:data\-)?(?:twine\-)?position="([^"]*?)"')
  348. pos = pos_re.search(source)
  349. if pos:
  350. coord = pos.group(1).split(',')
  351. if len(coord) == 2:
  352. try:
  353. self.pos = map(int, coord)
  354. except ValueError:
  355. pass
  356. # body text
  357. self.text = ''
  358. text_re = re.compile(r'<div(?:[^"]|(?:".*?"))*?>((?:[^<]|<(?!\/div>))*)<\/div>')
  359. text = text_re.search(source)
  360. if text:
  361. self.text = decode_text(text.group(1))
  362. # deobfuscate
  363. # Note that we call isObfuscateable() using the raw title and tags, since if
  364. # the tiddler is not obfuscatable, those will be stored non-obfuscated.
  365. if obfuscatekey and self.isObfuscateable():
  366. self.title = decode_obfuscate_swap(self.title)
  367. self.tags = [decode_obfuscate_swap(tag) for tag in self.tags]
  368. self.text = decode_obfuscate_swap(self.text)
  369. def toHtml(self, rot13):
  370. """Returns an HTML representation of this tiddler.
  371. The encoder arguments are sequences of functions that take a single text argument
  372. and return a modified version of the given text.
  373. """
  374. def applyRot13(text):
  375. return text.decode('rot13') if rot13 else text
  376. def iterArgs():
  377. yield 'tiddler', applyRot13(self.title.replace('"', '&quot;'))
  378. if self.tags:
  379. yield 'tags', ' '.join(applyRot13(tag) for tag in self.tags)
  380. return u'<div%s%s>%s</div>' % (
  381. ''.join(' %s="%s"' % arg for arg in iterArgs()),
  382. ' twine-position="%d,%d"' % tuple(self.pos) if hasattr(self, "pos") else "",
  383. encode_text(applyRot13(self.text))
  384. )
  385. def toTwee(self):
  386. """Returns a Twee representation of this tiddler."""
  387. output = u':: ' + self.title
  388. if len(self.tags) > 0:
  389. output += u' ['
  390. for tag in self.tags:
  391. output += tag + ' '
  392. output = output.strip()
  393. output += u']'
  394. output += u"\n" + self.text + u"\n\n\n"
  395. return output
  396. def isImage(self):
  397. return 'Twine.image' in self.tags
  398. def isAnnotation(self):
  399. return 'annotation' in self.tags
  400. def isStylesheet(self):
  401. return 'stylesheet' in self.tags
  402. def isScript(self):
  403. return 'script' in self.tags
  404. def isInfoPassage(self):
  405. return self.title in TiddlyWiki.INFO_PASSAGES
  406. def isStoryText(self):
  407. """ Excludes passages which do not contain renderable Twine code. """
  408. return self.title not in TiddlyWiki.UNFORMATTED_INFO_PASSAGES \
  409. and TiddlyWiki.INFO_TAGS.isdisjoint(self.tags)
  410. def isStoryPassage(self):
  411. """ A more restrictive variant of isStoryText that excludes the StoryTitle, StoryMenu etc."""
  412. return self.title not in TiddlyWiki.INFO_PASSAGES \
  413. and TiddlyWiki.INFO_TAGS.isdisjoint(self.tags)
  414. def isObfuscateable(self):
  415. """Returns true iff this tiddler can be obfuscated when placed in the data store."""
  416. return self.title != 'StorySettings' and not self.isImage()
  417. def linksAndDisplays(self):
  418. return list(set(self.links+self.displays))
  419. def update(self):
  420. """
  421. Update the lists of all passages linked/displayed by this one.
  422. Returns internal links and <<choice>>/<<actions>> macros.
  423. """
  424. if not self.isStoryText() and not self.isAnnotation() and not self.isStylesheet():
  425. self.displays = []
  426. self.links = []
  427. self.variableLinks = []
  428. self.images = []
  429. self.macros = []
  430. return
  431. images = set()
  432. macros = set()
  433. links = set()
  434. variableLinks = set()
  435. def addLink(link):
  436. style = TweeLexer.linkStyle(link)
  437. if style == TweeLexer.PARAM:
  438. variableLinks.add(link)
  439. elif style != TweeLexer.EXTERNAL:
  440. links.add(link)
  441. # <<display>>
  442. self.displays = list(set(re.findall(r'\<\<display\s+[\'"]?(.+?)[\'"]?\s?\>\>', self.text, re.IGNORECASE)))
  443. macros = set()
  444. # other macros (including shorthand <<display>>)
  445. for m in re.finditer(tweeregex.MACRO_REGEX, self.text):
  446. # Exclude shorthand <<print>>
  447. if m.group(1) and m.group(1)[0] != '$':
  448. macros.add(m.group(1))
  449. self.macros = list(macros)
  450. # Regular hyperlinks (also matches wiki-style links inside macros)
  451. for m in re.finditer(tweeregex.LINK_REGEX, self.text):
  452. addLink(m.group(2) or m.group(1))
  453. # Include images
  454. for m in re.finditer(tweeregex.IMAGE_REGEX, self.text):
  455. if m.group(5):
  456. addLink(m.group(5))
  457. # HTML data-passage links
  458. for m in re.finditer(tweeregex.HTML_REGEX, self.text):
  459. attrs = m.group(2)
  460. if attrs:
  461. dataPassage = re.search(r"""data-passage\s*=\s*(?:([^<>'"=`\s]+)|'((?:[^'\\]*\\.)*[^'\\]*)'|"((?:[^"\\]*\\.)*[^"\\]*)")""", attrs)
  462. if dataPassage:
  463. link = dataPassage.group(1) or dataPassage.group(2) or dataPassage.group(3)
  464. if m.group(1) == "img":
  465. images.add(link)
  466. else:
  467. addLink(link)
  468. # <<choice passage_name [link_text]>>
  469. for block in re.findall(r'\<\<choice\s+(.*?)\s?\>\>', self.text):
  470. item = re.match(r'(?:"([^"]*)")|(?:\'([^\']*)\')|([^"\'\[\s]\S*)', block)
  471. if item:
  472. links.add(''.join(item.groups('')))
  473. # <<actions '' ''>>
  474. for block in re.findall(r'\<\<actions\s+(.*?)\s?\>\>', self.text):
  475. links.update(re.findall(r'[\'"](.*?)[\'"]', block))
  476. self.links = list(links)
  477. self.variableLinks = list(variableLinks)
  478. # Images
  479. for block in re.finditer(tweeregex.IMAGE_REGEX, self.text):
  480. images.add(block.group(4))
  481. self.images = list(images)
  482. #
  483. # Helper functions
  484. #
  485. def encode_text(text):
  486. """Encodes a string for use in HTML output."""
  487. output = text \
  488. .replace('\\', '\s') \
  489. .replace('\t', '\\t') \
  490. .replace('&', '&amp;') \
  491. .replace('<', '&lt;') \
  492. .replace('>', '&gt;') \
  493. .replace('"', '&quot;') \
  494. .replace('\0', '&#0;')
  495. output = re.sub(r'\r?\n', r'\\n', output)
  496. return output
  497. def decode_text(text):
  498. """Decodes a string from HTML."""
  499. return text \
  500. .replace('\\n', '\n') \
  501. .replace('\\t', '\t') \
  502. .replace('\s', '\\') \
  503. .replace('&quot;', '"') \
  504. .replace('&gt;', '>') \
  505. .replace('&lt;', '<') \
  506. .replace('&amp;', '&')
  507. def encode_date(date):
  508. """Encodes a datetime in TiddlyWiki format."""
  509. return time.strftime('%Y%m%d%H%M', date)
  510. def decode_date(date):
  511. """Decodes a datetime from TiddlyWiki format."""
  512. return time.strptime(date, '%Y%m%d%H%M')