Files
emcp-tools-mapping/docs/02-ATOMIC-V4-GUTENBERG.md
T
Claude Code 8ada367bd0 docs: mapeamento completo do EMCP Tools (11 docs, ~6070 linhas)
Especificação funcional do plugin emcp-tools v3.12.1 (msrbuilds/elementor-mcp,
GPL-2.0-or-later) via leitura directa do código-fonte em emanuelalmeida.pt.

- 00: arquitectura (bootstrap, ability registrar, dispatcher, MCP adapter)
- 01: Elementor classico (paginas, layout, widgets, templates, globals)
- 02: Elementor Atomic v4 + Gutenberg
- 03: WordPress core (conteudo, media, settings, temas)
- 04: Themer (CPT, condicoes, render, PHP templates)
- 05: Redirects + change ledger unificado (rollback)
- 06: Sandbox PHP snippets + custom widgets
- 07: Filesystem/DB/WP-CLI/Security/Performance (maior risco)
- 08: Integracoes terceiros (ACF, Meta Box, forms, SEO)
- 09: Stock images + Cloud + OAuth
- 10: Sistema de modulos + inventario Pro-only (30 classes)
- INDEX: sintese, sequencia de construcao, tabela de risco

Produzido por 10 subagentes code-explorer em paralelo + revisao cruzada de
consistencia. Cada doc inclui blueprint de replica (copiar/simplificar/omitir).
2026-08-19 06:41:04 +01:00

43 KiB

02 — Elementor Atomic v4 (widgets/layout/global classes de leitura) e Gutenberg nativo

Fonte: leitura directa do código-fonte emcp-tools v3.12.1 (build Free), instalado em emanuelalmeida.pt (/home/ealmeida/emanuelalmeida.pt/wp-content/plugins/emcp-tools/), 19-08-2026. Complementa docs/00-ARQUITECTURA.md (arquitectura geral, contrato de emcp_tools_register_ability()) e skill://emcp-tools (postura de segurança/deny-list).

0. Panorama e gating

Este documento cobre três classes de abilities e seis serviços de suporte. Todas as três classes de abilities são instanciadas em EMCP_Tools_Ability_Registrar::register_groups() (includes/abilities/class-ability-registrar.php), mas com gating muito diferente:

Classe Onde é instanciada no registrar Gate adicional dentro da própria classe
EMCP_Tools_Atomic_Widget_Abilities Dentro do bloco if ( $elementor_active ) register() faz return cedo se EMCP_Tools_Atomic_Props::is_atomic_supported() for false — nenhuma das 10 tools regista
EMCP_Tools_Atomic_Layout_Abilities Dentro do bloco if ( $elementor_active ) Mesmo guard is_atomic_supported() — e, ao contrário do que a doc-comment do código sugere, isto também bloqueia detect-elementor-version (ver §3, gotcha)
EMCP_Tools_Gutenberg_Abilities Na secção "always-on" (topo de register_groups()), sem verificar $elementor_active Nenhum — regista sempre, mesmo com Elementor completamente ausente/inactivo

Consequência prática: num site com Elementor activo mas sem o Elementor 4.0+/atomic ligado (a maioria dos sites em 2026, dado que is_atomic_supported() não é uma simples verificação de versão — ver §4), as 19 tools atomic (10 + 3, menos 1 sobreposta, ver tabelas) não existem de todo no wp_get_abilities(); as 10 tools Gutenberg existem sempre, com ou sem Elementor.


1. EMCP_Tools_Atomic_Widget_Abilities — includes/abilities/class-atomic-widget-abilities.php

Condição de registo: classe instanciada só quando $elementor_active é true; dentro dela, register() só prossegue se EMCP_Tools_Atomic_Props::is_atomic_supported() devolver true (ver §4 para o mecanismo de detecção).

Duas tools "universais" (aceitam qualquer widget_type atómico com settings em bruto no formato $$type) mais oito tools de conveniência (uma por widget atómico, com parâmetros planos que a própria classe converte para $$type via EMCP_Tools_Atomic_Widget_Map).

Tool Input schema (resumo) O que faz permission_callback readonly / destructive / idempotent
add-atomic-widget post_id(int,req), parent_id(string,req), position(int, -1=append), widget_type(string,req, ex. e-heading), settings(object, valores já em $$type) Tool genérica: cria o elemento via EMCP_Tools_Element_Factory::create_atomic_widget() com settings passados tal-e-qual (sem conveniência), insere no parent_id na position dada, grava a página. check_edit_permission (edit_posts + edit_post do post_id se dado) false / false / false
update-atomic-widget post_id(int,req), element_id(string,req), settings(object,req, $$type-wrapped) Merge PARCIAL de settings num widget atómico já existente (só as chaves fornecidas mudam) via EMCP_Tools_Data::update_element_settings(). check_edit_permission false / false / true
add-atomic-heading post_id,parent_id(req), position, title, tag(enum h1-h6, default h2), link, css_id Widget e-heading. Mapeia title→prop title (html-v3), tag→prop tag (string). check_edit_permission false / false / false
add-atomic-paragraph post_id,parent_id(req), position, content, link, css_id Widget e-paragraph. Gotcha: a prop real chama-se paragraph, não text — ver §5. check_edit_permission false / false / false
add-atomic-button post_id,parent_id(req), position, text, link, target_blank(bool), css_id Widget e-button. link aceita target_blank. check_edit_permission false / false / false
add-atomic-image post_id,parent_id(req), position, image_id(int) OU image_url(string), alt, link, css_id Widget e-image. image_id XOR image_url. Para image_id, o alt é escrito em _wp_attachment_image_alt (não na prop) — ver §5. check_edit_permission false / false / false
add-atomic-svg post_id,parent_id(req), position, svg_id(int) OU svg_url(string), css_id Widget e-svg. Usa o tipo svg-src, distinto de image-src. check_edit_permission false / false / false
add-atomic-youtube post_id,parent_id,video_url(todos req), position, css_id Widget e-youtube. source é uma prop STRING simples (não um shape). check_edit_permission false / false / false
add-atomic-video post_id,parent_id(req), position, video_url(string) OU video_id(int), css_id Widget e-self-hosted-video. source é o shape video-src (XOR id/url) — diferente de add-atomic-youtube. check_edit_permission false / false / false
add-atomic-divider post_id,parent_id(req), position, css_id Widget e-divider. Sem conteúdo próprio; só a cauda partilhada (link/css_id/classes, mas divider não usa link na prática). check_edit_permission false / false / false

