File size: 13,723 Bytes
477ba05
58c5622
90d2399
2442fcd
ccfa465
2442fcd
90d2399
dd5deef
 
2442fcd
90d2399
2442fcd
 
 
 
cb892f1
 
dd5deef
cb892f1
 
 
 
 
477ba05
 
 
 
 
dd5deef
2442fcd
dd5deef
2442fcd
 
dd5deef
2442fcd
 
 
 
 
dd5deef
 
 
2442fcd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd5deef
2442fcd
 
dd5deef
 
2442fcd
 
dd5deef
2442fcd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cb892f1
2442fcd
 
477ba05
 
 
2442fcd
 
477ba05
 
2442fcd
477ba05
 
 
 
 
2442fcd
cb892f1
2442fcd
cb892f1
dd5deef
 
ccfa465
2442fcd
cb892f1
 
 
2442fcd
477ba05
ccfa465
477ba05
 
2442fcd
 
cb892f1
 
2442fcd
dd5deef
2442fcd
477ba05
cb892f1
2442fcd
cb892f1
2442fcd
ccfa465
cb892f1
2442fcd
 
 
 
477ba05
2442fcd
cb892f1
 
 
 
 
dd5deef
477ba05
2442fcd
 
 
 
 
 
 
 
 
477ba05
cb892f1
ccfa465
2442fcd
cb892f1
2442fcd
90d2399
cb892f1
90d2399
cb892f1
90d2399
2442fcd
 
 
 
477ba05
 
 
2442fcd
ccfa465
cb892f1
2442fcd
 
90d2399
2442fcd
cb892f1
dd5deef
2442fcd
 
 
 
cb892f1
ccfa465
2442fcd
cb892f1
2442fcd
 
477ba05
 
 
cb892f1
2442fcd
 
 
 
 
 
ccfa465
2442fcd
477ba05
cb892f1
90d2399
2442fcd
 
 
 
 
 
 
ccfa465
90d2399
cb892f1
fe16c68
2442fcd
dd5deef
2442fcd
 
 
cb892f1
fe16c68
58c5622
cb892f1
69c45ad
90d2399
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# -*- coding: utf-8 -*-
import gradio as gr
import html2text
from bs4 import BeautifulSoup, Comment
import logging
import re

logging.basicConfig(level=logging.INFO)

