protected function getBlocks($nodes) {
    // Each completed block.
    $blocks = array();
    // Container for any top-level inline elements.
    $block = '';
    // Add a completed block to $blocks.
    $add_block = function($string = NULL) use(&$blocks, &$block) {
      if (preg_match('/\S/u', $block)) {
        $blocks[] = trim($block, " \t");
      }
      if (preg_match('/\S/u', $string)) {
        $blocks[] = rtrim($string, " \t");
      }
      $block = '';
    };
    /** @var \DOMNode $node */
    foreach ($nodes as $node) {
      switch ($node->nodeType) {

        case XML_ELEMENT_NODE:
          /** @var \DOMElement $node */
          switch ($node->nodeName) {

            // Explicitly handle these elements.

            case 'p':
            case 'pre':
              $add_block($this->getBlock($node));
              break;

            case 'h1':
              $add_block('# ' . $this->getBlock($node));
              break;

            case 'h2':
              $add_block('## ' . $this->getBlock($node));
              break;

            case 'h3':
              $add_block('### ' . $this->getBlock($node));
              break;

            case 'h4':
              $add_block('#### ' . $this->getBlock($node));
              break;

            case 'h5':
              $add_block('##### ' . $this->getBlock($node));
              break;

            case 'h6':
              $add_block('###### ' . $this->getBlock($node));
              break;

            case 'hr':
              $add_block('***');
              break;

            case 'ul':
            case 'ol':
            case 'dl':
              $add_block($this->getBlockList($node));
              break;

            // Other block-level elements.
            case 'article':
            case 'aside':
            case 'blockquote':
            case 'dd':
            case 'div':
            case 'fieldset':
            case 'figcaption':
            case 'figure':
            case 'footer':
            case 'form':
            case 'header':
            case 'hgroup':
            case 'main':
            case 'nav':
            case 'output':
            case 'section':
              // Recursively handle any descendant elements we care about.
              foreach ($this->getBlocks($node->childNodes) as $b) {
                $add_block($b);
              }
              break;

            // Unsupported.
            case 'canvas':
            case 'noscript':
            case 'script':
            case 'table':
            case 'tfoot':
            case 'video':
              break;

            // Treat as inline element.
            default:
              // Append series of inline elements together, as if they were
              // inside a block-level element.
              $block .= $this->getBlock($node);
              break;

          }
          break;

        case XML_TEXT_NODE:
          /** @var \DOMText $node */
          $block .= self::escape($node->textContent);
          break;

      }
    }

    $add_block();
    return $blocks;
  }