| 1 | /* |
| 2 | * Copyright (C) 2015 Apple Inc. All rights reserved. |
| 3 | * |
| 4 | * Redistribution and use in source and binary forms, with or without |
| 5 | * modification, are permitted provided that the following conditions |
| 6 | * are met: |
| 7 | * 1. Redistributions of source code must retain the above copyright |
| 8 | * notice, this list of conditions and the following disclaimer. |
| 9 | * 2. Redistributions in binary form must reproduce the above copyright |
| 10 | * notice, this list of conditions and the following disclaimer in the |
| 11 | * documentation and/or other materials provided with the distribution. |
| 12 | * |
| 13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| 14 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| 15 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 16 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| 17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| 18 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| 19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| 20 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| 21 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 22 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 24 | */ |
| 25 | |
| 26 | #include "config.h" |
| 27 | #include "SlotAssignment.h" |
| 28 | |
| 29 | |
| 30 | #include "HTMLSlotElement.h" |
| 31 | #include "ShadowRoot.h" |
| 32 | #include "TypedElementDescendantIterator.h" |
| 33 | |
| 34 | namespace WebCore { |
| 35 | |
| 36 | using namespace HTMLNames; |
| 37 | |
| 38 | static const AtomicString& slotNameFromAttributeValue(const AtomicString& value) |
| 39 | { |
| 40 | return value == nullAtom() ? SlotAssignment::defaultSlotName() : value; |
| 41 | } |
| 42 | |
| 43 | static const AtomicString& slotNameFromSlotAttribute(const Node& child) |
| 44 | { |
| 45 | if (is<Text>(child)) |
| 46 | return SlotAssignment::defaultSlotName(); |
| 47 | |
| 48 | return slotNameFromAttributeValue(downcast<Element>(child).attributeWithoutSynchronization(slotAttr)); |
| 49 | } |
| 50 | |
| 51 | #if !ASSERT_DISABLED |
| 52 | static HTMLSlotElement* findSlotElement(ShadowRoot& shadowRoot, const AtomicString& slotName) |
| 53 | { |
| 54 | for (auto& slotElement : descendantsOfType<HTMLSlotElement>(shadowRoot)) { |
| 55 | if (slotNameFromAttributeValue(slotElement.attributeWithoutSynchronization(nameAttr)) == slotName) |
| 56 | return &slotElement; |
| 57 | } |
| 58 | return nullptr; |
| 59 | } |
| 60 | #endif |
| 61 | |
| 62 | static HTMLSlotElement* nextSlotElementSkippingSubtree(ContainerNode& startingNode, ContainerNode* skippedSubtree) |
| 63 | { |
| 64 | Node* node = &startingNode; |
| 65 | do { |
| 66 | if (UNLIKELY(node == skippedSubtree)) |
| 67 | node = NodeTraversal::nextSkippingChildren(*node); |
| 68 | else |
| 69 | node = NodeTraversal::next(*node); |
| 70 | } while (node && !is<HTMLSlotElement>(node)); |
| 71 | return downcast<HTMLSlotElement>(node); |
| 72 | } |
| 73 | |
| 74 | SlotAssignment::SlotAssignment() = default; |
| 75 | |
| 76 | SlotAssignment::~SlotAssignment() = default; |
| 77 | |
| 78 | HTMLSlotElement* SlotAssignment::findAssignedSlot(const Node& node, ShadowRoot& shadowRoot) |
| 79 | { |
| 80 | if (!is<Text>(node) && !is<Element>(node)) |
| 81 | return nullptr; |
| 82 | |
| 83 | auto* slot = m_slots.get(slotNameForHostChild(node)); |
| 84 | if (!slot) |
| 85 | return nullptr; |
| 86 | |
| 87 | return findFirstSlotElement(*slot, shadowRoot); |
| 88 | } |
| 89 | |
| 90 | inline bool SlotAssignment::hasAssignedNodes(ShadowRoot& shadowRoot, Slot& slot) |
| 91 | { |
| 92 | if (!m_slotAssignmentsIsValid) |
| 93 | assignSlots(shadowRoot); |
| 94 | return !slot.assignedNodes.isEmpty(); |
| 95 | } |
| 96 | |
| 97 | void SlotAssignment::renameSlotElement(HTMLSlotElement& slotElement, const AtomicString& oldName, const AtomicString& newName, ShadowRoot& shadowRoot) |
| 98 | { |
| 99 | ASSERT(m_slotElementsForConsistencyCheck.contains(&slotElement)); |
| 100 | |
| 101 | m_slotMutationVersion++; |
| 102 | |
| 103 | removeSlotElementByName(oldName, slotElement, nullptr, shadowRoot); |
| 104 | addSlotElementByName(newName, slotElement, shadowRoot); |
| 105 | } |
| 106 | |
| 107 | void SlotAssignment::addSlotElementByName(const AtomicString& name, HTMLSlotElement& slotElement, ShadowRoot& shadowRoot) |
| 108 | { |
| 109 | #ifndef NDEBUG |
| 110 | ASSERT(!m_slotElementsForConsistencyCheck.contains(&slotElement)); |
| 111 | m_slotElementsForConsistencyCheck.add(&slotElement); |
| 112 | #endif |
| 113 | |
| 114 | // FIXME: We should be able to do a targeted reconstruction. |
| 115 | shadowRoot.host()->invalidateStyleAndRenderersForSubtree(); |
| 116 | |
| 117 | auto& slotName = slotNameFromAttributeValue(name); |
| 118 | auto addResult = m_slots.ensure(slotName, [&] { |
| 119 | // Unlike named slots, assignSlots doesn't collect nodes assigned to the default slot |
| 120 | // to avoid always having a vector of all child nodes of a shadow host. |
| 121 | if (slotName == defaultSlotName()) |
| 122 | m_slotAssignmentsIsValid = false; |
| 123 | return std::make_unique<Slot>(); |
| 124 | }); |
| 125 | auto& slot = *addResult.iterator->value; |
| 126 | bool needsSlotchangeEvent = shadowRoot.shouldFireSlotchangeEvent() && hasAssignedNodes(shadowRoot, slot); |
| 127 | |
| 128 | slot.elementCount++; |
| 129 | if (slot.elementCount == 1) { |
| 130 | slot.element = makeWeakPtr(slotElement); |
| 131 | if (needsSlotchangeEvent) |
| 132 | slotElement.enqueueSlotChangeEvent(); |
| 133 | return; |
| 134 | } |
| 135 | |
| 136 | if (!needsSlotchangeEvent) { |
| 137 | ASSERT(slot.element || m_needsToResolveSlotElements); |
| 138 | slot.element = nullptr; |
| 139 | m_needsToResolveSlotElements = true; |
| 140 | return; |
| 141 | } |
| 142 | |
| 143 | resolveSlotsAfterSlotMutation(shadowRoot, SlotMutationType::Insertion); |
| 144 | } |
| 145 | |
| 146 | void SlotAssignment::removeSlotElementByName(const AtomicString& name, HTMLSlotElement& slotElement, ContainerNode* oldParentOfRemovedTreeForRemoval, ShadowRoot& shadowRoot) |
| 147 | { |
| 148 | #ifndef NDEBUG |
| 149 | ASSERT(m_slotElementsForConsistencyCheck.contains(&slotElement)); |
| 150 | m_slotElementsForConsistencyCheck.remove(&slotElement); |
| 151 | #endif |
| 152 | |
| 153 | if (auto* host = shadowRoot.host()) // FIXME: We should be able to do a targeted reconstruction. |
| 154 | host->invalidateStyleAndRenderersForSubtree(); |
| 155 | |
| 156 | auto* slot = m_slots.get(slotNameFromAttributeValue(name)); |
| 157 | RELEASE_ASSERT(slot && slot->hasSlotElements()); |
| 158 | bool needsSlotchangeEvent = shadowRoot.shouldFireSlotchangeEvent() && hasAssignedNodes(shadowRoot, *slot); |
| 159 | |
| 160 | slot->elementCount--; |
| 161 | if (!slot->elementCount) { |
| 162 | slot->element = nullptr; |
| 163 | if (needsSlotchangeEvent && m_slotResolutionVersion != m_slotMutationVersion) |
| 164 | slotElement.enqueueSlotChangeEvent(); |
| 165 | return; |
| 166 | } |
| 167 | |
| 168 | if (!needsSlotchangeEvent) { |
| 169 | ASSERT(slot->element || m_needsToResolveSlotElements); |
| 170 | slot->element = nullptr; |
| 171 | m_needsToResolveSlotElements = true; |
| 172 | return; |
| 173 | } |
| 174 | |
| 175 | bool elementWasRenamed = !oldParentOfRemovedTreeForRemoval; |
| 176 | if (elementWasRenamed && slot->element == &slotElement) |
| 177 | slotElement.enqueueSlotChangeEvent(); |
| 178 | |
| 179 | // A previous invocation to resolveSlotsAfterSlotMutation during this removal has updated this slot. |
| 180 | ASSERT(slot->element || (m_slotResolutionVersion == m_slotMutationVersion && !findSlotElement(shadowRoot, name))); |
| 181 | if (slot->element) { |
| 182 | resolveSlotsAfterSlotMutation(shadowRoot, elementWasRenamed ? SlotMutationType::Insertion : SlotMutationType::Removal, |
| 183 | m_willBeRemovingAllChildren ? oldParentOfRemovedTreeForRemoval : nullptr); |
| 184 | } |
| 185 | |
| 186 | if (slot->oldElement == &slotElement) { |
| 187 | slotElement.enqueueSlotChangeEvent(); |
| 188 | slot->oldElement = nullptr; |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | void SlotAssignment::resolveSlotsAfterSlotMutation(ShadowRoot& shadowRoot, SlotMutationType mutationType, ContainerNode* subtreeToSkip) |
| 193 | { |
| 194 | if (m_slotResolutionVersion == m_slotMutationVersion) |
| 195 | return; |
| 196 | m_slotResolutionVersion = m_slotMutationVersion; |
| 197 | |
| 198 | ASSERT(!subtreeToSkip || mutationType == SlotMutationType::Removal); |
| 199 | m_needsToResolveSlotElements = false; |
| 200 | |
| 201 | for (auto& slot : m_slots.values()) |
| 202 | slot->seenFirstElement = false; |
| 203 | |
| 204 | unsigned slotCount = 0; |
| 205 | HTMLSlotElement* currentElement = nextSlotElementSkippingSubtree(shadowRoot, subtreeToSkip); |
| 206 | for (; currentElement; currentElement = nextSlotElementSkippingSubtree(*currentElement, subtreeToSkip)) { |
| 207 | auto& currentSlotName = slotNameFromAttributeValue(currentElement->attributeWithoutSynchronization(nameAttr)); |
| 208 | auto* currentSlot = m_slots.get(currentSlotName); |
| 209 | if (!currentSlot) { |
| 210 | // A new slot may have been inserted with this node but appears later in the tree order. |
| 211 | // Such a slot would go through the fast path in addSlotElementByName, |
| 212 | // and any subsequently inserted slot of the same name would not result in any slotchange or invokation of this function. |
| 213 | ASSERT(mutationType == SlotMutationType::Insertion); |
| 214 | continue; |
| 215 | } |
| 216 | if (currentSlot->seenFirstElement) { |
| 217 | if (mutationType == SlotMutationType::Insertion && currentSlot->oldElement == currentElement) { |
| 218 | currentElement->enqueueSlotChangeEvent(); |
| 219 | currentSlot->oldElement = nullptr; |
| 220 | } |
| 221 | continue; |
| 222 | } |
| 223 | currentSlot->seenFirstElement = true; |
| 224 | slotCount++; |
| 225 | ASSERT(currentSlot->element || !hasAssignedNodes(shadowRoot, *currentSlot)); |
| 226 | if (currentSlot->element != currentElement) { |
| 227 | if (hasAssignedNodes(shadowRoot, *currentSlot)) { |
| 228 | currentSlot->oldElement = WTFMove(currentSlot->element); |
| 229 | currentElement->enqueueSlotChangeEvent(); |
| 230 | } |
| 231 | currentSlot->element = makeWeakPtr(*currentElement); |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | if (slotCount == m_slots.size()) |
| 236 | return; |
| 237 | |
| 238 | if (mutationType == SlotMutationType::Insertion) { |
| 239 | // This code path is taken only when continue above for !currentSlot is taken. |
| 240 | // i.e. there is a new slot being inserted into the tree but we have yet to invoke addSlotElementByName on it. |
| 241 | #if !ASSERT_DISABLED |
| 242 | for (auto& entry : m_slots) |
| 243 | ASSERT(entry.value->seenFirstElement || !findSlotElement(shadowRoot, entry.key)); |
| 244 | #endif |
| 245 | return; |
| 246 | } |
| 247 | |
| 248 | for (auto& slot : m_slots.values()) { |
| 249 | if (slot->seenFirstElement) |
| 250 | continue; |
| 251 | if (!slot->elementCount) { |
| 252 | // Taken the fast path for removal. |
| 253 | ASSERT(!slot->element); |
| 254 | continue; |
| 255 | } |
| 256 | // All slot elements have been removed for this slot. |
| 257 | slot->seenFirstElement = true; |
| 258 | ASSERT(slot->element); |
| 259 | if (hasAssignedNodes(shadowRoot, *slot)) |
| 260 | slot->oldElement = WTFMove(slot->element); |
| 261 | slot->element = nullptr; |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | void SlotAssignment::slotFallbackDidChange(HTMLSlotElement& slotElement, ShadowRoot& shadowRoot) |
| 266 | { |
| 267 | if (shadowRoot.mode() == ShadowRootMode::UserAgent) |
| 268 | return; |
| 269 | |
| 270 | bool usesFallbackContent = !assignedNodesForSlot(slotElement, shadowRoot); |
| 271 | if (usesFallbackContent) |
| 272 | slotElement.enqueueSlotChangeEvent(); |
| 273 | } |
| 274 | |
| 275 | void SlotAssignment::resolveSlotsBeforeNodeInsertionOrRemoval(ShadowRoot& shadowRoot) |
| 276 | { |
| 277 | ASSERT(shadowRoot.shouldFireSlotchangeEvent()); |
| 278 | m_slotMutationVersion++; |
| 279 | m_willBeRemovingAllChildren = false; |
| 280 | if (m_needsToResolveSlotElements) |
| 281 | resolveAllSlotElements(shadowRoot); |
| 282 | } |
| 283 | |
| 284 | void SlotAssignment::willRemoveAllChildren(ShadowRoot& shadowRoot) |
| 285 | { |
| 286 | m_slotMutationVersion++; |
| 287 | m_willBeRemovingAllChildren = true; |
| 288 | if (m_needsToResolveSlotElements) |
| 289 | resolveAllSlotElements(shadowRoot); |
| 290 | } |
| 291 | |
| 292 | void SlotAssignment::didChangeSlot(const AtomicString& slotAttrValue, ShadowRoot& shadowRoot) |
| 293 | { |
| 294 | auto& slotName = slotNameFromAttributeValue(slotAttrValue); |
| 295 | auto* slot = m_slots.get(slotName); |
| 296 | if (!slot) |
| 297 | return; |
| 298 | |
| 299 | slot->assignedNodes.clear(); |
| 300 | m_slotAssignmentsIsValid = false; |
| 301 | |
| 302 | auto slotElement = makeRefPtr(findFirstSlotElement(*slot, shadowRoot)); |
| 303 | if (!slotElement) |
| 304 | return; |
| 305 | |
| 306 | shadowRoot.host()->invalidateStyleAndRenderersForSubtree(); |
| 307 | |
| 308 | if (shadowRoot.shouldFireSlotchangeEvent()) |
| 309 | slotElement->enqueueSlotChangeEvent(); |
| 310 | } |
| 311 | |
| 312 | void SlotAssignment::hostChildElementDidChange(const Element& childElement, ShadowRoot& shadowRoot) |
| 313 | { |
| 314 | didChangeSlot(childElement.attributeWithoutSynchronization(slotAttr), shadowRoot); |
| 315 | } |
| 316 | |
| 317 | const Vector<Node*>* SlotAssignment::assignedNodesForSlot(const HTMLSlotElement& slotElement, ShadowRoot& shadowRoot) |
| 318 | { |
| 319 | ASSERT(slotElement.containingShadowRoot() == &shadowRoot); |
| 320 | const AtomicString& slotName = slotNameFromAttributeValue(slotElement.attributeWithoutSynchronization(nameAttr)); |
| 321 | auto* slot = m_slots.get(slotName); |
| 322 | RELEASE_ASSERT(slot); |
| 323 | |
| 324 | if (!m_slotAssignmentsIsValid) |
| 325 | assignSlots(shadowRoot); |
| 326 | |
| 327 | if (slot->assignedNodes.isEmpty()) |
| 328 | return nullptr; |
| 329 | |
| 330 | RELEASE_ASSERT(slot->hasSlotElements()); |
| 331 | if (slot->hasDuplicatedSlotElements() && findFirstSlotElement(*slot, shadowRoot) != &slotElement) |
| 332 | return nullptr; |
| 333 | |
| 334 | return &slot->assignedNodes; |
| 335 | } |
| 336 | |
| 337 | const AtomicString& SlotAssignment::slotNameForHostChild(const Node& child) const |
| 338 | { |
| 339 | return slotNameFromSlotAttribute(child); |
| 340 | } |
| 341 | |
| 342 | HTMLSlotElement* SlotAssignment::findFirstSlotElement(Slot& slot, ShadowRoot& shadowRoot) |
| 343 | { |
| 344 | if (slot.shouldResolveSlotElement()) |
| 345 | resolveAllSlotElements(shadowRoot); |
| 346 | |
| 347 | #ifndef NDEBUG |
| 348 | ASSERT(!slot.element || m_slotElementsForConsistencyCheck.contains(slot.element.get())); |
| 349 | ASSERT(!!slot.element == !!slot.elementCount); |
| 350 | #endif |
| 351 | |
| 352 | return slot.element.get(); |
| 353 | } |
| 354 | |
| 355 | void SlotAssignment::resolveAllSlotElements(ShadowRoot& shadowRoot) |
| 356 | { |
| 357 | ASSERT(m_needsToResolveSlotElements); |
| 358 | m_needsToResolveSlotElements = false; |
| 359 | |
| 360 | // FIXME: It's inefficient to reset all values. We should be able to void this in common case. |
| 361 | for (auto& entry : m_slots) |
| 362 | entry.value->seenFirstElement = false; |
| 363 | |
| 364 | unsigned slotCount = m_slots.size(); |
| 365 | for (auto& slotElement : descendantsOfType<HTMLSlotElement>(shadowRoot)) { |
| 366 | auto& slotName = slotNameFromAttributeValue(slotElement.attributeWithoutSynchronization(nameAttr)); |
| 367 | |
| 368 | auto* slot = m_slots.get(slotName); |
| 369 | RELEASE_ASSERT(slot); // slot must have been created when a slot was inserted. |
| 370 | |
| 371 | if (slot->seenFirstElement) |
| 372 | continue; |
| 373 | slot->seenFirstElement = true; |
| 374 | |
| 375 | slot->element = makeWeakPtr(slotElement); |
| 376 | slotCount--; |
| 377 | if (!slotCount) |
| 378 | break; |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | void SlotAssignment::assignSlots(ShadowRoot& shadowRoot) |
| 383 | { |
| 384 | ASSERT(!m_slotAssignmentsIsValid); |
| 385 | m_slotAssignmentsIsValid = true; |
| 386 | |
| 387 | for (auto& entry : m_slots) |
| 388 | entry.value->assignedNodes.shrink(0); |
| 389 | |
| 390 | auto& host = *shadowRoot.host(); |
| 391 | for (auto* child = host.firstChild(); child; child = child->nextSibling()) { |
| 392 | if (!is<Text>(*child) && !is<Element>(*child)) |
| 393 | continue; |
| 394 | auto slotName = slotNameForHostChild(*child); |
| 395 | assignToSlot(*child, slotName); |
| 396 | } |
| 397 | |
| 398 | for (auto& entry : m_slots) |
| 399 | entry.value->assignedNodes.shrinkToFit(); |
| 400 | } |
| 401 | |
| 402 | void SlotAssignment::assignToSlot(Node& child, const AtomicString& slotName) |
| 403 | { |
| 404 | ASSERT(!slotName.isNull()); |
| 405 | if (slotName == defaultSlotName()) { |
| 406 | auto defaultSlotEntry = m_slots.find(defaultSlotName()); |
| 407 | if (defaultSlotEntry != m_slots.end()) |
| 408 | defaultSlotEntry->value->assignedNodes.append(&child); |
| 409 | return; |
| 410 | } |
| 411 | |
| 412 | auto addResult = m_slots.ensure(slotName, [] { |
| 413 | return std::make_unique<Slot>(); |
| 414 | }); |
| 415 | addResult.iterator->value->assignedNodes.append(&child); |
| 416 | } |
| 417 | |
| 418 | } |
| 419 | |
| 420 | |
| 421 | |