header.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import os, imp, re, tweeregex, tweelexer
  2. from collections import OrderedDict
  3. class Header(object):
  4. # The name "id" is too short and is the name of a builtin, but it's part of the interface now.
  5. # pylint: disable=invalid-name,redefined-builtin
  6. def __init__(self, id, path, builtinPath):
  7. self.id = id.lower()
  8. self.path = path
  9. self.label = id.capitalize()
  10. self.builtinPath = builtinPath
  11. def filesToEmbed(self):
  12. """Returns an Ordered Dictionary of file names to embed into the output.
  13. The item key is the label to look for within the output.
  14. The item value is the name of the file who's contents will be embedded into the output.
  15. Internal headers referring to files outside their folders should use
  16. the following form for paths: self.builtinPath + ...
  17. External headers must use the following form for paths: self.path + "filename.js"
  18. """
  19. return OrderedDict([
  20. ('"JONAH"', self.builtinPath + os.sep + 'jonah' + os.sep + 'code.js'),
  21. ('"SUGARCANE"', self.builtinPath + os.sep + 'sugarcane' + os.sep + 'code.js'),
  22. ('"ENGINE"', self.builtinPath + os.sep + 'engine.js')
  23. ])
  24. def storySettings(self):
  25. """
  26. Returns a list of StorySettings dictionaries.
  27. Alternatively, it could return a string saying that it isn't supported, and suggesting an alternative.
  28. """
  29. return [{
  30. "type": "checkbox",
  31. "name": "undo",
  32. "label": "Let the player undo moves",
  33. "desc": "In Sugarcane, this enables the browser's back button.\n"
  34. "In Jonah, this lets the player click links in previous passages.",
  35. "default": "on"
  36. },{
  37. "type": "checkbox",
  38. "name": "bookmark",
  39. "label": "Let the player use passage bookmarks",
  40. "desc": "This enables the Bookmark links in Jonah and Sugarcane.\n"
  41. "(If the player can't undo, bookmarks are always disabled.)",
  42. "requires": "undo",
  43. "default": "on"
  44. },{
  45. "type": "checkbox",
  46. "name": "hash",
  47. "label": "Automatic URL hash updates",
  48. "desc": "The story's URL automatically updates, so that it always links to the\n"
  49. "current passage. Naturally, this renders the bookmark link irrelevant.",
  50. "requires": "undo",
  51. "default": "off"
  52. },{
  53. "type": "checkbox",
  54. "name": "exitprompt",
  55. "label": "Prompt before closing or reloading the page",
  56. "desc": "In most browsers, this asks the player to confirm closing or reloading the\n"
  57. "page after they've made at least 1 move.",
  58. "default": "off"
  59. },{
  60. "type": "checkbox",
  61. "name": "blankcss",
  62. "label": "Don't use the Story Format's default CSS",
  63. "desc": "Removes most of the story format's CSS styling, so that you can\n"
  64. "write stylesheets without having to override the default styles.\n"
  65. "Individual stylesheets may force this on by containing the text 'blank stylesheet'",
  66. "default": "off"
  67. },{
  68. "type": "checkbox",
  69. "name": "obfuscate",
  70. "label": "Use ROT13 to obscure spoilers in the HTML source code?",
  71. "values": ("rot13", "off"),
  72. "default": "off"
  73. },{
  74. "type": "checkbox",
  75. "name": "jquery",
  76. "label": "Include the jQuery script library?",
  77. "desc": "This enables the jQuery() function and the $() shorthand.\n"
  78. "Individual scripts may force this on by containing the text 'requires jQuery'.",
  79. },{
  80. "type": "checkbox",
  81. "name": "modernizr",
  82. "label": "Include the Modernizr script library?",
  83. "desc": "This adds CSS classes to the <html> element that can be used to write\n"
  84. "more compatible CSS or scripts. See http://modernizr.com/docs for details.\n"
  85. "Individual scripts/stylesheets may force this on by containing the\n"
  86. "text 'requires Modernizr'.",
  87. }]
  88. def isEndTag(self, name, tag):
  89. """Return true if the name is equal to an endtag."""
  90. return name == ('end' + tag)
  91. def nestedMacros(self):
  92. """Returns a list of macro names that support nesting."""
  93. return ['if', 'silently', 'nobr']
  94. def passageTitleColor(self, passage):
  95. """
  96. Returns a tuple pair of colours for a given passage's title.
  97. Colours can be HTML 1 hex strings like "#555753", or int triples (85, 87, 83)
  98. or wx.Colour objects.
  99. First is the normal colour, second is the Flat Design(TM) colour.
  100. """
  101. if passage.isScript():
  102. return ((89, 66, 28),(226, 170, 80))
  103. elif passage.isStylesheet():
  104. return ((111, 49, 83),(234, 123, 184))
  105. elif passage.isInfoPassage():
  106. return ((28, 89, 74), (41, 214, 113))
  107. elif passage.title == "Start":
  108. return ("#4ca333", "#4bdb24")
  109. def invisiblePassageTags(self):
  110. """Returns a list of passage tags which, for whatever reason, shouldn't be displayed on the Story Map."""
  111. return frozenset()
  112. def passageChecks(self):
  113. """
  114. Returns tuple of list of functions to perform on the passage whenever it's closed.
  115. The main tuple's three lists are: Twine checks, then Stylesheet checks, then Script checks.
  116. """
  117. """
  118. Twine code checks
  119. Each function should return an iterable (or be a generator) of tuples containing:
  120. * warning message string,
  121. * None, or a tuple:
  122. * start index where to begin substitution
  123. * string to substitute
  124. * end index
  125. """
  126. # Arguments are part of the interface even if the default implementation doesn't use them.
  127. # pylint: disable=unused-argument
  128. def checkUnmatchedMacro(tag, start, end, style, passage=None):
  129. if style == tweelexer.TweeLexer.BAD_MACRO:
  130. matchKind = "start" if "end" in tag else "end"
  131. yield ("The macro tag " + tag + "\ndoes not have a matching " + matchKind + " tag.", None)
  132. def checkInequalityExpression(tag, start, end, style, passage=None):
  133. if style == tweelexer.TweeLexer.MACRO:
  134. r = re.search(r"\s+((and|or|\|\||&&)\s+([gl]te?|is|n?eq|(?:[=!<]|>(?!>))=?))\s+" + tweeregex.UNQUOTED_REGEX, tag)
  135. if r:
  136. yield (tag + ' contains "' + r.group(1) + '", which isn\'t valid code.\n'
  137. + 'There should probably be an expression, or a variable, between "' + r.group(2) + '" and "' + r.group(3) + '".', None)
  138. def checkIfMacro(tag, start, end, style, passage=None):
  139. if style == tweelexer.TweeLexer.MACRO:
  140. ifMacro = re.search(tweeregex.MACRO_REGEX.replace(r"([^>\s]+)", r"(if\b|else ?if\b)"), tag)
  141. if ifMacro:
  142. # Check that the single = assignment isn't present in an if/elseif condition.
  143. r = re.search(r"([^=<>!~])(=(?!=))(.?)" + tweeregex.UNQUOTED_REGEX, tag)
  144. if r:
  145. warning = tag + " contains the = operator.\nYou must use 'is' instead of '=' in <<if>> and <<else if>> tags."
  146. insertion = "is"
  147. if r.group(1) != " ":
  148. insertion = " "+insertion
  149. if r.group(3) != " ":
  150. insertion += " "
  151. # Return the warning message, and a 3-tuple consisting of
  152. # start index of replacement, the replacement, end index of replacement
  153. yield (warning, (start+r.start(2), insertion, start+r.end(2)))
  154. def checkHTTPSpelling(tag, start, end, style, passage=None):
  155. if style == tweelexer.TweeLexer.EXTERNAL:
  156. # Corrects the incorrect spellings "http//" and "http:/" (and their https variants)
  157. regex = re.search(r"\bhttp(s?)(?:\/\/|\:\/(?=[^\/]))", tag)
  158. if regex:
  159. yield (r"You appear to have misspelled 'http" + regex.group(1) + "://'.",
  160. (start+regex.start(0), "http" + regex.group(1) + "://", start+regex.end(0)))
  161. """
  162. Script checks
  163. """
  164. def checkScriptTagsInScriptPassage(passage):
  165. # Check that a script passage does not contain "<script type='text/javascript'>" style tags.
  166. ret = []
  167. scriptTags = re.finditer(r"(?:</?script\b[^>]*>)" + tweeregex.UNQUOTED_REGEX, passage.text)
  168. for scriptTag in scriptTags:
  169. warning = "This script contains " + scriptTag.group(0) + ".\nScript passages should only contain Javascript code, not raw HTML."
  170. ret.append((warning, (scriptTag.start(0), "", scriptTag.end(0))))
  171. return ret
  172. return ([checkUnmatchedMacro, checkInequalityExpression, checkIfMacro, checkHTTPSpelling],[],[checkScriptTagsInScriptPassage])
  173. @staticmethod
  174. def factory(type, path, builtinPath):
  175. header_def = path + type + '.py'
  176. if os.access(header_def, os.R_OK):
  177. py_mod = imp.load_source(type, header_def)
  178. obj = py_mod.Header(type, path, builtinPath)
  179. else:
  180. obj = Header(type, path, builtinPath)
  181. return obj