fix: book canonical, fr/de pin warnings, pin grouping per locale, SEO checks
CI / Lint e auditorias (push) Successful in 48s
CI / Trivy (dependências e segredos) (push) Successful in 12s
CI / Imagem Docker (linux/arm64) (push) Successful in 3m41s
Release / Plano (reaproveitar a imagem?) (push) Successful in 6s
Release / ci (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Successful in 16s
Release / Cria a release no Gitea (push) Successful in 6s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-11-01 23:58:11 -03:00
1 parent 5a9fe676fc
commit fe1802f818
67 files changed
+11875 -9596

No files matched your search

+13 -10
View File
@@ -621,19 +621,22 @@ function verificarRegrasLivro() {
}
// ---------------------------------------------------------------------------
// 5. Português dentro dos dados de outros idiomas
// 5. Português ou inglês dentro dos dados de outros idiomas
// ---------------------------------------------------------------------------
// Valores que começam com número ("4 (SPI0/1 para Flash...)") passam pela allowlist
// técnica; esta varredura pega o texto em português que sobra dentro deles.
const PALAVRAS_SO_PORTUGUESAS = /\b(até|canais|uso geral|conforme a carga|podem ser|saída|resolução|integrada no|encapsulamento|memória|não|única|isócronos|proximidade e)\b/i;
const PALAVRAS_SO_PORTUGUESAS = /\b(até|canais|uso geral|conforme a carga|podem ser|saída|resolução|integrada no|encapsulamento|memória|não|única|isócronos|proximidade e|dividida em|a partir da|e mais \d+|ativa|ativo|baixo consumo|correspondentes|velocidade|com (GCM|LP|cache))\b/i;
// Frases de aviso em inglês que não podem aparecer nos dados dos outros idiomas (en é a origem delas).
const FRASES_EM_INGLES = /\b(Do not leave|Must not be|Not recommended|Recommended Voltage|For chips (with|without)|Dedicated (to|Chip|Hold|analog)|Analog input pin|Multifunction pin|Floating by default|Used to control|Voltage level must|Low-power|Primary Strapping|This pin does not)\b/;
function percorrer(valor, caminho, aoAchar) {
function percorrer(valor, caminho, aoAchar, ingles = true) {
if (typeof valor === "string") {
if (PALAVRAS_SO_PORTUGUESAS.test(valor)) aoAchar(caminho, valor);
if (PALAVRAS_SO_PORTUGUESAS.test(valor)) aoAchar(caminho, valor, "português");
else if (ingles && FRASES_EM_INGLES.test(valor)) aoAchar(caminho, valor, "inglês");
} else if (Array.isArray(valor)) {
valor.forEach((v, i) => percorrer(v, `${caminho}[${i}]`, aoAchar));
valor.forEach((v, i) => percorrer(v, `${caminho}[${i}]`, aoAchar, ingles));
} else if (valor && typeof valor === "object") {
for (const [k, v] of Object.entries(valor)) percorrer(v, `${caminho}.${k}`, aoAchar);
for (const [k, v] of Object.entries(valor)) percorrer(v, `${caminho}.${k}`, aoAchar, ingles);
}
}
@@ -647,13 +650,13 @@ function verificarPortuguesNosDados() {
if (existsSync(dirCon)) for (const a of readdirSync(dirCon)) arquivos.push(join(dirCon, a));
for (const arq of arquivos) {
if (!existsSync(arq)) continue;
percorrer(JSON.parse(readFileSync(arq, "utf8")), arq, (caminho, valor) => {
if (achados < 10) falha(`'${loc}': texto em português em ${caminho}: "${valor.slice(0, 80)}"`);
percorrer(JSON.parse(readFileSync(arq, "utf8")), arq, (caminho, valor, lingua) => {
if (achados < 10) falha(`'${loc}': texto em ${lingua} em ${caminho}: "${valor.slice(0, 80)}"`);
achados++;
});
}, loc !== "en");
}
}
if (achados === 0) sucesso("Nenhum texto em português nos dados dos outros idiomas.");
if (achados === 0) sucesso("Nenhum texto em português ou inglês solto nos dados dos outros idiomas.");
else if (achados > 10) falha(`... e mais ${achados - 10} ocorrência(s).`);
}
+3
View File
@@ -195,6 +195,9 @@ if [[ " $ENABLED_LOCALES " =~ " zh-CN " ]]; then
done
fi
# 9. SEO: canonical, hreflang e lang de cada URL do sitemap
python3 scripts/verifica-seo.py "$base" || falhas=$((falhas + 1))
# 8. Respostas da busca no idioma da página (o motor roda no navegador)
LOCALES="$ENABLED_LOCALES" node scripts/verifica-busca.mjs "$base" || falhas=$((falhas + 1))
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env node
import fs from "node:fs";
const root=process.argv[2]||".";
// Confere que a busca classifica os pinos (flash, PSRAM, USB, CAN...) igual ao português em todos os idiomas.
const norm=(s="")=>String(s).toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g,"");
const mods={pt:`${root}/lib/searchTopics.js`,en:`${root}/lib/data/en/topicos.js`,es:`${root}/lib/data/es/topicos.js`,fr:`${root}/lib/data/fr/topicos.js`,de:`${root}/lib/data/de/topicos.js`,"zh-CN":`${root}/lib/data/zh-CN/topicos.js`};
const T={};for(const l in mods)T[l]=(await import(new URL(mods[l],"file://"+process.cwd()+"/").href)).TOPICOS;
const dir=l=>l==="pt"?`${root}/public/conexoes`:`${root}/data/${l}/conexoes`;
const chips=fs.readdirSync(dir("pt")).filter(f=>f.endsWith(".json")).map(f=>f.slice(0,-5));
function niveis(l,chip,id){
const t=T[l].find(x=>x.id===id); const pins=JSON.parse(fs.readFileSync(`${dir(l)}/${chip}.json`,"utf8"));
return pins.map(p=>{const q={...p,_avs:(p.avisos||[]).map(norm),_fn:(p.funcoes||[]).map(norm),_blob:norm([p.nome,...(p.funcoes||[]),...(p.categorias||[]),...(p.avisos||[])].join(" "))};
if(t.pinoNivel) return t.pinoNivel(q)||"-";
if((t.pinoCats&&q.categorias?.some(c=>t.pinoCats.includes(c)))||(t.pinoRe&&t.pinoRe.test(q._blob))) return "forte"; return "-";});
}
let bad=0;
for(const id of ["flash","psram","ethernet","usb","jtag","adc","strapping","uart","spi","i2c","camera","lcd","can","touch","dac"]){
for(const chip of chips){const ref=niveis("pt",chip,id).join();
for(const l of Object.keys(mods)){ if(l==="pt")continue; const r=niveis(l,chip,id).join();
if(r!==ref){bad++; if(bad<=12) { console.log(`${id} ${chip} ${l}`); if(process.env.V){const a=ref.split(","),b=r.split(","),pp=JSON.parse(fs.readFileSync(`${dir(l)}/${chip}.json`,"utf8")),pq=JSON.parse(fs.readFileSync(`${dir("pt")}/${chip}.json`,"utf8"));a.forEach((x,i)=>{if(x!==b[i])console.log(" ",pp[i].nome,"pt="+x,l+"="+b[i],"\n pt:",(pq[i].avisos||[]).join(" | ").slice(0,160),"\n "+l+":",(pp[i].avisos||[]).join(" | ").slice(0,160),"\n fn:",JSON.stringify(pp[i].funcoes).slice(0,120))})}}}}}}
console.log(bad===0?" ✔ Busca de pinos: os 6 idiomas classificam igual ao português.":` ✖ Busca de pinos: ${bad} divergência(s) (rode com V=1).`);
process.exit(bad?1:0);
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env node
import fs from "node:fs";
const root=process.argv[2]||".";
// Confere que cada idioma agrupa os pinos restritos como o português (os padrões vêm do texto de cada idioma).
const src=fs.readFileSync(`${root}/components/PinRestrictions.jsx`,"utf8");
const LOCS={pt:"GRUPOS_PT",en:"GRUPOS_EN",es:"GRUPOS_ES",fr:"GRUPOS_FR",de:"GRUPOS_DE","zh-CN":"GRUPOS_ZH"};
function combinas(name){
const blk=src.match(new RegExp(`const ${name} = \\[([\\s\\S]*?)\\n\\];`))[1];
return blk.split(/\n\t\{\n/).slice(1).map(g=>{
const id=g.match(/id: "(\w+)"/)[1];
const m=g.match(/combina: ([\s\S]*?)\n\t\},?\s*$/)||g.match(/combina: ([\s\S]*?)\n\t\}/);
return {id,fn:eval(`(${m[1].replace(/,\s*$/,"")})`)};
});
}
const dir=(l)=>l==="pt"?`${root}/public/conexoes`:`${root}/data/${l}/conexoes`;
const chips=fs.readdirSync(dir("pt")).filter(f=>f.endsWith(".json")).map(f=>f.replace(".json",""));
const G={};for(const l in LOCS)G[l]=combinas(LOCS[l]);
function run(l,chip){
const pins=JSON.parse(fs.readFileSync(`${dir(l)}/${chip}.json`,"utf8"));
const res={};
for(const g of G[l]){res[g.id]=new Set();
pins.forEach((p,i)=>{ if(!["io","analog","dedicated"].includes(p.tipo))return;
const avisos=(p.avisos||[]).join(" ").toLowerCase(); const cats=p.categorias||[];
if(g.fn(avisos,cats)) res[g.id].add(i);});}
return {res,pins};
}
let bad=0;
for(const chip of chips){const ref=run("pt",chip).res;
for(const l in LOCS){ if(l==="pt")continue; const {res,pins}=run(l,chip);
for(const id in ref){const a=[...ref[id]].sort((x,y)=>x-y).join(),b=[...(res[id]||[])].sort((x,y)=>x-y).join();
if(a!==b){bad++;const falta=[...ref[id]].filter(i=>!res[id].has(i)),sobra=[...res[id]].filter(i=>!ref[id].has(i));
if(process.argv.includes("-v")) console.log(`${chip} ${l} ${id}: faltam ${falta.map(i=>pins[i].nome+":"+(pins[i].avisos||[]).join(" | ").slice(0,90))} | sobram ${sobra.map(i=>pins[i].nome)}`);
else console.log(`${chip} ${l} ${id}: faltam ${falta.length} sobram ${sobra.length}`);}}}}
console.log(bad===0?" ✔ Restrições de pinos: os 6 idiomas agrupam igual ao português.":` ✖ Restrições de pinos: ${bad} divergência(s) (rode com -v).`);
process.exit(bad?1:0);
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Confere canonical, hreflang e <html lang> de todas as URLs do sitemap.
python3 scripts/verifica-seo.py http://127.0.0.1:3998
Regras: o canonical é a própria URL; o conjunto de hreflang da página é o mesmo
do sitemap; há x-default quando existe mais de um idioma; o livro (só em pt)
não aponta para outros idiomas. Cada falha é impressa e o script sai com 1.
"""
import re
import sys
import urllib.request
from urllib.parse import urlparse
base = sys.argv[1].rstrip("/")
LANG = {"pt": "pt-BR", "en": "en", "es": "es-419", "fr": "fr", "de": "de", "zh-CN": "zh-CN"}
def get(url):
req = urllib.request.Request(url, headers={"User-Agent": "ESPDocs-SmokeTest/1.0"})
return urllib.request.urlopen(req, timeout=15).read().decode("utf-8")
def caminho(u):
return urlparse(u).path or "/"
sitemap = get(base + "/sitemap.xml")
falhas = 0
total = 0
for bloco in re.findall(r"<url>(.*?)</url>", sitemap, re.S):
loc = caminho(re.search(r"<loc>([^<]+)</loc>", bloco).group(1))
esperado = {
h: caminho(u)
for h, u in re.findall(r'<xhtml:link rel="alternate" hreflang="([^"]+)" href="([^"]+)"', bloco)
}
total += 1
try:
html = get(base + loc)
except Exception as e:
print(f"FALHOU {loc}: {e}")
falhas += 1
continue
head = html.split("</head>")[0]
idioma = loc.strip("/").split("/")[0]
m = re.search(r'<html[^>]*\blang="([^"]+)"', html)
if not m or m.group(1) != LANG.get(idioma):
print(f"FALHOU {loc}: <html lang> é {m and m.group(1)}, esperado {LANG.get(idioma)}")
falhas += 1
canon = re.search(r'<link rel="canonical" href="([^"]+)"', head)
if not canon or caminho(canon.group(1)) != loc:
print(f"FALHOU {loc}: canonical é {canon and caminho(canon.group(1))}")
falhas += 1
na_pagina = {
h: caminho(u)
for h, u in re.findall(r'<link rel="alternate" hrefLang="([^"]+)" href="([^"]+)"', head)
}
sem_default_pagina = {k: v for k, v in na_pagina.items() if k != "x-default"}
sem_default_sitemap = {k: v for k, v in esperado.items() if k != "x-default"}
if len(sem_default_sitemap) > 1 and sem_default_pagina != sem_default_sitemap:
print(f"FALHOU {loc}: hreflang da página difere do sitemap")
falhas += 1
if len(sem_default_sitemap) > 1 and "x-default" not in na_pagina:
print(f"FALHOU {loc}: sem x-default")
falhas += 1
if len(sem_default_sitemap) <= 1 and len(sem_default_pagina) > 1:
print(f"FALHOU {loc}: página de um idioma só com hreflang para outros: {sorted(sem_default_pagina)}")
falhas += 1
if falhas == 0:
print(f" ✔ SEO: canonical, hreflang e lang corretos em {total} URLs do sitemap.")
sys.exit(1 if falhas else 0)