LZ-SG/embedded-dev-tutorials
0
1#!/usr/bin/env python32"""SPA v1.7.0: Syntax highlighting + breadcrumb + related tutorials + copy toast"""3import re4 5PATH = r"C:\Users\LYS\.qclaw\workspace\embedded-dev-site\static\index.html"6with open(PATH, 'r', encoding='utf-8') as f:7 spa = f.read()8 9# ====== 1. Add CSS for new features ======10new_css = '''11/* ══ v1.7.0: 语法高亮 ══ */12.syn-kw{color:#ff7b72} /* 关键字 */13.syn-pp{color:#ff7b72} /* 预处理 */14.syn-type{color:#ffa657} /* 类型 */15.syn-str{color:#a5d6ff} /* 字符串 */16.syn-com{color:#8b949e;font-style:italic} /* 注释 */17.syn-num{color:#79c0ff} /* 数字 */18.syn-fn{color:#d2a8ff} /* 函数 */19.syn-op{color:#ff7b72} /* 运算符 */20/* ══ v1.7.0: 面包屑 ══ */21.breadcrumb{display:flex;align-items:center;gap:6px;font-size:.78em;color:var(--t2);margin:0 0 16px;padding:6px 0;flex-wrap:wrap}22.breadcrumb a{color:var(--a)}23.breadcrumb .sep{color:var(--br)}24/* ══ v1.7.0: 复制提示 ══ */25.copy-toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);background:var(--g);color:#fff;padding:8px 20px;border-radius:8px;font-size:.85em;z-index:400;pointer-events:none;opacity:0;transition:opacity .3s,transform .3s}26.copy-toast.show{opacity:1;transform:translateX(-50%) translateY(4px)}27/* ══ v1.7.0: 相关教程 ══ */28.related-tutorials{margin:24px 0;padding:16px 0;border-top:1px solid var(--br)}29.related-tutorials h3{font-size:.95em;color:var(--a);margin-bottom:10px}30.related-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px}31.related-card{background:var(--bg2);border:1px solid var(--br);border-radius:8px;padding:10px 14px;cursor:pointer;transition:border-color .2s;font-size:.82em}32.related-card:hover{border-color:var(--a)}33.related-card .rt-tag{display:inline-block;font-size:.65em;padding:1px 6px;border-radius:4px;margin-right:4px;background:var(--bg3);color:var(--t2)}34@media(max-width:600px){.related-grid{grid-template-columns:1fr}}35'''36 37spa = spa.replace('/* ══ v1.6.4: 快捷键面板 ══ */', new_css + '\n/* ══ v1.6.4: 快捷键面板 ══ */')38 39# ====== 2. Add copy toast + breadcrumb HTML ======40toast_html = '<div class="copy-toast" id="copyToast">✅ 代码已复制!</div>\n'41spa = spa.replace('<div class="shortcut-hint"', toast_html + '<div class="shortcut-hint"')42 43# ====== 3. Add JS functions ======44 45# ---- 3a. Syntax highlighting ----46syntax_js = '''47// ══ v1.7.0: 语法高亮 ══48const C_KEYWORDS = new Set(['auto','break','case','char','const','continue','default','do','double','else','enum','extern','float','for','goto','if','inline','int','long','register','return','short','signed','sizeof','static','struct','switch','typedef','union','unsigned','void','volatile','while']);49const C_TYPES = new Set(['uint8_t','uint16_t','uint32_t','uint64_t','int8_t','int16_t','int32_t','int64_t','size_t','ssize_t','bool','true','false','NULL','HAL_StatusTypeDef','esp_err_t','QueueHandle_t','SemaphoreHandle_t','TimerHandle_t','TaskHandle_t','GPIO_TypeDef','ADC_HandleTypeDef','UART_HandleTypeDef','I2C_HandleTypeDef','SPI_HandleTypeDef','TIM_HandleTypeDef','DMA_HandleTypeDef','RTC_HandleTypeDef','uintptr_t','float32_t']);50const C_PP = new Set(['#include','#define','#ifdef','#ifndef','#if','#else','#elif','#endif','#pragma','#error','#warning','#undef']);51const MAKEFILE_KW = new Set(['all','clean','flash','debug','release','.PHONY','ifeq','ifneq','ifdef','ifndef','else','endif','include','export','override','define','endef','eval','call','foreach','wildcard','patsubst','notdir','dir','addprefix','addsuffix','shell','subst','filter','strip','sort','word','words','firstword','lastword','error','warning','info']);52 53function highlightCodeBlocks(container) {54 container.querySelectorAll('pre code').forEach(el => {55 if (el.classList.contains('hl-done')) return;56 el.classList.add('hl-done');57 let text = el.innerHTML;58 if (text.includes('<span class="syn-')) return; // already highlighted59 60 const firstLine = text.split('\n')[0] || '';61 const isMakefile = firstLine.includes('# ') && (text.includes(' = ') || text.includes(':\t') || text.includes('$(')) && !text.includes('{');62 const isCPP = !isMakefile && (text.includes('#include') || text.includes('HAL_') || text.includes('void ') || text.includes('uint8_t') || text.includes('typedef') || text.includes('int main'));63 64 if (isMakefile) {65 text = text.replace(/(^|\n)(\s*)([.\/\\\w-]+)\s*:/gm, '$1$2<span class="syn-fn">$3</span>:');66 text = text.replace(/\$\((\w+)\)/g, '<span class="syn-kw">$$($1)</span>');67 text = text.replace(/\$[@<^+?*]/g, '<span class="syn-num">$&</span>');68 text = text.replace(/(^|\n)(\s*)(\w+)\t/g, '$1$2$3\t');69 text = text.replace(/^(\s*)(#.*)$/gm, '$1<span class="syn-com">$2</span>');70 } else if (isCPP) {71 // Strings72 text = text.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '<span class="syn-str">$&</span>');73 // Comments74 text = text.replace(/(\/\/.*$)/gm, '<span class="syn-com">$1</span>');75 text = text.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="syn-com">$1</span>');76 // Preprocessor77 text = text.replace(/^(\s*#\s*\w+)/gm, '<span class="syn-pp">$1</span>');78 // Numbers79 text = text.replace(/\b(0x[0-9a-fA-F]+|\d+\.?\d*(?:[eE][+-]?\d+)?[uUlLfF]*)\b/g, '<span class="syn-num">$1</span>');80 // Types81 C_TYPES.forEach(t => { const r = new RegExp('\\b'+t+'\\b','g'); text = text.replace(r, '<span class="syn-type">'+t+'</span>'); });82 // Keywords83 C_KEYWORDS.forEach(k => { const r = new RegExp('\\b'+k+'\\b','g'); text = text.replace(r, '<span class="syn-kw">'+k+'</span>'); });84 // Functions (identifier followed by opening paren)85 text = text.replace(/\b([a-zA-Z_]\w+)\s*\(/g, (m, name) => {86 if (['if','for','while','switch','return','sizeof','typeof'].includes(name)) return m;87 return `<span class="syn-fn">${name}</span>(`;88 });89 }90 el.innerHTML = text;91 });92}93'''94 95# Insert after searchCache declaration96spa = spa.replace("let searchCache = {};", "let searchCache = {};\n" + syntax_js)97 98# ---- 3b. Breadcrumb ----99breadcrumb_js = '''100// ══ v1.7.0: 面包屑 ══101const BREADCRUMB_MAP = {};102function buildBreadcrumbMap() {103 for (let i=0; i<=13; i++) {104 const id = `guide-ch${String(i).padStart(2,'0')}`;105 BREADCRUMB_MAP[id] = { section: '小白完全指南', title: `第${i}章` };106 }107 // Also add from cardsData108 if (typeof cardsData !== 'undefined') cardsData.forEach(c => {109 if (!BREADCRUMB_MAP[c.id]) BREADCRUMB_MAP[c.id] = { section: c.section || '教程', title: c.title };110 });111}112function renderBreadcrumb(pageId) {113 if (!BREADCRUMB_MAP[pageId]) return '';114 const b = BREADCRUMB_MAP[pageId];115 return `<div class="breadcrumb"><a href="#" onclick="loadPage('home')">🏠 首页</a><span class="sep">›</span><span>${b.section}</span><span class="sep">›</span><span>${b.title}</span></div>`;116}117setTimeout(buildBreadcrumbMap, 3000);118'''119 120spa = spa.replace("setTimeout(buildSearchIndex, 2500);", "setTimeout(buildSearchIndex, 2500);\nsetTimeout(buildBreadcrumbMap, 3500);" + breadcrumb_js)121 122# ---- 3c. Related tutorials ----123related_js = '''124// ══ v1.7.0: 相关教程推荐 ══125const RELATED_TAGS = {126 'guide-ch00': ['arm-gcc','cmake','makefile'],127 'guide-ch01': ['guide-ch02','guide-ch03','cmake','makefile'],128 'guide-ch02': ['guide-ch01','guide-ch03','makefile'],129 'guide-ch03': ['guide-ch02','cmake'],130 'guide-ch04': ['proj-uart-hello','protocols','serial'],131 'guide-ch05': ['flash-debug','proj-bootloader-iap'],132 'guide-ch06': ['guide-ch04','guide-ch05','guide-ch08','proj-led-button'],133 'guide-ch07': ['guide-ch06','proj-esp32-iot','bci-2026'],134 'guide-ch08': ['guide-ch06','proj-freertos-station','freertos'],135 'guide-ch09': ['protocols','proj-can-bus','proj-i2c'],136 'guide-ch10': ['guide-ch11','proj-low-power'],137 'guide-ch11': ['guide-ch10','proj-low-power'],138 'guide-ch12': ['proj-dht11-monitor','guide-ch09'],139 'guide-ch13': ['proj-smart-car','proj-freertos-station','practice-projects'],140 'proj-led-button': ['guide-ch06','proj-uart-hello'],141 'proj-uart-hello': ['guide-ch04','proj-led-button'],142 'proj-freertos-station': ['guide-ch08','freertos'],143 'proj-smart-car': ['guide-ch13','proj-pwm-motor'],144 'proj-esp32-iot': ['guide-ch07','bci-2026'],145 'bci-2026': ['guide-ch07','practice-projects'],146};147function renderRelated(pageId) {148 const ids = RELATED_TAGS[pageId];149 if (!ids) return '';150 const cards = ids.map(id => {151 const c = (typeof cardsData !== 'undefined' && cardsData.find(x => x.id === id))152 || (typeof cfgMap !== 'undefined' && cfgMap[id]);153 const title = c ? (c.title || c.label || id) : id;154 const tag = (id.startsWith('guide-') ? '指南' : id.startsWith('proj-') ? '实战' : '专题');155 return `<div class="related-card" onclick="loadPage('${id}')"><span class="rt-tag">${tag}</span>${title}</div>`;156 }).join('');157 return `<div class="related-tutorials"><h3>📎 相关教程</h3><div class="related-grid">${cards}</div></div>`;158}159'''160 161spa = spa.replace("function renderBreadcrumb(pageId) {", related_js + "\nfunction renderBreadcrumb(pageId) {")162 163# ---- 3d. Copy toast ----164# Find setupCopyButtons and enhance165old_copy = '''function setupCopyButtons(container) {166 container.querySelectorAll('pre').forEach(pre => {167 if (pre.querySelector('.copy-btn')) return;168 const btn = document.createElement('button');169 btn.className = 'copy-btn';170 btn.textContent = '📋';171 btn.title = '复制代码';172 btn.onclick = function() {173 const code = pre.querySelector('code');174 const text = code ? code.textContent : pre.textContent;175 navigator.clipboard.writeText(text).then(() => {176 btn.textContent = '✅';177 setTimeout(() => btn.textContent = '📋', 2000);178 });179 };180 pre.appendChild(btn);181 });182}'''183 184new_copy = '''function setupCopyButtons(container) {185 container.querySelectorAll('pre').forEach(pre => {186 if (pre.querySelector('.copy-btn')) return;187 const btn = document.createElement('button');188 btn.className = 'copy-btn';189 btn.textContent = '📋';190 btn.title = '复制代码';191 btn.onclick = function() {192 const code = pre.querySelector('code');193 const text = code ? code.textContent : pre.textContent;194 navigator.clipboard.writeText(text).then(() => {195 btn.textContent = '✅';196 const toast = document.getElementById('copyToast');197 if (toast) { toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 1500); }198 setTimeout(() => btn.textContent = '📋', 2000);199 }).catch(() => {200 // Fallback for older browsers201 const ta = document.createElement('textarea');202 ta.value = text; ta.style.position='fixed'; ta.style.left='-9999px';203 document.body.appendChild(ta); ta.select();204 document.execCommand('copy'); document.body.removeChild(ta);205 btn.textContent = '✅';206 const toast = document.getElementById('copyToast');207 if (toast) { toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 1500); }208 setTimeout(() => btn.textContent = '📋', 2000);209 });210 };211 pre.appendChild(btn);212 });213}'''214 215spa = spa.replace(old_copy, new_copy)216 217# ---- 3e. Wire highlights + breadcrumb + related into loadContent ----218# Find where content is rendered and add our calls219old_render_line = "div.innerHTML = html;"220if old_render_line in spa:221 spa = spa.replace(old_render_line, 222 'div.innerHTML = html;\n // v1.7.0: apply enhancements\n setTimeout(() => {\n setupCopyButtons(div);\n highlightCodeBlocks(div);\n const bc = renderBreadcrumb(currentPage); const related = renderRelated(currentPage);\n const bcEl = div.querySelector(\'.breadcrumb\');\n if (bc && !bcEl) { const tmp = document.createElement(\'div\'); tmp.innerHTML = bc; div.insertBefore(tmp.firstElementChild, div.firstElementChild); }\n if (related) { const relEl = document.createElement(\'div\'); relEl.innerHTML = related; div.appendChild(relEl.firstElementChild); }\n }, 100);')223else:224 # Try to find the right place - after content.innerHTML = html225 alt = 'contentDiv.innerHTML = html;'226 if alt in spa:227 spa = spa.replace(alt, 228 'contentDiv.innerHTML = html;\n setTimeout(() => {\n setupCopyButtons(contentDiv);\n highlightCodeBlocks(contentDiv);\n const bc = renderBreadcrumb(currentPage); const related = renderRelated(currentPage);\n if (bc) { const tmp = document.createElement(\'div\'); tmp.innerHTML = bc; if (contentDiv.firstElementChild) contentDiv.insertBefore(tmp.firstElementChild, contentDiv.firstElementChild); }\n if (related) { const tmp = document.createElement(\'div\'); tmp.innerHTML = related; contentDiv.appendChild(tmp.firstElementChild); }\n }, 100);')229 230# ---- 3f. Update version ----231spa = spa.replace('v1.6.1 ·', 'v1.7.0 ·')232spa = spa.replace('v1.6.4:', 'v1.7.0:')233 234with open(PATH, 'w', encoding='utf-8', newline='') as f:235 f.write(spa)236 237print(f"SPA v1.7.0 written: {len(spa)/1024:.1f}KB")238print("Features: syntax highlighting + breadcrumb + related tutorials + copy toast")239 