Mecanismo partilhado das 8 convenience tools: register_atomic_convenience() monta um schema comum (post_id,parent_id,position + os extra_props de cada widget) e um execute_callback genérico que: (1) chama $settings_fn($input) — um closure que invoca EMCP_Tools_Atomic_Widget_Map::settings($widget_type, $input); (2) constrói o elemento via $this->factory->create_atomic_widget(); (3) se o input tiver parâmetros de estilo comuns (padding, background_color, min_height, etc.), constrói-os via EMCP_Tools_Atomic_Styles::build_common_props() e aplica-os como uma classe de estilo local via create_local_class() + apply_to_element(); (4) insere e grava. Isto significa que qualquer convenience tool aceita implicitamente os parâmetros de estilo comuns (padding, background_color, min_height, width, border_radius, color, etc.) mesmo que não apareçam no extra_props explícito de cada tool individual, porque build_common_props() corre sobre o $input inteiro.

check_edit_permission($input): requer current_user_can('edit_posts'); se post_id for fornecido e não-zero, requer adicionalmente current_user_can('edit_post', $post_id).


2. EMCP_Tools_Atomic_Layout_Abilities — includes/abilities/class-atomic-layout-abilities.php

Condição de registo: idêntica à classe anterior — instanciada só com $elementor_active, e register() faz return cedo se is_atomic_supported() for false. Ver §3 para o gotcha sobre detect-elementor-version.

Tool Input schema (resumo) O que faz permission_callback readonly / destructive / idempotent
add-flexbox post_id(req), parent_id(vazio=top-level), position, tag(enum div/header/section/article/aside/footer), direction(row/column/…), justify, align, gap+gap_unit, wrap, css_id, padding, background_color, min_height Cria um container e-flexbox (Elementor 4.0+). As propriedades de layout (direction/justify/align/gap/wrap) e as comuns (padding/background/min-height) são extraídas de uma lista fixa de style_keys no execute_add_flexbox(), convertidas via EMCP_Tools_Atomic_Styles, e aplicadas como classe de estilo local — não via register_atomic_convenience() (esta tool tem o seu próprio execute_callback, não reutiliza o mecanismo da classe Widget). Se parent_id vazio, insere top-level (array_splice/append directo em vez de insert_element()). check_edit_permission false / false / false
add-div-block post_id(req), parent_id, position, tag(mesmo enum), css_id, padding, background_color Cria um container e-div-block (layout de fluxo/bloco, NÃO flex) — para quando não se quer um flexbox. Mesmo padrão de inserção top-level vs parent_id. check_edit_permission false / false / false
detect-elementor-version Sem input (properties: {}) Devolve elementor_version (ELEMENTOR_VERSION), elementor_pro_version, supports_atomic (via is_atomic_supported()), supports_container (via is_container_supported()), recommended_mode (atomic|legacy|unsupported) e, se unsupported, um warning a avisar que os experiments "Flexbox Container" / "Atomic Elements" estão ambos desligados e que páginas criadas via MCP vão gravar dados mas renderizar vazias. Ver gotcha em baixo — na prática, esta tool só está disponível quando recommended_mode já seria atomic. closure inline: current_user_can('edit_posts') true / false / true

GOTCHA de código encontrado (não documentado como tal no próprio ficheiro): a doc-comment acima de register_detect_elementor_version() diz literalmente // Detect version (always registers, even on < 4.0). Mas o método register() da classe é:

public function register(): void {
    if ( ! EMCP_Tools_Atomic_Props::is_atomic_supported() ) {
        return;   // <-- sai ANTES de chamar register_detect_elementor_version()
    }
    $this->register_add_flexbox();
    $this->register_add_div_block();
    $this->register_detect_elementor_version();
}

O return cedo bloqueia as TRÊS chamadas, incluindo a de detect-elementor-version — pelo que esta tool só existe quando o site JÁ suporta atomic, exactamente o cenário oposto ao mais útil (um agente que precisa de descobrir se deve usar tools legacy ou atomic não consegue chamar esta tool quando mais precisa dela — nos sites em legacy/unsupported a tool simplesmente não aparece em wp_get_abilities()). Numa réplica, isto seria trivial de corrigir: mover register_detect_elementor_version() para fora do guard (registá-la sempre, independentemente de is_atomic_supported()).


