#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 把瀏覽器採集到的 #Print_area HTML 轉成 Obsidian Markdown 筆記。 鐵律:解釋爭點/解釋文/理由書/主文/判決理由 一律逐字照錄,不改字、不改標點、不重排段落。 """ import json, re, sys, os, glob, html from pathlib import Path from bs4 import BeautifulSoup ROC_RE = re.compile(r'中華民國\s*(\d+)\s*年\s*(\d+)\s*月\s*(\d+)\s*日') def roc_to_iso(s): m = ROC_RE.search(s or '') if not m: return None y, mo, d = int(m.group(1)) + 1911, int(m.group(2)), int(m.group(3)) return f"{y:04d}-{mo:02d}-{d:02d}" def block_text(li_text): """把 li.text 轉成逐字文字,保留官網段次編號。""" paras = li_text.select('ul.paragraphs > li, ul.num-list > li') if paras: out = [] for li in paras: lab = li.find('label') lab = lab.get_text(strip=True) if lab else '' pre = li.find('pre') body = pre.get_text() if pre else li.get_text() #
 內的空白即官網原文排版,只去掉頭尾多餘換行,不動行內空白
            body = body.replace('\r\n', '\n').strip('\n').rstrip()
            out.append((lab, body))
        return out
    txt = li_text.get_text('\n').replace('\r\n', '\n').strip()
    return [('', txt)] if txt else []

def render_block(pairs):
    """有段號者以官網段次呈現;無段號者直接輸出。"""
    lines = []
    for lab, body in pairs:
        if lab:
            lines.append(f"**({lab})**")
        lines.append(body)
        lines.append('')
    return '\n'.join(lines).strip()

def parse_doc(key, rec):
    soup = BeautifulSoup(rec['html'], 'lxml')
    fields, files, laws = {}, [], []

    for ul in soup.select('ul.flex'):
        t = ul.find('li', class_='title')
        v = ul.find('li', class_='text')
        if not t or not v:
            continue
        name = t.get_text(strip=True)
        fields[name] = v

    # 檔案清單(意見書、抄本等)
    fl = fields.get('意見書、抄本等文件') or fields.get('意見書')
    if fl is not None:
        for a in fl.select('div.lawFileList a'):
            title = a.find('strong')
            title = title.get_text(strip=True) if title else a.get_text(strip=True)
            href = a.get('href', '')
            m = re.search(r'[?&]id=(\d+)', href)
            files.append({'標題': title, '下載id': m.group(1) if m else None,
                          '連結': ('https://cons.judicial.gov.tw' + href) if href.startswith('/') else href})

    # 相關法令
    ll = fields.get('相關法令')
    if ll is not None:
        seen = set()
        for a in ll.select('div.lawLinkList a'):
            p = a.find('p')
            name = (p.get_text(strip=True) if p else a.get_text(strip=True))
            if name and name not in seen:
                seen.add(name)
                laws.append(name)

    return fields, files, laws

def law_wikilink(raw):
    """「憲法第75條(36.01.01)」→ 顯示原文,連結去掉版本日期。"""
    base = re.sub(r'\s*[((][\d./]+[))]\s*$', '', raw).strip()
    return f"[[{base}]]" if base else raw

