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).
516 lines
43 KiB
Markdown
516 lines
43 KiB
Markdown
# 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 é:
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
'<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 `null`s) →
|
|
mantém o chunk antes do primeiro `null` e depois do último, reemite um `null` por filho
|
|
novo; (b) container estava vazio (sem `null`s) → "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.
|