← Visitor lesson
Lab

Visitor

// One method for each kind of node an article is made of.
interface Visitor {
  visitText(node: TextNode): void;
  visitImage(node: ImageNode): void;
  visitGroup(node: GroupNode): void;
}

interface ArticleNode {
  accept(visitor: Visitor): void;
}

class TextNode implements ArticleNode {
  text: string;
  constructor(text: string) {
    this.text = text;
  }
  accept(visitor: Visitor): void {
    visitor.visitText(this);
  }
}

class ImageNode implements ArticleNode {
  src: string;
  alt: string;
  constructor(src: string, alt: string) {
    this.src = src;
    this.alt = alt;
  }
  accept(visitor: Visitor): void {
    visitor.visitImage(this);
  }
}

class GroupNode implements ArticleNode {
  draft: boolean;
  children: ArticleNode[];
  constructor(draft: boolean, children: ArticleNode[]) {
    this.draft = draft;
    this.children = children;
  }
  // Dispatch only: the visitor decides whether to walk the children.
  accept(visitor: Visitor): void {
    visitor.visitGroup(this);
  }
}

// One operation: the readable text, without unpublished drafts.
class TextExport implements Visitor {
  lines: string[] = [];
  visitText(node: TextNode): void {
    this.lines.push(node.text);
  }
  visitImage(node: ImageNode): void {
    if (node.alt !== '') this.lines.push(node.alt);
  }
  visitGroup(node: GroupNode): void {
    if (node.draft) return;
    for (const child of node.children) child.accept(this);
  }
}

// Another: every image file, drafts included, so the build uploads them all.
class AssetInventory implements Visitor {
  files: string[] = [];
  visitText(_node: TextNode): void {}
  visitImage(node: ImageNode): void {
    this.files.push(node.src);
  }
  visitGroup(node: GroupNode): void {
    for (const child of node.children) child.accept(this);
  }
}

// The callers: a fresh visitor for each run.
function exportText(root: ArticleNode): string[] {
  const visitor = new TextExport();
  root.accept(visitor);
  return visitor.lines;
}

function listAssets(root: ArticleNode): string[] {
  const visitor = new AssetInventory();
  root.accept(visitor);
  return visitor.files;
}

const article = new GroupNode(false, [
  new TextNode('Trail guide'),
  new ImageNode('/cover.png', 'A branching trail'),
  new GroupNode(true, [
    new TextNode('Unpublished notes'),
    new ImageNode('/chart.png', 'Elevation chart'),
  ]),
]);

const text = exportText(article);
const assets = listAssets(article);
console.log(`text: ${text.join(' | ')}`);
console.log(`assets: ${assets.join(' | ')}`);