PageRenderTime 49ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/rubypants.rb

https://bitbucket.org/d1egoaz/d1egoaz-blog
Ruby | 489 lines | 158 code | 48 blank | 283 comment | 31 complexity | 284eac7ac38602e857b9e4dd1a8d36f6 MD5 | raw file
  1. #
  2. # = RubyPants -- SmartyPants ported to Ruby
  3. #
  4. # Ported by Christian Neukirchen <mailto:chneukirchen@gmail.com>
  5. # Copyright (C) 2004 Christian Neukirchen
  6. #
  7. # Incooporates ideas, comments and documentation by Chad Miller
  8. # Copyright (C) 2004 Chad Miller
  9. #
  10. # Original SmartyPants by John Gruber
  11. # Copyright (C) 2003 John Gruber
  12. #
  13. #
  14. # = RubyPants -- SmartyPants ported to Ruby
  15. #
  16. # == Synopsis
  17. #
  18. # RubyPants is a Ruby port of the smart-quotes library SmartyPants.
  19. #
  20. # The original "SmartyPants" is a free web publishing plug-in for
  21. # Movable Type, Blosxom, and BBEdit that easily translates plain ASCII
  22. # punctuation characters into "smart" typographic punctuation HTML
  23. # entities.
  24. #
  25. #
  26. # == Description
  27. #
  28. # RubyPants can perform the following transformations:
  29. #
  30. # * Straight quotes (<tt>"</tt> and <tt>'</tt>) into "curly" quote
  31. # HTML entities
  32. # * Backticks-style quotes (<tt>``like this''</tt>) into "curly" quote
  33. # HTML entities
  34. # * Dashes (<tt>--</tt> and <tt>---</tt>) into en- and em-dash
  35. # entities
  36. # * Three consecutive dots (<tt>...</tt> or <tt>. . .</tt>) into an
  37. # ellipsis entity
  38. #
  39. # This means you can write, edit, and save your posts using plain old
  40. # ASCII straight quotes, plain dashes, and plain dots, but your
  41. # published posts (and final HTML output) will appear with smart
  42. # quotes, em-dashes, and proper ellipses.
  43. #
  44. # RubyPants does not modify characters within <tt><pre></tt>,
  45. # <tt><code></tt>, <tt><kbd></tt>, <tt><math></tt> or
  46. # <tt><script></tt> tag blocks. Typically, these tags are used to
  47. # display text where smart quotes and other "smart punctuation" would
  48. # not be appropriate, such as source code or example markup.
  49. #
  50. #
  51. # == Backslash Escapes
  52. #
  53. # If you need to use literal straight quotes (or plain hyphens and
  54. # periods), RubyPants accepts the following backslash escape sequences
  55. # to force non-smart punctuation. It does so by transforming the
  56. # escape sequence into a decimal-encoded HTML entity:
  57. #
  58. # \\ \" \' \. \- \`
  59. #
  60. # This is useful, for example, when you want to use straight quotes as
  61. # foot and inch marks: 6'2" tall; a 17" iMac. (Use <tt>6\'2\"</tt>
  62. # resp. <tt>17\"</tt>.)
  63. #
  64. #
  65. # == Algorithmic Shortcomings
  66. #
  67. # One situation in which quotes will get curled the wrong way is when
  68. # apostrophes are used at the start of leading contractions. For
  69. # example:
  70. #
  71. # 'Twas the night before Christmas.
  72. #
  73. # In the case above, RubyPants will turn the apostrophe into an
  74. # opening single-quote, when in fact it should be a closing one. I
  75. # don't think this problem can be solved in the general case--every
  76. # word processor I've tried gets this wrong as well. In such cases,
  77. # it's best to use the proper HTML entity for closing single-quotes
  78. # ("<tt>&#8217;</tt>") by hand.
  79. #
  80. #
  81. # == Bugs
  82. #
  83. # To file bug reports or feature requests (except see above) please
  84. # send email to: mailto:chneukirchen@gmail.com
  85. #
  86. # If the bug involves quotes being curled the wrong way, please send
  87. # example text to illustrate.
  88. #
  89. #
  90. # == Authors
  91. #
  92. # John Gruber did all of the hard work of writing this software in
  93. # Perl for Movable Type and almost all of this useful documentation.
  94. # Chad Miller ported it to Python to use with Pyblosxom.
  95. #
  96. # Christian Neukirchen provided the Ruby port, as a general-purpose
  97. # library that follows the *Cloth API.
  98. #
  99. #
  100. # == Copyright and License
  101. #
  102. # === SmartyPants license:
  103. #
  104. # Copyright (c) 2003 John Gruber
  105. # (http://daringfireball.net)
  106. # All rights reserved.
  107. #
  108. # Redistribution and use in source and binary forms, with or without
  109. # modification, are permitted provided that the following conditions
  110. # are met:
  111. #
  112. # * Redistributions of source code must retain the above copyright
  113. # notice, this list of conditions and the following disclaimer.
  114. #
  115. # * Redistributions in binary form must reproduce the above copyright
  116. # notice, this list of conditions and the following disclaimer in
  117. # the documentation and/or other materials provided with the
  118. # distribution.
  119. #
  120. # * Neither the name "SmartyPants" nor the names of its contributors
  121. # may be used to endorse or promote products derived from this
  122. # software without specific prior written permission.
  123. #
  124. # This software is provided by the copyright holders and contributors
  125. # "as is" and any express or implied warranties, including, but not
  126. # limited to, the implied warranties of merchantability and fitness
  127. # for a particular purpose are disclaimed. In no event shall the
  128. # copyright owner or contributors be liable for any direct, indirect,
  129. # incidental, special, exemplary, or consequential damages (including,
  130. # but not limited to, procurement of substitute goods or services;
  131. # loss of use, data, or profits; or business interruption) however
  132. # caused and on any theory of liability, whether in contract, strict
  133. # liability, or tort (including negligence or otherwise) arising in
  134. # any way out of the use of this software, even if advised of the
  135. # possibility of such damage.
  136. #
  137. # === RubyPants license
  138. #
  139. # RubyPants is a derivative work of SmartyPants and smartypants.py.
  140. #
  141. # Redistribution and use in source and binary forms, with or without
  142. # modification, are permitted provided that the following conditions
  143. # are met:
  144. #
  145. # * Redistributions of source code must retain the above copyright
  146. # notice, this list of conditions and the following disclaimer.
  147. #
  148. # * Redistributions in binary form must reproduce the above copyright
  149. # notice, this list of conditions and the following disclaimer in
  150. # the documentation and/or other materials provided with the
  151. # distribution.
  152. #
  153. # This software is provided by the copyright holders and contributors
  154. # "as is" and any express or implied warranties, including, but not
  155. # limited to, the implied warranties of merchantability and fitness
  156. # for a particular purpose are disclaimed. In no event shall the
  157. # copyright owner or contributors be liable for any direct, indirect,
  158. # incidental, special, exemplary, or consequential damages (including,
  159. # but not limited to, procurement of substitute goods or services;
  160. # loss of use, data, or profits; or business interruption) however
  161. # caused and on any theory of liability, whether in contract, strict
  162. # liability, or tort (including negligence or otherwise) arising in
  163. # any way out of the use of this software, even if advised of the
  164. # possibility of such damage.
  165. #
  166. #
  167. # == Links
  168. #
  169. # John Gruber:: http://daringfireball.net
  170. # SmartyPants:: http://daringfireball.net/projects/smartypants
  171. #
  172. # Chad Miller:: http://web.chad.org
  173. #
  174. # Christian Neukirchen:: http://kronavita.de/chris
  175. #
  176. class RubyPants < String
  177. # Create a new RubyPants instance with the text in +string+.
  178. #
  179. # Allowed elements in the options array:
  180. #
  181. # 0 :: do nothing
  182. # 1 :: enable all, using only em-dash shortcuts
  183. # 2 :: enable all, using old school en- and em-dash shortcuts (*default*)
  184. # 3 :: enable all, using inverted old school en and em-dash shortcuts
  185. # -1 :: stupefy (translate HTML entities to their ASCII-counterparts)
  186. #
  187. # If you don't like any of these defaults, you can pass symbols to change
  188. # RubyPants' behavior:
  189. #
  190. # <tt>:quotes</tt> :: quotes
  191. # <tt>:backticks</tt> :: backtick quotes (``double'' only)
  192. # <tt>:allbackticks</tt> :: backtick quotes (``double'' and `single')
  193. # <tt>:dashes</tt> :: dashes
  194. # <tt>:oldschool</tt> :: old school dashes
  195. # <tt>:inverted</tt> :: inverted old school dashes
  196. # <tt>:ellipses</tt> :: ellipses
  197. # <tt>:convertquotes</tt> :: convert <tt>&quot;</tt> entities to
  198. # <tt>"</tt> for Dreamweaver users
  199. # <tt>:stupefy</tt> :: translate RubyPants HTML entities
  200. # to their ASCII counterparts.
  201. #
  202. def initialize(string, options=[2])
  203. super string
  204. @options = [*options]
  205. end
  206. # Apply SmartyPants transformations.
  207. def to_html
  208. do_quotes = do_backticks = do_dashes = do_ellipses = do_stupify = nil
  209. convert_quotes = false
  210. if @options.include? 0
  211. # Do nothing.
  212. return self
  213. elsif @options.include? 1
  214. # Do everything, turn all options on.
  215. do_quotes = do_backticks = do_ellipses = true
  216. do_dashes = :normal
  217. elsif @options.include? 2
  218. # Do everything, turn all options on, use old school dash shorthand.
  219. do_quotes = do_backticks = do_ellipses = true
  220. do_dashes = :oldschool
  221. elsif @options.include? 3
  222. # Do everything, turn all options on, use inverted old school
  223. # dash shorthand.
  224. do_quotes = do_backticks = do_ellipses = true
  225. do_dashes = :inverted
  226. elsif @options.include?(-1)
  227. do_stupefy = true
  228. else
  229. do_quotes = @options.include? :quotes
  230. do_backticks = @options.include? :backticks
  231. do_backticks = :both if @options.include? :allbackticks
  232. do_dashes = :normal if @options.include? :dashes
  233. do_dashes = :oldschool if @options.include? :oldschool
  234. do_dashes = :inverted if @options.include? :inverted
  235. do_ellipses = @options.include? :ellipses
  236. convert_quotes = @options.include? :convertquotes
  237. do_stupefy = @options.include? :stupefy
  238. end
  239. # Parse the HTML
  240. tokens = tokenize
  241. # Keep track of when we're inside <pre> or <code> tags.
  242. in_pre = false
  243. # Here is the result stored in.
  244. result = ""
  245. # This is a cheat, used to get some context for one-character
  246. # tokens that consist of just a quote char. What we do is remember
  247. # the last character of the previous text token, to use as context
  248. # to curl single- character quote tokens correctly.
  249. prev_token_last_char = nil
  250. tokens.each { |token|
  251. if token.first == :tag
  252. result << token[1]
  253. if token[1] =~ %r!<(/?)(?:pre|code|kbd|script|math)[\s>]!
  254. in_pre = ($1 != "/") # Opening or closing tag?
  255. end
  256. else
  257. t = token[1]
  258. # Remember last char of this token before processing.
  259. last_char = t[-1].chr
  260. unless in_pre
  261. t = process_escapes t
  262. t.gsub!(/&quot;/, '"') if convert_quotes
  263. if do_dashes
  264. t = educate_dashes t if do_dashes == :normal
  265. t = educate_dashes_oldschool t if do_dashes == :oldschool
  266. t = educate_dashes_inverted t if do_dashes == :inverted
  267. end
  268. t = educate_ellipses t if do_ellipses
  269. # Note: backticks need to be processed before quotes.
  270. if do_backticks
  271. t = educate_backticks t
  272. t = educate_single_backticks t if do_backticks == :both
  273. end
  274. if do_quotes
  275. if t == "'"
  276. # Special case: single-character ' token
  277. if prev_token_last_char =~ /\S/
  278. t = "&#8217;"
  279. else
  280. t = "&#8216;"
  281. end
  282. elsif t == '"'
  283. # Special case: single-character " token
  284. if prev_token_last_char =~ /\S/
  285. t = "&#8221;"
  286. else
  287. t = "&#8220;"
  288. end
  289. else
  290. # Normal case:
  291. t = educate_quotes t
  292. end
  293. end
  294. t = stupefy_entities t if do_stupefy
  295. end
  296. prev_token_last_char = last_char
  297. result << t
  298. end
  299. }
  300. # Done
  301. result
  302. end
  303. protected
  304. # Return the string, with after processing the following backslash
  305. # escape sequences. This is useful if you want to force a "dumb" quote
  306. # or other character to appear.
  307. #
  308. # Escaped are:
  309. # \\ \" \' \. \- \`
  310. #
  311. def process_escapes(str)
  312. str.gsub('\\\\', '&#92;').
  313. gsub('\"', '&#34;').
  314. gsub("\\\'", '&#39;').
  315. gsub('\.', '&#46;').
  316. gsub('\-', '&#45;').
  317. gsub('\`', '&#96;')
  318. end
  319. # The string, with each instance of "<tt>--</tt>" translated to an
  320. # em-dash HTML entity.
  321. #
  322. def educate_dashes(str)
  323. str.gsub(/--/, '&#8212;')
  324. end
  325. # The string, with each instance of "<tt>--</tt>" translated to an
  326. # en-dash HTML entity, and each "<tt>---</tt>" translated to an
  327. # em-dash HTML entity.
  328. #
  329. def educate_dashes_oldschool(str)
  330. str.gsub(/---/, '&#8212;').gsub(/--/, '&#8211;')
  331. end
  332. # Return the string, with each instance of "<tt>--</tt>" translated
  333. # to an em-dash HTML entity, and each "<tt>---</tt>" translated to
  334. # an en-dash HTML entity. Two reasons why: First, unlike the en- and
  335. # em-dash syntax supported by +educate_dashes_oldschool+, it's
  336. # compatible with existing entries written before SmartyPants 1.1,
  337. # back when "<tt>--</tt>" was only used for em-dashes. Second,
  338. # em-dashes are more common than en-dashes, and so it sort of makes
  339. # sense that the shortcut should be shorter to type. (Thanks to
  340. # Aaron Swartz for the idea.)
  341. #
  342. def educate_dashes_inverted(str)
  343. str.gsub(/---/, '&#8211;').gsub(/--/, '&#8212;')
  344. end
  345. # Return the string, with each instance of "<tt>...</tt>" translated
  346. # to an ellipsis HTML entity. Also converts the case where there are
  347. # spaces between the dots.
  348. #
  349. def educate_ellipses(str)
  350. str.gsub('...', '&#8230;').gsub('. . .', '&#8230;')
  351. end
  352. # Return the string, with "<tt>``backticks''</tt>"-style single quotes
  353. # translated into HTML curly quote entities.
  354. #
  355. def educate_backticks(str)
  356. str.gsub("``", '&#8220;').gsub("''", '&#8221;')
  357. end
  358. # Return the string, with "<tt>`backticks'</tt>"-style single quotes
  359. # translated into HTML curly quote entities.
  360. #
  361. def educate_single_backticks(str)
  362. str.gsub("`", '&#8216;').gsub("'", '&#8217;')
  363. end
  364. # Return the string, with "educated" curly quote HTML entities.
  365. #
  366. def educate_quotes(str)
  367. punct_class = '[!"#\$\%\'()*+,\-.\/:;<=>?\@\[\\\\\]\^_`{|}~]'
  368. str = str.dup
  369. # Special case if the very first character is a quote followed by
  370. # punctuation at a non-word-break. Close the quotes by brute
  371. # force:
  372. str.gsub!(/^'(?=#{punct_class}\B)/, '&#8217;')
  373. str.gsub!(/^"(?=#{punct_class}\B)/, '&#8221;')
  374. # Special case for double sets of quotes, e.g.:
  375. # <p>He said, "'Quoted' words in a larger quote."</p>
  376. str.gsub!(/"'(?=\w)/, '&#8220;&#8216;')
  377. str.gsub!(/'"(?=\w)/, '&#8216;&#8220;')
  378. # Special case for decade abbreviations (the '80s):
  379. str.gsub!(/'(?=\d\ds)/, '&#8217;')
  380. close_class = %![^\ \t\r\n\\[\{\(\-]!
  381. dec_dashes = '&#8211;|&#8212;'
  382. # Get most opening single quotes:
  383. str.gsub!(/(\s|&nbsp;|--|&[mn]dash;|#{dec_dashes}|&#x201[34];)'(?=\w)/,
  384. '\1&#8216;')
  385. # Single closing quotes:
  386. str.gsub!(/(#{close_class})'/, '\1&#8217;')
  387. str.gsub!(/'(\s|s\b|$)/, '&#8217;\1')
  388. # Any remaining single quotes should be opening ones:
  389. str.gsub!(/'/, '&#8216;')
  390. # Get most opening double quotes:
  391. str.gsub!(/(\s|&nbsp;|--|&[mn]dash;|#{dec_dashes}|&#x201[34];)"(?=\w)/,
  392. '\1&#8220;')
  393. # Double closing quotes:
  394. str.gsub!(/(#{close_class})"/, '\1&#8221;')
  395. str.gsub!(/"(\s|s\b|$)/, '&#8221;\1')
  396. # Any remaining quotes should be opening ones:
  397. str.gsub!(/"/, '&#8220;')
  398. str
  399. end
  400. # Return the string, with each RubyPants HTML entity translated to
  401. # its ASCII counterpart.
  402. #
  403. # Note: This is not reversible (but exactly the same as in SmartyPants)
  404. #
  405. def stupefy_entities(str)
  406. str.
  407. gsub(/&#8211;/, '-'). # en-dash
  408. gsub(/&#8212;/, '--'). # em-dash
  409. gsub(/&#8216;/, "'"). # open single quote
  410. gsub(/&#8217;/, "'"). # close single quote
  411. gsub(/&#8220;/, '"'). # open double quote
  412. gsub(/&#8221;/, '"'). # close double quote
  413. gsub(/&#8230;/, '...') # ellipsis
  414. end
  415. # Return an array of the tokens comprising the string. Each token is
  416. # either a tag (possibly with nested, tags contained therein, such
  417. # as <tt><a href="<MTFoo>"></tt>, or a run of text between
  418. # tags. Each element of the array is a two-element array; the first
  419. # is either :tag or :text; the second is the actual value.
  420. #
  421. # Based on the <tt>_tokenize()</tt> subroutine from Brad Choate's
  422. # MTRegex plugin. <http://www.bradchoate.com/past/mtregex.php>
  423. #
  424. # This is actually the easier variant using tag_soup, as used by
  425. # Chad Miller in the Python port of SmartyPants.
  426. #
  427. def tokenize
  428. tag_soup = /([^<]*)(<[^>]*>)/
  429. tokens = []
  430. prev_end = 0
  431. scan(tag_soup) {
  432. tokens << [:text, $1] if $1 != ""
  433. tokens << [:tag, $2]
  434. prev_end = $~.end(0)
  435. }
  436. if prev_end < size
  437. tokens << [:text, self[prev_end..-1]]
  438. end
  439. tokens
  440. end
  441. end