@@ -77,36 +77,56 @@ def copy_vendor_files(self, output_dir: str) -> None:
7777
7878 def rewrite_html_content (self , content : str ) -> str :
7979 """
80- Rewrite known CDN asset URLs in an HTML document to their corresponding local vendor paths.
81-
82- Replaces specific external CDN links (React, Babel, Tailwind, PrismJS, FontAwesome, etc.) with local /vendor/... paths so the returned HTML references vendored assets.
83-
80+ Rewrite CDN asset URLs in HTML to local /vendor/ paths.
81+
82+ Replaces known external CDN links (React, Babel, Tailwind, PrismJS, FontAwesome, etc.) with corresponding /vendor/... paths and removes `integrity` and `crossorigin` attributes from `<link>` and `<script>` tags that reference those local vendor files.
83+
84+ Parameters:
85+ content (str): HTML document content to rewrite.
86+
8487 Returns:
85- The input HTML string with matched CDN URLs substituted by local vendor URLs.
88+ str: The HTML content with matching CDN URLs substituted by local vendor URLs and SRI/crossorigin attributes stripped for vendored assets .
8689 """
8790 replacements = [
8891 # React
89- ('https://unpkg.com/react@18/ umd/react.development.js' , '/vendor/react/react.development.js' ),
90- ('https://unpkg.com/react@18/ umd/react.production.min.js' , '/vendor/react/react.production.min.js' ),
91- ('https://unpkg.com/react-dom@18/ umd/react-dom.development.js' , '/vendor/react-dom/react-dom.development.js' ),
92- ('https://unpkg.com/react-dom@18/ umd/react-dom.production.min.js' , '/vendor/react-dom/react-dom.production.min.js' ),
92+ (r 'https://unpkg\ .com/react@[^/]+/ umd/react\ .development\ .js' , '/vendor/react/react.development.js' ),
93+ (r 'https://unpkg\ .com/react@[^/]+/ umd/react\ .production\ .min\ .js' , '/vendor/react/react.production.min.js' ),
94+ (r 'https://unpkg\ .com/react-dom@[^/]+/ umd/react-dom\ .development\ .js' , '/vendor/react-dom/react-dom.development.js' ),
95+ (r 'https://unpkg\ .com/react-dom@[^/]+/ umd/react-dom\ .production\ .min\ .js' , '/vendor/react-dom/react-dom.production.min.js' ),
9396 # Babel
94- ('https://unpkg.com/@babel/standalone/ babel.min.js' , '/vendor/babel/babel.min.js' ),
95- ('https://unpkg.com/@babel/standalone/ babel.js' , '/vendor/babel/babel.min.js' ),
97+ (r 'https://unpkg\ .com/@babel/standalone(?:@[^/]+)?/ babel\ .min\ .js' , '/vendor/babel/babel.min.js' ),
98+ (r 'https://unpkg\ .com/@babel/standalone(?:@[^/]+)?/ babel\ .js' , '/vendor/babel/babel.min.js' ),
9699 # Tailwind
97- ('https://cdn.tailwindcss.com' , '/vendor/tailwindcss/script.js' ),
100+ (r 'https://cdn\ .tailwindcss\ .com(?:@[^/]+)? ' , '/vendor/tailwindcss/script.js' ),
98101 # PrismJS
99- # Handle minified vs unminified mapping. Node modules usually has unminified.
100- # We map the CDN .min.css requests to our local .css files (which we copied from node_modules)
101- ('https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css' , '/vendor/prismjs/themes/prism.css' ),
102- ('https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css' , '/vendor/prismjs/plugins/line-numbers/prism-line-numbers.css' ),
103- ('https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/toolbar/prism-toolbar.min.css' , '/vendor/prismjs/plugins/toolbar/prism-toolbar.css' ),
102+ (r'https://cdnjs\.cloudflare\.com/ajax/libs/prism/[^/]+/themes/prism\.min\.css' , '/vendor/prismjs/themes/prism.css' ),
103+ (r'https://cdnjs\.cloudflare\.com/ajax/libs/prism/[^/]+/plugins/line-numbers/prism-line-numbers\.min\.css' , '/vendor/prismjs/plugins/line-numbers/prism-line-numbers.css' ),
104+ (r'https://cdnjs\.cloudflare\.com/ajax/libs/prism/[^/]+/plugins/toolbar/prism-toolbar\.min\.css' , '/vendor/prismjs/plugins/toolbar/prism-toolbar.css' ),
104105 # FontAwesome
105- ('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/ css/all.min.css' , '/vendor/fontawesome/css/all.min.css' ),
106+ (r 'https://cdnjs\ .cloudflare\ .com/ajax/libs/font-awesome/[^/]+/ css/all\ .min\ .css' , '/vendor/fontawesome/css/all.min.css' ),
106107 ]
107108
108- for old , new in replacements :
109- content = content .replace (old , new )
109+ for pattern_str , new in replacements :
110+ content = re .sub (pattern_str , new , content )
111+
112+ # Strip integrity and crossorigin attributes from tags referencing local /vendor/ files
113+ def strip_sri (match ):
114+ """
115+ Remove Subresource Integrity (`integrity`) and `crossorigin` attributes from an HTML <link> or <script> tag if the tag references a `/vendor/` path.
116+
117+ Parameters:
118+ match (re.Match): A regex match object whose matched text is the full HTML tag.
119+
120+ Returns:
121+ str: The original tag text with `integrity` and `crossorigin` attributes removed when the tag contains `/vendor/`; otherwise the original tag text unchanged.
122+ """
123+ tag_text = match .group (0 )
124+ if '/vendor/' in tag_text :
125+ tag_text = re .sub (r'\s*integrity="[^"]+"' , '' , tag_text )
126+ tag_text = re .sub (r'\s*crossorigin="[^"]+"' , '' , tag_text )
127+ return tag_text
128+
129+ content = re .sub (r'<(?:link|script)[^>]+>' , strip_sri , content )
110130
111131 return content
112132
@@ -174,6 +194,12 @@ def generate_index(self) -> None:
174194 title = self .get_html_title (filepath )
175195 except Exception :
176196 title = os .path .basename (filepath )
197+
198+ # Append disambiguator if 'detailed' is in the filename
199+ if 'detailed' in filename .lower ():
200+ if '(detailed)' not in title .lower ():
201+ title += ' (detailed)'
202+
177203 structure [category ].append ((title , rel_path ))
178204
179205 # Sort categories and files
@@ -891,11 +917,11 @@ def generate_index(self) -> None:
891917 def render_category_files (structure , sorted_categories ):
892918 """
893919 Builds HTML fragments for category tabs, per-category file lists, and an aggregated all-files list.
894-
920+
895921 Parameters:
896922 structure (Dict[str, List[Tuple[str, str]]]): Mapping from category name to a list of (title, relative_path) pairs for files in that category.
897923 sorted_categories (List[str]): Ordered list of category names to render; determines the iteration order and tab order.
898-
924+
899925 Returns:
900926 Tuple[str, str, str]: A 3-tuple with:
901927 - tabs_html: HTML for the category tab buttons (includes icon and item count for each category).
@@ -925,7 +951,8 @@ def render_category_files(structure, sorted_categories):
925951 safe_title = html .escape (title )
926952 safe_github_path = html .escape (github_path ) # Escape path for display
927953
928- item_html = f'<li class="file-item" data-category="{ css_cat } "><a class="file-link" href="{ encoded_path } ">' \
954+ safe_encoded_path = html .escape (encoded_path , quote = True )
955+ item_html = f'<li class="file-item" data-category="{ css_cat } "><a class="file-link" href="{ safe_encoded_path } ">' \
929956 f'<span class="card-header"><span class="card-icon">{ icon } </span>' \
930957 f'<span class="card-title">{ safe_title } </span></span><span class="file-path">{ safe_github_path } </span></a></li>\n '
931958 category_files .append (item_html )
@@ -955,4 +982,4 @@ def render_category_files(structure, sorted_categories):
955982 print (f"Successfully updated { output_index_path } with vendored assets at { current_time } " )
956983
957984if __name__ == "__main__" :
958- Solution ().generate_index ()
985+ Solution ().generate_index ()
0 commit comments