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
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:
1 parent
5a9fe676fc
commit
fe1802f818
67 files changed
+11875
-9596
No files matched your search
+13
-10
@@ -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).`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Executable
+22
@@ -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);
|
||||
Executable
+35
@@ -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);
|
||||
Executable
+75
@@ -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)
|
||||
Reference in new issue
Block a user