public function filter()

in src/markdown-extensions/extracted-code-blocks/FilterBase.php [87:285]


  public function filter(
    Markdown\RenderContext $context,
    Markdown\ASTNode $node,
  ): vec<Markdown\ASTNode> {
    if (!$node is Blocks\CodeBlock || $node is MarkdownExt\PrettyCodeBlock) {
      return vec[$node];
    }

    $info_string = Str\trim($node->getInfoString() ?? '');
    if ($info_string === '') {
      return vec[$node];
    }

    $parts = Regex\split($info_string, re"/\\s+/");
    if (Str\compare_ci($parts[0], 'hack') === 0) {
      $parts = Vec\drop($parts, 1);
    }

    if (C\is_empty($parts)) {
      return vec[$node];
    }

    $hack_filename = $parts[0];
    if (
      !C\any(
        self::ALLOWED_EXTENSIONS,
        $ext ==> Str\ends_with($hack_filename, '.'.$ext),
      )
    ) {
      // Not a Hack code block, or a block without a filename specified -- don't
      // extract.
      return vec[$node];
    }

    $flags = Keyset\drop($parts, 1);
    foreach ($flags as $flag) {
      if (!$flag is Flags) {
        invariant_violation(
          '"%s" is not a valid code block option (valid options: %s)',
          $flag,
          Str\join(Flags::getValues(), ', '),
        );
      }
    }

    // To preserve old behavior, we also allow flags inside the filename.
    foreach (Flags::getValues() as $flag) {
      if (Str\contains($hack_filename, '.'.$flag.'.')) {
        $flags[] = $flag;
      }
    }

    // Another historical edge case: .inc files are never executed.
    if (Str\contains($hack_filename, '.inc.')) {
      $flags[] = Flags::NO_EXEC;
    }

    $file_lines = dict[$hack_filename => vec[]];
    $current_file = $hack_filename;
    foreach (Str\split($node->getCode(), "\n") as $line) {
      if (Str\starts_with($line, '```')) {
        $ext = Str\strip_prefix($line, '```')
          |> Str\trim($$)
          |> Str\strip_prefix($$, $hack_filename)
          |> Str\strip_prefix($$, '.')
          |> idx(self::FILE_ALIASES, $$, $$);
        invariant(
          $ext is Files,
          '``` must be followed by a valid file extension, got "%s" '.
          '(valid extensions: %s)',
          $ext,
          Str\join(Files::getValues(), ', '),
        );
        $current_file = $hack_filename.'.'.$ext;
        invariant(
          !C\contains_key($file_lines, $current_file),
          'Code block contains multiple "%s" sections!',
          $ext,
        );
        $file_lines[$current_file] = vec[];
        continue;
      }
      $file_lines[$current_file][] = $line;
    }

    $md = $context as MarkdownExt\RenderContext->getFilePath();
    invariant($md is nonnull, 'RenderContext is missing file path');
    invariant(Str\ends_with($md, '.md'), 'Expected a .md file, got "%s"', $md);
    $examples_dir = Str\strip_suffix($md, '.md')
      |> Str\replace_every($$, self::PATHS);
    $hack_file_path = $examples_dir.'/'.$hack_filename;
    $expected_type_errors =
      Str\ends_with($hack_filename, '.'.self::TYPE_ERRORS);

    $files = Dict\map(
      $file_lines,
      $lines ==> Str\join($lines, "\n") |> Str\trim($$)."\n",
    );

    $original_code = $files[$hack_filename];
    $files[$hack_filename] =
      self::addBoilerplate($hack_file_path, $files[$hack_filename]);

    $skipif = $hack_filename.'.'.Files::SKIPIF;
    if (C\contains_key($files, $skipif)) {
      $files[$skipif] = self::addBoilerplate($hack_file_path, $files[$skipif]);
    }

    // Write or verify files.
    $inputs_changed = false;
    foreach ($files as $name => $content) {
      $path = $examples_dir.'/'.$name;
      self::$validFiles[] = $path;
      $inputs_changed =
        static::processFile($path, $content) || $inputs_changed;
    }

    // If any inputs changed, delete all other files (mainly output/expect
    // files, but also other files like hhconfig/skipif that may have been
    // previously included in the example block but no longer are). This ensures
    // that the correct subset of output/expect files is regenerated below.
    if ($inputs_changed) {
      foreach (Files::getValues() as $suffix) {
        $name = $hack_filename.'.'.$suffix;
        if (C\contains_key($files, $name)) {
          continue;
        }
        $path = $examples_dir.'/'.$name;
        if (\file_exists($path)) {
          \unlink($path);
          invariant(!\file_exists($path), 'Failed to delete %s', $path);
        }
      }
    }

    // Auto-generate or verify *existence* of output files if missing.
    $output_title = 'Output';
    $output = '';

    // Generate/verify typechecker output, if type errors are expected.
    if ($expected_type_errors) {
      $show_output = !C\contains_key($flags, Flags::NO_AUTO_OUTPUT);
      if (
        !self::hasValidOutputFileSet(
          $hack_file_path,
          Files::TYPECHECKER_EXPECT,
          Files::EXAMPLE_TYPECHECKER_OUT,
          Files::TYPECHECKER_EXPECTF,
          Files::TYPECHECKER_EXPECTREGEX,
          // don't generate .example.out if output is not shown in the guide
          $show_output,
        )
      ) {
        static::processMissingTypecheckerOutput($hack_file_path, $show_output);
      }
      if ($show_output) {
        $output_title = 'Typechecker output';
        $output = \file_exists($hack_file_path.'.'.Files::TYPECHECKER_EXPECT)
          ? \file_get_contents($hack_file_path.'.'.Files::TYPECHECKER_EXPECT)
          : \file_get_contents(
              $hack_file_path.'.'.Files::EXAMPLE_TYPECHECKER_OUT,
            );
      }
    }

    // Generate/verify HHVM output.
    if (!C\contains_key($flags, Flags::NO_EXEC)) {
      $show_output = !$expected_type_errors &&
        !C\contains_key($flags, Flags::NO_AUTO_OUTPUT);
      if (
        !self::hasValidOutputFileSet(
          $hack_file_path,
          Files::HHVM_EXPECT,
          Files::EXAMPLE_HHVM_OUT,
          Files::HHVM_EXPECTF,
          Files::HHVM_EXPECTREGEX,
          // don't generate .example.out if output is not shown in the guide
          $show_output,
        )
      ) {
        static::processMissingHHVMOutput($hack_file_path, $show_output);
      }
      if ($show_output) {
        $output = \file_exists($hack_file_path.'.'.Files::HHVM_EXPECT)
          ? \file_get_contents($hack_file_path.'.'.Files::HHVM_EXPECT)
          : \file_get_contents($hack_file_path.'.'.Files::EXAMPLE_HHVM_OUT);
      }
    }
    $output = Str\trim($output);

    $ret = vec[
      new Blocks\FencedCodeBlock('Hack', Str\trim($original_code)),
    ];
    if ($output !== '' && !C\contains_key($flags, Flags::NO_AUTO_OUTPUT)) {
      $ret[] = new Blocks\HTMLBlock('<em>'.$output_title.'</em>');
      $ret[] = new Blocks\FencedCodeBlock(null, $output);
    }
    return $ret;
  }