def extrair_limpar_html_v5(html_bruto):
    """
    Extrai o conteúdo principal (priorizando .entry-content), remove
    elementos irmãos indesejados (tags, nav, comments, related), limpa
    o conteúdo principal e retorna o HTML limpo.
    V5: Adaptado para a estrutura HTML fornecida.

    :param html_bruto: String contendo o código HTML original.
    :return: String contendo o HTML limpo e focado no conteúdo principal.
    """
    if not html_bruto:
        return ""

    soup = BeautifulSoup(html_bruto, 'html.parser')

    # --- 0. Remover comentários HTML ---
    for comment in soup.find_all(string=lambda text: isinstance(text, Comment)):
        comment.extract()

    target_element = None
    main_container = None # Guarda o elemento que contém o target_element e os irmãos

    # --- 1. Encontrar o Contêiner Principal Específico (.entry-content) ---
    # Seletores em ordem de preferência para este site
    main_content_selectors = [
        '.entry-content',           # O mais provável para o corpo do post neste HTML
        '.wp-block-post-content',   # Alternativa
        'article',                  # Fallback
        'main',                     # Fallback mais amplo
        # '[role="main"]',          # Menos provável neste tema
    ]

    for selector in main_content_selectors:
        target_element = soup.select_one(selector)
        if target_element:
            logging.info(f"Conteúdo principal identificado usando o seletor: '{selector}'")
            # Tenta encontrar um pai razoável para procurar irmãos
            # Anda alguns níveis acima se necessário, mas não até o body/html se possível
            potential_main_container = target_element.parent
            levels_up = 0
            while potential_main_container and potential_main_container.name in ['div', 'section'] and levels_up < 3:
                 # Verifica se este pai contém os blocos indesejados como irmãos do target
                 if potential_main_container.select_one('.wp-block-post-terms, .wp-block-comments, .wp-block-query'):
                      main_container = potential_main_container
                      logging.info(f"Container principal para busca de irmãos definido como: <{main_container.name}>")
                      break
                 potential_main_container = potential_main_container.parent
                 levels_up += 1

            # Se não encontrou um container com irmãos indesejados, usa o pai direto
            if not main_container:
                 main_container = target_element.parent
                 if main_container:
                      logging.info(f"Container principal para busca de irmãos definido como pai direto: <{main_container.name}>")


            break # Para ao encontrar o primeiro target

    # Fallback se nenhum seletor específico funcionou
    if not target_element:
        logging.warning("Nenhum seletor de conteúdo principal específico (.entry-content, article, main) encontrado.")
        # Tenta usar o body, mas a limpeza de irmãos não será eficaz
        if soup.body:
            target_element = soup.body
            main_container = soup.body # Define main_container como body
            logging.info("Usando <body> como target_element e main_container.")
        else:
            logging.error("Falha crítica: Nenhum elemento de conteúdo ou body encontrado.")
            return "" # Não há nada para processar

    # Se não conseguiu definir um main_container, não pode remover irmãos
    if not main_container:
         logging.warning("Não foi possível determinar um container válido para remover irmãos.")
         # Prossegue limpando apenas o target_element encontrado

    # --- 2. Remover Elementos Irmãos Indesejados (SE main_container foi definido) ---
    if main_container and target_element is not main_container: # Só remove irmãos se o target não for o próprio container
        logging.info(f"Procurando irmãos indesejados de <{target_element.name}> dentro de <{main_container.name}>...")
        siblings_to_remove_selectors = [
            '.wp-block-post-terms',       # Bloco de Tags
            '.wp-container-core-group-is-layout-9b36172e', # Div que contém a navegação Prev/Next (baseado no HTML)
            '.wp-block-comments',         # Bloco de comentários inteiro
            '.wp-block-query',            # Bloco "Mais Posts" (Query Loop)
            # Poderíamos ser mais específicos para "Mais Posts", mas .wp-block-query parece ok aqui
            # Exemplo: 'div.wp-block-group:has(> h2:contains("Mais posts"))' # Requer análise mais complexa
        ]
        removed_siblings_count = 0
        # Itera sobre os elementos DENTRO do main_container
        for element in main_container.find_all(recursive=False): # Apenas filhos diretos ou netos? Melhor procurar em todo o container
             # Verifica se o elemento atual NÃO é o target_element ou um de seus pais
            if element is not target_element and not element.find(target_element):
                for selector in siblings_to_remove_selectors:
                     # Verifica se o elemento corresponde a um seletor indesejado
                     # Usamos select_one para garantir que estamos testando o próprio elemento
                     # Ou podemos usar element.matches(selector) se a versão do bs4 suportar bem
                     if element.select_one(f':is({selector})'): # :is() para testar o próprio elemento
                         logging.info(f"  Removendo irmão/elemento indesejado: <{element.name} class='{' '.join(element.get('class',[]))}'> (match com '{selector}')")
                         element.decompose()
                         removed_siblings_count += 1
                         break # Sai do loop de seletores para este elemento

        # Abordagem alternativa/complementar: Buscar DEPOIS do target_element
        for sibling in target_element.find_next_siblings():
             for selector in siblings_to_remove_selectors:
                 # Verifica se o irmão corresponde a um seletor indesejado
                 if sibling.select_one(f':is({selector})'): # :is() para testar o próprio irmão
                     logging.info(f"  Removendo irmão SEGUINTE indesejado: <{sibling.name} class='{' '.join(sibling.get('class',[]))}'> (match com '{selector}')")
                     sibling.decompose()
                     removed_siblings_count += 1
                     break # Vai para o próximo irmão


        if removed_siblings_count > 0:
            logging.info(f"Removidos {removed_siblings_count} elementos/irmãos indesejados.")
        else:
            logging.info("Nenhum elemento/irmão indesejado conhecido foi encontrado ou removido após o conteúdo principal.")

    # --- 3. Limpeza Geral DENTRO do target_element isolado ---
    logging.info(f"Iniciando limpeza geral DENTRO do target_element: <{target_element.name}>")
    tags_para_remover_geral = [
        'script', 'style', 'form', 'input', 'button', 'select', 'textarea', 'label',
        'footer', 'header', 'nav', 'aside', 'iframe', 'noscript', 'meta', 'link',
        'canvas', 'svg', 'audio', 'video', 'figure', # Remover figure, manter figcaption permitido
        # '.wp-block-button', # Remover botões? Pode ser útil manter alguns. Avaliar.
    ]
    removed_general_count = 0
    # Importante: usar find_all DENTRO do target_element
    for tag_name in tags_para_remover_geral:
         for tag in target_element.find_all(tag_name):
            tag.decompose()
            removed_general_count +=1
    if removed_general_count > 0:
        logging.info(f"Removidas {removed_general_count} tags gerais indesejadas dentro do target_element.")

    # --- 4. Limpar Atributos e Tags Restantes no target_element ---
    tags_permitidas = {
        'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'a', 'strong', 'b',
        'em', 'i', 'u', 's', 'strike', 'del', 'ul', 'ol', 'li', 'img',
        'table', 'thead', 'tbody', 'tr', 'th', 'td', 'blockquote', 'pre', 'code',
        'figcaption'
    }
    atributos_permitidos = {
        'a': ['href', 'title'],
        'img': ['src', 'alt', 'title', 'width', 'height'],
        'th': ['colspan', 'rowspan', 'scope'],
        'td': ['colspan', 'rowspan'],
        'blockquote': ['cite'],
        'ol': ['start'],
        'pre': [], # Geralmente não precisa de atributos
        'code': ['class'], # Permitir classe para syntax highlighting (ex: class="language-python")
    }

    # Iterar sobre uma cópia da lista de tags DENTRO do target_element
    for tag in list(target_element.find_all(True)):
        if not tag.parent: continue # Ignora tags já removidas

        if tag.name not in tags_permitidas:
            tag.unwrap() # Remove tag, mantém conteúdo
        else:
            # Limpa atributos
            atributos_para_manter = atributos_permitidos.get(tag.name, [])
            attrs_mantidos = {}
            # Mantém atributos essenciais primeiro
            if tag.name == 'a' and 'href' in tag.attrs: attrs_mantidos['href'] = tag.attrs['href']
            if tag.name == 'img' and 'src' in tag.attrs: attrs_mantidos['src'] = tag.attrs['src']
            if tag.name == 'img' and 'alt' in tag.attrs: attrs_mantidos['alt'] = tag.attrs['alt'] # Manter ALT

            # Adiciona outros permitidos
            for attr, value in tag.attrs.items():
                if attr in atributos_para_manter:
                    attrs_mantidos[attr] = value
            tag.attrs = attrs_mantidos

    # Retorna o HTML limpo e focado como string
    html_final = str(target_element)
    html_final = html_final.replace(' ', ' ')
    # Remover divs vazios que podem sobrar após unwrap
    soup_final = BeautifulSoup(html_final, 'html.parser')
    for div in soup_final.find_all('div'):
        if not div.get_text(strip=True) and not div.find(['img', 'br']): # Se não tem texto nem imagem/br
             div.decompose()
    html_final = str(soup_final)

    logging.info("Limpeza final do HTML concluída.")
    return html_final


