From f7178f5024feae68d6c9c411b1acfbf68c70eb7c Mon Sep 17 00:00:00 2001 From: Logan Gore Date: Wed, 17 Dec 2025 10:31:58 -0500 Subject: [PATCH 1/2] feat: Properly link to cross-referenced RFCs --- rfc2html.py | 50 ++++++++++++++++++++++++++++++++++++---------- tests.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/rfc2html.py b/rfc2html.py index 0e84e56..9182a55 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+)*)([^.]*?)[ \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+)*)([^.]*?)[ \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+)*)([^.]*?)\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+)*)([^.]*?)[ \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+)*)([^.]*?)[ \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+)*)([^.]*?)\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 From d32de8213b9a521f6313f4a64b4ba4fa1b8d2ddb Mon Sep 17 00:00:00 2001 From: Logan Gore Date: Thu, 18 Dec 2025 14:37:14 -0500 Subject: [PATCH 2/2] Another bugfix --- rfc2html.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rfc2html.py b/rfc2html.py index 9182a55..8fa0e16 100644 --- a/rfc2html.py +++ b/rfc2html.py @@ -210,21 +210,21 @@ def replacement(match): # 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+)*)([^.]*?)[ \t](of|in)[ \t]+(%s[- ]?)(\d+)" % n, + 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+)*)([^.]*?)[ \t](of|in)\n([ \t]+)(%s[- ]?)(\d+)" % n, + 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+)*)([^.]*?)\s(of|in)\s+(%s[- ]?)(\d+)" % n, + 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+)*)([^.]*?)[ \t](of|in)[ \t]+(%s[- ]?)(\d+)" % n, + 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+)*)([^.]*?)[ \t](of|in)\n([ \t]+)(%s[- ]?)(\d+)" % n, + 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+)*)([^.]*?)\s(of|in)\s+(%s[- ]?)(\d+)" % n, + 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