| 960 |
960 |
|
| 961 |
961 |
text = text.dup
|
| 962 |
962 |
macros = catch_macros(text)
|
|
963 |
contains_macros = macros.any?
|
| 963 |
964 |
|
| 964 |
965 |
if options[:formatting] == false
|
| 965 |
966 |
text = h(text)
|
| ... | ... | |
| 978 |
979 |
end
|
| 979 |
980 |
end
|
| 980 |
981 |
parse_sections(text, project, obj, attr, only_path, options)
|
|
982 |
fix_paragraphs_with_block_level_content(text) if contains_macros
|
| 981 |
983 |
parse_headings(text, project, obj, attr, only_path, options)
|
| 982 |
984 |
|
| 983 |
985 |
if @parsed_headings.any?
|
| ... | ... | |
| 1498 |
1500 |
end
|
| 1499 |
1501 |
end
|
| 1500 |
1502 |
|
|
1503 |
unless const_defined?(:PARAGRAPH_TAG_RE)
|
|
1504 |
PARAGRAPH_TAG_RE = /<p(\s[^>]*)?>/i
|
|
1505 |
PARAGRAPH_BOUNDARY_RE = %r{</?p(?:\s[^>]*)?>}i
|
|
1506 |
BLOCK_LEVEL_CONTENT_RE =
|
|
1507 |
%r{<(?:address|blockquote|div|dl|fieldset|figure|form|h[1-6]|hr|ol|p|pre|section|table|ul)[\s/>]}i
|
|
1508 |
end
|
|
1509 |
|
|
1510 |
# Macros are replaced by a placeholder before the text is formatted and are
|
|
1511 |
# executed afterwards, so a macro standing on its own line has its output
|
|
1512 |
# injected into the paragraph the formatter built for that placeholder.
|
|
1513 |
# Macros returning block-level content ({{include}}, {{collapse}} and macros
|
|
1514 |
# provided by plugins) therefore produce block-level elements inside a <p>,
|
|
1515 |
# which is invalid: browsers close the paragraph before the block element and
|
|
1516 |
# turn the then orphaned </p> into a second, empty paragraph. Both show up as
|
|
1517 |
# unexpected blank space around the macro output.
|
|
1518 |
# Such paragraphs are turned into divs to keep the markup valid.
|
|
1519 |
def fix_paragraphs_with_block_level_content(text)
|
|
1520 |
return text unless text.include?('</p>')
|
|
1521 |
|
|
1522 |
result = +''
|
|
1523 |
scanner = StringScanner.new(text)
|
|
1524 |
until scanner.eos?
|
|
1525 |
preceding = scanner.scan_until(PARAGRAPH_TAG_RE)
|
|
1526 |
if preceding.nil?
|
|
1527 |
result << scanner.rest
|
|
1528 |
break
|
|
1529 |
end
|
|
1530 |
|
|
1531 |
opening_tag = scanner.matched
|
|
1532 |
attributes = scanner[1]
|
|
1533 |
result << preceding[0...-opening_tag.length]
|
|
1534 |
content = scan_paragraph_content(scanner)
|
|
1535 |
|
|
1536 |
if content.nil?
|
|
1537 |
# Unbalanced paragraph, leave the remaining text untouched
|
|
1538 |
result << opening_tag << scanner.rest
|
|
1539 |
break
|
|
1540 |
elsif content.match?(BLOCK_LEVEL_CONTENT_RE)
|
|
1541 |
result << "<div#{attributes}>" << content << '</div>'
|
|
1542 |
else
|
|
1543 |
result << opening_tag << content << '</p>'
|
|
1544 |
end
|
|
1545 |
end
|
|
1546 |
text.replace(result)
|
|
1547 |
end
|
|
1548 |
|
|
1549 |
# Returns the content of the paragraph the scanner has just entered and moves
|
|
1550 |
# the scanner past the matching closing tag. Nested paragraphs are taken into
|
|
1551 |
# account, so that a paragraph belonging to injected macro output does not end
|
|
1552 |
# the enclosing one. Returns nil if the paragraph is never closed.
|
|
1553 |
def scan_paragraph_content(scanner)
|
|
1554 |
content = +''
|
|
1555 |
depth = 1
|
|
1556 |
until scanner.eos?
|
|
1557 |
chunk = scanner.scan_until(PARAGRAPH_BOUNDARY_RE)
|
|
1558 |
return nil if chunk.nil?
|
|
1559 |
|
|
1560 |
tag = scanner.matched
|
|
1561 |
content << chunk[0...-tag.length]
|
|
1562 |
if tag.start_with?('</')
|
|
1563 |
depth -= 1
|
|
1564 |
return content if depth.zero?
|
|
1565 |
else
|
|
1566 |
depth += 1
|
|
1567 |
end
|
|
1568 |
content << tag
|
|
1569 |
end
|
|
1570 |
nil
|
|
1571 |
end
|
|
1572 |
|
| 1501 |
1573 |
TOC_RE = /<p>\{\{((<|<)|(>|>))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
|
| 1502 |
1574 |
|
| 1503 |
1575 |
# Renders the TOC with given headings
|