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;
}