def html_para_markdown_final_v5(html_input):
    """
    Pipeline completo V5: Extrai .entry-content, remove irmãos, limpa, converte.
    """
    if not html_input:
        return "Por favor, insira algum código HTML."

    try:
        # 1. Extrai, remove irmãos indesejados e limpa HTML
        logging.info("--- Iniciando Extração e Limpeza V5 ---")
        html_processado = extrair_limpar_html_v5(html_input)
        logging.info("--- Extração e Limpeza V5 Concluída ---")

        soup_check = BeautifulSoup(html_processado, 'html.parser')
        if not html_processado or not soup_check.get_text(strip=True):
             logging.warning("HTML resultante V5 após limpeza está vazio ou sem texto.")
             return "HTML resultante após extração e limpeza está vazio ou não contém texto."

        # 2. Converte o HTML processado para Markdown (Config V4/V2)
        logging.info("--- Iniciando Conversão para Markdown V5 ---")
        converter = html2text.HTML2Text()
        converter.body_width = 0
        converter.ignore_links = False
        converter.ignore_images = False
        converter.ignore_emphasis = False
        converter.use_automatic_links = True
        converter.unicode_snob = True
        converter.escape_snob = True

        markdown_output = converter.handle(html_processado)
        logging.info("--- Conversão para Markdown V5 Concluída ---")

        # 3. Pós-processamento do Markdown (Simplificado - V4/V2)
        logging.info("--- Iniciando Pós-processamento do Markdown V5 ---")
        linhas = [line.strip() for line in markdown_output.splitlines()]
        linhas_filtradas = [line for line in linhas if line]
        markdown_output = "\n\n".join(linhas_filtradas)

        # Limpeza final extra
        markdown_output = re.sub(r' +', ' ', markdown_output) # Múltiplos espaços
        markdown_output = re.sub(r' +\n', '\n', markdown_output) # Espaços antes de \n
        # Remover marcadores de lista vazios ou estranhos que podem sobrar
        markdown_output = re.sub(r'\n\n[-*+]\s*\n\n', '\n\n', markdown_output)
        markdown_output = re.sub(r'^\s*[-*+]\s*\n\n', '', markdown_output) # No início

        logging.info("--- Pós-processamento do Markdown V5 Concluído ---")
        return markdown_output.strip()

    except Exception as e:
        logging.error(f"Erro durante o processo V5: {e}", exc_info=True)
        try: html_on_error = html_processado
        except NameError: html_on_error = "(HTML não disponível)"
        return (f"Ocorreu um erro V5: {str(e)}\n\n"
                f"Verifique os logs do Space.\n\n"
                f"HTML processado antes do erro:\n"
                f"{html_on_error[:2000]}...")


# --- Cria a interface Gradio ---
iface = gr.Interface(
    fn=html_para_markdown_final_v5, # Usando a função V5
    inputs=gr.Textbox(lines=20, label="Insira o HTML bruto aqui", placeholder="Cole o código-fonte HTML completo da página..."),
    outputs=gr.Textbox(lines=20, label="Markdown Resultante (Conteúdo Principal Limpo - V5)", show_copy_button=True),
    title="Conversor HTML para Markdown (V5 - Específico para Estrutura WP)",
    description="Cole o HTML. O script tenta isolar '.entry-content', remove tags/comentários/relacionados/nav que vêm *depois* dele, limpa o HTML restante e converte para Markdown (formatação V2/V4).",
    allow_flagging='never'
)

# --- Lança a interface ---
if __name__ == "__main__":
    iface.launch()