Module:MedicoInfobox: Difference between revisions
From determinar.ia.br - Determine suas informações
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
-- Module:MedicoInfobox — lê a entidade Wikibase (por ID ou sitelink da página) | |||
-- e renderiza o infobox do médico (web + celular). | |||
-- P-IDs conforme o esquema real de determinar.ia.br | |||
local p = {} | local p = {} | ||
-- | -- ============ CONFIGURAÇÃO (ajuste se criar novas propriedades) ============ | ||
local | local CFG = { | ||
' | foto = 'P1', -- localMedia -> nome do arquivo | ||
' | nome = 'P2', -- monolingualtext | ||
'P4', -- | registro = 'P4', -- monolingualtext (CRM / RQE) | ||
'P14', -- | especialidade = 'P10', -- wikibase-item | ||
'P28', -- | subesp = 'P11', -- wikibase-item | ||
'P29', -- | servicos = { 'P13', 'P14', 'P18', 'P19', 'P23' }, -- wikibase-item | ||
' | logradouro = 'P28', -- monolingualtext | ||
' | bairro = 'P29', -- monolingualtext | ||
' | email = 'P30', -- string | ||
' | website = 'P31', -- url | ||
' | agendamento = 'P32', -- url (wa.link / agendamento) | ||
' | lat = 'P33', -- string | ||
' | lon = 'P34', -- string | ||
' | convenio = 'P35', -- monolingualtext | ||
' | estado = 'P36', -- monolingualtext | ||
'P39', -- | operadora = 'P37', -- string | ||
'P40', -- | opinioes = 'P38', -- quantity | ||
' | estrelas = 'P39', -- quantity (0–5) | ||
'P75', -- Google Maps ( | duvidas = 'P40', -- quantity | ||
' | doctoralia = 'P57', -- url | ||
municipio = 'P71', -- wikibase-item (Q102 = Campinas) | |||
cep = 'P72', -- string | |||
bio = 'P73', -- string (dentro do escopo) | |||
escopo_neg = 'P74', -- string (fora do escopo / desambiguação) | |||
maps_cid = 'P75', -- external-id (Google Maps CID) | |||
horario = 'P76', -- string | |||
publicacoes = 'P77', -- url (múltiplos) | |||
scholar_user = 'P62', -- external-id (Google Scholar) | |||
fonte = 'P79', -- qualificador: wikibase-item | |||
coleta = 'P80', -- qualificador: time | |||
} | } | ||
-- ===================================================================== | |||
local function esc( s ) | |||
if not s then return '' end | |||
return mw.text.encode( tostring( s ), '<>&"' ) | |||
end | |||
local | local function rotulo( ent, lang ) | ||
if not ent then return nil end | |||
local | local seq, seen = {}, {} | ||
for _, l in ipairs( { 'pt-br', 'pt', 'mul', lang, 'en' } ) do | |||
if not seen[l] then seen[l] = true; seq[#seq + 1] = l end | |||
end | |||
for _, l in ipairs( seq ) do | |||
local lb = ent:getLabel( l ) | |||
if lb then return lb end | |||
end | |||
return nil | |||
end | |||
local function | local function descricao( ent, lang ) | ||
if not ent then return '' end | |||
if not | local seq, seen = {}, {} | ||
local | for _, l in ipairs( { 'pt-br', 'pt', 'mul', lang, 'en' } ) do | ||
if not seen[l] then seen[l] = true; seq[#seq + 1] = l end | |||
end | |||
return | for _, l in ipairs( seq ) do | ||
local d = ent:getDescription( l ) | |||
if d then return d end | |||
end | |||
return '' | |||
end | end | ||
local function | local function snakParaTexto( snak, lang ) | ||
if not snak. | if not snak or snak.snaktype ~= 'value' then return nil end | ||
local | local dv = snak.dataValue | ||
if not dv then return nil end | |||
if dv.type == 'string' or dv.type == 'url' or dv.type == 'external-id' then | |||
return dv.value | |||
elseif dv.type == 'monolingualtext' then | |||
return dv.value.text | |||
elseif dv.type == 'quantity' then | |||
return tostring( dv.value.amount ):gsub( '^%+', '' ) | |||
elseif dv.type == 'time' then | |||
return ( dv.value.time or '' ):match( '(%d+%-%d+%-%d+)' ) | |||
elseif dv.type == 'wikibase-entityid' then | |||
local alvo = mw.wikibase.getEntity( dv.value.id ) | |||
return rotulo( alvo, lang ) or dv.value.id | |||
end | |||
return nil | |||
end | |||
if | local function valor( ent, pid, lang ) | ||
if not ent then return nil end | |||
local | local sts = ent:getBestStatements( pid ) | ||
if | if not sts then return nil end | ||
for _, st in ipairs( sts ) do | |||
local v = snakParaTexto( st:getMainSnak(), lang ) | |||
if v and v ~= '' then return v end | |||
end | |||
return nil | |||
end | |||
local function valores( ent, pid, lang ) | |||
local out = {} | |||
if not ent then return out end | |||
local sts = ent:getBestStatements( pid ) | |||
if not sts then return out end | |||
for _, st in ipairs( sts ) do | |||
local v = snakParaTexto( st:getMainSnak(), lang ) | |||
if v and v ~= '' then out[#out + 1] = v end | |||
end | |||
return out | |||
end | |||
local function qualificador( ent, pid, qpid, lang ) | |||
if not ent then return nil end | |||
local sts = ent:getBestStatements( pid ) | |||
if not sts or not sts[1] then return nil end | |||
local qs = sts[1]:getQualifiers() | |||
if not qs then return nil end | |||
for _, q in ipairs( qs ) do | |||
if q:getPropertyId() == qpid then | |||
return snakParaTexto( q, lang ) | |||
end | end | ||
end | |||
return nil | |||
end | |||
local function chips( lista ) | |||
local s = '' | |||
for _, v in ipairs( lista ) do | |||
s = s .. '<span class="chip-especialidade">' .. esc( v ) .. '</span> ' | |||
end | |||
return s | |||
end | |||
local function estrelas( n ) | |||
n = math.floor( tonumber( n ) or 0 ) | |||
n = math.max( 0, math.min( 5, n ) ) | |||
return string.rep( '★', n ) .. string.rep( '☆', 5 - n ) | |||
end | |||
function p.main( frame ) | |||
local args = frame.args | |||
local id = ( args.ID or args[1] or '' ):match( '^%s*(.-)%s*$' ) | |||
local lang = mw.getContentLanguage():getCode() | |||
local ent = mw.wikibase.getEntity( id ~= '' and id or nil ) | |||
return | if not ent then | ||
return '<div class="infobox-medico"><div class="infobox-medico-titulo">Entidade não encontrada — informe ID=Q… ou conecte a página à entidade no Wikibase.</div></div>' | |||
end | end | ||
local | local nome = valor( ent, CFG.nome, lang ) or rotulo( ent, lang ) or '—' | ||
local | local desc = descricao( ent, lang ) | ||
local | local foto = valor( ent, CFG.foto, lang ) | ||
local regs = valores( ent, CFG.registro, lang ) | |||
local esp = valor( ent, CFG.especialidade, lang ) | |||
local sub = valor( ent, CFG.subesp, lang ) | |||
local logradouro = valor( ent, CFG.logradouro, lang ) | |||
local bairro = valor( ent, CFG.bairro, lang ) | |||
local municipio = valor( ent, CFG.municipio, lang ) | |||
local estado = valor( ent, CFG.estado, lang ) | |||
local cep = valor( ent, CFG.cep, lang ) | |||
local horario = valor( ent, CFG.horario, lang ) | |||
local convenio = valor( ent, CFG.convenio, lang ) | |||
local operadora = valor( ent, CFG.operadora, lang ) | |||
local email = valor( ent, CFG.email, lang ) | |||
local website = valor( ent, CFG.website, lang ) | |||
local agenda = valor( ent, CFG.agendamento, lang ) | |||
local doc = valor( ent, CFG.doctoralia, lang ) | |||
local estrelasN = valor( ent, CFG.estrelas, lang ) | |||
local opinioes = valor( ent, CFG.opinioes, lang ) | |||
local bio = valor( ent, CFG.bio, lang ) | |||
local cid = valor( ent, CFG.maps_cid, lang ) | |||
local pubs = valores( ent, CFG.publicacoes, lang ) | |||
local scholar = valor( ent, CFG.scholar_user, lang ) | |||
local escopoNeg = valores( ent, CFG.escopo_neg, lang ) | |||
local fonteTxt = qualificador( ent, CFG.registro, CFG.fonte, lang ) or qualificador( ent, CFG.bio, CFG.fonte, lang ) | |||
local coletaTxt = qualificador( ent, CFG.registro, CFG.coleta, lang ) or qualificador( ent, CFG.bio, CFG.coleta, lang ) | |||
for _, | local servicos = {} | ||
for _, spid in ipairs( CFG.servicos ) do | |||
for _, v in ipairs( valores( ent, spid, lang ) ) do | |||
servicos[#servicos + 1] = v | |||
end | end | ||
end | end | ||
local | local out = {} | ||
out[#out + 1] = '<div class="infobox-medico">' | |||
if | |||
-- Foto | |||
if foto then | |||
out[#out + 1] = '<div class="infobox-medico-imagem">' .. frame:preprocess( '[[File:' .. foto .. '|300px|center]]' ) .. '</div>' | |||
end | |||
-- Título + descrição | |||
out[#out + 1] = '<div class="infobox-medico-titulo">' .. esc( nome ) .. '</div>' | |||
if desc and desc ~= '' then | |||
out[#out + 1] = '<div class="infobox-medico-descricao">' .. esc( desc ) .. '</div>' | |||
end | |||
-- Chips de especialidade | |||
local espChips = {} | |||
if esp then espChips[#espChips + 1] = esp end | |||
if sub then espChips[#espChips + 1] = sub end | |||
if #espChips > 0 then | |||
out[#out + 1] = '<div class="infobox-medico-chips">' .. chips( espChips ) .. '</div>' | |||
end | |||
-- Avaliação Doctoralia | |||
if estrelasN then | |||
local nota = '<div class="medico-avaliacao"><span class="medico-estrelas">' .. estrelas( estrelasN ) .. '</span>' | |||
.. ' <span class="medico-nota">' .. esc( estrelasN ) .. '/5</span>' | |||
if opinioes then | |||
nota = nota .. ' · <a href="' .. esc( doc or '#' ) .. '" target="_blank" rel="noopener nofollow">' .. esc( opinioes ) .. ' opiniões</a>' | |||
end | end | ||
out[#out + 1] = nota .. '</div>' | |||
end | end | ||
table | |||
return | -- Tabela de dados | ||
out[#out + 1] = '<table class="infobox-medico-tabela">' | |||
local function linha( r, v ) | |||
if v and v ~= '' then | |||
return '<tr><th>' .. esc( r ) .. '</th><td>' .. v .. '</td></tr>' | |||
end | |||
return '' | |||
end | end | ||
if #regs > 0 then | |||
end | local regHtml = {} | ||
for _, r in ipairs( regs ) do regHtml[#regHtml + 1] = esc( r ) end | |||
out[#out + 1] = linha( 'Registro', table.concat( regHtml, '<br>' ) ) | |||
end | |||
local partesEnd = {} | |||
for _, v in ipairs( { logradouro, bairro, municipio, estado, cep } ) do | |||
if | if v and v ~= '' then partesEnd[#partesEnd + 1] = v end | ||
end | |||
if #partesEnd > 0 then | |||
out[#out + 1] = linha( 'Endereço', esc( table.concat( partesEnd, ', ' ) ) ) | |||
end | end | ||
if horario then out[#out + 1] = linha( 'Horário', esc( horario ) ) end | |||
if | if convenio then out[#out + 1] = linha( 'Convênio', esc( convenio ) ) end | ||
if operadora then out[#out + 1] = linha( 'Operadora', esc( operadora ) ) end | |||
if email then out[#out + 1] = linha( 'E-mail', '<a href="mailto:' .. esc( email ) .. '">' .. esc( email ) .. '</a>' ) end | |||
if website then | |||
local href = website:match( '^https?://' ) and website or ( 'https://' .. website ) | |||
out[#out + 1] = linha( 'Site', '<a href="' .. esc( href ) .. '" target="_blank" rel="noopener nofollow">' .. esc( website ) .. '</a>' ) | |||
end | end | ||
if doc then | |||
out[#out + 1] = linha( 'Doctoralia', '<a href="' .. esc( doc ) .. '" target="_blank" rel="noopener nofollow">Ver perfil</a>' ) | |||
end | |||
out[#out + 1] = '</table>' | |||
-- Bio (dentro do escopo) | |||
if bio and bio ~= '' then | |||
out[#out + 1] = '<div class="infobox-medico-bio">' .. esc( bio ) .. '</div>' | |||
end | |||
-- Serviços / procedimentos | |||
if #servicos > 0 then | |||
out[#out + 1] = '<div class="infobox-medico-secao">Procedimentos e serviços</div>' | |||
out[#out + 1] = '<div class="infobox-medico-chips">' .. chips( servicos ) .. '</div>' | |||
end | |||
-- | -- CTA agendamento (WhatsApp) | ||
if agenda then | |||
if | out[#out + 1] = '<div class="cta-agendar"><a href="' .. esc( agenda ) .. '" target="_blank" rel="noopener nofollow">Agendar consulta</a></div>' | ||
end | end | ||
-- Mapa (Google Maps via CID, usando sua tag <perfilexterno>) | |||
if | if cid and cid ~= '' then | ||
local mapUrl = 'https://maps.google.com/maps?q=&cid=' .. cid .. '&output=embed&hl=pt-BR' | |||
out[#out + 1] = frame:extensionTag( 'perfilexterno', '', { url = mapUrl, largura = '100%', altura = '300' } ) | |||
end | end | ||
-- | -- Publicações científicas | ||
if | if #pubs > 0 then | ||
local | out[#out + 1] = '<div class="infobox-medico-secao">Publicações científicas</div>' | ||
out[#out + 1] = '<ul class="infobox-medico-pubs">' | |||
if | local n = math.min( 5, #pubs ) | ||
for i = 1, n do | |||
local u = pubs[i] | |||
out[#out + 1] = '<li><a href="' .. esc( u ) .. '" target="_blank" rel="noopener nofollow">' .. esc( u:gsub( '^https?://', '' ) ) .. '</a></li>' | |||
end | |||
out[#out + 1] = '</ul>' | |||
if #pubs > n then | |||
local alvo = scholar and ( 'https://scholar.google.com/citations?user=' .. scholar .. '&hl=pt-BR' ) or '#' | |||
out[#out + 1] = '<div class="infobox-medico-mais"><a href="' .. esc( alvo ) .. '" target="_blank" rel="noopener nofollow">+ ' .. ( #pubs - n ) .. ' publicações</a></div>' | |||
end | end | ||
end | end | ||
-- Desambiguação (fora do escopo) | |||
if #escopoNeg > 0 then | |||
out[#out + 1] = '<details class="medico-detalhes"><summary>Desambiguação — o que este perfil <b>não</b> é</summary><ul>' | |||
for _, v in ipairs( escopoNeg ) do | |||
out[#out + 1] = '<li>' .. esc( v ) .. '</li>' | |||
end | |||
out[#out + 1] = '</ul></details>' | |||
end | |||
-- Proveniência (diferencial anti-alucinação) | |||
if fonteTxt or coletaTxt then | |||
out[#out + 1] = '<div class="infobox-medico-rodape">Dados curados · fonte: ' .. esc( fonteTxt or '—' ) | |||
if coletaTxt then out[#out + 1] = ' · verificado em ' .. esc( coletaTxt ) end | |||
out[#out + 1] = '</div>' | |||
end | end | ||
return | out[#out + 1] = '</div>' | ||
return frame:preprocess( table.concat( out ) ) | |||
end | end | ||
return p | return p | ||
Revision as of 16:10, 3 August 2026
Documentation for this module may be created at Module:MedicoInfobox/doc
-- Module:MedicoInfobox — lê a entidade Wikibase (por ID ou sitelink da página)
-- e renderiza o infobox do médico (web + celular).
-- P-IDs conforme o esquema real de determinar.ia.br
local p = {}
-- ============ CONFIGURAÇÃO (ajuste se criar novas propriedades) ============
local CFG = {
foto = 'P1', -- localMedia -> nome do arquivo
nome = 'P2', -- monolingualtext
registro = 'P4', -- monolingualtext (CRM / RQE)
especialidade = 'P10', -- wikibase-item
subesp = 'P11', -- wikibase-item
servicos = { 'P13', 'P14', 'P18', 'P19', 'P23' }, -- wikibase-item
logradouro = 'P28', -- monolingualtext
bairro = 'P29', -- monolingualtext
email = 'P30', -- string
website = 'P31', -- url
agendamento = 'P32', -- url (wa.link / agendamento)
lat = 'P33', -- string
lon = 'P34', -- string
convenio = 'P35', -- monolingualtext
estado = 'P36', -- monolingualtext
operadora = 'P37', -- string
opinioes = 'P38', -- quantity
estrelas = 'P39', -- quantity (0–5)
duvidas = 'P40', -- quantity
doctoralia = 'P57', -- url
municipio = 'P71', -- wikibase-item (Q102 = Campinas)
cep = 'P72', -- string
bio = 'P73', -- string (dentro do escopo)
escopo_neg = 'P74', -- string (fora do escopo / desambiguação)
maps_cid = 'P75', -- external-id (Google Maps CID)
horario = 'P76', -- string
publicacoes = 'P77', -- url (múltiplos)
scholar_user = 'P62', -- external-id (Google Scholar)
fonte = 'P79', -- qualificador: wikibase-item
coleta = 'P80', -- qualificador: time
}
-- =====================================================================
local function esc( s )
if not s then return '' end
return mw.text.encode( tostring( s ), '<>&"' )
end
local function rotulo( ent, lang )
if not ent then return nil end
local seq, seen = {}, {}
for _, l in ipairs( { 'pt-br', 'pt', 'mul', lang, 'en' } ) do
if not seen[l] then seen[l] = true; seq[#seq + 1] = l end
end
for _, l in ipairs( seq ) do
local lb = ent:getLabel( l )
if lb then return lb end
end
return nil
end
local function descricao( ent, lang )
if not ent then return '' end
local seq, seen = {}, {}
for _, l in ipairs( { 'pt-br', 'pt', 'mul', lang, 'en' } ) do
if not seen[l] then seen[l] = true; seq[#seq + 1] = l end
end
for _, l in ipairs( seq ) do
local d = ent:getDescription( l )
if d then return d end
end
return ''
end
local function snakParaTexto( snak, lang )
if not snak or snak.snaktype ~= 'value' then return nil end
local dv = snak.dataValue
if not dv then return nil end
if dv.type == 'string' or dv.type == 'url' or dv.type == 'external-id' then
return dv.value
elseif dv.type == 'monolingualtext' then
return dv.value.text
elseif dv.type == 'quantity' then
return tostring( dv.value.amount ):gsub( '^%+', '' )
elseif dv.type == 'time' then
return ( dv.value.time or '' ):match( '(%d+%-%d+%-%d+)' )
elseif dv.type == 'wikibase-entityid' then
local alvo = mw.wikibase.getEntity( dv.value.id )
return rotulo( alvo, lang ) or dv.value.id
end
return nil
end
local function valor( ent, pid, lang )
if not ent then return nil end
local sts = ent:getBestStatements( pid )
if not sts then return nil end
for _, st in ipairs( sts ) do
local v = snakParaTexto( st:getMainSnak(), lang )
if v and v ~= '' then return v end
end
return nil
end
local function valores( ent, pid, lang )
local out = {}
if not ent then return out end
local sts = ent:getBestStatements( pid )
if not sts then return out end
for _, st in ipairs( sts ) do
local v = snakParaTexto( st:getMainSnak(), lang )
if v and v ~= '' then out[#out + 1] = v end
end
return out
end
local function qualificador( ent, pid, qpid, lang )
if not ent then return nil end
local sts = ent:getBestStatements( pid )
if not sts or not sts[1] then return nil end
local qs = sts[1]:getQualifiers()
if not qs then return nil end
for _, q in ipairs( qs ) do
if q:getPropertyId() == qpid then
return snakParaTexto( q, lang )
end
end
return nil
end
local function chips( lista )
local s = ''
for _, v in ipairs( lista ) do
s = s .. '<span class="chip-especialidade">' .. esc( v ) .. '</span> '
end
return s
end
local function estrelas( n )
n = math.floor( tonumber( n ) or 0 )
n = math.max( 0, math.min( 5, n ) )
return string.rep( '★', n ) .. string.rep( '☆', 5 - n )
end
function p.main( frame )
local args = frame.args
local id = ( args.ID or args[1] or '' ):match( '^%s*(.-)%s*$' )
local lang = mw.getContentLanguage():getCode()
local ent = mw.wikibase.getEntity( id ~= '' and id or nil )
if not ent then
return '<div class="infobox-medico"><div class="infobox-medico-titulo">Entidade não encontrada — informe ID=Q… ou conecte a página à entidade no Wikibase.</div></div>'
end
local nome = valor( ent, CFG.nome, lang ) or rotulo( ent, lang ) or '—'
local desc = descricao( ent, lang )
local foto = valor( ent, CFG.foto, lang )
local regs = valores( ent, CFG.registro, lang )
local esp = valor( ent, CFG.especialidade, lang )
local sub = valor( ent, CFG.subesp, lang )
local logradouro = valor( ent, CFG.logradouro, lang )
local bairro = valor( ent, CFG.bairro, lang )
local municipio = valor( ent, CFG.municipio, lang )
local estado = valor( ent, CFG.estado, lang )
local cep = valor( ent, CFG.cep, lang )
local horario = valor( ent, CFG.horario, lang )
local convenio = valor( ent, CFG.convenio, lang )
local operadora = valor( ent, CFG.operadora, lang )
local email = valor( ent, CFG.email, lang )
local website = valor( ent, CFG.website, lang )
local agenda = valor( ent, CFG.agendamento, lang )
local doc = valor( ent, CFG.doctoralia, lang )
local estrelasN = valor( ent, CFG.estrelas, lang )
local opinioes = valor( ent, CFG.opinioes, lang )
local bio = valor( ent, CFG.bio, lang )
local cid = valor( ent, CFG.maps_cid, lang )
local pubs = valores( ent, CFG.publicacoes, lang )
local scholar = valor( ent, CFG.scholar_user, lang )
local escopoNeg = valores( ent, CFG.escopo_neg, lang )
local fonteTxt = qualificador( ent, CFG.registro, CFG.fonte, lang ) or qualificador( ent, CFG.bio, CFG.fonte, lang )
local coletaTxt = qualificador( ent, CFG.registro, CFG.coleta, lang ) or qualificador( ent, CFG.bio, CFG.coleta, lang )
local servicos = {}
for _, spid in ipairs( CFG.servicos ) do
for _, v in ipairs( valores( ent, spid, lang ) ) do
servicos[#servicos + 1] = v
end
end
local out = {}
out[#out + 1] = '<div class="infobox-medico">'
-- Foto
if foto then
out[#out + 1] = '<div class="infobox-medico-imagem">' .. frame:preprocess( '[[File:' .. foto .. '|300px|center]]' ) .. '</div>'
end
-- Título + descrição
out[#out + 1] = '<div class="infobox-medico-titulo">' .. esc( nome ) .. '</div>'
if desc and desc ~= '' then
out[#out + 1] = '<div class="infobox-medico-descricao">' .. esc( desc ) .. '</div>'
end
-- Chips de especialidade
local espChips = {}
if esp then espChips[#espChips + 1] = esp end
if sub then espChips[#espChips + 1] = sub end
if #espChips > 0 then
out[#out + 1] = '<div class="infobox-medico-chips">' .. chips( espChips ) .. '</div>'
end
-- Avaliação Doctoralia
if estrelasN then
local nota = '<div class="medico-avaliacao"><span class="medico-estrelas">' .. estrelas( estrelasN ) .. '</span>'
.. ' <span class="medico-nota">' .. esc( estrelasN ) .. '/5</span>'
if opinioes then
nota = nota .. ' · <a href="' .. esc( doc or '#' ) .. '" target="_blank" rel="noopener nofollow">' .. esc( opinioes ) .. ' opiniões</a>'
end
out[#out + 1] = nota .. '</div>'
end
-- Tabela de dados
out[#out + 1] = '<table class="infobox-medico-tabela">'
local function linha( r, v )
if v and v ~= '' then
return '<tr><th>' .. esc( r ) .. '</th><td>' .. v .. '</td></tr>'
end
return ''
end
if #regs > 0 then
local regHtml = {}
for _, r in ipairs( regs ) do regHtml[#regHtml + 1] = esc( r ) end
out[#out + 1] = linha( 'Registro', table.concat( regHtml, '<br>' ) )
end
local partesEnd = {}
for _, v in ipairs( { logradouro, bairro, municipio, estado, cep } ) do
if v and v ~= '' then partesEnd[#partesEnd + 1] = v end
end
if #partesEnd > 0 then
out[#out + 1] = linha( 'Endereço', esc( table.concat( partesEnd, ', ' ) ) )
end
if horario then out[#out + 1] = linha( 'Horário', esc( horario ) ) end
if convenio then out[#out + 1] = linha( 'Convênio', esc( convenio ) ) end
if operadora then out[#out + 1] = linha( 'Operadora', esc( operadora ) ) end
if email then out[#out + 1] = linha( 'E-mail', '<a href="mailto:' .. esc( email ) .. '">' .. esc( email ) .. '</a>' ) end
if website then
local href = website:match( '^https?://' ) and website or ( 'https://' .. website )
out[#out + 1] = linha( 'Site', '<a href="' .. esc( href ) .. '" target="_blank" rel="noopener nofollow">' .. esc( website ) .. '</a>' )
end
if doc then
out[#out + 1] = linha( 'Doctoralia', '<a href="' .. esc( doc ) .. '" target="_blank" rel="noopener nofollow">Ver perfil</a>' )
end
out[#out + 1] = '</table>'
-- Bio (dentro do escopo)
if bio and bio ~= '' then
out[#out + 1] = '<div class="infobox-medico-bio">' .. esc( bio ) .. '</div>'
end
-- Serviços / procedimentos
if #servicos > 0 then
out[#out + 1] = '<div class="infobox-medico-secao">Procedimentos e serviços</div>'
out[#out + 1] = '<div class="infobox-medico-chips">' .. chips( servicos ) .. '</div>'
end
-- CTA agendamento (WhatsApp)
if agenda then
out[#out + 1] = '<div class="cta-agendar"><a href="' .. esc( agenda ) .. '" target="_blank" rel="noopener nofollow">Agendar consulta</a></div>'
end
-- Mapa (Google Maps via CID, usando sua tag <perfilexterno>)
if cid and cid ~= '' then
local mapUrl = 'https://maps.google.com/maps?q=&cid=' .. cid .. '&output=embed&hl=pt-BR'
out[#out + 1] = frame:extensionTag( 'perfilexterno', '', { url = mapUrl, largura = '100%', altura = '300' } )
end
-- Publicações científicas
if #pubs > 0 then
out[#out + 1] = '<div class="infobox-medico-secao">Publicações científicas</div>'
out[#out + 1] = '<ul class="infobox-medico-pubs">'
local n = math.min( 5, #pubs )
for i = 1, n do
local u = pubs[i]
out[#out + 1] = '<li><a href="' .. esc( u ) .. '" target="_blank" rel="noopener nofollow">' .. esc( u:gsub( '^https?://', '' ) ) .. '</a></li>'
end
out[#out + 1] = '</ul>'
if #pubs > n then
local alvo = scholar and ( 'https://scholar.google.com/citations?user=' .. scholar .. '&hl=pt-BR' ) or '#'
out[#out + 1] = '<div class="infobox-medico-mais"><a href="' .. esc( alvo ) .. '" target="_blank" rel="noopener nofollow">+ ' .. ( #pubs - n ) .. ' publicações</a></div>'
end
end
-- Desambiguação (fora do escopo)
if #escopoNeg > 0 then
out[#out + 1] = '<details class="medico-detalhes"><summary>Desambiguação — o que este perfil <b>não</b> é</summary><ul>'
for _, v in ipairs( escopoNeg ) do
out[#out + 1] = '<li>' .. esc( v ) .. '</li>'
end
out[#out + 1] = '</ul></details>'
end
-- Proveniência (diferencial anti-alucinação)
if fonteTxt or coletaTxt then
out[#out + 1] = '<div class="infobox-medico-rodape">Dados curados · fonte: ' .. esc( fonteTxt or '—' )
if coletaTxt then out[#out + 1] = ' · verificado em ' .. esc( coletaTxt ) end
out[#out + 1] = '</div>'
end
out[#out + 1] = '</div>'
return frame:preprocess( table.concat( out ) )
end
return p
