"""記事の図解に共通の形を入れる(STYLE.md の「斜線と塗り」「角丸」・article_rules の HTML図解。2026-11-08)。 外側の枠をやめて白いカード、中の箱は灰の面、ボタンは角丸4px、小さい英字ラベルは灰、斜めの斜線は細かく。 色の意味づけ(青・赤・緑)は入れない(1本ずつ別に入れる)。""" import re, sys, pathlib INK = r"(?:#242424|var\(--(?:ink|black|fg|blk|bk)\)|#000|#0a0a0a|#111)" PAPERS = ("#f7f7f7", "var(--paper)", "var(--bg)") def split_rules(css): out = []; i = 0; n = len(css) while i < n: j = css.find('{', i) if j < 0: out.append(('raw', css[i:])); break sel = css[i:j] if sel.strip().startswith('@'): depth = 1; k = j + 1 while k < n and depth: if css[k] == '{': depth += 1 elif css[k] == '}': depth -= 1 k += 1 out.append(('at', sel, css[j+1:k-1])); i = k; continue k = css.find('}', j) if k < 0: out.append(('raw', css[i:])); break out.append(('rule', sel, css[j+1:k])); i = k + 1 return out def join_rules(rs, fix): s = [] for r in rs: if r[0] == 'raw': s.append(r[1]) elif r[0] == 'at': inner = r[2] if '{' in inner and r[1].strip().startswith('@media'): inner = join_rules(split_rules(inner), lambda sel, d: fix(sel, d, media=True)) s.append(r[1] + '{' + inner + '}') else: s.append(r[1] + '{' + fix(r[1], r[2]) + '}') return ''.join(s) def has(d, prop): return re.search(r'(^|;)\s*' + prop + r'\s*:', d) is not None def drop(d, prop): return re.sub(r'(^|;)\s*' + prop + r'\s*:[^;]*', r'\1', d) def setp(d, prop, val): d = drop(d, prop); d = d.rstrip().rstrip(';') return (d + ';' if d.strip() else '') + prop + ':' + val def fix_factory(wrap_sel): def fix(sel, d, media=False): s = sel.strip() # body if re.fullmatch(r'(html\s*,\s*)?body', s) and has(d, 'padding'): d = setp(d, 'padding', '8px 0') # 外側のカード if wrap_sel and re.search(r'(^|[\s,])' + re.escape(wrap_sel) + r'\s*$', s): d = drop(d, 'border') if not media: d = setp(d, 'background', '#fff'); d = setp(d, 'border-radius', '8px'); d = setp(d, 'padding', '28px 24px') elif has(d, 'padding'): d = setp(d, 'padding', '20px 14px') return d isbtn = 'button' in s or re.search(r'\.(btn|tab|seg|chip)\b', s) pressed = re.search(r'aria-pressed|aria-selected|\.on\b|\.active|\.sel\b|:hover|\.cur\b|\.is-', s) if isbtn: if pressed: if re.search(r'background\s*:\s*' + INK, d): d = setp(d, 'border-color', '#242424'); d = re.sub(r'color\s*:\s*(#f7f7f7|var\(--paper\)|var\(--bg\))', 'color:#fff', d) else: if re.search(r'border\s*:\s*1(\.5)?px\s+solid', d): d = re.sub(r'border\s*:\s*1(\.5)?px\s+solid\s+[^;]+', 'border:1px solid #e6e6e6', d); d = setp(d, 'border-radius', '4px') d = re.sub(r'background\s*:\s*(#f7f7f7|var\(--paper\)|var\(--bg\))', 'background:#fff', d) return d # 小さい英字のラベル m = re.search(r'letter-spacing\s*:\s*\.(1[0-9]|2[0-9])em', d) fs = re.search(r'font-size\s*:\s*(\d+(?:\.\d+)?)px', d) or re.search(r'font\s*:[^;]*?(\d+(?:\.\d+)?)px', d) if m and fs and float(fs.group(1)) <= 13: d = re.sub(r'letter-spacing\s*:\s*\.\d+em', 'letter-spacing:.08em', d) if not re.search(r'(^|;)\s*color\s*:', d): d = setp(d, 'color', '#888') # 表のマス if re.search(r'(^|[\s,>])(td|th)\b', s) and re.search(r'border(-\w+)?\s*:\s*1(\.5)?px\s+solid', d): d = re.sub(r'(border(?:-\w+)?)\s*:\s*1(\.5)?px\s+solid\s+[^;]+', r'\1:1px solid #e6e6e6', d) return d # 枠と余白のある箱 → 灰の面・角丸8px(データの棒などは padding がないので外れる) if re.search(r'(^|;)\s*border\s*:\s*1(\.5)?px\s+solid\s+' + INK, d) and has(d, 'padding') and not re.search(r'bar|seg|cell|dot|col\b|fill|chart|svg|canvas|grid', s): dark = re.search(r'background(-color)?\s*:\s*' + INK, d) d = drop(d, 'border'); d = setp(d, 'border-radius', '8px') if not dark: d = setp(d, 'background', '#f7f7f7') return d # 区切りの線(上か下だけ) if re.search(r'border-(top|bottom)\s*:\s*1(\.5)?px\s+solid\s+' + INK, d) and has(d, 'padding') and not re.search(r'chart|axis|ax\b|bar|plot|track|scale|tl\b', s): d = re.sub(r'(border-(?:top|bottom))\s*:\s*1(\.5)?px\s+solid\s+' + INK, r'\1:1px solid #e6e6e6', d) return d return fix def hatch(css): # 斜めの斜線を、1年のページと同じ細かさ(線2px・4px おき・-45°)にする。色は1本目の色のまま def r(m): ang, col, rest = m.group(1), m.group(2), m.group(3) return 'repeating-linear-gradient(-45deg,%s 0 2px,transparent 2px 4px)' % col return re.sub(r'repeating-linear-gradient\(\s*(-?45deg|135deg)\s*,\s*((?:#[0-9a-fA-F]{3,8}|var\(--[\w-]+\)|rgba?\([^)]*\)))\s+0(?:px)?\s+[\d.]+px\s*,\s*([^)]*?)\)', r, css) def process(path): h = path.read_text(encoding='utf-8'); o = h m = re.search(r'', h, re.S) if not m: return False css = m.group(1) w = re.search(r'([.#][A-Za-z_][\w-]*)\s*\{[^}]*max-width\s*:\s*7[0-9]0px', css) css2 = join_rules(split_rules(css), fix_factory(w.group(1) if w else None)) css2 = hatch(css2) h = h[:m.start(1)] + css2 + h[m.end(1):] h = hatch(h) # JS の中の style 文字列 if h != o: path.write_text(h, encoding='utf-8'); return True return False if __name__ == '__main__': n = 0 for name in open(sys.argv[1]).read().split(): n += process(pathlib.Path('public/embeds') / name) print('changed', n)