diff --git a/rfc2html.py b/rfc2html.py
index 0e84e56..8fa0e16 100644
--- a/rfc2html.py
+++ b/rfc2html.py
@@ -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):
@@ -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>\g<2>' % (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'\g<1> \g<2>\g<4> \g<5> \g<6>%s\g<7>%s' % (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'\g<1> \g<2>\g<4> \g<5>\n\g<6>\g<7>%s\g<8>%s' % (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'\g<1>\n\g<2>\g<3>\g<5> \g<6> \g<7>%s\g<8>%s' % (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'\g<1> \g<2>\g<4> \g<5> \g<6>%s\g<7>%s' % (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'\g<1> \g<2>\g<4> \g<5>\n\g<6>\g<7>%s\g<8>%s' % (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'\g<1>\n\g<2>\g<3>\g<5> \g<6> \g<7>%s\g<8>%s' % (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>\g<2>\g<4>\g<5>' % (script, extra), text)
# rfc and number on separate lines
text = re.sub(r"(?i)([^[/>\w-])(rfc([-]?))(\n +)([0-9]+)(\W)",
@@ -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'\g<1>\g<2>', text)
- text = re.sub(r"(?i)(section)\n(\s+)(\d+(\.\d+)*)", r'\g<1>\n\g<2>\g<3>', 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'\g<1>\g<2>', text)
+ text = re.sub(r"(?i)(?)(section)\n(\s+)(\d+(\.\d+)*)", r'\g<1>\n\g<2>\g<3>', text)
# Special cases for licensing boilerplate
text = text.replace('Section 4.e of the Trust Legal Provisions',
@@ -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' \g<1>\g<2>', text)
+ # appendix number link markup
+ text = re.sub(r" ([Aa]ppendix[ \t])([A-Z](\.\d+)*)", r' \g<1>\g<2>', text)
text = re.sub(r" ([Aa]ppendix)\n(\s+)([A-Z](\.\d+)*)", r' \g<1>\n\g<2>\g<3>', text)
# # section x of draft-y markup
@@ -391,16 +418,19 @@ def appendix_replacement(match):
# remove section link for section x.x (of|in)
old = text
- text = re.sub(r'(?i)]*>(section\s)(\d+(\.\d+)*)(\.?[a-z]*\s+(of|in)\s+)(\[?)]*)>(.*)(\]?)',
+ text = re.sub(r'(?i)]*>(section\s+)(\d+(\.\d+)*)(\.?[a-z]*\s+(of|in)\s+)(\[?)]*)>(.*)(\]?)',
r'\g<1>\g<2>\g<4>\g<6>>\g<9>\g<10>', text)
- text = re.sub(r'(?i)(\[?)]*)>(.*?)(\]?,\s+)]*>(section\s)(\d+(\.\d+)*)',
+ text = re.sub(r'(?i)(\[?)]*)>(.*?)(\]?,\s+)]*>(section\s+)(\d+(\.\d+)*)',
r'\g<1>>\g<4>\g<5>\g<6>\g<7>', text)
# Special fix for referring to the trust legal provisons in
# boilerplate text:
- text = re.sub(r'(?i)]*>(section\s)(\d+(\.\d+)*)(\.?[a-z]*\s+(of|in)\s*\n\s*the Trust Legal Provisions)',
+ text = re.sub(r'(?i)]*>(section\s+)(\d+(\.\d+)*)(\.?[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", "", text)
text = re.sub(r"\n?\f\n?", '\n', text)
diff --git a/tests.py b/tests.py
index edeeb76..b0d297b 100644
--- a/tests.py
+++ b/tests.py
@@ -37,6 +37,63 @@ def test_draft_ref_with_linebreak_in_header(self):
open_tag='',
))
+ def test_cross_rfc_section_simple(self):
+ html = markup('Section 2.4 of RFC 2595')
+ self.assertEqual(html, 'Section 2.4 of RFC 2595
')
+
+ def test_cross_rfc_appendix_with_description(self):
+ html = markup('Appendix B (Examples) of RFC 5678')
+ self.assertEqual(html, 'Appendix B (Examples) of RFC 5678
')
+
+ 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 = ('See Section 2.1 for details, '
+ 'but also Section 2.4 of RFC 2595 for comparison.
')
+ self.assertEqual(html, expected)
+
+ def test_cross_bcp_section(self):
+ html = markup('Section 3 of BCP 14')
+ self.assertEqual(html, 'Section 3 of BCP 14
')
+
+ def test_cross_std_section(self):
+ html = markup('Section 1.2 of STD 1')
+ self.assertEqual(html, 'Section 1.2 of STD 1
')
+
+ 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 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('RFC-793 Section ', html)
+ self.assertIn('3.3', html)
+ # Ensure the newline is preserved (line count should be 1)
+ content = html.replace('', '').replace('', '')
+ self.assertEqual(content.count('\n'), 1)
+
if __name__ == '__main__':
import unittest