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).
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:
apply_prop_aliases()— renomeia chaves alias (text/content/heading→ o nome canónicotitle) usando a MESMA metadata que o Elementor expõe ($prop->get_meta_item('aliases')), antes de qualquer validação. Crítico: oProps_Parserdo 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.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ópriosget_prop_types()/get_key()/get_shape()do prop Elementor (nunca hardcoded) e testa cada um contravalidate(), usando o primeiro aceite. Cobre tanto valores planos (string→envelope certo) como shapes compostos (ex.linklegado{url,is_external}→{destination,isTargetBlank}).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)— gerae-<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 umidnovo, as suas classes locais v4 (e-<id-antigo>-<hash>) continuam a embutir oidde ORIGEM e ficam partilhadas com a fonte — uma escrita posterior no mapastylessangra entre os dois, e o popover "Style Origin" do editor mostra entradas duplicadas. Este método re-minta as chaves do mapastyles(e oidde cadastyle_def) contra oidACTUAL do elemento, e repõesettings.classes.valuedas 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$$typeem kebab-case (flex-direction,justify-content, …).build_common_props($params)—width/min_height/border_radius(size simples);padding/marginviabuild_dimensions()(shorthand de 4 lados — um valor único aplica aos 4 lados,*_top/_right/_bottom/_leftdefinem por lado individualmente, o shorthand ganha se ambos presentes; não existe proppadding-block-startindividual, construir por lado sem este wrapper é descartado no save);background_color(viaAtomic_Props::background_color(), nunca uma propbackground-color);color(viaAtomic_Props::color(), nuncastring).apply_to_element(&$element,$class_id,$style_def)— push de$class_idemelement.settings.classes.value[]e de$style_defemelement.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_oncedisparar um fatal de compilação/parse (que umtry/catchnão apanha, porque um parse error num ficheiro incluído aborta o request), umregister_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()exigeemcp_tools_fs()->can_use_premium_code(); num build Free/sem licença (como este), tantoregister_widgets()comoregister_assets()saem imediatamente — é um NO-OP total neste site. - Regista handles de CSS/JS (
wp_register_style/wp_register_script) só como metadata emwp_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 (blockNamenull+ HTML em branco).at($blocks,$path)— resolve um path para um nó ounull.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) eedit_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:- Mover relativo a si próprio (
modebefore/after com$from === $to) é no-op. - 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.
- Correcção de deslocamento de índice:
remove()desloca cada irmão posterior sob o pai de$fromuma 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 deinsert(), aplicando-se a QUALQUER modo (before/after entre irmãos OU inside um container posterior) a qualquer profundidade.
- Mover relativo a si próprio (
summarize($blocks,$depth,$prefix)— vista compacta com path por get-post-blocks:{path, blockName, attributes, innerBlocksCount}, cominnerBlocksaninhado só até$depth(se dado).inner_content_for()— o internals mais delicado do ficheiro. Reconstrói o arrayinnerContentde um bloco container quando o número deinnerBlocksmuda, PRESERVANDO o HTML de wrapper do container.innerContentintercala chunks de string literal (o HTML do wrapper) com placeholdersnull(um por bloco filho, consumidos por ordem porserialize_block()). Dois caminhos: (a) o container já tinha filhos (havianulls) → mantém o chunk antes do primeironulle depois do último, reemite umnullpor filho novo; (b) container estava vazio (semnulls) → "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_Propsinteiro. É 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 emversion_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 reportamELEMENTOR_VERSION3.x.EMCP_Tools_Atomic_Widget_Map— pequeno (≈200 linhas) mas denso em armadilhas específicas do Elementor 4.0+ (chaveparagraphvstext;sourcestring vs shape; alt só funciona porurl, 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 deinner_content_for()e os três guards de segurança demove()(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 oelement_idembutido) é ditada directamente pelo formato nativostylesdo 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á$$typenem 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.php1049 linhas,catalog-woo.php92 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 à decatalog-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):
- "
insert_element()mutates$page_databy reference and returns a bool; save the modified$page_data, never the bool (issue #36)." — padrão repetido em quase todos osexecute_*callbacks das duas classes atomic; um erro comum e fácil seria gravar o valor de retorno booleano em vez da estrutura mutada. - "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. - "The
e-paragraphcontent prop is namedparagraph(Html_V3), nottext. Writingtextsilently dropped the content (issue #56)." - "There is no
background-colorstyle prop, so writing one is silently discarded" / "Elementor has no per-sidepadding-block-startstyle 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. - "The
e-svgwidget'ssvgprop is a distinctsvg-srctype — NOT theimage/image-srctype used bye-image" e "e-youtube's video prop issource, 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. - 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 dee_atomic_elements— um site pode ter a primeira ligada e a segunda desligada, escrever "com sucesso", e oDocument::save()sanitizar/remover os elementos atómicos silenciosamente. - Gotcha nosso, não assinalado como tal no código: a doc-comment
// Detect version (always registers, even on < 4.0)emclass-atomic-layout-abilities.phpestá desactualizada/incorrecta face ao guard clause real deregister()— ver §2 para o detalhe. Numa réplica, registardetect-elementor-versionINCONDICIONALMENTE (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. - "
wp_update_post()runswp_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." — emsave_tree()das Gutenberg abilities; um erro fácil de introduzir ao reescrever a persistência de blocos sem este detalhe. - 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.