Repository navigation
[Refactor] Improve the codebase - #10
Open
ashraful1971 wants to merge 38 commits into
Open
ashraful1971 wants to merge 38 commits into
ashraful1971 wants to merge 38 commits into
Conversation
Land the harden-first-release-architecture OpenSpec change: secure public loop pagination, non-destructive theme compatibility, early foreign-document preparation, shared library persistence primitives, feature availability, explicit service composition, the shared Loop layer, the Elementor compatibility boundary, the forms pipeline, schema and cache lifecycle, distribution and uninstall hardening, and the real WordPress/Elementor integration and browser suites. Verification gate on this exact tree (2026-09-19): - Fast harness: 270/270 - PHPCS (WPCS + PHPCompatibility): clean - Architecture dependency boundaries: clean - Static popup runner: 52/52 - Real WordPress + Elementor 4.2.3 integration suite (disposable site, --with-pro, free ACF): 203 tests, 1094 assertions, 6 skipped (ACF Pro repeater group), 0 failures - Real-browser suites: builders 4/4, loop pagination 7/7, popup assets 13/13, form submissions 13/13 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reorganise the free plugin into the layered layout documented in docs/ARCHITECTURE.md and finish the WordPress.org release gate. - Split the old Core layer into Core (lifecycle only), Http, Elementor, Loop, Conditions, TemplateLibrary, Integrations/Acf and Support. Shared layers never import modules and modules never import each other; Forms lives under Modules/Widgets and Modules/Mock is gone. - Module is a service provider: register() binds into the container, boot() wires hooks. Registries are bound by their owners and container-built services receive their collaborators by constructor. - Fix "Current Query" loops on category, tag, author, date and search archives, and show editors an inline notice explaining why a loop renders nothing for visitors. - Replace the dependency-boundaries script with tests/architecture/check.php (layering, Elementor boundary, no planning references in code) and add integration tests for the archive fix and the notice. - Add the release gate: verify-package.py checks version consistency, the readme, listing assets and the third-party inventory; CI runs Plugin Check on the built archive; docs/RELEASE.md records the accepted warnings and the post-review steps. - Add docs/ARCHITECTURE.md, docs/THIRD-PARTY.md, contributor recipes, generated listing images under .wordpress-org/ and the 1.0.0 readme changelog; trim docblocks to intent and drop planning references. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Module::is_default_active() replaces the hardcoded default-module list, so renaming a module can no longer silently break seeding. Core\ModuleCatalog becomes the single enumeration path shared by seeding and module loading, reading src/Config/modules.php with a plain require: seeding runs before modules load, and require_once would hand the second reader true instead of the array. It also settles each entry on the class name before constructing anything, so a configured non-module no longer has to be instantiated to be rejected. Elemacy::boot_providers() tracks booted providers by identity, making at-most-once boot the composition root's guarantee rather than each provider's. ServiceProviderInterface no longer asks implementations to defend themselves, and both CoreServiceProviders drop their $booted flag. Two instances of one provider class remain two contributions and each boots. RegistryIdentifierGuard now accepts a real implementation superseding a mock stand-in and refuses the reverse, which previously passed silently and could only downgrade a live feature. Multisite\SiteBatchIterator deliberately keeps its array return: each plugin's uninstall.php loads that class by path with no bootstrap and no Composer autoloader, so it must stay dependency-free.
Elementor Canvas no longer renders the Theme Builder header or footer, as with
Elementor Pro. The generic adapters printed the header on wp_body_open, which
Canvas fires, so a popup preview shared its body with the site header: the
preview CSS centred the two as a pair, and the header's widgets firing ready
events made an empty popup sprout a close button. Canvas is detected with
Elementor's own elementor-template-canvas body class, so every route to it is
covered. A header or footer template is also no longer resolved as its own
header or footer -- scoped to a request for a library post, because
get_queried_object_id() is a term id on a taxonomy archive and a user id on an
author archive, so an unscoped comparison would silently drop the header from
any archive whose term happened to carry a template's number.
Library listings build their DTOs from $query->posts instead of the_post().
wp_reset_postdata() only restores the main query's current post, and an
Elementor render_widget request for an archive preview has none, so the last
template listed stayed the global post and a Loop widget rendering its item
template tripped Elementor's recursion guard ("The Template ID cannot be the
same as the currently edited template").
A foreign template's atomic stylesheet now survives the editor canvas. Once the
edited document has atomic styles of its own, the editor removes every
local-<id>-...-css link in the preview frame; that pattern carries no document
id, so a header or footer built from atomic elements lost its CSS the moment an
atomic element was added to the page. Registered templates' link ids get an
elemacy- prefix, in the editor preview only.
The popup close button can be placed outside the popup. The box is no longer
the scroll container -- content scrolls in .elemacy-popup__scroll -- and the
Close Button controls share one selector carrying the element type, so they
outrank the popups.css fallback that loads later on the frontend and silently
won the tie for every close-button style setting.
Behavior changes: an ordinary page set to Elementor Canvas no longer shows the
site header and footer, which matches what Canvas means and what Elementor Pro
does; and an existing popup needs its Elementor CSS regenerated (save it, or
Elementor > Tools > Regenerate CSS) before the new close-button selector
reaches the live site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Template precedence: display conditions declare a specificity level and the most specific matching template wins; date only breaks ties. Header and Footer templates edit on their own chrome documents, and a first schema step reconciles stored document types against the registry. Schema syncs run on init, after every registry is populated. The generic theme adapter discards the theme's header.php/footer.php output instead of hiding it with CSS, with a per-side opt-out filter. Request-scoped render caches register with Core\RequestState, flushed on `wp` and keyed by blog id. Release readiness: composer.json ships beside vendor/, the 1.0.0 changelog is a plain feature list, persisted identifiers are a checked contract, and verify-package.py rejects stale or dirty archives. Coding standards: the PHPCS ruleset no longer silences whole standards; WordPress standards plus PSR-12 layout, zero violations. Tests: precedence, chrome documents and replacement (fixture and real classic themes, fresh-process renders), atomic rendering, request state, Pro entitlement and module seeding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Elemacy now only connects the plugin's parts. Site setup (activation, new-site and bounded network seeding, the per-request seed safety net, version sync) moves to Core\SiteInstaller, built by container auto-wiring so it works before any provider registers. The admin menu tree and REST/AJAX wiring move to CoreAdminMenu and HttpBootstrap, and the built-in library types to LibraryBootstrap, all booted by CoreServiceProvider. ModuleCatalog is container-shared between seeding and module loading. CoreServiceProvider builds every bootstrap through the container. To make that safe, PreviewManager and TemplateAssetsRegistrar now take required dependencies instead of `?Dep = null` defaults, and the provider binds PreviewContextProviderInterface to the Loop implementation, so PreviewManager no longer imports a Loop class. Provider boot drops SplObjectStorage tracking: init() runs once per request, which is what boots each provider at most once. Tests move from Elemacy reflection to SiteInstaller's public API; ARCHITECTURE.md records the container-for-services convention.
Refactor/slim composition root
Elemacy::init() now makes one call to Core\ModuleLoader, which registers the catalog's modules, fires REGISTER_MODULES_ACTION and runs init_modules() in the same order as before. ModuleManager is shared through the container and never bound, so get_module_manager(), the loader and REGISTER_MODULES_ACTION listeners all see one instance; calling get_module_manager() before init() now returns an empty manager instead of failing on an uninitialized property. Removes Elemacy::$module_manager, init_module_manager(), load_modules() and init_modules(). No hook, option, route or menu-order change.
Dynamic Tags now lists its tags in src/Modules/DynamicTags/Config/tags.php, keyed by group, like the Popups, ThemeBuilder and Widgets modules already do. A group can name a `requires` class; the WooCommerce group and its tags register only while WooCommerce is active, as before. TagManager drops its 51 imports and static singleton for a plain instance with an idempotent register_hooks(), and fires the new Hooks::DYNAMIC_TAGS_REGISTER_ACTION (elemacy/action/dynamic_tags/register) after the built-in tags so add-ons can add or replace tags by name. Support\ConfigList::instances() replaces the require/instantiate/type-check loop that ConditionsBootstrap and TriggerRuleBootstrap each repeated; build() does the same for a list already read. The registered tag and group list is unchanged, in the same order, with and without WooCommerce. New integration tests build Elementor's real tag list in a fresh process for each scenario.
…tion Register dynamic tags from config and share the config-list loop
Settle four stored shapes and names that would need a migration over
users' data once 1.0 ships:
- Rename the built-in post-url and post-excerpt dynamic tags to
elemacy-post-url and elemacy-post-excerpt. Elementor Pro registers tags
under the bare names, and Elementor's tag registry is keyed by name, so
one plugin's tag replaced the other's. The architecture check now
rejects a built-in tag without the elemacy- prefix, and lists every tag
name, widget name and loop data-source key as a persisted identifier.
- Replace the nullsafe operator in DocumentWriter::document_type(), which
does not parse on the declared PHP 7.4. The architecture check now scans
shipped PHP for PHP 8-only tokens; PHPCompatibility 9.x predates PHP 8
and cannot see them.
- Store elemacy_active_modules as slug => bool explicit choices. A module
with no recorded choice follows its own is_default_active(), so a
default-active module added later reaches existing sites; seeding
records no choices. A stored list of slugs still reads as exactly that
active set and becomes a map on the next toggle. SiteInstaller no
longer needs ModuleCatalog, and default_active_slugs() is removed.
- Store elemacy_library_index as {shape, index}. Any other shape, older,
newer or missing, is a miss and is rebuilt rather than read.
Also make the Post Excerpt tag render the whole excerpt when Excerpt
Length is blank, its default; it rendered nothing unless a length was set.
STORAGE.md and CONTRACTS.md now match what the code stores, including
the dynamic tag name table and the rule that released control and module
defaults are frozen. RELEASE.md requires green PHP 7.4 CI in both repos.
No migration step: no release holds the old shapes.
Elemacy Pro now registers every capability whenever it is active; its license covers updates and support only. Free carries no license logic. - Remove PageCachePurger and the elemacy/action/cache/purge hook: Pro's license transitions were their only caller. - Update the architecture, contracts, storage and release docs. - Rewrite the pro-entitlement integration test for an unlicensed Pro that still supplies its real conditions, rules, triggers and loop source. - Expect the new dashboard copy in the admin contract e2e test.
Route access now comes solely from attached middleware; a route without
middleware is public. The separate capability policy duplicated
AdminMiddleware on every protected route.
- Remove Route::require_capability(), public_access(), the group
`access` option and the "no policy" registration guard.
- Drop require_capability('manage_options') from every route; each keeps
AdminMiddleware, so responses are unchanged.
- Drop the redundant is_user_logged_in() check from AdminMiddleware.
- Rewrite the transport-policy tests around middleware and cover a
logged-in non-admin getting 403.
- Update ARCHITECTURE.md and CONTRIBUTING.md.
The plugin has not been released, so no site holds a wrongly typed library item that a fresh 1.0 install could need repaired, and a fresh install seeds the schema marker at latest and skips migrations anyway. Migrator::migrations() now ships empty; the init-priority scheduling, schema marker and lease stay for the first real migration. Removes the cursor option, its tests and its STORAGE.md entries.
Nothing has shipped, so there is no stored data or caller to stay compatible with. Drop SiteResponse::success()/error() (the form controller now calls json()), the analytics duplicate-counter test and fixture operation that exercised Pro's removed migration, and a stale class_alias comment. CONTRACTS.md now says the rename-needs-a-migration rule starts with the 1.0 release.
Pro enqueues its stylesheet with elemacy-admin-app as a dependency, but in dev mode Vite injects the app CSS through JS and the handle was never registered. WordPress silently skips a style with an unregistered dependency, so every pro:* utility was missing until a production build.
The Hello adapter marked the header side from get_header and only marked the footer side when a Footer template existed. With a Header-only setup the shared hello_elementor_header_footer filter therefore still answered for the header when Hello checked its footer, and the native footer disappeared. The window was also open during wp_head, when Elementor builds the kit. Hello registers its footer settings only if the chrome is shown at that moment, so the footer that did render was blank. Mark the header only for Hello's own check (the last wp_body_open priority) and the footer at get_footer, whether or not that side has a template. Cover the moments Hello asks in the fast harness, and Header-only, Footer-only and both-sides cases against real Hello in the integration suite.
The generic adapter discarded a theme's header.php or footer.php whole, so a theme that opens its layout wrappers in one and closes them in the other was left unbalanced when only one side was replaced. It now reads what it discarded (LayoutWrapperReader) and prints the unbalanced wrapper tags around the Elemacy template, falling back to the old output when the markup cannot be read with certainty. Every adapter now asks one question, ThemeBuilderManager::replaces_native_ chrome(), so the opt-out filter reaches all of them. Neve, Genesis and Storefront remove only the callbacks the theme defined (ThemeHookCleaner) instead of emptying the hook. Block-theme parts are matched by area, with the header/footer slugs as the fallback. Add a real-theme suite (Hello, GeneratePress, Kadence, Neve, OceanWP, Blocksy, Storefront, Twenty Twenty-One, Twenty Twenty, Hestia, Sydney, Zakra and Twenty Twenty-Five), run on the dedicated and the generic path, and install the pinned versions in CI. Rename DefaultCompatibility to HelloElementorCompatibility and GlobalCompatibility to ClassicThemeCompatibility.
A Single, Archive, Search or 404 template set to Elementor Canvas still got the site header and footer, because every matched location template went through one wrapper view that never read its layout. Serve a Canvas location template through Elementor's own Canvas file with the page-templates module's print callback set to render the location, which is how Elementor Pro's Theme Builder does it. Elementor's file tags the body as a Canvas page, so the existing header and footer guard needs no change. As in Pro, a singular post that has its own non-default layout (Canvas, Full Width or a theme page template) is served by that layout and the Single template is not applied to it. A Full Width location template keeps the wrapper, which also serves block themes and themes with no header.php, and gains Elementor's full-width body class. Cover it with a layout integration suite, fast tests for the layout checks and the missing-module fallback, and a browser journey that authors the Canvas layout through the editor.
Search templates now offer Display Conditions: a working Search Results condition in Free and a Search Results: Post Type placeholder that Pro replaces with the real condition. A rule with a post type outranks the global one. Saving a template refuses a newly added rule with no condition type, and the template form drops rules a newly chosen type does not offer.
Playwright journeys that click through the real Elemacy admin and the Elementor editor and then check what a visitor sees: module toggles, the template library, a popup, and Header and Footer authoring. They run against a disposable site that the harness provisions and cleans up, with an owned journey author, redacted capture artifacts, and an optional registration script for a managed release site. Free-standing: it only adds files under tests/ and does not change the plugin's runtime code.
tests/coverage lists every shipped feature (conditions, tags, widgets, templates, loop sources, popup parts, form parts) with how it is tested and whether a native browser journey covers it yet. check.py fails when a shipped feature is missing from the inventory, a declaration changed, or a test reference no longer resolves, and generates a readable roadmap. It records gaps; passing it is not a release pass, and --require-journeys fails until every journey is implemented. The pull request template and CONTRIBUTING ask each change to state its feature and journey impact.
The Archive Title tag said "Archives" on every Search Results page because WordPress's archive title has no search case. It now shows "Search Results for: term" there and is unchanged everywhere else. An Archive template could only preview a post type, so its title never matched a real archive. It now previews a taxonomy term (category, tag or any public taxonomy), an author or a month, and falls back to the post type archive when a saved choice no longer exists. The preview also forces the global post as a real archive request does; without it a month archive's title is empty in the editor. The term and author choices use live-search pickers built on a small resolver pattern behind QueryService: one class per kind (post, term, user), a registry that loads on first use, a config list and a register action for add-ons. The post picker moved onto it with unchanged results, and the Taxonomy and Users loop previews dropped their 200-entry dropdowns for the same pickers (stored ids are unchanged). Tests: unit tests for the tag and the routing, integration tests on real posts, terms, users and requests, and four browser journeys (free and Pro). The shared browser helper now closes Elementor's late Angie modal and redoes a heading step when the editor preview stalls; the header and footer journeys also accept the fixture theme's native markup. Coverage inventory, docs and release notes updated.
tests/integration/__pycache__/setup.cpython-314.pyc was committed by mistake. __pycache__ is already in .gitignore.
Current Query was a fake entry in the Posts source's post-type select and only worked, rebuilt from a restricted set of signed facts, inside Theme Builder archive and search templates. It is now a built-in source of its own that lists the request's main query as WordPress ran it, on any page. Its later pages are the page's own paginated URLs: Loop Grid fetches the clicked link, swaps the loop in place, keeps the address bar and history in step and falls back to a normal page load, so no query is rebuilt and the signed loop endpoint refuses the source. Posts loses every current_query branch and offers real post types only. The sanitiser, the Posts policy branch, the archive-only notice and the Theme Builder query facts that existed only for the old design are removed. Adds an integration suite, two real-browser runners (visitor pagination and editor authoring), the coverage inventory entry and the docs.
Loop Grid paginated only with numbers and Previous/Next links. It now offers Load More and Infinite Scroll, which append the next page's items in place for every data source. One loader (loop-incremental.js) serves both triggers and two transports: the signed-definition endpoint, whose response now says what page comes next, and, for Current Query, the next page's own URL. Items already on screen are skipped by a new per-item identity attribute, only appended items are hydrated, and a failed load falls back to a normal page load (Infinite Scroll stops auto-loading and shows the button first). The control is a real rel=next link styled like the existing page links, so it works without scripting and takes the existing Pagination style controls. The classic render now honours the requested page for a paginated loop. It always rendered page one for Posts, Terms, Users and ACF Repeater, which made every paginated URL of a loop show the first page again and would have broken the no-script link and the fallbacks. Adds an integration suite, two real-browser runners (visitor behaviour on both transports; editor authoring), the coverage inventory entries and the docs.
…cepts Elementor 4.3.5 changed the atomic heading title and button text props from an html-v3 object to a plain escaped-html string and rejects the old shape when a document is saved, so the two atomic rendering tests failed while creating their fixtures. The helper now reads the accepted prop types from the installed widget and builds the matching value, falling back to html-v3 for older Elementor.
pro-entitlement.php reads License::client()->is_active() instead of the removed LicenseManager. The admin-contract runner seeds an active license record under elemacy_pro_license and checks the license screen: the Active badge, the masked key, Check now and Deactivate license, and that the raw key never reaches the page.
An empty params array encoded as [] and failed the admin app's object-shaped schema, so Create & Edit never redirected to Elementor and the edit modal never finished loading for popups using Exit Intent, Logged-in Users or On Popup Close.
Replace the old Nav Menu with a rebuild that matches the Elementor Pro 4.2.2 Nav Menu: the same control IDs, defaults and panel layout, the same look, and the same submenu, toggle and responsive behaviour, on Elementor Free. - PHP split into focused parts: four control sections, a renderer, menu filters that only touch the widget's own menus, and breakpoint rules generated from the site's Elementor breakpoints. - Pro's stylesheet ported value for value under Elemacy's own classes. - SmartMenus behaviour reimplemented without jQuery plugins, including right-to-left placement. - A theme menu rendered after the widget keeps its item IDs. - Tests: control parity with Pro, real-WordPress rendering, 21 browser scenarios, a Pro side-by-side parity runner and an authoring journey.
Fixes found by a four-reviewer pre-release review of the Theme Builder. - Theme types use explicit conditions: no inclusion rule means displayed nowhere, 404 is location-wide, and new templates start as Draft with their type's broadest condition pre-filled. Popups keep "no conditions = everywhere". - Elemacy yields a location to Elementor Pro when Pro serves it. - Archive and search conditions, library visibility, location documents, template authoring and display, and frontend asset loading are covered by the matching specs. - Uninstall is opt-in via ELEMACY_REMOVE_ALL_DATA. - Tests: every fix has a test that fails without it; the integration suite passes in the free and Elementor Pro profiles, plus 15 browser journeys and single-site and network uninstall checks.
Visitor-facing fixes - Pro triggers and rules work under deferred, delayed and reordered scripts: an extension queue plus a per-popup readiness gate that fails closed. - Every Animation In/Out choice animates in both directions. - A popup opens automatically at most once per page view; trigger listeners are released; "After another popup closes" has a source and cannot loop. - Click triggers no longer follow dead links, including a link inside a matching Elementor widget wrapper. - Closing stops media; focus stays in the newest modal; scroll lock compensates for the scrollbar; ids are normalised; one broken popup cannot disable others. - A document that throws while rendering is logged and dropped (popups and Theme Builder alike); empty, password-protected and self-previewed popups are not delivered; top bars render without wp_body_open and show at first paint. - Saving rejects unregistered trigger/rule types and keeps percent-encoding. Contracts and tests - Public markup, events, JS API and extension queue are documented as permanent; the unreleased .elemacy-open-popup markup is removed. - Control defaults are frozen by a snapshot; trigger/rule ids, parameter keys, setting names and option values join the persisted-identifier check. - New route-authorization sweep, server-rendering and Pro delivery integration tests, static-browser cases, and the popup journey family (types, triggers, rules, interaction, analytics).
Theme Builder
- Header and footer templates no longer render on the wp action. Replacing
the theme's chrome is decided from the resolved template and the opt-out
filter alone; templates render at asset collection, after Elementor's
frontend and Kit styles, so nothing prints before <!DOCTYPE html> (Internal
Embedding, redirects, ?wc-ajax=) and template CSS loads after Elementor's.
- A header whose widget throws now leaves its area empty instead of falling
back to the theme header, as Single and Archive templates already did.
- Output Elementor prints while a foreign document is prepared stays in its
fragment.
- Block themes keep their own header and footer template parts around a
location template where no Elemacy template applies, rendered before the
head so their block-support styles load, inside .wp-site-blocks.
Popups
- "Close on Esc" is honoured for modals (default on); non-modal types ignore
Escape. Layers opened above a modal (Elementor's lightbox) appear above it,
keep focus and handle their own Escape.
- New popups start with a Page Load trigger; a popup with no triggers opens
only from links and the JavaScript API, and a trigger-less top bar is not
prerendered.
- Browser storage keys are scoped to the site (elemacy_{blog}_...).
Final public surface
- Every REST route lives under elemacy/v1.
- Author hooks follow elemacy-popup-{action}; the API is window.elemacyPopups,
the config elemacyPopupsConfig, events elemacy-popup:opened/closed/destroyed.
Retired names are removed with no alias.
- One condition id per display check: the Free placeholders for
general/search_results and archive/all are removed, search/all is offered on
Header and Footer templates, and Specific Post requires a post.
Tests
- The test asserting the theme-header fallback for a throwing header is
replaced: a published header is used as published, matching the rule for
intentionally empty templates.
- New architecture rules for retired public names and unversioned REST routes;
integration, static-browser and journey cases for every fix above.
Settles the form contract before sites save forms and add-ons build on it, so later features ship as additions, never as migrations. - A builder-neutral forms engine in src/Forms (Elemacy\Forms): field type, action and form source registries with registration hooks, public contracts, the submission pipeline and the renderer. The Form widget stays in the Widgets module as the Elementor UI and registers the `elementor` form source. - Stable field IDs (custom_id when valid and unique, else the row _id): values, errors and [field id="..."] email placeholders no longer move when fields are reordered. - New field types: URL, Number, Date, Time, Acceptance, Hidden and HTML. - Cache-safe submission: no public nonce and no 24-hour placement token, so cached pages keep working. A logged-in identity needs a valid nonce, otherwise the submission is anonymous. The endpoint is now elemacy_form_submit, with renamed request fields. - Actions return their own result: a failing action shows its message and stops the rest. New validation, action_run, submitted, rate-limit and client-IP hooks, plus elemacy-form:submit/success/error events. - Fixes: a required checkbox group needs one box, DOM IDs are unique per form instance, hidden labels stay accessible, the select placeholder works, and runtime strings are translatable. - Vanilla delegated runtime (no jQuery) handles forms in popups and late-loaded content, with elemacy-form-* classes. The widget keeps rendering on every request, so repeated forms (loop items) keep their own IDs. - Contract records: docs and the persisted-identifier check (type and action keys, control IDs, a defaults snapshot, retired names). - Tests: the integration suite passes in the free-only, free-pro and Elementor Pro profiles; form, popup-asset and journey browser runs, a real editor check and an Elementor Pro Form side-by-side check all pass.
PlacementResolversTest builds its fixtures through ArchitectureCatalog, which needs ACF Pro, so the test never ran in the free and free-pro profiles. With ACF Pro present it failed. Its header, single and footer had no display conditions, and since the Theme Builder change a template without an inclusion rule is shown nowhere, so none of them resolved. The test now saves each type's default conditions, as a new template gets in the admin. The shared catalog is unchanged, because the other tests that use it would render a different page with a site-wide header. The full integration suite passes with Elemacy Pro, Elementor Pro and ACF Pro: 596 tests, 0 failures, 0 errors.
Visitors now see the same loop the editor shows in every placement. - Only a plain page used to give the public policy a "current post", and nothing gave a "current term". An ACF Repeater set to Current Post was empty in Single templates, popups and nested loop items, and "Exclude current post/term" was ignored there and on term archives. - Loop\Placement\HostFacts decides both, the way the editor does: the post of the nearest enclosing post item (NativePostItemInterface), else the viewed singular post, and the viewed term on a term archive. Each is supplied only while publicly viewable. PlacementResolverRegistry merges them into every placement and owns those two names. - PaginationContextSigner re-checks both facts on later pages. The data-source contract is frozen for 1.0. - LoopDataSourceInterface::sanitize_ajax_settings() is removed: nothing called it. Public pages use PublicLoopSourcePolicyInterface. - The never-called PublicContextPlacementResolverInterface and resolve_public_context() are removed. - CONTRACTS.md states the rules: frozen methods, optional interfaces for anything new, a public policy before visitors see a source, and add-on control IDs prefixed with their source key. Saved loop settings are pinned. - loop-control-defaults.php and its fixture pin every loop control's ID, type and default (Free and Pro), fail on a control Elementor drops as a duplicate, and prove every source resolves the same public settings with or without its defaults. Loop Grid and Loop Carousel share their duplicated code through an abstract LoopWidget, and the empty-state text now says "items". New tests: loop-host-context.php (fails before this change), plus cases in placement-resolvers.php, pagination-context-signer.php, acf-repeater-query-pagination.php and public-loop-pagination.php.
Each built-in source's setting names and defaults now live in one settings
class (PostsSettings, TermsSettings, UsersSettings). Its Elementor control,
its query and its public policy all read the same constants, so a key is
never retyped as a string and the editor, page 1 and page 2 cannot
disagree on a default. Saved setting names and default values are
unchanged; loop-control-defaults.php still pins them.
The loop layer is grouped by feature, so a new contributor finds a whole
source in one folder:
- Loop\Sources\{Posts,Terms,Users,CurrentQuery}\ hold each source's query,
public policy and settings (formerly split across Sources\, Constants\
and Pagination\Policies\).
- Loop\Access\ holds what decides what a visitor may see, on page 1 and on
signed later pages: PublicRenderPolicy, PublicLoopLimits, the signer,
the saved-definition resolver and its request/definition objects
(formerly in Pagination\ and Pagination\DTO\).
- Loop\Pagination\ keeps only the pagination UI.
- WordPressPostScope moves into Items\, beside the item that uses it.
The Elementor half of each source stays in Modules\Widgets\DataSources\,
because Loop never touches Elementor. Namespaces, imports, tests, the
coverage catalog and the docs are updated; redundant same-namespace
imports are removed.
A loop inside another loop's item now works at any depth, on later pages and in the editor, for every data source. - Every rendered item gets an address: its parent's address plus one typed step (post, term, user, acf_row). LoopContext holds LoopFrames (item, address, source, template, index, render key) instead of bare items, and LoopRenderer pushes each frame before entering its item. - A later page of a nested loop used to render with no parent at all. The signed context now carries the parent address, a fingerprint of the chain's content and its render key. SignedParentChain rebuilds the chain through the step resolvers registered per kind, refuses it once a parent changed or stopped being public, seeds the template lineage, and enters every item root first before the policy, query and render run. - A parent chain is signed only when it rebuilds for a visitor, so a draft or private post ID never reaches the readable token. - One optional contract, ContextualLoopSourceInterface::query(LoopQuery), gives a source the enclosing frame; LoopDataSourceInterface is unchanged. LoopQueryDispatcher is the one caller. - One nesting limit, LoopLimits::MAX_DEPTH (10), for rendering, preview, signing and rebuilding; a loop past it tells an editor why. ACF row context is replayed, not walked. - AcfCursor replaces AcfRowScope: it enters a row at any depth by replaying its field-key path on a cleared loop stack, checks every level, sets the index directly and restores the exact prior stack. This fixes same-named nested repeaters advancing the outer loop, the row_index_offset assumption and the per-row walk. - Post, term and user items hide an enclosing ACF row while entered, so a same-named sub-field never answers for them. - A failed enter() no longer re-enters the outer item (which replaced an ACF row's snapshot); the renderer restores the post globals and the ACF loop stack it snapshotted instead. Styles, tags and the editor follow the chain. - LoopRenderIdentity includes the parent's render key, so one inner widget under two parents never shares a style scope. - The ACF dynamic tags get a Loop Level control (current item up to three levels up). - A loop edited inside a Loop Item template renders inside that template's preview item, also when re-rendered alone; the document preview never overrides a real item, and a nested preview item brings its ancestors. Tests: unit tests for addresses, resolvers, frames, the dispatcher and the preview provider; loop-addresses.php and nested-loops.php integration suites; the run-nested-repeater-load-more.mjs browser journey. Docs: ARCHITECTURE.md and CONTRACTS.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.