card-dom

HTML

<div>
<h2>Card DOM</h2>
<p>This is a JavaScript solution that creates a simple DOM for a Card Editor/Player from this article:
<a href="https://habr.com/en/articles/955158/">A Real Test for Programming Languages — How They Handle DOM-Like Data Structures</a></p>

<p>All requirements have been met. However, some of them required excessive coding, and some solutions are not efficient in terms of memory or runtime. Certain checks are possible only at runtime, which can lead to exceptions during execution.</p>

<p>What JavaScript provides “out of the box”:</p>
<ul>
<li>No memory leaks (eventually handled by the garbage collector)</li>
<li>Protection against accessing deleted objects that are still referenced from the stack</li>
</ul>

<p>What was implemented manually:</p>
<ul>
<li>Connectors and Button target links are set to null when their target is removed. This required manually registering links in hash sets and propagating detach events backwards. Alternatives exist but involve indirection and are harder to implement in this small example.</li>
<li>Topologically correct copy operations. A two-step process was implemented: first creating a copy, then resolving weak references using a Copy Context that maintains a hash map between original and copied objects.</li>
<li>“Unshare on mutation” strategy for styles and bitmaps. Due to JavaScript limitations, this is a runtime-only safety net; it reduces resilience but is acceptable for a typeless interpreted language like JS.</li>
<li>Safety net against multiparenting and cycles (e.g., GroupItem inside itself). Runtime-only check that scans the parent chain on each attachment.</li>
</ul>
</div>

CSS

div{
  font: 11pt sans-serif;
  line-height: 14pt;
  max-width: 500px;
  margin: auto;
}
div * {
  margin: 10px 0px;
}

JavaScript

class DeepCopyContext {
  constructor() {
    this.nodeMap = new Map(); // original → copy
  }

  register(original, copy) {
    this.nodeMap.set(original, copy);
    return copy;
  }

  resolve(original) {
    return this.nodeMap.get(original) || original;
  }
}

class Document {
  constructor() {
    this.cards = [];
  }

  addCard(card) {
    card.setParent(this);
    this.cards.push(card);
  }

  removeCard(card) {
    this.cards = this.cards.filter(c => c !== card);
    card.detach();
  }
  deepCopy(ctx) {
    const newDoc = new Document();
    for (const card of this.cards) {
      newDoc.addCard(card.deepCopy(ctx));
    }
    return ctx.register(this, newDoc);
  }

  resolve(ctx) {
    for (const card of this.cards) {
      card.resolve(ctx);
    }
  }
}

class Card {
  constructor() {
    this.items = [];
    this.parent = null;
    this.inboundButtons = new Set(); // Track buttons pointing to this card
  }

  addItem(item) {
    item.setParent(this);
    this.items.push(item);
  }

  removeItem(item) {
    this.items = this.items.filter(i => i !== item);
    item.detach();
  }

  registerButton(button) {
    this.inboundButtons.add(button);
  }

  unregisterButton(button) {
    this.inboundButtons.delete(button);
  }

  setParent(parent) {
    if (this.parent && this.parent !== parent) {
      throw new Error("Card already has a parent");
    }
    this.parent = parent;
  }

  detach() {
    for (const item of this.items) item.detach();
    for (const button of this.inboundButtons) {
      button.setTargetCard(null);
    }
    this.inboundButtons.clear();
  }

  deepCopy(ctx) {
    const newCard = new Card();
    for (const item of this.items) {
      newCard.addItem(item.deepCopy(ctx));
    }
    return ctx.register(this, newCard);
  }

  resolve(ctx) {
    for (const item of this.items) {
      item.resolve(ctx);
    }
  }
}

class CardItem {
 ...