3. EMCP_Tools_Gutenberg_Abilities — includes/abilities/class-gutenberg-abilities.php

Condição de registo: sempre — está na secção "always-on" do registrar ($gutenberg = new EMCP_Tools_Gutenberg_Abilities(); $gutenberg->register();), sem nenhum if ($elementor_active) nem verificação de módulo. É pura WordPress core: opera sobre post_content de qualquer post via parse_blocks()/serialize_blocks() (funções nativas do WP) e EMCP_Tools_Block_Tree (§7). Dez tools no total, desenhadas como um fluxo discover→schema→edit incremental por PATH.

Tool Input schema (resumo) O que faz permission_callback readonly / destructive / idempotent
list-blocks category, search (ambos opcionais) Lista block types registados via WP_Block_Type_Registry::get_instance()->get_all_registered(), filtrável por categoria/substring em nome+título. Devolve {name,title,category} por linha. Passo 1 do fluxo "construir página de blocos". check_read_permission true / false / true
get-block-schema name(string) OU names(string[], lote) Devolve {name,title,category,attributes,supports,example} por block type — example é um snippet mínimo de markup gerado (<!-- wp:{short} -->…<!-- /wp:{short} -->). Passo 2, antes de add-block. Nomes não registados devolvem {name,error} em vez de falhar o lote inteiro. check_read_permission true / false / true
get-post-blocks post_id(req), depth(opcional, limita profundidade) Devolve a árvore de blocos do post com um PATH de índices por bloco (ex. [2,1]), via EMCP_Tools_Block_Tree::from_markup()+summarize(). Chamada obrigatória antes de qualquer update-block/remove-block/move-block/duplicate-block para obter os paths actuais. check_read_permission true / false / true
list-patterns search, category (opcionais) Lista block patterns registados via WP_Block_Patterns_Registry, filtrável. Devolve {name,title,categories,description}. check_read_permission true / false / true
add-block post_id(req), markup(string,req, pode conter vários blocos), position({mode,path}) Insere markup Gutenberg bruto numa posição. position.mode: append|prepend|before|after|inside (os últimos três exigem position.path, resolvido via get-post-blocks). Valida que o path resolve para um bloco antes de inserir. check_write_permission false / false / false
update-block post_id(req), path(int[],req), markup(string,req) Substitui o bloco no path por novo markup (pode expandir para vários blocos). check_write_permission false / false / false
remove-block post_id(req), path(int[],req) Apaga o bloco no path (e os seus innerBlocks). Única tool Gutenberg marcada destructive:true. check_write_permission false / true / false
move-block post_id(req), path(int[],req), position({mode,path},req) Move o bloco de path para uma nova posição. Delegado a EMCP_Tools_Block_Tree::move(), que tem guards de segurança próprios (ver §7). check_write_permission false / false / false
duplicate-block post_id(req), path(int[],req) Clona o bloco no path, insere a cópia imediatamente a seguir. Devolve o path da cópia (calculado como path com o último índice +1 — assume que duplicate() sempre insere logo a seguir ao original no mesmo nível). check_write_permission false / false / false
insert-pattern post_id(req), pattern_name(string,req, de list-patterns), position({mode,path}) Insere um pattern registado (resolvido via WP_Block_Patterns_Registry) numa posição, expandindo o content do pattern para blocos via parse_blocks(). check_write_permission false / false / false

Permissões: check_read_permission($input) requer edit_posts, mais edit_post($post_id) se post_id for dado (mas não é required em todas as tools de leitura — só get-post-blocks o exige no schema). check_write_permission($input) é mais estrito: requer post_id presente e não-zero e edit_post($post_id) — nunca aceita uma escrita sem post_id concreto.

