bsc

Comprehensive codebase and cou...
Log | Files | Refs | README | LICENSE

md.py (20438B)


      1 import os
      2 import re
      3 
      4 FILES_LIST   = "list.txt"
      5 BSC_ROOT     = "."
      6 DOCS_OUTPUT  = "docs"
      7 RAW_BASE_URL = "https://raw.usercontent.amit.is-a.dev"
      8 
      9 # Protected paths that should never be overwritten
     10 PROTECTED_INDEX_FILES = {
     11     os.path.normpath(os.path.join(DOCS_OUTPUT, f"semester_{i}", "index.md"))
     12     for i in range(1, 9)
     13 }
     14 PROTECTED_INDEX_FILES.add(os.path.normpath(os.path.join(DOCS_OUTPUT, "index.md")))
     15 
     16 ALGO_FOLDER_NAME = "algorithms"  # folder name that contains algorithm .md files
     17 
     18 # Directories excluded from index generation
     19 IGNORED_FOLDERS = {"stylesheets", "overrides", "assets", ".vitepress", "node_modules", "public"}
     20 
     21 # Language config mapped by file extension
     22 SUPPORTED_LANGS = {
     23     '.c':    {'label': 'C',          'fence': 'c',          'style': 'c'},
     24     '.cpp':  {'label': 'C++',        'fence': 'cpp',        'style': 'c'},
     25     '.h':    {'label': 'C Header',   'fence': 'c',          'style': 'c'},
     26     '.hpp':  {'label': 'C++ Header', 'fence': 'cpp',        'style': 'c'},
     27     '.r':    {'label': 'R',          'fence': 'r',          'style': 'hash'},
     28     '.py':   {'label': 'Python',     'fence': 'python',     'style': 'hash'},
     29     '.java': {'label': 'Java',       'fence': 'java',       'style': 'c'},
     30     '.js':   {'label': 'JavaScript', 'fence': 'javascript', 'style': 'c'},
     31     '.ts':   {'label': 'TypeScript', 'fence': 'typescript', 'style': 'c'},
     32     '.sh':   {'label': 'Bash',       'fence': 'bash',       'style': 'hash'},
     33 }
     34 
     35 def read_block_comment(lines, start):
     36     result = []
     37     i = start
     38     n = len(lines)
     39     first = lines[i].strip()
     40 
     41     if first.startswith('/*') and '*/' in first:
     42         inner = first[2: first.index('*/')].strip().strip('*').strip()
     43         if inner:
     44             result.append(inner)
     45         return result, i + 1
     46 
     47     inner = first[2:].strip().strip('*').strip()
     48     if inner:
     49         result.append(inner)
     50     i += 1
     51 
     52     while i < n:
     53         line = lines[i].strip()
     54         if '*/' in line:
     55             text = line[: line.index('*/')].strip().strip('*').strip()
     56             if text:
     57                 result.append(text)
     58             return result, i + 1
     59         text = line.strip('*').strip()
     60         if text:
     61             result.append(text)
     62         i += 1
     63 
     64     return result, i
     65 
     66 def esc_yaml(s: str) -> str:
     67     return s.replace("'", "''")
     68  
     69  
     70 def esc_html(s: str) -> str:
     71     """Escape characters that break Vue template compilation in VitePress."""
     72     return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
     73 
     74 def format_author(author: str) -> str:
     75     """Return a mailto markdown link if the author string contains an email."""
     76     m = re.search(r'(.*?)\s*<(.*?)>', author)
     77     if m:
     78         name  = esc_html(m.group(1).strip())
     79         email = esc_html(m.group(2).strip())
     80         return f"[{name}](mailto:{email})"
     81     return esc_html(author)
     82 
     83 def parse_c_style(content):
     84     lines = content.splitlines()
     85     n = len(lines)
     86     i = 0
     87     author = date = repo = license_str = problem_statement = ""
     88 
     89     while i < n and not lines[i].strip():
     90         i += 1
     91 
     92     # Extract metadata block
     93     if i < n and lines[i].strip().startswith('/*'):
     94         block, i = read_block_comment(lines, i)
     95         for line in block:
     96             # Handles both newline-per-field and pipe-separated formats
     97             parts = [p.strip() for p in line.split('|')]
     98             for part in parts:
     99                 if ':' in part:
    100                     key, _, val = part.partition(':')
    101                     key = key.strip().lower()
    102                     val = val.strip()
    103                     if 'author'    in key: author       = val
    104                     elif 'date'    in key: date         = val
    105                     elif 'repo'    in key: repo         = val
    106                     elif 'license' in key: license_str  = val
    107 
    108     while i < n and not lines[i].strip():
    109         i += 1
    110 
    111     # Extract problem statement block (ensures it's not the actual code starting)
    112     if i < n and lines[i].strip().startswith('/*'):
    113         peek_block, peek_i = read_block_comment(lines, i)
    114         block_text = ' '.join(peek_block)
    115         if '#include' not in block_text and 'import ' not in block_text:
    116             problem_statement = ' '.join(p for p in peek_block if p).strip()
    117             i = peek_i
    118 
    119     # Locate the beginning of actual source code
    120     code_start = None
    121     for j in list(range(i, n)) + list(range(0, i)):
    122         line_strip = lines[j].strip()
    123         if line_strip.startswith('#include') or line_strip.startswith('import '):
    124             code_start = j
    125             break
    126 
    127     code = '\n'.join(lines[code_start:]).strip() if code_start is not None else content.strip()
    128     return author, date, repo, license_str, problem_statement, code
    129 
    130 def parse_hash_style(content):
    131     lines = content.splitlines()
    132     n = len(lines)
    133     i = 0
    134     author = date = repo = license_str = problem_statement = ""
    135 
    136     while i < n and not lines[i].strip():
    137         i += 1
    138 
    139     # Extract metadata block
    140     meta_lines = []
    141     while i < n and lines[i].strip().startswith('#'):
    142         meta_lines.append(lines[i].strip()[1:].strip())
    143         i += 1
    144 
    145     for raw_line in meta_lines:
    146         for part in raw_line.split('|'):
    147             if ':' in part:
    148                 key, _, val = part.partition(':')
    149                 key = key.strip().lower()
    150                 val = val.strip()
    151                 if 'author'    in key: author       = val
    152                 elif 'date'    in key: date         = val
    153                 elif 'repo'    in key: repo         = val
    154                 elif 'license' in key: license_str  = val
    155 
    156     while i < n and not lines[i].strip():
    157         i += 1
    158 
    159     # Extract problem statement block
    160     ps_lines = []
    161     while i < n and lines[i].strip().startswith('#'):
    162         text = lines[i].strip()[1:].strip()
    163         if text:
    164             ps_lines.append(text)
    165         i += 1
    166 
    167     if ps_lines:
    168         problem_statement = ' '.join(ps_lines).strip()
    169 
    170     code = content.strip()
    171     return author, date, repo, license_str, problem_statement, code
    172 
    173 
    174 ALGO_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline; margin-bottom:-2px; margin-right:6px;" class="lucide lucide-binary"><rect x="14" y="14" width="4" height="6" rx="2"/><rect x="6" y="4" width="4" height="6" rx="2"/><path d="M6 20h4"/><path d="M14 10h4"/><path d="M6 14h2v6"/><path d="M14 4h2v6"/></svg>'
    175 
    176 def parse_algo_md(content):
    177     """Parse a GitHub-style algorithm .md file and convert to VitePress format."""
    178     lines = content.splitlines()
    179     title = ""
    180     problem_statement = ""
    181     body_lines = []
    182     i = 0
    183     n = len(lines)
    184 
    185     # Extract title from first # heading
    186     while i < n:
    187         line = lines[i]
    188         if line.startswith("# "):
    189             title = line[2:].strip()
    190             i += 1
    191             break
    192         i += 1
    193 
    194     # Parse rest: find problem statement in > blockquote under ### Problem Statement
    195     in_problem_section = False
    196     ps_lines = []
    197 
    198     while i < n:
    199         line = lines[i]
    200         stripped = line.strip()
    201 
    202         if stripped.lower().startswith("### problem statement"):
    203             in_problem_section = True
    204             i += 1
    205             continue
    206 
    207         if in_problem_section:
    208             # Allow blank lines between heading and blockquote
    209             if stripped == "":
    210                 i += 1
    211                 continue
    212             if stripped.startswith("> "):
    213                 ps_lines.append(stripped[2:].strip())
    214                 i += 1
    215                 continue
    216             elif stripped == ">":
    217                 i += 1
    218                 continue
    219             else:
    220                 # Non-blockquote, non-blank line ends problem section
    221                 in_problem_section = False
    222                 problem_statement = " ".join(ps_lines).strip()
    223                 body_lines.append(line)
    224         else:
    225             body_lines.append(line)
    226 
    227         i += 1
    228 
    229     if in_problem_section and ps_lines:
    230         problem_statement = " ".join(ps_lines).strip()
    231 
    232     return title, problem_statement, body_lines
    233 
    234 def build_algo_md(filename_base, title, problem_statement, body_lines, source_path=""):
    235     """Build VitePress .md from parsed algo content."""
    236     desc = problem_statement if problem_statement else f"Algorithm — {title}"
    237 
    238     fm = [
    239         "---",
    240         f"title: '{ALGO_ICON_SVG} {esc_yaml(title)}'",
    241         f"description: '{esc_yaml(desc)}'",
    242         f"source: '{source_path}'",
    243         "---",
    244         "",
    245     ]
    246 
    247     body = [f"# {title}", ""]
    248 
    249     if problem_statement:
    250         body += [
    251             "### Problem Statement",
    252             "",
    253             "::: tip Problem Statement",
    254             esc_html(problem_statement),
    255             ":::",
    256             "",
    257         ]
    258 
    259     # Replace > blockquote problem section in body with nothing (already handled above)
    260     # Just append the remaining body lines (Algorithm, Pseudocode, Complexity etc.)
    261     skip_next_blockquote = False
    262     cleaned = []
    263     blines = body_lines[:]
    264     j = 0
    265     while j < len(blines):
    266         l = blines[j]
    267         s = l.strip()
    268         if s.lower().startswith("### problem statement"):
    269             # skip until blockquote ends
    270             j += 1
    271             while j < len(blines) and (blines[j].strip().startswith(">") or blines[j].strip() == ""):
    272                 j += 1
    273             continue
    274         cleaned.append(l)
    275         j += 1
    276 
    277     # Remove leading blank lines from cleaned
    278     while cleaned and not cleaned[0].strip():
    279         cleaned.pop(0)
    280 
    281     body += cleaned
    282 
    283     return "\n".join(fm + body)
    284 
    285 
    286 def build_md(filename, lang_label, fence_lang, author, date, repo, license_str,
    287              problem_statement, code, raw_url, github_url, rel_url):
    288 
    289     desc = problem_statement if problem_statement else f"{lang_label} program — {filename}"
    290     icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline; margin-bottom:-2px; margin-right:6px;"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1"/><path d="M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1"/></svg>'
    291 
    292     fm_lines = [
    293         "---",
    294         f"title: '{icon_svg} {esc_yaml(filename)}'",
    295         f"description: '{esc_yaml(desc)}'",
    296         f"source: '{rel_url}'",
    297         "---",
    298     ]
    299 
    300     body = [
    301         "",
    302         f"# {filename}",
    303         ""
    304     ]
    305 
    306     has_meta = any([author, date, license_str])
    307     if has_meta:
    308         body.extend([
    309             "### Metadata",
    310             ""
    311         ])
    312         
    313     if author:
    314         body.append(f"- **Author** — {format_author(author)}")
    315     if date:
    316         body.append(f"- **Last updated** — {esc_html(date)}")
    317     if license_str:
    318         body.append(
    319             f"- **License** — [{esc_html(license_str)}]"
    320             f"(https://github.com/notamitgamer/bsc/blob/main/LICENSE)"
    321         )
    322         
    323         body.append("")
    324 
    325     if problem_statement:
    326         body += [ 
    327             "",
    328             "### Problem Statement",
    329             "",
    330             f"::: tip {filename}",
    331             esc_html(problem_statement),
    332             ":::",
    333             "",
    334         ]
    335 
    336     body += [
    337     "## Source Code",
    338         "",
    339         f"```{fence_lang} [{filename}]",
    340         code,
    341         "```",
    342         "---",
    343         # Action links — minimal, right-aligned feel via HTML
    344         '<div style="display:flex;gap:12px;margin-bottom:12px;">',
    345         f'  <a href="{github_url}" target="_blank" rel="noopener noreferrer"'
    346         '   style="display:inline-flex;align-items:center;gap:6px;font-size:0.85rem;'
    347         '          text-decoration:none;color:var(--vp-c-brand);">',
    348         '    <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24"'
    349         '         fill="currentColor" style="vertical-align:middle;">',
    350         '      <path d="M12 0C5.37 0 0 5.373 0 12c0 5.303 3.438 9.8 8.205 11.387.6.113.82-.258'
    351         '             .82-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.757'
    352         '             -1.333-1.757-1.089-.745.083-.729.083-.729 1.205.084 1.84 1.236 1.84 1.236'
    353         '             1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3'
    354         '             -5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523'
    355         '             .105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006'
    356         '             2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873'
    357         '             .12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92'
    358         '             .42.36.81 1.096.81 2.22v3.293c0 .319.21.694.825.576C20.565 21.795'
    359         '             24 17.298 24 12c0-6.627-5.373-12-12-12z"/>',
    360         '    </svg>',
    361         '    View on GitHub',
    362         '  </a>',
    363         f'  <a href="{raw_url}" target="_blank" rel="noopener noreferrer"'
    364         '   style="display:inline-flex;align-items:center;gap:6px;font-size:0.85rem;'
    365         '          text-decoration:none;color:var(--vp-c-text-2);">',
    366         '    <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"'
    367         '         fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"'
    368         '         stroke-linejoin="round" style="vertical-align:middle;">',
    369         '      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>',
    370         '      <polyline points="7 10 12 15 17 10"/>',
    371         '      <line x1="12" y1="15" x2="12" y2="3"/>',
    372         '    </svg>',
    373         '    Download Raw',
    374         '  </a>',
    375         '</div>',
    376         "",
    377         "::: info Printing the code",
    378         "To print this file, open it on GitHub and click **Raw** before printing, or use the **Download Raw** button above and print directly from that page.",
    379         ":::",
    380         "",
    381     ]
    382 
    383     return '\n'.join(fm_lines + body)
    384 
    385 def _get_md_title(md_path: str) -> str:
    386     try:
    387         with open(md_path, encoding='utf-8') as f:
    388             content = f.read()
    389         # Extracts actual title text, ignoring prepended SVGs or icons
    390         m = re.search(r'^title:\s*\'(?:.*?</svg>\s*)?(.+)\'$', content, re.MULTILINE)
    391         if m:
    392             return m.group(1).strip()
    393     except OSError:
    394         pass
    395     return os.path.splitext(os.path.basename(md_path))[0]
    396 
    397 def create_folder_indexes(docs_root):
    398     for root, dirs, files in os.walk(docs_root):
    399         dirs[:] = [d for d in dirs if d not in IGNORED_FOLDERS]
    400 
    401         folder_name = os.path.basename(root)
    402 
    403         if folder_name in IGNORED_FOLDERS:
    404             continue
    405 
    406         index_path = os.path.normpath(os.path.join(root, 'index.md'))
    407 
    408         if index_path in PROTECTED_INDEX_FILES and os.path.exists(index_path):
    409             continue
    410 
    411         md_files = sorted(
    412             f for f in files
    413             if f.endswith('.md') and f.lower() not in {'index.md', 'readme.md', 'default.md', 'home.md', 'tags.md'}
    414         )
    415         subdirs = sorted(dirs)
    416 
    417         if root == docs_root:
    418             title = "BSc Code Index"
    419             intro  = "Select a category from the left sidebar or the table below."
    420         else:
    421             title = folder_name.replace('_', ' ').replace('-', ' ').title()
    422             intro  = f"Files and sub-folders in **{title}**."
    423 
    424         with open(index_path, 'w', encoding='utf-8') as f:
    425             f.write(f"# {title}\n\n{intro}\n\n")
    426 
    427             if subdirs:
    428                 f.write("## Folders\n\n")
    429                 f.write("| # | Folder | Link |\n")
    430                 f.write("|---|---|---|\n")
    431                 for idx, d in enumerate(subdirs, 1):
    432                     dir_title = d.replace('_', ' ').replace('-', ' ').title()
    433                     f.write(f"| {idx} | {dir_title} | [Open]({d}/index.md) |\n")
    434                 f.write("\n")
    435 
    436             if md_files:
    437                 f.write("## Files\n\n")
    438                 f.write("| # | File | Link |\n")
    439                 f.write("|---|---|---|\n")
    440                 for idx, md_file in enumerate(md_files, 1):
    441                     src_name = _get_md_title(os.path.join(root, md_file))
    442                     f.write(f"| {idx} | `{src_name}` | [View Code]({md_file}) |\n")
    443                 f.write("\n")
    444 
    445 def main():
    446     with open(FILES_LIST, 'r', encoding='utf-8') as f:
    447         file_paths = [line.strip() for line in f if line.strip()]
    448 
    449     generated = skipped = 0
    450 
    451     for full_path in file_paths:
    452         full_path = os.path.normpath(full_path)
    453         ext = os.path.splitext(full_path)[1].lower()
    454 
    455         # Handle algorithm .md files
    456         if ext == '.md':
    457             parent_folder = os.path.basename(os.path.dirname(full_path))
    458             if parent_folder.lower() != ALGO_FOLDER_NAME:
    459                 print(f"SKIP (non-algo .md): {full_path}")
    460                 skipped += 1
    461                 continue
    462 
    463             try:
    464                 with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
    465                     content_md = f.read()
    466             except FileNotFoundError:
    467                 print(f"NOT FOUND: {full_path}")
    468                 skipped += 1
    469                 continue
    470 
    471             filename_base = os.path.splitext(os.path.basename(full_path))[0]
    472             rel_path = os.path.relpath(full_path, BSC_ROOT)
    473             title, problem_statement, body_lines = parse_algo_md(content_md)
    474             if not title:
    475                 title = filename_base
    476             rel_url_algo = rel_path.replace('\\', '/')
    477             md_content = build_algo_md(filename_base, title, problem_statement, body_lines, rel_url_algo)
    478 
    479             rel_path = os.path.relpath(full_path, BSC_ROOT)
    480             md_rel = os.path.splitext(rel_path)[0] + '.md'
    481             md_out = os.path.normpath(os.path.join(DOCS_OUTPUT, md_rel))
    482 
    483             if md_out in PROTECTED_INDEX_FILES and os.path.exists(md_out):
    484                 print(f"SKIP (Protected Index File): {md_out}")
    485                 skipped += 1
    486                 continue
    487 
    488             os.makedirs(os.path.dirname(md_out), exist_ok=True)
    489             with open(md_out, 'w', encoding='utf-8') as f:
    490                 f.write(md_content)
    491 
    492             print(f"OK  {md_rel}")
    493             generated += 1
    494             continue
    495 
    496         if ext not in SUPPORTED_LANGS:
    497             print(f"SKIP (unsupported extension): {full_path}")
    498             skipped += 1
    499             continue
    500 
    501         lang_info = SUPPORTED_LANGS[ext]
    502 
    503         try:
    504             rel_path = os.path.relpath(full_path, BSC_ROOT)
    505         except ValueError:
    506             print(f"SKIP (relpath failed): {full_path}")
    507             skipped += 1
    508             continue
    509 
    510         rel_url    = rel_path.replace('\\', '/')
    511         raw_url    = f"{RAW_BASE_URL}/{rel_url}"
    512         github_url = f"https://github.com/notamitgamer/bsc/blob/main/{rel_url}"
    513 
    514         try:
    515             with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
    516                 content = f.read()
    517         except FileNotFoundError:
    518             print(f"NOT FOUND: {full_path}")
    519             skipped += 1
    520             continue
    521 
    522         filename = os.path.basename(full_path)
    523 
    524         if lang_info['style'] == 'c':
    525             author, date, repo, license_str, problem_statement, code = parse_c_style(content)
    526         else:
    527             author, date, repo, license_str, problem_statement, code = parse_hash_style(content)
    528 
    529         md_content = build_md(
    530             filename, lang_info['label'], lang_info['fence'], author, date, repo, license_str,
    531             problem_statement, code, raw_url, github_url, rel_url,
    532         )
    533 
    534         md_rel = os.path.splitext(rel_path)[0] + '.md'
    535         md_out = os.path.normpath(os.path.join(DOCS_OUTPUT, md_rel))
    536 
    537         if md_out in PROTECTED_INDEX_FILES and os.path.exists(md_out):
    538             print(f"SKIP (Protected Index File): {md_out}")
    539             skipped += 1
    540             continue
    541 
    542         os.makedirs(os.path.dirname(md_out), exist_ok=True)
    543         with open(md_out, 'w', encoding='utf-8') as f:
    544             f.write(md_content)
    545 
    546         print(f"OK  {md_rel}")
    547         generated += 1
    548 
    549     print(f"\nDone - {generated} generated, {skipped} skipped.")
    550     print("Generating folder index pages...")
    551     create_folder_indexes(DOCS_OUTPUT)
    552     print("Folder indexes created.")
    553 
    554 if __name__ == "__main__":
    555     main()
© notamitgamer • Site Built: 2026-07-21 13:58:23 UTC • git-mirror commit: 1037f62 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror