Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions rfc2html.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

BOM_CODE = 65279

# Constants for cross-reference markers using Unicode private use area
CROSSREF_START_TAG = "\uE000"
CROSSREF_END_TAG = "\uE001"


def markup(text, path=".", script="", extra="", name=None):

Expand Down Expand Up @@ -201,9 +205,31 @@ def replacement(match):
text = re.sub(r"([^/#=\?\w>=-])(draft-[-a-zA-Z0-9]+[a-zA-Z0-9](.txt)?)",
r'\g<1><a href="%s?%sdraft=\g<2>">\g<2></a>' % (script, extra), text)

# Handle cross-RFC section references BEFORE RFC linking
for n in ['rfc', 'bcp', 'fyi', 'std']:
# section x of rfc y markup (unlinked) - mark RFC as processed with special marker
# Use more restrictive pattern to avoid matching across sentences
# Pattern for same-line references (don't match newlines)
text = re.sub(r"(?i)(section)\s+(\d+(\.\d+)*)([^.\n]*?)[ \t](of|in)[ \t]+(%s[- ]?)(\d+)" % n,
r'<a href="%s?%s%s=\g<7>#section-\g<2>">\g<1>&nbsp;\g<2>\g<4> \g<5> \g<6>%s\g<7>%s</a>' % (script, extra, n, CROSSREF_START_TAG, CROSSREF_END_TAG), text)
# Pattern for when "of/in RFC" spans lines (preserve the newline)
text = re.sub(r"(?i)(section)\s+(\d+(\.\d+)*)([^.\n]*?)[ \t](of|in)\n([ \t]+)(%s[- ]?)(\d+)" % n,
r'<a href="%s?%s%s=\g<8>#section-\g<2>">\g<1>&nbsp;\g<2>\g<4> \g<5>\n\g<6>\g<7>%s\g<8>%s</a>' % (script, extra, n, CROSSREF_START_TAG, CROSSREF_END_TAG), text)
text = re.sub(r"(?i)(section)\n(\s+)(\d+(\.\d+)*)([^.\n]*?)\s(of|in)\s+(%s[- ]?)(\d+)" % n,
r'<a href="%s?%s%s=\g<8>#section-\g<3>">\g<1></a>\n\g<2><a href="%s?%s%s=\g<8>#section-\g<3>">\g<3>\g<5> \g<6> \g<7>%s\g<8>%s</a>' % (script, extra, n, script, extra, n, CROSSREF_START_TAG, CROSSREF_END_TAG), text)
# appendix x of rfc y markup (unlinked)
# Pattern for same-line references (don't match newlines)
text = re.sub(r"(?i)(appendix)\s+([A-Z](\.\d+)*)([^.\n]*?)[ \t](of|in)[ \t]+(%s[- ]?)(\d+)" % n,
r'<a href="%s?%s%s=\g<7>#appendix-\g<2>">\g<1>&nbsp;\g<2>\g<4> \g<5> \g<6>%s\g<7>%s</a>' % (script, extra, n, CROSSREF_START_TAG, CROSSREF_END_TAG), text)
# Pattern for when "of/in RFC" spans lines (preserve the newline)
text = re.sub(r"(?i)(appendix)\s+([A-Z](\.\d+)*)([^.\n]*?)[ \t](of|in)\n([ \t]+)(%s[- ]?)(\d+)" % n,
r'<a href="%s?%s%s=\g<8>#appendix-\g<2>">\g<1>&nbsp;\g<2>\g<4> \g<5>\n\g<6>\g<7>%s\g<8>%s</a>' % (script, extra, n, CROSSREF_START_TAG, CROSSREF_END_TAG), text)
text = re.sub(r"(?i)(appendix)\n(\s+)([A-Z](\.\d+)*)([^.\n]*?)\s(of|in)\s+(%s[- ]?)(\d+)" % n,
r'<a href="%s?%s%s=\g<8>#appendix-\g<3>">\g<1></a>\n\g<2><a href="%s?%s%s=\g<8>#appendix-\g<3>">\g<3>\g<5> \g<6> \g<7>%s\g<8>%s</a>' % (script, extra, n, script, extra, n, CROSSREF_START_TAG, CROSSREF_END_TAG), text)