Persistência (save_tree()): todas as seis tools de escrita convergem em save_tree(), que faz wp_update_post(['ID'=>…, 'post_content'=>wp_slash(Block_Tree::to_markup($tree))]). Gotcha citado no código: wp_update_post() corre wp_unslash() sobre os dados, e a serialização de blocos emite escapes de barra invertida (&, \", \\, …) nos atributos — por isso o markup TEM de ser "slashed" antes de chegar a wp_update_post(), senão esses escapes são removidos e o bloco corrompe-se. save_tree() também regista a alteração no change-ledger via EMCP_Tools_Change_Recorder::record_post_fields() (domínio gutenberg, action block-write), guardando o post_content ANTERIOR — é o que permite rollback-change (doc 05) desfazer uma edição de blocos.


4. Serviços de suporte

4.1 EMCP_Tools_Atomic_Props — includes/class-atomic-props.php (973 linhas)

O coração do sistema $$type. Todo valor de prop atómico Elementor 4.0+ é um envelope { '$$type': '<tipo>', 'value': <dados> } — o objectivo desta classe é (a) construir esses envelopes a partir de valores simples que um agente de IA escreveria naturalmente, e (b) fazer o caminho inverso para leitura (unwrap()), mais (c) uma camada de auto-correcção que salvou o plugin de uma classe inteira de bugs de produção (ver os números de issue citados no próprio código).

Builders de envelope (métodos estáticos, um por tipo primitivo/composto):

Método Tipo $$type produzido Nota de design
string($v) string Trivial.
number($v) number Trivial.
boolean($v) boolean Trivial.
size($size,$unit='px') size → {size,unit} Usado para qualquer dimensão CSS.
color($color) color Não é string — a prop color é um Color_Prop_Type e exige o envelope color; um string é rejeitado.
background_color($color) background → {color: <color-prop>} Não existe prop background-color — o Elementor guarda fundo como Background_Prop_Type cujo campo color é ele próprio um color-prop aninhado. Escrever background-color directamente é silenciosamente descartado.
dimensions($sides) dimensions → {block-start,block-end,inline-start,inline-end} Shape partilhado por padding/margin. Não existe prop padding-block-start individual — construir por lado sem usar este wrapper é descartado no save.
html($text) html-v3 → {content:<string-prop>, children:[]} Usado para qualquer conteúdo de texto rico (heading, paragraph, button…). O nome do tipo já evoluiu html→html-v2→html-v3; a classe segue a Elementor como fonte de verdade em vez de fixar o nome (ver coerce_against_prop).
url($url) url Trivial.
link($url,$target_blank=false) link → {destination:<url-prop>, tag:<string-prop>('a'), isTargetBlank?:<boolean-prop>} isTargetBlank só é incluído quando true (omitido, não false, quando não pedido).
classes($ids=[]) classes Array de IDs de classe (locais e-* ou globais g-*).
image($id,$url='',$alt='') image → {src:{$$type:'image-src', value:{id,url}}} id XOR url — Image_Src_Prop_Type exige exactamente um dos dois, o outro TEM de ser null (não omitido). Passar ambos, ou um id como number em vez de image-attachment-id, produz image: invalid_value (issue #74). alt só entra no envelope quando é uma imagem por url; para attachment é ignorado pelo Elementor (renderiza sempre o alt da media library) — ver EMCP_Tools_Atomic_Widget_Map::image().
video_src($id,$url='') video-src → {id:{$$type:'video-attachment-id',...}} OU {url:<url-prop>} Shape distinto de image-src; um envelope url simples faz o Elementor rejeitar o elemento inteiro (source: invalid_value) em vez de só ignorar o valor — foi o que impedia add-atomic-video de funcionar de todo no Elementor 4.2 antes desta correcção.
svg($id,$url='') svg-src Tipo distinto de image-src — usar image() para um e-svg falha (issue #74).

Introspecção de schema (props_schema()): em vez de fixar hard-coded que prop cada widget espera, a classe pergunta directamente ao próprio Elementor: \Elementor\Plugin::$instance->widgets_manager->get_widget_types($widget_type)::get_props_schema(), com cache estática por $widget_type (uma passagem de coerção sobre uma página inteira pergunta pelo mesmo punhado de schemas centenas de vezes).

A camada de auto-correcção (coerce_settings/coerce_with_schema/coerce_tree): um agente de IA vai escrever naturalmente 'title' => 'Hello' em vez do envelope {'$$type':'html-v3', value:{...}}. Sem correcção, o Elementor cai para o valor por omissão da prop (elemento renderiza texto placeholder) e todo save subsequente dessa página passa a falhar com Settings validation failed — a página fica impossível de editar tanto via API como via editor (issue #101). A correcção:

  1. apply_prop_aliases() — renomeia chaves alias (text/content/heading → o nome canónico title) usando a MESMA metadata que o Elementor expõe ($prop->get_meta_item('aliases')), antes de qualquer validação. Crítico: o Props_Parser do Elementor descarta silenciosamente chaves que não reconhece (não rejeita, apaga) — por isso uma chave alias não corrigida a tempo perde o conteúdo em vez de ser rejeitada com erro visível (issue #102). Um valor já presente sob o nome canónico nunca é substituído por um alias.
  2. coerce_against_prop()/candidates_for()/coerce_shape() — para cada prop, se o valor já for aceite por $prop->validate(), fica como está; senão constrói candidatos a partir dos próprios get_prop_types()/get_key()/get_shape() do prop Elementor (nunca hardcoded) e testa cada um contra validate(), usando o primeiro aceite. Cobre tanto valores planos (string→envelope certo) como shapes compostos (ex. link legado {url,is_external} → {destination,isTargetBlank}).
  3. coerce_tree() — corre sobre a ÁRVORE INTEIRA no save, não só o elemento tocado, porque o Elementor valida a página inteira de uma vez: um único widget por corrigir, em qualquer parte da página, bloqueava até a própria edição destinada a reparar a página (issue #102).

unwrap()/unwrap_array(): direcção inversa — usado por get-element-settings (doc 01) para devolver valores planos e legíveis a um agente em vez do envelope $$type bruto.

is_atomic_supported() / is_container_supported(): a peça mais subtil de todo o ficheiro. Não é baseada em version_compare(ELEMENTOR_VERSION, '4.0.0', '>=') — o Elementor lança o atomic/v4 como experiment opt-in enquanto ELEMENTOR_VERSION continua a reportar um valor 3.x. O sinal AUTORITATIVO é se os TIPOS de elemento e-flexbox/e-div-block estão realmente REGISTADOS ($elementor->elements_manager->get_element_types()), porque é isso que garante que Document::save() preserva os dados em vez de os sanitizar silenciosamente. Deliberadamente NÃO usa o experiment e_opt_in_v4_page (que liga o EDITOR v4 sem garantir que os tipos de elemento estão registados — um site pode ter esse experiment ligado e e_atomic_elements desligado, escrever "com sucesso" e _elementor_data fica vazio após o save). Cai depois para os experiments e_atomic_elements/atomic_widgets, e só por último para o version_compare genérico (fallback forward-compatible). Comentário explícito no código: "NB: do NOT use class_exists('\Elementor\Modules\AtomicWidgets\Module') as a signal — that class is autoloaded even when the atomic experiment is OFF". is_container_supported() segue o mesmo padrão para o experiment legado (3.x) container.

4.2 EMCP_Tools_Atomic_Widget_Map — includes/class-atomic-widget-map.php

Mapa único de "parâmetros amigáveis → settings $$type", partilhado por §1 (convenience tools) E pela tool composta build-page (doc 01) — razão de existir: build-page passava settings de widgets atómicos em bruto, e como props complexas (e-image.image, e-self-hosted-video.source) não têm chave equivalente em bruto, o widget ficava vazio. Ao centralizar aqui, ambos os caminhos produzem settings byte-idênticas para o mesmo input.

atomic_types(): os 8 tipos conhecidos — e-heading, e-paragraph, e-button, e-image, e-svg, e-youtube, e-self-hosted-video, e-divider. settings($widget_type,$params) despacha para um builder privado por tipo; is_atomic($widget_type) verifica pertença.

Builder Gotcha documentado no código
heading() Directo — title→html, tag→string.
paragraph() A prop chama-se paragraph, não text (Html_V3) — escrever text apagava o conteúdo silenciosamente (issue #56).
button() Directo, mas passa $link_target_blank=true ao finish() partilhado (só o botão honra target_blank).
image() image_id XOR image_url. Para image_id, escreve o alt em update_post_meta($image_id, '_wp_attachment_image_alt', $alt) — a única forma que faz efeito, porque e-image não tem prop alt de topo e para uma attachment o Elementor renderiza sempre o alt da media library.
svg() Usa EMCP_Tools_Atomic_Props::svg() (tipo svg-src), nunca image().
youtube() source é EMCP_Tools_Atomic_Props::string() — um union de string simples, não um shape.
video() source é EMCP_Tools_Atomic_Props::video_src() — um shape XOR id/url, distinto de youtube() apesar do nome de prop idêntico (source). Um envelope url simples faz o Elementor recusar o elemento inteiro.
divider() Vazio — só a cauda partilhada.

finish($settings,$params,$link_target_blank=false): cauda partilhada por todos os builders — adiciona link (se presente, com esc_url_raw()), _cssid (se presente, com sanitize_text_field()), e sempre classes (vazio, ponto de ancoragem para EMCP_Tools_Atomic_Styles::apply_to_element() adicionar depois uma classe local).

4.3 EMCP_Tools_Atomic_Styles — includes/class-atomic-styles.php

Constrói e aplica o mecanismo v4 de "classe de estilo local": em vez de propriedades CSS inline no elemento, o v4 guarda estilo num mapa styles no próprio elemento, referenciado por ID de classe em settings.classes.value[].

  • create_local_class($element_id,$props,$breakpoint='desktop',$state=null) — constrói UM variant (par breakpoint+state) de uma definição de classe: {id,label:'local',type:'class', variants:[{meta:{breakpoint,state}, props, custom_css:null}]}.
  • mint_class_id($element_id) — gera e-<element_id>-<7hex>; o ID incorpora deliberadamente o ID do elemento dono, porque as classes locais v4 pertencem a um único elemento.
  • remap_local_classes(&$element) — corrige um bug real de duplicação (issue #97): quando um elemento é duplicado com um id novo, as suas classes locais v4 (e-<id-antigo>-<hash>) continuam a embutir o id de ORIGEM e ficam partilhadas com a fonte — uma escrita posterior no mapa styles sangra entre os dois, e o popover "Style Origin" do editor mostra entradas duplicadas. Este método re-minta as chaves do mapa styles (e o id de cada style_def) contra o id ACTUAL do elemento, e repõe settings.classes.value das IDs antigas para as novas — só toca em classes LOCAIS deste elemento; classes globais (g-…) referenciadas ficam intocadas.
  • build_flex_props($params) — mapeia parâmetros planos (direction/flex_direction, justify/justify_content, align/align_items, wrap/flex_wrap, gap+gap_unit, row_gap, column_gap) para props CSS $$type em kebab-case (flex-direction, justify-content, …).
  • build_common_props($params) — width/min_height/border_radius (size simples); padding/margin via build_dimensions() (shorthand de 4 lados — um valor único aplica aos 4 lados, *_top/_right/_bottom/_left definem por lado individualmente, o shorthand ganha se ambos presentes; não existe prop padding-block-start individual, construir por lado sem este wrapper é descartado no save); background_color (via Atomic_Props::background_color(), nunca uma prop background-color); color (via Atomic_Props::color(), nunca string).
  • apply_to_element(&$element,$class_id,$style_def) — push de $class_id em element.settings.classes.value[] e de $style_def em element.styles[$class_id].

4.4 EMCP_Tools_Widget_Loader — includes/class-widget-loader.php

Fora do âmbito directo atomic/Gutenberg — pertence ao mecanismo Sandbox de widgets Elementor gerados (doc 06), mas foi incluído nesta batch de leitura. Padrão de design digno de nota:

  • Carregamento manifest-only — nunca faz scan-and-include de um directório; lê um manifesto de widgets activos, verifica cada ficheiro contra o seu sha256 registado (guarda contra adulteração), e inclui dentro de isolamento de erro fatal.
  • Shutdown handler de atribuição — se um include_once disparar um fatal de compilação/parse (que um try/catch não apanha, porque um parse error num ficheiro incluído aborta o request), um register_shutdown_function() regista o $this->loading (post ID do widget a meio de inclusão) e, no shutdown, atribui o fatal a esse widget e desactiva-o — garantindo que um widget mau nunca consegue white-screenar o site repetidamente.
  • Gate Pro total — has_access() exige emcp_tools_fs()->can_use_premium_code(); num build Free/sem licença (como este), tanto register_widgets() como register_assets() saem imediatamente — é um NO-OP total neste site.
  • Regista handles de CSS/JS (wp_register_style/wp_register_script) só como metadata em wp_enqueue_scripts — o Elementor só enfileira efectivamente quando o widget está presente na página, mantendo o custo baixo mesmo com muitos widgets activos.

4.5 EMCP_Tools_Widget_Catalog + includes/widgets/catalog-free.php — includes/widgets/class-widget-catalog.php

Fonte única de metadata para widgets Elementor CLÁSSICOS (pré-4.0/não-atomic) — usada por list-widgets, get-widget-schema, add-free-widget, add-pro-widget (documentadas no doc 01, Elementor clássico). Não é usada pelas tools atomic desta doc (essas usam EMCP_Tools_Atomic_Widget_Map + introspecção ao vivo do props_schema() do Elementor).

EMCP_Tools_Widget_Catalog::get() funde três ficheiros de dados estáticos (catalog-free.php+catalog-pro.php+catalog-woo.php) num único array chaveado por widget_type, com cache em memória estática (self::$catalog). API de leitura: get_widget($type), all_types(), by_tier($tier), tier_of($type), is_pro($type), search($query) (substring case-insensitive sobre type+title+use_case+keywords, usado por list-widgets para pesquisa por intenção), flush_cache() (seam de teste).

catalog-free.php — 680 linhas, 26 widgets clássicos gratuitos. Cada entrada é um array com a forma:

'<widget_type>' => [
    'tier'     => 'free',
    'title'    => 'Nome legível',
    'category' => 'basic',
    'requires' => null,               // ou o slug do plugin exigido (null nos gratuitos)
    'use_case' => 'Frase para pesquisa por intenção.',
    'keywords' => ['palavra1', 'palavra2', ...],
    'params'   => [ 'nome_prop' => ['type'=>..., 'enum'=>[...], 'description'=>...], ... ],
    'required' => ['prop_obrigatoria'],
    'defaults' => ['prop' => valor],
],

Os 26 widgets: heading, text-editor, image, button, video, icon, spacer, divider, icon-box, accordion, alert, counter, icon-list, image-box, image-carousel, progress, social-icons, star-rating, tabs, testimonial, toggle, html, menu-anchor, shortcode, rating, text-path. Cada params é um schema simplificado mas fiel aos formatos NATIVOS de controlo Elementor (não $$type — isto é o formato clássico _elementor_data, ex.: {size,unit} para dimensões, {url,is_external, nofollow} para links, {value,library} para ícones, yes/'' para toggles clássicos em vez de booleanos reais). Exemplo representativo (button): 24 params cobrindo texto, link, tamanho, tipo, alinhamento, ícone+posição, cores (normal/hover, fundo/texto/borda), animação de hover, borda (estilo/largura/cor/raio), box-shadow, tipografia completa (família, tamanho, peso, transform, letter-spacing), text-shadow, padding — o nível de detalhe é tipicamente 15-25 params por widget, reflectindo directamente os controlos Elementor reais.

catalog-pro.php — 1049 linhas, 30 widgets Elementor Pro (existência confirmada, conteúdo NÃO lido em detalhe por instrução de âmbito). Estrutura de dados idêntica a catalog-free.php (mesma forma de array, tier'=>'pro', requires'=>'elementor-pro'). Pelos nomes das chaves top-level visíveis no ficheiro (sem ler os params internos): form, posts, countdown, price-table, flip-box, animated-headline, call-to-action, slides, testimonial-carousel, price-list, gallery, share-buttons, table-of-contents, blockquote, lottie, hotspot, nav-menu, loop-grid, loop-carousel, media-carousel, nested-tabs, nested-accordion, portfolio, author-box, login, code-highlight, reviews, off-canvas, progress-tracker, search — parecem cobrir formulários, grids de posts dinâmicos (loop), navegação, carrosséis multimédia, e widgets de UI avançada (nested-tabs/accordion, off-canvas, progress-tracker), tudo dependente de elementor-pro.

catalog-woo.php — 92 linhas, 5 widgets WooCommerce (existência confirmada, conteúdo NÃO lido em detalhe por instrução de âmbito). Mesma forma de array, tier'=>'woo', requires'=>'woocommerce'. Chaves: woocommerce-products, wc-add-to-cart, woocommerce-cart, woocommerce-checkout-page, woocommerce-menu-cart — cobrem grid de produtos, botão de compra, e as páginas completas de carrinho/checkout como widgets embebíveis, mais um mini-carrinho para menu/header.

4.6 EMCP_Tools_Block_Tree — includes/class-block-tree.php

Transformações puras e sem estado sobre parse_blocks()/serialize_blocks() (funções core do WordPress). Blocos são endereçados por um PATH de índices (array de ints): [0] = primeiro bloco top-level (após remover blocos separadores em branco), [2,1] = innerBlocks[1] do bloco top-level de índice 2. Todos os métodos de mutação devolvem uma ÁRVORE NOVA; nenhum muta in-place.

  • from_markup()/to_markup() — wrappers de parse/serialize (blocos unidos por linha em branco); strip_separators() remove blocos top-level de separador (blockName null + HTML em branco).
  • at($blocks,$path) — resolve um path para um nó ou null.
  • insert/replace/remove/duplicate/move — as 5 mutações principais, todas construídas sobre dois primitivos de baixo nível: edit_siblings() (navega até ao array de irmãos que CONTÉM o nó do path, aplica um callback que recebe (siblings, index) e devolve o novo array de irmãos) e edit_node() (wrapper fino para editar o próprio nó).
  • move() tem três guards de segurança explícitos que valem a pena replicar tal-e-qual:
    1. Mover relativo a si próprio (mode before/after com $from === $to) é no-op.
    2. Rejeita um movimento cujo alvo está DENTRO da própria subárvore do nó movido — senão o nó seria removido e depois a inserção falharia (o path já não resolve), perdendo o bloco silenciosamente.
    3. Correcção de deslocamento de índice: remove() desloca cada irmão posterior sob o pai de $from uma posição para a esquerda. Quando o path-alvo passa pelo MESMO pai numa posição posterior à de $from, esse índice fica desactualizado — é decrementado antes de insert(), aplicando-se a QUALQUER modo (before/after entre irmãos OU inside um container posterior) a qualquer profundidade.
  • summarize($blocks,$depth,$prefix) — vista compacta com path por get-post-blocks: {path, blockName, attributes, innerBlocksCount}, com innerBlocks aninhado só até $depth (se dado).
  • inner_content_for() — o internals mais delicado do ficheiro. Reconstrói o array innerContent de um bloco container quando o número de innerBlocks muda, PRESERVANDO o HTML de wrapper do container. innerContent intercala chunks de string literal (o HTML do wrapper) com placeholders null (um por bloco filho, consumidos por ordem por serialize_block()). Dois caminhos: (a) o container já tinha filhos (havia nulls) → mantém o chunk antes do primeiro null e depois do último, reemite um null por filho novo; (b) container estava vazio (sem nulls) → "descasca" a sequência final de tags de fecho via regex (/((?:\s*<\/[a-zA-Z][a-zA-Z0-9]*>)+\s*)$/) para que os filhos inseridos fiquem DENTRO do wrapper em vez de depois dele.

5. Blueprint para réplica

Copiar quase 1:1 (alto valor, baixo risco de reescrever mal):

  • EMCP_Tools_Atomic_Props inteiro. É a peça mais valiosa deste documento. Reescrever do zero equivaleria a reproduzir ~2 anos de bugs de produção já corrigidos e documentados nas próprias issues citadas no código (#36, #56, #74, #97, #101, #102, #111). Atenção especial a três decisões de design: (1) coerce_tree() corre sobre a ÁRVORE INTEIRA no save, não só o elemento tocado — porque o Elementor valida a página inteira de uma vez; (2) apply_prop_aliases() corre ANTES da validação, nunca depois — porque o parser de props do Elementor descarta silenciosamente chaves não reconhecidas em vez de as rejeitar; (3) is_atomic_supported()/is_container_supported() NÃO se baseiam em version_compare() mas em introspecção de tipos de elemento realmente registados — o Elementor já enviou atomic como experiment opt-in em versões que ainda reportam ELEMENTOR_VERSION 3.x.
  • EMCP_Tools_Atomic_Widget_Map — pequeno (≈200 linhas) mas denso em armadilhas específicas do Elementor 4.0+ (chave paragraph vs text; source string vs shape; alt só funciona por url, para attachment vai para post meta). Copiar incluindo os comentários com o número de issue — são a única documentação que existe destas armadilhas.
  • EMCP_Tools_Block_Tree — ~330 linhas, código puro sem qualquer dependência Elementor. A lógica de inner_content_for() e os três guards de segurança de move() (auto-referência, alvo-dentro-da-subárvore, correcção de índice) representam bugs subtis já resolvidos; portar tal-e-qual evita reintroduzir os mesmos erros ao reescrever de raiz.

Vale a pena simplificar:

  • EMCP_Tools_Atomic_Styles — a estrutura de "classe de estilo local" (variants por breakpoint+state, ID mintado com o element_id embutido) é ditada directamente pelo formato nativo styles do Elementor 4.0+, por isso tem de ser replicada fielmente SE a réplica quiser gerar CSS local por elemento — mas se só forem necessários estilos simples sem responsividade/estados, pode simplificar-se para um único variant fixo (desktop/null) e cortar a generalização de breakpoint/state.
  • As Gutenberg abilities (a classe de abilities em si, não Block_Tree) — muito mais simples de reescrever de raiz do que as atomic, porque não há $$type nem dependência Elementor nenhuma; a única peça deste grupo que vale a pena copiar exactamente é EMCP_Tools_Block_Tree, pelas razões acima.

Vale a pena deixar de fora:

  • EMCP_Tools_Widget_Loader — 100% Pro-gated e específico ao mecanismo Sandbox de widgets gerados por IA (doc 06); sem relação directa com atomic/Gutenberg. Só relevante se a réplica também for construir um "widget builder" próprio a partir de código PHP gerado.
  • Os tiers Pro/Woo do widget catalog (catalog-pro.php 1049 linhas, catalog-woo.php 92 linhas) — dados estáticos de descrição de widgets de terceiros que só fazem sentido se a réplica também for suportar Elementor Pro/WooCommerce como dependência opcional; a ESTRUTURA de dados (idêntica à de catalog-free.php) é trivial de reproduzir, o valor real está no CONTEÚDO (descrições/enums correctos por widget), que teria de ser levantado widget a widget contra a documentação oficial Elementor Pro/WooCommerce — não vale a pena tentar adivinhar a partir dos nomes de chave.

Riscos/gotchas não óbvios encontrados no código (citações directas, valem ouro):

  1. "insert_element() mutates $page_data by reference and returns a bool; save the modified $page_data, never the bool (issue #36)." — padrão repetido em quase todos os execute_* callbacks das duas classes atomic; um erro comum e fácil seria gravar o valor de retorno booleano em vez da estrutura mutada.
  2. "Elementor's Props_Parser SILENTLY DISCARDS keys it does not recognise and still reports the result as valid" (issue #102) — motivo estrutural pelo qual apply_prop_aliases() tem de correr ANTES da validação, nunca depois.
  3. "The e-paragraph content prop is named paragraph (Html_V3), not text. Writing text silently dropped the content (issue #56)."
  4. "There is no background-color style prop, so writing one is silently discarded" / "Elementor has no per-side padding-block-start style prop, so writing those individually is silently discarded on save" — classe inteira de bugs "escreveu mas não aconteceu nada, sem erro" nas style props do v4; qualquer réplica precisa de mapear explicitamente cada shorthand em vez de assumir que props CSS planas funcionam.
  5. "The e-svg widget's svg prop is a distinct svg-src type — NOT the image/image-src type used by e-image" e "e-youtube's video prop is source, a plain string (union), NOT the video-src shape the self-hosted video widget uses" — dois pares de widgets com nomes de prop idênticos (source, formatos tipo "src") mas shapes incompatíveis; confundir os dois quebra o widget silenciosamente ou faz o Elementor rejeitar o elemento inteiro.
  6. Detecção de suporte atomic/container não é por número de versão — ver §4.1, último parágrafo. Uma implicação prática: nem sequer basta verificar a feature flag do editor (e_opt_in_v4_page), porque é uma experiment SEPARADA de e_atomic_elements — um site pode ter a primeira ligada e a segunda desligada, escrever "com sucesso", e o Document::save() sanitizar/remover os elementos atómicos silenciosamente.
  7. Gotcha nosso, não assinalado como tal no código: a doc-comment // Detect version (always registers, even on < 4.0) em class-atomic-layout-abilities.php está desactualizada/incorrecta face ao guard clause real de register() — ver §2 para o detalhe. Numa réplica, registar detect-elementor-version INCONDICIONALMENTE (fora de qualquer guard de suporte atomic) resolve a inconsistência e torna a tool útil precisamente no cenário em que mais falta faz.
  8. "wp_update_post() runs wp_unslash() on the data, and block serialization emits backslash escapes (&, ", \ …) in attributes — so the markup MUST be slashed here or those escapes get stripped and the block corrupts." — em save_tree() das Gutenberg abilities; um erro fácil de introduzir ao reescrever a persistência de blocos sem este detalhe.
  9. Issue #97 (remap_local_classes): duplicar um elemento atomic sem re-mintar as suas classes de estilo locais faz com que o duplicado e o original PARTILHEM a mesma classe, causando "style bleed" entre os dois e entradas duplicadas no popover "Style Origin" do editor Elementor — um bug de UX subtil que só aparece depois de duplicar e depois estilizar um dos dois separadamente.

Fonte

Leitura directa (19-08-2026) de: includes/abilities/class-atomic-widget-abilities.php, includes/abilities/class-atomic-layout-abilities.php, includes/abilities/class-gutenberg-abilities.php, includes/class-atomic-props.php (973 linhas, completo), includes/class-atomic-widget-map.php (completo), includes/class-atomic-styles.php (completo), includes/class-widget-loader.php (completo), includes/widgets/class-widget-catalog.php (completo), includes/widgets/catalog-free.php (680 linhas, completo), includes/class-block-tree.php (completo); existência + estrutura de chaves top-level (sem leitura de params internos) de includes/widgets/catalog-pro.php (1049 linhas, 30 widgets) e includes/widgets/catalog-woo.php (92 linhas, 5 widgets). Cruzado com includes/abilities/class-ability-registrar.php (já lido em sessão anterior, ver docs/00-ARQUITECTURA.md) para as condições exactas de gating de cada classe.