@@ -76,6 +76,7 @@ class HybridMarkdown
7676 PLACEHOLDER_RE = /\0 (\d +)\0 / . freeze
7777 ESCAPABLE_CHARS_RE = /\\ ([!"#$%&'()*+,\- .\/ :;<=>?@\[ \\ \] ^_`{|}~])/ . freeze
7878 AUTOLINK_RE = /<([A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\s ]*|[A-Za-z0-9.!#$%&'*+\/ =?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\. [A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+)>/ . freeze
79+ TAB_WIDTH = 4
7980
8081 def initialize ( text )
8182 @references = { }
@@ -231,7 +232,7 @@ def parse_yard_indented_code(lines, index)
231232
232233 while index < lines . length
233234 line = lines [ index ]
234- break unless blank_line? ( line ) || line =~ /^(?: {2,}| \t )/
235+ break unless blank_line? ( line ) || indented_code_start? ( line )
235236 body << line
236237 index += 1
237238 end
@@ -280,7 +281,7 @@ def parse_list(lines, index)
280281 list_indent = marker [ :indent ]
281282
282283 while index < lines . length
283- break if items . any? && thematic_break? ( lines [ index ] ) && leading_spaces ( lines [ index ] ) <= list_indent + 3
284+ break if items . any? && thematic_break? ( lines [ index ] ) && leading_columns ( lines [ index ] ) <= list_indent + 3
284285
285286 item_marker = parse_list_marker ( lines [ index ] )
286287 break unless item_marker && same_list_type? ( marker , item_marker )
@@ -318,13 +319,14 @@ def parse_list(lines, index)
318319 if blank_line? ( line )
319320 item_lines << "\n "
320321 blank_seen = true
321- elsif blank_seen && ( line . start_with? ( "\t " ) || leading_spaces ( line ) >= content_indent )
322- break if first_line . empty? && item_lines . all? { |item_line | item_line == "\n " } && !line . start_with? ( "\t " ) && leading_spaces ( line ) == content_indent
322+ elsif blank_seen && indented_to? ( line , content_indent )
323+ break if first_line . empty? && item_lines . all? { |item_line | item_line == "\n " } &&
324+ leading_columns ( line ) == content_indent
323325 item_loose = true if loose_list_item_continuation? ( item_lines )
324326 stripped = strip_list_item_indent ( line , content_indent )
325327 item_lines << stripped
326328 blank_seen = false
327- elsif !blank_seen && ( line . start_with? ( " \t " ) || leading_spaces ( line ) >= content_indent )
329+ elsif !blank_seen && indented_to? ( line , content_indent )
328330 stripped = strip_list_item_indent ( line , content_indent )
329331 item_lines << stripped
330332 blank_seen = false
@@ -432,9 +434,8 @@ def parse_blockquote(lines, index)
432434 if blank_line? ( line )
433435 quoted_lines << "\n "
434436 previous_blank = true
435- elsif blockquote_start? ( line )
436- line =~ BLOCKQUOTE_RE
437- quoted_lines << "#{ $1} \n "
437+ elsif ( stripped = strip_blockquote_marker ( line ) )
438+ quoted_lines << stripped
438439 saw_quote = true
439440 previous_blank = false
440441 else
@@ -809,25 +810,25 @@ def fenced_code_start?(line)
809810 end
810811
811812 def indented_code_start? ( line )
812- line =~ /^(?: {2,}| \t )/
813+ leading_columns ( line ) >= 2
813814 end
814815
815816 def indented_code_block_start? ( lines , index )
816817 return false unless indented_code_start? ( lines [ index ] )
817- return true if lines [ index ] =~ /^(?: {4,}| \t )/
818+ return true if leading_columns ( lines [ index ] ) >= 4
818819
819820 !index . zero? && blank_line? ( lines [ index - 1 ] )
820821 end
821822
822823 def yard_indented_code_start? ( lines , index )
823- return false unless lines [ index ] =~ /^(?: {2,}|\t )!!!([\w .+-]+)[ \t ]*$/
824+ return false unless leading_columns ( lines [ index ] ) >= 2
825+ return false unless consume_columns ( lines [ index ] , 2 ) =~ /^!!!([\w .+-]+)[ \t ]*$/
824826 return false if index + 1 >= lines . length
825827
826- indented_code_block_start? ( lines , index ) && lines [ index + 1 ] =~ /^(?: {2,}| \t )/
828+ indented_code_block_start? ( lines , index ) && indented_code_start? ( lines [ index + 1 ] )
827829 end
828830
829831 def list_start? ( line , interrupt_paragraph = false )
830- return true if line =~ UNORDERED_LIST_RE || line =~ RDOC_ORDERED_LIST_RE
831832 return false unless ( marker = parse_list_marker ( line ) )
832833 return true unless interrupt_paragraph
833834
@@ -843,7 +844,7 @@ def labeled_list_start?(lines, index)
843844 end
844845
845846 def blockquote_start? ( line )
846- line =~ BLOCKQUOTE_RE
847+ ! strip_blockquote_marker ( line ) . nil?
847848 end
848849
849850 def html_block_start? ( line , interrupt_paragraph = false )
@@ -875,26 +876,14 @@ def table_alignment(cell)
875876
876877 def unindent ( lines )
877878 indent = lines . reject { |line | blank_line? ( line ) } . map do |line |
878- line . start_with? ( " \t " ) ? 4 : line [ / \A +/ , 0 ] . to_s . length
879+ leading_columns ( line )
879880 end . min || 4
880881
881- lines . map do |line |
882- if line . start_with? ( "\t " )
883- line . sub ( /^\t / , '' )
884- else
885- line . sub ( /\A {0,#{ indent } }/ , '' )
886- end
887- end . join
882+ lines . map { |line | consume_columns ( line , indent ) } . join
888883 end
889884
890885 def unindent_indented_code ( lines )
891- lines . map do |line |
892- if line . start_with? ( "\t " )
893- line . sub ( /^\t / , '' )
894- else
895- line . sub ( /^ {0,4}/ , '' )
896- end
897- end . join
886+ lines . map { |line | consume_columns ( line , 4 ) } . join
898887 end
899888
900889 def code_block ( text , lang = nil )
@@ -961,29 +950,58 @@ def strip_fenced_indent(line, indent)
961950 end
962951
963952 def parse_list_marker ( line )
964- if line =~ /^(\s {0,3})([*+-])([ \t ]*)(.*)$/
965- match = Regexp . last_match
966- return nil if match [ 3 ] . empty? && !match [ 4 ] . empty?
967-
968- return { :ordered => false , :bullet => match [ 2 ] , :indent => match [ 1 ] . length ,
969- :marker_length => 1 , :padding => match [ 3 ] . length , :content => match [ 4 ] }
953+ source = line . to_s . sub ( /\n \z / , '' )
954+ indent , index = scan_leading_columns ( source )
955+ return nil if indent > 3
956+ return nil if index >= source . length
957+
958+ char = source [ index , 1 ]
959+ current_column = indent
960+
961+ if '*+-' . include? ( char )
962+ marker_length = 1
963+ marker_end = index + 1
964+ current_column += 1
965+ padding , marker_end = scan_padding_columns ( source , marker_end , current_column )
966+ content = source [ marker_end ..-1 ] . to_s
967+ return nil if padding . zero? && !content . empty?
968+
969+ return { :ordered => false , :bullet => char , :indent => indent ,
970+ :marker_length => marker_length , :padding => padding , :content => content }
970971 end
971972
972- if line =~ /^(\s {0,3})(\d {1,9})([.)])([ \t ]*)(.*)$/
973- match = Regexp . last_match
974- return nil if match [ 4 ] . empty? && !match [ 5 ] . empty?
975-
976- return { :ordered => true , :delimiter => match [ 3 ] , :start => match [ 2 ] . to_i ,
977- :indent => match [ 1 ] . length , :marker_length => match [ 2 ] . length + 1 ,
978- :padding => match [ 4 ] . length , :content => match [ 5 ] }
973+ number = source [ index ..-1 ] [ /^\d {1,9}/ ]
974+ if number
975+ marker_end = index + number . length
976+ delimiter = source [ marker_end , 1 ]
977+ if delimiter == '.' || delimiter == ')'
978+ marker_length = number . length + 1
979+ current_column += marker_length
980+ marker_end += 1
981+ padding , marker_end = scan_padding_columns ( source , marker_end , current_column )
982+ content = source [ marker_end ..-1 ] . to_s
983+ return nil if padding . zero? && !content . empty?
984+
985+ return { :ordered => true , :delimiter => delimiter , :start => number . to_i ,
986+ :indent => indent , :marker_length => marker_length ,
987+ :padding => padding , :content => content }
988+ end
979989 end
980990
981- return nil unless line =~ /^\s {0,3}([A-Za-z])\. (?:[ \t ]+(.*))$/
991+ if source [ index , 2 ] =~ /\A [A-Za-z]\. \z /
992+ marker_length = 2
993+ marker_end = index + marker_length
994+ current_column += marker_length
995+ padding , marker_end = scan_padding_columns ( source , marker_end , current_column )
996+ content = source [ marker_end ..-1 ] . to_s
997+ return nil if padding . zero? && !content . empty?
998+
999+ return { :ordered => true , :delimiter => '.' , :start => 1 ,
1000+ :indent => indent , :marker_length => marker_length ,
1001+ :padding => padding , :content => content }
1002+ end
9821003
983- match = Regexp . last_match
984- { :ordered => true , :delimiter => '.' , :start => 1 ,
985- :indent => line [ /\A */ ] . to_s . length , :marker_length => 2 , :padding => 1 ,
986- :content => ( match [ 2 ] || '' ) }
1004+ nil
9871005 end
9881006
9891007 def list_item_padding ( marker )
@@ -1403,7 +1421,7 @@ def whitespace_char?(char)
14031421 def punctuation_char? ( char )
14041422 return false if char . nil?
14051423
1406- char =~ /[[:punct:]]/ || unicode_symbol_char? ( char )
1424+ ascii_punctuation_char? ( char ) || unicode_symbol_char? ( char )
14071425 end
14081426
14091427 def unicode_symbol_char? ( char )
@@ -1420,20 +1438,28 @@ def unicode_symbol_char?(char)
14201438 ( 0x20A0 ..0x20CF ) . include? ( codepoint )
14211439 end
14221440
1423- def leading_spaces ( line )
1424- line [ /\A */ , 0 ] . to_s . length
1441+ def ascii_punctuation_char? ( char )
1442+ return false unless ascii_only_compat? ( char )
1443+
1444+ byte = char . to_s . unpack ( 'C' ) . first
1445+ return false unless byte
1446+
1447+ ( 0x21 ..0x2F ) . include? ( byte ) ||
1448+ ( 0x3A ..0x40 ) . include? ( byte ) ||
1449+ ( 0x5B ..0x60 ) . include? ( byte ) ||
1450+ ( 0x7B ..0x7E ) . include? ( byte )
1451+ end
1452+
1453+ def leading_columns ( line )
1454+ scan_leading_columns ( line . to_s ) . first
14251455 end
14261456
14271457 def indented_to? ( line , indent )
1428- line . start_with? ( " \t " ) || leading_spaces ( line ) >= indent
1458+ leading_columns ( line ) >= indent
14291459 end
14301460
14311461 def strip_list_item_indent ( line , content_indent )
1432- if line . start_with? ( "\t " )
1433- line . sub ( /^\t / , '' )
1434- else
1435- line . sub ( /\A {0,#{ content_indent } }/ , '' )
1436- end
1462+ consume_columns ( line , content_indent , 0 , true )
14371463 end
14381464
14391465 def escape_url ( url )
@@ -1525,9 +1551,9 @@ def split_reference_container_prefix(line)
15251551 prefix = ''
15261552 content = line . chomp
15271553
1528- while content =~ / \A ( \s {0,3}> ?)(.*) \z /
1529- prefix << $1
1530- content = $2
1554+ while ( split = split_blockquote_prefix ( content ) )
1555+ prefix << split [ 0 ]
1556+ content = split [ 1 ] . chomp
15311557 end
15321558
15331559 [ prefix , content ]
@@ -1681,6 +1707,102 @@ def split_lines(text)
16811707 text . to_s . split ( /^/ , -1 )
16821708 end
16831709
1710+ def scan_leading_columns ( text )
1711+ index = 0
1712+ column = 0
1713+ source = text . to_s
1714+
1715+ while index < source . length
1716+ char = source [ index , 1 ]
1717+ if char == ' '
1718+ column += 1
1719+ elsif char == "\t "
1720+ column += TAB_WIDTH - ( column % TAB_WIDTH )
1721+ else
1722+ break
1723+ end
1724+ index += 1
1725+ end
1726+
1727+ [ column , index ]
1728+ end
1729+
1730+ def scan_padding_columns ( text , index , start_column )
1731+ column = start_column
1732+ padding = 0
1733+ source = text . to_s
1734+
1735+ while index < source . length
1736+ char = source [ index , 1 ]
1737+ if char == ' '
1738+ column += 1
1739+ padding += 1
1740+ elsif char == "\t "
1741+ advance = TAB_WIDTH - ( column % TAB_WIDTH )
1742+ column += advance
1743+ padding += advance
1744+ else
1745+ break
1746+ end
1747+ index += 1
1748+ end
1749+
1750+ [ padding , index ]
1751+ end
1752+
1753+ def consume_columns ( text , columns , start_column = 0 , normalize_remaining = false )
1754+ index = 0
1755+ column = start_column
1756+ remaining = columns
1757+ prefix_width = 0
1758+ source = text . to_s
1759+
1760+ while index < source . length && remaining > 0
1761+ char = source [ index , 1 ]
1762+ if char == ' '
1763+ column += 1
1764+ remaining -= 1
1765+ index += 1
1766+ elsif char == "\t "
1767+ advance = TAB_WIDTH - ( column % TAB_WIDTH )
1768+ if advance <= remaining
1769+ column += advance
1770+ remaining -= advance
1771+ index += 1
1772+ else
1773+ prefix_width += advance - remaining if normalize_remaining
1774+ column += advance
1775+ remaining = 0
1776+ index += 1
1777+ end
1778+ else
1779+ break
1780+ end
1781+ end
1782+
1783+ if normalize_remaining
1784+ while index < source . length
1785+ char = source [ index , 1 ]
1786+ if char == ' '
1787+ prefix_width += 1
1788+ column += 1
1789+ index += 1
1790+ elsif char == "\t "
1791+ advance = TAB_WIDTH - ( column % TAB_WIDTH )
1792+ prefix_width += advance
1793+ column += advance
1794+ index += 1
1795+ else
1796+ break
1797+ end
1798+ end
1799+
1800+ ( ' ' * prefix_width ) + source [ index ..-1 ] . to_s
1801+ else
1802+ source [ index ..-1 ] . to_s
1803+ end
1804+ end
1805+
16841806 def lazy_blockquote_continuation? ( quoted_lines , line )
16851807 return false if block_boundary? ( line )
16861808 return false if indented_code_start? ( line ) && !blockquote_paragraph_context? ( quoted_lines )
@@ -1726,6 +1848,27 @@ def normalize_heading_line(line)
17261848 normalize_paragraph_line ( line ) . rstrip
17271849 end
17281850
1851+ def split_blockquote_prefix ( line )
1852+ source = line . to_s
1853+ indent , index = scan_leading_columns ( source )
1854+ return nil if indent > 3
1855+ return nil unless source [ index , 1 ] == '>'
1856+
1857+ prefix = source [ 0 ..index ]
1858+ rest = source [ ( index + 1 ) ..-1 ] . to_s
1859+ if rest . start_with? ( ' ' ) || rest . start_with? ( "\t " )
1860+ prefix << rest [ 0 , 1 ]
1861+ rest = consume_columns ( rest , 1 , indent + 1 , true )
1862+ end
1863+
1864+ [ prefix , rest . end_with? ( "\n " ) ? rest : "#{ rest } \n " ]
1865+ end
1866+
1867+ def strip_blockquote_marker ( line )
1868+ split = split_blockquote_prefix ( line )
1869+ split && split [ 1 ]
1870+ end
1871+
17291872 def loose_list_item_continuation? ( item_lines )
17301873 return false if open_fence_in_lines? ( item_lines )
17311874
0 commit comments