# rfc markup
# rfc and number on the same line
text = re.sub(r'(?i)([^[/>\w-])(rfc([- ]?))([0-9]+)(\W)',
# rfc and number on the same line (skip RFC numbers already processed in cross-refs)
text = re.sub(r'(?i)([^[/>\w\-' + re.escape(CROSSREF_START_TAG) + r'])(rfc([- ]?))([0-9]+)(?!' + re.escape(CROSSREF_END_TAG) + r')(\W)',
r'\g<1><a href="%s?%srfc=\g<4>">\g<2>\g<4></a>\g<5>' % (script, extra), text)
# rfc and number on separate lines
text = re.sub(r"(?i)([^[/>\w-])(rfc([-]?))(\n +)([0-9]+)(\W)",
Expand Down Expand Up @@ -309,9 +335,10 @@ def section_anchor_replacement(match):

text = re.sub(r"(?im)^(\d+(\.\d+)*)(\.?[ ]+\S.*?(\n +\w+.*)?( |$))", section_anchor_replacement, text)
#text = re.sub("(?i)(\n *\n *)(\d+(\.\d+)*)(\.?[ ].*)", section_replacement, text)
# section number link markup
text = re.sub(r"(?i)(section\s)(\d+(\.\d+)*)", r'<a href="#section-\g<2>">\g<1>\g<2></a>', text)
text = re.sub(r"(?i)(section)\n(\s+)(\d+(\.\d+)*)", r'<a href="#section-\g<3>">\g<1></a>\n\g<2><a href="#section-\g<3>">\g<3></a>', text)
# section number link markup (only for local sections not already inside links)
# Use negative lookbehind to avoid matching inside existing links
text = re.sub(r"(?i)(?<!>)(section[ \t]+)(\d+(\.\d+)*)", r'<a href="#section-\g<2>">\g<1>\g<2></a>', text)
text = re.sub(r"(?i)(?<!>)(section)\n(\s+)(\d+(\.\d+)*)", r'<a href="#section-\g<3>">\g<1></a>\n\g<2><a href="#section-\g<3>">\g<3></a>', text)

# Special cases for licensing boilerplate
text = text.replace('<a href="#section-4">Section 4</a>.e of the Trust Legal Provisions',
Expand Down Expand Up @@ -340,8 +367,8 @@ def appendix_replacement(match):

text = re.sub(r"(?m)^(Appendix |)([A-Z](\.|\.\d+)+)(\.?[ ].*)$", appendix_replacement, text)
#text = re.sub("(?i)(\n *\n *)(\d+(\.\d+)*)(\.?[ ].*)", appendix_replacement, text)
# appendix number link markup
text = re.sub(r" ([Aa]ppendix\s)([A-Z](\.\d+)*)", r' <a href="#appendix-\g<2>">\g<1>\g<2></a>', text)
# appendix number link markup
text = re.sub(r" ([Aa]ppendix[ \t])([A-Z](\.\d+)*)", r' <a href="#appendix-\g<2>">\g<1>\g<2></a>', text)
text = re.sub(r" ([Aa]ppendix)\n(\s+)([A-Z](\.\d+)*)", r' <a href="#appendix-\g<3>">\g<1></a>\n\g<2><a href="#appendix-\g<3>">\g<3></a>', text)

# # section x of draft-y markup
Expand Down Expand Up @@ -391,16 +418,19 @@ def appendix_replacement(match):

# remove section link for section x.x (of|in) <something else>
old = text
text = re.sub(r'(?i)<a href="[^"]*"[^>]*>(section\s)(\d+(\.\d+)*)</a>(\.?[a-z]*\s+(of|in)\s+)(\[?)<a href="([^"]*)"([^>]*)>(.*)</a>(\]?)',
text = re.sub(r'(?i)<a href="[^"]*"[^>]*>(section\s+)(\d+(\.\d+)*)</a>(\.?[a-z]*\s+(of|in)\s+)(\[?)<a href="([^"]*)"([^>]*)>(.*)</a>(\]?)',
r'\g<1>\g<2>\g<4>\g<6><a href="\g<7>"\g<8>>\g<9></a>\g<10>', text)
text = re.sub(r'(?i)(\[?)<a href="([^"]*#ref[^"]*)"([^>]*)>(.*?)</a>(\]?,\s+)<a href="[^"]*"[^>]*>(section\s)(\d+(\.\d+)*)</a>',
text = re.sub(r'(?i)(\[?)<a href="([^"]*#ref[^"]*)"([^>]*)>(.*?)</a>(\]?,\s+)<a href="[^"]*"[^>]*>(section\s+)(\d+(\.\d+)*)</a>',
r'\g<1><a href="\g<2>"\g<3>>\g<4></a>\g<5>\g<6>\g<7>', text)

# Special fix for referring to the trust legal provisons in
# boilerplate text:
text = re.sub(r'(?i)<a href="[^"]*"[^>]*>(section\s)(\d+(\.\d+)*)</a>(\.?[a-z]*\s+(of|in)\s*\n\s*the Trust Legal Provisions)',
text = re.sub(r'(?i)<a href="[^"]*"[^>]*>(section\s+)(\d+(\.\d+)*)</a>(\.?[a-z]*\s+(of|in)\s*\n\s*the Trust Legal Provisions)',
r'\g<1>\g<2>\g<4>', text)

# Clean up cross-RFC markers - convert them back to normal RFC text
text = re.sub(re.escape(CROSSREF_START_TAG) + r'(\d+)' + re.escape(CROSSREF_END_TAG), r'\1', text)

#
#text = re.sub("\f", "<div class=\"newpage\" />", text)
text = re.sub(r"\n?\f\n?", '</pre>\n<pre class="newpage">', text)
Expand Down
57 changes: 57 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,63 @@ def test_draft_ref_with_linebreak_in_header(self):
open_tag='<a href="./draft-ietf-some-name-00">',
))

def test_cross_rfc_section_simple(self):
html = markup('Section 2.4 of RFC 2595')
self.assertEqual(html, '<pre><a href="./rfc2595#section-2.4">Section&nbsp;2.4 of RFC 2595</a></pre>')

def test_cross_rfc_appendix_with_description(self):
html = markup('Appendix B (Examples) of RFC 5678')
self.assertEqual(html, '<pre><a href="./rfc5678#appendix-B">Appendix&nbsp;B (Examples) of RFC 5678</a></pre>')

def test_mixed_same_and_cross_rfc_sections(self):
html = markup('See Section 2.1 for details, but also Section 2.4 of RFC 2595 for comparison.')
expected = ('<pre>See <a href="#section-2.1">Section 2.1</a> for details, '
'but also <a href="./rfc2595#section-2.4">Section&nbsp;2.4 of RFC 2595</a> for comparison.</pre>')
self.assertEqual(html, expected)

def test_cross_bcp_section(self):
html = markup('Section 3 of BCP 14')
self.assertEqual(html, '<pre><a href="./bcp14#section-3">Section&nbsp;3 of BCP 14</a></pre>')

def test_cross_std_section(self):
html = markup('Section 1.2 of STD 1')
self.assertEqual(html, '<pre><a href="./std1#section-1.2">Section&nbsp;1.2 of STD 1</a></pre>')

def test_rfc7817_abstract_example(self):
text = ('It replaces Section 2.4 (Server Identity Check) of RFC 2595 and updates '
'Section 4.1 (Processing After the STARTTLS Command) of RFC 3207, '
'Section 11.1 (STARTTLS Security Considerations) of RFC 3501, and '
'Section 2.2.1 (Server Identity Check) of RFC 5804.')
html = markup(text)

# Check that all cross-RFC section references are correctly linked
self.assertIn('href="./rfc2595#section-2.4"', html)
self.assertIn('href="./rfc3207#section-4.1"', html)
self.assertIn('href="./rfc3501#section-11.1"', html)
self.assertIn('href="./rfc5804#section-2.2.1"', html)

# Ensure no local section links for cross-RFC references
self.assertNotIn('href="#section-2.4"', html)
self.assertNotIn('href="#section-4.1"', html)
self.assertNotIn('href="#section-11.1"', html)
self.assertNotIn('href="#section-2.2.1"', html)

def test_cross_rfc_section_with_newline(self):
html = markup('Section 5.1 of\n RFC 4279')
# The newline should be preserved within the link, and nbsp should be used between Section and number
self.assertIn('Section&nbsp;5.1 of\n RFC 4279', html)
self.assertIn('href="./rfc4279#section-5.1"', html)

def test_rfc_section_split_across_lines(self):
# Test case for issue where "RFC-XXX Section\n Y.Z" was being merged into one anchor
html = markup(' 4.2.2.9 Initial Sequence Number Selection: RFC-793 Section\n 3.3, page 27')
# Should have two separate anchor tags with newline preserved
self.assertIn('<a href="./rfc793#section-3.3">RFC-793 Section&nbsp;</a>', html)
self.assertIn('<a href="#section-3.3">3.3</a>', html)
# Ensure the newline is preserved (line count should be 1)
content = html.replace('<pre>', '').replace('</pre>', '')
self.assertEqual(content.count('\n'), 1)


if __name__ == '__main__':
import unittest
Expand Down