rubypants.rb
上传用户:netsea168
上传日期:2022-07-22
资源大小:4652k
文件大小:16k
源码类别:

Ajax

开发平台:

Others

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