def build_note(key, rec, fields, files, laws, ai_block=None):
    is_jie = key.startswith('釋字')
    get = lambda *names: next((fields[n] for n in names if n in fields), None)

    num_txt = get('解釋字號', '判決字號')
    num_txt = num_txt.get_text(' ', strip=True) if num_txt is not None else key
    m = re.match(r'(釋字第\d+號|\d+年憲判字第\d+號)\s*(?:【(.*?)】)?', num_txt)
    zihao = m.group(1) if m else num_txt
    casename = (m.group(2) if m and m.group(2) else '')

    pub = get('解釋公布院令', '判決日期', '判決公布日期', '公布日期', '宣示日期')
    pub_txt = pub.get_text(' ', strip=True) if pub is not None else ''
    iso = roc_to_iso(pub_txt)
    roc = ROC_RE.search(pub_txt).group(0).replace('中華民國', '').strip() if ROC_RE.search(pub_txt) else ''
    decree = re.sub(r'.*?日\s*', '', pub_txt).strip() if pub_txt else ''

    争 = get('解釋爭點', '案由')
    争_txt = render_block(block_text(争)) if 争 is not None else ''

    sections = []
    for label in ['解釋文', '主文', '理由書', '理由', '判決理由',
                  '主筆大法官記載', '大法官就主文所採立場表', '判決摘要', '相關文件']:
        node = fields.get(label)
        if node is not None:
            body = render_block(block_text(node))
            if body:
                sections.append((label, body))

    op_files = [f for f in files if '意見書' in f['標題']]

    fm = {
        '類型': '釋字' if is_jie else '憲judgment',
        '字號': zihao,
        '案名': casename,
        '公布日期': iso or '',
        '公布日期_民國': roc,
        '院令字號': decree,
        '爭點': re.sub(r'\s+', ' ', 争_txt)[:400],
        '相關法條': [re.sub(r'\s*[((][\d./]+[))]\s*$', '', l).strip() for l in laws],
        '聲請人': (get('聲請人').get_text(' ', strip=True)[:200] if get('聲請人') is not None else ''),
        '原分案號': (get('原分案號').get_text(' ', strip=True)[:120] if get('原分案號') is not None else ''),
        '意見書篇數': len(op_files),
        '原始出處': rec['url'] if rec['url'].startswith('http') else 'https://cons.judicial.gov.tw' + rec['url'].lstrip('.'),
        '抓取時間': rec['at'],
    }
    if not is_jie:
        fm['類型'] = '憲判'

    y = ['---']
    for k, v in fm.items():
        if isinstance(v, list):
            y.append(f"{k}: [" + ', '.join('"' + x.replace('"', "'") + '"' for x in v) + "]")
        elif isinstance(v, int):
            y.append(f'{k}: {v}')
        else:
            s = str(v).replace('\n', ' ').replace('"', "'")
            y.append(f'{k}: "{s}"')
    y.append('---')

    title = f"{zihao}" + (f"【{casename}】" if casename else '')
    md = ['\n'.join(y), '', f'# {title}', '',
          '> [!info] 原始出處',
          f"> 司法院憲法法庭網站 · [官網原文]({fm['原始出處']}) · 抓取於 {rec['at'][:10]}",
          '']
    if 争_txt:
        md += ['## ' + ('解釋爭點' if is_jie else '案由'), '', 争_txt, '']
    for label, body in sections:
        md += [f'## {label}', '', body, '']

    if op_files:
        md += ['## 意見書', '']
        for i, f in enumerate(op_files, 1):
            md += [f"### {i}. {f['標題']}", '', f"> 原始檔:[{f['標題']}.pdf]({f['連結']})", '',
                   '', '']

    if laws:
        md += ['## 相關法條', '']
        for l in laws:
            ver = re.search(r'[((]([\d./]+)[))]\s*$', l)
            md.append('- ' + law_wikilink(l) + (f'(版本 {ver.group(1)})' if ver else ''))
        md += ['']

    if ai_block:
        md += ['---', '', '## 白話解說(AI 生成,非官方原文)', '',
               '> [!warning] 以下由 AI 撰寫,僅供理解輔助。撰寫論文時請引用上方原文,不得引用本區塊。', '',
               ai_block, '']
    return md_join(md), fm

def md_join(parts):
    return '\n'.join(parts).replace('\n\n\n\n', '\n\n').rstrip() + '\n'

def main(indir, outdir):
    outdir = Path(outdir)
    (outdir / '10_釋字').mkdir(parents=True, exist_ok=True)
    (outdir / '20_憲判').mkdir(parents=True, exist_ok=True)
    index = []
    for path in sorted(glob.glob(os.path.join(indir, '*.json'))):
        bundle = json.load(open(path, encoding='utf-8'))
        for key, rec in bundle.get('docs', bundle).items():
            fields, files, laws = parse_doc(key, rec)
            note, fm = build_note(key, rec, fields, files, laws)
            sub = '10_釋字' if key.startswith('釋字') else '20_憲判'
            fn = fm['字號'] if not key.startswith('釋字') else '釋字第%04d號' % int(re.search(r'\d+', key).group())
            (outdir / sub / f'{fn}.md').write_text(note, encoding='utf-8')
            index.append({'key': key, 'file': f'{sub}/{fn}.md', **{k: fm[k] for k in ('字號','案名','公布日期','意見書篇數','原始出處')},
                          '相關法條': fm['相關法條']})
    json.dump(index, open(outdir / '_pipeline' / '索引.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
    print(f'notes={len(index)}')

if __name__ == '__main__':
    main(sys.argv[1], sys.argv[2])