import { TOPICOS, normalizar } from "./searchTopics.js"; /** * Motor da busca inteligente (puro: sem React nem DOM). * * Em vez de só casar texto, ele INTERPRETA a consulta: * "flash s3" → chip = ESP32-S3, assunto = Flash * e então monta respostas diretas: o que a série diz sobre aquele assunto, * quais pinos do diagrama se relacionam a ele (com link já filtrado) e, por * fim, os resultados de texto de sempre (glossário, erros, componentes...). */ const STOP = new Set([ "de", "do", "da", "dos", "das", "o", "a", "os", "as", "um", "uma", "no", "na", "nos", "nas", "em", "para", "pra", "pro", "com", "sem", "que", "qual", "quais", "como", "e", "ou", "ver", "sobre", "eh", "preciso", "quero", "onde", "fica", "ficam", "esta", "estao", "meu", "minha", "usar", "uso", ]); const INTENCAO_PINOS = new Set(["pino", "pinos", "pinagem", "pinout", "pin", "pins", "gpios"]); const INTENCAO_DADOS = new Set([ "dados", "especificacao", "especificacoes", "spec", "specs", "informacao", "informacoes", "info", "quanto", "quantos", "quantas", "capacidade", "limite", "limites", "tem", "possui", "suporta", ]); const EXEMPLOS_TOPICOS = ["flash", "psram", "usb", "adc", "i2c", "deep sleep", "wifi", "strapping"]; /** Distância de edição com corte: devolve > max assim que passa do limite. */ function distancia(a, b, max) { if (Math.abs(a.length - b.length) > max) return max + 1; let anterior = Array.from({ length: b.length + 1 }, (_, i) => i); for (let i = 1; i <= a.length; i++) { const atual = [i]; let minimo = i; for (let j = 1; j <= b.length; j++) { const v = Math.min(anterior[j] + 1, atual[j - 1] + 1, anterior[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)); atual[j] = v; if (v < minimo) minimo = v; } if (minimo > max) return max + 1; anterior = atual; } return anterior[b.length]; } const palavrasDe = (texto) => texto.split(/[^a-z0-9]+/).filter((w) => w.length > 1); /** Normaliza o payload do servidor uma única vez, na abertura da busca. */ export function preparar(dados) { if (!dados) return { itens: [], series: {}, porFuncao: new Map(), aliases: new Map(), chaves: [] }; const itensRaw = Array.isArray(dados) ? dados : (dados.itens ?? []); const seriesRaw = Array.isArray(dados) ? {} : (dados.series ?? {}); const itens = itensRaw.map((d) => { const _t = normalizar(d.titulo ?? ""); const _d = normalizar(d.descricao ?? ""); const _k = normalizar(d.keywords ?? ""); return { ...d, _t, _d, _k, _all: `${_t} ${_k} ${_d}`, _w: new Set(palavrasDe(`${_t} ${_k}`)) }; }); const series = {}; const porFuncao = new Map(); // "gpio26" | "mtck" -> [{ chave, pino }] const aliases = new Map(); for (const [chave, s] of Object.entries(seriesRaw)) { const pinos = (s.pinos ?? []).map((p) => { const fs = (p.f ?? []).map(normalizar); const avs = (p.a ?? []).map(normalizar); return { ...p, _fs: fs, _fn: fs, _avs: avs, _nome: normalizar(p.nome ?? ""), _blob: normalizar([p.nome ?? "", ...(p.f ?? []), ...(p.c ?? []), ...(p.a ?? [])].join(" ")), }; }); series[chave] = { ...s, pinos }; for (const p of pinos) { for (const f of new Set([p._nome, ...p._fs])) { if (!/^[a-z0-9_]{3,}$/.test(f)) continue; if (!porFuncao.has(f)) porFuncao.set(f, []); porFuncao.get(f).push({ chave, pino: p }); } } const k = normalizar(chave); aliases.set(k, chave); aliases.set(k.replace("-", ""), chave); if (k.startsWith("esp32-")) aliases.set(k.slice(6), chave); } return { itens, series, porFuncao, aliases, chaves: Object.keys(series) }; } /** Tenta casar um termo do dicionário com os tokens livres. Devolve { idx[], qualidade } ou null. */ function casarTermo(termo, livres) { const partes = termo.split(" "); if (partes.length > 1) { for (let i = 0; i + partes.length <= livres.length; i++) { if (partes.every((p, j) => livres[i + j].t === p)) { return { idx: partes.map((_, j) => livres[i + j].i), qualidade: 3 + partes.length }; } } return null; } for (const { t, i } of livres) { if (t === termo) return { idx: [i], qualidade: 3 }; if (t === `${termo}s` || `${t}s` === termo) return { idx: [i], qualidade: 2.5 }; if (termo.length >= 5 && t.length >= 5 && distancia(t, termo, 1) <= 1) return { idx: [i], qualidade: 1 }; } return null; } export function interpretar(indice, consulta) { const bruto = normalizar(consulta) .replace(/gpio\s*-?\s*(\d+)/g, "gpio$1") .replace(/[^a-z0-9.+\-_ ]/g, " "); const tokens = bruto.split(/\s+/).filter(Boolean); const usados = new Set(); // 1) Chips const chips = []; tokens.forEach((t, i) => { const chave = indice.aliases.get(t); if (chave && t !== "esp32") { if (!chips.includes(chave)) chips.push(chave); usados.add(i); } }); tokens.forEach((t, i) => { if (t !== "esp32") return; usados.add(i); if (!chips.length && indice.series.ESP32) chips.push("ESP32"); }); // 2) Pinos citados (GPIO26) e palavras de intenção / ligação const gpios = []; let querPinos = false; let querDados = false; tokens.forEach((t, i) => { if (/^gpio\d+$/.test(t)) { gpios.push(t); usados.add(i); } else if (INTENCAO_PINOS.has(t)) { querPinos = true; usados.add(i); } else if (INTENCAO_DADOS.has(t)) { querDados = true; usados.add(i); } else if (STOP.has(t)) { usados.add(i); } }); // 3) Assuntos: os melhores casamentos, sem reaproveitar token const livres = tokens.map((t, i) => ({ t, i })).filter(({ i }) => !usados.has(i)); const candidatos = []; for (const topico of TOPICOS) { let melhor = null; for (const termo of topico.termos) { const m = casarTermo(normalizar(termo), livres); if (m && (!melhor || m.qualidade > melhor.qualidade)) melhor = m; } if (melhor) candidatos.push({ topico, ...melhor }); } candidatos.sort((a, b) => b.qualidade - a.qualidade); const topicos = []; for (const c of candidatos) { if (topicos.length >= 3) break; if (c.idx.some((i) => usados.has(i))) continue; c.idx.forEach((i) => usados.add(i)); topicos.push(c.topico); } // 4) Nome/função de pino exato (ex.: "mtck", "xtal_p") const funcoes = []; tokens.forEach((t, i) => { if (usados.has(i) || t.length < 3) return; if (indice.porFuncao.has(t)) { funcoes.push(t); usados.add(i); } }); const restantes = tokens.filter((_, i) => !usados.has(i)); return { tokens, chips, topicos, gpios, funcoes, restantes, querPinos, querDados }; } const rotuloPino = (p) => { const gpio = p.f.find((f) => /^GPIO\d+$/i.test(f)); return gpio && gpio.toLowerCase() !== p.nome.toLowerCase() ? `${gpio}/${p.nome}` : p.nome; }; /** Pinos do diagrama ligados ao assunto, já separados por nível de certeza. */ function pinosDoTopico(serie, topico) { const forte = []; const fraco = []; for (const p of serie.pinos) { let nivel = null; if (topico.pinoNivel) nivel = topico.pinoNivel(p); else if ((topico.pinoCats && p.c.some((c) => topico.pinoCats.includes(c))) || (topico.pinoRe && topico.pinoRe.test(p._blob))) nivel = "forte"; if (nivel === "forte") forte.push(p); else if (nivel === "fraco") fraco.push(p); } return { forte, fraco }; } const hrefPinos = (chave, pinos, motivo) => `/series/${chave}?pinos=${pinos.map((p) => p.n).join(",")}&motivo=${encodeURIComponent(motivo)}#connections`; function respostas(indice, it) { const saida = []; const alvos = it.chips.slice(0, 2); for (const chave of alvos) { const serie = indice.series[chave]; if (!serie) continue; for (const topico of it.topicos.slice(0, 2)) { const specs = serie.specs.filter((s) => s.topicos.includes(topico.id)); const cartaoSpec = specs.length ? { tipo: "resposta", titulo: `${topico.nome} no ${serie.nome}`, descricao: "Dados da série", linhas: specs.slice(0, 5).map((s) => ({ k: s.label, v: s.valor })), href: `/series/${chave}#${specs[0].secao}`, } : null; const { forte, fraco } = pinosDoTopico(serie, topico); const principais = forte.length ? forte : fraco; const cartaoPinos = principais.length ? { tipo: "pino", titulo: `Pinos do ${serie.nome} ${topico.pinoRotulo}`, descricao: forte.length && fraco.length ? `${forte.length} ocupados + ${fraco.length} que dependem do modo de uso (ex.: SPI de 8 linhas) · abre o diagrama filtrado` : forte.length ? `${forte.length} ${forte.length === 1 ? "pino" : "pinos"} · abre o diagrama já filtrado` : `${fraco.length} pinos que só são afetados em certos modos de uso · abre o diagrama filtrado`, chips: forte .slice(0, 12) .map(rotuloPino) .concat(forte.length > 12 ? [`+${forte.length - 12}`] : []) .concat(fraco.length && forte.length ? [`+${fraco.length} condicionais`] : forte.length ? [] : fraco.slice(0, 10).map(rotuloPino)), href: hrefPinos(chave, [...forte, ...fraco], `${topico.nome}: pinos ${topico.pinoRotulo}`), } : null; (it.querPinos ? [cartaoPinos, cartaoSpec] : [cartaoSpec, cartaoPinos]).filter(Boolean).forEach((c) => saida.push(c)); } if (!it.topicos.length && !it.gpios.length && !it.funcoes.length) { if (it.querPinos && serie.pinos.length) { saida.push({ tipo: "pino", titulo: `Diagrama de pinos do ${serie.nome}`, descricao: `${serie.pinos.length} pinos com funções, restrições e avisos`, href: `/series/${chave}#connections`, }); } if (it.querDados) { saida.push({ tipo: "resposta", titulo: `Especificações do ${serie.nome}`, descricao: "Processador, memória, conectividade, periféricos e mais", href: `/series/${chave}` }); } } } // Assunto sem chip: panorama por série ("qual série tem PSRAM?") if (!alvos.length && it.topicos.length) { const topico = it.topicos[0]; const linhas = []; for (const chave of indice.chaves) { const serie = indice.series[chave]; const spec = serie.specs.find((s) => s.topicos.includes(topico.id)); if (spec) linhas.push({ k: serie.nome, v: spec.valor.length > 46 ? `${spec.valor.slice(0, 45)}…` : spec.valor }); } if (linhas.length > 1) { saida.push({ tipo: "resposta", titulo: `${topico.nome} em cada série`, descricao: "Comparativo rápido · adicione o chip na busca para detalhar", linhas, href: "/comparacao" }); } } // GPIO26 e nomes de função: "gpio26 s3", "mtck" const buscas = [...it.gpios, ...it.funcoes]; for (const termo of buscas) { const achados = indice.porFuncao.get(termo) ?? []; const filtrados = it.chips.length ? achados.filter((a) => it.chips.includes(a.chave)) : achados; for (const { chave, pino } of filtrados.slice(0, it.chips.length ? 3 : 5)) { const serie = indice.series[chave]; saida.push({ tipo: "pino", titulo: `${rotuloPino(pino)} no ${serie.nome}`, descricao: [pino.f.join(" · "), pino.a[0]].filter(Boolean).join(" — "), href: hrefPinos(chave, [pino], `Pino ${rotuloPino(pino)}`), }); } } return saida; } function pontuarTermo(item, t) { if (item._t === t) return 100; if (item._t.startsWith(t)) return 60; if (item._t.includes(t)) return 40; if (item._k.includes(t)) return 15; if (item._d.includes(t)) return 10; if (t.length >= 4) { const max = t.length >= 8 ? 2 : 1; for (const w of item._w) if (distancia(w, t, max) <= max) return 6; } return 0; } function casaTopico(item, topico) { let melhor = 0; for (const termo of topico.termos) { const n = normalizar(termo); if (n.includes(" ") ? item._all.includes(n) : item._w.has(n)) { melhor = Math.max(melhor, item._t.includes(n) ? 2 : 1); } } return melhor; } const topicoCasou = (item, topicos) => topicos.some((t) => casaTopico(item, t)); export function buscar(indice, consulta) { const it = interpretar(indice, consulta); const interpretacao = { chips: it.chips.map((c) => ({ chave: c, nome: indice.series[c]?.nome ?? c })), topicos: it.topicos.map((t) => ({ id: t.id, nome: t.nome })), }; const smart = respostas(indice, it).map((r, i) => ({ id: `r${i}`, keywords: "", ...r })); const nomesChips = it.chips.map((c) => new RegExp(`(? { if (item.tipo === "serie" && item.titulo === it.chips[i]) { s += 300; acertos++; } else if (nome.test(item._all)) { s += 25; acertos++; } }); const ehSerie = item.tipo === "serie" && it.chips.includes(item.titulo); if (!it.restantes.length && !(it.topicos.length ? acertos > (ehSerie ? 0 : 0) && topicoCasou(item, it.topicos) : ehSerie)) continue; if (s > 0) pontuados.push({ item, s }); } pontuados.sort((a, b) => b.s - a.s); // Quando já respondemos com cartões, a série que repete a resposta fica no fim. const texto = pontuados.slice(0, 24).map((p) => p.item); const sugestoes = []; if (it.topicos.length && !it.chips.length) { for (const c of indice.chaves.slice(0, 12)) sugestoes.push({ rotulo: indice.series[c].nome, anexar: c }); } else if (it.chips.length && !it.topicos.length && !it.gpios.length) { for (const nome of EXEMPLOS_TOPICOS) sugestoes.push({ rotulo: nome, anexar: nome }); } return { resultados: [...smart, ...texto], interpretacao, sugestoes, totalRespostas: smart.length }; } export const EXEMPLOS_DE_BUSCA = ["flash s3", "pinos usb c6", "deep sleep p4", "gpio0 esp32", "secure boot c3", "psram"];